Threejs 開發 3D 地圖實踐總結

發表於2017-07-11

前段時間連續上了一個月班,加班加點完成了一個3D攻堅專案。也算是由傳統web轉型到webgl圖形學開發中,坑不少,做了一下總結分享。

1、法向量問題

  法線是垂直於我們想要照亮的物體表面的向量。法線代表表面的方向因此他們為光源和物體的互動建模中具有決定性作用。每一個頂點都有一個關聯的法向量。
412020-20170709170600415-1988713710
  如果一個頂點被多個三角形共享,共享頂點的法向量等於共享頂點在不同的三角形中的法向量的和。N=N1+N2;
412020-20170709170634322-1938738967
  所以如果不做任何處理,直接將3維物體的點傳遞給BufferGeometry,那麼由於法向量被合成,經過片元著色器插值後,就會得到這個黑不溜秋的效果
  412020-20170709171549212-1331135144
  我的處理方式使頂點的法向量保持唯一,那麼就需要在共享頂點處,拷貝一份頂點,並重新計算索引,是的每個被多個面共享的頂點都有多份,每一份有一個單獨的法向量,這樣就可以使得每個面都有一個相同的顏色
  412020-20170709171918400-1225020906
2、光源與面塊顏色
  開發過程中設計給了一套配色,然而一旦有光源,面塊的最終顏色就會與光源混合,顏色自然與最終設計的顏色大相徑庭。下面是Lambert光照模型的混合演算法。
412020-20170709172207025-1953457545
  而且產品的要求是頂面保持設計的顏色,側面需要加入光源變化效果,當對地圖做操作時,側面顏色需要根據視角發生變化。那麼我的處理方式是將頂面與側面分別繪製(建立兩個Mesh),頂面使用MeshLambertMaterial的emssive屬性設定自發光顏色與設計顏色保持一致,也就不會有光照效果,側面綜合使用Emssive與color來應用光源效果。
  412020-20170709172851993-1133814272

3、POI標註

Three中建立始終朝向相機的POI可以使用Sprite類,同時可以將文字和圖片繪製在canvas上,將canvas作為紋理貼圖放到Sprite上。但這裡的一個問題是canvas影象將會失真,原因是沒有合理的設定sprite的scale,導致圖片被拉伸或縮放失真。

412020-20170709174129853-703816190

問題的解決思路是要保證在3d世界中的縮放尺寸,經過一系列變換投影到相機螢幕後仍然與canvas在螢幕上的大小保持一致。這需要我們計算出螢幕畫素與3d世界中的長度單位的比值,然後將sprite縮放到合適的3d長度。

412020-20170709174614181-28850415

  Threejs 開發 3D 地圖實踐總結
4、點選拾取問題
  webgl中3D物體繪製到螢幕將經過以下幾個階段
  412020-20170709180434431-158340541

  所以要在3D應用做點選拾取,首先要將螢幕座標系轉化成ndc座標系,這時候得到ndc的xy座標,由於2d螢幕並沒有z值所以,螢幕點轉化成3d座標的z可以隨意取值,一般取0.5(z在-1到1之間)。

 

  然後將ndc座標轉化成3D座標:
  ndc = P * MV * Vec4
  Vec4 = MV-1 * P -1 * ndc
  這個過程在Three中的Vector3類中已經有實現:

將得到的3d點與相機位置結合起來做一條射線,分別與場景中的物體進行碰撞檢測。首先與物體的外包球進行相交性檢測,與球不相交的排除,與球相交的儲存進入下一步處理。將所有外包球與射線相交的物體按照距離相機遠近進行排序,然後將射線與組成物體的三角形做相交性檢測。求出相交物體。當然這個過程也由Three中的RayCaster做了封裝,使用起來很簡單:

5、效能優化

隨著場景中的物體越來越多,繪製過程越來越耗時,導致手機端幾乎無法使用。

412020-20170709205647197-296902662

在圖形學裡面有個很重要的概念叫“one draw all”一次繪製,也就是說呼叫繪圖api的次數越少,效能越高。比如canvas中的fillRect、fillText等,webgl中的drawElements、drawArrays;所以這裡的解決方案是對相同樣式的物體,把它們的側面和頂面統一放到一個BufferGeometry中。這樣可以大大降低繪圖api的呼叫次數,極大的提升渲染效能。

412020-20170709210231634-2053049991

這樣解決了渲染效能問題,然而帶來了另一個問題,現在是吧所有樣式相同的面放在一個BufferGeometry中(我們稱為樣式圖形),那麼在麵點選時候就無法單獨判斷出到底是哪個物體(我們稱為物體圖形)被選中,也就無法對這個物體進行高亮縮放處理。我的處理方式是,把所有的物體單獨生成物體圖形儲存在記憶體中,做麵點選的時候用這部分資料來做相交性檢測。對於選中物體後的高亮縮放處理,首先把樣式面中相應部分裁減掉,然後把選中的物體圖形加入到場景中,對它進行縮放高亮處理。裁剪方法是,記錄每個物體在樣式圖形中的其實索引位置,在需要裁切時候將這部分索引制零。在需要恢復的地方在把這部分索引恢復成原狀。

6、麵點選移動到螢幕中央

這部分也是遇到了不少坑,首先的想法是:

  面中心點目前是在世界座標系內的座標,先用center.project(camera)得到歸一化裝置座標,在根據ndc得到螢幕座標,而後根據面中心點螢幕座標與螢幕中心點座標做插值,得到偏移量,在根據OribitControls中的pan方法來更新相機位置。這種方式最終以失敗告終,因為相機可能做各種變換,所以螢幕座標的偏移與3d世界座標系中的位置關係並不是線性對應的。

  最終的想法是:

  我們現在想將點選面的中心點移到螢幕中心,螢幕中心的ndc座標永遠都是(0,0)我們的觀察視線與近景面的焦點的ndc座標也是0,0;也就是說我們要將面中心點作為我們的觀察點(螢幕的中心永遠都是相機的觀察視線),這裡我們可以直接將面中心所謂視線的觀察點,利用lookAt方法求取相機矩陣,但如果這樣簡單處理後的效果就會給人感覺相機的姿態變化了,也就是會感覺並不是平移過去的,所以我們要做的是保持相機當前姿態將面中心作為相機觀察點。
  回想平移時我們將螢幕移動轉化為相機變化的過程是知道螢幕偏移求target,這裡我們要做的就是知道target反推螢幕偏移的過程。首先根據當前target與面中心求出相機的偏移向量,根據相機偏移向量求出在相機x軸和up軸的投影長度,根據投影長度就能返推出應該在螢幕上的平移量。

7、2/3D切換

23D切換的主要內容就是當相機的視線軸與場景的平面垂直時,使用平行投影,這樣使用者只能看到頂面給人的感覺就是2D檢視。所以要根據透視的視錐體計算出平行投影的世景體。

412020-20170709212308368-1441394381

因為使用者會在2D、3D場景下做很多操作,比如平移、縮放、旋轉,要想無縫切換,這個關鍵在於將平行投影與視錐體相機的位置、lookAt方式保持一致;以及將他們放大縮小的關鍵點:distance的比例與zoom來保持一致。

  平行投影中,zoom越大代表六面體的首尾兩個面面積越小,放大越大。
412020-20170709213133790-2053016925
8、3D中地理級別
  地理級別實際是畫素跟墨卡託座標系下米的對應關係,這個有通用的標準以及計算公式:

各個級別中畫素與米的對應關係如下:

3D中的計算策略是,首先需要將3D世界中的座標與墨卡託單位的對應關係搞清楚,如果已經是以mi來做單位,那麼就可以直接將相機的投影螢幕的高度與螢幕的畫素數目做比值,得出的結果跟上面的ranking做比較,選擇不用的級別資料以及比例尺。注意3D地圖中的比例尺並不是在所有螢幕上的所有位置與現實世界都滿足這個比例尺,只能說是相機中心點在螢幕位置處的畫素是滿足這個關係的,因為平行投影有近大遠小的效果。

9、poi碰撞

由於標註是永遠朝著相機的,所以標註的碰撞就是把標註點轉換到螢幕座標系用寬高來計算矩形相交問題。至於具體的碰撞演算法,大家可以在網上找到,這裡不展開。下面是計算poi矩形的程式碼

相關文章