本节摘要:同样的光照公式,在哪个粒度上求值,画面的平滑程度截然不同。本节对比三种着色频率:flat(逐三角形,棱角分明)、Gouraud(逐顶点算光照、像素间插值颜色,高效但高光易丢)、Phong(逐像素插值法线再算光照,顺滑精准)。核心洞察是"插什么"的差别:插颜色与插法线,成本与质量此消彼长。承接 5.1 的公式与 3.1 的重心坐标插值。
阅读完本节,你应当能够:
5.1 的 phong_shade 接收法线、光方向、视线方向。这三个量随位置连续变化,但计算机只能离散地算。三种选择:
flat 着色:每三角形算一次(面法线),三个顶点共享结果 Gouraud 着色:每个顶点算一次,三角形内部用重心坐标插"颜色" Phong 着色:插"法线",每个像素用插值法线独立算一次光照
import numpy as np def shade_flat(face_normal, L, V): # 逐面:整个三角形一个亮度 return max(0.0, np.dot(face_normal, L)) def shade_gouraud(vertex_normals, w, L, V, spec_fn): # 逐顶点算颜色,内部按重心坐标插值颜色 colors = [max(0.0, np.dot(n, L)) + spec_fn(n, L, V) for n in vertex_normals] return sum(c*wi for c, wi in zip(colors, w)) # w 为重心坐标 def shade_phong(vertex_normals, w, L, V, spec_fn): # 逐像素:先插值法线,再算光照 n = sum(n*wi for n, wi in zip(vertex_normals, w)) n = n / np.linalg.norm(n) # 插值后必须重新归一化 return max(0.0, np.dot(n, L)) + spec_fn(n, L, V)
三者的差异在小高光上暴露无遗。设想一个只有几百个面的球:高光斑可能比一个三角形还小。flat 着色的球是 Facet 分明的"多面体";Gouraud 在顶点处算光照,若高光恰好落在三角形内部、没盖住任何顶点,插值后的颜色里根本没有它的踪影——亮斑凭空消失;Phong 着色逐像素插法线,即使高光只覆盖两三个像素也如实呈现。
球面小高光的三种命运: flat → 整块面变亮,棱面感强 Gouraud → 高光必须碰巧压到顶点才可见,否则丢失 Phong → 高光如实呈现为小亮斑,代价是每像素一次完整光照
3.1 节的光栅化已经给出每个像素的重心坐标 (α, β, γ),插值只是拿它当权重混合顶点属性。但有个升级细节必须注意:插值出来的法线不再是单位长度,用前必须归一化;且屏幕空间直接插值在透视下会偏离(透视校正插值,硬件自动做,软件渲染器要手工除以 w——6.3 节展开机制)。
def lerp_normal(n0, n1, n2, w): # 重心坐标插值法线 + 重新归一化 a, b, c = w n = a*np.array(n0) + b*np.array(n1) + c*np.array(n2) return n / np.linalg.norm(n) n = lerp_normal((1,0,0), (0,1,0), (0,0,1), (1/3, 1/3, 1/3)) print(np.round(n, 4)) # 输出: [0.5774 0.5774 0.5774] 三顶点法线均匀混合,归一化后各分量 1/√3 # 不归一化直接点积,亮度会系统性偏低——新手常犯
| 场景 | 推荐 | 理由 |
|---|---|---|
| 风格化低模、建筑体块 | flat | 棱面感正是美术语言 |
| 远景 crowd、大规模粒子 | Gouraud/顶点光照 | 顶点数远小于像素数,省算力 |
| 近景角色、载具、地面 | 逐像素(Phong/PBR) | 高光与材质细节是观感核心 |
| 移动端中低档 | 顶点光照 + 光照贴图烘焙 | 把动态算不起的部分离线烤好 |
现代实时渲染里"Phong 着色"这个词已基本被"逐像素着色"取代:片元着色器里跑的可能是 PBR(基于物理的渲染)公式,但"插法线、逐像素算光照"这个框架没有变——变化的是公式本身更物理、参数更有依据。
⚠️ 常见坑:法线贴图(5.3 节)与顶点法线插值混用时,两者必须在同一切线空间。直接把贴图里的法线当世界坐标用,光照方向全错,表面呈现"噪点式闪烁"。
把三种着色频率放到同一颗低面数球(比如 12 条经线 × 8 条纬线)上跑一遍,差异一目了然:flat 版的球看得到每一个四边形面片,明暗呈块状跳变,适合机械感、宝石切面感;Gouraud 版面片消失,明暗过渡连续,但高光位置若不巧落在面片内部就整球无高光,且高光随相机移动会出现"跳变到下一个顶点"的瞬移;Phong 版高光稳定圆润,逐像素的法线插值忠实呈现微表面感。这个实验自己跑一遍(几十行代码)比读十遍文字记得牢。更进一层:给这颗球套一张法线贴图(5.3 节),Phong 着色下的凹凸感立现,而 Gouraud 下贴图效果近乎不可见——因为顶点太少,法线细节根本没有采样点可依附。着色频率的选择本质是"细节频率与采样频率"的匹配问题,1.3 节的采样定律在着色阶段再次生效。
import numpy as np def sphere_mesh(nu=12, nv=8): # 生成球面网格顶点与法线(法线 = 单位化坐标) verts, normals = [], [] for j in range(nv+1): v = np.pi * j / nv for i in range(nu+1): u = 2*np.pi * i / nu p = np.array([np.sin(v)*np.cos(u), np.cos(v), np.sin(v)*np.sin(u)]) verts.append(p); normals.append(p) # 球面法线即位置单位化 return verts, normals verts, normals = sphere_mesh() print(len(verts), "顶点 →", (12*8*2), "三角形") # 输出: 117 顶点 → 192 三角形 索引化后顶点数约为三角形数的六成
光照公式知道在哪儿算了,但物体表面还穿着素色外衣——5.3 节讲纹理如何给每个像素送来专属颜色。