前言

工业互联网,物联网,可视化等名词在我们现在信息化的大背景下已经是耳熟能详,日常生活的交通,出行,吃穿等可能都可以用信息化的方式来为我们表达,在传统的可视化监控领域,一般都是基于 Web SCADA 的前端技术来实现 2D 可视化监控,本系统采用 Hightopo 的 HT for Web 产品来构造轻量化的 3D 可视化场景,该 3D 场景从正面展示了一个地铁站的现实场景,包括地铁的实时运行情况,地铁上下行情况,视频监控,烟雾报警,电梯运行情况等等,帮助我们直观的了解当前的地铁站。

系统中为了帮助用户更直观友好的浏览当前地铁站,提供了三种交互模式:

  • 第一人称模式 -- 操作就类似行人或车在行进的效果,可以通过键盘鼠标控制前进后退。
  • 自动巡检模式 -- 该模式下用户不需要任何操作,场景自动前进后退来巡查当前地铁站的场景。
  • 鼠标操作模式 -- 左键旋转场景,右键平移场景。

本篇文章通过对地铁站可视化场景的搭建,动画代码的实现,交互模式的原理解析,以及主要功能点的实现进行阐述,帮助我们了解如何使用 

地铁从站外开到站内的效果为透明度逐渐增加,速度逐渐降低。

漫游效果

上述为自动巡检的漫游效果,场景自动进行前进旋转。

监控设备交互效果

当我们点击场景中的监控设备时可以查看当前设备的运行情况,运行数据等信息。

场景搭建

该系统中的大部分模型都是通过 3dMax 建模生成的,该建模工具可以导出 obj 与 mtl 文件,在 HT 中可以通过解析 obj 与 mtl 文件来生成 3d 场景中的所有复杂模型,当然如果是某些简单的模型可以直接使用 HT 来绘制,这样会比 obj 模型更轻量化,所以大部分简单的模型都是采用 HT for Web 产品轻量化 HTML5/WebGL 建模的方案,具体的解析代码如下:

复制代码
复制代码
 1 // 分别为 obj 文件地址,mtl 文件地址 2 ht.Default.loadObj('obj/metro.obj', 'obj/metro.mtl', { 3     center: true, 4     // 模型是否居中,默认为 false,设置为 true 则会移动模型位置使其内容居中 5     r3: [0, -Math.PI / 2, 0], 6     // 旋转变化参数,格式为 [rx, ry, rz] 7     s3: [0.15, 0.15, 0.15], 8     // 大小变化参数,格式为 [sx, sy, sz] 9     finishFunc: function(modelMap, array, rawS3) {10         if (modelMap) {11             ht.Default.setShape3dModel('metro', array); // 注册一个名字为 metro 的模型12         }13     }14 });
复制代码
复制代码

上面通过加载 obj 模型之后注册了一个名字为 metro 的模型,之后如果要使用该模型可以通过以下代码来实现:

复制代码
1 var node = new ht.Node();2 node.s({3     'shape3d': 'metro'4 });
复制代码

上面代码新建了一个 node 对象,通过设置 style 对象的 shape3d 属性可以把模型名称为 metro 用到该 node 对象上去,之后便是我们场景中看到的地铁列车模型。

动画代码分析

地铁动画代码的实现分析

场景中地铁的运行是通过 HT 提供的调度插件来实现,调度的具体用法可以参考 HT for Web 的

通过上图可以知道地铁在 3D 场景中的坐标系,如果要实现地铁的移动则只需要将地铁往图中所示红色箭头的方向进行移动,即 x 轴的方向,通过 setX 这个方法不断的修改地铁的位置达到地铁行进的目的,代码中通过 getSpeedByX 以及 getOpacityByX 两个方法来不断获取此时的列车速度以及列车透明度,以下为关键代码实现:

复制代码
复制代码
 1 let metroTask = { 2     interval: 50, 3     // 每五十秒执行一次 4     action: (data) = >{ // 即上文所提回调函数 5         // 判断当时传进来的节点是否为地铁列车节点 6         if (data === currentMetro) { 7             // 获取地铁此时的 X 轴位置以及行进的方向 8             let currentX = data.getX(), 9             direction = data.a('direction');10             // 根据当前的 X 轴位置获取当前的列车速度11             let speed = this.getSpeedByX(currentX);12             // 根据当前的 X 轴位置获取当前的列车透明度13             let opacity = this.getOpacityByX(currentX);14             // 判断此时 X 轴位置是否超过某个值 即地铁是在某个范围内移动15             if (Math.abs(currentX) <= 5000) {16                 // 设置当前的透明度17                 opacity !== 1 ? currentMetro.s({18                     'shape3d.transparent': true,19                     'shape3d.opacity': opacity20                 }) : currentMetro.s({21                     'shape3d.transparent': false22                 });23                 // 设置当前的 X 轴位置24                 data.setX(currentX + direction * speed);25                 // 判断此时地铁的速度为 0,所以此时应该执行开门的动画26                 if (speed === 0) this.doorAnimation(currentMetro, direction);27             }28             // 右方向地铁开到头,进行复位29             if (currentX > 5000 && direction === 1) {30                 currentMetro = leftMetro;31                 currentMetro.setX(5000);32             }33             // 左方向地铁开到头,进行复位34             if (currentX < -5000 && direction === -1) {35                 currentMetro = rightMetro;36                 currentMetro.setX( - 5000);37             }38         }39     }40 };41 dm3d.addScheduleTask(metroTask);
复制代码
复制代码

通过以上代码可以知道地铁在运行的过程中,主要通过修改地铁的 x 轴位置来产生前进的动画,并且需要让地铁在某个区间内进行运动,需要判断边界,而且为了模拟出真实的效果需要根据地铁当前的位置不断获取当前的列车速度以及列车透明度,以下为流程图:

上图所示的为地铁进站时候的流程,当地铁停靠完毕关门后需要进行出站,此时我们只需要把地铁位置重新设置一下不为 0 即可,以下为部分代码实现:

1 currentMetro.setX(direction * 10); // 设置出站列车的位置

当执行上面那句代码之后上方的 metroTask 调度任务执行到 getSpeedByX 这个方法之后获取到的 speed 速度不为 0,因此此时会继续执行地铁行进的动画,此时的速度就是由慢至快,透明度由深至浅。以下为开门动画执行流程:

自动巡检代码的实现分析

系统中自动巡检的实现主要是通过修改 3D 场景中的 eye 以及 center 的值,HT 中提供了 rotatewalk 两个方法来控制视角的旋转以及视角的行进,rotate 方法在非第一人称模式时,旋转是以 center 为中心进行旋转,也就是围绕中心物体旋转,当为第一人称时旋转以 eye 为中心进行旋转,也就是旋转眼睛朝向方向。walk 函数同时改变 eye 和 center 的位置,也就是 eye 和 center 在两点建立的矢量方向上同时移动相同的偏移量。该系统中我没有采用 rotate 函数而是自己实现了视角的旋转,因为原本的 rotate 函数旋转某个角度会马上旋转过去而不会有一个旋转的过程,所以我重新实现了旋转的方法,该系统中视角旋转是通过不断修改 center 的数值来实现,具体实现过程原理如下图所示:

部分实现代码如下:

复制代码
复制代码
 1 rotateStep() { 2     // 即上图辅助点 C 3     let fromCenter = this.fromCenter; 4     // 即上图 B 点 5     let toCenter = this.toCenter; 6     // 每帧转一度 7     let rotateValue = this.rotateFrame || Math.PI / 180; 8     // 辅助点 C 与 B 点之间建立一个方向向量 9     let centerVector = new ht.Math.Vector2(toCenter.x - fromCenter.x, toCenter.y - fromCenter.y);10     let centerVectorLength = centerVector.length();11     // 此时旋转百分比12     let rotatePercent = rotateValue * this.stepNum / this.curRotateVal;13     if (rotatePercent >= 1) {14         rotatePercent = 1;15         this.stepNum = -2;16     }17     let newLength = rot