把模型横向拉长两倍,轮廓变化正确,高光却可能出现在错误位置。顶点位置和法线若都乘同一个缩放矩阵,程序不会报错,但法线已经不再垂直于表面。

本篇先用点积定位这个问题,再用同一个32三角形曲面比较平面与平滑着色。几何、覆盖与光源不变,只改变着色法线;所有颜色仍在线性域计算,最后沿用第04篇的sRGB输出。

法线保存的是垂直关系

三角形顶点为p0,p1,p2p_0,p_1,p_2时,两条边e1=p1p0e_1=p_1-p_0e2=p2p0e_2=p_2-p_0位于表面内。非退化三角形的单位几何法线为:

ng=e1×e2e1×e2.n_g=\frac{e_1\times e_2}{\|e_1\times e_2\|}.

交换顶点绕序会改变叉积符号。零面积三角形没有唯一的面方向,本系列仍拒绝对零向量归一化,不通过更换分母掩盖退化几何。

顶点上还可以存一组用于着色的法线。它们可以来自原始光滑曲面,也可以由相邻面按明确权重平均。几何法线描述三角形本身,着色法线描述希望使用的局部朝向;两者用途不同,不能因为数组都只有三个分量就相互替代。

例如,一个球面由少量平面三角形近似时,面法线在每片内恒定,而顶点法线可以指向球心外侧。插值后的着色看起来更平滑,但三角形的真实位置和轮廓没有改变。

同乘缩放矩阵的反例

取切向量t=(1,1,0)t=(1,1,0)、法线n=(1,1,0)n=(1,-1,0),两者点积为零。将x方向放大两倍,线性矩阵为A=diag(2,1,1)A=\operatorname{diag}(2,1,1)。切向量必须随几何变为At=(2,1,0)At=(2,1,0)

若法线也直接乘A,得到(2,1,0)(2,-1,0),于是:

(At)(An)=2×2+1×(1)=3.(At)\cdot(An)=2\times2+1\times(-1)=3.

这不是单位长度问题。把两个结果各自归一化,非零点积仍不会变成零;方向已经错了。只在着色前补一个normalize无法修复错误的变换。

要求新法线nn'继续满足nTAt=0n'^TAt=0。令n=ATnn'=A^{-T}n,则:

(ATn)TAt=nTA1At=nTt=0.(A^{-T}n)^TAt=n^TA^{-1}At=n^Tt=0.

因此仿射变换的法线使用线性部分的逆转置。当前例子得到(0.5,1,0)(0.5,-1,0),与(2,1,0)(2,1,0)点积恰为零。检查器实际输出错误路径3、正确路径0,比较发生在归一化之前,便于手算。

A仅取模型或模型视图变换的三阶线性部分,不含投影。平移不改变切向量,也不进入这个法线矩阵。若法线在世界空间、光方向却在相机空间,即使两个向量都归一化,点积也没有正确的几何含义。

旋转、剪切与奇异情况

纯旋转矩阵满足R1=RTR^{-1}=R^T,所以RT=RR^{-T}=R,法线恰好能与方向使用相同矩阵。这解释了为什么某些程序旋转时正常,一旦非均匀缩放就出错。只用旋转测试,无法证明法线处理普遍正确。

累计实现transform_normal用三阶矩阵列向量的叉积构造逆转置作用,不额外引入通用矩阵库。若A的列为a、b、c,行列式d=a(b×c)d=a\cdot(b\times c),则:

ATn=nx(b×c)+ny(c×a)+nz(a×b)d.A^{-T}n=\frac{n_x(b\times c)+n_y(c\times a)+n_z(a\times b)}d.

除对角缩放外,实验还检查x=x+0.5yx'=x+0.5y的剪切。原切向量变成(1.5,1,0)(1.5,1,0),法线应变成(1,1.5,0)(1,-1.5,0),点积仍为零。这项检查能暴露“只对每个分量除缩放值”的不完整实现。

零缩放会使矩阵奇异,逆不存在。接口拒绝零行列式、非有限矩阵和非仿射矩阵;运算得到非有限法线也拒绝。它没有实现近奇异矩阵的条件数估计,因此不承诺极端尺度下的数值稳定性。

负行列式还会改变朝向。代数恒等式(Au)×(Av)=det(A)AT(u×v)(Au)\times(Av)=\det(A)A^{-T}(u\times v)表明,变换后重新叉积的几何法线可能与直接逆转置的法线反向。镜像模型需要连同顶点绕序、外侧约定和剔除策略处理。本次图像没有镜像,不用它证明镜像路径已经验收。

余弦项与经验高光

设单位法线为n,单位光方向l从表面指向光源。对正面受光的一侧,漫反射余弦因子为d=max(0,nl)d=\max(0,n\cdot l)。这里先把它称为因子,避免将一个无量纲余弦误当成完整辐射亮度。

Lambert反射模型的BRDF为ρ/π\rho/\pi,最终出射光还依赖入射光及其方向积分。当前直接光教学式把光强等常量折入系数,计算kddk_d d;它不包含完整照明积分,也没有处理间接反射和遮挡。下一篇才加入阴影。

经验高光使用Blinn–Phong形式。单位视线v也从表面向外,半程向量h=normalize(l+v)h=\operatorname{normalize}(l+v),高光因子为:

s=max(0,nh)p,p>0.s=\max(0,n\cdot h)^p,\qquad p>0.

本实现对背光或背向视线的表面关闭这个直接高光。l与v相反时,和向量可能为零,也不能归一化。参数p控制高光集中程度,不等于一个已经标定的物理粗糙度。

n=(0,0,1)n=(0,0,1)l=v=(0,3/2,0.5)l=v=(0,\sqrt3/2,0.5)、p=4。因为h=l,漫反射因子为0.5,高光为0.54=1/160.5^4=1/16。实际检查结果为0.5与0.0625000000000001,误差在101210^{-12}阈值内。反向光源样本的高光为零。

这个经验式没有被声明为能量守恒的完整材质。随意叠加任意大的漫反射和高光系数,可能让反射能量失去物理解释。当前系数只是固定图像对照条件,不能据此推导真实材料参数。

同一块曲面,两种着色

实验取单位球面前部的一块网格。x范围为[-0.75,0.75],间隔0.375;y范围为[-0.5,0.5],间隔0.25。25个顶点的位置为:

p=(x,y,1x2y23).p=(x,y,\sqrt{1-x^2-y^2}-3).

每个网格四边形拆成两个三角形,共32片。顶点着色法线取(x,y,1x2y2)(x,y,\sqrt{1-x^2-y^2}),其长度为1。模型位于相机前方−Z区域,面法线朝+Z。

采用正交投影,屏幕位置为(128(x+1),128(1y))(128(x+1),128(1-y)),w均为1。这使本篇能单独观察法线插值;一般透视相机仍须像第07篇UV那样先按1/w修正权重,再对插值法线归一化。

平面版本每个三角形使用一个几何法线。平滑版本按屏幕重心权重组合三个顶点法线,再归一化。单位向量的线性平均一般不是单位向量,例如(1,0,0)(1,0,0)(0,1,0)(0,1,0)的平均长度只有1/21/\sqrt2,所以最后一步不能省。

光方向为归一化的(-1,1,2),正交视线为(0,0,1),高光指数32。线性颜色固定为:

C=(0.04,0.03,0.02)+(0.6,0.3,0.12)d+(0.25,0.25,0.25)s.C=(0.04,0.03,0.02)+(0.6,0.3,0.12)d+(0.25,0.25,0.25)s.

第一项只是教学底色,不冒充环境光的物理计算。各分量最大不超过0.89,因此本实验不触发高光压缩,直接进行一次sRGB编码。

32个三角形采用各自几何法线,明暗呈面片变化 相同网格插值顶点法线,高光与明暗连续变化,外轮廓不变

两张图来自实际C++输出,原始尺寸256×256,经sips转换PNG。第一张可见三角形面片,第二张出现连续高光;外侧仍是相同矩形裁取范围,并不是生成了更多几何。

数值证据能排除哪些误判

两个版本共享同一次覆盖判断,共写入24,576个样本,即192×128。24,190个像素的最终RGB不同。这个差异数不评价哪种风格更好,只确认法线改变确实影响了着色,而不是误把相同图像当成对照。

平滑法线的nn1|n\cdot n-1|最大为4.44089209850063×10164.44089209850063\times10^{-16}。它验证当前样本归一化后的长度,不证明法线等于精确球面在每个像素处的法线。顶点法线在三角形内插值仍是近似,几何表面本身也是分片平面。

本实验没有比较投射阴影、曲面细分或透视投影的法线图。平滑着色改变的是光照输入,不会修复粗糙轮廓、错误深度或遮挡关系。若误把着色法线用于几何可见性,可能制造另一类错误。

在仓库根目录运行:

1
2
make -C examples/computer-graphics check
examples/computer-graphics/build/lighting_check examples/computer-graphics/build

输出目录须存在。累计检查复跑00–09,原始结果在writing-plans/computer-graphics/evidence/09-cpu.txt,两张图为lighting-flat.ppmlighting-smooth.ppm。实验确定性运行,没有随机种子、性能计时或GPU结论。

练习与自检

练习一:把缩放改为A=diag(2,2,2)A=\operatorname{diag}(2,2,2),法线直接乘A再归一化,与逆转置再归一化是否一致?这个结果能否推广到非均匀缩放?

答案:正的均匀缩放只改变长度,两条路径归一化后方向相同。非均匀缩放会改变分量比例,正文的点积3与0反例已说明不能推广。负缩放还涉及朝向,需另行处理。

练习二:将所有顶点法线都替换成各自面法线,再进行插值,能否让低多边形轮廓变圆?为什么相同面的三个相同法线插值后仍保持平面着色?

答案:不会改变顶点位置与覆盖,轮廓不变。同一面三份相同法线的加权和仍为原法线,因为权重和为1;归一化也不改变方向。要改变轮廓需要修改几何,不能只更换着色输入。

系列导航与资料

前篇:08:怎样减少锯齿与纹理闪烁系列入口。下一篇:10:光源看不见的地方怎样变暗

PBRT 4ed §3.10.3 Normals、§3.10.7 Handedness,法线逆转置及变换朝向。§6.5 Triangle Meshes§9.2 Diffuse Reflection用于核对几何与着色法线、Lambert反射的边界。

Microsoft Specular Lighting,核对半程向量与高光指数。来源记录见evidence/research-09-11.md,法线变换和高光页面于2026-09-20再次查阅。上述手算样本、网格与颜色参数由本系列独立选定。