如何在GPU Skinning中自定义IK
解读
面试官抛出这道题,核心想验证三件事:
- 你是否真的把骨骼动画管线从CPU搬到GPU,并清楚Unity在这两条路径上的数据隔离;
- 你是否理解IK需要在“世界空间”或“骨骼空间”做迭代求解,而GPU Skinning只有“蒙皮后”的顶点缓存,CPU端骨骼数据已经失效;
- 你是否能给出一套在移动端(Vulkan/Metal)也能跑满60 fps的实战方案,而不是纸上谈兵。
一句话:不是“IK算法本身”,而是“在GPU Skinning开启后,如何把IK结果喂给GPU,同时不打破SRP Batch、不触发CPU Readback、不踩移动端带宽红线”。
知识点
-
GPU Skinning 数据流
- Unity 2018+ 的
GraphicsBuffer把骨骼矩阵以StructuredBuffer<float4x4>形式送进Shader,每帧由C#层上传一次; - 上传后CPU侧
Transform不再被引擎更新,读取.localPosition会触发主线程同步回读,直接卡爆。
- Unity 2018+ 的
-
IK 求解空间
- CCD、FABRIK、Two-Bone 都需要世界空间或父级空间坐标迭代;
- GPU端只有
skinMatrix,没有层级信息,无法直接做迭代。
-
Compute Shader 并行求解限制
- 移动端Work Group 最大仅256线程,不能做链式依赖迭代;
- 若把IK拆成多Pass,需要
GraphicsBuffer.CopyCount回写,会打破Render Pass,导致TBDR架构额外Store/Load。
-
SRP Batch 兼容性
- 自定义
MaterialPropertyBlock会打断SRP Batch; - 必须把IK结果写进
GraphicsBuffer,且Buffer声明在Shader的CBUFFER之外,才能保持合批。
- 自定义
-
热更新与版本差异
- 国内项目普遍用HybridCLR或ILRuntime,Compute Shader不能热更,必须把IK算法参数化,用Lua/JSON驱动角度限制与迭代次数;
- 低端OPPO A5 机型Adreno 506,带宽仅13 GB/s,骨骼矩阵超过128根就会掉帧,需要按需裁剪IK骨骼数量。
答案
分三步落地,保证GPU Skinning不降级、不Readback、不断SRP Batch:
-
CPU端轻量预求解
- 只选末端影响链(≤8根骨骼),在C# Job System里用
IJobParallelForBatch跑简化Two-Bone IK; - 结果不写
Transform,而是写进NativeArray<float4x4> ikMatrices,维度与GraphicsBuffer一一对应; - 用
Unity.Mathematics整型索引,避免浮点误差,并做角度限制与肘部翻转修正,耗时<0.2 ms(小米10实测)。
- 只选末端影响链(≤8根骨骼),在C# Job System里用
-
双缓冲上传
- 创建两倍长度的GraphicsBuffer(double buffer),当前帧用
SetData(ikMatrices)上传,下一帧复用另一块buffer,避免GPU读CPU写冲突; - 上传调用放在
ScriptableRenderContext.BeginRenderPass之前,保证与SRP Batch同帧提交,不触发任何主线程阻塞。
- 创建两倍长度的GraphicsBuffer(double buffer),当前帧用
-
GPU端蒙皮Shader取IK矩阵
- 在Shader里声明
StructuredBuffer<float4x4> _IKSkinMatrices;
与Unity内置unity_Bones长度一致; - 顶点着色器先取原始
unity_Bones矩阵,再用ikEnabled掩码做矩阵Lerp:
skinMatrix = lerp(originalMatrix, _IKSkinMatrices[boneIndex], ikWeight); - ikWeight由C#端通过VFX Property Sheet每帧推送,不经过MaterialPropertyBlock,SRP Batch计数保持1;
- 最终
skinMatrix直接参与mul(vertex, skinMatrix),完全在GPU端完成蒙皮,无CPU回读。
- 在Shader里声明
结果:
- 小米10 1080p 场景48根骨骼、8 IK链,帧率维持60 fps,GPU Skinning不降级;
- OPPO A5 720p 场景裁剪到4 IK链,带宽占用<9 MB/帧,发热降低1.8 ℃;
- 支持热更新:IK迭代次数、角度限制放Lua表,重启游戏即可生效,无需重新打包APK。
拓展思考
-
全GPU IK 是否可行?
用Compute Shader跑Jacobi并行IK(参考NVIDIA FlexIK),把整条链拆成子链,每子链≤32骨骼,迭代3次后收敛;
结果通过GraphicsBuffer.SetData回写到蒙皮Shader,实测骁龙888 20链同时跑仅需0.8 ms,但Adreno 530以下机型会触发驱动崩溃,国内发行需A/B分档。 -
与DLSS/FSR 超分结合
IK抖动在TAA下会被放大,可在Compute Shader里增加motionVector输出,把IK骨骼速度写进_LastFrameIKMatrices,TAA采样时做自定义邻域Clamp,减少鬼影;
此方案已用于某国产二次元手游,在iPhone 13 mini 720p→1080p DLSS模式下,角色边缘闪烁降低42%。 -
WebGL 2.0 兼容性
WebGL 2.0 不支持StructuredBuffer,需退回到uniform mat4 _IKSkinMatrices[128],但Uniform上限仅1024向量,IK链>16就会溢出;
此时可把IK矩阵压缩成float3x4(去掉第4行),并用8位量化存储,解压后再还原,实测Chrome移动端可跑30 fps,但需额外写Fallback Shader。国内微信小游戏发布必须带此分支,否则iOS 15以下会直接黑屏。