中EN
精修课概念页

经典光照:环境光、漫反射和一点高光

Shading Equation & Classic Lighting Models

从渲染方程到一个求和式

完整的渲染方程要对一个半球上所有方向的入射光做积分,实时渲染算不起。《游戏引擎架构》(GEA) 用两条假设把它化简:

  1. 场景里只有理想化的点光源和方向光,没有面光源;
  2. 间接光照忽略不计,或者只做粗略的近似。

理想光源照到表面上的一点,只有一个入射方向 ℓk\ell_k。所以积分变成了对光源的求和,再加上一个代表全部间接光的环境光项,就得到了着色方程:

Lv(v)=Lv,amb+∑k=1nfr(ℓk→v) Ev,k (n⋅ℓk)+L_v(\mathbf{v}) = L_{v,\text{amb}} + \sum_{k=1}^{n} f_r(\ell_k \to \mathbf{v})\, E_{v,k}\, (\mathbf{n} \cdot \ell_k)^+

翻译成大白话:看到的亮度 = 环境光 + 每个光源各自贡献一份。每一份由三样东西相乘:材质的反射特性 frf_r(BRDF)、光源的照度 Ev,kE_{v,k},以及法线和光线夹角的余弦(小于 0 时取 0)。RGB 之间的乘除都是逐分量的,正是第三课讲的「颜色相乘」。

这个方程给不出照片级的真实感,但能产生可信的画面。它是早期游戏的全部光照,也是理解 BRDF 怎么用的最好入门。

几个向量

所有的经典光照模型都围绕表面上一点的几个单位向量展开:

nℓrvhℓ 指向光源,v 指向眼睛r:ℓ 的镜面反射方向h:ℓ 和 v 的半向量
n 是表面法线;ℓ 从着色点指向光源;v 从着色点指向眼睛;r 是 ℓ 关于法线的镜面反射方向;h 是 ℓ 和 v 的半向量,正好平分它们的夹角。
  • 反射向量:r=2(n⋅ℓ) n−ℓ\mathbf{r} = 2(\mathbf{n}\cdot\ell)\,\mathbf{n} - \ell,入射角等于反射角。GLSL 里写作 reflect(-l, n)。
  • 半向量:h=(ℓ+v)/∥ℓ+v∥\mathbf{h} = (\ell + \mathbf{v}) / \lVert \ell + \mathbf{v} \rVert,GLSL 里写作 normalize(l + v)。

三个分量

LearnOpenGL 把经典光照拆成三项,分别对应三种现象:

环境光漫反射高光合起来++=
环境光让背光面不至于全黑;漫反射决定明暗的大形;高光是一个亮点,位置随视线移动。三项加起来,就是一个有体积感的球。

环境光

一个常数因子乘以光的颜色,模拟所有没被建模的间接光,免得背光面一片漆黑。

真实的间接光在场景里是变化的,但通常变得很平缓。所以更讲究的做法是把它存成一个随位置和方向缓慢变化的函数,比如一张环境贴图。低频的模型表达不了表面细节和自遮挡,通常还要再乘一个环境光遮蔽项。

漫反射:Lambert

理想的漫反射表面把光均匀地散射到各个方向。在着色器里,就是法线和光线方向点积,再截掉负数:

float ndl = max(dot(n, l), 0.0);
vec3 diffuse = albedo * lightColor * ndl;

夹角越大,同样的光摊在越大的面积上,看起来就越暗。

高光:Phong 与 Blinn-Phong

漫反射在各个方向上都一样亮,高光却只在某些方向上亮:眼睛越接近镜面反射的方向,看到的高光越强。

  • Phong(1975):比较反射向量 r\mathbf{r} 和视线 v\mathbf{v},pow(max(dot(r, v), 0.0), α);
  • Blinn-Phong(1977):比较半向量 h\mathbf{h} 和法线 n\mathbf{n},pow(max(dot(n, h), 0.0), α)。

指数 α\alpha 叫光泽度:越大,高光越小、越集中。高光通常更接近光的颜色,而不是物体的漫反射色。顺带一提,「银色」其实不是一种颜色,而是一个处处镜面反射的白色物体。

下面是一个在片元着色器里「假装」出来的球:每个像素按它在圆盘上的位置算出法线。把鼠标移进画布,光源会跟着鼠标走:

拖着光源照一个球
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

分量 0 是三项相加,1、2、3 分别只看环境光、漫反射、高光。Blinn 为 0 时换成原始的 Phong。

为什么要换成半向量

Blinn-Phong 不只是换了一种写法,它修了 Phong 的一个毛病。

Phong 比较的是视线和反射方向的夹角。光泽度很低(高光很大很散)时,这个夹角一超过 90°,点积就变成负数,被 max 截成 0,高光在那里被硬生生切断。漫反射在 90° 以外归零是合理的,镜面反射测的却是视线和反射方向的夹角,低指数时那里本该还有一点贡献。

半向量和法线的夹角大约只有视线和反射方向夹角的一半,只要光源不在表面下方,就不会超过 90°,也就没有这道硬切口。

两种高光,随夹角怎么衰减

直接改上面的表达式就能重画:x 是 0 到 1 的横坐标,可以用 sin、pow、smoothstep 等函数。

横轴是视线和反射方向的夹角,从 0° 到 180°。第一个预设是 Phong,在 90°(x = 0.5)处被硬切成 0;第二个是 Blinn-Phong(这里按光线、视线、法线在同一平面的情况画,半向量的夹角正好是一半),一直平滑地降到 180°。光泽度很低时,差别最明显。

三个版本对照

GEA 列出了三种写法。它们都不是物理准确的,只是合理的近似:

模型镜面项说明
原始 Phong(1975)Cspecπ(r⋅v)αn⋅ℓ\dfrac{C_{\text{spec}}}{\pi}\dfrac{(\mathbf{r}\cdot\mathbf{v})^\alpha}{\mathbf{n}\cdot\ell}能量不守恒,也不满足 Helmholtz 互易
修正 Phongα+22πCspec(r⋅v)α\dfrac{\alpha+2}{2\pi}C_{\text{spec}}(\mathbf{r}\cdot\mathbf{v})^\alpha去掉除以 n⋅ℓ\mathbf{n}\cdot\ell,乘上归一化因子
Blinn-Phong(1977)α+88πCspec(n⋅h)α\dfrac{\alpha+8}{8\pi}C_{\text{spec}}(\mathbf{n}\cdot\mathbf{h})^\alpha半向量在微表面理论的推导里也会自然出现,有物理依据

实际的引擎常常把归一化因子简化掉,并进镜面色里,得到最常见的写法:

Lv(v)=Lv,amb+∑k=1n(Cdiff′+Cspec′ (n⋅hk)α) Ev,k (n⋅ℓk)+L_v(\mathbf{v}) = L_{v,\text{amb}} + \sum_{k=1}^{n} \big(C'_{\text{diff}} + C'_{\text{spec}}\,(\mathbf{n}\cdot\mathbf{h}_k)^\alpha\big)\, E_{v,k}\, (\mathbf{n}\cdot\ell_k)^+

有些教材只把余弦乘在漫反射上,不乘在高光上。GEA 的看法是:Blinn-Phong 本来就是早于现代辐射度量方法的一个 hack,余弦放不放进镜面项,都不是百分之百物理正确。

在 3D 里试试

下面换成一个真正的三维模型。可以切换光照模型、换形状,用滑块调光源的方位、仰角和各项强度:

三种光照模型
Lambert 只有漫反射;Phong 和 Blinn-Phong 多了高光。把光泽度调低,在 Phong 下转动光源,留意高光边缘有没有被切断。

在哪里算:逐顶点还是逐像素

同一个光照模型,放在管线的不同位置算,效果差别很大:

  • 平面着色 (flat shading):每个三角形只算一次,通常只有一个光源、不算高光。最早的引擎这么做,现在主要用作刻意的风格。
  • Gouraud 着色:只在顶点上算光照,三角形内部对颜色做线性插值。网格密时效果尚可,三角形一大,线性插值的毛病就很明显,高光尤其容易走样。
  • Phong 着色:在片元着色器里逐像素计算。注意,这里的「Phong」说的是在哪算,和 Phong 光照模型是两回事。
左边逐顶点,右边逐像素
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

左边只在网格的顶点上算光照,格子里面插值颜色;右边每个像素都算。网格越粗,左边的高光越模糊、越走形。

在游戏里怎么用

取舍与常见的坑

小结与练习

  • 着色方程 = 环境光 + 对每个光源求和:BRDF × 照度 × 余弦。
  • Lambert 漫反射看 n⋅ℓ\mathbf{n}\cdot\ell;Phong 高光看 r⋅v\mathbf{r}\cdot\mathbf{v},Blinn-Phong 看 n⋅h\mathbf{n}\cdot\mathbf{h}。半向量避免了 Phong 在低光泽度时的硬切口。
  • 同一个模型可以逐三角形、逐顶点或逐像素计算;逐像素最平滑,代价也最高。

练习:在「拖着光源照一个球」的代码里加第二个光源:一盏从右下方打过来的蓝色补光,强度只有主光的三分之一。把两盏灯的漫反射和高光分别加起来,看看球的立体感有什么变化。

  1. Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 12 章 12.4.1、12.4.3.1–12.4.3.5、12.5.1、12.5.8、12.5.10 节
  3. Lighting — Basic Lighting
  4. learnopengl.com/Lighting/Basic-Lighting
  5. Advanced Lighting — Blinn-Phong
  6. learnopengl.com/Advanced-Lighting/Advanced-Lighting

相关页面

图形学渲染方程Rendering Equation渲染方程 (Kajiya, 1986) 把"某点接收到的光就是其他点发出或反射的光"写成递归积分,概括了光从光源出发经任意次反弹到达眼睛的所有路径。反射方程是它的受限特例。离线用辐射度 (Radiosity) 或蒙特卡洛路径追踪求解;实时渲染则靠简化和预计算逼近它(RTR4 第 11 章)。图形学双向反射分布函数BRDFBRDF f(\mathbf{l},\mathbf{v}) 量化局部反射:只依赖入射方向 \mathbf{l} 和出射(观察)方向 \mathbf{v},涵盖表面反射与局部次表面散射。它通过反射方程把入射辐射亮度转换为出射辐射亮度,物理上需满足 Helmholtz 互易性与能量守恒,用方向反照率检验。方向光和精确光源可从小面光源的极限推导,使积分变为对光源的求和。本页对应 RTR4 9.3–9.4 节。图形学着色模型与光源Shading Models & Light Sources着色模型描述物体颜色如何随表面朝向、观察方向和光照变化。光源通过两个参数与着色模型交互:指向光源的方向 \mathbf{l} 和光的颜色 \mathbf{c}{\text{light}}。方向光、点光源、聚光灯的区别就在于这两个量如何随场景变化;距离衰减和方向衰减函数的选择同时影响画面与性能。图形学基于物理的着色Physically Based Shading基于物理的着色从光与物质交互的物理出发,用 BRDF 和反射方程计算表面沿视线方向射出的辐射亮度:镜面项来自微表面理论与菲涅耳反射,漫反射项来自局部次表面散射,布料、波动光学与分层材质再作扩展。本页是 RTR4 第 9 章的总览,并收录该章第 9.13 节的材质混合与材质过滤(镜面抗锯齿)。图形学着色频率与材质系统Shading Frequency & Material Systems实现着色时要按"计算频率"划分工作:整个 draw call 不变的放到应用端作为 uniform,其余放在相应的可编程阶段。实践中大多数着色在像素级完成,顶点着色器负责变换和变形。材质系统把面向美术的材质与底层着色器组织起来,处理海量着色器变体的组合问题。图形学像素着色器Pixel Shader像素着色器(OpenGL 里叫片元着色器)对光栅化产生的每个片元运行,用插值后的顶点数据算出颜色,也可以输出不透明度、修改深度或直接丢弃片元。多渲染目标让它一次输出多组数据,催生了延迟着色。它通常只能写自己所在的像素,但能通过 2×2 quad 拿到相邻像素的导数。图形学环境光遮蔽Ambient Occlusion环境光遮蔽 (AO) 是余弦加权后半球上未被遮挡的比例,用来在光照缺乏方向变化时仍给出形状线索(缝隙变暗)。它由 Landis 在 ILM 为《珍珠港》开发,可离线烘焙,也可在物体空间或屏幕空间动态计算(SSAO、HBAO、GTAO 等)。AO 适合漫反射和平滑环境光,不适合点光源或小面光源的可见性(RTR4 11.3 节)。图形学环境贴图Environment Mapping把球面函数记录在一张或多张图像里、用纹理查找来解码,就是环境贴图。它比其他球面表示占内存多,但解码简单快速,能表达任意高频和任意动态范围。最基本的用法是反射贴图:假设镜面 BRDF,用反射视线向量查表。本页比较经纬度、球面、立方体、双抛物面和八面体等投影。图形学随机光线追踪与路径追踪Stochastic Ray Tracing & Path Tracing光线追踪用投射光线同时解决可见性问题和光照问题。GEA 从最简单的"眼睛光线 + 着色方程"讲起,逐步加入阴影光线、透射光线、用蒙特卡洛估计渲染方程的随机光线追踪、递归间接光照,再到每次只走一条路径的路径追踪、双向路径追踪和 Metropolis 光传输,最后讨论降噪与光线投射效率(GEA4 卷 II 12.6.3–12.6.5)。图形学泛光与镜头光晕Bloom & Lens Flare镜头光晕是光经镜头系统或眼睛的间接反射等非预期路径产生的现象;泛光 (bloom) 是镜头和眼睛中的散射在亮光周围形成辉光、并降低其他地方对比度。显示器亮度有限,加入这些眩光效果可以暗示更高的亮度。实现上,光晕常用随光源位置变化的公告板或对亮部做图像处理,泛光则是"提取亮部 → 降采样模糊 → 加回原图"(RTR4 12.3 节)。图形学可编程着色器与 GPU 管线Programmable Shaders & GPU PipelineGPU 把第 2 章的概念管线实现为一组硬件阶段,分为完全可编程、可配置但不可编程、完全固定三类。现代 GPU 采用统一着色器架构:各类着色器共用同一套编程模型和核心,由 GPU 自行分配负载。着色器用 HLSL、GLSL 等类 C 语言编写,输入分为 uniform 和 varying 两类。图形学片元着色器与 GLSL 基础Fragment Shaders & GLSL Basics片元着色器(fragment shader)是在每个像素(片元)上并行执行的小程序:输入位置等信息,输出颜色。GLSL 是 OpenGL / OpenGL ES / WebGL 共用的着色语言;本书用全屏 billboard 上的像素着色练习,建立程序化图形的基础心智模型。图形学网格整合与顶点法线Mesh Consolidation & Vertex Normals整合 (consolidation) 指把零散多边形合并成共享顶点的网格、让所有面朝向一致、判断是否为实体,并生成平滑的顶点法线与折痕边。这些步骤是背面剔除、折痕检测、阴影体、碰撞检测、3D 打印等的前提。图形学着色器中的颜色Color in Shaders在 GLSL 里颜色与空间坐标共用 vec2/vec3/vec4:可用 .rgba / .xyzw / .stpq 或下标访问,并 swizzle 任意重排。mix 在 [0,1] 上混合;HSB(HSV)比 RGB 更直观,还可映到极坐标色轮。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原