模型矩阵与逆矩阵:世界坐标和局部坐标变换详解 1. 模型矩阵局部坐标是怎么变成世界坐标的1.1 模型矩阵的组成T、R、S做渲染的老哥都知道一个物体能出现在屏幕上靠的是四套矩阵一路接力模型矩阵、视图矩阵、投影矩阵最后才是视口变换。很多人天天写代码调shader但模型矩阵到底在干嘛、它的逆矩阵有什么用其实没仔细想过。这里我就把最基础也最容易被忽视的一环拆开聊透。模型矩阵Model Matrix干的事情简单说就是“把物体的局部坐标搬到世界坐标”。一个顶点在建模软件里定义时坐标原点是物体自己的锚点比如一个茶壶模型壶底中心可能就是(0,0,0)。但是引擎里这个茶壶被放在了一个场景的某个位置朝向某个方向还被缩放成了某个大小。这中间发生的变化就是模型矩阵。模型矩阵通常由三个基本变换复合而成缩放矩阵S用sx、sy、sz分别缩放三个轴。旋转矩阵R用绕x、y、z三个轴的角度组合描述物体朝向。平移矩阵T用tx、ty、tz把物体挪到目标位置。写成矩阵就是M T × R × S。这个顺序不是随便定的而是经过了无数图形学前辈的验证。原因后面单独讲。很多资料喜欢把旋转矩阵直接写成一个3×3矩阵然后拼进4×4齐次矩阵的左上角平移塞到第四列前三行缩放作用在最里层。这是标准做法。我这么多年看下来真正容易出问题的反而是顺序搞反或者说“世界回归局部”时不知道要反过来用逆矩阵。1.2 矩阵乘法顺序先缩放、再旋转、后平移这里有个核心概念必须说清楚矩阵乘法不满足交换律。先旋转后平移和先平移后旋转结果完全是两回事。想象一个立方体先绕原点旋转45度再沿x轴移动5个单位——移动方向是世界空间的x轴如果先移动5个单位再旋转45度等于把已经偏移的立方体再旋转旋转后它的位置不仅偏移了5个x还偏移到了别的方向。这就是顺序的差别。标准约定是先缩放、再旋转、最后平移。这样做的好处是缩放围绕物体自身原点进行不会因为先平移导致缩放把物体“拽跑”旋转也是围绕物体自身原点旋转旋转轴不会受到平移影响平移永远发生在这个已经缩放和旋转完毕的“局部框架”里位置控制最直观。所以在C或引擎代码里组合矩阵时常见的写法是mat4 model translate(tx, ty, tz) * rotate(angle, axis) * scale(sx, sy, sz);如果你不小心把顺序写成scale * rotate * translate物体就会整体天旋地转而且位置完全不受控制。这是几乎所有图形学新手都会踩的第一个大坑。1.3 为什么需要“逆过程”世界坐标回局部坐标的业务需求模型矩阵能把局部坐标变到世界坐标那反过来呢我经常遇到有人问我有了世界空间里的一个点怎么知道它在物体局部空间里的坐标这个需求不是凭空想出来的实际项目中太常见了。举几个例子射线拾取鼠标点击屏幕发射一条射线到世界空间射线和某个模型撞上了。碰撞点返回的是世界坐标但如果你要判断碰撞点落在模型的哪个子区域比如判断是否击中了车门还是车灯就必须把世界坐标转换到模型的局部空间和模型本身的几何数据做比较。贴花与局部效果在模型表面生成一个贴花需要知道接触点相对于模型锚点的局部位置才能正确映射贴花的uv或形状。动画与IK骨骼动画系统里经常需要把世界空间的目标位置转换到某根骨骼的局部空间才能计算关节转角。模拟与物理一些物理引擎返回的接触点默认在世界空间做局部约束时也要转回局部。这些场景统一指向一个数学工具模型矩阵的逆矩阵。世界坐标乘以模型矩阵的逆就能回到局部坐标。公式很简单局部坐标 M⁻¹ × 世界坐标但这背后有一堆值得深挖的东西。2. 逆矩阵的几何意义与计算逻辑2.1 逆矩阵到底在干什么反向变换的三步模型矩阵做的事情是把物体从局部搬到世界那逆矩阵做的事情就是把世界坐标搬回局部。这正好对应了前面说的三步变换只不过每一步都反向执行先把世界坐标平移回去减去模型矩阵的平移量。再反向旋转把世界方向转回模型自身的朝向。最后反向缩放把世界坐标的尺度还原到局部尺度。写成矩阵就是M⁻¹ S⁻¹ × R⁻¹ × T⁻¹注意顺序反过来了。原本M T × R × S求逆后顺序完全颠倒第一步做的是T⁻¹最后一步才是S⁻¹。这个颠倒逻辑是理解“世界回归局部”的关键。如果你不想死记硬背可以这样推理对任意一个点v世界坐标 T × R × S × v。想解出v就得两边同时左乘逆矩阵。又因为(ABC)⁻¹ C⁻¹B⁻¹A⁻¹所以M⁻¹ S⁻¹ × R⁻¹ × T⁻¹。这就是矩阵运算的基本法则。实际代码里不用每次从头推导直接用现成的矩阵求逆函数即可。但理解了这个展开式你才能诊断出各种诡异问题。2.2 正交矩阵与旋转矩阵的简化转置代替求逆如果模型矩阵只包含平移和旋转没有缩放那么这东西有个极好的性质旋转矩阵是正交矩阵正交矩阵的逆矩阵等于它的转置矩阵。数学上的含义是旋转矩阵的每一行、每一列都是单位向量且互相正交把它转置一下就正好是逆矩阵。这意味着你不需要跑一遍高斯消元或者伴随矩阵算法直接转置就能拿到逆矩阵性能和数值稳定性都更好。实际的3D引擎和游戏里很多模型不做非均匀缩放或者完全没有缩放那么求逆这件事就非常轻量// 三维旋转矩阵求逆 转置 mat3 rotation mat3(model); mat3 inverseRotation transpose(rotation);这里mat3(model)取的是模型矩阵的旋转线性部分不包含平移列。对一个纯旋转加平移的变换逆变换就是M⁻¹ 先转置旋转矩阵再对旋转后的位置减去平移量注意顺序。很多引擎底层都会做这个优化因为求逆计算比转置昂贵得多。如果你自己写图形学代码遇到纯旋转平移的模型矩阵大胆用转置别傻乎乎去跑通用的求逆函数。2.3 法线变换的例外为什么这里不能直接用模型逆矩阵说到逆矩阵法线变换是绕不开的特例。很多人以为法线也是向量直接用模型矩阵带到世界空间就行了结果光照一塌糊涂模型某些面亮得发黑。法线变换的正确做法是用模型矩阵的逆转置矩阵也就是( M⁻¹ )ᵀ。这个逻辑折磨过无数人。原理其实不复杂法线定义是表面切线的垂直方向切向量经过模型矩阵带缩放变换后依然贴合表面但你直接拿模型矩阵去变法线向量当模型有非均匀缩放时法线角度会被扭曲不再垂直于表面。举个例子一个平面法线朝上(0,1,0)模型沿y轴放大2倍。这时候表面拉伸了真正的法线方向没变还应该是(0,1,0)。但如果直接用模型矩阵作用得到(0,2,0)归一化后依然是(0,1,0)碰巧没问题。如果你沿x轴放大2倍、y轴不变表面法线就会倾斜直接乘模型矩阵就错了。正确的思路是法线在局部空间垂直于一切表面切向量变换后必须依然垂直于变换后的切向量。这个约束最后推导出来的结论就是法线必须乘以模型矩阵的逆转置。有兴趣的可以拿两个切向量和内积去推十分钟能推完。注意如果模型矩阵只有旋转和平移没有缩放那么M的逆的转置恰好等于M本身所以法线可以直接乘模型矩阵。这也是为什么大部分无缩放场景下没人发现这个坑。3. 实战操作把世界坐标打回局部坐标3.1 基本流程求逆、乘法、选项把一个世界坐标点转回局部坐标标准流程就三步拿到物体的模型矩阵M。计算M⁻¹。用M⁻¹乘世界坐标。但这里有个非常容易忽略的细节点向量和方向向量用的是同一个矩阵还是不同的矩阵位置点齐次坐标w1直接用M⁻¹乘完整4分量。方向向量齐次坐标w0不能用带平移的逆矩阵去乘。因为方向向量不应该被平移一旦你把平移量混进去方向就错了。也就是说把一个世界方向向量转回局部方向正确操作是取M⁻¹的3×3线性部分去掉平移然后乘方向向量。这也正是法线变换里“逆转置”产生的一个分支场景。很多图形学代码里封装了一个函数专门做“变换方向”。我强烈建议你也这么干避免每次手算矩阵然后忘了去掉第四列平移。3.2 代码示例GLSL内置inverse / 手动矩阵求逆在shader里GLSL直接提供了inverse()函数能对一个mat4求逆。这个函数大部分显卡都支持但要注意的是统一在CPU端求好逆矩阵并传进去比在shader里每顶点调用inverse()要划算得多。我见过有人图省事在顶点着色器里写inverse(modelMatrix)结果一个三角面几百个顶点每个都在算一遍逆矩阵GPU气得发抖。推荐的shader写法uniform mat4 modelMatrix; uniform mat4 inverseModelMatrix; // CPU侧算好传入 vec3 worldPos ...; // 已知的世界空间坐标 // 世界空间点 - 局部空间点 vec3 localPos (inverseModelMatrix * vec4(worldPos, 1.0)).xyz; // 世界空间方向 - 局部空间方向 vec3 localDir mat3(inverseModelMatrix) * worldDir;如果你实在需要在CPU端手动实现矩阵求逆常见做法是伴随矩阵法或高斯消元。4×4矩阵求逆有固定公式但对一般人来说直接调数学库就好。常见的数学库GLM、D3DX、UE4的FMatrix等都有现成求逆别自己重复造轮子除非你在写一个没有依赖的引擎demo。不过我还是强烈建议你自己实现一遍逆矩阵算法——不只是为了面试而是为了理解矩阵运算的本质。写一遍之后你会对“矩阵求逆为什么有数值稳定性问题”有切身体会。3.3 性能与精度该不该每帧求逆这是个很实在的问题。答案是分场景。如果一个模型的模型矩阵在一帧内没有变化那就应该在CPU端每帧算一次逆矩阵然后传给shader而不是在每个顶点或每个像素里重复算。如果模型矩阵稳定不变甚至可以只在初始化时算一次缓存起来。但如果物体每帧都在动比如刚体模拟、角色动画里的骨骼矩阵那逆矩阵也得跟着每帧更新。这也是一个常见的性能陷阱有团队图省事直接在shader里调用inverse()因为平时模型数量少没感觉到问题可一旦进入场景物体数量爆表GPU开销立刻变大。另一个被忽略的点是数值精度。通用矩阵求逆在矩阵接近奇异时结果会变得非常不稳定。模型矩阵里的缩放极小或极大时求逆结果会引入很大的数值误差。一个经验做法是当你只是想把一个世界点转回局部点时尽量用“先分解出平移、旋转、缩放然后反向计算”的方式而不是直接对着4×4矩阵求逆。因为平移和旋转在数值上非常干净算出的逆矩阵误差小得多。我自己的习惯是写一个InverseModelMatrix的辅助函数内部先提取旋转部分左上3×3用它做转置然后算出反向平移最后组合成4×4。这个方案在绝大多数项目里够用而且数值稳定。3.4 射线拾取案例把世界碰撞点打回局部空间举一个完整的实战案例帮你把这套流程串起来。假设场景里有一个门模型你要做鼠标点击拾取点击后要知道玩家点的是门把手、门板还是门框。门模型有复杂的层级关系门板上有门把手门把手的模型矩阵等于“门的变换”乘“门把手相对门的变换”。点击时从屏幕坐标生成射线把射线转换到世界空间做包围盒或三角形求交得到世界空间下的碰撞点比如(4, 1.5, 2)。现在需要判断这个点在门把手的局部空间里落在哪个区域。拿到门把手的完整模型矩阵注意是链式复合矩阵父物体矩阵×子物体矩阵。求这个复合矩阵的逆矩阵用逆矩阵乘世界碰撞点得到门把手局部坐标。拿局部坐标和门把手自身的包围盒、分片几何数据比对判断命中区域。这里有个特别容易翻车的地方很多引擎的射线检测函数默认返回的是“命中点”的世界坐标这个坐标很精确但模型的包围盒是在局部空间定义的。不转换直接比较轻则误判重则点永远点不中。我见过有同学把世界坐标直接和局部包围盒比大小怎么调都差一点最后发现是忘了逆变换。另外还要注意如果射线命中的是子物体比如门把手你需要的是门把手的模型矩阵而不是门板的。这两个矩阵不一样用错了会得到非常诡异的结果。4. 常见问题速查表与避坑经验4.1 我踩过的坑向量和点的区别第一个大坑就是前面反复提到的把点当方向向量或把方向向量当点。一个世界方向向量比如(0, 1, 0)你想转回局部方向如果用完整的4×4逆矩阵乘向量会被平移量带偏。尤其是模型在世界空间有较大平移量时误差会被拉得特别大。我自己有一次给某图像处理Demo写天空盒反射把反射向量误用完整模型逆矩阵乘了一次结果反射方向全乱了天空盒的倒影以一种奇怪的方式扭曲。排查了半天才意识到我乘完矩阵之后根本就没把w分量的平移动效去掉。现在的习惯是做方向变换时永远先用一个明确的函数内部要么把第四行去掉要么构造一个新的3×3矩阵来算。这比每次祈祷自己没忘要可靠得多。4.2 问题速查表逆矩阵与坐标变换常见病这里整理一张速查表基本覆盖了大多数实际问题。遇到诡异情况先照着排查一遍。现象可能原因排查方法世界坐标转局部后位置偏移巨大用了转置矩阵代替逆矩阵且模型含非均匀缩放检查M是否为只含旋转和平移的正交部分用完整求逆验证法线方向错误明暗不对法线直接乘了模型矩阵没乘逆转置改为transpose(inverse(model))乘法线方向向量包含莫名其妙的位移把w0向量乘了完整4×4矩阵只乘3×3线性部分点击射线命中但局部坐标计算不对用了错误的模型矩阵用了父矩阵而不是子矩阵确认拿的是被命中物体的矩阵逆矩阵计算结果NaN或无穷模型矩阵里包含0缩放检查物体是否有退化的缩放轴避免为0缩放CPU端求逆每帧开销大向量化或批量求逆没做好缓存静态物体的逆矩阵仅对动态物体每帧求逆这里面最容易被忽视的是“用错了模型矩阵”这条。场景里经常有父子物体嵌套你如果把世界坐标打回去的时候用了父物体的矩阵或者用了某个构建半截的矩阵结果自然错得离谱。所以每次做逆变换前先花10秒确认你手里的矩阵是真的、完整的。4.3 链式变换的逆层级变换中的复合矩阵现实项目里很少只有一个模型矩阵摆在那更多是嵌套层级。比如角色模型有骨骼骨骼树上有子骨骼再比如载具模型里门板是父节点门把手是子节点。这时候要算某个子节点局部坐标的逆变换得拿到了子节点完整的模型矩阵。递推公式是子节点完整模型矩阵 父节点模型矩阵 × 子节点相对父节点的变换矩阵求逆就变成子节点逆矩阵 子节点相对父节点的逆 × 父节点的逆这个顺序非常容易写错。和前面四则运算一样乘积的逆等于逆的反序乘积。我见过一个项目为了求子物体局部坐标先求了父逆再乘子逆结果顶点全都跑到了奇怪的位置。另外如果你在引擎里同时持有物体组件、父子关系表请务必使用引擎提供的接口拿“完整世界矩阵”或“全身矩阵”不要自己手动从父到子逐级乘。手动乘的时候矩阵顺序一错后面全错而且查起来特别费时间。4.4 实战经验什么时候别用通用求逆函数通用矩阵求逆函数不是万能的。在图形学里模型矩阵的结构其实很特殊它有平移、旋转、缩放但没有透视裁剪那种病态结构。利用这一点我们可以写出更快、更数值稳定的专用方法。我的经验法则是如果只有旋转和平移用转置反向平移最快最稳。如果还带均匀缩放先归一化旋转部分再求旋转转置最后缩放取倒数依然很快。如果有非均匀缩放稍微复杂一点可以提取出左上3×3对其求逆比如用伴随矩阵法或高斯消元平移部分单独处理。最保险的做法还是调用数学库的通用求逆然后在关键路径上缓存。最后分享一个我自己一直在用的小技巧在你的代码库里写一个专门用于“变换逆矩阵”的辅助类它内部维护“模型矩阵”和“逆模型矩阵”所有变换都同时更新两边这样整个项目里就不会有人去重复计算模型逆矩阵也从根本上避免了方向向量误乘带平移矩阵的低级错误。这套变换逻辑入门看起来不过是一个矩阵求逆实际用起来却能牵扯出向量分量、层级结构、数值稳定、性能开销这么多细节。下次再看到有人问“世界坐标怎么变回局部坐标”你可以把这篇文章甩给他然后告诉他慢点来先把自己手里的矩阵搞清楚。