blog: 使用导航网格实现寻路
All checks were successful
🚀 Build and deploy by ftp / 🎉 Deploy (push) Successful in 11m37s

This commit is contained in:
Ivan Li 2023-09-14 14:05:57 +00:00
parent f3a25f7a46
commit 44174c5f36
3 changed files with 9034 additions and 1 deletions

View File

@ -1 +1,41 @@
{"arch-linux":3,"环境搭建":3,"vps":3,"linux":1,"pve":2,"xray":2,"acme":1,"acmesh":1,"docker":3,"docker-compose":1,"内网穿透":1,"github-actions":1,"cicd":1,"de":1,"debian":1,"blog":1,"markdown":1,"nextjs":1,"tailwind-css":1,"verdaccio":1,"self-hosted":3,"caddy":2,"registry":1,"nodejs":1,"zerotier":1,"tailscale":1,"sd-wan":1,"nat":1,"frp":1,"react":1,"baas":1,"appwrite":1,"nhost":1,"supabase":1,"sni":1,"tls":1,"reverse-proxy":1,"反向代理":1,"vless":1}
{
"github-actions": 1,
"cicd": 1,
"docker": 3,
"react": 1,
"baas": 1,
"self-hosted": 3,
"appwrite": 1,
"nhost": 1,
"supabase": 1,
"pve": 2,
"de": 1,
"环境搭建": 3,
"debian": 1,
"arch-linux": 3,
"vps": 3,
"zerotier": 1,
"tailscale": 1,
"sd-wan": 1,
"nat": 1,
"frp": 1,
"verdaccio": 1,
"caddy": 2,
"registry": 1,
"nodejs": 1,
"sni": 1,
"tls": 1,
"reverse-proxy": 1,
"反向代理": 1,
"xray": 2,
"vless": 1,
"blog": 1,
"markdown": 1,
"nextjs": 1,
"tailwind-css": 1,
"acme": 1,
"acmesh": 1,
"docker-compose": 1,
"内网穿透": 1,
"linux": 1
}

View File

@ -0,0 +1,84 @@
---
title: 使用导航网格实现寻路
date: '2023-09-12'
tags: ['Three.js', 'Navigation Mesh', '3D', 'Game', 'Path Finding']
draft: false
summary: 本文结合 three-pathfinding 项目的源代码,简要分析如何使用导航网格实现寻路功能。 three-pathfinding 是一个 Three.js 导航网格寻路库。
images: ['https://s3.ivanli.cc/ivan-public/uPic/2023/mZ9HNo.jpeg']
---
本文结合 [donmccurdy/three-pathfinding](https://github.com/donmccurdy/three-pathfinding) 项目的源代码,简要分析如何使用导航网格实现寻路功能。 three-pathfinding 是一个基于 [PatrolJS](https://github.com/nickjanssen/PatrolJS) 实现的 Three.js 导航网格寻路库。
在 Three.js 中使用导航网格实现寻路功能,主要用到以下三个部分:
- 导航网格Navmesh, Navigation Mesh寻路用的地图
- A\*A Star搜索算法用于寻路
- 漏斗算法Funnel Algorithm: 用于在二维平面找到绕过障碍物的最短路径
## 导航网格
导航网格由若干个可供角色行走的、相邻的凸多边形组成,在我们的用例中,是三角形。导航网格的作用是寻路算法提供所需的顶点数据的,本身并没有任何算法。导航网格可以通过 [UPBGE](https://tl.ivanli.cc/m/28) 生成,生成后就是一个 Mesh 物体,所以顶点数据可以直接通过 GLTF 等格式分发。
## A Star 搜索
网络上有很多文章介绍这个算法:
- [A Star Algorithm 总结与实现 | Cheng Wei's Blog](https://shiori.ivanli.cc/bookmark/47/archive/)
本文结合实际应用再简要地说明下。
首先,我们除了会给算法传入三角形的定点数据,还会传了起点和终点,这里分为三个情况:
#### 起点和终点顶点都在同一个三角形之中
不需要执行 A Star 搜索算法,直接将两点用直线连接就是目标路径。
#### 起点或终点不在任意一个的三角形之中
要么通过其他算法将原始的起点和终点在三角形中找到最接近的点,要么放弃这次寻路。因为这是不可处理的意外情况。
#### 起点和终点在不同的三角形之中
这样就能正式执行 A Star 搜索算法了。
### 数据准备
[Builder.\_buildNavigationMesh()](https://github.com/donmccurdy/three-pathfinding/blob/538b57b65e78b37fe033ff845a09659ebed426dd/src/Builder.js#L150) 方法将从 NavMesh 提取所有三角形的顶点,并通过三角形的三个顶点的 x 分量作为 ID找到了每个三角形相邻的三角形数组。
[Build_buildPolygonGroups()](https://github.com/donmccurdy/three-pathfinding/blob/538b57b65e78b37fe033ff845a09659ebed426dd/src/Builder.js#L103) 方法将从上一个方法中返回三角形及其相邻的三角形作为一个参数,返回了若干组三角形,每组内的三角形都能相互联通,也就是在这组三角形上的任意两点都能找到连接的路径。
最后每个三角形会被结构化成 Node长这样
```javascript
{
id: number, // ID
neighbours: nb[], // 相邻的三角形的 id 数组Node.id 数组)
vertexIds: vertexIds[], // 顶点 ID使用三个顶点的 X 分量组成
centroid: Vector3, // 重心
portals: [number, number][] // 与其他三角形公用的边的顶点的索引数组,
// 一般情况下是两个,即一个边与另一个三角形相邻,但是也不排除会三个边都相邻
// 如果三边都相邻,则是 [number, number, number]
}
```
### 开始使用 A Star 搜索路径
A Star 算法怎么跑的本文就不赘述了。
执行 [Pathfinding.findPath()](https://github.com/donmccurdy/three-pathfinding/blob/abc331195143d7ea1242debed4b52500bda8b7fe/src/Pathfinding.js#L106) 时,需要传入 zoneID 和 groupID。通过前面对数据准备的分析我们知道同组的三角形是相通的只要保证起点和终点都在这组三角形的面上正常情况下 findPath 就能求出路径了。
A Star 算法本质上是在若干个点之间求出一组点,连接这些点就是导航路径。这里使用三角形的重心作为这个点。
通过 A Star 算法搜索出路径后, 会获得一组有序的 Node。
## “拉绳”
A Star 算法寻得的路径是比较粗粒度的路径,接下来使用漏斗算法来拉出一条最短路径。
[Channel.js](https://github.com/donmccurdy/three-pathfinding/blob/364fdc5e6c41c6f3835d881edd00565c45ab0401/src/Channel.js) 里便是使用漏斗算法来获取最短路径。值得注意的一点是,这个算法适合平面,并不适合有高度落差联通的导航网格。
漏斗算法参考这篇文章:[图解NavMesh寻路中的漏斗算法 - PointerSMQ - 博客园](https://shiori.ivanli.cc/bookmark/48/archive/)
![](https://s3.ivanli.cc/ivan-public/uPic/2023/JJjp9f.png ' =666x428')
漏斗算法能够让最终路径在绕过障碍物的同时,保证路径最短。从上面的文章中,可以总结一个核心逻辑,每次生成的路径如果超过左边界或右边界,就会增加一个节点,并从此处构造新的漏斗。直到到达终点。

8909
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff