Skip to content

Commit

Permalink
Merge pull request #89034 from smix8/navobstacle_bake_n_carve
Browse files Browse the repository at this point in the history
Add NavigationObstacle options to affect navigation mesh baking
  • Loading branch information
akien-mga committed Mar 15, 2024
2 parents 0175be8 + 5d5e85f commit f2fd015
Show file tree
Hide file tree
Showing 16 changed files with 664 additions and 15 deletions.
35 changes: 35 additions & 0 deletions doc/classes/NavigationMeshSourceGeometryData2D.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
Adds the outline points of a shape as obstructed area.
</description>
</method>
<method name="add_projected_obstruction">
<return type="void" />
<param index="0" name="vertices" type="PackedVector2Array" />
<param index="1" name="carve" type="bool" />
<description>
Adds a projected obstruction shape to the source geometry. If [param carve] is [code]true[/code] the carved shape will not be affected by additional offsets (e.g. agent radius) of the navigation mesh baking process.
</description>
</method>
<method name="add_traversable_outline">
<return type="void" />
<param index="0" name="shape_outline" type="PackedVector2Array" />
Expand All @@ -29,12 +37,26 @@
Clears the internal data.
</description>
</method>
<method name="clear_projected_obstructions">
<return type="void" />
<description>
Clears all projected obstructions.
</description>
</method>
<method name="get_obstruction_outlines" qualifiers="const">
<return type="PackedVector2Array[]" />
<description>
Returns all the obstructed area outlines arrays.
</description>
</method>
<method name="get_projected_obstructions" qualifiers="const">
<return type="Array" />
<description>
Returns the projected obstructions as an [Array] of dictionaries. Each [Dictionary] contains the following entries:
- [code]vertices[/code] - A [PackedFloat32Array] that defines the outline points of the projected shape.
- [code]carve[/code] - A [bool] that defines how the projected shape affects the navigation mesh baking. If [code]true[/code] the projected shape will not be affected by addition offsets, e.g. agent radius.
</description>
</method>
<method name="get_traversable_outlines" qualifiers="const">
<return type="PackedVector2Array[]" />
<description>
Expand All @@ -61,6 +83,19 @@
Sets all the obstructed area outlines arrays.
</description>
</method>
<method name="set_projected_obstructions">
<return type="void" />
<param index="0" name="projected_obstructions" type="Array" />
<description>
Sets the projected obstructions with an Array of Dictionaries with the following key value pairs:
[codeblocks]
[gdscript]
"vertices" : PackedFloat32Array
"carve" : bool
[/gdscript]
[/codeblocks]
</description>
</method>
<method name="set_traversable_outlines">
<return type="void" />
<param index="0" name="traversable_outlines" type="PackedVector2Array[]" />
Expand Down
41 changes: 41 additions & 0 deletions doc/classes/NavigationMeshSourceGeometryData3D.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,44 @@
Adds an [Array] the size of [constant Mesh.ARRAY_MAX] and with vertices at index [constant Mesh.ARRAY_VERTEX] and indices at index [constant Mesh.ARRAY_INDEX] to the navigation mesh baking data. The array must have valid triangulated mesh data to be considered. Since [NavigationMesh] resources have no transform, all vertex positions need to be offset by the node's transform using [param xform].
</description>
</method>
<method name="add_projected_obstruction">
<return type="void" />
<param index="0" name="vertices" type="PackedVector3Array" />
<param index="1" name="elevation" type="float" />
<param index="2" name="height" type="float" />
<param index="3" name="carve" type="bool" />
<description>
Adds a projected obstruction shape to the source geometry. The [param vertices] are considered projected on a xz-axes plane, placed at the global y-axis [param elevation] and extruded by [param height]. If [param carve] is [code]true[/code] the carved shape will not be affected by additional offsets (e.g. agent radius) of the navigation mesh baking process.
</description>
</method>
<method name="clear">
<return type="void" />
<description>
Clears the internal data.
</description>
</method>
<method name="clear_projected_obstructions">
<return type="void" />
<description>
Clears all projected obstructions.
</description>
</method>
<method name="get_indices" qualifiers="const">
<return type="PackedInt32Array" />
<description>
Returns the parsed source geometry data indices array.
</description>
</method>
<method name="get_projected_obstructions" qualifiers="const">
<return type="Array" />
<description>
Returns the projected obstructions as an [Array] of dictionaries. Each [Dictionary] contains the following entries:
- [code]vertices[/code] - A [PackedFloat32Array] that defines the outline points of the projected shape.
- [code]elevation[/code] - A [float] that defines the projected shape placement on the y-axis.
- [code]height[/code] - A [float] that defines how much the projected shape is extruded along the y-axis.
- [code]carve[/code] - A [bool] that defines how the obstacle affects the navigation mesh baking. If [code]true[/code] the projected shape will not be affected by addition offsets, e.g. agent radius.
</description>
</method>
<method name="get_vertices" qualifiers="const">
<return type="PackedFloat32Array" />
<description>
Expand Down Expand Up @@ -72,6 +98,21 @@
[b]Warning:[/b] Inappropriate data can crash the baking process of the involved third-party libraries.
</description>
</method>
<method name="set_projected_obstructions">
<return type="void" />
<param index="0" name="projected_obstructions" type="Array" />
<description>
Sets the projected obstructions with an Array of Dictionaries with the following key value pairs:
[codeblocks]
[gdscript]
"vertices" : PackedFloat32Array
"elevation" : float
"height" : float
"carve" : bool
[/gdscript]
[/codeblocks]
</description>
</method>
<method name="set_vertices">
<return type="void" />
<param index="0" name="vertices" type="PackedFloat32Array" />
Expand Down
17 changes: 12 additions & 5 deletions doc/classes/NavigationObstacle2D.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="NavigationObstacle2D" inherits="Node2D" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
2D Obstacle used in navigation to constrain avoidance controlled agents outside or inside an area.
2D obstacle used to affect navigation mesh baking or constrain velocities of avoidance controlled agents.
</brief_description>
<description>
2D Obstacle used in navigation to constrain avoidance controlled agents outside or inside an area. The obstacle needs a navigation map and outline vertices defined to work correctly.
If the obstacle's vertices are winded in clockwise order, avoidance agents will be pushed in by the obstacle, otherwise, avoidance agents will be pushed out. Outlines must not cross or overlap.
Obstacles are [b]not[/b] a replacement for a (re)baked navigation mesh. Obstacles [b]don't[/b] change the resulting path from the pathfinding, obstacles only affect the navigation avoidance agent movement by altering the suggested velocity of the avoidance agent.
Obstacles using vertices can warp to a new position but should not moved every frame as each move requires a rebuild of the avoidance map.
An obstacle needs a navigation map and outline [member vertices] defined to work correctly. The outlines can not cross or overlap.
Obstacles can be included in the navigation mesh baking process when [member affect_navigation_mesh] is enabled. They do not add walkable geometry, instead their role is to discard other source geometry inside the shape. This can be used to prevent navigation mesh from appearing in unwanted places. If [member carve_navigation_mesh] is enabled the baked shape will not be affected by offsets of the navigation mesh baking, e.g. the agent radius.
With [member avoidance_enabled] the obstacle can constrain the avoidance velocities of avoidance using agents. If the obstacle's vertices are wound in clockwise order, avoidance agents will be pushed in by the obstacle, otherwise, avoidance agents will be pushed out. Obstacles using vertices and avoidance can warp to a new position but should not be moved every single frame as each change requires a rebuild of the avoidance map.
</description>
<tutorials>
<link title="Using NavigationObstacles">$DOCS_URL/tutorials/navigation/navigation_using_navigationobstacles.html</link>
Expand Down Expand Up @@ -49,12 +48,20 @@
</method>
</methods>
<members>
<member name="affect_navigation_mesh" type="bool" setter="set_affect_navigation_mesh" getter="get_affect_navigation_mesh" default="false">
If enabled and parsed in a navigation mesh baking process the obstacle will discard source geometry inside its [member vertices] defined shape.
</member>
<member name="avoidance_enabled" type="bool" setter="set_avoidance_enabled" getter="get_avoidance_enabled" default="true">
If [code]true[/code] the obstacle affects avoidance using agents.
</member>
<member name="avoidance_layers" type="int" setter="set_avoidance_layers" getter="get_avoidance_layers" default="1">
A bitfield determining the avoidance layers for this obstacle. Agents with a matching bit on the their avoidance mask will avoid this obstacle.
</member>
<member name="carve_navigation_mesh" type="bool" setter="set_carve_navigation_mesh" getter="get_carve_navigation_mesh" default="false">
If enabled the obstacle vertices will carve into the baked navigation mesh with the shape unaffected by additional offsets (e.g. agent radius).
It will still be affected by further postprocessing of the baking process, like edge and polygon simplification.
Requires [member affect_navigation_mesh] to be enabled.
</member>
<member name="radius" type="float" setter="set_radius" getter="get_radius" default="0.0">
Sets the avoidance radius for the obstacle.
</member>
Expand Down
17 changes: 12 additions & 5 deletions doc/classes/NavigationObstacle3D.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="NavigationObstacle3D" inherits="Node3D" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
3D Obstacle used in navigation to constrain avoidance controlled agents outside or inside an area.
3D obstacle used to affect navigation mesh baking or constrain velocities of avoidance controlled agents.
</brief_description>
<description>
3D Obstacle used in navigation to constrain avoidance controlled agents outside or inside an area. The obstacle needs a navigation map and outline vertices defined to work correctly.
If the obstacle's vertices are winded in clockwise order, avoidance agents will be pushed in by the obstacle, otherwise, avoidance agents will be pushed out. Outlines must not cross or overlap.
Obstacles are [b]not[/b] a replacement for a (re)baked navigation mesh. Obstacles [b]don't[/b] change the resulting path from the pathfinding, obstacles only affect the navigation avoidance agent movement by altering the suggested velocity of the avoidance agent.
Obstacles using vertices can warp to a new position but should not moved every frame as each move requires a rebuild of the avoidance map.
An obstacle needs a navigation map and outline [member vertices] defined to work correctly. The outlines can not cross or overlap and are restricted to a plane projection. This means the y-axis of the vertices is ignored, instead the obstacle's global y-axis position is used for placement. The projected shape is extruded by the obstacles height along the y-axis.
Obstacles can be included in the navigation mesh baking process when [member affect_navigation_mesh] is enabled. They do not add walkable geometry, instead their role is to discard other source geometry inside the shape. This can be used to prevent navigation mesh from appearing in unwanted places, e.g. inside "solid" geometry or on top of it. If [member carve_navigation_mesh] is enabled the baked shape will not be affected by offsets of the navigation mesh baking, e.g. the agent radius.
With [member avoidance_enabled] the obstacle can constrain the avoidance velocities of avoidance using agents. If the obstacle's vertices are wound in clockwise order, avoidance agents will be pushed in by the obstacle, otherwise, avoidance agents will be pushed out. Obstacles using vertices and avoidance can warp to a new position but should not be moved every single frame as each change requires a rebuild of the avoidance map.
</description>
<tutorials>
<link title="Using NavigationObstacles">$DOCS_URL/tutorials/navigation/navigation_using_navigationobstacles.html</link>
Expand Down Expand Up @@ -49,12 +48,20 @@
</method>
</methods>
<members>
<member name="affect_navigation_mesh" type="bool" setter="set_affect_navigation_mesh" getter="get_affect_navigation_mesh" default="false">
If enabled and parsed in a navigation mesh baking process the obstacle will discard source geometry inside its [member vertices] and [member height] defined shape.
</member>
<member name="avoidance_enabled" type="bool" setter="set_avoidance_enabled" getter="get_avoidance_enabled" default="true">
If [code]true[/code] the obstacle affects avoidance using agents.
</member>
<member name="avoidance_layers" type="int" setter="set_avoidance_layers" getter="get_avoidance_layers" default="1">
A bitfield determining the avoidance layers for this obstacle. Agents with a matching bit on the their avoidance mask will avoid this obstacle.
</member>
<member name="carve_navigation_mesh" type="bool" setter="set_carve_navigation_mesh" getter="get_carve_navigation_mesh" default="false">
If enabled the obstacle vertices will carve into the baked navigation mesh with the shape unaffected by additional offsets (e.g. agent radius).
It will still be affected by further postprocessing of the baking process, like edge and polygon simplification.
Requires [member affect_navigation_mesh] to be enabled.
</member>
<member name="height" type="float" setter="set_height" getter="get_height" default="1.0">
Sets the obstacle height used in 2D avoidance. 2D avoidance using agent's ignore obstacles that are below or above them.
</member>
Expand Down
105 changes: 105 additions & 0 deletions modules/navigation/2d/nav_mesh_generator_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "core/config/project_settings.h"
#include "scene/2d/mesh_instance_2d.h"
#include "scene/2d/multimesh_instance_2d.h"
#include "scene/2d/navigation_obstacle_2d.h"
#include "scene/2d/physics/static_body_2d.h"
#include "scene/2d/polygon_2d.h"
#include "scene/2d/tile_map.h"
Expand Down Expand Up @@ -233,6 +234,7 @@ void NavMeshGenerator2D::generator_parse_geometry_node(Ref<NavigationPolygon> p_
generator_parse_polygon2d_node(p_navigation_mesh, p_source_geometry_data, p_node);
generator_parse_staticbody2d_node(p_navigation_mesh, p_source_geometry_data, p_node);
generator_parse_tilemap_node(p_navigation_mesh, p_source_geometry_data, p_node);
generator_parse_navigationobstacle_node(p_navigation_mesh, p_source_geometry_data, p_node);

if (p_recurse_children) {
for (int i = 0; i < p_node->get_child_count(); i++) {
Expand Down Expand Up @@ -660,6 +662,58 @@ void NavMeshGenerator2D::generator_parse_tilemap_node(const Ref<NavigationPolygo
}
}

void NavMeshGenerator2D::generator_parse_navigationobstacle_node(const Ref<NavigationPolygon> &p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data, Node *p_node) {
NavigationObstacle2D *obstacle = Object::cast_to<NavigationObstacle2D>(p_node);
if (obstacle == nullptr) {
return;
}

if (!obstacle->get_affect_navigation_mesh()) {
return;
}

const Transform2D node_xform = p_source_geometry_data->root_node_transform * Transform2D(0.0, obstacle->get_global_position());

const float obstacle_radius = obstacle->get_radius();

if (obstacle_radius > 0.0) {
Vector<Vector2> obstruction_circle_vertices;

// The point of this is that the moving obstacle can make a simple hole in the navigation mesh and affect the pathfinding.
// Without, navigation paths can go directly through the middle of the obstacle and conflict with the avoidance to get agents stuck.
// No place for excessive "round" detail here. Every additional edge adds a high cost for something that needs to be quick, not pretty.
static const int circle_points = 12;

obstruction_circle_vertices.resize(circle_points);
Vector2 *circle_vertices_ptrw = obstruction_circle_vertices.ptrw();
const real_t circle_point_step = Math_TAU / circle_points;

for (int i = 0; i < circle_points; i++) {
const float angle = i * circle_point_step;
circle_vertices_ptrw[i] = node_xform.xform(Vector2(Math::cos(angle) * obstacle_radius, Math::sin(angle) * obstacle_radius));
}

p_source_geometry_data->add_projected_obstruction(obstruction_circle_vertices, obstacle->get_carve_navigation_mesh());
}

const Vector<Vector2> &obstacle_vertices = obstacle->get_vertices();

if (obstacle_vertices.is_empty()) {
return;
}

Vector<Vector2> obstruction_shape_vertices;
obstruction_shape_vertices.resize(obstacle_vertices.size());

const Vector2 *obstacle_vertices_ptr = obstacle_vertices.ptr();
Vector2 *obstruction_shape_vertices_ptrw = obstruction_shape_vertices.ptrw();

for (int i = 0; i < obstacle_vertices.size(); i++) {
obstruction_shape_vertices_ptrw[i] = node_xform.xform(obstacle_vertices_ptr[i]);
}
p_source_geometry_data->add_projected_obstruction(obstruction_shape_vertices, obstacle->get_carve_navigation_mesh());
}

void NavMeshGenerator2D::generator_parse_source_geometry_data(Ref<NavigationPolygon> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data, Node *p_root_node) {
List<Node *> parse_nodes;

Expand Down Expand Up @@ -779,6 +833,30 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation
obstruction_polygon_paths.push_back(clip_path);
}

const Vector<NavigationMeshSourceGeometryData2D::ProjectedObstruction> &projected_obstructions = p_source_geometry_data->_get_projected_obstructions();

if (!projected_obstructions.is_empty()) {
for (const NavigationMeshSourceGeometryData2D::ProjectedObstruction &projected_obstruction : projected_obstructions) {
if (projected_obstruction.carve) {
continue;
}
if (projected_obstruction.vertices.is_empty() || projected_obstruction.vertices.size() % 2 != 0) {
continue;
}

Path64 clip_path;
clip_path.reserve(projected_obstruction.vertices.size() / 2);
for (int i = 0; i < projected_obstruction.vertices.size() / 2; i++) {
const Point64 &point = Point64(projected_obstruction.vertices[i * 2], projected_obstruction.vertices[i * 2 + 1]);
clip_path.push_back(point);
}
if (!IsPositive(clip_path)) {
std::reverse(clip_path.begin(), clip_path.end());
}
obstruction_polygon_paths.push_back(clip_path);
}
}

Rect2 baking_rect = p_navigation_mesh->get_baking_rect();
if (baking_rect.has_area()) {
Vector2 baking_rect_offset = p_navigation_mesh->get_baking_rect_offset();
Expand Down Expand Up @@ -809,6 +887,33 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation
if (agent_radius_offset > 0.0) {
path_solution = InflatePaths(path_solution, -agent_radius_offset, JoinType::Miter, EndType::Polygon);
}

if (!projected_obstructions.is_empty()) {
obstruction_polygon_paths.resize(0);
for (const NavigationMeshSourceGeometryData2D::ProjectedObstruction &projected_obstruction : projected_obstructions) {
if (!projected_obstruction.carve) {
continue;
}
if (projected_obstruction.vertices.is_empty() || projected_obstruction.vertices.size() % 2 != 0) {
continue;
}

Path64 clip_path;
clip_path.reserve(projected_obstruction.vertices.size() / 2);
for (int i = 0; i < projected_obstruction.vertices.size() / 2; i++) {
const Point64 &point = Point64(projected_obstruction.vertices[i * 2], projected_obstruction.vertices[i * 2 + 1]);
clip_path.push_back(point);
}
if (!IsPositive(clip_path)) {
std::reverse(clip_path.begin(), clip_path.end());
}
obstruction_polygon_paths.push_back(clip_path);
}
if (obstruction_polygon_paths.size() > 0) {
path_solution = Difference(path_solution, obstruction_polygon_paths, FillRule::NonZero);
}
}

//path_solution = RamerDouglasPeucker(path_solution, 0.025); //

real_t border_size = p_navigation_mesh->get_border_size();
Expand Down
Loading

0 comments on commit f2fd015

Please sign in to comment.