3D骨骼动画是实现较为复杂3D场景的重要技术,Babylon.js引擎内置了对骨骼动画的支持,但Babylon.js使用的骨骼动画的模型多是从3DsMax、Blender等3D建模工具转换而来,骨骼动画的具体生成方式被透明化。本文从babylon格式的3D模型文件入手,对骨骼动画数据的生成方式进行具体分析,并尝试建立一个简易的3D骨骼动画生成工具。
一、模型文件分析
我们从Babylon.js官方网站上的一个骨骼动画示例开始分析:
(示例地址:https://www.babylonjs-playground.com/frame.html#DMLMIP#1)
下载示例中的3D模型dummy3.babylon文件后,在JSON工具中展开:
(使用的JSON工具是bejson在线工具,地址:https://www.bejson.com/jsoneditoronline/)
可以看到,这个模型文件中包含了作者信息、背景色、雾效、物理效果、相机、光照、网格、声音、材质、粒子系统、光晕效果、阴影效果、骨骼、行为响应、额外信息、异步碰撞运算标志等场景信息。(也许称之为“场景文件”更合适)
我们主要关注其中与骨骼动画有关的网格数据和骨骼数据,展开网格数据:
其中,positions保存每个顶点在网格自身坐标系中的位置(数组中的每三个元素对应一个顶点),normals保存每个顶点对应的法线方向,uvs是顶点的纹理坐标,indices是顶点的绘制索引。
matricesIndices中保存每一个顶点属于哪一块骨骼,在这个模型里matricesIndices数组每个元素都是数字索引,但是从Babylon.js的这一段代码可以看出:
View Code
数组的一个元素也可以保存四个骨骼索引,并且可以使用扩展模式使一个顶点同时和八块骨骼关联。
matricesWeights数组保存每个顶点默认的四块骨骼对顶点姿态影响的权重。
展开骨骼数据:
可以看出同一个场景文件中可以包含多套不同id的骨骼,通过网格的skeletonId属性可以标示使用哪一套骨骼。网格的ranges属性里保存不同动作对应的动画帧数范围,比如第127帧到148帧动画对应机器人奔跑的动作。
展开一个骨骼元素:
经过试验得知,网格的matricesIndices属性中应该保存bones数组的自然索引,而不是bone的index元素。
bone的parentBoneIndex属性表示这个骨骼的“父骨骼”的索引,parentBoneIndex为-1表示这块骨头没有父骨骼(经过试验,Babylon.js只支持一个parentBoneIndex为-1的“根骨骼”,且根骨骼在骨骼数组中的位置应先于所有其他网格,所以可以添加一个不包含动画变化和顶点关联的“空骨骼”作为唯一的根骨骼,以避免出现多个根骨骼)
matrix属性是和骨头关联的顶点在进行了一系列变化之后,最终附加的一个在骨骼当前坐标系中的姿态变化矩阵(?)。
每一块骨骼有一个animation属性保存这个骨骼的动画信息,animation中的dataType=3表示这个动画是对矩阵类型的值进行动态变化,framePerSecond=30表示默认每秒播放30帧,keys里保存了每一个关键帧对应的矩阵值:
在关键帧时和这块骨骼关联的顶点会在骨骼的自身坐标系里进行values矩阵所表示的姿态变化,其父骨骼的姿态变化会和它的姿态变化叠加。
可以看出这个动画的每一个帧都是关键帧,这些数据应该是通过动作捕捉技术获取的。
二、生成并导出骨骼模型:
1、html文件:
复制代码
1
2
3
4
5
最简单元素测试
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
26
27
28
29
30
77
复制代码
引用的文件中stat.js用来生成窗口右上方的帧数显示;
Events.js、FileText.js、View.js里有一些导出模型文件时用到的方法,具体说明可以参考http://www.cnblogs.com/ljzc002/p/5511510.html,当然你也可以使用别的方式导出场景文件;
bones6_sc.js里是建立3D场景的代码;
bones6_br.js是建立渲染循环的代码;
arr_bones.js里是一个直接定义的bones数组;
ExportBabylonBones.js用来生成babylon格式的JSON数据。
52行到71行使用Babylon.js的gui功能在场景中绘制了一个导出按钮(gui操作可以参考http://www.cnblogs.com/ljzc002/p/7699162.html),点击导出按钮时执行场景文件导出操作,较旧版本的Babylon.js需要额外引用dat.gui.min.js和gui_br.js库来支持gui功能,最新的版本则集成了gui功能。
需要注意的是,如果导出操作中包含对顶点位置的计算,则ExportMesh3方法必须在场景开始正常渲染后执行,否则顶点的矩阵信息可能还没有初始化,计算会发生错误,将这一操作放在gui按钮的响应方法里是一个可行的解决方案,因为gui按钮可以点击时,场景一定已经处于正常渲染的状态了。
2、bones6_br.js
复制代码
1 /**
2 * Created by Administrator on 2017/8/30.
3 */
4 //在这里做每一帧之前要做的事,特别是控制输入,要放在这里!!
5 //var flag_export=0;
6 function MyBeforeRender()
7 {
8 scene.registerBeforeRender(function() {
9 if(scene.isReady())
10 {
11
12 }
13 });
14 engine.runRenderLoop(function () {
15 engine.hideLoadingUI();
16 if (divFps) {
17 // Fps
18 divFps.innerHTML = engine.getFps().toFixed() + " fps";
19 }
20 scene.render();
21 /*if(flag_export==0)
22 {
23 ExportMesh3(arr_mesh);
24 flag_export=1;
25 }*/
26 });
27 }
复制代码
渲染循环,没什么特殊的。
3、bones6_sc.js
复制代码
1 /**
2 * Created by Administrator on 2017/8/30.
3 */
4 var arr_mesh=[];
5 var createScene = function (engine) {
6 scene = new BABYLON.Scene(engine);
7 camera0 =new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, 0), scene);
8 camera0.position=new BABYLON.Vector3(0, 0, -80);
9 camera0.attachControl(canvas, true);
10 light0 = new BABYLON.HemisphericLight("Hemi0", new BABYLON.Vector3(0, 1, 0), scene);
11
12 var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
13 mat_frame.wireframe = true;
14
15 //头部
16 var vd_head=new BABYLON.VertexData.CreateSphere({diameter:8,diameterY:64,segments:16});
17 var data_pos=vd_head.positions;
18 var len =data_pos.length/3;
19 arr_index=[];
20 for(var i=0;i
16)
24 {
25 arr_index.push(2);
26 }
27 else if(posy>0)
28 {
29 arr_index.push(0);
30 }
31 else if(posy>-16)
32 {
33 arr_index.push(1);
34 }
35 else{
36 arr_index.push(3);
37 }
38
39 }
40 BABYLON.VertexData.ComputeNormals(vd_head.positions, vd_head.indices, vd_head.normals);//计算法线
41 var mesh_head=new BABYLON.Mesh("mesh_head",scene);
42 vd_head.applyToMesh(mesh_head, true);
43 mesh_head.vertexData=vd_head;
44 mesh_head.material=mat_frame;
45 //mesh_head.position.y=58.5;
46 //mesh_head.position.y=0;
47 //mesh_head.index_bone=[0,1,2,3];
48 //mesh_head.index_parentbone=[-1,1,2,3];
49
50
51 var mesh_root=new BABYLON.Mesh("mesh_root",scene);
52 arr_mesh.push(mesh_root);
53 mesh_root.index_bone=[-1];
54
55 mesh_head.parent=mesh_root;
56 arr_mesh.push(mesh_head);
57
58 //ExportMesh3(arr_mesh);
59
60 return scene;
61 };
复制代码
其中arr_mesh是保存场景中所有网格的数组;
场景中包含一个自由相机、一个半球形光源;
mat_frame是一个只显示网格线的材质对象;
VertexData是Babylon.js定义的“顶点数据类”,这里建立了一个椭球体顶点数据对象,球的直径是8,Y轴直径是64,曲面细分度是16;
取得顶点数据对象中的顶点位置数据,按照顶点的Y坐标将其分为四类;
arr_index对应网格的matricesIndices,这里根据高度的不同将顶点和不同的骨骼关联起来;
接下来用这个顶点数据生成一个网格mesh_head,模仿dummy3.babylon的设置,这里还建立了空网格mesh_root作为mesh_head的父网格。需要注意的是“父网格”和“父骨骼”是两回事,父子网格之间的运动传递和父子骨骼之间的运动传递互不影响。
4、arr_bones.js
复制代码
1 /**
2 * Created by lz on 2018/4/17.
3 */
4 var vec_temp2=BABYLON.Vector3.TransformCoordinates(new BABYLON.Vector3(0,16,0),BABYLON.Matrix.RotationX(Math.PI/3))
5 .add(BABYLON.Vector3.TransformCoordinates(new BABYLON.Vector3(0,16,0),BABYLON.Matrix.RotationX(Math.PI/6)).negate())
6 .negate();
7
8 var arr_bones1=[
9 {//根骨骼
10 'animation':{
11 dataType:3,
12 framePerSecond:30,
13 keys:[{
14 frame:0,
15 values:BABYLON.Matrix.Identity().toArray()
16 },{
17 frame:120,
18 values:BABYLON.Matrix.RotationX(Math.PI/6).toArray()
19 },{
20 frame:240,
21 values:BABYLON.Matrix.Identity().toArray()
22 }],
23 loopBehavior:1,
24 name:'_bone'+0+'Animation',
25 property:'_matrix'
26 },
27 'index':0,
28 'matrix':BABYLON.Matrix.Identity().toArray(),//首先尝试把每个基本变换矩阵都设为单位阵
29 /*这时同一mesh里的不同骨骼不会叠加运动,整个网格表现为一个整体*/
30 //'matrix':mesh.parent._worldMatrix.clone().invert().multiply(mesh._worldMatrix.clone()).toArray(),//尝试矩阵变化量
31 //'matrix':mesh._worldMatrix.clone().toArray(),
32 'name':'_bone'+0,
33 'parentBoneIndex':-1//是否要求它的父骨骼必须先出现?根骨骼需要最先出现
34
35 },
36 {
37 'animation':{
38 dataType:3,
39 framePerSecond:30,
40 keys:[{
41 frame:0,
42 values:BABYLON.Matrix.Identity().toArray()
43 },{
44 frame:120,
45 values:BABYLON.Matrix.RotationX(Math.PI/6).toArray()
46 },{
47 frame:240,
48 values:BABYLON.Matrix.Identity().toArray()
49 }],
50 loopBehavior:1,
51 name:'_bone'+1+'Animation',
52 property:'_matrix'
53 },
54 'index':1,
55 //'matrix':BABYLON.Matrix.Identity().multiply(BABYLON.Matrix.Translation(0, 16, 0)).toArray(),//首先尝试把每个基本变换矩阵都设为单位阵
56 'matrix':BABYLON.Matrix.Identity().toArray(),
57 /*这时同一mesh里的不同骨骼不会叠加运动,整个网格表现为一个整体*/
58 //'matrix':mesh.parent._worldMatrix.clone().invert().multiply(mesh._worldMatrix.clone()).toArray(),//尝试矩阵变化量
59 //'matrix':mesh._worldMatrix.clone().toArray(),
60 'name':'_bone'+1,
61 'parentBoneIndex':0//所谓的根骨骼只能有一个?还是根必须最先出现?
62
63 },
64
65 {//最上面一节
66 'animation':{
67 dataType:3,
68 framePerSecond:30,
69 keys:[{
70 frame:0,
71 values:BABYLON.Matrix.Identity().toArray()
72 },{
73 frame:120,
74 /*values:BABYLON.Matrix.RotationX(Math.PI/6).multiply(
75 BABYLON.Matrix.Translation(vec_temp2.x,vec_temp2.y,vec_temp2.z)
76 )
77 .toArray()*/
78 values:BABYLON.Matrix.RotationX(Math.PI/6).toArray()
79 },{
80 frame:240,
81 values:BABYLON.Matrix.Identity().toArray()
82 }],
83 loopBehavior:1,
84 name:'_bone'+2+'Animation',
85 property:'_matrix'
86 },
87 'index':2,
88 'matrix':BABYLON.Matrix.Identity().toArray(),//首先尝试把每个基本变换矩阵都设为单位阵
89 /*这时同一mesh里的不同骨骼不会叠加运动,整个网格表现为一个整体*/
90 //'matrix':mesh.parent._worldMatrix.clone().invert().multiply(mesh._worldMatrix.clone()).toArray(),//尝试矩阵变化量
91 //'matrix':mesh._worldMatrix.clone().toArray(),
92 'name':'_bone'+2,
93 'parentBoneIndex':0
94
95 },
96 {//最下面一节
97 'animation':{
98 dataType:3,
99 framePerSecond:30,
100 keys:[{
101 frame:0,
102 values:BABYLON.Matrix.Identity().toArray()
103 },{
104 frame:120,
105 values:BABYLON.Matrix.RotationX(Math.PI/6).toArray()
106 },{
107 frame:240,
108 values:BABYLON.Matrix.Identity().toArray()
109 }],
110 loopBehavior:1,
111 name:'_bone'+3+'Animation',
112 property:'_matrix'
113 },
114 'index':3,
115 'matrix':BABYLON.Matrix.Identity().toArray(),//首先尝试把每个基本变换矩阵都设为单位阵
116 /*这时同一mesh里的不同骨骼不会叠加运动,整个网格表现为一个整体*/
117 //'matrix':mesh.parent._worldMatrix.clone().invert().multiply(mesh._worldMatrix.clone()).toArray(),//尝试矩阵变化量
118 //'matrix':mesh._worldMatrix.clone().toArray(),
119 'name':'_bone'+3,
120 'parentBoneIndex':1
121
122 }
123 ]
复制代码
arr_bones1中定义了4块骨骼,参考bones6_sc.js可知,中间偏上的顶点与根骨骼关联,上面和中间偏下的顶点关联的骨骼以根骨骼为父骨骼,下面的顶点关联的骨骼以中间偏下的骨骼为父骨骼。每个骨骼的动画都是从原始姿态起绕x轴旋转30度再返回原始姿态。
5、ExportBabylonBones.js
复制代码
1 //重复一个小数组若干次,用来形成巨型数组
2 function repeatArr(arr,times)
3 {
4 var arr_result=[];
5 for(var i=0;i