在上一篇文章中我们讲了在智慧矿山等项目中的巷道人员定位实现,其中提到了导航片和寻路这两个点,博主在在一开始的时候也是对这两个概念非常感兴趣,于是在项目开发过程中也做了一些研究。
导航片可以是一个mesh面,一个gltf,也可以是其他任意一个形状的东西,具体取决于项目的需求和建模人员选择的实现方式。在巷道模型中,导航片可能被用于标记路径、起点、终点或其他重要位置。
这个"片"的概念更侧重于导航信息的表示,而不仅仅局限于几何形状。导航片可以包含额外的元数据,例如位置、标识符等。在Three.js中,这可以是一个Mesh对象,但它可能带有额外的自定义属性或命名规范,以便程序可以正确地读取和使用这些信息。
这么解释,肯定还是不够通俗易懂,这么来讲,如果我们在井下做导航片,那其实就是贴着巷道的路面做了一整个片状的模型,或者就叫他路网模型。其实就可以理解成其就是一条条线段,这些线段可以是在3D空间中的几何对象,也可以是在巷道或场景中的虚拟路径。
导航片的目的是为了在程序中帮助确定路径,通常包括起点、终点以及连接它们的路径信息。这些信息可以在Three.js中用于实现自动寻路或导航功能。
我们在项目中加载并拿到这个路网模型(导航片),提取上面的点位信息
var loader = new THREE.GLTFLoader();
loader.load('path/to/your/model.gltf', function (gltf) {
var navigationModel = gltf.scene || gltf.scenes[0];
// 处理导航模型
});
// 假设导航模型是一个Mesh
var navigationMesh = navigationModel.children[0];
// 获取顶点信息
var vertices = navigationMesh.geometry.attributes.position.array;
// 处理顶点信息,构建线段等
寻路方法的实现原理通常涉及到图论和搜索算法。在三维场景中,通常是在场景中的网格或者节点图上进行的。列举几个常见的寻路方法和它们的基本原理:
A 算法:* A算法是一种启发式搜索算法,用于在图中找到从起点到终点的最短路径。它结合了Dijkstra算法的精确性和贪心算法的高效性。A算法使用启发式估算函数(heuristic function)来估计从当前节点到目标节点的代价,并选择最佳的路径。
Dijkstra 算法: Dijkstra算法是一种基于广度优先搜索的算法,用于在加权图中找到从一个起点到所有其他节点的最短路径。它通过维护一个优先队列来选择距离起点最短的节点。
导航网格: 在实时应用中,特别是在游戏开发中,导航网格是常见的寻路工具。场景被划分为规则的网格,每个网格作为一个节点。A*算法等寻路算法可以在这样的网格上进行,以确定从一个网格到另一个网格的路径。
跳点搜索(Jump Point Search): 跳点搜索是一种优化的路径搜索算法,特别适用于网格地图。它通过跳过无法影响最终路径的节点,以减少搜索的复杂性。
我们先用这四种算法都来实现一版简单的寻路,对比一下哪一种更适合我们描述的场景
首先是A算法
// 定义节点类
class Node {
constructor(x, y) {
this.x = x;
this.y = y;
this.g = 0; // 累积的移动代价
this.h = 0; // 启发式估算到目标点的代价
this.f = 0; // f = g + h
this.parent = null;
}
}
// A*算法实现
function astar(start, end, grid) {
// 初始化开放集和关闭集
let openSet = [start];
let closedSet = [];
while (openSet.length > 0) {
// 从开放集中选择f值最小的节点
let currentNode = openSet[0];
for (let i = 1; i < openSet.length; i++) {
if (openSet[i].f < currentNode.f || (openSet[i].f === currentNode.f && openSet[i].h < currentNode.h)) {
currentNode = openSet[i];
}
}
// 将当前节点从开放集移至关闭集
openSet = openSet.filter(node => node !== currentNode);
closedSet.push(currentNode);
// 如果当前节点是目标节点,构建路径并返回
if (currentNode === end) {
let path = [];
let current = currentNode;
while (current) {
path.unshift({ x: current.x, y: current.y });
current = current.parent;
}
return path;
}
// 获取当前节点的邻居节点
let neighbors = getNeighbors(currentNode, grid);
for (let neighbor of neighbors) {
if (closedSet.includes(neighbor)) {
continue; // 跳过已经在关闭集中的节点
}
// 计算移动代价
let tentativeG = currentNode.g + distance(currentNode, neighbor);
// 如果邻居节点不在开放集中或新的代价更小,更新邻居节点的信息
if (!openSet.includes(neighbor) || tentativeG < neighbor.g) {
neighbor.parent = currentNode;
neighbor.g = tentativeG;
neighbor.h = distance(neighbor, end);
neighbor.f = neighbor.g + neighbor.h;
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
}
}
}
}
return null; // 如果开放集为空,表示无法到达目标点
}
// 计算两节点之间的直线距离
function distance(nodeA, nodeB) {
return Math.sqrt(Math.pow(nodeB.x - nodeA.x, 2) + Math.pow(nodeB.y - nodeA.y, 2));
}
// 获取节点的邻居节点
function getNeighbors(node, grid) {
// 这里简化为四邻接,实际中可能需要八邻接或其他更复杂的邻接关系
let neighbors = [];
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) {
continue; // 跳过自身
}
let x = node.x + i;
let y = node.y + j;
// 检查边界
if (x >= 0 && x < grid.length && y >= 0 && y < grid[0].length) {
neighbors.push(grid[x][y]);
}
}
}
return neighbors;
}
// 示例:创建一个简单的网格
let gridSize = 5;
let grid = [];
for (let i = 0; i < gridSize; i++) {
grid[i] = [];
for (let j = 0; j < gridSize; j++) {
grid[i][j] = new Node(i, j);
}
}
// 设置起点和终点
let startNode = grid[0][0];
let endNode = grid[gridSize - 1][gridSize - 1];
// 设置障碍物
grid[1][1].isObstacle = true;
grid[2][1].isObstacle = true;
grid[3][1].isObstacle = true;
// 运行A*算法
let path = astar(startNode, endNode, grid);
console.log(path);
这个例子演示了一个简单的A*算法实现,其中包含了节点类、距离计算、邻居获取等函数。可以根据我们的实际场景和网格结构进行配置和修改。
Dijkstra算法
// 定义节点类
class Node {
constructor(x, y, weight = 1) {
this.x = x;
this.y = y;
this.weight = weight; // 节点权重,默认为1
this.distance = Infinity; // 到达该节点的距离,初始为无穷大
this.visited = false; // 是否被访问过
this.parent = null; // 记录最短路径上的父节点
}
}
// Dijkstra算法实现
function dijkstra(start, end, grid) {
start.distance = 0; // 起点距离设为0
let unvisited = grid.flat(); // 将二维数组展平,形成一维数组
while (unvisited.length > 0) {
// 从未访问的节点中选择距离最小的节点
let currentNode = unvisited.reduce((minNode, node) => (node.distance < minNode.distance ? node : minNode), unvisited[0]);
if (currentNode.distance === Infinity) {
// 如果无法继续访问,说明不可达目标点
break;
}
currentNode.visited = true;
unvisited = unvisited.filter(node => node !== currentNode);
// 获取当前节点的邻居节点
let neighbors = getNeighbors(currentNode, grid);
for (let neighbor of neighbors) {
if (neighbor.visited) {
continue; // 跳过已经访问过的节点
}
let tentativeDistance = currentNode.distance + neighbor.weight;
if (tentativeDistance < neighbor.distance) {
neighbor.distance = tentativeDistance;
neighbor.parent = currentNode;
}
}
}
// 构建路径
let path = [];
let current = end;
while (current) {
path.unshift({ x: current.x, y: current.y });
current = current.parent;
}
return path;
}
// 获取节点的邻居节点
function getNeighbors(node, grid) {
let neighbors = [];
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) {
continue; // 跳过自身
}
let x = node.x + i;
let y = node.y + j;
// 检查边界
if (x >= 0 && x < grid.length && y >= 0 && y < grid[0].length) {
neighbors.push(grid[x][y]);
}
}
}
return neighbors;
}
// 示例:创建一个简单的权重网格
let gridSize = 5;
let grid = [];
for (let i = 0; i < gridSize; i++) {
grid[i] = [];
for (let j = 0; j < gridSize; j++) {
// 设置一些节点的权重
let weight = Math.random() > 0.8 ? Infinity : Math.ceil(Math.random() * 5);
grid[i][j] = new Node(i, j, weight);
}
}
// 设置起点和终点
let startNode = grid[0][0];
let endNode = grid[gridSize - 1][gridSize - 1];
// 运行Dijkstra算法
let path = dijkstra(startNode, endNode, grid);
console.log(path);
该算法也包含了节点类、获取邻居节点等函数。但值得注意的是Dijkstra算法的时间复杂度较高,对于大型网格可能不太适用。
导航网格算法
// 定义节点类
class Node {
constructor(x, y, walkable = true) {
this.x = x;
this.y = y;
this.walkable = walkable; // 表示节点是否可行走
this.parent = null;
this.visited = false; // 是否已经被访问
}
}
// 导航网格类
class NavigationGrid {
constructor(width, height) {
this.width = width;
this.height = height;
this.grid = this.initializeGrid();
}
initializeGrid() {
let grid = [];
for (let i = 0; i < this.width; i++) {
grid[i] = [];
for (let j = 0; j < this.height; j++) {
grid[i][j] = new Node(i, j);
}
}
return grid;
}
setWalkable(x, y, walkable) {
this.grid[x][y].walkable = walkable;
}
getNode(x, y) {
return this.grid[x][y];
}
// 获取节点的邻居节点
getNeighbors(node) {
let neighbors = [];
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) {
continue; // 跳过自身
}
let newX = node.x + i;
let newY = node.y + j;
// 检查边界
if (newX >= 0 && newX < this.width && newY >= 0 && newY < this.height) {
neighbors.push(this.grid[newX][newY]);
}
}
}
return neighbors.filter(neighbor => neighbor.walkable);
}
}
// 寻路算法,使用BFS(广度优先搜索)
function findPath(grid, start, end) {
let queue = [start];
start.visited = true;
while (queue.length > 0) {
let currentNode = queue.shift();
if (currentNode === end) {
// 构建路径
let path = [];
let current = currentNode;
while (current) {
path.unshift({ x: current.x, y: current.y });
current = current.parent;
}
return path;
}
let neighbors = grid.getNeighbors(currentNode);
for (let neighbor of neighbors) {
if (!neighbor.visited) {
neighbor.visited = true;
neighbor.parent = currentNode;
queue.push(neighbor);
}
}
}
return null; // 如果未找到路径,返回null
}
// 示例:创建一个简单的导航网格
let grid = new NavigationGrid(5, 5);
// 设置一些障碍物
grid.setWalkable(1, 1, false);
grid.setWalkable(2, 1, false);
grid.setWalkable(3, 1, false);
// 设置起点和终点
let startNode = grid.getNode(0, 0);
let endNode = grid.getNode(4, 4);
// 运行寻路算法
let path = findPath(grid, startNode, endNode);
console.log(path);
该算法中包含了节点类、导航网格类、获取邻居节点等函数。这里使用的是BFS(广度优先搜索)算法,当然我们也可以考虑其他寻路算法,具体根要根据需求选择。
跳点搜索
// 定义节点类
class Node {
constructor(x, y, walkable = true) {
this.x = x;
this.y = y;
this.walkable = walkable;
this.parent = null;
this.g = 0; // 从起点到该节点的代价
this.h = 0; // 启发式估算到目标点的代价
this.f = 0; // f = g + h
this.diagonal = false; // 标记是否是对角跳点
}
}
// 寻路算法,使用Jump Point Search
function jumpPointSearch(grid, start, end) {
let openSet = [start];
let closedSet = [];
while (openSet.length > 0) {
// 从开放集中选择f值最小的节点
let currentNode = openSet[0];
for (let i = 1; i < openSet.length; i++) {
if (openSet[i].f < currentNode.f || (openSet[i].f === currentNode.f && openSet[i].h < currentNode.h)) {
currentNode = openSet[i];
}
}
// 将当前节点从开放集移至关闭集
openSet = openSet.filter(node => node !== currentNode);
closedSet.push(currentNode);
// 如果当前节点是目标节点,构建路径并返回
if (currentNode === end) {
let path = [];
let current = currentNode;
while (current) {
path.unshift({ x: current.x, y: current.y });
current = current.parent;
}
return path;
}
// 获取当前节点的邻居节点
let neighbors = getNeighbors(grid, currentNode);
for (let neighbor of neighbors) {
if (closedSet.includes(neighbor)) {
continue; // 跳过已经在关闭集中的节点
}
// 计算移动代价
let tentativeG = currentNode.g + distance(currentNode, neighbor);
if (!openSet.includes(neighbor) || tentativeG < neighbor.g) {
neighbor.parent = currentNode;
neighbor.g = tentativeG;
neighbor.h = distance(neighbor, end);
neighbor.f = neighbor.g + neighbor.h;
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
}
}
}
}
return null; // 如果开放集为空,表示无法到达目标点
}
// 计算两节点之间的直线距离
function distance(nodeA, nodeB) {
return Math.sqrt(Math.pow(nodeB.x - nodeA.x, 2) + Math.pow(nodeB.y - nodeA.y, 2));
}
// 获取节点的邻居节点,使用Jump Point Search优化
function getNeighbors(grid, node) {
let neighbors = [];
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) {
continue; // 跳过自身
}
let x = node.x + i;
let y = node.y + j;
// 检查边界
if (x >= 0 && x < grid.length && y >= 0 && y < grid[0].length) {
let neighbor = grid[x][y];
// 检查对角线上是否存在跳点
if (i !== 0 && j !== 0 && canJump(grid, node, i, j)) {
neighbor.diagonal = true;
neighbors.push(neighbor);
} else if (!neighbor.walkable) {
// 进行强制跳点
let forcedNeighbors = getForcedNeighbors(grid, node, i, j);
neighbors.push(...forcedNeighbors);
} else {
neighbors.push(neighbor);
}
}
}
}
return neighbors;
}
// 判断是否可以跳到指定点
function canJump(grid, node, dx, dy) {
let x = node.x + dx;
let y = node.y + dy;
// 到达目标点
if (x === endNode.x && y === endNode.y) {
return true;
}
// 障碍物或越界
if (!grid[x] || !grid[x][y].walkable) {
return false;
}
// 检查对角线方向上的强制邻居
if (dx !== 0 && dy !== 0) {
if (!grid[node.x][y].walkable || !grid[x][node.y].walkable) {
return false;
}
}
// 递归检查
return canJump(grid, grid[x][y], dx, dy);
}
// 获取强制邻居节点
function getForcedNeighbors(grid, node, dx, dy) {
let neighbors = [];
let x = node.x + dx;
let y = node.y + dy;
// 判断是否在对角线上
if (dx !== 0 && dy !== 0) {
let neighbor1 = grid[x][node.y];
let neighbor2 = grid[node.x][y];
if (neighbor1.walkable) {
neighbors.push(neighbor1);
}
if (neighbor2.walkable) {
neighbors.push(neighbor2);
}
}
return neighbors;
}
// 示例:创建一个简单的网格
let gridSize = 5;
let grid = [];
for (let i = 0; i < gridSize; i++) {
grid[i] = [];
for (let j = 0; j < gridSize; j++) {
grid[i][j] = new Node(i, j, Math.random() > 0.2);
}
}
// 设置起点和终点
let startNode = grid[0][0];
let endNode = grid[gridSize - 1][gridSize - 1];
// 运行Jump Point Search算法
let path = jumpPointSearch(grid, startNode, endNode);
console.log(path);
该示例中包含了节点类、距离计算、获取邻居节点等函数。 可以看到,跳点是一种比较高效的寻路算法,其算法效率高消耗小,特别适用于大规模网格地图。
对比上面的四种,我们可以得出结论,跳点算法是相对高效的寻路算法,但是,在项目中,路网并没有网格信息,所以再看,A算法是一种常用于路径搜索的算法,适用于离散或连续空间。它通过使用启发式估算函数(heuristic function)来优化搜索过程,通常表现良好。在没有网格的场景中,A算法是一个很好的选择,特别是对于连续空间的路径规划。
以下是一些情境中适合使用A*算法的例子:
无网格场景: 当场景表示为连续空间而不是离散网格时,A*算法是一种有效的选择。例如,在地图中,节点可以表示为坐标点而不是离散的网格单元。
启发式估算可用: A算法的效率受益于良好的启发式估算函数。如果你能设计一个准确的启发式函数,A算法可以更快速地找到最优路径。
实时路径规划: A*算法通常适用于需要实时计算路径的场景,例如实时游戏中的角色导航。其相对较低的计算成本使其成为处理实时性要求的不错选择。
路径平滑性要求: A*算法在生成路径时可以比较容易地实现平滑效果,通过在最终路径上进行一些后处理操作。
选到了适合的算法,那么我们就用导航片和A算法来实现一个完整的简单的寻路示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Pathfinding with Three.js and glTF</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script type="module">
import * as THREE from 'https://threejs.org/build/three.module.js';
import { GLTFLoader } from 'https://threejs.org/examples/jsm/loaders/GLTFLoader.js';
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xeeeeee);
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 加载gltf模型
const loader = new GLTFLoader();
loader.load('your_model.gltf', (gltf) => {
const model = gltf.scene;
scene.add(model);
// 提取导航信息
const navigationGraph = extractNavigationInfo(model);
// 设置起点和终点
const startNode = findNodeByPosition(navigationGraph, { x: 0, y: 0, z: 0 });
const endNode = findNodeByPosition(navigationGraph, { x: 10, y: 0, z: 0 });
// 运行A*算法
const path = aStar(startNode, endNode);
console.log(path);
// 渲染函数
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
// 启动渲染循环
render();
});
// 寻路算法 - A*
function aStar(start, goal) {
const openSet = [start];
const closedSet = new Set();
while (openSet.length > 0) {
// 从开放集中选择f值最小的节点
let currentIndex = 0;
for (let i = 1; i < openSet.length; i++) {
if (openSet[i].f < openSet[currentIndex].f) {
currentIndex = i;
}
}
const currentNode = openSet[currentIndex];
if (currentNode === goal) {
// 构建路径
const path = [];
let current = currentNode;
while (current) {
path.unshift(current.position.clone());
current = current.parent;
}
return path;
}
openSet.splice(currentIndex, 1);
closedSet.add(currentNode);
// 获取当前节点的邻居节点
const neighbors = currentNode.neighbors;
for (const neighbor of neighbors) {
if (!closedSet.has(neighbor)) {
const tentativeG = currentNode.g + 1; // 在这个简单的示例中,假设每个导航片的代价是1
if (!openSet.includes(neighbor) || tentativeG < neighbor.g) {
neighbor.g = tentativeG;
neighbor.h = heuristic(neighbor, goal);
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = currentNode;
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
}
}
}
}
}
return null; // 如果未找到路径,返回null
}
// 从模型中提取导航信息
function extractNavigationInfo(model) {
const navigationGraph = [];
model.traverse((node) => {
if (node.isMesh) {
const navigable = node.material.color.getHex() !== 0xff0000; // 假设红色表示不可通行
const navNode = new NavigationNode(node.position, navigable);
navigationGraph.push(navNode);
}
});
// 设置导航片的邻居节点
for (const node of navigationGraph) {
node.neighbors = findNeighbors(node, navigationGraph);
}
return navigationGraph;
}
// 根据位置查找最近的导航节点
function findNodeByPosition(navigationGraph, position) {
let closestNode = null;
let closestDistance = Infinity;
for (const node of navigationGraph) {
const distance = node.position.distanceTo(position);
if (distance < closestDistance) {
closestDistance = distance;
closestNode = node;
}
}
return closestNode;
}
// 计算两个节点之间的启发式距离 - 曼哈顿距离
function heuristic(node, goal) {
return Math.abs(node.position.x - goal.position.x) +
Math.abs(node.position.y - goal.position.y) +
Math.abs(node.position.z - goal.position.z);
}
// 查找导航片的邻居节点
function findNeighbors(node, navigationGraph) {
const neighbors = [];
for (const otherNode of navigationGraph) {
if (otherNode !== node && node.isNeighbor(otherNode)) {
neighbors.push(otherNode);
}
}
return neighbors;
}
// 导航节点类
class NavigationNode {
constructor(position, navigable) {
this.position = position.clone();
this.navigable = navigable;
this.neighbors = [];
this.parent = null;
this.g = Infinity;
this.h = 0;
this.f = Infinity;
}
// 判断两个节点是否是邻居
isNeighbor(otherNode) {
return this.position.distanceTo(otherNode.position) < 1.1; // 在这个简单的示例中,假设邻居节点之间的距离小于1.1表示相邻
}
}
</script>
</body>
</html>
欢迎交流讨论!