RealTime-Rendering10-Gamma Correction

Gamma Correction

人类视觉与物理亮度

人类的眼睛对暗部变化更敏感,对亮部变化较迟钝。显示器利用这一特性,在有限的位深(通常是 8 位)下,将更多精度分配给暗部,从而在视觉上提供更均匀的亮度分布。

显示器的非线性

CRT 显示器(以及现代 LCD 模拟其行为)具有非线性响应。这种非线性特性实际上与人类视觉系统相匹配。输入电压与输出亮度之间的关系大致为:

01.png

其中 γ≈2.2。这意味着如果我们在帧缓冲区中存储线性光照值,显示器会错误地显示它们,使暗部变得更暗。

02.png

显示器的伽马响应曲线。横轴是输入信号,纵轴是输出亮度。注意曲线在暗部更为陡峭。

这种非线性响应实际上对人类视觉是有益的,因为人类的眼睛对暗部变化更为敏感。然而,这对计算机图形学带来了挑战。

伽马矫正

早期的图形程序员很快发现了这个问题。为了在显示器上获得看起来正确的图像,他们需要对输出进行补偿。这就是”伽马校正”的起源——在将值写入帧缓冲区之前,将其提升到 1/γ 次幂(约 0.45)。
为了在显示器上正确显示图像,我们需要对输入进行”预校正”——即伽马校正:

03.png

这样,显示器的伽马响应(提升到 2.2 次幂)与这个预校正相互抵消,最终产生线性的亮度输出。

正确的渲染管线应该是:

1
线性空间光照计算 → 伽马校正(pow(1/2.2))→ 帧缓冲区 → 显示器(pow(2.2))→ 线性亮度感知

纹理中的伽马

大多数图像(照片、手绘纹理)已经在非线性空间(sRGB)中存储。这是因为:

  • 数码相机自动应用伽马校正
  • 图像编辑软件默认工作在 sRGB 空间
  • 显示器期望 sRGB 输入
    如果直接用于光照计算而不转换,会导致错误:
1
2
3
4
// ❌ 错误:在非线性纹理上进行光照计算
float3 texColor = tex2D(diffuseMap, uv); // 已经在gamma空间
float3 lit = texColor * lightIntensity; // 错误!应该是线性空间
// 结果:暗部过于黑暗,对比度错误

sRGB色彩空间

sRGB是一种标准颜色空间,它定义了:

  • 色域(红绿蓝三原色的坐标)
  • 伽马响应曲线(近似于 gamma 2.2,但有一段线性部分)
    关键事实:大多数图像(照片、互联网图片、纹理)都存储在 sRGB 空间中。这意味着它们已经被伽马校正过。
    由于光照计算是在线性空间进行的,针对sRGB纹理, 需要先将其从sRGB空间转换到线性空间,有两种方式:
    第一种手动在shader中转换:
1
2
float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));

第二种使用sRGB纹理格式:

1
2
3
4
5
6
7
// 创建纹理时指定 sRGB 格式
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0,
GL_RGB, GL_UNSIGNED_BYTE, data);

// 或者带 Alpha 通道
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, data);

当在着色器中采样时,硬件自动将 sRGB 值转换为线性值(pow 2.2):

1
2
3
4
5
6
7
8
9
10
11
uniform sampler2D diffuseTexture;  // 绑定为 sRGB 格式

void main() {
vec3 texColor = texture(diffuseTexture, TexCoords).rgb;
// texColor 已经是线性的!硬件自动转换

vec3 result = texColor * lightColor * cosTheta;
// ... 其他线性计算 ...

FragColor = vec4(result, 1.0); // 如果 GL_FRAMEBUFFER_SRGB 启用,自动输出校正
}
纹理类型 存储格式 采样行为
漫反射/基础颜色 GL_SRGB8 / DXGI_FORMAT_R8G8B8A8_UNORM_SRGB 硬件自动解伽马
法线贴图 GL_RGBA8 / DXGI_FORMAT_R8G8B8A8_UNORM 直接读取(已是线性)
粗糙度/金属度/AO GL_R8 / DXGI_FORMAT_R8_UNORM 直接读取(线性)
HDR 环境贴图 GL_RGBA16F / DXGI_FORMAT_R16G16B16A16_FLOAT 直接读取(线性)
光照贴图 GL_RGB9_E5 / DXGI_FORMAT_R9G9B9E5_SHAREDEXP 根据生成方式决定

openGL中实现gamma矫正

方法 1:手动在片元着色器中校正

1
2
3
4
5
6
7
8
9
void main() {
// 在光照计算后,输出前进行伽马校正
vec3 color = /* 光照计算结果 */;

float gamma = 2.2;
color = pow(color, vec3(1.0 / gamma));

FragColor = vec4(color, 1.0);
}

方法2:OpenGL提供了自动伽马校正机制:

1
2
// 启用 sRGB 帧缓冲区
glEnable(GL_FRAMEBUFFER_SRGB);

启用后,当写入颜色缓冲区时,OpenGL 会自动将线性颜色值转换为 sRGB 空间(近似于 pow 1/2.2)。

HDR 渲染中的顺序

先进行色调映射再进行伽马矫正

1
2
3
4
5
// HDR 管线
vec3 hdrColor = /* 线性空间光照 */;
vec3 mapped = toneMap(hdrColor); // 色调映射,输出仍在线性空间
mapped = pow(mapped, vec3(1.0 / 2.2)); // 最后伽马校正
FragColor = vec4(mapped, 1.0);