很多人学矩阵时,前面都还挺顺。
但我自己观察下来,真正把很多人拦住的不是 MVP,而是法线。因为前面的空间变换大多还像“位置搬家”,到法线这里你第一次会碰到“看起来像方向,处理方式却不能照抄”的情况。
旋转、缩放、平移都能理解,MVP 也能接受。
但一到法线,就开始出现一种很典型的疑惑:
顶点位置明明用 ObjectToWorld 变过去就行,为什么法线不能也这样直接乘?
答案通常在非等比缩放这里被彻底暴露出来。
一、法线不是普通方向向量
从形式上看,法线像一个方向向量。
但它承担的语义不是“朝哪里走”,而是“这个面垂直于哪个方向”。
也就是说,法线最重要的特征不是长度,而是它和切平面的垂直关系。
这决定了法线在变换时,不能简单地照抄顶点位置的处理方式。
二、为什么纯旋转时问题不明显
如果一个物体只做旋转,法线直接跟着旋转通常没问题。
因为旋转会保持角度关系,垂直关系不会被破坏。
等比缩放时,问题也不算突出,因为各轴缩放比例一致,整体几何关系相对稳定。
但一旦出现非等比缩放,比如:
1 | scale = (2, 1, 0.5) |
事情就变了。
三、非等比缩放为什么会破坏法线直觉
假设一个表面原本是斜着的。
你把模型在 x、y、z 三个轴上按不同倍数拉伸后,表面切线方向变了,但如果你还把法线只当普通向量同样去乘,它就不一定还和新表面保持垂直。
这就会直接影响光照结果:
- 高光位置不对
- 明暗过渡怪异
- Lambert 或 PBR 结果不稳定
所以问题的根本不是“数值不漂亮”,而是物理含义被破坏了。
四、normal matrix 的本质
法线要保持和表面切平面的垂直关系,正确的处理方式通常不是直接乘 Model 矩阵,而是乘它左上 3x3 部分的逆转置。
也就是常说的 normal matrix。
写法上常见表达是:
$$
N = (M^{-1})^T
$$
更准确地说,是针对线性部分来处理,而不是把整个位移也拿进去。
五、为什么是逆转置
这里如果硬推导,文章会变得太学术。
工程上你可以先抓住一句话:
它是在修正非等比缩放带来的“垂直关系失真”。
顶点位置关心的是点怎么落位。
法线关心的是面法向关系怎么保持正确。
这两件事不是一回事,所以对应的矩阵处理也不一样。
六、Unity 里什么时候你会明显撞上这个问题
至少有几类场景特别容易中招:
1. 角色或道具被父节点非等比缩放
模型位置没问题,但光照开始怪。
2. Shader 里自己写世界空间法线
如果你偷懒直接拿对象空间法线乘 unity_ObjectToWorld,在非等比缩放下很容易错。
3. 自定义网格或程序化模型
如果法线是你自己算、自己传,后续变换链稍微处理错一点,画面就会有明显缝隙或阴影错误。
七、Unity Shader 里通常怎么避免自己踩这个坑
很多内置宏或常见写法已经帮你做了处理。
比如一些世界空间法线转换宏,本质上就是在替你做正确的矩阵变换。
所以如果只是常规表面着色,尽量复用成熟宏比自己乱写稳定得多。
但前提是你得知道:
这些宏不是魔法,它们是在替你处理法线专用的变换规则。
如果你是自己写顶点到世界空间法线,最少要保证思路像下面这样,而不是直接拿位置那套乘法照抄:
1 | float3 worldNormal = normalize(mul((float3x3)unity_WorldToObject, v.normal)); |
或者直接走 Unity 已有的辅助函数:
1 | float3 worldNormal = UnityObjectToWorldNormal(v.normal); |
后者在工程里通常更稳,因为它已经把很多容易忘的细节封装掉了。
八、一个很实用的排查方法
如果你怀疑光照异常跟法线有关,可以先做这几步:
- 暂时把父节点非等比缩放改成
(1,1,1)。 - 观察异常是否立刻消失。
- 在 Shader 里把法线可视化输出成颜色。
- 对比对象空间法线、世界空间法线是否连续合理。
如果一改缩放就恢复正常,十有八九就是法线变换链有问题。
九、客户端工程里一个常见误区
很多人会把“法线问题”归类成纯美术或纯 Shader 问题。
其实不完全是。
因为运行时层级、挂点、骨骼、程序化缩放策略,都是客户端逻辑在定。
如果业务代码里大量引入非等比缩放,却没有意识到它会影响渲染表现,那最后排查成本会非常高。
十、一个更务实的团队建议
如果不是特别必要:
- 逻辑层尽量少用非等比缩放
- 美术资源层尽量在 DCC 工具里把比例处理好
- Shader 层优先使用成熟法线转换宏
这样会比等到线上出现光照怪异再追矩阵链省事得多。
十一、总结
法线之所以难,不是因为概念多,而是它逼着你意识到:
“看起来都是方向向量”的东西,在不同语义下,矩阵处理方式并不相同。
理解了这一点,你后面写光照、自定义材质、程序化模型时会稳很多。
下一篇我会把话题拉回客户端更高频的地方:UI、屏幕、世界坐标之间的来回换算。