Deep Read

性能优化 -GPU动画

GPU 动画原理

GPU 动画的核心思想是把传统由 CPU 负责的蒙皮(Skinning)计算转移到 GPU 上并行处理,以突破 CPU 端的性能瓶颈,实现大规模同屏角色的高效渲染——实践中通常与 GPU InstancingGPU InstancingGPU Instancing 做的事:一次 Draw Call + 一份 Mesh/Shader 状态,通过 InstanceID 在 GPU 端索引每个实例的差异数据(transform、color 等),复用顶点数据,省掉 CPU → GPU 的重复提交。 问题:CPU 循环发 Draw Call 的开销   传统流程每画一个物体,CPU 都要调用图形API 发 Draw Call ,给到图形驱动,由驱动翻译成 GPU 原生指令写进 Command Buffer,GPU 再异步取出执行(这条 CPU→驱动→GPU 的提交链路见 [[性能优化大纲]])。驱动每翻译一条 Draw Call 都有一笔固定的 CPU 耗时,场景里几千个物体,这笔翻译成本就付几千遍。 image.png 问题在于: 如果这几千个物体用的是同一份网格和同一个材质,CPU 其实在为同一件事反复发命令,每次唯一的差别只是变换矩阵。GPU Instancing 从这里切入:它不让每次提交更便宜(那是 [[性能优化-SRP合批|SRP Batcher]] 的活),而是直接把提交次数从 N 压到 1。 底 配合使用。

传统 CPU 蒙皮:每一帧,CPU 都要为每个 SkinnedMeshRenderer 计算所有骨骼的最终变换矩阵(Bone Matrix),再遍历模型所有顶点,根据每个顶点绑定的骨骼索引(Bone Index)和权重(Bone Weight)把骨骼矩阵应用到顶点上,算出最终位置。角色数量一多,这个过程就会吃掉大量 CPU 时间。

GPU 动画(GPU Skinning):预先把所有动画数据(所有帧的骨骼变换矩阵)烘焙进一张或多张纹理,同时把模型的原始(Bind Pose)顶点数据(位置、法线、骨骼索引、骨骼权重)一次性上传 GPU。运行时 CPU 不再逐顶点计算,只需告诉 GPU 当前播放的是哪个动画的第几帧(一个浮点索引);GPU 的顶点着色器据此从动画纹理采样出当前帧的骨骼矩阵,在 Vertex Shader 内部完成蒙皮、变换顶点。

整个计算流程从 CPU 转移到了高度并行的 GPU 上,极大解放了 CPU。

底层工作流程

离线烘焙(数据准备)

  1. 动画数据提取:写一个编辑器工具,遍历模型的 AnimationClip。
  2. 数据编码:以固定采样率(如 30 FPS)对每个 AnimationClip 的每一帧采样,获取每个骨骼相对模型根节点的变换矩阵(Matrix4x4)。
  3. 存入纹理/Buffer:把这些矩阵线性地存进一张 Texture2D(通常用 RGBAFloatRGBAHalf 高精度格式)或 ComputeBuffer。例如纹理的每个像素(RGBA)存矩阵的一行,4 个连续像素存一个完整的 4×4 矩阵。

运行时(GPU 计算)

数据传递:

  • 模型 Mesh 数据(含骨骼索引 bone index、权重 bone weight)作为顶点属性传给 Vertex Shader。
  • 烘焙好的动画纹理/Buffer 作为 uniform 全局变量传给 Shader。
  • CPU 通过 MaterialPropertyBlock 或全局 Shader 变量,传入当前动画的起始索引和播放时间/帧索引。

顶点着色器内的计算: 对每个顶点依次做四步——

  1. 获取顶点数据:读取该顶点的 position、normal、bone indices 和 bone weights。

  2. 获取骨骼矩阵:根据 CPU 传入的动画索引和时间,结合顶点的 bone indices 计算出采样的纹理坐标(UV),从动画纹理采样得到该顶点关联的几个骨骼的当前变换矩阵。

  3. 线性混合蒙皮(LBS):用标准 Linear Blend Skinning,按骨骼权重对采样出的多个骨骼矩阵加权混合,得到最终蒙皮矩阵 skinningMatrix

    M_skin = Σ (w_i · M_i),  i = 0..n
    

    其中 w_i 是第 i 个骨骼的权重,M_i 是从动画纹理采样出的第 i 个骨骼的变换矩阵。

  4. 顶点变换:把最终的蒙皮矩阵应用到原始顶点位置和法线上,算出最终世界空间坐标:

    v_final = M_world · M_skin · v_bindpose
    

这个过程对每个顶点独立且并行,完美契合 GPU 的架构。

Shader 流程

数据输入:

  • 动画数据:通过 uniform 变量 _AnimTex(高精度 RGBAFloat 纹理)接收所有骨骼在所有动画帧的变换矩阵。
  • 蒙皮数据:通过顶点属性 BLENDINDICESBLENDWEIGHTS 接收每个顶点自身的骨骼索引和权重。
  • 驱动信号:CPU 每帧传入一个 float 变量 _CurrentFrame,作为查找动画数据的唯一索引。

GPU 计算(顶点着色器内):

  1. 寻址:根据 _CurrentFrame 和顶点的 boneIndices,算出骨骼矩阵在 _AnimTex 中的准确像素坐标。
  2. 采样:用 Texture.Load() 做无过滤的精确像素读取,从纹理重构出影响当前顶点的骨骼矩阵。之所以要用 Load 而非普通采样,是为了避免双线性过滤把相邻矩阵数据混在一起、破坏矩阵数值。
  3. 蒙皮:执行标准 LBS,将多个骨骼矩阵按 boneWeights 加权求和,得到最终变换矩阵。
  4. 变换:把该矩阵应用到顶点的原始(Bind Pose)位置和法线上,完成动画。