基於 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。並且還能學到不少工業領域的知識。

相關文章
相關標籤/搜索