使用骨骼动画技术可以将网格的顶点分配给若干骨头,通过给骨头设定关键帧和父子关系,可以赋予网格高度动态并具有传递性的变形 效果。这里结合之前的相关研究在网页端使用JavaScript实现了一个简单的骨骼动画编辑和模型生成工具。
一、显示效果:
1、访问https://ljzc002.github.io/Bones/HTML/CstestSpaceCraft2.html查看测试页面:
屏幕右侧的Babylon.js场景中是一个初始网格。
2、在Chrome浏览器控制台输入“ImportMesh("","../ASSETS/SCENE/","SpaceCraft.babylon")”,载入之前编写的一个宇宙飞船模型,关于这个模型的编写方式可以参考https://www.cnblogs.com/ljzc002/p/9473438.html
3、点击“新增骨骼”按钮,会在左侧建立一个可折叠的骨头编辑区(标签的类名是div_flexible),一个编辑区分为六行,每行包括四个文本框。
4、在第一行的四个文本框中输入-1、0、0、0,点击这个编辑区的刷新按钮,将在场景中建立一个朝向(-1,0,0)方向距原点距离为0的平面,所有包含平面正面(或平面上)顶点的线会被标为绿色(“正面”可以理解为从平面出发,沿着平面法线方向移动可以到达这个顶点,数学上可以说“这个顶点到平面的距离为正”)
当顶点的数量较多时,上述计算会花费一定时间,控制台里会打印出当前的查找进度。
在编辑区的第二行输入0、1、0、-3(表示沿法线的反方向到原点的距离为3)会建立另一个平面,同时处于两个平面正面的顶点会被选中,最多可以建立6个这样的区分平面。
5、选定这些顶点作为一号骨头后,点击“编辑关键帧”按钮将打开一号骨头的关键帧编辑对话框
其中父骨骼索引设为0号骨骼,0号骨骼可以理解为模型的原点,在整个动画过程中保持不变;关节点坐标由三个文本框组成,表示这一块骨头和父骨头的连接点的位置,这里一号骨头的关节点设为(0,0,0)。下面的文本框里是表示关键帧矩阵的脚本,解读规则为“帧数@矩阵对象#帧数@矩阵对象”,其中的ms.xx是简写的Babylon.js矩阵构造函数,其对应关系如下:(代码位于CookBones.js文件中)
复制代码
1 //在这里写对关键帧脚本的处理和骨骼模型导出
2 //定义一种简单的脚本简化输入
3 var ms={}//MatrixScript
4 ms.rx=function(rad)//绕x轴旋转
5 {
6 return BABYLON.Matrix.RotationX(rad);
7 }
8 ms.ry=function(rad)//绕y轴旋转
9 {
10 return BABYLON.Matrix.RotationY(rad);
11 }
12 ms.rz=function(rad)//绕z轴旋转
13 {
14 return BABYLON.Matrix.RotationZ(rad);
15 }
16 ms.m1=function(){//生成一个单位阵
17 return BABYLON.Matrix.Identity();
18 }
19 ms.sc=function(x,y,z)//缩放,因为做了矩阵标准化,在现在的场景里缩放不会起作用!!
20 {
21 return BABYLON.Matrix.Scaling(x,y,z);
22 }
23 ms.tr=function(x,y,z)//位移
24 {
25 return BABYLON.Matrix.Translation(x,y,z);
26 }
27 //0@ms.m1()#120@ms.rx(2)#240@ms.m1()
28 ms.fa=function(arr)//从数组生成矩阵
29 {
30 return BABYLON.Matrix.FromArray(arr);
31 }
32
33 var vs={}//VectorScript
34 vs.tr=function(vec3,matrix)//对向量进行矩阵变化
35 {
36 return BABYLON.Vector3.TransformCoordinates(vec3.clone(),matrix);
37 }
38 var pi=Math.PI;
复制代码
点击“写入初始关键帧”,则关键帧设置被保存,同时编辑区上的复选框会被选中。
6、再给宇宙飞船的翅膀设置骨骼:
设置关键帧
因为是取z值小于等于-6的顶点组成骨头2,所以将关节点位置设为(0,0,-6),当然,也可以把关节点设在其他位置,如过这样做翅膀的运动方式将有所不同。
对称的设置右侧的翅膀,生成骨头3。
7、设置完成后,点击“预览模型”按钮,将在场景的x方向显示骨骼动画效果:
这里体现出当前工具的一个缺点:尚未允许同一顶点绑定多块骨头,对于重复选取的顶点,后设置的骨头会覆盖之前的设置。
因为博客园对图片大小有限制,只能截取骨骼动画的一部分。
点击“导出模型”则可以文本方式导出上述含有骨骼动画的模型。
8、父骨头对子骨头的影响:
可以访问https://ljzc002.github.io/Bones/HTML/Cstest2.html页面,刷新并编辑三块骨头的关键帧可以看到骨骼动画的传递效果。
二、代码实现
1、工程结构:
其中ClickButton.js里是除矩阵计算外所有和按钮响应相关的代码,ComputeMatrix.js里是所有和矩阵计算有关的代码,Flex.js没用。
2、html2D网页绘制(并不是重点)。
html文件:(其中包括建立一个基础Babylon.js场景的js代码)
View Code
css文件:
View Code
控制编辑区展缩的JavaScript代码:(位于ClickButton.js文件中)
View Code
3、模型对象的初始化:
Babylon.js格式模型的层次结构可以参考https://www.cnblogs.com/ljzc002/p/8927221.html
a、在场景中建立一个用来导出3D模型的对象:
建立这个对象的方法在newland.js文件中:
复制代码
1 //返回一个最简单的Babylon.js场景格式
2 newland.CreateObjScene=function()
3 {
4 var obj_scene=
5 {
6 'autoClear': true,
7 'clearColor': [0,0,0],
8 'ambientColor': [0,0,0],
9 'gravity': [0,-9.81,0],
10 'cameras':[],
11 'activeCamera': null,
12 'lights':[],
13 'materials':[],
14 'geometries': {},
15 'meshes': [],
16 'multiMaterials': [],
17 'shadowGenerators': [],
18 'skeletons': [],
19 'sounds': []
20 };
21 return obj_scene;
22 }
复制代码
b、向模型中添加一个网格:
将网格对象的各种属性交给模型对象
复制代码
1 //向场景格式中加入一个网格对象
2 newland.AddMesh2Model=function(obj_scene,mesh,name)
3 {
4 var obj_mesh={};
5 obj_mesh.name=name?name:mesh.name;//防止在本页面加载导致网格重名
6 obj_mesh.id=name?name:mesh.id;
7 //obj_mesh.materialId=mat.id;//为避免出现重名材质,先不添加这个属性
8 obj_mesh.position=[mesh.position.x,mesh.position.y,mesh.position.z];
9 obj_mesh.rotation=[mesh.rotation.x,mesh.rotation.y,mesh.rotation.z];
10 obj_mesh.scaling=[mesh.scaling.x,mesh.scaling.y,mesh.scaling.z];
11 obj_mesh.isVisible=true;
12 obj_mesh.isEnabled=true;
13 obj_mesh.checkCollisions=false;
14 obj_mesh.billboardMode=0;
15 obj_mesh.receiveShadows=true;
16 obj_mesh.metadata=mesh.metadata;
17 if(mesh.matricesIndices)
18 {
19 obj_mesh.matricesIndices=mesh.matricesIndices;
20 obj_mesh.matricesWeights=mesh.matricesWeights;
21 obj_mesh.skeletonId=mesh.skeletonId;
22 }
23 if(mesh.geometry)//是有实体的网格
24 {
25 var vb=mesh.geometry._vertexBuffers;
26 obj_mesh.positions=newland.BuffertoArray2(vb.position._buffer._data);
27 obj_mesh.normals=newland.BuffertoArray2(vb.normal._buffer._data);
28 obj_mesh.uvs= newland.BuffertoArray2(vb.uv._buffer._data);
29 obj_mesh.indices=newland.BuffertoArray2(mesh.geometry._indices);
30 obj_mesh.subMeshes=[{
31 'materialIndex': 0,
32 'verticesStart': 0,
33 'verticesCount': mesh.geometry._vertexBuffers.position._buffer._data.length,//mesh.geometry._totalVertices,
34 'indexStart': 0,
35 'indexCount': mesh.geometry._indices.length
36 }];
37 obj_mesh.parentId=mesh.parent?mesh.parent.id:null;
38 }
39 else
40 {
41 obj_mesh.positions=[];
42 obj_mesh.normals=[];
43 obj_mesh.uvs=[];
44 obj_mesh.indices=[];
45 obj_mesh.subMeshes=[{
46 'materialIndex': 0,
47 'verticesStart': 0,
48 'verticesCount': 0,
49 'indexStart': 0,
50 'indexCount': 0
51 }];
52 obj_mesh.parentId=null;
53 }
54 obj_scene.meshes.push(obj_mesh);
55 }
复制代码
c、向模型中添加骨骼并向骨骼中添加骨头:
复制代码
1 newland.AddSK2Model=function(obj_scene,skname)
2 {
3 var obj_sk={id:obj_scene.skeletons.length,name:skname,bones:[],ranges:[]
4 ,needInitialSkinMatrix:false}
5 obj_scene.skeletons.push(obj_sk);
6 }
7 newland.AddBone2SK=function(obj_scene,i,bone)
8 {
9 obj_scene.skeletons[i].bones.push(bone)//也许应该用splice??
10 }
复制代码
d、用上述方法初始化网格与模型:(在html文件里)
复制代码
1 //在这里设置一个初始的默认网格,
2 mesh_origin=new BABYLON.MeshBuilder.CreateSphere("mesh_origin",{diameter:8,diameterY:64,segments:16},scene);
3 mesh_origin.material=mat_frame;
4 var vb=mesh_origin.geometry._vertexBuffers;
5 var data_pos=vb.position._buffer._data;
6 var len_pos=data_pos.length;
7 mesh_origin.matricesIndices=newland.repeatArr([0],len_pos/3);//顶点的骨头索引
8 mesh_origin.matricesWeights=newland.repeatArr([1,0,0,0],len_pos/3);//顶点的骨头权重
9 mesh_origin.skeletonId=0;
10 obj_scene=newland.CreateObjScene();
11 newland.AddMesh2Model(obj_scene,mesh_origin,"mesh_origin2");
12 newland.AddSK2Model(obj_scene,"sk_test1");//向模型中添加骨骼
13 var bone={
14 'animation':{
15 dataType:3,
16 framePerSecond:num_fps,
17 keys:[],
18 loopBehavior:1,
19 name:'_bone'+0+'Animation',
20 property:'_matrix'
21 },
22 'index':0,
23 'matrix':BABYLON.Matrix.Identity().toArray(),
24 'name':'_bone'+0,
25 'parentBoneIndex':-1
26 };
27 //bone.
28 newland.ExtendKeys(bone,sum_frame);//初始扩展根骨骼的关键帧,认为根骨骼是一直保持不变的
29 newland.AddBone2SK(obj_scene,0,bone);// 向骨骼中添加骨头
30 arr_bone=obj_scene.skeletons[0].bones;
31 BABYLON.Animation.AllowMatricesInterpolation = true;//动画矩阵插值
复制代码
这里建立了骨头0作为所有骨骼最底层的根骨骼,它保持不变,不参加后面的各项设置。
e、添加一个编辑区(一块骨头):(在ClickButton.js文件中)
复制代码
1 function addBone()//向列表里添加一块骨骼
2 {
3 var container=document.getElementById("div_flexcontainer");
4 container.appendChild(document.querySelectorAll("#div_hiden .div_flexible")[0].cloneNode(true));
5 var divs=container.querySelectorAll(".div_flexible");
6 var len=divs.length;
7 divs[len-1].number=len;//这个属性并不能准确的使用
8 divs[len-1].querySelectorAll(".str_flexlen")[0].innerHTML=len+"";
9 var bone={
10 'animation':{
11 dataType:3,
12 framePerSecond:num_fps,
13 keys:[],
14 loopBehavior:1,
15 name:'_bone'+len+'Animation',
16 property:'_matrix'
17 },
18 'index':len,
19 'matrix':BABYLON.Matrix.Identity().toArray(),
20 'name':'_bone'+len,
21 'parentBoneIndex':0
22 }
23 newland.AddBone2SK(obj_scene,0,bone);
24 }
复制代码
4、导入其他模型
作为模型编辑工具不可能只处理初始模型,使用ImportMesh方法导入其他的Babylon.js模型代替初始模型:
复制代码
1 /*
2 * ImportMesh("","../ASSETS/SCENE/","10.babylon")
3 * ImportMesh("","../ASSETS/SCENE/","SpaceCraft.babylon")
4 * */
5 function ImportMesh(objname,filepath,filename)
6 {
7
8 BABYLON.SceneLoader.ImportMesh(objname, filepath, filename, scene
9 , function (newMeshes, particleSystems, skeletons)
10 {//载入完成的回调函数
11 newland.ClearMeshinModel(obj_scene);
12 if(mesh_origin&&mesh_origin.dispose)
13 {
14 mesh_origin.dispose();
15 }
16 mesh_origin=newMeshes[0];
17 mesh_origin.material=mat_frame;
18 //mesh_origin.layerMask=2;
19 var vb=mesh_origin.geometry._vertexBuffers;
20 var data_pos=vb.position._buffer._data;
21 var len_pos=data_pos.length;
22 mesh_origin.matricesIndices=newland.repeatArr([0],len_pos/3);
23 mesh_origin.matricesWeights=newland.repeatArr([1,0,0,0],len_pos/3);
24 mesh_origin.skeletonId=0;
25 newland.AddMesh2Model(obj_scene,mesh_origin,"mesh_origin2");
26 }
27 );
28 }
复制代码
5、骨骼划分:
a、点击刷新按钮时根据编辑区的输入建立平面:
复制代码
1 function ClearAllClip()//只清理所有的斜面,不处理突出的顶点
2 {
3 var len=arr_plane.length;
4 for(var i=0;i