built more assets and started playing with foliage painting

This commit is contained in:
derek
2024-12-04 17:02:46 -06:00
parent dd960cc00e
commit 478e2822d2
359 changed files with 34172 additions and 178 deletions

View File

@@ -0,0 +1,836 @@
@tool
extends PanelContainer
#class_name Terrain3DAssetDock
signal confirmation_closed
signal confirmation_confirmed
signal confirmation_canceled
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
var _current_list: ListContainer
var _last_thumb_update_time: int = 0
const MAX_UPDATE_TIME: int = 1000
var placement_opt: OptionButton
var floating_btn: Button
var pinned_btn: Button
var size_slider: HSlider
var box: BoxContainer
var buttons: BoxContainer
var textures_btn: Button
var meshes_btn: Button
var asset_container: ScrollContainer
var confirm_dialog: ConfirmationDialog
var _confirmed: bool = false
# Used only for editor, so change to single visible/hiddden
enum {
HIDDEN = -1,
SIDEBAR = 0,
BOTTOM = 1,
WINDOWED = 2,
}
var state: int = HIDDEN
enum {
POS_LEFT_UL = 0,
POS_LEFT_BL = 1,
POS_LEFT_UR = 2,
POS_LEFT_BR = 3,
POS_RIGHT_UL = 4,
POS_RIGHT_BL = 5,
POS_RIGHT_UR = 6,
POS_RIGHT_BR = 7,
POS_BOTTOM = 8,
POS_MAX = 9,
}
var slot: int = POS_RIGHT_BR
var _initialized: bool = false
var plugin: EditorPlugin
var window: Window
var _godot_last_state: Window.Mode = Window.MODE_FULLSCREEN
func initialize(p_plugin: EditorPlugin) -> void:
if p_plugin:
plugin = p_plugin
_godot_last_state = plugin.godot_editor_window.mode
placement_opt = $Box/Buttons/PlacementOpt
pinned_btn = $Box/Buttons/Pinned
floating_btn = $Box/Buttons/Floating
floating_btn.owner = null
size_slider = $Box/Buttons/SizeSlider
size_slider.owner = null
box = $Box
buttons = $Box/Buttons
textures_btn = $Box/Buttons/TexturesBtn
meshes_btn = $Box/Buttons/MeshesBtn
asset_container = $Box/ScrollContainer
texture_list = ListContainer.new()
texture_list.plugin = plugin
texture_list.type = Terrain3DAssets.TYPE_TEXTURE
asset_container.add_child(texture_list)
mesh_list = ListContainer.new()
mesh_list.plugin = plugin
mesh_list.type = Terrain3DAssets.TYPE_MESH
mesh_list.visible = false
asset_container.add_child(mesh_list)
_current_list = texture_list
load_editor_settings()
# Connect signals
resized.connect(update_layout)
textures_btn.pressed.connect(_on_textures_pressed)
meshes_btn.pressed.connect(_on_meshes_pressed)
placement_opt.item_selected.connect(set_slot)
floating_btn.pressed.connect(make_dock_float)
pinned_btn.toggled.connect(_on_pin_changed)
pinned_btn.visible = ( window != null )
size_slider.value_changed.connect(_on_slider_changed)
plugin.ui.toolbar.tool_changed.connect(_on_tool_changed)
meshes_btn.add_theme_font_size_override("font_size", 16 * EditorInterface.get_editor_scale())
textures_btn.add_theme_font_size_override("font_size", 16 * EditorInterface.get_editor_scale())
_initialized = true
update_dock()
update_layout()
func _ready() -> void:
if not _initialized:
return
# 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 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")
floating_btn.text = ""
update_thumbnails()
confirm_dialog = ConfirmationDialog.new()
add_child(confirm_dialog)
confirm_dialog.hide()
confirm_dialog.confirmed.connect(func(): _confirmed = true; \
emit_signal("confirmation_closed"); \
emit_signal("confirmation_confirmed") )
confirm_dialog.canceled.connect(func(): _confirmed = false; \
emit_signal("confirmation_closed"); \
emit_signal("confirmation_canceled") )
func get_current_list() -> ListContainer:
return _current_list
## Dock placement
func set_slot(p_slot: int) -> void:
p_slot = clamp(p_slot, 0, POS_MAX-1)
if slot != p_slot:
slot = p_slot
placement_opt.selected = slot
save_editor_settings()
plugin.select_terrain()
update_dock()
func remove_dock(p_force: bool = false) -> void:
if state == SIDEBAR:
plugin.remove_control_from_docks(self)
state = HIDDEN
elif state == BOTTOM:
plugin.remove_control_from_bottom_panel(self)
state = HIDDEN
# If windowed and destination is not window or final exit, otherwise leave
elif state == WINDOWED and p_force and window:
var parent: Node = get_parent()
if parent:
parent.remove_child(self)
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() # return window to side/bottom
func update_dock() -> void:
if not _initialized or window:
return
update_assets()
# Move dock to new destination
remove_dock()
# 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")
plugin.make_bottom_panel_item_visible(self)
func update_layout() -> void:
if not _initialized:
return
# Detect if we have a new window from Make floating, grab it so we can free it properly
if not window and get_parent() and get_parent().get_parent() is Window:
window = get_parent().get_parent()
make_dock_float()
return # Will call this function again upon display
var size_parent: Control = size_slider.get_parent()
# Vertical layout in window / sidebar
if window or slot < POS_BOTTOM:
box.vertical = true
buttons.vertical = false
if size.x >= 500 and size_parent != buttons:
size_slider.reparent(buttons)
buttons.move_child(size_slider, 3)
elif size.x < 500 and size_parent != box:
size_slider.reparent(box)
box.move_child(size_slider, 1)
floating_btn.reparent(buttons)
buttons.move_child(floating_btn, 4)
# Wide layout on bottom bar
else:
size_slider.reparent(buttons)
buttons.move_child(size_slider, 3)
floating_btn.reparent(box)
box.vertical = false
buttons.vertical = true
save_editor_settings()
func update_thumbnails() -> void:
if not is_instance_valid(plugin.terrain):
return
if _current_list.type == Terrain3DAssets.TYPE_MESH and \
Time.get_ticks_msec() - _last_thumb_update_time > MAX_UPDATE_TIME:
plugin.terrain.assets.create_mesh_thumbnails()
_last_thumb_update_time = Time.get_ticks_msec()
for mesh_asset in mesh_list.entries:
mesh_asset.queue_redraw()
## Dock Button handlers
func _on_pin_changed(toggled: bool) -> void:
if window:
window.always_on_top = pinned_btn.button_pressed
save_editor_settings()
func _on_slider_changed(value: float) -> void:
if texture_list:
texture_list.set_entry_width(value)
if mesh_list:
mesh_list.set_entry_width(value)
save_editor_settings()
func _on_textures_pressed() -> void:
_current_list = texture_list
texture_list.update_asset_list()
texture_list.visible = true
mesh_list.visible = false
textures_btn.button_pressed = true
meshes_btn.button_pressed = false
texture_list.set_selected_id(texture_list.selected_id)
if plugin.is_terrain_valid():
EditorInterface.edit_node(plugin.terrain)
save_editor_settings()
func _on_meshes_pressed() -> void:
_current_list = mesh_list
mesh_list.update_asset_list()
mesh_list.visible = true
texture_list.visible = false
meshes_btn.button_pressed = true
textures_btn.button_pressed = false
mesh_list.set_selected_id(mesh_list.selected_id)
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 in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]:
_on_textures_pressed()
## Update Dock Contents
func update_assets() -> void:
if not _initialized:
return
# Verify signals to individual lists
if plugin.is_terrain_valid() and plugin.terrain.assets:
if not plugin.terrain.assets.textures_changed.is_connected(texture_list.update_asset_list):
plugin.terrain.assets.textures_changed.connect(texture_list.update_asset_list)
if not plugin.terrain.assets.meshes_changed.is_connected(mesh_list.update_asset_list):
plugin.terrain.assets.meshes_changed.connect(mesh_list.update_asset_list)
_current_list.update_asset_list()
## Window Management
func make_dock_float() -> void:
# 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))
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:
window = Window.new()
window.wrap_controls = true
var mc := MarginContainer.new()
mc.set_anchors_preset(PRESET_FULL_RECT, false)
mc.add_child(self)
window.add_child(mc)
window.set_transient(false)
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()
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
if event is InputEventKey and event.keycode == KEY_S and event.pressed and event.is_command_or_control_pressed():
save_editor_settings()
EditorInterface.save_scene()
func _on_godot_window_entered() -> void:
if is_instance_valid(window) and window.has_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 plugin.godot_editor_window.mode != Window.MODE_MINIMIZED:
window.show()
_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 plugin.godot_editor_window.mode == Window.MODE_MINIMIZED:
window.hide()
_godot_last_state = plugin.godot_editor_window.mode
## Manage Editor Settings
func load_editor_settings() -> void:
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)
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
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:
plugin.set_setting(ES_DOCK_WINDOW_SIZE, window.size)
plugin.set_setting(ES_DOCK_WINDOW_POSITION, window.position)
##############################################################
## class ListContainer
##############################################################
class ListContainer extends Container:
var plugin: EditorPlugin
var type := Terrain3DAssets.TYPE_TEXTURE
var entries: Array[ListEntry]
var selected_id: int = 0
var height: float = 0
var width: float = 83
var focus_style: StyleBox
func _ready() -> void:
set_v_size_flags(SIZE_EXPAND_FILL)
set_h_size_flags(SIZE_EXPAND_FILL)
focus_style = get_theme_stylebox("focus", "Button").duplicate()
focus_style.set_border_width_all(2)
focus_style.set_border_color(Color(1, 1, 1, .67))
func clear() -> void:
for e in entries:
e.get_parent().remove_child(e)
e.queue_free()
entries.clear()
func update_asset_list() -> void:
clear()
# Grab terrain
var t: Terrain3D
if plugin.is_terrain_valid():
t = plugin.terrain
elif is_instance_valid(plugin._last_terrain) and plugin.is_terrain_valid(plugin._last_terrain):
t = plugin._last_terrain
else:
return
if not t.assets:
return
if type == Terrain3DAssets.TYPE_TEXTURE:
var texture_count: int = t.assets.get_texture_count()
for i in texture_count:
var texture: Terrain3DTextureAsset = t.assets.get_texture(i)
add_item(texture)
if texture_count < Terrain3DAssets.MAX_TEXTURES:
add_item()
else:
var mesh_count: int = t.assets.get_mesh_count()
for i in mesh_count:
var mesh: Terrain3DMeshAsset = t.assets.get_mesh_asset(i)
add_item(mesh, t.assets)
if mesh_count < Terrain3DAssets.MAX_MESHES:
add_item()
if selected_id >= mesh_count or selected_id < 0:
set_selected_id(0)
func add_item(p_resource: Resource = null, p_assets: Terrain3DAssets = null) -> void:
var entry: ListEntry = ListEntry.new()
entry.focus_style = focus_style
var id: int = entries.size()
entry.set_edited_resource(p_resource)
entry.hovered.connect(_on_resource_hovered.bind(id))
entry.selected.connect(set_selected_id.bind(id))
entry.inspected.connect(_on_resource_inspected)
entry.changed.connect(_on_resource_changed.bind(id))
entry.type = type
entry.asset_list = p_assets
add_child(entry)
entries.push_back(entry)
if p_resource:
entry.set_selected(id == selected_id)
if not p_resource.id_changed.is_connected(set_selected_after_swap):
p_resource.id_changed.connect(set_selected_after_swap)
func _on_resource_hovered(p_id: int):
if type == Terrain3DAssets.TYPE_MESH:
if plugin.terrain:
plugin.terrain.assets.create_mesh_thumbnails(p_id)
func set_selected_after_swap(p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int) -> void:
set_selected_id(clamp(p_new_id, 0, entries.size() - 2))
func set_selected_id(p_id: int) -> void:
selected_id = p_id
for i in entries.size():
var entry: ListEntry = entries[i]
entry.set_selected(i == selected_id)
plugin.select_terrain()
# Select Paint tool if clicking a 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)
plugin.ui._on_tool_changed(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE)
elif type == Terrain3DAssets.TYPE_MESH and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER:
var instancer_btn: Button = plugin.ui.toolbar.get_node_or_null("InstanceMeshes")
if instancer_btn:
instancer_btn.set_pressed(true)
plugin.ui._on_tool_changed(Terrain3DEditor.INSTANCER, Terrain3DEditor.ADD)
# Update editor with selected brush
plugin.ui._on_setting_changed()
func _on_resource_inspected(p_resource: Resource) -> void:
await get_tree().create_timer(.01).timeout
EditorInterface.edit_resource(p_resource)
func _on_resource_changed(p_resource: Resource, p_id: int) -> void:
if not p_resource:
var asset_dock: Control = get_parent().get_parent().get_parent()
if type == Terrain3DAssets.TYPE_TEXTURE:
asset_dock.confirm_dialog.dialog_text = "Are you sure you want to clear this texture?"
else:
asset_dock.confirm_dialog.dialog_text = "Are you sure you want to clear this mesh and delete all instances?"
asset_dock.confirm_dialog.popup_centered()
await asset_dock.confirmation_closed
if not asset_dock._confirmed:
update_asset_list()
return
if not plugin.is_terrain_valid():
plugin.select_terrain()
await get_tree().create_timer(.01).timeout
if plugin.is_terrain_valid():
if type == Terrain3DAssets.TYPE_TEXTURE:
plugin.terrain.get_assets().set_texture(p_id, p_resource)
else:
plugin.terrain.get_assets().set_mesh_asset(p_id, p_resource)
await get_tree().create_timer(.01).timeout
plugin.terrain.assets.create_mesh_thumbnails(p_id)
# If removing an entry, clear inspector
if not p_resource:
EditorInterface.inspect_object(null)
# If null resource, remove last
if not p_resource:
var last_offset: int = 2
if p_id == entries.size()-2:
last_offset = 3
set_selected_id(clamp(selected_id, 0, entries.size() - last_offset))
# Update editor with selected brush
plugin.ui._on_setting_changed()
func get_selected_id() -> int:
return selected_id
func set_entry_width(value: float) -> void:
width = clamp(value, 56, 230)
redraw()
func get_entry_width() -> float:
return width
func redraw() -> void:
height = 0
var id: int = 0
var separation: float = 4
var columns: int = 3
columns = clamp(size.x / width, 1, 100)
for c in get_children():
if is_instance_valid(c):
c.size = Vector2(width, width) - Vector2(separation, separation)
c.position = Vector2(id % columns, id / columns) * width + \
Vector2(separation / columns, separation / columns)
height = max(height, c.position.y + width)
id += 1
# Needed to enable ScrollContainer scroll bar
func _get_minimum_size() -> Vector2:
return Vector2(0, height)
func _notification(p_what) -> void:
if p_what == NOTIFICATION_SORT_CHILDREN:
redraw()
##############################################################
## class ListEntry
##############################################################
class ListEntry extends VBoxContainer:
signal hovered()
signal selected()
signal changed(resource: Resource)
signal inspected(resource: Resource)
var resource: Resource
var type := Terrain3DAssets.TYPE_TEXTURE
var _thumbnail: Texture2D
var drop_data: bool = false
var is_hovered: bool = false
var is_selected: bool = false
var asset_list: Terrain3DAssets
var button_clear: TextureButton
var button_edit: TextureButton
var name_label: Label
@onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons")
@onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons")
@onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons")
@onready var background: StyleBox = get_theme_stylebox("pressed", "Button")
var focus_style: StyleBox
func _ready() -> void:
var icon_size: Vector2 = Vector2(12, 12)
button_clear = TextureButton.new()
button_clear.set_texture_normal(clear_icon)
button_clear.set_custom_minimum_size(icon_size)
button_clear.set_h_size_flags(Control.SIZE_SHRINK_END)
button_clear.set_visible(resource != null)
button_clear.pressed.connect(clear)
add_child(button_clear)
button_edit = TextureButton.new()
button_edit.set_texture_normal(edit_icon)
button_edit.set_custom_minimum_size(icon_size)
button_edit.set_h_size_flags(Control.SIZE_SHRINK_END)
button_edit.set_visible(resource != null)
button_edit.pressed.connect(edit)
add_child(button_edit)
name_label = Label.new()
add_child(name_label, true)
name_label.visible = false
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
name_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
name_label.add_theme_color_override("font_shadow_color", Color.BLACK)
name_label.add_theme_constant_override("shadow_offset_x", 1)
name_label.add_theme_constant_override("shadow_offset_y", 1)
name_label.add_theme_font_size_override("font_size", 15)
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
if type == Terrain3DAssets.TYPE_TEXTURE:
name_label.text = "Add Texture"
else:
name_label.text = "Add Mesh"
func _notification(p_what) -> void:
match p_what:
NOTIFICATION_DRAW:
var rect: Rect2 = Rect2(Vector2.ZERO, get_size())
if !resource:
draw_style_box(background, rect)
draw_texture(add_icon, (get_size() / 2) - (add_icon.get_size() / 2))
else:
if type == Terrain3DAssets.TYPE_TEXTURE:
name_label.text = (resource as Terrain3DTextureAsset).get_name()
self_modulate = resource.get_albedo_color()
_thumbnail = resource.get_albedo_texture()
if _thumbnail:
draw_texture_rect(_thumbnail, rect, false)
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
else:
name_label.text = (resource as Terrain3DMeshAsset).get_name()
var id: int = (resource as Terrain3DMeshAsset).get_id()
_thumbnail = resource.get_thumbnail()
if _thumbnail:
draw_texture_rect(_thumbnail, rect, false)
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
else:
draw_rect(rect, Color(.15, .15, .15, 1.))
name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10)
if drop_data:
draw_style_box(focus_style, rect)
if is_hovered:
draw_rect(rect, Color(1, 1, 1, 0.2))
if is_selected:
draw_style_box(focus_style, rect)
NOTIFICATION_MOUSE_ENTER:
is_hovered = true
name_label.visible = true
emit_signal("hovered")
queue_redraw()
NOTIFICATION_MOUSE_EXIT:
is_hovered = false
name_label.visible = false
drop_data = false
queue_redraw()
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.is_pressed():
match p_event.get_button_index():
MOUSE_BUTTON_LEFT:
# If `Add new` is clicked
if !resource:
if type == Terrain3DAssets.TYPE_TEXTURE:
set_edited_resource(Terrain3DTextureAsset.new(), false)
else:
set_edited_resource(Terrain3DMeshAsset.new(), false)
edit()
else:
emit_signal("selected")
MOUSE_BUTTON_RIGHT:
if resource:
edit()
MOUSE_BUTTON_MIDDLE:
if resource:
clear()
func _can_drop_data(p_at_position: Vector2, p_data: Variant) -> bool:
drop_data = false
if typeof(p_data) == TYPE_DICTIONARY:
if p_data.files.size() == 1:
queue_redraw()
drop_data = true
return drop_data
func _drop_data(p_at_position: Vector2, p_data: Variant) -> void:
if typeof(p_data) == TYPE_DICTIONARY:
var res: Resource = load(p_data.files[0])
if res is Texture2D and type == Terrain3DAssets.TYPE_TEXTURE:
var ta := Terrain3DTextureAsset.new()
if resource is Terrain3DTextureAsset:
ta.id = resource.id
ta.set_albedo_texture(res)
set_edited_resource(ta, false)
resource = ta
elif res is Terrain3DTextureAsset and type == Terrain3DAssets.TYPE_TEXTURE:
if resource is Terrain3DTextureAsset:
res.id = resource.id
set_edited_resource(res, false)
elif res is PackedScene and type == Terrain3DAssets.TYPE_MESH:
var ma := Terrain3DMeshAsset.new()
if resource is Terrain3DMeshAsset:
ma.id = resource.id
ma.set_scene_file(res)
set_edited_resource(ma, false)
resource = ma
elif res is Terrain3DMeshAsset and type == Terrain3DAssets.TYPE_MESH:
if resource is Terrain3DMeshAsset:
res.id = resource.id
set_edited_resource(res, false)
emit_signal("selected")
emit_signal("inspected", resource)
func set_edited_resource(p_res: Resource, p_no_signal: bool = true) -> void:
resource = p_res
if resource:
resource.setting_changed.connect(_on_resource_changed)
resource.file_changed.connect(_on_resource_changed)
if button_clear:
button_clear.set_visible(resource != null)
queue_redraw()
if !p_no_signal:
emit_signal("changed", resource)
func _on_resource_changed() -> void:
emit_signal("changed", resource)
func set_selected(value: bool) -> void:
is_selected = value
queue_redraw()
func clear() -> void:
if resource:
set_edited_resource(null, false)
func edit() -> void:
emit_signal("selected")
emit_signal("inspected", resource)

View File

@@ -0,0 +1,93 @@
[gd_scene load_steps=2 format=3 uid="uid://dkb6hii5e48m2"]
[ext_resource type="Script" path="res://addons/terrain_3d/src/asset_dock.gd" id="1_e23pg"]
[node name="Terrain3D" type="PanelContainer"]
custom_minimum_size = Vector2(256, 95)
offset_right = 766.0
offset_bottom = 100.0
script = ExtResource("1_e23pg")
[node name="Box" type="BoxContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
vertical = true
[node name="Buttons" type="BoxContainer" parent="Box"]
layout_mode = 2
[node name="TexturesBtn" type="Button" parent="Box/Buttons"]
custom_minimum_size = Vector2(80, 30)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
theme_override_font_sizes/font_size = 16
toggle_mode = true
button_pressed = true
text = "Textures"
[node name="MeshesBtn" type="Button" parent="Box/Buttons"]
custom_minimum_size = Vector2(80, 30)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
theme_override_font_sizes/font_size = 16
toggle_mode = true
text = "Meshes"
[node name="PlacementOpt" type="OptionButton" parent="Box/Buttons"]
custom_minimum_size = Vector2(80, 30)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
item_count = 9
selected = 7
popup/item_0/text = "Left_UL"
popup/item_0/id = 0
popup/item_1/text = "Left_BL"
popup/item_1/id = 1
popup/item_2/text = "Left_UR"
popup/item_2/id = 2
popup/item_3/text = "Left_BR"
popup/item_3/id = 3
popup/item_4/text = "Right_UL"
popup/item_4/id = 4
popup/item_5/text = "Right_BL "
popup/item_5/id = 5
popup/item_6/text = "Right_UR"
popup/item_6/id = 6
popup/item_7/text = "Right_BR"
popup/item_7/id = 7
popup/item_8/text = "Bottom"
popup/item_8/id = 8
[node name="SizeSlider" type="HSlider" parent="Box/Buttons"]
custom_minimum_size = Vector2(80, 10)
layout_mode = 2
size_flags_horizontal = 3
min_value = 56.0
max_value = 230.0
value = 83.0
[node name="Floating" type="Button" parent="Box/Buttons"]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "Pop this dock out to a floating window."
toggle_mode = true
text = "F"
flat = true
[node name="Pinned" type="Button" parent="Box/Buttons"]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "Make this window \"Always on top\"."
toggle_mode = true
text = "P"
flat = true
[node name="ScrollContainer" type="ScrollContainer" parent="Box"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3

View File

@@ -0,0 +1,28 @@
@tool
extends ConfirmationDialog
var lod: int = 0
var description: String = ""
func _ready() -> void:
set_unparent_when_invisible(true)
about_to_popup.connect(_on_about_to_popup)
visibility_changed.connect(_on_visibility_changed)
%LodBox.value_changed.connect(_on_lod_box_value_changed)
func _on_about_to_popup() -> void:
lod = %LodBox.value
func _on_visibility_changed() -> void:
# Change text on the autowrap label only when the popup is visible.
# Works around Godot issue #47005:
# https://github.com/godotengine/godot/issues/47005
if visible:
%DescriptionLabel.text = description
func _on_lod_box_value_changed(p_value: float) -> void:
lod = %LodBox.value

View File

@@ -0,0 +1,43 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]
[ext_resource type="Script" path="res://addons/terrain_3d/src/bake_lod_dialog.gd" id="1_sf76d"]
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 155)
visible = true
script = ExtResource("1_sf76d")
[node name="MarginContainer" type="MarginContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
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="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="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "LOD:"
[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="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2

View File

@@ -0,0 +1,400 @@
extends Node
const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/src/bake_lod_dialog.tscn")
const BAKE_MESH_DESCRIPTION: String = "This will create a child MeshInstance3D. LOD4+ is recommended. LOD0 is slow and dense with vertices every 1 unit. It is not an optimal mesh."
const BAKE_OCCLUDER_DESCRIPTION: String = "This will create a child OccluderInstance3D. LOD4+ is recommended and will take 5+ seconds per region to generate. LOD0 is unnecessarily dense and slow."
const SET_UP_NAVIGATION_DESCRIPTION: String = "This operation will:
- Create a NavigationRegion3D node,
- Assign it a blank NavigationMesh resource,
- Move the Terrain3D node to be a child of the new node,
- And bake the nav mesh.
Once setup is complete, you can modify the settings on your nav mesh, and rebake
without having to run through the setup again.
If preferred, this setup can be canceled and the steps performed manually. For
the best results, adjust the settings on the NavigationMesh resource to match
the settings of your navigation agents and collisions."
var plugin: EditorPlugin
var bake_method: Callable
var bake_lod_dialog: ConfirmationDialog
var confirm_dialog: ConfirmationDialog
func _enter_tree() -> void:
bake_lod_dialog = BakeLodDialog.instantiate()
bake_lod_dialog.hide()
bake_lod_dialog.confirmed.connect(func(): bake_method.call())
bake_lod_dialog.set_unparent_when_invisible(true)
confirm_dialog = ConfirmationDialog.new()
confirm_dialog.hide()
confirm_dialog.confirmed.connect(func(): bake_method.call())
confirm_dialog.set_unparent_when_invisible(true)
func _exit_tree() -> void:
bake_lod_dialog.queue_free()
confirm_dialog.queue_free()
func bake_mesh_popup() -> void:
if plugin.terrain:
bake_method = _bake_mesh
bake_lod_dialog.description = BAKE_MESH_DESCRIPTION
EditorInterface.popup_dialog_centered(bake_lod_dialog)
func _bake_mesh() -> void:
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
var undo: EditorUndoRedoManager = plugin.get_undo_redo()
undo.create_action("Terrain3D Bake ArrayMesh")
var mesh_instance := plugin.terrain.get_node_or_null(^"MeshInstance3D") as MeshInstance3D
if !mesh_instance:
mesh_instance = MeshInstance3D.new()
mesh_instance.name = &"MeshInstance3D"
mesh_instance.set_skeleton_path(NodePath())
mesh_instance.mesh = mesh
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", EditorInterface.get_edited_scene_root())
undo.add_do_reference(mesh_instance)
else:
undo.add_do_property(mesh_instance, &"mesh", mesh)
undo.add_undo_property(mesh_instance, &"mesh", mesh_instance.mesh)
if mesh_instance.mesh.resource_path:
var path := mesh_instance.mesh.resource_path
undo.add_do_method(mesh, &"take_over_path", path)
undo.add_undo_method(mesh_instance.mesh, &"take_over_path", path)
undo.add_do_method(ResourceSaver, &"save", mesh)
undo.add_undo_method(ResourceSaver, &"save", mesh_instance.mesh)
undo.commit_action()
func bake_occluder_popup() -> void:
if plugin.terrain:
bake_method = _bake_occluder
bake_lod_dialog.description = BAKE_OCCLUDER_DESCRIPTION
EditorInterface.popup_dialog_centered(bake_lod_dialog)
func _bake_occluder() -> void:
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
assert(mesh.get_surface_count() == 1)
var undo: EditorUndoRedoManager = plugin.get_undo_redo()
undo.create_action("Terrain3D Bake Occluder3D")
var occluder := ArrayOccluder3D.new()
var arrays: Array = mesh.surface_get_arrays(0)
assert(arrays.size() > Mesh.ARRAY_INDEX)
assert(arrays[Mesh.ARRAY_INDEX] != null)
occluder.set_arrays(arrays[Mesh.ARRAY_VERTEX], arrays[Mesh.ARRAY_INDEX])
var occluder_instance := plugin.terrain.get_node_or_null(^"OccluderInstance3D") as OccluderInstance3D
if !occluder_instance:
occluder_instance = OccluderInstance3D.new()
occluder_instance.name = &"OccluderInstance3D"
occluder_instance.occluder = occluder
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", EditorInterface.get_edited_scene_root())
undo.add_do_reference(occluder_instance)
else:
undo.add_do_property(occluder_instance, &"occluder", occluder)
undo.add_undo_property(occluder_instance, &"occluder", occluder_instance.occluder)
if occluder_instance.occluder.resource_path:
var path := occluder_instance.occluder.resource_path
undo.add_do_method(occluder, &"take_over_path", path)
undo.add_undo_method(occluder_instance.occluder, &"take_over_path", path)
undo.add_do_method(ResourceSaver, &"save", occluder)
undo.add_undo_method(ResourceSaver, &"save", occluder_instance.occluder)
undo.commit_action()
func find_nav_region_terrains(p_nav_region: NavigationRegion3D) -> Array[Terrain3D]:
var result: Array[Terrain3D] = []
if not p_nav_region.navigation_mesh:
return result
var source_mode: NavigationMesh.SourceGeometryMode
source_mode = p_nav_region.navigation_mesh.geometry_source_geometry_mode
if source_mode == NavigationMesh.SOURCE_GEOMETRY_ROOT_NODE_CHILDREN:
result.append_array(p_nav_region.find_children("", "Terrain3D", true, true))
return result
var group_nodes: Array = p_nav_region.get_tree().get_nodes_in_group(p_nav_region.navigation_mesh.geometry_source_group_name)
for node in group_nodes:
if node is Terrain3D:
result.push_back(node)
if source_mode == NavigationMesh.SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN:
result.append_array(node.find_children("", "Terrain3D", true, true))
return result
func find_terrain_nav_regions(p_terrain: Terrain3D) -> Array[NavigationRegion3D]:
var result: Array[NavigationRegion3D] = []
var root: Node = EditorInterface.get_edited_scene_root()
if not root:
return result
for nav_region in root.find_children("", "NavigationRegion3D", true, true):
if find_nav_region_terrains(nav_region).has(p_terrain):
result.push_back(nav_region)
return result
func bake_nav_mesh() -> void:
if plugin.nav_region:
# A NavigationRegion3D is selected. We only need to bake that one navmesh.
_bake_nav_region_nav_mesh(plugin.nav_region)
print("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
# cut down on the amount of geometry to bake. In a large open-world RPG, for instance, there
# could be a navmesh for each town.)
var nav_regions: Array[NavigationRegion3D] = find_terrain_nav_regions(plugin.terrain)
for nav_region in nav_regions:
_bake_nav_region_nav_mesh(nav_region)
print("Terrain3DNavigation: Finished baking %d NavigationMesh(es)." % nav_regions.size())
func _bake_nav_region_nav_mesh(p_nav_region: NavigationRegion3D) -> void:
var nav_mesh: NavigationMesh = p_nav_region.navigation_mesh
assert(nav_mesh != null)
var source_geometry_data := NavigationMeshSourceGeometryData3D.new()
NavigationMeshGenerator.parse_source_geometry_data(nav_mesh, source_geometry_data, p_nav_region)
for terrain in find_nav_region_terrains(p_nav_region):
var aabb: AABB = nav_mesh.filter_baking_aabb
aabb.position += nav_mesh.filter_baking_aabb_offset
aabb = p_nav_region.global_transform * aabb
var faces: PackedVector3Array = terrain.generate_nav_mesh_source_geometry(aabb)
if not faces.is_empty():
source_geometry_data.add_faces(faces, Transform3D.IDENTITY)
NavigationMeshGenerator.bake_from_source_geometry_data(nav_mesh, source_geometry_data)
_postprocess_nav_mesh(nav_mesh)
# Assign null first to force the debug display to actually update:
p_nav_region.set_navigation_mesh(null)
p_nav_region.set_navigation_mesh(nav_mesh)
# Trigger save to disk if it is saved as an external file
if not nav_mesh.get_path().is_empty():
ResourceSaver.save(nav_mesh, nav_mesh.get_path(), ResourceSaver.FLAG_COMPRESS)
# Let other editor plugins and tool scripts know the nav mesh was just baked:
p_nav_region.bake_finished.emit()
func _postprocess_nav_mesh(p_nav_mesh: NavigationMesh) -> void:
# Post-process the nav mesh to work around Godot issue #85548
# Round all the vertices in the nav_mesh to the nearest cell_size/cell_height so that it doesn't
# contain any edges shorter than cell_size/cell_height (one cause of #85548).
var vertices: PackedVector3Array = _postprocess_nav_mesh_round_vertices(p_nav_mesh)
# Rounding vertices can collapse some edges to 0 length. We remove these edges, and any polygons
# that have been reduced to 0 area.
var polygons: Array[PackedInt32Array] = _postprocess_nav_mesh_remove_empty_polygons(p_nav_mesh, vertices)
# Another cause of #85548 is baking producing overlapping polygons. We remove these.
_postprocess_nav_mesh_remove_overlapping_polygons(p_nav_mesh, vertices, polygons)
p_nav_mesh.clear_polygons()
p_nav_mesh.set_vertices(vertices)
for polygon in polygons:
p_nav_mesh.add_polygon(polygon)
func _postprocess_nav_mesh_round_vertices(p_nav_mesh: NavigationMesh) -> PackedVector3Array:
assert(p_nav_mesh != null)
assert(p_nav_mesh.cell_size > 0.0)
assert(p_nav_mesh.cell_height > 0.0)
var cell_size: Vector3 = Vector3(p_nav_mesh.cell_size, p_nav_mesh.cell_height, p_nav_mesh.cell_size)
# Round a little harder to avoid rounding errors with non-power-of-two cell_size/cell_height
# causing the navigation map to put two non-matching edges in the same cell:
var round_factor := cell_size * 1.001
var vertices: PackedVector3Array = p_nav_mesh.get_vertices()
for i in range(vertices.size()):
vertices[i] = (vertices[i] / round_factor).floor() * round_factor
return vertices
func _postprocess_nav_mesh_remove_empty_polygons(p_nav_mesh: NavigationMesh, p_vertices: PackedVector3Array) -> Array[PackedInt32Array]:
var polygons: Array[PackedInt32Array] = []
for i in range(p_nav_mesh.get_polygon_count()):
var old_polygon: PackedInt32Array = p_nav_mesh.get_polygon(i)
var new_polygon: PackedInt32Array = []
# Remove duplicate vertices (introduced by rounding) from the polygon:
var polygon_vertices: PackedVector3Array = []
for index in old_polygon:
var vertex: Vector3 = p_vertices[index]
if polygon_vertices.has(vertex):
continue
polygon_vertices.push_back(vertex)
new_polygon.push_back(index)
# If we removed some vertices, we might be able to remove the polygon too:
if new_polygon.size() <= 2:
continue
polygons.push_back(new_polygon)
return polygons
func _postprocess_nav_mesh_remove_overlapping_polygons(p_nav_mesh: NavigationMesh, p_vertices: PackedVector3Array, p_polygons: Array[PackedInt32Array]) -> void:
# Occasionally, a baked nav mesh comes out with overlapping polygons:
# https://github.com/godotengine/godot/issues/85548#issuecomment-1839341071
# Until the bug is fixed in the engine, this function attempts to detect and remove overlapping
# polygons.
# This function has to make a choice of which polygon to remove when an overlap is detected,
# because in this case the nav mesh is ambiguous. To do this it uses a heuristic:
# (1) an 'overlap' is defined as an edge that is shared by 3 or more polygons.
# (2) a 'bad polygon' is defined as a polygon that contains 2 or more 'overlaps'.
# The function removes the 'bad polygons', which in practice seems to be enough to remove all
# overlaps without creating holes in the nav mesh.
var cell_size: Vector3 = Vector3(p_nav_mesh.cell_size, p_nav_mesh.cell_height, p_nav_mesh.cell_size)
# `edges` is going to map edges (vertex pairs) to arrays of polygons that contain that edge.
var edges: Dictionary = {}
for polygon_index in range(p_polygons.size()):
var polygon: PackedInt32Array = p_polygons[polygon_index]
for j in range(polygon.size()):
var vertex: Vector3 = p_vertices[polygon[j]]
var next_vertex: Vector3 = p_vertices[polygon[(j + 1) % polygon.size()]]
# edge_key is a key we can use in the edges dictionary that uniquely identifies the
# edge. We use cell coordinates here (Vector3i) because with a non-power-of-two
# cell_size, rounding errors can cause Vector3 vertices to not be equal.
# Array.sort IS defined for vector types - see the Godot docs. It's necessary here
# because polygons that share an edge can have their vertices in a different order.
var edge_key: Array = [Vector3i(vertex / cell_size), Vector3i(next_vertex / cell_size)]
edge_key.sort()
if !edges.has(edge_key):
edges[edge_key] = []
edges[edge_key].push_back(polygon_index)
var overlap_count: Dictionary = {}
for connections in edges.values():
if connections.size() <= 2:
continue
for polygon_index in connections:
overlap_count[polygon_index] = overlap_count.get(polygon_index, 0) + 1
var bad_polygons: Array = []
for polygon_index in overlap_count.keys():
if overlap_count[polygon_index] >= 2:
bad_polygons.push_back(polygon_index)
bad_polygons.sort()
for i in range(bad_polygons.size() - 1, -1, -1):
p_polygons.remove_at(bad_polygons[i])
func set_up_navigation_popup() -> void:
if plugin.terrain:
bake_method = _set_up_navigation
confirm_dialog.dialog_text = SET_UP_NAVIGATION_DESCRIPTION
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()
nav_region.name = &"NavigationRegion3D"
nav_region.navigation_mesh = NavigationMesh.new()
var undo_redo: EditorUndoRedoManager = plugin.get_undo_redo()
undo_redo.create_action("Terrain3D Set up Navigation")
undo_redo.add_do_method(self, &"_do_set_up_navigation", nav_region, terrain)
undo_redo.add_undo_method(self, &"_undo_set_up_navigation", nav_region, terrain)
undo_redo.add_do_reference(nav_region)
undo_redo.commit_action()
EditorInterface.inspect_object(nav_region)
assert(plugin.nav_region == nav_region)
bake_nav_mesh()
func _do_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrain3D) -> void:
var parent: Node = p_terrain.get_parent()
var index: int = p_terrain.get_index()
var t_owner: Node = p_terrain.owner
parent.remove_child(p_terrain)
p_nav_region.add_child(p_terrain)
parent.add_child(p_nav_region, true)
parent.move_child(p_nav_region, index)
p_nav_region.owner = t_owner
p_terrain.owner = t_owner
func _undo_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrain3D) -> void:
assert(p_terrain.get_parent() == p_nav_region)
var parent: Node = p_nav_region.get_parent()
var index: int = p_nav_region.get_index()
var t_owner: Node = p_nav_region.get_owner()
parent.remove_child(p_nav_region)
p_nav_region.remove_child(p_terrain)
parent.add_child(p_terrain, true)
parent.move_child(p_terrain, index)
p_terrain.owner = t_owner

View File

@@ -0,0 +1,463 @@
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 {
INFO,
WARN,
ERROR,
}
enum {
IMAGE_ALBEDO,
IMAGE_HEIGHT,
IMAGE_NORMAL,
IMAGE_ROUGHNESS
}
var plugin: EditorPlugin
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
var queue_pack_normal_roughness: bool = false
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 window != null:
window.show()
window.move_to_foreground()
window.move_to_center()
return
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"
(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]
window.queue_free()
window = null
func _init_file_dialogs() -> void:
save_file_dialog = EditorFileDialog.new()
save_file_dialog.set_filters(PackedStringArray(["*.png"]))
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)
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") 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
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
_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)
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_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_message(p_level: int, p_text: String) -> void:
status_label.text = p_text
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()
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()
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_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"
save_file_dialog.popup_centered_ratio()
if packing_normal_roughness:
queue_pack_normal_roughness = true
return
if packing_normal_roughness:
save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness"
save_file_dialog.title = "Save Packed Normal/Roughness Texture"
save_file_dialog.popup_centered_ratio()
func _on_save_file_selected(p_dst_path) -> void:
last_saved_directory = p_dst_path.get_base_dir() + "/"
var error: int
if packing_albedo:
error = _pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false,
invert_height_checkbox.button_pressed, false, height_channel_selected)
else:
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 _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() 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_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)
_create_import_file(p_dst_path)
_show_message(INFO, "Packed to " + p_dst_path + ".")
return OK
else:
_show_message(ERROR, "Failed to load one or more textures")
return FAILED

View File

@@ -0,0 +1,553 @@
[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)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lcvna"]
bg_color = Color(0.168627, 0.211765, 0.266667, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.270588, 0.435294, 0.580392, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cb0xf"]
bg_color = Color(0.137255, 0.137255, 0.137255, 1)
draw_center = false
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.784314, 0.784314, 0.784314, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"]
[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(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
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf")
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.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
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="AlbedoHeightPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"]
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"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
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="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer"]
layout_mode = 2
[node name="AlbedoVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="AlbedoLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
text = "Albedo texture"
[node name="AlbedoHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
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
[node name="HeightLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
text = "Height texture"
[node name="HeightHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
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, 290)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
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="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer"]
layout_mode = 2
[node name="NormalVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="NormalLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
text = "Normal texture"
[node name="NormalHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="NormalWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
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
size_flags_horizontal = 3
[node name="RoughnessLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
text = "Roughness texture"
[node name="RoughnessHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[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, 60)
layout_mode = 2
horizontal_alignment = 1
autowrap_mode = 3
[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"]

View File

@@ -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])

View File

@@ -0,0 +1,32 @@
[remap]
importer="texture"
type="CompressedTexture2D"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="$SOURCE_FILE"
[params]
compress/mode=2
compress/high_quality=$HIGH_QUALITY
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=2
compress/channel_pack=0
mipmaps/generate=$GENERATE_MIPMAPS
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

View File

@@ -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()

View File

@@ -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

View File

@@ -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))

View File

@@ -0,0 +1,55 @@
extends "res://addons/terrain_3d/src/operation_builder.gd"
const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd")
func _get_point_picker() -> MultiPicker:
return tool_settings.settings["gradient_points"]
func _get_brush_size() -> float:
return tool_settings.get_setting("size")
func _is_drawable() -> bool:
return tool_settings.get_setting("drawable")
func is_picking() -> bool:
return not _get_point_picker().all_points_selected()
func pick(p_global_position: Vector3, p_terrain: Terrain3D) -> void:
if not _get_point_picker().all_points_selected():
_get_point_picker().add_point(p_global_position)
func is_ready() -> bool:
return _get_point_picker().all_points_selected() and not _is_drawable()
func apply_operation(p_editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void:
var points: PackedVector3Array = _get_point_picker().get_points()
assert(points.size() == 2)
assert(not _is_drawable())
var brush_size: float = _get_brush_size()
assert(brush_size > 0.0)
var start: Vector3 = points[0]
var end: Vector3 = points[1]
p_editor.start_operation(start)
var dir: Vector3 = (end - start).normalized()
var pos: Vector3 = start
while dir.dot(end - pos) > 0.0:
p_editor.operate(pos, p_camera_direction)
pos += dir * brush_size * 0.2
p_editor.stop_operation()
_get_point_picker().clear()

View File

@@ -0,0 +1,86 @@
extends HBoxContainer
signal pressed
signal value_changed
const ICON_PICKER_CHECKED: String = "res://addons/terrain_3d/icons/picker_checked.svg"
const MAX_POINTS: int = 2
var icon_picker: Texture2D
var icon_picker_checked: Texture2D
var points: PackedVector3Array
var picking_index: int = -1
func _enter_tree() -> void:
icon_picker = get_theme_icon("ColorPick", "EditorIcons")
icon_picker_checked = load(ICON_PICKER_CHECKED)
points.resize(MAX_POINTS)
for i in range(MAX_POINTS):
var button := Button.new()
button.icon = icon_picker
button.tooltip_text = "Pick point on the Terrain"
button.set_meta(&"point_index", i)
button.pressed.connect(_on_button_pressed.bind(i))
add_child(button)
_update_buttons()
func _on_button_pressed(button_index: int) -> void:
points[button_index] = Vector3.ZERO
picking_index = button_index
_update_buttons()
pressed.emit()
func _update_buttons() -> void:
for child in get_children():
if child is Button:
_update_button(child)
func _update_button(button: Button) -> void:
var index: int = button.get_meta(&"point_index")
if points[index] != Vector3.ZERO:
button.icon = icon_picker_checked
else:
button.icon = icon_picker
func clear() -> void:
points.fill(Vector3.ZERO)
_update_buttons()
value_changed.emit()
func all_points_selected() -> bool:
return points.count(Vector3.ZERO) == 0
func add_point(p_value: Vector3) -> void:
if points.has(p_value):
return
# If manually selecting a point individually
if picking_index != -1:
points[picking_index] = p_value
picking_index = -1
else:
# Else picking a sequence of points (non-drawable)
for i in range(MAX_POINTS):
if points[i] == Vector3.ZERO:
points[i] = p_value
break
_update_buttons()
value_changed.emit()
func get_points() -> PackedVector3Array:
return points

View File

@@ -0,0 +1,23 @@
extends RefCounted
const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd")
var tool_settings: ToolSettings
func is_picking() -> bool:
return false
func pick(p_global_position: Vector3, p_terrain: Terrain3D) -> void:
pass
func is_ready() -> bool:
return false
func apply_operation(editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void:
pass

View File

@@ -0,0 +1,66 @@
extends EditorNode3DGizmo
var material: StandardMaterial3D
var selection_material: StandardMaterial3D
var region_position: Vector2
var region_size: float
var grid: Array[Vector2i]
var use_secondary_color: bool = false
var show_rect: bool = true
var main_color: Color = Color.GREEN_YELLOW
var secondary_color: Color = Color.RED
var grid_color: Color = Color.WHITE
var border_color: Color = Color.BLUE
func _init() -> void:
material = StandardMaterial3D.new()
material.set_flag(BaseMaterial3D.FLAG_DISABLE_DEPTH_TEST, true)
material.set_flag(BaseMaterial3D.FLAG_ALBEDO_FROM_VERTEX_COLOR, true)
material.set_shading_mode(BaseMaterial3D.SHADING_MODE_UNSHADED)
material.set_albedo(Color.WHITE)
selection_material = material.duplicate()
selection_material.set_render_priority(0)
func _redraw() -> void:
clear()
var rect_position = region_position * region_size
if show_rect:
var modulate: Color = main_color if !use_secondary_color else secondary_color
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)
for pos in grid:
var grid_tile_position = Vector2(pos) * region_size
if show_rect and grid_tile_position == rect_position:
# Skip this one, otherwise focused region borders are not always visible due to draw order
continue
draw_rect(Vector2(region_size,region_size)*.5 + grid_tile_position, region_size, material, grid_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:
var lines: PackedVector3Array = [
Vector3(-1, 0, -1),
Vector3(-1, 0, 1),
Vector3(1, 0, 1),
Vector3(1, 0, -1),
Vector3(-1, 0, 1),
Vector3(1, 0, 1),
Vector3(1, 0, -1),
Vector3(-1, 0, -1),
]
for i in lines.size():
lines[i] = ((lines[i] / 2.0) * p_size) + Vector3(p_pos.x, 0, p_pos.y)
add_lines(lines, p_material, false, p_modulate)

View File

@@ -0,0 +1,81 @@
extends HBoxContainer
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 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_SEPARATOR2,
MENU_SET_UP_NAVIGATION,
MENU_BAKE_NAV_MESH,
}
func _enter_tree() -> void:
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("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("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 _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_SET_UP_NAVIGATION:
baker.set_up_navigation_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)
if plugin.terrain:
var nav_regions: Array[NavigationRegion3D] = baker.find_terrain_nav_regions(plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, nav_regions.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, nav_regions.size() != 0)
elif plugin.nav_region:
var terrains: Array[Terrain3D] = baker.find_nav_region_terrains(plugin.nav_region)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, terrains.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)
else:
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, true)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)

View File

@@ -0,0 +1,670 @@
extends PanelContainer
signal picking(type, callback)
signal setting_changed
enum Layout {
HORIZONTAL,
VERTICAL,
GRID,
}
enum SettingType {
CHECKBOX,
COLOR_SELECT,
DOUBLE_SLIDER,
OPTION,
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 ES_TOOL_SETTINGS: String = "terrain3d/tool_settings/"
# Add settings flags
const NONE: int = 0x0
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 # 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: HFlowContainer
var advanced_list: VBoxContainer
var height_list: VBoxContainer
var scale_list: VBoxContainer
var rotation_list: VBoxContainer
var color_list: VBoxContainer
var settings: Dictionary = {}
func _ready() -> void:
# 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)
add_brushes(main_list)
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":33,
"unit":"%", "range":Vector3(1, 100, 1), "flags":ALLOW_LARGER })
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 })
add_setting({ "name":"color", "type":SettingType.COLOR_SELECT, "list":main_list,
"default":Color.WHITE, "flags":ADD_SEPARATOR })
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":-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, "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,
"unit":"%", "range":Vector3(0, 337.5, 22.5), "flags":NO_LABEL })
add_setting({ "name":"angle_picker", "type":SettingType.PICKER, "list":main_list,
"default":Terrain3DEditor.ANGLE, "flags":NO_LABEL })
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,
"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 sculpting brush
add_setting({ "name":"gradient_points", "type":SettingType.MULTI_PICKER, "label":"Points",
"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)
## Instancer
height_list = create_submenu(main_list, "Height", Layout.VERTICAL)
add_setting({ "name":"height_offset", "type":SettingType.SLIDER, "list":height_list, "default":0,
"unit":"m", "range":Vector3(-10, 10, 0.05), "flags":ALLOW_OUT_OF_BOUNDS })
add_setting({ "name":"random_height", "label":"Random Height ±", "type":SettingType.SLIDER,
"list":height_list, "default":0, "unit":"m", "range":Vector3(0, 10, 0.05),
"flags":ALLOW_OUT_OF_BOUNDS })
scale_list = create_submenu(main_list, "Scale", Layout.VERTICAL)
add_setting({ "name":"fixed_scale", "type":SettingType.SLIDER, "list":scale_list, "default":100,
"unit":"%", "range":Vector3(1, 1000, 1), "flags":ALLOW_OUT_OF_BOUNDS })
add_setting({ "name":"random_scale", "label":"Random Scale ±", "type":SettingType.SLIDER, "list":scale_list,
"default":20, "unit":"%", "range":Vector3(0, 99, 1), "flags":ALLOW_OUT_OF_BOUNDS })
rotation_list = create_submenu(main_list, "Rotation", Layout.VERTICAL)
add_setting({ "name":"fixed_spin", "label":"Fixed Spin (Around Y)", "type":SettingType.SLIDER, "list":rotation_list,
"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_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_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 })
color_list = create_submenu(main_list, "Color", Layout.VERTICAL)
add_setting({ "name":"vertex_color", "type":SettingType.COLOR_SELECT, "list":color_list,
"default":Color.WHITE })
add_setting({ "name":"random_hue", "label":"Random Hue Shift ±", "type":SettingType.SLIDER,
"list":color_list, "default":0, "unit":"°", "range":Vector3(0, 360, 1) })
add_setting({ "name":"random_darken", "type":SettingType.SLIDER, "list":color_list, "default":50,
"unit":"%", "range":Vector3(0, 100, 1) })
#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, "", 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,
"default":true })
advanced_list.add_child(HSeparator.new(), true)
add_setting({ "name":"gamma", "type":SettingType.SLIDER, "list":advanced_list, "default":1.0,
"unit":"γ", "range":Vector3(0.1, 2.0, 0.01) })
add_setting({ "name":"jitter", "type":SettingType.SLIDER, "list":advanced_list, "default":50,
"unit":"%", "range":Vector3(0, 100, 1) })
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()
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.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
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):
Layout.GRID:
sublist = GridContainer.new()
Layout.VERTICAL:
sublist = VBoxContainer.new()
Layout.HORIZONTAL, _:
sublist = HBoxContainer.new()
p_parent.add_child(menu_button, true)
menu_button.add_child(submenu, true)
submenu.add_child(sublist, true)
return sublist
func _on_show_submenu(p_toggled: bool, p_button: Button) -> void:
# Don't show if mouse already down (from painting)
if p_toggled and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
return
# 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 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)
popup.set_visible(p_toggled)
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)
brush_list.name = "BrushList"
var brush_button_group: ButtonGroup = ButtonGroup.new()
brush_button_group.pressed.connect(_on_setting_changed)
var default_brush_btn: Button
var dir: DirAccess = DirAccess.open(BRUSH_PATH)
if dir:
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
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()
btn.set_custom_minimum_size(Vector2.ONE * 100)
btn.set_button_icon(tex)
btn.set_meta("image", img)
btn.set_expand_icon(true)
btn.set_material(_get_brush_preview_material())
btn.set_toggle_mode(true)
btn.set_button_group(brush_button_group)
btn.mouse_entered.connect(_on_brush_hover.bind(true, btn))
btn.mouse_exited.connect(_on_brush_hover.bind(false, btn))
brush_list.add_child(btn, true)
if file_name == DEFAULT_BRUSH:
default_brush_btn = btn
var lbl: Label = Label.new()
btn.name = file_name.get_basename().to_pascal_case()
btn.add_child(lbl, true)
lbl.text = btn.name
lbl.visible = false
lbl.position.y = 70
lbl.add_theme_color_override("font_shadow_color", Color.BLACK)
lbl.add_theme_constant_override("shadow_offset_x", 1)
lbl.add_theme_constant_override("shadow_offset_y", 1)
lbl.add_theme_font_size_override("font_size", 16)
file_name = dir.get_next()
brush_list.columns = sqrt(brush_list.get_child_count()) + 2
if not default_brush_btn:
default_brush_btn = brush_button_group.get_buttons()[0]
default_brush_btn.set_pressed(true)
settings["brush"] = brush_button_group
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_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:
if p_button.get_child_count() > 0:
var child = p_button.get_child(0)
if child is Label:
if p_hovering:
child.visible = true
else:
child.visible = false
func _on_pick(p_type: Terrain3DEditor.Tool) -> void:
emit_signal("picking", p_type, _on_picked)
func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3) -> void:
match p_type:
Terrain3DEditor.HEIGHT:
settings["height"].value = p_color.r if not is_nan(p_color.r) else 0
Terrain3DEditor.COLOR:
settings["color"].color = p_color if not is_nan(p_color.r) else Color.WHITE
Terrain3DEditor.ROUGHNESS:
# 200... -.5 converts 0,1 to -100,100
settings["roughness"].value = round(200 * (p_color.a - 0.5)) if not is_nan(p_color.r) else 0.499
Terrain3DEditor.ANGLE:
settings["angle"].value = p_color.r
Terrain3DEditor.SCALE:
settings["scale"].value = p_color.r
_on_setting_changed()
func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void:
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.SCULPT)
var point: Vector3 = p_global_position
point.y = p_color.r
settings[p_name].add_point(point)
_on_setting_changed()
func add_setting(p_args: Dictionary) -> void:
var p_name: StringName = p_args.get("name", "")
var p_label: String = p_args.get("label", "") # Optional replacement for name
var p_type: SettingType = p_args.get("type", SettingType.TYPE_MAX)
var p_list: Control = p_args.get("list")
var p_default: Variant = p_args.get("default")
var p_suffix: String = p_args.get("unit", "")
var p_range: Vector3 = p_args.get("range", Vector3(0, 0, 1))
var p_minimum: float = p_range.x
var p_maximum: float = p_range.y
var p_step: float = p_range.z
var p_flags: int = p_args.get("flags", NONE)
if p_name.is_empty() or p_type == SettingType.TYPE_MAX:
return
var container: HBoxContainer = HBoxContainer.new()
container.set_v_size_flags(SIZE_EXPAND_FILL)
var control: Control # Houses the setting to be saved
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()
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
SettingType.COLOR_SELECT:
var picker := ColorPickerButton.new()
picker.set_custom_minimum_size(Vector2(100, 25))
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)
pending_children.push_back(picker)
control = picker
SettingType.PICKER:
var button := Button.new()
button.set_v_size_flags(SIZE_SHRINK_CENTER)
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)
control = button
SettingType.MULTI_PICKER:
var multi_picker: HBoxContainer = MultiPicker.new()
multi_picker.pressed.connect(_on_point_pick.bind(p_default, p_name))
multi_picker.value_changed.connect(_on_setting_changed)
pending_children.push_back(multi_picker)
control = multi_picker
SettingType.OPTION:
var option := OptionButton.new()
for i in int(p_maximum):
option.add_item("a", i)
option.selected = p_minimum
option.item_selected.connect(_on_setting_changed)
pending_children.push_back(option)
control = option
SettingType.SLIDER, SettingType.DOUBLE_SLIDER:
var slider: Control
if p_type == SettingType.SLIDER:
# Create an editable value box
var spin_slider := EditorSpinSlider.new()
spin_slider.set_flat(false)
spin_slider.set_hide_slider(true)
spin_slider.value_changed.connect(_on_setting_changed)
spin_slider.set_max(p_maximum)
spin_slider.set_min(p_minimum)
spin_slider.set_step(p_step)
spin_slider.set_suffix(p_suffix)
spin_slider.set_v_size_flags(SIZE_SHRINK_CENTER)
spin_slider.set_custom_minimum_size(Vector2(65, 0))
# Create horizontal slider linked to the above box
slider = HSlider.new()
slider.share(spin_slider)
if p_flags & ALLOW_LARGER:
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(60, 0))
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT)
slider = DoubleSlider.new()
slider.label = label
slider.suffix = p_suffix
slider.value_changed.connect(_on_setting_changed)
pending_children.push_back(slider)
pending_children.push_back(label)
control = slider
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(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
# Setup button labels
if not (p_flags & NO_LABEL):
# Labels are actually buttons styled to look like labels
var label := Button.new()
label.set("theme_override_styles/normal", get_theme_stylebox("normal", "Label"))
label.set("theme_override_styles/hover", get_theme_stylebox("normal", "Label"))
label.set("theme_override_styles/pressed", get_theme_stylebox("normal", "Label"))
label.set("theme_override_styles/focus", get_theme_stylebox("normal", "Label"))
label.pressed.connect(_on_label_pressed.bind(p_name, p_default))
if p_label.is_empty():
label.set_text(p_name.capitalize() + ": ")
else:
label.set_text(p_label.capitalize() + ": ")
pending_children.push_front(label)
# Add separators to front
if p_flags & ADD_SEPARATOR:
pending_children.push_front(VSeparator.new())
if p_flags & ADD_SPACER:
var spacer := Control.new()
spacer.set_custom_minimum_size(Vector2(5, 0))
pending_children.push_front(spacer)
# Add all children to container and list
for child in pending_children:
container.add_child(child, true)
p_list.add_child(container, true)
# If label button is pressed, reset value to default or toggle checkbox
func _on_label_pressed(p_name: String, p_default: Variant) -> void:
var control: Control = settings.get(p_name)
if not control:
return
if control is CheckBox:
set_setting(p_name, !control.button_pressed)
elif p_default != null:
set_setting(p_name, p_default)
func get_settings() -> Dictionary:
var dict: Dictionary
for key in settings.keys():
dict[key] = get_setting(key)
return dict
func get_setting(p_setting: String) -> Variant:
var object: Object = settings.get(p_setting)
var value: Variant
if object is Range:
value = object.get_value()
# Adjust widths of all sliders on update of values
var digits: float = count_digits(value)
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 = 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 ]
elif object is CheckBox:
value = object.is_pressed()
elif object is ColorPickerButton:
value = object.color
elif object is MultiPicker:
value = object.get_points()
if value == null:
value = 0
return value
func set_setting(p_setting: String, p_value: Variant) -> void:
var object: Object = settings.get(p_setting)
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 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():
if button.name == p_value[0]:
button.button_pressed = p_value[1]
elif object is CheckBox:
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)
func show_settings(p_settings: PackedStringArray) -> void:
for setting in settings.keys():
var object: Object = settings[setting]
if object is Control:
if setting in p_settings:
object.get_parent().show()
else:
object.get_parent().hide()
if select_brush_button:
if not "brush" in p_settings:
select_brush_button.hide()
else:
select_brush_button.show()
func _on_setting_changed(p_data: Variant = null) -> void:
# If a button was clicked on a submenu
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())
# 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")
func _on_drawable_toggled(p_button_pressed: bool) -> void:
if not p_button_pressed:
settings["gradient_points"].clear()
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"
code += " v_vertex_color = COLOR;\n"
code += "}\n"
code += "void fragment(){\n"
code += " vec4 tex = texture(TEXTURE, UV);\n"
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
for i in range(5, 0, -1):
if abs(p_value) >= pow(10, i):
count = i+1
break
if p_value - floor(p_value) >= .1:
count += 1 # For the decimal
if p_value*10 - floor(p_value*10.) >= .1:
count += 1
if p_value*100 - floor(p_value*100.) >= .1:
count += 1
if p_value*1000 - floor(p_value*1000.) >= .1:
count += 1
# Negative sign
if p_value < 0:
count += 1
return count

View File

@@ -0,0 +1,143 @@
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_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"
const ICON_PAINT_TEXTURE: String = "res://addons/terrain_3d/icons/texture_paint.svg"
const ICON_SPRAY_TEXTURE: String = "res://addons/terrain_3d/icons/texture_spray.svg"
const ICON_COLOR: String = "res://addons/terrain_3d/icons/color_paint.svg"
const ICON_WETNESS: String = "res://addons/terrain_3d/icons/wetness.svg"
const ICON_AUTOSHADER: String = "res://addons/terrain_3d/icons/autoshader.svg"
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 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:
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_child(HSeparator.new())
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 })
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_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)
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:
# 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))

538
addons/terrain_3d/src/ui.gd Normal file
View File

@@ -0,0 +1,538 @@
extends Node
#class_name Terrain3DUI Cannot be named until Godot #75388
# 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 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(.02, .02, .02)
const COLOR_SMOOTH := Color(0.5, 0, .1)
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.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(.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 tool_settings: ToolSettings
var terrain_menu: TerrainMenu
var setting_has_changed: bool = false
var visible: bool = false
var picking: int = Terrain3DEditor.TOOL_MAX
var picking_callback: Callable
var decal: Decal
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:
toolbar = Toolbar.new()
toolbar.hide()
toolbar.connect("tool_changed", _on_tool_changed)
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_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, tool_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_menu)
_on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD)
decal = Decal.new()
add_child(decal)
decal_timer = Timer.new()
decal_timer.wait_time = .5
decal_timer.one_shot = true
decal_timer.timeout.connect(Callable(func(node):
if node:
get_tree().create_tween().tween_property(node, "albedo_mix", 0.0, 0.15)).bind(decal))
add_child(decal_timer)
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, tool_settings)
toolbar.queue_free()
tool_settings.queue_free()
terrain_menu.queue_free()
decal.queue_free()
decal_timer.queue_free()
for gradient_decal in gradient_decals:
gradient_decal.queue_free()
gradient_decals.clear()
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
func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
clear_picking()
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")
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")
to_show.push_back("dynamic_angle")
to_show.push_back("enable_scale")
to_show.push_back("scale")
to_show.push_back("scale_picker")
Terrain3DEditor.COLOR:
to_show.push_back("brush")
to_show.push_back("size")
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")
to_show.push_back("size")
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("remove")
Terrain3DEditor.INSTANCER:
to_show.push_back("size")
to_show.push_back("strength")
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(tool_settings.scale_list, true)
to_show.push_back("fixed_scale")
to_show.push_back("random_scale")
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_tilt")
to_show.push_back("random_tilt")
to_show.push_back("align_to_normal")
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("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")
tool_settings.show_settings(to_show)
operation_builder = null
if p_operation == Terrain3DEditor.GRADIENT:
operation_builder = GradientOperationBuilder.new()
operation_builder.tool_settings = tool_settings
if plugin.editor:
plugin.editor.set_tool(p_tool)
plugin.editor.set_operation(_modify_operation(p_operation))
last_tool = p_tool
last_operation = p_operation
_on_setting_changed()
plugin.update_region_grid()
func _on_setting_changed() -> void:
if not plugin.asset_dock:
return
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:
# If not a state that should show the decal, hide everything and return
if not visible or \
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
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.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):
decal.rotation.y = cam.rotation.y
else:
decal.rotation.y = 0
# Set texture and color
if picking != Terrain3DEditor.TOOL_MAX:
decal.texture_albedo = ring_texture
decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing()
match picking:
Terrain3DEditor.HEIGHT:
decal.modulate = COLOR_PICK_HEIGHT
Terrain3DEditor.COLOR:
decal.modulate = COLOR_PICK_COLOR
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_PICK_ROUGH
decal.modulate.a = 1.0
else:
decal.texture_albedo = brush_data["brush"][1]
match plugin.editor.get_tool():
Terrain3DEditor.SCULPT:
match plugin.editor.get_operation():
Terrain3DEditor.ADD:
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:
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.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 = .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 = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.COLOR:
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 = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.AUTOSHADER:
decal.modulate = COLOR_AUTOSHADER
decal.modulate.a = .7
Terrain3DEditor.HOLES:
decal.modulate = COLOR_HOLES
decal.modulate.a = .85
Terrain3DEditor.NAVIGATION:
decal.modulate = COLOR_NAVIGATION
decal.modulate.a = .85
Terrain3DEditor.INSTANCER:
decal.texture_albedo = ring_texture
decal.modulate = COLOR_INSTANCER
decal.modulate.a = 1.0
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
if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT:
var index := 0
for point in brush_data["gradient_points"]:
if point != Vector3.ZERO:
var point_decal: Decal = _get_gradient_decal(index)
point_decal.visible = true
point_decal.position = point
index += 1
update_compatibility_decal()
func _get_gradient_decal(index: int) -> Decal:
if gradient_decals.size() > index:
return gradient_decals[index]
var gradient_decal := Decal.new()
gradient_decal = Decal.new()
gradient_decal.texture_albedo = ring_texture
gradient_decal.modulate = COLOR_SLOPE
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)
gradient_decals.push_back(gradient_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
func _on_picking(p_type: int, p_callback: Callable) -> void:
picking = p_type
picking_callback = p_callback
update_decal()
func clear_picking() -> void:
picking = Terrain3DEditor.TOOL_MAX
func is_picking() -> bool:
if picking != Terrain3DEditor.TOOL_MAX:
return true
if operation_builder and operation_builder.is_picking():
return true
return false
func pick(p_global_position: Vector3) -> void:
if picking != Terrain3DEditor.TOOL_MAX:
var color: Color
match picking:
Terrain3DEditor.HEIGHT, Terrain3DEditor.SCULPT:
color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_HEIGHT, p_global_position)
Terrain3DEditor.ROUGHNESS:
color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_COLOR, p_global_position)
Terrain3DEditor.COLOR:
color = plugin.terrain.data.get_color(p_global_position)
Terrain3DEditor.ANGLE:
color = Color(plugin.terrain.data.get_control_angle(p_global_position), 0., 0., 1.)
Terrain3DEditor.SCALE:
color = Color(plugin.terrain.data.get_control_scale(p_global_position), 0., 0., 1.)
_:
push_error("Unsupported picking type: ", picking)
return
picking_callback.call(picking, color, p_global_position)
picking = Terrain3DEditor.TOOL_MAX
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")