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的这一段代码可以看出:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 if (parsedGeometry.matricesIndices) { 2 if (!parsedGeometry.matricesIndices._isExpanded) { 3 var floatIndices = []; 4 for (var i = 0; i < parsedGeometry.matricesIndices.length; i++) { 5 var matricesIndex = parsedGeometry.matricesIndices[i]; 6 floatIndices.push(matricesIndex & 0x000000FF); 7 floatIndices.push((matricesIndex & 0x0000FF00) >> 8); 8 floatIndices.push((matricesIndex & 0x00FF0000) >> 16); 9 floatIndices.push(matricesIndex >> 24); 10 } 11 mesh.setVerticesData(BABYLON.VertexBuffer.MatricesIndicesKind, floatIndices, parsedGeometry.matricesIndices._updatable); 12 } 13 else { 14 delete parsedGeometry.matricesIndices._isExpanded; 15 mesh.setVerticesData(BABYLON.VertexBuffer.MatricesIndicesKind, parsedGeometry.matricesIndices, parsedGeometry.matricesIndices._updatable); 16 } 17 } 18 if (parsedGeometry.matricesIndicesExtra) { 19 if (!parsedGeometry.matricesIndicesExtra._isExpanded) { 20 var floatIndices = []; 21 for (var i = 0; i < parsedGeometry.matricesIndicesExtra.length; i++) { 22 var matricesIndex = parsedGeometry.matricesIndicesExtra[i]; 23 floatIndices.push(matricesIndex & 0x000000FF); 24 floatIndices.push((matricesIndex & 0x0000FF00) >> 8); 25 floatIndices.push((matricesIndex & 0x00FF0000) >> 16); 26 floatIndices.push(matricesIndex >> 24); 27 } 28 mesh.setVerticesData(BABYLON.VertexBuffer.MatricesIndicesExtraKind, floatIndices, parsedGeometry.matricesIndicesExtra._updatable); 29 } 30 else { 31 delete parsedGeometry.matricesIndices._isExpanded; 32 mesh.setVerticesData(BABYLON.VertexBuffer.MatricesIndicesExtraKind, parsedGeometry.matricesIndicesExtra, parsedGeometry.matricesIndicesExtra._updatable); 33 } 34 }
数组的一个元素也可以保存四个骨骼索引,并且可以使用扩展模式使一个顶点同时和八块骨骼关联。
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 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>最简单元素测试</title> 6 <link href="../../CSS/newland.css" rel="stylesheet"> 7 <link href="../../CSS/cannon_cloth.css" rel="stylesheet"> 8 <script src="../../JS/LIB/jquery-1.11.3.min.js"></script> 9 <script src="../../JS/LIB/stat.js"></script> 10 <script src="../../JS/LIB/babylon.32.all.max.js"></script><!--这个是3.1--> 11 <!--script src="../../JS/LIB/numeric-1.2.6.min.js"></script--> 12 <script src="../../JS/MYLIB/Events.js"></script> 13 <script src="../../JS/MYLIB/FileText.js"></script> 14 <script src="../../JS/MYLIB/View.js"></script> 15 <script src="bones6_sc.js"></script> 16 <script src="bones6_br.js"></script> 17 <script src="arr_bones.js"></script> 18 <script src="ExportBabylonBones.js"></script> 19 <!--script src="../../JS/MYLIB/exportbabylon.js"></script--> 20 </head> 21 <body> 22 <div id="all_base" style=""> 23 <canvas id="renderCanvas"></canvas> 24 <div id="fps" style="z-index: 301;"></div> 25 </div> 26 </body> 27 28 <!--script src="../../JS/LIB/dat.gui.min.js"></script--> 29 <!--script src="gui_br.js"></script--> 30 <script> 31 var canvas,engine,scene,gl; 32 canvas = document.getElementById("renderCanvas"); 33 engine = new BABYLON.Engine(canvas, true); 34 BABYLON.SceneLoader.ShowLoadingScreen = false; 35 engine.displayLoadingUI(); 36 var divFps = document.getElementById("fps"); 37 //全局对象 38 var light0//全局光源 39 ,camera0//主相机 40 ; 41 42 window.onload=webGLStart; 43 window.addEventListener("resize", function () { 44 engine.resize(); 45 }); 46 47 function webGLStart() 48 { 49 //alert("放置断点!"); 50 gl=engine._gl; 51 createScene(); 52 var advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI"); 53 var UiPanel = new BABYLON.GUI.StackPanel(); 54 UiPanel.width = "220px"; 55 UiPanel.fontSize = "14px"; 56 UiPanel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT; 57 UiPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER; 58 advancedTexture.addControl(UiPanel); 59 // .. 60 var button = BABYLON.GUI.Button.CreateSimpleButton("but1", "Play Idle"); 61 button.paddingTop = "10px"; 62 button.width = "100px"; 63 button.height = "50px"; 64 button.color = "white"; 65 button.background = "green"; 66 button.onPointerDownObservable.add(function(state,info,coordinates) { 67 if (state) { 68 ExportMesh3(arr_mesh); 69 } 70 }); 71 UiPanel.addControl(button); 72 73 MyBeforeRender(); 74 //ExportMesh3(arr_mesh);//这时导出可能还没有进行矩阵计算!! 75 } 76 </script> 77 </html>
引用的文件中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<len;i++) 21 { 22 var posy=data_pos[i*3+1]; 23 if(posy>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<times;i++) 6 { 7 arr_result=arr_result.concat(arr.concat()); 8 } 9 return arr_result; 10 } 11 function ExportMesh3(arr_mesh) 12 { 13 obj_scene= 14 { 15 'autoClear': true, 16 'clearColor': [0,0,0], 17 'ambientColor': [0,0,0], 18 'gravity': [0,-9.81,0], 19 'cameras':[], 20 'activeCamera': null, 21 'lights':[], 22 'materials':[{ 23 'name': 'mat_frame', 24 'id': 'mat_frame', 25 'ambient': [1,1,1], 26 'diffuse': [1,1,1], 27 'specular': [1,1,1], 28 'specularPower': 50, 29 'emissive': [0,0,0], 30 'alpha': 1, 31 'backFaceCulling': true, 32 'diffuseTexture': { }, 33 'wireframe':true 34 }], 35 'geometries': {}, 36 'meshes': [], 37 'multiMaterials': [], 38 'shadowGenerators': [], 39 'skeletons': [{id:0,name:"mixamorig:Skin",bones:[],ranges:[],needInitialSkinMatrix:false}], 40 'sounds': [], 41 'metadata':{'walkabilityMatrix':[]} 42 43 } 44 var len=arr_mesh.length; 45 //推入每一个网格 46 for(var i=0;i<len;i++) 47 { 48 var obj_mesh={}; 49 var mesh=arr_mesh[i]; 50 51 obj_mesh.name=mesh.name; 52 obj_mesh.id=mesh.id; 53 obj_mesh.materialId='mat_frame'; 54 obj_mesh.position=[mesh.position.x,mesh.position.y,mesh.position.z]; 55 obj_mesh.rotation=[mesh.rotation.x,mesh.rotation.y,mesh.rotation.z]; 56 obj_mesh.scaling=[mesh.scaling.x,mesh.scaling.y,mesh.scaling.z]; 57 obj_mesh.isVisible=true; 58 obj_mesh.isEnabled=true; 59 obj_mesh.checkCollisions=false; 60 obj_mesh.billboardMode=0; 61 obj_mesh.receiveShadows=true; 62 if(mesh.geometry)//是有实体的网格 63 { 64 var vb=mesh.geometry._vertexBuffers; 65 obj_mesh.positions=vb.position._buffer._data; 66 obj_mesh.normals=vb.normal._buffer._data; 67 obj_mesh.uvs= vb.uv._buffer._data; 68 obj_mesh.indices=mesh.geometry._indices; 69 obj_mesh.subMeshes=[{ 70 'materialIndex': 0, 71 'verticesStart': 0, 72 'verticesCount': vb.position._buffer._data.length, 73 'indexStart': 0, 74 'indexCount': mesh.geometry._indices.length 75 }]; 76 //这里简单规定每个网格区块对应一块骨骼 77 //假设所有的区块都绑定到同一个网格上! 78 //obj_mesh.matricesIndices=repeatArr([mesh.index_bone],vb.position._buffer._data.length/3); 79 obj_mesh.matricesIndices=arr_index;//repeatArr([mesh.index_bone],vb.position._buffer._data.length/3); 80 obj_mesh.matricesWeights=repeatArr([1,0,0,0],vb.position._buffer._data.length/3); 81 obj_mesh.metadata={'rate_depart':0.5}; 82 //InsertBone2(mesh); 83 84 } 85 else 86 { 87 obj_mesh.positions=[]; 88 obj_mesh.normals=[]; 89 obj_mesh.uvs=[]; 90 obj_mesh.indices=[]; 91 obj_mesh.subMeshes=[{ 92 'materialIndex': 0, 93 'verticesStart': 0, 94 'verticesCount': 0, 95 'indexStart': 0, 96 'indexCount': 0 97 }]; 98 obj_mesh.matricesIndices=[]; 99 obj_mesh.matricesWeights=[]; 100 obj_mesh.metadata={'rate_depart':0}; 101 } 102 103 if(!mesh.parent)//如果是最顶层元素 104 { 105 obj_mesh.parentId=null; 106 obj_mesh.skeletonId=-1; 107 } 108 else 109 { 110 obj_mesh.parentId=mesh.parent.id; 111 obj_mesh.skeletonId=0; 112 } 113 obj_scene.meshes.push(obj_mesh); 114 } 115 obj_scene.skeletons[0].bones=arr_bones1;//装填事先预制的骨骼系统 116 var str_data=JSON.stringify(obj_scene); 117 DownloadText(MakeDateStr()+"testscene",str_data,".babylon"); 118 }
其中13到41行,用尽量简单的方式定义了场景文件的场景信息;
接下来将网格数组中每一个网格的信息放入obj_scene对象中;
然后把obj_scene对象转化为 JSON字符串导出,(这里因为Chrome浏览器不支持babylon类型文件,实际使用txt类型文件导出,需要手动把文件后缀名改为babylon。)
三、加载骨骼模型:
1、加载的html页面:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>预览导出的模型</title> 6 <link href="../../CSS/newland.css" rel="stylesheet"> 7 <link href="../../CSS/cannon_cloth.css" rel="stylesheet"> 8 <script src="../../JS/LIB/jquery-1.11.3.min.js"></script> 9 <script src="../../JS/LIB/stat.js"></script> 10 <script src="../../JS/LIB/babylon.32.all.max.js"></script><!--这个是3.1--> 11 <!--script src="../../JS/LIB/numeric-1.2.6.min.js"></script--> 12 <script src="../../JS/MYLIB/Events.js"></script> 13 <script src="../../JS/MYLIB/FileText.js"></script> 14 <script src="../../JS/MYLIB/View.js"></script> 15 <!--script src="../../JS/MYLIB/exportbabylon.js"></script--> 16 </head> 17 <body> 18 <div id="all_base" style=""> 19 <canvas id="renderCanvas"></canvas> 20 <div id="fps" style="z-index: 301;"></div> 21 </div> 22 </body> 23 <script src="bonesView_sc.js"></script> 24 <script src="bonesView_br.js"></script> 25 <!--script src="ExportBabylonBones.js"></script--> 26 <!--script src="../../JS/LIB/dat.gui.min.js"></script--> 27 <!--script src="gui_br.js"></script--> 28 <script> 29 var canvas,engine,scene,gl; 30 canvas = document.getElementById("renderCanvas"); 31 engine = new BABYLON.Engine(canvas, true); 32 BABYLON.SceneLoader.ShowLoadingScreen = false; 33 engine.displayLoadingUI(); 34 var divFps = document.getElementById("fps"); 35 //全局对象 36 var light0//全局光源 37 ,camera0//主相机 38 ; 39 40 window.onload=webGLStart; 41 window.addEventListener("resize", function () { 42 engine.resize(); 43 }); 44 45 function webGLStart() 46 { 47 //alert("放置断点!"); 48 gl=engine._gl; 49 createScene(); 50 MyBeforeRender(); 51 //ExportMesh1(arr_mesh); 52 } 53 </script> 54 </html>
前台没有什么特殊的,渲染循环代码也与导出时一样
2、bonesView_sc.js
1 /** 2 * Created by Administrator on 2017/8/30. 3 */ 4 var skeleton; 5 var createScene = function (engine) { 6 scene = new BABYLON.Scene(engine); 7 BABYLON.Animation.AllowMatricesInterpolation = true; 8 camera0 =new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, 0), scene); 9 camera0.position=new BABYLON.Vector3(0, 0, -80); 10 camera0.attachControl(canvas, true); 11 light0 = new BABYLON.HemisphericLight("Hemi0", new BABYLON.Vector3(0, 1, 0), scene); 12 13 var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene); 14 mat_frame.wireframe = true; 15 var mat_green = new BABYLON.StandardMaterial("mat_green", scene); 16 mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0); 17 18 var mesh_base=new BABYLON.MeshBuilder.CreateSphere("mesh_base",{diameter:1},scene); 19 mesh_base.material=mat_green; 20 var mesh_base1=new BABYLON.MeshBuilder.CreateSphere("mesh_base1",{diameter:1},scene); 21 mesh_base1.position.y=16; 22 mesh_base1.material=mat_green; 23 var mesh_base2=new BABYLON.MeshBuilder.CreateSphere("mesh_base2",{diameter:1},scene); 24 mesh_base2.position.y=-16; 25 mesh_base2.material=mat_green; 26 27 BABYLON.SceneLoader.ImportMesh("", "", "testscene3.babylon", scene 28 , function (newMeshes, particleSystems, skeletons) {//载入完成的回调函数 29 var mesh_test=newMeshes[0]; 30 //var totalFrame=skeletons[0]._scene._activeSkeletons.data.length; 31 skeleton=skeletons[0]; 32 scene.beginAnimation(skeleton, 0, 240, true, 0.2);//缺失了中间的部分!!没有自动插值!!!! 33 34 }); 35 36 return scene; 37 };
这段代码建立了skeleton全局对象保存从场景文件中加载的骨骼信息(skeleton翻译成中文是“骷髅”,指多块骨头的联合体);
第18到25行建立三个小圆球作为空间中的参考点;
接下来使用Babylon.js的场景加载器SceneLoader加载场景文件,Babylon.js还有一个可以加载场景文件的工具叫做资源管理器AssetsManager,后者的功能更加丰富。
ImportMesh的第一个参数是指明加载文件中的哪个对象,为空表示全部加载,第二个参数是资源url的绝对路径或者相对路径,第三个参数是文件名,第五个参数是加载成功的回调函数。回调函数的第一个参数是加载的网格列表,第二个参数是加载的粒子系统列表,第三个参数是骨骼列表。
语句scene.beginAnimation(skeleton, 0, 240, true, 0.2);启动骨骼动画skeleton,执行0到240帧,循环标志为true,用0.2倍速播放。需要注意的是3.1版本之前的Babylon.js不支持对动画中的矩阵变换进行插值操作,3.1之后的版本可以通过设置BABYLON.Animation.AllowMatricesInterpolation = true;来开启矩阵插值。
3、观察骨骼动画运行效果:
可以观察到,所有顶点的矩阵变化都以网格的中心为坐标原点(所以不和中心直接相连的骨骼会出现“断裂”),并且子骨骼会继承父骨骼的姿态变化,比如中间偏上的骨骼旋转了30度,中间偏下的骨骼则偏转了60度。
四、坐标变换
经过试验,更改bone的matrix属性并不能解决骨骼断裂问题(?),所以考虑在每一个关键帧中加入坐标变换,将子骨骼的所有顶点移动到与父骨骼相连的位置。
1、位移向量计算:
这里假设中间偏上的骨骼对应向量A,最上面的骨骼对应向量B,因为骨骼B一直相对于网格原点进行姿态变化,所以骨骼B的底部顶点就好像是从原点经过向量C位移得到的一样,所以求得的向量D=A-C就是把骨骼B的底部移动到骨骼A顶部所需的向量。
计算代码如下:
1 vec_temp2=BABYLON.Vector3.TransformCoordinates(new BABYLON.Vector3(0,16,0),BABYLON.Matrix.RotationX(Math.PI/6)) 2 .subtract(BABYLON.Vector3.TransformCoordinates(new BABYLON.Vector3(0,16,0),BABYLON.Matrix.RotationX(Math.PI/3)));
其中的矩阵运算方法和向量计算API可以查看Babylon.js的官方文档:http://doc.babylonjs.com/api/
通过矩阵变换,让骨骼B的所有顶点都移动这个向量:
1 //最上面一节 2 'animation':{ 3 dataType:3, 4 framePerSecond:30, 5 keys:[{ 6 frame:0, 7 values:BABYLON.Matrix.Identity().toArray() 8 },{ 9 frame:120, 10 values:BABYLON.Matrix.RotationX(Math.PI/6).multiply( 11 BABYLON.Matrix.Translation(vec_temp2.x,vec_temp2.y,vec_temp2.z) 12 ) 13 .toArray() 14 //values:BABYLON.Matrix.RotationX(Math.PI/6).toArray() 15 },{ 16 frame:240, 17 values:BABYLON.Matrix.Identity().toArray() 18 }], 19 loopBehavior:1, 20 name:'_bone'+2+'Animation', 21 property:'_matrix' 22 },
重新导出场景文件并加载:
可以看到断裂情况得到了改善
2、矩阵变换简介:
数学家通过观察发现:向量的缩放、位置和姿态变化信息可以保存在一些矩形排列的数字阵里,同时这些矩阵之间可以定义矩阵的乘法,其乘积就是两个矩阵的变化效果的累积。
对于表示三维空间中位置和运动的三维向量来说,它使用保存变化信息的矩阵是4*4的方阵,这种:
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
左上到右下对角线为1,其他全为0的方阵叫做单位矩阵,它表示不对向量的缩放、位置、姿态进行任何变化。
在OpenGL中矩阵通常用数组形式表示,矩阵被从左到右分成列,每一列从上到下推入数组,上面的矩阵再OpenGL中以类似[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]的数据结构保存。
具体的矩阵变换规则如下:
虽然OpenGL默认使用右手坐标系,但Babylon.js默认使用的是左手坐标系,矩阵变换规则也要随之调整
如果非对角线元素不为0,则把矩阵的这一列当做一个向量,这个向量的长度就是这一维度的缩放比例(?)。
(图片取自《OpenGL ES 3.x 游戏开发》,吴亚峰编著,人民邮电出版社出版)
五、在多个网格中实现骨骼动画
dummy3.babylon将物体的所有区块整合成一个非常复杂的网格,让后将同一个网格的不同位置顶点和不同的骨骼关联起来,这时所有的骨骼动画都将网格的中心作为坐标原点,那么是否可以同时使用多个网格,给每个网格绑定不同的骨骼呢?
1、html文件:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>验证基础关键帧的变换</title> 6 <link href="../../CSS/newland.css" rel="stylesheet"> 7 <link href="../../CSS/cannon_cloth.css" rel="stylesheet"> 8 <script src="../../JS/LIB/jquery-1.11.3.min.js"></script> 9 <script src="../../JS/LIB/stat.js"></script> 10 <script src="../../JS/LIB/babylon.32.all.max.js"></script><!--这个是3.1--> 11 <!--script src="../../JS/LIB/numeric-1.2.6.min.js"></script--> 12 <script src="../../JS/MYLIB/Events.js"></script> 13 <script src="../../JS/MYLIB/FileText.js"></script> 14 <script src="../../JS/MYLIB/View.js"></script> 15 <!--script src="../../JS/MYLIB/exportbabylon.js"></script--> 16 </head> 17 <body> 18 <div id="all_base" style=""> 19 <canvas id="renderCanvas"></canvas> 20 <div id="fps" style="z-index: 301;"></div> 21 </div> 22 </body> 23 <script src="bones5_sc.js"></script> 24 <script src="bones5_br.js"></script> 25 <script src="ExportBabylonBones2.js"></script> 26 <!--script src="CookBones.js"></script--> 27 <!--script src="../../JS/LIB/dat.gui.min.js"></script--> 28 <!--script src="gui_br.js"></script--> 29 <script> 30 var canvas,engine,scene,gl; 31 canvas = document.getElementById("renderCanvas"); 32 engine = new BABYLON.Engine(canvas, true); 33 BABYLON.SceneLoader.ShowLoadingScreen = false; 34 engine.displayLoadingUI(); 35 var divFps = document.getElementById("fps"); 36 //全局对象 37 var light0//全局光源 38 ,camera0//主相机 39 ; 40 41 window.onload=webGLStart; 42 window.addEventListener("resize", function () { 43 engine.resize(); 44 }); 45 46 function webGLStart() 47 { 48 //alert("放置断点!"); 49 gl=engine._gl; 50 createScene(); 51 var advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI"); 52 var UiPanel = new BABYLON.GUI.StackPanel(); 53 UiPanel.width = "220px"; 54 UiPanel.fontSize = "14px"; 55 UiPanel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT; 56 UiPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER; 57 advancedTexture.addControl(UiPanel); 58 // .. 59 var button = BABYLON.GUI.Button.CreateSimpleButton("button", "Play Idle"); 60 button.paddingTop = "10px"; 61 button.width = "100px"; 62 button.height = "50px"; 63 button.color = "white"; 64 button.background = "green"; 65 button.onPointerDownObservable.add(function(state,info,coordinates) { 66 if (state) { 67 ExportMesh(arr_mesh,0); 68 } 69 }); 70 UiPanel.addControl(button); 71 var button1 = BABYLON.GUI.Button.CreateSimpleButton("button1", "Export"); 72 button1.paddingTop = "10px"; 73 button1.width = "100px"; 74 button1.height = "50px"; 75 button1.color = "white"; 76 button1.background = "green"; 77 button1.onPointerDownObservable.add(function(state,info,coordinates) { 78 if (state) { 79 ExportMesh(arr_mesh,1); 80 } 81 }); 82 UiPanel.addControl(button1); 83 MyBeforeRender(); 84 } 85 </script> 86 </html>
为了方便测试,将场景文件的生成和预览放在同一个网页之中进行,这里添加了一个直接在当前窗口预览场景文件的gui按钮,渲染循环文件无变化
2、bones5_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 BABYLON.Animation.AllowMatricesInterpolation = true; 8 camera0 =new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, 0), scene); 9 camera0.position=new BABYLON.Vector3(0, 0, -80); 10 camera0.attachControl(canvas, true); 11 light0 = new BABYLON.HemisphericLight("Hemi0", new BABYLON.Vector3(0, 1, 0), scene); 12 13 var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene); 14 mat_frame.wireframe = true; 15 var mat_reb = new BABYLON.StandardMaterial("mat_reb", scene); 16 mat_reb.diffuseColor = new BABYLON.Color3(1, 0, 0); 17 var mat_green = new BABYLON.StandardMaterial("mat_green", scene); 18 mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0); 19 var mat_blue = new BABYLON.StandardMaterial("mat_blue", scene); 20 mat_blue.diffuseColor = new BABYLON.Color3(0, 0, 1); 21 22 var mesh_base=new BABYLON.MeshBuilder.CreateSphere("mesh_base",{diameter:1},scene); 23 mesh_base.material=mat_green; 24 mesh_base.position.x=20; 25 var mesh_base1=new BABYLON.MeshBuilder.CreateSphere("mesh_base1",{diameter:1},scene); 26 mesh_base1.position.y=10; 27 mesh_base1.position.x=20; 28 mesh_base1.material=mat_green; 29 var mesh_base2=new BABYLON.MeshBuilder.CreateSphere("mesh_base2",{diameter:1},scene); 30 mesh_base2.position.y=-10; 31 mesh_base2.position.x=20; 32 mesh_base2.material=mat_green; 33 34 var mesh1=new BABYLON.MeshBuilder.CreateCylinder("mesh1",{height :10,diameter:0.5},scene); 35 mesh1.position.x=-20; 36 mesh1.material=mat_frame; 37 mesh1.arr_anikey=[BABYLON.Matrix.RotationX(Math.PI/2)]; 38 mesh1.arr_anikey2=[]; 39 mesh1.arr_anikey3=[]; 40 var vb=mesh1.geometry._vertexBuffers; 41 mesh1.matricesIndices=repeatArr([1],vb.position._buffer._data.length/3); 42 mesh1.matricesWeights=repeatArr([1,0,0,0],vb.position._buffer._data.length/3); 43 mesh1.bones=[{ 44 'animation':{ 45 dataType:3, 46 framePerSecond:30, 47 keys:[{ 48 frame:0, 49 values:BABYLON.Matrix.Identity().toArray() 50 },{ 51 frame:120, 52 values:mesh1.arr_anikey[0].toArray() 53 },{ 54 frame:240, 55 values:BABYLON.Matrix.Identity().toArray() 56 }], 57 loopBehavior:1, 58 name:'_bone'+1+'Animation', 59 property:'_matrix' 60 }, 61 'index':1, 62 'matrix':BABYLON.Matrix.Identity().toArray(), 63 'name':'_bone'+1, 64 'parentBoneIndex':0 65 }]; 66 mesh1.bones[0].animation.keys=ExtendAnimations2(mesh1); 67 arr_mesh.push(mesh1); 68 69 var mesh2=new BABYLON.MeshBuilder.CreateCylinder("mesh2",{height :10,diameter:0.5},scene); 70 mesh2.parent=mesh1; 71 mesh2.position.y=10; 72 mesh2.material=mat_frame; 73 var vb=mesh2.geometry._vertexBuffers; 74 mesh2.matricesIndices=repeatArr([2],vb.position._buffer._data.length/3); 75 mesh2.matricesWeights=repeatArr([1,0,0,0],vb.position._buffer._data.length/3); 76 mesh2.arr_jointpos=[new BABYLON.Vector3(0,5,0)];//如果这个网格的骨骼是子元素,那么在这里保存这个子元素的关节点在其父元素坐标中的位置 77 mesh2.arr_anikey=[BABYLON.Matrix.RotationZ(Math.PI/2)];//骨骼在网格自身坐标系中的关键帧 78 mesh2.arr_anikey2=[BABYLON.Matrix.RotationX(Math.PI/2)];//这个骨骼的父骨骼的坐标变换量 79 mesh2.vec3_temp=BABYLON.Vector3.TransformCoordinates(mesh2.arr_jointpos[0].clone(),mesh2.arr_anikey2[0]) 80 .add(BABYLON.Vector3.TransformCoordinates(mesh2.position.clone().subtract(mesh2.arr_jointpos[0]),mesh2.arr_anikey[0].multiply(mesh2.arr_anikey2[0]))) 81 .add(mesh2.position.clone().negate()); 82 83 mesh2.bones=[{ 84 'animation':{ 85 dataType:3, 86 framePerSecond:30, 87 keys:[{ 88 frame:0, 89 values:BABYLON.Matrix.Identity().toArray() 90 },{ 91 frame:120, 92 //values:mesh2.arr_anikey[0].multiply(BABYLON.Matrix.Translation(mesh2.vec3_temp.x,mesh2.vec3_temp.y,mesh2.vec3_temp.z)).toArray() 93 values:mesh2.arr_anikey[0].toArray() 94 },{ 95 frame:240, 96 values:BABYLON.Matrix.Identity().toArray() 97 }], 98 loopBehavior:1, 99 name:'_bone'+2+'Animation', 100 property:'_matrix' 101 }, 102 'index':2, 103 'matrix':BABYLON.Matrix.Identity().toArray(), 104 'name':'_bone'+2, 105 'parentBoneIndex':1 106 }]; 107 arr_mesh.push(mesh2); 108 109 var mesh3=new BABYLON.MeshBuilder.CreateCylinder("mesh3",{height :10,diameter:0.5},scene); 110 mesh3.parent=mesh2; 111 mesh3.position.y=10; 112 mesh3.material=mat_frame; 113 var vb=mesh3.geometry._vertexBuffers; 114 var data_pos=vb.position._buffer._data; 115 var len=data_pos.length/3; 116 mesh3.matricesIndices=repeatArr([3],vb.position._buffer._data.length/3); 117 mesh3.matricesWeights=repeatArr([1,0,0,0],vb.position._buffer._data.length/3); 118 mesh3.arr_jointpos=[new BABYLON.Vector3(0,5,0)];//如果这个网格的骨骼是子元素,那么在这里保存这个子元素的关节点在其父元素坐标中的位置 119 mesh3.arr_anikey=[BABYLON.Matrix.RotationX(Math.PI/2)];//骨骼在网格自身坐标系中的关键帧 120 mesh3.arr_anikey2=[BABYLON.Matrix.RotationX(Math.PI/2).multiply(BABYLON.Matrix.RotationZ(Math.PI/2))];//经过父元素层层累加后的关键帧 121 mesh3.arr_anikey3=[]; 122 mesh3.vec3_temp=BABYLON.Vector3.TransformCoordinates(mesh3.arr_jointpos[0].clone(),mesh3.arr_anikey2[0]) 123 .add(BABYLON.Vector3.TransformCoordinates(mesh3.position.clone().subtract(mesh3.arr_jointpos[0]),mesh3.arr_anikey[0].multiply(mesh3.arr_anikey2[0]))) 124 .add(mesh3.position.clone().negate()); 125 126 mesh3.bones=[{ 127 'animation':{ 128 dataType:3, 129 framePerSecond:30, 130 keys:[{ 131 frame:0, 132 values:BABYLON.Matrix.Identity().toArray(), 133 },{ 134 frame:120, 135 //values:mesh3.arr_anikey[0].multiply(BABYLON.Matrix.Translation(mesh3.vec3_temp.x,mesh3.vec3_temp.y,mesh3.vec3_temp.z)).toArray() 136 values:mesh3.arr_anikey[0].toArray() 137 },{ 138 frame:240, 139 values:BABYLON.Matrix.Identity().toArray(), 140 }], 141 loopBehavior:1, 142 name:'_bone'+3+'Animation', 143 property:'_matrix' 144 }, 145 'index':3, 146 'matrix':BABYLON.Matrix.Identity().toArray(), 147 'name':'_bone'+3, 148 'parentBoneIndex':2 149 }]; 150 arr_mesh.push(mesh3); 151 152 return scene; 153 };
这段代码将场景的构建和骨骼的设置合并在同一个文件中,一共建立了mesh1、mesh2、mesh3三个首尾相连的圆柱体网格,(这里同时设置了三个网格之间的父子关系和三个骨骼之间的父子关系,但实验表明网格的父子关系并不影响骨骼的父子关系,这里把骨骼和网格的父子关系设置成一样只是为了看起来和谐)
arr_anikey保存这块骨骼的关键帧,考虑到未来一个网格可能被绑定给多个骨骼,arr_anikey是一个数组。mesh1绕X轴旋转90度,mesh2绕Z轴旋转90度,mesh3绕X轴旋转90度。
3、ExportBabylonBones2.js
1 /** 2 * Created by lz on 2018/4/18. 3 */ 4 var obj_scene; 5 var skeleton; 6 var mesh_test; 7 function ExportMesh(arr_mesh,flag) 8 { 9 obj_scene= 10 { 11 'autoClear': true, 12 'clearColor': [0,0,0], 13 'ambientColor': [0,0,0], 14 'gravity': [0,-9.81,0], 15 'cameras':[], 16 'activeCamera': null, 17 'lights':[], 18 'materials':[{ 19 'name': 'mat_frame', 20 'id': 'mat_frame', 21 'ambient': [1,1,1], 22 'diffuse': [1,1,1], 23 'specular': [1,1,1], 24 'specularPower': 50, 25 'emissive': [0,0,0], 26 'alpha': 1, 27 'backFaceCulling': true, 28 'diffuseTexture': { }, 29 'wireframe':true 30 }], 31 'geometries': {}, 32 'meshes': [], 33 'multiMaterials': [], 34 'shadowGenerators': [], 35 'skeletons': [{id:0,name:"mixamorig:Skin",bones:[{ 36 'animation':{ 37 dataType:3, 38 framePerSecond:30, 39 keys:[{ 40 frame:0, 41 values:BABYLON.Matrix.Identity().toArray() 42 },{ 43 frame:120, 44 values:BABYLON.Matrix.Identity().toArray() 45 },{ 46 frame:240, 47 values:BABYLON.Matrix.Identity().toArray() 48 }], 49 loopBehavior:1, 50 name:'_bone'+0+'Animation', 51 property:'_matrix' 52 }, 53 'index':0, 54 'matrix':BABYLON.Matrix.Identity().toArray(), 55 'name':'_bone'+0, 56 'parentBoneIndex':-1 57 }],ranges:[],needInitialSkinMatrix:false}], 58 'sounds': [], 59 'metadata':{'walkabilityMatrix':[]} 60 61 }; 62 63 var len=arr_mesh.length; 64 //推入每一个网格 65 for(var i=0;i<len;i++) 66 { 67 var obj_mesh={}; 68 var mesh=arr_mesh[i]; 69 70 obj_mesh.name=mesh.name; 71 obj_mesh.id=mesh.id; 72 obj_mesh.materialId='mat_frame'; 73 obj_mesh.position=[mesh.position.x,mesh.position.y,mesh.position.z]; 74 obj_mesh.rotation=[mesh.rotation.x,mesh.rotation.y,mesh.rotation.z]; 75 obj_mesh.scaling=[mesh.scaling.x,mesh.scaling.y,mesh.scaling.z]; 76 obj_mesh.isVisible=true; 77 obj_mesh.isEnabled=true; 78 obj_mesh.checkCollisions=false; 79 obj_mesh.billboardMode=0; 80 obj_mesh.receiveShadows=true; 81 if(mesh.geometry)//是有实体的网格 82 { 83 var vb=mesh.geometry._vertexBuffers; 84 obj_mesh.positions=vb.position._buffer._data; 85 obj_mesh.normals=vb.normal._buffer._data; 86 obj_mesh.uvs= vb.uv._buffer._data; 87 obj_mesh.indices=mesh.geometry._indices; 88 obj_mesh.subMeshes=[{ 89 'materialIndex': 0, 90 'verticesStart': 0, 91 'verticesCount': vb.position._buffer._data.length, 92 'indexStart': 0, 93 'indexCount': mesh.geometry._indices.length 94 }]; 95 obj_mesh.matricesIndices=mesh.matricesIndices; 96 obj_mesh.matricesWeights=mesh.matricesWeights; 97 obj_mesh.metadata={'rate_depart':0.5}; 98 obj_mesh.parentId=mesh.parent?mesh.parent.id:null; 99 obj_mesh.skeletonId=0; 100 101 } 102 else 103 { 104 obj_mesh.positions=[]; 105 obj_mesh.normals=[]; 106 obj_mesh.uvs=[]; 107 obj_mesh.indices=[]; 108 obj_mesh.subMeshes=[{ 109 'materialIndex': 0, 110 'verticesStart': 0, 111 'verticesCount': 0, 112 'indexStart': 0, 113 'indexCount': 0 114 }]; 115 obj_mesh.matricesIndices=[]; 116 obj_mesh.matricesWeights=[]; 117 obj_mesh.metadata={'rate_depart':0}; 118 obj_mesh.parentId=null; 119 obj_mesh.skeletonId=-1; 120 } 121 obj_scene.meshes.push(obj_mesh); 122 obj_scene.skeletons[0].bones=obj_scene.skeletons[0].bones.concat(mesh.bones); 123 } 124 var str_data=JSON.stringify(obj_scene); 125 if(flag==1)//点击导出按钮 126 { 127 DownloadText(MakeDateStr()+"testscene",str_data,".babylon"); 128 } 129 else if(flag==0)//点击现场演示按钮 130 { 131 BABYLON.SceneLoader.ImportMesh("", "", "data:"+str_data, scene 132 , function (newMeshes, particleSystems, skeletons) {//载入完成的回调函数 133 if(mesh_test) 134 { 135 mesh_test.dispose(); 136 } 137 mesh_test=newMeshes[0]; 138 mesh_test.position.x=20; 139 //var totalFrame=skeletons[0]._scene._activeSkeletons.data.length; 140 skeleton=skeletons[0]; 141 scene.beginAnimation(skeleton, 0, 240, true, 0.5);//缺失了中间的部分!!没有自动插值!!!! 142 143 }); 144 } 145 } 146 //重复一个小数组若干次,用来形成巨型数组 147 function repeatArr(arr,times) 148 { 149 var arr_result=[]; 150 for(var i=0;i<times;i++) 151 { 152 arr_result=arr_result.concat(arr.concat()); 153 } 154 return arr_result; 155 }
添加了一个省略ajax加载模型数据,直接使用本地JSON字符串进行预览的功能;
同时在obj_scene里设置了一个不包含任何姿态变化的“空骨骼”作为唯一的根骨骼
在不添加坐标变换的情况下执行程序:
可以看到每个网格的骨骼动画都是以本网格的中心为坐标原点的,但是骨骼动画的变化量是按父子层次累积的
4、添加坐标变换:
如图,变换的目的是将杆B的下端移动到杆A的上端,这个位移可以用向量4表示,而向量4的值是向量1、2、3的和,向量1和2的值都可以通过杆的中心到杆的接头(arr_jointpos)的向量乘以一层层父骨骼的关键帧变换矩阵获得,向量3则是两个杆的中心之间的向量
计算代码如下:
1 mesh3.arr_jointpos=[new BABYLON.Vector3(0,5,0)];//如果这个网格的骨骼是子元素,那么在这里保存这个子元素的关节点在其父元素坐标中的位置 2 mesh3.arr_anikey=[BABYLON.Matrix.RotationX(Math.PI/2)];//骨骼在网格自身坐标系中的关键帧 3 mesh3.arr_anikey2=[BABYLON.Matrix.RotationX(Math.PI/2).multiply(BABYLON.Matrix.RotationZ(Math.PI/2))];//经过父元素层层累加后的关键帧 4 mesh3.arr_anikey3=[]; 5 mesh3.vec3_temp=BABYLON.Vector3.TransformCoordinates(mesh3.arr_jointpos[0].clone(),mesh3.arr_anikey2[0]) 6 .add(BABYLON.Vector3.TransformCoordinates(mesh3.position.clone().subtract(mesh3.arr_jointpos[0]),mesh3.arr_anikey[0].multiply(mesh3.arr_anikey2[0]))) 7 .add(mesh3.position.clone().negate()); 8 mesh3.vec3_temp=BABYLON.Vector3.TransformCoordinates(mesh3.vec3_temp,mesh3.arr_anikey2[0].clone().invert());
1 { 2 frame:120, 3 values:mesh3.arr_anikey[0].multiply(BABYLON.Matrix.Translation(mesh3.vec3_temp.x,mesh3.vec3_temp.y,mesh3.vec3_temp.z)).toArray() 4 //values:mesh3.arr_anikey[0].toArray() 5 },
这里需要注意两点:一是乘以父元素变换矩阵的顺序是从最近父元素到根元素,而不是从根元素到父元素,因为线性代数课基本都没听,所以我也不知道这究竟是什么原理。但能够确定的是矩阵变换的顺序不同,变换的结果可能不一样,比如有两个变换,一个是在x轴平移10个单位,一个是在x轴拉伸十倍,如果先进行第一个变换再进行第二个则最终的平移量将是100单位,反过来执行则只有10个单位。
二是通过加和向量1、2、3获得的向量4是这个位移在世界坐标系中的表现,但在骨骼动画的父子继承过程中values属性会自动继承父骨骼的所有变换矩阵,这些继承的变换矩阵会扭曲向量4的姿态,所以要让向量4进行这些继承的变换矩阵的逆变换,这也就是代码1的第八行做的事情。(似乎可以对上述变换进行化简?)
执行程序:
明显骨骼动画存在问题:一是两段网格之间存在缺口,并且网格层级越多缺口越大;
二是缺口在关键帧时最小,在两关键帧之间最大;
三是骨骼的缩放发生变化;
四是旋转角度设置的越大上述三个变化越明显。
经过分析发现这些问题是Babylon.js对矩阵的线性插值导致的,以下是Babylon.js中的矩阵插值代码:
1 Matrix.Lerp = function (startValue, endValue, gradient) { 2 var result = Matrix.Zero(); 3 for (var index = 0; index < 16; index++) { 4 result.m[index] = startValue.m[index] * (1.0 - gradient) + endValue.m[index] * gradient; 5 } 6 result._markAsUpdated(); 7 return result; 8 };
在这种线性插值的情况下,假设矩阵的一个列向量初始值为1,0,0,0结束值为0,0,1,0,那么插值的中间值就是0.5,0,0.5,0,显然列向量的长度不是1,顶点的坐标发生缩放;
另一方面线性插值也可能会导致父子骨骼之间的矩阵变换不同步,子骨骼的姿态变换里包含父骨骼姿态矩阵的积累影响,直接对子骨骼的姿态矩阵插值得到的结果,和对每一层矩阵分别插值再将插值结果相乘得到的结果,在非关键帧时是不一样的,这也说明了为什么关键帧时误差最小。
要解决这些插值产生的问题,我们需要通过程序对插值过程进行干预,并且要设置尽量多的关键帧(官方示例模型中的每个帧都是关键帧也可能与此有关)
六、插值调整与关键帧扩展
1、对骨骼设置进行一些调整:
1 var mesh3=new BABYLON.MeshBuilder.CreateCylinder("mesh3",{height :10,diameter:0.5},scene); 2 mesh3.parent=mesh2; 3 mesh3.position.y=10; 4 mesh3.material=mat_frame; 5 var vb=mesh3.geometry._vertexBuffers; 6 var data_pos=vb.position._buffer._data; 7 var len=data_pos.length/3; 8 mesh3.matricesIndices=repeatArr([3],vb.position._buffer._data.length/3); 9 mesh3.matricesWeights=repeatArr([1,0,0,0],vb.position._buffer._data.length/3); 10 mesh3.arr_jointpos=[new BABYLON.Vector3(0,5,0)];//如果这个网格的骨骼是子元素,那么在这里保存这个子元素的关节点在其父元素坐标中的位置 11 mesh3.arr_anikey=[BABYLON.Matrix.RotationX(Math.PI/2)];//骨骼在网格自身坐标系中的关键帧 12 mesh3.arr_anikey2=[];//经过父元素层层累加后的关键帧,父元素的自身坐标系的变化情况 13 mesh3.arr_anikey3=[];//父元素在父元素的自身坐标系中的关键帧 14 mesh3.bones=[{ 15 'animation':{ 16 dataType:3, 17 framePerSecond:30, 18 keys:[{ 19 frame:0, 20 values:BABYLON.Matrix.Identity().toArray(), 21 },{ 22 frame:120, 23 values:mesh3.arr_anikey[0].toArray(), 24 },{ 25 frame:240, 26 values:BABYLON.Matrix.Identity().toArray(), 27 }], 28 loopBehavior:1, 29 name:'_bone'+3+'Animation', 30 property:'_matrix' 31 }, 32 'index':3, 33 'matrix':BABYLON.Matrix.Identity().toArray(), 34 'name':'_bone'+3, 35 'parentBoneIndex':2 36 }]; 37 mesh3.bones[0].animation.keys=ExtendAnimations2(mesh3); 38 arr_mesh.push(mesh3);
在第120帧时不写入坐标变换,只写骨骼在自身坐标里的变化情况,后面会通过它来生成更多的关键帧;
通过ExtendAnimations2方法将0到240之间的所有帧都转变为关键帧
2、添加了一个CookBones.js文件:
1 function ExtendAnimations2(mesh)//如果没有父网格,则不需要考虑关节对位,否则要累加每一层父网格在这一帧时的全部偏移量 2 {//只考虑最基础的情况 3 var arr_keys=[];//扩展后的结果 4 var keys=mesh.bones[0].animation.keys; 5 var len=keys.length; 6 if(len>2) 7 { 8 var count_frame_all=0; 9 arr_keys.push({frame:0,values:keys[0].values}); 10 mesh.arr_anikey2.push([BABYLON.Matrix.Identity()]);//假设每个网格里只有一个骨骼 11 mesh.arr_anikey3.push([BABYLON.Matrix.Identity()]); 12 for(var i=0;i<len-1;i++) 13 {//对于其中两个相邻的原始关键帧 14 var m1=new BABYLON.Matrix.FromArray(keys[i].values); 15 var m2=new BABYLON.Matrix.FromArray(keys[i+1].values); 16 var count_frame=keys[i+1].frame-keys[i].frame;//实际插值的次数是count_frame-1次 17 //arr_keys.push({frame:0,values:keys[i].values});//假设所有动画都是从零位开始 18 19 for(var j=1;j<=count_frame;j++)//对于细分的每一帧 20 { 21 var rate=j/count_frame; 22 var m_lerp=BABYLON.Matrix.Lerp(m1,m2,rate); 23 //m_lerp.toNormalMatrix(m_lerp); 24 NormalizeMatrix(m_lerp); 25 if(!mesh.parent)//如果没有父元素,直接把插值原样插入关键帧 26 { 27 arr_keys.push({frame:count_frame_all+j,values:m_lerp.toArray()}); 28 } 29 else//如果有父元素,则要继承父元素的偏移量 30 { 31 //var m0=BABYLON.Matrix.Identity(); 32 ///var m3=new BABYLON.Matrix.FromArray(keys[i].values2);//取这个关键帧相对于根节点的变换矩阵 33 //var m4=new BABYLON.Matrix.FromArray(keys[i+1].values2); 34 var matrix; 35 if(mesh.parent.parent) 36 {//父元素在这一帧时的全局偏移量,乘以这一帧的插值偏移量 37 //matrix =mesh.parent.arr_anikey2[0][count_frame_all+j].clone().multiply(mesh.parent.arr_anikey3[0][count_frame_all+j].clone()); 38 matrix =mesh.parent.arr_anikey3[0][count_frame_all+j].clone().multiply(mesh.parent.arr_anikey2[0][count_frame_all+j].clone()); 39 //matrix.toNormalMatrix(matrix); 40 mesh.arr_anikey2[0][count_frame_all+j]=matrix; 41 mesh.arr_anikey3[0][count_frame_all+j]=m_lerp; 42 } 43 else// 44 { 45 matrix =new BABYLON.Matrix.FromArray(mesh.parent.bones[0].animation.keys[count_frame_all+j].values)//BABYLON.Matrix.Lerp(m1,m2,rate); 46 mesh.arr_anikey2[0][count_frame_all+j]=matrix;//当前帧中元素的父元素的世界坐标偏移量 47 mesh.arr_anikey3[0][count_frame_all+j]=m_lerp;//当前帧中元素的自身坐标偏移量 48 } 49 50 var lerp_vec3_temp=BABYLON.Vector3.TransformCoordinates(mesh.arr_jointpos[0].clone(),matrix.clone())//它的父元素的世界偏移量 51 //.add(BABYLON.Vector3.TransformCoordinates(mesh.position.clone().subtract(mesh.arr_jointpos[0].clone()),matrix.clone().multiply(m_lerp))) 52 .add(BABYLON.Vector3.TransformCoordinates(mesh.position.clone().subtract(mesh.arr_jointpos[0].clone()),m_lerp.clone().multiply(matrix))) 53 .add(mesh.position.clone().negate());//BABYLON.Matrix.Lerp(m3,m4.clone().invert(),rate))); 54 lerp_vec3_temp=BABYLON.Vector3.TransformCoordinates(lerp_vec3_temp,matrix.clone().invert()); 55 arr_keys.push({frame:count_frame_all+j,values:m_lerp.multiply(BABYLON.Matrix.Translation(lerp_vec3_temp.x,lerp_vec3_temp.y,lerp_vec3_temp.z)).toArray()}); 56 //arr_keys.push({frame:count_frame_all+j,values:BABYLON.Matrix.Lerp(m1,m2,rate).toArray()}); 57 } 58 } 59 count_frame_all+=(count_frame-1);//记录这两个关键帧之间有多少扩展帧 60 } 61 return arr_keys; 62 } 63 else{ 64 return keys; 65 } 66 67 } 68 function NormalizeMatrix(matrix)//去掉线性插值矩阵的缩放效果 69 { 70 var m=matrix.m; 71 var vec1=new BABYLON.Vector3(m[0],m[1],m[2]).normalize(); 72 m[0]=vec1.x; 73 m[1]=vec1.y; 74 m[2]=vec1.z; 75 vec1=new BABYLON.Vector3(m[4],m[5],m[6]).normalize(); 76 m[4]=vec1.x; 77 m[5]=vec1.y; 78 m[6]=vec1.z; 79 vec1=new BABYLON.Vector3(m[8],m[9],m[10]).normalize(); 80 m[8]=vec1.x; 81 m[9]=vec1.y; 82 m[10]=vec1.z; 83 84 }
从最底层骨骼开始,循环处理每个骨骼的每一帧:
这里将直接定义的0、120、240帧称为“原始关键帧”,首先通过线性插值将0到120帧分成121个扩展关键帧,第一帧单独处理,其后每一帧在自身坐标系里的姿态变化都是m_lerp。然后使用自己编写的NormalizeMatrix方法去掉m_lerp中因线性插值产生的缩放变化。
如果这个非空骨骼没有非空的父元素则直接把插值结果设为关键帧的值(暂时把这种没有非空父元素的非空骨骼称为“第一层骨骼”),如果骨骼有非空的父元素,则要想办法将每一父子层次在这一帧时的插值结果记录下来一同作用在本骨骼的姿态矩阵上:
如果骨骼的父元素是第一层骨骼,则将第一层骨骼在第一层骨骼的坐标系中这一帧的动画效果记录在arr_anikey2数组中,将本骨骼在本骨骼坐标系中这一帧的动画效果记录在arr_anikey3数组中,我们将这样的骨骼称为第二层骨骼。
如果骨骼的父元素是第二层骨骼或第二层骨骼的后代,则将其父骨骼A在骨骼A的坐标系内这一帧的动画效果矩阵乘以其父骨骼的坐标系在世界坐标系中的变化矩阵的结果矩阵记录在arr_anikey2数组中。
接着对这一帧的数据套用前面用过的向量计算方法,得到在这一帧中使网格保持相连所需的坐标变换量,并将其作为扩展的关键帧写入关键帧数组,这样我们就成功的把3个基础关键帧扩展成了241个扩展关键帧。
将第一根杆的动画设为绕X轴绕90度,第二根绕Y轴90度,第三根绕Z轴90度,执行程序:
可以看到前面的问题得到解决。
七、下一步
计划编写一个实用的简易人体骨骼模型或骨骼模型生成工具,研究如何结合使用多网格和单网格骨骼动画,研究如何使用其他已有的骨骼模型中的动作数据。