Unity 使用 bvh 驱动骨骼动作

欢迎点击链接跳转 Github Page 博客,享受更舒适的排版界面。

本篇主要介绍如何在 Unity 中使用 bvh 驱动骨骼动画,同时从理论与实操两个方面进行阐述。

代码已打包成 unitypackage,见链接 BVHParser

前言

先简单介绍一些相关的理论基础。

BVH

BVH 文件是使用设备对人体运动进行捕获后产生的文件,它包含角色的骨骼和肢体关节旋转数据,是一种通用的人体特征动画文件格式。

BVH

上图是 bvh 最常记录的骨骼点,图用节点表示关节,连线表示躯干,身体的各个部分形成子树的形式。

BVH 文件的第一部分定义了关节树、每个关节点的名称、关节与关节之间的相对位置(偏移量),即基本骨架。Hips 关节点作为整个人体的根节点,拥有三维空间位置参数,从而完成了对人体运动情况的完整描述。

BVH 文件的第二部分记录了运动的数据,定义了动作数据持续的长度(帧数)以及每帧之间的时间间隔。且按照第一部分定义的关节顺序提供每帧数据,记录了每一帧中各个关节点的位置信息和旋转信息(局部旋转量)。

BVH 文件示例见 Example.bvh

角色姿势

一般来说,角色模型或 BVH 都有它的内置姿势,即创建模型时所设定的姿势。将一个模型所有关节的局部旋转量设置为单位四元数,则可显示出其内置姿势。

在对角色模型或 BVH 的处理中,通常涉及到三种姿势:A 型姿势(A-Pose)、T 型姿势(T-Pose)和其它姿势,其中 A-Pose 和 T-Pose 通常作为内置姿势或第一帧骨骼姿势。

驱动理论

了解了一些基础理论,接下来就来介绍一下 bvh 驱动的基本原理。

由于 bvh 的骨架与 unity 所使用和展示的骨架差异较大,因此仅赋值是无法实现需求的。但不管是 bvh 还是 unity 骨架,大多数都是基于 Tpose 的,且不同骨架的 Tpose 姿势一致,因此我们利用 Tpose 作为媒介,将 bvh 中的所有动画帧迁移至 unity 中。

接下来的推证,前提是 bvh 的第一帧是 Tpose

转换流程

CSDN图床炸了

理解一下这个图。起初我们所拿到的数据是,Unity 模型的骨骼点信息和 BVH 每一帧动画关节点的局部旋转量;要实现的是,求出图中矩阵 T 5 T_{5} T5,应用到 Unity 每个关节的旋转量上,使 Unity 模型展示出相应动作。

事实上,将该图命名为流程图不太合适,可以理解成 T 5 T_{5} T5求解过程吧。

先看 BVH。将 BVH 某一帧的所有关节点旋转量乘上矩阵 T 2 T_{2} T2,变换成 Tpose,再乘上 T 4 T_{4} T4,变化为相应动画。之所以这样做,是为了拿出 Tpose 这个中间媒介。Unity 和 BVH 骨架的 Tpose 姿势一致,想要展示的动作也一致,因此 T 4 T_{4} T4 作用于 Unity 的 Tpose上,可以在 Unity 模型上展示出动作。

这样的话,求解 T 5 T_{5} T5 仅需再求出 T 1 T_{1} T1 即可实现,我们下面具体介绍一下求解原理。

矩阵求解

变换矩阵 T 1 T_{1} T1/ T 2 T_{2} T2

通常而言,CMU 等提供的 BVH 动画,第一帧通常就是 Tpose,Unity 导入模型后通常也是 T 型姿势,因此无需复杂的转换。

但需要注意,我们所获得的数据信息,是骨骼节点相对于父节点的局部旋转量,需要使用下面公式将其转换为全局旋转矩阵 (Rotation)。

R i = R p × r i R_{i}=R_{p}\times r_{i} Ri=Rp×ri

其中, R i R_{i} Ri 为所求的当前节点的全局旋转(四元数), R p R_{p} Rp 为父节点的全局旋转, r i r_{i} ri 为当前节点的局部旋转。

使用上式将 Unity 模型和 BVH 第一帧的局部旋转转化为全局旋转,则得到了矩阵 T 1 T_{1} T1 T 2 T_{2} T2

变换矩阵 T 3 T_{3} T3

BVH 中记录了完整的动画数据,我们可以根据它们计算出每一帧所有关节的坐标位置 (Position)。

在 BVH 中,根节点比其它节点多了个位置信息,根据根节点的位置信息 Root Position、关节层次关系 Hierachy、各节点相对父节点的偏移量 Offset (BVH 初始姿势)和每一帧节点旋转量,就可以推算出所有关节的坐标位置:

P o s i = P o s p + R p × O f f s e t i Pos_{i} = Pos_{p} + R_{p} \times Offset_{i} Posi=Posp+Rp×Offseti

其中, P o s i Pos_{i} Posi 为当前关节点坐标, P o s p Pos_{p} Posp 为父节点坐标, R p R_{p} Rp 为父节点全局旋转, O f f s e t i Offset_{i} Offseti 为当前节点相对父节点的偏移量 (Vector3)。

变换矩阵 T 4 T_{4} T4

前面我们说过, T 4 T_{4} T4 是迁移的关键矩阵,计算出它,我们就能计算出 T 5 T_{5} T5

观察流程图,我们可以推出:

T 2 × T 4 = T 3 T_{2}\times T_{4} = T_{3} T2×T4=T3

T 4 = T 2 − 1 × T 3 T_{4} = T_{2}^{-1} \times T_{3} T4=T21×T3

变换矩阵 T 5 T_{5} T5

Tpose 姿势一致,动画效果一致,因此 T 4 T_{4} T4 可作用于 Unity 的 Tpose 上。故我们可求出 T 5 T_{5} T5

T 5 = T 1 × T 4 = T 1 × T 2 − 1 × T 3 T_{5} = T_{1} \times T_{4} = T_{1} \times T_{2}^{-1} \times T_{3} T5=T1×T4=T1×T21×T3

位置调整

上述矩阵都是作用于各关节点的旋转量上的,但动画除此之外还有根节点的位置,通过调整它来调整人物的位置。

因此 BVH 的人物大小和 Unity 模型大小不同,所以我们通常根据某根骨骼的长度计算缩放比例,然后对 BVH 的根节点位置乘以缩放比例,就得到 Unity 根节点的位置了。

P o s r ( u n i t y ) = P o s r ( b v h ) × S c a l e Pos_{r}^{(unity)} = Pos_{r}^{(bvh)} \times Scale Posr(unity)=Posr(bvh)×Scale

代码实现

前面讲解了 BVH 驱动相关原理,接下来大致讲述一下核心代码实现。项目代码见 BVHParser

核心代码

获取关节父子关系

public Dictionary<string,string> getHierachy()
{
    Dictionary<string, string> hierachy = new Dictionary<string, string>();
    foreach (BVHBone bb in boneList)
    {
        foreach (BVHBone bbc in bb.children)
        {
            hierachy.Add(bbc.name, bb.name);
        }
    }
    return hierachy;
}

欧拉角转四元数

注意所用的 bvh 数据是否是 ZYX 顺序,若不是,需要根据 bvh 的顺序修改函数参数顺序。

private Quaternion eul2quat(float z, float y, float x)
{
    z = z * Mathf.Deg2Rad;
    y = y * Mathf.Deg2Rad;
    x = x * Mathf.Deg2Rad;

    // 动捕数据是ZYX,但是unity是ZXY
    float[] c = new float[3];
    float[] s = new float[3];
    c[0] = Mathf.Cos(x / 2.0f); c[1] = Mathf.Cos(y / 2.0f); c[2] = Mathf.Cos(z / 2.0f);
    s[0] = Mathf.Sin(x / 2.0f); s[1] = Mathf.Sin(y / 2.0f); s[2] = Mathf.Sin(z / 2.0f);

    return new Quaternion(
        c[0] * c[1] * s[2] - s[0] * s[1] * c[2],
        c[0] * s[1] * c[2] + s[0] * c[1] * s[2],
        s[0] * c[1] * c[2] - c[0] * s[1] * s[2],
        c[0] * c[1] * c[2] + s[0] * s[1] * s[2]
    );
}

获取关键帧的全局旋转数据

public Dictionary<string,Quaternion> getKeyFrame(int frameIdx)
{
    Dictionary<string, string> hierachy = getHierachy();
    Dictionary<string, Quaternion> boneData = new Dictionary<string, Quaternion>();
    boneData.Add("pos", new Quaternion(
        boneList[0].channels[0].values[frameIdx],
        boneList[0].channels[1].values[frameIdx],
        boneList[0].channels[2].values[frameIdx],0));

    boneData.Add(boneList[0].name, eul2quat(
        boneList[0].channels[3].values[frameIdx],
        boneList[0].channels[4].values[frameIdx],
        boneList[0].channels[5].values[frameIdx]));
    foreach (BVHBone bb in boneList)
    {
        if (bb.name != boneList[0].name)
        {
            Quaternion localrot = eul2quat(bb.channels[3].values[frameIdx],
                                           bb.channels[4].values[frameIdx],
                                           bb.channels[5].values[frameIdx]);
            boneData.Add(bb.name, boneData[hierachy[bb.name]] * localrot);
        }                
    }            
    return boneData;
}

获取 BVH 初始姿势每个关节相对于父关节的偏移量

public Dictionary<string,Vector3> getOffset(float ratio) {
    Dictionary<string, Vector3> offset = new Dictionary<string, Vector3>();
    foreach(BVHBone bb in boneList)
    {
        offset.Add(bb.name, new Vector3(bb.offsetX * ratio, bb.offsetY * ratio, bb.offsetZ * ratio));
    }
    return offset;
}

获取 BVH 的全局旋转,即 T 2 T_{2} T2

bvhT = bp.getKeyFrame(0);

计算 T 5 T_{5} T5

foreach (BoneMap bm in bonemaps)
{
    Transform currBone = anim.GetBoneTransform(bm.humanoid_bone);
    currBone.rotation = (currFrame[bm.bvh_name] * Quaternion.Inverse(bvhT[bm.bvh_name])) * unityT[bm.humanoid_bone];
}

注意事项

  • 确保 bvh 动捕数据第一帧为 Tpose
  • 运行时 Scene 界面用红线画出了 bvh 动作骨架,可用以鉴别 bvh 动作是否导入成功
  • 若使用的 bvh 文件的旋转量不是 ZYX 顺序,请相应修改 BVHParser.cs 中的 eul2quat 函数,一般只需调换该函数参数顺序即可

总结

本文先简要介绍了几个基础知识 BVH 和角色姿势,后阐述了 BVH 驱动动作生成的理论(以 Tpose 为中间媒介求解转换矩阵),并使用代码实现。

项目地址见 BVHParser

参考

[1] Unity 中 BVH 骨骼动画驱动的可视化理论与实现 - CSDN

[2] BVH - 百度百科

猜你喜欢

转载自blog.csdn.net/weixin_45497461/article/details/119702716