891 lines
28 KiB
GDScript
891 lines
28 KiB
GDScript
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 PackedTextureImporter = preload("./packed_textures/packed_texture_importer.gd")
|
|
const PackedTextureArrayImporter = preload("./packed_textures/packed_texture_array_importer.gd")
|
|
const PreviewGenerator = preload("./preview_generator.gd")
|
|
const Brush = preload("./brush/terrain_painter.gd")
|
|
const BrushDecal = preload("./brush/decal.gd")
|
|
const Util = preload("../util/util.gd")
|
|
const EditorUtil = preload("./util/editor_util.gd")
|
|
const LoadTextureDialog = preload("./load_texture_dialog.gd")
|
|
const GlobalMapBaker = preload("./globalmap_baker.gd")
|
|
const ImageFileCache = preload("../util/image_file_cache.gd")
|
|
const Logger = preload("../util/logger.gd")
|
|
|
|
# TODO Suffix with Scene
|
|
const EditPanel = preload("./panel.tscn")
|
|
const ProgressWindow = preload("./progress_window.tscn")
|
|
const GeneratorDialog = preload("./generator/generator_dialog.tscn")
|
|
const ImportDialog = preload("./importer/importer_dialog.tscn")
|
|
const GenerateMeshDialog = preload("./generate_mesh_dialog.tscn")
|
|
const ResizeDialog = preload("./resize_dialog/resize_dialog.tscn")
|
|
const ExportImageDialog = preload("./exporter/export_image_dialog.tscn")
|
|
const TextureSetEditor = preload("./texture_editor/set_editor/texture_set_editor.tscn")
|
|
const TextureSetImportEditor = preload("./texture_editor/set_editor/texture_set_import_editor.tscn")
|
|
const 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 = null
|
|
var _toolbar = null
|
|
var _toolbar_brush_buttons = {}
|
|
var _generator_dialog = null
|
|
# TODO Rename _import_terrain_dialog
|
|
var _import_dialog = null
|
|
var _export_image_dialog = null
|
|
var _progress_window = null
|
|
var _generate_mesh_dialog = null
|
|
var _preview_generator = null
|
|
var _resize_dialog = null
|
|
var _about_dialog = null
|
|
var _menu_button : MenuButton
|
|
var _lookdev_menu : PopupMenu
|
|
var _texture_set_editor = null
|
|
var _texture_set_import_editor = null
|
|
|
|
var _globalmap_baker = null
|
|
var _terrain_had_data_previous_frame = false
|
|
var _image_cache : ImageFileCache
|
|
|
|
# Import
|
|
var _packed_texture_importer := PackedTextureImporter.new()
|
|
var _packed_texture_array_importer := PackedTextureArrayImporter.new()
|
|
|
|
var _brush : Brush = null
|
|
var _brush_decal : BrushDecal = null
|
|
var _mouse_pressed := false
|
|
#var _pending_paint_action = null
|
|
var _pending_paint_commit := false
|
|
|
|
var _logger = Logger.get_for(self)
|
|
|
|
|
|
static func get_icon(name: String) -> Texture:
|
|
return load("res://addons/zylann.hterrain/tools/icons/icon_" + name + ".svg") as Texture
|
|
|
|
|
|
func _enter_tree():
|
|
_logger.debug("HTerrain plugin Enter tree")
|
|
|
|
var dpi_scale = EditorUtil.get_dpi_scale(get_editor_interface().get_editor_settings())
|
|
_logger.debug(str("DPI scale: ", dpi_scale))
|
|
|
|
add_custom_type("HTerrain", "Spatial", HTerrain, get_icon("heightmap_node"))
|
|
add_custom_type("HTerrainDetailLayer", "Spatial", 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)
|
|
|
|
add_import_plugin(_packed_texture_importer)
|
|
add_import_plugin(_packed_texture_array_importer)
|
|
|
|
_preview_generator = PreviewGenerator.new()
|
|
get_editor_interface().get_resource_previewer().add_preview_generator(_preview_generator)
|
|
|
|
_brush = Brush.new()
|
|
_brush.set_brush_size(5)
|
|
_brush.connect("changed", self, "_on_brush_changed")
|
|
add_child(_brush)
|
|
|
|
_brush_decal = BrushDecal.new()
|
|
_brush_decal.set_size(_brush.get_brush_size())
|
|
|
|
_image_cache = ImageFileCache.new("user://temp_hterrain_image_cache")
|
|
|
|
var editor_interface := get_editor_interface()
|
|
var base_control := editor_interface.get_base_control()
|
|
|
|
_panel = EditPanel.instance()
|
|
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_brush", _brush)
|
|
_panel.call_deferred("setup_dialogs", base_control)
|
|
_panel.set_undo_redo(get_undo_redo())
|
|
_panel.set_image_cache(_image_cache)
|
|
_panel.connect("detail_selected", self, "_on_detail_selected")
|
|
_panel.connect("texture_selected", self, "_on_texture_selected")
|
|
_panel.connect("detail_list_changed", self, "_update_brush_buttons_availability")
|
|
_panel.connect("edit_texture_pressed", self, "_on_Panel_edit_texture_pressed")
|
|
_panel.connect("import_textures_pressed", self, "_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.connect("about_to_show", self, "_on_lookdev_menu_about_to_show")
|
|
_lookdev_menu.connect("id_pressed", self, "_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().connect("id_pressed", self, "_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[Brush.MODE_RAISE] = get_icon("heightmap_raise")
|
|
mode_icons[Brush.MODE_LOWER] = get_icon("heightmap_lower")
|
|
mode_icons[Brush.MODE_SMOOTH] = get_icon("heightmap_smooth")
|
|
mode_icons[Brush.MODE_FLATTEN] = get_icon("heightmap_flatten")
|
|
# TODO Have different icons
|
|
mode_icons[Brush.MODE_SPLAT] = get_icon("heightmap_paint")
|
|
mode_icons[Brush.MODE_COLOR] = get_icon("heightmap_color")
|
|
mode_icons[Brush.MODE_DETAIL] = get_icon("grass")
|
|
mode_icons[Brush.MODE_MASK] = get_icon("heightmap_mask")
|
|
mode_icons[Brush.MODE_LEVEL] = get_icon("heightmap_level")
|
|
mode_icons[Brush.MODE_ERODE] = get_icon("heightmap_erode")
|
|
|
|
var mode_tooltips := {}
|
|
mode_tooltips[Brush.MODE_RAISE] = "Raise height"
|
|
mode_tooltips[Brush.MODE_LOWER] = "Lower height"
|
|
mode_tooltips[Brush.MODE_SMOOTH] = "Smooth height"
|
|
mode_tooltips[Brush.MODE_FLATTEN] = "Flatten (flatten to a specific height)"
|
|
mode_tooltips[Brush.MODE_SPLAT] = "Texture paint"
|
|
mode_tooltips[Brush.MODE_COLOR] = "Color paint"
|
|
mode_tooltips[Brush.MODE_DETAIL] = "Grass paint"
|
|
mode_tooltips[Brush.MODE_MASK] = "Cut holes"
|
|
mode_tooltips[Brush.MODE_LEVEL] = "Level (smoothly flattens to average)"
|
|
mode_tooltips[Brush.MODE_ERODE] = "Erode"
|
|
|
|
_toolbar.add_child(VSeparator.new())
|
|
|
|
# I want modes to be in that order in the GUI
|
|
var ordered_brush_modes := [
|
|
Brush.MODE_RAISE,
|
|
Brush.MODE_LOWER,
|
|
Brush.MODE_SMOOTH,
|
|
Brush.MODE_LEVEL,
|
|
Brush.MODE_FLATTEN,
|
|
Brush.MODE_ERODE,
|
|
Brush.MODE_SPLAT,
|
|
Brush.MODE_COLOR,
|
|
Brush.MODE_DETAIL,
|
|
Brush.MODE_MASK
|
|
]
|
|
|
|
var mode_group := ButtonGroup.new()
|
|
|
|
for mode in ordered_brush_modes:
|
|
var button := ToolButton.new()
|
|
button.icon = mode_icons[mode]
|
|
button.set_tooltip(mode_tooltips[mode])
|
|
button.set_toggle_mode(true)
|
|
button.set_button_group(mode_group)
|
|
|
|
if mode == _brush.get_mode():
|
|
button.set_pressed(true)
|
|
|
|
button.connect("pressed", self, "_on_mode_selected", [mode])
|
|
_toolbar.add_child(button)
|
|
|
|
_toolbar_brush_buttons[mode] = button
|
|
|
|
_generator_dialog = GeneratorDialog.instance()
|
|
_generator_dialog.connect("progress_notified", self, "_terrain_progress_notified")
|
|
_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 = ImportDialog.instance()
|
|
_import_dialog.connect("permanent_change_performed", self, "_on_permanent_change_performed")
|
|
Util.apply_dpi_scale(_import_dialog, dpi_scale)
|
|
base_control.add_child(_import_dialog)
|
|
|
|
_progress_window = ProgressWindow.instance()
|
|
base_control.add_child(_progress_window)
|
|
|
|
_generate_mesh_dialog = GenerateMeshDialog.instance()
|
|
_generate_mesh_dialog.connect(
|
|
"generate_selected", self, "_on_GenerateMeshDialog_generate_selected")
|
|
Util.apply_dpi_scale(_generate_mesh_dialog, dpi_scale)
|
|
base_control.add_child(_generate_mesh_dialog)
|
|
|
|
_resize_dialog = ResizeDialog.instance()
|
|
_resize_dialog.connect("permanent_change_performed", self, "_on_permanent_change_performed")
|
|
Util.apply_dpi_scale(_resize_dialog, dpi_scale)
|
|
base_control.add_child(_resize_dialog)
|
|
|
|
_globalmap_baker = GlobalMapBaker.new()
|
|
_globalmap_baker.connect("progress_notified", self, "_terrain_progress_notified")
|
|
_globalmap_baker.connect("permanent_change_performed", self, "_on_permanent_change_performed")
|
|
add_child(_globalmap_baker)
|
|
|
|
_export_image_dialog = ExportImageDialog.instance()
|
|
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 = AboutDialogScene.instance()
|
|
Util.apply_dpi_scale(_about_dialog, dpi_scale)
|
|
base_control.add_child(_about_dialog)
|
|
|
|
_texture_set_editor = TextureSetEditor.instance()
|
|
_texture_set_editor.set_undo_redo(get_undo_redo())
|
|
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 = TextureSetImportEditor.instance()
|
|
_texture_set_import_editor.set_undo_redo(get_undo_redo())
|
|
_texture_set_import_editor.set_editor_file_system(
|
|
get_editor_interface().get_resource_filesystem())
|
|
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.connect("import_selected", self, "_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")
|
|
|
|
remove_import_plugin(_packed_texture_importer)
|
|
_packed_texture_importer = null
|
|
|
|
remove_import_plugin(_packed_texture_array_importer)
|
|
_packed_texture_array_importer = null
|
|
|
|
|
|
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.disconnect("tree_exited", self, "_terrain_exited_scene")
|
|
|
|
_node = node
|
|
|
|
if _node != null:
|
|
_node.connect("tree_exited", self, "_terrain_exited_scene")
|
|
|
|
_update_brush_buttons_availability()
|
|
|
|
_panel.set_terrain(_node)
|
|
_generator_dialog.set_terrain(_node)
|
|
_import_dialog.set_terrain(_node)
|
|
_brush.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 Spatial:
|
|
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[Brush.MODE_DETAIL]
|
|
button.disabled = false
|
|
else:
|
|
var button = _toolbar_brush_buttons[Brush.MODE_DETAIL]
|
|
if button.pressed:
|
|
_select_brush_mode(Brush.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: Camera):# -> 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 = viewport.get_parent()
|
|
var screen_pos = mouse_position * viewport.size / viewport_container.rect_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_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool:
|
|
if _node == null || _node.get_data() == null:
|
|
return false
|
|
|
|
_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 == BUTTON_LEFT or mb.button_index == 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.control) and (not mb.alt) and mb.button_index == BUTTON_LEFT:
|
|
if mb.pressed:
|
|
_mouse_pressed = true
|
|
|
|
captured_event = true
|
|
|
|
if not _mouse_pressed:
|
|
# Just finished painting
|
|
_pending_paint_commit = true
|
|
|
|
if _brush.get_mode() == Brush.MODE_FLATTEN and _brush.has_meta("pick_height") \
|
|
and _brush.get_meta("pick_height"):
|
|
_brush.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]))
|
|
_brush.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(BUTTON_LEFT):
|
|
_brush.paint_input(hit_pos_in_cells)
|
|
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()
|
|
|
|
return captured_event
|
|
|
|
|
|
func _process(delta: float):
|
|
if _node == null:
|
|
return
|
|
|
|
var has_data = (_node.get_data() != null)
|
|
|
|
if _pending_paint_commit:
|
|
if has_data:
|
|
if _brush.has_modified_chunks() and not _brush.is_operation_pending():
|
|
_pending_paint_commit = false
|
|
_logger.debug("Paint completed")
|
|
var changes : Dictionary = _brush.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 = OS.get_ticks_msec()
|
|
|
|
var heightmap_data = _node.get_data()
|
|
assert(heightmap_data != null)
|
|
|
|
var chunk_positions : Array = changes.chunk_positions
|
|
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 := _brush.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 = OS.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))
|
|
_brush.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(Brush.MODE_SPLAT)
|
|
_brush.set_texture_index(index)
|
|
|
|
|
|
func _on_detail_selected(index: int):
|
|
# Switch to detail paint mode when a detail item is selected
|
|
_select_brush_mode(Brush.MODE_DETAIL)
|
|
_brush.set_detail_index(index)
|
|
|
|
|
|
func _select_brush_mode(mode: int):
|
|
_toolbar_brush_buttons[mode].pressed = true
|
|
_on_mode_selected(mode)
|
|
|
|
|
|
static func get_size_from_raw_length(flen: int):
|
|
var side_len = round(sqrt(float(flen/2)))
|
|
return int(side_len)
|
|
|
|
|
|
func _terrain_progress_notified(info: Dictionary):
|
|
if info.has("finished") and info.finished:
|
|
_progress_window.hide()
|
|
|
|
else:
|
|
if not _progress_window.visible:
|
|
_progress_window.popup_centered()
|
|
|
|
var message = ""
|
|
if info.has("message"):
|
|
message = info.message
|
|
|
|
_progress_window.show_progress(info.message, info.progress)
|
|
# TODO Have builtin modal progress bar
|
|
# https://github.com/godotengine/godot/issues/17763
|
|
|
|
|
|
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 := MeshInstance.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_changed():
|
|
_brush_decal.set_size(_brush.get_brush_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 := Util.find_first_node(root, HTerrain) as HTerrain
|
|
if terrain == null:
|
|
return
|
|
|
|
var test_root : Spatial
|
|
if not terrain.has_node("__DEBUG"):
|
|
test_root = Spatial.new()
|
|
test_root.name = "__DEBUG"
|
|
terrain.add_child(test_root)
|
|
else:
|
|
test_root = terrain.get_node("__DEBUG")
|
|
|
|
var space_state := terrain.get_world().direct_space_state
|
|
var hit_material = SpatialMaterial.new()
|
|
hit_material.albedo_color = Color(0, 1, 1)
|
|
var cube = CubeMesh.new()
|
|
|
|
for zi in 16:
|
|
for xi in 16:
|
|
var hit_name = str(xi, "_", zi)
|
|
var pos = Vector3(xi * 16, 1000, zi * 16)
|
|
var hit = space_state.intersect_ray(pos, pos + Vector3(0, -2000, 0))
|
|
var mi : MeshInstance
|
|
if not test_root.has_node(hit_name):
|
|
mi = MeshInstance.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.empty():
|
|
mi.hide()
|
|
else:
|
|
mi.show()
|
|
mi.translation = 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 = SpatialMaterial.new()
|
|
mat.flags_transparent = true
|
|
mat.albedo_color = Color(1,1,1,0.2)
|
|
data._chunked_vertical_bounds.lock()
|
|
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 = MeshInstance.new()
|
|
mi.mesh = CubeMesh.new()
|
|
var cs = HTerrainData.VERTICAL_BOUNDS_CHUNK_SIZE
|
|
mi.mesh.size = Vector3(cs, maxv - minv, cs)
|
|
mi.translation = Vector3(
|
|
(float(cx) + 0.5) * cs,
|
|
minv + mi.mesh.size.y * 0.5,
|
|
(float(cy) + 0.5) * cs)
|
|
mi.translation *= _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()
|
|
|
|
data._chunked_vertical_bounds.unlock()
|
|
|
|
# if p_event is InputEventKey:
|
|
# if p_event.pressed == false:
|
|
# if p_event.scancode == KEY_SPACE and p_event.control:
|
|
# _spawn_vertical_bound_boxes()
|