三维组态部件动画解决方案
前言
最近做了一个制冷系统的三维组态监控系统。大致的效果如下图所示:
其中涉及到的设备有冷却塔、水泵、螺杆机、离心机 、分水器(集水器)、阀门,以及管路。
其中冷却塔,水泵,螺杆机,离心机都有停机/开机状态,开机状态下要有叶轮转动效果。
因此我们需要给这几种模型,在开机状态下做叶轮转动的动画。
分离模型发方案
我们知道一般模型的动画主要是模型的位置,旋转角度等发生变化。位置旋转角度的变化是比较容易实现的一种动画。直接调用模型的setPosition方法和setRotation方法即可。
因此要做叶轮的旋转动画,一种比较简便的模式就是把叶轮的模型,从主体模型里面分离出来,做成一个独立的模型。然后针对独立模型不断的调用setRotation方法即可。示意代码如下:
1function animate(obj){ 2 new mono.Animate({ 3 dur:1000, 4 from:0, 5 to:1, 6 onUpdate(v){ 7 obj.setRotationX(v * Math.PI *2); 8 } 9 }).play(); 10}
分离模型的好处是动画的实现方式比较简单。但是也有缺点,增加了模型的数量。比如一个设备有多少个部件要进行动画,那就得独立出多少个模型。另外一个缺点就是独立的出来的部件的模型,需要和建模人员沟通部件在整个模型中的位置,开发人员需要进行位置的摆放,增加了开发的复杂度。
为了解决上面的缺点,我们的想法是使用整体模型。
整体模型方案
不分离模型,把所有的部件都做到一个设备模型上面,但是对部件进行分组命名。这样我们在导入整个模型之后,可以通过分组命名获取到部件子模型。然后对部件子模型进行动画操作。
但是这个里面有一个问题是此时部件子模型的建模中心点不在子模型本身的中心,而是整体设备的中心。因此我们不能通过简单的setRotation的方法来进行模型的旋转动画。而是首先需要把旋转的中心点移动到子模型真实的中心,然后再进行旋转操作。
那么问题来了如何获取子模型本身的中心点呢?
包围盒(BoundingBox)
首先想到的是通过计算模型的包围盒来计算部件的中心点。但是由于通过OBJ格式导入的模型,它的每一个部件的包围盒都是整个模型的大小。所以我们需要修改包外盒的计算逻辑。之所以部件包围盒的大小和整个模型的大小一样,是因为所有部件的顶点都是共享了一个顶点数组,该顶点数组包括了所有的部件顶点的集合。而计算部件的包围盒的时候,是通过所有顶点来进行计算的。而部件的实际的包围盒应该是只能包括自己本身的顶点,因此我们重新构建一个包围盒的计算方法。代码如下:
1mono.Node.prototype.computeRealBoundingBox = function(){ 2 if (this.realBoundingBox == null) { 3 this.realBoundingBox = new TGL.BoundingBox(); 4 } 5 const vertices = this.vertices, 6 faces = this.faces, 7 realVertices = []; 8 9 let indices = [],set = new Set(); 10 for(let i =0; i < faces.length;i ++){ 11 let {a,b,c,d} = faces[i]; 12 set.add(a); 13 set.add(b); 14 set.add(c); 15 if(d != null){ 16 set.add(d); 17 } 18 } 19 indices = [...set]; 20 21 indices.forEach((i) =>{ 22 realVertices.push(vertices[i]); 23 }); 24 this.realBoundingBox.setFromPoints(realVertices); 25 return this.realBoundingBox; 26}
通过包围盒得到了部件的实际中心点之后,可以使用先平移再旋转的方法来进行旋转操作。大致代码如下:
1 function rotate(obj,axisDir){ 2 let bbox = (obj._realBoundingBox = obj._realBoundingBox || obj.computeRealBoundingBox()); 3 let center = bbox .center(); 4 obj.rotateFromAxis(axisDir,center,0.2); 5 }
首先获取部件真实的包围盒。然后调用对象的rotateFromAxis方法进行旋转,该方法内部的第一个参数指定旋转的轴,第二个参数指定旋转的中心点,第三个参数指定了旋转的角度;该方法内部实际上是先进行平移操作,在进行旋转操作,具体实现上通过矩阵的变换叠加来实现。看看效果:
看起来好像完美,直到遇到这个模型:
感觉不对了,好像没有绕着中心点旋转。这是因为通过包围盒计算模型中心点的时候,模型需要是一个基于中心点可以任意均分的形状。上面第一个张图和第二张图就是这样的(限于x轴方向),而第三张图中的模型明显不是。
那应该如何计算第三种模型的中心点呢?答案是使用包围球。
包围球(BoundingSphere)
首先,构造一个方法,计算模型的包围球体。
1 mono.Node.prototype.computeRealBoundingSphere = function() { 2 if (this.realBoundingSphere == null) { 3 this.realBoundingSphere = new TGL.BoundingSphere(); 4 } 5 const vertices = this.vertices, 6 faces = this.faces, 7 realVertices = [] 8 9 let indices = [],set = new Set(); 10 for(let i =0; i < faces.length;i ++){ 11 let {a,b,c,d} = faces[i]; 12 set.add(a); 13 set.add(b); 14 set.add(c); 15 if(d != null){ 16 set.add(d); 17 } 18 } 19 indices = [...set]; 20 21 indices.forEach((i) =>{ 22 realVertices.push(vertices[i]); 23 }); 24 25 var center = new mono.Vec3(), 26 vertexNum = realVertices.length; 27 realVertices.forEach((v) =>{ 28 center.x += v.x / vertexNum; 29 center.y += v.y / vertexNum; 30 center.z += v.z / vertexNum; 31 }) 32 33 this.realBoundingSphere.setFromCenterAndPoints(center, realVertices); 34 35 return this.realBoundingSphere; 36 }
计算出部件的包围球之后,获取中心点,后面就是旋转模型,旋转的方法和前面说的一样。
1 function rotate(obj,axisDir){ 2 let bSphere = (obj._realBoundingSphere = obj._realBoundingSphere ||obj.computeRealBoundingSphere()); 3 let center = bSphere.center; 4 obj.rotateFromAxis(axisDir,center,0.2); 5 }
使用包围球后的效果:
可以看到中心旋转的效果出来了。
整体效果
最后,上一张整体的三维组态运行效果图:
总结
这种整体模型下部件进行动画的技巧,减少了需要建模师的工作量,同时也提高了开发人员的开发效率和开发的整体复杂度。
同时,这种方式有利于我们在三维组态编辑器中进行模型的整体导入和动画配置。因为如果是分离的模型,我们还需要在编辑器中导入多个模型再进行模型的拼装,位置对齐等难度很大。