基于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                 }            
View Code

数组的一个元素也可以保存四个骨骼索引,并且可以使用扩展模式使一个顶点同时和八块骨骼关联。

matricesWeights数组保存每个顶点默认的四块骨骼对顶点姿态影响的权重。

 

展开骨骼数据:

可以看出同一个场景文件中可以包含多套不同id的骨骼,通过网格的skeletonId属性可以标示使用哪一套骨骼。网格的ranges属性里保存不同动作对应的动画帧数范围,比如第127帧到148帧动画对应机器人奔跑的动作。

展开一个骨骼元素:

经过试验得知,网格的matricesIndices属性中应该保存bones数组的自然索引,而不是bone的index元素。

bone的parentBoneIndex属性表示这个骨骼的“父骨骼”的索引,parentBoneIndex为-1表示这块骨头没有父骨骼(经过试验,Babylon.js只支持一个parentBoneIndex为-1的“根骨骼”,且根骨骼在骨骼数组中的位置应先于所有其他网格,所以可以添加一个不包含动画变化和顶点关联的“空骨骼”作为唯一的根骨骼,以避免出现多个根骨骼)

matrix属性是和骨头关联的顶点在进行了一系列变化之后,最终附加的一个在骨骼当前坐标系中的姿态变化矩阵(?)。

每一块骨骼有一个animation属性保存这个骨骼的动画信息,animation中的dataType=3表示这个动画是对矩阵类型的值进行动态变化,framePerSecond=30表示默认每秒播放30帧,keys里保存了每一个关键帧对应的矩阵值:

在关键帧时和这块骨骼关联的顶点会在骨骼的自身坐标系里进行values矩阵所表示的姿态变化,其父骨骼的姿态变化会和它的姿态变化叠加。

可以看出这个动画的每一个帧都是关键帧,这些数据应该是通过动作捕捉技术获取的。

二、生成并导出骨骼模型:

1、html文件:

 1 <!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骨骼动画的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D 骨骼动画原理学习笔记

什么是骨骼动画 3d动效 spine

android OpenGL渲染带骨骼动画的3D模型

Unity3D:导入关键帧动画(不是基于骨骼的动画)

在 android 上渲染骨骼动画 FBX 3d 模型

动画骨骼模型文件格式fbx