骨骼动画原理

225 阅读4分钟

3D变换

普通的

1.local坐标转世界坐标

Debug.Log(transform.position);
Debug.Log(transform.parent.TransformPoint(transform.localPosition));
Debug.Log(transform.parent.localToWorldMatrix.MultiplyPoint(transform.localPosition));

运行结果

image.png

World转local

Debug.Log(transform.localPosition);
Debug.Log(transform.parent.InverseTransformPoint(transform.position));
Debug.Log(transform.parent.worldToLocalMatrix.MultiplyPoint(transform.position));

运行结果如下

image.png

多层父子关系

层级结构

image.png

Debug.Log(t4.position);
var m1 = Matrix4x4.TRS(t1.localPosition, t1.localRotation, t1.localScale);
var m2 = Matrix4x4.TRS(t2.localPosition, t2.localRotation, t2.localScale);
var m3 = Matrix4x4.TRS(t3.localPosition, t3.localRotation, t3.localScale);
Debug.Log(t1.localToWorldMatrix.MultiplyPoint(m2.MultiplyPoint(m3.MultiplyPoint(t4.localPosition))));
Debug.Log((t1.localToWorldMatrix * m2 * m3).MultiplyPoint(t4.localPosition));
Debug.Log((m1 * m2 * m3).MultiplyPoint(t4.localPosition));

输出结果

image.png

var resultPosint = m2.MultiplyPoint(m3.MultiplyPoint(t4.localPosition))
//这行代码对t4的坐标进行变换,变换后,相当于t4直接成为了t1的一个直接子节点,resultPosint也就是t4再t1下的局部坐标。
//后续t1.localToWorldMatrix.MultiplyPoint(resultPosint)把子节点坐标转为世界坐标

骨骼动画

骨骼动画基本原理:蒙皮mesh受到一个或多个骨骼点影响(最多四个,每个骨骼点有自己的影响权重,四个权重相加为1),美术制作动画片段时,只需要制作骨骼点的关键帧,导入unity后,unity会对骨骼点的信息进行插值,从而改变骨骼点的位置,旋转,缩放信息;而mesh顶点又可以通过受到的骨骼点的信息和权重,计算出,这一帧中,顶点的信息,从而实现动画效果。

但是mesh是如何受到骨骼点的影响呢?比如骨骼点移动旋转的时候,mesh为什么会形变呢?二者看起来毫无联系

image.png

如果不考虑骨骼动画,单纯让一个物体移动影响另一个物体的移动,最简单的办法就是把B物体设为A物体的子节点,移动A物体,B物体也会相应移动。

骨骼动画原理类似,在T——Pos或者A——pos的时候,可以记录某个矩阵信息,用于将mesh顶点转化到骨骼空间(通俗的理解,也就是让mesh顶点作为骨骼点的子物体),但是如何求得这个矩阵呢?

首先mesh和骨骼点的世界空间是相同的(任何物体共用一个世界空间),所以可以将mesh的顶点先转化到世界空间,在将世界空间的顶点,转化到骨骼空间。

bindPoses[i] = bones[i].worldToLocalMatrix * transform.localToWorldMatrix;
bonePos = bindPos * meshPoint

所以unity里SkinMeshRender的BindPos主要是用来将顶点从物体空间转化到骨骼空间,转化完以后,模型相当于骨骼点的一个子物体,顶点在骨骼下的相对坐标保持不变

image.png

通过上面讲的多层父子局部转世界坐标,可以轻松算出顶点的位置。

最终代码

bindPoses[i] = bones[i].worldToLocalMatrix * transform.localToWorldMatrix;
var m1 = Matrix4x4.TRS(t1.localPosition, t1.localRotation, t1.localScale);
var m2 = Matrix4x4.TRS(t2.localPosition, t2.localRotation, t2.localScale);
var m3 = Matrix4x4.TRS(t3.localPosition, t3.localRotation, t3.localScale);
var newVertex = skinRender.transform.worldToLocalMatrix * m1 * m2 * m3 * bindPoses[i] * vertex

//m1 * m2 * m3 * bindPoses[i] * vertex 已经转化为世界坐标,
//m1 * m2 * m3 = localToWorldMatrix
//最后再通过skinRender.worldToLocalMatrix

美术制作的AnimationClip的关键帧,改变的是骨骼点的位置旋转信息,骨骼点信息改变后,相应的上面的 m1 * m2 * m3矩阵也会变化,所以最终顶点会发生变化

受到多个影响骨骼影响的话,LBS 算法,当然还有其他算法

var newVertex = skinRender.transform.worldToLocalMatrix * (bone1ResultMat * wight1 * vertex1 + bone2ResultMat * wight2 * vertex1 ....)

最终案例

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkinRenderer : MonoBehaviour
{
    public SkinnedMeshRenderer renderer;
    public Animation animation;
    Mesh newMesh;
    Material mat;
    Transform[] bones;
    Transform rootBones;
    Vector3[] vertices;
    BoneWeight[] boneWeights;
    Vector3[] newVertices;
    Matrix4x4[] bindPose;
    Matrix4x4[] skinMartixs;
    public SkinQuality skinQuailty = SkinQuality.Bone1;
    void Start()
    {
        Mesh mesh = renderer.sharedMesh;
        newMesh = Object.Instantiate(mesh);
        newMesh.MarkDynamic();
        newMesh.name = "SkinnedMesh";
        vertices = mesh.vertices;
        bindPose = mesh.bindposes;
        newVertices = new Vector3[vertices.Length];
        boneWeights = mesh.boneWeights;
        mat = renderer.sharedMaterial;
        bones = renderer.bones;
        skinMartixs = new Matrix4x4[bones.Length];
        
        ////////=========================================================///**重要**
        rootBones = renderer. transform;

        // var m1 = renderer. transform.worldToLocalMatrix;
        // var m2 = renderer. rootBone.worldToLocalMatrix;
        renderer.gameObject.SetActive(false);
        //animation的cullingType默认是renderer.isVisible = true才计算,但是原始renderer已经被我们隐藏了,所以暂时改成AlwaysAnimate
        animation.cullingType = AnimationCullingType.AlwaysAnimate;
    }

    void Update()
    {
        int vCount = vertices.Length;
        Matrix4x4 worldToRoot = rootBones.worldToLocalMatrix;
        //计算蒙皮矩阵
        for (int i = 0; i < bones.Length; i++) 
        {
            skinMartixs[i] = GetSkinMatrix(ref worldToRoot, bones, i);
        }
        
        //进行蒙皮
        Matrix4x4 blendSkin = Matrix4x4.identity;
        for (int i = 0; i < vCount; i++)
        {
            if (skinQuailty == SkinQuality.Bone1)
            {
                int boneIndex = boneWeights[i].boneIndex0;
                blendSkin = skinMartixs[boneIndex];
            }
            else if (skinQuailty == SkinQuality.Bone2) 
            {
                ref BoneWeight bw = ref boneWeights[i];
                //2根骨骼权重混合
                blendSkin = MatrixWeight(skinMartixs[bw.boneIndex0], bw.weight0);
                blendSkin = MatrixAdd(blendSkin, MatrixWeight(skinMartixs[bw.boneIndex1], bw.weight1));
            }
            else if (skinQuailty == SkinQuality.Bone4)
            {
                ref BoneWeight bw = ref boneWeights[i];
                //4根骨骼权重混合
                blendSkin = MatrixWeight(skinMartixs[bw.boneIndex0], bw.weight0);
                blendSkin = MatrixAdd(blendSkin, MatrixWeight(skinMartixs[bw.boneIndex1], bw.weight1));
                blendSkin = MatrixAdd(blendSkin, MatrixWeight(skinMartixs[bw.boneIndex2], bw.weight2));
                blendSkin = MatrixAdd(blendSkin, MatrixWeight(skinMartixs[bw.boneIndex3], bw.weight3));
            }
            newVertices[i] = blendSkin.MultiplyPoint3x4(vertices[i]);
        }
        newMesh.vertices = newVertices;
        newMesh.UploadMeshData(false);
        //使用root矩阵绘制
        Graphics.DrawMesh(newMesh, rootBones.localToWorldMatrix, mat, 0);
    }
   
    Matrix4x4 GetSkinMatrix( ref Matrix4x4 worldToRoot, Transform[] bones, int index) 
    {
        //因为骨骼的parent相同时会有重复的计算,不过为了思路清晰,暂时不作优化
        Transform bone = bones[index];
        //模型空间 ->(乘bindPose) T/A Pose骨骼空间  ->(乘骨骼的LocalToWorld) 动画计算后的世界空间 ->(乘Root的WorldToLocal) root空间
        return worldToRoot * GetLocalToWorld(bone) * bindPose[index];
       // return worldToRoot * bone.localToWorldMatrix * bindPose[index];
    }

    //矩阵乘float
    Matrix4x4 MatrixWeight(Matrix4x4 matrix4X4,float weight) 
    {
        matrix4X4.m00 *= weight;
        matrix4X4.m01 *= weight;
        matrix4X4.m02 *= weight;
        matrix4X4.m03 *= weight;
        matrix4X4.m10 *= weight;
        matrix4X4.m11 *= weight;
        matrix4X4.m12 *= weight;
        matrix4X4.m13 *= weight;
        matrix4X4.m20 *= weight;
        matrix4X4.m21 *= weight;
        matrix4X4.m22 *= weight;
        matrix4X4.m23 *= weight;
        matrix4X4.m30 *= weight;
        matrix4X4.m31 *= weight;
        matrix4X4.m32 *= weight;
        matrix4X4.m33 *= weight;
        return matrix4X4;
    }
    //矩阵相加
    Matrix4x4 MatrixAdd(Matrix4x4 a, Matrix4x4 b)
    {
        a.m00 += b.m00;
        a.m01 += b.m01;
        a.m02 += b.m02;
        a.m03 += b.m03;
        a.m10 += b.m10;
        a.m11 += b.m11;
        a.m12 += b.m12;
        a.m13 += b.m13;
        a.m20 += b.m20;
        a.m21 += b.m21;
        a.m22 += b.m22;
        a.m23 += b.m23;
        a.m30 += b.m30;
        a.m31 += b.m31;
        a.m32 += b.m32;
        a.m33 += b.m33;
        return a;
    }


    Stack<Transform> tfParents = new Stack<Transform>();
    Matrix4x4 GetLocalToWorld(Transform tf) 
    {
        if (tf != null)
        {
            tfParents.Push(tf);
            Transform tempTf = tf;
            //收集父节点
            while (tempTf.parent != null)
            {
                tempTf = tempTf.parent;
                tfParents.Push(tempTf);
            }
        }
        Matrix4x4 result = Matrix4x4.identity;
        while (tfParents.Count > 0) 
        {
            //从最顶层往下算
            Transform child = tfParents.Pop();
            result *= Matrix4x4.TRS(child.localPosition, child.localRotation, child.localScale);
        }
        return result;
    }
}

参考