aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/tools/plugin.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/zylann.hterrain/tools/plugin.gd')
-rw-r--r--game/addons/zylann.hterrain/tools/plugin.gd884
1 files changed, 884 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/plugin.gd b/game/addons/zylann.hterrain/tools/plugin.gd
new file mode 100644
index 0000000..741ce8d
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/plugin.gd
@@ -0,0 +1,884 @@
+@tool # https://www.youtube.com/watch?v=Y7JG63IuaWs
+
+extends EditorPlugin
+
+const HTerrain = preload("../hterrain.gd")
+const HTerrainDetailLayer = preload("../hterrain_detail_layer.gd")
+const HTerrainData = preload("../hterrain_data.gd")
+const HTerrainMesher = preload("../hterrain_mesher.gd")
+const HTerrainTextureSet = preload("../hterrain_texture_set.gd")
+const HT_PreviewGenerator = preload("./preview_generator.gd")
+const HT_TerrainPainter = preload("./brush/terrain_painter.gd")
+const HT_BrushDecal = preload("./brush/decal.gd")
+const HT_Util = preload("../util/util.gd")
+const HT_EditorUtil = preload("./util/editor_util.gd")
+const HT_LoadTextureDialog = preload("./load_texture_dialog.gd")
+const HT_GlobalMapBaker = preload("./globalmap_baker.gd")
+const HT_ImageFileCache = preload("../util/image_file_cache.gd")
+const HT_Logger = preload("../util/logger.gd")
+const HT_EditPanel = preload("./panel.gd")
+const HT_GeneratorDialog = preload("./generator/generator_dialog.gd")
+const HT_TextureSetEditor = preload("./texture_editor/set_editor/texture_set_editor.gd")
+const HT_TextureSetImportEditor = \
+ preload("./texture_editor/set_editor/texture_set_import_editor.gd")
+const HT_ProgressWindow = preload("./progress_window.gd")
+
+const HT_EditPanelScene = preload("./panel.tscn")
+const HT_ProgressWindowScene = preload("./progress_window.tscn")
+const HT_GeneratorDialogScene = preload("./generator/generator_dialog.tscn")
+const HT_ImportDialogScene = preload("./importer/importer_dialog.tscn")
+const HT_GenerateMeshDialogScene = preload("./generate_mesh_dialog.tscn")
+const HT_ResizeDialogScene = preload("./resize_dialog/resize_dialog.tscn")
+const HT_ExportImageDialogScene = preload("./exporter/export_image_dialog.tscn")
+const HT_TextureSetEditorScene = preload("./texture_editor/set_editor/texture_set_editor.tscn")
+const HT_TextureSetImportEditorScene = \
+ preload("./texture_editor/set_editor/texture_set_import_editor.tscn")
+const HT_AboutDialogScene = preload("./about/about_dialog.tscn")
+
+const DOCUMENTATION_URL = "https://hterrain-plugin.readthedocs.io/en/latest"
+
+const MENU_IMPORT_MAPS = 0
+const MENU_GENERATE = 1
+const MENU_BAKE_GLOBALMAP = 2
+const MENU_RESIZE = 3
+const MENU_UPDATE_EDITOR_COLLIDER = 4
+const MENU_GENERATE_MESH = 5
+const MENU_EXPORT_HEIGHTMAP = 6
+const MENU_LOOKDEV = 7
+const MENU_DOCUMENTATION = 8
+const MENU_ABOUT = 9
+
+
+# TODO Rename _terrain
+var _node : HTerrain = null
+
+# GUI
+var _panel : HT_EditPanel = null
+var _toolbar : Container = null
+var _toolbar_brush_buttons := {}
+var _generator_dialog : HT_GeneratorDialog = null
+# TODO Rename _import_terrain_dialog
+var _import_dialog = null
+var _export_image_dialog = null
+
+# This window is only used for operations not triggered by an existing dialog.
+# In Godot it has been solved by automatically reparenting the dialog:
+# https://github.com/godotengine/godot/pull/71209
+# But `get_exclusive_child()` is not exposed. So dialogs triggering a progress
+# dialog may need their own child instance...
+var _progress_window : HT_ProgressWindow = null
+
+var _generate_mesh_dialog = null
+var _preview_generator : HT_PreviewGenerator = null
+var _resize_dialog = null
+var _about_dialog = null
+var _menu_button : MenuButton
+var _lookdev_menu : PopupMenu
+var _texture_set_editor : HT_TextureSetEditor = null
+var _texture_set_import_editor : HT_TextureSetImportEditor = null
+
+var _globalmap_baker : HT_GlobalMapBaker = null
+var _terrain_had_data_previous_frame := false
+var _image_cache : HT_ImageFileCache
+var _terrain_painter : HT_TerrainPainter = null
+var _brush_decal : HT_BrushDecal = null
+var _mouse_pressed := false
+#var _pending_paint_action = null
+var _pending_paint_commit := false
+
+var _logger := HT_Logger.get_for(self)
+
+
+func get_icon(icon_name: String) -> Texture2D:
+ return HT_EditorUtil.load_texture(
+ "res://addons/zylann.hterrain/tools/icons/icon_" + icon_name + ".svg", _logger)
+
+
+func _enter_tree():
+ _logger.debug("HTerrain plugin Enter tree")
+
+ var dpi_scale = get_editor_interface().get_editor_scale()
+ _logger.debug(str("DPI scale: ", dpi_scale))
+
+ add_custom_type("HTerrain", "Node3D", HTerrain, get_icon("heightmap_node"))
+ add_custom_type("HTerrainDetailLayer", "Node3D", HTerrainDetailLayer,
+ get_icon("detail_layer_node"))
+ add_custom_type("HTerrainData", "Resource", HTerrainData, get_icon("heightmap_data"))
+ # TODO Proper texture
+ add_custom_type("HTerrainTextureSet", "Resource", HTerrainTextureSet, null)
+
+ _preview_generator = HT_PreviewGenerator.new()
+ get_editor_interface().get_resource_previewer().add_preview_generator(_preview_generator)
+
+ _terrain_painter = HT_TerrainPainter.new()
+ _terrain_painter.set_brush_size(5)
+ _terrain_painter.get_brush().size_changed.connect(_on_brush_size_changed)
+ add_child(_terrain_painter)
+
+ _brush_decal = HT_BrushDecal.new()
+ _brush_decal.set_size(_terrain_painter.get_brush_size())
+
+ _image_cache = HT_ImageFileCache.new("user://temp_hterrain_image_cache")
+
+ var editor_interface := get_editor_interface()
+ var base_control := editor_interface.get_base_control()
+
+ _panel = HT_EditPanelScene.instantiate()
+ HT_Util.apply_dpi_scale(_panel, dpi_scale)
+ _panel.hide()
+ add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, _panel)
+ # Apparently _ready() still isn't called at this point...
+ _panel.call_deferred("set_terrain_painter", _terrain_painter)
+ _panel.call_deferred("setup_dialogs", base_control)
+ _panel.set_undo_redo(get_undo_redo())
+ _panel.set_image_cache(_image_cache)
+ _panel.detail_selected.connect(_on_detail_selected)
+ _panel.texture_selected.connect(_on_texture_selected)
+ _panel.detail_list_changed.connect(_update_brush_buttons_availability)
+ _panel.edit_texture_pressed.connect(_on_Panel_edit_texture_pressed)
+ _panel.import_textures_pressed.connect(_on_Panel_import_textures_pressed)
+
+ _toolbar = HBoxContainer.new()
+ add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, _toolbar)
+ _toolbar.hide()
+
+ var menu := MenuButton.new()
+ menu.set_text("Terrain")
+ menu.get_popup().add_item("Import maps...", MENU_IMPORT_MAPS)
+ menu.get_popup().add_item("Generate...", MENU_GENERATE)
+ menu.get_popup().add_item("Resize...", MENU_RESIZE)
+ menu.get_popup().add_item("Bake global map", MENU_BAKE_GLOBALMAP)
+ menu.get_popup().add_separator()
+ menu.get_popup().add_item("Update Editor Collider", MENU_UPDATE_EDITOR_COLLIDER)
+ menu.get_popup().add_separator()
+ menu.get_popup().add_item("Generate mesh (heavy)", MENU_GENERATE_MESH)
+ menu.get_popup().add_separator()
+ menu.get_popup().add_item("Export heightmap", MENU_EXPORT_HEIGHTMAP)
+ menu.get_popup().add_separator()
+ _lookdev_menu = PopupMenu.new()
+ _lookdev_menu.name = "LookdevMenu"
+ _lookdev_menu.about_to_popup.connect(_on_lookdev_menu_about_to_show)
+ _lookdev_menu.id_pressed.connect(_on_lookdev_menu_id_pressed)
+ menu.get_popup().add_child(_lookdev_menu)
+ menu.get_popup().add_submenu_item("Lookdev", _lookdev_menu.name, MENU_LOOKDEV)
+ menu.get_popup().id_pressed.connect(_menu_item_selected)
+ menu.get_popup().add_separator()
+ menu.get_popup().add_item("Documentation", MENU_DOCUMENTATION)
+ menu.get_popup().add_item("About HTerrain...", MENU_ABOUT)
+ _toolbar.add_child(menu)
+ _menu_button = menu
+
+ var mode_icons := {}
+ mode_icons[HT_TerrainPainter.MODE_RAISE] = get_icon("heightmap_raise")
+ mode_icons[HT_TerrainPainter.MODE_LOWER] = get_icon("heightmap_lower")
+ mode_icons[HT_TerrainPainter.MODE_SMOOTH] = get_icon("heightmap_smooth")
+ mode_icons[HT_TerrainPainter.MODE_FLATTEN] = get_icon("heightmap_flatten")
+ mode_icons[HT_TerrainPainter.MODE_SPLAT] = get_icon("heightmap_paint")
+ mode_icons[HT_TerrainPainter.MODE_COLOR] = get_icon("heightmap_color")
+ mode_icons[HT_TerrainPainter.MODE_DETAIL] = get_icon("grass")
+ mode_icons[HT_TerrainPainter.MODE_MASK] = get_icon("heightmap_mask")
+ mode_icons[HT_TerrainPainter.MODE_LEVEL] = get_icon("heightmap_level")
+ mode_icons[HT_TerrainPainter.MODE_ERODE] = get_icon("heightmap_erode")
+
+ var mode_tooltips := {}
+ mode_tooltips[HT_TerrainPainter.MODE_RAISE] = "Raise height"
+ mode_tooltips[HT_TerrainPainter.MODE_LOWER] = "Lower height"
+ mode_tooltips[HT_TerrainPainter.MODE_SMOOTH] = "Smooth height"
+ mode_tooltips[HT_TerrainPainter.MODE_FLATTEN] = "Flatten (flatten to a specific height)"
+ mode_tooltips[HT_TerrainPainter.MODE_SPLAT] = "Texture paint"
+ mode_tooltips[HT_TerrainPainter.MODE_COLOR] = "Color paint"
+ mode_tooltips[HT_TerrainPainter.MODE_DETAIL] = "Grass paint"
+ mode_tooltips[HT_TerrainPainter.MODE_MASK] = "Cut holes"
+ mode_tooltips[HT_TerrainPainter.MODE_LEVEL] = "Level (smoothly flattens to average)"
+ mode_tooltips[HT_TerrainPainter.MODE_ERODE] = "Erode"
+
+ _toolbar.add_child(VSeparator.new())
+
+ # I want modes to be in that order in the GUI
+ var ordered_brush_modes := [
+ HT_TerrainPainter.MODE_RAISE,
+ HT_TerrainPainter.MODE_LOWER,
+ HT_TerrainPainter.MODE_SMOOTH,
+ HT_TerrainPainter.MODE_LEVEL,
+ HT_TerrainPainter.MODE_FLATTEN,
+ HT_TerrainPainter.MODE_ERODE,
+ HT_TerrainPainter.MODE_SPLAT,
+ HT_TerrainPainter.MODE_COLOR,
+ HT_TerrainPainter.MODE_DETAIL,
+ HT_TerrainPainter.MODE_MASK
+ ]
+
+ var mode_group := ButtonGroup.new()
+
+ for mode in ordered_brush_modes:
+ var button := Button.new()
+ button.flat = true
+ button.icon = mode_icons[mode]
+ button.tooltip_text = mode_tooltips[mode]
+ button.set_toggle_mode(true)
+ button.set_button_group(mode_group)
+
+ if mode == _terrain_painter.get_mode():
+ button.button_pressed = true
+
+ button.pressed.connect(_on_mode_selected.bind(mode))
+ _toolbar.add_child(button)
+
+ _toolbar_brush_buttons[mode] = button
+
+ _generator_dialog = HT_GeneratorDialogScene.instantiate()
+ _generator_dialog.set_image_cache(_image_cache)
+ _generator_dialog.set_undo_redo(get_undo_redo())
+ base_control.add_child(_generator_dialog)
+ _generator_dialog.apply_dpi_scale(dpi_scale)
+
+ _import_dialog = HT_ImportDialogScene.instantiate()
+ _import_dialog.permanent_change_performed.connect(_on_permanent_change_performed)
+ HT_Util.apply_dpi_scale(_import_dialog, dpi_scale)
+ base_control.add_child(_import_dialog)
+
+ _progress_window = HT_ProgressWindowScene.instantiate()
+ base_control.add_child(_progress_window)
+
+ _generate_mesh_dialog = HT_GenerateMeshDialogScene.instantiate()
+ _generate_mesh_dialog.generate_selected.connect(_on_GenerateMeshDialog_generate_selected)
+ HT_Util.apply_dpi_scale(_generate_mesh_dialog, dpi_scale)
+ base_control.add_child(_generate_mesh_dialog)
+
+ _resize_dialog = HT_ResizeDialogScene.instantiate()
+ _resize_dialog.permanent_change_performed.connect(_on_permanent_change_performed)
+ HT_Util.apply_dpi_scale(_resize_dialog, dpi_scale)
+ base_control.add_child(_resize_dialog)
+
+ _globalmap_baker = HT_GlobalMapBaker.new()
+ _globalmap_baker.progress_notified.connect(_progress_window.handle_progress)
+ _globalmap_baker.permanent_change_performed.connect(_on_permanent_change_performed)
+ add_child(_globalmap_baker)
+
+ _export_image_dialog = HT_ExportImageDialogScene.instantiate()
+ HT_Util.apply_dpi_scale(_export_image_dialog, dpi_scale)
+ base_control.add_child(_export_image_dialog)
+ # Need to call deferred because in the specific case where you start the editor
+ # with the plugin enabled, _ready won't be called at this point
+ _export_image_dialog.call_deferred("setup_dialogs", base_control)
+
+ _about_dialog = HT_AboutDialogScene.instantiate()
+ HT_Util.apply_dpi_scale(_about_dialog, dpi_scale)
+ base_control.add_child(_about_dialog)
+
+ _texture_set_editor = HT_TextureSetEditorScene.instantiate()
+ _texture_set_editor.set_undo_redo(get_undo_redo())
+ HT_Util.apply_dpi_scale(_texture_set_editor, dpi_scale)
+ base_control.add_child(_texture_set_editor)
+ _texture_set_editor.call_deferred("setup_dialogs", base_control)
+
+ _texture_set_import_editor = HT_TextureSetImportEditorScene.instantiate()
+ _texture_set_import_editor.set_undo_redo(get_undo_redo())
+ _texture_set_import_editor.set_editor_file_system(
+ get_editor_interface().get_resource_filesystem())
+ HT_Util.apply_dpi_scale(_texture_set_import_editor, dpi_scale)
+ base_control.add_child(_texture_set_import_editor)
+ _texture_set_import_editor.call_deferred("setup_dialogs", base_control)
+
+ _texture_set_editor.import_selected.connect(_on_TextureSetEditor_import_selected)
+
+
+func _exit_tree():
+ _logger.debug("HTerrain plugin Exit tree")
+
+ # Make sure we release all references to edited stuff
+ _edit(null)
+
+ _panel.queue_free()
+ _panel = null
+
+ _toolbar.queue_free()
+ _toolbar = null
+
+ _generator_dialog.queue_free()
+ _generator_dialog = null
+
+ _import_dialog.queue_free()
+ _import_dialog = null
+
+ _progress_window.queue_free()
+ _progress_window = null
+
+ _generate_mesh_dialog.queue_free()
+ _generate_mesh_dialog = null
+
+ _resize_dialog.queue_free()
+ _resize_dialog = null
+
+ _export_image_dialog.queue_free()
+ _export_image_dialog = null
+
+ _about_dialog.queue_free()
+ _about_dialog = null
+
+ _texture_set_editor.queue_free()
+ _texture_set_editor = null
+
+ _texture_set_import_editor.queue_free()
+ _texture_set_import_editor = null
+
+ get_editor_interface().get_resource_previewer().remove_preview_generator(_preview_generator)
+ _preview_generator = null
+
+ # TODO Manual clear cuz it can't do it automatically due to a Godot bug
+ _image_cache.clear()
+
+ # TODO https://github.com/godotengine/godot/issues/6254#issuecomment-246139694
+ # This was supposed to be automatic, but was never implemented it seems...
+ remove_custom_type("HTerrain")
+ remove_custom_type("HTerrainDetailLayer")
+ remove_custom_type("HTerrainData")
+ remove_custom_type("HTerrainTextureSet")
+
+
+func _handles(object):
+ return _get_terrain_from_object(object) != null
+
+
+func _edit(object):
+ _logger.debug(str("Edit ", object))
+
+ var node = _get_terrain_from_object(object)
+
+ if _node != null:
+ _node.tree_exited.disconnect(_terrain_exited_scene)
+
+ _node = node
+
+ if _node != null:
+ _node.tree_exited.connect(_terrain_exited_scene)
+
+ _update_brush_buttons_availability()
+
+ _panel.set_terrain(_node)
+ _generator_dialog.set_terrain(_node)
+ _import_dialog.set_terrain(_node)
+ _terrain_painter.set_terrain(_node)
+ _brush_decal.set_terrain(_node)
+ _generate_mesh_dialog.set_terrain(_node)
+ _resize_dialog.set_terrain(_node)
+ _export_image_dialog.set_terrain(_node)
+
+ if object is HTerrainDetailLayer:
+ # Auto-select layer for painting
+ if object.is_layer_index_valid():
+ _panel.set_detail_layer_index(object.get_layer_index())
+ _on_detail_selected(object.get_layer_index())
+
+ _update_toolbar_menu_availability()
+
+
+static func _get_terrain_from_object(object):
+ if object != null and object is Node3D:
+ if not object.is_inside_tree():
+ return null
+ if object is HTerrain:
+ return object
+ if object is HTerrainDetailLayer and object.get_parent() is HTerrain:
+ return object.get_parent()
+ return null
+
+
+func _update_brush_buttons_availability():
+ if _node == null:
+ return
+ if _node.get_data() != null:
+ var data = _node.get_data()
+ var has_details = (data.get_map_count(HTerrainData.CHANNEL_DETAIL) > 0)
+
+ if has_details:
+ var button = _toolbar_brush_buttons[HT_TerrainPainter.MODE_DETAIL]
+ button.disabled = false
+ else:
+ var button = _toolbar_brush_buttons[HT_TerrainPainter.MODE_DETAIL]
+ if button.button_pressed:
+ _select_brush_mode(HT_TerrainPainter.MODE_RAISE)
+ button.disabled = true
+
+
+func _update_toolbar_menu_availability():
+ var data_available := false
+ if _node != null and _node.get_data() != null:
+ data_available = true
+ var popup : PopupMenu = _menu_button.get_popup()
+ for i in popup.get_item_count():
+ #var id = popup.get_item_id(i)
+ # Turn off items if there is no data for them to work on
+ if data_available:
+ popup.set_item_disabled(i, false)
+ popup.set_item_tooltip(i, "")
+ else:
+ popup.set_item_disabled(i, true)
+ popup.set_item_tooltip(i, "Terrain has no data")
+
+
+func _make_visible(visible: bool):
+ _panel.set_visible(visible)
+ _toolbar.set_visible(visible)
+ _brush_decal.update_visibility()
+
+ # TODO Workaround https://github.com/godotengine/godot/issues/6459
+ # When the user selects another node,
+ # I want the plugin to release its references to the terrain.
+ # This is important because if we don't do that, some modified resources will still be
+ # loaded in memory, so if the user closes the scene and reopens it later, the changes will
+ # still be partially present, and this is not expected.
+ if not visible:
+ _edit(null)
+
+
+# TODO Can't hint return as `Vector2?` because it's nullable
+func _get_pointed_cell_position(mouse_position: Vector2, p_camera: Camera3D):# -> Vector2:
+ # Need to do an extra conversion in case the editor viewport is in half-resolution mode
+ var viewport := p_camera.get_viewport()
+ var viewport_container : Control = viewport.get_parent()
+ var screen_pos = mouse_position * Vector2(viewport.size) / viewport_container.size
+
+ var origin = p_camera.project_ray_origin(screen_pos)
+ var dir = p_camera.project_ray_normal(screen_pos)
+
+ var ray_distance := p_camera.far * 1.2
+ return _node.cell_raycast(origin, dir, ray_distance)
+
+
+func _forward_3d_gui_input(p_camera: Camera3D, p_event: InputEvent) -> int:
+ if _node == null || _node.get_data() == null:
+ return AFTER_GUI_INPUT_PASS
+
+ _node._edit_update_viewer_position(p_camera)
+ _panel.set_camera_transform(p_camera.global_transform)
+
+ var captured_event = false
+
+ if p_event is InputEventMouseButton:
+ var mb = p_event
+
+ if mb.button_index == MOUSE_BUTTON_LEFT or mb.button_index == MOUSE_BUTTON_RIGHT:
+ if mb.pressed == false:
+ _mouse_pressed = false
+
+ # Need to check modifiers before capturing the event,
+ # because they are used in navigation schemes
+ if (not mb.ctrl_pressed) and (not mb.alt_pressed) and mb.button_index == MOUSE_BUTTON_LEFT:
+ if mb.pressed:
+ # TODO Allow to paint on click
+ # TODO `pressure` is not available in button press events
+ # So I have to assume zero to avoid discontinuities with move events
+ #_terrain_painter.paint_input(hit_pos_in_cells, 0.0)
+ _mouse_pressed = true
+
+ captured_event = true
+
+ if not _mouse_pressed:
+ # Just finished painting
+ _pending_paint_commit = true
+ _terrain_painter.get_brush().on_paint_end()
+
+ if _terrain_painter.get_mode() == HT_TerrainPainter.MODE_FLATTEN \
+ and _terrain_painter.has_meta("pick_height") \
+ and _terrain_painter.get_meta("pick_height"):
+ _terrain_painter.set_meta("pick_height", false)
+ # Pick height
+ var hit_pos_in_cells = _get_pointed_cell_position(mb.position, p_camera)
+ if hit_pos_in_cells != null:
+ var h = _node.get_data().get_height_at(
+ int(hit_pos_in_cells.x), int(hit_pos_in_cells.y))
+ _logger.debug("Picking height {0}".format([h]))
+ _terrain_painter.set_flatten_height(h)
+
+ elif p_event is InputEventMouseMotion:
+ var mm = p_event
+ var hit_pos_in_cells = _get_pointed_cell_position(mm.position, p_camera)
+ if hit_pos_in_cells != null:
+ _brush_decal.set_position(Vector3(hit_pos_in_cells.x, 0, hit_pos_in_cells.y))
+
+ if _mouse_pressed:
+ if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
+ _terrain_painter.paint_input(hit_pos_in_cells, mm.pressure)
+ captured_event = true
+
+ # This is in case the data or textures change as the user edits the terrain,
+ # to keep the decal working without having to noodle around with nested signals
+ _brush_decal.update_visibility()
+
+ if captured_event:
+ return AFTER_GUI_INPUT_STOP
+ return AFTER_GUI_INPUT_PASS
+
+
+func _process(delta: float):
+ if _node == null:
+ return
+
+ var has_data = (_node.get_data() != null)
+
+ if _pending_paint_commit:
+ if has_data:
+ if not _terrain_painter.is_operation_pending():
+ _pending_paint_commit = false
+ if _terrain_painter.has_modified_chunks():
+ _logger.debug("Paint completed")
+ var changes : Dictionary = _terrain_painter.commit()
+ _paint_completed(changes)
+ else:
+ _pending_paint_commit = false
+
+ # Poll presence of data resource
+ if has_data != _terrain_had_data_previous_frame:
+ _terrain_had_data_previous_frame = has_data
+ _update_toolbar_menu_availability()
+
+
+func _paint_completed(changes: Dictionary):
+ var time_before = Time.get_ticks_msec()
+
+ var heightmap_data = _node.get_data()
+ assert(heightmap_data != null)
+
+ var chunk_positions : Array = changes.chunk_positions
+ # Should not create an UndoRedo action if nothing changed
+ assert(len(chunk_positions) > 0)
+ var changed_maps : Array = changes.maps
+
+ var action_name := "Modify HTerrainData "
+ for i in len(changed_maps):
+ var mm = changed_maps[i]
+ var map_debug_name := HTerrainData.get_map_debug_name(mm.map_type, mm.map_index)
+ if i > 0:
+ action_name += " and "
+ action_name += map_debug_name
+
+ var redo_maps := []
+ var undo_maps := []
+ var chunk_size := _terrain_painter.get_undo_chunk_size()
+
+ for map in changed_maps:
+ # Cache images to disk so RAM does not continuously go up (or at least much slower)
+ for chunks in [map.chunk_initial_datas, map.chunk_final_datas]:
+ for i in len(chunks):
+ var im : Image = chunks[i]
+ chunks[i] = _image_cache.save_image(im)
+
+ redo_maps.append({
+ "map_type": map.map_type,
+ "map_index": map.map_index,
+ "chunks": map.chunk_final_datas
+ })
+ undo_maps.append({
+ "map_type": map.map_type,
+ "map_index": map.map_index,
+ "chunks": map.chunk_initial_datas
+ })
+
+ var undo_data := {
+ "chunk_positions": chunk_positions,
+ "chunk_size": chunk_size,
+ "maps": undo_maps
+ }
+ var redo_data := {
+ "chunk_positions": chunk_positions,
+ "chunk_size": chunk_size,
+ "maps": redo_maps
+ }
+
+# {
+# chunk_positions: [Vector2, Vector2, ...]
+# chunk_size: int
+# maps: [
+# {
+# map_type: int
+# map_index: int
+# chunks: [
+# int, int, ...
+# ]
+# },
+# ...
+# ]
+# }
+
+ var ur := get_undo_redo()
+
+ ur.create_action(action_name)
+ ur.add_do_method(heightmap_data, "_edit_apply_undo", redo_data, _image_cache)
+ ur.add_undo_method(heightmap_data, "_edit_apply_undo", undo_data, _image_cache)
+
+ # Small hack here:
+ # commit_actions executes the do method, however terrain modifications are heavy ones,
+ # so we don't really want to re-run an update in every chunk that was modified during painting.
+ # The data is already in its final state,
+ # so we just prevent the resource from applying changes here.
+ heightmap_data._edit_set_disable_apply_undo(true)
+ ur.commit_action()
+ heightmap_data._edit_set_disable_apply_undo(false)
+
+ var time_spent = Time.get_ticks_msec() - time_before
+ _logger.debug(str(action_name, " | ", len(chunk_positions), " chunks | ", time_spent, " ms"))
+
+
+func _terrain_exited_scene():
+ _logger.debug("HTerrain exited the scene")
+ _edit(null)
+
+
+func _menu_item_selected(id: int):
+ _logger.debug(str("Menu item selected ", id))
+
+ match id:
+ MENU_IMPORT_MAPS:
+ _import_dialog.popup_centered()
+
+ MENU_GENERATE:
+ _generator_dialog.popup_centered()
+
+ MENU_BAKE_GLOBALMAP:
+ var data = _node.get_data()
+ if data != null:
+ _globalmap_baker.bake(_node)
+
+ MENU_RESIZE:
+ _resize_dialog.popup_centered()
+
+ MENU_UPDATE_EDITOR_COLLIDER:
+ # This is for editor tools to be able to use terrain collision.
+ # It's not automatic because keeping this collider up to date is
+ # expensive, but not too bad IMO because that feature is not often
+ # used in editor for now.
+ # If users complain too much about this, there are ways to improve it:
+ #
+ # 1) When the terrain gets deselected, update the terrain collider
+ # in a thread automatically. This is still expensive but should
+ # be easy to do.
+ #
+ # 2) Bullet actually support modifying the heights dynamically,
+ # as long as we stay within min and max bounds,
+ # so PR a change to the Godot heightmap collider to support passing
+ # a Float Image directly, and make it so the data is in sync
+ # (no CoW plz!!). It's trickier than 1) but almost free.
+ #
+ _node.update_collider()
+
+ MENU_GENERATE_MESH:
+ if _node != null and _node.get_data() != null:
+ _generate_mesh_dialog.popup_centered()
+
+ MENU_EXPORT_HEIGHTMAP:
+ if _node != null and _node.get_data() != null:
+ _export_image_dialog.popup_centered()
+
+ MENU_LOOKDEV:
+ # No actions here, it's a submenu
+ pass
+
+ MENU_DOCUMENTATION:
+ OS.shell_open(DOCUMENTATION_URL)
+
+ MENU_ABOUT:
+ _about_dialog.popup_centered()
+
+
+func _on_lookdev_menu_about_to_show():
+ _lookdev_menu.clear()
+ _lookdev_menu.add_check_item("Disabled")
+ _lookdev_menu.set_item_checked(0, not _node.is_lookdev_enabled())
+ _lookdev_menu.add_separator()
+ var terrain_data : HTerrainData = _node.get_data()
+ if terrain_data == null:
+ _lookdev_menu.add_item("No terrain data")
+ _lookdev_menu.set_item_disabled(0, true)
+ else:
+ for map_type in HTerrainData.CHANNEL_COUNT:
+ var count := terrain_data.get_map_count(map_type)
+ for map_index in count:
+ var map_name := HTerrainData.get_map_debug_name(map_type, map_index)
+ var lookdev_item_index := _lookdev_menu.get_item_count()
+ _lookdev_menu.add_item(map_name, lookdev_item_index)
+ _lookdev_menu.set_item_metadata(lookdev_item_index, {
+ "map_type": map_type,
+ "map_index": map_index
+ })
+
+
+func _on_lookdev_menu_id_pressed(id: int):
+ var meta = _lookdev_menu.get_item_metadata(id)
+ if meta == null:
+ _node.set_lookdev_enabled(false)
+ else:
+ _node.set_lookdev_enabled(true)
+ var data : HTerrainData = _node.get_data()
+ var map_texture = data.get_texture(meta.map_type, meta.map_index)
+ _node.set_lookdev_shader_param("u_map", map_texture)
+ _lookdev_menu.set_item_checked(0, not _node.is_lookdev_enabled())
+
+
+func _on_mode_selected(mode: int):
+ _logger.debug(str("On mode selected ", mode))
+ _terrain_painter.set_mode(mode)
+ _panel.set_brush_editor_display_mode(mode)
+
+
+func _on_texture_selected(index: int):
+ # Switch to texture paint mode when a texture is selected
+ _select_brush_mode(HT_TerrainPainter.MODE_SPLAT)
+ _terrain_painter.set_texture_index(index)
+
+
+func _on_detail_selected(index: int):
+ # Switch to detail paint mode when a detail item is selected
+ _select_brush_mode(HT_TerrainPainter.MODE_DETAIL)
+ _terrain_painter.set_detail_index(index)
+
+
+func _select_brush_mode(mode: int):
+ _toolbar_brush_buttons[mode].button_pressed = true
+ _on_mode_selected(mode)
+
+
+static func get_size_from_raw_length(flen: int):
+ var side_len = roundf(sqrt(float(flen/2)))
+ return int(side_len)
+
+
+func _on_GenerateMeshDialog_generate_selected(lod: int):
+ var data := _node.get_data()
+ if data == null:
+ _logger.error("Terrain has no data, cannot generate mesh")
+ return
+ var heightmap := data.get_image(HTerrainData.CHANNEL_HEIGHT)
+ var scale := _node.map_scale
+ var mesh := HTerrainMesher.make_heightmap_mesh(heightmap, lod, scale, _logger)
+ var mi := MeshInstance3D.new()
+ mi.name = str(_node.name, "_FullMesh")
+ mi.mesh = mesh
+ mi.transform = _node.transform
+ _node.get_parent().add_child(mi)
+ mi.set_owner(get_editor_interface().get_edited_scene_root())
+
+
+# TODO Workaround for https://github.com/Zylann/godot_heightmap_plugin/issues/101
+func _on_permanent_change_performed(message: String):
+ var data := _node.get_data()
+ if data == null:
+ _logger.error("Terrain has no data, cannot mark it as changed")
+ return
+ var ur := get_undo_redo()
+ ur.create_action(message)
+ ur.add_do_method(data, "_dummy_function")
+ #ur.add_undo_method(data, "_dummy_function")
+ ur.commit_action()
+
+
+func _on_brush_size_changed(size):
+ _brush_decal.set_size(size)
+
+
+func _on_Panel_edit_texture_pressed(index: int):
+ var ts := _node.get_texture_set()
+ _texture_set_editor.set_texture_set(ts)
+ _texture_set_editor.select_slot(index)
+ _texture_set_editor.popup_centered()
+
+
+func _on_TextureSetEditor_import_selected():
+ _open_texture_set_import_editor()
+
+
+func _on_Panel_import_textures_pressed():
+ _open_texture_set_import_editor()
+
+
+func _open_texture_set_import_editor():
+ var ts := _node.get_texture_set()
+ _texture_set_import_editor.set_texture_set(ts)
+ _texture_set_import_editor.popup_centered()
+
+
+################
+# DEBUG LAND
+
+# TEST
+#func _physics_process(delta):
+# if Input.is_key_pressed(KEY_KP_0):
+# _debug_spawn_collider_indicators()
+
+
+func _debug_spawn_collider_indicators():
+ var root = get_editor_interface().get_edited_scene_root()
+ var terrain := HT_Util.find_first_node(root, HTerrain) as HTerrain
+ if terrain == null:
+ return
+
+ var test_root : Node3D
+ if not terrain.has_node("__DEBUG"):
+ test_root = Node3D.new()
+ test_root.name = "__DEBUG"
+ terrain.add_child(test_root)
+ else:
+ test_root = terrain.get_node("__DEBUG")
+
+ var space_state := terrain.get_world_3d().direct_space_state
+ var hit_material := StandardMaterial3D.new()
+ hit_material.albedo_color = Color(0, 1, 1)
+ var cube := BoxMesh.new()
+
+ for zi in 16:
+ for xi in 16:
+ var hit_name := str(xi, "_", zi)
+ var pos := Vector3(xi * 16, 1000, zi * 16)
+
+ var query := PhysicsRayQueryParameters3D.new()
+ query.from = pos
+ query.to = pos + Vector3(0, -2000, 0)
+
+ var hit := space_state.intersect_ray(query)
+
+ var mi : MeshInstance3D
+ if not test_root.has_node(hit_name):
+ mi = MeshInstance3D.new()
+ mi.name = hit_name
+ mi.material_override = hit_material
+ mi.mesh = cube
+ test_root.add_child(mi)
+ else:
+ mi = test_root.get_node(hit_name)
+ if hit.is_empty():
+ mi.hide()
+ else:
+ mi.show()
+ mi.position = hit.position
+
+
+func _spawn_vertical_bound_boxes():
+ var data := _node.get_data()
+# var sy = data._chunked_vertical_bounds_size_y
+# var sx = data._chunked_vertical_bounds_size_x
+ var mat := StandardMaterial3D.new()
+ mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
+ mat.albedo_color = Color(1,1,1,0.2)
+ for cy in range(30, 60):
+ for cx in range(30, 60):
+ var vb := data._chunked_vertical_bounds.get_pixel(cx, cy)
+ var minv := vb.r
+ var maxv := vb.g
+ var mi := MeshInstance3D.new()
+ mi.mesh = BoxMesh.new()
+ var cs := HTerrainData.VERTICAL_BOUNDS_CHUNK_SIZE
+ mi.mesh.size = Vector3(cs, maxv - minv, cs)
+ mi.position = Vector3(
+ (float(cx) + 0.5) * cs,
+ minv + mi.mesh.size.y * 0.5,
+ (float(cy) + 0.5) * cs)
+ mi.position *= _node.map_scale
+ mi.scale = _node.map_scale
+ mi.material_override = mat
+ _node.add_child(mi)
+ mi.owner = get_editor_interface().get_edited_scene_root()
+
+# if p_event is InputEventKey:
+# if p_event.pressed == false:
+# if p_event.scancode == KEY_SPACE and p_event.control:
+# _spawn_vertical_bound_boxes()