计算机图形:gamma校正

Gamma校正是什么

Gamma校正:让显示器的亮度,和人眼感知的亮度保持一致的修正过程.

本质上,是因为人眼对光的感知是非线性的,数字设备(如显示器)显示光强也是非线性的.

当我们计算出场景中所有像素最终颜色后,需要将其显示在显示器上. 大部分显示器(如阴极射线管显示器,CRT)有个物理特性:2倍的输入电压 并不能产生 2倍的亮度. 将输入电压加倍产生的亮度,约为输入电压的2.2次幂,这叫显示器Gamma.

Gamma也叫灰度系数,每种显示设备都有自己的Gamma值,都不相同. 有个公式:

\[设备输出亮度 = 电压的Gamma次幂, \]

几乎任何设备Gamma都不会等于1,1是一种理想状态:如果电压和亮度\(∈[0,1]\),那么多少电压就等于多少亮度.
对于CRT,Gamma通常为2.2. 于是,

\[显示器的输出亮度 = 输入电压的2.2次幂 \]

人类感知的亮度,刚好跟CRT所显示的相似指数关系非常匹配. 如下图:

img

第一行(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次幂),所以物理亮度通过显示器能被映射到顶部的非线性亮度. 如此,显示器的亮度变化效果看起来还不错.

下图展示了显示器的非线性颜色、亮度的映射关系:

img

横轴代表我们定义的颜色(我们想输出的颜色),纵轴是亮度.

从右到左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校正的方式:

  1. 使用OpenGL内建的sRGB帧缓冲;
  2. 自己在像素着色器中进行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 存储空间里,让人眼看到的颜色尽可能均匀、好看、不浪费精度.

具体来说,体现在这几点:

  1. 人眼对亮度感知并非线性
  • 亮度从 0 → 0.1:人眼觉得变化巨大
  • 亮度从 0.9 → 1.0:人眼几乎看不出区别

如果不用sRGB,而用线性RGB存储颜色,那么0–1 平均分配 256 级(0~255),结果就是:暗部只有很少几级,容易出现色带、断层;亮部一大堆精度,完全浪费.

  1. 显示器也不是线性输出的

CRT显示天然特性:电压 -> 亮度 呈现非线性曲线特性,\(γ≈2.2\)

电压0.5,显示亮度不是0.5,而是\(0.5^{2.2}≈0.218\),会更暗.

  1. 于是,专家设计了sRGB标准

提前将图片颜色做一次Gamma压缩(Gamma编码),即颜色的\(1/2.2\)次幂.

RGB空间和sRGB空间关系示意图:

                     ┌─────────────────────────────────────┐
                     │          物理/线性空间               │
                     │    (光照计算、渲染方程、物理模拟)     │
                     │    数值与光子数量成正比              │
                     └───────────────┬─────────────────────┘
                                     │
            ┌────────────────────────┼────────────────────────┐
            │                        │                        │
            ▼                        ▼                        ▼
   ┌────────────────┐      ┌────────────────┐      ┌────────────────┐
   │  相机传感器    │      │  渲染输出      │      │  GPU 采样时    │
   │  线性响应      │      │  (帧缓冲)      │      │  自动解码      │
   └───────┬────────┘      └────────────────┘      │  sRGB→Linear  │
           │                                        └───────┬────────┘
           │ 编码 (1/2.2)                                   │
           ▼                                                │
   ┌────────────────┐                                       │
   │  sRGB 图像文件 │ ◀─────────────────────────────────────┘
   │  (PNG/JPG)    │     采样时硬件自动转回线性
   │  存储优化     │
   │  显示器兼容   │
   └───────┬────────┘
           │
           │ 显示时,显示器天然 Gamma (~2.2)
           │ (sRGB 显示器已校准,输出正确亮度)
           ▼
   ┌────────────────┐
   │   人眼感知     │
   │   (非线性)    │
   └────────────────┘

在我们的OpenGL程序中,如果要计算高级光照,流程是这样的:

  1. 加载 sRGB 图片,stbi_load
  2. 转成线性空间(用 GL_SRGB 纹理格式),glTexImage2D
  3. 在线性空间计算光照,片段着色器中,用户自定义程序
  4. 最后输出时做 gamma 校正(^1/2.2),片段着色器输出颜色前的最后一步
  5. 显示器再 \(γ 2.2\) → 最终正确,显示器自动完成

注意:现代液晶(LCD / LED / OLED)显示器,全都在刻意模拟 CRT 那套~2.2 的 Gamma 曲线,因为标准强制要求,否则CRT Gamma 2.2条件下生成的内容都会过亮、发白、完全看不清.

参考

Gamma校正 | LearnOpenGL

posted @ 2026-03-25 16:37  明明1109  阅读(23)  评论(0)    收藏  举报