数字孪生项目中的导航片及寻路实现算法的探索

在上一篇文章中我们讲了在智慧矿山等项目中的巷道人员定位实现,其中提到了导航片和寻路这两个点,博主在在一开始的时候也是对这两个概念非常感兴趣,于是在项目开发过程中也做了一些研究。

什么是导航片?

导航片可以是一个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;

// 处理顶点信息,构建线段等

第二步:寻路

寻路方法的实现原理通常涉及到图论和搜索算法。在三维场景中,通常是在场景中的网格或者节点图上进行的。列举几个常见的寻路方法和它们的基本原理:

  1. A 算法:* A算法是一种启发式搜索算法,用于在图中找到从起点到终点的最短路径。它结合了Dijkstra算法的精确性和贪心算法的高效性。A算法使用启发式估算函数(heuristic function)来估计从当前节点到目标节点的代价,并选择最佳的路径。

  2. Dijkstra 算法: Dijkstra算法是一种基于广度优先搜索的算法,用于在加权图中找到从一个起点到所有其他节点的最短路径。它通过维护一个优先队列来选择距离起点最短的节点。

  3. 导航网格: 在实时应用中,特别是在游戏开发中,导航网格是常见的寻路工具。场景被划分为规则的网格,每个网格作为一个节点。A*算法等寻路算法可以在这样的网格上进行,以确定从一个网格到另一个网格的路径。

  4. 跳点搜索(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*算法的例子:

  1. 无网格场景: 当场景表示为连续空间而不是离散网格时,A*算法是一种有效的选择。例如,在地图中,节点可以表示为坐标点而不是离散的网格单元。

  2. 启发式估算可用: A算法的效率受益于良好的启发式估算函数。如果你能设计一个准确的启发式函数,A算法可以更快速地找到最优路径。

  3. 实时路径规划: A*算法通常适用于需要实时计算路径的场景,例如实时游戏中的角色导航。其相对较低的计算成本使其成为处理实时性要求的不错选择。

  4. 路径平滑性要求: A*算法在生成路径时可以比较容易地实现平滑效果,通过在最终路径上进行一些后处理操作。

选到了适合的算法,那么我们就用导航片和A算法来实现一个完整的简单的寻路示例




    
    
    3D Pathfinding with Three.js and glTF
    


    


欢迎交流讨论!

你可能感兴趣的:(三维可视化,前端,javascript,开发语言)