让我们的MD5模型动起来,以及CPU渲染那些事~

md5Anim文件中各字段,我的理解是
hierarchy 是层级关系
数据内容为: 骨骼名称,父级索引,flag, 索引起始位
其中flag是一个用来做二进制求与关系来决定当前的数据是 偏移Tx,Ty,TZ 还是 旋转Qx,Qy,Qz
索引起始位是 frame 动画帧字段中数据的起始索引

bounds 是包围盒
通过 min 和 max 可以决定当前动作的 AABB. 不做碰撞检测的话,一般用不到

baseframe 基础帧信息
实际上模型的各骨骼动画都是以此为起始值进行偏移的
6个值分别是 Tx,Ty,Tz,Qx,Qy,Qz

frame 具体的动画帧信息
数据内容为: 帧索引, numAnimatedComponents 定义的长度数量的 动画数据。 其中的数值具体有什么用,需要查询 hierarchy 中的 flag.

具体转换骨骼动画的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private function CalcMeshAnim(frameData : MD5FrameData) : void
        {
            //取出关节数据
            var joints : Vector.<MD5Joint> = md5MeshParser.md5_joint;
            var jointsNum : int = joints.length;
           
            cpuAnimMatrix ||= new Vector.<Matrix3D>(jointsNum * 4, true);
           
            var joint : MD5Joint;
            var parentJoint : MD5Joint;
            for(var i : int = 0; i < jointsNum; i++)
            {
                //从基本帧开始偏移
                var baseFrame : MD5BaseFrameData = md5AnimParser.baseFrameData[i];
                var animatedPos : Vector3D = baseFrame.position;
                var animatedOrient : Quaternion = baseFrame.orientation;
               
                //将帧数据替换掉基本帧中对应的数据
                var hierachy : MD5HierarchyData = md5AnimParser.hierarchy[i];
               
                var flags : int = hierachy.flags;
                var j : int = 0;
                if(flags & 1) //tx
                    animatedPos.x = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 2) //ty
                    animatedPos.y = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 4)
                    animatedPos.z = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 8)
                    animatedOrient.x = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 16)
                    animatedOrient.y = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 32)
                    animatedOrient.z = frameData.components[hierachy.startIndex + j++];
               
                //计算w
                var t : Number = 1 - animatedOrient.x * animatedOrient.x - animatedOrient.y * animatedOrient.y -
                    animatedOrient.z * animatedOrient.z;
                animatedOrient.w = t < 0 ? 0 : - Math.sqrt(t);
               
                var matrix3D : Matrix3D = animatedOrient.toMatrix3D();
                matrix3D.appendTranslation(animatedPos.x, animatedPos.y, animatedPos.z);
               
                //取出当前关节
                joint = joints[i];
               
                if(joint.parentIndex < 0){
                    joint.bindPose = matrix3D;
                }else{
                    //如果该关节有父级,需要先附带上父级的旋转和偏移
                    parentJoint = joints[joint.parentIndex];
                    matrix3D.append(parentJoint.bindPose);
                    joint.bindPose = matrix3D;
                }
               
                matrix3D = joint.inverseBindPose.clone();
                matrix3D.append(joint.bindPose);
               
                var vc : int = i * 4;
                if(useCPU){
                    cpuAnimMatrix[vc] = matrix3D;
                }else{
                    context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, vc, matrix3D, true);
                }
            }

最函数的最后有做一个判断 if(useCPU) cpuAnimMatrix[vc] = matrix3D;
实际上 AGAL 只有128个常量寄存器, 一个包含骨骼动画偏移及旋转的矩阵需要占用 4个寄存器。 这就是 var vc : int = i *4 这个算寄存器索引的原因。
实际上我们还需要上传一个投影矩阵给AGAL. 因此,实际上我们只能用 124 / 4 即 31个骨骼来表示我们的模型动画。 (这里不考虑优化)

那么一旦我们有超过31个骨骼该如何办呢,那只能用CPU来计算顶点蒙皮了。 而用CPU来计算蒙皮可能会导致性能问题,这也是部分Away3D游戏帧数不稳定的元凶之一。
蒙皮的原理就是
取出一个顶点,将该顶点根据包含旋转和平移的骨骼动画矩阵进行转换,最后遍历骨骼权重的数量,乘以对应的权重

假设骨骼权重是4个
那么AGAL为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
m44 vt1, va0, vc[va2.x];
mul vt1, vt1, va3.x;
mov vt2, vt1;

m44 vt1, va0, vc[va2.y];
mul vt1, vt1, va3.y;
add vt2, vt2, vt1;

m44 vt1, va0, vc[va2.z];
mul vt1, vt1, va3.z;
add vt2, vt2, vt1;

m44 vt1, va0, vc[va2.w];
mul vt1, vt1, va3.w;
add vt2, vt2, vt1;

m44 op, vt2, vc124
mov v0, va1

vc 是包含了所有骨骼动画矩阵信息的常量寄存器。 va2 是骨骼的索引值,通过这个索引值来取得对应的矩阵 m44 vt1, va0, vc[va2.x];
va3 是骨骼权重信息, 当前顶点被矩阵转换后,需要乘以骨骼权重。
vt2 是最终的顶点,它积累了一个顶点被多次处理后的最终值。

CPU处理骨骼动画的代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        private function cpuCalcJoint(meshIndex : int, view : Matrix3D) : void
        {
            var meshData : MeshData = md5MeshParser.md5_mesh[meshIndex];
            var vertexLen : int = meshData.md5_vertex.length;
           
            //当前索引
            var indices : Vector.<Number> = meshData.jointIndexRawData;
            //当前顶点
            var vertex : Vector.<Number> = meshData.vertexRawData;
            //当前权重
            var weight : Vector.<Number> = meshData.jointWeightRawData;
           
            var result : Vector3D = new Vector3D();
            var temp : Vector3D = new Vector3D();
            var curVert : Vector3D = new Vector3D();
           
            cpuAnimVertexRawData ||= new Vector.<Number>();
            cpuAnimVertexRawData.length = 0;
           
            var l : int = 0;
            for(var i : int = 0; i < vertexLen; i++)
            {
                var startIndex : int = i * 3;
                //初始化当前的顶点和索引
                curVert.setTo(vertex[startIndex],vertex[startIndex+1],vertex[startIndex+2]);
                result.setTo(0,0,0);
                for(var j : int = 0; j < md5MeshParser.maxJointCount; j++)
                {
                    //当前索引对应的矩阵
                    var curIndex : Number = indices[l];
                    var curWeight : Number = weight[l];
                    var curMatrix : Matrix3D = cpuAnimMatrix[curIndex];
                    temp = curMatrix.transformVector(curVert);
                    temp.scaleBy(curWeight);
                    result = result.add(temp);
                    l++;
                }

                cpuAnimVertexRawData.push(result.x,result.y,result.z);
            }
           
            cpuAnimVertexBuffer ||= context.createVertexBuffer(cpuAnimVertexRawData.length/3,3);
            cpuAnimVertexBuffer.uploadFromVector(cpuAnimVertexRawData,0,cpuAnimVertexRawData.length/3);
        }

在用GPU处理动画矩阵的时候
context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, vc, matrix3D, true);
我们设置最后一个参数为true, 表示该矩阵会被转置即行列对换。 在CPU处理时,我们需要考虑如下对应关系。
如果你使用 Matrix3D.transformVector(vertex) 这个API来转换你的顶点,实际上内部实现是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        //向量左乘矩阵
        public static function subjectMat(vec : Vector3D, mat : Matrix3D, out : Vector3D = null) : Vector3D
        {
            var data : Vector.<Number> = mat.rawData;
           
            out ||= new Vector3D();
           
            out.x = (vec.x * data[0] + vec.y * data[4] + vec.z * data[8] + vec.w * data[12]);
            out.y = (vec.x * data[1] + vec.y * data[5] + vec.z * data[9] + vec.w * data[13]);
            out.z = (vec.x * data[2] + vec.y * data[6] + vec.z * data[10] + vec.w * data[14]);
            out.w = (vec.x * data[3] + vec.y * data[7] + vec.z * data[11] + vec.w * data[15]);
           
            return out;
        }

他是将一个Vector3D 转换成 4行1列的矩阵进行左乘的。

而AGAL中的 m44 方法其内部实现是

1
2
3
4
5
6
7
8
9
10
11
12
13
        public static function subjectMat2(vec : Vector3D, mat : Matrix3D, out : Vector3D = null) : Vector3D
        {
            var data : Vector.<Number> = mat.rawData;
           
            out ||= new Vector3D();
           
            out.x = (vec.x * data[0] + vec.y * data[1] + vec.z * data[2] + vec.w * data[3]);
            out.y = (vec.x * data[4] + vec.y * data[5] + vec.z * data[6] + vec.w * data[7]);
            out.z = (vec.x * data[8] + vec.y * data[9] + vec.z * data[10] + vec.w * data[11]);
            out.w = (vec.x * data[12] + vec.y * data[13] + vec.z * data[14] + vec.w * data[15]);
           
            return out;
        }

他将Vector3D 转换成 1行4列的矩阵进行左乘的。

因此如果你用transformVector来转换你的顶点,那么就不需要转置矩阵。

最后是Demo:
http://www.dreamfairy.cn/blog/work/flash/3d/stage3dmd5/md5test.html

GitHub:
https://github.com/dreamfairy/3DDemo/blob/master/StencilShadow/src/MD5Test.as

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.