公司要做智慧消防楼层可视化,需要用到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.设备动画
待续
8.鼠标绘制
待续