显示编码与 sRGB
Display Encoding & sRGB
光照、纹理等计算假设数值是线性的,但显示缓冲和纹理使用非线性编码。简短的回答是:着色器输出 [0,1] 的颜色要取 次幂(gamma 校正),读入的纹理和颜色做相反的变换,多数情况下可以让 GPU 自动完成。忽略它会导致画面偏暗、色相偏移、光照叠加错误和抗锯齿边缘"绳索化"。
为什么有非线性编码
- CRT 的输入电压与发出辐亮度呈大于 1 的幂函数关系(例如幂为 2 时,50% 的像素只发出 1/4 的光)。LCD 等显示器内置转换电路来模仿 CRT 的响应。
- 这个幂函数几乎正好是人眼亮度感知的逆,所以编码近似感知均匀:相邻编码值的感知差异在整个范围内大致恒定(人眼在很多条件下能察觉约 1% 的亮度差)。这样在有限精度的显示缓冲和纹理中能最大程度减少色带。
- 显示传递函数 (EOTF) 描述缓冲中的数字值与显示器发出辐亮度的关系,是硬件的一部分;采集设备一端是 OETF。
编码与解码
flowchart LR A["PNG 等颜色纹理 显示编码"] -->|"解码"| B["线性值: 着色与色调映射"] B -->|"编码"| C["帧缓冲"] C -->|"显示器传递函数"| D["发出的辐亮度 与线性计算值成正比"]
上图对应书中 Figure 5.39:读纹理时解码成线性值,着色和色调映射后编码写入帧缓冲;编码函数与显示器传递函数相互抵消,最终辐亮度与计算值成正比。
- 个人电脑显示器的标准传递函数由 sRGB 规范定义。多数 API 可以设置在读取纹理、写入颜色缓冲时自动做 sRGB 转换;mipmap 生成会考虑 sRGB 编码;双线性插值会先转成线性再插值;alpha 混合会先解码、混合、再编码。
- 转换要在渲染的最后阶段写入显示帧缓冲时进行。如果在显示编码之后再做后处理,就是在非线性值上计算,通常错误且会产生伪影。
- 可以把显示编码看作一种最大程度保留感知效果的压缩。
公式
线性 → sRGB 编码: sRGB → 线性解码:
- 线性小段是为了让变换在数字硬件上可完全逆转。幂函数部分可近似为 ,,这就是"gamma 校正"名称的来源。
- 移动端和浏览器有时用更简单的 、,虽然粗糙,但比完全忽略好。
- 截图或拾色器得到的颜色、PNG/JPEG/GIF 中的值都是显示编码的数据,参与着色计算前必须解码。
常见错误
- 手动转换时用了编码后的颜色当线性值,或者重复编码/解码两次。
- 不编码:线性 0.1、0.2、0.5 本应编码为 0.351、0.481、0.730,直接输出会让暗部过暗、部分颜色色相偏移。过去建模人员常把暗色人为调亮来补偿,相当于把逆显示变换"烤"进了资产。(0 和 1 在任何变换下不变。)
- 光照叠加错误:两盏聚光灯 0.6 与 0.4 在非线性空间相加,左边看起来亮得多,重叠处亮得不真实。
- 抗锯齿边缘:线性覆盖率 0.125、0.375、0.625、0.875 若不经编码直接显示会过暗,边缘看起来像扭曲的绳子,称为 roping。
之后的发展
sRGB 标准于 1996 年制定。更亮、色域更宽的显示器出现后,相关内容见第 8 章(色度学与色彩空间、色调映射)。
GEA 的视角:位图颜色格式与 gamma(卷 II 11.3.3、11.3.4.2)
位图、渲染目标与颜色格式
- 位图是数字编码颜色的二维表,单元格即像素。引擎的输出是叫帧缓冲的位图;引擎绘制进去的任何缓冲(帧缓冲或离屏图像)都叫渲染目标 (render target);贴在表面上的位图叫纹理(卷 II 11.3.3)。
- 分辨率与宽高比:早期电视 4:3(fullscreen),电影偏好 16:9(widescreen),如今 16:9 是电影和游戏的事实标准,部分游戏支持 21:9、32:9 超宽屏。"4K"指约 4,000 像素宽的 16:9 格式(精确为 3,840),又称 2160p,后者引用的是高度;p 指逐行扫描,与隔行扫描相对。可用 验证 16:9。
原书问题(11.3.3.1):
- GEA 说一百万像素"更精确地说是 1,048,576 即 像素"。百万像素 (megapixel) 的 mega 是十进制 SI 前缀,1 MP = 像素(Wikipedia "Pixel" 的 Megapixel 一节);书中自己的例子也只在十进制下成立:5,312 × 2,988 = 15,872,256 ≈ 15.9 × ,若按 算只有约 15.1 MP。
- GEA 说 IMAX 画幅"高大于宽(通常 1:1.43 或 1:1.90)"。IMAX 的 1.43:1 和 1.90:1 是宽:高,画面仍是宽大于高,只是比 16:9(1.78:1)更方或略宽(Wikipedia "IMAX")。
- "电影人偏好 16:9"不准确:影院宽银幕常用 1.85:1 和 2.39:1;16:9 是 SMPTE 的 Kerns Powers 在 1980 年代为 HDTV 选的折中比例(约为 4:3 与 2.35:1 的几何平均),便于以最少的黑边兼容各种画幅(Wikipedia "16:9 aspect ratio")。
- 颜色格式描述编码方式、每通道如何存储(浮点或整数)以及位数,常以"编码 + 每像素位数"概括:
| 格式 | 每像素 | 说明 |
|---|---|---|
| 3×FP32 | 96 位 | 引擎内部做光照计算用,HDR 需要高精度 |
| RGB888 | 24 位 | 每通道 0–255 |
| RGB565 | 16 位 | 红蓝 5 位、绿 6 位 |
| I8 / R8 | 8 位 | 只存亮度的灰度图 |
| 调色板 | 少量位 | 像素存颜色表索引,早期 2D 像素画常用 |
| RGBA8888 | 32 位 | 加 8 位 alpha |
| RGBA5551 | 16 位 | 1 位 alpha,只能全透明或全不透明 |
| log-LUV | 32 位 | HDR:亮度 L 以 2 为底取对数存 16 位,色度 U、V 各 8 位(人眼对亮度变化比对色度更敏感) |
| FP16 | 每通道 16 位 | HDR:IEEE 754-2008 半精度,1 位符号、5 位指数、10 位尾数(含隐含位 11 位) |
- alpha 表示不透明度,0 为全透明、1 为全不透明。GEA 指出它在光线追踪引擎里用处不大,但在光栅化引擎里被广泛用作模拟光穿过参与介质的"hack"(见 透明渲染与 Alpha 混合)。FP16 的位布局见 数值表示与浮点数。
gamma 校正
- 把 [0,1] 浮点颜色线性映射到 [0,255] 会在人眼不敏感的亮部浪费精度、在敏感的暗部精度不足,所以写入整数格式时要先做 gamma 校正(卷 II 11.3.4.2)。
- GEA 把公式写成 : 叫编码 gamma(gamma 压缩),用于把线性浮点颜色存进位图; 叫解码 gamma(gamma 展开),用于读入压缩位图后转回浮点参与着色。所有光照计算都应在线性空间进行。
- 由来:老式 CRT 对输入信号的响应是约 的幂函数;现代显示器没有这种固有曲线,但为了一致性仍模仿它,所以引擎写帧缓冲前要做 gamma 编码,由显示器抵消。
与本页 RTR4 内容的对照
- 记号不同:RTR4 把编码写成 、;GEA 把指数本身叫 ,编码时 。两者等价,读公式时要看清 指的是哪个数。
- sRGB 帧缓冲的描述:RTR4 说缓冲中存的是显示编码后的非线性值,API 在写入颜色缓冲时自动做 sRGB 转换;GEA 卷 II 11.3.4 则写道"今天的显卡支持 sRGB 格式,颜色以线性空间存储,gamma 校正由硬件自动应用"。按 RTR4 的说法,硬件自动转换的是"写入"和"读取"这两步,缓冲中的数据本身是编码过的;GEA 这句话容易被理解为缓冲里存的是线性值。这里并列记录。
原书问题(11.3.4):按字面理解 GEA 的说法是错的。D3D 的
*_SRGB格式(如DXGI_FORMAT_R8G8B8A8_UNORM_SRGB)中存的是 sRGB 编码后的值,硬件在着色器读取时解码为线性、在写入渲染目标(以及混合)时编码回 sRGB(MS Learn "Gamma correction" / "Converting data for the color space");所以着色器"看到"的是线性值,内存里存的不是。RTR4 的描述正确。
PBRT 的视角:ColorEncoding(PBRT 4e B.5.6)
- 动机:CRT 的亮度与电压呈 关系;如今显示器不再用电子枪,但量化存储(8 位)仍应使用非线性编码,因为按韦伯定律 (Weber's law) 人眼大约需要 1% 的相对变化才能察觉,线性编码会把码值浪费在看不出的差别上。
- sRGB 编码: 时 ,否则 ;逆变换以 为界。pbrt 的
LinearToSRGB8()用有理多项式近似避免std::pow,8 位解码用 256 项查找表,任意浮点用另一个有理近似。 - pbrt 内部全部用线性浮点,只在读写 PNG 等定点格式时编解码;输出推荐 OpenEXR 或 PFM 这类浮点格式以保留高动态范围。
LearnOpenGL 的视角:线性工作流与 sRGB 纹理(Advanced Lighting / Gamma Correction)
来源:Gamma Correction;摘录 Advanced Lighting — Gamma Correction。与上文 RTR/PBRT 的显示编码对照;此处补 OpenGL 开关与着色器落地。
监视器近似把电压按 γ≈2.2 映射亮度(sRGB 空间接近该曲线)。若在「看起来正确」的监视器空间里配光再当线性用,中间亮度会被压暗,翻倍光源在屏上可亮四倍以上。做法:在最终输出前做逆 gamma(×^(1/2.2)),或 glEnable(GL_FRAMEBUFFER_SRGB) 让硬件在写入颜色缓冲前校正。多 pass 时中间缓冲保持线性,只在最后一档校正。
纹理:美术在监视器上画的漫反射图已在 sRGB;若渲染端再做一次 gamma,会过亮。可用 pow(tex, 2.2) 或内部格式 GL_SRGB / GL_SRGB_ALPHA 在采样时自动转到线性。镜面/法线等数据贴图通常已是线性,不要标成 sRGB。
衰减:无 gamma 时 往往过猛,人们改用 或可调二次式;进入线性工作流后物理上的 突然更合理,因为以前等于又被监视器 gamma 再压了一次。更复杂的 仍可用,但参数要按 gamma 场景重调。PBR 章要求输入全线性,与此一致。
GPU Gems 的视角:保持线性的重要性(GPU Gems 3 第 24 章)
来源:Gritz & d'Eon《The Importance of Being Linear》;摘录 Chapter 24. The Importance of Being Linear — Gritz, d'Eon。与上文 RTR/PBRT/LearnOpenGL/GEA 的 gamma 讨论同源;本章把症状与皮肤着色绑在一起讲透。
光传输是线性的,CRT/仿 CRT 的 LCD 响应接近指数(γ≈2.0–2.4)。JPEG 默认按 γ≈2.2 预校正,在非线性空间做光照、mip 平均、alpha 合成都会算错。典型症状:Lambert 球体 60° 处本应一半亮,γ=2 屏上只剩四分之一;mip 边界亮度跳动;皮肤次表面在非线性空间调参会出现蜡感与阴影边缘青绿色;高光加到漫反射上发黄。
处方三步:假定玩家显示器约 2.2(或提供校准图);颜色纹理用 sRGB 格式取样时硬件线性化(GF8+ 在过滤前线性化),法线/高度/alpha 不要线性化;最后一步才做输出 gamma(优先 sRGB 帧缓冲,混合也会先线性化再校正)。中间 8 位线性缓冲会丢掉暗部精度,中间色多用 16F 或 sRGB FBO。「纹理也不线性化、输出也不校正」的双重错误有时看起来还行,但一改灯光颜色就穿帮——游戏与电影 CG 观感差距里,色彩管理是常被低估的一项。
出处
- realtime_rendering_4ed.pdf 第 5 章 Shading Basics
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 11 章 11.3.3、11.3.4.2 节
- PBRT 4e source excerpt: chapter B (Utilities)
- Pharr, Jakob, Humphreys《Physically Based Rendering: From Theory to Implementation》第 4 版(MIT Press, 2023)附录 B B.2–B.7 节与延伸阅读,pbr-book.org/4ed/Utilities
- Chapter 24. The Importance of Being Linear — Gritz, d'Eon
- developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-24-importance-being-linear
- Advanced Lighting — Gamma Correction
- learnopengl.com/Advanced-Lighting/Gamma-Correction