基于three.js实现3D楼层可视化与交互功能详解

作者:微光时间:2026-10-30 21:59:57浏览:257

本文介绍使用three.js开发小型3D楼层可视化的完整流程。涵盖地理坐标转墨卡托及本地坐标系处理、Collada模型加载与翻转、Sprite标注的自适应缩放与碰撞检测、设备点击交互、弹出框定位及火焰粒子动画实现,为Web3D应用提供实用参考。

在构建智慧消防楼层可视化系统时,Web3D技术是核心需求。虽然Cesium等三维地球引擎功能强大,但在处理小型楼层场景时显得过于厚重且体验不佳,因此最终选择了轻量且功能强大的three.js作为开发引擎。

three.js本质上是一个图形渲染引擎,而Web端的二维或三维地图应用均建立在图形引擎之上。因此,利用three.js开发需求相对简单的三维地图应用是完全可行的方案。

1.坐标转换

实际地理坐标包含经度、纬度和高度,而three.js采用右手坐标系(x、y、z)。起初考虑将经纬度转换为墨卡托坐标再映射到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支持多种模型格式。本项目使用草图大师(SketchUp)建模,并直接转换为Collada格式,随后通过three.js的Collada加载器导入。由于模型默认位于x-z平面,需进行翻转等操作以适配three.js的坐标系要求。

3.创建标注

在three.js中,创建始终面向相机的POI(兴趣点)标注可使用Sprite类。另一种方法是通过Canvas绘制图标和文字,并将其作为Sprite的纹理。Sprite默认具有固定的3D长度,相机距离越近,屏幕显示越大,反之越小。当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];
}

1.坐标转换

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是一个Object3D对象,用于保存所有设备Sprite的集合。

 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
    };
}

1.坐标转换

8.设备动画

简单的设备动画可通过修改设备的材质、大小或位置来实现。例如,通过定时更改设备材质,实现设备图标的闪烁效果,以提示状态变化。

项目中需模拟火情,因此参考网络资源并利用粒子系统实现了火焰动画。首先通过循环创建构成火焰的所有点,使用THREE.Vector3对象,并将这些点加入THREE.Geometry对象的vertices数组中。接着,使用Canvas创建火焰纹理图形,传递给THREE.PointsMaterial对象(设置transparent为true启用透明,并设置THREE.AddictiveBlending为加法混合模式)。最后,利用前述的THREE.Geometry和THREE.PointsMaterial创建THREE.Points对象,完成火焰粒子系统的初始化。

每个粒子拥有独立的坐标,通过特定的算法规律驱动粒子的移动,从而实现流畅的火焰动画效果。

1.坐标转换

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;
}

1.坐标转换

至此,关于使用three.js开发3D地图的实现示例介绍完毕。更多相关three.js 3D地图内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章