基于 HTML5 WebGL 的 3D 工控裙房系统

前言

工业物联网在中国的发展如火如荼,网络基础设施建设,以及工业升级的迫切须要都为工业物联网发展提供了很大的机遇。中国工业物联网企业目前呈现两种发展形式并存情况:一方面是大型通信、IT企业的布局;一方面是传统工业软件和工业网络企业自发地延伸,由产品提供商发展为方案供应商。什么叫作裙房?裙房是指附属于主高楼并与之连成一体的低层建筑。本文的 Demo 是针对于裙房作的,可是在工业监控系统中有不少雷同的部分,好比动画、点击切换、点击隐藏、故障展现、开关、数据展现等等,都是比较通用的一些功能。因此针对这个 Demo 将这些内容作一个记录,在这个 Demo 中我也遇到了一些问题,如何解决的都会拿出来跟你们分享。html

http://www.hightopo.com/demo/annexMonitor/node

代码实现

整个 Demo 咱们要实现的部分有:json

  • 3D 场景的搭建
  • 场景中叶轮的转动
  • 容器水位的上升降低
  • 叶轮转动故障展现
  • 开启/关闭动画
  • 灯光的开启/关闭
  • 点击切换模型
  • 点击隐藏/显示属性窗口
  • 文本内容/颜色变换

上面的功能看起来蛮多的,实际上实现起来仍是比较容易的,总共就用了 200+ 行的代码。数组

3D 场景搭建

 三维场景地基的搭建就 2 行代码:网络

var g3d = new ht.graph3d.Graph3dView();// Hightopo 的 3D 组件(三维场景地基)
g3d.addToDOM();// 将 3D 组将添加到 body 体中
HT 的组件通常都会嵌入 BorderPane( https://hightopo.com/guide/guide/core/borderpane/ht-borderpane-guide.html)、SplitView( https://hightopo.com/guide/guide/core/splitview/ht-splitview-guide.html) 和 TabView( https://hightopo.com/guide/guide/core/tabview/ht-tabview-guide.html) 等容器中使用,而最外层的 HT 组件则须要用户手工将 getView() 返回的底层 div 元素添加到页面的 DOM 元素中,这里须要注意的是,当父容器大小变化时,若是父容器是 BorderPane 和 SplitView 等这些HT预约义的容器组件,则 HT 的容器会自动递归调用孩子组件 invalidate 函数通知更新。但若是父容器是原生的 html 元素, 则 HT 组件没法获知须要更新,所以最外层的 HT 组件通常须要监听 window 的窗口大小变化事件,调用最外层组件 invalidate 函数进行更新。
为了最外层组件加载填充满窗口的方便性,HT 的全部组件都有 addToDOM 函数,其实现逻辑以下,其中 iv 是 invalidate 的简写:
addToDOM = function(){
    var self = this,
        view = self.getView(),//获取组件的底层 div
        style = view.style;
    document.body.appendChild(view);//将组件底层div添加进body中
    style.left = '0';//ht 默认将全部的组件的position都设置为absolute绝对定位
    style.right = '0';
    style.top = '0';
    style.bottom = '0';
    window.addEventListener('resize', function () { self.iv(); }, false);//窗口大小改变事件,调用刷新函数
}

接下来咱们要向场景中添加各类模型,用代码生成模型是很是无敌痛苦的,咱们将整个场景的模型都放到一个 JSON 文件中,并经过 ht.Default.xhrLoad 方法将这个 JSON 转换为 3D 场景显示在界面上:app

var dm = g3d.dm();// 获取 HT 3D 组件的数据容器
ht.Default.xhrLoad('scenes/system.json', function(text) {
    dm.deserialize(text);// 将函数的 text(json)参数传给 deserialize 反序列化方法,可将 json 内容中的元素添加到 dataModel 数据容器中进行显示
}

 ht.Default.xhrLoad 方法是一个异步加载 json 文件的方法,第一个参数为传入的 json 文件,路径是相对于 html 文件的,第二个参数是回调函数,在传入的 json 文件解析完毕以后作的操做。此方法为异步加载,所以须要对 dm 数据容器中的数据进行获取或操做的话,须要将获取/操做的代码写在 dm.deserialize(text) 方法以后,由于此时 dm 数据容器中才有节点。dom

上面将 JSON 文件发序列化到 dm 数据容器中后界面显示以下:异步

上图中整个场景的背景是我后期用代码添加的,经过前面的 addToDOM 函数能够知道咱们能够经过 getView 方法获取 HT 3D 组件的底层 div,所以要在此 div 上添加一张背景图也就不难了。剩下的 3D 模型部分都是由 JSON 反序列化出来的。ide

场景中叶轮的转动

固然转动不多是整个模型在转动,而是中间的“滚轮”在转动,这要求设计师在建立模型的时候就将这个部分分离出来,而后我给此部分设置 tag 惟一标识为“yelun”,经过 dm.getDataByTag('yelun') 便可获取到这个节点,而后给这个节点设置旋转动画。函数

 HT 中调度进行的流程是,先经过 DataModel(https://hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html) 添加调度任务,DataModel 会在调度任务指定的时间间隔到达时, 遍历 DataModel 全部图元回调调度任务的 action 函数,可在该函数中对传入的 Data 图元作相应的属性修改以达到动画效果。

根据上面对调度任务的说明,咱们了解到向 dm 数据容器中添加调度任务会遍历整个数据容器,数据容器中内容很少的时候可能感受不到,但当数据容器中内容多且模型重的状况下,对 dm 数据容器进行过滤就很是有必要了,并且若是添加多个调度任务都遍历了整个数据容器,那么对电脑的性能要求可想而知。我一开始使用的时候就是遗漏了对 dm 数据容器的过滤,由于场景不大,因此一开始没有感受,后来加了灯光后很重,就立马出现问题了,可是一直找不到缘由,后来在高人指点下才发下遗漏了对 data 的过滤判断。

所以,调度任务传入的参数对象中 action 方法传入了一个 data 值,用于设置当前动画的对象,不是此对象的直接能够 return 掉,不作任何操做:

var task = [];
var yelun = dm.getDataByTag('yelun');// 获取 tag 为 yelun 的节点
// 建立一个动画调度任务
task.yelunTask = {
    interval: 100,// 动画持续时间
    action: function(data) {// 动画内容
        if (data !== yelun) return;
        // 设置 yelun 节点的 x 轴旋转为当前 x 轴旋转值再加上 Math.PI/12
        yelun.setRotationX(yelun.getRotationX() + Math.PI/12);
    }
}
dm.addScheduleTask(task.yelunTask);// 将调度任务添加到数据容器中

容器水位的上升降低

这里将容器水位的上升降低放到一个动画调度任务里了,也就是说经过 dm 数据容器操做这个调度任务就可以同时操做这两个部分的动画,将上一小节中的 yelunTask 调度任务的 action 更改一下,由于上面的代码只对 yelun 节点进行了操做,咱们须要对装水的容器也进行操做。首先获取装水的容器,这里将这个节点的惟一标识 tag 设置为“cylinder”:

var cylinder = dm.getDataByTag('cylinder');

而后更改调度中的 action 部分代码:

action: function(data) {
    if (!(data === yelun || data === cylinder)) return;
    // 叶轮转动
    yelun.setRotationX(yelun.getRotationX() + Math.PI/12);

    // 容器水位变化
    if (cylinder.getTall() === 100) {
        cylinder.setTall(0);// 容器水位高度到达 100 的值时,重置为 0 
    }
    else cylinder.setTall(cylinder.getTall() + 1);
}

叶轮转动故障展现

 

由于没有数据的传输,因此这边故障信息我只能本身造假数据了,我建立了一个 10 之内的整数随机数,判断这个值是否为 1,若是为 1 就将运做正常的图标变换成告警图标,同时我还经过这个值来设置 dm 数据容器添加/移除调度任务来控制当前叶轮转动/中止、容器水位变化与否:

var alarm = dm.getDataByTag('alarm');// 获取告警图标节点
setInterval(function() {
    var random = Math.floor(Math.random()*5);
    if (random === 1) {
        alarm.s('shape3d.image', 'symbols/电信/故障 2.json');// 设置告警图标节点为“故障”图标
        dm.removeScheduleTask(task.yelunTask);// 将叶轮的动画加上
    }
    else {
        alarm.s('shape3d.image', 'symbols/电信/正常 2.json');// 设置告警图标节点为“正常”图标
        dm.addScheduleTask(task.yelunTask);// 移除叶轮的动画
    }
}, 1000);

开启/关闭动画

上一小节咱们已经提到了开启/关闭动画的方式,这边咱们运用 form 表单,手动操做动画的开启和关闭(注:这里只说明第一行的“水流开关”)。

首先,咱们须要建立一个 formPane 表单组件(https://hightopo.com/guide/guide/plugin/form/ht-form-guide.html),在这个表单组件中添加行数据,这边操做动画的开启和关闭我是用的 checkbox,值变化只有 true 和 false,这个状况用这个是比较优的选择。而后经过监听这个 checkbox 的值的变化事件,设置动画的开启(添加)或者关闭(移除)。

function createForm(task) {
    var form = new ht.widget.FormPane();// 建立 form 表单组件对象
    form.setWidth(160);// 设置表单组件的宽度
    form.setHeight(90);// 设置表单组件的高度
    // 设置表单组件底层 div 的样式属性
    form.getView().style.right = '10px';
    form.getView().style.top = '10px';
    form.getView().style.background = 'rgba(255, 255, 255, 0.2)';
    form.getView().style.borderRadius = '5px';
    document.body.appendChild(form.getView());// 将 form 表单底层 div 添加到 body 体中

    // 水阀开启和关闭
    form.addRow([// 给 form 表单添加一行数据
        {
            checkBox: {// 复选框类,HT 将此封装到 form 中 实际上建立了一个 ht.widget.CheckBox 组件
                label: '水流开关',// 设置 checkbox 的文本内容
                labelColor: '#fff',// 设置 checkbox 文本颜色
                selected: true,// 设置此 checkbox 是否选中
                onValueChanged: function() {// 监听值变化事件
                    if (this.isSelected()) dm.addScheduleTask(task.arrowTask);// 若是这个 checkBox 选中,则添加动画(开启水阀)
                    else dm.removeScheduleTask(task.arrowTask);// 若是这个 checkBox 未被选中,则移除动画(关闭水阀)
                }
            }
        }
    ], [0.1]);// 设置这个行数据中列的宽度
    return form;
}

addRow 方法上面代码中一言两语解释不清楚,参考以下说明:

addRow(items, widths, height, params) 添加一行组件

  • items为元素数组,元素可为字符串、json 格式描述的组件参数信息、html 元素或者为 null 的空
  • widths 为每一个元素宽度信息数组,宽度值大于 1 表明固定绝对值,小于等于 1 表明相对值,也可为 80+0.3 的组合
  • height 为行高信息,值大于 1 表明固定绝对值,小于等于 1 表明相对值,也可为 80+0.3 的组合,为空时采用默认行高
  • params 为 json 格式的额外参数,例如插入行索引以及行边框或背景颜色等,如{index: 2, background: 'yellow', borderColor: 'red'}

上面代码中提到的 arrowTask 是对场景中的“箭头”流动添加的动画调度任务,经过控制 form 表单中 checkbox 复选框是否选中可直接操做 dm 是否添加/移除动画调度任务。

灯光的开启/关闭

控制灯光的开启和关闭,这里也是经过 form 表单上的 checkbox 复选框来进行操做的。通常建议不要使用灯光,渲染太烧性能了,这里只是为了效果而添加作一个说明。

首先咱们须要建立一个“灯”节点,而后经过设置样式属性 setStyle 来设置灯的类型、颜色、灯照范围等等属性:

// 添加灯光
var light = new ht.Light();// 建立一个灯节点(继承于 ht.Node) (https://hightopo.com/guide/guide/core/lighting/ht-lighting-guide.html
light.p3([15, 120, 50]);// 设置此节点的位置
light.setTag('light');// 设置此节点的惟一标识
dm.add(light);// 将此节点添加到 dm 数据容器中进行显示
light.s({// 设置此节点的样式属性 setStyle  简写为 s
    'light.type': 'point',// 设置灯类型
    'light.color': 'rgb(252,252,149)',// 设置灯颜色
    'light.range': 1400,// 设置灯照范围
    '3d.visible': false// 设置此节点在 3d 上不可见
});

 

而后在 form 表单上添加一行用来控制灯的开关、灯的颜色灯功能:

// 九、灯光开启和关闭 以及颜色切换
form.addRow([// form 中添加一行
    {
        id: 'lightDisabled',// 设置此项的 id 值,可经过 form.getItemById 获取此项
        checkBox: {// 复选框组件
            label: '开关灯',// 设置复选框文本内容
            labelColor: '#fff',// 设置复选框文本颜色
            selected: true,// 设置复选框是否选中
            onValueChanged: function() {// 监听值变化事件
                dm.getDataByTag('light').s('light.disabled', !this.getValue());// 获取灯节点并设置是否关闭灯光效果,light.disabled 属性默认为false,可设置为true关闭灯效果
            }
        }
    },
    {
        colorPicker: {// 颜色选择器组件
            value: 'rgb(252,252,149)',// 设置当前值
            instant: true,// 设置是否处于即时状态,将会实时改变模型值
            onValueChanged: function() {// 监听值变化事件
                dm.getDataByTag('light').s('light.color', this.getValue())// 设置灯的颜色为当前选中的颜色
            }
        }
    }
], [0.1, 0.1]);

点击切换模型

 HT 将事件监听封装到 mi 事件(https://hightopo.com/guide/guide/core/3d/ht-3d-guide.html#ref_interactionlistener)中,mi 方法中有多种事件,这里咱们须要的是单击节点的事件监听 clickData 事件,经过判断事件类型 e.kind 是否为 clickData,以后对节点的设置模型便可:

var waterPump6 = dm.getDataByTag('水泵06');// 获取 tag 为“水泵06”的节点
waterPump6.s({// 设置该节点的样式属性
    'note': '点我切换模型',// 设置标注文字内容
    'note.transparent': true,// 设置标注在 3D 下是否透明
    'note.t3': [0, 0, -50],// 设置标注在 3D 下的偏移
    'note.reverse.flip': true//设置标注背面是否显示正面的内容
});
g3d.mi(function(e) {// 监听 3D 组件上的事件
    if(e.kind === 'clickData') {// 点击节点事件
        // 模型点击切换
        if (e.data === waterPump6 && e.data.s('shape3d') === 'models/裙房系统/水泵.json') e.data.s('shape3d', 'models/fengji.json');// 设置点击节点的 shape3d 样式属性
        else if (e.data === waterPump6 && e.data.s('shape3d') === 'models/fengji.json') e.data.s('shape3d', 'models/裙房系统/水泵.json');// 设置点击节点的 shape3d 样式属性
    }
});

 HT 设置模型是经过设置节点的样式属性 node.setStyle(简写为 node.s)为 shape3d 来实现的。

点击隐藏/显示属性窗口

上面说到了事件的监听,既然同为点击事件,咱们就在一个监听事件里面进行具体的操做便可,在上面的 if (e.kind === 'clickData') 判断中添加显示/隐藏属性窗口的逻辑:

var waterPump5 = dm.getDataByTag('水泵05');
waterPump6.s({
    'note': '点我切换模型',
    'note.transparent': true,
    'note.t3': [0, 0, -50],
    'note.reverse.flip': true
});
g3d.mi(function(e) {
    if(e.kind === 'clickData') {
        // 模型点击切换
        if (e.data === waterPump6 && e.data.s('shape3d') === 'models/裙房系统/水泵.json') e.data.s('shape3d', 'models/fengji.json');
        else if (e.data === waterPump6 && e.data.s('shape3d') === 'models/fengji.json') e.data.s('shape3d', 'models/裙房系统/水泵.json');
        
        // 模型点击 隐藏/显示属性窗口
        if (e.data === waterPump5) {// 判断点击的图元是否为 waterPump5
            if(giveWater.s('3d.visible')) {// 判断当前属性窗口是否为显示状态
                giveWater.s('3d.visible', false);// 设置属性窗口不可见
                e.data.s('note', '点我显示属性窗口');// 更改标注中的显示内容
            }
            else {
                giveWater.s('3d.visible', true);// 设置属性窗口可见
                e.data.s('note', '点我隐藏属性窗口')// 更改标注中的显示内容
            }
        }
    }
});

文本内容/颜色变换

经过 tag 获取场景中对应的属性窗口的节点,此节点为一个面板,至关于六面体有六面,这个节点类型就只有一面,并经过设置属性 shape3d.image 设置此节点上的图片为 tooltips.json 矢量图标(https://hightopo.com/guide/guide/core/vector/ht-vector-guide.html)。矢量在 Hightopo(HT)中是矢量图形的简称,常见的 png 和 jpg 这类的栅格位图, 经过存储每一个像素的颜色信息来描述图形,这种方式的图片在拉伸放大或缩小时会出现图形模糊,线条变粗出现锯齿等问题。 而矢量图片经过点、线和多边形来描述图形,所以在无限放大和缩小图片的状况下依然能保持一致的精确度。并且 HT 的矢量图形还有一个很是重要的特色,就是可以对矢量图形上的任何一个部分都进行数据绑定,也就是说上图中的五张图,咱们能够只绘制一张图,经过数据绑定来改变这张图上的文本以及数值内容。

矢量图标中的数据绑定能够用在工业中的生产看板、大屏中的数据显示等等,都可以以一种高效的方式进行产品的整合。

矢量图形的数据绑定可以再写一篇文章进行阐述了,这里就很少提,你们自行去官网上查看“矢量手册”以及“数据绑定手册”,说明的比较详细。

获取到对应的节点以后,经过 node.a 方法能够获取和设置数据绑定(https://hightopo.com/guide/guide/core/databinding/ht-databinding-guide.html#ref_vector)的属性,这里咱们绑定的是文本内容“label”和数值“value”以及数值颜色“valueColor”:

var billboardArray = [];
// 经过 tag 获取节点
var temperature1 = dm.getDataByTag('回水温度1');// 获取 tag 为"回水温度1"的节点
billboardArray.push(temperature1);
var temperature2 = dm.getDataByTag('回水温度2');
billboardArray.push(temperature2);
var returnPress = dm.getDataByTag('回水压力');
billboardArray.push(returnPress);
var givePress = dm.getDataByTag('供水压力');
billboardArray.push(givePress);
var giveTemp = dm.getDataByTag('供水温度');
billboardArray.push(giveTemp);
var giveWater = dm.getDataByTag('供水流量');
billboardArray.push(giveWater);
// 文字标签内容变换
billboardArray.forEach(function(billboard) {
    billboard.a('label', billboard.getTag());// 设置数据绑定属性为 label 的属性值为当前节点的 tag 内容
});
// 文字标签数字变换+颜色变换 更改图标中绑定的 value 属性值
setInterval(function() {
    billboardArray.forEach(function(billboard) {
        var random = Math.random()*100;
        billboard.a('value', random.toFixed(2));

        // 设置图标中“数值内容颜色”
        if (random > 70 && random <= 80) billboard.a('valueColor', '#00FFFF');
        else if (random > 80 && random <= 90) billboard.a('valueColor', '#FFA000');
        else if (random > 90) billboard.a('valueColor', '#FF0000');
        else billboard.a('valueColor', '');
    });
}, 1000);

 

相关文章
相关标签/搜索