前言
在工业互联网以及物联网的影响下,人们对于机械的管理,机械的可视化,机械的操作可视化提出了更高的要求。如何在一个系统中完整的显示机械的运行情况,机械的运行轨迹,或者机械的机械动作显得尤为的重要,因为这会帮助一个不了解这个机械的小白可以直观的了解机械的运行情况,以及机械的所有可能发生的动作,对于三一或者其它国内国外重工机械的公司能够有一个更好的展示或者推广。
挖掘机,又称挖掘机械(excavating machinery),从近几年工程机械的发展来看,挖掘机的发展相对较快,挖掘机已经成为工程建设中最主要的工程机械之一。所以该系统实现了对挖掘机的 3D 可视化,在传统行业一般都是基于 Web SCADA 的前端技术来实现 2D 可视化监控,而且都是 2D 面板部分数据的监控,从后台获取数据前台显示数据,但是对于挖掘机本身来说,挖掘机的模型,挖掘机的动作,挖掘机的运行可视化却是更让人眼前一亮的,所以该系统对于挖机的 3D 模型做出了动作的可视化,大体包括以下几个方面:
- 前进后退 — 用户可以通过键盘 wasd 实现前后左右,或者点击 2D 界面 WASD 来实现挖机的前进后退。
- 机身旋转 — 用户可以通过键盘左右键实现机身的旋转,或者点击 2D 界面 < > 来实现挖机机身的旋转。
- 大臂旋转 — 用户可点击 2D 界面第一个滑块部分实现大臂的旋转。
- 小臂旋转 — 用户可点击 2D 界面第二个滑块部分实现小臂的旋转。
- 挖斗挖掘 — 用户可点击 2D 界面第三个滑块部分实现挖斗部分的旋转挖掘。
- 挖机动画 — 用户可点击 2D 界面铲子图标,点击之后系统会把挖机本身几个动画做一个串联展示。
本篇文章通过对挖掘机可视化场景的搭建,挖机机械动作代码的实现进行阐述,帮助我们了解如何使用 HT 实现一个挖掘机的可视化。
预览地址:基于 HTML5 WebGL 的挖掘机 3D 可视化应用 http://www.hightopo.com/demo/ht-excavator/
界面效果预览
挖机机械运动效果
通过上面 gif 图片可以看出挖掘机的几个主要动作。
挖机挖斗运动效果
滑动页面的第三个滑杆控制挖斗的旋转挖掘。
挖机机身运动
通过上面 gif 图片可以看出挖掘机的前进后退以及机身旋转几个运动。
场景搭建
该 3D 场景中所有形状都是用 HT 内部的墙面工具进行构建,通过设置墙面透明属性 shape3d.transparent 为 true 以及对构建出的墙面进行贴图来构造出场景中的类似建筑的显示效果,具体的样式可以参考 HT 的 风格手册,场景效果:
通过上图我们可以看到场景中有许许多多的墙面建筑,所以它们有许多相同的地方,例如样式以及贴图都是一样的,所以在 HT 中可以通过批量的操作对这些墙面进行处理,批量的意思指的是在当前未处理的情况下的墙面图元是一个个独立绘制的模型,所以性能会比较差,而当一批图元聚合成一个大模型进行一次性的绘制时,则会极大提高 WebGL 刷新性能,这就是批量所以要做的事情,具体可以参考 HT 的 批量手册。
该系统 2d 面板部分则也是通过 HT 的矢量进行绘制,面板部分主要包括当前挖机的作业情况,工作时间,保修信息,故障信息等,通过二维的方式展示这些数据信息,面板截图:
机械运动代码分析
该系统中挖机的动作是十分的重要和关键的,大小臂运动时液压杠该如何运动,挖斗运动时液压杆,旋转点零件,以及连接到挖斗上的零部件如何联动起来是关键点,机械动画中用到大部分数学知识进行点面位置的计算,以下是几个关键的数学知识点作为基础:
在数学中,向量(也称为几何向量、矢量),指具有大小和方向的量。它可以形象化地表示为带箭头的线段。系统中会通过向量的叉乘算出与某个面垂直的向量即法向量,在计算挖斗旋转时需要计算出与挖斗面垂直的法向量来进行点的计算,HT 中封装了 ht.Math 的数学函数,里面的 ht.Math.Vector2 指的即为二维向量,ht.Math.Vector3 则为三维的向量,可以传入三个数值进行初始化向量,向量的原型中有 cross 方法用来计算两个向量的法向量,例如以下伪代码:
1 var Vector3 = ht.Math.Vector3; 2 var a = new Vector3([10, 10, 0]); 3 var b = new Vector3([10, 0, 0]); 4 var ab = a.clone().cross(b);
以上代码中 ab 即为计算法向量,a.clone 是为了避免 cross 运算会修改原本的 a 内容,所以克隆出一个新的向量进行叉乘,以下为示意图:
挖斗机械运动分析
在进行挖斗部分的机械代码时会将挖斗的位置以及挖斗所有连接点的设备转化为相对于某个节点的相对位置,例如节点 A 在世界中的坐标为 [100, 100, 100],世界中还有一个节点 B,而且节点 B 的坐标为 [10, 10, 10] 则节点 A 相对于节点 B 的相对位置即为 [90, 90, 90],因为在计算挖斗的位置时,挖机可能此时已经运动到某一点或者旋转到某一个轴,所以此时不能使用相对世界的坐标,需要使用相对挖机机身的相对坐标来进行计算,代码中提供了 toLocalPostion(node, worldPosition) 用来将世界的坐标 worldPosition 转化为相对 node 的相对坐标,以下为代码实现:
1 var Matrix4 = ht.Math.Matrix4, 2 Vector3 = ht.Math.Vector3; 3 var mat = new Matrix4().fromArray(this.getNodeMat(g3d, node)), 4 matInverse = new Matrix4().getInverse(mat), 5 position = new Vector3(worldPosition).applyMatrix4(matInverse); 6 return position.toArray();
该函数的返回值即为相对坐标,挖机中需要转化的坐标为连接着挖斗以及小臂的两个零部件,系统中用 armHinge 以及 bucketHinge 来分别表示小臂枢纽以及挖斗枢纽这两个零部件,可以从侧面来看挖斗的动作,从下图可以看出,关键点是算出交点 P 的坐标,交点 P 的坐标则是以 armHinge 与 bucketHinge位置为圆心,armHinge 与 bucketHinge 的长度为半径的两个圆的交点,而且这两个圆的圆心在挖斗旋转的过程中是不断变化的,所以需要通过数学计算不断算出交点的位置,以下为示意图:
通过上图可以知道交点的位置有两个 p1 以及 p2,程序中通过计算圆心 1 与圆心 2 构成的向量 c2ToC1,以下为伪代码:
1 var Vector2 = ht.Math.Vector2; 2 var c2ToC1 = new Vector2({ x: c1.x, y: c1.y }).sub(new Vector2({ x: c2.x, y: c2.y }));
c1 和 c2 为 armHinge 以及 bucketHinge 的圆心坐标,接下来是计算圆心 2 与点 p1 以及 p2 构成的向量 c2ToP1 以及 c2ToP2,以下为伪代码:
1 var Vector2 = ht.Math.Vector2; 2 var c2ToP1 = new Vector2({ x: p1.x, y: p1.y }).sub(new Vector2({ x: c2.x, y: c2.y })); 3 var c2ToP2 = new Vector2({ x: p2.x, y: p2.y }).sub(new Vector2({ x: c2.x, y: c2.y }));
通过上述操作我们可以获得三个向量 c2ToC1,c2ToP1,c2ToP2 所以我们可以用到我上述讲的向量叉乘的概念进行 p1 与 p2 点的选取,通过向量 c2ToC1 与 c2ToP1,以及向量 c2ToC1 与 c2ToP2 分别进行叉乘得到的结果肯定一个是大于 0 一个小于 0,二维向量的叉乘可以直接把它们视为 3d 向量,z轴补 0 的三维向量,不过二维向量叉乘的结果 result 不是向量而是数值,如果 result > 0 时,那么 a 正旋转到 b 的角度为 <180°,如果 k < 0,那么 a 正旋转到 b 的角度为 >180°,如果 k = 0 那么a,b向量平行,所以通过上面的理论知识我们可以知道结果肯定是一个大于 0 一个小于 0,我们可以在程序中测下可以知道我们需要获取的是大于 0 的那个点 P1,所以每次可以通过上述的方法进行两个交点的选择。
以下为挖斗部分动画的执行流程图:
通过上述运算之后我们可以获取到最终需要的点 P 坐标,点 P 坐标即为挖斗与小臂连接部分的一个重要点,获取该点之后我们可以通过 HT 中提供的 lookAtX 函数来实现接下来的操作,lookAtX 函数的作用为让某个物体看向某一点,使用方式如下:
1 node.lookAtX(position, 'bottom');
node 即为需要看向某一个点的节点,position 为看向的点的坐标,第二个参数有六个枚举值可以选择,分别为 ‘bottom’,’back’,’front’,’top’,’right’,’left’,第二个参数的作用是当我们需要把某个物体看向某一个点的时候我们也要指定该物体的哪一个面看向该点,所以需要提供第二个参数来明确,获取到该函数之后我们可以通过将 bucketHinge 看向点 P,armHinge 看向点 P,就可以保持这两个连接的设备永远朝向该点,以下为部分伪代码:
1 bucketHinge.lookAtX(P, 'front'); 2 armHinge.lookAtX(P, 'bottom');
所以通过上述操作之后我们已经把挖斗部分的两个关键零件的位置已经摆放正确,接下来是要正确的摆放与挖斗连接的小臂上液压部分的位置,下一部分为介绍该节点如何进行摆放。
液压联动分析
在场景中我们可以看到液压主要分为两个部分,一部分为白色的较细的液压杆,一部分为黑色的较厚的液压杆,白色的液压杆插在黑色的液压杆中,所以在小臂或者挖斗旋转的过程中我们要保持两个节点始终保持相对的位置,通过上一步骤中我们可以知道 lookAtX 这个函数的作用,所以在液压杆部分我们也是照样用该函数来实现。
在上一步我们获取到了挖斗旋转过程中的关键点 P,所以在挖斗旋转的过程我们小臂上的液压杆也要相应的进行变化,具体的操作就是将小臂的白色液压杆的位置设置为上步中计算出来的点 P 的位置,当然需要把白色液压杆的锚点进行相应的设置,之后让白色液压杆 lookAt 黑色液压杆,同时让黑色液压杆 lookAt 白色液压杆,这样下来两个液压杆都在互相看着对方,所以它们呈现出来的效果就是白色液压杆在黑色液压杆中进行伸缩,以下为伪代码:
1 bucketWhite.p3(P); 2 bucketWhite.lookAtX(bucketBlack.p3(), 'top'); 3 bucketBlack.lookAtX(P, 'bottom');
代码中 bucketWhite 节点即为小臂上白色液压杆,bucketBlack 节点为小臂上黑色液压杆,通过以上的设置就可以实现伸缩的动画效果,以下为液压的运行图:
同理挖机身上的大臂的液压动作以及机身与大臂连接部分的液压动作都是使用上面的方法来实现,以下为这两部分的代码:
1 rotateBoom: (rotateVal) = >{ 2 excavatorBoomNode.setRotationX(dr * rotateVal); 3 let archorVector = [0.5 - 0.5, 0.56 - 0.5, 0.22 - 0.5]; 4 let pos = projectUtil.toWorldPosition(g3d, excavatorBoomNode, archorVector); 5 boomWhite.lookAtX(boomBlack.p3(), 'bottom'); 6 boomBlack.lookAtX(pos, 'top'); 7 }, 8 rotateArm: (rotateVal) = >{ 9 projectUtil.applyRelativeRotation(excavatorArmNode, excavatorBoomNode, -rotateVal); 10 let archorVector = [0.585 - 0.5, 0.985 - 0.5, 0.17 - 0.5]; 11 let pos = projectUtil.toWorldPosition(g3d, excavatorArmNode, archorVector); 12 armWhite.lookAtX(armBlack.p3(), 'bottom'); 13 armBlack.lookAtX(pos, 'top'); 14 }
我将两部分的运动封装为两个函数 rotateBoom 以及 rotateArm 分别是大臂与机身连接处的液压运动与大臂上的液压运动,在该部分中为了精确的获取看向的点,我通过 toWorldPosition 方法将相对坐标转化为世界坐标,相对坐标为黑白液压杆的锚点坐标,转化为相对大臂或者机身的世界坐标。
基本运动分析
挖机的基本运动包括前进后退,机身旋转,这一部分会相对上面的运动简单许多,在 HT 的三维坐标系中,不断修改挖机机身的 x,y,z 的坐标值就可以实现挖机的前进后退,通过修改机身的 y 轴旋转角度则可以控制机身的旋转,当然挖机身体上的所有其它零部件需要吸附在机身身上,当机身进行旋转时其它零部件则会进行相应的旋转,在进行前进的时候挖机底部的履带会进行对应的滚动,当然履带我们这边是用了一个履带的贴图贴在上面,当挖机前进的时候修改贴图的偏移值就可以实现履带的滚动,修改偏移值的伪代码如下:
1 node.s('shape3d.uv.offset', [x, y]);
上面的 x,y 分别为 x 轴与 y 轴方向的偏移值,在挖机前进后退的过程中不断修改 y 的值可以实现履带的滚动效果,具体的文档说明可以查看 3D手册
在挖机前进后退的过程中我们可以 wasd 四个键同时按下,并且可以对按键进行一直的响应,在 js 中可以通过 document.addEventListener(‘keydown’, (e) => {}) 以及 document.addEventListener(‘keyup’, (e) => {}) 进行监听,但是这只能每次执行一次需要执行的动作,所以我们可以在外部起一个定时器,来执行 keydown 时候需要不断执行的动作,可以用一个 keyMap 来记录当前已经点击的按键,在 keydown 的时候纪录为 true 在 keyup 的时候记录为 false,所以我们可以在定时器中判断这个 bool 值,当为 true 的时候则执行相应的动作,否则不执行,以下为对应的部分关键代码:
1 let key_pressed = { 2 65 : { 3 status: false, 4 action: turnLeft 5 }, 6 87 : { 7 status: false, 8 action: goAhead 9 }, 10 68 : { 11 status: false, 12 action: turnRight 13 }, 14 83 : { 15 status: false, 16 action: back 17 }, 18 37 : { 19 status: false, 20 action: bodyTurnLeft 21 }, 22 39 : { 23 status: false, 24 action: bodyTurnRight 25 } 26 }; 27 setInterval(() = >{ 28 for (let key in key_pressed) { 29 let { 30 status, 31 action 32 } = key_pressed[key]; 33 if (status) { 34 action(); 35 } 36 } 37 }, 38 50); 39 document.addEventListener('keydown', (event) = >{ 40 let keyCode = event.keyCode; 41 key_pressed[keyCode] && (key_pressed[keyCode].status = true); 42 event.stopPropagation(); 43 }, 44 true); 45 document.addEventListener('keyup', (event) = >{ 46 let keyCode = event.keyCode; 47 key_pressed[keyCode] && (key_pressed[keyCode].status = false); 48 event.stopPropagation(); 49 }, 50 true);
从上面代码可以看出我在 key_pressed 变量中记录对应按键以及按键对应的 action 动作,在 keydown 与 keyup 的时候对应修改当前 key 的 status 的状态值,所以可以在 Interval 中根据 key_pressed 这个变量的 status 值执行对应的 action 动作,以下为执行流程图:
HT 的轻量化,自适应让当前系统在手机端也能流畅的运行,当然目前移动端与电脑端的 2D 图纸部分是加载不同的图纸,在移动端的 2D 部分只留下操作挖机的操作部分,其它部分进行了相应的舍弃,不然在移动端小屏幕下无法展示如此多的数据,在 3D 场景部分都是共用同一个场景,通过场景搭建部分的批量操作使得 3D 在手机端也十分流畅的运行,以下为手机端运行截图:
总结
物联网已经融入了现代生活,通过内嵌到机械设备中的电子设备,我们能够完成对机械设备的运转、性能的监控,以及对机械设备出现的问题进行及时的预警。在该系统 2D 面板监控部分就是对采集过来的数据进行可视化的展示,而且我们可以借助大数据和物联网技术,将一台台机械通过机载控制器、传感器和无线通讯模块,与一个庞大的网络连接,每挥动一铲、行动一步,都形成数据痕迹。大数据精准描绘出基础建设开工率等情况,成为观察固定资产投资等经济变化的风向标。所以在实现上述挖机动作之后,通过与挖机传感器进行连接之后,可以将挖掘机此时的真实动作通过数据传递到系统,系统则会根据动作进行相应的真实操作,真正实现了挖机与网络的互联互通。
程序运行截图: