基于 HTML5 Canvas 的 3D 渲染引擎构建机架式服务器

前言

今天找到了 HT 的官网里的 Demo 网站( http://www.hightopo.com/demos/index.html ),看的我眼花缭乱,目不暇接。 而且 HT 的用户手册,将例子和文档无缝融合一体,小小 10 来兆开发包居然包含了四十五份手册,数百个活生生的 HTML5 例子,还没体验过的朋友赶紧来看一看,这回可玩嗨了!

对于 HT 初学者,面对这一堆数百个涵括通用组件、网络拓扑图组件、3D 组件、矢量图形、各种编辑器等等五法八门的 HTML5 例子盛宴,往往无从下手。为此,老郑我打算为像我一样喜欢这方面的新手朋友多写几篇这样的博客,慢慢的给大家讲述各种各样的越来越多的有趣的小功能!

效果图

基于 HTML5 Canvas 的 3D 渲染引擎构建机架式服务器

( https://hightopo.com/demo/CabinetAnimat/

代码实现

HT 提供了基于 WebGL 的 3D 技术的图形组件 ht.graph3d.Graph3dView,WebGL 基于 OpenGL ES 2.0 图形接口,因此 WebGL 属于底层的图形 API 接口,二次开发还是有很高的门槛,HT 的 Graph3dView 组件通过对 WebGL 底层技术的封装,与 HT 其他组件一样,基于 HT 统一的 DataModel 数据模型来驱动图形显示,极大降低了 3D 图形技术开发的门槛。同时 HT 提供了强大的完全基于 HTML5 技术 3D 图形建模设计器,用户无需编码即可快速可视化搭建各种 3D 场景,可以说 HT 的 3D 开发模式完全打破了传统 3D 开发模式,绝大部分应用不再需要依赖精通 3ds Max 或 Maya 的专业 3D 设计师来建模,也不需要整合 Unity3d 等引擎做图形渲染,HT 一站式的提供了从建模到渲染,包括和 2D 组件呈现和数据融合的一站式解决方案。

我本次讲解的就是这个 3D 的界面,所以我们首先要创建 3D 渲染引擎组件,可视化呈现数据模型的三维环境场景。

var dm = new ht.DataModel()
var g3d = new ht.graph3d.Graph3dView(dm)

我们还要设置眼睛(或Camera)所在位置以及中心点(目标)的位置,格式均为 [x, y, z] 。

g3d.setEye([-376, 270, 896])
g3d.setCenter([-16, 118, -186])

基于 HTML5 Canvas 的 3D 渲染引擎构建机架式服务器

这里给大家说一下,可参考 3D 手册( http://www.hightopo.com/guide/guide/core/3d/ht-3d-guide.html )。如上图所示,透视投影最终显示到屏幕上的内容只有截头锥体 ( View Frustum ) 部分的内容, 因此 Graph3dView 提供了 eye ,center , up ,far ,near ,fovy 和 aspect 参数来控制截头锥体的具体范围:

  • getEye() | setEye([x, y, z]) ,决定眼睛(或 Camera )所在位置,默认值为 [0, 300, 1000]
  • getCenter() | setCenter([x, y, z]) ,决定目标中心点(或 Target )所在位置,默认值为 [0, 0, 0]
  • getUp() | setUp([x, y, z]) ,决定摄像头正上方向,该参数一般较少改动,默认值为 [0, 1, 0]
  • getNear() | setNear(near) ,决定近端截面位置,默认值为 10
  • getFar() | setFar(far) ,决定远端截面位置,默认值为 10000
  • getFovy() | setFovy(fovy) ,fovy 决定垂直方向的视觉张角弧度,默认值为 Math.PI/4
  • getAspect() | setAspect(aspect) ,决定截头锥体的宽高比,该参数默认自动根据屏幕的宽高比决定,一般不需要设置。

然后我们再给它加上一些选中效果。Graph3dView 中被选中的图元会显示为较暗的状态,变暗系数是由图元 style 的 brightness 和 select.brightness 属性决定,select.brightness 属性默认值为 0.7,最终返回值大于 1 变亮,小于 1 变暗,等于 1 或为空则不变化。Graph3dView#getBrightness 函数控制最终图元亮度,因此也可以通过重载覆盖该函数自定义选中图元亮度。

g3d.getBrightness = function (data) {
  if (data.s('isFocused')) {
  return 0.7;
}
  return null;
};
lastFocusData = null;
g3d.getView().addEventListener('mousemove', function (e) {
  // 传入逻辑坐标点或者交互 event 事件参数,返回当前点下的图元
var data = g3d.getDataAt(e);
if (data !== lastFocusData) {
  if (lastFocusData) {
  astFocusData.s('isFocused', false);
}
if (data) {
data.s('isFocused', true);
}
astFocusData = data;
  }
});

接下来我们要为这些零件设置吸附:

dm.getDataByTag('机柜').setHost(dm.getDataByTag('地板'))
dm.getDataByTag('设备').setHost(dm.getDataByTag('机柜'))
dm.getDataByTag('门').setHost(dm.getDataByTag('机柜'))
...

吸附功能对于设计有层次关系的模型非常方便,例如设备面板吸附上设备机框,设备端口吸附上设备面板,这样从机框 - 面板 - 端口的层次关系吸附,使得用户拖动整体机框时所有这个层次下的图元都会跟随移动。对于 3D 的场景下,吸附的概念更进一步延伸,当机框在三维空间进行任意位置偏移以及任意角度旋转时,所有吸附的相关图元都会正确的跟随平移,并做出相应位置对应的旋转,以达到整体设备各个图形部分保持物理相对位置一致。简单来说就是当图元吸附上宿主图元时,宿主移动或旋转时会带动所有吸附者。

  • Node#getHost() 和 Node#setHost(node) 获取和设置吸附的图元对象
  • Node#getAttaches() 返回目前吸附到该图元的所有对象,返回 ht.List 链表对象,无吸附对象时返回空
  • Node#isHostOn(node) 判断该图元是否吸附到指定图元对象上
  • Node#isLoopedHostOn(node) 判断该图元是否与指定图元相互形成环状吸附,例如 A 吸附 B ,B 吸附 C,C 又吸附回 A,则 A,B 和 C 图元相互环状吸附

因为我这里是有 6 个设备,我要把每一个都给一个属性值来记录变化的状态一会儿用到:

dm.getDataByTag('门').a('open', false)
for (var i = 1; i < 7; i++) {
  dm.getDataByTag('设备' + i).a('open', false)
}

上一篇关于 SCADA 组态电机的随笔里面咱们已经用到过动画,这回让我们通过事件监听在双击它的时候来为其加上动画效果:

// 监听事件
g3d.mi(function (event) {
  if (event.kind === 'doubleClickData') {
    var tag = event.data.getTag()
    if (tag === '门') {
      if (anim) {
        anim.stop(true)
      }
      //获取旋转角度
      var oldAngles = event.data.getRotation(),
         angles = (open ? 2 : -2)
      //启动动画
      anim = ht.Default.startAnim({
        action : function(t) {
          event.data.setRotation(oldAngles + t * angles)
        },
      })
      open = !open
    }
    //检测字符串是否以指定的前缀开始
    else if (tag.startsWith('设备')) {
      //设备动画函数
      animation(event.data)
    }
  }
}

设备动画的函数在这里:

function animation(data) {
  //设置每个设备依次的变化参数
  var v
  for (var i = 1; i < 7; i++) {
    if (tag === '设备'+ i) {
    v= i / 2
    }
  }
  if (data.anim) {
    data.anim.stop(true)
    data.anim = null
  }
  var open = data.a('open'),
    p3 = data.p3(),
    s3 = data.s3()
  if (open) {
    data.anim = ht.Default.startAnim({
      action : function(t) {
        data.p3(p3[0], p3[1], p3[2] + t * -s3[2] * v)
      }
    })
    data.a('open', false)
  }
  else {
    data.anim = ht.Default.startAnim({
      action : function(t) {
        data.p3(p3[0], p3[1], p3[2] + t * s3[2] * v)
      }
    })
    data.a('open', true)
  }
}

这里面有一些简写跟大伙儿说下。比如 mi 是增加交互事件监听器,addInteractorListener 的缩写,另外 event 格式有:

  • kind: 'clickData', // 事件类型
  • data: data, // 事件相关的数据元素
  • part: "part", // 事件的区域, icon 、label 等
  • event: e // html 原生事件

同时还有 3D 的一些:

  • setPosition3d(x, y, z) | setPosition3d([x, y, z]) 可简写为 p3(x, y, z) | p3([x, y, z])
  • getPosition3d() 可简写为 p3()
  • setSize3d(x, y, z) | setSize3d([x, y, z]) 可简写为 s3(x, y, z) | s3([x, y, z])
  • getSize3d() 可简写为 s3()
  • setRotation3d(x, y, z) | setRotation3d([x, y, z]) 可简写为 r3(x, y, z) | r3([x, y, z])
  • getRotation3d() 可简写为 r3()

建议熟记常用函数简写可提高编码效率,可参考入门手册中函数简写( http://www.hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html )。最后别忘记 g3d.addToDOM() 呦!嘿嘿~~~

总结

这个小 demo 就说到这里吧,我会不定期的写一些技术随笔,既帮助自己整理知识,也能够跟大家一起学习,我们由浅至深,循序渐进。希望看了我的文章能得你们带来帮助,同时也希望大家能多多支持和鼓励!

上一篇:Big Event in HDU(HDU1171)可用背包和母函数求解


下一篇:R语言包的安装