图形数学 essentials:点积、叉积、法线与齐次坐标

Unity 知识 · Shader 区域 · 原理 · 第 5 篇 更新于 2026-10-15

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)) 变换法线,什么情况下是对的、什么情况下错?