基于babylon3D模型研究3D骨骼动画
Posted ljzc002
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于babylon3D模型研究3D骨骼动画相关的知识,希望对你有一定的参考价值。
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以上是关于基于babylon3D模型研究3D骨骼动画的主要内容,如果未能解决你的问题,请参考以下文章