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的這一段代碼可以看出:
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頁面:
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文件:
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度,執行程序:

可以看到前面的問題得到解決。
七、下一步
計划編寫一個實用的簡易人體骨骼模型或骨骼模型生成工具,研究如何結合使用多網格和單網格骨骼動畫,研究如何使用其他已有的骨骼模型中的動作數據。
