Improve gamma corrected resampling by acknowledging the perceptual luminance

This commit is contained in:
Filippo Tarpini 2025-04-12 05:02:45 +03:00 committed by GitHub
parent 5285b1168a
commit f6ca48bd26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -38,6 +38,13 @@ float3 LinearTosRGBGamma(float3 color)
return color;
}
float3 Rec709_Luminance = float3(0.2126f, 0.7152f, 0.0722f);
float Luminance(float3 color)
{
return dot(color, Rec709_Luminance);
}
/***** COLOR SAMPLING *****/
// Non filtered gamma corrected sample (nearest neighbor)
@ -87,9 +94,21 @@ float4 BilinearSample(float3 uvw, float gamma)
float4 c21 = QuickSampleByPixel(int_pixel + float2(1.5, 0.5), uvw.z, gamma);
float4 c12 = QuickSampleByPixel(int_pixel + float2(0.5, 1.5), uvw.z, gamma);
float4 c22 = QuickSampleByPixel(int_pixel + float2(1.5, 1.5), uvw.z, gamma);
// Blend the 4 samples by their weight
return lerp(lerp(c11, c21, frac_pixel.x), lerp(c12, c22, frac_pixel.x), frac_pixel.y);
float4 avg_c = lerp(lerp(c11, c21, frac_pixel.x), lerp(c12, c22, frac_pixel.x), frac_pixel.y);
// Calculate the average of the gamma space luminance, as that can be used to
// properly determine the perceptual brightness the color should have.
float target_avg_c_lum = lerp(lerp(pow(Luminance(c11.rgb), 1.0 / gamma), pow(Luminance(c21.rgb), 1.0 / gamma), frac_pixel.x), lerp(pow(Luminance(c12.rgb), 1.0 / gamma), pow(Luminance(c22.rgb), 1.0 / gamma), frac_pixel.x), frac_pixel.y);
float avg_c_lum = Luminance(avg_c.rgb);
if (avg_c_lum != 0.0)
{
avg_c.rgb *= pow(target_avg_c_lum, gamma) / avg_c_lum;
}
return avg_c;
}
/***** Bicubic Interpolation *****/
@ -173,7 +192,7 @@ float4 SharpBilinearSample(float3 uvw, float gamma)
float2 texel = uvw.xy * source_size;
float2 texel_floored = floor(texel);
float2 s = fract(texel);
float scale = max(floor(max(target_size.x * inverted_source_size.x, target_size.y * inverted_source_size.y)), 1.f);
float scale = max(floor(max(target_size.x * inverted_source_size.x, target_size.y * inverted_source_size.y)), 1.0);
float region_range = 0.5 - (0.5 / scale);
// Figure out where in the texel to sample to get correct pre-scaled bilinear.
@ -228,15 +247,28 @@ float4 AreaSampling(float3 uvw, float gamma)
// Initialize the color accumulator.
float4 avg_color = float4(0.0, 0.0, 0.0, 0.0);
float avg_luminance = 0.0;
float4 temp_color;
float luminance_gamma = gamma; // For now, default to the user selected gamma (alternatively we could fix it to anything between 2.2 and 3.0)
float luminance_inv_gamma = 1.0 / luminance_gamma;
// Prevents rounding errors due to the coordinates flooring above
const float2 offset = float2(0.5, 0.5);
// Accumulate corner pixels.
avg_color += area_nw * QuickSampleByPixel(float2(f_beg.x, f_beg.y) + offset, uvw.z, gamma);
avg_color += area_ne * QuickSampleByPixel(float2(f_end.x, f_beg.y) + offset, uvw.z, gamma);
avg_color += area_sw * QuickSampleByPixel(float2(f_beg.x, f_end.y) + offset, uvw.z, gamma);
avg_color += area_se * QuickSampleByPixel(float2(f_end.x, f_end.y) + offset, uvw.z, gamma);
temp_color = QuickSampleByPixel(float2(f_beg.x, f_beg.y) + offset, uvw.z, gamma);
avg_color += area_nw * temp_color;
avg_luminance += area_nw * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
temp_color = QuickSampleByPixel(float2(f_end.x, f_beg.y) + offset, uvw.z, gamma);
avg_color += area_ne * temp_color;
avg_luminance += area_ne * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
temp_color = QuickSampleByPixel(float2(f_beg.x, f_end.y) + offset, uvw.z, gamma);
avg_color += area_sw * temp_color;
avg_luminance += area_sw * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
temp_color = QuickSampleByPixel(float2(f_end.x, f_end.y) + offset, uvw.z, gamma);
avg_color += area_se * temp_color;
avg_luminance += area_se * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
// Determine the size of the pixel box.
int x_range = int(f_end.x - f_beg.x - 0.5);
@ -258,8 +290,12 @@ float4 AreaSampling(float3 uvw, float gamma)
if (ix < x_range)
{
float x = f_beg.x + 1.0 + float(ix);
avg_color += area_n * QuickSampleByPixel(float2(x, f_beg.y) + offset, uvw.z, gamma);
avg_color += area_s * QuickSampleByPixel(float2(x, f_end.y) + offset, uvw.z, gamma);
temp_color = QuickSampleByPixel(float2(x, f_beg.y) + offset, uvw.z, gamma);
avg_color += area_n * temp_color;
avg_luminance += area_n * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
temp_color = QuickSampleByPixel(float2(x, f_end.y) + offset, uvw.z, gamma);
avg_color += area_s * temp_color;
avg_luminance += area_s * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
}
}
@ -269,16 +305,22 @@ float4 AreaSampling(float3 uvw, float gamma)
if (iy < y_range)
{
float y = f_beg.y + 1.0 + float(iy);
avg_color += area_w * QuickSampleByPixel(float2(f_beg.x, y) + offset, uvw.z, gamma);
avg_color += area_e * QuickSampleByPixel(float2(f_end.x, y) + offset, uvw.z, gamma);
temp_color = QuickSampleByPixel(float2(f_beg.x, y) + offset, uvw.z, gamma);
avg_color += area_w * temp_color;
avg_luminance += area_w * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
temp_color = QuickSampleByPixel(float2(f_end.x, y) + offset, uvw.z, gamma);
avg_color += area_e * temp_color;
avg_luminance += area_e * pow(Luminance(temp_color.rgb), luminance_inv_gamma);
for (int ix = 0; ix < max_iterations; ++ix)
{
if (ix < x_range)
{
float x = f_beg.x + 1.0 + float(ix);
avg_color += QuickSampleByPixel(float2(x, y) + offset, uvw.z, gamma);
temp_color = QuickSampleByPixel(float2(x, y) + offset, uvw.z, gamma);
avg_color += temp_color;
avg_luminance += pow(Luminance(temp_color.rgb), luminance_inv_gamma);
}
}
}
@ -288,9 +330,22 @@ float4 AreaSampling(float3 uvw, float gamma)
float area_corners = area_nw + area_ne + area_sw + area_se;
float area_edges = float(x_range) * (area_n + area_s) + float(y_range) * (area_w + area_e);
float area_center = float(x_range) * float(y_range);
float4 nrm_color = avg_color / (area_corners + area_edges + area_center);
float target_nrm_color_luminance = avg_luminance / (area_corners + area_edges + area_center);
// Restore the averaged "gamma" space luminance, for better gamma correction.
// This retains the best feature of gamma correct sampling (no hue shifts),
// while also maintaining the perceptual "brightness" level of blending two colors with an alpha
// (in linear space a 0.5 alpha won't produce a color that has a perceptual brightness in the middle point of the two source colors).
float nrm_color_luminance = Luminance(nrm_color.rgb);
if (nrm_color_luminance != 0.0)
{
nrm_color.rgb *= pow(target_nrm_color_luminance, luminance_gamma) / nrm_color_luminance;
}
// Return the normalized average color.
return avg_color / (area_corners + area_edges + area_center);
return nrm_color;
}
/***** Main Functions *****/
@ -401,7 +456,7 @@ void main()
// Restore the original gamma without changes
else
{
color.rgb = pow(color.rgb, float3(1.0 / game_gamma));
color.rgb = pow(color.rgb, float3(1.0 / game_gamma));
}
SetOutput(color);