GPU 顶点动画(Vertex Animation Texture, VAT)
GPU 顶点动画(Vertex Animation Texture, VAT)烘焙的核心思想是: 在 CPU
端预先计算动画顶点数据,并存储到纹理(Texture2D)中,在 GPU 端通过 Shader 读取纹理来播放动画。
📌 1. 烘焙的核心步骤
- 采样动画帧(AnimationClip)
- 获取 SkinnedMeshRenderer 在每帧的顶点数据
- 存储到 Texture2D作为动画纹理
- 在 GPU 端使用 Shader 读取并应用动画
🎯 Step 1: 获取角色的 SkinnedMeshRenderer
角色网格 = (SkinnedMeshRenderer)EditorGUILayout.ObjectField("角色网格", 角色网格, typeof(SkinnedMeshRenderer), true);
这里 角色网格 是 SkinnedMeshRenderer,它是 Unity 蒙皮网格(Skinned Mesh)动画系统的核心组件。
为什么要用 SkinnedMeshRenderer?
Unity 角色动画一般都是骨骼动画,蒙皮网格的顶点由骨骼驱动,因此我们要从SkinnedMeshRenderer 获取最终变形后的顶点数据,而不能直接用 MeshFilter.mesh。
🎯 Step 2: 采样动画帧
在 角色动画烘焙类.cs 里,我们通过 AnimationMode.SampleAnimationClip 让角色处于特定动画帧。
AnimationMode.StartAnimationMode();
AnimationMode.BeginSampling();
AnimationMode.StartAnimationMode()
让 Unity 进入动画采样模式,这样可以在不播放动画的情况下手动设定帧数并获取角色姿态。
AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 归一化时间);
让角色静态停留在某个时间点的动画帧。
🎯 Step 3: 获取烘焙网格数据
角色网格.BakeMesh(烘焙网格);
Vector3[] 顶点数据 = 烘焙网格.vertices;
BakeMesh():
让 SkinnedMeshRenderer 计算当前帧的最终顶点位置,存入 Mesh 对象中。
这个方法会把蒙皮骨骼变换后的顶点复制出来,避免骨骼动画影响原始 MeshFilter.mesh。
烘焙网格.vertices:
获取角色在当前动画帧的所有变形后的顶点位置。
🎯 Step 4: 存储动画数据到 Texture2D
如果需要存储发现数据和切线数据,则可以另外存储
动画纹理.SetPixel(i, j, new Color(顶点.x, 顶点.y, 顶点.z, 1));
动画纹理.SetPixel(i, animLength + j + previewAnimationLength, normalsData); // 存储法线数据 (偏移 animLength 行)
// 处理切线数据 (直接存储)
Color tangentsData = bakeMeshTangents[j];
动画纹理.SetPixel(i, animLength * 2 + j + previewAnimationLength, tangentsData); // 存储切线数据 (偏移 2 * animLength 行)
🎯 Step 5: 处理所有帧
for (int i = 0; i < 总帧数; i++)
{float 归一化时间 = (float)i / (float)(总帧数 - 1) * 动画片段.length;AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 归一化时间);角色网格.BakeMesh(烘焙网格);Vector3[] 顶点数据 = 烘焙网格.vertices;for (int j = 0; j < 顶点数据.Length; j++){Vector3 顶点 = 顶点数据[j];动画纹理.SetPixel(i, j, new Color(顶点.x, 顶点.y, 顶点.z, 1));}
}
动画纹理.Apply();
遍历每一帧,采样动画数据
总帧数 由 动画片段长度 × 采样帧率 确定
归一化时间 归一化时间 = i / (总帧数 - 1) * 动画片段.length
每帧都 BakeMesh(),然后 SetPixel() 存入纹理
🎯 Step 6: 导出并保存 .png
byte[] 纹理数据 = 动画纹理.EncodeToPNG();
File.WriteAllBytes(完整路径, 纹理数据);
AssetDatabase.Refresh();
EncodeToPNG():把 Texture2D 转换为 PNG 格式
File.WriteAllBytes():将数据保存到硬盘
AssetDatabase.Refresh():通知 Unity 刷新资源库,🌟 最终结果
✅ 我们获得了一张 动画数据纹理,类似下面这样:
动画帧(宽度) → 顶点 1 顶点 2 顶点 3 …
帧 1 (x,y,z) (x,y,z) (x,y,z) …
帧 2 (x,y,z) (x,y,z) (x,y,z) …
… … … … …
🔹 横向 代表动画帧,🔹 纵向 代表模型的每个顶点。
在 GPU 端,我们可以 直接用 Shader 采样这个纹理,快速应用动画,而无需再让 CPU 计算骨骼动画!让 .png 文件显示在 Project 视图中
🎯 总结
步骤 | 代码操作 | 作用 |
---|---|---|
获取角色网格 | SkinnedMeshRenderer | 选择要烘焙的角色 |
启动动画模式 | AnimationMode.StartAnimationMode() | 让 Unity 进入动画编辑模式 |
逐帧采样动画 | AnimationMode.SampleAnimationClip() | 让角色停留在特定动画帧 |
获取顶点数据 | BakeMesh().vertices | 采集当前帧的蒙皮网格数据 |
存储到纹理 | SetPixel() | 将动画帧存入 Texture2D |
保存文件 | EncodeToPNG() | 导出动画数据纹理 |
✅ 烘焙后的动画纹理可以直接用于 GPU 渲染,实现海量动画角色的高效渲染!
附上完整代码
using UnityEngine;
using UnityEditor;
using System.IO;/// <summary>
/// GPU 顶点动画烘焙窗口
/// </summary>
public class 角色动画烘焙窗口类 : EditorWindow
{private SkinnedMeshRenderer 角色网格;private AnimationClip 动画片段;private int 采样帧数 = 30;private string 存储路径 = "Assets/GPUAnimations/";[MenuItem("工具/GPU 顶点动画烘焙")]public static void 打开窗口方法(){角色动画烘焙窗口类 窗口 = (角色动画烘焙窗口类)GetWindow(typeof(角色动画烘焙窗口类));窗口.titleContent = new GUIContent("GPU 顶点动画烘焙");窗口.Show();}private void OnGUI(){GUILayout.Label("GPU 顶点动画烘焙", EditorStyles.boldLabel);角色网格 = (SkinnedMeshRenderer)EditorGUILayout.ObjectField("角色网格", 角色网格, typeof(SkinnedMeshRenderer), true);动画片段 = (AnimationClip)EditorGUILayout.ObjectField("动画片段", 动画片段, typeof(AnimationClip), false);采样帧数 = EditorGUILayout.IntField("采样帧数", 采样帧数);存储路径 = EditorGUILayout.TextField("存储路径", 存储路径);if (GUILayout.Button("开始烘焙")){if (角色网格 == null || 动画片段 == null){EditorUtility.DisplayDialog("错误", "请指定角色网格和动画片段!", "确定");return;}开始烘焙方法();}}private void 开始烘焙方法(){角色动画烘焙类.烘焙动画方法(角色网格, 动画片段, 采样帧数, 存储路径);}
}
/// <summary>
/// 角色 GPU 顶点动画烘焙
/// </summary>
public class 角色动画烘焙类
{public static void 烘焙动画方法(SkinnedMeshRenderer 角色网格, AnimationClip 动画片段, int 采样帧数, string 存储路径){Mesh 烘焙网格 = new Mesh();int 总帧数 = 采样帧数;int 顶点数 = 角色网格.sharedMesh.vertexCount;string 文件名 = 角色网格.gameObject.name + "_" + 动画片段.name + ".png";string 完整路径 = Path.Combine(存储路径, 文件名);// 创建纹理Texture2D 动画纹理 = new Texture2D(总帧数, 顶点数, TextureFormat.RGBAFloat, false);Animator 角色动画器 = 角色网格.GetComponentInParent<Animator>();if (角色动画器 == null){Debug.LogError("角色缺少 Animator 组件!");return;}// 预览播放动画//让 Unity 进入动画采样模式,这样可以在不播放动画的情况下手动设定帧数并获取角色姿态。AnimationMode.StartAnimationMode();AnimationMode.BeginSampling();//让角色静态停留在某个时间点的动画帧。AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 0);for (int i = 0; i < 总帧数; i++){float 归一化时间 = (float)i / (float)(总帧数 - 1) * 动画片段.length;AnimationMode.SampleAnimationClip(角色网格.gameObject, 动画片段, 归一化时间);角色网格.BakeMesh(烘焙网格);Vector3[] 顶点数据 = 烘焙网格.vertices;Vector3[] 法线数据 = 烘焙网格.normals; // 获取法线Vector4[] 切线数据 = 烘焙网格.tangents; // 获取切线// 存储顶点位置、法线和切线for (int j = 0; j < 顶点数据.Length; j++){Vector3 顶点 = 顶点数据[j];Vector3 法线 = 法线数据[j];Vector4 切线 = 切线数据[j];// 使用 RGBA 通道存储数据// R: 顶点位置 X// G: 顶点位置 Y// B: 顶点位置 Z// A: 法线 X// 将法线的 Y、Z、W 存储到 RGB 通道中Color pixelData = new Color(顶点.x, 顶点.y, 顶点.z, 法线.x); // 顶点位置和法线X// 存储到纹理中动画纹理.SetPixel(i, j, pixelData);// 将法线Y、Z以及切线的X、Y、Z存储到同一纹理的其他通道pixelData = new Color(法线.y, 法线.z, 切线.x, 切线.y); // 法线 Y, Z 和 切线 X, Y动画纹理.SetPixel(i, j + 顶点数, pixelData);// 存储切线的 W 分量pixelData = new Color(切线.z, 切线.w, 0, 0);动画纹理.SetPixel(i, j + 2 * 顶点数, pixelData);}}动画纹理.Apply();// 保存到本地byte[] 纹理数据 = 动画纹理.EncodeToPNG();File.WriteAllBytes(完整路径, 纹理数据);AssetDatabase.Refresh();AnimationMode.EndSampling();AnimationMode.StopAnimationMode();Debug.Log($"GPU 顶点动画烘焙完成!文件保存至 {完整路径}");}
}
🚀 下一步:使用 Shader 读取纹理,在 GPU 端播放动画!