Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 2D navigation mesh baking #80796

Merged
merged 1 commit into from
Sep 26, 2023
Merged

Conversation

smix8
Copy link
Contributor

@smix8 smix8 commented Aug 19, 2023

Adds 2D navigation mesh baking for NavigationRegion2D and NavigationServer2D.

Slight changed behavior on the baking process of the NavigationPolygon, see info further below.

navmesh2d_baking

Supersedes #70724.

Fixed Issues

Related older Issues

Implemented proposals

New 2D navigation mesh baking

The navigation mesh baking for 2D is accessible through NavigationRegion2D.

The NavigationRegion2D has new buttons in the Editor for baking or clearing the current NavigationPolygon.
The NavigationRegion2D also has a new function bake_navigation_polygon() to bake the navigation polygon with scripts.

navigationregion2dbakemenu

The 2D navigation mesh baking workflow is in general kept very similar to 3D but there are few key differences.

2D requires at least 1 bounding outline that defines the outer limits of the parsed area and baked surface.
Everything inside those bounds is by default traversable surface and all parsed objects are considered obstructions.

navregion2d

Another, more advanced way to bake navigation meshes for 2D is available for scripts when using the NavigationServer2D singleton directly. The NavigationServer2D has new, dedicated 2D functions for parsing and baking 2D navigation meshes.

  • parse_source_geometry_data() can be used to only parse source geometry to a reuseable and serializable NavigationMeshSourceGeometryData2D resource without the baking step (see description below).
  • bake_from_source_geometry_data() can be used to bake a NavigationPolygon from already parsed data e.g. to avoid runtime performance issues with (redundant) parsing.
  • bake_from_source_geometry_data_async() is the same but bakes the NavigationPolygon deferred with threads, not blocking the main thread.

The source geometry parsing is implemented with dedicated parsers for each supported node type. Currently the following node parsers are added:

  • StaticBody2D and all CollisionShape2D
  • Polygon2D
  • TileMap (Layer0 only for now)
  • MeshInstance2D (2D meshes only)
  • MultiMesh2D (2D meshes only)

Note that only 2D meshes are parsed and all 3D meshes are ignored. Meshes are considered 2D when the mesh format ARRAY_FLAG_USE_2D_VERTICES flag is set.

Note that all rounded default shapes have been hardcoded to far low amount of edges compared to their visuals.

  • Circle shapes use 12 edges
  • Capsule shapes use 14 edges

Too many edges not only drag the bake performance down but also result in polygon "star" artifacts in the navmesh that cause pathfinding issues.

In general rounded or other overdetailed objects are not recommended to be used as source geometry for navigation mesh baking and should all be replaced by polygons with a more simplified shape. Also prefer physics collision shapes over visual polygons.

TileMap navigation mesh baking

The TileMap is a special case for 2D navigation mesh baking as it is a node that combines internally both traversable surfaces as well as obstructions. Baking a TileMap has certain limitations due to the current TileMap design.

  • Only the first TileMap Layer 0 is considered in the navigation mesh baking.
  • TileMap Layer 0 cells with a navigation polygon count as traversable.
  • TileMap Layer 0 cells with a collision polygon count as obstruction.

A NavigationRegion2D can bake a navigation mesh for the entire TileMap by merging the navigation polygons and collision polygons from all used TileMap cells on the first Tilemap layer.

217048075-9cd9c4c5-5dfa-4c76-bf86-ea64331caeca

This full TileMap cell merge is required as it is the only reliable way to get working navigation meshes that are fitted for an agent size. It yields the best possible result for both navigation performance and quality by removing all those little TileMap cell edge seams that are responsible for a lot of pathfinding and performance problems with the TileMap build-in navigation.

NavigationMeshSourceGeometryData2D

NavigationMeshSourceGeometryData is the resulting data of a parsing operation done with the NavigationServer used for navigation mesh baking.

The advantage of having this data available in a resources is that it can be stored and loaded from disk or resued. This helps to avoid runtime performance issues with parsing operations on larger scenes. NavigationMeshSourceGeometryData makes it possibility to split the source geometry parsing process from the navigation mesh baking process. It also makes it possible to reuse the same source data to bake multiple meshes with different parameters, e.g. use one source geometry to bake navigation meshes for multiple different agent sizes.

Compatibility

The NavigationPolygon has some changed behavior now.

  • The Editor drawn outlines now define the surfaces that should be baked to a 2D navigation mesh.
  • Both traversable outlines and obstruction outlines are merged before the baking step, e.g. they can now have cross edges without errors. This also means that outlines "nested" inside other outlines will no longer create holes by default as this was just a constant source of user bugs. If you want reliable holes add an obstruction.
  • Dragging outlines in the Editor will not auto-bake polygons all the time, hit the bake button when you are done editing. The old auto-bake was really laggy when editing more complex polygons.

The NavigationPolygons.make_polygons_from_outlines() function is deprecated. It still works like before with all known bugs and issues. If possible consider upgrading old projects to the new baking with the NavigationServer.

Existing NavigationPolygons that were already baked with make_polygons_from_outlines() are compatible.

Existing outlines are also compatible but if baked with the new bake the result might be interpreted differently when nested "hole" outlines were used. Consider using obstacle outlines to cut holes in the traversable polygons instead of nesting outlines into other outlines which has always a chance to flip the polygon in unintended ways.

Old NavigationPolygon that have at least one outline will bake just fine with the new bake but since the NavigationPolygon by default has an agent radius of 10 pixels this will shrink new polygons. If you don't want this set the agent radius to zero to create polygons similar to the old polygons.

Internals

  • Adds a NavigationServer2D dummy same as the NavigationServer3D dummy.
  • Makes the NavigationServer2D pure virtual with a GodotNavigationServer2D override.
  • NavigationServer sync() point to dispatch async baked navmeshes now happens each iteration just before the physics loop. The old had it as part of the physics process loop which made it unreliable and frustrating to use.

Thirdparty

This pr includes thirdparty Clipper2 library as a requirement for the polygon operations without touching any of the existing Clipper1 uses in the engine.

I couldn't get any good results with the old Clipper1 as it broke far to often with edge cases or very complex polygons and was in general also way too slow for runtime navmesh baking so upgrading was imo mandatory.

Clipper1 is currently used across the engine e.g. the Geometry2D class and some Sprite ops. I added Clipper2 to the core thirdparty so it is available for the rest of the engine so all those old parts can be upgraded over time. Since I never really use those 2D functions I don't want to touch them myself.

@smix8 smix8 requested review from a team as code owners August 19, 2023 21:06
@smix8 smix8 force-pushed the navgenerator_2d_4.x branch 2 times, most recently from 689a2f7 to 7b15806 Compare August 19, 2023 21:18
@smix8 smix8 added the bug label Aug 19, 2023
@Calinou Calinou added this to the 4.x milestone Aug 19, 2023
@vpellen
Copy link

vpellen commented Aug 20, 2023

Been waiting for this, glad you managed to pull past the various breaking changes!

Question: Does this do a bunch of polygon ops or does it do the same thing that the 3D version does and do a bunch of point casts? I'd assume it's the former based on circle edges and the fact that you're using a polygon clipping library, but if that's the case, the "cell size" parameter confuses me, as I can't imagine it'd be relevant.

Also, regarding circle edge density: Have you considered making it an adjustable factor of their radius?
Also also, is the agent radius just an inverse extrude on the final mesh? Would I be correct in assuming that was what was causing some of the miserable clipping issues that warranted an external lib upgrade?

@smix8
Copy link
Contributor Author

smix8 commented Aug 20, 2023

Question: Does this do a bunch of polygon ops ...

It uses Clipper2 polygon ops like Union and Difference under the hood.

... the "cell size" parameter confuses me, as I can't imagine it'd be relevant.

The NavigationPolygon.cell_size property is primarily for the navmesh merge on the navigation map on the NavigationServer. The navigation map also has a cell_size used to rasterize the navmesh edges by edgekey to merge them. If the NavigationPolygon cell_size and navigation map cell_size are mismatched merge errors can occur.

Previously the cell_size was also used to upscale the arrays as is required for the rasterization of the Clipper1 lib to avoid bugs but this slowed things down considerably. After switching to Clipper2 I found no reason to do this anymore, instead found one more reason why Clipper1 is so obsolete.

Also, regarding circle edge density: Have you considered making it an adjustable factor of their radius?

The physics shapes have no way for users to change their detail level which is by default to high for navmesh baking. If users really need more edge detail for navmesh baking they should add additional shapes. Complicating the interface for everyone with properties for a niche detail that can be easily solved by adding another shape is imo not a good idea.

... is the agent radius just an inverse extrude on the final mesh?

The agent_radius is the deflating amount that the final navmesh polygon is shrunk when baking it.

@KoB-Kirito
Copy link

KoB-Kirito commented Aug 23, 2023

Only the first TileMap Layer 0 is considered in the navigation mesh baking

Would it be very hard to consider all layers?
All layers can contain collision. You would have to create an extra layer and layout all collisions there again just to bake the map, which is immense work in some scenarios.

Edit: All layers with a specific z_index would make sense logically.

@smix8
Copy link
Contributor Author

smix8 commented Aug 23, 2023

Would it be very hard to consider all layers?

It is primarily a user interface issue as the parsing process does not care what it parses from the TileMap but it also can not make random assumptions how a TileMap user intends to use and mix its TileMap layers. Some users mix everything together while others use their TileMap layers very distinct and separated or with their own logic.

There is no one-size-fits-all how the TileMap Layers are used and this is the entire reason this TileMap navmesh baking limitation currently exists.

The issue for the TileMap was already discussed in dev chat a few times and we basically settled that the TileMap devs would take care of this problem in a later TileMap update after finishing their current reworks.

This limitation also only exists for the automated parsing as you can perfectly fine create and populate your own NavigationMeshSourceGeometryData2D with the data from your TileMap that you want to add and bake from that.

@smix8 smix8 force-pushed the navgenerator_2d_4.x branch from 5ae35b1 to 423a839 Compare August 23, 2023 06:37
@smix8 smix8 modified the milestones: 4.x, 4.2 Aug 24, 2023
@smix8 smix8 force-pushed the navgenerator_2d_4.x branch from 423a839 to 31914ae Compare August 26, 2023 02:17
@smix8 smix8 force-pushed the navgenerator_2d_4.x branch from 31914ae to c651984 Compare September 7, 2023 15:28
@ghmart
Copy link

ghmart commented Sep 12, 2023

I've set up some crazy experiment: using NavigationPolygon for GridMap. This is a project, that allows adding / destroying cells with navigation at runtime. I'm not using standard baking, rolled my own algorithm, that utilizes "make_polygons_from_outlines" at some stage. Sometimes it fails to detect holes / islands, say 10% of time.

I tried this project with your version (with some changes - multiply cells' coordinates by 100, for example, because it can't create navigation mesh with floating-point coordinates, maybe because this version uses integer Point64 in Clipper2). Still it fails more often than with "make_polygons_from_outlines", maybe around 30% of time or even more. Sometimes it even can't handle similar cases.

navigation

So now, I gave up to find the cause of error. Maybe it's in Clipper2 library, or in my algorithm (maybe need to use only positive coordinates), or in new version of navigation mesh creation.

Anyway, would be great to have new mesh baking version, alongside Clipper2 in Godot!

EDIT: One idea comes to mind: error is in how I detecting holes / islands: check if first point of each other outlines is in most outer polygon. It should check instead if this point is in any of each convex polygons of this outer polygon after decomposing it. Wonder if this would be an overkill to performance. And why it finds right answer in many cases then...

EDIT2: Problem solved. Just have to make outlines cw / ccw according to their type (island / hole), then use add_outline function of NavigationPolygon. With your version all works 100% perfectly!

@ghmart
Copy link

ghmart commented Sep 15, 2023

Holes didn't recognized when manually adding points not in opposite order.

manual

Outer outline is clockwise here.

@smix8 smix8 force-pushed the navgenerator_2d_4.x branch from 6cbd56e to cf90759 Compare September 16, 2023 15:42
@smix8
Copy link
Contributor Author

smix8 commented Sep 16, 2023

Changes:

  • Fixed various transform related bugs for some parsed node types.
  • Limited the NavigationPolygon cell_size Editor slider range to full pixels.
    It is still a float for technical reasons but the entire baking process happens on an integer grid so all decimal fractional parts are ignored. Floats can still be set with a script in case a custom navigation map cell_size that uses small float values is used but the baking will not support such small scaling and if ignored this might run into precision and navigation map sync errors.
  • Reduced the amount of code indentations in the parse functions to make them more readable on small screens.

@JustMog

root node children doesn't seem to work for me

Since you added the "nav" group to both the NavigationPolygon and your main root Node2D in the scene I think what you wanted to use instead was the SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN "Group with Children" mode. This mode parses all nodes with this group_name that are inside the SceneTree and also parses recursive all the children of those nodes.

SOURCE_GEOMETRY_ROOT_NODE_CHILDREN "Root Node Children" does recursive parse the child nodes of the set root_node. This is NOT the root node of the SceneTree, it is whatever root_node is set with the parsing function and in case of baking with a NavigationRegion2D.bake_navigation_polygon() or baking with the Editor Button this node automatically sets itself as the root_node. In the repro project the NavigationRegion2D has no children in that scene so it only bakes the surface of your defined outline in that mode.

it tripped me up that the transform of the navregion isn't taken into account

Some node types had wrong xform calculations but should be fixed with the last update, please retry.

@JustMog
Copy link

JustMog commented Sep 16, 2023

This is NOT the root node of the SceneTree

ah, my mistake.

Some node types had wrong xform calculations but should be fixed with the last update, please retry.

works now! thanks!

@LakshayaG73
Copy link

hi. will the scenario below for tilemaps still function correctly?

image

@smix8
Copy link
Contributor Author

smix8 commented Sep 19, 2023

hi. will the scenario below for tilemaps still function correctly?

As mentioned in chat as long as a polygon has a surface it should work. Be aware that precision is a thing that might go wrong sometimes so better use 2-4 pixels to have some error margin when you use very thin polygons.

By default the 2D navigation mesh baking is more focused on 2D "top-down" because that is how the agent radius offset is done. It also works for 2D sideview platformers with some prep work.

For 2D sideview platforms a TileMap like this can also work as long as everything is on TileMap Layer0. Just you do not draw an outline with the NavigationRegion2D. Without any outline the navigation mesh baking will parse and merge all the cell navigation polygons from the TileMap, then parses and merge all the collision polygons, then diff them so you still should have all that "empty space" for your sideview platformer while getting the agent radius offset from the obstruction so your agents dont get stuck on collision all the time.

@smix8
Copy link
Contributor Author

smix8 commented Sep 21, 2023

Added back everyone's favorite semi-broken function NavigationPolygon.make_polygons_from_outlines().

The function is still deprecated and as broken as it was. It is only added back for compatibility for those projects that can not do without the old bake behavior for some reason. Everyone else enjoy the more robust NavigationServer baking.

@smix8 smix8 force-pushed the navgenerator_2d_4.x branch from 23bee5b to 6cd704c Compare September 21, 2023 18:49
@ghmart
Copy link

ghmart commented Sep 22, 2023

Added back everyone's favorite semi-broken function NavigationPolygon.make_polygons_from_outlines().

The function is still deprecated and as broken as it was. It is only added back for compatibility for those projects that can not do without the old bake behavior for some reason. Everyone else enjoy the more robust NavigationServer baking.

Not sure how broken was "make_polygons_from_outlines", but in my case it detects holes / islands automatically. I'd prefer to use new approach, but then I need to detect type of outlines myself like this:

var inside: int
for i in range(0, outlines.size()):
	inside = 0
	for j in range(0, outlines.size() - 1):
		if i == j:
			continue
		if Geometry2D.is_point_in_polygon(outlines[i][0], outlines[j]):
			inside += 1
	if inside % 2 == 0:
		if not Geometry2D.is_polygon_clockwise(outlines[i]):
			outlines[i].reverse()
	else:
		if Geometry2D.is_polygon_clockwise(outlines[i]):
			outlines[i].reverse()

@smix8
Copy link
Contributor Author

smix8 commented Sep 22, 2023

Not sure how broken was "make_polygons_from_outlines", but in my case it detects holes / islands automatically.

For starters as soon as you created overlap everything would break with the old bake. Also had many edge cases where even if the user placed everything correct it would still break. As you say it detects it only in many cases, this just not enough, you can't do a project with e.g. user placed objects and runtime rebake when there is a high chance that suddenly the entire navigation mesh is gone.

This is also why the new bake now merges everything first. This allows users to partially overlap their polygons without issues where the old bake would already break. Just do not fully place outlines inside each other which would again trigger the hole calculation.

That the holes sometimes work even with the new bake is more an afterthought because the underlying Clipper2 tries to figure out wtf the user tried to do here with so many outlines stacked inside each other. The result is still based on luck, because a single wrong placement is enough and the entire navigation mesh flips to the wrong side and interprets a large junk of the traversable polygon surface as a hole. Clipper2 has fillrules which would allow to select different ways how it interprets the outlines but this is a really difficult concept to grasp for most users and would make the baking process not accessible so that property is hidden and it runs with NONZERO fillrule by default.

Holes should preferably be added now by placing obstacle polygons, not by drawing inner outlines into other outlines for the traversable polygons. Because we merge the traversable polygons first, then merge the obstacles second, then get the diff between those two it is the far safer way to bake without stuff easily exploding in your face because a single vertex was placed on the edge of another polygon.

@ghmart
Copy link

ghmart commented Sep 22, 2023

Thank you for such detailed response. I agree with everything you said and will try using obstacles.

Main goal in my case was to speed up navigation mesh baking for GridMap. Most performance-critical part is in building outlines. Example in documentation ("Navmesh for 3D GridMaps") is unsuitable for real-time creation / destroying of cells with navigation.
Maybe new baking system and new ability to use threads will change things.

In action (recorded at 30 fps):

godot_nav_test.mp4

WIP. Still far from what I'm trying to achieve, chunking system and initial navmesh generation using compute shader not implemented yet.

@smix8 smix8 force-pushed the navgenerator_2d_4.x branch 2 times, most recently from 32f2f71 to 2c71939 Compare September 25, 2023 16:14
Adds 2D navigation mesh baking.
@smix8 smix8 force-pushed the navgenerator_2d_4.x branch from 2c71939 to 0ee7e31 Compare September 25, 2023 17:48
@akien-mga akien-mga merged commit 7dccb9e into godotengine:master Sep 26, 2023
@akien-mga
Copy link
Member

Thanks!

@xaqbr
Copy link

xaqbr commented Oct 17, 2023

Doesn't seem like enabling/disabling NavigationRegion2Ds at runtime works anymore after this.

@YuriSizov
Copy link
Contributor

@wayjack Please open an issue report with a reproduction project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment