javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > Cesium室内路径规划

Cesium基于pathfinding.js室内路径规划实现思路及步骤

作者:lemoggy

PathFinding.js是一个综合性的JavaScript路径查找库,这个项目的目的是提供一个可以很容易地纳入网页游戏的路径查找库,这篇文章主要介绍了Cesium基于pathfinding.js室内路径规划实现思路及步骤的相关资料,需要的朋友可以参考下

😶 前面都是废话,有需要直接拉到底看完整代码。

目标:

一、方案选择

最终选择:二维网格 + A*。

二、整体思路

一句话概括:

把三维室内空间,抽象成一个二维可通行网格,用 A 算最短路,再映射回 Cesium 三维坐标*。

拆开来看,就是下面几个步骤:

三、网格化建模

Demo 中直接在经纬度空间内对室内可行走区域进行网格化,这在小范围室内场景下是完全可行的。

可行走区域定义(多边形)

在 Demo 中,室内可行走区域由一个不规则多边形定义:

let moveArea = [
  { longitude: 118.1799664, latitude: 39.6243076 },
  { longitude: 118.1799603, latitude: 39.6243600 },
  ...
];

该多边形本质上就是「室内地面轮廓」,在 Cesium 中也同步绘制成了一个 Polygon:

viewer.entities.add({
  polygon: {
    hierarchy: polygonPositions,
    material: Cesium.Color.AQUAMARINE.withAlpha(0.5)
  }
});

这样做有两个好处:

网格尺寸与经纬度换算

通过 gridStep 控制网格精度:

gridStep: 0.000005

为了避免经纬度在不同纬度下比例失真,构造网格时做了一次修正:

const avgLat = (minLat + maxLat) / 2;
const latFactor = Math.cos(Cesium.Math.toRadians(avgLat));

this.latStep = gridStep;
this.lonStep = gridStep / latFactor;

实践中的经验值:0.3m ~ 0.5m 一个格子,在室内效果比较平衡。

点在多边形内判断(含洞)

本 Demo 没有直接使用 NavMesh,而是采用 Point In Polygon 的方式判断某个网格中心点是否可行走:

#isWalkable(lon, lat) {
  const point = { longitude: lon, latitude: lat };
  const inOuter = this.#pointInPolygon(point, this.walkableArea);
  const inHole = this.holes.some(hole => this.#pointInPolygon(point, hole));
  return inOuter && !inHole;
}

这样可以非常自然地支持:

常见做法是:

这一步不追求 100% 准确,只要保证不穿墙即可。

四、路径计算

路径计算部分交给 pathfinding.js:

初始化 Grid

import PF from 'pathfinding';

const grid = new PF.Grid(width, height, matrix);

起点 / 终点拾取与网格映射

在 Demo 中,起点和终点通过 Cesium 点击拾取:

const pos = viewer.scene.globe.pick(ray, viewer.scene);

随后将世界坐标映射到网格坐标:

x = Math.floor((lon - minLon) / lonStep);
y = Math.floor((lat - minLat) / latStep);
const finder = new PF.AStarFinder();

const path = finder.findPath(
  startX,
  startY,
  endX,
  endY,
  grid.clone()
);

路径平滑处理

A* 算法输出的是严格沿网格的折线路径:

const smoothPath = PF.Util.smoothenPath(this.grid, path);

平滑后的路径在视觉和实际行走逻辑上都会更自然。

得到的 path 是类似这样的数组:

[
  [3, 5],
  [4, 5],
  [5, 5],
  [5, 6]
]

这是 网格坐标,不是 Cesium 世界坐标。

五、映射回 Cesium 世界坐标

网格 → 局部坐标

const x = originX + gridX * cellSize;
const y = originY + gridY * cellSize;

这里的 origin 是网格左下角对应的世界坐标。

局部坐标 → Cesium 世界坐标

如果场景是局部坐标系:

const position = new Cesium.Cartesian3(x, y, height);

如果是地理坐标系,需要再做一次转换。

六、路径可视化

Demo 中加入了 debug 模式,用于辅助调试:

new PathFinder(moveArea, [], {
  viewer,
  debug: true,
  gridStep: 0.000005
});

常见方式是用 Polyline 或 Primitive:

viewer.entities.add({
  polyline: {
    positions: positionsArray,
    width: 3,
    material: Cesium.Color.CYAN
  }
});

至此,一条完整的室内最短路径就出来了。

后续可以结合模型中添加楼梯口及楼梯转角节点然后在cesium中根据节点名称获取坐标来打通上下楼路径。

七、完整示例代码:

<!DOCTYPE html>
<html>

<head>
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.110/Build/Cesium/Cesium.js"></script>
    <script type="text/javascript" src="js/pathfinding-bower-0.4.18/pathfinding-browser.min.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.110/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
    <style>
        html,
        body,
        #cesiumContainer {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }

        #toolbar {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 999;
            background: rgba(42, 42, 42, 0.8);
            padding: 10px;
            border-radius: 5px;
        }

        button {
            margin: 5px;
            padding: 5px 10px;
        }
    </style>
</head>

<body>
    <div id="toolbar">
        <button id="reSet">重新绘制</button>
    </div>
    <div id="cesiumContainer" style="width: 100%; height: 100vh;"></div>
    <script>
        class PathFinder {
            constructor(walkableArea, holes = [], options = {}) {
                this.walkableArea = walkableArea;
                this.holes = holes;
                this.gridStep = this.gridStep = (typeof options.gridStep === 'number' && options.gridStep > 0)
                    ? options.gridStep
                    : 0.00001;
                this.upHeight = options.upHeight ?? 0.5;
                this.debug = options.debug ?? false;
                this.viewer = options.viewer ?? null;

                if (this.debug && !this.viewer) {
                    console.warn('[PathFinder] 开启 debug 但未提供 viewer,调试图形将不会绘制');
                }

                this.#calculateBounds();
                this.#buildGrid();
            }

            #calculateBounds() {
                const longitudes = this.walkableArea.map(p => p.longitude);
                const latitudes = this.walkableArea.map(p => p.latitude);

                this.minLon = Math.min(...longitudes);
                this.maxLon = Math.max(...longitudes);
                this.minLat = Math.min(...latitudes);
                this.maxLat = Math.max(...latitudes);

                const avgLat = (this.minLat + this.maxLat) / 2;
                const latFactor = Math.cos(Cesium.Math.toRadians(avgLat));

                this.latStep = this.gridStep;
                this.lonStep = this.gridStep / latFactor;
            }

            #pointInPolygon(point, polygon) {
                let inside = false;
                for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
                    const { longitude: xi, latitude: yi } = polygon[i];
                    const { longitude: xj, latitude: yj } = polygon[j];

                    const intersect = ((yi > point.latitude) !== (yj > point.latitude)) &&
                        (point.longitude < ((xj - xi) * (point.latitude - yi)) / (yj - yi) + xi);

                    if (intersect) inside = !inside;
                }
                return inside;
            }

            #isWalkable(lon, lat) {
                const point = { longitude: lon, latitude: lat };
                const inOuter = this.#pointInPolygon(point, this.walkableArea);
                const inHole = this.holes.some(hole => this.#pointInPolygon(point, hole));
                return inOuter && !inHole;
            }

            #buildGrid() {
                this.gridWidth = Math.ceil((this.maxLon - this.minLon) / this.lonStep);
                this.gridHeight = Math.ceil((this.maxLat - this.minLat) / this.latStep);
                this.grid = new PF.Grid(this.gridWidth, this.gridHeight);

                for (let y = 0; y < this.gridHeight; y++) {
                    for (let x = 0; x < this.gridWidth; x++) {
                        const lon = this.minLon + x * this.lonStep + this.lonStep / 2;
                        const lat = this.minLat + y * this.latStep + this.latStep / 2;

                        const walkable = this.#isWalkable(lon, lat);
                        this.grid.setWalkableAt(x, y, walkable);

                        if (walkable && this.debug && this.viewer) {
                            this.#drawDebugCell(x, y, lon, lat);
                        }
                    }
                }
            }

            #drawDebugCell(x, y, lon, lat) {
                const halfLon = this.lonStep / 2;
                const halfLat = this.latStep / 2;
                const corners = [
                    { longitude: lon - halfLon, latitude: lat - halfLat },
                    { longitude: lon + halfLon, latitude: lat - halfLat },
                    { longitude: lon + halfLon, latitude: lat + halfLat },
                    { longitude: lon - halfLon, latitude: lat + halfLat }
                ];
                const positions = corners.map(c =>
                    Cesium.Cartesian3.fromDegrees(c.longitude, c.latitude, this.upHeight)
                );

                this.viewer.entities.add({
                    polyline: {
                        positions,
                        width: 1,
                        material: Cesium.Color.GREEN.withAlpha(0.2)
                    }
                });
            }

            #getGridPosition(cartesian) {
                const carto = Cesium.Cartographic.fromCartesian(cartesian);
                const lon = Cesium.Math.toDegrees(carto.longitude);
                const lat = Cesium.Math.toDegrees(carto.latitude);

                let x = Math.floor((lon - this.minLon) / this.lonStep);
                let y = Math.floor((lat - this.minLat) / this.latStep);

                if (this.#isValidWalkable(x, y)) return { x, y };

                const offsets = [
                    [0, 0], [-1, 0], [1, 0], [0, -1], [0, 1],
                    [-1, -1], [-1, 1], [1, -1], [1, 1]
                ];
                for (const [dx, dy] of offsets) {
                    const nx = x + dx, ny = y + dy;
                    if (this.#isValidWalkable(nx, ny)) return { x: nx, y: ny };
                }
                return null;
            }

            #isValidWalkable(x, y) {
                return x >= 0 && y >= 0 && x < this.gridWidth && y < this.gridHeight && this.grid.isWalkableAt(x, y);
            }

            #drawDebugPath(path) {
                if (!this.viewer || !this.debug) return;

                const positions = path.map(([x, y]) => {
                    const lon = this.minLon + x * this.lonStep + this.lonStep / 2;
                    const lat = this.minLat + y * this.latStep + this.latStep / 2;
                    return Cesium.Cartesian3.fromDegrees(lon, lat, this.upHeight);
                });

                this.viewer.entities.add({
                    polyline: {
                        positions,
                        width: 4,
                        material: Cesium.Color.BLUE.withAlpha(0.2)
                    }
                });
            }

            findPath(startCartesian, endCartesian) {
                const start = this.#getGridPosition(startCartesian);
                const end = this.#getGridPosition(endCartesian);

                if (!start || !end) {
                    const missing = [];
                    if (!start) missing.push('起始点');
                    if (!end) missing.push('结束点');
                    return { success: false, error: `${missing.join('、')} 不在有效路径区域内` };
                }

                const finder = new PF.AStarFinder();
                const path = finder.findPath(start.x, start.y, end.x, end.y, this.grid.clone());

                if (path.length === 0) {
                    return { success: false, error: '无法找到路径' };
                }

                this.#drawDebugPath(path);

                const smoothPath = PF.Util.smoothenPath(this.grid, path);

                return {
                    success: true,
                    path: smoothPath.map(([x, y]) => {
                        const lon = this.minLon + x * this.lonStep + this.lonStep / 2;
                        const lat = this.minLat + y * this.latStep + this.latStep / 2;
                        return [lon, lat];
                    })
                };
            }
        }

        Cesium.Ion.defaultAccessToken = '你的token';
        // Cesium初始化
        const viewer = new Cesium.Viewer('cesiumContainer', {
            geocoder: false,// 搜索
            homeButton: false,// 返回初始视图
            baseLayerPicker: false,// 默认基础图层选择器
            selectionIndicator: false,// 显示选择指示器
            navigationHelpButton: false,// 显示导航帮助
            sceneModePicker: false,// 显示场景模式选择器
            infoBox: false,
        });

        let moveArea = [
            { longitude: 118.17996647742031, latitude: 39.62430762883727, height: 1 },
            { longitude: 118.17996035796666, latitude: 39.62436000550777, height: 1 },
            { longitude: 118.17988686458494, latitude: 39.62436156070048, height: 1 },
            { longitude: 118.17988748779658, latitude: 39.62422268343063, height: 1 },
            { longitude: 118.17965715859314, latitude: 39.62422045106713, height: 1 },
            { longitude: 118.17966204622869, latitude: 39.62416348004258, height: 1 },
            { longitude: 118.17988793209759, latitude: 39.62416297918548, height: 1 },
            { longitude: 118.17988054674845, latitude: 39.62399184423954, height: 1 },
            { longitude: 118.17995540035955, latitude: 39.6239953325062, height: 1 },
            { longitude: 118.1799627247085, latitude: 39.62425940608208, height: 1 },
            { longitude: 118.17998743297957, latitude: 39.6242616728335, height: 1 },
            { longitude: 118.17998278738285, latitude: 39.62399311184268, height: 1 },
            { longitude: 118.18006237646806, latitude: 39.62399267126263, height: 1 },
            { longitude: 118.18004939518086, latitude: 39.624301197839344, height: 1 },
            { longitude: 118.17996647742031, latitude: 39.62430762883727, height: 1 },
        ]
        // 定义一个不规则的多边形(例如,边缘不规则的多边形)
        const polygonPositions = moveArea.map(el => Cesium.Cartesian3.fromDegrees(el.longitude, el.latitude))

        // 创建一个Cesium Polygon
        const polygon = viewer.entities.add({
            id: 'polygon',
            polygon: {
                hierarchy: polygonPositions,
                material: Cesium.Color.AQUAMARINE.withAlpha(0.5),
            }
        });

        // 拾取点击点设置起点终点
        let start = null;
        let end = null;
        const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);

        handler.setInputAction((click) => {
            const ray = viewer.camera.getPickRay(click.position);
            if (!ray) return;
            const pos = viewer.scene.globe.pick(ray, viewer.scene);
            if (!pos) return;
            if (!start) {
                start = pos;
                console.log(pos)
                viewer.entities.add({
                    position: pos,
                    point: { pixelSize: 10, color: Cesium.Color.CYAN },
                    allowPicking: false,
                });
            } else if (!end) {
                end = pos;
                viewer.entities.add({
                    position: pos,
                    point: { pixelSize: 10, color: Cesium.Color.GREENYELLOW },
                    allowPicking: false,
                });
                const pf = new PathFinder(moveArea, [], { viewer, debug: true, gridStep: 0.000005});
                const result = pf.findPath(start, end);
                if (result.success) {
                    let p = result.path.map(([lon, lat]) => {
                        return Cesium.Cartesian3.fromDegrees(lon, lat, 0.1);
                    })
                    viewer.entities.add({
                        polyline: {
                            positions: p,
                            width: 4,
                            material: Cesium.Color.YELLOW
                        }
                    });
                }
            }
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

        // 设置相机视角
        viewer.flyTo(polygon);

        document.getElementById('reSet').addEventListener("click", (event) => {
            const entities = viewer.entities.values;
            const toRemove = entities.filter(
                (entity) => entity.id && !String(entity.id).includes("polygon")
            );
            // 移除这些实体
            toRemove.forEach((entity) => viewer.entities.remove(entity));
            start = null;
            end = null;
        })
    </script>
</body>

</html>

总结

到此这篇关于Cesium基于pathfinding.js室内路径规划实现思路及步骤的文章就介绍到这了,更多相关Cesium室内路径规划内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文