图形数学 essentials:点积、叉积、法线与齐次坐标
shader 里 90% 的数学只有四个概念:点积、叉积、法线、齐次坐标。光照公式、菲涅尔、切线空间、法线逆转置,全是它们的组合。这篇把四个概念和它们的 shader 用法一次对齐。
点积:两个方向有多一致
dot(a, b) = |a||b|cosθ(单位向量时就是 cosθ)。它是 shader 里出现频率最高的函数,因为光照的本质就是「朝向与方向的匹配度」:
- 漫反射:
dot(N, L)——表面朝向与光线方向多一致,就是 Lambert 的核心,见 /notes/unity/lighting-1/。 - 高光:
dot(N, H)——表面朝向与「视线和光线的中间方向」多一致,Blinn-Phong 的核心。 - 菲涅尔:
dot(N, V)——表面朝向与视线多一致;边缘处这个值趋近 0,所以边缘反射强,见 /notes/unity/effects-2/。
两个直觉要记牢:点积为负 = 背对(光照里通常 max(0, dot) 截断);点积不需要归一化之外的任何三角函数,cos 都不用算——这就是图形学爱用它的原因。
叉积:造出垂直于两个方向的方向
cross(a, b) 给出同时垂直于 a、b 的向量,长度 = |a||b|sinθ(平行时为零向量——这是叉积最常见的 bug 来源)。用途集中在构建坐标系:
- 切线空间 = 切线 T、副法线 B、法线 N 三个互相垂直的轴;已知 T 和 N 时
B = cross(T, N) * w(w 是镜像符号,从网格导入),见 /notes/unity/effects-1/。 - 由三角形两条边求面法线:
cross(edge1, edge2)。
注意叉积不满足交换律:cross(a,b) = -cross(b,a),顺序反了方向就反,光照/描边立刻不对。
法线:为什么不能直接乘 MVP
法线是「朝向」,不是「位置」。对顶点适用的矩阵作用到法线上会出错——非均匀缩放时:
缩放 (2,1,1):表面被横向拉长,
顶点跟着拉长了,但倾斜面的朝向只轻微变平——
直接乘 Model 矩阵会把法线斜得更厉害,光照就错了。
推导一句话:法线 n 与切线 t 满足 dot(n, t) = 0;切线可以用 M 变换(它是两顶点之差),设法线用矩阵 X 变换,则 dot(Xn, Mt) = 0 对所有成立,解出 X = (M⁻¹)ᵀ——逆转置。均匀缩放时逆转置 = 原矩阵(差一个标量),所以「直接乘 Model 再 normalize」在均匀缩放下侥幸正确,非均匀缩放立刻暴露。
Unity 里的对应写法:
o.normalWS = UnityObjectToWorldNormal(v.normal); // 内部就是逆转置 + 处理镜像
// 手写等价:
o.normalWS = normalize(mul(transpose(unity_WorldToObject), v.normal));
(unity_WorldToObject 本身就是世界→模型的逆转置矩阵,所以这里直接乘它。)
齐次坐标:w 分量是干嘛的
三维向量加第四个分量成 (x, y, z, w):
- 点:w = 1;方向:w = 0。这个区分让矩阵乘法自动正确——平移只作用于点、不作用于方向(法线是方向,也不该被平移搬走)。
- 透视投影把「深度」写进 w(见 /notes/unity/principles-2/),硬件除以 w 完成近大远小。
- 齐次让「平移」也能写成矩阵乘法,于是 Model/View/Projection 可以预乘成一个 MVP。
实践规则:shader 里做方向运算(法线、光照方向)时确认 w = 0 或用 mul(matrix, float4(dir, 0));做位置运算时用 w = 1。混用是「物体移动后光照方向诡异地跟着动」这类 bug 的根源。
左手系与绕序
Unity 用左手坐标系(+Z 朝屏幕内),观察空间摄像机看向 -Z;三角形正面是顺时针绕序。跨引擎/跨 API(OpenGL/Vulkan 常见右手系、逆时针正面)移植 shader 时,绕序与叉积顺序是最先要核对的两处——背面剔除失效通常就是这个。
自检三问
dot(N, L) < 0在物理上意味着什么?什么情况下你不该把它截断为 0?- 一个模型被脚本每帧做非均匀缩放,光照开始出错,第一反应查什么?
- 用
mul(unity_ObjectToWorld, float4(normal, 0))变换法线,什么情况下是对的、什么情况下错?