three是圖形引擎,而web二維三維地圖都是基於圖形引擎的,因此拿three來開發需求簡單的三維地圖應用是沒什麼問題的。web
1.座標轉換canvas
實際地理座標爲經度、緯度、高度,而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;
}spa
2.加載模型3d
three.js支持多種模型加載,我是用草圖大師建的模型,因而直接轉成collada模型,而後使用three的collada模型加載器加載模型。由於要和three.js對應,而模型默認位於x-z軸上,因此要進行模型翻轉等操做。orm
3.建立標註對象
three中,建立始終朝向相機的POI標註可使用Sprite類,也可使用canvas建立圖標+文字類型的圖形做爲Sprite的紋理。sprite默認是有一個固定的3d長度,相機距離sprite越近,sprite在屏幕上越大,反之越小,過大或者太小都會致使sprite的canvas失真模糊,解決方案是計算出該點的屏幕像素與3d座標長度的比值,而後將sprite縮放到一個合適的3d長度。three
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;}