逐顶点动画进阶:ComputeVertexPosition节点原理与实战拆解

逐顶点动画进阶:ComputeVertexPosition节点原理与实战拆解
如果你也想让网格上的顶点“活”起来——不是整体平移、旋转那种刚体运动而是像布料、水面、植被一样每个顶点有各自的位移那你大概率会在某个节点图或者渲染脚本里撞见ComputeVertexPosition这个节点。我最早认真研究它是因为要做一个“粒子冲击波把地面顶点层层推开”的场景当时拿着官方文档硬啃了很久真正跑通之后才发现这个节点背后的原理远比它的名字看起来有意思。这篇内容我想用自己实际折腾过的方式把ComputeVertexPosition从输入到输出、从坐标空间到实际用例完整拆一遍。不管你是正在做特效的 TA、写渲染逻辑的程序员还是刚接触节点系统的新人只要你需要在 GPU 上按顶点批量计算新位置这篇都应该能帮你少走几天的弯路。1. 为什么顶点位置需要“单独被算一遍”先说一个很多新手会困惑的问题一个静态模型放进引擎里明明能正常显示顶点位置早就有了为什么还需要一个专门的节点去“计算顶点位置”这不脱裤子放屁吗还真不是。模型自带的顶点坐标只是“出厂数据”它描述的是这个模型在建模软件里的样子。渲染一帧画面时GPU 要把这些原始顶点从模型空间搬到世界空间再搬到摄像机空间最后变成屏幕上的像素位置。这一套变换流程是顶点着色器在每一帧里对每个顶点重复执行的。问题在于引擎默认的顶点变换非常“规矩”一个顶点最终在哪只取决于模型本身的位置、旋转、缩放和摄像机参数结果就是模型永远是刚体。可实际项目里大量效果需要打破这种“规矩”地面被一个从天而降的冲击波砸中波形范围内的顶点要按距离往下凹陷骨牌被玩家撞倒时不是整块模型倒下而是每个顶点根据物理约束一点点弯曲草和麦穗在角色走过时自动压弯离开后再慢慢回弹角色裙摆、披风要跟随腿部动作摆动。这些效果的共同点是每个顶点的最终位置不能只靠模型自身的变换矩阵决定还需要外部输入受力点、时间、噪声、粒子属性参与逐顶点计算。这时候你就需要一种机制在渲染管线的某个环节把“默认位置计算”替换成“自定义位置计算”。ComputeVertexPosition这类节点就是干这个的。在不少引擎的材质编辑器和粒子渲染器中这个节点被挂在顶点位置Vertex Position / World Position Offset通道上每帧对所有受影响网格的顶点跑一遍。它的存在感很弱却决定了整个效果能不能立住。1.1 节点在渲染管线里的真实位置如果把一次渲染拆成“CPU准备数据 - 顶点着色器 - 像素着色器”三段ComputeVertexPosition负责的是顶点着色器里最关键的那一步给定一个顶点的本地坐标结合一堆外部参数算出这个顶点该去的位置。它不是某个引擎独有的魔法而是图形开发里很常见的一层抽象。不同引擎叫法不同引擎/环境类似功能打开位置Unreal 材质编辑器World Position Offset 输入材质节点图 / 自定义表达式Unreal NiagaraCustom Mesh Vertex Position网格渲染器的位置绑定Unity Shader GraphVertex Position Block顶点着色器 Master StackBlender Geometry NodesPosition / Set Position 组合几何节点编辑器纯 HLSL/GLSL自己写 vert 函数Shader 代码很多团队会把通用的位置计算逻辑封装成一个内部节点顺手命名为ComputeVertexPosition方便在各类节点图里复用。理解它的通用原理比记某个按钮在哪个菜单里更重要。1.2 这个节点的输入输出账本要理解ComputeVertexPosition先把它当做一个函数。我用 HLSL 概念伪代码描述一下它通常接收什么、产出什么float3 ComputeVertexPosition( float3 objectPosition, // 模型自带的顶点坐标 float3 objectNormal, // 模型自带的法线 float4x4 objectToWorld, // 模型到世界空间的变换矩阵 float4x4 worldToClip, // 世界空间到裁剪空间的变换矩阵 float4 customData // 外部传入的顶点参数比如顶点色、权重 ) { // 第一步根据外部参数计算一个“偏移量” float3 offset ComputeOffset(objectPosition, objectNormal, customData); // 第二步把偏移后的本地坐标转换到裁剪空间 float3 worldPosition mul(objectToWorld, float4(objectPosition offset, 1.0)).xyz; float4 clipPosition mul(worldToClip, float4(worldPosition, 1.0)); return clipPosition; }真正在节点图里使用时你看到的并不是这样一个函数而是一堆连线。但它的输入输出逻辑高度一致输入原始顶点坐标、法线、顶点色、自定义属性、外部动态参数时间、冲击波中心点、力场距离等输出一个新的顶点坐标通常会经过空间变换后直接成为渲染阶段的顶点位置。这也是我后来排查问题的核心思路如果一个顶点动画没生效先拆这条链路看是输入的数据没进来还是计算逻辑写错了还是输出没有接到正确通道上。2. 核心运行逻辑索引、坐标空间与逐顶点遍历ComputeVertexPosition最值得琢磨的地方有三个它跑在什么粒度上、它处理的数据在哪个空间里、它为什么能在 GPU 上支撑成千上万个顶点同时计算。2.1 顶点索引理解节点的第一把钥匙GPU 的顶点着色器有一个特点它是按顶点并行执行的。一个网格如果有 10 万个顶点GPU 会把这 10 万个顶点的计算任务拆成很多小的执行单元同时跑而不是像 CPU 那样用 for 循环一个个来。ComputeVertexPosition在节点图里看起来只有一个节点其实它被展开了在每一个顶点上各跑一次。每次执行时当前是第几号顶点当前顶点的原始坐标是什么都以“属性”的方式喂给节点。这个“按顶点展开”的模型决定了你写位置计算逻辑时不能像写普通程序一样依赖前后顺序。很多新手写位置变形时会下意识想“我能不能先算第 0 个顶点再把结果传给第 1 个顶点”在纯顶点位置计算节点里做不到因为 GPU 不保证顶点之间的执行顺序。如果某个效果必须依赖邻居顶点的状态那要考虑的不是在这个节点里做全局通信而是把数据写到纹理/缓冲里做多 pass 处理或者换到 Compute Shader 里跑。2.2 坐标空间顶点炸飞大多是空间没统一ComputeVertexPosition里最容易出错也最值得展开讲的是坐标空间问题。一个顶点在进入这个节点前可能以多种身份存在空间含义常见用途模型/本地空间以模型自身轴心为原点做“基于模型自身形状”的变形最方便世界空间以场景世界原点为原点做“受外部世界坐标影响”的效果必须转到这里观察/相机空间以相机为原点跟屏幕空间效果相关时使用裁剪空间归一化后的设备坐标最终交给光栅化的坐标我见过最多的问题就是模型坐标直接和世界空间的力场中心做距离计算。模型坐标里一个顶点可能是(0.5, 1.2, 0.3)范围很小。世界空间里地形可能偏移到(15000, 0, 8000)。如果你用模型坐标去和世界坐标的冲击波中心做距离得到的数值会大到离谱或小到失真效果自然不对。正确的做法是在同一个空间里计算。通常我习惯这样拆先用TransformObjectToWorld把顶点坐标转到世界空间在世界空间里施加所有“基于外部世界位置”的力冲击波、角色位置、风场如果引擎的顶点位置通道期望的是本地空间偏移那就把结果转回本地输出偏移量如果引擎期望的是世界空间坐标那就直接输出最终世界坐标让引擎内部做后续变换。这里没有万能的模板因为不同引擎的输入要求不一样。但你可以靠一个简单的判断“效果的计算源头在哪个空间就把所有数据统一到哪个空间”。2.3 为什么不用 CPU 循环算位置有些从后端或者工具开发转过来的朋友问我“既然这么麻烦在 CPU 上每帧把顶点算好更新 Mesh 的顶点数组不行吗”技术上可以但要分场景。静态模型几百个顶点CPU 更新完全没有压力。可如果是一大片地形、一整片草地顶点数上百万甚至更多每帧在 CPU 上循环遍历所有顶点、计算位移、写入缓冲区、再上传 GPU这个成本会直接吃掉一帧的预算。GPU 的优势在于它是大规模的并行处理器ComputeVertexPosition这类节点让位置计算发生在 GPU 已经常驻的数据上不需要把几何数据从 CPU 拷贝到 GPU也不需要频繁回读结果。我做地形波纹效果时同一套算法CPU 更新顶点数 2 万时还能跑 60 帧顶点数到 30 万直接个位数帧率GPU 节点方式顶点数 30 万帧率几乎没有可感知的波动。道理很简单GPU 顶点着色器的设计目标就是每帧处理数百万个顶点ComputeVertexPosition只是在这个已经有大量硬件优化的阶段里加了一段自定义逻辑。2.4 一个典型的逐顶点计算流程为了说清楚执行流程我画一个文字版的逻辑流[GPU 启动顶点着色器] ↓ [读取当前顶点的本地坐标和法线] ↓ [从网格/材质取顶点色、ID、UV 等属性] ↓ComputeVertexPosition 插入点 [根据外部参数计算位置偏移或直接改坐标] ↓ [输出到裁剪空间] ↓ [进入光栅化像素着色器着色]平时你调材质参数、连节点做的只是中间那一块。真正理解这个流程以后你会明白为什么很多动态顶点效果必须开“逐顶点动画”相关选项为什么位置变了但像素着色效果没变也更容易定位是哪个环节出了问题。3. 从静态到动态粒子冲击波推平地面顶点的实际案例概念讲完分享一个我实际做过的效果一个球体粒子从高空落下撞击平面网格的瞬间以撞击点为中心产生一圈向外扩散的凹陷超过范围后逐渐恢复。这正是ComputeVertexPosition的典型应用场景。3.1 场景参数与思路这个案例的关键参数有四个平面网格横竖各 200 段顶点数约 4 万个冲击波中心点由一个粒子的位置提供会随时间变化冲击波强度粒子落地那一帧达到最大随后衰减影响半径比如 3 米半径外的顶点不受影响。我的计算逻辑这样设计对每个顶点取它的世界坐标计算到冲击波中心的水平距离距离大于半径的偏移量为 0距离小于半径的根据距离做衰减越靠近中心凹陷越深把凹陷量作为顶点位置偏移加进去。3.2 核心计算函数把上面逻辑写成函数大概是这样一个形状这里以 HLSL 风格为例方便你翻译到自己的引擎float3 ApplyImpact( float3 vertexWorldPos, float3 impactCenter, float impactStrength, float impactRadius, float3 planeNormal) { // 忽略高度差来计算水平距离 float3 toCenter vertexWorldPos - impactCenter; float horizontalDist length(toCenter.xz); // 衰减曲线1.0 在中心0.0 在半径边界 float falloff 1.0 - saturate(horizontalDist / impactRadius); falloff falloff * falloff; // 用平方让边缘过渡更自然 // 沿法线通常是垂直方向产生偏移 float3 offset planeNormal * (impactStrength * falloff); return vertexWorldPos offset; }这里有几个细节是我实际试出来的衰减用1.0 - saturate(distance / radius)而不是直接阶跃不然凹陷边缘会出现生硬的折痕衰减函数再乘一次自己也就是平方衰减中心凹陷会更明显边缘更平滑只用水平距离是因为地面网格高度本身可能因为凹陷而变化继续用三维距离会把已经凹陷的顶点筛掉形成错误的环状纹路。3.3 在节点图里的连接方式在 Unreal 材质编辑器里我会创建一个 World Position Offset 输入把上面的距离计算用蓝图节点连出来或者直接用 Custom Expression 写 HLSL 并暴露Center、Strength、Radius三个参数。需要注意World Position Offset 期望的是“从原始位置出发的偏移量”所以我最后返回的是offset而不是绝对坐标。在 Unity Shader Graph 里连接对象是 Vertex Position Block。先算好世界坐标再转回 Object Space 输出也可以直接在那个通道里做偏移。在 Blender Geometry Nodes 里我觉得等价做法是用 Position 节点取出每个顶点的坐标采样一个用来表示冲击波位置的 Empty 物体然后按距离生成位移最后接到 Set Position 的 Offset 输入。底层思想和 Shader 里是一致的只是 Blender 的节点是在 CPU/GeoNode 执行环境里跑的不适合做超大网格的逐帧动画。落地效果上这类“粒子驱动顶点”的做法很适合做爆炸地面的冲击坑、战车碾过泥地、角色踩到沼泽地的凹陷变形。它最大的好处是网格顶点数据不需要每一帧由 CPU 重新提交只要把粒子的位置、半径、强度作为变量传给 GPU 就行。3.4 位置变了光照也要跟上顶点位置变完以后千万别忽略法线问题。默认情况下模型法线只反映原始几何形状。当一个平面被冲击波压出一个凹陷顶点的实际位置已经变了但法线还指向原来的方向光照效果就会看起来像“假凹陷”表面是平的却画着一个坑的形状。解决办法有几种数学方法计算凹陷后重新求法线。对于规则网格可以用相邻顶点的位置差分算出切线和副切线叉积得到法线数值方法在顶点着色器里多做几次位置计算做微小偏移用中心差分估算梯度简化方法效果要求不高时直接把法线朝冲击中心方向偏转一点视觉上也能骗过眼睛。我自己的习惯是网格规则且顶点密度高时先用中心差分法重建法线如果网格不规则就在 CPU 或预计算阶段准备额外的切线权重降低 GPU 上的计算压力。4. 另一个高频应用顶点色驱动的植被弯曲与交互压平如果说冲击波例子是“一个中心点影响一堆顶点”那植被交互则更像“角色移动路径影响一片草叶”。这类效果在开放世界里很常见玩家走过草地草自动倒伏汽车开过麦田麦秆被压在轮辙里。ComputeVertexPosition在这里的用法比单一冲击波更讲究“遮罩”和“权重”。4.1 用顶点色做权重让草叶“根不动、尖动”直接对所有顶点施加相同力度的偏移结果是整株草连根拔起没有任何自然感。现实中草被压弯时靠近地面的部分几乎不动越靠叶尖偏移越大。这个根部到尖部的权重变化在美术资产制作时就可以预处理好。做法是用顶点色或者单独的顶点权重属性的 R 通道表示“可动程度”根部顶点 R0叶尖顶点 R1。计算时每个顶点的最终偏移量乘上这个权重。float bendFactor vertexColor.r; float3 windOffset windDirection * windStrength * bendFactor;这个操作在ComputeVertexPosition里非常划算因为它只是每顶点多读一个属性、多乘一个数几乎不增加额外开销。4.2 角色压平草地的关键逻辑再看角色走过草地被压平的效果。核心不是每个草叶都像被同一个冲击波推开而是需要更精确的影响范围角色脚掌附近草倒伏最强远一点逐渐恢复。我通常的做法是获取角色脚底的世界坐标和脚掌包围半径计算草叶每个顶点与脚底的水平距离距离小于半径时让草叶顶点沿着“远离角色前进方向”的方向倾倒倾倒力度按1 - distance / radius衰减角色离开后用一个恢复权重让草慢慢立回来。叶子倒伏最自然的方向不是纯水平而是稍微带一点向下的弧度模拟茎秆被压弯、回弹时的韧性。这个可以通过把偏移方向从“水平推离”改成“沿茎干下垂方向旋转”来实现如果不想做得太重单用水平加垂直的混合偏移就够了。4.3 配合时间做回弹现在再引入时间。如果一个草被压弯后立刻恢复原状视觉上非常“弹”如果永远不恢复又像被压死了。更接近真实的是压下去的瞬间有延迟恢复时先快后慢。在节点里实现时我把“被压弯状态”做成一个随时间衰减的标量float recoveryFactor saturate(1.0 - _RecoveryTime / 1.5); recoveryFactor recoveryFactor * recoveryFactor; // 先快后慢实际计算位置偏移时用recoveryFactor去缩放压弯量。这样草会在角色离开后 1.5 秒左右逐渐竖直速度由快变慢观感上很接近真实草叶的回弹。4.4 顶点动画和像素效果的配合植被弯曲场景还需要处理一个细节顶点位置变了但贴图里的颜色、边缘高光、风的方向感都是按“原始模型的 UV 和法线”做的。只调位置不管像素着色草变成波浪形很容易穿帮。我在植被项目里一般会额外做三件事在顶点着色器把“当前偏移量”写入一个自定义顶点数据像素阶段采样后用来助推光影这样草叶弯曲的一侧能有一点点颜色加深如果引擎支持把弯曲后的法线传递到像素着色阶段阴影阶段尽量也用同样的位置计算否则物体本身的影子会停留在未变形的位置。因为好几种效果的顶点位置计算逻辑是共用的我更倾向于把整套计算封装成一个独立的函数或子图命名就叫ComputeVertexPosition然后材质主节点的位置通道、阴影通道都调用它。这样只要改一个地方其他引用处同步生效不会出现“主视角正常但阴影视图错位”的尴尬。5. 性能、精度与不同引擎封装的差异很多人以为顶点动画开销很大其实ComputeVertexPosition这个节点本身的代价取决于你让它做了多少事。一个只做“顶点色乘风力”的节点可能比一个复杂像素着色器还便宜因为它只跑在顶点数上而像素着色器跑在屏幕像素数上。一个模型的顶点通常远少于屏幕像素所以顶点阶段做计算相对划算。5.1 几个影响性能的真实因素实际项目里影响顶点位置计算性能的因素主要有这么几类顶点数这是最直接的。顶点越多计算量线性增加。这里的顶点数要看“提交给 GPU 的实例顶点总数”而不是单个模型的顶点数。如果一个植被模型有 400 个顶点你用 GPU Instancing 种了 1 万棵那顶点计算就是 400 万次节点复杂程度每多读一张纹理、多做一个sin/cos计算、多跑一次分支都会增加指令数。虽然 GPU 并行度强但指令多了照样吃帧率分支和动态索引GPU 上做分支没有 CPU 那么“免费”。如果条件非常不均等会造成线程组内部执行路径分化降低吞吐。对于小半径范围影响的效果建议用saturate、smoothstep这类函数代替if外部变量读取频率不少节点每帧要从 CPU 更新参数。参数数量不太影响瓶颈但如果为了一个效果每帧上传几百个浮点数还是该考虑用纹理或结构缓冲。5.2 精度问题浮点数的坑顶点位置计算在动辄上千米的大型地图里还会遇到浮点精度问题。世界坐标数值一旦非常大float32 的精度就喂不饱细微的位置偏移。一个 10000 单位的坐标float32 大约只能区分 0.001 单位的变化看起来没问题但当偏移量是毫米级别就会出现闪烁或不稳定。解决方案通常是让顶点动画在“相对坐标”中做在世界空间只计算方向和粗偏移把精细的、局部的修改放到网格本地空间或者相机相对空间不要让超大绝对坐标参与连续多次的乘法和除法运算。我见过不少崩溃案例地形顶点动画在地图原点附近一切正常一旦玩家跑到地图角落顶点开始抖动甚至撕裂最后定位就是模型坐标和世界坐标混在一个节点里反复变换精度被白白浪费。5.3 三类引擎在顶点位置上的封装差异用不同引擎做同一个顶点动画其实底层原理相似但节点位置完全不同我整理了一张对照表方便你在项目里快速找到入口平台入口位置输出内容典型注意事项Unreal 材质World Position Offset本地空间偏移量需要额外处理阴影通道时注意是否单独接线Unreal Niagara网格渲染器 / 自定义位置绑定根据粒子系统驱动顶点常用于粒子带动网格局部变形Unity Shader GraphVertex Position Block模型空间坐标接口不同版本差异大URP 和 HDRP 的默认变换不完全相同Blender Geometry NodesSet Position 节点位置字段可以就地修改适合离线资产生成不适合高频实时动画纯 OpenGL/DirectXVertex Shader 中自定义函数通常输出裁剪空间位置自由度最大也最容易犯错不要试图把某个平台的连线直接照搬到另一个平台。最关键的是确认“这个平台在这个通道里想要的是绝对坐标还是相对偏移”以及“它默认使用的空间是模型空间还是世界空间”。这两个点确认清楚了迁移成本会大幅下降。6. 实战排障顶点动画不对时我按什么顺序检查最后分享一套我自己排查顶点位置问题的顺序。每次有人拿着“节点看起来没问题但效果不对”来找我我基本都按这个套路查。6.1 先看空间是否统一出问题的概率里空间不统一占一半以上。我会先问自己外部输入的力场中心、方向向量是在哪个空间里拿到的顶点坐标又在哪个空间里把节点里所有参与距离、点积、角度运算的数据先确认它们是不是同一个空间。一个容易忽略的坑是有些变换节点在接受矩阵时如果传入的是行主序/列主序不一致的数据计算出来的结果看起来完全随机。在跨引擎移植时我会专门写一个测试把一个已知坐标的顶点用已知变换矩阵算一遍和预期的世界坐标比对数值对上了再继续。6.2 再查参数有没有真正传到 GPU如果空间没问题但效果纹丝不动下一步查动态参数传递。我常在节点里故意做一次“幅值拉满”的测试把偏移量直接设成一个肉眼可见的大数比如 10 米看网格是不是瞬间被拉飞。如果拉飞了说明节点执行正常问题在参数数值范围或参数更新频率如果纹丝不动说明节点可能根本没接到顶点位置通道或者对应通道被引擎其他设置覆盖了。这个排查方法虽然粗暴但特别有效。它能把“系统问题”和“参数问题”快速切分开不用在节点里反复猜。6.3 检查法线和阴影是否同步更新如果主视角效果是好的但阴影和光照不太对那多半是法线没更新或阴影阶段没有用同一套位置计算。在支持多通道的引擎里我建议把位置计算封装成一个公共函数材质主节点、阴影节点都调用从结构上避免不一致。封装后一旦遇到问题你只需要检查一个函数而不是在很多地方散落着重复逻辑。6.4 关于几个常见怪现象的笔记顶点飞出去变成一条直线或一个点通常是模型矩阵和世界矩阵乘了两次或者把裁剪空间坐标当世界空间坐标继续算动画有节奏但密集体抖动很可能是浮点精度不够调整到相对坐标或提高精度按顶点色做权重时有人半边模型有效果另外半边没有检查顶点色有没有合并且未丢失很多建模工具默认顶点色不是保存在所有顶点上的需要重新焊接或烘焙使用几何节点软件里的工具时出现“索引越界”或“结果不对”检查是否有多个网格改名/合并后索引顺序变化因为顶点位置计算强依赖顶点排列顺序合并网格后索引会重新排列。6.5 推荐的一步步验证流程如果从头开始做一个顶点位置效果我会按照下面的流程来验证每一步都确认无误再进入下一步先用纯常量偏移测试节点连接是否生效然后用顶点坐标的某个分量比如 y 坐标做偏移验证顶点读取是否正确再引入外部参数做最简单的线性影响逐步增加衰减、权重、法线重建等复杂逻辑每一层逻辑都通过一个开关快速禁用/启用方便对照效果最后分别检查主视角、阴影、像素法线三处是否一致。这套流程看起来慢实际是效率最高的。我见过太多人直接从最终效果开始调参数结果一个衰减公式写错了在节点里试了两个小时始终认为是数值问题而不是公式结构问题。关于ComputeVertexPosition这种节点我个人的体会是它看起来是一个小节点背后牵扯的却是“空间变换 数据传递 并行执行”三条知识线。只要这三条线能串起来以后再遇到任何逐顶点动画需求你都不会在节点图里迷失方向。至于那些按钮藏在哪个菜单里反倒是其次了。

最新新闻

日新闻

周新闻

月新闻