GPU ECS AnimationBaker 烘焙动画方案原理
目录
- 方案概述
- 烘焙阶段(离线)
- 骨骼矩阵纹理布局
- CPU 运行时工作
- 数据如何传给 GPU
- GPU 运行时工作
- 加权蒙皮数学原理
- 挂载道具(剑/枪)
- 项目实例:Zombunny
1. 方案概述
核心思想
将传统Animator + SkinnedMeshRenderer的骨骼动画离线烘焙为纹理数据,运行时:
- CPU:只计算当前该查纹理的哪一行、帧间插值多少、多路动画如何混合(约 64 字节/角色/帧)
- GPU:查纹理拿骨骼矩阵,在 Vertex Shader 里完成顶点蒙皮
三阶段分工
| 阶段 | 执行时机 | 做什么 | 数据量 |
|---|---|---|---|
| 烘焙 | Editor 一次性 | 采样动画 → 算蒙皮矩阵 → 写纹理 + UV 权重 | 大(MB 级,静态) |
| CPU | 每帧 | 推进时间 → 算frameIndex+ 混合参数 | 小(~64B/实例/帧) |
| GPU | 每顶点并行 | 查纹理 → LBS 蒙皮 → 输出变形顶点 | 并行 |
与本项目的关系
- Shader:
Assets/Res/Shader/EnemyGpu.shadergraph、PlayerGpu.shadergraph - 烘焙资源:
Assets/Res/Prefabs/EntityAnimG/BakedAssets_*Gpu/ - 运行时 System:
GpuEcsAnimatorSystem、GpuEcsAnimatedMeshSystem - ECS 控制:
GpuEcsAnimatorAspect.RunAnimation()
2. 烘焙阶段(离线)
2.1 烘焙产出物
| 产出 | Shader 属性 / 存储位置 | 说明 |
|---|---|---|
| 骨骼矩阵纹理 | _AnimatedBoneMatrices | 所有动画 × 所有帧 × 所有骨骼的 4×4 蒙皮矩阵 |
| 骨骼权重 | Mesh UV1 / UV2 / UV3 | 每顶点最多 6 根骨骼的(boneIndex, weight) |
| 动画元数据 | GpuEcsAnimatorBehaviour/ ECS Buffer | 每段动画的startFrameIndex、帧数、是否循环 |
| 挂点数据(可选) | GpuEcsAttachmentAnchorData | 挂点 Transform 每帧的 4×4 矩阵 |
| Material | 绑定矩阵纹理 + Shader | 运行时不变 |
关键代码入口:GpuEcsAnimationBakerServices.GenerateAnimationObjectFromModel
2.2 骨骼矩阵如何计算
不是从骨骼上直接读取现成矩阵,而是逐帧采样 Animator 后计算:
// GpuEcsAnimationBakeServices.cs - BakeAnimationMatricesTexturefor每一帧:Animator.Play(stateName,-1,progressRatio);Animator.Update(0);// 只更新骨骼 Transformfor每一根骨骼:matrix=inv(网格世界矩阵)× bone.localToWorldMatrix × bindpose[boneIndex]写入纹理(4像素/矩阵)公式含义:
matrix = inv(SkinnedMeshRenderer.transform) × boneTransform.localToWorldMatrix × bindpose与 Unity SkinnedMeshRenderer 标准 GPU 蒙皮矩阵一致:将顶点从绑定姿态(模型空间)变换到当前动画姿态(模型空间)。
2.3 采样参数
- 采样帧率:30 FPS(
GlobalConstants.SampleFrameRate) - 每段动画帧数:
(int)(clip.length × 30) + 1 - 支持SingleClip和DualClipBlend(Blend Tree 预采样)
2.4 烘焙时顶点是否参与
不参与。逐帧循环只更新骨骼并计算矩阵;顶点权重在独立步骤BakeBoneWeightsIntoMesh中只做一次静态写入 UV,与动画时间无关。
3. 骨骼矩阵纹理布局
3.1 尺寸公式
| 维度 | 公式 | 含义 |
|---|---|---|
| 宽度 | 骨骼数 × 4 | 每根骨骼占 4 列像素(4×4 矩阵的 4 列) |
| 高度 | totalNbrOfFrames | 所有动画帧数累加 |
| 格式 | RGBAFloat | 每像素 16 字节(float4) |
体积估算:宽 × 高 × 16字节
3.2 像素与矩阵
- 1 像素= 矩阵的 1 列(4 个 float)
- 4 像素= 完整 4×4 矩阵 =64 字节
- 存储方式:按列写入,
float4(m[0][i], m[1][i], m[2][i], m[3][i])
3.3 查表坐标
Shader 中Load坐标:
// GpuEcsAnimator.cginc animatedBoneMatrices.Load(int3((boneIndex * 4) + col, frameIndex, 0))- X=
boneIndex × 4 + col(col = 0, 1, 2, 3) - Y=
frameIndex
3.4 布局示意
[B0×4列] [B1×4列] [B2×4列] ... ← 宽 = 骨骼数 × 4 frame 0 │ M0 │ M1 │ M2 │ frame 1 │ M0 │ M1 │ M2 │ ... │ ... │ ... │ ... │ frame N │ M0 │ M1 │ M2 │ ← 高 = totalNbrOfFrames4. CPU 运行时工作
4.1 核心原则
CPU不算蒙皮、不传骨骼矩阵,只算查表参数。
4.2 数据流
游戏 System(RunAnimation) ↓ 写 animationID / speedFactor / blendFactor GpuEcsAnimatorSystem(Burst Job) ↓ 推进 normalizedTime,算 frameIndex、帧间插值、多路混合 ↓ 写入 GpuEcsAnimatorShaderDataComponent.shaderData GpuEcsAnimatedMeshSystem ↓ 拷贝到 GpuEcsMaterialAnimationState.Value Entities Graphics [MaterialProperty] ↓ 渲染时自动上传 GPU4.3 每帧传给 GPU 的实例数据
| Shader 属性 | ECS 组件 | 类型 | 含义 |
|---|---|---|---|
_AnimationState | GpuEcsMaterialAnimationState | float4x4 | 查哪一行、插多少、怎么混 |
_EnableAnimation | GpuEcsMaterialEnableAnimation | float | 0=跳过蒙皮,1=启用 |
_AnimationState每行(最多 4 行):
| 分量 | 含义 |
|---|---|
[0]blendFactor | 该路动画混合权重 |
[1]transitionToNextFrame | 当前帧 → 下一帧插值比例(0~1) |
[2]frameIndex | 纹理行索引(整数帧) |
[3] | 保留 |
正常播放用 1~2 行;动画切换过渡时 4 行全用(旧动画 + 新动画 crossfade)。
4.4 游戏逻辑如何驱动
// GpuEcsAnimatorAspectgpuEcsAnimatorAspect.RunAnimation((int)AnimationIdsEnemyGpu.Death);写入GpuEcsAnimatorControlComponent:animationID、speedFactor、blendFactor等。
5. 数据如何传给 GPU
5.1 不是什么
| 方式 | 本方案是否使用 |
|---|---|
| ComputeBuffer / StructuredBuffer | 否 |
| MaterialPropertyBlock | 否 |
| 每帧 SetTexture 传矩阵 | 否 |
本项目血条、阴影等系统才用ComputeBuffer;GPUECSAnimationBaker不用。
5.2 实际机制:ECS MaterialProperty + DOTS Instancing
[MaterialProperty("_AnimationState")]publicstructGpuEcsMaterialAnimationState:IComponentData{publicfloat4x4Value;}- 业务代码只写 ECS 组件:
gpuEcsMaterialAnimationState.Value = shaderData - UnityEntities Graphics在渲染时自动收集实例数据,写入GPU Per-Instance Buffer(DOTS Instancing)
- Material 需启用 Instancing:
m_EnableInstancingVariants: 1
5.3 两类数据的传法
| 数据 | 传法 | 更新频率 |
|---|---|---|
_AnimationState、_EnableAnimation | [MaterialProperty]→ Instance Buffer | 每帧 |
_AnimatedBoneMatrices纹理 | Material 绑定(烘焙时SetTexture) | 静态 |
| Mesh UV1/2/3 权重 | Mesh 顶点缓冲 | 静态 |
LocalTransform世界矩阵 | ECS 标准渲染 | 每帧 |
6. GPU 运行时工作
全部在Vertex Shader(GpuEcsAnimator.cginc+EnemyGpu.shadergraph子图)完成。
6.1 流程总览
读 _AnimationState → 解析 frameIndex ↓ 读 UV1/2/3 → 解析 (boneIndex, weight) ↓ Load 纹理 → 拼 4×4 矩阵 ↓ 矩阵变换 + 加权累加(LBS) ↓ 帧间插值 → 多路动画混合 ↓ 输出 Position / Normal / Tangent ↓ Branch(_EnableAnimation) ? 蒙皮结果 : 原始顶点 ↓ LocalToWorld → MVP → 光照片元着色6.2 读矩阵并拼接
float4 m0 = loadBoneMatrixTexture(..., frameIndex, boneIndex, 0); float4 m1 = loadBoneMatrixTexture(..., frameIndex, boneIndex, 1); float4 m2 = loadBoneMatrixTexture(..., frameIndex, boneIndex, 2); float4 m3 = loadBoneMatrixTexture(..., frameIndex, boneIndex, 3); float4x4 animatedBoneMatrix = float4x4(m0, m1, m2, m3);6.3 单帧多骨骼加权
对每个影响顶点骨骼(最多 6 根):
positionOut += boneWeight * mul(animatedBoneMatrix, float4(position, 1)).xyz; normalOut += boneWeight * mul(rotationMatrix, normal); // 只用旋转部分 tangentOut += boneWeight * mul(rotationMatrix, tangent);6.4 帧间插值
pos = (1 - t) * pos_frameN + t * pos_frameN+1t来自_AnimationState[i][1],frameIndex来自_AnimationState[i][2]。
6.5 多路混合
最多 4 路(动画过渡、DualClipBlend),每路乘blendFactor累加。
6.6 蒙皮之后
- 对象位移/旋转(角色在场景中移动)→ 常规 ECS
LocalTransform(LocalToWorld) - 骨骼变形(挥刀、跑步)→ GPU 蒙皮(模型空间)
- 两者分离:先模型空间蒙皮,再世界变换
7. 加权蒙皮数学原理
7.1 权重存储(烘焙进 UV)
| UV 通道 | 内容 |
|---|---|
| UV1 | (bone0索引, weight0, bone1索引, weight1) |
| UV2 | (bone2索引, weight2, bone3索引, weight3) |
| UV3 | (bone4索引, weight4, bone5索引, weight5) |
超出maxNumberOfBonesPerVertex的骨骼会被截断并重新归一化,保证 Σweight = 1。
7.2 Linear Blend Skinning(LBS)
单帧公式:
p' = Σ (w_i × M_i × p) n' = Σ (w_i × R(M_i) × n)M_i:完整 4×4 蒙皮矩阵(含位移)R(M_i):提取的纯旋转矩阵(去掉缩放,避免法线错误)
7.3 法线为何不用完整矩阵
蒙皮矩阵含非均匀缩放时,直接变换法线会出错。extractRotationMatrix对前三列归一化,只保留旋转。
8. 挂载道具(剑/枪)
烘焙方案没有运行时骨骼 Transform 层级,挂道具需额外处理。
8.1 方案一:绑骨骼蒙皮(GPU,推荐纯视觉道具)
原理:剑/枪作为 SkinnedMesh,顶点 100% 绑在手骨,与身体共用 Animator,GPU 蒙皮自动跟随。
本项目 Player 的 Gun 即如此:
PlayerGpu_GpuEcsAnimator ├── Player ← GpuEcsAnimatedMeshBehaviour └── Gun ← GpuEcsAnimatedMeshBehaviour(绑手骨)| 优点 | 缺点 |
|---|---|
| 实现简单,与身体完全一致 | 不易做独立实体(拾取、碰撞) |
| GPU 并行,无额外 CPU | 需重新烘焙 |
8.2 方案二:Attachment Anchor 挂点(CPU,推荐可交互道具)
原理:烘焙时额外记录手部挂点 Transform 每帧矩阵;运行时 CPU 插值后写入剑实体的LocalTransform;GPU当普通 Mesh 渲染(不做 Shader 蒙皮)。
烘焙:
// 配置 AttachmentAnchor,指向骨骼下 WeaponSocket 空节点anchorTransforms[frameIndex]=anchorTransform.localToWorldMatrix;运行时:
GpuEcsAnimatorSystem → 插值挂点矩阵 → GpuEcsCurrentAttachmentAnchorComponent GpuEcsAttachmentSystem → 剑.LocalTransform = 挂点矩阵 GPU → 常规 LocalToWorld × 顶点(整物体变换,非逐顶点蒙皮)| 优点 | 缺点 |
|---|---|
| 剑是独立 ECS 实体,可挂逻辑/碰撞 | 额外 CPU System |
| GPU 开销低(标准 MVP) | 需烘焙挂点数据 |
注意:方案二的 GPU不会在 Shader 里读挂点矩阵做逐顶点计算;挂点矩阵在 CPU 侧写入实体 Transform。
9. 项目实例:Zombunny
| 项 | 值 |
|---|---|
| 骨骼数 | 35 |
| 纹理尺寸 | 140 × 383(140 = 35×4) |
| 总体积 | ≈ 838 KB(140×383×16 字节) |
| 总帧数 | 383 |
动画分段(AnimationIdsEnemyGpu):
| 动画 | startFrame | 帧数 | 累计 | 循环 |
|---|---|---|---|---|
| Move | 0 | 38 | 38 | 是 |
| Idle | 38 | 301 | 339 | 否 |
| Death | 339 | 44 | 383 | 否 |
每帧矩阵数据:35 × 64 字节 ≈2.2 KB/帧(仅矩阵纹理部分)。
10. 关键点速查
烘焙贴图
- 烘焙的是蒙皮矩阵,不是顶点位置
- 矩阵 =
inv(网格矩阵) × 骨骼世界矩阵 × bindpose,计算得来非直接提取 - 逐帧采样只动骨骼,顶点不参与
- 宽 = 骨骼数×4,高 = 总帧数,每矩阵 4 像素(RGBAFloat)
CPU 工作
- 每帧只传
_AnimationState(float4x4)+_EnableAnimation - 传的是 frameIndex、插值 t、混合权重,不传骨骼矩阵
- 通过
[MaterialProperty]+ Entities Graphics,不是 ComputeBuffer
GPU 工作
- Load 纹理 → 拼矩阵 → LBS 加权 → 帧间 lerp → 多路 blend
- 法线用旋转矩阵,位置用完整矩阵
- 蒙皮在模型空间,世界位移走
LocalTransform
挂道具
- 绑骨骼:GPU 蒙皮,与身体同流程
- 挂点:CPU 写 Transform,GPU 普通 Mesh 渲染
优缺点
| 优点 | 缺点 |
|---|---|
| CPU 开销极低,适合大量同屏 | 内存随帧数线性增长 |
| DOTS Instancing 友好 | 动画需离线烘焙,不能运行时改 |
| 实现相对简单 | LBS 动画混合可能有体积塌陷 |
与 SkinnedMeshRenderer 对比
| SkinnedMeshRenderer | GPUECSAnimationBaker | |
|---|---|---|
| 骨骼矩阵 | CPU 每帧计算 | 烘焙进纹理 |
| CPU 每帧 | 更新所有骨骼 | 只传 ~64B 查表参数 |
| 蒙皮位置 | CPU 或 GPU(引擎内部) | GPU Vertex Shader 查纹理 |
| Animator | 运行时完整状态机 | 烘焙固定动画集 |
相关文件索引
| 类型 | 路径 |
|---|---|
| 烘焙逻辑 | Assets/Thirds/GPUECSAnimationBaker/Engine/Baker/GpuEcsAnimationBakeServices.cs |
| CPU 动画 System | Assets/Thirds/GPUECSAnimationBaker/Engine/AnimatorSystem/GpuEcsAnimatorSystem.cs |
| Material 同步 System | Assets/Thirds/GPUECSAnimationBaker/Engine/AnimatorSystem/GpuEcsAnimatedMeshSystem.cs |
| MaterialProperty 组件 | Assets/Thirds/GPUECSAnimationBaker/Engine/AnimatorSystem/GpuEcsAnimatedMeshComponents.cs |
| GPU 蒙皮 HLSL | Assets/Thirds/GPUECSAnimationBaker/Engine/Shader/GpuEcsAnimator.cginc |
| 挂点 System | Assets/Thirds/GPUECSAnimationBaker/Engine/AnimatorSystem/GpuEcsAttachmentSystem.cs |
| 敌人 Shader | Assets/Res/Shader/EnemyGpu.shadergraph |
| 烘焙资源示例 | Assets/Res/Prefabs/EntityAnimG/BakedAssets_ZombunnyGpu/ |
