为了让你直观地感受到 Gamma 校正(Gamma Encoding) 的威力,我们来做一次具体的数值对比。
我们假设使用的标准 Gamma 值为 2.2。
公式为:
$$ \text{Gamma值} = \text{线性值}^{(1 / 2.2)} \approx \text{线性值}^{0.4545} $$
这里我们将对比 线性空间(Linear,物理真实亮度) 和 Gamma 空间(sRGB,存入图片或屏幕显示的数值)。为了方便理解,我同时列出了 0.0 - 1.0 (浮点数) 和 0 - 255 (8位整数) 的对应值。
1. 数值对比表
请特别注意暗部的变化幅度:
| 描述 | 线性值 (Linear) 物理光照强度 |
线性 8-bit (如果直接存) |
Gamma 校正后 (sRGB) 存入显存/图片的数值 |
sRGB 8-bit (最终色值) |
变化分析 |
|---|---|---|---|---|---|
| 纯黑 | 0.0 | 0 | 0.0 | 0 | 无变化 |
| 极暗部 | 0.01 (1% 亮度) | 2 | 0.123 | 31 | 暴增 12 倍! 保留了暗部细节 |
| 暗影 | 0.1 (10% 亮度) | 25 | 0.351 | 90 | 提升约 3.5 倍,从几乎看不见变成了深灰 |
| 物理中灰 | 0.218 (21.8% 亮度) | 55 | 0.500 | 128 | 关键点:物理亮度的 21% 对应数据上的 50% |
| 中间色 | 0.5 (50% 亮度) | 128 | 0.730 | 186 | 提升显著,物理的一半亮度看起来像 73% 的白 |
| 亮部 | 0.8 (80% 亮度) | 204 | 0.903 | 230 | 提升幅度变小 |
| 纯白 | 1.0 | 255 | 1.0 | 255 | 无变化 |
2. 核心观察与图解分析
A. 暗部的“扩容” (最重要的作用)
看表格中的第二行(极暗部):
- 线性世界:亮度只有
0.01。如果在 0-255 的整数中,它对应2。这意味着如果你不做 Gamma 校正直接存,0 到 1% 的亮度之间只有 0, 1, 2 这三个台阶,会出现极度明显的色带,暗部细节全部丢失(死黑)。 - Gamma 校正后:数值变成了
0.123(对应 8-bit 的31)。 - 结论:Gamma 校正把暗部极小的一段物理亮度,强行“拉伸”到了较大的数据空间中。它把宝贵的 8-bit 存储空间更多地分配给了人眼敏感的暗部。
B. 著名的“中灰” (Middle Gray)
看表格中的第四行:
- 在摄影和渲染中,有一个经典的认知:18%~22% 的物理反射率,人眼看起来是 50% 的灰色。
- 线性值 0.218 $\rightarrow$ Gamma 值 0.5。
- 这就是为什么如果你错误地使用了线性工作流(比如忘记做 Gamma 校正),直接把 0.218 输出到屏幕,屏幕会显示出很暗的颜色(物理上的 21%),而你原本期望的是一种“正中间的灰色”。
C. 为什么不用 SRGBA8 会导致画面“黑死”?
如果你在 Shader 里算出的光照是 0.1 (线性):
- **错误做法 (用 RGBA8)**:写入
0.1$\rightarrow$ 屏幕收到0.1$\rightarrow$ 屏幕 Gamma 2.2 压暗 $\rightarrow$ 最终人眼看到亮度 $0.1^{2.2} \approx 0.006$。- 结果:本来该有影子的细节,直接变成了纯黑,画面对比度极高,像烧焦了一样。
- **正确做法 (用 SRGBA8)**:写入
0.1$\rightarrow$ 硬件自动转为0.35(存入显存) $\rightarrow$ 屏幕收到0.35$\rightarrow$ 屏幕 Gamma 2.2 压暗 ($0.35^{2.2} \approx 0.1$) $\rightarrow$ 人眼看到亮度0.1。- 结果:所算即所见,光影柔和自然。
3. 代码模拟
如果把这个过程写成 Shader 代码,区别就是这样:
假设我们计算出的颜色是 vec3 linearColor = vec3(0.218, 0.218, 0.218);
1 | // 情况 1: 设置了 kEAGLColorFormatRGBA8 (普通格式) |
总结
Gamma 校正本质上是一个“劫富济贫”的过程:它压缩了亮部的数据精度,极大地扩充了暗部的数据精度,以符合人眼对暗部细节敏感的生理特性。