
开篇想聊一个我实际遇到的场景做一个程序化散布项目时需要在高低起伏的山坡上放几千块石头要求每块石头都严丝合缝地贴在坡面上朝向跟随地形法线方向大小随机但不能穿模。最开始我用的是最土的办法——每个石头单独算位置、算旋转、算缩放再把三个结果填进一个Transform节点里。数据量一上来节点图乱成蜘蛛网改一个参数要拖半天而且旋转用欧拉角硬怼出来的结果总在某些角度出现诡异翻转。后来我把整套思路换成MatrixConstruction节点也就是矩阵构建节点的思路把位置、旋转、缩放这三件事打包成一整个矩阵作为一份完整的空间变换数据流在节点图里传递需要时再拆开用。这么一改节点图清爽得多运行效率也上来了而且逻辑上一下就通了——因为你处理的不再是三个互不相干的属性而是一个完整的“坐标系”。这篇东西就把我折腾下来对矩阵构建节点的理解、实际用法、以及踩过的坑完整梳理一遍适合已经在用程序化建模、但还没认真对待矩阵这个概念的读者也适合想搞懂程序化散布和实例化工具底层逻辑的朋友。1. MatrixConstruction节点到底解决什么问题程序化建模里最常干的一件事就是把一堆实例或者部件按规则放到场景里。位置、旋转、缩放每一项单独拿出来都不难搞定难的是把它们组合成一个整体并且让这个整体可以被继续传递、继续变换。矩阵构建节点就是干这个的。1.1 核心痛点程序化散布为什么离不开矩阵构建想象一下你要在一面弧形的墙上贴瓷砖。每块瓷砖有自己的位置这个位置要贴合墙面曲线有自己的朝向要垂直于墙面的每个局部表面还得根据离视点的远近稍微调整大小模拟透视效果。如果只用“Set Position Set Rotation Set Scale”这种分散属性去处理每一个实例都要经历三轮数据流转而且一旦后面要整体旋转这面弧形墙你那些已经算好的绝对位置全部作废得全链路重算。用矩阵就不一样了。矩阵把位置、旋转、缩放编码在一个结构里中间任何一步都可以直接对这个结构做乘法——左乘一个旋转整体就转向右乘一个缩放局部就放大。它像一个“带完整姿态的容器”装进去之后后续怎么折腾都是在容器层面操作而不是回去改每个容器的零件。1.2 谁最需要认真理解这个节点我自己的感受是三种人最需要把矩阵构建玩明白。第一种是做大规模程序化场景的TA和地编散布植被、碎石、建筑模块矩阵帮助他们把实例的“放置逻辑”和“内容本身”解耦。第二种是做程序化动画的齿轮传动、链条跟随、骨骼匹配本质上都是矩阵在时间轴上的连续变化。第三种就是单纯想把节点图写得更干净的人——哪怕只是做几十个物体的排列用矩阵也比堆一堆Transform连线舒服得多。2. 矩阵的底层原理一个4x4矩阵里到底装了什么先别被4x4吓到。图形学里的矩阵没有你想的那么神秘它就是一张有规则的表格把空间变换的信息整齐地放在固定位置。2.1 一个4x4矩阵的信息布局通常我们用四阶矩阵表示三维空间里的任意变换因为只有四阶矩阵能同时描述平移和旋转缩放的组合。矩阵内部是分区的左上角3x3块负责旋转和缩放右上角3x1列负责平移最下面一行固定为0 0 0 1这个结构不是随便定的。之所以多出一维是因为平移在数学上没法直接用3x3矩阵表达——它本质上是加法不是线性变换。把坐标升到四维加上一个恒为1的w分量平移就能写成矩阵乘法的形式。这也是为什么图形接口全线用四阶矩阵不是刻意绕是数学上最优雅的做法。为了直观理解你可以把矩阵看作“先搬到新地方”和“在新地方摆了个姿势”的合体描述。右上角告诉你它挪到了哪左上角告诉你它面朝哪、站姿什么样。2.2 从位置旋转缩放到矩阵的映射逻辑构建矩阵时最典型的顺序是TRS先缩放再旋转最后平移。缩放只影响局部大小旋转决定局部朝向平移把整个局部坐标系放到世界坐标系里。通俗地讲先把它做成一个小模型再把小模型转个方向最后把小模型放到目标地点。如果你在一个接龙式的节点链里从左到右依次接入Scale、Rotation、Translation那生成的矩阵正好就是“先局部缩放 → 再旋转朝向 → 再移动到位”的顺序。这也是为什么MatrixConstruction节点的输入口通常就是这三个——位置、旋转、缩放节点内部按TRS组合。这还没完编译到GPU层面时这个矩阵会被用来变换成千上万个顶点。所以你在节点图里构建的一个矩阵最后承担的是“生成一个局部坐标系”的工作而不是简简单单的“改几个数字”。2.3 矩阵乘法就是“先做什么、再做什么”矩阵乘法是整个体系里最值钱的思想。每一个矩阵都可以理解成一条指令把物体从当前状态变成新状态。两个矩阵相乘就是连续执行两条指令。关键坑在于矩阵乘法不满足交换律。A乘以B通常不等于B乘以A。我习惯用一个生活例子解释先穿袜子再穿鞋和先穿鞋再穿袜子结果完全不同。矩阵乘法里左边和右边的位置就是“先”“后”的差异。在MatrixConstruction节点体系里如果你对外层再乘一个整体旋转矩阵那相当于所有实例放到世界之后又整体转了一遍如果你在构建阶段就乘上局部旋转矩阵则是每个实例各自在局部坐标系里自转。这两种效果经常看起来相似实际结果完全不同。3. 实际应用用矩阵构建在山坡上散布岩石并贴合法线理论说得再多不如直接跑一个完整案例。这个案例我实际跑过很多遍也是我最初被矩阵构建折服的项目。3.1 先定目标在起伏地形上随机放一批岩石目标效果是一座随机噪波生成的山丘上面散布大约500块岩石岩石贴合地形表面朝向跟随法线大小随机控制在0.3到1.5倍之间还要稍微避免岩石互相穿插。如果不考虑矩阵你会自然地想先用“散布点”把位置撒出去采样地形高度再采样法线用Align Euler to Vector算出旋转最后喂给实例化节点。问题出现在“对齐法线”这一步——Align Euler to Vector能给出一个方向对齐的欧拉角但当地形有陡峭的褶皱时欧拉角在特定角度附近会产生抖动这就是俗称的万向锁而且调试起来非常痛苦。3.2 核心链路采样位置、求法线、构建矩阵、实例化我用的资产是Blender的Geometry Nodes但思路在所有带节点化矩阵的工具里完全通用。核心链路分成四段用“分布点于面上”生成候选点传入密度参数用“采样邻近表面”分别取到每个点的位置和法线从法线构建旋转最稳妥的做法是拿法线做“对齐欧拉到向量”的Z轴再用随机值旋转这个欧拉角用MatrixConstruction节点的三个输入组装出完整变换矩阵送进实例化组装这一步是精髓。位置来自采样旋转来自法线对齐缩放来自一个挂在线性随机分布上的数值三者一次性编码进矩阵。后面的“实例化于点”只需读取这一个矩阵就能准确地把岩石摆到世界坐标里。比起向实例化节点分别提供Position、Rotation、Scale三个接口的做法矩阵方案的差异在于中间层。矩阵你给我的是一个统一的变换数据我再把它矩阵乘到实例的原始变换上。之后需要做整体旋转偏移时直接对矩阵做乘法就行不用回头改散布参数。3.3 参数化的细节密度、缩放、随机种子怎么调实际操作中我习惯把每个关键量都做成输入参数这样在UI里调起来才够快。密度参数直接控制散布点数量500个改成5000个不需要动节点结构缩放范围我用最小值0.3倍、最大值1.5倍再挂一个随机种子保证每次调整都能复现。随机种子的作用不仅仅是“重新随机”那么简单。种子变了矩阵里的旋转随机分量和缩放随机分量都会变但如果你的随机数生成器没和“位置”这个字段绑定可能出现同一块岩石每次刷新都在小幅抖动。我建议把随机种子关联到点的原始索引上而不是依赖每次生成都是全新随机数流的默认行为。施工完后能看到一个非常直观的现象石头的朝向严格贴合地形曲面整个坡面的岩石像“长”在上面一样而不是被摁进去的。最终检查时也不需要去看几千个物体的单项属性只要检查烘出来的矩阵是不是连续变化的就够了。4. 跨工具对照Houdini和Unreal里是怎么构建矩阵的矩阵构建的思路不绑定任何特定软件。只要你搞清楚自己在构建什么换平台只是换一组节点名或API的问题。4.1 Houdini中的矩阵构建思路Houdini VEX里构建矩阵代码本身就说明了一切。matrix m ident(); translate(m, P); rotate(m, radians(ch(angle)), {0,1,0}); scale(m, ch(scale_factor)); vector pos_after m * {0,0,0};这四行做的事情和MatrixConstruction节点的逻辑完全一致先单位化再平移再绕Y轴旋转再缩放。Houdini的VEX里矩阵就是一个float4x4的变量乘在点位置上就等于对点做了一次空间变换。很多用Houdini的程序化老手习惯了直接在Wrangle节点里写这几行反而把可视化节点图里的矩阵构建组件给冷落了。4.2 Unreal PCG里看矩阵Unreal的PCG框架里的Transform本质上也不是三个独立属性而是FTransform内部包含位置、旋转、缩放底层同样是一个4x4矩阵的分解形式。你在PCG里做的“Transform Points”其实就是在改FTransform里的矩阵分量。PCG的节点图没有把Matrix直接暴露成一条可编辑的数据流但概念还是一样的每一个点实例都带一个变换矩阵程序化的核心就是批量生成和管理这些矩阵。4.3 横向对比矩阵构建的共通点环境矩阵构建的表现形式我常用的入口Blender GNCombine Transform / Matrix构建位置、旋转、缩放三个输入汇成矩阵HoudiniVEX matrix 类型 / Make TransformVEX的translate、rotate、scale函数Unreal PCGFTransform矩阵的分解形式Transform Points节点图形ShaderModel矩阵 / Local to World引擎自动生成概念等价从中能得出的唯一结论是与其问“我的工具支不支持矩阵”不如问“我的数据流应该怎么在矩阵的拓扑里流动”。一旦接受了矩阵这种表达任何平台上的变换逻辑都变得极其统一。5. 构建和消费矩阵时最容易翻车的三个点哪怕你理解了矩阵概念实操中还是有几个地方反复出幺蛾子。我踩过的这些坑列出来能帮后来人省不少事。5.1 乘法顺序错乱先旋转还是先平移我最早吃这个亏是在做一个齿轮阵列时。需求是每个齿轮不仅围绕自己的轴旋转还要围绕中心轴公转。如果构建矩阵时先乘公转旋转再乘自转旋转结果就会变成齿轮被强行搬离自己的位置——因为矩阵乘法先作用在你写的左边如果你把公转放到局部自转之后实际上就改变了物体在空间中的绝对位置。记住一句话想把每个实例自身旋转转的是局部矩阵想让整个阵列一起动乘的是外层矩阵。两个旋转要用两次矩阵乘法完成顺序颠倒了整套阵列就散架。在节点图里表现出的现象往往很迷惑——你看每个参数都对但结果就是一个位置偏掉了。5.2 非均匀缩放下法线也跟着变形第二个坑是大规模散布时容易遇到的。给岩石设置缩放时如果你让X、Y、Z三个轴的缩放相互独立做出来的石头就会出现一个莫名其妙的现象朝特定角度的石面法线是歪的光照和碰撞全部异常。这是因为非均匀缩放矩阵进入3x3旋转子块后正交性被破坏了。标准的法线变换要用原矩阵的逆转置矩阵如果你只对整个矩阵做普通乘法法线方向会跟着变形。解决方法要么是非均匀缩放尽量放在实例生成的最后一步要么单独保存法线变换数据不要偷懒直接复用位置变换矩阵。5.3 把局部轴当世界轴用导致的旋转错位还有一个极常见的问题区分不了“这个旋转在世界空间生效”和“这个旋转在局部空间生效”。MatrixConstruction节点的旋转输入默认规则通常是全局欧拉角它构建出来的矩阵作用点是世界坐标系的原点。如果你想做“沿自己的局部Z轴翻滚”的效果就要先构建一个局部旋转矩阵然后用矩阵乘法把它作用到原矩阵的右侧而不是单纯在旋转输入框里改数字。调试这个问题的标志很明显旋转一个物体时它绕着世界中心转而不是绕着自己的几何中心转。出现这种状况一定是矩阵乘法的操作对象放错了。5.4 调试矩阵的实用技巧调试矩阵这回事如果工具没有提供直接的矩阵预览最容易的办法是做一个“探针点”把矩阵变换到一个位于原点的小球上然后观察小球在视口里的位置、朝向和缩放。坐标轴判断法也很有用把矩阵变换后的X轴单位向量可视化出来看它指向哪里就能知道你构建的旋转方向对不对。另一个办法是反向拆矩阵。市面上几乎所有DCC都提供了“拆分变换”节点能把矩阵重新拆成位置、旋转、缩放三个分量。调试时在构建节点后面接一个拆分逐个核对分量比直接盯着数值矩阵猜要高效得多。6. 进阶思路把矩阵当成动画和数据的桥梁矩阵构建不只服务于静态散布更高级的用法是拿它当动画和数据之间的桥梁。6.1 用矩阵插值让物体沿任意路径运动传统的位移动画用关键帧记录位置但如果你想让一个物体沿着一条曲线精确运动且运动过程中姿态连续变化手动K帧是不可能完成的。正确思路是把曲线上采样得到的位置和切向方向实时构建成矩阵然后把这个矩阵作为模型的变换输入。曲线形状改矩阵跟着变动画自动更新。这就是所谓“数据驱动动画”。这个玩法在机械装置模拟中尤其好使。链条上一个链节的位置由链条曲线决定朝向由曲线的切线方向决定这样一个矩阵就把链节死死地钉在正确姿态上只要曲线参数化处理得当整个链条动画平滑到离谱。6.2 矩阵是实例系统的大规模并行数据从性能角度讲矩阵还有一层更实际的价值。实例化系统在GPU端本质上是对每个实例执行一次矩阵变换把本地几何体变换到世界坐标。你用MatrixConstruction节点批量生成的矩阵列表可以直接作为GPU实例化缓冲区的数据来源。大量实例共用一个几何体只变更各自的矩阵渲染一帧的代价远小于直接创建几千个独立物体。如果你做的是动辄上万实例的场景把变换数据整理成矩阵列表丢给实例化管线比逐个在CPU端计算Transform再同步给GPU要快出一个量级。这也是为什么所有现代引擎都在走“实例缓冲”路线矩阵正是这个缓冲的核心字段。6.3 一点个人体感我从开始硬着头皮啃矩阵到现在把它当成日常工具最大的体感是程序化生成的核心本来就不是“放一堆东西”而是“定义一套变换规则再批量应用”。矩阵构建节点把规则和运算放在同一个数据层面让这套玩法从底层就清爽起来。至于新手建议别急着背公式先在工具里把一个球放到和别人不一样的位置和姿态再用矩阵方式复现一次感受一下“规则即矩阵、矩阵即规则”这个过程。跑通一次后面一切都顺了。