前段時間連續上了一個月班,加班加點完成了一個3D攻堅項目。也算是由傳統web轉型到webgl圖形學開發中,坑很多,作了一下總結分享。web
一、法向量問題算法
var material1 = new __WEBPACK_IMPORTED_MODULE_0_three__["MeshLambertMaterial"]({ emissive: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0], style.fillStyle[1], style.fillStyle[2]), side: __WEBPACK_IMPORTED_MODULE_0_three__["DoubleSide"], shading: __WEBPACK_IMPORTED_MODULE_0_three__["FlatShading"], vertexColors: __WEBPACK_IMPORTED_MODULE_0_three__["VertexColors"] }); var material2 = new __WEBPACK_IMPORTED_MODULE_0_three__["MeshLambertMaterial"]({ color: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0] * 0.1, style.fillStyle[1] * 0.1, style.fillStyle[2] * 0.1), emissive: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0] * 0.9, style.fillStyle[1] * 0.9, style.fillStyle[2] * 0.9), side: __WEBPACK_IMPORTED_MODULE_0_three__["DoubleSide"], shading: __WEBPACK_IMPORTED_MODULE_0_three__["FlatShading"], vertexColors: __WEBPACK_IMPORTED_MODULE_0_three__["VertexColors"] });
三、POI標註canvas
Three中建立始終朝向相機的POI可使用Sprite類,同時能夠將文字和圖片繪製在canvas上,將canvas做爲紋理貼圖放到Sprite上。但這裏的一個問題是canvas圖像將會失真,緣由是沒有合理的設置sprite的scale,致使圖片被拉伸或縮放失真。api
問題的解決思路是要保證在3d世界中的縮放尺寸,通過一系列變換投影到相機屏幕後仍然與canvas在屏幕上的大小保持一致。這須要咱們計算出屏幕像素與3d世界中的長度單位的比值,而後將sprite縮放到合適的3d長度。 性能優化
function fromSreenToNdc(x, y, container) { return { x: x / container.offsetWidth * 2 - 1, y: -y / container.offsetHeight * 2 + 1, z: 1 }; } function fromNdcToScreen(x, y, container) { return { x: (x + 1) / 2 * container.offsetWidth, y: (1 - y) / 2 * container.offsetHeight }; }
unproject: function () { var matrix = new Matrix4(); return function unproject( camera ) { matrix.multiplyMatrices( camera.matrixWorld, matrix.getInverse( camera.projectionMatrix ) ); return this.applyMatrix4( matrix ); }; }(),
將獲得的3d點與相機位置結合起來作一條射線,分別與場景中的物體進行碰撞檢測。首先與物體的外包球進行相交性檢測,與球不相交的排除,與球相交的保存進入下一步處理。將全部外包球與射線相交的物體按照距離相機遠近進行排序,而後將射線與組成物體的三角形作相交性檢測。求出相交物體。固然這個過程也由Three中的RayCaster作了封裝,使用起來很簡單:app
mouse.x = ndcPos.x; mouse.y = ndcPos.y; this.raycaster.setFromCamera(mouse, camera); var intersects = this.raycaster.intersectObjects(this._getIntersectMeshes(floor, zoom), true);
五、性能優化dom
隨着場景中的物體愈來愈多,繪製過程愈來愈耗時,致使手機端幾乎沒法使用。ide
在圖形學裏面有個很重要的概念叫「one draw all」一次繪製,也就是說調用繪圖api的次數越少,性能越高。好比canvas中的fillRect、fillText等,webgl中的drawElements、drawArrays;因此這裏的解決方案是對相一樣式的物體,把它們的側面和頂面統一放到一個BufferGeometry中。這樣能夠大大下降繪圖api的調用次數,極大的提高渲染性能。性能
這樣解決了渲染性能問題,然而帶來了另外一個問題,如今是吧全部樣式相同的面放在一個BufferGeometry中(咱們稱爲樣式圖形),那麼在麪點擊時候就沒法單獨判斷出究竟是哪一個物體(咱們稱爲物體圖形)被選中,也就沒法對這個物體進行高亮縮放處理。個人處理方式是,把全部的物體單獨生成物體圖形保存在內存中,作麪點擊的時候用這部分數據來作相交性檢測。對於選中物體後的高亮縮放處理,首先把樣式面中相應部分裁減掉,而後把選中的物體圖形加入到場景中,對它進行縮放高亮處理。裁剪方法是,記錄每一個物體在樣式圖形中的其實索引位置,在須要裁切時候將這部分索引制零。在須要恢復的地方在把這部分索引恢復成原狀。測試
六、麪點擊移動到屏幕中央
這部分也是遇到了很多坑,首先的想法是:
this.unprojectPan = function(deltaVector, moveDown) { // var getProjectLength() var element = scope.domElement === document ? scope.domElement.body : scope.domElement; var cxv = new Vector3(0, 0, 0).setFromMatrixColumn(scope.object.matrix, 0);// 相機x軸 var cyv = new Vector3(0, 0, 0).setFromMatrixColumn(scope.object.matrix, 1);// 相機y軸 // 相機軸都是單位向量 var pxl = deltaVector.dot(cxv)/* / cxv.length()*/; // 向量在相機x軸的投影 var pyl = deltaVector.dot(cyv)/* / cyv.length()*/; // 向量在相機y軸的投影 // offset=dx * vector(cx) + dy * vector(cy.project(xoz).normalize) // offset由相機x軸方向向量+相機y軸向量在xoz平面的投影組成 var dv = deltaVector.clone(); dv.sub(cxv.multiplyScalar(pxl)); pyl = dv.length(); if ( scope.object instanceof PerspectiveCamera ) { // perspective var position = scope.object.position; var offset = new Vector3(0, 0, 0); offset.copy(position).sub(scope.target); var distance = 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 ); var deltaX = pxl * element.clientHeight / (2 * distance); var deltaY = pyl * element.clientHeight / (2 * distance) * (moveDown ? -1 : 1); return [deltaX, deltaY]; } else if ( scope.object instanceof OrthographicCamera ) { // 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 ); var deltaX = pxl * element.clientWidth * scope.object.zoom / (scope.object.right - scope.object.left); var deltaY = pyl * element.clientHeight * scope.object.zoom / (scope.object.top - scope.object.bottom); return [deltaX, deltaY]; } else { // camera neither orthographic nor perspective console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); } }
七、2/3D切換
23D切換的主要內容就是當相機的視線軸與場景的平面垂直時,使用平行投影,這樣用戶只能看到頂面給人的感受就是2D視圖。因此要根據透視的視錐體計算出平行投影的世景體。
由於用戶會在2D、3D場景下作不少操做,好比平移、縮放、旋轉,要想無縫切換,這個關鍵在於將平行投影與視錐體相機的位置、lookAt方式保持一致;以及將他們放大縮小的關鍵點:distance的比例與zoom來保持一致。
r=6378137
resolution=2*PI*r/(2^zoom*256)
各個級別中像素與米的對應關係以下:
resolution zoom 2048 blocksize 256 blocksize scale(dpi=160)
156543.0339 0 320600133.5 40075016.69 986097851.5
78271.51696 1 160300066.7 20037508.34 493048925.8
39135.75848 2 80150033.37 10018754.17 246524462.9
19567.87924 3 40075016.69 5009377.086 123262231.4
9783.939621 4 20037508.34 2504688.543 61631115.72
4891.96981 5 10018754.17 1252344.271 30815557.86
2445.984905 6 5009377.086 626172.1357 15407778.93
1222.992453 7 2504688.543 313086.0679 7703889.465
611.4962263 8 1252344.271 156543.0339 3851944.732
305.7481131 9 626172.1357 78271.51696 1925972.366
152.8740566 10 313086.0679 39135.75848 962986.1831
76.4370283 11 156543.0339 19567.87924 481493.0916
38.2185141 12 78271.51696 9783.939621 240746.5458
19.1092571 13 39135.75848 4891.96981 120373.2729
9.5546285 14 19567.87924 2445.984905 60186.63645
4.7773143 15 9783.939621 1222.992453 30093.31822
2.3886571 16 4891.96981 611.4962263 15046.65911
1.1943286 17 2445.984905 305.7481131 7523.329556
0.5971643 18 1222.992453 152.8740566 3761.664778
0.2985821 19 611.4962263 76.43702829 1880.832389
0.1492911 20 305.7481131 38.21851414 940.4161945
0.0746455 21
0.0373227 22
3D中的計算策略是,首先須要將3D世界中的座標與墨卡託單位的對應關係搞清楚,若是已是以mi來作單位,那麼就能夠直接將相機的投影屏幕的高度與屏幕的像素數目作比值,得出的結果跟上面的ranking作比較,選擇不用的級別數據以及比例尺。注意3D地圖中的比例尺並非在全部屏幕上的全部位置與現實世界都知足這個比例尺,只能說是相機中心點在屏幕位置處的像素是知足這個關係的,由於平行投影有近大遠小的效果。
九、poi碰撞
因爲標註是永遠朝着相機的,因此標註的碰撞就是把標註點轉換到屏幕座標系用寬高來計算矩形相交問題。至於具體的碰撞算法,你們能夠在網上找到,這裏不展開。下面是計算poi矩形的代碼
export function getPoiRect(poi, zoomLevel, wrapper) { let style = getStyle(poi.styleId, zoomLevel); if (!style) { console.warn("style is invalid!"); return; } let labelStyle = getStyle(style.labelid, zoomLevel); if (!labelStyle) { console.warn("labelStyle is invalid!"); return; } if (!poi.text) { return; } let charWidth = (TEXTPROP.charWidth || 11.2) * // 11.2是根據測試獲得的估值 (labelStyle.fontSize / (TEXTPROP.fontSize || 13)); // 13是獲得11.2時的fontSize // 返回2d座標 let x = 0;//poi.points[0].x; let y = 0;//-poi.points[0].z; let path = []; let icon = iconSet[poi.styleId]; let iconWidh = (icon && icon.width) || 32; let iconHeight = (icon && icon.height) || 32; let multi = /\//g; let firstLinePos = []; let textAlign = null; let baseLine = null; let hOffset = (iconWidh / 2) * ICONSCALE; let vOffset = (iconHeight / 2) * ICONSCALE; switch(poi.direct) { case 2: { // 左 firstLinePos.push(x - hOffset - 2); firstLinePos.push(y); textAlign = 'right'; baseLine = 'middle'; break; }; case 3: { // 下 firstLinePos.push(x); firstLinePos.push(y - vOffset - 2); textAlign = 'center'; baseLine = 'top'; break; }; case 4: { // 上 firstLinePos.push(x); firstLinePos.push(y + vOffset + 2); textAlign = 'center'; baseLine = 'bottom'; break; }; case 1:{ // 右 firstLinePos.push(x + hOffset + 2); firstLinePos.push(y); textAlign = 'left'; baseLine = 'middle'; break; }; default: { firstLinePos.push(x); firstLinePos.push(y); textAlign = 'center'; baseLine = 'middle'; } } path = path.concat(firstLinePos); let minX = null, maxX = null; let minY = null, maxY = null; let parts = poi.text.split(multi); let textWidth = 0; if (wrapper) { // 漢字和數字的寬度是不一樣的,因此必須使用measureText來精確測量 let textWidth1 = wrapper.context.measureText(parts[0]).width; let textWidth2 = wrapper.context.measureText(parts[1] || '').width; textWidth = Math.max(textWidth1, textWidth2); } else { textWidth = Math.max(parts[0].length, parts[1] ? parts[1].length : 0) * charWidth; } if (textAlign === 'left') { minX = x - hOffset; maxX = path[0] + textWidth; // 只用第一行文本 } else if (textAlign === 'right') { minX = path[0] - textWidth; maxX = x + hOffset; } else { // center minX = x - Math.max(textWidth / 2, hOffset); maxX = x + Math.max(textWidth / 2, hOffset); } if (baseLine === 'top') { maxY = y + vOffset; minY = y - vOffset - labelStyle.fontSize * parts.length; } else if (baseLine === 'bottom') { maxY = y + vOffset + labelStyle.fontSize * parts.length; minY = y - vOffset; } else { // middle minY = Math.min(y - vOffset, path[1] - labelStyle.fontSize / 2); maxY = Math.max(y + vOffset, path[1] + labelStyle.fontSize * (parts.length + 0.5 - 1)); } return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } }; }