5.如何获取某个动画中所有帧的顶点信息
5.1 知识点
为何要获取某个动画中所有帧的顶点信息
- 最终目的是生成
VAT(顶点动画纹理)。 - 其中存储的数据就是动画每一帧顶点数据的集合。
- 上一节已经能拿到目标对象使用的全部动画切片信息。
- 接下来只要知道如何取出其中每一帧的顶点数据,核心问题就解决了。
获取某个动画中所有帧的顶点信息
主要目的
- 对给定
AnimationClip,按时间采样,把某一时刻蒙皮后的顶点拉出来,后面才能写进纹理或做校验。
主要思路
AnimationClip.SampleAnimation(GameObject go, float time):把time秒处的姿势套到go子层级上。前提是骨骼层级和SkinnedMeshRenderer跟这份Clip对得上。SkinnedMeshRenderer.BakeMesh(Mesh mesh):把当前帧算好的蒙皮网格写进一个普通Mesh,之后读vertices/normals/tangents就是静态几何那一套 API。
主要步骤
- 拿到目标
AnimationClip(与第 4 篇一样:从AnimatorController里枚举状态,把motion转成AnimationClip)。 - 帧索引
j对应时间j / clip.frameRate,调用SampleAnimation。 - 调
BakeMesh,从mesh里取顶点数据。
下面这一段把第 4 篇「取 childAnimatorStateArray」的代码放在最前,再接到采样与烘焙;单独复制这一节也能在场景里跑通(仍需 targetGameObject 上有 Animator 和蒙皮网格)。
using UnityEngine;
using UnityEditor.Animations;
// —— 第 4 篇:Animator → AnimatorController → 状态数组 ——
Animator animator = targetGameObject.GetComponent<Animator>();
if (animator == null)
{
return;
}
AnimatorController animatorController = animator.runtimeAnimatorController as AnimatorController;
if (animatorController == null)
{
return;
}
AnimatorStateMachine animatorStateMachine = animatorController.layers[0].stateMachine;
ChildAnimatorState[] childAnimatorStateArray = animatorStateMachine.states;
//之所以绕一圈去获取动画切片 是为了之后做铺垫
//我们可以结合Animator,蒙皮网格渲染器,利用他们一起去得到某一个动画某一帧对应的顶点数据
// 拿到SkinnedMeshRenderer和创建Mesh
// —— 第 5 篇:蒙皮网格 + 用于承接 Bake 结果的 Mesh ——
SkinnedMeshRenderer skinnedMeshRenderer = targetGameObject.GetComponentInChildren<SkinnedMeshRenderer>();
Mesh mesh = new Mesh();
for (int i = 0; i < childAnimatorStateArray.Length; i++)
{
AnimatorState animatorState = childAnimatorStateArray[i].state;
AnimationClip animationClip = animatorState.motion as AnimationClip;
if (animationClip == null)
{
continue;
}
// animationClip.frameRate 帧率
// animationClip.length 时长
int frameCount = Mathf.CeilToInt(animationClip.frameRate * animationClip.length); //得到这个clip多少帧
//需要把每一帧对应的时间点传递进去 => 帧数转时间 帧数/帧率
for (int j = 0; j < frameCount; j++)
{
//一帧一帧的得到对应的顶点数据 存储到VAT中
//让对象定格到了某一帧
animationClip.SampleAnimation(targetGameObject, j / animationClip.frameRate);
//得到当前帧的顶点信息
//每次烘焙时把上一次的数据清理了
mesh.Clear(false);
//得到了此时此刻的网格相关的数据
skinnedMeshRenderer.BakeMesh(mesh);
//得到了此时此刻网格对应的所有顶点位置
// 获取网格的顶点数组:每个顶点存储了三维空间中的坐标(x,y,z)
Vector3[] vertices = mesh.vertices;
// 获取网格的法线数组:每个顶点的法线方向,用于计算光照效果
Vector3[] normals = mesh.normals;
// 获取网格的切线数组:用于法线贴图(凹凸贴图)的计算,是四维向量(x,y,z,w),w 用于控制切线方向
Vector4[] tangents = mesh.tangents;
}
}
vertices 等数组接下来按你的 VAT 布局写进贴图或 ComputeBuffer 即可。角色若有多块蒙皮网格,就每个 SkinnedMeshRenderer 各 BakeMesh 一次,再在纹理里分区域或拆多张图存,逻辑不变。
5.2 知识点代码
BakeAnimationFramesToMeshExample.cs
using UnityEngine;
using UnityEditor.Animations;
// 将本脚本放在项目的 Editor 目录下,并确保程序集引用了 UnityEditor
public static class BakeAnimationFramesToMeshExample
{
public static void ExampleBakeAllFrames(GameObject targetGameObject)
{
if (targetGameObject == null)
{
return;
}
//因为我们的最终目的是生成VAT(顶点动画纹理)
//其中存储的数据就是动画每帧顶点数据的集合
//我们上节课已经可以获取到目标对象使用的所有动画切片信息了
//现在我们只需要知道如何获取其中每一帧的顶点数据便可解决核心问题
//主要目的:
//能够准确得到动画切片中某一帧的模型顶点位置信息
//主要思路:
//1.利用AnimationClip中的SampleAnimation(GameObject go, float time)采样动画的方法
// 该API可以将动画切片某一时刻的动画信息绑定到蒙皮网格渲染中
// 调用了该方法,只要该对象上的蒙皮网格渲染器和该动画匹配即可生效
// 使用限制:对象或对象的子对象上要有蒙皮网格渲染器,并且关联了和动画关联的骨骼和网格信息
//2.利用SkinnedMeshRenderer中的BakeMesh(Mesh mesh)烘焙网格的方法
// 该API可以将蒙皮网格渲染器内当前时刻的网格信息存储烘焙下来
//主要步骤:
//1.得到目标动画切片AnimationClip信息
//2.利用AnimationClip中的SampleAnimation(GameObject go, float time)方法
// 让对象定格在某一帧
//3.利用SkinnedMeshRenderer中的BakeMesh(Mesh mesh)方法
// 获取到定格帧的所有顶点位置信息
// —— 第 4 篇:Animator → AnimatorController → 状态数组 ——
Animator animator = targetGameObject.GetComponent<Animator>();
if (animator == null)
{
return;
}
AnimatorController animatorController = animator.runtimeAnimatorController as AnimatorController;
if (animatorController == null)
{
return;
}
AnimatorStateMachine animatorStateMachine = animatorController.layers[0].stateMachine;
ChildAnimatorState[] childAnimatorStateArray = animatorStateMachine.states;
//之所以绕一圈去获取动画切片 是为了之后做铺垫
//我们可以结合Animator,蒙皮网格渲染器,利用他们一起去得到某一个动画某一帧对应的顶点数据
// 拿到SkinnedMeshRenderer和创建Mesh
// —— 第 5 篇:蒙皮网格 + Bake 用的 Mesh ——
SkinnedMeshRenderer skinnedMeshRenderer = targetGameObject.GetComponentInChildren<SkinnedMeshRenderer>();
if (skinnedMeshRenderer == null)
{
return;
}
Mesh mesh = new Mesh();
for (int i = 0; i < childAnimatorStateArray.Length; i++)
{
AnimatorState animatorState = childAnimatorStateArray[i].state;
AnimationClip animationClip = animatorState.motion as AnimationClip;
if (animationClip == null)
{
continue;
}
// animationClip.frameRate 帧率
// animationClip.length 时长
int frameCount = Mathf.CeilToInt(animationClip.frameRate * animationClip.length); //得到这个clip多少帧
//需要把每一帧对应的时间点传递进去 => 帧数转时间 帧数/帧率
for (int j = 0; j < frameCount; j++)
{
//一帧一帧的得到对应的顶点数据 存储到VAT中
//让对象定格到了某一帧
animationClip.SampleAnimation(targetGameObject, j / animationClip.frameRate);
//得到当前帧的顶点信息
//每次烘焙时把上一次的数据清理了
mesh.Clear(false);
//得到了此时此刻的网格相关的数据
skinnedMeshRenderer.BakeMesh(mesh);
//得到了此时此刻网格对应的所有顶点位置
// 获取网格的顶点数组:每个顶点存储了三维空间中的坐标(x,y,z)
Vector3[] vertices = mesh.vertices;
// 获取网格的法线数组:每个顶点的法线方向,用于计算光照效果
Vector3[] normals = mesh.normals;
// 获取网格的切线数组:用于法线贴图(凹凸贴图)的计算,是四维向量(x,y,z,w),w 用于控制切线方向
Vector4[] tangents = mesh.tangents;
}
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com