前言
在当今 工业4.0 新时代的推动下,不仅迎来了 工业互联网 的发展,还开启了5G时代的新次元。而伴随着带宽的提升,网络信息飞速发展,能源管控上与实时预警在工业互联网中也占着举足轻重的地位,而对于高炉炼铁的发展上来看,目前已完成国内260座高炉的数字化和智能化落地,并推动炼铁大数据平台在俄罗斯、越南、伊朗、印尼等“一带一路”国家钢铁企业中应用,充分体现了高炉智能化大屏产业应运而生。我们将使用Hightopo(以下简称 HT )的HT for Web产品上的web组态跟大家介绍一下通过 2/3D 融合搭建的高炉炼铁厂可视化系统。
HT 可以快速实现丰富的 2D 组态和3D 组态效果,可以根据需求发挥自己的想象,玩转很多新奇的功能,并且通过优势互补的作用下,完善出一套完整的可视化系统解决方案。所以在可视化系统的实现上,3D 场景采用以HT 轻量化 HTML5/WebGL 建模的方案,实现快速建模、运行时轻量化到甚至手机终端浏览器即可 3D 可视化运维的良好效果;而在对应的 2D 图纸上,使用特有的矢量,在各种比例下不失真,加上布局机制,解决了不同屏幕比例下的展示问题。
本文将从以下三个方面与大家分享高炉炼铁厂在大屏展示上的实现:
1、页面搭建:介绍基础的 2D 图纸与 3D 场景融合的项目搭建;
2、数据对接:进行面板数据的对接展示;
3、动画实现:铁水罐车运输、传送带运送以及场景漫游的实现;
界面简介及效果预览
在整个高炉炼铁厂可视化系统的 2D 面板上,呈现了昨日历史与今日实时的一些重要预警数据,在管控上能起到实时监控的作用,也能与历史数据进行对比,从而使生产与安全达到预期的预警效果;其次 3D 场景通过轻量化的模型呈现出一座高炉炼铁厂的基本运作流程以及铁水罐车运送钢铁的动画,加上环绕的漫游效果,起到全方位的实时监控状态的变化。
代码实现
一、页面搭建
在内容实现上,采用了 HT 轻量化模型以及 web 组态,以 2/3D 结合的方式,通过的 json 反序列化得到 2D 图纸和 3D 场景的完整呈现。首先会通过创建 ht.graph.GraphView和ht.graph3d.Graph3dView来呈现 2D 和 3D 的内容。 2D 视图组件和 3D 视图组件进行 deserialize() 反序列化对应的 url 寄存的 json 呈现出场景与图纸的内容,两者通过对数据模型 DataModel 里的子元素设置标签来进行数据绑定,实现功能上的展示。
// 三维拓扑视图 let g2d = new ht.graph.GraphView(); let g3dDm = g2d.dm(); // 三维拓扑视图 let g3d = new ht.graph3d.Graph3dView(); let g3dDm = g3d.dm(); // 2D 视图组件和 3D 视图组件进行反序列化 g2d.deserialize('displays/index.json'); g3d.deserialize('scenes/index.json');
在内容呈现上还需要将组件加入到 body 下,一般 2/3D 结合的项目上,都会使用 2D 组件加入到 3D 组件的根 div 下,然后 3D 组件再加入到 body下的方式实现面板与场景的加载。
// 将 3D 组件加入到 body 下 g3d.addToDOM(); // 将 2D 组件加入到 3D 组件的根 div 下,父子 DOM 事件会冒泡,这样不会影响 3D 场景的交互 g2d.addToDOM(g3d.getView());
同时,在交互与呈现上改变了一些实现方式。例如,修改了左右键的交互方式,设置左键点击旋转 3D 场景,右键点击为 pan 抓图的场景移动方式。其次,在点击 2D 有点到图元像素时,我们希望不触发 3D 的交互,例如在对 2D 面板表格中用滚轮滑动的时候,会触发 3D 场景的缩放,这里通过监听 moudedown、touchstart 和 wheel 三种交互来进行控制,对于 wheel 的监听方式,为了保证兼容性就通过封装一个 getWheelEventName() 的方法来得到事件名。
// 修改左右键交互方式 let mapInteractor = new ht.graph3d.MapInteractor(this.g3d); g3d.setInteractors([mapInteractor]); // 设置修改最大仰角为 PI / 2 mapInteractor.maxPhi = Math.PI / 2; // 避免 2D 与 3D 交互重叠 let div2d = g2d.getView(); const handler = e => { if (g2d.getDataAt(e)) { e.stopPropagation(); } }; div2d.addEventListener('mousedown', handler); div2d.addEventListener('touchstart', handler); div2d.addEventListener(getWheelEventName(div2d), handler); // 在一个 HTMLElement 上,可能支持下面三个事件的一种或者两种,但实际回调只会回调一种事件,优先回调标准事件,触发标准事件后,不会触发兼容性事件 function getWheelEventName(element) { if ('onwheel' in element) { // 标准事件 return 'wheel'; } else if (document.onmousewheel !== undefined) { // 通用旧版事件 return 'mousewheel'; } else { // 旧版 Firefox 事件 return 'DOMMouseScroll'; } }
二、数据对接
在 2D 面板的呈现上,会有许多的图表数据信息,我们可以通过访问后台数据接口得到数据,然后在 2D 或者 3D 对应的组件上取得相应的数据模型 dataModel,通过对数据模型里设置唯一的标识 tag 的子节点进行对接数据就可以了。例如现在我们要对 2D 面板的数据进行绑定,我们只需要通过 2D 组件的 g2d 得到数据模型,通过 g2d.dm().getDataByTag(tag) 就可以得到设置有唯一标识的 tag 节点,来对接数据或者设置状态展示了。
对于数据接口的获取,可以运用主流的 jQuery 框架下的ajax、基于 promise 的 HTTP 库的axios 通过轮询调用接口实时获取数据或者使用 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议WebSocket,可以双向进行数据传输,在选择运用上可以匹配自己的实现需求,而本系统是采用通过 axios 调用接口获取实时数据。
// 昨日利用系数数据对接 axios.get('/yesterdayUse').then(res => { setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2)); }); // 昨日燃料比数据对接 axios.get('/yesterdayFuel').then(res => { setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2)); }); // 昨日入炉品位数据对接 axios.get('/yesterdayIn').then(res => { setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2)); }); // 昨日燃气利用率数据对接 axios.get('/yesterdayCoal').then(res => { setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2)); }); // 实时警报信息面板表格轮询载入数据进行滚动播放 this.addTableRow(); setInterval(() => { this.addTableRow(); }, 5000);
通过 axios 轮询调用接口,实时获取安全指数和实时数据信息(风量、风温和富氧量):
requestData() { let dm = this.view.dm(); // 安全指数数据对接并载入圆环动画 axios.get('/levelData').then(res => { setBindingDatasWithAnim(dm, res, 800, v => Math.round(v)); }); // 实时数据(风量、风温和富氧量)数据对接并载入进度条动画 axios.post('/nature', [ 'windNumber', 'windTemp', 'oxygenNumber' ]).then(res => { setBindingDatasWithAnim(dm, res, 800, v => parseFloat(v.toFixed(1))); }); }
对接数据后,实现一些圆环或者进度条值的增减动画,其本质上是运用 HT 自带的动画函数 ht.Default.startAnim(),通过判断数据绑定的属性后,设定新值与旧值差额的范围动画,然后用户定义函数 easing 参数通过数学公式来控制动画的运动的快慢,例如匀速变化、先慢后快等效果。
这里通过动画函数封装了一个差值的动画效果,参数如下:
} // 默认通过取值器 getter 得到数据绑定的值 else { oldValue = node[ht.Default.getter(name)](); } // 设置新旧值的差额 let range = value - oldValue; // 执行动画函数 ht.Default.startAnim({ duration: duration, easing: function (t) { return 1 - (--t) * t * t * t; }, action: (v, t) => { // 新值增长的动画范围 let newValue = oldValue + range * v; // 判断有格式则制定数据格式 if (format) { newValue = format(newValue); } // 判断数据绑定为自定义属性 attr 后设定新值 if (accesstype === 'a') { node.a(name, newValue); } // 判断数据绑定为样式属性 style 后设定新值 else if (accesstype === 's') { node.s(name, newValue); } // 默认通过存值器 setter 设置数据绑定的新值 else { node[ht.Default.setter(name)]()(node, newValue); } } }); }
我们时常会在公开的预警场合或者宣传场合看见轮播滚动的数据信息,采用这种方法在公示的同时也不会遗漏掉任何一条数据信息,如果搭配上一些例如淡入淡出的过场效果,更会吸引关注的眼球。而对于实时警报信息的面板表格的实现,也是在添加新数据时,实现了一种过渡的 UI 交互上的沉浸感,主要还是运用了 HT 自带的动画函数 ht.Default.startAnim(),横向通过滚动 100 宽度并数据透明度慢慢浮现,纵向采用向下偏移一行表格行高 54 来添加新的警报信息。
addTableRow() { // 获取表格节点 let table = this.right3; // 通过 axios 的 promise 请求接口数据 axios.get('getEvent').then(res => { // 获取表格节点滚动信息的数据绑定 let tableData = table.a('dataSource'); // 通过向 unshift() 方法可向滚动信息数组的开头添加一个或更多元素 tableData.unshift(res); // 初始化表格的纵向偏移 table.a('ty', -54); // 开启表格滚动动画 ht.Default.startAnim({ duration: 600, // 动画执行函数 action action: (v, t) => { table.a({ // 通过添加数据后,横向滚动 100 'firstRowTx': 100 * (1 - v), // 第一行行高出现的透明度渐变效果 'firstRowOpacity': v, // 纵向偏移 54 的高度 'ty': (v - 1) * 54 }); } }); }); }
三、动画实现
在静态的场景以及面板下,很难直观地去体现一个 2/3D 嵌合的系统的优越性。动画却是赋予生命灵魂的所在,一个恰到好处的 UI 动画设计可以使面板的交互体验鲜活起来,而在 3D 场景中,通过一组简单形象的铁水罐车运输和传送带运送可以让人清晰地明白生产运输的流程,对于模型建筑的管控,利用好视角切入点,我们可以设置全方位的沉浸式漫游巡视。综上,通过轻量模型场景与矢量组件面板的优势叠加,可以呈现出一套灵活的高炉炼铁厂生产预警系统。
在漫游巡视下,为了更全方位地体现场景,我们通过裁剪的方式来显示和隐藏两侧的面板数据,以下以隐藏面板的裁剪动画为例:
hidePanel() { // 将左侧数据绑定裁剪的子元素存放进一个数组里 let leftStartClipIndexs = (() => { let arr = []; for (let i = 1; i <= 4; i++) arr.push(this['left' + i].s('clip.percentage')); return arr; })(); // 将右侧数据绑定裁剪的子元素存放进一个数组里 let rightStartClipIndexs = (() => { let arr = []; for (let i = 1; i <= 3; i++) arr.push(this['right' + i].s('clip.percentage')); return arr; })(); // 设置面板裁剪的延迟时间,使得视觉上更有层次感 let delayArrays = [400, 800, 1200, 1600]; // 动画执行函数 let action = (index) => { ht.Default.startAnim({ duration: 700, easing: Easing.swing, action: (v, t) => { this['left' + index].s('clip.percentage', leftStartClipIndexs[index - 1] + (0 - leftStartClipIndexs[index - 1]) * v); this['right' + index].s('clip.percentage', rightStartClipIndexs[index - 1] + (0 - rightStartClipIndexs[index - 1]) * v); } }); }; // 通过判定延迟时间数组的长度,回调 action 动画的执行 for (let i = 0, l = delayArrays.length; i < l; i++) { ht.Default.callLater(action, this, [i + 1], delayArrays.shift()); } }
data.s('clip.percentage') 是 HT 节点自带的样式属性,其本质意义就是可以通过指定的方向进行对于整个矢量图标的裁剪:
一部电影可以通过各种镜头的切换下呈现不尽相同的叙事效果,日剧夕阳下热血跑的急速切换或者幽暗角落下惊恐的淡入淡出,都是一种叙事的处理手段。在 HT 设定的 3D 场景中同样地也存在着许许多多叙述的手法,最为基础的设定就是通过场景中的主观眼睛eye和场景中心center来搭配各种动画的实现,可以自己设定值的方法函数来修改,也可以通过 HT 自身封装的方法函数来处理,例如flyTo()和moveCamera()就是最为基础的相机动画,有兴趣的话可以了解一下,自己动手尝试搭配,肯定能最大地发挥 3D 场景的优势所在。
漫游动画是为了更好地从不同的视角去巡视场景,只要通过设置几组眼睛视角,运用HT的moveCamera()相机视角移动的动画,依次去对应眼睛的视角就可以自动地切换不同视角下场景的效果。
// 默认设置的眼睛视角数组 const ROAM_EYES = [ [1683.6555274005063, 939.9999999999993, 742.6554147474625], [1717.1004359371925, 512.9256996098727, -1223.5575465999652], [-181.41773461002046, 245.58303266170844, -2043.6755074222654], [-1695.7113902533574, 790.0214102589537, -877.645744191523], [-1848.1700283399357, 1105.522705042774, 1054.1519814237804], [-108, 940, 1837] ]; // 开启相机移动漫游动画 playRoam() { // 设置场景眼睛视角 let eye = ROAM_EYES[this.roamIndex]; // 开启相机视角移动动画 moveCamera this._roamAnim = this.view.moveCamera(eye, [0, 0, 0], { duration: this.roamIndex ? 3000 : 4000, easing: Easing.easeOut, finishFunc: () => { this.roamIndex ++; let nextEye = ROAM_EYES[this.roamIndex]; // 判断是否有下一组眼睛视角,有的话继续执行相机视角移动动画,反之则重置漫游动画 if (nextEye) { this.playRoam(); } else { // 事件派发执行显示面板动画 event.fire(EVENT_SHOW_PANEL); this.resetRoam(); } } }); }
如果说场景视角漫游是一种大局整体观的体现,那么铁水罐车装载与运输以及传送带的运送则是一个高炉炼铁流程的拼图。通过一系列动画流程的表达,你会很清晰地发现,特定的 3D 场景下的讲解说明具有完整的故事串联性。
以下是铁水罐车装载与运输的动画流程:
在 3D 场景中是用 x, y, z 来分别表示三个轴,通过不断修改节点的 3D 坐标就可以实现位移效果 car.setPosition3d(x, y, z),而对于铁水罐车上的装载标签则使用吸附的功能,使其吸附在铁水罐车上就能跟着一起行驶移动,然后在指定的空间坐标位置上通过 car.s('3d.visible', true | false) 来控制铁水罐车的出现与隐藏的效果。
而关于传送带上煤块、铁矿的传输和管道气体流通的指示,通过使用 UV 纹理贴图的偏移来实现会方便很多,先来看看效果上的呈现:
对于三维模型,有两个重要的坐标系统,就是顶点的位置坐标(X、Y、Z)以及 UV 坐标。形象地说,UV 就是贴图影射到模型表面的依据,U 和 V 分别是图片在显示器水平、垂直方向上的坐标,取值一般都是0~1。而传送带以及管道的指示就是用这种方法实现的,HT 的模型节点自带 uv 值的样式属性,我们只需要不断地控制其偏移变化,就能实现传输的效果:
// 设置初始偏移值 let offset1 = 0, trackOffset = 0; // 一直调用设置偏移值 setInterval(() => { flows.each(node => { node.s({ 'top.uv.offset': [-offset1, 0], 'front.uv.offset': [-offset1, 0], }); }); track.s('shape3d.uv.offset', [0, -trackOffset]); // 偏移值增加 offset1 += 0.1; trackOffset += 0.03; }, 100);
总结
数字化 和 智能化大屏管控是 工业互联网 的发展趋势,在很大程度上解放了人力和劳力,在信息飞速传讯的时代,大数据可视化和智能管控的结合,会演绎出许多惊奇的效果碰撞。对实时数据监管下,预警信息也相当重要,保障生产有序进行的同时,我们也要关注安全问题,所以在大屏上呈现的许多内容,都极其具有行业跟上工业互联网的步伐代表性。
2019 我们也更新了数百个工业互联网 2D/3D 可视化案例集,在这里你能发现许多新奇的实例,也能发掘出不一样的工业互联网:https://mp.weixin.qq.com/s/ZbhB6LO2kBRPrRIfHlKGQA
同时,你也可以查看更多案例及效果:https://www.hightopo.com/demos/index.html