Merge pull request #29118 from JFonS/improve_navmesh_generation

Various improvements to NavigationMesh generation
This commit is contained in:
Rémi Verschelde 2019-05-27 17:27:14 +02:00 committed by GitHub
commit 4c77332e32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 296 additions and 26 deletions

View File

@ -54,26 +54,28 @@ void NavigationMeshEditor::_notification(int p_option) {
}
void NavigationMeshEditor::_bake_pressed() {
button_bake->set_pressed(false);
ERR_FAIL_COND(!node);
const String conf_warning = node->get_configuration_warning();
if (!conf_warning.empty()) {
err_dialog->set_text(conf_warning);
err_dialog->popup_centered_minsize();
button_bake->set_pressed(false);
return;
}
NavigationMeshGenerator::clear(node->get_navigation_mesh());
NavigationMeshGenerator::bake(node->get_navigation_mesh(), node);
EditorNavigationMeshGenerator::get_singleton()->clear(node->get_navigation_mesh());
EditorNavigationMeshGenerator::get_singleton()->bake(node->get_navigation_mesh(), node);
node->update_gizmo();
if (node) {
node->update_gizmo();
}
}
void NavigationMeshEditor::_clear_pressed() {
if (node)
NavigationMeshGenerator::clear(node->get_navigation_mesh());
EditorNavigationMeshGenerator::get_singleton()->clear(node->get_navigation_mesh());
button_bake->set_pressed(false);
bake_info->set_text("");

View File

@ -29,14 +29,31 @@
/*************************************************************************/
#include "navigation_mesh_generator.h"
#include "core/math/quick_hull.h"
#include "core/os/thread.h"
#include "editor/editor_settings.h"
#include "scene/3d/collision_shape.h"
#include "scene/3d/mesh_instance.h"
#include "scene/3d/physics_body.h"
#include "scene/resources/box_shape.h"
#include "scene/resources/capsule_shape.h"
#include "scene/resources/concave_polygon_shape.h"
#include "scene/resources/convex_polygon_shape.h"
#include "scene/resources/cylinder_shape.h"
#include "scene/resources/plane_shape.h"
#include "scene/resources/primitive_meshes.h"
#include "scene/resources/shape.h"
#include "scene/resources/sphere_shape.h"
void NavigationMeshGenerator::_add_vertex(const Vector3 &p_vec3, Vector<float> &p_verticies) {
EditorNavigationMeshGenerator *EditorNavigationMeshGenerator::singleton = NULL;
void EditorNavigationMeshGenerator::_add_vertex(const Vector3 &p_vec3, Vector<float> &p_verticies) {
p_verticies.push_back(p_vec3.x);
p_verticies.push_back(p_vec3.y);
p_verticies.push_back(p_vec3.z);
}
void NavigationMeshGenerator::_add_mesh(const Ref<Mesh> &p_mesh, const Transform &p_xform, Vector<float> &p_verticies, Vector<int> &p_indices) {
void EditorNavigationMeshGenerator::_add_mesh(const Ref<Mesh> &p_mesh, const Transform &p_xform, Vector<float> &p_verticies, Vector<int> &p_indices) {
int current_vertex_count = 0;
for (int i = 0; i < p_mesh->get_surface_count(); i++) {
@ -91,23 +108,132 @@ void NavigationMeshGenerator::_add_mesh(const Ref<Mesh> &p_mesh, const Transform
}
}
void NavigationMeshGenerator::_parse_geometry(const Transform &p_base_inverse, Node *p_node, Vector<float> &p_verticies, Vector<int> &p_indices) {
void EditorNavigationMeshGenerator::_add_faces(const PoolVector3Array &p_faces, const Transform &p_xform, Vector<float> &p_verticies, Vector<int> &p_indices) {
int face_count = p_faces.size() / 3;
int current_vertex_count = p_verticies.size() / 3;
if (Object::cast_to<MeshInstance>(p_node)) {
for (int j = 0; j < face_count; j++) {
_add_vertex(p_xform.xform(p_faces[j * 3 + 0]), p_verticies);
_add_vertex(p_xform.xform(p_faces[j * 3 + 1]), p_verticies);
_add_vertex(p_xform.xform(p_faces[j * 3 + 2]), p_verticies);
p_indices.push_back(current_vertex_count + (j * 3 + 0));
p_indices.push_back(current_vertex_count + (j * 3 + 2));
p_indices.push_back(current_vertex_count + (j * 3 + 1));
}
}
void EditorNavigationMeshGenerator::_parse_geometry(Transform p_accumulated_transform, Node *p_node, Vector<float> &p_verticies, Vector<int> &p_indices, int p_generate_from, uint32_t p_collision_mask) {
if (Object::cast_to<MeshInstance>(p_node) && p_generate_from != NavigationMesh::PARSED_GEOMETRY_STATIC_COLLIDERS) {
MeshInstance *mesh_instance = Object::cast_to<MeshInstance>(p_node);
Ref<Mesh> mesh = mesh_instance->get_mesh();
if (mesh.is_valid()) {
_add_mesh(mesh, p_base_inverse * mesh_instance->get_global_transform(), p_verticies, p_indices);
_add_mesh(mesh, p_accumulated_transform * mesh_instance->get_transform(), p_verticies, p_indices);
}
}
if (Object::cast_to<StaticBody>(p_node) && p_generate_from != NavigationMesh::PARSED_GEOMETRY_MESH_INSTANCES) {
StaticBody *static_body = Object::cast_to<StaticBody>(p_node);
if (static_body->get_collision_layer() & p_collision_mask) {
for (int i = 0; i < p_node->get_child_count(); ++i) {
Node *child = p_node->get_child(i);
if (Object::cast_to<CollisionShape>(child)) {
CollisionShape *col_shape = Object::cast_to<CollisionShape>(child);
Transform transform = p_accumulated_transform * static_body->get_transform() * col_shape->get_transform();
Ref<Mesh> mesh;
Ref<Shape> s = col_shape->get_shape();
BoxShape *box = Object::cast_to<BoxShape>(*s);
if (box) {
Ref<CubeMesh> cube_mesh;
cube_mesh.instance();
cube_mesh->set_size(box->get_extents() * 2.0);
mesh = cube_mesh;
}
CapsuleShape *capsule = Object::cast_to<CapsuleShape>(*s);
if (capsule) {
Ref<CapsuleMesh> capsule_mesh;
capsule_mesh.instance();
capsule_mesh->set_radius(capsule->get_radius());
capsule_mesh->set_mid_height(capsule->get_height() / 2.0);
mesh = capsule_mesh;
}
CylinderShape *cylinder = Object::cast_to<CylinderShape>(*s);
if (cylinder) {
Ref<CylinderMesh> cylinder_mesh;
cylinder_mesh.instance();
cylinder_mesh->set_height(cylinder->get_height());
cylinder_mesh->set_bottom_radius(cylinder->get_radius());
cylinder_mesh->set_top_radius(cylinder->get_radius());
mesh = cylinder_mesh;
}
SphereShape *sphere = Object::cast_to<SphereShape>(*s);
if (sphere) {
Ref<SphereMesh> sphere_mesh;
sphere_mesh.instance();
sphere_mesh->set_radius(sphere->get_radius());
sphere_mesh->set_height(sphere->get_radius() * 2.0);
mesh = sphere_mesh;
}
ConcavePolygonShape *concave_polygon = Object::cast_to<ConcavePolygonShape>(*s);
if (concave_polygon) {
_add_faces(concave_polygon->get_faces(), transform, p_verticies, p_indices);
}
ConvexPolygonShape *convex_polygon = Object::cast_to<ConvexPolygonShape>(*s);
if (convex_polygon) {
Vector<Vector3> varr = Variant(convex_polygon->get_points());
Geometry::MeshData md;
Error err = QuickHull::build(varr, md);
if (err == OK) {
PoolVector3Array faces;
for (int j = 0; j < md.faces.size(); ++j) {
Geometry::MeshData::Face face = md.faces[j];
for (int k = 2; k < face.indices.size(); ++k) {
faces.push_back(md.vertices[face.indices[0]]);
faces.push_back(md.vertices[face.indices[k - 1]]);
faces.push_back(md.vertices[face.indices[k]]);
}
}
_add_faces(faces, transform, p_verticies, p_indices);
}
}
if (mesh.is_valid()) {
_add_mesh(mesh, transform, p_verticies, p_indices);
}
}
}
}
}
if (Object::cast_to<Spatial>(p_node)) {
Spatial *spatial = Object::cast_to<Spatial>(p_node);
p_accumulated_transform = p_accumulated_transform * spatial->get_transform();
}
for (int i = 0; i < p_node->get_child_count(); i++) {
_parse_geometry(p_base_inverse, p_node->get_child(i), p_verticies, p_indices);
_parse_geometry(p_accumulated_transform, p_node->get_child(i), p_verticies, p_indices, p_generate_from, p_collision_mask);
}
}
void NavigationMeshGenerator::_convert_detail_mesh_to_native_navigation_mesh(const rcPolyMeshDetail *p_detail_mesh, Ref<NavigationMesh> p_nav_mesh) {
void EditorNavigationMeshGenerator::_convert_detail_mesh_to_native_navigation_mesh(const rcPolyMeshDetail *p_detail_mesh, Ref<NavigationMesh> p_nav_mesh) {
PoolVector<Vector3> nav_vertices;
@ -135,7 +261,7 @@ void NavigationMeshGenerator::_convert_detail_mesh_to_native_navigation_mesh(con
}
}
void NavigationMeshGenerator::_build_recast_navigation_mesh(Ref<NavigationMesh> p_nav_mesh, EditorProgress *ep,
void EditorNavigationMeshGenerator::_build_recast_navigation_mesh(Ref<NavigationMesh> p_nav_mesh, EditorProgress *ep,
rcHeightfield *hf, rcCompactHeightfield *chf, rcContourSet *cset, rcPolyMesh *poly_mesh, rcPolyMeshDetail *detail_mesh,
Vector<float> &vertices, Vector<int> &indices) {
rcContext ctx;
@ -257,7 +383,18 @@ void NavigationMeshGenerator::_build_recast_navigation_mesh(Ref<NavigationMesh>
detail_mesh = 0;
}
void NavigationMeshGenerator::bake(Ref<NavigationMesh> p_nav_mesh, Node *p_node) {
EditorNavigationMeshGenerator *EditorNavigationMeshGenerator::get_singleton() {
return singleton;
}
EditorNavigationMeshGenerator::EditorNavigationMeshGenerator() {
singleton = this;
}
EditorNavigationMeshGenerator::~EditorNavigationMeshGenerator() {
}
void EditorNavigationMeshGenerator::bake(Ref<NavigationMesh> p_nav_mesh, Node *p_node) {
ERR_FAIL_COND(!p_nav_mesh.is_valid());
@ -267,7 +404,7 @@ void NavigationMeshGenerator::bake(Ref<NavigationMesh> p_nav_mesh, Node *p_node)
Vector<float> vertices;
Vector<int> indices;
_parse_geometry(Object::cast_to<Spatial>(p_node)->get_global_transform().affine_inverse(), p_node, vertices, indices);
_parse_geometry(Object::cast_to<Spatial>(p_node)->get_transform().affine_inverse(), p_node, vertices, indices, p_nav_mesh->get_parsed_geometry_type(), p_nav_mesh->get_collision_mask());
if (vertices.size() > 0 && indices.size() > 0) {
@ -297,9 +434,14 @@ void NavigationMeshGenerator::bake(Ref<NavigationMesh> p_nav_mesh, Node *p_node)
ep.step(TTR("Done!"), 11);
}
void NavigationMeshGenerator::clear(Ref<NavigationMesh> p_nav_mesh) {
void EditorNavigationMeshGenerator::clear(Ref<NavigationMesh> p_nav_mesh) {
if (p_nav_mesh.is_valid()) {
p_nav_mesh->clear_polygons();
p_nav_mesh->set_vertices(PoolVector<Vector3>());
}
}
void EditorNavigationMeshGenerator::_bind_methods() {
ClassDB::bind_method(D_METHOD("bake", "nav_mesh", "root_node"), &EditorNavigationMeshGenerator::bake);
ClassDB::bind_method(D_METHOD("clear", "nav_mesh"), &EditorNavigationMeshGenerator::clear);
}

View File

@ -31,20 +31,23 @@
#ifndef NAVIGATION_MESH_GENERATOR_H
#define NAVIGATION_MESH_GENERATOR_H
#include "core/os/thread.h"
#include "editor/editor_node.h"
#include "editor/editor_settings.h"
#include "scene/3d/mesh_instance.h"
#include "scene/3d/navigation_mesh.h"
#include "scene/resources/shape.h"
#include <Recast.h>
class NavigationMeshGenerator {
class EditorNavigationMeshGenerator : public Object {
GDCLASS(EditorNavigationMeshGenerator, Object);
static EditorNavigationMeshGenerator *singleton;
protected:
static void _bind_methods();
static void _add_vertex(const Vector3 &p_vec3, Vector<float> &p_verticies);
static void _add_mesh(const Ref<Mesh> &p_mesh, const Transform &p_xform, Vector<float> &p_verticies, Vector<int> &p_indices);
static void _parse_geometry(const Transform &p_base_inverse, Node *p_node, Vector<float> &p_verticies, Vector<int> &p_indices);
static void _add_faces(const PoolVector3Array &p_faces, const Transform &p_xform, Vector<float> &p_verticies, Vector<int> &p_indices);
static void _parse_geometry(Transform p_accumulated_transform, Node *p_node, Vector<float> &p_verticies, Vector<int> &p_indices, int p_generate_from, uint32_t p_collision_mask);
static void _convert_detail_mesh_to_native_navigation_mesh(const rcPolyMeshDetail *p_detail_mesh, Ref<NavigationMesh> p_nav_mesh);
static void _build_recast_navigation_mesh(Ref<NavigationMesh> p_nav_mesh, EditorProgress *ep,
@ -52,8 +55,13 @@ protected:
rcPolyMeshDetail *detail_mesh, Vector<float> &vertices, Vector<int> &indices);
public:
static void bake(Ref<NavigationMesh> p_nav_mesh, Node *p_node);
static void clear(Ref<NavigationMesh> p_nav_mesh);
static EditorNavigationMeshGenerator *get_singleton();
EditorNavigationMeshGenerator();
~EditorNavigationMeshGenerator();
void bake(Ref<NavigationMesh> p_nav_mesh, Node *p_node);
void clear(Ref<NavigationMesh> p_nav_mesh);
};
#endif // NAVIGATION_MESH_GENERATOR_H

View File

@ -32,8 +32,23 @@
#include "navigation_mesh_editor_plugin.h"
#ifdef TOOLS_ENABLED
EditorNavigationMeshGenerator *_nav_mesh_generator = NULL;
#endif
void register_recast_types() {
#ifdef TOOLS_ENABLED
EditorPlugins::add_by_type<NavigationMeshEditorPlugin>();
_nav_mesh_generator = memnew(EditorNavigationMeshGenerator);
ClassDB::register_class<EditorNavigationMeshGenerator>();
Engine::get_singleton()->add_singleton(Engine::Singleton("NavigationMeshGenerator", EditorNavigationMeshGenerator::get_singleton()));
#endif
}
void unregister_recast_types() {}
void unregister_recast_types() {
#ifdef TOOLS_ENABLED
if (_nav_mesh_generator) {
memdelete(_nav_mesh_generator);
}
#endif
}

View File

@ -73,6 +73,41 @@ int NavigationMesh::get_sample_partition_type() const {
return static_cast<int>(partition_type);
}
void NavigationMesh::set_parsed_geometry_type(int p_value) {
ERR_FAIL_COND(p_value >= PARSED_GEOMETRY_MAX);
parsed_geometry_type = static_cast<ParsedGeometryType>(p_value);
_change_notify();
}
int NavigationMesh::get_parsed_geometry_type() const {
return parsed_geometry_type;
}
void NavigationMesh::set_collision_mask(uint32_t p_mask) {
collision_mask = p_mask;
}
uint32_t NavigationMesh::get_collision_mask() const {
return collision_mask;
}
void NavigationMesh::set_collision_mask_bit(int p_bit, bool p_value) {
uint32_t mask = get_collision_mask();
if (p_value)
mask |= 1 << p_bit;
else
mask &= ~(1 << p_bit);
set_collision_mask(mask);
}
bool NavigationMesh::get_collision_mask_bit(int p_bit) const {
return get_collision_mask() & (1 << p_bit);
}
void NavigationMesh::set_cell_size(float p_value) {
cell_size = p_value;
}
@ -204,6 +239,7 @@ bool NavigationMesh::get_filter_walkable_low_height_spans() const {
void NavigationMesh::set_vertices(const PoolVector<Vector3> &p_vertices) {
vertices = p_vertices;
_change_notify();
}
PoolVector<Vector3> NavigationMesh::get_vertices() const {
@ -217,6 +253,7 @@ void NavigationMesh::_set_polygons(const Array &p_array) {
for (int i = 0; i < p_array.size(); i++) {
polygons.write[i].indices = p_array[i];
}
_change_notify();
}
Array NavigationMesh::_get_polygons() const {
@ -235,6 +272,7 @@ void NavigationMesh::add_polygon(const Vector<int> &p_polygon) {
Polygon polygon;
polygon.indices = p_polygon;
polygons.push_back(polygon);
_change_notify();
}
int NavigationMesh::get_polygon_count() const {
@ -340,6 +378,15 @@ void NavigationMesh::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_sample_partition_type", "sample_partition_type"), &NavigationMesh::set_sample_partition_type);
ClassDB::bind_method(D_METHOD("get_sample_partition_type"), &NavigationMesh::get_sample_partition_type);
ClassDB::bind_method(D_METHOD("set_parsed_geometry_type", "geometry_type"), &NavigationMesh::set_parsed_geometry_type);
ClassDB::bind_method(D_METHOD("get_parsed_geometry_type"), &NavigationMesh::get_parsed_geometry_type);
ClassDB::bind_method(D_METHOD("set_collision_mask", "mask"), &NavigationMesh::set_collision_mask);
ClassDB::bind_method(D_METHOD("get_collision_mask"), &NavigationMesh::get_collision_mask);
ClassDB::bind_method(D_METHOD("set_collision_mask_bit", "bit", "value"), &NavigationMesh::set_collision_mask_bit);
ClassDB::bind_method(D_METHOD("get_collision_mask_bit", "bit"), &NavigationMesh::get_collision_mask_bit);
ClassDB::bind_method(D_METHOD("set_cell_size", "cell_size"), &NavigationMesh::set_cell_size);
ClassDB::bind_method(D_METHOD("get_cell_size"), &NavigationMesh::get_cell_size);
@ -405,10 +452,16 @@ void NavigationMesh::_bind_methods() {
BIND_CONSTANT(SAMPLE_PARTITION_MONOTONE);
BIND_CONSTANT(SAMPLE_PARTITION_LAYERS);
BIND_CONSTANT(PARSED_GEOMETRY_MESH_INSTANCES);
BIND_CONSTANT(PARSED_GEOMETRY_STATIC_COLLIDERS);
BIND_CONSTANT(PARSED_GEOMETRY_BOTH);
ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR3_ARRAY, "vertices", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "set_vertices", "get_vertices");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "polygons", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "_set_polygons", "_get_polygons");
ADD_PROPERTY(PropertyInfo(Variant::INT, "sample_partition_type/sample_partition_type", PROPERTY_HINT_ENUM, "Watershed,Monotone,Layers"), "set_sample_partition_type", "get_sample_partition_type");
ADD_PROPERTY(PropertyInfo(Variant::INT, "geometry/parsed_geometry_type", PROPERTY_HINT_ENUM, "Mesh Instances,Static Colliders,Both"), "set_parsed_geometry_type", "get_parsed_geometry_type");
ADD_PROPERTY(PropertyInfo(Variant::INT, "geometry/collision_mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_collision_mask", "get_collision_mask");
ADD_PROPERTY(PropertyInfo(Variant::REAL, "cell/size", PROPERTY_HINT_RANGE, "0.1,1.0,0.01,or_greater"), "set_cell_size", "get_cell_size");
ADD_PROPERTY(PropertyInfo(Variant::REAL, "cell/height", PROPERTY_HINT_RANGE, "0.1,1.0,0.01,or_greater"), "set_cell_height", "get_cell_height");
@ -429,6 +482,15 @@ void NavigationMesh::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "filter/filter_walkable_low_height_spans"), "set_filter_walkable_low_height_spans", "get_filter_walkable_low_height_spans");
}
void NavigationMesh::_validate_property(PropertyInfo &property) const {
if (property.name == "geometry/collision_mask") {
if (parsed_geometry_type == PARSED_GEOMETRY_MESH_INSTANCES) {
property.usage = 0;
return;
}
}
}
NavigationMesh::NavigationMesh() {
cell_size = 0.3f;
cell_height = 0.2f;
@ -445,7 +507,8 @@ NavigationMesh::NavigationMesh() {
detail_sample_max_error = 1.0f;
partition_type = SAMPLE_PARTITION_WATERSHED;
parsed_geometry_type = PARSED_GEOMETRY_MESH_INSTANCES;
collision_mask = 0xFFFFFFFF;
filter_low_hanging_obstacles = false;
filter_ledge_spans = false;
filter_walkable_low_height_spans = false;
@ -566,8 +629,17 @@ void NavigationMeshInstance::set_navigation_mesh(const Ref<NavigationMesh> &p_na
navigation->navmesh_remove(nav_id);
nav_id = -1;
}
if (navmesh.is_valid()) {
navmesh->remove_change_receptor(this);
}
navmesh = p_navmesh;
if (navmesh.is_valid()) {
navmesh->add_change_receptor(this);
}
if (navigation && navmesh.is_valid() && enabled) {
nav_id = navigation->navmesh_add(navmesh, get_relative_transform(navigation), this);
}
@ -617,6 +689,11 @@ void NavigationMeshInstance::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled"), "set_enabled", "is_enabled");
}
void NavigationMeshInstance::_changed_callback(Object *p_changed, const char *p_prop) {
update_gizmo();
update_configuration_warning();
}
NavigationMeshInstance::NavigationMeshInstance() {
debug_view = NULL;
@ -625,3 +702,8 @@ NavigationMeshInstance::NavigationMeshInstance() {
enabled = true;
set_notify_transform(true);
}
NavigationMeshInstance::~NavigationMeshInstance() {
if (navmesh.is_valid())
navmesh->remove_change_receptor(this);
}

View File

@ -57,6 +57,7 @@ class NavigationMesh : public Resource {
protected:
static void _bind_methods();
virtual void _validate_property(PropertyInfo &property) const;
void _set_polygons(const Array &p_array);
Array _get_polygons() const;
@ -69,6 +70,13 @@ public:
SAMPLE_PARTITION_MAX
};
enum ParsedGeometryType {
PARSED_GEOMETRY_MESH_INSTANCES = 0,
PARSED_GEOMETRY_STATIC_COLLIDERS,
PARSED_GEOMETRY_BOTH,
PARSED_GEOMETRY_MAX
};
protected:
float cell_size;
float cell_height;
@ -85,6 +93,8 @@ protected:
float detail_sample_max_error;
SamplePartitionType partition_type;
ParsedGeometryType parsed_geometry_type;
uint32_t collision_mask;
bool filter_low_hanging_obstacles;
bool filter_ledge_spans;
@ -95,6 +105,15 @@ public:
void set_sample_partition_type(int p_value);
int get_sample_partition_type() const;
void set_parsed_geometry_type(int p_value);
int get_parsed_geometry_type() const;
void set_collision_mask(uint32_t p_mask);
uint32_t get_collision_mask() const;
void set_collision_mask_bit(int p_bit, bool p_value);
bool get_collision_mask_bit(int p_bit) const;
void set_cell_size(float p_value);
float get_cell_size() const;
@ -174,6 +193,7 @@ class NavigationMeshInstance : public Spatial {
protected:
void _notification(int p_what);
static void _bind_methods();
void _changed_callback(Object *p_changed, const char *p_prop);
public:
void set_enabled(bool p_enabled);
@ -185,6 +205,7 @@ public:
String get_configuration_warning() const;
NavigationMeshInstance();
~NavigationMeshInstance();
};
#endif // NAVIGATION_MESH_H