用three.js開發三維地圖實例


公司要做智慧消防樓層可視化,需要用到web3d,開源的引擎中先研究了cesium三維地球,但cesium做樓層感覺是大材小用,而且體驗也不好,最終選用的是功能強大、更適合小型場景的three。

three是圖形引擎,而web二維三維地圖都是基於圖形引擎的,所以拿three來開發需求簡單的三維地圖應用是沒什么問題的。

1.坐標轉換

      實際地理坐標為經度、緯度、高度,而three.js使用的是右手坐標系x、y、z,本來考慮的是將經緯度坐標轉換成墨卡托,再去和three的坐標系對應。而實際項目中,經緯度轉墨卡托后,墨卡托的值太大,對應到three坐標系中,坐標距離原點太遠,用戶交互后,會有精度損失,於是先定義一個中間點,然后將墨卡托的結果減去這個中間點的值。(我自己是經度對應z軸,緯度對應x軸,高度對應y軸)

function lonlatToMercator(lon,lat,height){
    var z = height ? height:0;
    var x = (lon / 180.0) * 20037508.3427892;
    var y = (Math.PI / 180.0) * lat;
    var tmp = Math.PI / 4.0 + y / 2.0;
    y = 20037508.3427892 * Math.log(Math.tan(tmp)) / Math.PI;
    return {x: x,y: y,z: z};
}
var center = lonlatToMercator(lonVal,latVal,heightVal);
function lonlatToThree(lon,lat,height){
    var z = height? height:0;
    var x = (lon / 180.0) * 20037508.3427892;
    var y = (Math.PI / 180.0) * lat;
    var tmp = Math.PI / 4.0 + y / 2.0;
    y = 20037508.3427892 * Math.log(Math.tan(tmp)) / Math.PI;
    var result = {
        x: x - center.x,
        y: y - center.y,
        z: z -center.z
    };
    return result;
}

2.加載模型

 

    three.js支持多種模型加載,我是用草圖大師建的模型,於是直接轉成collada模型,然后使用three的collada模型加載器加載模型。因為要和three.js對應,而模型默認位於x-z軸上,所以要進行模型翻轉等操作。

3.創建標注

    three中,創建始終朝向相機的POI標注可以使用Sprite類,也可以使用canvas創建圖標+文字類型的圖形作為Sprite的紋理。sprite默認是有一個固定的3d長度,相機距離sprite越近,sprite在屏幕上越大,反之越小,過大或者過小都會導致sprite的canvas失真模糊,解決方案是計算出該點的屏幕像素與3d坐標長度的比值,然后將sprite縮放到一個合適的3d長度。

var position = sprite.position;
var canvas = sprite.material.map.image;
if(canvas){
    var poiRect = {w:canvas.width,h:canvas.height};
    var scale = getPoiScale(position,poiRect);
    sprite.scale.set(scale[0],scale[1],1.0);
}
 
function getPoiScale(position,poiRect){
    if(!position) return;
    var distance = camera.position.distanceTo(position);
    var top = Math.tan(camera.fov / 2 * Math.PI / 180)*distance;    //camera.fov 相機的拍攝角度
    var meterPerPixel = 2*top/container.clientHeight;
    var scaleX = poiRect.w * meterPerPixel;
    var scaleY = poiRect.h * meterPerPixel;
    return [scaleX,scaleY,1.0];
}

 

 4.標注碰撞

      創建標注之后,放縮時難免會出現標注相互遮蓋的情況,這樣既影響美觀也會遮蓋住地圖信息,這里需要檢測標注間的遮蓋,顯示和不顯示一些標注。

        這里主要是將標注點3d坐標轉成屏幕坐標,再根據sprite中canvas的長度和高度,就可以知道sprite在屏幕的矩形范圍。接下來就是計算各個標注點sprite的矩形相交了。

 var sprite1 = {x:x1,y:y1,w:w1,h:h1};    //sprite1左下角x,y,寬度、高度
    var sprite2 = {x:x2,y:y2,w:w2,h:h2};    //sprite2左下角x,y,寬度、高度
  //檢測兩個標注sprite是否碰撞
    function isPOIRect(sprite1,sprite2){
        var x1 = sprite1.x,y1=sprite1.y,w1=sprite1.w,h1=sprite1.h;
        var x2 = sprite2.x,y2=sprite2.y,w1=sprite2.w,h1=sprite2.h;
        if (x1 >= x2 && x1 >= x2 + w2) {
            return false;
        } else if (x1 <= x2 && x1 + w1 <= x2) {
            return false;
        } else if (y1 >= y2 && y1 >= y2 + h2) {
            return false;
        } else if (y1 <= y2 && y1 + h1 <= y2) {
            return false;
        }else{
            return true;
        }
    }

 

5.加載設備

      創建設備,我同樣使用的是Sprite類,跟創建標注類似,放縮之后,sprite在屏幕上的大小保持不變。

6.設備點擊

    raycaster類用於在3d中被鼠標選中的物體,這同樣可以選中sprite對象,於是用此方法模擬設備的點擊。其中deviceGroup是保存所有設備sprite的object3d對象。

 function onDocumentMouseDown(e) {
        e.preventDefault();
        mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
        //新建一個三維單位向量 假設z方向就是0.5
        //根據照相機,把這個向量轉換到視點坐標系
        var vector = new THREE.Vector3(mouse.x, mouse.y,0.5).unproject(camera);
        //在視點坐標系中形成射線,射線的起點向量是照相機, 射線的方向向量是照相機到點擊的點,這個向量應該歸一標准化。
        var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
        //射線和模型求交,選中一系列直線
        var intersects = raycaster.intersectObjects([deviceGroup],true);          
        if (intersects.length > 0) {
            var intersected = intersects[0].object;
            if(intersected instanceof THREE.Sprite){
                //點擊到設備圖標
            }  
        }
    }

 

7.彈出框

       設備點擊之后,一般都會以彈出框形式展示設備的具體信息,這里需要先定義彈出框的樣式,然后將彈出點設備的三維坐標轉換成屏幕坐標,設置一定的偏移量,再將彈出框放到偏移后的屏幕位置上。然后每次更改相機,重新計算彈出框的位置。

//three世界坐標轉為屏幕坐標
function threeToScreen(position,camera){
    var worldVector = new THREE.Vector3(
            position.x,
            position.y,
            position.z
    );
    var standardVector = worldVector.project(camera);//世界坐標轉標准設備坐標
    var a = window.innerWidth / 2;
    var b = window.innerHeight / 2;
    var x = Math.round(standardVector.x * a + a);//標准設備坐標轉屏幕坐標
    var y = Math.round(-standardVector.y * b + b);//標准設備坐標轉屏幕坐標
    return {
        x: x,
        y: y
    };
}

 

8.設備動畫

        簡單設備動畫可以通過更改設備的材質、大小、位置來實現,比如通過定時更改設備的材質來實現設備圖標的閃爍。

        項目中要模擬火情,因此花了些時間網上參考並用粒子系統做了個火焰動畫,這里先用一個循環通過THREE.Vector3對象創建構成火焰的全部的點,放到THREE.Geometry對象的vertices中;再使用canvas創建火焰的紋理圖形,傳給THREE.PointsMaterial對象(並設置材質透明transparent:true和加法混合THREE.AddictiveBlending),最后以前面的THREE.Geometry和THREE.PointsMaterial創建THREE.Points對象,完成該火焰粒子系統的初始化。

        每個粒子都有單獨的坐標,最后用一定的規律驅動粒子的移動達到動畫的效果。

 

9.鼠標繪制

        在3d中,鼠標的位置對應到三維坐標中是一條射線,因此需要添加繪制平面,點擊時獲取鼠標和繪制平面的交點,作為繪制點。繪制時監聽鼠標的單擊和移動事件。

        繪制線時,鼠標點擊和移動時,直接更改線的geometry中的vertices;繪制面時,不僅僅要更改vertices還要計算所有頂點組合的三角面(我使用的是Earcut.js),作為geometry的faces,最后創建一個以這個geometry為幾何形狀的多邊形mesh。

//positions 三維坐標數組[[x,y,z],[x,y,z],...]
function createPolygon(positions){
    var shapePositons = [];
     for(var i=0;i<positions.length;i++){
        var position = positions[i];
        shapePositons.push(new THREE.Vector3(position[0],position[1],position[2]));
    }
    var data = [];
    for(var i=0;i<positions.length;i++){
        var position = positions[i];
        data.push(position[0],position[1]);
    }
    var faces = [];
    var triangles = Earcut.triangulate(data);
    if(triangles && triangles.length != 0){
        for(var i=0;i<triangles.length;i++){
            var length = triangles.length;
            if(i%3==0 && i < length-2){
                faces.push(new THREE.Face3(triangles[i],triangles[i+1],triangles[i+2]));
            }
        }
    }
    var geometry = new THREE.BufferGeometry();
    geometry.vertices = shapePositons;
    geometry.faces = faces;
    
    var mesh = new THREE.Mesh(geometry,material);
    return mesh;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM