diff --git a/addons/terrain_3d/LICENSE.txt b/addons/terrain_3d/LICENSE.txt index b4293b7..14089b6 100644 --- a/addons/terrain_3d/LICENSE.txt +++ b/addons/terrain_3d/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Cory Petkovsek, Roope Palmroos, and Contributors. +Copyright (c) 2024 Cory Petkovsek, Roope Palmroos, and Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/addons/terrain_3d/README.md b/addons/terrain_3d/README.md index 47f2953..00dd210 100644 --- a/addons/terrain_3d/README.md +++ b/addons/terrain_3d/README.md @@ -1,4 +1,4 @@ - +![Terrain3D Logo](/doc/docs/images/terrain3d.jpg) # Terrain3D A high performance, editable terrain system for Godot 4. @@ -7,7 +7,7 @@ A high performance, editable terrain system for Godot 4. * Written in C++ as a GDExtension addon, which works with official engine builds * Can be accessed by GDScript, C#, and any language Godot supports * Geometric Clipmap Mesh Terrain, as used in The Witcher 3. See [System Architecture](https://terrain3d.readthedocs.io/en/stable/docs/system_architecture.html) -* Up to 16k x 16k in 1k regions (imagine multiple islands without paying for 16k^2 vram) +* Terrains as small as 64x64m up to 65.5x65.5km (4295km^2) in variable sized regions * Up to 32 textures * Up to 10 levels of detail * Foliage instancing diff --git a/addons/terrain_3d/bin/libterrain.android.debug.arm32.so b/addons/terrain_3d/bin/libterrain.android.debug.arm32.so index 4429d13..670eb77 100644 Binary files a/addons/terrain_3d/bin/libterrain.android.debug.arm32.so and b/addons/terrain_3d/bin/libterrain.android.debug.arm32.so differ diff --git a/addons/terrain_3d/bin/libterrain.android.debug.arm64.so b/addons/terrain_3d/bin/libterrain.android.debug.arm64.so index 1e2905f..124db2a 100644 Binary files a/addons/terrain_3d/bin/libterrain.android.debug.arm64.so and b/addons/terrain_3d/bin/libterrain.android.debug.arm64.so differ diff --git a/addons/terrain_3d/bin/libterrain.android.release.arm32.so b/addons/terrain_3d/bin/libterrain.android.release.arm32.so index 8e6ba2f..1432530 100644 Binary files a/addons/terrain_3d/bin/libterrain.android.release.arm32.so and b/addons/terrain_3d/bin/libterrain.android.release.arm32.so differ diff --git a/addons/terrain_3d/bin/libterrain.android.release.arm64.so b/addons/terrain_3d/bin/libterrain.android.release.arm64.so index 6ff140b..74fbdec 100644 Binary files a/addons/terrain_3d/bin/libterrain.android.release.arm64.so and b/addons/terrain_3d/bin/libterrain.android.release.arm64.so differ diff --git a/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib b/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib index 58c56bc..f44a88e 100644 Binary files a/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib and b/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib differ diff --git a/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib b/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib index be5b107..376fe7e 100644 Binary files a/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib and b/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib differ diff --git a/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so b/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so index e78a649..0a14b72 100644 Binary files a/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so and b/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so differ diff --git a/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so b/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so index 8f8756c..9d96cd7 100644 Binary files a/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so and b/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so differ diff --git a/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug b/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug index d340a95..87c6b96 100644 Binary files a/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug and b/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug differ diff --git a/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release b/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release index 9d39905..a32f56d 100644 Binary files a/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release and b/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release differ diff --git a/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll b/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll index 1904d12..81d9653 100644 Binary files a/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll and b/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll differ diff --git a/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll b/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll index ed84c94..ef17f31 100644 Binary files a/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll and b/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll differ diff --git a/addons/terrain_3d/editor.gd b/addons/terrain_3d/editor.gd index 14062ef..15861b0 100644 --- a/addons/terrain_3d/editor.gd +++ b/addons/terrain_3d/editor.gd @@ -7,29 +7,36 @@ extends EditorPlugin const UI: Script = preload("res://addons/terrain_3d/src/ui.gd") const RegionGizmo: Script = preload("res://addons/terrain_3d/src/region_gizmo.gd") const ASSET_DOCK: String = "res://addons/terrain_3d/src/asset_dock.tscn" -const PS_DOCK_POSITION: String = "terrain3d/config/dock_position" -const PS_DOCK_PINNED: String = "terrain3d/config/dock_pinned" + +var modifier_ctrl: bool +var modifier_alt: bool +var modifier_shift: bool +var _last_modifiers: int = 0 +var _input_mode: int = 0 # -1: camera move, 0: none, 1: operating var terrain: Terrain3D var _last_terrain: Terrain3D var nav_region: NavigationRegion3D var editor: Terrain3DEditor +var editor_settings: EditorSettings var ui: Node # Terrain3DUI see Godot #75388 var asset_dock: PanelContainer var region_gizmo: RegionGizmo -var visible: bool var current_region_position: Vector2 var mouse_global_position: Vector3 = Vector3.ZERO - -# Track negative input (CTRL) -var _negative_input: bool = false -# Track state prior to pressing CTRL: -1 not tracked, 0 false, 1 true -var _prev_enable_state: int = -1 +var godot_editor_window: Window # The Godot Editor window +func _init() -> void: + # Get the Godot Editor window. Structure is root:Window/EditorNode/Base Control + godot_editor_window = EditorInterface.get_base_control().get_parent().get_parent() + godot_editor_window.focus_entered.connect(_on_godot_focus_entered) + + func _enter_tree() -> void: editor = Terrain3DEditor.new() + setup_editor_settings() ui = UI.new() ui.plugin = self add_child(ui) @@ -41,7 +48,7 @@ func _enter_tree() -> void: asset_dock = load(ASSET_DOCK).instantiate() asset_dock.initialize(self) - + func _exit_tree() -> void: asset_dock.remove_dock(true) asset_dock.queue_free() @@ -49,25 +56,46 @@ func _exit_tree() -> void: editor.free() scene_changed.disconnect(_on_scene_changed) + godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered) +func _on_godot_focus_entered() -> void: + _read_input() + ui.update_decal() + + +## EditorPlugin selection function call chain isn't consistent. Here's the map of calls: +## Assume we handle Terrain3D and NavigationRegion3D +# Click Terrain3D: _handles(Terrain3D), _make_visible(true), _edit(Terrain3D) +# Deselect: _make_visible(false), _edit(null) +# Click other node: _handles(OtherNode) +# Click NavRegion3D: _handles(NavReg3D), _make_visible(true), _edit(NavReg3D) +# Click NavRegion3D, Terrain3D: _handles(Terrain3D), _edit(Terrain3D) +# Click Terrain3D, NavRegion3D: _handles(NavReg3D), _edit(NavReg3D) func _handles(p_object: Object) -> bool: if p_object is Terrain3D: return true + elif p_object is NavigationRegion3D and is_instance_valid(_last_terrain): + return true # Terrain3DObjects requires access to EditorUndoRedoManager. The only way to make sure it # always has it, is to pass it in here. _edit is NOT called if the node is cut and pasted. - if p_object is Terrain3DObjects: + elif p_object is Terrain3DObjects: p_object.editor_setup(self) elif p_object is Node3D and p_object.get_parent() is Terrain3DObjects: p_object.get_parent().editor_setup(self) - if is_instance_valid(_last_terrain) and _last_terrain.is_inside_tree() and p_object is NavigationRegion3D: - return true - return false +func _make_visible(p_visible: bool, p_redraw: bool = false) -> void: + if p_visible and is_selected(): + ui.set_visible(true) + asset_dock.update_dock() + else: + ui.set_visible(false) + + func _edit(p_object: Object) -> void: if !p_object: _clear() @@ -77,39 +105,41 @@ func _edit(p_object: Object) -> void: return terrain = p_object _last_terrain = terrain + terrain.set_plugin(self) + terrain.set_editor(editor) editor.set_terrain(terrain) region_gizmo.set_node_3d(terrain) terrain.add_gizmo(region_gizmo) - terrain.set_plugin(self) + ui.set_visible(true) + terrain.set_meta("_edit_lock_", true) + + # Deprecated 0.9.3 - Remove 1.0 + if terrain.storage: + ui.terrain_menu.directory_setup.directory_setup_popup() - # Connect to new Assets resource + # Get alerted when a new asset list is loaded if not terrain.assets_changed.is_connected(asset_dock.update_assets): terrain.assets_changed.connect(asset_dock.update_assets) asset_dock.update_assets() - # Connect to new Storage resource - if not terrain.storage_changed.is_connected(_load_storage): - terrain.storage_changed.connect(_load_storage) - _load_storage() + # Get alerted when the region map changes + if not terrain.data.region_map_changed.is_connected(update_region_grid): + terrain.data.region_map_changed.connect(update_region_grid) + update_region_grid() else: _clear() - if is_instance_valid(_last_terrain) and _last_terrain.is_inside_tree(): + if is_terrain_valid(_last_terrain): if p_object is NavigationRegion3D: + ui.set_visible(true, true) nav_region = p_object else: nav_region = null - -func _make_visible(p_visible: bool, p_redraw: bool = false) -> void: - visible = p_visible - ui.set_visible(visible) - update_region_grid() - asset_dock.update_dock(visible) - - + func _clear() -> void: if is_terrain_valid(): - terrain.storage_changed.disconnect(_load_storage) + if terrain.data.region_map_changed.is_connected(update_region_grid): + terrain.data.region_map_changed.disconnect(update_region_grid) terrain.clear_gizmos() terrain = null @@ -124,29 +154,12 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> if not is_terrain_valid(): return AFTER_GUI_INPUT_PASS - ## Track negative input (CTRL) - if p_event is InputEventKey and not p_event.echo and p_event.keycode == KEY_CTRL: - if p_event.is_pressed(): - _negative_input = true - _prev_enable_state = int(ui.toolbar_settings.get_setting("enable")) - ui.toolbar_settings.set_setting("enable", false) - else: - _negative_input = false - ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state)) - _prev_enable_state = -1 + _read_input(p_event) ## Handle mouse movement if p_event is InputEventMouseMotion: - if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT): - return AFTER_GUI_INPUT_PASS + ## Setup active camera & viewport - if _prev_enable_state >= 0 and not Input.is_key_pressed(KEY_CTRL): - _negative_input = false - ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state)) - _prev_enable_state = -1 - - ## Setup for active camera & viewport - # Snap terrain to current camera terrain.set_camera(p_viewport_camera) @@ -169,88 +182,131 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> else: # Else look for intersection with terrain var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir) - if intersection_point.z > 3.4e38 or is_nan(intersection_point.z): # max double or nan + if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan return AFTER_GUI_INPUT_STOP mouse_global_position = intersection_point - - ## Update decal - ui.decal.global_position = mouse_global_position - ui.decal.albedo_mix = 1.0 - if ui.decal_timer.is_stopped(): - ui.update_decal() - else: - ui.decal_timer.start() - ## Update region highlight - var region_size = terrain.get_storage().get_region_size() - var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \ - / (region_size * terrain.get_mesh_vertex_spacing()) ).floor() - if current_region_position != region_position: - current_region_position = region_position - update_region_grid() - - if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and editor.is_operating(): - editor.operate(mouse_global_position, p_viewport_camera.rotation.y) - return AFTER_GUI_INPUT_STOP - - elif p_event is InputEventMouseButton: ui.update_decal() - - if p_event.get_button_index() == MOUSE_BUTTON_LEFT: - if p_event.is_pressed(): - if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT): - return AFTER_GUI_INPUT_STOP - - # If picking - if ui.is_picking(): - ui.pick(mouse_global_position) - if not ui.operation_builder or not ui.operation_builder.is_ready(): - return AFTER_GUI_INPUT_STOP - - # If adjusting regions - if editor.get_tool() == Terrain3DEditor.REGION: - # Skip regions that already exist or don't - var has_region: bool = terrain.get_storage().has_region(mouse_global_position) - var op: int = editor.get_operation() - if ( has_region and op == Terrain3DEditor.ADD) or \ - ( not has_region and op == Terrain3DEditor.SUBTRACT ): - return AFTER_GUI_INPUT_STOP - - # If an automatic operation is ready to go (e.g. gradient) - if ui.operation_builder and ui.operation_builder.is_ready(): - ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y) - return AFTER_GUI_INPUT_STOP - - # Mouse clicked, start editing - editor.start_operation(mouse_global_position) + + if _input_mode != -1: # Not cam rotation + ## Update region highlight + var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \ + / (terrain.get_region_size() * terrain.get_vertex_spacing()) ).floor() + if current_region_position != region_position: + current_region_position = region_position + update_region_grid() + + if _input_mode > 0 and editor.is_operating(): + # Inject pressure - Relies on C++ set_brush_data() using same dictionary instance + ui.brush_data["mouse_pressure"] = p_event.pressure + editor.operate(mouse_global_position, p_viewport_camera.rotation.y) return AFTER_GUI_INPUT_STOP - elif editor.is_operating(): - # Mouse released, save undo data - editor.stop_operation() + return AFTER_GUI_INPUT_PASS + + ui.update_decal() + + if p_event is InputEventMouseButton and _input_mode > 0: + if p_event.is_pressed(): + # If picking + if ui.is_picking(): + ui.pick(mouse_global_position) + if not ui.operation_builder or not ui.operation_builder.is_ready(): + return AFTER_GUI_INPUT_STOP + + # If adjusting regions + if editor.get_tool() == Terrain3DEditor.REGION: + # Skip regions that already exist or don't + var has_region: bool = terrain.data.has_regionp(mouse_global_position) + var op: int = editor.get_operation() + if ( has_region and op == Terrain3DEditor.ADD) or \ + ( not has_region and op == Terrain3DEditor.SUBTRACT ): + return AFTER_GUI_INPUT_STOP + + # If an automatic operation is ready to go (e.g. gradient) + if ui.operation_builder and ui.operation_builder.is_ready(): + ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y) return AFTER_GUI_INPUT_STOP - + + # Mouse clicked, start editing + editor.start_operation(mouse_global_position) + editor.operate(mouse_global_position, p_viewport_camera.rotation.y) + return AFTER_GUI_INPUT_STOP + + # _input_apply released, save undo data + elif editor.is_operating(): + editor.stop_operation() + return AFTER_GUI_INPUT_STOP + return AFTER_GUI_INPUT_PASS + +func _read_input(p_event: InputEvent = null) -> void: + ## Determine if user is moving camera or applying + if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) or \ + p_event is InputEventMouseButton and p_event.is_released() and \ + p_event.get_button_index() == MOUSE_BUTTON_LEFT: + _input_mode = 1 + else: + _input_mode = 0 -func _load_storage() -> void: - if terrain: - update_region_grid() + match get_setting("editors/3d/navigation/navigation_scheme", 0): + 2, 1: # Modo, Maya + if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \ + ( Input.is_key_pressed(KEY_ALT) and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) ): + _input_mode = -1 + if p_event is InputEventMouseButton and p_event.is_released() and \ + ( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \ + ( Input.is_key_pressed(KEY_ALT) and p_event.get_button_index() == MOUSE_BUTTON_LEFT )): + ui.last_rmb_time = Time.get_ticks_msec() + 0, _: # Godot + if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \ + Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE): + _input_mode = -1 + if p_event is InputEventMouseButton and p_event.is_released() and \ + ( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \ + p_event.get_button_index() == MOUSE_BUTTON_MIDDLE ): + ui.last_rmb_time = Time.get_ticks_msec() + if _input_mode < 0: + return + + ## Determine modifiers pressed + modifier_shift = Input.is_key_pressed(KEY_SHIFT) + modifier_ctrl = Input.is_key_pressed(KEY_CTRL) + # Keybind enum: Alt,Space,Meta,Capslock + var alt_key: int + match get_setting("terrain3d/config/alt_key_bind", 0): + 3: alt_key = KEY_CAPSLOCK + 2: alt_key = KEY_META + 1: alt_key = KEY_SPACE + 0, _: alt_key = KEY_ALT + modifier_alt = Input.is_key_pressed(alt_key) + + # Return if modifiers haven't changed AND brush_data has them; + # modifiers disappear from brush_data when clicking asset_dock (Why?) + var current_mods: int = int(modifier_shift) | int(modifier_ctrl) << 1 | int(modifier_alt) << 2 + if _last_modifiers == current_mods and ui.brush_data.has("modifier_shift"): + return + + _last_modifiers = current_mods + ui.brush_data["modifier_shift"] = modifier_shift + ui.brush_data["modifier_ctrl"] = modifier_ctrl + ui.brush_data["modifier_alt"] = modifier_alt + ui.update_modifiers() func update_region_grid() -> void: if not region_gizmo: return + region_gizmo.set_hidden(not ui.visible) - region_gizmo.set_hidden(not visible) - if is_terrain_valid(): region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT region_gizmo.region_position = current_region_position - region_gizmo.region_size = terrain.get_storage().get_region_size() * terrain.get_mesh_vertex_spacing() - region_gizmo.grid = terrain.get_storage().get_region_offsets() + region_gizmo.region_size = terrain.get_region_size() * terrain.get_vertex_spacing() + region_gizmo.grid = terrain.get_data().get_region_locations() terrain.update_gizmos() return @@ -278,22 +334,80 @@ func is_terrain_valid(p_terrain: Terrain3D = null) -> bool: t = p_terrain else: t = terrain - if is_instance_valid(t) and t.is_inside_tree() and t.get_storage(): + if is_instance_valid(t) and t.is_inside_tree() and t.data: return true return false func is_selected() -> bool: - var selected: Array[Node] = get_editor_interface().get_selection().get_selected_nodes() + var selected: Array[Node] = EditorInterface.get_selection().get_selected_nodes() for node in selected: - if node.get_instance_id() == _last_terrain.get_instance_id(): - return true - + if ( is_instance_valid(_last_terrain) and node.get_instance_id() == _last_terrain.get_instance_id() ) or \ + node is Terrain3D: + return true return false func select_terrain() -> void: if is_instance_valid(_last_terrain) and is_terrain_valid(_last_terrain) and not is_selected(): - var es: EditorSelection = get_editor_interface().get_selection() + var es: EditorSelection = EditorInterface.get_selection() es.clear() es.add_node(_last_terrain) + + +## Editor Settings + + +func setup_editor_settings() -> void: + editor_settings = EditorInterface.get_editor_settings() + if not editor_settings.has_setting("terrain3d/config/alt_key_bind"): + editor_settings.set("terrain3d/config/alt_key_bind", 0) + var property_info = { + "name": "terrain3d/config/alt_key_bind", + "type": TYPE_INT, + "hint": PROPERTY_HINT_ENUM, + "hint_string": "Alt,Space,Meta,Capslock" + } + editor_settings.add_property_info(property_info) + + _cleanup_old_settings() + + +# Remove or rename old settings +func _cleanup_old_settings() -> void: + # Rename deprecated settings - Remove in 1.0 + var value: Variant + var rename_arr := [ "terrain3d/config/dock_slot", "terrain3d/config/dock_tile_size", + "terrain3d/config/dock_floating", "terrain3d/config/dock_always_on_top", + "terrain3d/config/dock_window_size", "terrain3d/config/dock_window_position", ] + for es: String in rename_arr: + if editor_settings.has_setting(es): + value = editor_settings.get_setting(es) + editor_settings.erase(es) + editor_settings.set_setting(es.replace("/config/dock_", "/dock/"), value) + + # Special handling + var es: String = "terrain3d/tool_settings/slope" + if editor_settings.has_setting(es): + value = editor_settings.get_setting(es) + if typeof(value) == TYPE_FLOAT: + editor_settings.erase(es) + + +func set_setting(p_str: String, p_value: Variant) -> void: + editor_settings.set_setting(p_str, p_value) + + +func get_setting(p_str: String, p_default: Variant) -> Variant: + if editor_settings.has_setting(p_str): + return editor_settings.get_setting(p_str) + else: + return p_default + + +func has_setting(p_str: String) -> bool: + return editor_settings.has_setting(p_str) + + +func erase_setting(p_str: String) -> void: + editor_settings.erase(p_str) diff --git a/addons/terrain_3d/extras/import_sgt.gd b/addons/terrain_3d/extras/import_sgt.gd index b8b45aa..706a686 100644 --- a/addons/terrain_3d/extras/import_sgt.gd +++ b/addons/terrain_3d/extras/import_sgt.gd @@ -10,7 +10,7 @@ # 1. Click import. The output window and console will report when finished. # 1. Clear the script from your Terrain3D node, and save your scene. # -# The instance transforms are now stored in your Storage resource. +# The instance transforms are now stored in your region files. # # Use clear_instances to erase all instances that match the assign_mesh_id. # diff --git a/addons/terrain_3d/extras/minimum.gdshader b/addons/terrain_3d/extras/minimum.gdshader index 1cedd32..0b50d75 100644 --- a/addons/terrain_3d/extras/minimum.gdshader +++ b/addons/terrain_3d/extras/minimum.gdshader @@ -1,30 +1,41 @@ // This shader is the minimum needed to allow the terrain to function, without any texturing. shader_type spatial; -render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx; +render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform; // Private uniforms uniform float _region_size = 1024.0; uniform float _region_texel_size = 0.0009765625; // = 1/1024 -uniform float _mesh_vertex_spacing = 1.0; -uniform float _mesh_vertex_density = 1.0; // = 1/_mesh_vertex_spacing -uniform int _region_map_size = 16; -uniform int _region_map[256]; -uniform vec2 _region_offsets[256]; +uniform float _vertex_spacing = 1.0; +uniform float _vertex_density = 1.0; // = 1/_vertex_spacing +uniform int _region_map_size = 32; +uniform int _region_map[1024]; +uniform vec2 _region_locations[1024]; uniform sampler2DArray _height_maps : repeat_disable; uniform usampler2DArray _control_maps : repeat_disable; uniform sampler2DArray _color_maps : source_color, filter_linear_mipmap_anisotropic, repeat_disable; uniform sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable; +uniform sampler2D noise_texture : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform float _texture_uv_scale_array[32]; -uniform float _texture_uv_rotation_array[32]; +uniform float _texture_detile_array[32]; uniform vec4 _texture_color_array[32]; uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2 uniform uint _mouse_layer = 0x80000000u; // Layer 32 +// Public uniforms +uniform float vertex_normals_distance : hint_range(0, 1024) = 128.0; + +// Varyings & Types +varying flat vec3 v_vertex; // World coordinate vertex location +varying flat vec3 v_camera_pos; +varying float v_vertex_xz_dist; +varying flat ivec3 v_region; varying flat vec2 v_uv_offset; varying flat vec2 v_uv2_offset; +varying vec3 v_normal; +varying float v_region_border_mask; //////////////////////// // Vertex @@ -33,26 +44,22 @@ varying flat vec2 v_uv2_offset; // Takes in UV world space coordinates, returns ivec3 with: // XY: (0 to _region_size) coordinates within a region // Z: layer index used for texturearrays, -1 if not in a region -ivec3 get_region_uv(vec2 uv) { - uv *= _region_texel_size; - ivec2 pos = ivec2(floor(uv)) + (_region_map_size / 2); - int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size); +ivec3 get_region_uv(const vec2 uv) { + ivec2 pos = ivec2(floor(uv * _region_texel_size)) + (_region_map_size / 2); + int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1; - return ivec3(ivec2((uv - _region_offsets[layer_index]) * _region_size), layer_index); + return ivec3(ivec2(mod(uv,_region_size)), layer_index); } // Takes in UV2 region space coordinates, returns vec3 with: // XY: (0 to 1) coordinates within a region // Z: layer index used for texturearrays, -1 if not in a region -vec3 get_region_uv2(vec2 uv) { - // Vertex function added half a texel to UV2, to center the UV's. vertex(), fragment() and get_height() - // call this with reclaimed versions of UV2, so to keep the last row/column within the correct - // window, take back the half pixel before the floor(). - ivec2 pos = ivec2(floor(uv - vec2(_region_texel_size * 0.5))) + (_region_map_size / 2); - int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size); +vec3 get_region_uv2(const vec2 uv2) { + // Remove Texel Offset to ensure correct region index. + ivec2 pos = ivec2(floor(uv2 - vec2(_region_texel_size * 0.5))) + (_region_map_size / 2); + int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1; - // The return value is still texel-centered. - return vec3(uv - _region_offsets[layer_index], float(layer_index)); + return vec3(uv2 - _region_locations[layer_index], float(layer_index)); } // 1 lookup @@ -66,63 +73,75 @@ float get_height(vec2 uv) { } void vertex() { + // Get camera pos in world vertex coords + v_camera_pos = INV_VIEW_MATRIX[3].xyz; + // Get vertex of flat plane in world coordinates and set world UV - vec3 vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; - + v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + + // Camera distance to vertex on flat plane + v_vertex_xz_dist = length(v_vertex.xz - v_camera_pos.xz); + // UV coordinates in world space. Values are 0 to _region_size within regions - UV = round(vertex.xz * _mesh_vertex_density); + UV = round(v_vertex.xz * _vertex_density); + + // UV coordinates in region space + texel offset. Values are 0 to 1 within regions + UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size)); // Discard vertices for Holes. 1 lookup - ivec3 region = get_region_uv(UV); - uint control = texelFetch(_control_maps, region, 0).r; + v_region = get_region_uv(UV); + uint control = texelFetch(_control_maps, v_region, 0).r; bool hole = bool(control >>2u & 0x1u); + // Show holes to all cameras except mouse camera (on exactly 1 layer) if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) && - (hole || (_background_mode == 0u && region.z < 0)) ) { - VERTEX.x = 0./0.; - } else { - // UV coordinates in region space + texel offset. Values are 0 to 1 within regions - UV2 = (UV + vec2(0.5)) * _region_texel_size; - - // Get final vertex location and save it + (hole || (_background_mode == 0u && (get_region_uv(UV - _region_texel_size) & v_region).z < 0))) { + VERTEX.x = 0. / 0.; + } else { + // Set final vertex height & calculate vertex normals. 3 lookups. VERTEX.y = get_height(UV2); + v_vertex.y = VERTEX.y; + v_normal = vec3( + v_vertex.y - get_height(UV2 + vec2(_region_texel_size, 0)), + _vertex_spacing, + v_vertex.y - get_height(UV2 + vec2(0, _region_texel_size)) + ); + // Due to a bug caused by the GPUs linear interpolation across edges of region maps, + // mask region edges and use vertex normals only across region boundaries. + v_region_border_mask = mod(UV.x + 2.5, _region_size) - fract(UV.x) < 5.0 || mod(UV.y + 2.5, _region_size) - fract(UV.y) < 5.0 ? 1. : 0.; } - + // Transform UVs to local to avoid poor precision during varying interpolation. - v_uv_offset = MODEL_MATRIX[3].xz * _mesh_vertex_density; + v_uv_offset = MODEL_MATRIX[3].xz * _vertex_density; UV -= v_uv_offset; v_uv2_offset = v_uv_offset * _region_texel_size; UV2 -= v_uv2_offset; + + // Convert model space to view space w/ skip_vertex_transform render mode + VERTEX = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + VERTEX = (VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; + NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); + BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz); + TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz); } //////////////////////// // Fragment //////////////////////// -// 3 lookups +// 0 - 3 lookups vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) { - // Get the height of the current vertex - float height = get_height(uv); - - // Get the heights to the right and in front, but because of hardware - // interpolation on the edges of the heightmaps, the values are off - // causing the normal map to look weird. So, near the edges of the map - // get the heights to the left or behind instead. Hacky solution that - // reduces the artifact, but doesn't fix it entirely. See #185. - float u, v; - if(mod(uv.y*_region_size, _region_size) > _region_size-2.) { - v = get_height(uv + vec2(0, -_region_texel_size)) - height; - } else { - v = height - get_height(uv + vec2(0, _region_texel_size)); - } - if(mod(uv.x*_region_size, _region_size) > _region_size-2.) { - u = get_height(uv + vec2(-_region_texel_size, 0)) - height; + float u, v, height; + vec3 normal; + // Use vertex normals within radius of vertex_normals_distance, and along region borders. + if (v_region_border_mask > 0.5 || v_vertex_xz_dist < vertex_normals_distance) { + normal = normalize(v_normal); } else { + height = get_height(uv); u = height - get_height(uv + vec2(_region_texel_size, 0)); + v = height - get_height(uv + vec2(0, _region_texel_size)); + normal = normalize(vec3(u, _vertex_spacing, v)); } - - vec3 normal = vec3(u, _mesh_vertex_spacing, v); - normal = normalize(normal); tangent = cross(normal, vec3(0, 0, 1)); binormal = cross(normal, tangent); return normal; @@ -143,4 +162,3 @@ void fragment() { // Apply PBR ALBEDO=vec3(.2); } - diff --git a/addons/terrain_3d/extras/project_on_terrain3d.gd b/addons/terrain_3d/extras/project_on_terrain3d.gd index 976fd4f..c36fc02 100644 --- a/addons/terrain_3d/extras/project_on_terrain3d.gd +++ b/addons/terrain_3d/extras/project_on_terrain3d.gd @@ -61,8 +61,8 @@ #warning += """No Terrain3D node found""" #return # - #if not _terrain.storage: - #warning += """Terrain3D storage is not initialized""" + #if not _terrain.data: + #warning += """Terrain3DData is not initialized""" #return # ## Get global transform @@ -70,8 +70,8 @@ #var gt_inverse := gt.affine_inverse() #for i in transforms.list.size(): #var location: Vector3 = (gt * transforms.list[i]).origin - #var height: float = _terrain.storage.get_height(location) - #var normal: Vector3 = _terrain.storage.get_normal(location) + #var height: float = _terrain.data.get_height(location) + #var normal: Vector3 = _terrain.data.get_normal(location) # #if align_with_collision_normal and not is_nan(normal.x): #transforms.list[i].basis.y = normal diff --git a/addons/terrain_3d/icons/picker.svg b/addons/terrain_3d/icons/picker.svg deleted file mode 100644 index a9f7578..0000000 --- a/addons/terrain_3d/icons/picker.svg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f47275d112d3e0fb5c107c45cb7b828ab3cbe4e8cd08e72b82d86352ff757922 -size 1250 diff --git a/addons/terrain_3d/icons/picker.svg.import b/addons/terrain_3d/icons/picker.svg.import deleted file mode 100644 index 4a7369e..0000000 --- a/addons/terrain_3d/icons/picker.svg.import +++ /dev/null @@ -1,38 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://c11ip32w7ln4v" -path="res://.godot/imported/picker.svg-0ed48f8d7e66014d2aac4b303bc65df6.ctex" -metadata={ -"has_editor_variant": true, -"vram_texture": false -} - -[deps] - -source_file="res://addons/terrain_3d/icons/picker.svg" -dest_files=["res://.godot/imported/picker.svg-0ed48f8d7e66014d2aac4b303bc65df6.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 -svg/scale=1.0 -editor/scale_with_editor_scale=true -editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/plugin.cfg b/addons/terrain_3d/plugin.cfg index b738b1d..9b57dfa 100644 --- a/addons/terrain_3d/plugin.cfg +++ b/addons/terrain_3d/plugin.cfg @@ -3,5 +3,5 @@ name="Terrain3D" description="A high performance, editable terrain system for Godot 4." author="Cory Petkovsek & Roope Palmroos" -version="0.9.2" +version="0.9.3a" script="editor.gd" diff --git a/addons/terrain_3d/src/asset_dock.gd b/addons/terrain_3d/src/asset_dock.gd index b4fea39..e54bab5 100644 --- a/addons/terrain_3d/src/asset_dock.gd +++ b/addons/terrain_3d/src/asset_dock.gd @@ -6,12 +6,13 @@ signal confirmation_closed signal confirmation_confirmed signal confirmation_canceled -const PS_DOCK_SLOT: String = "terrain3d/config/dock_slot" -const PS_DOCK_TILE_SIZE: String = "terrain3d/config/dock_tile_size" -const PS_DOCK_FLOATING: String = "terrain3d/config/dock_floating" -const PS_DOCK_PINNED: String = "terrain3d/config/dock_always_on_top" -const PS_DOCK_WINDOW_POSITION: String = "terrain3d/config/dock_window_position" -const PS_DOCK_WINDOW_SIZE: String = "terrain3d/config/dock_window_size" +const ES_DOCK_SLOT: String = "terrain3d/dock/slot" +const ES_DOCK_TILE_SIZE: String = "terrain3d/dock/tile_size" +const ES_DOCK_FLOATING: String = "terrain3d/dock/floating" +const ES_DOCK_PINNED: String = "terrain3d/dock/always_on_top" +const ES_DOCK_WINDOW_POSITION: String = "terrain3d/dock/window_position" +const ES_DOCK_WINDOW_SIZE: String = "terrain3d/dock/window_size" +const ES_DOCK_TAB: String = "terrain3d/dock/tab" var texture_list: ListContainer var mesh_list: ListContainer @@ -40,10 +41,6 @@ enum { } var state: int = HIDDEN -var window: Window -var _godot_editor_window: Window # The main Godot Editor window -var _godot_last_state: Window.Mode = Window.MODE_FULLSCREEN - enum { POS_LEFT_UL = 0, POS_LEFT_BL = 1, @@ -59,17 +56,15 @@ enum { var slot: int = POS_RIGHT_BR var _initialized: bool = false var plugin: EditorPlugin -var editor_settings: EditorSettings +var window: Window +var _godot_last_state: Window.Mode = Window.MODE_FULLSCREEN func initialize(p_plugin: EditorPlugin) -> void: if p_plugin: plugin = p_plugin - - # Get editor window. Structure is root:Window/EditorNode/Base Control - _godot_editor_window = plugin.get_editor_interface().get_base_control().get_parent().get_parent() - _godot_last_state = _godot_editor_window.mode + _godot_last_state = plugin.godot_editor_window.mode placement_opt = $Box/Buttons/PlacementOpt pinned_btn = $Box/Buttons/Pinned floating_btn = $Box/Buttons/Floating @@ -93,7 +88,6 @@ func initialize(p_plugin: EditorPlugin) -> void: asset_container.add_child(mesh_list) _current_list = texture_list - editor_settings = EditorInterface.get_editor_settings() load_editor_settings() # Connect signals @@ -103,7 +97,7 @@ func initialize(p_plugin: EditorPlugin) -> void: placement_opt.item_selected.connect(set_slot) floating_btn.pressed.connect(make_dock_float) pinned_btn.toggled.connect(_on_pin_changed) - pinned_btn.visible = false + pinned_btn.visible = ( window != null ) size_slider.value_changed.connect(_on_slider_changed) plugin.ui.toolbar.tool_changed.connect(_on_tool_changed) @@ -111,7 +105,7 @@ func initialize(p_plugin: EditorPlugin) -> void: textures_btn.add_theme_font_size_override("font_size", 16 * EditorInterface.get_editor_scale()) _initialized = true - update_dock(plugin.visible) + update_dock() update_layout() @@ -122,7 +116,7 @@ func _ready() -> void: # Setup styles set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel")) # Avoid saving icon resources in tscn when editing w/ a tool script - if plugin.get_editor_interface().get_edited_scene_root() != self: + if EditorInterface.get_edited_scene_root() != self: pinned_btn.icon = get_theme_icon("Pin", "EditorIcons") pinned_btn.text = "" floating_btn.icon = get_theme_icon("MakeFloating", "EditorIcons") @@ -154,7 +148,7 @@ func set_slot(p_slot: int) -> void: placement_opt.selected = slot save_editor_settings() plugin.select_terrain() - update_dock(plugin.visible) + update_dock() func remove_dock(p_force: bool = false) -> void: @@ -167,50 +161,42 @@ func remove_dock(p_force: bool = false) -> void: state = HIDDEN # If windowed and destination is not window or final exit, otherwise leave - elif state == WINDOWED and p_force: - if not window: - return + elif state == WINDOWED and p_force and window: var parent: Node = get_parent() if parent: parent.remove_child(self) - _godot_editor_window.mouse_entered.disconnect(_on_godot_window_entered) - _godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered) - _godot_editor_window.focus_exited.disconnect(_on_godot_focus_exited) - window.hide() - window.queue_free() - window = null + plugin.godot_editor_window.mouse_entered.disconnect(_on_godot_window_entered) + plugin.godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered) + plugin.godot_editor_window.focus_exited.disconnect(_on_godot_focus_exited) + window.hide() + window.queue_free() + window = null floating_btn.button_pressed = false floating_btn.visible = true pinned_btn.visible = false placement_opt.visible = true state = HIDDEN - update_dock(plugin.visible) # return window to side/bottom + update_dock() # return window to side/bottom -func update_dock(p_visible: bool) -> void: +func update_dock() -> void: + if not _initialized or window: + return + update_assets() - if not _initialized: - return - - if window: - return - elif floating_btn.button_pressed: - # No window, but floating button pressed, occurs when from editor settings - make_dock_float() - return + # Move dock to new destination remove_dock() - # Add dock to new destination # Sidebar if slot < POS_BOTTOM: state = SIDEBAR plugin.add_control_to_dock(slot, self) + # Bottom elif slot == POS_BOTTOM: state = BOTTOM plugin.add_control_to_bottom_panel(self, "Terrain3D") - if p_visible: - plugin.make_bottom_panel_item_visible(self) - + plugin.make_bottom_panel_item_visible(self) + func update_layout() -> void: if not _initialized: @@ -257,6 +243,8 @@ func update_thumbnails() -> void: _last_thumb_update_time = Time.get_ticks_msec() for mesh_asset in mesh_list.entries: mesh_asset.queue_redraw() + + ## Dock Button handlers @@ -282,7 +270,9 @@ func _on_textures_pressed() -> void: textures_btn.button_pressed = true meshes_btn.button_pressed = false texture_list.set_selected_id(texture_list.selected_id) - plugin.get_editor_interface().edit_node(plugin.terrain) + if plugin.is_terrain_valid(): + EditorInterface.edit_node(plugin.terrain) + save_editor_settings() func _on_meshes_pressed() -> void: @@ -293,14 +283,16 @@ func _on_meshes_pressed() -> void: meshes_btn.button_pressed = true textures_btn.button_pressed = false mesh_list.set_selected_id(mesh_list.selected_id) - plugin.get_editor_interface().edit_node(plugin.terrain) + if plugin.is_terrain_valid(): + EditorInterface.edit_node(plugin.terrain) update_thumbnails() + save_editor_settings() func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void: if p_tool == Terrain3DEditor.INSTANCER: _on_meshes_pressed() - elif p_tool == Terrain3DEditor.TEXTURE: + elif p_tool in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: _on_textures_pressed() @@ -320,24 +312,34 @@ func update_assets() -> void: _current_list.update_asset_list() + ## Window Management func make_dock_float() -> void: - # If already created (eg from editor Make Floating) + # If not already created (eg from editor panel 'Make Floating' button) if not window: remove_dock() create_window() state = WINDOWED + visible = true # Asset dock contents are hidden when popping out of the bottom! pinned_btn.visible = true floating_btn.visible = false placement_opt.visible = false window.title = "Terrain3D Asset Dock" window.always_on_top = pinned_btn.button_pressed window.close_requested.connect(remove_dock.bind(true)) - visible = true # Is hidden when pops off of bottom. ?? - _godot_editor_window.grab_focus() + window.window_input.connect(_on_window_input) + window.focus_exited.connect(save_editor_settings) + window.mouse_exited.connect(save_editor_settings) + window.size_changed.connect(save_editor_settings) + plugin.godot_editor_window.mouse_entered.connect(_on_godot_window_entered) + plugin.godot_editor_window.focus_entered.connect(_on_godot_focus_entered) + plugin.godot_editor_window.focus_exited.connect(_on_godot_focus_exited) + plugin.godot_editor_window.grab_focus() + update_assets() + save_editor_settings() func create_window() -> void: @@ -348,79 +350,80 @@ func create_window() -> void: mc.add_child(self) window.add_child(mc) window.set_transient(false) - window.set_size(get_setting(PS_DOCK_WINDOW_SIZE, Vector2i(512, 512))) - window.set_position(get_setting(PS_DOCK_WINDOW_POSITION, Vector2i(704, 284))) + window.set_size(plugin.get_setting(ES_DOCK_WINDOW_SIZE, Vector2i(512, 512))) + window.set_position(plugin.get_setting(ES_DOCK_WINDOW_POSITION, Vector2i(704, 284))) plugin.add_child(window) window.show() - window.window_input.connect(_on_window_input) - window.focus_exited.connect(_on_window_focus_exited) - _godot_editor_window.mouse_entered.connect(_on_godot_window_entered) - _godot_editor_window.focus_entered.connect(_on_godot_focus_entered) - _godot_editor_window.focus_exited.connect(_on_godot_focus_exited) + + +func clamp_window_position() -> void: + if window and window.visible: + var bounds: Vector2i + if EditorInterface.get_editor_settings().get_setting("interface/editor/single_window_mode"): + bounds = EditorInterface.get_base_control().size + else: + bounds = DisplayServer.screen_get_position(window.current_screen) + bounds += DisplayServer.screen_get_size(window.current_screen) + var margin: int = 40 + window.position.x = clamp(window.position.x, -window.size.x + 2*margin, bounds.x - margin) + window.position.y = clamp(window.position.y, 25, bounds.y - margin) func _on_window_input(event: InputEvent) -> void: - # Capture CTRL+S when doc focused to save scene) + # Capture CTRL+S when doc focused to save scene if event is InputEventKey and event.keycode == KEY_S and event.pressed and event.is_command_or_control_pressed(): save_editor_settings() - plugin.get_editor_interface().save_scene() - - -func _on_window_focus_exited() -> void: - # Capture window position w/o other changes - save_editor_settings() + EditorInterface.save_scene() func _on_godot_window_entered() -> void: if is_instance_valid(window) and window.has_focus(): - _godot_editor_window.grab_focus() + plugin.godot_editor_window.grab_focus() func _on_godot_focus_entered() -> void: # If asset dock is windowed, and Godot was minimized, and now is not, restore asset dock window if is_instance_valid(window): - if _godot_last_state == Window.MODE_MINIMIZED and _godot_editor_window.mode != Window.MODE_MINIMIZED: + if _godot_last_state == Window.MODE_MINIMIZED and plugin.godot_editor_window.mode != Window.MODE_MINIMIZED: window.show() - _godot_last_state = _godot_editor_window.mode - _godot_editor_window.grab_focus() + _godot_last_state = plugin.godot_editor_window.mode + plugin.godot_editor_window.grab_focus() func _on_godot_focus_exited() -> void: - if is_instance_valid(window) and _godot_editor_window.mode == Window.MODE_MINIMIZED: + if is_instance_valid(window) and plugin.godot_editor_window.mode == Window.MODE_MINIMIZED: window.hide() - _godot_last_state = _godot_editor_window.mode + _godot_last_state = plugin.godot_editor_window.mode ## Manage Editor Settings - -func get_setting(p_str: String, p_default: Variant) -> Variant: - if editor_settings.has_setting(p_str): - return editor_settings.get_setting(p_str) - else: - return p_default - - func load_editor_settings() -> void: - floating_btn.button_pressed = get_setting(PS_DOCK_FLOATING, false) - pinned_btn.button_pressed = get_setting(PS_DOCK_PINNED, true) - size_slider.value = get_setting(PS_DOCK_TILE_SIZE, 83) - set_slot(get_setting(PS_DOCK_SLOT, POS_BOTTOM)) + floating_btn.button_pressed = plugin.get_setting(ES_DOCK_FLOATING, false) + pinned_btn.button_pressed = plugin.get_setting(ES_DOCK_PINNED, true) + size_slider.value = plugin.get_setting(ES_DOCK_TILE_SIZE, 83) _on_slider_changed(size_slider.value) - # Window pos/size set on window creation in update_dock - update_dock(plugin.visible) - - + set_slot(plugin.get_setting(ES_DOCK_SLOT, POS_BOTTOM)) + if floating_btn.button_pressed: + make_dock_float() + # TODO Don't save tab until thumbnail generation more reliable + #if plugin.get_setting(ES_DOCK_TAB, 0) == 1: + # _on_meshes_pressed() + + func save_editor_settings() -> void: if not _initialized: return - editor_settings.set_setting(PS_DOCK_SLOT, slot) - editor_settings.set_setting(PS_DOCK_TILE_SIZE, size_slider.value) - editor_settings.set_setting(PS_DOCK_FLOATING, floating_btn.button_pressed) - editor_settings.set_setting(PS_DOCK_PINNED, pinned_btn.button_pressed) + clamp_window_position() + plugin.set_setting(ES_DOCK_SLOT, slot) + plugin.set_setting(ES_DOCK_TILE_SIZE, size_slider.value) + plugin.set_setting(ES_DOCK_FLOATING, floating_btn.button_pressed) + plugin.set_setting(ES_DOCK_PINNED, pinned_btn.button_pressed) + # TODO Don't save tab until thumbnail generation more reliable + # plugin.set_setting(ES_DOCK_TAB, 0 if _current_list == texture_list else 1) if window: - editor_settings.set_setting(PS_DOCK_WINDOW_SIZE, window.size) - editor_settings.set_setting(PS_DOCK_WINDOW_POSITION, window.position) + plugin.set_setting(ES_DOCK_WINDOW_SIZE, window.size) + plugin.set_setting(ES_DOCK_WINDOW_POSITION, window.position) ############################################################## @@ -527,7 +530,8 @@ class ListContainer extends Container: plugin.select_terrain() # Select Paint tool if clicking a texture - if type == Terrain3DAssets.TYPE_TEXTURE and plugin.editor.get_tool() != Terrain3DEditor.TEXTURE: + if type == Terrain3DAssets.TYPE_TEXTURE and \ + not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: var paint_btn: Button = plugin.ui.toolbar.get_node_or_null("PaintBaseTexture") if paint_btn: paint_btn.set_pressed(true) @@ -545,7 +549,7 @@ class ListContainer extends Container: func _on_resource_inspected(p_resource: Resource) -> void: await get_tree().create_timer(.01).timeout - plugin.get_editor_interface().edit_resource(p_resource) + EditorInterface.edit_resource(p_resource) func _on_resource_changed(p_resource: Resource, p_id: int) -> void: @@ -575,7 +579,7 @@ class ListContainer extends Container: # If removing an entry, clear inspector if not p_resource: - plugin.get_editor_interface().inspect_object(null) + EditorInterface.inspect_object(null) # If null resource, remove last if not p_resource: @@ -694,7 +698,7 @@ class ListEntry extends VBoxContainer: else: name_label.text = "Add Mesh" - + func _notification(p_what) -> void: match p_what: NOTIFICATION_DRAW: diff --git a/addons/terrain_3d/src/bake_lod_dialog.tscn b/addons/terrain_3d/src/bake_lod_dialog.tscn index 1011c72..d679712 100644 --- a/addons/terrain_3d/src/bake_lod_dialog.tscn +++ b/addons/terrain_3d/src/bake_lod_dialog.tscn @@ -5,37 +5,39 @@ [node name="bake_lod_dialog" type="ConfirmationDialog"] title = "Bake Terrain3D Mesh" position = Vector2i(0, 36) -size = Vector2i(400, 115) +size = Vector2i(400, 155) visible = true script = ExtResource("1_sf76d") -[node name="VBoxContainer" type="VBoxContainer" parent="."] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 +[node name="MarginContainer" type="MarginContainer" parent="."] offset_left = 8.0 offset_top = 8.0 -offset_right = -8.0 -offset_bottom = -49.0 -grow_horizontal = 2 -grow_vertical = 2 +offset_right = 392.0 +offset_bottom = 106.0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] layout_mode = 2 theme_override_constants/separation = 20 -[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"] layout_mode = 2 text = "LOD:" -[node name="LodBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"] +[node name="LodBox" type="SpinBox" parent="MarginContainer/VBoxContainer/HBoxContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 max_value = 8.0 value = 4.0 -[node name="DescriptionLabel" type="Label" parent="VBoxContainer"] +[node name="DescriptionLabel" type="Label" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 autowrap_mode = 2 diff --git a/addons/terrain_3d/src/baker.gd b/addons/terrain_3d/src/baker.gd index 0a53ba4..3ed9fa0 100644 --- a/addons/terrain_3d/src/baker.gd +++ b/addons/terrain_3d/src/baker.gd @@ -44,11 +44,14 @@ func bake_mesh_popup() -> void: if plugin.terrain: bake_method = _bake_mesh bake_lod_dialog.description = BAKE_MESH_DESCRIPTION - plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog) + EditorInterface.popup_dialog_centered(bake_lod_dialog) func _bake_mesh() -> void: - var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_NEAREST) + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions to bake") + return + var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DData.HEIGHT_FILTER_NEAREST) if !mesh: push_error("Failed to bake mesh from Terrain3D") return @@ -65,7 +68,7 @@ func _bake_mesh() -> void: undo.add_do_method(plugin.terrain, &"add_child", mesh_instance, true) undo.add_undo_method(plugin.terrain, &"remove_child", mesh_instance) - undo.add_do_property(mesh_instance, &"owner", plugin.terrain.owner) + undo.add_do_property(mesh_instance, &"owner", EditorInterface.get_edited_scene_root()) undo.add_do_reference(mesh_instance) else: @@ -86,11 +89,14 @@ func bake_occluder_popup() -> void: if plugin.terrain: bake_method = _bake_occluder bake_lod_dialog.description = BAKE_OCCLUDER_DESCRIPTION - plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog) + EditorInterface.popup_dialog_centered(bake_lod_dialog) func _bake_occluder() -> void: - var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_MINIMUM) + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions to bake") + return + var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DData.HEIGHT_FILTER_MINIMUM) if !mesh: push_error("Failed to bake mesh from Terrain3D") return @@ -113,7 +119,7 @@ func _bake_occluder() -> void: undo.add_do_method(plugin.terrain, &"add_child", occluder_instance, true) undo.add_undo_method(plugin.terrain, &"remove_child", occluder_instance) - undo.add_do_property(occluder_instance, &"owner", plugin.terrain.owner) + undo.add_do_property(occluder_instance, &"owner", EditorInterface.get_edited_scene_root()) undo.add_do_reference(occluder_instance) else: @@ -153,7 +159,7 @@ func find_nav_region_terrains(p_nav_region: NavigationRegion3D) -> Array[Terrain func find_terrain_nav_regions(p_terrain: Terrain3D) -> Array[NavigationRegion3D]: var result: Array[NavigationRegion3D] = [] - var root: Node = plugin.get_editor_interface().get_edited_scene_root() + var root: Node = EditorInterface.get_edited_scene_root() if not root: return result for nav_region in root.find_children("", "NavigationRegion3D", true, true): @@ -169,6 +175,9 @@ func bake_nav_mesh() -> void: print("Terrain3DNavigation: Finished baking 1 NavigationMesh.") elif plugin.terrain: + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions to bake") + return # 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 @@ -329,11 +338,17 @@ 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) + EditorInterface.popup_dialog_centered(confirm_dialog) func _set_up_navigation() -> void: assert(plugin.terrain) + if plugin.terrain == EditorInterface.get_edited_scene_root(): + push_error("Terrain3D Navigation setup not possible if Terrain3D node is scene root") + return + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions") + return var terrain: Terrain3D = plugin.terrain var nav_region := NavigationRegion3D.new() @@ -348,7 +363,7 @@ func _set_up_navigation() -> void: undo_redo.add_do_reference(nav_region) undo_redo.commit_action() - plugin.get_editor_interface().inspect_object(nav_region) + EditorInterface.inspect_object(nav_region) assert(plugin.nav_region == nav_region) bake_nav_mesh() diff --git a/addons/terrain_3d/src/channel_packer.gd b/addons/terrain_3d/src/channel_packer.gd index 47c814d..41d5910 100644 --- a/addons/terrain_3d/src/channel_packer.gd +++ b/addons/terrain_3d/src/channel_packer.gd @@ -1,21 +1,38 @@ -extends Object +extends RefCounted const WINDOW_SCENE: String = "res://addons/terrain_3d/src/channel_packer.tscn" const TEMPLATE_PATH: String = "res://addons/terrain_3d/src/channel_packer_import_template.txt" - +const DRAG_DROP_SCRIPT: String = "res://addons/terrain_3d/src/channel_packer_dragdrop.gd" enum { - IMAGE_ALBEDO, - IMAGE_HEIGHT, - IMAGE_NORMAL, - IMAGE_ROUGHNESS, + INFO, + WARN, + ERROR, +} + +enum { + IMAGE_ALBEDO, + IMAGE_HEIGHT, + IMAGE_NORMAL, + IMAGE_ROUGHNESS } var plugin: EditorPlugin -var editor_interface: EditorInterface -var dialog: AcceptDialog -var save_file_dialog: FileDialog -var open_file_dialog: FileDialog +var window: Window +var save_file_dialog: EditorFileDialog +var open_file_dialog: EditorFileDialog var invert_green_checkbox: CheckBox +var invert_smooth_checkbox: CheckBox +var invert_height_checkbox: CheckBox +var lumin_height_button: Button +var generate_mipmaps_checkbox: CheckBox +var high_quality_checkbox: CheckBox +var align_normals_checkbox: CheckBox +var resize_toggle_checkbox: CheckBox +var resize_option_box: SpinBox +var height_channel: Array[Button] +var height_channel_selected: int = 0 +var roughness_channel: Array[Button] +var roughness_channel_selected: int = 0 var last_opened_directory: String var last_saved_directory: String var packing_albedo: bool = false @@ -24,133 +41,283 @@ var images: Array[Image] = [null, null, null, null] var status_label: Label var no_op: Callable = func(): pass var last_file_selected_fn: Callable = no_op +var normal_vector: Vector3 func pack_textures_popup() -> void: - if dialog != null: - print("Terrain3DChannelPacker: Cannot open pack tool, dialog already open.") + if window != null: + window.show() + window.move_to_foreground() + window.move_to_center() return - - dialog = (load(WINDOW_SCENE) as PackedScene).instantiate() - dialog.confirmed.connect(_on_close_requested) - dialog.canceled.connect(_on_close_requested) - status_label = dialog.find_child("StatusLabel") - invert_green_checkbox = dialog.find_child("InvertGreenChannelCheckBox") - - editor_interface = plugin.get_editor_interface() - _init_file_dialogs() - editor_interface.popup_dialog_centered(dialog) - - _init_texture_picker(dialog.find_child("AlbedoVBox"), IMAGE_ALBEDO) - _init_texture_picker(dialog.find_child("HeightVBox"), IMAGE_HEIGHT) - _init_texture_picker(dialog.find_child("NormalVBox"), IMAGE_NORMAL) - _init_texture_picker(dialog.find_child("RoughnessVBox"), IMAGE_ROUGHNESS) + window = (load(WINDOW_SCENE) as PackedScene).instantiate() + window.close_requested.connect(_on_close_requested) + window.window_input.connect(func(event:InputEvent): + if event is InputEventKey: + if event.pressed and event.keycode == KEY_ESCAPE: + _on_close_requested() + ) + window.find_child("CloseButton").pressed.connect(_on_close_requested) + + status_label = window.find_child("StatusLabel") as Label + invert_green_checkbox = window.find_child("InvertGreenChannelCheckBox") as CheckBox + invert_smooth_checkbox = window.find_child("InvertSmoothCheckBox") as CheckBox + invert_height_checkbox = window.find_child("ConvertDepthToHeight") as CheckBox + lumin_height_button = window.find_child("LuminanceAsHeightButton") as Button + generate_mipmaps_checkbox = window.find_child("GenerateMipmapsCheckBox") as CheckBox + high_quality_checkbox = window.find_child("HighQualityCheckBox") as CheckBox + align_normals_checkbox = window.find_child("AlignNormalsCheckBox") as CheckBox + resize_toggle_checkbox = window.find_child("ResizeToggle") as CheckBox + resize_option_box = window.find_child("ResizeOptionButton") as SpinBox + height_channel = [ + window.find_child("HeightChannelR") as Button, + window.find_child("HeightChannelG") as Button, + window.find_child("HeightChannelB") as Button, + window.find_child("HeightChannelA") as Button + ] + roughness_channel = [ + window.find_child("RoughnessChannelR") as Button, + window.find_child("RoughnessChannelG") as Button, + window.find_child("RoughnessChannelB") as Button, + window.find_child("RoughnessChannelA") as Button + ] + + height_channel[0].pressed.connect(func() -> void: height_channel_selected = 0) + height_channel[1].pressed.connect(func() -> void: height_channel_selected = 1) + height_channel[2].pressed.connect(func() -> void: height_channel_selected = 2) + height_channel[3].pressed.connect(func() -> void: height_channel_selected = 3) + + roughness_channel[0].pressed.connect(func() -> void: roughness_channel_selected = 0) + roughness_channel[1].pressed.connect(func() -> void: roughness_channel_selected = 1) + roughness_channel[2].pressed.connect(func() -> void: roughness_channel_selected = 2) + roughness_channel[3].pressed.connect(func() -> void: roughness_channel_selected = 3) + + plugin.add_child(window) + _init_file_dialogs() + + # the dialog disables the parent window "on top" so, restore it after 1 frame to alow the dialog to clear. + var set_on_top_fn: Callable = func(_file: String = "") -> void: + await RenderingServer.frame_post_draw + window.always_on_top = true + save_file_dialog.file_selected.connect(set_on_top_fn) + save_file_dialog.canceled.connect(set_on_top_fn) + open_file_dialog.file_selected.connect(set_on_top_fn) + open_file_dialog.canceled.connect(set_on_top_fn) + + _init_texture_picker(window.find_child("AlbedoVBox"), IMAGE_ALBEDO) + _init_texture_picker(window.find_child("HeightVBox"), IMAGE_HEIGHT) + _init_texture_picker(window.find_child("NormalVBox"), IMAGE_NORMAL) + _init_texture_picker(window.find_child("RoughnessVBox"), IMAGE_ROUGHNESS) + var pack_button_path: String = "Panel/MarginContainer/VBoxContainer/PackButton" - (dialog.get_node(pack_button_path) as Button).pressed.connect(_on_pack_button_pressed) + (window.get_node(pack_button_path) as Button).pressed.connect(_on_pack_button_pressed) func _on_close_requested() -> void: last_file_selected_fn = no_op images = [null, null, null, null] - dialog.queue_free() - dialog = null + window.queue_free() + window = null func _init_file_dialogs() -> void: - save_file_dialog = FileDialog.new() + save_file_dialog = EditorFileDialog.new() save_file_dialog.set_filters(PackedStringArray(["*.png"])) - save_file_dialog.set_file_mode(FileDialog.FILE_MODE_SAVE_FILE) - save_file_dialog.access = FileDialog.ACCESS_FILESYSTEM + save_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_SAVE_FILE) + save_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM save_file_dialog.file_selected.connect(_on_save_file_selected) - - open_file_dialog = FileDialog.new() - open_file_dialog.set_filters(PackedStringArray(["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", ".ktx"])) - open_file_dialog.set_file_mode(FileDialog.FILE_MODE_OPEN_FILE) - open_file_dialog.access = FileDialog.ACCESS_FILESYSTEM - - dialog.add_child(save_file_dialog) - dialog.add_child(open_file_dialog) + save_file_dialog.ok_button_text = "Save" + save_file_dialog.size = Vector2i(550, 550) + #save_file_dialog.transient = false + #save_file_dialog.exclusive = false + #save_file_dialog.popup_window = true + + open_file_dialog = EditorFileDialog.new() + open_file_dialog.set_filters(PackedStringArray( + ["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", "*.ktx", "*.dds"])) + open_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_OPEN_FILE) + open_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM + open_file_dialog.ok_button_text = "Open" + open_file_dialog.size = Vector2i(550, 550) + #open_file_dialog.transient = false + #open_file_dialog.exclusive = false + #open_file_dialog.popup_window = true + + window.add_child(save_file_dialog) + window.add_child(open_file_dialog) func _init_texture_picker(p_parent: Node, p_image_index: int) -> void: - var line_edit: LineEdit = p_parent.find_child("LineEdit") - var file_pick_button: Button = p_parent.find_child("PickButton") - var clear_button: Button = p_parent.find_child("ClearButton") - var texture_rect: TextureRect = p_parent.find_child("TextureRect") - var texture_button: Button = p_parent.find_child("TextureButton") - + var line_edit: LineEdit = p_parent.find_child("LineEdit") as LineEdit + var file_pick_button: Button = p_parent.find_child("PickButton") as Button + var clear_button: Button = p_parent.find_child("ClearButton") as Button + var texture_rect: TextureRect = p_parent.find_child("TextureRect") as TextureRect + var texture_button: Button = p_parent.find_child("TextureButton") as Button + texture_button.set_script(load(DRAG_DROP_SCRIPT) as GDScript) + + var set_channel_fn: Callable = func(used_channels: int) -> void: + var channel_count: int = 4 + # enum Image.UsedChannels + match used_channels: + Image.USED_CHANNELS_L, Image.USED_CHANNELS_R: channel_count = 1 + Image.USED_CHANNELS_LA, Image.USED_CHANNELS_RG: channel_count = 2 + Image.USED_CHANNELS_RGB: channel_count = 3 + Image.USED_CHANNELS_RGBA: channel_count = 4 + if p_image_index == IMAGE_HEIGHT: + for i in 4: + height_channel[i].visible = i < channel_count + height_channel[0].button_pressed = true + height_channel[0].pressed.emit() + elif p_image_index == IMAGE_ROUGHNESS: + for i in 4: + roughness_channel[i].visible = i < channel_count + roughness_channel[0].button_pressed = true + roughness_channel[0].pressed.emit() + + var load_image_fn: Callable = func(path: String): + var image: Image = Image.new() + var error: int = OK + # Special case for dds files + if path.get_extension() == "dds": + image = ResourceLoader.load(path).get_image() + if not image.is_empty(): + # if the dds file is loaded, we must clear any mipmaps and + # decompress if needed in order to do per pixel operations. + image.clear_mipmaps() + image.decompress() + else: + error = FAILED + else: + error = image.load(path) + if error != OK: + _show_message(ERROR, "Failed to load texture '" + path + "'") + texture_rect.texture = null + images[p_image_index] = null + else: + _show_message(INFO, "Loaded texture '" + path + "'") + texture_rect.texture = ImageTexture.create_from_image(image) + images[p_image_index] = image + _set_wh_labels(p_image_index, image.get_width(), image.get_height()) + if p_image_index == IMAGE_NORMAL: + _set_normal_vector(image) + if p_image_index == IMAGE_HEIGHT or p_image_index == IMAGE_ROUGHNESS: + set_channel_fn.call(image.detect_used_channels()) + + var os_drop_fn: Callable = func(files: PackedStringArray) -> void: + # OS drag drop holds mouse focus until released, + # Get mouse pos and check directly if inside texture_rect + var rect = texture_button.get_global_rect() + var mouse_position = texture_button.get_global_mouse_position() + if rect.has_point(mouse_position): + if files.size() != 1: + _show_message(ERROR, "Cannot load multiple files") + else: + line_edit.text = files[0] + load_image_fn.call(files[0]) + + var godot_drop_fn: Callable = func(path: String) -> void: + path = ProjectSettings.globalize_path(path) + line_edit.text = path + load_image_fn.call(path) + var open_fn: Callable = func() -> void: open_file_dialog.current_path = last_opened_directory if last_file_selected_fn != no_op: open_file_dialog.file_selected.disconnect(last_file_selected_fn) last_file_selected_fn = func(path: String) -> void: line_edit.text = path - line_edit.caret_column = path.length() - last_opened_directory = path.get_base_dir() + "/" - var image: Image = Image.new() - var code: int = image.load(path) - if code != OK: - _show_error("Failed to load texture '" + path + "'") - texture_rect.texture = null - images[p_image_index] = null - else: - _show_success("Loaded texture '" + path + "'") - texture_rect.texture = ImageTexture.create_from_image(image) - images[p_image_index] = image + load_image_fn.call(path) open_file_dialog.file_selected.connect(last_file_selected_fn) open_file_dialog.popup_centered_ratio() - + + var line_edit_submit_fn: Callable = func(path: String) -> void: + line_edit.text = path + load_image_fn.call(path) + var clear_fn: Callable = func() -> void: line_edit.text = "" texture_rect.texture = null images[p_image_index] = null - - # allow user to edit textbox and press enter because Godot's file picker doesn't work 100% of the time - var line_edit_submit_fn: Callable = func(path: String) -> void: - var image: Image = Image.new() - var code: int = image.load(path) - if code != OK: - _show_error("Failed to load texture '" + path + "'") - texture_rect.texture = null - images[p_image_index] = null - else: - texture_rect.texture = ImageTexture.create_from_image(image) - images[p_image_index] = image - + _set_wh_labels(p_image_index, -1, -1) + line_edit.text_submitted.connect(line_edit_submit_fn) file_pick_button.pressed.connect(open_fn) texture_button.pressed.connect(open_fn) clear_button.pressed.connect(clear_fn) - _set_button_icon(file_pick_button, "Folder") - _set_button_icon(clear_button, "Remove") + texture_button.dropped.connect(godot_drop_fn) + window.files_dropped.connect(os_drop_fn) + + if p_image_index == IMAGE_HEIGHT: + var lumin_fn: Callable = func() -> void: + if !images[IMAGE_ALBEDO]: + _show_message(ERROR, "Albedo Image Required for Operation") + else: + line_edit.text = "Generated Height" + var height_texture: Image = Terrain3DUtil.luminance_to_height(images[IMAGE_ALBEDO]) + if height_texture.is_empty(): + _show_message(ERROR, "Height Texture Generation error") + # blur the image by resizing down and back.. + var w: int = height_texture.get_width() + var h: int = height_texture.get_height() + height_texture.resize(w / 4, h / 4) + height_texture.resize(w, h, Image.INTERPOLATE_CUBIC) + # "Load" the height texture + images[IMAGE_HEIGHT] = height_texture + texture_rect.texture = ImageTexture.create_from_image(images[IMAGE_HEIGHT]) + _set_wh_labels(IMAGE_HEIGHT, height_texture.get_width(), height_texture.get_height()) + set_channel_fn.call(Image.USED_CHANNELS_R) + _show_message(INFO, "Height Texture generated sucsessfully") + lumin_height_button.pressed.connect(lumin_fn) + plugin.ui.set_button_editor_icon(file_pick_button, "Folder") + plugin.ui.set_button_editor_icon(clear_button, "Remove") -func _set_button_icon(p_button: Button, p_icon_name: String) -> void: - var editor_base: Control = editor_interface.get_base_control() - var icon: Texture2D = editor_base.get_theme_icon(p_icon_name, "EditorIcons") - p_button.icon = icon +func _set_wh_labels(p_image_index: int, width: int, height: int) -> void: + var w: String = "" + var h: String = "" + if width > 0 and height > 0: + w = "w: " + str(width) + h = "h: " + str(height) + match p_image_index: + 0: + window.find_child("AlbedoW").text = w + window.find_child("AlbedoH").text = h + 1: + window.find_child("HeightW").text = w + window.find_child("HeightH").text = h + 2: + window.find_child("NormalW").text = w + window.find_child("NormalH").text = h + 3: + window.find_child("RoughnessW").text = w + window.find_child("RoughnessH").text = h -func _show_error(p_text: String) -> void: - push_error("Terrain3DChannelPacker: " + p_text) +func _show_message(p_level: int, p_text: String) -> void: status_label.text = p_text - status_label.add_theme_color_override("font_color", Color(0.9, 0, 0)) - - -func _show_success(p_text: String) -> void: - print("Terrain3DChannelPacker: " + p_text) - status_label.text = p_text - status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14)) + match p_level: + INFO: + print("Terrain3DChannelPacker: " + p_text) + status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14)) + WARN: + push_warning("Terrain3DChannelPacker: " + p_text) + status_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0)) + ERROR,_: + push_error("Terrain3DChannelPacker: " + p_text) + status_label.add_theme_color_override("font_color", Color(0.9, 0, 0)) func _create_import_file(png_path: String) -> void: var dst_import_path: String = png_path + ".import" - var file: FileAccess = FileAccess.open(TEMPLATE_PATH, FileAccess.READ) var template_content: String = file.get_as_text() file.close() - - var import_content: String = template_content.replace("$SOURCE_FILE", png_path) + template_content = template_content.replace( + "$SOURCE_FILE", png_path).replace( + "$HIGH_QUALITY", str(high_quality_checkbox.button_pressed)).replace( + "$GENERATE_MIPMAPS", str(generate_mipmaps_checkbox.button_pressed) + ) + var import_content: String = template_content file = FileAccess.open(dst_import_path, FileAccess.WRITE) file.store_string(import_content) file.close() @@ -159,11 +326,10 @@ func _create_import_file(png_path: String) -> void: func _on_pack_button_pressed() -> void: packing_albedo = images[IMAGE_ALBEDO] != null and images[IMAGE_HEIGHT] != null var packing_normal_roughness: bool = images[IMAGE_NORMAL] != null and images[IMAGE_ROUGHNESS] != null - + if not packing_albedo and not packing_normal_roughness: - _show_error("Please select an albedo and height texture or a normal and roughness texture.") + _show_message(WARN, "Please select an albedo and height texture or a normal and roughness texture") return - if packing_albedo: save_file_dialog.current_path = last_saved_directory + "packed_albedo_height" save_file_dialog.title = "Save Packed Albedo/Height Texture" @@ -179,34 +345,119 @@ func _on_pack_button_pressed() -> void: func _on_save_file_selected(p_dst_path) -> void: last_saved_directory = p_dst_path.get_base_dir() + "/" + var error: int if packing_albedo: - _pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false) + error = _pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false, + invert_height_checkbox.button_pressed, false, height_channel_selected) else: - _pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path, invert_green_checkbox.button_pressed) + error = _pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path, + invert_green_checkbox.button_pressed, invert_smooth_checkbox.button_pressed, + align_normals_checkbox.button_pressed, roughness_channel_selected) + + if error == OK: + EditorInterface.get_resource_filesystem().scan() + if window.visible: + window.hide() + await EditorInterface.get_resource_filesystem().resources_reimported + # wait 1 extra frame, to ensure the UI is responsive. + await RenderingServer.frame_post_draw + window.show() if queue_pack_normal_roughness: queue_pack_normal_roughness = false packing_albedo = false save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness" save_file_dialog.title = "Save Packed Normal/Roughness Texture" + save_file_dialog.call_deferred("popup_centered_ratio") + save_file_dialog.call_deferred("move_to_foreground") -func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool) -> void: +func _alignment_basis(normal: Vector3) -> Basis: + var up: Vector3 = Vector3(0, 0, 1) + var v: Vector3 = normal.cross(up) + var c: float = normal.dot(up) + var k: float = 1.0 / (1.0 + c) + + var vxy: float = v.x * v.y * k + var vxz: float = v.x * v.z * k + var vyz: float = v.y * v.z * k + + return Basis(Vector3(v.x * v.x * k + c, vxy - v.z, vxz + v.y), + Vector3(vxy + v.z, v.y * v.y * k + c, vyz - v.x), + Vector3(vxz - v.y, vyz + v.x, v.z * v.z * k + c) + ) + + +func _set_normal_vector(source: Image, quiet: bool = false) -> void: + # Calculate texture normal sum direction + var normal: Image = source + var sum: Color = Color(0.0, 0.0, 0.0, 0.0) + for x in normal.get_height(): + for y in normal.get_width(): + sum += normal.get_pixel(x, y) + var div: float = normal.get_height() * normal.get_width() + sum /= Color(div, div, div) + sum *= 2.0 + sum -= Color(1.0, 1.0, 1.0) + normal_vector = Vector3(sum.r, sum.g, sum.b).normalized() + if normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999 && !quiet: + _show_message(WARN, "Normal Texture Not Orthoganol to UV plane.\nFor Compatability with Detiling and Rotation, Select Orthoganolize Normals") + + +func _align_normals(source: Image, iteration: int = 0) -> void: + # generate matrix to re-align the normalmap + var mat3: Basis = _alignment_basis(normal_vector) + # re-align the normal map pixels + for x in source.get_height(): + for y in source.get_width(): + var old_pixel: Color = source.get_pixel(x, y) + var vector_pixel: Vector3 = Vector3(old_pixel.r, old_pixel.g, old_pixel.b) + vector_pixel *= 2.0 + vector_pixel -= Vector3.ONE + vector_pixel = vector_pixel.normalized() + vector_pixel = vector_pixel * mat3 + vector_pixel += Vector3.ONE + vector_pixel *= 0.5 + var new_pixel: Color = Color(vector_pixel.x, vector_pixel.y, vector_pixel.z, old_pixel.a) + source.set_pixel(x, y, new_pixel) + _set_normal_vector(source, true) + if normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999 && iteration < 3: + ++iteration + _align_normals(source, iteration) + + +func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool, + p_invert_smooth: bool, p_align_normals : bool, p_alpha_channel: int) -> Error: if p_rgb_image and p_a_image: - if p_rgb_image.get_size() != p_a_image.get_size(): - _show_error("Textures must be the same size.") - return - - var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image, p_invert_green) - + if p_rgb_image.get_size() != p_a_image.get_size() and !resize_toggle_checkbox.button_pressed: + _show_message(ERROR, "Textures must be the same size.\nEnable resize to override image dimensions") + return FAILED + + if resize_toggle_checkbox.button_pressed: + var size: int = max(128, resize_option_box.value) + p_rgb_image.resize(size, size, Image.INTERPOLATE_CUBIC) + p_a_image.resize(size, size, Image.INTERPOLATE_CUBIC) + + if p_align_normals and normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999: + _align_normals(p_rgb_image) + elif p_align_normals: + _show_message(INFO, "Alignment OK, skipping Normal Orthogonalization") + + var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image, + p_invert_green, p_invert_smooth, p_alpha_channel) + if not output_image: - _show_error("Failed to pack textures.") - return - + _show_message(ERROR, "Failed to pack textures") + return FAILED + if output_image.detect_used_channels() != 5: + _show_message(ERROR, "Packing Error, Alpha Channel empty") + return FAILED + output_image.save_png(p_dst_path) - editor_interface.get_resource_filesystem().scan_sources() _create_import_file(p_dst_path) - _show_success("Packed to " + p_dst_path + ".") + _show_message(INFO, "Packed to " + p_dst_path + ".") + return OK else: - _show_error("Failed to load one or more textures.") + _show_message(ERROR, "Failed to load one or more textures") + return FAILED diff --git a/addons/terrain_3d/src/channel_packer.tscn b/addons/terrain_3d/src/channel_packer.tscn index ebd2f0c..e39866f 100644 --- a/addons/terrain_3d/src/channel_packer.tscn +++ b/addons/terrain_3d/src/channel_packer.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=5 format=3 uid="uid://nud6dwjcnj5v"] +[gd_scene load_steps=7 format=3 uid="uid://nud6dwjcnj5v"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ysabf"] bg_color = Color(0.211765, 0.239216, 0.290196, 1) @@ -30,21 +30,21 @@ corner_radius_bottom_left = 5 [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"] -[node name="AcceptDialog" type="AcceptDialog"] +[sub_resource type="ButtonGroup" id="ButtonGroup_wnxik"] + +[sub_resource type="ButtonGroup" id="ButtonGroup_bs6ki"] + +[node name="Window" type="Window"] title = "Terrain3D Channel Packer" initial_position = 1 -size = Vector2i(660, 900) -visible = true -ok_button_text = "Close" +size = Vector2i(680, 835) +unresizable = true +always_on_top = true [node name="Panel" type="Panel" parent="."] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 8.0 -offset_top = 8.0 -offset_right = -8.0 -offset_bottom = -49.0 grow_horizontal = 2 grow_vertical = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf") @@ -54,10 +54,10 @@ layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 4.0 -offset_top = 4.0 -offset_right = -1.0 -offset_bottom = -53.0 +offset_left = 5.0 +offset_top = 5.0 +offset_right = -5.0 +offset_bottom = 5.0 grow_horizontal = 2 grow_vertical = 2 theme_override_constants/margin_left = 5 @@ -67,12 +67,12 @@ theme_override_constants/margin_bottom = 5 [node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"] layout_mode = 2 -size_flags_vertical = 0 theme_override_constants/separation = 10 [node name="AlbedoHeightPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"] -custom_minimum_size = Vector2(0, 250) +custom_minimum_size = Vector2(0, 290) layout_mode = 2 +mouse_filter = 1 theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna") [node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel"] @@ -147,6 +147,27 @@ grow_horizontal = 2 grow_vertical = 2 theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") +[node name="AlbedoWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"] +layout_mode = 2 +alignment = 1 + +[node name="AlbedoW" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="AlbedoH" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"] +layout_mode = 2 +alignment = 1 + +[node name="LuminanceAsHeightButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/HBoxContainer2"] +layout_mode = 2 +text = " Generate Height from Luminance" +icon_alignment = 2 + [node name="HeightVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 @@ -204,8 +225,63 @@ grow_horizontal = 2 grow_vertical = 2 theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") +[node name="HeightWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +alignment = 1 + +[node name="HeightW" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HeightH" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +alignment = 1 + +[node name="ConvertDepthToHeight" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer2"] +layout_mode = 2 +text = " Convert Depth to Height" +icon_alignment = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +alignment = 1 + +[node name="HeightChannelLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +text = " Source Channel: " +horizontal_alignment = 2 + +[node name="HeightChannelR" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_pressed = true +button_group = SubResource("ButtonGroup_wnxik") +text = "R" + +[node name="HeightChannelB" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_wnxik") +text = "G" + +[node name="HeightChannelG" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_wnxik") +text = "B" + +[node name="HeightChannelA" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_wnxik") +text = "A" + [node name="NormalRoughnessPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"] -custom_minimum_size = Vector2(0, 280) +custom_minimum_size = Vector2(0, 290) layout_mode = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna") @@ -281,9 +357,33 @@ grow_horizontal = 2 grow_vertical = 2 theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") -[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +[node name="NormalWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] layout_mode = 2 -text = "Convert DirectX to OpenGL" +alignment = 1 + +[node name="NormalW" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="NormalH" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 +alignment = 1 + +[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer"] +layout_mode = 2 +text = " Convert DirectX to OpenGL" + +[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 +alignment = 1 + +[node name="AlignNormalsCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer2"] +layout_mode = 2 +text = " Orthoganolise Normals" [node name="RoughnessVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"] layout_mode = 2 @@ -342,18 +442,112 @@ grow_horizontal = 2 grow_vertical = 2 theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") -[node name="NormalRoughnessHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"] +[node name="RoughnessWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] layout_mode = 2 +alignment = 1 + +[node name="RoughnessW" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="RoughnessH" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 +alignment = 1 + +[node name="InvertSmoothCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer2"] +layout_mode = 2 +text = " Convert Smoothness to Roughness" + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 +alignment = 1 + +[node name="RoughnessChannelLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +text = " Source Channel: " +horizontal_alignment = 2 + +[node name="RoughnessChannelR" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_pressed = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "R" + +[node name="RoughnessChannelG" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "G" + +[node name="RoughnessChannelB" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "B" + +[node name="RoughnessChannelA" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "A" + +[node name="GeneralOptionsLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "General Options" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="GeneralOptionsHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 35) +layout_mode = 2 +alignment = 1 + +[node name="ResizeToggle" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 +text = " Resize Packed Image" + +[node name="ResizeOptionButton" type="SpinBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +visible = false +layout_mode = 2 +tooltip_text = "A value of 0 disables resizing." +min_value = 128.0 +max_value = 4096.0 +step = 128.0 +value = 1024.0 + +[node name="VSeparator" type="VSeparator" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 + +[node name="GenerateMipmapsCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 +button_pressed = true +text = "Generate Mipmaps" + +[node name="HighQualityCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 +text = "Import High Quality" [node name="PackButton" type="Button" parent="Panel/MarginContainer/VBoxContainer"] layout_mode = 2 text = "Pack textures as..." [node name="StatusLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"] -custom_minimum_size = Vector2(0, 10) +custom_minimum_size = Vector2(0, 60) layout_mode = 2 -theme_override_colors/font_color = Color(1, 1, 1, 1) -text = "Use this to create a packed Albedo + Height texture and/or a packed Normal + Roughness texture. +horizontal_alignment = 1 +autowrap_mode = 3 -You can then use these textures with Terrain3D." -autowrap_mode = 2 +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="CloseButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Close" + +[connection signal="toggled" from="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeToggle" to="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeOptionButton" method="set_visible"] diff --git a/addons/terrain_3d/src/channel_packer_dragdrop.gd b/addons/terrain_3d/src/channel_packer_dragdrop.gd new file mode 100644 index 0000000..26c5a98 --- /dev/null +++ b/addons/terrain_3d/src/channel_packer_dragdrop.gd @@ -0,0 +1,15 @@ +@tool +extends Button + +signal dropped + +func _can_drop_data(p_position, p_data) -> bool: + if typeof(p_data) == TYPE_DICTIONARY: + if p_data.files.size() == 1: + match p_data.files[0].get_extension(): + "png", "bmp", "exr", "hdr", "jpg", "jpeg", "tga", "svg", "webp", "ktx", "dds": + return true + return false + +func _drop_data(p_position, p_data) -> void: + dropped.emit(p_data.files[0]) diff --git a/addons/terrain_3d/src/channel_packer_import_template.txt b/addons/terrain_3d/src/channel_packer_import_template.txt index e5da5eb..55003e3 100644 --- a/addons/terrain_3d/src/channel_packer_import_template.txt +++ b/addons/terrain_3d/src/channel_packer_import_template.txt @@ -14,12 +14,12 @@ source_file="$SOURCE_FILE" [params] compress/mode=2 -compress/high_quality=false +compress/high_quality=$HIGH_QUALITY compress/lossy_quality=0.7 compress/hdr_compression=1 compress/normal_map=2 compress/channel_pack=0 -mipmaps/generate=true +mipmaps/generate=$GENERATE_MIPMAPS mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" @@ -29,4 +29,4 @@ process/normal_map_invert_y=false process/hdr_as_srgb=false process/hdr_clamp_exposure=false process/size_limit=0 -detect_3d/compress_to=1 \ No newline at end of file +detect_3d/compress_to=1 diff --git a/addons/terrain_3d/src/directory_setup.gd b/addons/terrain_3d/src/directory_setup.gd new file mode 100644 index 0000000..00d20af --- /dev/null +++ b/addons/terrain_3d/src/directory_setup.gd @@ -0,0 +1,116 @@ +extends Node + +const DIRECTORY_SETUP: String = "res://addons/terrain_3d/src/directory_setup.tscn" + +var plugin: EditorPlugin +var dialog: ConfirmationDialog +var select_dir_btn: Button +var selected_dir_le: LineEdit +var select_upg_btn: Button +var upgrade_file_le: LineEdit +var editor_file_dialog: EditorFileDialog + + +func _init() -> void: + editor_file_dialog = EditorFileDialog.new() + editor_file_dialog.set_filters(PackedStringArray(["*.res"])) + editor_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_SAVE_FILE) + editor_file_dialog.access = EditorFileDialog.ACCESS_RESOURCES + editor_file_dialog.ok_button_text = "Open" + editor_file_dialog.title = "Open a folder or file" + editor_file_dialog.file_selected.connect(_on_file_selected) + editor_file_dialog.dir_selected.connect(_on_dir_selected) + editor_file_dialog.size = Vector2i(850, 550) + editor_file_dialog.transient = false + editor_file_dialog.exclusive = false + editor_file_dialog.popup_window = true + add_child(editor_file_dialog) + + +func directory_setup_popup() -> void: + dialog = load(DIRECTORY_SETUP).instantiate() + dialog.hide() + + # Nodes + select_dir_btn = dialog.get_node("Margin/VBox/DirHBox/SelectDir") + selected_dir_le = dialog.get_node("Margin/VBox/DirHBox/LineEdit") + select_upg_btn = dialog.get_node("Margin/VBox/UpgradeHBox/SelectResFile") + upgrade_file_le = dialog.get_node("Margin/VBox/UpgradeHBox/LineEdit") + upgrade_file_le.text = "" + + if plugin.terrain.data_directory: + selected_dir_le.text = plugin.terrain.data_directory + + if plugin.terrain.storage: + upgrade_file_le.text = plugin.terrain.storage.get_path() + + # Icons + plugin.ui.set_button_editor_icon(select_upg_btn, "Folder") + plugin.ui.set_button_editor_icon(select_dir_btn, "Folder") + + #Signals + select_upg_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_FILE)) + select_dir_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_DIR)) + dialog.confirmed.connect(_on_close_requested) + dialog.canceled.connect(_on_close_requested) + dialog.get_ok_button().pressed.connect(_on_ok_pressed) + + # Popup + EditorInterface.popup_dialog_centered(dialog) + + +func _on_close_requested() -> void: + dialog.queue_free() + dialog = null + + +func _on_select_file_pressed(file_mode: EditorFileDialog.FileMode) -> void: + editor_file_dialog.file_mode = file_mode + editor_file_dialog.popup_centered() + + +func _on_dir_selected(path: String) -> void: + selected_dir_le.text = path + + +func _on_file_selected(path: String) -> void: + upgrade_file_le.text = path + + +func _on_ok_pressed() -> void: + if not plugin.terrain: + push_error("Not connected terrain. Click the Terrain3D node first") + return + if selected_dir_le.text.is_empty(): + push_error("No data directory specified") + return + if not DirAccess.dir_exists_absolute(selected_dir_le.text): + push_error("Directory doesn't exist: ", selected_dir_le.text) + return + # Check if directory empty of terrain files + var data_found: bool = false + var files: Array = DirAccess.get_files_at(selected_dir_le.text) + for file in files: + if file.begins_with("terrain3d") || file.ends_with(".res"): + data_found = true + break + + print("Setting terrain directory: ", selected_dir_le.text) + plugin.terrain.data_directory = selected_dir_le.text + + if not upgrade_file_le.text.is_empty(): + if data_found: + push_warning("Target directory already has terrain data. Specify an empty directory to upgrade") + return + if not FileAccess.file_exists(upgrade_file_le.text): + push_error("File doesn't exist: ", upgrade_file_le.text) + return + + if not plugin.terrain.storage or \ + ( plugin.terrain.storage and plugin.terrain.storage.get_path() != upgrade_file_le.text): + print("Loading storage file: ", upgrade_file_le.text) + plugin.terrain.set_storage(load(upgrade_file_le.text)) + + if plugin.terrain.storage: + print("Begining upgrade of: ", upgrade_file_le.text) + plugin.terrain.split_storage() diff --git a/addons/terrain_3d/src/directory_setup.tscn b/addons/terrain_3d/src/directory_setup.tscn new file mode 100644 index 0000000..dbe2c3e --- /dev/null +++ b/addons/terrain_3d/src/directory_setup.tscn @@ -0,0 +1,66 @@ +[gd_scene format=3 uid="uid://by3kr2nqbqr67"] + +[node name="DirectorySetup" type="ConfirmationDialog"] +title = "Terrain3D Data Directory Setup" +position = Vector2i(0, 36) +size = Vector2i(750, 574) +visible = true + +[node name="Margin" type="MarginContainer" parent="."] +offset_left = 8.0 +offset_top = 8.0 +offset_right = 742.0 +offset_bottom = 525.0 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="VBox" type="VBoxContainer" parent="Margin"] +layout_mode = 2 + +[node name="Instructions" type="Label" parent="Margin/VBox"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +text = "Terrain3D now stores data in a directory instead of a single file. Each region is stored in a separate file named `terrain3d[-_]##[-_]##.res`. For instance, the region at location (-1, 1) would be named `terrain3d-01_01.res`. Enable Terrain3D / Debug / Show Region Labels for a visual display." +autowrap_mode = 3 + +[node name="DirectoryLabel" type="Label" parent="Margin/VBox"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +text = " +Specify the directory to store your data. Any existing region files will be loaded." +autowrap_mode = 3 + +[node name="DirHBox" type="HBoxContainer" parent="Margin/VBox"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="Margin/VBox/DirHBox"] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Data directory" + +[node name="SelectDir" type="Button" parent="Margin/VBox/DirHBox"] +layout_mode = 2 + +[node name="UpgradeLabel" type="Label" parent="Margin/VBox"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +text = " +If you wish to upgrade a storage file from v0.8.4 - v0.9.2, specify it below. Data will be stored in the directory above upon save. The original file will not be touched." +autowrap_mode = 3 + +[node name="UpgradeHBox" type="HBoxContainer" parent="Margin/VBox"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="Margin/VBox/UpgradeHBox"] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Storage .res to upgrade" + +[node name="SelectResFile" type="Button" parent="Margin/VBox/UpgradeHBox"] +layout_mode = 2 + +[node name="Spacer" type="Control" parent="Margin/VBox"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 diff --git a/addons/terrain_3d/src/double_slider.gd b/addons/terrain_3d/src/double_slider.gd new file mode 100644 index 0000000..7061726 --- /dev/null +++ b/addons/terrain_3d/src/double_slider.gd @@ -0,0 +1,124 @@ +@tool +class_name DoubleSlider +extends Control + +signal value_changed(Vector2) +var label: Label +var suffix: String +var grabbed_handle: int = 0 # -1 left, 0 none, 1 right +var min_value: float = 0.0 +var max_value: float = 100.0 +var step: float = 1.0 +var range := Vector2(0, 100) + + +func _ready() -> void: + update_label() + + +func set_min(p_value: float) -> void: + min_value = p_value + if range.x <= min_value: + range.x = min_value + set_value(range) + update_label() + + +func get_min() -> float: + return min_value + + +func set_max(p_value: float) -> void: + max_value = p_value + if range.y == 0 or range.y >= max_value: + range.y = max_value + set_value(range) + update_label() + + +func get_max() -> float: + return max_value + + +func set_step(p_step: float) -> void: + step = p_step + + +func get_step() -> float: + return step + + +func set_value(p_range: Vector2) -> void: + range.x = clamp(p_range.x, min_value, max_value) + range.y = clamp(p_range.y, min_value, max_value) + if range.y < range.x: + var tmp: float = range.x + range.x = range.y + range.y = tmp + + update_label() + emit_signal("value_changed", Vector2(range.x, range.y)) + queue_redraw() + + +func get_value() -> Vector2: + return range + + +func update_label() -> void: + if label: + label.set_text(str(range.x) + suffix + "/" + str(range.y) + suffix) + + +func _gui_input(p_event: InputEvent) -> void: + if p_event is InputEventMouseButton: + if p_event.get_button_index() == MOUSE_BUTTON_LEFT: + if p_event.is_pressed(): + var mid_point = (range.x + range.y) / 2.0 + var xpos: float = p_event.get_position().x * 2.0 + if xpos >= mid_point: + grabbed_handle = 1 + else: + grabbed_handle = -1 + set_slider(p_event.get_position().x) + else: + grabbed_handle = 0 + + if p_event is InputEventMouseMotion: + if grabbed_handle != 0: + set_slider(p_event.get_position().x) + + +func set_slider(p_xpos: float) -> void: + if grabbed_handle == 0: + return + var xpos_step: float = clamp(snappedf((p_xpos / size.x) * max_value, step), min_value, max_value) + if(grabbed_handle < 0): + range.x = xpos_step + else: + range.y = xpos_step + set_value(range) + + +func _notification(p_what: int) -> void: + if p_what == NOTIFICATION_DRAW: + # Draw background bar + var bg: StyleBox = get_theme_stylebox("slider", "HSlider") + var bg_height: float = bg.get_minimum_size().y + var mid_y: float = (size.y - bg_height) / 2.0 + draw_style_box(bg, Rect2(Vector2(0, mid_y), Vector2(size.x, bg_height))) + + # Draw foreground bar + var handle: Texture2D = get_theme_icon("grabber", "HSlider") + var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider") + var h: float = size.y / 2 - handle.get_size().y / 2 + var startx: float = (range.x / max_value) * size.x + var endx: float = (range.y / max_value) * size.x #- startx + draw_style_box(area, Rect2(Vector2(startx, mid_y), Vector2(endx - startx, bg_height))) + + # Draw handles, slightly in so they don't get on the outside edges + var handle_pos: Vector2 + handle_pos.x = clamp(startx - handle.get_size().x/2, -5, size.x) + handle_pos.y = clamp(endx - handle.get_size().x/2, 0, size.x - 10) + draw_texture(handle, Vector2(handle_pos.x, -mid_y)) + draw_texture(handle, Vector2(handle_pos.y, -mid_y)) diff --git a/addons/terrain_3d/src/multi_picker.gd b/addons/terrain_3d/src/multi_picker.gd index aa228bd..6b8e4d0 100644 --- a/addons/terrain_3d/src/multi_picker.gd +++ b/addons/terrain_3d/src/multi_picker.gd @@ -5,7 +5,6 @@ signal pressed signal value_changed -const ICON_PICKER: String = "res://addons/terrain_3d/icons/picker.svg" const ICON_PICKER_CHECKED: String = "res://addons/terrain_3d/icons/picker_checked.svg" const MAX_POINTS: int = 2 @@ -16,8 +15,8 @@ var points: PackedVector3Array var picking_index: int = -1 -func _init() -> void: - icon_picker = load(ICON_PICKER) +func _enter_tree() -> void: + icon_picker = get_theme_icon("ColorPick", "EditorIcons") icon_picker_checked = load(ICON_PICKER_CHECKED) points.resize(MAX_POINTS) diff --git a/addons/terrain_3d/src/region_gizmo.gd b/addons/terrain_3d/src/region_gizmo.gd index af2a6cd..5c9fa6f 100644 --- a/addons/terrain_3d/src/region_gizmo.gd +++ b/addons/terrain_3d/src/region_gizmo.gd @@ -32,7 +32,7 @@ func _redraw() -> void: if show_rect: var modulate: Color = main_color if !use_secondary_color else secondary_color - if abs(region_position.x) > 8 or abs(region_position.y) > 8: + if abs(region_position.x) > Terrain3DData.REGION_MAP_SIZE*.5 or abs(region_position.y) > Terrain3DData.REGION_MAP_SIZE*.5: modulate = Color.GRAY draw_rect(Vector2(region_size,region_size)*.5 + rect_position, region_size, selection_material, modulate) @@ -44,7 +44,7 @@ func _redraw() -> void: draw_rect(Vector2(region_size,region_size)*.5 + grid_tile_position, region_size, material, grid_color) - draw_rect(Vector2.ZERO, region_size * 16.0, material, border_color) + draw_rect(Vector2.ZERO, region_size * Terrain3DData.REGION_MAP_SIZE, material, border_color) func draw_rect(p_pos: Vector2, p_size: float, p_material: StandardMaterial3D, p_modulate: Color) -> void: diff --git a/addons/terrain_3d/src/terrain_tools.gd b/addons/terrain_3d/src/terrain_menu.gd similarity index 66% rename from addons/terrain_3d/src/terrain_tools.gd rename to addons/terrain_3d/src/terrain_menu.gd index 91efc3c..a1b47bc 100644 --- a/addons/terrain_3d/src/terrain_tools.gd +++ b/addons/terrain_3d/src/terrain_menu.gd @@ -1,68 +1,72 @@ extends HBoxContainer -const Baker: Script = preload("res://addons/terrain_3d/src/baker.gd") +const DirectoryWizard: Script = preload("res://addons/terrain_3d/src/directory_setup.gd") const Packer: Script = preload("res://addons/terrain_3d/src/channel_packer.gd") +const Baker: Script = preload("res://addons/terrain_3d/src/baker.gd") var plugin: EditorPlugin var menu_button: MenuButton = MenuButton.new() -var baker: Baker = Baker.new() +var directory_setup: DirectoryWizard = DirectoryWizard.new() var packer: Packer = Packer.new() +var baker: Baker = Baker.new() +# These are IDs and order must be consistent with add_item and set_disabled IDs enum { + MENU_DIRECTORY_SETUP, + MENU_PACK_TEXTURES, + MENU_SEPARATOR, MENU_BAKE_ARRAY_MESH, MENU_BAKE_OCCLUDER, - MENU_BAKE_NAV_MESH, - MENU_SEPARATOR, + MENU_SEPARATOR2, MENU_SET_UP_NAVIGATION, - MENU_PACK_TEXTURES, + MENU_BAKE_NAV_MESH, } func _enter_tree() -> void: - baker.plugin = plugin + directory_setup.plugin = plugin packer.plugin = plugin - + baker.plugin = plugin + add_child(directory_setup) 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_item("Directory Setup...", MENU_DIRECTORY_SETUP) + menu_button.get_popup().add_item("Pack Textures...", MENU_PACK_TEXTURES) 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().add_separator("", MENU_SEPARATOR) - menu_button.get_popup().add_item("Pack Textures", MENU_PACK_TEXTURES) + 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_separator("", MENU_SEPARATOR2) + menu_button.get_popup().add_item("Set up Navigation...", MENU_SET_UP_NAVIGATION) + menu_button.get_popup().add_item("Bake NavMesh...", MENU_BAKE_NAV_MESH) 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) -func _exit_tree() -> void: - # TODO: If packer isn't freed, Godot complains about ObjectDB instances leaked and - # resources still in use at exit. Figure out why. - packer.free() - - func _on_menu_pressed(p_id: int) -> void: match p_id: + MENU_DIRECTORY_SETUP: + directory_setup.directory_setup_popup() + MENU_PACK_TEXTURES: + packer.pack_textures_popup() MENU_BAKE_ARRAY_MESH: 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() - MENU_PACK_TEXTURES: - packer.pack_textures_popup() + MENU_BAKE_NAV_MESH: + baker.bake_nav_mesh() func _on_menu_about_to_popup() -> void: + menu_button.get_popup().set_item_disabled(MENU_DIRECTORY_SETUP, not plugin.terrain) + menu_button.get_popup().set_item_disabled(MENU_PACK_TEXTURES, not plugin.terrain) 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) - menu_button.get_popup().set_item_disabled(MENU_PACK_TEXTURES, not plugin.terrain) if plugin.terrain: var nav_regions: Array[NavigationRegion3D] = baker.find_terrain_nav_regions(plugin.terrain) diff --git a/addons/terrain_3d/src/tool_settings.gd b/addons/terrain_3d/src/tool_settings.gd index ad9a195..dd14104 100644 --- a/addons/terrain_3d/src/tool_settings.gd +++ b/addons/terrain_3d/src/tool_settings.gd @@ -17,13 +17,14 @@ enum SettingType { PICKER, MULTI_PICKER, SLIDER, + LABEL, TYPE_MAX, } const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd") const DEFAULT_BRUSH: String = "circle0.exr" const BRUSH_PATH: String = "res://addons/terrain_3d/brushes" -const PICKER_ICON: String = "res://addons/terrain_3d/icons/picker.svg" +const ES_TOOL_SETTINGS: String = "terrain3d/tool_settings/" # Add settings flags const NONE: int = 0x0 @@ -31,13 +32,14 @@ const ALLOW_LARGER: int = 0x1 const ALLOW_SMALLER: int = 0x2 const ALLOW_OUT_OF_BOUNDS: int = 0x3 # LARGER|SMALLER const NO_LABEL: int = 0x4 -const ADD_SEPARATOR: int = 0x8 -const ADD_SPACER: int = 0x10 +const ADD_SEPARATOR: int = 0x8 # Add a vertical line before this entry +const ADD_SPACER: int = 0x10 # Add a space before this entry +const NO_SAVE: int = 0x20 # Don't save this in EditorSettings +var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors var brush_preview_material: ShaderMaterial var select_brush_button: Button - -var main_list: HBoxContainer +var main_list: HFlowContainer var advanced_list: VBoxContainer var height_list: VBoxContainer var scale_list: VBoxContainer @@ -47,21 +49,26 @@ var settings: Dictionary = {} func _ready() -> void: - main_list = HBoxContainer.new() + # Remove old editor settings + for setting in ["lift_floor", "flatten_peaks", "lift_flatten", "automatic_regions"]: + plugin.erase_setting(ES_TOOL_SETTINGS + setting) + + # Setup buttons + main_list = HFlowContainer.new() add_child(main_list, true) - ## Common Settings add_brushes(main_list) - add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":50, "unit":"m", - "range":Vector3(2, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER }) + add_setting({ "name":"instructions", "label":"Click the terrain to add a region. CTRL+Click to remove. Or select another tool on the left.", + "type":SettingType.LABEL, "list":main_list, "flags":NO_LABEL|NO_SAVE }) + + add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":20, "unit":"m", + "range":Vector3(0.1, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER }) - add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":10, + add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":33, "unit":"%", "range":Vector3(1, 100, 1), "flags":ALLOW_LARGER }) - add_setting({ "name":"enable", "type":SettingType.CHECKBOX, "list":main_list, "default":true }) - - add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":50, + add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":20, "unit":"m", "range":Vector3(-500, 500, 0.1), "flags":ALLOW_OUT_OF_BOUNDS }) add_setting({ "name":"height_picker", "type":SettingType.PICKER, "list":main_list, "default":Terrain3DEditor.HEIGHT, "flags":NO_LABEL }) @@ -71,14 +78,21 @@ func _ready() -> void: add_setting({ "name":"color_picker", "type":SettingType.PICKER, "list":main_list, "default":Terrain3DEditor.COLOR, "flags":NO_LABEL }) - add_setting({ "name":"roughness", "type":SettingType.SLIDER, "list":main_list, "default":0, + add_setting({ "name":"roughness", "type":SettingType.SLIDER, "list":main_list, "default":-65, "unit":"%", "range":Vector3(-100, 100, 1), "flags":ADD_SEPARATOR }) add_setting({ "name":"roughness_picker", "type":SettingType.PICKER, "list":main_list, "default":Terrain3DEditor.ROUGHNESS, "flags":NO_LABEL }) add_setting({ "name":"enable_texture", "label":"Texture", "type":SettingType.CHECKBOX, - "list":main_list, "default":true }) + "list":main_list, "default":true, "flags":ADD_SEPARATOR }) + add_setting({ "name":"margin", "type":SettingType.SLIDER, "list":main_list, "default":0, + "unit":"", "range":Vector3(-50, 50, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + + # Slope painting filter + add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list, "default":Vector2(0, 90), + "unit":"°", "range":Vector3(0, 90, 1), "flags":ADD_SEPARATOR }) + add_setting({ "name":"enable_angle", "label":"Angle", "type":SettingType.CHECKBOX, "list":main_list, "default":true, "flags":ADD_SEPARATOR }) add_setting({ "name":"angle", "type":SettingType.SLIDER, "list":main_list, "default":0, @@ -88,18 +102,16 @@ func _ready() -> void: add_setting({ "name":"dynamic_angle", "label":"Dynamic", "type":SettingType.CHECKBOX, "list":main_list, "default":false, "flags":ADD_SPACER }) - add_setting({ "name":"enable_scale", "label":"Scale ±", "type":SettingType.CHECKBOX, + add_setting({ "name":"enable_scale", "label":"Scale", "type":SettingType.CHECKBOX, "list":main_list, "default":true, "flags":ADD_SEPARATOR }) add_setting({ "name":"scale", "label":"±", "type":SettingType.SLIDER, "list":main_list, "default":0, "unit":"%", "range":Vector3(-60, 80, 20), "flags":NO_LABEL }) add_setting({ "name":"scale_picker", "type":SettingType.PICKER, "list":main_list, "default":Terrain3DEditor.SCALE, "flags":NO_LABEL }) - ## Slope - add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list, - "default":0, "unit":"°", "range":Vector3(0, 180, 1) }) + ## Slope sculpting brush add_setting({ "name":"gradient_points", "type":SettingType.MULTI_PICKER, "label":"Points", - "list":main_list, "default":Terrain3DEditor.HEIGHT, "flags":ADD_SEPARATOR }) + "list":main_list, "default":Terrain3DEditor.SCULPT, "flags":ADD_SEPARATOR }) add_setting({ "name":"drawable", "type":SettingType.CHECKBOX, "list":main_list, "default":false, "flags":ADD_SEPARATOR }) settings["drawable"].toggled.connect(_on_drawable_toggled) @@ -123,9 +135,9 @@ func _ready() -> void: "default":0, "unit":"°", "range":Vector3(0, 360, 1) }) add_setting({ "name":"random_spin", "type":SettingType.SLIDER, "list":rotation_list, "default":360, "unit":"°", "range":Vector3(0, 360, 1) }) - add_setting({ "name":"fixed_angle", "label":"Fixed Angle (From Y)", "type":SettingType.SLIDER, "list":rotation_list, + add_setting({ "name":"fixed_tilt", "label":"Fixed Tilt", "type":SettingType.SLIDER, "list":rotation_list, "default":0, "unit":"°", "range":Vector3(-85, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS }) - add_setting({ "name":"random_angle", "label":"Random Angle ±", "type":SettingType.SLIDER, "list":rotation_list, + add_setting({ "name":"random_tilt", "label":"Random Tilt ±", "type":SettingType.SLIDER, "list":rotation_list, "default":10, "unit":"°", "range":Vector3(0, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS }) add_setting({ "name":"align_to_normal", "type":SettingType.CHECKBOX, "list":rotation_list, "default":false }) @@ -139,14 +151,17 @@ func _ready() -> void: #add_setting({ "name":"blend_mode", "type":SettingType.OPTION, "list":color_list, "default":0, #"range":Vector3(0, 3, 1) }) + if DisplayServer.is_touchscreen_available(): + add_setting({ "name":"remove", "label":"Invert", "type":SettingType.CHECKBOX, "list":main_list, "default":false, "flags":ADD_SEPARATOR }) + var spacer: Control = Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL main_list.add_child(spacer, true) ## Advanced Settings Menu - advanced_list = create_submenu(main_list, "Advanced", Layout.VERTICAL) - add_setting({ "name":"automatic_regions", "type":SettingType.CHECKBOX, "list":advanced_list, - "default":true }) + advanced_list = create_submenu(main_list, "", Layout.VERTICAL, false) + add_setting({ "name":"auto_regions", "label":"Add regions while sculpting", "type":SettingType.CHECKBOX, + "list":advanced_list, "default":true }) add_setting({ "name":"align_to_view", "type":SettingType.CHECKBOX, "list":advanced_list, "default":true }) add_setting({ "name":"show_cursor_while_painting", "type":SettingType.CHECKBOX, "list":advanced_list, @@ -158,23 +173,44 @@ func _ready() -> void: "unit":"%", "range":Vector3(0, 100, 1) }) -func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout) -> Container: +func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true) -> Container: var menu_button: Button = Button.new() - menu_button.set_text(p_button_name) + if p_button_name.is_empty(): + menu_button.icon = get_theme_icon("GuiTabMenuHl", "EditorIcons") + else: + menu_button.set_text(p_button_name) menu_button.set_toggle_mode(true) menu_button.set_v_size_flags(SIZE_SHRINK_CENTER) menu_button.toggled.connect(_on_show_submenu.bind(menu_button)) var submenu: PopupPanel = PopupPanel.new() - submenu.popup_hide.connect(menu_button.set_pressed_no_signal.bind(false)) + submenu.popup_hide.connect(menu_button.set_pressed.bind(false)) var panel_style: StyleBox = get_theme_stylebox("panel", "PopupMenu").duplicate() panel_style.set_content_margin_all(10) submenu.set("theme_override_styles/panel", panel_style) submenu.add_to_group("terrain3d_submenus") # Pop up menu on hover, hide on exit - menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button)) - submenu.mouse_exited.connect(_on_show_submenu.bind(false, menu_button)) + if p_hover_pop: + menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button)) + + submenu.mouse_entered.connect(func(): submenu.set_meta("mouse_entered", true)) + + submenu.mouse_exited.connect(func(): + # On mouse_exit, hide popup unless LineEdit focused + var focused_element: Control = submenu.gui_get_focus_owner() + if not focused_element is LineEdit: + _on_show_submenu(false, menu_button) + submenu.set_meta("mouse_entered", false) + return + + focused_element.focus_exited.connect(func(): + # Close submenu once lineedit loses focus + if not submenu.get_meta("mouse_entered"): + _on_show_submenu(false, menu_button) + submenu.set_meta("mouse_entered", false) + ) + ) var sublist: Container match(p_layout): @@ -200,20 +236,21 @@ func _on_show_submenu(p_toggled: bool, p_button: Button) -> void: # Hide menu if mouse is not in button or panel var button_rect: Rect2 = Rect2(p_button.get_screen_transform().origin, p_button.get_global_rect().size) var in_button: bool = button_rect.has_point(DisplayServer.mouse_get_position()) - var panel: PopupPanel = p_button.get_child(0) - var panel_rect: Rect2 = Rect2(panel.position, panel.size) - var in_panel: bool = panel_rect.has_point(DisplayServer.mouse_get_position()) - if not p_toggled and ( in_button or in_panel ): + var popup: PopupPanel = p_button.get_child(0) + var popup_rect: Rect2 = Rect2(popup.position, popup.size) + var in_popup: bool = popup_rect.has_point(DisplayServer.mouse_get_position()) + if not p_toggled and ( in_button or in_popup ): return # Hide all submenus before possibly enabling the current one get_tree().call_group("terrain3d_submenus", "set_visible", false) - var popup: PopupPanel = p_button.get_child(0) - var popup_pos: Vector2 = p_button.get_screen_transform().origin popup.set_visible(p_toggled) - popup_pos.y -= popup.get_size().y + var popup_pos: Vector2 = p_button.get_screen_transform().origin + popup_pos.y -= popup.size.y + if popup.get_child_count()>0 and popup.get_child(0) == advanced_list: + popup_pos.x -= popup.size.x - p_button.size.x popup.set_position(popup_pos) - + func add_brushes(p_parent: Control) -> void: var brush_list: GridContainer = create_submenu(p_parent, "Brush", Layout.GRID) @@ -231,6 +268,8 @@ func add_brushes(p_parent: Control) -> void: if !dir.current_is_dir() and file_name.ends_with(".exr"): var img: Image = Image.load_from_file(BRUSH_PATH + "/" + file_name) img = Terrain3DUtil.black_to_alpha(img) + if img.get_width() < 1024 and img.get_height() < 1024: + img.resize(1024, 1024, Image.INTERPOLATE_CUBIC) var tex: ImageTexture = ImageTexture.create_from_image(img) var btn: Button = Button.new() @@ -270,10 +309,11 @@ func add_brushes(p_parent: Control) -> void: select_brush_button = brush_list.get_parent().get_parent() # Optionally erase the main brush button text and replace it with the texture -# select_brush_button.set_button_icon(default_brush_btn.get_button_icon()) -# select_brush_button.set_custom_minimum_size(Vector2.ONE * 36) -# select_brush_button.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER) -# select_brush_button.set_expand_icon(true) + select_brush_button.set_text("") + select_brush_button.set_button_icon(default_brush_btn.get_button_icon()) + select_brush_button.set_custom_minimum_size(Vector2.ONE * 36) + select_brush_button.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER) + select_brush_button.set_expand_icon(true) func _on_brush_hover(p_hovering: bool, p_button: Button) -> void: @@ -307,13 +347,12 @@ func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void: - assert(p_type == Terrain3DEditor.HEIGHT) + assert(p_type == Terrain3DEditor.SCULPT) emit_signal("picking", p_type, _on_point_picked.bind(p_name)) func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void: - assert(p_type == Terrain3DEditor.HEIGHT) - + assert(p_type == Terrain3DEditor.SCULPT) var point: Vector3 = p_global_position point.y = p_color.r settings[p_name].add_point(point) @@ -342,9 +381,22 @@ func add_setting(p_args: Dictionary) -> void: var pending_children: Array[Control] match p_type: + SettingType.LABEL: + var label := Label.new() + label.set_text(p_label) + pending_children.push_back(label) + control = label + SettingType.CHECKBOX: var checkbox := CheckBox.new() - checkbox.set_pressed_no_signal(p_default) + if !(p_flags & NO_SAVE): + checkbox.set_pressed_no_signal(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) + checkbox.toggled.connect( ( + func(value, path): + plugin.set_setting(path, value) + ).bind(ES_TOOL_SETTINGS + p_name) ) + else: + checkbox.set_pressed_no_signal(p_default) checkbox.pressed.connect(_on_setting_changed) pending_children.push_back(checkbox) control = checkbox @@ -352,19 +404,24 @@ func add_setting(p_args: Dictionary) -> void: SettingType.COLOR_SELECT: var picker := ColorPickerButton.new() picker.set_custom_minimum_size(Vector2(100, 25)) - picker.color = Color.WHITE picker.edit_alpha = false picker.get_picker().set_color_mode(ColorPicker.MODE_HSV) + if !(p_flags & NO_SAVE): + picker.set_pick_color(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) + picker.color_changed.connect( ( + func(value, path): + plugin.set_setting(path, value) + ).bind(ES_TOOL_SETTINGS + p_name) ) + else: + picker.set_pick_color(p_default) picker.color_changed.connect(_on_setting_changed) - var popup: PopupPanel = picker.get_popup() - popup.mouse_exited.connect(Callable(func(p): p.hide()).bind(popup)) pending_children.push_back(picker) control = picker SettingType.PICKER: var button := Button.new() button.set_v_size_flags(SIZE_SHRINK_CENTER) - button.icon = load(PICKER_ICON) + button.icon = get_theme_icon("ColorPick", "EditorIcons") button.tooltip_text = "Pick value from the Terrain" button.pressed.connect(_on_pick.bind(p_default)) pending_children.push_back(button) @@ -397,10 +454,9 @@ func add_setting(p_args: Dictionary) -> void: spin_slider.set_max(p_maximum) spin_slider.set_min(p_minimum) spin_slider.set_step(p_step) - spin_slider.set_value(p_default) spin_slider.set_suffix(p_suffix) spin_slider.set_v_size_flags(SIZE_SHRINK_CENTER) - spin_slider.set_custom_minimum_size(Vector2(75, 0)) + spin_slider.set_custom_minimum_size(Vector2(65, 0)) # Create horizontal slider linked to the above box slider = HSlider.new() @@ -409,27 +465,38 @@ func add_setting(p_args: Dictionary) -> void: slider.set_allow_greater(true) if p_flags & ALLOW_SMALLER: slider.set_allow_lesser(true) + pending_children.push_back(slider) pending_children.push_back(spin_slider) control = spin_slider - + else: # DOUBLE_SLIDER var label := Label.new() - label.set_custom_minimum_size(Vector2(75, 0)) + label.set_custom_minimum_size(Vector2(60, 0)) + label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT) slider = DoubleSlider.new() slider.label = label slider.suffix = p_suffix - slider.setting_changed.connect(_on_setting_changed) + slider.value_changed.connect(_on_setting_changed) pending_children.push_back(slider) pending_children.push_back(label) control = slider - slider.set_max(p_maximum) slider.set_min(p_minimum) + slider.set_max(p_maximum) slider.set_step(p_step) slider.set_value(p_default) slider.set_v_size_flags(SIZE_SHRINK_CENTER) - slider.set_custom_minimum_size(Vector2(60, 10)) + slider.set_custom_minimum_size(Vector2(50, 10)) + + if !(p_flags & NO_SAVE): + slider.set_value(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) + slider.value_changed.connect( ( + func(value, path): + plugin.set_setting(path, value) + ).bind(ES_TOOL_SETTINGS + p_name) ) + else: + slider.set_value(p_default) control.name = p_name.to_pascal_case() settings[p_name] = control @@ -491,8 +558,8 @@ func get_setting(p_setting: String) -> Variant: var width: float = clamp( (1 + count_digits(value)) * 19., 50, 80) * clamp(EditorInterface.get_editor_scale(), .9, 2) object.set_custom_minimum_size(Vector2(width, 0)) elif object is DoubleSlider: - value = Vector2(object.get_min_value(), object.get_max_value()) - elif object is ButtonGroup: + value = object.get_value() + elif object is ButtonGroup: # "brush" var img: Image = object.get_pressed_button().get_meta("image") var tex: Texture2D = object.get_pressed_button().get_button_icon() value = [ img, tex ] @@ -509,11 +576,10 @@ func get_setting(p_setting: String) -> Variant: func set_setting(p_setting: String, p_value: Variant) -> void: var object: Object = settings.get(p_setting) - if object is Range: + if object is DoubleSlider: # Expects p_value is Vector2 + object.set_value(p_value) + elif object is Range: object.set_value(p_value) - elif object is DoubleSlider: # Expects p_value is Vector2 - object.set_min_value(p_value.x) - object.set_max_value(p_value.y) elif object is ButtonGroup: # Expects p_value is Array [ "button name", boolean ] if p_value is Array and p_value.size() == 2: for button in object.get_buttons(): @@ -523,6 +589,7 @@ func set_setting(p_setting: String, p_value: Variant) -> void: object.button_pressed = p_value elif object is ColorPickerButton: object.color = p_value + plugin.set_setting(ES_TOOL_SETTINGS + p_setting, p_value) # Signal doesn't fire on CPB elif object is MultiPicker: # Expects p_value is PackedVector3Array object.points = p_value _on_setting_changed(object) @@ -548,13 +615,12 @@ func _on_setting_changed(p_data: Variant = null) -> void: if p_data is Button and p_data.get_parent().get_parent() is PopupPanel: if p_data.get_parent().name == "BrushList": # Optionally Set selected brush texture in main brush button -# p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon()) + p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon()) # Hide popup p_data.get_parent().get_parent().set_visible(false) # Hide label if p_data.get_child_count() > 0: p_data.get_child(0).visible = false - emit_signal("setting_changed") @@ -567,7 +633,6 @@ func _get_brush_preview_material() -> ShaderMaterial: if !brush_preview_material: brush_preview_material = ShaderMaterial.new() var shader: Shader = Shader.new() - var code: String = "shader_type canvas_item;\n" code += "varying vec4 v_vertex_color;\n" code += "void vertex() {\n" @@ -578,13 +643,11 @@ func _get_brush_preview_material() -> ShaderMaterial: code += " COLOR.a *= pow(tex.r, 0.666);\n" code += " COLOR.rgb = v_vertex_color.rgb;\n" code += "}\n" - shader.set_code(code) brush_preview_material.set_shader(shader) return brush_preview_material - # Counts digits of a number including negative sign, decimal points, and up to 3 decimals func count_digits(p_value: float) -> int: var count: int = 1 @@ -605,75 +668,3 @@ func count_digits(p_value: float) -> int: count += 1 return count - -#### Sub Class DoubleSlider - -class DoubleSlider extends Range: - signal setting_changed(Vector2) - var label: Label - var suffix: String - var grabbed: bool = false - var _max_value: float - # TODO Needs to clamp min and max values. Currently allows max slider to go negative. - - func _gui_input(p_event: InputEvent) -> void: - if p_event is InputEventMouseButton: - if p_event.get_button_index() == MOUSE_BUTTON_LEFT: - grabbed = p_event.is_pressed() - set_min_max(p_event.get_position().x) - - if p_event is InputEventMouseMotion: - if grabbed: - set_min_max(p_event.get_position().x) - - - func _notification(p_what: int) -> void: - if p_what == NOTIFICATION_RESIZED: - pass - if p_what == NOTIFICATION_DRAW: - var bg: StyleBox = get_theme_stylebox("slider", "HSlider") - var bg_height: float = bg.get_minimum_size().y - draw_style_box(bg, Rect2(Vector2(0, (size.y - bg_height) / 2), Vector2(size.x, bg_height))) - - var grabber: Texture2D = get_theme_icon("grabber", "HSlider") - var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider") - var h: float = size.y / 2 - grabber.get_size().y / 2 - - var minpos: Vector2 = Vector2((min_value / _max_value) * size.x - grabber.get_size().x / 2, h) - var maxpos: Vector2 = Vector2((max_value / _max_value) * size.x - grabber.get_size().x / 2, h) - - draw_style_box(area, Rect2(Vector2(minpos.x + grabber.get_size().x / 2, (size.y - bg_height) / 2), Vector2(maxpos.x - minpos.x, bg_height))) - - draw_texture(grabber, minpos) - draw_texture(grabber, maxpos) - - - func set_max(p_value: float) -> void: - max_value = p_value - if _max_value == 0: - _max_value = max_value - update_label() - - - func set_min_max(p_xpos: float) -> void: - var mid_value_normalized: float = ((max_value + min_value) / 2.0) / _max_value - var mid_value: float = size.x * mid_value_normalized - var min_active: bool = p_xpos < mid_value - var xpos_ranged: float = snappedf((p_xpos / size.x) * _max_value, step) - - if min_active: - min_value = xpos_ranged - else: - max_value = xpos_ranged - - min_value = clamp(min_value, 0, max_value - 10) - max_value = clamp(max_value, min_value + 10, _max_value) - - update_label() - emit_signal("setting_changed", Vector2(min_value, max_value)) - queue_redraw() - - - func update_label() -> void: - if label: - label.set_text(str(min_value) + suffix + "/" + str(max_value) + suffix) diff --git a/addons/terrain_3d/src/toolbar.gd b/addons/terrain_3d/src/toolbar.gd index 4128400..df407cc 100644 --- a/addons/terrain_3d/src/toolbar.gd +++ b/addons/terrain_3d/src/toolbar.gd @@ -1,14 +1,11 @@ -extends VBoxContainer - - +extends VFlowContainer + signal tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) const ICON_REGION_ADD: String = "res://addons/terrain_3d/icons/region_add.svg" const ICON_REGION_REMOVE: String = "res://addons/terrain_3d/icons/region_remove.svg" const ICON_HEIGHT_ADD: String = "res://addons/terrain_3d/icons/height_add.svg" const ICON_HEIGHT_SUB: String = "res://addons/terrain_3d/icons/height_sub.svg" -const ICON_HEIGHT_MUL: String = "res://addons/terrain_3d/icons/height_mul.svg" -const ICON_HEIGHT_DIV: String = "res://addons/terrain_3d/icons/height_div.svg" const ICON_HEIGHT_FLAT: String = "res://addons/terrain_3d/icons/height_flat.svg" const ICON_HEIGHT_SLOPE: String = "res://addons/terrain_3d/icons/height_slope.svg" const ICON_HEIGHT_SMOOTH: String = "res://addons/terrain_3d/icons/height_smooth.svg" @@ -21,57 +18,126 @@ const ICON_HOLES: String = "res://addons/terrain_3d/icons/holes.svg" const ICON_NAVIGATION: String = "res://addons/terrain_3d/icons/navigation.svg" const ICON_INSTANCER: String = "res://addons/terrain_3d/icons/multimesh.svg" -var tool_group: ButtonGroup = ButtonGroup.new() +var add_tool_group: ButtonGroup = ButtonGroup.new() +var sub_tool_group: ButtonGroup = ButtonGroup.new() func _init() -> void: set_custom_minimum_size(Vector2(20, 0)) - func _ready() -> void: - tool_group.connect("pressed", _on_tool_selected) + add_tool_group.connect("pressed", _on_tool_selected) + sub_tool_group.connect("pressed", _on_tool_selected) + + add_tool_button({ "tool":Terrain3DEditor.REGION, + "add_text":"Add Region", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_REGION_ADD, + "sub_text":"Remove Region", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_REGION_REMOVE }) - add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.ADD, "Add Region", load(ICON_REGION_ADD), tool_group) - add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.SUBTRACT, "Remove Region", load(ICON_REGION_REMOVE), tool_group) add_child(HSeparator.new()) - add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.ADD, "Raise", load(ICON_HEIGHT_ADD), tool_group) - add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.SUBTRACT, "Lower", load(ICON_HEIGHT_SUB), tool_group) - add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.MULTIPLY, "Expand (Away from 0)", load(ICON_HEIGHT_MUL), tool_group) - add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.DIVIDE, "Reduce (Towards 0)", load(ICON_HEIGHT_DIV), tool_group) - add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.REPLACE, "Flatten", load(ICON_HEIGHT_FLAT), tool_group) - add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.GRADIENT, "Slope", load(ICON_HEIGHT_SLOPE), tool_group) - add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.AVERAGE, "Smooth", load(ICON_HEIGHT_SMOOTH), tool_group) - add_child(HSeparator.new()) - add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE, "Paint Base Texture", load(ICON_PAINT_TEXTURE), tool_group) - add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.ADD, "Spray Overlay Texture", load(ICON_SPRAY_TEXTURE), tool_group) - add_tool_button(Terrain3DEditor.AUTOSHADER, Terrain3DEditor.REPLACE, "Autoshader", load(ICON_AUTOSHADER), tool_group) - add_child(HSeparator.new()) - add_tool_button(Terrain3DEditor.COLOR, Terrain3DEditor.REPLACE, "Paint Color", load(ICON_COLOR), tool_group) - add_tool_button(Terrain3DEditor.ROUGHNESS, Terrain3DEditor.REPLACE, "Paint Wetness", load(ICON_WETNESS), tool_group) - add_child(HSeparator.new()) - add_tool_button(Terrain3DEditor.HOLES, Terrain3DEditor.REPLACE, "Create Holes", load(ICON_HOLES), tool_group) - add_tool_button(Terrain3DEditor.NAVIGATION, Terrain3DEditor.REPLACE, "Paint Navigable Area", load(ICON_NAVIGATION), tool_group) - add_tool_button(Terrain3DEditor.INSTANCER, Terrain3DEditor.ADD, "Instance Meshes", load(ICON_INSTANCER), tool_group) + + add_tool_button({ "tool":Terrain3DEditor.SCULPT, + "add_text":"Raise", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_ADD, + "sub_text":"Lower", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_SUB }) - var buttons: Array[BaseButton] = tool_group.get_buttons() + add_tool_button({ "tool":Terrain3DEditor.SCULPT, + "add_text":"Smooth", "add_op":Terrain3DEditor.AVERAGE, "add_icon":ICON_HEIGHT_SMOOTH }) + + add_tool_button({ "tool":Terrain3DEditor.HEIGHT, + "add_text":"Height", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_FLAT, + "sub_text":"Zero", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_FLAT }) + + add_tool_button({ "tool":Terrain3DEditor.SCULPT, + "add_text":"Slope", "add_op":Terrain3DEditor.GRADIENT, "add_icon":ICON_HEIGHT_SLOPE }) + + add_child(HSeparator.new()) + + add_tool_button({ "tool":Terrain3DEditor.TEXTURE, + "add_text":"Paint Base Texture", "add_op":Terrain3DEditor.REPLACE, "add_icon":ICON_PAINT_TEXTURE }) + + add_tool_button({ "tool":Terrain3DEditor.TEXTURE, + "add_text":"Spray Overlay Texture", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_SPRAY_TEXTURE }) + + add_tool_button({ "tool":Terrain3DEditor.AUTOSHADER, + "add_text":"Enable Autoshader", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_AUTOSHADER, + "sub_text":"Disable Autoshader", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_child(HSeparator.new()) + + add_tool_button({ "tool":Terrain3DEditor.COLOR, + "add_text":"Paint Color", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_COLOR, + "sub_text":"Remove Color", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_tool_button({ "tool":Terrain3DEditor.ROUGHNESS, + "add_text":"Paint Wetness", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_WETNESS, + "sub_text":"Remove Wetness", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_child(HSeparator.new()) + + add_tool_button({ "tool":Terrain3DEditor.HOLES, + "add_text":"Add Holes", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HOLES, + "sub_text":"Remove Holes", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_tool_button({ "tool":Terrain3DEditor.NAVIGATION, + "add_text":"Paint Navigable Area", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_NAVIGATION, + "sub_text":"Remove Navigable Area", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_tool_button({ "tool":Terrain3DEditor.INSTANCER, + "add_text":"Instance Meshes", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_INSTANCER, + "sub_text":"Remove Meshes", "sub_op":Terrain3DEditor.SUBTRACT }) + + # Select first button + var buttons: Array[BaseButton] = add_tool_group.get_buttons() buttons[0].set_pressed(true) + show_add_buttons(true) -func add_tool_button(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation, - p_tip: String, p_icon: Texture2D, p_group: ButtonGroup) -> void: - - var button: Button = Button.new() - button.set_name(p_tip.to_pascal_case()) - button.set_meta("Tool", p_tool) - button.set_meta("Operation", p_operation) - button.set_tooltip_text(p_tip) - button.set_button_icon(p_icon) - button.set_button_group(p_group) +func add_tool_button(p_params: Dictionary) -> void: + # Additive button + var button := Button.new() + button.set_name(p_params.get("add_text", "blank").to_pascal_case()) + button.set_meta("Tool", p_params.get("tool", 0)) + button.set_meta("Operation", p_params.get("add_op", 0)) + button.set_meta("ID", add_tool_group.get_buttons().size() + 1) + button.set_tooltip_text(p_params.get("add_text", "blank")) + button.set_button_icon(load(p_params.get("add_icon"))) button.set_flat(true) button.set_toggle_mode(true) button.set_h_size_flags(SIZE_SHRINK_END) - add_child(button) + button.set_button_group(p_params.get("group", add_tool_group)) + add_child(button, true) + + # Subtractive button + var button2: Button + if p_params.has("sub_text"): + button2 = Button.new() + button2.set_name(p_params.get("sub_text", "blank").to_pascal_case()) + button2.set_meta("Tool", p_params.get("tool", 0)) + button2.set_meta("Operation", p_params.get("sub_op", 0)) + button2.set_meta("ID", button.get_meta("ID")) + button2.set_tooltip_text(p_params.get("sub_text", "blank")) + button2.set_button_icon(load(p_params.get("sub_icon", p_params.get("add_icon")))) + button2.set_flat(true) + button2.set_toggle_mode(true) + button2.set_h_size_flags(SIZE_SHRINK_END) + else: + button2 = button.duplicate() + button2.set_button_group(p_params.get("group", sub_tool_group)) + add_child(button2, true) + + +func show_add_buttons(p_enable: bool) -> void: + for button in add_tool_group.get_buttons(): + button.visible = p_enable + for button in sub_tool_group.get_buttons(): + button.visible = !p_enable func _on_tool_selected(p_button: BaseButton) -> void: - emit_signal("tool_changed", p_button.get_meta("Tool", -1), p_button.get_meta("Operation", -1)) + # Select same tool on negative bar + var group: ButtonGroup = p_button.get_button_group() + var change_group: ButtonGroup = add_tool_group if group == sub_tool_group else sub_tool_group + var id: int = p_button.get_meta("ID", -2) + for button in change_group.get_buttons(): + button.set_pressed_no_signal(button.get_meta("ID", -1) == id) + + emit_signal("tool_changed", p_button.get_meta("Tool", Terrain3DEditor.TOOL_MAX), p_button.get_meta("Operation", Terrain3DEditor.OP_MAX)) diff --git a/addons/terrain_3d/src/ui.gd b/addons/terrain_3d/src/ui.gd index bc6df8a..4e9114f 100644 --- a/addons/terrain_3d/src/ui.gd +++ b/addons/terrain_3d/src/ui.gd @@ -5,34 +5,38 @@ extends Node # Includes const Toolbar: Script = preload("res://addons/terrain_3d/src/toolbar.gd") const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd") -const TerrainTools: Script = preload("res://addons/terrain_3d/src/terrain_tools.gd") +const TerrainMenu: Script = preload("res://addons/terrain_3d/src/terrain_menu.gd") const OperationBuilder: Script = preload("res://addons/terrain_3d/src/operation_builder.gd") const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/src/gradient_operation_builder.gd") const COLOR_RAISE := Color.WHITE -const COLOR_LOWER := Color.BLACK +const COLOR_LOWER := Color(.02, .02, .02) const COLOR_SMOOTH := Color(0.5, 0, .1) -const COLOR_EXPAND := Color.ORANGE -const COLOR_REDUCE := Color.BLUE_VIOLET -const COLOR_FLATTEN := Color(0., 0.32, .4) +const COLOR_LIFT := Color.ORANGE +const COLOR_FLATTEN := Color.BLUE_VIOLET +const COLOR_HEIGHT := Color(0., 0.32, .4) const COLOR_SLOPE := Color.YELLOW -const COLOR_PAINT := Color.FOREST_GREEN -const COLOR_SPRAY := Color.SEA_GREEN +const COLOR_PAINT := Color.DARK_GREEN +const COLOR_SPRAY := Color.PALE_GREEN const COLOR_ROUGHNESS := Color.ROYAL_BLUE const COLOR_AUTOSHADER := Color.DODGER_BLUE const COLOR_HOLES := Color.BLACK -const COLOR_NAVIGATION := Color.REBECCA_PURPLE +const COLOR_NAVIGATION := Color(.15, .0, .255) const COLOR_INSTANCER := Color.CRIMSON const COLOR_PICK_COLOR := Color.WHITE const COLOR_PICK_HEIGHT := Color.DARK_RED const COLOR_PICK_ROUGH := Color.ROYAL_BLUE +const OP_NONE: int = 0x0 +const OP_POSITIVE_ONLY: int = 0x01 +const OP_NEGATIVE_ONLY: int = 0x02 + const RING1: String = "res://addons/terrain_3d/brushes/ring1.exr" @onready var ring_texture := ImageTexture.create_from_image(Terrain3DUtil.black_to_alpha(Image.load_from_file(RING1))) var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors var toolbar: Toolbar -var toolbar_settings: ToolSettings -var terrain_tools: TerrainTools +var tool_settings: ToolSettings +var terrain_menu: TerrainMenu var setting_has_changed: bool = false var visible: bool = false var picking: int = Terrain3DEditor.TOOL_MAX @@ -42,6 +46,16 @@ var decal_timer: Timer var gradient_decals: Array[Decal] var brush_data: Dictionary var operation_builder: OperationBuilder +var last_tool: Terrain3DEditor.Tool +var last_operation: Terrain3DEditor.Operation +var last_rmb_time: int = 0 # Set in editor.gd + +# Compatibility decals, indices; 0 = main brush, 1 = slope point A, 2 = slope point B +var editor_decal_position: Array[Vector2] +var editor_decal_rotation: Array[float] +var editor_decal_size: Array[float] +var editor_decal_color: Array[Color] +var editor_decal_visible: Array[bool] func _enter_tree() -> void: @@ -49,18 +63,19 @@ func _enter_tree() -> void: toolbar.hide() toolbar.connect("tool_changed", _on_tool_changed) - toolbar_settings = ToolSettings.new() - toolbar_settings.connect("setting_changed", _on_setting_changed) - toolbar_settings.connect("picking", _on_picking) - toolbar_settings.hide() + tool_settings = ToolSettings.new() + tool_settings.connect("setting_changed", _on_setting_changed) + tool_settings.connect("picking", _on_picking) + tool_settings.plugin = plugin + tool_settings.hide() - terrain_tools = TerrainTools.new() - terrain_tools.plugin = plugin - terrain_tools.hide() + terrain_menu = TerrainMenu.new() + terrain_menu.plugin = plugin + terrain_menu.hide() plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar) - plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings) - plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_tools) + plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, tool_settings) + plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_menu) _on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD) @@ -77,10 +92,10 @@ func _enter_tree() -> void: func _exit_tree() -> void: plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar) - plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings) + plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, tool_settings) toolbar.queue_free() - toolbar_settings.queue_free() - terrain_tools.queue_free() + tool_settings.queue_free() + terrain_menu.queue_free() decal.queue_free() decal_timer.queue_free() for gradient_decal in gradient_decals: @@ -88,14 +103,27 @@ func _exit_tree() -> void: gradient_decals.clear() -func set_visible(p_visible: bool) -> void: - visible = p_visible - terrain_tools.set_visible(p_visible) - toolbar.set_visible(p_visible) - toolbar_settings.set_visible(p_visible) - update_decal() +func set_visible(p_visible: bool, p_menu_only: bool = false) -> void: + terrain_menu.set_visible(p_visible) + if p_menu_only: + toolbar.set_visible(false) + tool_settings.set_visible(false) + else: + visible = p_visible + toolbar.set_visible(p_visible) + tool_settings.set_visible(p_visible) + update_decal() + if(plugin.editor): + if(p_visible): + await get_tree().create_timer(.01).timeout # Won't work, otherwise. + _on_tool_changed(last_tool, last_operation) + else: + plugin.editor.set_tool(Terrain3DEditor.TOOL_MAX) + plugin.editor.set_operation(Terrain3DEditor.OP_MAX) + + func set_menu_visibility(p_list: Control, p_visible: bool) -> void: if p_list: p_list.get_parent().get_parent().visible = p_visible @@ -103,33 +131,45 @@ func set_menu_visibility(p_list: Control, p_visible: bool) -> void: func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void: clear_picking() - set_menu_visibility(toolbar_settings.advanced_list, true) - set_menu_visibility(toolbar_settings.scale_list, false) - set_menu_visibility(toolbar_settings.rotation_list, false) - set_menu_visibility(toolbar_settings.height_list, false) - set_menu_visibility(toolbar_settings.color_list, false) + set_menu_visibility(tool_settings.advanced_list, true) + set_menu_visibility(tool_settings.scale_list, false) + set_menu_visibility(tool_settings.rotation_list, false) + set_menu_visibility(tool_settings.height_list, false) + set_menu_visibility(tool_settings.color_list, false) # Select which settings to show. Options in tool_settings.gd:_ready var to_show: PackedStringArray = [] match p_tool: + Terrain3DEditor.REGION: + to_show.push_back("instructions") + to_show.push_back("remove") + set_menu_visibility(tool_settings.advanced_list, false) + + Terrain3DEditor.SCULPT: + to_show.push_back("brush") + to_show.push_back("size") + to_show.push_back("strength") + if p_operation in [Terrain3DEditor.ADD, Terrain3DEditor.SUBTRACT]: + to_show.push_back("remove") + elif p_operation == Terrain3DEditor.GRADIENT: + to_show.push_back("gradient_points") + to_show.push_back("drawable") + Terrain3DEditor.HEIGHT: to_show.push_back("brush") to_show.push_back("size") to_show.push_back("strength") - if p_operation == Terrain3DEditor.REPLACE: - to_show.push_back("height") - to_show.push_back("height_picker") - if p_operation == Terrain3DEditor.GRADIENT: - to_show.push_back("gradient_points") - to_show.push_back("drawable") - - Terrain3DEditor.TEXTURE: + to_show.push_back("height") + to_show.push_back("height_picker") + + Terrain3DEditor.TEXTURE: to_show.push_back("brush") to_show.push_back("size") to_show.push_back("enable_texture") if p_operation == Terrain3DEditor.ADD: to_show.push_back("strength") + to_show.push_back("slope") to_show.push_back("enable_angle") to_show.push_back("angle") to_show.push_back("angle_picker") @@ -144,6 +184,10 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor to_show.push_back("strength") to_show.push_back("color") to_show.push_back("color_picker") + to_show.push_back("slope") + to_show.push_back("enable_texture") + to_show.push_back("margin") + to_show.push_back("remove") Terrain3DEditor.ROUGHNESS: to_show.push_back("brush") @@ -151,52 +195,59 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor to_show.push_back("strength") to_show.push_back("roughness") to_show.push_back("roughness_picker") + to_show.push_back("slope") + to_show.push_back("enable_texture") + to_show.push_back("margin") + to_show.push_back("remove") Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION: to_show.push_back("brush") to_show.push_back("size") - to_show.push_back("enable") + to_show.push_back("remove") Terrain3DEditor.INSTANCER: to_show.push_back("size") to_show.push_back("strength") - to_show.push_back("enable") - set_menu_visibility(toolbar_settings.height_list, true) + to_show.push_back("slope") + set_menu_visibility(tool_settings.height_list, true) to_show.push_back("height_offset") to_show.push_back("random_height") - set_menu_visibility(toolbar_settings.scale_list, true) + set_menu_visibility(tool_settings.scale_list, true) to_show.push_back("fixed_scale") to_show.push_back("random_scale") - set_menu_visibility(toolbar_settings.rotation_list, true) + set_menu_visibility(tool_settings.rotation_list, true) to_show.push_back("fixed_spin") to_show.push_back("random_spin") - to_show.push_back("fixed_angle") - to_show.push_back("random_angle") + to_show.push_back("fixed_tilt") + to_show.push_back("random_tilt") to_show.push_back("align_to_normal") - set_menu_visibility(toolbar_settings.color_list, true) + set_menu_visibility(tool_settings.color_list, true) to_show.push_back("vertex_color") to_show.push_back("random_darken") to_show.push_back("random_hue") + to_show.push_back("remove") _: pass # Advanced menu settings - to_show.push_back("automatic_regions") + to_show.push_back("auto_regions") to_show.push_back("align_to_view") to_show.push_back("show_cursor_while_painting") to_show.push_back("gamma") to_show.push_back("jitter") - toolbar_settings.show_settings(to_show) + tool_settings.show_settings(to_show) operation_builder = null if p_operation == Terrain3DEditor.GRADIENT: operation_builder = GradientOperationBuilder.new() - operation_builder.tool_settings = toolbar_settings + operation_builder.tool_settings = tool_settings if plugin.editor: plugin.editor.set_tool(p_tool) - plugin.editor.set_operation(p_operation) + plugin.editor.set_operation(_modify_operation(p_operation)) + last_tool = p_tool + last_operation = p_operation _on_setting_changed() plugin.update_region_grid() @@ -205,31 +256,67 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor func _on_setting_changed() -> void: if not plugin.asset_dock: return - brush_data = toolbar_settings.get_settings() + brush_data = tool_settings.get_settings() brush_data["asset_id"] = plugin.asset_dock.get_current_list().get_selected_id() update_decal() plugin.editor.set_brush_data(brush_data) + plugin.editor.set_operation(_modify_operation(plugin.editor.get_operation())) + + +func update_modifiers() -> void: + toolbar.show_add_buttons(not plugin.modifier_ctrl) + + if plugin.modifier_shift and not plugin.modifier_ctrl: + plugin.editor.set_tool(Terrain3DEditor.SCULPT) + plugin.editor.set_operation(Terrain3DEditor.AVERAGE) + else: + plugin.editor.set_tool(last_tool) + if plugin.modifier_ctrl: + plugin.editor.set_operation(_modify_operation(last_operation)) + else: + plugin.editor.set_operation(last_operation) + + +func _modify_operation(p_operation: Terrain3DEditor.Operation) -> Terrain3DEditor.Operation: + var remove_checked: bool = false + if DisplayServer.is_touchscreen_available(): + var removable_tools := [Terrain3DEditor.REGION, Terrain3DEditor.SCULPT, Terrain3DEditor.HEIGHT, Terrain3DEditor.AUTOSHADER, + Terrain3DEditor.HOLES, Terrain3DEditor.INSTANCER, Terrain3DEditor.NAVIGATION, + Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS] + remove_checked = brush_data.get("remove", false) && plugin.editor.get_tool() in removable_tools + + if plugin.modifier_ctrl or remove_checked: + return _invert_operation(p_operation, OP_NEGATIVE_ONLY) + return _invert_operation(p_operation, OP_POSITIVE_ONLY) + + +func _invert_operation(p_operation: Terrain3DEditor.Operation, flags: int = OP_NONE) -> Terrain3DEditor.Operation: + if p_operation == Terrain3DEditor.ADD and ! (flags & OP_POSITIVE_ONLY): + return Terrain3DEditor.SUBTRACT + elif p_operation == Terrain3DEditor.SUBTRACT and ! (flags & OP_NEGATIVE_ONLY): + return Terrain3DEditor.ADD + return p_operation func update_decal() -> void: - var mouse_buttons: int = Input.get_mouse_button_mask() + # If not a state that should show the decal, hide everything and return 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 \ - plugin.editor.get_tool() == Terrain3DEditor.REGION: - decal.visible = false - for gradient_decal in gradient_decals: - gradient_decal.visible = false - return - else: - # Wait for cursor to recenter after right-click before revealing + not plugin.terrain or \ + plugin._input_mode < 0 or \ + # Wait for cursor to recenter after moving camera before revealing # See https://github.com/godotengine/godot/issues/70098 - await get_tree().create_timer(.05).timeout - decal.visible = true + Time.get_ticks_msec() - last_rmb_time <= 30 or \ + brush_data.is_empty() or \ + plugin.editor.get_tool() == Terrain3DEditor.REGION or \ + (plugin._input_mode > 0 and not brush_data["show_cursor_while_painting"]): + decal.visible = false + for gradient_decal in gradient_decals: + gradient_decal.visible = false + return - decal.size = Vector3.ONE * brush_data["size"] + decal.position = plugin.mouse_global_position + decal.visible = true + decal.size = Vector3.ONE * maxf(brush_data["size"], .5) if brush_data["align_to_view"]: var cam: Camera3D = plugin.terrain.get_camera(); if (cam): @@ -240,7 +327,7 @@ func update_decal() -> void: # Set texture and color if picking != Terrain3DEditor.TOOL_MAX: decal.texture_albedo = ring_texture - decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing() + decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing() match picking: Terrain3DEditor.HEIGHT: decal.modulate = COLOR_PICK_HEIGHT @@ -252,62 +339,66 @@ func update_decal() -> void: else: decal.texture_albedo = brush_data["brush"][1] match plugin.editor.get_tool(): - Terrain3DEditor.HEIGHT: + Terrain3DEditor.SCULPT: match plugin.editor.get_operation(): Terrain3DEditor.ADD: - decal.modulate = COLOR_RAISE + if plugin.modifier_alt: + decal.modulate = COLOR_LIFT + decal.modulate.a = clamp(brush_data["strength"], .2, .5) + else: + decal.modulate = COLOR_RAISE + decal.modulate.a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.SUBTRACT: - decal.modulate = COLOR_LOWER - Terrain3DEditor.MULTIPLY: - decal.modulate = COLOR_EXPAND - Terrain3DEditor.DIVIDE: - decal.modulate = COLOR_REDUCE - Terrain3DEditor.REPLACE: - decal.modulate = COLOR_FLATTEN + if plugin.modifier_alt: + decal.modulate = COLOR_FLATTEN + decal.modulate.a = clamp(brush_data["strength"], .2, .5) + else: + decal.modulate = COLOR_LOWER + decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .5 Terrain3DEditor.AVERAGE: decal.modulate = COLOR_SMOOTH + decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .2 Terrain3DEditor.GRADIENT: decal.modulate = COLOR_SLOPE - _: - decal.modulate = Color.WHITE - decal.modulate.a = max(.3, brush_data["strength"] * .01) + decal.modulate.a = clamp(brush_data["strength"], .2, .5) + Terrain3DEditor.HEIGHT: + decal.modulate = COLOR_HEIGHT + decal.modulate.a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.TEXTURE: match plugin.editor.get_operation(): Terrain3DEditor.REPLACE: decal.modulate = COLOR_PAINT - decal.modulate.a = 1.0 + decal.modulate.a = .7 + Terrain3DEditor.SUBTRACT: + decal.modulate = COLOR_PAINT + decal.modulate.a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.ADD: decal.modulate = COLOR_SPRAY - decal.modulate.a = max(.3, brush_data["strength"] * .01) - _: - decal.modulate = Color.WHITE + decal.modulate.a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.COLOR: - decal.modulate = brush_data["color"].srgb_to_linear()*.5 - decal.modulate.a = max(.3, brush_data["strength"] * .01) + decal.modulate = brush_data["color"].srgb_to_linear() + decal.modulate.a *= clamp(brush_data["strength"], .2, .5) Terrain3DEditor.ROUGHNESS: decal.modulate = COLOR_ROUGHNESS - decal.modulate.a = max(.3, brush_data["strength"] * .01) + decal.modulate.a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.AUTOSHADER: decal.modulate = COLOR_AUTOSHADER - decal.modulate.a = 1.0 + decal.modulate.a = .7 Terrain3DEditor.HOLES: decal.modulate = COLOR_HOLES - decal.modulate.a = 1.0 + decal.modulate.a = .85 Terrain3DEditor.NAVIGATION: decal.modulate = COLOR_NAVIGATION - decal.modulate.a = 1.0 + decal.modulate.a = .85 Terrain3DEditor.INSTANCER: decal.texture_albedo = ring_texture decal.modulate = COLOR_INSTANCER decal.modulate.a = 1.0 - _: - decal.modulate = Color.WHITE - decal.modulate.a = max(.3, brush_data["strength"] * .01) decal.size.y = max(1000, decal.size.y) decal.albedo_mix = 1.0 decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 ) decal_timer.start() - + for gradient_decal in gradient_decals: gradient_decal.visible = false @@ -320,6 +411,8 @@ func update_decal() -> void: point_decal.position = point index += 1 + update_compatibility_decal() + func _get_gradient_decal(index: int) -> Decal: if gradient_decals.size() > index: @@ -329,7 +422,7 @@ func _get_gradient_decal(index: int) -> Decal: gradient_decal = Decal.new() gradient_decal.texture_albedo = ring_texture gradient_decal.modulate = COLOR_SLOPE - gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing() + gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing() gradient_decal.size.y = 1000. gradient_decal.cull_mask = decal.cull_mask add_child(gradient_decal) @@ -338,6 +431,61 @@ func _get_gradient_decal(index: int) -> Decal: return gradient_decal +func update_compatibility_decal() -> void: + if not plugin.terrain.is_compatibility_mode(): + return + + # Verify setup + if editor_decal_position.size() != 3: + editor_decal_position.resize(3) + editor_decal_rotation.resize(3) + editor_decal_size.resize(3) + editor_decal_color.resize(3) + editor_decal_visible.resize(3) + decal_timer.timeout.connect(func(): + var mat_rid: RID = plugin.terrain.material.get_material_rid() + editor_decal_visible[0] = false + RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible) + ) + + # Update compatibility decal + var mat_rid: RID = plugin.terrain.material.get_material_rid() + if decal.visible: + editor_decal_position[0] = Vector2(decal.global_position.x, decal.global_position.z) + editor_decal_rotation[0] = decal.rotation.y + editor_decal_size[0] = brush_data.get("size") + editor_decal_color[0] = decal.modulate + editor_decal_visible[0] = decal.visible + RenderingServer.material_set_param( + mat_rid, "_editor_decal_0", decal.texture_albedo.get_rid() + ) + if gradient_decals.size() >= 1: + editor_decal_position[1] = Vector2(gradient_decals[0].global_position.x, + gradient_decals[0].global_position.z) + editor_decal_rotation[1] = gradient_decals[0].rotation.y + editor_decal_size[1] = 10.0 + editor_decal_color[1] = gradient_decals[0].modulate + editor_decal_visible[1] = gradient_decals[0].visible + RenderingServer.material_set_param( + mat_rid, "_editor_decal_1", gradient_decals[0].texture_albedo.get_rid() + ) + if gradient_decals.size() >= 2: + editor_decal_position[2] = Vector2(gradient_decals[1].global_position.x, + gradient_decals[1].global_position.z) + editor_decal_rotation[2] = gradient_decals[1].rotation.y + editor_decal_size[2] = 10.0 + editor_decal_color[2] = gradient_decals[1].modulate + editor_decal_visible[2] = gradient_decals[1].visible + RenderingServer.material_set_param( + mat_rid, "_editor_decal_2", gradient_decals[1].texture_albedo.get_rid() + ) + RenderingServer.material_set_param(mat_rid, "_editor_decal_position", editor_decal_position) + RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation) + RenderingServer.material_set_param(mat_rid, "_editor_decal_size", editor_decal_size) + RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color) + RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible) + + func set_decal_rotation(p_rot: float) -> void: decal.rotation.y = p_rot @@ -366,16 +514,16 @@ func pick(p_global_position: Vector3) -> void: if picking != Terrain3DEditor.TOOL_MAX: var color: Color match picking: - Terrain3DEditor.HEIGHT: - color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_HEIGHT, p_global_position) + Terrain3DEditor.HEIGHT, Terrain3DEditor.SCULPT: + color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_HEIGHT, p_global_position) Terrain3DEditor.ROUGHNESS: - color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position) + color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_COLOR, p_global_position) Terrain3DEditor.COLOR: - color = plugin.terrain.get_storage().get_color(p_global_position) + color = plugin.terrain.data.get_color(p_global_position) Terrain3DEditor.ANGLE: - color = Color(plugin.terrain.get_storage().get_angle(p_global_position), 0., 0., 1.) + color = Color(plugin.terrain.data.get_control_angle(p_global_position), 0., 0., 1.) Terrain3DEditor.SCALE: - color = Color(plugin.terrain.get_storage().get_scale(p_global_position), 0., 0., 1.) + color = Color(plugin.terrain.data.get_control_scale(p_global_position), 0., 0., 1.) _: push_error("Unsupported picking type: ", picking) return @@ -385,3 +533,6 @@ func pick(p_global_position: Vector3) -> void: elif operation_builder and operation_builder.is_picking(): operation_builder.pick(p_global_position, plugin.terrain) + +func set_button_editor_icon(p_button: Button, p_icon_name: String) -> void: + p_button.icon = EditorInterface.get_base_control().get_theme_icon(p_icon_name, "EditorIcons") diff --git a/addons/terrain_3d/tools/importer.gd b/addons/terrain_3d/tools/importer.gd index b563844..56a9817 100644 --- a/addons/terrain_3d/tools/importer.gd +++ b/addons/terrain_3d/tools/importer.gd @@ -12,64 +12,83 @@ func reset_settings(p_value) -> void: height_file_name = "" control_file_name = "" color_file_name = "" - import_position = Vector3.ZERO - import_offset = 0.0 + destination_directory = "" + import_position = Vector2i.ZERO + height_offset = 0.0 import_scale = 1.0 r16_range = Vector2(0, 1) r16_size = Vector2i(1024, 1024) - storage = null material = null assets = null func reset_terrain(p_value) -> void: - if p_value: - storage = null + data_directory = "" func update_heights(p_value) -> void: - if p_value and storage: - storage.update_height_range() + if p_value and data: + data.update_height_range() @export_group("Import File") @export_global_file var height_file_name: String = "" @export_global_file var control_file_name: String = "" @export_global_file var color_file_name: String = "" -@export var import_position: Vector3 = Vector3.ZERO +@export var import_position: Vector2i = Vector2i(0, 0) : set = set_import_position @export var import_scale: float = 1.0 -@export var import_offset: float = 0.0 +@export var height_offset: float = 0.0 @export var r16_range: Vector2 = Vector2(0, 1) -@export var r16_size: Vector2i = Vector2i(1024, 1024) +@export var r16_size: Vector2i = Vector2i(1024, 1024) : set = set_r16_size @export var run_import: bool = false : set = start_import +@export_dir var destination_directory: String = "" +@export var save_to_disk: bool = false : set = save_data + + +func set_import_position(p_value: Vector2i) -> void: + import_position.x = clamp(p_value.x, -8192, 8192) + import_position.y = clamp(p_value.y, -8192, 8192) + + +func set_r16_size(p_value: Vector2i) -> void: + r16_size.x = clamp(p_value.x, 0, 16384) + r16_size.y = clamp(p_value.y, 0, 16384) + + func start_import(p_value: bool) -> void: if p_value: print("Terrain3DImporter: Importing files:\n\t%s\n\t%s\n\t%s" % [ height_file_name, control_file_name, color_file_name]) - if not storage: - storage = Terrain3DStorage.new() var imported_images: Array[Image] - imported_images.resize(Terrain3DStorage.TYPE_MAX) + imported_images.resize(Terrain3DRegion.TYPE_MAX) var min_max := Vector2(0, 1) var img: Image if height_file_name: img = Terrain3DUtil.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size) min_max = Terrain3DUtil.get_min_max(img) - imported_images[Terrain3DStorage.TYPE_HEIGHT] = img + imported_images[Terrain3DRegion.TYPE_HEIGHT] = img if control_file_name: img = Terrain3DUtil.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE) - imported_images[Terrain3DStorage.TYPE_CONTROL] = img + imported_images[Terrain3DRegion.TYPE_CONTROL] = img if color_file_name: img = Terrain3DUtil.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE) - imported_images[Terrain3DStorage.TYPE_COLOR] = img + imported_images[Terrain3DRegion.TYPE_COLOR] = img if assets.get_texture_count() == 0: material.show_checkered = false material.show_colormap = true - storage.import_images(imported_images, import_position, import_offset, import_scale) + var pos := Vector3(import_position.x, 0, import_position.y) + data.import_images(imported_images, pos, height_offset, import_scale) print("Terrain3DImporter: Import finished") +func save_data(p_value: bool) -> void: + if destination_directory.is_empty(): + push_error("Set destination directory first") + return + data.save_directory(destination_directory) + + @export_group("Export File") enum { TYPE_HEIGHT, TYPE_CONTROL, TYPE_COLOR } @export_enum("Height:0", "Control:1", "Color:2") var map_type: int = TYPE_HEIGHT @@ -77,6 +96,6 @@ enum { TYPE_HEIGHT, TYPE_CONTROL, TYPE_COLOR } @export var run_export: bool = false : set = start_export func start_export(p_value: bool) -> void: - var err: int = storage.export_image(file_name_out, map_type) + var err: int = data.export_image(file_name_out, map_type) print("Terrain3DImporter: Export error status: ", err, " ", error_string(err)) diff --git a/addons/terrain_3d/tools/importer.tscn b/addons/terrain_3d/tools/importer.tscn index cfa152c..4bc43c3 100644 --- a/addons/terrain_3d/tools/importer.tscn +++ b/addons/terrain_3d/tools/importer.tscn @@ -1,16 +1,63 @@ -[gd_scene load_steps=5 format=3 uid="uid://blaieaqp413k7"] +[gd_scene load_steps=9 format=3 uid="uid://blaieaqp413k7"] [ext_resource type="Script" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"] -[sub_resource type="Terrain3DStorage" id="Terrain3DStorage_rmuvl"] +[sub_resource type="Gradient" id="Gradient_88f3t"] +offsets = PackedFloat32Array(0.2, 1) +colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1) -[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_cjpaa"] +[sub_resource type="FastNoiseLite" id="FastNoiseLite_muvel"] +noise_type = 2 +frequency = 0.03 +cellular_jitter = 3.0 +cellular_return_type = 0 +domain_warp_enabled = true +domain_warp_type = 1 +domain_warp_amplitude = 50.0 +domain_warp_fractal_type = 2 +domain_warp_fractal_lacunarity = 1.5 +domain_warp_fractal_gain = 1.0 + +[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_ve0yk"] +seamless = true +color_ramp = SubResource("Gradient_88f3t") +noise = SubResource("FastNoiseLite_muvel") + +[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_p55u0"] +_shader_parameters = { +"blend_sharpness": 0.87, +"height_blending": true, +"macro_variation1": Color(1, 1, 1, 1), +"macro_variation2": Color(1, 1, 1, 1), +"noise1_angle": 0.0, +"noise1_offset": Vector2(0.5, 0.5), +"noise1_scale": 0.04, +"noise2_scale": 0.076, +"noise3_scale": 0.225, +"noise_texture": SubResource("NoiseTexture2D_ve0yk"), +"vertex_normals_distance": 128.0 +} show_checkered = true -[sub_resource type="Terrain3DAssets" id="Terrain3DAssets_gbxcd"] +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_8rvqy"] +cull_mode = 2 +vertex_color_use_as_albedo = true +backlight_enabled = true +backlight = Color(0.5, 0.5, 0.5, 1) + +[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_7je72"] +height_offset = 0.5 +density = 10.0 +material_override = SubResource("StandardMaterial3D_8rvqy") +generated_type = 1 + +[sub_resource type="Terrain3DAssets" id="Terrain3DAssets_op32e"] +mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_7je72")]) [node name="Importer" type="Terrain3D"] -storage = SubResource("Terrain3DStorage_rmuvl") -material = SubResource("Terrain3DMaterial_cjpaa") -assets = SubResource("Terrain3DAssets_gbxcd") +material = SubResource("Terrain3DMaterial_p55u0") +assets = SubResource("Terrain3DAssets_op32e") +mesh_lods = 8 +top_level = true script = ExtResource("1_60b8f") +metadata/_edit_lock_ = true diff --git a/addons/terrain_3d/utils/terrain_3d_objects.gd b/addons/terrain_3d/utils/terrain_3d_objects.gd index 55d338b..34b1a08 100644 --- a/addons/terrain_3d/utils/terrain_3d_objects.gd +++ b/addons/terrain_3d/utils/terrain_3d_objects.gd @@ -42,22 +42,22 @@ func editor_setup(p_plugin) -> void: func get_terrain() -> Terrain3D: var terrain := instance_from_id(_terrain_id) as Terrain3D if not terrain or terrain.is_queued_for_deletion() or not terrain.is_inside_tree(): - var terrains: Array[Node] = EditorInterface.get_edited_scene_root().find_children("", "Terrain3D") + var terrains: Array[Node] = Engine.get_singleton(&"EditorInterface").get_edited_scene_root().find_children("", "Terrain3D") if terrains.size() > 0: terrain = terrains[0] _terrain_id = terrain.get_instance_id() if terrain else 0 - if terrain and terrain.storage and not terrain.storage.maps_edited.is_connected(_on_maps_edited): - terrain.storage.maps_edited.connect(_on_maps_edited) + if terrain and terrain.data and not terrain.data.maps_edited.is_connected(_on_maps_edited): + terrain.data.maps_edited.connect(_on_maps_edited) return terrain func _get_terrain_height(p_global_position: Vector3) -> float: var terrain: Terrain3D = get_terrain() - if not terrain or not terrain.storage: + if not terrain or not terrain.data: return 0.0 - var height: float = terrain.storage.get_height(p_global_position) + var height: float = terrain.data.get_height(p_global_position) if is_nan(height): return 0.0 return height @@ -105,7 +105,7 @@ func _on_child_exiting_tree(p_node: Node) -> void: func _is_node_selected(p_node: Node) -> bool: - var editor_sel = EditorInterface.get_selection() + var editor_sel = Engine.get_singleton(&"EditorInterface").get_selection() return editor_sel.get_transformable_selected_nodes().has(p_node) diff --git a/levels/debug_level/debug_level.tscn b/levels/debug_level/debug_level.tscn index 757f103..c49c8fd 100644 --- a/levels/debug_level/debug_level.tscn +++ b/levels/debug_level/debug_level.tscn @@ -1,6 +1,5 @@ -[gd_scene load_steps=56 format=3 uid="uid://bm2o3mex10v11"] +[gd_scene load_steps=55 format=3 uid="uid://bm2o3mex10v11"] -[ext_resource type="Terrain3DStorage" uid="uid://bu1gewgsgc5hm" path="res://levels/debug_level/terrain_3d_storage.res" id="1_nlsu2"] [ext_resource type="Texture2D" path="res://assets/textures/grass_fairway/grass_fairway_albedo.dds" id="2_e4m27"] [ext_resource type="Texture2D" path="res://assets/textures/grass_rough/grass_rough_albedo.dds" id="4_p0awb"] [ext_resource type="Texture2D" path="res://assets/textures/grass_rough/grass_rough_normal.dds" id="4_yhjhv"] @@ -51,30 +50,33 @@ _shader_parameters = { "noise3_scale": 0.021, "noise_texture": SubResource("NoiseTexture2D_sb318"), "vertex_normals_distance": 128.0, -"world_noise_blend_far": 1.0, -"world_noise_blend_near": 0.48, "world_noise_height": 74.0, "world_noise_lod_distance": 16384.0, "world_noise_max_octaves": 8, "world_noise_min_octaves": 2, "world_noise_offset": Vector3(0, -8, 0), +"world_noise_region_blend": 0.33, "world_noise_scale": 5.0 } world_background = 2 texture_filtering = 1 auto_shader = true +show_region_grid = true -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_2u1w6"] +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_l1sla"] +transparency = 4 cull_mode = 2 vertex_color_use_as_albedo = true backlight_enabled = true backlight = Color(0.5, 0.5, 0.5, 1) +distance_fade_mode = 1 +distance_fade_min_distance = 960.0 +distance_fade_max_distance = 480.0 -[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_8g62o"] -name = "palm_tree" +[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_iaq2t"] height_offset = 0.5 density = 10.0 -material_override = SubResource("StandardMaterial3D_2u1w6") +material_override = SubResource("StandardMaterial3D_l1sla") generated_type = 1 [sub_resource type="Terrain3DTextureAsset" id="Terrain3DTextureAsset_kocfk"] @@ -105,7 +107,7 @@ normal_texture = ExtResource("9_rrguh") uv_scale = 0.02 [sub_resource type="Terrain3DAssets" id="Terrain3DAssets_eq5uw"] -mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_8g62o")]) +mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_iaq2t")]) texture_list = Array[Terrain3DTextureAsset]([SubResource("Terrain3DTextureAsset_kocfk"), SubResource("Terrain3DTextureAsset_vmhlw"), SubResource("Terrain3DTextureAsset_k6h8c"), SubResource("Terrain3DTextureAsset_13d2a")]) [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_h8tes"] @@ -252,9 +254,11 @@ point_count = 5 [node name="TestLevel" type="Node3D"] [node name="Terrain3D" type="Terrain3D" parent="."] -storage = ExtResource("1_nlsu2") +data_directory = "res://levels/debug_level/terrain_3d_data" material = SubResource("Terrain3DMaterial_woy2k") assets = SubResource("Terrain3DAssets_eq5uw") +show_grid = true +metadata/_edit_lock_ = true [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment_k6wwx") diff --git a/levels/debug_level/terrain_3d_data/terrain3d-01_00.res b/levels/debug_level/terrain_3d_data/terrain3d-01_00.res new file mode 100644 index 0000000..9895318 --- /dev/null +++ b/levels/debug_level/terrain_3d_data/terrain3d-01_00.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49031defad8760fd80cb03b52652ab347c6b4f425e825f114a6c531b7e8dd069 +size 499333 diff --git a/levels/debug_level/terrain_3d_data/terrain3d_00_00.res b/levels/debug_level/terrain_3d_data/terrain3d_00_00.res new file mode 100644 index 0000000..1b0a5aa --- /dev/null +++ b/levels/debug_level/terrain_3d_data/terrain3d_00_00.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:856b688276a678e328c69379ba04b5b98b8833a4872fc649bd8f1c037e8e6f6a +size 546199 diff --git a/levels/debug_level/terrain_3d_storage.res b/levels/debug_level/terrain_3d_storage.res deleted file mode 100644 index 22f446a..0000000 --- a/levels/debug_level/terrain_3d_storage.res +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:87a757aefe30546760291982475766000c22080965006bc0e2689606220efa19 -size 1753865