1,介绍
该示例使用的是 r95版本Three.js库。这里利用A*算法实现室内室外寻路、导航功能。
效果图如下:
2,主要说明
1,初始化路网
2,设置路障,我这里是使用鼠标点击设置路障,如下图
3,调用算法传入路网数据实现导航
3,源码
<!DOCTYPE html> <html> <head> <title>Threejs中使用A*算法寻路导航,Threejs室内室外地图导航</title> <script type="text/javascript" src="libs/three.js"></script> <script type="text/javascript" src="libs/OrbitControls.js"></script> <script type="text/javascript" charset="UTF-8" src="libs/other/Tween.min.js"></script> <script type="text/javascript" charset="UTF-8" src="libs/three/loaders/GLTFLoader.js"></script> <script type="text/javascript" src="libs/OBJLoader.js"></script> <script type="text/javascript" src="libs/MTLLoader.js"></script> <script type="text/javascript" src="libs/util.js"></script> <script type="text/javascript" src="libs/util/dat.gui.js"></script> <script type="text/javascript" src="libs/astar.js"></script> <style> body { margin: 0; overflow: hidden; } </style> </head> <body> <div id="webgl-output"></div> <script type="text/javascript"> var camera; var renderer; var clock = new THREE.Clock(); var mixer = new THREE.AnimationMixer(); var clipAction; var animationClip; var length = 36; var ws = 2; var graph = []; // 记录地图 var meshArr = new Array(); // 记录障碍物对象数据 var meshxy = [{ "x": 35, "y": 7 }, { "x": 35, "y": 7 }, { "x": 35, "y": 9 }, { "x": 35, "y": 11 }, { "x": 29, "y": 11 }, { "x": 29, "y": 13 }, { "x": 33, "y": 13 }, { "x": 33, "y": 11 }, { "x": 33, "y": 9 }, { "x": 33, "y": 5 }, { "x": 33, "y": 7 }, { "x": 29, "y": 5 }, { "x": 31, "y": 5 }, { "x": 31, "y": 11 }, { "x": 27, "y": 5 }, { "x": 25, "y": 5 }, { "x": 25, "y": 7 }, { "x": 25, "y": 9 }, { "x": 25, "y": 11 }, { "x": 25, "y": 13 }, { "x": 23, "y": 9 }, { "x": 21, "y": 9 }, { "x": 19, "y": 9 }, { "x": 19, "y": 11 }, { "x": 19, "y": 13 }, { "x": 15, "y": 9 }, { "x": 13, "y": 7 }, { "x": 13, "y": 9 }, { "x": 13, "y": 11 }, { "x": 11, "y": 7 }, { "x": 13, "y": 5 }, { "x": 11, "y": 5 }, { "x": 7, "y": 5 }, { "x": 5, "y": 5 }, { "x": 5, "y": 7 }, { "x": 7, "y": 7 }, { "x": 7, "y": 9 }, { "x": 7, "y": 11 }, { "x": 5, "y": 11 }, { "x": 5, "y": 9 }, { "x": 7, "y": 15 }, { "x": 7, "y": 17 }, { "x": 7, "y": 19 }, { "x": 11, "y": 23 }, { "x": 11, "y": 17 }, { "x": 11, "y": 21 }, { "x": 11, "y": 19 }, { "x": 17, "y": 25 }, { "x": 17, "y": 27 }, { "x": 17, "y": 29 }, { "x": 19, "y": 25 }, { "x": 21, "y": 25 }, { "x": 17, "y": 21 }, { "x": 17, "y": 23 }, { "x": 17, "y": 19 }, { "x": 15, "y": 19 }, { "x": 13, "y": 19 }, { "x": 7, "y": 29 }, { "x": 7, "y": 31 }, { "x": 7, "y": 33 }, { "x": 9, "y": 33 }, { "x": 13, "y": 33 }, { "x": 11, "y": 33 }, { "x": 13, "y": 31 }, { "x": 13, "y": 29 }, { "x": 15, "y": 33 }, { "x": 17, "y": 33 }, { "x": 19, "y": 33 }, { "x": 21, "y": 33 }, { "x": 25, "y": 29 }, { "x": 25, "y": 31 }, { "x": 25, "y": 33 }, { "x": 27, "y": 29 }, { "x": 29, "y": 29 }, { "x": 29, "y": 23 }, { "x": 27, "y": 23 }, { "x": 25, "y": 23 }, { "x": 29, "y": 21 }, { "x": 29, "y": 19 }, { "x": 33, "y": 19 }, { "x": 33, "y": 21 }, { "x": 33, "y": 23 }, { "x": 33, "y": 25 }, { "x": 33, "y": 27 }, { "x": 33, "y": 29 }, { "x": 33, "y": 31 }, { "x": 33, "y": 33 }, { "x": 31, "y": 35 }, { "x": 29, "y": 35 }, { "x": 27, "y": 35 }, { "x": 25, "y": 35 }, { "x": 7, "y": 23 }, { "x": 11, "y": 15 }, { "x": 9, "y": 15 }, { "x": 35, "y": 9 }, { "x": 35, "y": 11 }, { "x": 29, "y": 11 }, { "x": 29, "y": 13 }, { "x": 33, "y": 13 }, { "x": 33, "y": 11 }, { "x": 33, "y": 9 }, { "x": 33, "y": 5 }, { "x": 33, "y": 7 }, { "x": 29, "y": 5 }, { "x": 31, "y": 5 }, { "x": 31, "y": 11 }, { "x": 27, "y": 5 }, { "x": 25, "y": 5 }, { "x": 25, "y": 7 }, { "x": 25, "y": 9 }, { "x": 25, "y": 11 }, { "x": 25, "y": 13 }, { "x": 23, "y": 9 }, { "x": 21, "y": 9 }, { "x": 19, "y": 9 }, { "x": 19, "y": 11 }, { "x": 19, "y": 13 }, { "x": 15, "y": 9 }, { "x": 13, "y": 7 }, { "x": 13, "y": 9 }, { "x": 13, "y": 11 }, { "x": 11, "y": 7 }, { "x": 13, "y": 5 }, { "x": 11, "y": 5 }, { "x": 7, "y": 5 }, { "x": 5, "y": 5 }, { "x": 5, "y": 7 }, { "x": 7, "y": 7 }, { "x": 7, "y": 9 }, { "x": 7, "y": 11 }, { "x": 5, "y": 11 }, { "x": 5, "y": 9 }, { "x": 7, "y": 15 }, { "x": 7, "y": 17 }, { "x": 7, "y": 19 }, { "x": 11, "y": 23 }, { "x": 11, "y": 17 }, { "x": 11, "y": 21 }, { "x": 11, "y": 19 }, { "x": 17, "y": 25 }, { "x": 17, "y": 27 }, { "x": 17, "y": 29 }, { "x": 19, "y": 25 }, { "x": 21, "y": 25 }, { "x": 17, "y": 21 }, { "x": 17, "y": 23 }, { "x": 17, "y": 19 }, { "x": 15, "y": 19 }, { "x": 13, "y": 19 }, { "x": 7, "y": 29 }, { "x": 7, "y": 31 }, { "x": 7, "y": 33 }, { "x": 9, "y": 33 }, { "x": 13, "y": 33 }, { "x": 11, "y": 33 }, { "x": 13, "y": 31 }, { "x": 13, "y": 29 }, { "x": 15, "y": 33 }, { "x": 17, "y": 33 }, { "x": 19, "y": 33 }, { "x": 21, "y": 33 }, { "x": 25, "y": 29 }, { "x": 25, "y": 31 }, { "x": 25, "y": 33 }, { "x": 27, "y": 29 }, { "x": 29, "y": 29 }, { "x": 29, "y": 23 }, { "x": 27, "y": 23 }, { "x": 25, "y": 23 }, { "x": 29, "y": 21 }, { "x": 29, "y": 19 }, { "x": 33, "y": 19 }, { "x": 33, "y": 21 }, { "x": 33, "y": 23 }, { "x": 33, "y": 25 }, { "x": 33, "y": 27 }, { "x": 33, "y": 29 }, { "x": 33, "y": 31 }, { "x": 33, "y": 33 }, { "x": 31, "y": 35 }, { "x": 29, "y": 35 }, { "x": 27, "y": 35 }, { "x": 25, "y": 35 }, { "x": 7, "y": 23 }, { "x": 11, "y": 15 }, { "x": 9, "y": 15 }, { "x": 35, "y": 9 }, { "x": 35, "y": 11 }, { "x": 29, "y": 11 }, { "x": 29, "y": 13 }, { "x": 33, "y": 13 }, { "x": 33, "y": 11 }, { "x": 33, "y": 9 }, { "x": 33, "y": 5 }, { "x": 33, "y": 7 }, { "x": 29, "y": 5 }, { "x": 31, "y": 5 }, { "x": 31, "y": 11 }, { "x": 27, "y": 5 }, { "x": 25, "y": 5 }, { "x": 25, "y": 7 }, { "x": 25, "y": 9 }, { "x": 25, "y": 11 }, { "x": 25, "y": 13 }, { "x": 23, "y": 9 }, { "x": 21, "y": 9 }, { "x": 19, "y": 9 }, { "x": 19, "y": 11 }, { "x": 19, "y": 13 }, { "x": 15, "y": 9 }, { "x": 13, "y": 7 }, { "x": 13, "y": 9 }, { "x": 13, "y": 11 }, { "x": 11, "y": 7 }, { "x": 13, "y": 5 }, { "x": 11, "y": 5 }, { "x": 7, "y": 5 }, { "x": 5, "y": 5 }, { "x": 5, "y": 7 }, { "x": 7, "y": 7 }, { "x": 7, "y": 9 }, { "x": 7, "y": 11 }, { "x": 5, "y": 11 }, { "x": 5, "y": 9 }, { "x": 7, "y": 15 }, { "x": 7, "y": 17 }, { "x": 7, "y": 19 }, { "x": 11, "y": 23 }, { "x": 11, "y": 17 }, { "x": 11, "y": 21 }, { "x": 11, "y": 19 }, { "x": 17, "y": 25 }, { "x": 17, "y": 27 }, { "x": 17, "y": 29 }, { "x": 19, "y": 25 }, { "x": 21, "y": 25 }, { "x": 17, "y": 21 }, { "x": 17, "y": 23 }, { "x": 17, "y": 19 }, { "x": 15, "y": 19 }, { "x": 13, "y": 19 }, { "x": 7, "y": 29 }, { "x": 7, "y": 31 }, { "x": 7, "y": 33 }, { "x": 9, "y": 33 }, { "x": 13, "y": 33 }, { "x": 11, "y": 33 }, { "x": 13, "y": 31 }, { "x": 13, "y": 29 }, { "x": 15, "y": 33 }, { "x": 17, "y": 33 }, { "x": 19, "y": 33 }, { "x": 21, "y": 33 }, { "x": 25, "y": 29 }, { "x": 25, "y": 31 }, { "x": 25, "y": 33 }, { "x": 27, "y": 29 }, { "x": 29, "y": 29 }, { "x": 29, "y": 23 }, { "x": 27, "y": 23 }, { "x": 25, "y": 23 }, { "x": 29, "y": 21 }, { "x": 29, "y": 19 }, { "x": 33, "y": 19 }, { "x": 33, "y": 21 }, { "x": 33, "y": 23 }, { "x": 33, "y": 25 }, { "x": 33, "y": 27 }, { "x": 33, "y": 29 }, { "x": 33, "y": 31 }, { "x": 33, "y": 33 }, { "x": 31, "y": 35 }, { "x": 29, "y": 35 }, { "x": 27, "y": 35 }, { "x": 25, "y": 35 }, { "x": 7, "y": 23 }, { "x": 11, "y": 15 }, { "x": 9, "y": 15 }, { "x": 19, "y": 21 }, { "x": 21, "y": 21 }, { "x": 21, "y": 19 }, { "x": 29, "y": 27 }, { "x": 29, "y": 25 } ]; // 记录障碍坐标物数据 var lineArr = new Array(); // 记录路网数据 var meshbool = false; // 显示障碍物 var linebool = false; // 显示路网 var resultArray = new Array(); var isCaculate = false; function init() { // 创建一个场景,它将包含我们所有的元素,如物体,相机和灯光。 var scene = new THREE.Scene(); var urls = [ 'assets/textures/cubemap/flowers/posx.jpg', 'assets/textures/cubemap/flowers/negx.jpg', 'assets/textures/cubemap/flowers/posy.jpg', 'assets/textures/cubemap/flowers/negy.jpg', 'assets/textures/cubemap/flowers/posz.jpg', 'assets/textures/cubemap/flowers/negz.jpg' ]; var cubeLoader = new THREE.CubeTextureLoader(); scene.background = cubeLoader.load(urls); // 创建一个摄像机,它定义了我们正在看的地方 camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000); // 将摄像机对准场景的中心 camera.position.x = 60; camera.position.y = 35; camera.position.z = 60; camera.lookAt({ x: 0, y: 5, z: -20 }); var orbit = new THREE.OrbitControls(camera); orbit.target = camera.position; orbit.update(); // 创建一个渲染器并设置大小,WebGLRenderer将会使用电脑显卡来渲染场景 // initialize basic renderer renderer = initRenderer({ antialias: true, logarithmicDepthBuffer: true, }); // 将平面添加到场景中 var plane = createPlaneGeometryBasicMaterial(); scene.add(plane); // 在屏幕上显示坐标轴 var axes = new THREE.AxesHelper(100); scene.add(axes); scene.add(new THREE.AmbientLight(0x666666)); scene.add(new THREE.AmbientLight("#ffffff", 1)); initModel(); // initPeople(); initGround(); initGrid(); var i = 0; function tweenComplete() { if (i < points.length) { switch (i) { case 0: aaa.rotateY(Math.PI); break; case 1: case 5: case 8: case 9: aaa.rotateY(-0.5 * Math.PI); break; case 2: case 3: case 4: case 6: case 7: aaa.rotateY(0.5 * Math.PI); break; case 10: mixer.stopAllAction(); break; } tween = new TWEEN.Tween(points[i]) .to(points[i + 1], 3000) .easing(TWEEN.Easing.Linear.None) .onUpdate(function() { aaa.position.set(this.x, this.y, this.z); }) .onComplete(tweenComplete) .start(); i++; } } // 使用GUI调试库 var controls = new function() { this.clickBool = false; this.displayRoadGrid = function() { for (var i = 0; i < lineArr.length; i++) { if (linebool) { // 显示路网 scene.add(lineArr[i].obj); } else { // 隐藏路网 var obj = scene.getObjectByName(lineArr[i].name); scene.remove(obj); } } linebool = !linebool } this.displayObstacles = function() { for (var i = 0; i < meshArr.length; i++) { if (meshbool) { // 显示障碍物 scene.add(meshArr[i].obj); } else { // 隐藏障碍物 var obj = scene.getObjectByName(meshArr[i].name); scene.remove(obj); } } meshbool = !meshbool } this.outputObstacles = function() { for (var i = 0; i < meshArr.length; i++) { meshxy.push({ x: meshArr[i].x, y: meshArr[i].y }) } console.log(meshArr) console.log(lineArr) console.log(meshxy) localStorage.setItem('meshArr', JSON.stringify(meshArr)); localStorage.setItem('lineArr', JSON.stringify(lineArr)); localStorage.setItem('meshxy', JSON.stringify(meshxy)); } } var gui = new dat.GUI(); // 隐藏显示障路网 gui.add(controls, 'displayRoadGrid'); // 隐藏显示障碍物 gui.add(controls, 'displayObstacles'); // 输出障碍物对象 gui.add(controls, 'outputObstacles'); gui.add(controls, "clickBool"); // 启动动画 renderScene(); // 添加模型 function initModel() { var mtlLoader = new THREE.MTLLoader(); mtlLoader.setPath("assets/models/obj_mtl/") mtlLoader.load('city.mtl', function(materials) { materials.preload(); var objLoader = new THREE.OBJLoader(); objLoader.setMaterials(materials); objLoader.load('assets/models/obj_mtl/city.obj', function(object) { var mesh = object; mesh.scale.set(3, 3, 3); mesh.position.set(18, 0, 18); scene.add(mesh); }); }); } // 添加人物模型 function initPeople() { var loader = new THREE.GLTFLoader(); loader.load('assets/models/CesiumMan/CesiumMan.gltf', function(result) { result.scene.scale.set(1, 1, 1); result.scene.translateY(0); aaa = result.scene; scene.add(result.scene); tweenComplete(); mixer = new THREE.AnimationMixer(result.scene); animationClip = result.animations[0]; clipAction = mixer.clipAction(animationClip).play(); animationClip = clipAction.getClip(); }); } // 创建一个地面并设置草坪材质 function createPlaneGeometryBasicMaterial() { var textureLoader = new THREE.TextureLoader(); var cubeMaterial = new THREE.MeshStandardMaterial({ map: textureLoader.load("assets/textures/stone/cd.jpg"), }); cubeMaterial.map.wrapS = THREE.RepeatWrapping; cubeMaterial.map.wrapT = THREE.RepeatWrapping; cubeMaterial.map.repeat.set(18, 18) // 创建地平面并设置大小 var planeGeometry = new THREE.PlaneGeometry(500, 500); var plane = new THREE.Mesh(planeGeometry, cubeMaterial); // 设置平面位置并旋转 plane.rotation.x = -0.5 * Math.PI; plane.position.x = 0; plane.position.z = 0; return plane; } // 绘制线路 function initLine(pArr) { var points = []; var geometry = new THREE.Geometry(); for (var i = 0; i < pArr.length; i++) { var randomX = pArr[i].x; var randomY = pArr[i].y; var randomZ = pArr[i].z; var vector = new THREE.Vector3(randomX, randomY, randomZ); geometry.vertices.push(vector); points.push(vector); } var material = new THREE.LineBasicMaterial({ color: 0x0000FF }); var line = new THREE.Line(geometry, material); scene.add(line); return points; } // 绘制路网 function initGround() { var geometry = new THREE.Geometry(); geometry.vertices.push(new THREE.Vector3(0, 0, 0)); geometry.vertices.push(new THREE.Vector3(length, 0, 0)); for (var i = 0; i <= length / ws; i++) { var material = new THREE.LineBasicMaterial({ color: 0x808080 }); var line = new THREE.Line(geometry, material); line.position.z = i * ws; line.name = "line_" + i; lineArr.push({ name: "line_" + i, obj: line }); scene.add(line); var line = new THREE.Line(geometry, material); line.position.x = i * ws; line.position.z = length; line.rotation.y = 90 * Math.PI / 180; line.name = "line_" + i + 180; lineArr.push({ name: "line_" + i + 180, obj: line }); scene.add(line); } } // 初始化障碍物随机 function initGrid() { for (var i = 0; i < length / ws; i++) { var nodeRow = []; for (var j = 0; j < length / ws; j++) { nodeRow.push(1); } graph.push(nodeRow); } if (meshxy.length > 0) { for (var i = 0; i < meshxy.length; i++) { initObstacles(meshxy[i].x, meshxy[i].y); } } else { for (var i = 0; i < length / ws; i++) { var nodeRow = []; for (var j = 0; j < length / ws; j++) { var salt = Math.random() * 7; if (salt > 2) { nodeRow.push(1); } else { nodeRow.push(0); } if (salt <= 2) { var cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1), new THREE .MeshBasicMaterial({ color: 0xC0C0C0 })); let x = ws * j + ws / 2; let z = ws * i + ws / 2; cube.position.set(x, 1.2, z); scene.add(cube); } } graph.push(nodeRow); } } } //计算路径 function caculatePath(resultArray) { var maps = new Graph(graph); // 地图 var startX = parseInt(resultArray[0].position.z / ws); var startY = parseInt(resultArray[0].position.x / ws); var endX = parseInt(resultArray[1].position.z / ws); var endY = parseInt(resultArray[1].position.x / ws); var start = maps.grid[startX][startY]; var end = maps.grid[endX][endY]; result = astar.search(maps, start, end); if (result.length == 0) { alert("无可到达路径"); cleanSphere(); return; } var nArr = [{ x: resultArray[0].position.x, z: resultArray[0].position.z, y: 1.2 }]; for (var i = 0; i < result.length; i++) { let d = { x: result[i].y * ws + ws / 2, y: 1.2, z: result[i].x * ws + ws / 2, } nArr.push(d); } initLine(nArr); } //清除小球 function cleanSphere() { let child = scene.children; //获取场景中的所有子对象 for (var i = 0; i < child.length; i++) { if (child[i].geometry instanceof THREE.SphereGeometry) { //几何对象是球体几何 scene.remove(child[i]); //从场景中移除 i--; } } isCaculate = false; } //初始球体 function initSphere(x, z) { if (isCaculate) { cleanSphere(); } var geometry = new THREE.SphereGeometry(ws / 2, 30, 30); //球体几何 var material = new THREE.MeshBasicMaterial({ color: 0xffff00 }); //网格基础材料 if (resultArray.length == 0) { var sphere = new THREE.Mesh(geometry, material); sphere.position.x = x; sphere.position.y = 1; sphere.position.z = z; resultArray.push(sphere); scene.add(sphere); } else if (resultArray[0].position.x != x || resultArray[0].position.z != z) { var sphere = new THREE.Mesh(geometry, material); sphere.position.x = x; sphere.position.y = 1; sphere.position.z = z; resultArray.push(sphere); scene.add(sphere); caculatePath(resultArray); isCaculate = true; resultArray = new Array(); } } // 绘制障碍物 function initObstacles(x, z) { var name = "mesh_" + x + z; var obj = scene.getObjectByName(name); if (obj) { scene.remove(obj); for (var i = 0; i < meshArr.length; i++) { if (meshArr[i].name == name) { meshArr.splice(i, 1); } } graph[parseInt(z / ws)][parseInt(x / ws)] = 1; } else { var geometry = new THREE.BoxGeometry(ws, ws, ws); //球体几何 var material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); var sphere = new THREE.Mesh(geometry, material); sphere.position.x = x; sphere.position.y = 1; sphere.position.z = z; sphere.name = name; scene.add(sphere); meshArr.push({ name: name, obj: sphere, x: x, y: z, }); graph[parseInt(z / ws)][parseInt(x / ws)] = 0; } } // 拾取对象 function pickupObjects(event) { // 点击屏幕创建一个向量 var raycaster = new THREE.Raycaster(); var vector = new THREE.Vector2((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window .innerHeight) * 2 + 1); var fxl = new THREE.Vector3(0, 1, 0); var groundplane = new THREE.Plane(fxl, 0); raycaster.setFromCamera(vector, camera); var ray = raycaster.ray; let intersects = ray.intersectPlane(groundplane); let x = intersects.x; let z = intersects.z; if (x < 0 || z < 0 || length < z || length < x) { return; } var k, m; for (var i = 0; i < length; i += ws) { if (x >= i && x < i + ws) { k = i + ws / 2; } } for (var j = 0; j < length; j += ws) { if (z >= j && z < j + ws) { m = j + ws / 2; } } if (controls.clickBool) { initSphere(k, m); //初始化球体 } else { initObstacles(k, m); } } document.addEventListener('click', pickupObjects, false); //监听单击拾取对象初始化球体 // 动画渲染 var step = 5; function renderScene() { TWEEN.update(); var delta = clock.getDelta(); orbit.update(); mixer.update(delta); // 使用requestAnimationFrame函数进行渲染 requestAnimationFrame(renderScene); renderer.render(scene, camera); } // 渲染的场景 renderer.render(scene, camera); // 创建一个球形几何体 function createSphereGeometryLambertMaterial(point) { // 创建一个球体 var sphereGeometry = new THREE.SphereGeometry(0.2, 20, 20); var sphereMaterial = new THREE.MeshBasicMaterial({ color: 0x7777ff, wireframe: true }); var sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 }); var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); // 设置该物体投射阴影 sphere.castShadow = true; // 位置范围 sphere.position.x = point.x; sphere.position.y = point.y; sphere.position.z = point.z; return sphere; } } window.onload = init; function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // 监听调整大小事件 window.addEventListener('resize', onResize, false); </script> </body> </html>