three.js 製作機房(上)

郭先生的部落格發表於2020-08-11

three.js使用的人太少了,一個博文就幾百個人看,之前發js基礎哪怕是d3都會有幾千的閱讀量,看看以後考慮說一說d3了,哈哈。吐槽完畢迴歸正題。前幾天郭先生看到網上有人開發了3D機房,正愁部落格沒什麼寫的,於是昨天熬夜也做了一個,今天就把大體的流程告訴萌新們,先說說主要功能模組。

  1. 牆體、地面、窗戶以及門的實現(雙擊門禁門可開關)。
  2. 機櫃實現(機櫃門的開關、機箱的推拉以及開關推拉的條件)。
  3. 機箱儲存佔用比率(用顏色表示佔用率,並附顏色譜圖)。
  4. 監控攝像視角(包括監控攝像機的模型匯入,和四視角監控)。
  5. 紅外防控報警。
  6. 強弱電線的鋪設。
  7. 以及風向。

下面對應這7個功能模組附圖。

看圖是不是感覺很好呢?不過細分下來每個點都是十分簡單的。那麼我們就按照模組分析一下。

1. 牆體、地面、窗戶以及門的實現

這一塊主要就是對於3d空間位置的理解,旋轉的使用以及uv的使用。考慮到牆面和窗戶程式碼的重複使用,這裡封裝一下。封裝完以後,改變更加靈活方便,門的旋轉參見之前發的部落格模擬門轉動

1. 牆的實現

這裡我們看下牆的資料,陣列的每一項就是一面牆(這裡我要求每一面牆最多隻能有一個門位和窗戶位,如果想兩個窗戶,那麼就在原本的一面牆上設定兩個陣列),s表示牆的size,p表示牆的position(這裡用不到選不考慮旋轉),hasDoor表示有門,ds表示門的size,dp表示門的position,hasWindow表示是否有窗,ws表示窗的size,wp表示窗的position,是不是挺簡單的(當然每個人設計的都不相同)。這樣繪製出來的圖,就如我上面發的圖。下面上程式碼

var wallArr = [
    {s: [1, 20, 61], p: [45, 10, 0], dir: 'z', hasDoor: true, ds: [1, 18, 18], dp: [45, 9, 15], hasWindow: true, ws: [1, 10, 24], wp: [45, 10, -13]}, 
    {s: [1, 20, 61], p: [-45, 10, 0], dir: 'z', hasDoor: false, hasWindow: true, ws: [1.2, 10, 50], wp: [-45, 10, 0]}, 
    // {s: [46, 20, 1], p: [22.5, 10, 30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [22.5, 10, 30]}, 
    // {s: [46, 20, 1], p: [-22.5, 10, 30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [-22.5, 10, 30]}, 
    // {s: [46, 20, 1], p: [22.5, 10, -30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [22.5, 10, -30]},
    // {s: [46, 20, 1], p: [-22.5, 10, -30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [-22.5, 10, -30]},
    {s: [91, 20, 1], p: [0, 10, 30], dir: 'x', hasDoor: false, hasWindow: true, ws: [80, 10, 1], wp: [0, 10, 30]}, 
    {s: [91, 20, 1], p: [0, 10, -30], dir: 'x', hasDoor: false, hasWindow: true, ws: [80, 10, 1], wp: [0, 10, -30]}
];

就是一個這樣的陣列。我們將註釋開啟,並註釋掉後兩行,得到如圖效果。

ok測試沒有問題。
再說說牆的實現,這裡使用了ThreeBSP,之前我也說過這個東西,它可以實現幾何體的二元操作(A與B的和、A與B的差,A與B的交集)。這個東西我們用來在牆體中扣出窗戶和門的位置。

2. 門的實現

接下來說一說門的紋理,ps一張門的圖,記得將底圖加上顏色和透明度,門把手不加透明,匯出png,然後製作材質記得加上transparent。這裡會有一個問題,那就是uv,因為boxGeometry各個面的uv都是[0,1],[1,1],[0,0],[1,0](如果沒記錯的話),多以這個門的反正面的把手肯定是不一樣的方向,這樣我們就改變一下uv

doorGeom1.faceVertexUvs[0][2] = [new THREE.Vector2(1,1), new THREE.Vector2(1,0), new THREE.Vector2(0,1)];
doorGeom1.faceVertexUvs[0][3] = [new THREE.Vector2(1,0), new THREE.Vector2(0,0), new THREE.Vector2(0,1)];
doorGeom2.faceVertexUvs[0][0] = [new THREE.Vector2(1,1), new THREE.Vector2(1,0), new THREE.Vector2(0,1)];
doorGeom2.faceVertexUvs[0][1] = [new THREE.Vector2(1,0), new THREE.Vector2(0,0), new THREE.Vector2(0,1)];

具體的旋轉,之前部落格有實現過。

3. 地面的實現

地面相信大家都會弄,主要是調整一個repeat和wrap,不多說。

floorT.wrapS = floorT.wrapT = THREE.RepeatWrapping;
floorT.repeat.x = floorT.repeat.y = 12;

2. 機櫃的實現

這是裡面對相對複雜的模組,不過裡面還是用到了ThreeBSP,平移旋轉、uv以及射線方面的知識,對於單個繪製來說,相信大家都能後信手拈來,不多對於大數量的機櫃實現開關門、推拉伺服器的點選操作邏輯(機櫃關門是不允許推拉伺服器操作,機櫃中有伺服器來出來是,不允許開關門操作),和機櫃的隱藏顯示。

1. 機櫃架子的實現

機櫃框架使用了ThreeBSP,將兩個BoxGeometry相減既會出現一個沒有門的框架,我們在加上門即可,門的旋轉之前講過了,

2. 伺服器的實現

伺服器的uv貼圖只需要正面的即可,所以除了前兩個三角形,其他的設定成這樣就可以了。

for(let i=2; i<serGeom.faceVertexUvs[0].length; i++) {
    serGeom.faceVertexUvs[0][i] = [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()];
}

3.伺服器和櫃門的點選事件

這裡面我們考慮使用THREE.Raycaster類。
這是一個射線類,原理是滑鼠在螢幕上點選的時候,得到二維座標p(x, y),再加上深度座標的範圍(0, 1), 就可以形成兩個三位座標A(x1, y1, 0), B(x2, y, 1),這兩個點的線穿過的Object3D由近及遠返回一個陣列,第一個便是我們點選到的物件。我們給之前的伺服器機櫃和伺服器都加上名字方便我們知道點到的是哪一個。

let intersectFrameDoor = raycaster.intersectObjects(motorGroup.children, true);
let tempArr = intersectFrameDoor[0].object.name.split('-');//得到[機櫃門/伺服器名字,機櫃編號,伺服器編號[
if(tempArr[0] == 'mdoor') {
    if(!this.motorServerFlag[tempArr[1]]) {
        if(this.motorDoorFlag[tempArr[1]]) {
            this.doAnimate(Math.PI * 3 / 5, 0, 500, intersectFrameDoor[0].object.parent, ['rotation', 'y'])
            this.motorDoorFlag[tempArr[1]] = false;
        } else {
            this.doAnimate(0, Math.PI * 3 / 5, 500, intersectFrameDoor[0].object.parent, ['rotation', 'y'])
            this.motorDoorFlag[tempArr[1]] = true;
        }
    }
} else if(tempArr[0] == 'mserver') {
    if(this.motorDoorFlag[tempArr[1]]) {
        let posx = intersectFrameDoor[0].object.position.x;
        if(posx == 0) {
            this.doAnimate(0, 2, 500, intersectFrameDoor[0].object, ['position', 'x']);
            this.motorServerFlag[tempArr[1]] += 1;
        } else {
            this.doAnimate(2, 0, 500, intersectFrameDoor[0].object, ['position', 'x']);
            this.motorServerFlag[tempArr[1]] -= 1;
        }
    }
}

4. 封裝動畫

這裡面有很多動畫,例如各種門的轉動,伺服器的平移,如果直接改變屬性閒得很突兀,那麼我們有幾種選擇,

  1. 關鍵幀動畫
  2. Tween動畫
  3. 自制動畫

這裡我們練習自己封裝一個小動畫,他雖然可能不夠精確,但是十分實用。

doAnimate(s, e, t, o, a) { //開始的屬性值、結束的屬性值 時間 物件 屬性
    let temp = s;
    let step = t / 20;
    let stepLen = (e - s) / step;
    let animationObj = setInterval(() => {
        temp += stepLen;
        if(stepLen > 0 && temp >= e) {
            o[a[0]][a[1]] = e;
            clearInterval(animationObj);
        } else if(stepLen < 0 && temp <= e) {
            o[a[0]][a[1]] = e;
            clearInterval(animationObj);
        } else {
            o[a[0]][a[1]] = temp;
        }
    }, 20)
}

在fpx大於50的情況下,基本準確。

今天就先講這兩個模組,下一篇繼續,覺得可以的話,點個贊吧。

 

轉載請註明地址:郭先生的部落格

相關文章