基于Babylon.js编写简单的骨骼动画生成器

时间:2023-03-08 22:33:10

  使用骨骼动画技术可以将网格的顶点分配给若干骨头,通过给骨头设定关键帧和父子关系,可以赋予网格高度动态并具有传递性的变形 效果。这里结合之前的相关研究在网页端使用JavaScript实现了一个简单的骨骼动画编辑和模型生成工具。  

  一、显示效果:

1、访问https://ljzc002.github.io/Bones/HTML/CstestSpaceCraft2.html查看测试页面:

基于Babylon.js编写简单的骨骼动画生成器

  屏幕右侧的Babylon.js场景中是一个初始网格。

2、在Chrome浏览器控制台输入“ImportMesh("","../ASSETS/SCENE/","SpaceCraft.babylon")”,载入之前编写的一个宇宙飞船模型,关于这个模型的编写方式可以参考https://www.cnblogs.com/ljzc002/p/9473438.html

基于Babylon.js编写简单的骨骼动画生成器

3、点击“新增骨骼”按钮,会在左侧建立一个可折叠的骨头编辑区(标签的类名是div_flexible),一个编辑区分为六行,每行包括四个文本框。

基于Babylon.js编写简单的骨骼动画生成器

4、在第一行的四个文本框中输入-1、0、0、0,点击这个编辑区的刷新按钮,将在场景中建立一个朝向(-1,0,0)方向距原点距离为0的平面,所有包含平面正面(或平面上)顶点的线会被标为绿色(“正面”可以理解为从平面出发,沿着平面法线方向移动可以到达这个顶点,数学上可以说“这个顶点到平面的距离为正”)

基于Babylon.js编写简单的骨骼动画生成器

  当顶点的数量较多时,上述计算会花费一定时间,控制台里会打印出当前的查找进度。

  在编辑区的第二行输入0、1、0、-3(表示沿法线的反方向到原点的距离为3)会建立另一个平面,同时处于两个平面正面的顶点会被选中,最多可以建立6个这样的区分平面。

基于Babylon.js编写简单的骨骼动画生成器

5、选定这些顶点作为一号骨头后,点击“编辑关键帧”按钮将打开一号骨头的关键帧编辑对话框

基于Babylon.js编写简单的骨骼动画生成器

  其中父骨骼索引设为0号骨骼,0号骨骼可以理解为模型的原点,在整个动画过程中保持不变;关节点坐标由三个文本框组成,表示这一块骨头和父骨头的连接点的位置,这里一号骨头的关节点设为(0,0,0)。下面的文本框里是表示关键帧矩阵的脚本,解读规则为“帧数@矩阵对象#帧数@矩阵对象”,其中的ms.xx是简写的Babylon.js矩阵构造函数,其对应关系如下:(代码位于CookBones.js文件中)

 //在这里写对关键帧脚本的处理和骨骼模型导出
//定义一种简单的脚本简化输入
var ms={}//MatrixScript
ms.rx=function(rad)//绕x轴旋转
{
return BABYLON.Matrix.RotationX(rad);
}
ms.ry=function(rad)//绕y轴旋转
{
return BABYLON.Matrix.RotationY(rad);
}
ms.rz=function(rad)//绕z轴旋转
{
return BABYLON.Matrix.RotationZ(rad);
}
ms.m1=function(){//生成一个单位阵
return BABYLON.Matrix.Identity();
}
ms.sc=function(x,y,z)//缩放,因为做了矩阵标准化,在现在的场景里缩放不会起作用!!
{
return BABYLON.Matrix.Scaling(x,y,z);
}
ms.tr=function(x,y,z)//位移
{
return BABYLON.Matrix.Translation(x,y,z);
}
//0@ms.m1()#120@ms.rx(2)#240@ms.m1()
ms.fa=function(arr)//从数组生成矩阵
{
return BABYLON.Matrix.FromArray(arr);
} var vs={}//VectorScript
vs.tr=function(vec3,matrix)//对向量进行矩阵变化
{
return BABYLON.Vector3.TransformCoordinates(vec3.clone(),matrix);
}
var pi=Math.PI;

  点击“写入初始关键帧”,则关键帧设置被保存,同时编辑区上的复选框会被选中。

6、再给宇宙飞船的翅膀设置骨骼:

基于Babylon.js编写简单的骨骼动画生成器

  设置关键帧

基于Babylon.js编写简单的骨骼动画生成器

  因为是取z值小于等于-6的顶点组成骨头2,所以将关节点位置设为(0,0,-6),当然,也可以把关节点设在其他位置,如过这样做翅膀的运动方式将有所不同。

  对称的设置右侧的翅膀,生成骨头3。

基于Babylon.js编写简单的骨骼动画生成器

7、设置完成后,点击“预览模型”按钮,将在场景的x方向显示骨骼动画效果:

基于Babylon.js编写简单的骨骼动画生成器

  这里体现出当前工具的一个缺点:尚未允许同一顶点绑定多块骨头,对于重复选取的顶点,后设置的骨头会覆盖之前的设置。

  因为博客园对图片大小有限制,只能截取骨骼动画的一部分。

  点击“导出模型”则可以文本方式导出上述含有骨骼动画的模型。

8、父骨头对子骨头的影响:

可以访问https://ljzc002.github.io/Bones/HTML/Cstest2.html页面,刷新并编辑三块骨头的关键帧可以看到骨骼动画的传递效果。

  二、代码实现

1、工程结构:

基于Babylon.js编写简单的骨骼动画生成器

其中ClickButton.js里是除矩阵计算外所有和按钮响应相关的代码,ComputeMatrix.js里是所有和矩阵计算有关的代码,Flex.js没用。

2、html2D网页绘制(并不是重点)。

html文件:(其中包括建立一个基础Babylon.js场景的js代码)

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>载入宇宙飞船模型进行测试</title>
<link href="../CSS/newland.css" rel="stylesheet">
<link href="../CSS/stat.css" rel="stylesheet">
<script src="../JS/LIB/babylon.40v.all.max.js"></script>
<script src="../JS/LIB/stat.js"></script>
<script src="../JS/MYLIB/Events.js"></script>
<script src="../JS/MYLIB/FileText.js"></script>
<script src="../JS/MYLIB/newland.js"></script>
<script src="../JS/MYLIB/View.js"></script>
<script src="../JS/MYLIB/Att7.js"></script>
<!--script src="../JS/MYLIB/ExportBabylonBones2.js"></script-->
<script src="../JS/PAGE/ClickButton.js"></script>
<script src="../JS/PAGE/ComputeMatrix.js"></script>
<script src="../JS/PAGE/CookBones.js"></script>
<style>
.div_flexible{float:left;width:100%; }
.div_flextop{width:100%;height:36px;background-color: #15428B;float: left}
.floatleft{float:left;margin-left: 10px;margin-top:6px}
.div_flexbottom{width:270px;margin-left:5px;height: 300px;display: none;overflow: hidden;border:1px solid #15428B;float: left}
.div_flexcell{float:left;height:50px;width:100%}
.div_key{}
</style>
</head>
<body oncontextmenu="return false;">
<!--https://ljzc002.github.io/Bones/HTML/Cstest2.html-->
<div style="position:absolute;top: 0px;left:0px;width:300px;height: 100%;overflow: hidden;">
<button style="float: left;margin-top:10px;margin-left: 10px" onclick="addBone()">新增骨骼</button>
<button style="float: left;margin-top:10px;margin-left: 10px" onclick="ExportMesh(obj_scene,0)">预览模型</button>
<button style="float: left;margin-top:10px;margin-left: 10px" onclick="ExportMesh(obj_scene,1)">导出模型</button>
<div style="position:absolute;top:50px;overflow-y: scroll;width:300px;height:500px" id="div_flexcontainer">
<!--这以内的内容都是可复制粘贴的-->
<div class="div_flexible" number="1">
<div class="div_flextop">
<span class="floatleft str_flexlen" style="color:darkgoldenrod;font-size: 16px;width:32px">1</span>
<button class="floatleft" onclick="flex()">收缩</button>
<button class="floatleft" onclick="OpenDivKey()">编辑关键帧</button>
<input class="floatleft checkbone" style="width: 20px;height: 20px;" type="checkbox">
<!--勾选表示导出骨骼时包含这一块,否则不导出它-》考虑到可能存在的复杂层级关系,决定导出所有骨头-->
<button class="floatleft" onclick="ShowClip2()">刷新</button>
</div>
<div class="div_flexbottom" style="display: block;">
<!--在这里设置最多六个切割平面-->
<div class="div_flexcell" number="1">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<div class="div_comment" style="display: none;">[-1,0,0,0]</div></div>
<div class="div_flexcell" number="2">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="3">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="4">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="5">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="6">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
</div>
<div class="div_comment0" style="display: none;">{"str_indexp":0,"str_posjx":0,"str_posjy":0,"str_posjz":0,"text_key":"0@ms.m1()#30@ms.rz(0.5)#60@ms.m1()#90@ms.rz(-0.5)#120@ms.m1()#150@ms.rz(0.5)#180@ms.m1()#210@ms.rz(-0.5)#240@ms.m1()"}</div></div>
<!--复制粘贴的截止线-->
</div>
</div>
<div id="div_allbase" style="position:absolute;top: 0px;right: 0px;left:301px;height: 100%">
<canvas id="renderCanvas"></canvas>
<div id="fps" style="z-index: 301;"></div>
</div>
<div id="div_hiden" style="display: none">
<div class="div_hidecell">
<div class="div_flexible" number="1">
<div class="div_flextop">
<span class="floatleft str_flexlen" style="color:darkgoldenrod;font-size: 16px;width:32px">1</span>
<button class="floatleft" onclick="flex()">展开</button>
<button class="floatleft" onclick="OpenDivKey()" disabled="disabled">编辑关键帧</button>
<input class="floatleft checkbone" style="width: 20px;height: 20px;" type="checkbox">
<!--勾选表示导出骨骼时包含这一块,否则不导出它-》考虑到可能存在的复杂层级关系,决定导出所有骨头-->
<button class="floatleft" onclick="ShowClip2()">刷新</button>
</div>
<div class="div_flexbottom">
<!--在这里设置最多六个切割平面-->
<div class="div_flexcell" number="1">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="2">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="3">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="4">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="5">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
<div class="div_flexcell" number="6">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
<input class="floatleft" style="width:50px" onchange="ShowClip()">
</div>
</div>
</div>
</div>
<div class="div_hidecell">
<div class="div_key"><!--它的样式由open_div设定了-->
<span class="floatleft"></span><br>
<span class="floatleft ">父骨骼索引:</span><input class="floatleft str_indexp" value="0">
<!--span class="floatleft ">每秒帧数:</span><input class="floatleft str_fps" value="30"--><br>
<span class="floatleft ">关节点坐标:</span><input class="floatleft str_posjx" value="0"><input class="floatleft str_posjy" value="0"><input class="floatleft str_posjz" value="0">
<button class="floatleft" onclick="InsertKey()">写入初始关键帧</button>
<!--在这里使用一种格式化的文本,体现关键帧与矩阵,num_key@matrix#-->
<textarea class="floatleft text_key" style="width:90%;top:40px;height: 250px;"></textarea>
<button class="floatleft" onclick="delete_div('div_open');delete_div('div_mask');">取消</button>
</div>
</div>
</div>
</body>
<script>
var VERSION=1.0,AUTHOR="lz_newland@163.com";
var machine,canvas,engine,scene,gl,MyGame={};
canvas = document.getElementById("renderCanvas");
engine = new BABYLON.Engine(canvas, true);
engine.displayLoadingUI();
gl=engine._gl;//决定在这里结合使用原生OpenGL和Babylon.js;
scene = new BABYLON.Scene(engine);
var divFps = document.getElementById("fps");
window.onload=beforewebGL;
function beforewebGL()
{
if(engine._webGLVersion==2.0)//输出ES版本
{
console.log("ES3.0");
}
else{
console.log("ES2.0");
}
//MyGame=new Game(0,"first_pick","","http://127.0.0.1:8082/");
/*0-startWebGL
* */
webGLStart();
}
//从下面开始分成简单测试和对象框架两种架构
//全局对象
var light0//全局光源
,camera0//主相机
,arr_bone;//除了根骨骼之外所有骨骼的集合
var obj_scene=null;
var num_fps=30;//一开始设置好动画帧数和总帧数
var sum_frame=241;//其实是包括0到240的241帧
var mesh_origin=null;
function webGLStart()
{
window.addEventListener("resize", function () {
engine.resize();
});
if(true)
{
camera0 =new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, -80), scene);
camera0.attachControl(canvas, true);
camera0.speed=0.5;
camera0.minZ=0.01;//问题出在这里!!设置的过小,会导致鼠标pick失败!!!!
light0 = new BABYLON.HemisphericLight("Hemi0", new BABYLON.Vector3(0, 1, 0), scene);
light0.groundColor=new BABYLON.Color3(0.5,0.5,0.5);
light0.specular = new BABYLON.Color3(1, 1, 1);
light0.diffuse = new BABYLON.Color3(1, 1, 1);
var advancedTexture=BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui1");
var mat_green = new BABYLON.StandardMaterial("mat_green", scene);
mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0);
mat_green.backFaceCulling=false;
var mesh_base=new BABYLON.MeshBuilder.CreateSphere("mesh_base",{diameter:1},scene);
mesh_base.material=mat_green;
mesh_base.position.x=0;
//mesh_base.layerMask=2;
var mesh_base1=new BABYLON.MeshBuilder.CreateSphere("mesh_base1",{diameter:1},scene);
mesh_base1.position.y=10;
mesh_base1.position.x=0;
mesh_base1.material=mat_green;
//mesh_base1.layerMask=2;
var mesh_base2=new BABYLON.MeshBuilder.CreateSphere("mesh_base2",{diameter:1},scene);
mesh_base2.position.y=-10;
mesh_base2.position.x=0;
mesh_base2.material=mat_green;
//mesh_base2.layerMask=2; mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
mat_frame.wireframe = true;
mat_frame.freeze(); mat_alpha_yellow=new BABYLON.StandardMaterial("mat_alpha_yellow", scene);
mat_alpha_yellow.diffuseColor = new BABYLON.Color3(1,1,0);
mat_alpha_yellow.alpha=0.2;//不透明度
mat_alpha_yellow.freeze(); mat_red=new BABYLON.StandardMaterial("mat_red", scene);
mat_red.diffuseColor = new BABYLON.Color3(1,0,0);
mat_red.wireframe=true;
mat_red.freeze();
} //在这里设置一个初始的默认网格,
mesh_origin=new BABYLON.MeshBuilder.CreateSphere("mesh_origin",{diameter:8,diameterY:64,segments:16},scene);
mesh_origin.material=mat_frame;
var vb=mesh_origin.geometry._vertexBuffers;
var data_pos=vb.position._buffer._data;
var len_pos=data_pos.length;
mesh_origin.matricesIndices=newland.repeatArr([0],len_pos/3);
mesh_origin.matricesWeights=newland.repeatArr([1,0,0,0],len_pos/3);
mesh_origin.skeletonId=0;
obj_scene=newland.CreateObjScene();
newland.AddMesh2Model(obj_scene,mesh_origin,"mesh_origin2");
newland.AddSK2Model(obj_scene,"sk_test1");//向模型中添加骨骼
var bone={
'animation':{
dataType:3,
framePerSecond:num_fps,
keys:[],
loopBehavior:1,
name:'_bone'+0+'Animation',
property:'_matrix'
},
'index':0,
'matrix':BABYLON.Matrix.Identity().toArray(),
'name':'_bone'+0,
'parentBoneIndex':-1
};
//bone.
newland.ExtendKeys(bone,sum_frame);//初始扩展根骨骼的关键帧,认为根骨骼是一直保持不变的
newland.AddBone2SK(obj_scene,0,bone);// 向骨骼中添加骨头
arr_bone=obj_scene.skeletons[0].bones;
BABYLON.Animation.AllowMatricesInterpolation = true;//动画矩阵插值
//建立两个gui显示进度,但是这样是不行的,因为此时主线程已经阻塞了,gui是不会刷新的!!
/*if(true)
{
var UiPanel2 = new BABYLON.GUI.StackPanel();
UiPanel2.width = "220px";
UiPanel2.height="30px";
UiPanel2.fontSize = "14px";
UiPanel2.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
UiPanel2.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP
UiPanel2.color = "white";
UiPanel2.background = "green";
advancedTexture.addControl(UiPanel2);
text1 = new BABYLON.GUI.TextBlock();
text1.text = "0";
text1.background = "blue";
text1.color="white";
text1.height="30px";
text1.width="30px";
text1.left="0px";
UiPanel2.addControl(text1);
text2 = new BABYLON.GUI.TextBlock();
text2.color="white";
text2.text = "/1";
text2.height="30px";
UiPanel2.addControl(text2);
}*/ reInit();//如果dom内容都是粘贴过来的,需要重新初始化一下arr_bone,相当于重新执行addbone
ImportMesh("","../ASSETS/SCENE/","SpaceCraft.babylon")
MyBeforeRender();
}
function MyBeforeRender()
{
scene.registerBeforeRender(function() {
if(scene.isReady())
{
}
});
engine.runRenderLoop(function () {
engine.hideLoadingUI();
if (divFps) {
// Fps
divFps.innerHTML = engine.getFps().toFixed() + " fps";
}
scene.render();
});
}
/*
* ImportMesh("","../../ASSETS/SCENE/","10.babylon")
* ImportMesh("","../ASSETS/SCENE/","SpaceCraft.babylon")
* */
function ImportMesh(objname,filepath,filename)
{ BABYLON.SceneLoader.ImportMesh(objname, filepath, filename, scene
, function (newMeshes, particleSystems, skeletons)
{//载入完成的回调函数
newland.ClearMeshinModel(obj_scene);
if(mesh_origin&&mesh_origin.dispose)
{
mesh_origin.dispose();
}
mesh_origin=newMeshes[0];
mesh_origin.material=mat_frame;
//mesh_origin.layerMask=2;
var vb=mesh_origin.geometry._vertexBuffers;
var data_pos=vb.position._buffer._data;
var len_pos=data_pos.length;
mesh_origin.matricesIndices=newland.repeatArr([0],len_pos/3);
mesh_origin.matricesWeights=newland.repeatArr([1,0,0,0],len_pos/3);
mesh_origin.skeletonId=0;
newland.AddMesh2Model(obj_scene,mesh_origin,"mesh_origin2");
}
);
}
</script>
</html>

css文件:

 /*通用属性*/
body{ margin:; padding:; border:; text-align: center; overflow: hidden;width: 100%;
height: 100%;position: fixed; font-family: verdana,arial,sans-serif; touch-action: none;
-ms-touch-action: none;font-size: 12px;min-width: 600px;}
ul { list-style: none; margin:; padding:;}
li{ list-style: none; margin:; padding:;}
ul li { float: left;}
button{ cursor: pointer; height: 23px;}
a:link{ text-decoration: none;} /*顶层属性*/ #div_control{height: 100%;width:100%;background-color: transparent;z-index:;position: absolute;top: 0px;left: 0px;pointer-events: none;}
#renderCanvas { width: 100%; height: 100%; outline: none;}
.div_col{width: 80px;height: 26px;padding: 5px;overflow: visible;position: relative;pointer-events: none;}
.to_left{float: left;text-align: left}
.to_right{float: right;text-align: right}
.btn_first{text-align: center;width: 60px;pointer-events:auto}
.btn_second{text-align: center;overflow: visible;overflow-wrap: normal;display: block;position: absolute;pointer-events:auto}
.hidden{display: none}
.btn_third{text-align: center;display: block;pointer-events:auto}
.div_mask{ height: 100%; width: 100%; display:block; z-index:; background: #cccccc; position: absolute;
left: 0px; top: 0px; filter: alpha(opacity=40); opacity: 0.40; overflow: hidden;} /*弹出层的一些属性*/
.div_cook2{ margin-top: 5px; margin-left: 0px; margin-right: auto; width: 100%; height: 35px; font-family: 宋体; font-size: 12px; float: left;}
.div_cook2 ul{float: left;margin-top: 5px;margin-bottom: 15px}
.div_cook2 li{ float: left; /*margin-top: 10px;*/ margin-left: 5px; margin-right: 10px; /*color:darkred;*/ color:#020202;}
.div_cook2 span{ width: 120px; float: left; text-align: right; /*color: darkred;*/ color: #15428b; padding-right: 5px; font-weight: bold;}
.div_cook2 button{ float: left; margin-right: 5px; margin-left: 5px; width: 52px; height: 20px; text-align: center; background-color: #d6e7ef; border: solid 1px #020202;}
.btn_close{ float:right;position:static; width: 14px;height: 14px; margin:;margin-top: 2px;margin-right:2px;padding: 0
;background: url(../ASSETS/IMAGE/close.png) no-repeat;border: 0px;vertical-align:top;z-index:;}
.str_number{ border: solid 1px #020202; width: 60px;}
.str_normal{ border: solid 1px #020202; width: 120px;}
.str_date{ width: 120px;}
.str_text{ border: solid 1px #020202; width:220px ; }
#fps { position: absolute; right: 20px; top: 5em; font-size: 20px; color: white;/*帧数显示*/
text-shadow: 2px 2px 0 black;} /*表格的属性*/
#all_base{min-height: 400px;min-width: 250px;height: 100%;width:100%;position: relative;overflow-x:hidden;overflow-y: hidden;}
td input{ height: 100%; width: 100%; border:; text-align: center; background-color: inherit;}
.div_tab{float: left;position: relative;width:4000px;overflow-x: hidden;overflow-y: scroll}
.div_tab td{ text-align: center; /*border: solid 1px #15428B;*/ border-right:solid 1px #15428B; border-bottom: solid 1px #15428B;
line-height: 16px; font-size: 13px; height: 24px; padding: 1px; background-color: inherit; word-break: keep-all;
/*display: inline-block*/}
.div_tab th{ text-align: center; /*border: solid 1px #15428B;*/ line-height: 16px; font-size: 13px; height: 36px;
padding: 1px; text-align: center; border-right: solid 1px #15428B; border-bottom: solid 1px #15428B; word-break: keep-all;
white-space:nowrap; overflow: hidden; text-overflow: ellipsis;/*display: inline-block*/}
.div_tab table{ float: left; width: auto; border-right-width:0px; border: solid 1px #15428B; table-layout: fixed;}
.div_tab tr{ width: auto; vertical-align: middle; /*border: solid 1px #15428B;*/ padding: 1px;}
td a{ cursor: pointer;}
td button{ cursor: pointer;}
.div_mask2{ display:block; left: 0px; top: 0px; /*filter: alpha(opacity=50); opacity: 0.50;*/ overflow: hidden;/*锁定的表头表列*/
position: absolute; float: left; overflow-x: hidden}
table{ border-spacing:;}
.div_mask2 td{ text-align: center; /*border: solid 1px #15428B;*/ border-right:solid 1px #15428B; border-bottom: solid 1px #15428B;
line-height: 16px; font-size: 13px; height: 24px; padding: 1px; background-color: inherit; word-break: keep-all;}
.div_mask2 th{ text-align: center; /*border: solid 1px #15428B;*/ line-height: 16px; font-size: 13px; height: 36px;
padding: 1px; text-align: center; border-right: solid 1px #15428B; border-bottom: solid 1px #15428B; word-break: keep-all;
white-space:nowrap; overflow: hidden; text-overflow: ellipsis;}
.div_mask2 table{ float: left; width: auto; border-right-width:0px; border: solid 1px #15428B; table-layout: fixed;
position: absolute;}
.div_mask2 tr{ width: auto; vertical-align: middle; /*border: solid 1px #15428B;*/ padding: 1px;}
.combo-panel li{ float:none;}
.btn_limlen{ /*float: left;*/ height: 20px; width: 20px; border: 1px solid; /*margin-top: 6px;*/ /*margin-left: 4px;*/
background: url(../ASSETS/IMAGE/play.png) no-repeat; position: absolute; -moz-border-radius: 3px; /* Gecko browsers圆角 */
-webkit-border-radius: 3px; /* Webkit browsers */ border-radius:3px; /* W3C syntax */ position: absolute;
top: 6px; right: 4px;} /*帧数显示*/
#fps {
position: absolute;
right: 20px;
top: 5px;
font-size: 20px;
color: white;
text-shadow: 2px 2px 0 black;
} #stats {
position: absolute;
right: 20px;
top: 11em;
font-size: 14px;
color: white;
text-align: right;
text-shadow: 2px 2px 0 black;
} #status {
position: absolute;
left: 20px;
bottom: 20px;
font-size: 14px;
color: white;
text-shadow: 2px 2px 0 black;
}

控制编辑区展缩的JavaScript代码:(位于ClickButton.js文件中)

 //放和左侧伸缩菜单相关的内容
var flex_current=null;//当前展开的flex
function flex()//展缩一块骨骼的配置菜单
{
var evt=evt||window.event||arguments[0];
cancelPropagation(evt);
var obj=evt.currentTarget?evt.currentTarget:evt.srcElement;//obj是展缩按钮
if(obj.innerHTML=="展开")
{ var divs=document.querySelectorAll(".div_flexbottom");//要把其他展开状态的都关掉,同时还要改变高亮顶点(或者边线)状态
var len=divs.length;
for(var i=0;i<len;i++)
{
divs[i].style.display="none";
divs[i].parentNode.querySelectorAll("button")[0].innerHTML="展开";
divs[i].parentNode.querySelectorAll("button")[1].disabled="disabled";
}
obj.innerHTML="收缩";
obj.parentNode.parentNode.querySelectorAll(".div_flexbottom")[0].style.display="block";
obj.parentNode.querySelectorAll("button")[1].disabled=null;
ClearAllClip();
if(lines_inpicked&&lines_inpicked.dispose)
{
lines_inpicked.dispose();
}
flex_current=obj.parentNode.parentNode;
var divs=flex_current.querySelectorAll(".div_flexcell");//根据可能存在的初始值初始化文本框,但是还需要手动点击每个骨骼的刷新按钮
var len2=divs.length;
for(var i=0;i<len2;i++)
{
var div_comment=divs[i].querySelectorAll(".div_comment")[0];
if(div_comment)//如果这个平面有记录的数据
{
var arr=JSON.parse(div_comment.innerHTML);
var inputs=divs[i].querySelectorAll("input");
inputs[0].value=arr[0];
inputs[1].value=arr[1];
inputs[2].value=arr[2];
inputs[3].value=arr[3];
}
}
}
else if(obj.innerHTML=="收缩")
{
obj.innerHTML="展开";
obj.parentNode.parentNode.querySelectorAll(".div_flexbottom")[0].style.display="none";
obj.parentNode.querySelectorAll("button")[1].disabled="disabled";
ClearAllClip();
if(lines_inpicked&&lines_inpicked.dispose)
{
lines_inpicked.dispose();
}
}
}

3、模型对象的初始化:

Babylon.js格式模型的层次结构可以参考https://www.cnblogs.com/ljzc002/p/8927221.html

a、在场景中建立一个用来导出3D模型的对象:

建立这个对象的方法在newland.js文件中:

 //返回一个最简单的Babylon.js场景格式
newland.CreateObjScene=function()
{
var obj_scene=
{
'autoClear': true,
'clearColor': [0,0,0],
'ambientColor': [0,0,0],
'gravity': [0,-9.81,0],
'cameras':[],
'activeCamera': null,
'lights':[],
'materials':[],
'geometries': {},
'meshes': [],
'multiMaterials': [],
'shadowGenerators': [],
'skeletons': [],
'sounds': []
};
return obj_scene;
}

b、向模型中添加一个网格:

将网格对象的各种属性交给模型对象

 //向场景格式中加入一个网格对象
newland.AddMesh2Model=function(obj_scene,mesh,name)
{
var obj_mesh={};
obj_mesh.name=name?name:mesh.name;//防止在本页面加载导致网格重名
obj_mesh.id=name?name:mesh.id;
//obj_mesh.materialId=mat.id;//为避免出现重名材质,先不添加这个属性
obj_mesh.position=[mesh.position.x,mesh.position.y,mesh.position.z];
obj_mesh.rotation=[mesh.rotation.x,mesh.rotation.y,mesh.rotation.z];
obj_mesh.scaling=[mesh.scaling.x,mesh.scaling.y,mesh.scaling.z];
obj_mesh.isVisible=true;
obj_mesh.isEnabled=true;
obj_mesh.checkCollisions=false;
obj_mesh.billboardMode=0;
obj_mesh.receiveShadows=true;
obj_mesh.metadata=mesh.metadata;
if(mesh.matricesIndices)
{
obj_mesh.matricesIndices=mesh.matricesIndices;
obj_mesh.matricesWeights=mesh.matricesWeights;
obj_mesh.skeletonId=mesh.skeletonId;
}
if(mesh.geometry)//是有实体的网格
{
var vb=mesh.geometry._vertexBuffers;
obj_mesh.positions=newland.BuffertoArray2(vb.position._buffer._data);
obj_mesh.normals=newland.BuffertoArray2(vb.normal._buffer._data);
obj_mesh.uvs= newland.BuffertoArray2(vb.uv._buffer._data);
obj_mesh.indices=newland.BuffertoArray2(mesh.geometry._indices);
obj_mesh.subMeshes=[{
'materialIndex': 0,
'verticesStart': 0,
'verticesCount': mesh.geometry._vertexBuffers.position._buffer._data.length,//mesh.geometry._totalVertices,
'indexStart': 0,
'indexCount': mesh.geometry._indices.length
}];
obj_mesh.parentId=mesh.parent?mesh.parent.id:null;
}
else
{
obj_mesh.positions=[];
obj_mesh.normals=[];
obj_mesh.uvs=[];
obj_mesh.indices=[];
obj_mesh.subMeshes=[{
'materialIndex': 0,
'verticesStart': 0,
'verticesCount': 0,
'indexStart': 0,
'indexCount': 0
}];
obj_mesh.parentId=null;
}
obj_scene.meshes.push(obj_mesh);
}

c、向模型中添加骨骼并向骨骼中添加骨头:

 newland.AddSK2Model=function(obj_scene,skname)
{
var obj_sk={id:obj_scene.skeletons.length,name:skname,bones:[],ranges:[]
,needInitialSkinMatrix:false}
obj_scene.skeletons.push(obj_sk);
}
newland.AddBone2SK=function(obj_scene,i,bone)
{
obj_scene.skeletons[i].bones.push(bone)//也许应该用splice??
}

d、用上述方法初始化网格与模型:(在html文件里)

 //在这里设置一个初始的默认网格,
mesh_origin=new BABYLON.MeshBuilder.CreateSphere("mesh_origin",{diameter:8,diameterY:64,segments:16},scene);
mesh_origin.material=mat_frame;
var vb=mesh_origin.geometry._vertexBuffers;
var data_pos=vb.position._buffer._data;
var len_pos=data_pos.length;
mesh_origin.matricesIndices=newland.repeatArr([0],len_pos/3);//顶点的骨头索引
mesh_origin.matricesWeights=newland.repeatArr([1,0,0,0],len_pos/3);//顶点的骨头权重
mesh_origin.skeletonId=0;
obj_scene=newland.CreateObjScene();
newland.AddMesh2Model(obj_scene,mesh_origin,"mesh_origin2");
newland.AddSK2Model(obj_scene,"sk_test1");//向模型中添加骨骼
var bone={
'animation':{
dataType:3,
framePerSecond:num_fps,
keys:[],
loopBehavior:1,
name:'_bone'+0+'Animation',
property:'_matrix'
},
'index':0,
'matrix':BABYLON.Matrix.Identity().toArray(),
'name':'_bone'+0,
'parentBoneIndex':-1
};
//bone.
newland.ExtendKeys(bone,sum_frame);//初始扩展根骨骼的关键帧,认为根骨骼是一直保持不变的
newland.AddBone2SK(obj_scene,0,bone);// 向骨骼中添加骨头
arr_bone=obj_scene.skeletons[0].bones;
BABYLON.Animation.AllowMatricesInterpolation = true;//动画矩阵插值

  这里建立了骨头0作为所有骨骼最底层的根骨骼,它保持不变,不参加后面的各项设置。

e、添加一个编辑区(一块骨头):(在ClickButton.js文件中)

 function addBone()//向列表里添加一块骨骼
{
var container=document.getElementById("div_flexcontainer");
container.appendChild(document.querySelectorAll("#div_hiden .div_flexible")[0].cloneNode(true));
var divs=container.querySelectorAll(".div_flexible");
var len=divs.length;
divs[len-1].number=len;//这个属性并不能准确的使用
divs[len-1].querySelectorAll(".str_flexlen")[0].innerHTML=len+"";
var bone={
'animation':{
dataType:3,
framePerSecond:num_fps,
keys:[],
loopBehavior:1,
name:'_bone'+len+'Animation',
property:'_matrix'
},
'index':len,
'matrix':BABYLON.Matrix.Identity().toArray(),
'name':'_bone'+len,
'parentBoneIndex':0
}
newland.AddBone2SK(obj_scene,0,bone);
}

4、导入其他模型

作为模型编辑工具不可能只处理初始模型,使用ImportMesh方法导入其他的Babylon.js模型代替初始模型:

 /*
* ImportMesh("","../ASSETS/SCENE/","10.babylon")
* ImportMesh("","../ASSETS/SCENE/","SpaceCraft.babylon")
* */
function ImportMesh(objname,filepath,filename)
{ BABYLON.SceneLoader.ImportMesh(objname, filepath, filename, scene
, function (newMeshes, particleSystems, skeletons)
{//载入完成的回调函数
newland.ClearMeshinModel(obj_scene);
if(mesh_origin&&mesh_origin.dispose)
{
mesh_origin.dispose();
}
mesh_origin=newMeshes[0];
mesh_origin.material=mat_frame;
//mesh_origin.layerMask=2;
var vb=mesh_origin.geometry._vertexBuffers;
var data_pos=vb.position._buffer._data;
var len_pos=data_pos.length;
mesh_origin.matricesIndices=newland.repeatArr([0],len_pos/3);
mesh_origin.matricesWeights=newland.repeatArr([1,0,0,0],len_pos/3);
mesh_origin.skeletonId=0;
newland.AddMesh2Model(obj_scene,mesh_origin,"mesh_origin2");
}
);
}

5、骨骼划分:

a、点击刷新按钮时根据编辑区的输入建立平面:

 function ClearAllClip()//只清理所有的斜面,不处理突出的顶点
{
var len=arr_plane.length;
for(var i=0;i<len;i++)
{
var plane=arr_plane[i];
plane.cylinder.dispose();
plane.mesh.dispose();
plane.lines_normal.dispose(); plane=null;
}
arr_plane=[];
} var arr_plane=[];//保存所有的平面
function ShowClip()//先预留,否则以后要用时添加起来很麻烦
{ }
function ShowClip2()//再点击刷新时根据斜面计算当前骨骼区域
{
var evt=evt||window.event||arguments[0];
cancelPropagation(evt);
var obj=evt.currentTarget?evt.currentTarget:evt.srcElement;//obj是刷新按钮
ClearAllClip();//先清空所有可能存在的平面
var divs=obj.parentNode.parentNode.querySelectorAll(".div_flexbottom")[0].querySelectorAll(".div_flexcell");
var str_number=obj.parentNode.parentNode.querySelectorAll("span")[0].innerHTML//.getAttribute("number");//这是骨骼索引编号
var len=6;
for(var i=0;i<len;i++)//遍历每个斜面设置,绘制出相应斜面
{
var div=divs[i];
var inputs=div.querySelectorAll("input");
var len2=4;
var flag=0;
var arr=[];
for(var j=0;j<len2;j++)//判断这个斜面是否正常设置
{
if(isNaN(parseFloat(inputs[j].value)))//如果这个文本框没有内容或者内容不是数字
{
flag=1;
break;
}
else
{
arr.push(parseFloat(inputs[j].value));
}
}
if(flag==0)//如果可以构成平面
{
var plane=new BABYLON.Plane(arr[0], arr[1], arr[2], arr[3]);
var div_comment=div.querySelectorAll(".div_comment")[0];
if(!div_comment)//如果以前没有这个注释内容
{
div_comment=document.createElement("div");//建立一个隐形元素把设置持久化
div_comment.style.display="none";
div_comment.className="div_comment";
div.appendChild(div_comment);
}
div_comment.innerHTML=JSON.stringify(arr); plane.normalize();//必须先把平面标准化,否则生成的平面网格不准确(会参考向量长度生成)
var mesh_plane=new BABYLON.MeshBuilder.CreatePlane("mesh_plane"+i
,{sourcePlane:plane,sideOrientation:BABYLON.Mesh.DOUBLESIDE,size:50},scene);
//sourcePlane倾斜时sourcePlane有Bug!!!!??
mesh_plane.material=mat_alpha_yellow;//由plane生成的mesh没有rotation??
var pos1=mesh_plane.position.clone();
var vec_nomal=plane.normal.clone().normalize();
var pos2=pos1.add(vec_nomal);
var lines=[[pos1,pos2]];
var lines_normal=new BABYLON.MeshBuilder.CreateLineSystem("lines_normal"+i,{lines:lines,updatable:false},scene);
lines_normal.color=new BABYLON.Color3(1, 0, 0);
var cylinder = BABYLON.MeshBuilder.CreateCylinder("cylinder"+i,{height:1,diameterTop:0,diameterBottom:0.2 } ,scene);
cylinder.parent=mesh_plane;
cylinder.rotation.x-=Math.PI/2;
cylinder.position.z-=1.5;
cylinder.material=mat_red; plane.mesh=mesh_plane;
plane.lines_normal=lines_normal;
plane.cylinder=cylinder;
arr_plane.push(plane);
}
else
{
var div_comment=div.querySelectorAll("div_comment")[0];//如果这个平面设置不成立,但又有记录的数据,则清空记录的数据
if(div_comment)
{
delete_div(div_comment);
}
}
}
requestAnimFrame(function(){FindVertex(str_number);});
//FindVertex(str_number);//寻找属于这块骨骼的顶点
}

  第28行使用编辑区左上角的数字区分当前编辑的是哪一块骨头。

  接下来遍历编辑区的每一行,如果这一行的输入符合构成平面的要求,则建立一个平面对象,然后把这一行输入的内容以隐形标签的形式保存在dom文档中。

  接下来对平面进行标准化操作,在用标准化平面建立平面网格(63行),这里要注意“平面”是Babylon.js中的一类数学对象,并不实际显示,平面网格才是实际显示的对象。所谓标准化指保持方向不变让平面的方向向量模为1。

  再接下来在平面网格的*建立一条线段和一个圆锥体网格代表法向量。

  最后告知浏览器在下一帧渲染时执行顶点查找计算,发现直接执行FindVertex方法程序会出错,限于时间并未深入研究原因。

b、使用平面对象选择顶点:

 var lines_inpicked=null;
function FindVertex(str_number)//突出显示骨骼范围内的所有顶点
{
if (divFps) {
// Fps
divFps.innerHTML = "0fps";
}
if(!mesh_origin||!mesh_origin.dispose)
{
console.log("尚未加载模型");
return;
}
if(lines_inpicked&&lines_inpicked.dispose)
{
lines_inpicked.dispose();
}
var len=arr_plane.length;
if(len>0)//如果有平面,则开始遍历模型顶点
{
var mesh=mesh_origin;
var vb=mesh.geometry._vertexBuffers;
var data_pos=vb.position._buffer._data;
var len_pos=data_pos.length;
var data_index=mesh.geometry._indices;
var len_index=data_index.length;
var lines=[];
var matricesIndices=mesh_origin.matricesIndices;
var matricesWeights=mesh_origin.matricesWeights; for(var i=0;i<len_pos;i+=3)//对于每个顶点
{
console.log(i/3+1+"/"+len_pos/3);//显示当前操作到第几个顶点
if(matricesIndices[i/3]==parseInt(str_number))//要清空旧的设定
{
matricesIndices[i/3]=0;
}
var pos=new BABYLON.Vector3(data_pos[i],data_pos[i+1],data_pos[i+2]);
var flag=0;
for(var j=0;j<len;j++)//对于每一个切分平面
{
var num=arr_plane[j].signedDistanceTo(pos);
if(num<0)
{
flag=1;
break;
}
}
if(flag==0)
{
var index_vertex=i/3;
var vec=pos;
matricesIndices[index_vertex]=parseInt(str_number);//修改这个顶点的骨骼绑定
//下面进行突出显示,遍历索引?
for(var j=0;j<len_index;j+=3)
{
if(index_vertex==data_index[j])//三角形的第一个顶点
{
var num2=data_index[j+1]*3;
var num3=data_index[j+2]*3;
var vec2=new BABYLON.Vector3(data_pos[num2],data_pos[num2+1],data_pos[num2+2]);
var vec3=new BABYLON.Vector3(data_pos[num3],data_pos[num3+1],data_pos[num3+2]);
lines.push([vec,vec2]);
lines.push([vec,vec3]);
}
else if(index_vertex==data_index[j+1])//三角形的第一个顶点
{
var num2=data_index[j]*3;
var num3=data_index[j+2]*3;
var vec2=new BABYLON.Vector3(data_pos[num2],data_pos[num2+1],data_pos[num2+2]);
var vec3=new BABYLON.Vector3(data_pos[num3],data_pos[num3+1],data_pos[num3+2]);
lines.push([vec,vec2]);
lines.push([vec,vec3]);
}
else if(index_vertex==data_index[j+2])//三角形的第一个顶点
{
var num2=data_index[j]*3;
var num3=data_index[j+1]*3;
var vec2=new BABYLON.Vector3(data_pos[num2],data_pos[num2+1],data_pos[num2+2]);
var vec3=new BABYLON.Vector3(data_pos[num3],data_pos[num3+1],data_pos[num3+2]);
lines.push([vec,vec2]);
lines.push([vec,vec3]);
}
}
}
}
lines_inpicked=new BABYLON.MeshBuilder.CreateLineSystem("lines_inpicked",{lines:lines,updatable:false},scene);
lines_inpicked.color=new BABYLON.Color3(0, 1, 0);
}
else
{
console.log("没有设置符合规则的斜面");
return;
}
}

  对于每一个顶点,首先清空它当前的骨头索引(35行,未来考虑设置多骨头)。

  接着在41行用signedDistanceTo方法获得这个顶点到平面的距离,如果顶点在平面的法线方向(正向)则距离为正,反之为负。

  如果对于所有切分平面(最多六个)这个顶点都在正向,则将这个顶点的骨头索引设为当前编辑的骨头。并且高亮显示这个顶点所在的边。(高亮显示顶点的方法可以参考https://www.cnblogs.com/ljzc002/p/9353101.html)

6、关键帧脚本解析:

  为了简化关键帧矩阵的设置,我建立了一套简单的关键帧脚本规则,它的解析方法如下:(代码位于ComputeMatrix.js文件中)

 function InsertKey()//根据关键帧对mesh的骨骼的关键帧矩阵进行修改
{
var div_open=document.querySelectorAll("#div_open")[0];//弹出的窗口对象
var obj={};
obj.str_indexp=parseInt(div_open.querySelectorAll(".str_indexp")[0].value);
//obj.str_fps=div_open.querySelectorAll(".str_fps")[0].value;
obj.str_posjx=parseInt(div_open.querySelectorAll(".str_posjx")[0].value);
obj.str_posjy=parseInt(div_open.querySelectorAll(".str_posjy")[0].value);
obj.str_posjz=parseInt(div_open.querySelectorAll(".str_posjz")[0].value);
obj.text_key=div_open.querySelectorAll(".text_key")[0].value;
var str_key=obj.text_key;
str_key.replace("?","");//替换掉换行符,在Chrome里换行符是回车的竖折箭头,但这里被显示成了?
str_key.replace("\r","");
str_key.replace("\n","");
var arr_key=str_key.split("#");
var len=arr_key.length;
//计算这一块骨骼的关键帧
var bone=arr_bone[flex_current.querySelectorAll("span")[0].innerHTML]//.getAttribute("number")];
//var inputs=document.querySelectorAll("#div_open input");
//bone.animation.framePerSecond=parseInt(inputs[1].value);
bone.parentBoneIndex=obj.str_indexp;
bone.animation.keys=[];//每次点击计算时都会重写这块骨头的所有初始关键帧-》扩展关键帧要放在后面的环节!!!!
var div_comment=flex_current.querySelectorAll(".div_comment0")[0];//注释信息从伸缩对象中提取
if(!div_comment)//将对于关键帧的各项设置保存在一个隐形标签里,这样下次打开对话框就不需要重新输入了
{
div_comment=document.createElement("div");
div_comment.style.display="none";
div_comment.className="div_comment0";
flex_current.appendChild(div_comment);
}
div_comment.innerHTML=JSON.stringify(obj);
try
{
var pos_gj=new BABYLON.Vector3(obj.str_posjx,obj.str_posjy,obj.str_posjz);//关节点的坐标
bone.pos_gj=pos_gj;//记录这块骨头和父骨头之间的关节点的全局位置
for(var i=0;i<len;i++)//对于每一个关键帧
{
var key=arr_key[i];//单条关键帧的脚本代码
var arr=key.split("@");
var num_frame=parseInt(arr[0]);//当前帧数
var script_frame=arr[1];
var matrix=eval(script_frame);//根据脚本计算出矩阵对象 //var count=bone.animation.keys.length;
//var matrix2=LoadParent4ThisKey(bone,count,true); //var vec_temp2=BABYLON.Vector3.TransformCoordinates(pos_gj,matrix2)
// .subtract(BABYLON.Vector3.TransformCoordinates(pos_gj,matrix.multiply(matrix2)));
//bone.animation.keys.push({frame:num_frame,values:matrix.multiply(BABYLON.Matrix.Translation(vec_temp2.x,vec_temp2.y,vec_temp2.z)).toArray()
//});//推入每一条关键帧
bone.animation.keys.push({frame:num_frame,values:matrix.toArray()}) }
flex_current.querySelectorAll(".checkbone")[0].checked=true;
}
catch(e)
{
console.error(e);
}
finally
{//操作完毕后关闭对话框
delete_div('div_open');
delete_div("div_mask");
} }

7、预览模型

  完成前面的骨头编辑后点击预览模型,程序开始进入最关键的矩阵计算流程,首先是预览和导出模型的代码:

 var mesh_test=null;//必须先期声明一下,否则在ImportMesh的回调中会报错!!!!
function ExportMesh(obj_scene,flag)//这个工程专用的导出方法
{ if(flag==1)//点击导出按钮,默认此时已经完成了扩展关键帧和矩阵传递的计算
{
var str_data=JSON.stringify(obj_scene);
DownloadText(MakeDateStr()+"testscene",str_data,".babylon");
}
else if(flag==0)//点击现场演示按钮
{
var str_data="";
//var bones=obj_scene.skeletons[0].bones;
HandleBones();//对骨骼动画的关键帧进行处理 str_data=JSON.stringify(obj_scene);
//在现场演示环节里添加扩展关键帧的计算??
BABYLON.SceneLoader.ImportMesh("", "", "data:"+str_data, scene
, function (newMeshes, particleSystems, skeletons) {//载入完成的回调函数
try{
if(mesh_test)
{
mesh_test.dispose();
}
mesh_test=newMeshes[0];
mesh_test.position.x=50;
mesh_test.material=mat_frame;
//var totalFrame=skeletons[0]._scene._activeSkeletons.data.length;
skeleton=skeletons[0];
scene.beginAnimation(skeleton, 0, sum_frame, true, 0.5);//启动骨骼动画
}
catch(e)//在这里拦截异常,否则异常进入Babylon.js会导致浏览器卡顿!!
{
console.log(e);
} });
}
}

  可以看到,只有现场演示按钮的响应里具备HandleBones方法,所以在完成现场演示之后才可以点击导出模型。

  另一个需要注意的问题是在调试模式下,一旦ImportMesh的回调函数中出现未拦截的异常,浏览器将尝试打开Babylon.js文件进行调试,并将整个模型文件的文本作为异常信息输出,这会消耗极大的CPU资源,造成浏览器卡顿甚至崩溃,所以尝试catch一下防止这类问题。

8、关键帧扩展

  在前面的脚本里我们只设置了9个间隔30帧的关键帧,虽然Babylon.js也支持自动对动画关键帧矩阵进行插值,但为了避免线性插值造成的骨骼缩放和错位(https://www.cnblogs.com/ljzc002/p/8927221.html展示了不扩展关键帧的缺点),在这里主动将9个初始关键帧扩展为241个扩展关键帧。

 function HandleBones()//对每个骨头扩展关键帧,并建立矩阵传递
{
var len=arr_bone.length;
var total=len*sum_frame;
console.log("开始扩展非根骨头的关键帧");
for(var i=1;i<len;i++)//重新扩展根骨骼之外的所有骨头
{
var bone=arr_bone[i];
newland.ExtendKeys(bone,sum_frame);
console.log(i+"/"+(len-1));
}
 newland.ExtendKeys=function(bone,sum_frame)
{
var keys=bone.animation.keys;
var keys2=[];
if(keys.length==0)//如果是根骨骼一类没有初始关键帧的骨骼
{
for(var i=0;i<sum_frame;i++)
{
keys2.push({frame:i,values:BABYLON.Matrix.Identity().toArray()});
}
}
else
{
var count_frame=0;//当前扩展到第几帧
for(var i=0;i<keys.length;i++)//对于每一个初始关键帧
{
var key1=keys[i];
if(i<(keys.length-1))
{
var key2=keys[i+1];
var frame_between=key2.frame-key1.frame;
var j=0;
var m1=BABYLON.Matrix.FromArray(key1.values);
var m2=BABYLON.Matrix.FromArray(key2.values);
while(count_frame<=key2.frame)
{
var rate=j/frame_between;
var m_lerp=BABYLON.Matrix.Lerp(m1,m2,rate);
newland.NormalizeMatrix(m_lerp);
keys2.push({frame:count_frame,values:m_lerp.toArray()});
count_frame++;
j++;
}
}
else
{
while(count_frame<sum_frame)
{
keys2.push({frame:count_frame,values:key1.values});//这values应该是一个对象,但只是导出JSON的话似乎并不用克隆新对象
count_frame++;
}
}
}
}
bone.animation.keys=keys2;
return keys2;
}

   ExtendKeys方法遍历骨骼中已经设定的初始关键帧,如果没有设置初始关键帧,则使用不含任何变化的单位矩阵填满241个扩展关键帧(比如根骨骼),如果设置了初始关键帧,则先用线性插值获取扩展关键帧,并对扩展关键帧进行标准化操作。

  矩阵标准化避免了线性插值导致的骨头尺寸缩放,但代价是让使用者无法设置尺寸缩放的骨骼动画,考虑另加一个维护尺寸变化的变量,对标准化之后的矩阵再次缩放。 

  另外一个需要注意的地方是,Babylon.js骨头对象里以一维数组的形式保存矩阵(matrix.toArray()),而实际进行矩阵变换时则要用矩阵类对象进行操作(BABYLON.Matrix.FromArray(arr);)。

  经过上述计算,所有的骨头都被扩展为241了个关键帧。

9、继承父骨骼的矩阵变换:

  因为大学数学学的不好,这一段的算法并没有足够的理论基础,主要使用高中数学知识通过推理和反复试验得出:(HandleBones的下半部分)

 var joints=[];
//var joint0=arr_bone[1].pos_gj;
console.log("开始调整非根骨头的关键帧")
for(var i=1;i<len;i++)//对于除根骨头之外的每一块骨头计算传递矩阵
{//回溯每块骨头的所有父骨头
var bone=arr_bone[i];
joints=[];//提取所有的关节距离
var sum_j=new BABYLON.Vector3(0,0,0);
var bone_now=bone;
while(true)
{//获取这块骨头和它所有父骨头的关节信息
var parent=arr_bone[bone_now.parentBoneIndex];//取父骨骼 if(parent.index!=0)//如果还没有回溯到根骨骼
{
joints.unshift({pos_gj:bone_now.pos_gj.clone().subtract(parent.pos_gj),index:parent.index});
sum_j=sum_j.add(bone_now.pos_gj.clone().subtract(parent.pos_gj));
bone_now=parent;
}
else
{
joints.unshift({pos_gj:bone_now.pos_gj,index:0});
sum_j=sum_j.add(bone_now.pos_gj);
break;
}
}
var keys=bone.animation.keys;
var keys2=[];//数组形式表示的矩阵的数组
bone.keys2=keys2;
bone.vec_adjusts=[];
bone.temp1=[];//父。multiply子
bone.temp1b=[];//子。multiply父
bone.sum_j=sum_j;
bone.temp3=[];
bone.joint=joints[joints.length-1].pos_gj.clone();//这块骨头的上一块骨头的形态
for(var j=0;j<sum_frame;j++)//对于这块骨头的每一个帧
{
var matrix=ms.fa(keys[j].values);//这一帧的局部变换矩阵
var parent=arr_bone[bone.parentBoneIndex];
if(bone.parentBoneIndex==0)//第一层骨头
{
bone.temp1.push(matrix);
bone.temp1b.push(matrix);
bone.vec_adjusts.push(bone.pos_gj.clone().subtract(vs.tr(sum_j.clone(),matrix)));//这个是全局坐标系中的位移
bone.temp3.push(bone.pos_gj.clone());
}
else//第二层及以上骨头
{ var matrix2=parent.temp1[j].clone().multiply(matrix);
bone.temp1.push(matrix2);
var matrix2b=(matrix.clone()).multiply(parent.temp1b[j].clone());
bone.temp1b.push(matrix2b);
var vec=vs.tr(bone.joint.clone(),parent.temp1b[j].clone());
bone.temp3.push(parent.temp3[j].clone().add(vec));
bone.vec_adjusts.push((parent.temp3[j].clone().add(vec).subtract(vs.tr(bone.sum_j,matrix2))));
}
var vec_adjust=bone.vec_adjusts[j].clone();
if(bone.parentBoneIndex!=0)
{
vec_adjust=vs.tr(vec_adjust.subtract(parent.vec_adjusts[j]),parent.temp1b[j].clone().invert());
}
var matrix_adjusted=matrix.multiply(ms.tr(vec_adjust.x,vec_adjust.y,vec_adjust.z))//变换后的这块骨头这一帧的矩阵
keys2.push({frame:j,values:matrix_adjusted.toArray()});
//var vec_adjust1=joint0.clone(),vec_adjust2=vs.tr(sum_j.clone(),matrix);//默认至少有一块一层骨头
//对于每一层父元素
//var len2=joints.length;//关节的数量比骨头的数量少一
/*for(var k=1;k<len2;k++)//对于每一个上游关节
{//这种写法是强制进行所有计算
var vec1=joints[k].pos_gj;
for(var l=1;l<=k;l++)
{
vec1=vs.tr(vec1,ms.fa(arr_bone[joints[l].index].animation.keys[j].values));
}
vec_adjust1=vec_adjust1.add(vec1); vs.tr(vec_adjust2,ms.fa(arr_bone[joints[k].index].animation.keys[j].values))//取这个关节所在的骨头在这一帧的变换矩阵 }*/
/*for(var k=0;k<len2;k++)//对于每一个上游关节
{ }
var vec_adjust=vec_adjust1.subtract(vec_adjust2);
var matrix_adjusted=matrix.multiply(ms.tr(vec_adjust.x,vec_adjust.y,vec_adjust.z))//变换后的这块骨头这一帧的矩阵
keys2.push({frame:j,values:matrix_adjusted.toArray()});
bone.vec_adjusts.push(vec_adjust);//以此优化之*/
}
console.log(i+"/"+(len-1));
}
for(var i=1;i<len;i++)//对于除根骨头之外的每一块骨头用刚才计算出的keys2替换keys
{
var bone=arr_bone[i];
//var temp=bone.animation.keys;
bone.animation.keys=bone.keys2;//交换
//bone.keys2=temp;
delete bone.keys2;//节省文件大小
delete bone.temp1;
delete bone.vec_adjusts;
}
}

  a、下面用几幅图介绍上述代码中的算法:

  假设所有的骨骼动画都是绕x轴旋转,ABC表示关节点,abc表示转过的角度,j0、j1、j2表示两个关节点之间的向量,j0·a表示对向量j0施加转过角度a的矩阵变换。

基于Babylon.js编写简单的骨骼动画生成器

  图一为只有一块非根骨头,且骨头的关节位置不在原点的情况,我们把不动的根骨头叫做“骨头0”绕A点旋转的部分叫做“骨头1”。我们希望骨头1绕A点旋转a角度到达图中所示实线位置,但如果我们只把骨头1的关键帧矩阵设为a,则在实际执行动画时骨骼1会以原点为轴旋转a处于图中虚线位置,我们将从虚线平移到实线的向量定义为“vec_adjust”(修正向量)。对骨头的每个帧的矩阵施加这一修正向量即可将关键帧调整到目的位置。

  在图一的情况下vec_adjust=j0-j0·a。

  图二中有两块非根骨头(摄像条件有限,凑活看吧),修正向量为j0+j1·a-(j0+j1)·a·b。

  图三中有三块非根骨头,为了简化思考让A点与原点重合,修正向量为j1·a+j2·a·b-(j1+j2)·a·b·c。

  综合以上三种情况猜测修正向量的计算规律为:

  vec_adjust=j0+j1·a+j2·a·b-(j0+j1+j2)·a·b·c

  后经过实际测试发现规律应该是:

  vec_adjust=j0+j1·a+j2·b·a-(j0+j1+j2)·a·b·c

  但并不知道原理是什么。

  观察可知子骨头的修正向量里包含父骨头的修正向量的组成部分,为了节省计算消耗,对于骨头2设j0+j1·a=temp3、a·b为temp1、b·a为temp1b、j0+j1对应关节向量的和sum_j。

  于是对于这块骨头的241个帧,算出每个帧的vec_adjust。

b、接下来对计算出的修正向量再进行两步处理

  首先,子骨骼会继承父骨骼在同一帧的矩阵变换,这其中也包含了父骨骼的修正向量,所以子骨骼的修正向量应该改为子骨骼修正向量与父骨骼修正向量的差值。(这里也许可以对计算进行化简)

  另外,我们刚才计算出的修正向量是世界坐标系中的,需要将它转化到骨头的局部坐标系中与骨头一同接受父骨头的影响。

  上述代码的第63行完成了这两步操作。

c、对关键帧矩阵施加修正向量代表的矩阵变化,并且清理掉bone对象中的多余属性,以减小生成的模型文件的尺寸。

  如此就完成了骨骼模型的矩阵计算工作。

10、保存和载入骨骼设置

  直接使用控制台复制div_flexcontainer标签中的内容粘贴到一个复制的html页中即可。

  页面加载时会执行reInit方法读取之前的设置:

 function reInit()
{
var flexs=document.querySelectorAll("#div_flexcontainer")[0].querySelectorAll(".div_flexible");
var len=flexs.length;
for(var i=0;i<len;i++)//对于加载的每一个flex对象
{
var flex=flexs[i];
var bone={
'animation':{
dataType:3,
framePerSecond:num_fps,
keys:[],
loopBehavior:1,
name:'_bone'+(i+1)+'Animation',
property:'_matrix'
},
'index':(i+1),
'matrix':BABYLON.Matrix.Identity().toArray(),
'name':'_bone'+(i+1),
'parentBoneIndex':0
}
newland.AddBone2SK(obj_scene,0,bone); var divs=flex.querySelectorAll(".div_flexcell");//根据可能存在的初始值初始化文本框,但是还需要手动点击每个骨骼的刷新按钮
var len2=divs.length;
for(var j=0;j<len2;j++)//初始化每个斜面的输入值
{
var div_comment=divs[j].querySelectorAll(".div_comment")[0];
if(div_comment)//如果这个平面有记录的数据
{
var arr=JSON.parse(div_comment.innerHTML);
var inputs=divs[j].querySelectorAll("input");
inputs[0].value=arr[0];
inputs[1].value=arr[1];
inputs[2].value=arr[2];
inputs[3].value=arr[3];
}
} var div_bottom=flex.querySelectorAll(".div_flexbottom")[0];
if(div_bottom.style.display=="block")
{
flex_current=flex;
}
}
}

三、总结

  目前的骨骼编辑器功能非常原始,不支持多骨骼绑定、不支持缩放类骨骼动画,算法也未经过严格测试,可能存在各种问题,欢迎各位大佬帮忙测试指出问题。