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 NavigationObstacle options to affect navigation mesh baking #89034

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" />
smix8 marked this conversation as resolved.
Show resolved Hide resolved
<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) {
smix8 marked this conversation as resolved.
Show resolved Hide resolved
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;
smix8 marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading