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

Navigation mesh baking #253

Merged
merged 4 commits into from
Dec 1, 2023
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]

[ext_resource type="Script" path="res://addons/terrain_3d/editor/components/bake_dialog.gd" id="1_57670"]
[ext_resource type="Script" path="res://addons/terrain_3d/editor/components/bake_lod_dialog.gd" id="1_57670"]

[node name="bake_dialog" type="ConfirmationDialog"]
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 115)
Expand Down
185 changes: 172 additions & 13 deletions project/addons/terrain_3d/editor/components/baker.gd
Original file line number Diff line number Diff line change
@@ -1,29 +1,54 @@
extends Object
extends Node

const BakeDialog: PackedScene = preload("res://addons/terrain_3d/editor/components/bake_dialog.tscn")
const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/editor/components/bake_lod_dialog.tscn")
const BAKE_MESH_DESCRIPTION: String = "This will create a child MeshInstance3D. LOD4+ is recommended. LOD0 is slow and dense with vertices every 1 unit. It is not an optimal mesh."
const BAKE_OCCLUDER_DESCRIPTION: String = "This will create a child OccluderInstance3D. LOD4+ is recommended and will take 5+ seconds per region to generate. LOD0 is unnecessarily dense and slow."
const SET_UP_NAVIGATION_DESCRIPTION: String = "This operation will:

- Create a NavigationRegion3D node,
- Assign it a blank NavigationMesh resource,
- Move the Terrain3D node to be a child of the new node,
- And bake the nav mesh.

Once setup is complete, you can modify the settings on your nav mesh, and rebake
without having to run through the setup again.

If preferred, this setup can be canceled and the steps performed manually. For
the best results, adjust the settings on the NavigationMesh resource to match
the settings of your navigation agents and collisions."

var plugin: EditorPlugin
var bake_method: Callable
var bake_dialog: ConfirmationDialog
var bake_lod_dialog: ConfirmationDialog
var confirm_dialog: ConfirmationDialog


func _enter_tree() -> void:
bake_lod_dialog = BakeLodDialog.instantiate()
bake_lod_dialog.hide()
bake_lod_dialog.confirmed.connect(func(): bake_method.call())
bake_lod_dialog.set_unparent_when_invisible(true)

confirm_dialog = ConfirmationDialog.new()
confirm_dialog.hide()
confirm_dialog.confirmed.connect(func(): bake_method.call())
confirm_dialog.set_unparent_when_invisible(true)

func _init() -> void:
bake_dialog = BakeDialog.instantiate()
bake_dialog.hide()
bake_dialog.confirmed.connect(func(): bake_method.call())

func _exit_tree():
bake_lod_dialog.queue_free()
confirm_dialog.queue_free()


func bake_mesh_popup() -> void:
if plugin.terrain:
bake_method = _bake_mesh
bake_dialog.description = BAKE_MESH_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_dialog)
bake_lod_dialog.description = BAKE_MESH_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog)


func _bake_mesh() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_NEAREST)
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_NEAREST)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
Expand All @@ -45,12 +70,12 @@ func _bake_mesh() -> void:
func bake_occluder_popup() -> void:
if plugin.terrain:
bake_method = _bake_occluder
bake_dialog.description = BAKE_OCCLUDER_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_dialog)
bake_lod_dialog.description = BAKE_OCCLUDER_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog)


func _bake_occluder() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_MINIMUM)
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_MINIMUM)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
Expand All @@ -74,3 +99,137 @@ func _bake_occluder() -> void:
undo.add_do_property(occluder_instance, &"owner", plugin.terrain.owner)
undo.add_do_reference(occluder_instance)
undo.commit_action()


func find_nav_region_terrains(nav_region: NavigationRegion3D) -> Array[Terrain3D]:
var result: Array[Terrain3D] = []
if not nav_region.navigation_mesh:
return result

var source_mode := nav_region.navigation_mesh.geometry_source_geometry_mode
if source_mode == NavigationMesh.SOURCE_GEOMETRY_ROOT_NODE_CHILDREN:
result.append_array(nav_region.find_children("", "Terrain3D", true, true))
return result

var group_nodes := nav_region.get_tree().get_nodes_in_group(nav_region.navigation_mesh.geometry_source_group_name)
for node in group_nodes:
if node is Terrain3D:
result.push_back(node)
if source_mode == NavigationMesh.SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN:
result.append_array(node.find_children("", "Terrain3D", true, true))

return result


func find_terrain_nav_regions(terrain: Terrain3D) -> Array[NavigationRegion3D]:
var result: Array[NavigationRegion3D] = []
var root := plugin.get_editor_interface().get_edited_scene_root()
if not root:
return result
for nav_region in root.find_children("", "NavigationRegion3D", true, true):
if find_nav_region_terrains(nav_region).has(terrain):
result.push_back(nav_region)
return result


func bake_nav_mesh() -> void:
if plugin.nav_region:
# A NavigationRegion3D is selected. We only need to bake that one navmesh.
_bake_nav_region_nav_mesh(plugin.nav_region)
print("Baking one NavigationMesh - finished.")

elif plugin.terrain:
# A Terrain3D is selected. There are potentially multiple navmeshes to bake and we need to
# find them all. (The multiple navmesh use-case is likely on very large scenes with lots of
# geometry. Each navmesh in this case would define its own, non-overlapping, baking AABB, to
# cut down on the amount of geometry to bake. In a large open-world RPG, for instance, there
# could be a navmesh for each town.)
var nav_regions := find_terrain_nav_regions(plugin.terrain)
for nav_region in nav_regions:
_bake_nav_region_nav_mesh(nav_region)
print("Baking %d NavigationMesh(es) - finished." % nav_regions.size())


func _bake_nav_region_nav_mesh(nav_region: NavigationRegion3D) -> void:
var nav_mesh := nav_region.navigation_mesh
assert(nav_mesh != null)

var source_geometry_data := NavigationMeshSourceGeometryData3D.new()
NavigationMeshGenerator.parse_source_geometry_data(nav_mesh, source_geometry_data, nav_region)

for terrain in find_nav_region_terrains(nav_region):
var aabb := nav_mesh.filter_baking_aabb
aabb.position += nav_mesh.filter_baking_aabb_offset
aabb = nav_region.global_transform * aabb
var faces := terrain.generate_nav_mesh_source_geometry(aabb)
if not faces.is_empty():
source_geometry_data.add_faces(faces, nav_region.global_transform.inverse())

NavigationMeshGenerator.bake_from_source_geometry_data(nav_mesh, source_geometry_data)

# Assign null first to force the debug display to actually update:
nav_region.set_navigation_mesh(null)
nav_region.set_navigation_mesh(nav_mesh)

# Let other editor plugins and tool scripts know the nav mesh was just baked:
nav_region.bake_finished.emit()


func set_up_navigation_popup() -> void:
if plugin.terrain:
bake_method = _set_up_navigation
confirm_dialog.dialog_text = SET_UP_NAVIGATION_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(confirm_dialog)


func _set_up_navigation() -> void:
assert(plugin.terrain)
var terrain: Terrain3D = plugin.terrain

var nav_region := NavigationRegion3D.new()
nav_region.name = &"NavigationRegion3D"
nav_region.navigation_mesh = NavigationMesh.new()

var undo_redo := plugin.get_undo_redo()

undo_redo.create_action("Terrain3D Set up Navigation")
undo_redo.add_do_method(self, &"_do_set_up_navigation", nav_region, terrain)
undo_redo.add_undo_method(self, &"_undo_set_up_navigation", nav_region, terrain)
undo_redo.add_do_reference(nav_region)
undo_redo.commit_action()

plugin.get_editor_interface().inspect_object(nav_region)
assert(plugin.nav_region == nav_region)

bake_nav_mesh()


func _do_set_up_navigation(nav_region: NavigationRegion3D, terrain: Terrain3D) -> void:
var parent := terrain.get_parent()
var index := terrain.get_index()
var owner := terrain.owner

parent.remove_child(terrain)
nav_region.add_child(terrain)

parent.add_child(nav_region, true)
parent.move_child(nav_region, index)

nav_region.owner = owner
terrain.owner = owner


func _undo_set_up_navigation(nav_region: NavigationRegion3D, terrain: Terrain3D) -> void:
assert(terrain.get_parent() == nav_region)

var parent := nav_region.get_parent()
var index := nav_region.get_index()
var owner := nav_region.get_owner()

parent.remove_child(nav_region)
nav_region.remove_child(terrain)

parent.add_child(terrain, true)
parent.move_child(terrain, index)

terrain.owner = owner
31 changes: 30 additions & 1 deletion project/addons/terrain_3d/editor/components/terrain_tools.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@ var baker: Baker = Baker.new()

enum {
MENU_BAKE_ARRAY_MESH,
MENU_BAKE_OCCLUDER
MENU_BAKE_OCCLUDER,
MENU_BAKE_NAV_MESH,
MENU_SEPARATOR,
MENU_SET_UP_NAVIGATION
}


func _enter_tree() -> void:
baker.plugin = plugin
add_child(baker)

menu_button.text = "Terrain3D Tools"
menu_button.get_popup().add_item("Bake ArrayMesh", MENU_BAKE_ARRAY_MESH)
menu_button.get_popup().add_item("Bake Occluder3D", MENU_BAKE_OCCLUDER)
menu_button.get_popup().add_item("Bake NavMesh", MENU_BAKE_NAV_MESH)
menu_button.get_popup().add_separator("", MENU_SEPARATOR)
menu_button.get_popup().add_item("Set up Navigation", MENU_SET_UP_NAVIGATION)
menu_button.get_popup().id_pressed.connect(_on_menu_pressed)
menu_button.about_to_popup.connect(_on_menu_about_to_popup)
add_child(menu_button)


Expand All @@ -29,3 +37,24 @@ func _on_menu_pressed(id: int) -> void:
baker.bake_mesh_popup()
MENU_BAKE_OCCLUDER:
baker.bake_occluder_popup()
MENU_BAKE_NAV_MESH:
baker.bake_nav_mesh()
MENU_SET_UP_NAVIGATION:
baker.set_up_navigation_popup()


func _on_menu_about_to_popup() -> void:
menu_button.get_popup().set_item_disabled(MENU_BAKE_ARRAY_MESH, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_OCCLUDER, not plugin.terrain)

if plugin.terrain:
var nav_regions := baker.find_terrain_nav_regions(plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, nav_regions.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, nav_regions.size() != 0)
elif plugin.nav_region:
var terrains := baker.find_nav_region_terrains(plugin.nav_region)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, terrains.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)
else:
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, true)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)
11 changes: 6 additions & 5 deletions project/addons/terrain_3d/editor/components/ui.gd
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,17 @@ func _exit_tree() -> void:

func set_visible(p_visible: bool) -> void:
visible = p_visible
toolbar.set_visible(p_visible)
toolbar.set_visible(p_visible and plugin.terrain)
terrain_tools.set_visible(p_visible)

if p_visible:
if p_visible and plugin.terrain:
p_visible = plugin.editor.get_tool() != Terrain3DEditor.REGION
toolbar_settings.set_visible(p_visible)
toolbar_settings.set_visible(p_visible and plugin.terrain)
update_decal()


func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
if not visible:
if not visible or not plugin.terrain:
return

if plugin.editor:
Expand Down Expand Up @@ -157,7 +157,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor


func _on_setting_changed() -> void:
if not visible:
if not visible or not plugin.terrain:
return
brush_data = {
"size": int(toolbar_settings.get_setting("size")),
Expand All @@ -184,6 +184,7 @@ func _on_setting_changed() -> void:
func update_decal() -> void:
var mouse_buttons: int = Input.get_mouse_button_mask()
if not visible or \
not plugin.terrain or \
brush_data.is_empty() or \
mouse_buttons & MOUSE_BUTTON_RIGHT or \
(mouse_buttons & MOUSE_BUTTON_LEFT and not brush_data["show_cursor_while_painting"]) or \
Expand Down
26 changes: 21 additions & 5 deletions project/addons/terrain_3d/editor/editor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ const RegionGizmo: Script = preload("res://addons/terrain_3d/editor/components/r
const TextureDock: Script = preload("res://addons/terrain_3d/editor/components/texture_dock.gd")

var terrain: Terrain3D
var nav_region: NavigationRegion3D

var editor: Terrain3DEditor
var ui: Node # Terrain3DUI see Godot #75388
var texture_dock: TextureDock
var texture_dock_container: CustomControlContainer = CONTAINER_INSPECTOR_BOTTOM

var visible: bool
var region_gizmo: RegionGizmo
var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO
Expand Down Expand Up @@ -46,7 +48,7 @@ func _exit_tree() -> void:


func _handles(object: Object) -> bool:
return object is Terrain3D
return object is Terrain3D or object is NavigationRegion3D


func _edit(object: Object) -> void:
Expand All @@ -68,14 +70,28 @@ func _edit(object: Object) -> void:
if not terrain.storage_changed.is_connected(_load_storage):
terrain.storage_changed.connect(_load_storage)
_load_storage()
else:
terrain = null

if object is NavigationRegion3D:
nav_region = object
else:
nav_region = null

_update_visibility()


func _make_visible(visible: bool) -> void:
ui.set_visible(visible)
texture_dock.set_visible(visible)
update_region_grid()
region_gizmo.set_hidden(!visible)
self.visible = visible
_update_visibility()


func _update_visibility() -> void:
ui.set_visible(visible)
texture_dock.set_visible(visible and terrain)
if terrain:
update_region_grid()
region_gizmo.set_hidden(not visible or not terrain)

func _clear() -> void:
if is_terrain_valid():
Expand Down
Loading