着色基础 (Shading Basics)
欢迎来到实时渲染的核心地带。第五章是后续所有高级光照和材质技术的基础。本章的核心目标是:定义一个物体“看起来是什么样”的规则,并理解光如何与这些规则互动。
着色
着色(Shading)的本质是计算一个物体表面上某个点的颜色。这个计算过程依赖于多种因素,包括但不限于:物体本身的材质属性、表面朝向、观察者的位置以及场景中的光照。
5.1 着色模型 (Shading Model)
核心观点: 着色模型是一套数学公式和规则,它精确描述了如何根据输入信息(如法线、视角、光照方向)计算出最终的像素颜色。它可以是追求物理真实的,也可以是完全风格化的。
- 
关键输入向量 (Key Input Vectors): 几乎所有着色模型都离不开这三个基本单位向量: - 表面法线 (Normal): 垂直于物体表面的向量,决定了表面的朝向。
- 观察方向 (View): 从表面指向摄像机或眼睛的向量。
- 光照方向 (Light): 从表面指向光源的向量。
 
- 
常见的数学工具 (Common Mathematical Tools): 着色模型是这些基础运算的组合艺术。 - 点乘 (Dot Product): 。核心用途是衡量两个向量的对齐程度。例如, 的结果(即夹角余弦值)直接反映了光线照射到表面的强度。
- Clamp (范围限制): 将一个值限制在特定范围内,如 clamp(x, 0, 1)。在着色中极其常用,以避免出现负值光照或超出范围的颜色。本书常用 表示max(x, 0),用 表示clamp(x, 0, 1)。
- 线性插值 (Linear Interpolation): lerp(a, b, t)或mix(a, b, t)。用于在两个值(如颜色)之间进行平滑过渡,是实现混合效果的基础。
- 反射 (Reflection): reflect(i, n)。计算入射向量i相对于法线n的反射向量,是实现高光的关键。
 
- 
示例:Gooch 着色模型 这是一个经典的**非真实感渲染 (NPR)**模型,目标是技术图纸的清晰度。它的核心思想很简单: - 用一个暖色调 () 去表现被光照亮的区域。
- 用一个冷色调 () 去表现背光的区域。
- 根据法线 和光照方向 的点乘结果,在冷暖色调之间进行线性插值。
- 额外叠加一层高光 (),增加细节和立体感。
 最终公式结构如下,它完美地体现了“混合”与“插值”的思想: 
5.2 光源 (Light Sources)
核心观点: 光源为着色模型提供关键的输入参数:光照方向 和 光照颜色/强度 。不同类型的光源,其 和 的计算方式也不同。
- 
通用着色结构 (General Shading Structure): 现代渲染引擎中,一个通用的多光源着色模型可以被拆分为 unlit 和 lit 两部分。 - 部分: 模拟物体在没有直接光源照射时的基础外观,通常用于表现环境光或间接光。
- 部分: 计算单个直接光源对物体表面的贡献。
 
- 
兰伯特余弦定律 (Lambert's Cosine Law): 这是图形学中最基础、最重要的光照物理原理。它指出:表面接收到的光照强度正比于表面法线 和光照方向 之间夹角的余弦值。 - 实现上就是 max(0, dot(n, l))或者 。
- 这个因子几乎是所有基于物理的着色模型的起点。最简单的兰伯特 (Lambertian) 模型就是将这个因子与表面颜色和光颜色相乘。
 
- 实现上就是 
5.2.1 方向光 (Directional Light)
- 核心特性: 模拟一个无限远的光源,例如太阳。
- 关键属性:
- 光照方向 是一个恒定向量,在场景中任何位置都一样。
- 没有位置的概念,因此没有距离衰减。
- 光照颜色 通常也是恒定的。
 
5.2.2 精确光源 (Punctual Lights)
- 
核心特性: 光源来自空间中的一个无限小的点,因此它有明确的位置。 
- 
光照方向 的计算: 对于表面上的点 和光源位置 : 
- 
点光源/泛光灯 (Point Light / Omni Light): - 向所有方向均匀发光。
- 核心机制:距离衰减 (Distance Attenuation)。物理上正确的衰减是平方反比衰减 (Inverse-Square Falloff),即光强与距离 的平方成反比 ()。
- 实践中的问题与解决方案:
-  时,光强  (奇点问题):
- 方案A: 在分母上加一个极小值 :。
- 方案B: 限制最小距离 ,认为光源有物理半径:。
 
- 衰减范围无限远 (性能问题):
- 解决方案: 乘以一个窗函数 (Window Function)。该函数在最大距离 处平滑地将光强降为 0,从而限定光源的影响范围,便于性能优化。例如:
 
 
-  时,光强  (奇点问题):
 
- 
聚光灯 (Spotlight): - 本质上是一个增加了方向性衰减的点光源,光线被限制在一个圆锥体内。
- 关键参数:
- 光锥方向 (Spot Direction): 聚光灯的朝向。
- 本影角 (Umbra Angle): 光锥完全衰减为0的角度。
- 半影角 (Penumbra Angle): 光锥内部保持最亮强度的角度。
 
- 实现: 强度从半影角到本影角之间平滑过渡。通常通过计算光线反方向  与聚光灯方向  的夹角余弦值,并使用 smoothstep或类似函数实现平滑衰减。
 
- 
其他精确光源: - IES 配置文件 (IES Profiles): 使用照明工程学会定义的真实世界灯具测量数据,来模拟复杂和非对称的光照分布,极大增强了真实感。
 
5.2.3 其他光源类型
- 面光源 (Area Lights):
- 这是对真实世界光源更精确的模拟。光源具有物理上的尺寸和形状(如矩形、圆形),而不仅仅是一个点。
- 它们是实现软阴影 (Soft Shadows) 和镜面反射中光源形状的关键。
- 实时渲染面光源的技术正在快速发展,是高级渲染的重要课题。
 
本章总结与展望
第五章为你揭示了着色计算的“配方”:着色模型(做什么) + 光源(用什么原料) = 最终颜色。你已经掌握了最经典的光源类型和它们在着色方程中的作用。
- 核心要点:
- 着色就是解一个关于法线、视角、光照的颜色方程。
- 方向光、点光源、聚光灯是实时渲染的三大基础光源类型,它们的区别在于如何计算 和 。
- 距离衰减和方向衰减是控制光照范围和形状的关键工具,并且在实践中需要做很多性能与效果的权衡。
- 面光源是通往更高级真实感渲染的大门。
 
有了这些基础,你已经准备好进入更复杂的领域,如材质的物理原理(第九章及以后)、阴影(第七章)和全局光照(第十章、第十一章)等。
这次聚焦于将理论付诸实践的实现细节以及渲染中一个永恒的挑战——抗锯齿。这部分内容对于引擎开发者来说至关重要,它决定了渲染效率和最终的画面质量。
着色实现与抗锯齿
我们已经了解了着色模型和光源的理论基础。现在,我们要探讨如何高效地在代码中实现它们,并解决渲染过程中最常见的视觉瑕疵:锯齿。
5.3 实现着色模型 (Implementing Shading Models)
核心观点: 高效的着色实现不仅仅是把公式翻译成代码,更关键在于决定计算的执行位置和频率 (Where and When)。优化渲染性能的第一步就是将计算尽可能地移到更新频率更低的地方。
5.3.1 计算频率 (Frequency of Computation)
这是一个性能优化的金字塔,从最不频繁(最快)到最频繁(最慢)排列:
- 编译时/预计算 (Compile-Time / Pre-computation): 对永远不变的常量,直接在着色器代码中硬编码,或者在程序加载时计算一次。成本几乎为零。
- CPU 端计算 (Per-Frame / Per-Object):
- 对于每一帧、每个物体或每次 draw call 才变化一次的数据(如视图投影矩阵、光源位置、材质颜色),在 CPU 上计算好,然后通过 Uniform 变量传入 GPU。
- 原则: 避免在 GPU 上为每个顶点/像素重复计算相同的值。
 
- GPU 端计算 (Varying Computation):
- 顶点着色器 (Vertex Shader - VS): 逐顶点执行。适合几何变换、顶点动画等与顶点相关的计算。
- 像素着色器 (Pixel Shader - PS): 逐像素执行。这是绝大多数光照和材质计算发生的地方。
 
- 
关键实践:为何选择逐像素着色? - 像高光这类非线性的着色效果,如果在顶点着色器中计算,然后由光栅化器进行线性插值,会导致严重的视觉错误(如棱角分明的高光)。
- 因此,现代渲染管线普遍采用逐像素着色 (Per-Pixel Shading) 以保证高质量的视觉效果。顶点着色器的主要职责是准备数据(如将位置、法线变换到世界空间)并传递给像素着色器。
 
- 
实现中的“坑”与最佳实践: - 法线归一化: 由于光栅化插值会改变向量长度,必须在像素着色器中重新归一化 (Normalize) 法线向量。同时,在顶点着色器中也最好归一化一次,以避免因模型缩放等操作导致的插值错误。
- 向量插值: 不要对归一化后的方向向量(如光照方向 、观察方向 )进行插值,这会产生错误的方向。正确的做法是:在 VS 中传递顶点位置,然后在 PS 中用插值后的位置重新计算这些方向向量。
 
5.3.2 实现示例 (Implementation Example)
这里通过一个多光源 Gooch 着色器的 GLSL 示例,展示上述原则:
- 着色器结构:
- Uniforms: 接收来自 CPU 的不变数据,如光源数组 uLights、相机位置uEyePosition、表面颜色相关的常量uWarmColor等。
- VS → PS 的数据流:
- Vertex Shader: 接收模型顶点数据(位置、法线),将其变换到世界空间,然后作为 out变量 (Varying) 传递出去。
- Pixel Shader: 接收来自 VS 的 in变量(插值后的世界空间位置vPos和法线vNormal)。
 
- Vertex Shader: 接收模型顶点数据(位置、法线),将其变换到世界空间,然后作为 
- Pixel Shader main()函数逻辑:- 准备输入: normalize(vNormal)和normalize(uEyePosition.xyz - vPos),获取干净的法线和观察向量。
- 初始化颜色: 设置 unlit部分的颜色。
- 循环处理光源: 遍历 uLights数组,对每个光源:- 计算光照方向 l。
- 计算兰伯特因子 dot(n, l)。
- 调用一个独立的 lit()函数计算该光源的贡献。
- 将贡献累加到最终颜色上。
 
- 计算光照方向 
 
- 准备输入: 
 
- Uniforms: 接收来自 CPU 的不变数据,如光源数组 
5.3.3 材质系统 (Material System)
核心观点: 材质系统是渲染引擎中用于管理和组合海量着色器变体的框架。它在艺术家和底层着色器代码之间建立了一座桥梁。
- 
核心概念区分: - 着色器 (Shader): 底层的 GPU 程序,是实现。
- 材质 (Material): 艺术家使用的资源,封装了表面的视觉外观(颜色、粗糙度等参数),是概念。
 
- 
管理复杂性的策略: - 代码复用 (#include): 将公用函数(如光照计算、数学工具)放在共享文件中。
- 做减法 - 超级着色器 (Ubershader): 创建一个包含所有可能功能的大而全的着色器,然后通过预处理指令 (#if) 和动态分支 (if) 来启用或禁用特定功能。这是目前最主流的方案。
- 做加法 - 节点化 (Node-Based): 在可视化编辑器(如 UE 材质编辑器)中,将功能封装成节点,艺术家通过连接节点来创建新材质。编辑器在后台将节点图“翻译”成着色器代码。
- 基于模板 (Template-Based): 定义标准接口(如“表面着色器”),允许开发者替换不同的实现,同时保持管线其他部分不变。
 
- 代码复用 (
5.4 锯齿和抗锯齿 (Aliasing and Antialiasing)
核心观点: 锯齿 (Aliasing) 本质上是一个采样问题。因为我们用离散的像素点去表示一个连续的几何世界,当采样率不足时,高频信息(如三角形边缘)就会丢失或被错误地表示,产生阶梯状的“锯齿”。
5.4.1 采样与滤波理论基础
- 奈奎斯特采样定理 (Nyquist Sampling Theorem): 要想完美地从采样点中重建原始信号,采样频率必须大于信号最高频率的两倍。
- 图形学困境: 三角形边缘、阴影边界等在数学上是突变的,含有无限高的频率。因此,无论我们用多高的分辨率,理论上都无法完全避免锯齿。
 
- 重建 (Reconstruction): 使用一个滤波器 (Filter) 从离散的采样点恢复连续信号的过程。
- Box 滤波器 (最近邻): 效果最差,产生块状、不连续的结果。
- Tent 滤波器 (线性插值): 效果稍好,结果连续但不平滑。
- Sinc 滤波器 (理想低通): 理论上能完美重建,但因其无限范围和负值区域,在实践中无法直接使用。
 
- 重采样 (Resampling):
- 放大 (Upsampling): 先重建,再用更高密度采样。
- 缩小 (Downsampling): 必须先滤波(模糊以去除高频信息),再用更低密度采样。否则会产生严重的摩尔纹等走样瑕疵。这是纹理 Mipmapping 的理论基础。
 
5.4.2 基于屏幕的抗锯齿 (Screen-Based AA)
核心策略: 通过在单个像素区域内采集更多的样本信息,来更精确地估算该像素的最终颜色。
- 
1. 超采样 (SSAA - Supersampling Antialiasing): - 方法: 以数倍于目标分辨率渲染整个场景,然后将高分辨率图像下采样(缩小)到目标分辨率。
- 优点: 质量最高,实现最简单。
- 缺点: 性能开销巨大,因为着色、内存带宽等所有成本都成倍增加。
 
- 
2. 多重采样 (MSAA - Multisampling Antialiasing): - 核心思想: 将可见性(覆盖率)采样与着色采样解耦,是 SSAA 的关键优化。
- 方法:
- 每个像素有多个子采样点,每个点都有独立的深度/模板信息。
- 像素着色器只对每个图元(三角形)执行一次。
- 将这一次的着色结果,复制到所有被该图元覆盖的子采样点上。
- 最后,将一个像素内所有子采样点的颜色进行平均,得到最终颜色。
 
- 优点: 性能远高于 SSAA,能有效处理几何边缘锯齿。
- 缺点: 对着色器内部产生的锯齿(如高光、纹理)无效。
 
- 
3. 时域抗锯齿 (TAA - Temporal Antialiasing): - 核心思想: 复用历史帧的采样信息来提升当前帧的等效采样率。
- 方法:
- 每一帧都对投影矩阵进行微小的抖动 (Jitter),使采样点落在不同的亚像素位置。
- 利用运动向量 (Motion Vectors) 将前一帧的图像“重投影”到当前帧的像素空间,使其与当前帧对齐。
- 将当前帧与重投影后的历史帧进行混合。
 
- 优点: 以极低的性能开销实现非常高的采样质量,是目前游戏中最主流的 AA 方案。
- 缺点: 运动物体容易产生鬼影 (Ghosting) 和模糊 (Blur),需要复杂的算法来抑制这些瑕疵。
 
- 
4. 形态学方法 (Morphological Methods - FXAA, SMAA): - 核心思想: 作为后处理,通过图像分析技术来检测和抚平已渲染图像中的锯齿边缘。
- 方法:
- 渲染一幅带锯齿的图像。
- 运行一个专用的后处理着色器,该着色器会分析像素邻域的颜色/亮度差异,识别出锯齿的“阶梯”模式。
- 根据识别出的模式,智能地在像素间进行颜色混合,从而平滑边缘。
 
- 优点: 性能开销极小,且与渲染管线解耦(例如,可以用于延迟渲染)。
- 缺点: 不是真正的“抗”锯齿,而是“后处理”锯齿。可能导致整体画面轻微模糊,或错误处理精细细节(如文字)。
 
第五章的最后一个核心技术部分:透明度、Alpha 和合成。这在实时渲染中是一个经典难题,因为它挑战了 Z-Buffer 的基本工作方式。理解了这里的各种技术和它们的权衡,你就能在项目中做出明智的决策。
透明度、Alpha 与合成
本节的核心是解决一个问题:如何让物体看起来是“透”的? 这意味着我们需要看到一个物体“背后”的东西,这与我们之前处理不透明物体的逻辑完全不同。
- 核心概念区分:
- 基于视图的透明 (View-based): 我们本节的焦点。它模拟透明物体如何衰减或混合其背后物体的颜色,产生视觉上的透明感。
- 基于光线的透明 (Light-based): 更复杂的物理现象,如焦散(光线穿过透明物体后聚焦)、彩色阴影等。这会在后续章节深入探讨。
 
引子:一些不依赖混合的透明技术
在深入主流的 Alpha Blending 之前,了解一些另类的技术有助于拓宽思路。
- 点阵剔除半透明 (Screen-Door Transparency):
- 方法: 像纱窗一样,用棋盘格图案来绘制透明物体,只填充部分像素,让背景“漏”出来。
- 优点: 极其简单高效,无需排序,没有复杂的混合状态。
- 缺点: 效果粗糙,通常只能模拟约 50% 的透明度,且多层透明物体叠加时效果很差。
 
- 随机透明度 (Stochastic Transparency):
- 方法: 在亚像素级别使用随机模式来决定是否丢弃片元,用片元的“存活率”来模拟 Alpha 值。
- 优点: 概念上统一了抗锯齿和透明度,无需混合。
- 缺点: 会产生噪点,需要非常高的采样率才能获得干净的图像,内存开销大。
 
5.5.1 主流方法:Alpha 混合与排序问题
核心观点: 当前最主流的透明效果实现方式是 Alpha 混合 (Alpha Blending),它通过将前景(透明物体)和背景的颜色进行混合来实现透明感。但这种方法的正确性严重依赖于渲染顺序。
- 
Alpha () 的含义: - Alpha 是一个介于 [0, 1] 之间的值,它同时封装了两个概念:
- 不透明度 (Opacity): 材质本身有多“实”。
- 像素覆盖率 (Coverage): 这个片元覆盖了当前像素的多少面积。
 
 
- Alpha 是一个介于 [0, 1] 之间的值,它同时封装了两个概念:
- 
over运算符 (Theoveroperator):- 这是实现标准半透明效果的基石公式,模拟了将一个半透明颜色“叠加”在背景颜色之上的过程。
 - : 源 (Source) 的颜色和 Alpha,即你正要绘制的透明物体。
- : 目标 (Destination) 的颜色,即帧缓冲中已经存在的颜色。
- : 输出 (Output) 的最终颜色。
- 直观理解: 最终颜色 = Alpha * 源颜色 + (1 - Alpha) * 目标颜色。这是一个加权平均。
 
- 
叠加混合 (Additive Blending): - 公式:
- 用途: 模拟发光、自发光的效果,如火焰、火花、闪电。它只向场景中增加光亮,而不会遮挡背景。
 
- 
核心难题:排序问题 (The Sorting Problem) - 规则: 要想使用 over运算符得到正确的结果,所有透明物体必须严格地按照从后往前 (Back-to-Front) 的顺序进行渲染。
- 为什么是难题:
- 对物体按中心点排序是一种粗略的近似,对于相互穿插、凹面或巨大的物体,这种排序会完全失效。
- 逐三角形排序的开销在实时渲染中是无法接受的。
- 这是传统透明渲染技术最大的痛点。
 
 
- 规则: 要想使用 
5.5.2 解决方案:顺序无关的透明度 (Order-Independent Transparency - OIT)
核心观点: OIT 是一系列旨在摆脱从后往前排序限制的算法。它们的目标是无论物体以何种顺序提交渲染,都能得到正确或近似正确的透明效果。
以下是几种主流的 OIT 技术,它们在精度、性能、内存开销之间做出了不同的权衡:
- 
深度剥离 (Depth Peeling): - 思想: 像剥洋葱一样,一“层”一“层”地渲染。
- 方法: 通过多个 Pass 渲染。第一个 Pass 找出最靠近相机的透明层,第二个 Pass 找出第二近的层,以此类推。最后将所有层从后往前混合。
- 优点: 结果精确。
- 缺点: 性能开销巨大,因为每个 Pass 都需要重新绘制所有透明几何体。
 
- 
基于链表的 A-Buffer (Linked-List A-Buffer): - 思想: 在每个像素上,建立一个存储所有穿过该像素的透明片元的链表。
- 方法: 利用现代 GPU 的原子操作等特性,在渲染时动态构建片元链表。在所有几何体绘制完毕后,启动一个全屏 Pass,在每个像素上遍历其链表,就地排序并混合。
- 优点: 像素级别的精确排序,内存按需分配。
- 缺点: 性能和内存开销没有上界。在毛发、烟雾等具有大量重叠透明层的复杂场景中,性能会急剧下降。
 
- 
K-Buffer / 多层 Alpha 混合 (Multi-Layer Alpha Blending): - 思想: A-Buffer 的一个有界 (bounded) 变体,是性能和质量的折中。
- 方法: 为每个像素预先分配固定大小(例如 4 或 8 个)的存储空间。只对最靠近相机的 K 个片元进行精确排序和存储。对于超出 K 层的更深层片元,则通过一些近似方法(如与最远的已有片元合并)进行处理。
- 优点: 性能和内存开销可控,可以优雅地降级质量。
- 缺点: 当透明层数超过 K 时,结果是近似的。
 
- 
加权混合 OIT (Weighted Blended OIT): - 思想: 完全放弃排序,通过一个近似的数学公式,在一个 Pass 内完成混合。
- 方法: 渲染所有透明物体时,不直接写入最终颜色,而是将它们的贡献累加到几个中间缓冲区中。最后通过一个全屏 Pass,使用一个加权平均公式(通常会考虑深度值,让近的物体权重更高)计算出最终颜色。
- 优点: 速度快,单 Pass,硬件兼容性好。
- 缺点: 结果是物理不正确的近似值。它混合(blend)颜色而非叠加(layer)颜色,对于高 Alpha 值的物体效果较差。
 
5.5.3 实用技巧:Alpha 预乘与合成 (Premultiplied Alpha & Compositing)
核心观点: Alpha 预乘是一种存储 RGBA 数据的不同方式,它可以简化混合运算并解决图像滤波时的常见瑕疵。
- 
两种 Alpha 存储方式: - 未预乘 Alpha (Unmultiplied / Straight Alpha): (R, G, B, A)。存储的是物体的“纯色”和独立的 Alpha 值。例如,一个 50% 透明的红色(1,0,0)物体,存储为(1, 0, 0, 0.5)。这是艺术家在 Photoshop 中最熟悉的方式。
- 预乘 Alpha (Premultiplied / Associated Alpha): (R*A, G*A, B*A, A)。RGB 分量已经预先乘以了 Alpha 值。同一个物体,存储为(0.5, 0, 0, 0.5)。
 
- 未预乘 Alpha (Unmultiplied / Straight Alpha): 
- 
为什么 Alpha 预乘更好? - 更高效的混合: over运算符的计算被简化。 原来的两次乘法和一次加法,变成了一次乘法和一次加法。
- 正确的滤波和插值: 对未预乘 Alpha 的 RGBA 值进行线性插值(例如纹理的 Mipmapping)是错误的,会导致物体边缘出现不自然的黑色或暗色条纹。对预乘 Alpha 的值进行插值则能得到正确的结果。
- 渲染的自然产物: 在黑色背景上渲染一个带抗锯齿的物体,其输出的 RGBA 值天然就是预乘形式的。
 
- 更高效的混合: 
结论: 在进行渲染和图像处理的管线中,应尽可能早地将数据转换为 Alpha 预乘格式,并在该格式下进行所有混合与滤波操作。
我们来完成第五章的最后一部分,这也是一个非常关键但常常被误解的概念:显示编码 (Display Encoding)。正确处理它,你的渲染结果才能在屏幕上呈现出物理上和感知上都正确的样子。
显示编码
核心观点: 我们的渲染计算(光照、混合等)都假设在一个线性 (Linear) 空间中进行,即亮度值 0.2 + 0.2 应该等于 0.4。然而,我们日常使用的显示器和图像文件(如 PNG, JPEG)都工作在一个非线性 (Non-linear) 空间,这个空间通常被称为 Gamma 空间或 sRGB 空间。显示编码就是在这两个空间之间进行正确转换的过程,以确保最终画面不会失真。
一句话总结工作流: 从文件/输入解码到线性空间 → 在线性空间中进行所有计算 → 将计算结果编码回非线性空间用于存储/显示。
5.6.1 为什么存在非线性空间?(The "Why")
你可能会问,为什么不都用线性空间,岂不是更简单?原因有二:
- 历史原因 (Historical): 老式的 CRT 显示器在物理特性上,其输入电压和输出亮度之间本身就是一种幂函数关系 (power-law),而非线性关系。现代显示器为了兼容性,也模拟了这一行为。
- 感知原因 (Perceptual): 这恰好是一个“美丽的意外”。人眼对亮度的感知也是非线性的,我们对暗部区域的亮度变化比对亮部区域更敏感。sRGB 编码方式将更多的存储精度(在有限的 8-bit 中)分配给了暗部区域,这恰好迎合了人眼的特性,能够以最少的数据量最大化地减少我们能感知到的色带瑕疵 (Banding Artifacts)。可以把它理解为一种感知上的压缩算法。
5.6.2 核心概念与流程 (The "How")
- 伽马校正 (Gamma Correction): 这是一个通用术语,指应用一个幂函数来校正图像亮度的过程。
- sRGB: 这是目前 PC 显示器和 web 图像最广泛使用的标准化的非线性色彩空间。它是一种特定的伽马曲线。
- 黄金法则: 所有渲染计算(光照、混合、抗锯齿等)必须在线性颜色空间中进行。
正确的渲染管线工作流如下:
- 
输入端 - 解码 (Decode): - 当你从一张 sRGB 格式的纹理(如 .png文件)中采样颜色时,这个颜色值是非线性的。
- 你必须先将其解码 (Decode) 为线性值,才能在着色器中进行正确的计算。
- 现代图形 API (DirectX, OpenGL, Vulkan) 可以在硬件层面自动完成这个过程,你只需要在创建纹理时指定其格式为 SRGB即可。
 
- 当你从一张 sRGB 格式的纹理(如 
- 
计算端 - 线性空间运算: - 在你的像素着色器中,所有的输入颜色都已经是线性的了。
- 在这里执行你所有的光照计算、颜色混合等操作。
 
- 
输出端 - 编码 (Encode): - 着色器计算出的最终颜色是线性的。
- 在将这个颜色写入到用于显示的帧缓冲 (Framebuffer) 之前,必须将其编码 (Encode) 回 sRGB 空间。
- 同样,现代图形 API 也可以自动完成这个过程,只需将你的交换链/渲染目标的格式设置为 SRGB。
 
图示:伽马校正流程图。纹理(sRGB) → 解码 → 线性空间中的着色计算 → 编码 → 帧缓冲(sRGB) → 显示器 (sRGB EOTF)。编码和显示器的转换函数相互抵消,保证了线性计算结果的正确呈现。
5.6.3 转换公式 (The Math)
虽然 GPU 通常会帮你处理,但理解背后的数学原理很重要。设 为线性空间的值, 为 sRGB 空间的值(均在 [0, 1] 范围内)。
- 通用伽马近似 ()
- 这是最常用也最容易理解的简化版本。
- 编码 (线性 → sRGB):
- 解码 (sRGB → 线性):
 
- 精确的 sRGB 标准
- sRGB 曲线在接近黑色的地方其实是一段线性区,以避免幂函数在零点附近的无限斜率。
- 编码 (线性 → sRGB):
- 解码 (sRGB → 线性):
 
5.6.4 如果不做伽马校正,会发生什么?(The Consequences)
忽略伽马校正会在多个方面严重破坏你的渲染结果:
- 亮度错误: 整个场景的中间色调会显得过暗。艺术家可能会为了补偿而手动调亮贴图,但这是一种错误的做法。
- 光照计算错误: 在非线性空间中进行加法运算是错误的。如下图所示,两盏灯光叠加的区域会异常地亮,不符合物理规律。
- 抗锯齿 (AA) 效果错误: 物体边缘的抗锯齿效果会被破坏。混合出来的中间色调(灰色)会因为过暗而失效,导致边缘看起来更硬,甚至产生一种被称为扭绳 (roping) 的瑕疵。
- 颜色混合错误: Alpha Blending 的结果也是不正确的,因为混合的加权平均是在非线性空间中进行的。
结论: 始终保持你的渲染管线是“伽马正确 (Gamma Correct)”的。 这是实现高质量、物理可信渲染的基础要求。幸运的是,现代引擎和图形 API 已经将大部分复杂性自动化了,但作为引擎开发者,你必须深刻理解其背后的原理,才能在出现问题时进行正确的调试。