计算机图形:gamma校正
Gamma校正是什么
Gamma校正:让显示器的亮度,和人眼感知的亮度保持一致的修正过程.
本质上,是因为人眼对光的感知是非线性的,数字设备(如显示器)显示光强也是非线性的.
当我们计算出场景中所有像素最终颜色后,需要将其显示在显示器上. 大部分显示器(如阴极射线管显示器,CRT)有个物理特性:2倍的输入电压 并不能产生 2倍的亮度. 将输入电压加倍产生的亮度,约为输入电压的2.2次幂,这叫显示器Gamma.
Gamma也叫灰度系数,每种显示设备都有自己的Gamma值,都不相同. 有个公式:
\[设备输出亮度 = 电压的Gamma次幂, \]
几乎任何设备Gamma都不会等于1,1是一种理想状态:如果电压和亮度\(∈[0,1]\),那么多少电压就等于多少亮度.
对于CRT,Gamma通常为2.2. 于是,
人类感知的亮度,刚好跟CRT所显示的相似指数关系非常匹配. 如下图:

第一行(Perceived (lienar) brightness)是人眼感知亮度. 人眼所感知到的正常的灰阶,亮度增加1倍(如从0.1到0.2),我们才会感觉到比原来亮了1倍,才能感受到明显的颜色变化. 例如,颜色值从0.1到0.2,我们会感受到1倍的颜色变化,而从0.4到0.8 我们才能感受到相同程度的颜色变化(变亮1倍).
第二行是物理世界的真实亮度. 亮度加倍时,返回的也是真实的物理亮度(光子数量)
注:从0~1的过程中,亮度要增加1倍,我们才会感受到明显的颜色变化(即变亮1倍),即物理亮度.
也就是说,人眼看到颜色的亮度,并非线性亮度;而显示器使用的也是一种指数关系(电压的2.2次幂),所以物理亮度通过显示器能被映射到顶部的非线性亮度. 如此,显示器的亮度变化效果看起来还不错.
下图展示了显示器的非线性颜色、亮度的映射关系:

横轴代表我们定义的颜色(我们想输出的颜色),纵轴是亮度.
从右到左3条线描述的颜色、亮度关系,分别是:CRT显示器(gamma 2.2),理想状态(gamma 1),gamma校正(gamma 1/2.2)
例如,光的颜色\(L=(0.5,0.0,0.0)\)代表半暗红色,如果将其颜色翻倍,那么颜色变成\((1.0,0.0,0.0)\).
对于理想状态(中间的点线),颜色翻倍,则亮度翻倍;
对于显示器,显示的颜色0.5对应亮度0.218,颜色翻倍后,亮度翻倍4.59. 这显然不是我们想要的结果!
于是,我们引入Gamma校正,来修正这种非线性关系.
Gamma校正怎么做
流程
流程:
计算线性颜色(纹理采样、光照计算等)---> Gamma校正 ---> 显示颜色(显示器特性,应用Gamma 2.2曲线)
Gamma校正(Gamma Correction,伽马校正)思路:在最终的颜色输出到显示器之前,将Gamma的倒数作用到颜色上. 即上图最左边的那条虚线(称为逆伽马曲线),作用于显示器的Gamma 2.2曲线相反. 将每个线性输出颜色 乘以 逆伽马曲线,这样,显示器最终显示颜色时,就会变成线性的.
应用伽马校正前,显示器中间颜色偏暗;应用伽马校正后,中间颜色会变亮,整体会平衡.
例如,我们想显示暗红色\((0.5,0.0,0.0)\),在将颜色显示到显示器前,先对其应用Gamma校正曲线(逆Gamma曲线).
理想的亮度是0.5(中间点线),在显示器上显示相当于降低了2.2次幂的亮度(右边实线),即\((0.5,0.0,0.0)^{2.2}=(0.218,0.0,0.0)\)
因此,Gamma校正要将倒数\(1/2.2\)次幂作用到该颜色上,即\((0.5,0.0,0.0)^{1/2.2}=(0.5,0.0,0.0)^{0.45}=(0.73,0.0,0.0)\)
所以,最终显示器显示的颜色\((0.73,0.0,0.0)^{2.2}=(0.5,0.0,0.0)\) (颜色0.5,亮度0.5)
具体方法
在我们的场景中,有2种应用Gamma校正的方式:
- 使用OpenGL内建的sRGB帧缓冲;
- 自己在像素着色器中进行Gamma校正.
方式1最简单,缺点是会丧失一些控制权. 开启 GL_FRAMEBUFFER_SRGB选项,告诉OpenGL每个后续的绘制命令,在颜色存储到颜色缓冲之前,先校正sRGB颜色. sRGB颜色空间大致对应Gamma 2.2,也是大多数设备的标准. 开启该选项后,每次像素着色器运行后续帧缓冲,OpenGL自动执行Gamma校正,包括默认帧缓冲.
开启 GL_FRAMEBUFFER_SRGB:
glEnable(GL_FRAMEBUFFER_SRGB);
注意:所有的工作由OpenGL自动完成,我们无需添加额外的Gamma校正相关操作.
方式2灵活,但需要我们做更多工作. 我们在每个相关的像素着色器运行结束之前应用Gamma校正,最终颜色发送到显示器之前就结束了Gamma校正.
// 片段着色器
void main()
{
// 计算光照等
...
// 输出颜色前的最后一步
// 应用Gamma校正
float gamma = 2.2;
fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}
注意:Gamma校正将颜色从线性空间转换为非线性空间. Gamma校正必须在输出颜色前的最后一步进行,如果在这之前进行,就不是在线性空间操作颜色值.
sRGB纹理
sRGB纹理 是指纹理图像的颜色数据被存储在 sRGB 色彩空间(而非 线性RGB色彩空间)中的纹理. sRGB空间就是经过 Gamma 校正后的颜色空间,是非线性的.
平时,我们看到的普通图片,如jbg、png等,几乎都是已经自带 Gamma 压缩的图片,即sRGB纹理
为什么会存在sRGB纹理?
为了在有限的 8bit 存储空间里,让人眼看到的颜色尽可能均匀、好看、不浪费精度.
具体来说,体现在这几点:
- 人眼对亮度感知并非线性
- 亮度从 0 → 0.1:人眼觉得变化巨大
- 亮度从 0.9 → 1.0:人眼几乎看不出区别
如果不用sRGB,而用线性RGB存储颜色,那么0–1 平均分配 256 级(0~255),结果就是:暗部只有很少几级,容易出现色带、断层;亮部一大堆精度,完全浪费.
- 显示器也不是线性输出的
CRT显示天然特性:电压 -> 亮度 呈现非线性曲线特性,\(γ≈2.2\)
电压0.5,显示亮度不是0.5,而是\(0.5^{2.2}≈0.218\),会更暗.
- 于是,专家设计了sRGB标准
提前将图片颜色做一次Gamma压缩(Gamma编码),即颜色的\(1/2.2\)次幂.
RGB空间和sRGB空间关系示意图:
┌─────────────────────────────────────┐
│ 物理/线性空间 │
│ (光照计算、渲染方程、物理模拟) │
│ 数值与光子数量成正比 │
└───────────────┬─────────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 相机传感器 │ │ 渲染输出 │ │ GPU 采样时 │
│ 线性响应 │ │ (帧缓冲) │ │ 自动解码 │
└───────┬────────┘ └────────────────┘ │ sRGB→Linear │
│ └───────┬────────┘
│ 编码 (1/2.2) │
▼ │
┌────────────────┐ │
│ sRGB 图像文件 │ ◀─────────────────────────────────────┘
│ (PNG/JPG) │ 采样时硬件自动转回线性
│ 存储优化 │
│ 显示器兼容 │
└───────┬────────┘
│
│ 显示时,显示器天然 Gamma (~2.2)
│ (sRGB 显示器已校准,输出正确亮度)
▼
┌────────────────┐
│ 人眼感知 │
│ (非线性) │
└────────────────┘
在我们的OpenGL程序中,如果要计算高级光照,流程是这样的:
- 加载 sRGB 图片,
stbi_load - 转成线性空间(用 GL_SRGB 纹理格式),
glTexImage2D - 在线性空间计算光照,片段着色器中,用户自定义程序
- 最后输出时做 gamma 校正(^1/2.2),片段着色器输出颜色前的最后一步
- 显示器再 \(γ 2.2\) → 最终正确,显示器自动完成
注意:现代液晶(LCD / LED / OLED)显示器,全都在刻意模拟 CRT 那套~2.2 的 Gamma 曲线,因为标准强制要求,否则CRT Gamma 2.2条件下生成的内容都会过亮、发白、完全看不清.

浙公网安备 33010602011771号