Giter Club home page Giter Club logo

polyanya's Introduction

Polyanya - Compromise-free Pathfinding on a Navigation Mesh

MIT/Apache 2.0 Release Doc Crate

Implementation of Polyanya in Rust! Polyanya is a any-angle path planning algorithm.

A WASM demo made with Bevy is available here.

Usage

use glam::Vec2;
use polyanya::*;

fn main() {
    // Build a mesh from a list of vertices and polygons
    let mesh = Mesh::new(
        vec![
            Vertex::new(Vec2::new(0., 6.), vec![0, -1]),           // 0
            Vertex::new(Vec2::new(2., 5.), vec![0, -1, 2]),        // 1
            Vertex::new(Vec2::new(5., 7.), vec![0, 2, -1]),        // 2
            Vertex::new(Vec2::new(5., 8.), vec![0, -1]),           // 3
            Vertex::new(Vec2::new(0., 8.), vec![0, -1]),           // 4
            Vertex::new(Vec2::new(1., 4.), vec![1, -1]),           // 5
            Vertex::new(Vec2::new(2., 1.), vec![1, -1]),           // 6
            Vertex::new(Vec2::new(4., 1.), vec![1, -1]),           // 7
            Vertex::new(Vec2::new(4., 2.), vec![1, -1, 2]),        // 8
            Vertex::new(Vec2::new(2., 4.), vec![1, 2, -1]),        // 9
            Vertex::new(Vec2::new(7., 4.), vec![2, -1, 4]),        // 10
            Vertex::new(Vec2::new(10., 7.), vec![2, 4, 6, -1, 3]), // 11
            Vertex::new(Vec2::new(7., 7.), vec![2, 3, -1]),        // 12
            Vertex::new(Vec2::new(11., 8.), vec![3, -1]),          // 13
            Vertex::new(Vec2::new(7., 8.), vec![3, -1]),           // 14
            Vertex::new(Vec2::new(7., 0.), vec![5, 4, -1]),        // 15
            Vertex::new(Vec2::new(11., 3.), vec![4, 5, -1]),       // 16
            Vertex::new(Vec2::new(11., 5.), vec![4, -1, 6]),       // 17
            Vertex::new(Vec2::new(12., 0.), vec![5, -1]),          // 18
            Vertex::new(Vec2::new(12., 3.), vec![5, -1]),          // 19
            Vertex::new(Vec2::new(13., 5.), vec![6, -1]),          // 20
            Vertex::new(Vec2::new(13., 7.), vec![6, -1]),          // 21
            Vertex::new(Vec2::new(1., 3.), vec![1, -1]),           // 22
        ],
        vec![
            Polygon::new(vec![0, 1, 2, 3, 4], true),           // 0
            Polygon::new(vec![5, 22, 6, 7, 8, 9], true),       // 1
            Polygon::new(vec![1, 9, 8, 10, 11, 12, 2], false), // 2
            Polygon::new(vec![12, 11, 13, 14], true),          // 3
            Polygon::new(vec![10, 15, 16, 17, 11], false),     // 4
            Polygon::new(vec![15, 18, 19, 16], true),          // 5
            Polygon::new(vec![11, 17, 20, 21], true),          // 6
        ],
    );

    // Get the path between two points
    let from = Vec2::new(12.0, 0.0);
    let to = Vec2::new(3.0, 1.0);
    let path = mesh.path(from, to);

    assert_eq!(
        path.unwrap().path,
        vec![
            Vec2::new(7.0, 4.0),
            Vec2::new(4.0, 2.0),
            Vec2::new(3.0, 1.0)
        ]
    );
}

The code above will build the following mesh, with polygons marked in green, and vertices in red:

example mesh

Original Work

Check the cpp implementation.

index;micro;successor_calls;generated;pushed;popped;pruned_post_pop;length;gridcost
0;4960.92;6974;4368;4313;3823;21;1123.222637572437;1199.73

This crate seems to generate a few more nodes, but tends to be faster than the cpp implementation. There are a few known cases to still improve it:

  • collinear optimisation, when a search node root and interval are all on a same line
  • triangle optimisation, when searching in a triangle polygon
  • when an intersection is very close to a vertex, it sometimes generates an extra slim search node
  • searching start and end nodes is costlier

Compiling this crate with feature stats will output almost the same level of information as the default cpp implementation output.

index;micros;successor_calls;generated;pushed;popped;pruned_post_pop;length
0;2990.083;6983;7748;4314;3828;21;1123.2228

The verbose feature will give the same output as setting verbose to 1.

        pushing: root=(993, 290); left=(989, 303); right=(1001, 288); f=1020.21, g=0.00
        pushing: root=(993, 290); left=(984, 301); right=(988, 303); f=1016.98, g=0.00
        pushing: root=(993, 290); left=(982, 300); right=(984, 301); f=1016.06, g=0.00
        pushing: root=(993, 290); left=(994, 285); right=(981, 299); f=1014.84, g=0.00
popped off: root=(993, 290); left=(994, 285); right=(981, 299); f=1014.84, g=0.00
        intermediate: root=(993, 290); left=(988, 282); right=(981, 299); f=1014.84, g=0.00
        pushing: root=(993, 290); left=(977, 299); right=(980, 299); f=1015.14, g=0.00
        pushing: root=(993, 290); left=(984, 280); right=(976, 297); f=1014.84, g=0.00
popped off: root=(993, 290); left=(984, 280); right=(976, 297); f=1014.84, g=0.00
        pushing: root=(993, 290); left=(973, 296); right=(976, 297); f=1014.84, g=0.00
        pushing: root=(993, 290); left=(970, 295); right=(973, 296); f=1014.86, g=0.00
        pushing: root=(993, 290); left=(967, 294); right=(970, 295); f=1015.01, g=0.00
        pushing: root=(993, 290); left=(965, 293); right=(967, 294); f=1015.28, g=0.00
        pushing: root=(993, 290); left=(977, 276); right=(965, 293); f=1015.58, g=0.00
        pushing: root=(993, 290); left=(983, 279); right=(979, 277); f=1023.95, g=0.00
popped off: root=(993, 290); left=(973, 296); right=(976, 297); f=1014.84, g=0.00
popped off: root=(993, 290); left=(970, 295); right=(973, 296); f=1014.86, g=0.00
popped off: root=(993, 290); left=(967, 294); right=(970, 295); f=1015.01, g=0.00
popped off: root=(993, 290); left=(977, 299); right=(980, 299); f=1015.14, g=0.00
popped off: root=(993, 290); left=(965, 293); right=(967, 294); f=1015.28, g=0.00
popped off: root=(993, 290); left=(977, 276); right=(965, 293); f=1015.58, g=0.00
        pushing: root=(993, 290); left=(963, 292); right=(965, 293); f=1015.58, g=0.00
        pushing: root=(993, 290); left=(961, 291); right=(963, 292); f=1015.94, g=0.00
        pushing: root=(993, 290); left=(971, 273); right=(959, 289); f=1017.13, g=0.00
...

The mesh files used in tests are coming from the cpp implementation and are available under MIT license.

polyanya's People

Contributors

66oj66 avatar elabajaba avatar janhohenheim avatar metadorius avatar mjohnson459 avatar mockersf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

polyanya's Issues

Multiple meshes

I have multiple meshes that represents the various wall/barrier tiles in my tilemap. Currently, polyanya only accepts one mesh that represents the entire map.
It would be nice if we can input multiple meshes instead.

create own mesh formats

create a mesh format:

  • fast and easy to load
  • that can store all baking and optimisations already done

Polyanya should be able to read and write this format

Blocking vs async performance

Hi! Recently did a small test to measure the performance and got very surprising results. (on aurora merged)

Average time per 1000 runs per 34707 verts: 336ms 231us 70ns
Average time per 1000 runs per 1000 verts: 9ms 889us 149ns
Average time per 1000 runs per 34707 verts (async): 217us 79ns
Average time per 1000 runs per 1000 verts (async): 6us 384ns

Can someone explain if maybe I did something wrong (see code below) or how the async version is so much faster? It doesn't look multithreaded, so how?

#![feature(core_intrinsics)]

use std::sync::Arc;

use glam::Vec2;
use humantime::format_duration;
use polyanya::{Mesh, PolyanyaFile};
use tokio::sync::RwLock;

fn run_and_measure<F: FnOnce() -> R, R>(f: F) -> (R, std::time::Duration) {
    let start = std::time::Instant::now();
    let result = f();
    let elapsed = start.elapsed();
    (result, elapsed)
}

#[tokio::main]
async fn main() {
    let mesh: Mesh = PolyanyaFile::from_file("meshes/aurora-merged.mesh").into();
    let mesh_min_x = mesh
        .vertices
        .iter()
        .map(|v| v.coords.x)
        .fold(f32::INFINITY, |a, b| a.min(b));
    let mesh_max_x = mesh
        .vertices
        .iter()
        .map(|v| v.coords.x)
        .fold(f32::NEG_INFINITY, |a, b| a.max(b));
    let mesh_min_y = mesh
        .vertices
        .iter()
        .map(|v| v.coords.y)
        .fold(f32::INFINITY, |a, b| a.min(b));
    let mesh_max_y = mesh
        .vertices
        .iter()
        .map(|v| v.coords.y)
        .fold(f32::NEG_INFINITY, |a, b| a.max(b));
    let width = mesh_max_x - mesh_min_x;
    let height = mesh_max_y - mesh_min_y;

    let runs = 10_000;
    let duration = run_and_measure(|| {
        for _ in 0..runs {
            let start_x = rand::random::<f32>() * width + mesh_min_x;
            let start_y = rand::random::<f32>() * height + mesh_min_y;
            let end_x = rand::random::<f32>() * width + mesh_min_x;
            let end_y = rand::random::<f32>() * height + mesh_min_y;

            std::intrinsics::black_box(
                mesh.path(Vec2::new(start_x, start_y), Vec2::new(end_x, end_y)),
            );
        }
    });

    let duration_per_1000_runs = duration.1 / (runs / 1_000);
    let vertices = mesh.vertices.len();
    println!(
        "Average time per 1000 runs per {} verts: {}",
        vertices,
        format_duration(duration_per_1000_runs).to_string()
    );

    let duration_per_1000_runs_per_1000_verts = duration_per_1000_runs / (vertices as u32 / 1_000);
    println!(
        "Average time per 1000 runs per 1000 verts: {}",
        format_duration(duration_per_1000_runs_per_1000_verts).to_string()
    );

    // Now try the async version
    let runs = 1_000_000;
    let rw_lock_mesh = Arc::new(RwLock::new(mesh));
    let duration = run_and_measure(|| {
        let mut futures = vec![];
        for _ in 0..runs {
            let start_x = rand::random::<f32>() * width + mesh_min_x;
            let start_y = rand::random::<f32>() * height + mesh_min_y;
            let end_x = rand::random::<f32>() * width + mesh_min_x;
            let end_y = rand::random::<f32>() * height + mesh_min_y;

            let rw_lock_mesh = rw_lock_mesh.clone();
            futures.push(async move {
                let mesh = rw_lock_mesh.read().await;
                std::intrinsics::black_box(
                    mesh.get_path(Vec2::new(start_x, start_y), Vec2::new(end_x, end_y))
                        .await,
                );
            });
        }
        futures::future::join_all(futures)
    });

    let duration_per_1000_runs = duration.1 / (runs / 1_000);
    println!(
        "Average time per 1000 runs per {} verts (async): {}",
        vertices,
        format_duration(duration_per_1000_runs).to_string()
    );

    let duration_per_1000_runs_per_1000_verts = duration_per_1000_runs / (vertices as u32 / 1_000);
    println!(
        "Average time per 1000 runs per 1000 verts (async): {}",
        format_duration(duration_per_1000_runs_per_1000_verts).to_string()
    );
}

Thanks for the answer in advance! Very interested in the details :P

Emit polygon path

For 2D pathfinding, the current Path type works fine, but if you want to do 3D pathfinding (with multiple overlapping floors etc.), the current interface is insufficient. In particular, I'm imagining that you project out the X and Y positions of each point in a mesh, which may result in duplicate points. Polyanya supports duplicate points and overlapping/zero-area triangles, at least from my experimentation, but since it only outputs a list of positions it's impossible to know which of many overlapping triangles a given element in a path is from (and therefore what its Z position is). If the indices of polygons traversed were included in the Path then this wouldn't be an issue.

error if obstacles overlap all of the initial outer boundary

I'm was getting errornous behaviour if error if obstacles overlap all of the initial outer boundary of the Polyana::Triangulation.
This resulted in an empty navmesh, because the merge_obstacle function has/had an error/wasn't intended to handle this case.
I think I solved this problem, althought I'm not sure if you want the changes there to be merged upstream, you might want some changes to be done.

Anyway if anyone has a similar problem, for the time beeing you can check if the branch/commit here is doing what you want: main...AnAverageGitUser:polyanya:aagu/myapp current commit 82f2ec7

Support for variable agent sizes

It's not uncommon to have units of different sizes, and you might have areas of the navmesh that are accessible to smaller units but not larger units.

Ideally there'd be some way to pass in the unit radius when searching for a path, and it'd return a valid path for a unit of that size.

In the below image, a small unit would take the leftmost passage, a medium unit could go up, and a large unit has to go right and go all the way around.
agent_size_12-1
(image from https://www.jdxdev.com/blog/2021/07/06/rts-pathfinding-2-dynamic-navmesh-with-constrained-delaunay-triangles/)

It might be possible to just compare the interval size against the unit radius, and reject intervals that are too small, but that might end up being too pessimistic (or I might be misremembering how polyanya works)?

Per-triangle movement cost

It would be nice to support non-uniform cost pathfinding. For instance, agents would be able to avoid crossing water or other difficult terrain.

I don't know much about the algorithm inner workings, but I would imagine this would require being able to compute movement cost along any given line, which may or may not need another acceleration structure, and will certainly hurt performance. To avoid that, navmeshes could be made generic over the movement cost, with () being default.

Another option would be to support pathfinding over multiple navmeshes, connected by links, where each navmesh would have its own movement cost. This would have an added benefit of supporting chunked worlds, as well as multiple 3d levels.

Interactive example

Goal

Provide an example showcasing pathfinding interactivity.

Something as simple as possible, 2D, working on wasm should be the objective.

Existing work

Below showcases a 3d interactivity, which code might be relevant: https://github.com/Vrixyz/meshquisse)

navmesh_visualizer.mp4

outdated bvh2d

I tried to build a project with polyanya as the dependency on a new machine and I got this error:

error: failed to select a version for the requirement `bvh2d = "^0.3"`
candidate versions found which didn't match: 0.4.0
location searched: Git repository https://github.com/mockersf/bvh2d
required by package `polyanya v0.4.0 (https://github.com/vleue/polyanya?branch=radius-baking#17e68a44)`       
    ... which satisfies git dependency `polyanya` of package `bevy_pathmesh v0.5.0 (https://github.com/vleue/bevy_pathmesh?branch=interactive#70d3c5f4)`

Checking the toml on polyanya shows, that it uses 0.3 and the git repository seems to be 0.3, but the cargo.toml show 0.4.

merge polygons from a mesh

  • find the biggest polygon in a mesh
  • try to merge with its neighbour, keeping everything convex
  • repeat until all done

Infinite loop with some meshes and paths

In some circumstances I'm hitting an infinite loop when trying to generate a path. It seems to be related to the delta.

For example, with this mesh and a delta of 0.01:

mesh
2
10 8
3.775000 2.675000 2 -1 5
3.775000 1.175000 4 -1 4 5 7
2.125000 1.175000 6 -1 0 1 3 4 6
2.125000 1.275000 2 -1 0
1.775000 1.275000 3 -1 0 1
1.775000 1.175000 4 -1 1 2 3
0.375000 1.175000 2 -1 2
0.375000 0.375000 4 -1 2 3 6
4.075000 0.375000 4 -1 4 6 7
4.075000 2.675000 3 -1 5 7
3 4 2 3
3 5 2 4
3 7 5 6
3 5 7 2
3 1 2 8
3 0 1 9
3 7 8 2
3 8 9 1

These path fails to generate:

Creating path from Vec2(2.0705771, 1.2326102) to Vec2(2.074013, 1.0023998)
Creating path from Vec2(2.0911932, 1.2326102) to Vec2(2.7302842, 1.0573754)
Creating path from Vec2(1.8231869, 1.2119944) to Vec2(1.9846778, 0.93711627)

With a delta of 0.1, I can't get the infinite loop to happen on this mesh but these paths are wrong:

# should be direct but goes via a vertex
Creating path from Vec2(1.9406888, 1.1835598) to Vec2(2.0856483, 0.85704863)

# goes outside grid
Creating path from Vec2(2.1137958, 1.2623727) to Vec2(3.8983479, 1.3031867)

With all that said, I'm assuming there is something wrong with the mesh but as far as I can tell, it is valid based on the documentation. For reference it looks like this with the failures generally happening with polygons 0 and 1:

simple_mesh

Rules about valid polygons?

I'm curious about what are the requirements for a given polygon.

To clarify, why not just create a single polygon containing all of the points for the example in the README? I presume this is invalid somehow.

Polygon::new(vec![0, 1, 9, 5, 22, 6, 7, 8, 10, 15, 18, 19, 16, 17, 20, 21, 11, 13, 14, 12, 2, 3, 4], true)

image

Consider making `delta` in `get_point_location` configurable

Right now, from and to in the path finding can be off by up to 0.1 from any vertex in the path mesh. This value seems arbitrary, as the size of the mesh can vary greatly by use case. It seems it would be useful to change this offset, e.g. you could set it to 1 if you knew that your mesh is guaranteed to have a vertex every 1 unit.

Polyanya can't load mesh files it writes

When creating a mesh file using PolyanyaFile it creates the file mostly to spec but doesn't include the polygon neighbours as defined here.

Using the same example, instead of the expected

mesh
2
4 2
0.0 0.0 2 0 -1
1.5 0.0 3 0 1 -1
1.5 1.5 2 1 -1
0.0 1.5 3 -1 1 0
3 0 1 3 -1 -1 1
3 1 2 3 0 -1 -1

it writes

mesh
2
4 2
0.0 0.0 2 0 -1
1.5 0.0 3 0 1 -1
1.5 1.5 2 1 -1
0.0 1.5 3 -1 1 0
3 0 1 3
3 1 2 3

Loading this file then fails this assert

assert!(data.len() == nb * 2);

While removing this assert does allow the file to be loaded, the one-way flag is wrong as it defaults to true when there are no neighbours.

I saved the file with this code:

        let mut mesh = PolyanyaMesh::new(vertices, polygons);
        let file: PolyanyaFile = mesh.clone().into();
        file.to_file(&"nav_mesh.mesh");

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.