Zylann / godot_voxel

Voxel module for Godot Engine
MIT License
2.49k stars 233 forks source link

Navmesh for Non-Blocky Voxel (Lod) Terrain #610

Open ARandomKrilios opened 3 months ago

ARandomKrilios commented 3 months ago

Is your feature request related to a problem? Please describe. It's rather difficult to consider having any kind of in-game AI when you're unable to pathfind with it, a basic, even if rudimentary, Navmesh system for non-blocky Voxel Terrain probably has probably been long overdue and from what I've been able to research, there hasn't been particular progress as to making one.

Describe the solution you'd like I've done a fair amount of research and considered one way of doing things that maybe would be acceptable in performance. (Though I'm still unexperienced with both C++ and Godot Module development, so pardon if parts of this solution are not possible)

We begin when the Voxel Engine has just created the physics mesh (Or at the same time, using the same source) for a block in the terrain, we take that mesh and hand it to Godot, either to NavigationServer3D's bake_from_source_geometry_data, its async variant or with NavigationMeshGenerator's. (This is assuming that calling these Godot methods are viable/performant)

Doing this we would get a NavigationMesh for this block of terrain but it wouldn't quite work just yet because of how agent_radius works. There's going to be a small margin to the borders of the physics mesh (where it would connect to other voxel blocks) so we will have to fix it.

nav0

We probably would save this result somewhere, so when the terrain is updated only a single block is parsed at a time instead of the whole terrain.

Finally, the navmesh is stitched to the other navmeshes, making a single one. We're assuming that there would only be one NavigationRegion3D or equivalent, thus, the feature it has for "automatically merging navmeshes with other regions" will not apply as this is all going to be inside a single region, additionally, overlapping navmeshes are very unpredictable in behavior, so merging is required.

In an ideal world, perhaps this merging step can be done in a similar fashion it is done when making the visual or physics mesh but maybe it will be necessary to make its own implementation for navmeshes. And this is still mostly a "barebones" kind of implementation, as all the Navmesh would be:

But we've gotta start somewhere.

Describe alternatives you've considered Firstly, I tried the obvious, try to use NavigationRegion3D on a VoxelLodTerrain... but it literally gave me nothing, then I've read other issues that talked about Navigation on non-blocky terrain here, but most of the "solutions" I've seen were either based on mostly static terrain, baked the entire terrain each time it was called (In addition of using 2 plugins) or finally requires the user to just "figure it out", while barely having the tools to properly have access to all of the critical voxel data to make a navigation mesh of some sort (And most of these issues are at least 2-3 years old).

Additional context While I'm still learning C++, any indication of where I could search within the source code of the module to make a prototype of some sort would be appreciated. In addition of any observations/critiques/etc.

This is a great module for Godot, and I truly want to make some projects with it, but without any kind of Navigation for AI, it's losing out a lot.

Zylann commented 3 months ago

*TLDR is that I have to start investigating this eventually, just can't tell when. Will mark the issue as "enhancement" to have an explicit one tracking the state of it.


What you describe isn't really new to what I was thinking so far, the same problems remain in terms of actually doing these things:

One thing however, is that it's true I haven't looked in more details for this, as I've been focused on other things. I'm working on this engine on free time, so priorities are partly what people ask, but also partly what I want to do. While I have experience in navigation from Unity, I haven't used navigation much in Godot, so there are things I can't really judge yet. For example your description shows certain things without saying how it's actually done: after baking a navmesh, how do we "extend" the borders? Is there an API to do that? And is it assuming there is a navmesh per chunk and Godot would somehow connect them up, hoping border vertices exactly match? I also can't tell how costly this actually is.

Maybe it's simpler than I realize (putting aside the point I listed earlier). I imagine I could start with doing "a thing" after each chunk mesh loads/updates/unloads. Chunked navigation is a go-to I think. But right now it's mostly speculations, not much actionable yet. For now I guess the first step is, I have to start looking into it, but again can't tell when that will happen yet.

ARandomKrilios commented 3 months ago

Oh wow, thank you for responding so quickly lol I already assumed that there was some investigation being done on "AI Navigation on Voxels" in general (And to be fair, AI Navigation has always been a complicated part of games), but I found it weird how there was no 'Issue' about it, hence why I threw my 2 cents per se.

I admit that I've put a lot of weight on Godot's Navigation system, as if it were a perfect system (it is not). However having a system that outputs a NavigationMesh of some sort would still be ideal (Maybe just not one explicitly made for Godot's Navigation system). And to give a bit more detail from my own research on how Godot's NavigationAgent attempts to pathfind when Navmeshes overlap:

The point about the "LOD" you brought up is also very important, however, an AI in Godot will just simply travel to the closest point to its objective within the navigation mesh (So it won't stay still even if the target is very far away from the navigation mesh, unlike Unity), a very "low detail" version for a far away navmesh could still be useful for the AI to evade "A traps" (As the AI's path also updates slightly with every point it crosses). It also probably would be best for the user to use a different pathfinding algorithm, which Godot seemingly supports (There's an option to change it per NavigationAgent, though without any plugin it will only give you the option for A, a HPA* plugin would be ideal for this, but that's another topic).

One of the things you mentioned in the second to last point, having a Signal (Especially on VoxelLodTerrain as it doesn't have any), or anything of the sort, could be really useful if niche, cause for one it would allow to make a crude Navmesh using GDScript lol, but it could also maybe have some other use cases that are escaping my mind right now.

To answer one of the final questions, a NavigationMesh is just that, a mesh (Vector3 Vertices and Polygons that reference those vertices, you also have the function create_from_mesh), so it could be modified like any other mesh and thus it is possible to merge 2 meshes into a single mesh (Already did a small prototype of just this in GDScript, though obviously a performant way of doing this is the critical thing here). The original idea i had was to bake the mesh per block, pick the vertices at the border and extend them to the block's border, then stitching everything up. In an ideal world, having a custom-made Navmesher that doesn't create the small "margin" to the borders of the block so we don't need to innecessarily extend it back is what we would want. But that would just be its own can of worms.

I'll probably just end up taking a look at how Godot actually bakes a NavigationMesh, and maybe see what parts of it could be useful for Voxel navmeshes. Again, I'm still currently getting comfortable with C++, so excuse me for having a "naive" view of things lol.

Zylann commented 3 months ago

I also just thought about yet another annoyance with chunked navmesh:

Let's say you just use individual chunk meshes to make a navmesh. That will work if you consider just one mesh, but then what if that chunk represents only the floor of a very narrow cave? Because it's voxel terrain, it could mean the chunk above it (which might not even be ready yet!) contains a ceiling that's too close for the agent, meaning the navmesh will be incorrect. Same problem with walls on side and below chunks, and same problem if neighbor chunks contain some obstacle spawning on top. Which brings to an eternal struggle with chunks: you can't bake a chunk's navmesh if you don't also have neighbor chunks AND obstacles on them. Because of this alone, rebaking a single navmesh for loaded chunks even if only a small part changes sounds easier than chunking (but obviously sounds slower). Surely with infinite time a navmesh system adapted to all these constraints is certainly doable, but we have to work with what Godot provides unfortunately^^"

And then if you add neighbor chunks into a bake, it must still only produce a navmesh within that chunk, and not extend further just because the neighbors are provided to the baker. Not sure if there are APIs to limit this.

And this is even more problematic when we consider that meshes themselves need neighbor voxel chunks to even be produced. The end result being, That meshes currently appear only inside an inset-by-1-chunk region of voxels, and then navmesh chunks would end up themselves as an inset-by-1-chunk region of meshes... (on top of that, voxel chunks can have a different size than meshes!).

Checking 26 neighbors volumetrically in all space for various things is one area of the engine that can can be relatively expensive. It's happening so often with voxel chunks that I'm considering to alter the data structure to trade memory usage for speed, but still not the panacea. There are less mesh chunks than voxel chunks tho (if we ignore empty ones).

People often underestimate the sheer amount of shortcuts that just don't work with streaming voxel terrain^^" Had a peek at another voxel plugin in another engine and the struggle is real there too; though people sometimes find solutions that don't involve a navmesh, like sprinkling a network of waypoints around progressively or using dynamic avoidance, which is probably less precise but sometimes work well enough, especially in very dynamic environments. Because the end goal isn't so much to have a navmesh, but have a way to compute paths for AIs to follow (even though a navmesh allows to use Godot's navigation nodes, which are general-purpose, but again not the one-and-only solution to everything, even if that means having to do more work).


Also I guess this is one more place to reference this proposal: https://github.com/godotengine/godot-proposals/issues/5138 There was a pending PR addressing it for a while, but after several re-tries it ended up dropping the 3D part and now it seems nobody is working on that.

Zylann commented 3 months ago

I spent some time making a prototype of automatic navmesh baker for VoxelTerrain: image

This is a new node which for now I called VoxelTerrainNavigation, that must be child of VoxelTerrain. For now this is pretty much gathering the geometry of all chunks, combining them into a single mesh without extra processing (I dont spend time sharing vertices between chunks for example, this should really not be necessary), and passing that to NavigationServer3D::bake_from_source_geometry_data to make a single navmesh.

I intentionally chose 3D noise here because it creates overhangs and kinda stress-tests the navigation system, which we really need to be robust enough to handle all the crazyness that can exist on such procedural/player-editable terrains, without crashing or breaking somehow.

I havent tried using an agent to pathfind on the resulting navmesh for now.

"Sync" problem

Unfortunately, things are in a rough state, mainly because of this incredibly annoying error that keeps getting thrown by Godot:

ERROR: Navigation map synchronization error. Attempted to merge a navigation mesh polygon edge with another already-merged edge. This is usually caused by crossing edges, overlapping polygons, or a mismatch of the NavigationMesh / NavigationPolygon baked 'cell_size' and navigation map 'cell_size'.
   at: NavMap::sync (modules\navigation\nav_map.cpp:862)

It doesn't always occur, but as terrain generates, it happens really quickly.

I'm already pretty sure meshes are reasonably manifold ("reasonably" because I didnt run a definite test on it, but from having looked at Transvoxel meshes for a while, I can say they are devoid of self-intersections).

See https://github.com/godotengine/godot/issues/85548 for more details.

I can't really be sure if all points mentionned to use merge_rasterizer_cell_scale are satisfied, as there is no tool to verify it AFAIK. The error doesn't come with a way to find where exactly the problem is so I don't know what kind of particular case the terrain is producing that causes the problem. It is also logged with ERR_PRINT_ONCE so once it happened you have to constantly restart Godot to test again.

I even wonder if the error is actually a big problem or something that could be tolerated to some degree. Either way, even if it were to happen due to terrain being too harsh in game, it should not be something we have to constrain manually up-front somehow (because the world is not pre-authored!) and then crossing fingers hoping it doesnt happen when players generate and edit new terrain... there has to be another way to deal with it at runtime with minimal cost, but for now I don't what.

Transvoxel derives from marching cubes, which naturally produce triangles that can be very thin. I have no idea whether that has an impact or not here. That's why I though of trying meshoptimizer to "clean" it up, but that didn't help. Besides, simplifying the mesh has some downsides (lower visual quality, much longer meshing time, and trouble getting perfectly-matching cell triangles when using detail rendering, requiring to keep more memory allocated with the non-simplified meshes).

Regarding the editor

while the node also bakes inside the editor and uses a NavigationRegion3D internally, Godot doesn't render any debug preview of the navmesh. I don't know why. The code to render it is also not exposed. So right now I have no idea how to show that, apart from recoding it from scratch. The only way to preview it is launching the game with debug navigation. One option of course would be to inherit from NavigationRegion3D or make it separate, however this is not practical for the following reasons:

Branch

I pushed my WIP to the navigation branch if you want to look at the code or test it. It's not final, not optimized, and not threaded, so expect heavy stalls.

ARandomKrilios commented 3 months ago

Wow you work very fast, I was still doing some research on pathfinding and navmeshing in general and you already pushed a pretty decent rudimentary prototype, I will for sure take a look! I'll probably make some tests with an agent just to see if it's able to path properly, though, sounds like baking the entire terrain into a navmesh, instead of only updating the chunks that are different, is going to be a sizeable bottleneck in terms of performance.

Already doing some basic tests, the remeshing of the navmesh causes AI paths to be completely recalculated, because even the part of the navmesh the AI was standing on gets re-updated, having a big tendency to get stuck. I'm using an agent that doesn't use gravity nor inertia on purpose, as that's probably the most sensitive agent to changes possible, though even when re-enabling the gravity, it still gets stuck on wilder terrain (Using Noise Terrain).

Particularly with paths such as this: image (This is mostly a Godot issue more than anything though, and again, if the Agent had some sort of inertia it would probably fix itself)


I was thinking, now that we got this very rudimentary base, if it would still be possible to make the baking be "Chunked", however with some changes to the original implementation I suggested:

  1. We get the cube we want to work with as an AABB variable and also get either all of the voxel terrain or the affected block and neighboring blocks.
  2. We clear out all the vertices and polygons in the cube (and probably re-index the polygons that are outside the affected cube)
  3. Then we bake (using a different NavigationMesh as we don't want to overwrite our old one), this bake will have its filter_baking_aabb to be the AABB we have.
  4. Once the bake is done, we "stitch" this new Navmesh with the old one. How we do it is different to how I originally suggested it: instead of "extending" the vertices so they are at the edges and somehow merge them, we can do something easier, make new polygons that use the vertices on both sides, likely to be much easier to do and performant too. (Problem here would be identifying what vertices we want to use in a very efficient way)

I will start looking around and messing with things, but I'm quite unskilled with C++ so I will be slow, will try to make my code easy to read though, just so it's easier to optimize lol

Zylann commented 3 months ago

sounds like baking the entire terrain into a navmesh, instead of only updating the chunks that are different, is going to be a sizeable bottleneck in terms of performance.

It currently is because I havent optimized anything. The main parts that take time are:

Also, I chose a single-navmesh approach for now because it is A LOT easier to do. Chunking would be ideal in theory, but from what I've seen so far in my experiment, it brings up tons of problems and even makes certain things worse, so I haven't figured it out.

the remeshing of the navmesh causes AI paths to be completely recalculated, because even the part of the navmesh the AI was standing on gets re-updated, having a big tendency to get stuck

This is unfortunate, and I'm afraid this will keep happening even if chunking is implemented. I think that's a Godot issue. Godot should keep the last path it was using and eventually re-path without interrupting agents, otherwise it defeats any attempts at dynamically updating the navmesh. With chunks you could do any edit in a chunk agents are going through and that would bother them as well, which is dumb. Personally I haven't often used agents. Back in Unity (which uses Recast too) they have a bunch of stuff built-in, but their movement model makes a lot of assumptions, is too restrictive and robotic, not viable for anything that isn't kinematic. Instead I only got the paths and managed the following myself using context steering and other custom logic, which also allows to control how the AI actually moves and plans.

I'm using an agent that doesn't use gravity nor inertia on purpose, as that's probably the most sensitive agent to changes possible, though even when re-enabling the gravity, it still gets stuck on wilder terrain (Using Noise Terrain).

Not much the voxel engine can do about this, you have to tweak navmesh settings and the way your agent follows its path maybe.

I tried some chunking today on a separate project without the voxel engine. I used a very simple heightmap terrain divided in chunks and one region per chunk. I guess I eventually got it to a working-ish state, but it was awful:

Sorry I got no screenshot of that project as I'm on another computer.

We get the cube we want to work with as an AABB variable and also get either all of the voxel terrain or the affected block and neighboring blocks.

As you might have figured out from my first experiment, that means instead of gathering everything once, gathering will have to run for every chunk AND its theoretical 26 neighbors, overall resulting in much more work done by the CPU, due to redundancy of data and lookups. Though it's of course true that incremental changes are then cheaper.

We clear out all the vertices and polygons in the cube (and probably re-index the polygons that are outside the affected cube)

As seen in my experiment, I'm not sure of the relevance of manually filtering triangles that are not within the baking AABB. The baker can already do it for us, the downside is there is a lot more data to fetch from the gathering phase because Godot wants a single big list of vertices and indices. So I don't know what will turn out to be faster. Also raw props (nodes in the tree) are not chunked, but they also have to be in that single list of vertices and indices. Either we'd have to iterate them all and keep only those close enough, or make them part of some data structure to quickly get which ones affect every chunk.

Once the bake is done, we "stitch" this new Navmesh with the old one

In my experiment I did this with multiple regions, so I had multiple navmeshes, I didnt try to make a single one. Though as mentionned, there are weird problems I dont know how to get rid of.

we can do something easier, make new polygons that use the vertices on both sides, likely to be much easier to do and performant too. (Problem here would be identifying what vertices we want to use in a very efficient way)

Not sure if that's a good idea given the API we have access to. Also I don't know if that's doable efficiently. Navmesh polygons in each chunk should line up and therefore be detected by the navigation map connection logic. That said, I don't know how fast that system is (one more thing the single-navmesh approach doesnt need hehe)

ARandomKrilios commented 3 months ago

I feel slow seeing the progress you're doing ^^'

the remeshing of the navmesh causes AI paths to be completely recalculated, because even the part of the navmesh the AI was standing on gets re-updated, having a big tendency to get stuck

Going back to this point, what happens is that the NavigationMesh visibly changes whenever it updates, so with a chunking method it wouldn't happen as often, or at least not cause it to get stuck.

Some polygons at chunk borders had a magenta thick line across them. I don't know what that means.

Thick magenta lines means that the two navmeshes are considered as "Connected" automatically by Godot, which is one benefit of having each "Chunk" have its own region, as basically Godot does the stitching for us, I do wonder if it would actually be better or worse, I imagine it would end up being more memory intensive than just having one Region.

Zylann commented 3 months ago

Thick magenta lines means that the two navmeshes are considered as "Connected" automatically by Godot, which is one benefit of having each "Chunk" have its own region, as basically Godot does the stitching for us, I do wonder if it would actually be better or worse, I imagine it would end up being more memory intensive than just having one Region.

If that's so, then it's spectacularly bad, as very few polygons were connected, despite almost all of them were visually touching each other... (excluding the few ones that unexpectedly deviated)

ARandomKrilios commented 3 months ago

Yes, it is bad, cause it can only connect if:

I actually considered suggesting the idea of having one Region per block/chunk, but then figured that might've been a "naive" solution, though at the same time, it would get rid of having to check what polygon should be merged or created.

Angular-Angel commented 3 months ago

I've been working on a navigation solution for my game using one region per chunk, and I think it's workable. There are some difficulties - in particular, you have to turn off 'Use Edge Connections', which means that it can only connect an edge is both vertices that define the edge are in the same spots, and it's still a little slower than I'd like. I added a proposal for Godot to make adding new nav regions more efficient, and I think it'll get added eventually: https://github.com/godotengine/godot-proposals/issues/9381

You can see my current effort here, written entirely in GDScript. It does greedy meshing for the nav mesh atm, which doesn't actually work, but it at least demonstrates the performance is manageable: https://gitlab.com/AngularAngel/omnicraft-infinite-war/-/blob/master/Scenes/Game/NavigationGenerator.gd?ref_type=heads

Zylann commented 3 months ago

I've done some more investigation regarding the sync error: https://github.com/godotengine/godot/issues/85548#issuecomment-2026344751 I'm still out of ideas to avoid the issue. It's too unreliable, and the only solution actually silences the error rather than fixing the problem, as well as potentially introducing another problem with connecting regions.

Angular-Angel commented 3 months ago

I tried out Zylanns current experiment, and it works great! Other than the lag spikes on generating new chunks, anyway.

Oh, no sync errors for me, either. No idea what the deal with those is.

Though actually, now that I think of it, I occasionally got sync errors with my own solution? So, maybe that's just something that happens when working with nav meshes. :/

Zylann commented 3 months ago

Your terrain is blocky, so maybe that plays a role here. What I'm testing with is a relatively intense smooth terrain.

Zylann commented 3 months ago

I noticed that while I can thread most of the baking, Godot's NavigationServer still does something afterwards in NavMap::sync, which is very slow and of course runs on the main thread by default. I don't know what this sync is and why it's so slow, almost as much as the baking itself :( Sync what? I wonder if that can be configured to run on a different thread?

Sadly it seems there is nothing to run that on a thread. It runs directly in the middle of physics process, it's just going to stall all the time for every change we make to a navmesh (chunked or not): https://github.com/godotengine/godot/blob/29b3d9e9e538f0aa8effc8ad8bf19a2915292a89/main/main.cpp#L3980

(one extreme workaround that starts flying in my head is to implement a whole navigation query system entirely in the voxel module to be better tailored to streaming procedural open-worlds, and completely skip Godot's regions system... but that would be really, really sad)

Zylann commented 3 months ago

Another wonderful thing: While bake_from_source_geometry_data_async exists to bake on a different thread, it won't actually use threads in the editor. So it will stall constantly, and you'll have to turn off navigation baking while working on the terrain. For some reason, it has been hardcoded here: https://github.com/godotengine/godot/blob/29b3d9e9e538f0aa8effc8ad8bf19a2915292a89/modules/navigation/3d/nav_mesh_generator_3d.cpp#L88 The reasons invoked are vague and really sound wrong for desktop platforms where 99% of people develop games on.

Update: apparently it's possible to call the synchronous bake function from a custom thread. So we may have to do that if the editor case is too much of a problem.

Also, NavMap::sync is related to the region connection feature. It was designed for users having small maps, is very slow and not threaded for large terrains, but it can be turned off apparently. Maybe that also removes the need to comply with what the sync errors were raising as well? That's one point against region-chunking though. Edit: it does NOT get rid of sync errors, so the margin hack is still required to silence them... Also while it makes the NavMap::sync a lot faster, it remains really slow because NavMap still does the work of connecting polygons of the navmesh itself on the main thread, which doesn't actually happen during baking. Seems the state of NavigationServer in Godot is such that even if everything is threaded and made fast on the gather+bake side, there will always be CPU wasted by NavigationServer's sync system because it can't easily be threaded without significant refactorings. I found some optimizations to do in the area I was looking at, but I dont have expertise on the whole system so I can't contribute that kind of refactoring at the moment.

Update 2, regarding chunking: apparently using the border option is preferable instead of padding the AABB (see docs). Recast will make sure the edges match, without the need to use Godot's sync margins. Also, that option might also enable us to pass only the relevant chunk to baking, simplifying the gathering process. It could even allow us to bake from the meshing tasks themselves. That would be wonderful, EXCEPT... neighbor chunks are still required in case they contain a wall, ceiling or platform right next to the current chunk, which prevents that whole shortcut... also, props may still have to be figured out, because that involves the scene tree. This will need investigation.


Current state as of 2024/04/11: The navigation branch has something somewhat functional, it bakes a navmesh fully threaded, but performance is still bad due to Godot doing a bunch of work on the main thread (While baking can be threaded with our own tricks, NavigationServer is not multithreaded, and Godot doesn't seem well optimized to handle dynamic navmeshes on large environments). There is no easy workaround to this. As listed earlier an obvious step would be to find a way to chunk the navmesh, but that isn't so straightforward and Godot is still going to do a lot of work on the main thread just to connect polygons of the navmesh and connect navmeshes together. For now I left the branch as-is and working on other things.

Zylann commented 2 months ago

Quick update:

Godot just merged a PR that allows plugins to register custom navmesh geometry sources: https://github.com/godotengine/godot/pull/90876 This is good news, and will likely solve half of the reason I opened the proposal (my older heightmap plugin; the other half being Godot hardcoding its own modules into the navigation module).

However this is unfortunately not well-suited with the kind of terrain the voxel module generates, unless maybe you want to bake a fixed navmesh in the editor and never update it at runtime. So it's not really "missing" it.

I suppose it would work if used at runtime, however the general-purpose logic Godot does to gather geometry for the baker is IMO not scalable enough to be done frequently at runtime in a large 3D procedural streaming environment. What I've done already in the navigation branch does it more efficiently than what the exposed API allows, and it still suffers from performance issues (mostly coming from stuff after baking).

My thoughts about why the new Godot API is not suitable for runtime with this module:

Regarding the navigation branch, it is still in the same state as mentionned in https://github.com/Zylann/godot_voxel/issues/610#issuecomment-2027510950

I also added a doc page: https://voxel-tools.readthedocs.io/en/latest/navigation/