基于 HTML5 WebGL 的楼宇智能化集成系统(一)

前言
      随着现代通信技术、计算机技术、控制技术的飞速发展,智能建筑已经成为现代建筑发展的主流。智能建筑是以建筑物为平台,兼备信息设施系统、信息化应用系统、建筑设备管理系统、公共安全系统等。集结构、系统、服务、管理及其优化组合为一体,向人们提供一个安全、高效、便携、节能、环保、健康的建筑环境。
 
      IBMS(Intelligent Building Management System),即智能化集成系统,是指在 BAS 的基础上更进一步的与通信网络系统、信息网络系统实现更高一层的建筑集成管理系统。在信息化时代的今天,诸多建筑与集成的管理系统融合,生成了一套高效的智慧建筑集成解决方案。IBMS 更多突出的是管理方面的功能,即如何的全面实现优化控制和管理,节能降耗、高效、舒适、环境安全这样一个目的,可以这样说,判断一个建筑物是否具有智能建筑特点,要看它是否具有 IBMS 的系统集成。这是很重要的判定条件。另一个重要的前提是,在做好这项工程的同时不要忽视了同步建设的信息化工程。一个成功的 IBMS 系统集成会在诸多的管理方面能发挥其显著的经济优势。
 
      传统的 智慧楼宇/楼宇自动化/楼宇安防/智慧园区 常会采用 BIM(建筑信息模型 Building information modeling)软件,如 Autodesk 的 Revit 或 Bentley 这类建筑和工程软件,但这些 BIM 建模模型的数据往往过于庞大臃肿,绝大部分细节信息对楼宇自控意义不大,反而影响拖累了行业 Web SCADA 或 Web 组态监控的趋势,所以我们采用以 Hightopo 的 HT for Web 产品轻量化 HTML5/WebGL 建模的方案,实现快速建模、运行时轻量化到甚至手机终端浏览器即可 3D 可视化运维的良好效果。
 
      本系列文章为了帮助用户更直观友好的浏览当前的楼宇智控系统,分成了三个小节来介绍场景以及效果实现的运用:
1、冷站热站中央空调末端智慧群控系统 以及 3D 动画效果以及切换漫游;
2、面板组件动画效果和 楼层监控系统 视频的引入;
3、智慧楼宇管理系统电梯监控系统 以及 停车场管理系统
 
界面简介及效果预览
界面初始化及漫游效果
      场景在加载的时候会读取模型信息,确认模型加载完毕后才开始执行动画效果,然后通过场景的漫游效果,可以直观地去巡视整座大楼建筑的场景信息。
 
 
冷站场景效果:优化冷水机组效率,按需供给
      在智慧楼宇的中央空调冷热源系统中,冷站场景分有冷却水系统、冷水机组以及冷冻水系统所组成。冷却水系统的作用是为冷水机组的冷凝器提供冷却水,吸收制冷剂的冷凝热量,并将冷凝热量转移到大气中去。冷冻水系统的作用是为冷水机组的蒸发器提供的冷量通过冷冻水输送到各类冷水用户。
 
 
热站场景效果:优化热泵机组效率,按需供给
      在智慧楼宇的中央空调冷热源系统中,热站场景分有冷却机组系统和热水泵系统所组成。通过冷却机组系统的换热器不断加热了中央空调系统内的空调水,并通过热水循环中的热水泵系统进行循环给用户提供热量。
 
中央空调末端智慧群控系统场景效果:灵活对应多样智能调节空间
      在智慧楼宇的中央空调冷热源系统中,末端智能节能控制系统,通过室内温湿度可以进行模块内部的调节送风温度,水阀开度及风机频率,在保证末端舒适度的前提下,使供冷量与需求相匹配,最大限度地降低风机能耗。
 
智慧楼宇管理系统优化效果
      主要包括冷站、热站和中央末端智慧群控的联合作用,以及楼层智慧照明,通过清晰的动画体现出整栋大楼智慧节能运作的流程,可以通过面板详情的演示细致地介绍每个场景的作用以及串联的用处。
 
电梯以及楼层监控效果
      可视化地实时监控电梯在楼层间的工作运行状态,并且能够准确地浏览每个电梯内的实时监控画面。
 
 
停车场管理系统监控效果
      停车场作为现在楼宇监控不可缺失的一环,这里主要可以体现出实时的车位监控,通过简单的动画演示来表现出整个停车场车辆的运行状态,方便管理。
 
代码实现
一、场景搭建
      界面通过 2D 图纸叠加在3D 场景上来实现 2D 界面 与 3D 场景的融合,2D 界面通过自动布局的机制实现了手机端与电脑端的响应式呈现。
      通过 2D 视图的组件 ht.graph.GraphView 和 3D 视图的组件 ht.graph3d.Graph3dView 创建出呈现 2D 视图的组件类 g2d 以及呈现 3D 视图的组件类 g3d,在分别获取各自的数据模型 DataModel,来对图纸场景做一些数据可视化的操作,这里值得一提的是,我对于 2D 界面和 3D 场景的融合,是通过把 getView() 获取到 g3d 拓扑组件的根层 div,然后 addToDOM() 将 g2d 组件加入到指定的 DOM 元素底下。
      可以通过<HT的入门手册>了解到更多视图与数据模型之间的内容。
// 创建二维拓扑视图  this.g2d = new ht.graph.GraphView();  this.g2dDm = this.g2d.dm();    // 创建三维拓扑视图  this.g3d = new ht.graph3d.Graph3dView();  this.g3dDm = this.g3d.dm();    // 将二维图纸嵌入到三维场景中  this.g2d.addToDOM(this.g3d.getView());    // 修改左右键交互方式  let mapInteractor = new ht.graph3d.MapInteractor(this.g3d);  this.g3d.setInteractors([mapInteractor]);    // 修改最大仰角为 PI / 2  mapInteractor.maxPhi = Math.PI / 2;    const G = {};  window.G = G;  // 事件派发  G.event = new ht.Notifier();

 

 

3D 场景加载主视图为:

 

      首先我搭建了一个 3D 的场景用来放置我们的 json 场景数据,利用 ht.Default.xhrLoad 函数解析 json 场景数据,并通过 deserialize 将反序列化的对象加入DataModel来显示加载 3D 场景,有兴趣的可以通过<HT的序列化手册>来了解这一机制的实现。

ht.Default.xhrLoad('scenes/demo.json', (json) => {      if (!json) return;      g3dDm.deserialize(json);        // 设置三维视图的中心点和相机位置      g3d.setCenter([-342, -64, 389]);      g3d.setEye([-355, 10833, 2642]);        // 设置最远距离      g3d.setFar(1000000);      // 获取球图标,设置为天空球      let skybox = g3dDm.getDataByTag('skyBox');      g3d.setSkyBox(skybox);        // 模型加载完后执行动画      const modelList = [];      g3dDm.each(d => {          const shape3d = d.s('shape3d');          if (!shape3d || !shape3d.endsWith('.json')) return;          if (ht.Default.getShape3dModel(shape3d)) return;          modelList.push(shape3d);      });      ht.Default.handleModelLoaded = (name, model) => {          const index = modelList.indexOf(name);          if (index < 0) return;          modelList.splice(index, 1);          if (modelList.length > 91) return;          ht.Default.handleModelLoaded = () => {          };            // 模型加载完侯,默认执行场景切换动画          g3d.moveCamera([257, 713, 1485], [7, 40, 144], {              duration: 2000,              finishFunc: () => {                  this.load2D();              }          });      };  });

 

2D 面板加载视图为:

 

      同样,我搭建了一个 2D 的场景用来放置我们的 json 矢量图,利用 ht.Default.xhrLoad 函数将 json 矢量背景图反序列化显示在 2D 面板数据。

ht.Default.xhrLoad('displays/demo.json', (json) => {      if (!json) return;      g2dDm.deserialize(json);        // 面板动画入口      this.tittleAnim();      this.panelTime();        // 2D图纸加载完后执行事件处理      this.loaded2DHandler();  });

 

 

二、3D 动画效果以及切换漫游

      对于 3D 建模下的楼宇建筑,加上场景的全方位漫游,可使用户达到一种沉浸式的体验,更加直观地去感受这个楼宇下各个场景的联系,依次地介绍了冷站、智慧末端以及热站的位置以及功能运作的动画 。主要运用的方法是通过借助 HT 提供的 ht.Shape 图元类型,可以在 GraphView 和 Graph3dView 组件上展示出各种二维和三维的形状效果,而漫游的管道路线就是由其扩展子类 ht.Polyline 去绘制实现一条三维的管道,然后用这条绘制的管道加上漫游的时间去调用这个漫游的方法,其本质上是围绕着中心点,然后根据管道去不断地改变视角下的 eye 和 center 的数值,达到环视这个建筑的整体视角。

      这里可以了解一下关于空间轨道的绘制,详见<HT的形状手册>的空间管线章节。

      以下是环视漫游动画的伪代码:

polyLineRoam(polyLine, time) {      const g3d = this.g3d;      const g3dDm = this.g3dDm;      this.roamButton.a('active', true);      this.roamAnim = ht.Default.startAnim({          duration: time,          easing: t => t,          action: (v, t) => {              let length = this.main.g3d.getLineLength(polyLine),              offset = this.main.g3d.getLineOffset(polyLine, length * v),              point = offset.point,              px = point.x,              py = point.y,              pz = point.z;                g3d.setEye(px, py, pz);              g3d.setCenter(7, 40, 144);          },          finishFunc: () => {              this.roam1();          }      });  }

 

      在整体建筑的环视漫游完后,我们可以通过拉近各个场景的视角,来依次巡视各个场景所执行的动画。在根据管道改变 eye 和 center 环视漫游方法结束后,用动画的结束回调 finishFunc 去调用下一个动画的执行,而巡视漫游就在这里去调用,以下我们以巡视冷站的漫游动画为例去介绍实现的方法。

      巡视漫游的主要实现方法是通过 HT 核心包的相机移动 moveCamera 来实现的, 通过参数 (eye, center, animation) 来调用这个方法:

  • eye:新的相机位置,形如[-291, -8, 283],如果为 null 则使用当前相机的位置;
  • center:新的目标中心点位置(相机看向的位置),形如[148, -400, 171],如果为 null 则使用当前中心点位置;
  • animation:默认 false,是否启用动画,可以设置为 true 或者 flase 或者 animation 动画对象;

      每次执行完一个场景的视角移动后,再通过相机移动动画的结束回调 finishFunc 调用下一个相机移动的动画,达到巡视漫游的效果。

// 切换到冷站视角  roam1() {      const g3d = this.g3d;      const g3dDm = this.g3dDm;      this.roamAnim = g3d.moveCamera([-291, -8, 283], [148, -400, 171], {          duration: 500,          easing: t => t * t,          finishFunc: () => {              this.roam2();          }      });  }

 

      在环视漫游和巡视漫游的执行下,我们也可以触发 2D 图纸右面板下的按钮面板去观看我们想要浏览的指定场景,这时候就会关闭当前在执行的环视漫游或者巡视漫游,再次点击改按钮则返回场景的主视角,或者点击左上角漫游按钮又可以进入环视漫游,这样的交互体验,可以方便用户即使地查看想要浏览的场景,而不用依靠等待逐一漫游下去查看,也不会干扰到漫游的整体体验。相应地通过介绍冷站按钮的点击触发介绍一下实现的方法。

      一般的交互方式存在三种事件交互的方法,包括事件通知管理器 ht.Notifier 类,内置的 Interator 在交互过程会派发出事件和数据绑定的监听来实现,而这里使用的是第三种交互方式。

      通过数据绑定监听到 onDown 执行按下的事件后,通过改变按下和再次按下的按钮状态 active 来分别执行相机移动去切换视角,主要实现的伪代码如下:

// 设置图元可交互  this.coolingCentralStationButton.s('interactive', true);  // 通过数据绑定监听到onDown执行按下的事件  this.coolingCentralStationButton.s('onDown', () => {  // 切换到冷站时,2d面板所执行的切换动画  this.switchToColdStation();  // 按钮初始化  this.buttonTearDown();  // 按钮按下效果的状态      let active = this.coolingCentralStationButton.a('active');      // button为按钮集合数组,当按下电梯按钮,其他按钮默认false      button.forEach(btn => {          btn.a('active', false);      });      // 冷站按钮的状态切换      this.coolingCentralStationButton.a('active', !active);      // 根据冷站按钮的状态执行切换到冷站或者切换回主视角      if (active) {          // 相机移动切换到主视角          moveCamera(g3d, [257, 713, 1485], [7, 40, 144], {              duration: 2000,              easing: t => t * t          });      } else {          // 漫游动画对象如果不为空,则暂停漫游动画对象并且设置为空          if (this.roamAnim !== null) {              this.roamAnim.pause();              this.roamAnim = null;          }          // 相机移动切换到冷站视角          coolingCentralStationAnimation = moveCamera(g3d, [-291, -8, 283], [148, -400, 171], {              duration: 2000,              easing: t => t * t          });      }  });

 

      当然,在 3D 场景下还有一些很有趣的动画效果,比如车流效果、飞光效果和圆环扩散效果。车流效果主要通过采用了贴图的 uv 的偏移来实现达到车流穿梭的科技感效果;而飞光效果则是采用调度动画的方法来间隔设置飞光的高度,达到最高点则消失然后重新轮回动画展示;圆环扩散效果则是同样采用调度动画的方法来间隔设置圆环的缩放值和透明度,来达到扩散消失的效果。

      对于间隔的调度动画,为了实现动画的流畅性,这里调度使用的 loop 是运用到自己封装 HT 的动画 ht.Default.startAnim 的一个方法:

  • frames 动画帧数,这里不锁定帧数,可以适应本身动画的帧数;
  • interval 动画间隔,单位ms,默认设置20ms。
loop(action, interval = 20) {      return ht.Default.startAnim({          frames: Infinity,          interval: interval,          action: action      });  }

 

 

      然后通过调用这个 loop 的间隔动画方法,我们来实现车流效果、飞光效果和圆环扩散效果,实现的参考伪代码如下:

// 车流图元的初始化  let traffic = g3dDm.getDataByTag('traffic');  // 圆环扩散图元的初始化  let lightRing = this.lightRing = g3dDm.getDataByTag('lightRing');  // 飞光图元设置三种透明状态数组集合flyMap的初始化  [1, 2, 3].forEach(i => {      const data = flyMap['fly' + i] = g3dDm.getDataByTag('fly' + i);      data.eachChild(d => {          d.s({              // 打开透明度              'shape3d.transparent': true,              // 根据不同的数组集合设置不同的透明度              'shape3d.opacity': i === 3 ? 0.5 : 0.7,              // 设置沿着y轴自动旋转              'shape3d.autorotate': 'y'          });      });  });    if (this.flyAnim) return;  this.flyAnim = loop(() => {      // 飞光根据间隔设置高度来达到上升的效果      for (let k in flyMap) {          const data = flyMap[k];          let e = data.getElevation() + flyDltMap[k];          if (e >= 500) e = -400;          data.setElevation(e);      }        // 车流根据设置间隔增长uv偏移量来实现穿梭的效果      traffic.eachChild(c => {          c.s('all.uv.offset', [location, 0]);      });      location -= 0.03;        // 旋转震荡波透明度渐降      let percent = lightRing.a('percent') || 0,          scale = 15 * percent + 0.5;      lightRing.setScale3d([scale + 1, scale, scale + 1]);      lightRing.s('shape3d.opacity', (1 - percent) * 0.5);      percent += 0.01;      if (percent >= 1) {          percent = 0;      }      lightRing.a('percent', percent);  }, 50);

 

三、冷站场景和热站场景的动画实现

      场景动画中机组的风扇、集水器的蓄满以及水的流动效果:

      动画的实现主要还是通过 HT 自带的 ht.Default.startAnim 动画函数,支持 Frame-Based 和 Time-Based 两种方式的动画。同样的,我们这里使用的是 Frame-Based 来封装一个 loop 函数来执行每一帧间隔的动画。

      一般来说,动画可通过自行配置来达到自己想要实现的方法,这里可以了解< HT 的入门手册>关于动画函数的介绍。

if (this.stationAnim) return;  this.stationAnim = loop(() => {      // 冷站水管流动      coldFlow_blue.eachChild(c => {          c.s('shape3d.uv.offset', [-location, 0]);      });      coldFlow_yellow.eachChild(c => {          c.s('shape3d.uv.offset', [location, 0]);      });        // 热站水管流动      heatFlow_blue.eachChild(c => {          c.s('shape3d.uv.offset', [-location, 0]);      });      heatFlow_yellow.eachChild(c => {          c.s('shape3d.uv.offset', [location, 0]);      });        location -= 0.03;        // 冷站风扇旋转      cold_fan.eachChild(c => {          c.setRotation3d(c.r3()[0], c.r3()[1] + (Math.PI / 10), c.r3()[2]);      });      // 热站风扇旋转      heat_fan.eachChild(c => {          c.setRotation3d(c.r3()[0], c.r3()[1] + (Math.PI / 10), c.r3()[2]);      });        // 集水器水位变化      HotWaterTankTall += 0.25;      if (HotWaterTankTall > 15) {          HotWaterTankTall = 0;      }      coldWaterTankTall1 += 0.25;      if (coldWaterTankTall1 > 20) {          coldWaterTankTall1 = 0;      }      coldWaterTankTall2 += 0.25;      if (coldWaterTankTall2 > 20) {          coldWaterTankTall2 = 0;      }      hotWaterTank.setTall(HotWaterTankTall);      coldWaterTank1.setTall(coldWaterTankTall1);      coldWaterTank2.setTall(coldWaterTankTall2);  }, 50);

 

 

四、中央空调末端智慧群控系统场景效果

      这里采用了模拟数据的方式来体现末端智能节能控制的效果。应用于真实项目的时候,可以采用数据接口的方式来实时对接真实数据,可以达到实时监控的效果。

      我使用了自己 mock 的末端群控的数据参数,格式如下:

var boxData =      [          [{              // 设备编号              id: 'box1',              // 设备的温度              temperature: 23.8,              // 设备的频率              frequency: 45.8          }, ...]          ...      ];

 

 

      这里的实现也是通过 loop 循环执行数据的读取,当数组指标 index 读取到最后一个数据时,立即关闭循环并清空 loop调度。

boxAnimation = loop(() => {          for (let i = 0, l = 16; i <= l-1; i++) {              let roomTag, roomBox, tag;              tag = i+1;              roomTag = 'boxPanel' + tag;              roomBox = 'box' + tag;                let panel = g3dDm.getDataByTag(roomTag);              let box = g3dDm.getDataByTag(roomBox);              if (panel) {                  panel.a('valueT', boxData[index][i].temperature + '℃');                  panel.a('valueK', boxData[index][i].frequency + 'Hz');                  // 手动更新缓存的面板信息                  g3d.invalidateShape3dCachedImage(panel);                  // 根据温度判断设备的颜色                  if (box && parseFloat(panel.a('valueT')) < 26) {                      box.s('shape3d.blend', 'rgb(4,67,176)');                      box.s('wf.color', 'rgb(4,67,176)');                  } else if (box && parseFloat(panel.a('valueT')) >= 26 && parseFloat(panel.a('valueT')) <= 28) {                      box.s('shape3d.blend', 'rgb(28,189,87)');                      box.s('wf.color', 'rgb(28,189,87)');                  } else if (box && parseFloat(panel.a('valueT')) > 28) {                      box.s('shape3d.blend', 'rgb(181,43,43)');                      box.s('wf.color', 'rgb(181,43,43)');                  }              }          }          index++;          if (index >= 10) {              boxAnimation.pause();              boxAnimation = null;          }      }, 500);

 

 

总结

      IBMS 智能化集成系统管理对于建筑园区管理的重要性日趋上升,在信息时代里不仅可以很好地体现出信息数据管理的明确性,也体现了智慧管理的便利有效性。通过 3D 场景楼宇园区的动画加上环视漫游和巡视漫游的配合,充分体现了 3D 场景的拟真优点,但是如何实现场景动画的触发实现呢?这里当然必不可少了 2D 面板上的交互和动画,在下期我们会为大家介绍一些 2D 面板的交互和动画实现,带您解读不一样的 2D/3D 融合。
 
      2019 我们也更新了数百个工业互联网 2D/3D 可视化案例集,在这里你能发现许多新奇的实例,也能发掘出不一样的工业互联网:https://mp.weixin.qq.com/s/ZbhB6LO2kBRPrRIfHlKGQA
      同时,你也可以查看更多案例及效果:https://www.hightopo.com/demos/index.html