Threejs 開發 3D 地圖實踐總結
作者:木的樹
www.cnblogs.com/dojo-lzz/p/7143276.html
前段時間連續上了一個月班,加班加點完成了一個3D攻堅項目。也算是由傳統web轉型到webgl圖形學開發中,坑不少,做了一下總結分享。
1、法向量問題
法線是垂直於我們想要照亮的物體表面的向量。法線代表表面的方向因此他們為光源和物體的交互建模中具有決定性作用。每一個頂點都有一個關聯的法向量。
如果一個頂點被多個三角形共享,共享頂點的法向量等於共享頂點在不同的三角形中的法向量的和。N=N1+N2;
所以如果不做任何處理,直接將3維物體的點傳遞給BufferGeometry,那麼由於法向量被合成,經過片元著色器插值後,就會得到這個黑不溜秋的效果
我的處理方式使頂點的法向量保持唯一,那麼就需要在共享頂點處,拷貝一份頂點,並重新計算索引,是的每個被多個面共享的頂點都有多份,每一份有一個單獨的法向量,這樣就可以使得每個面都有一個相同的顏色
2、光源與面塊顏色
開發過程中設計給了一套配色,然而一旦有光源,面塊的最終顏色就會與光源混合,顏色自然與最終設計的顏色大相徑庭。下面是Lambert光照模型的混合演算法。
而且產品的要求是頂面保持設計的顏色,側面需要加入光源變化效果,當對地圖做操作時,側面顏色需要根據視角發生變化。那麼我的處理方式是將頂面與側面分別繪製(創建兩個Mesh),頂面使用MeshLambertMaterial的emssive屬性設置自發光顏色與設計顏色保持一致,也就不會有光照效果,側面綜合使用Emssive與color來應用光源效果。
3、POI標註
Three中創建始終朝向相機的POI可以使用Sprite類,同時可以將文字和圖片繪製在canvas上,將canvas作為紋理貼圖放到Sprite上。但這裡的一個問題是canvas圖像將會失真,原因是沒有合理的設置sprite的scale,導致圖片被拉伸或縮放失真。
問題的解決思路是要保證在3d世界中的縮放尺寸,經過一系列變換投影到相機屏幕後仍然與canvas在屏幕上的大小保持一致。這需要我們計算出屏幕像素與3d世界中的長度單位的比值,然後將sprite縮放到合適的3d長度。
4、點擊拾取問題
webgl中3D物體繪製到屏幕將經過以下幾個階段
所以要在3D應用做點擊拾取,首先要將屏幕坐標系轉化成ndc坐標系,這時候得到ndc的xy坐標,由於2d屏幕並沒有z值所以,屏幕點轉化成3d坐標的z可以隨意取值,一般取0.5(z在-1到1之間)。
functionfromSreenToNdc(x,y,container){
return{
x:x/container.offsetWidth *2-1,
y: -y/container.offsetHeight *2+1,
z:1
};
}
functionfromNdcToScreen(x,y,container){
return{
x:(x+1)/2*container.offsetWidth,
y:(1-y)/2*container.offsetHeight
};
}
然後將ndc坐標轉化成3D坐標:
ndc = P * MV * Vec4
Vec4 = MV-1 * P -1 * ndc
unproject:function(){
varmatrix=newMatrix4();
returnfunctionunproject(camera){
matrix.multiplyMatrices(camera.matrixWorld,matrix.getInverse(camera.projectionMatrix));
returnthis.applyMatrix4(matrix);
};
}(),
將得到的3d點與相機位置結合起來做一條射線,分別與場景中的物體進行碰撞檢測。首先與物體的外包球進行相交性檢測,與球不相交的排除,與球相交的保存進入下一步處理。將所有外包球與射線相交的物體按照距離相機遠近進行排序,然後將射線與組成物體的三角形做相交性檢測。求出相交物體。當然這個過程也由Three中的RayCaster做了封裝,使用起來很簡單:
mouse.x = ndcPos.x;
mouse.y = ndcPos.y;
5、性能優化
隨著場景中的物體越來越多,繪製過程越來越耗時,導致手機端幾乎無法使用。
在圖形學裡面有個很重要的概念叫「one draw all」一次繪製,也就是說調用繪圖api的次數越少,性能越高。比如canvas中的fillRect、fillText等,webgl中的drawElements、drawArrays;所以這裡的解決方案是對相同樣式的物體,把它們的側面和頂面統一放到一個BufferGeometry中。這樣可以大大降低繪圖api的調用次數,極大的提升渲染性能。
這樣解決了渲染性能問題,然而帶來了另一個問題,現在是吧所有樣式相同的面放在一個BufferGeometry中(我們稱為樣式圖形),那麼在面點擊時候就無法單獨判斷出到底是哪個物體(我們稱為物體圖形)被選中,也就無法對這個物體進行高亮縮放處理。我的處理方式是,把所有的物體單獨生成物體圖形保存在內存中,做面點擊的時候用這部分數據來做相交性檢測。對於選中物體後的高亮縮放處理,首先把樣式面中相應部分裁減掉,然後把選中的物體圖形加入到場景中,對它進行縮放高亮處理。裁剪方法是,記錄每個物體在樣式圖形中的其實索引位置,在需要裁切時候將這部分索引制零。在需要恢復的地方在把這部分索引恢復成原狀。
6、面點擊移動到屏幕中央
這部分也是遇到了不少坑,首先的想法是:
面中心點目前是在世界坐標系內的坐標,先用center.project(camera)得到歸一化設備坐標,在根據ndc得到屏幕坐標,而後根據面中心點屏幕坐標與屏幕中心點坐標做插值,得到偏移量,在根據OribitControls中的pan方法來更新相機位置。這種方式最終以失敗告終,因為相機可能做各種變換,所以屏幕坐標的偏移與3d世界坐標系中的位置關係並不是線性對應的。
最終的想法是:
我們現在想將點擊面的中心點移到屏幕中心,屏幕中心的ndc坐標永遠都是(0,0)我們的觀察視線與近景面的焦點的ndc坐標也是0,0;也就是說我們要將面中心點作為我們的觀察點(屏幕的中心永遠都是相機的觀察視線),這裡我們可以直接將面中心所謂視線的觀察點,利用lookAt方法求取相機矩陣,但如果這樣簡單處理後的效果就會給人感覺相機的姿態變化了,也就是會感覺並不是平移過去的,所以我們要做的是保持相機當前姿態將面中心作為相機觀察點。
回想平移時我們將屏幕移動轉化為相機變化的過程是知道屏幕偏移求target,這裡我們要做的就是知道target反推屏幕偏移的過程。首先根據當前target與面中心求出相機的偏移向量,根據相機偏移向量求出在相機x軸和up軸的投影長度,根據投影長度就能返推出應該在屏幕上的平移量。
this.unprojectPan=function(deltaVector,moveDown){
// var getProjectLength()
varelement=scope.domElement===document?scope.domElement.body:scope.domElement;
varcxv=newVector3(,,).setFromMatrixColumn(scope.object.matrix,);// 相機x軸
varcyv=newVector3(,,).setFromMatrixColumn(scope.object.matrix,1);// 相機y軸
// 相機軸都是單位向量
varpxl=deltaVector.dot(cxv)/* / cxv.length()*/;// 向量在相機x軸的投影
varpyl=deltaVector.dot(cyv)/* / cyv.length()*/;// 向量在相機y軸的投影
// offset=dx * vector(cx) + dy * vector(cy.project(xoz).normalize)
// offset由相機x軸方向向量+相機y軸向量在xoz平面的投影組成
vardv=deltaVector.clone();
dv.sub(cxv.multiplyScalar(pxl));
pyl=dv.length();
if(scope.objectinstanceofPerspectiveCamera){
// perspective
varoffset=newVector3(,,);
offset.copy(position).sub(scope.target);
vardistance=offset.length();
distance *=Math.tan(scope.object.fov/2*Math.PI/180);
// var xd = 2 * distance * deltaX / element.clientHeight;
// var yd = 2 * distance * deltaY / element.clientHeight;
// panLeft( xd, scope.object.matrix );
// panUp( yd, scope.object.matrix );
vardeltaX=pxl *element.clientHeight/(2*distance);
vardeltaY=pyl *element.clientHeight/(2*distance)*(moveDown?-1:1);
return[deltaX,deltaY];
}elseif(scope.objectinstanceofOrthographicCamera){
// orthographic
// panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
// panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
return[deltaX,deltaY];
}else{
// camera neither orthographic nor perspective
console.warn( WARNING: OrbitControls.js encountered an unknown camera type - pan disabled. );
}
}
7、2/3D切換
23D切換的主要內容就是當相機的視線軸與場景的平面垂直時,使用平行投影,這樣用戶只能看到頂面給人的感覺就是2D視圖。所以要根據透視的視錐體計算出平行投影的世景體。
因為用戶會在2D、3D場景下做很多操作,比如平移、縮放、旋轉,要想無縫切換,這個關鍵在於將平行投影與視錐體相機的位置、lookAt方式保持一致;以及將他們放大縮小的關鍵點:distance的比例與zoom來保持一致。
平行投影中,zoom越大代表六面體的首尾兩個面面積越小,放大越大。
8、3D中地理級別
地理級別實際是像素跟墨卡托坐標系下米的對應關係,這個有通用的標準以及計算公式:
r=6378137
resolution=2*PI*r/(2^zoom*256)
各個級別中像素與米的對應關係如下:
resolutionzoom2048blocksize256blocksize scale(dpi=160)
9783.939621420037508.342504688.54361631115.72
3D中的計算策略是,首先需要將3D世界中的坐標與墨卡托單位的對應關係搞清楚,如果已經是以mi來做單位,那麼就可以直接將相機的投影屏幕的高度與屏幕的像素數目做比值,得出的結果跟上面的ranking做比較,選擇不用的級別數據以及比例尺。注意3D地圖中的比例尺並不是在所有屏幕上的所有位置與現實世界都滿足這個比例尺,只能說是相機中心點在屏幕位置處的像素是滿足這個關係的,因為平行投影有近大遠小的效果。
9、poi碰撞
由於標註是永遠朝著相機的,所以標註的碰撞就是把標註點轉換到屏幕坐標系用寬高來計算矩形相交問題。至於具體的碰撞演算法,大家可以在網上找到,這裡不展開。下面是計算poi矩形的代碼
覺得本文對你有幫助?請分享給更多人
關注「前端大全」,提升前端技能
※你一定要知道的 Chrome DevTool 新功能
※十大經典排序演算法的 JS 版
※探索前端黑科技——通過 png 圖的 rgba 值緩存數據
※webpack2 終極優化
TAG:前端大全 |
※iHealth基於Docker的DevOps CI/CD實踐
※基於 hapi的Node.js 小程序後端開發實踐指南
※Apache Storm流計算模型 及WordCount源碼實踐
※實踐出真知!KTM 790 Duke硬懟Triumph Street Triple R
※TalkingData的Spark On Kubernetes實踐
※研發實踐:Mozilla分享如何開發一款WebVR小遊戲
※技術解析系列PouchContainer Goroutine Leak 檢測實踐
※華為在OpenStack Days China上分享混合雲行業場景及優秀實踐
※Georgia Tech-真正的理論與實踐結合
※AI Talk:TensorFlow 分散式訓練的線性加速實踐
※5分鐘了解TencentHub技術架構與DevOps實踐揭秘
※第55期:Python機器學習實踐指南、Tensorflow 實戰Google深度學習框架
※Epic Games發布《虛擬製片實踐指南》
※TensorFlow中國研發負責人李雙峰:TensorFlow從研究到實踐
※RPC框架實踐之:Apache Thrift
※Istio技術與實踐02:源碼解析之Istio on Kubernetes 統一服務發現
※Spring Cloud Contract 在永輝雲創的研發實踐
※GemFire與Greenplum的最佳集成實踐之實施經驗談
※Android Studio中的13條Git實踐
※9項Kubernetes安全最佳實踐