基于 H5 与 WebGL 的风力发电场 3D 展现

前言node

风能是一种开发中的洁净能源,它取之不尽、用之不竭。固然,建风力发电场首先应考虑气象条件和社会天然条件。近年来,我国海上和陆上风电发展迅猛。海水、陆地为咱们的风力发电提供了很好地质保障。正是这些场地为咱们的风力提供了用之不竭的能源。如今咱们正在努力探索这些领域。web

本文章实现了风力发电场的总体流程。能让你们可以看到一套完整风力发电预览体系。json

须要注意的是,本次项目是使用 Hightopo 的  HT for Web  产品来搭建的。数组

预览地址:https://hightopo.com/demo/wind-power-station/ 函数

大体流程性能

 下面是整个项目的流程图。咱们从首页能够进入到场区分布页面和集控页面。优化

  场区分布页面又包括两个不一样的 3D 场景,分别是陆地风机场和海上风机场。点击两个 3D 风机场最终都会进入到 3D 风机场景。动画

 

预览效果this

首页:spa

1. 世界地图效果

2. 中国地图效果

2. 城市地图效果

 集控中心页面(没有动画效果):

场区分布页面(没有动画效果)

陆地风机场: 

海上风机场:

代码实现

 咱们能够看到,首页的地球有三种视角状态,世界地图、中国地图、城市地图。点击每一个状态相机就会转到对应的位置。在这以前咱们要先预先存一下对应的 center 和 eye 。

 咱们最好新建一个 data.js 文件,专门用来提供数据。

 相关伪代码以下:

// 记录位置
var cameraLocations = {

earth: {
    eye: \[-73, 448, 2225\],
    center: \[0, 0, 0\]
},

china: {
    eye: \[-91, 476, 916\],
    center: \[0, 0, 0\]
},

tsankiang: {
    eye: \[35, 241, 593\],
    center: \[0, 0, 0\]
} }

 好了,有了数据以后。咱们接下来该监听事件了。咱们能够点击按钮,也能够点击高亮区域(世界地图只有按钮能够点击)进入到中国地图视角。

 咱们能够这样先获取这两个节点,而后对它们的点击事件进行相同的处理。可是,我以为这种方式能够进行优化,更换一种思考方式。

 咱们能够先将事件进行过滤,咱们建立两个数组,一个保存着相似 click、onEnter 这样能够执行的事件,一个保存着全部能够触发事件的节点。这样能够有利于咱们维护,也可使结构更加清晰。

 下图,咱们能够看到,若是当前节点没有事件权限或者当前事件自己就没有权限的话,就会被过滤掉。若是均可以正确返回,则执行对应的事件。

 相关伪代码以下:

// 权限事件
this.eventMap = {

clickData: true,
onEnter: true,
onLeave: true }

// 权限节点
this.nodeMap = {

outline: true,
outline2: true,
earth: true,
bubbles: true,
circle: true }

/**
* 监听事件
*/ initMonitor() {

var gv = this.gv

   var self = this

var evntFlow = function (e) {
    var event = e.kind
    var tag = e.data && e.data.getTag()

     // 检查当前事件或者节点是否可以被执行
     if (!self.eventMap\[event\] && !self.nodeMap\[tag\]) return false

     self.nodeEvent(event, tag)
}

gv.mi(eventFlow)

}

 只要咱们当前要执行的节点符合要求,咱们就会把 event (当前执行的事件) 和 tag (节点标签) 传给执行函数 nodeEvent 执行这样就不会浪费资源去处理那些无效的事件或者节点了。

 咱们接下来来看看 nodeEvent 怎么处理吧!

 相关伪代码以下:

/**
* 气泡事件
* @param { string } event 当前事件
* @param { string } propertyName 当前节点标签
*/ bubblesEvent(event, propertyName) {

var dm = this.dm
var account = dm.getDataByTag('account')
var currentNode = dm.getDataByTag(propertyName)
var self = this

var clickData = function() {
    // 执行清除动做

self.clearAction()

}

var onEnter = function() {
   // do something

}

var onLeave = function() {

    // do something
}

var allEvent = { clickData, onEnter, onLeave }

allEvent\[event\] && allEvent\[event\]()

}

能够看到,咱们能够利用 propertyName(节点标签) 字符串拼接组成一个方法名。好比当前拿到的节点标签是 bubbles , this[`${ properName }Event`] 以后,拿到就是 this['bubblesEvent'] 这个方法。固然,这个方法是咱们事先定义好的。

在具体的节点方法里面,咱们建立了对应的事件函数。根据传过来的 event 来判断是否拥有对应的方法。若是有的话执行,不然返回 false 。这样作的好处是:解耦、结构简洁、出现问题可以快速定位。

可是,若是咱们仔细想一想,咱们点击世界地图和中国地图的时候,功能都差很少!若是咱们能够将他们合并的话,就会方便不少了!!咱们来改造一下代码。

 相关伪代码以下:

/**
* 执行节点事件
*/ nodeEvent(event, propertyName) {

// 过滤是否有能够合并的事件
var filterEvents = function(propertyName) {
    var isCombine = false

     var self = this

if (\['earth', 'china'\].includes(propertyName)) {
        self.changeCameraLocaltions(event, propertyName)
        isCombine = true }

    return !isCombine
}

var eventFun = this[`${propertyName}Event`]
// 执行对应的节点事件
filterEvents(propertyName)
&&
eventFun
  &&
eventFun(event, propertyName)
}

 咱们事先判断当前事件是否能合并,若是能的话返回 false ,再也不执行下面的代码,而后执行本身的函数。

 这时候,咱们就能够经过对应的节点标签,从 data.js  cameraLocations 变量中取到对应的 center、eye 。

 相关伪代码以下:

/**
* 移动镜头动画
* @param { object } config 坐标对象
*/ moveCameraAnim(gv, config) { var eye = config.eye
  var center = config.center
  // 若是动画已经存在,进行清空
if(globalAnim.moveCameraAnim) {

globalAnim.moveCameraAnim.stop()

    globalAnim.moveCameraAnim = null
}

var animConfig = {
  duration: 2e3 }

globalAnim.moveCameraAnim = gv.moveCamera(eye, center, animConfig)
}

// 须要改变相机位置
changeCameraLocaltions(event, properName) {

var config = cameraLocations\[properName\]

// 移动相机
this.moveCameraAnim(this.gv, config)

}

 移动镜头动画使用到了 gv 的 moveCamera 方法,该方法接受 3 个参数,eye (相机)center (目标),animConfig (动画配置) 。而后咱们把当前动画返回给 globalAnim 的 moveCameraAnim 属性,方便咱们进行清理。

 接下来,就是切换页面了,这点须要很是当心谨慎。由于一旦没有把某个属性清除的话,将会致使内存泄漏等问题,性能会愈来愈慢。将会致使页面卡死的状况!

 因此咱们须要一个专门用来清除数据模型的函数 clearAction 。咱们应该把全部的动画对象放到一个对象或者数组中。这样方便切换页面的时候清理掉。

 相关伪代码以下:

/**
* 清除动做
*/ clearAction(index) {

var { dm, gv } = this
var { g3d, d3d } = window

allListener.mi3d && g3d.umi(allListener.mi3d)
allListener.mi2d && gv.umi(allListener.mi2d)
dm.removeScheduleTask(this.schedule)

dm && dm.clear()
d3d && d3d.clear()

window.d3d = null window.dm = null

for (var i in globalAnim) {
    globalAnim\[i\] && globalAnim\[i\].pause()
    globalAnim\[i\] = null }

// 清除对应的 3D 图纸

ht.Default.removeHTML(g3d) gv.addToDOM()

ht.Default.xhrLoad(\`displays/HT-project\_2019/风电/${index}.json\`, function (text) {
    let json = ht.Default.parse(text)
    gv.deserialize(json, function(json, dm2, gv2, datas) {
        if (json.title) document.title = json.title

        if (json.a\['json.background'\]) {
            let bgJSON = json.a\['json.background'\]
            if (bgJSON.indexOf('scenes') === 0) {
                var bgG3d

                if (g3d) {
                    bgG3d = g3d
                } else {
                    bgG3d = new ht.graph3d.Graph3dView()
                }

                var bgG3dStyle = bgG3d.getView()
                bgG3dStyle.className = index === 1 ? '' : index === 3 ? 'land' : 'offshore' bgG3d.deserialize(bgJSON, function(json, dm3, gv3, datas) {
                    init3d(dm3, gv3)
                })

                bgG3d.addToDOM()
                gv.addToDOM(bgG3dStyle)
            }
            gv.handleScroll = function () {}
        }

        init2d(dm2, gv2)
    })
})

}

首先咱们须要把 dm(数据模型) 和 gv(图纸) 清除掉。还要注意:mi(监听函数)schedule(调度任务) 应该在 dm.clear() 以前 remove。全部的动画进行 stop() 操做,而后将其值设为 null 。这里须要注意的是, 执行 stop 以后,会调用一次 finishFunc 回调函数。

当咱们的 2D 图纸里面包含 3D 背景的状况下,须要判断是否已经存在了 3D 的实例,若是存在不须要再次建立。有兴趣能够了解一下 webGL 的应用内存泄漏问题。

当进入两个 3D 场景场景的时候,咱们须要一个开场动画,如开头效果 gif 图同样。因此咱们,须要把两个开场动画的 center 和 eye 都存到咱们已经定义好的 cameraLocations 中。

// 记录位置
var cameraLocations = {

earth: {
    eye: \[-73, 448, 2225\],
    center: \[0, 0, 0\]
},

china: {
    eye: \[-91, 476, 916\],
    center: \[0, 0, 0\]
},

tsankiang: {
    eye: \[35, 241, 593\],
    center: \[0, 0, 0\]
},

offshoreStart: {
    eye: \[-849, 15390, -482\],
    center: \[0, 0, 0\]

},

landStart: {
    eye: \[61, 27169, 55\],
    center: \[0, 0, 0\]
},

offshoreEnd: {
    eye: \[-3912, 241, 834\],
    center: \[0, 0, 0\]
},

landEnd: {
    eye: \[4096, 4122, -5798\],
    center: \[1261, 2680, -2181\]
}

}

 offshoreStart、offshoreEnd、landStart、landEnd 表示海上和陆上发电场的开始位置和结束位置

咱们须要判断当前加载的是海上发电场仍是陆上发电场。咱们能够在加载对应图纸的时候添加 className 。

咱们在 clearAction 这个函数已经定义了 index 这个参数,若是点击的是陆地发电场传的就是数字3,若是是海上发电场的话,就是数字4。

好比我须要加载陆地发电场,那么就能够经过判断 g3d.className = index === 3 ? 'land' : 'offshore' 来添加 className 。

而后在 init 里面进行初始化的判断。

 相关伪代码以下:

init() {

var className = **g3d**.getView().className

// 执行单独的事件
this.selfAnimStart(className)  
this.initData()

// 监听事件
this.monitor()

}

 咱们拿到对应的 className ,传入相对应的类型而且执行对应的初始化事件,经过咱们已经定义好的 moveCameraAnim 函数进行相机的动画。

 相关伪代码以下:

/**
* 不一样风电场的开场动画
*/ selfAnimStart(type) {

var gv = this.gv
var { eye, center } = cameraLocations\[\`${type}End\`\]
var config = {
    duration: 3000,
    eye,
    center,
 }

 this.moveCameraAnim(gv, config)

}

**总结


 这个项目让咱们更加了解了风力发电。不论是风力发电场的地区优点,仍是风机的结构、运转原理。

 作完这个项目,本身获得了不少的成长和感悟。对于技术快速成长的一个好方法就是去不断的抠细节。项目是一件艺术品,须要不断对其进行打磨,要作到本身满意为止。每一个细微的点都会影响后面的性能。因此,咱们应该以匠人的精神去作任何事。

 固然,我也但愿一些伙伴可以敢于探索工业互联网领域。咱们可以实现的远远不止于此。这须要发挥咱们的想象力,为这个领域增添更多好玩的、实用的 demo。并且还能学到不少工业领域的知识。

相关文章
相关标签/搜索