引言

前置阅读:建议先阅读向量(点积)和渲染管线

光照是计算机图形学的灵魂。虽然现代引擎使用复杂的 PBR (Physically Based Rendering),但理解经典的光照模型仍然是基础。 经典光照模型通常由三部分组成: \[ Color = Ambient + Diffuse + Specular \]

Lighting Models

1. 漫反射 (Diffuse) - Lambert

漫反射模拟的是粗糙表面(如砖墙、布料)对光的散射。 光线照射到表面后,向四面八方均匀反射。 强度取决于光线方向 \(\vec{L}\) 与表面法线 \(\vec{N}\) 的夹角。

\[ I_{diffuse} = \max(0, \vec{N} \cdot \vec{L}) \]

  • 当光线垂直表面 (\(\theta = 0^\circ\)),\(\cos\theta = 1\),最亮。
  • 当光线平行表面 (\(\theta = 90^\circ\)),\(\cos\theta = 0\),无光。
  • 背面光照会被 \(\max(0, \dots)\) 截断为 0。

2. 镜面反射 (Specular) - Phong

镜面反射模拟的是光滑表面(如金属、塑料)的高光 (Highlight)。 它取决于观察方向 \(\vec{V}\) 和反射方向 \(\vec{R}\) 的夹角。

\[ \vec{R} = \text{reflect}(-\vec{L}, \vec{N}) \] \[ I_{specular} = (\max(0, \vec{R} \cdot \vec{V}))^{shininess} \]

  • Shininess (光泽度): 指数越高,高光点越小越亮(越光滑)。

3. 改进模型 - Blinn-Phong

Phong 模型有一个缺点:计算反射向量 \(\vec{R}\) 比较昂贵。 Jim Blinn 提出了一种优化:使用半程向量 (Halfway Vector) \(\vec{H}\)\(\vec{H}\) 是光线方向 \(\vec{L}\) 和观察方向 \(\vec{V}\) 的中间向量。

\[ \vec{H} = \text{normalize}(\vec{L} + \vec{V}) \] \[ I_{specular} = (\max(0, \vec{N} \cdot \vec{H}))^{shininess} \]

  • 优点: 计算 \(\vec{H}\)\(\vec{R}\) 快。
  • 效果: 高光看起来更柔和、更真实,是 OpenGL/DirectX 固定管线的默认选择。

4. 环境光 (Ambient)

为了模拟间接光照(光线在墙壁间反弹),经典模型简单地加上一个常数颜色。 \[ I_{ambient} = K_a \] 这是一种非常粗糙的近似,现代游戏通常使用全局光照 (GI) 或环境光遮蔽 (AO) 来替代。

5. PBR 基础 (Physically Based Rendering)

PBR 是一个庞大的主题,但掌握核心概念对游戏开发者至关重要。本节覆盖能量守恒、菲涅尔效应和 Cook-Torrance BRDF 的整体结构。如果你想深入学习 PBR,推荐 Google 的 Filament 文档 和 Naty Hoffman 在 SIGGRAPH 的演讲。

现代游戏引擎(Unity URP/HDRP, Unreal)默认使用 PBR。它比 Blinn-Phong 更符合物理规律。

能量守恒 (Energy Conservation)

出射光线的能量不能超过入射光线的能量。 漫反射和镜面反射是互斥的:光线要么进入物体内部(漫反射),要么被表面反弹(镜面反射)。 \[ k_D + k_S \le 1 \] 物体越光滑(金属),镜面反射越强,漫反射就越弱(看起来越黑)。

菲涅尔效应 (Fresnel Effect)

观察一下你身边的物体(比如桌子或手机屏幕)。当你垂直看它时,反光可能不强;但当你以近乎平行的角度(掠射角)看它时,反光会变得非常强烈,像镜子一样。 这就是菲涅尔效应。 Schlick 近似公式: \[ F(\theta) = F_0 + (1 - F_0)(1 - \cos\theta)^5 \] 其中 \(F_0\) 是基础反射率(垂直观察时的反射率)。不同材质的 \(F_0\) 差异巨大——非金属通常在 0.02–0.05 之间,金属则可以达到 0.5–1.0。

Cook-Torrance BRDF

PBR 的镜面反射部分通常使用 Cook-Torrance 微表面 BRDF:

\[ f_{spec} = \frac{D \cdot G \cdot F}{4(\vec{N} \cdot \vec{L})(\vec{N} \cdot \vec{V})} \]

三个核心项各自描述了一种物理现象:

名称 作用
D 法线分布函数 (NDF) 控制有多少微表面朝向半程向量 \(\vec{H}\),直接决定高光的形状和大小。常用 GGX / Trowbridge-Reitz。
G 几何遮蔽函数 模拟微表面之间的互相遮挡(self-shadowing),粗糙度越高遮蔽越强。常用 Smith-GGX。
F 菲涅尔项 即上文的 Schlick 近似,决定不同角度的反射率。

分母 \(4(\vec{N} \cdot \vec{L})(\vec{N} \cdot \vec{V})\) 是校正因子,确保 BRDF 在数学上保持能量守恒。

最终的光照方程将漫反射和镜面反射组合起来: \[ L_o = (k_D \frac{c}{\pi} + f_{spec}) \cdot L_i \cdot (\vec{N} \cdot \vec{L}) \] 其中 \(c\) 是表面颜色 (albedo),\(L_i\) 是入射光辐照度。

6. 材质参数参考 (Material Parameters)

下表列出了常见材质的 PBR 参数,对美术和 Shader 开发者非常实用:

材质 F₀ (线性值) Roughness 范围 Metallic
水 (Water) 0.02 0.0–0.1 0
皮肤 (Skin) 0.028 0.3–0.5 0
塑料 (Plastic) 0.04 0.3–0.6 0
布料 (Fabric) 0.04 0.8–1.0 0
玻璃 (Glass) 0.04 0.0–0.05 0
铁 (Iron) 0.56 0.3–0.7 1
铜 (Copper) (0.95, 0.64, 0.54) 0.2–0.5 1
金 (Gold) (1.0, 0.76, 0.33) 0.1–0.4 1
银 (Silver) (0.97, 0.96, 0.91) 0.1–0.3 1

注意:非金属的 \(F_0\) 是单通道灰度值(通常 0.02–0.05),金属的 \(F_0\) 是 RGB 三通道(因为不同波长的反射率不同,这正是金属呈现颜色的原因)。

业界存在两种主流 PBR 工作流,了解它们的区别有助于跨引擎协作和资产迁移。

Metallic/Roughness 工作流

这是 Unity Standard ShaderUnreal Engine 4/5 的默认工作流。

  • BaseColor (Albedo): 非金属 → 表面固有色;金属 → 充当 \(F_0\)(反射颜色)。
  • Metallic: 0 或 1(二值化)。中间值(如 0.5)在物理上没有意义,只用于过渡区域(如生锈边缘)。
  • Roughness: 0 = 完美镜面,1 = 完全粗糙。

Specular/Glossiness 工作流

较早的工作流,部分引擎仍支持(如 Unity 的 Specular Setup)。

  • Diffuse: 与 BaseColor 类似,但金属的 diffuse 为黑色。
  • Specular: 直接指定镜面反射颜色(即 \(F_0\)),给予美术更多自由度,但也更容易违反物理规则。
  • Glossiness: 与 Roughness 相反,Glossiness = 1 - Roughness。

对比表

特性 Metallic/Roughness Specular/Glossiness
贴图数量 3 张 (BaseColor, Metallic, Roughness) 3 张 (Diffuse, Specular, Glossiness)
金属反射色 由 BaseColor 提供 由 Specular 贴图提供
物理正确性 更容易保持——metallic 只有 0/1 需要美术手动控制 Specular 范围
引擎支持 Unity Standard, Unreal, Godot, Filament Unity Specular Setup, 部分移动端引擎
推荐场景 新项目首选 旧资产迁移或需要精细控制

实践建议:新项目统一使用 Metallic/Roughness 工作流。它更简洁,不容易出错,引擎支持最广泛。

8. Shader 调试技巧

光照效果不对是 Shader 开发中最常见的问题。以下是几种典型症状和排查方法:

症状 可能原因 修复方法
高光过亮 / 泛白 (Blown out) 未做能量守恒或缺少 HDR Tonemapping 确保 \(k_D + k_S \le 1\);启用 ACES 或其他 Tonemapping
物体看起来很”平” (Flat) 法线方向错误或 N·L 计算有误 将法线可视化(color = N * 0.5 + 0.5)检查是否正确
边缘菲涅尔光环过强 \(F_0\) 值设置过高 非金属 \(F_0\) 应在 0.04 左右,不要超过 0.05
金属看起来像塑料 Metallic 不是 1.0 纯金属 Metallic 必须为 1.0;0.5 在物理上无意义
高光形状怪异 / 拉伸 切线空间法线贴图的切线/副切线错误 检查 tangent 和 bitangent 是否正交且方向正确

一个非常实用的调试手法:在 Fragment Shader 中临时输出中间变量作为颜色,例如:

// 可视化法线方向
FragColor = vec4(N * 0.5 + 0.5, 1.0);

// 可视化 NdotL(漫反射贡献)
float NdotL = max(dot(N, L), 0.0);
FragColor = vec4(vec3(NdotL), 1.0);

// 可视化 Fresnel
float fresnel = pow(1.0 - max(dot(N, V), 0.0), 5.0);
FragColor = vec4(vec3(fresnel), 1.0);

9. Shader 代码示例 (HLSL/GLSL)

// 输入: N (法线), L (光照方向), V (观察方向)
vec3 N = normalize(Normal);
vec3 L = normalize(LightDir);
vec3 V = normalize(ViewDir);

// Diffuse
float diff = max(dot(N, L), 0.0);

// Specular (Blinn-Phong)
vec3 H = normalize(L + V);
float spec = pow(max(dot(N, H), 0.0), 32.0);

// Result
vec3 color = (ambient + diff + spec) * objectColor;

10. 总结

  • 点积再次成为主角,用于计算光照强度。
  • Blinn-Phong 是对 Phong 的一种高效近似,至今仍被广泛使用。
  • Cook-Torrance BRDF 是现代 PBR 镜面反射的核心,由 NDF、几何遮蔽和菲涅尔三项组成。
  • 掌握 材质参数工作流差异 能帮你在引擎中快速调出正确的材质效果。
  • 遇到光照 Bug 时,善用 中间变量可视化 是最高效的调试手段。

< 上一篇: 渲染管线 | 回到目录 | 下一篇: 曲线数学 >

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。