👽 interstellar delivery

This commit is contained in:
anyreso 2024-10-16 21:43:01 +00:00
parent 547c97bfba
commit 97c8292858
257 changed files with 7309 additions and 4637 deletions

2
.gitignore vendored
View file

@ -19,4 +19,4 @@ addons/godot-jolt/windows/~godot-jolt_windows-x64_editor.dll
addons/terrain_3d/bin/~libterrain.windows.debug.x86_64.dll addons/terrain_3d/bin/~libterrain.windows.debug.x86_64.dll
build build
dist

View file

@ -8,10 +8,9 @@ Download `Godot Engine` at https://godotengine.org/download
## Getting started ## Getting started
1. Clone the repository 1. Clone the repository and open `Godot Engine`
2. Open Godot Engine 2. Navigate to the repository and import the `project.godot`
3. Navigate to the repository and import the `project.godot` 3. Start coding or run the project!
4. Start coding or run the project!
## Contributing ## Contributing
@ -21,7 +20,7 @@ We welcome contributions from the community, feel free to submit pull requests,
## Community ## Community
Connect with fellow contributors, ask questions and stay updated on the latest developments by joining our [Discord](https://discord.gg/tdmV3MxCn5). Connect, ask questions and stay updated on the latest developments by joining our [Discord](https://discord.gg/tdmV3MxCn5).
## License ## License

View file

@ -17,7 +17,7 @@ See [Project Status](https://terrain3d.readthedocs.io/en/stable/docs/project_sta
## Getting Started ## Getting Started
1. Read through our [documentation](https://terrain3d.readthedocs.io/en/stable/index.html), starting with [Installation](https://terrain3d.readthedocs.io/en/stable/docs/installation.html). 1. Read the [Installation](https://terrain3d.readthedocs.io/en/stable/docs/installation.html) instructions, and the rest of the [documentation](https://terrain3d.readthedocs.io/en/stable/index.html).
2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) or join our [Discord server](https://tokisan.com/discord). 2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) or join our [Discord server](https://tokisan.com/discord).

View file

@ -4,23 +4,36 @@ extends EditorPlugin
# Includes # Includes
const UI: Script = preload("res://addons/terrain_3d/editor/components/ui.gd") const UI: Script = preload("res://addons/terrain_3d/src/ui.gd")
const RegionGizmo: Script = preload("res://addons/terrain_3d/editor/components/region_gizmo.gd") const RegionGizmo: Script = preload("res://addons/terrain_3d/src/region_gizmo.gd")
const TextureDock: Script = preload("res://addons/terrain_3d/editor/components/texture_dock.gd") const ASSET_DOCK: String = "res://addons/terrain_3d/src/asset_dock.tscn"
const PS_DOCK_POSITION: String = "terrain3d/config/dock_position"
const PS_DOCK_PINNED: String = "terrain3d/config/dock_pinned"
var terrain: Terrain3D var terrain: Terrain3D
var nav_region: NavigationRegion3D var nav_region: NavigationRegion3D
var editor: Terrain3DEditor var editor: Terrain3DEditor
var ui: Node # Terrain3DUI see Godot #75388 var ui: Node # Terrain3DUI see Godot #75388
var texture_dock: TextureDock
var texture_dock_container: CustomControlContainer = CONTAINER_INSPECTOR_BOTTOM
var visible: bool
var region_gizmo: RegionGizmo var region_gizmo: RegionGizmo
var visible: bool
var current_region_position: Vector2 var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO var mouse_global_position: Vector3 = Vector3.ZERO
enum DOCK_STATE {
HIDDEN = -1,
SIDEBAR = 0,
BOTTOM = 1,
}
var asset_dock: Control
var dock_state: DOCK_STATE = -1
var dock_position: DockSlot = DOCK_SLOT_RIGHT_BL
# Track negative input (CTRL)
var _negative_input: bool = false
# Track state prior to pressing CTRL: -1 not tracked, 0 false, 1 true
var _prev_enable_state: int = -1
func _enter_tree() -> void: func _enter_tree() -> void:
editor = Terrain3DEditor.new() editor = Terrain3DEditor.new()
@ -28,27 +41,38 @@ func _enter_tree() -> void:
ui.plugin = self ui.plugin = self
add_child(ui) add_child(ui)
texture_dock = TextureDock.new()
texture_dock.hide()
texture_dock.resource_changed.connect(_on_texture_dock_resource_changed)
texture_dock.resource_inspected.connect(_on_texture_dock_resource_selected)
texture_dock.resource_selected.connect(ui._on_setting_changed)
region_gizmo = RegionGizmo.new() region_gizmo = RegionGizmo.new()
add_control_to_container(texture_dock_container, texture_dock)
texture_dock.get_parent().visibility_changed.connect(_on_texture_dock_visibility_changed)
scene_changed.connect(_on_scene_changed)
if ProjectSettings.has_setting(PS_DOCK_POSITION):
dock_position = ProjectSettings.get_setting(PS_DOCK_POSITION)
asset_dock = load(ASSET_DOCK).instantiate()
await asset_dock.ready
if ProjectSettings.has_setting(PS_DOCK_PINNED):
asset_dock.placement_pin.button_pressed = ProjectSettings.get_setting(PS_DOCK_PINNED)
asset_dock.placement_pin.toggled.connect(_on_asset_dock_pin_changed)
asset_dock.placement_option.selected = dock_position
asset_dock.placement_changed.connect(_on_asset_dock_placement_changed)
asset_dock.resource_changed.connect(_on_asset_dock_resource_changed)
asset_dock.resource_inspected.connect(_on_asset_dock_resource_inspected)
asset_dock.resource_selected.connect(_on_asset_dock_resource_selected)
func _exit_tree() -> void: func _exit_tree() -> void:
remove_control_from_container(texture_dock_container, texture_dock) asset_dock.queue_free()
texture_dock.queue_free()
ui.queue_free() ui.queue_free()
editor.free() editor.free()
scene_changed.disconnect(_on_scene_changed)
func _handles(p_object: Object) -> bool: func _handles(p_object: Object) -> bool:
return p_object is Terrain3D or p_object is NavigationRegion3D if p_object is Terrain3D or p_object is NavigationRegion3D:
return true
if p_object is Terrain3DObjects or (p_object is Node3D and p_object.get_parent() is Terrain3DObjects):
return true
return false
func _edit(p_object: Object) -> void: func _edit(p_object: Object) -> void:
@ -77,21 +101,37 @@ func _edit(p_object: Object) -> void:
nav_region = p_object nav_region = p_object
else: else:
nav_region = null nav_region = null
if p_object is Terrain3DObjects:
p_object.editor_setup(self)
elif p_object is Node3D and p_object.get_parent() is Terrain3DObjects:
p_object.get_parent().editor_setup(self)
_update_visibility()
func _make_visible(p_visible: bool) -> void: func _make_visible(p_visible: bool, p_redraw: bool = false) -> void:
visible = p_visible visible = p_visible
_update_visibility()
func _update_visibility() -> void:
ui.set_visible(visible) ui.set_visible(visible)
texture_dock.set_visible(visible and terrain) update_region_grid()
if terrain:
update_region_grid() # Manage Asset Dock position and visibility
region_gizmo.set_hidden(not visible or not terrain) if visible and dock_state == DOCK_STATE.HIDDEN:
if dock_position < DOCK_SLOT_MAX:
add_control_to_dock(dock_position, asset_dock)
dock_state = DOCK_STATE.SIDEBAR
asset_dock.move_slider(true)
else:
add_control_to_bottom_panel(asset_dock, "Terrain3D")
make_bottom_panel_item_visible(asset_dock)
dock_state = DOCK_STATE.BOTTOM
asset_dock.move_slider(false)
elif not visible and dock_state != DOCK_STATE.HIDDEN:
var pinned: bool = false
if p_redraw or ( asset_dock.placement_pin and not asset_dock.placement_pin.button_pressed):
if dock_state == DOCK_STATE.SIDEBAR:
remove_control_from_docks(asset_dock)
else:
remove_control_from_bottom_panel(asset_dock)
dock_state = DOCK_STATE.HIDDEN
func _clear() -> void: func _clear() -> void:
@ -111,21 +151,39 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
if not is_terrain_valid(): if not is_terrain_valid():
return AFTER_GUI_INPUT_PASS return AFTER_GUI_INPUT_PASS
## Track negative input (CTRL)
if p_event is InputEventKey and not p_event.echo and p_event.keycode == KEY_CTRL:
if p_event.is_pressed():
_negative_input = true
_prev_enable_state = int(ui.toolbar_settings.get_setting("enable"))
ui.toolbar_settings.set_setting("enable", false)
else:
_negative_input = false
ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
_prev_enable_state = -1
## Handle mouse movement ## Handle mouse movement
if p_event is InputEventMouseMotion: if p_event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT): if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_PASS return AFTER_GUI_INPUT_PASS
## Get mouse location on terrain if _prev_enable_state >= 0 and not Input.is_key_pressed(KEY_CTRL):
_negative_input = false
ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
_prev_enable_state = -1
## Setup for active camera & viewport
# Snap terrain to current camera # Snap terrain to current camera
terrain.set_camera(p_viewport_camera) terrain.set_camera(p_viewport_camera)
# Detect if viewport is set to half_resolution # Detect if viewport is set to half_resolution
# Structure is: Node3DEditorViewportContainer/Node3DEditorViewport/SubViewportContainer/SubViewport/Camera3D # Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D
var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent() var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true
## Get mouse location on terrain
# Project 2D mouse position to 3D position and direction # Project 2D mouse position to 3D position and direction
var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2 var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2
var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos) var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
@ -204,29 +262,22 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
func is_terrain_valid() -> bool: func is_terrain_valid() -> bool:
var valid: bool = false if is_instance_valid(terrain) and terrain.get_storage():
if is_instance_valid(terrain): return true
valid = terrain.get_storage() != null return false
return valid
func update_texture_dock(p_args: Array) -> void:
texture_dock.clear()
if is_terrain_valid() and terrain.texture_list:
var texture_count: int = terrain.texture_list.get_texture_count()
for i in texture_count:
var texture: Terrain3DTexture = terrain.texture_list.get_texture(i)
texture_dock.add_item(texture)
if texture_count < Terrain3DTextureList.MAX_TEXTURES:
texture_dock.add_item()
func _load_storage() -> void:
if terrain:
update_region_grid()
func update_region_grid() -> void: func update_region_grid() -> void:
if !region_gizmo.get_node_3d(): if not region_gizmo:
return return
region_gizmo.set_hidden(not visible)
if is_terrain_valid(): if is_terrain_valid():
region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION
region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT
@ -242,41 +293,63 @@ func update_region_grid() -> void:
region_gizmo.grid = [Vector2i.ZERO] region_gizmo.grid = [Vector2i.ZERO]
# Signal handlers
func _load_textures() -> void: func _load_textures() -> void:
if terrain and terrain.texture_list: if terrain and terrain.texture_list:
if not terrain.texture_list.textures_changed.is_connected(update_texture_dock): if not terrain.texture_list.textures_changed.is_connected(update_asset_dock):
terrain.texture_list.textures_changed.connect(update_texture_dock) terrain.texture_list.textures_changed.connect(update_asset_dock)
update_texture_dock(Array()) update_asset_dock()
func _load_storage() -> void: func update_asset_dock(p_texture_list: Terrain3DTextureList = null) -> void:
if terrain: asset_dock.clear()
update_region_grid()
if is_terrain_valid() and terrain.texture_list:
var texture_count: int = terrain.texture_list.get_texture_count()
for i in texture_count:
var texture: Terrain3DTexture = terrain.texture_list.get_texture(i)
asset_dock.add_item(texture)
if texture_count < Terrain3DTextureList.MAX_TEXTURES:
asset_dock.add_item()
func _on_texture_dock_resource_changed(texture: Resource, index: int) -> void: func _on_asset_dock_pin_changed(toggled: bool) -> void:
ProjectSettings.set_setting(PS_DOCK_PINNED, toggled)
ProjectSettings.save()
func _on_asset_dock_placement_changed(index: int) -> void:
dock_position = clamp(index, 0, DOCK_SLOT_MAX)
ProjectSettings.set_setting(PS_DOCK_POSITION, dock_position)
ProjectSettings.save()
_make_visible(false, true) # Hide to redraw
_make_visible(true)
func _on_asset_dock_resource_changed(p_texture: Resource, p_index: int) -> void:
if is_terrain_valid(): if is_terrain_valid():
# If removing last entry and its selected, clear inspector # If removing last entry and its selected, clear inspector
if not texture and index == texture_dock.get_selected_index() and \ if not p_texture and p_index == asset_dock.get_selected_index() and \
texture_dock.get_selected_index() == texture_dock.entries.size() - 2: asset_dock.get_selected_index() == asset_dock.entries.size() - 2:
get_editor_interface().inspect_object(null) get_editor_interface().inspect_object(null)
terrain.get_texture_list().set_texture(index, texture) terrain.get_texture_list().set_texture(p_index, p_texture)
call_deferred("_load_storage")
func _on_texture_dock_resource_selected(texture) -> void: func _on_asset_dock_resource_selected() -> void:
# If not on a texture painting tool, then switch to Paint
if editor.get_tool() != Terrain3DEditor.TEXTURE:
var paint_btn: Button = ui.toolbar.get_node_or_null("PaintBaseTexture")
if paint_btn:
paint_btn.set_pressed(true)
ui._on_tool_changed(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE)
ui._on_setting_changed()
func _on_asset_dock_resource_inspected(texture: Resource) -> void:
get_editor_interface().inspect_object(texture, "", true) get_editor_interface().inspect_object(texture, "", true)
func _on_texture_dock_visibility_changed() -> void: func _on_scene_changed(scene_root: Node) -> void:
if texture_dock.get_parent() != null: if scene_root:
remove_control_from_container(texture_dock_container, texture_dock) for node in scene_root.find_children("", "Terrain3DObjects"):
node.editor_setup(self)
if texture_dock.get_parent() == null:
texture_dock_container = CONTAINER_INSPECTOR_BOTTOM
if get_editor_interface().is_distraction_free_mode_enabled():
texture_dock_container = CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT
add_control_to_container(texture_dock_container, texture_dock)

View file

@ -1,460 +0,0 @@
extends PanelContainer
signal picking(type, callback)
signal setting_changed
enum Layout {
HORIZONTAL,
VERTICAL,
GRID,
}
enum SettingType {
CHECKBOX,
SLIDER,
DOUBLE_SLIDER,
COLOR_SELECT,
PICKER,
POINT_PICKER,
}
const PointPicker: Script = preload("res://addons/terrain_3d/editor/components/point_picker.gd")
const DEFAULT_BRUSH: String = "circle0.exr"
const BRUSH_PATH: String = "res://addons/terrain_3d/editor/brushes"
const PICKER_ICON: String = "res://addons/terrain_3d/icons/icon_picker.svg"
const NONE: int = 0x0
const ALLOW_LARGER: int = 0x1
const ALLOW_SMALLER: int = 0x2
const ALLOW_OUT_OF_BOUNDS: int = 0x3
var brush_preview_material: ShaderMaterial
var list: HBoxContainer
var advanced_list: VBoxContainer
var settings: Dictionary = {}
func _ready() -> void:
list = HBoxContainer.new()
add_child(list, true)
add_brushes(list)
add_setting(SettingType.SLIDER, "size", 50, list, "m", 4, 200, 1, ALLOW_LARGER)
add_setting(SettingType.SLIDER, "opacity", 10, list, "%", 1, 100)
add_setting(SettingType.CHECKBOX, "enable", true, list)
add_setting(SettingType.COLOR_SELECT, "color", Color.WHITE, list)
add_setting(SettingType.PICKER, "color picker", Terrain3DEditor.COLOR, list)
add_setting(SettingType.SLIDER, "roughness", 0, list, "%", -100, 100, 1)
add_setting(SettingType.PICKER, "roughness picker", Terrain3DEditor.ROUGHNESS, list)
add_setting(SettingType.SLIDER, "height", 50, list, "m", -500, 500, 0.1, ALLOW_OUT_OF_BOUNDS)
add_setting(SettingType.PICKER, "height picker", Terrain3DEditor.HEIGHT, list)
add_setting(SettingType.DOUBLE_SLIDER, "slope", 0, list, "°", 0, 180, 1)
add_setting(SettingType.POINT_PICKER, "gradient_points", Terrain3DEditor.HEIGHT, list)
add_setting(SettingType.CHECKBOX, "drawable", false, list)
settings["drawable"].toggled.connect(_on_drawable_toggled)
var spacer: Control = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
list.add_child(spacer, true)
## Advanced Settings Menu
advanced_list = create_submenu(list, "Advanced", Layout.VERTICAL)
add_setting(SettingType.CHECKBOX, "automatic_regions", true, advanced_list)
add_setting(SettingType.CHECKBOX, "align_to_view", true, advanced_list)
add_setting(SettingType.CHECKBOX, "show_cursor_while_painting", true, advanced_list)
advanced_list.add_child(HSeparator.new(), true)
add_setting(SettingType.SLIDER, "gamma", 1.0, advanced_list, "γ", 0.1, 2.0, 0.01)
add_setting(SettingType.SLIDER, "jitter", 50, advanced_list, "%", 0, 100)
func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout) -> Container:
var menu_button: Button = Button.new()
menu_button.set_text(p_button_name)
menu_button.set_toggle_mode(true)
menu_button.set_v_size_flags(SIZE_SHRINK_CENTER)
menu_button.connect("toggled", _on_show_submenu.bind(menu_button))
var submenu: PopupPanel = PopupPanel.new()
submenu.connect("popup_hide", menu_button.set_pressed_no_signal.bind(false))
submenu.set("theme_override_styles/panel", get_theme_stylebox("panel", "PopupMenu"))
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:
var popup: PopupPanel = p_button.get_child(0)
var popup_pos: Vector2 = p_button.get_screen_transform().origin
popup.set_visible(p_toggled)
popup_pos.y -= popup.get_size().y
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.connect("pressed", _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)
_black_to_alpha(img)
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.add_child(lbl, true)
lbl.text = file_name.get_basename()
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
# Optionally erase the main brush button text and replace it with the texture
# var select_brush_btn: Button = brush_list.get_parent().get_parent()
# select_brush_btn.set_button_icon(default_brush_btn.get_button_icon())
# select_brush_btn.set_custom_minimum_size(Vector2.ONE * 36)
# select_brush_btn.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER)
# select_brush_btn.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
Terrain3DEditor.COLOR:
settings["color"].color = p_color
Terrain3DEditor.ROUGHNESS:
# 200... -.5 converts 0,1 to -100,100
settings["roughness"].value = round(200 * (p_color.a - 0.5))
_on_setting_changed()
func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void:
assert(p_type == Terrain3DEditor.HEIGHT)
emit_signal("picking", p_type, _on_point_picked.bind(p_name))
func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void:
assert(p_type == Terrain3DEditor.HEIGHT)
var point: Vector3 = p_global_position
point.y = p_color.r
settings[p_name].add_point(point)
_on_setting_changed()
func add_setting(p_type: SettingType, p_name: StringName, p_value: Variant, p_parent: Control,
p_suffix: String = "", p_min_value: float = 0.0, p_max_value: float = 0.0, p_step: float = 1.0,
p_flags: int = NONE) -> void:
var container: HBoxContainer = HBoxContainer.new()
var label: Label = Label.new()
var control: Control
container.set_v_size_flags(SIZE_EXPAND_FILL)
match p_type:
SettingType.SLIDER, SettingType.DOUBLE_SLIDER:
label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
label.set_custom_minimum_size(Vector2(32, 0))
label.set_v_size_flags(SIZE_SHRINK_CENTER)
label.set_text(p_name.capitalize() + ": ")
container.add_child(label, true)
var slider: Control
if p_type == SettingType.SLIDER:
control = EditorSpinSlider.new()
control.set_flat(true)
control.set_hide_slider(true)
control.connect("value_changed", _on_setting_changed)
control.set_max(p_max_value)
control.set_min(p_min_value)
control.set_step(p_step)
control.set_value(p_value)
control.set_suffix(p_suffix)
control.set_v_size_flags(SIZE_SHRINK_CENTER)
slider = HSlider.new()
slider.share(control)
if p_flags & ALLOW_LARGER:
slider.set_allow_greater(true)
if p_flags & ALLOW_SMALLER:
slider.set_allow_lesser(true)
else:
control = Label.new()
control.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
control.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
slider = DoubleSlider.new()
slider.label = control
slider.suffix = p_suffix
slider.connect("value_changed", _on_setting_changed)
control.set_custom_minimum_size(Vector2(75, 0))
slider.set_max(p_max_value)
slider.set_min(p_min_value)
slider.set_step(p_step)
slider.set_value(p_value)
slider.set_v_size_flags(SIZE_SHRINK_CENTER)
slider.set_h_size_flags(SIZE_SHRINK_END | SIZE_EXPAND)
slider.set_custom_minimum_size(Vector2(100, 10))
container.add_child(slider, true)
SettingType.CHECKBOX:
control = CheckBox.new()
control.set_text(p_name.capitalize())
control.set_pressed_no_signal(p_value)
control.connect("pressed", _on_setting_changed)
SettingType.COLOR_SELECT:
control = ColorPickerButton.new()
control.set_custom_minimum_size(Vector2(100, 10))
control.color = Color.WHITE
control.edit_alpha = false
control.get_picker().set_color_mode(ColorPicker.MODE_HSV)
control.connect("color_changed", _on_setting_changed)
SettingType.PICKER:
control = Button.new()
control.icon = load(PICKER_ICON)
control.tooltip_text = "Pick value from the Terrain"
control.connect("pressed", _on_pick.bind(p_value))
SettingType.POINT_PICKER:
control = PointPicker.new()
control.connect("pressed", _on_point_pick.bind(p_value, p_name))
control.connect("value_changed", _on_setting_changed)
container.add_child(control, true)
p_parent.add_child(container, true)
settings[p_name] = control
func get_setting(p_setting: String) -> Variant:
var object: Object = settings[p_setting]
var value: Variant
if object is Range:
value = object.get_value()
elif object is DoubleSlider:
value = [object.get_min_value(), object.get_max_value()]
elif object is ButtonGroup:
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 PointPicker:
value = object.get_points()
return value
func hide_settings(p_settings: PackedStringArray) -> void:
for setting in settings.keys():
var object: Object = settings[setting]
if object is Control:
object.get_parent().show()
for setting in p_settings:
if settings.has(setting):
var object: Object = settings[setting]
if object is Control:
object.get_parent().hide()
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
func _black_to_alpha(p_image: Image) -> void:
if p_image.get_format() != Image.FORMAT_RGBAF:
p_image.convert(Image.FORMAT_RGBAF)
for y in p_image.get_height():
for x in p_image.get_width():
var color: Color = p_image.get_pixel(x,y)
color.a = color.get_luminance()
p_image.set_pixel(x, y, color)
#### Sub Class DoubleSlider
class DoubleSlider extends Range:
var label: Label
var suffix: String
var grabbed: bool = false
var _max_value: float
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
grabbed = p_event.is_pressed()
set_min_max(p_event.get_position().x)
if p_event is InputEventMouseMotion:
if grabbed:
set_min_max(p_event.get_position().x)
func _notification(p_what: int) -> void:
if p_what == NOTIFICATION_RESIZED:
pass
if p_what == NOTIFICATION_DRAW:
var bg: StyleBox = get_theme_stylebox("slider", "HSlider")
var bg_height: float = bg.get_minimum_size().y
draw_style_box(bg, Rect2(Vector2(0, (size.y - bg_height) / 2), Vector2(size.x, bg_height)))
var grabber: Texture2D = get_theme_icon("grabber", "HSlider")
var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
var h: float = size.y / 2 - grabber.get_size().y / 2
var minpos: Vector2 = Vector2((min_value / _max_value) * size.x - grabber.get_size().x / 2, h)
var maxpos: Vector2 = Vector2((max_value / _max_value) * size.x - grabber.get_size().x / 2, h)
draw_style_box(area, Rect2(Vector2(minpos.x + grabber.get_size().x / 2, (size.y - bg_height) / 2), Vector2(maxpos.x - minpos.x, bg_height)))
draw_texture(grabber, minpos)
draw_texture(grabber, maxpos)
func set_max(p_value: float) -> void:
max_value = p_value
if _max_value == 0:
_max_value = max_value
update_label()
func set_min_max(p_xpos: float) -> void:
var mid_value_normalized: float = ((max_value + min_value) / 2.0) / _max_value
var mid_value: float = size.x * mid_value_normalized
var min_active: bool = p_xpos < mid_value
var xpos_ranged: float = snappedf((p_xpos / size.x) * _max_value, step)
if min_active:
min_value = xpos_ranged
else:
max_value = xpos_ranged
min_value = clamp(min_value, 0, max_value - 10)
max_value = clamp(max_value, min_value + 10, _max_value)
update_label()
emit_signal("setting_changed", value)
queue_redraw()
func update_label() -> void:
if label:
label.set_text(str(min_value) + suffix + "/" + str(max_value) + suffix)

View file

@ -1,4 +1,4 @@
// This shader is the minimum needed to allow the terrain to function. // This shader is the minimum needed to allow the terrain to function, without any texturing.
shader_type spatial; shader_type spatial;
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx; render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
@ -12,8 +12,19 @@ uniform int _region_map_size = 16;
uniform int _region_map[256]; uniform int _region_map[256];
uniform vec2 _region_offsets[256]; uniform vec2 _region_offsets[256];
uniform sampler2DArray _height_maps : repeat_disable; uniform sampler2DArray _height_maps : repeat_disable;
uniform usampler2DArray _control_maps : repeat_disable;
uniform sampler2DArray _color_maps : source_color, filter_linear_mipmap_anisotropic, repeat_disable;
uniform sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable;
varying vec3 v_vertex; // World coordinate vertex location uniform float _texture_uv_scale_array[32];
uniform float _texture_uv_rotation_array[32];
uniform vec4 _texture_color_array[32];
uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
uniform uint _mouse_layer = 0x80000000u; // Layer 32
varying flat vec2 v_uv_offset;
varying flat vec2 v_uv2_offset;
//////////////////////// ////////////////////////
// Vertex // Vertex
@ -34,12 +45,17 @@ ivec3 get_region_uv(vec2 uv) {
// XY: (0 to 1) coordinates within a region // XY: (0 to 1) coordinates within a region
// Z: layer index used for texturearrays, -1 if not in a region // Z: layer index used for texturearrays, -1 if not in a region
vec3 get_region_uv2(vec2 uv) { vec3 get_region_uv2(vec2 uv) {
ivec2 pos = ivec2(floor(uv)) + (_region_map_size / 2); // Vertex function added half a texel to UV2, to center the UV's. vertex(), fragment() and get_height()
// call this with reclaimed versions of UV2, so to keep the last row/column within the correct
// window, take back the half pixel before the floor().
ivec2 pos = ivec2(floor(uv - vec2(_region_texel_size * 0.5))) + (_region_map_size / 2);
int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size); int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size);
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1; int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
// The return value is still texel-centered.
return vec3(uv - _region_offsets[layer_index], float(layer_index)); return vec3(uv - _region_offsets[layer_index], float(layer_index));
} }
// 1 lookup
float get_height(vec2 uv) { float get_height(vec2 uv) {
highp float height = 0.0; highp float height = 0.0;
vec3 region = get_region_uv2(uv); vec3 region = get_region_uv2(uv);
@ -51,23 +67,39 @@ float get_height(vec2 uv) {
void vertex() { void vertex() {
// Get vertex of flat plane in world coordinates and set world UV // Get vertex of flat plane in world coordinates and set world UV
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; vec3 vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
// UV coordinates in world space. Values are 0 to _region_size within regions // UV coordinates in world space. Values are 0 to _region_size within regions
UV = round(v_vertex.xz * _mesh_vertex_density); UV = round(vertex.xz * _mesh_vertex_density);
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions // Discard vertices for Holes. 1 lookup
UV2 = (UV + vec2(0.5)) * _region_texel_size; ivec3 region = get_region_uv(UV);
uint control = texelFetch(_control_maps, region, 0).r;
bool hole = bool(control >>2u & 0x1u);
// Show holes to all cameras except mouse camera (on exactly 1 layer)
if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
(hole || (_background_mode == 0u && region.z < 0)) ) {
VERTEX.x = 0./0.;
} else {
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
UV2 = (UV + vec2(0.5)) * _region_texel_size;
// Get final vertex location and save it // Get final vertex location and save it
VERTEX.y = get_height(UV2); VERTEX.y = get_height(UV2);
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
// Transform UVs to local to avoid poor precision during varying interpolation.
v_uv_offset = MODEL_MATRIX[3].xz * _mesh_vertex_density;
UV -= v_uv_offset;
v_uv2_offset = v_uv_offset * _region_texel_size;
UV2 -= v_uv2_offset;
} }
//////////////////////// ////////////////////////
// Fragment // Fragment
//////////////////////// ////////////////////////
// 3 lookups
vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) { vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) {
// Get the height of the current vertex // Get the height of the current vertex
float height = get_height(uv); float height = get_height(uv);
@ -97,9 +129,13 @@ vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) {
} }
void fragment() { void fragment() {
// Calculate Terrain Normals // Recover UVs
vec2 uv = UV + v_uv_offset;
vec2 uv2 = UV2 + v_uv2_offset;
// Calculate Terrain Normals. 4 lookups
vec3 w_tangent, w_binormal; vec3 w_tangent, w_binormal;
vec3 w_normal = get_normal(UV2, w_tangent, w_binormal); vec3 w_normal = get_normal(uv2, w_tangent, w_binormal);
NORMAL = mat3(VIEW_MATRIX) * w_normal; NORMAL = mat3(VIEW_MATRIX) * w_normal;
TANGENT = mat3(VIEW_MATRIX) * w_tangent; TANGENT = mat3(VIEW_MATRIX) * w_tangent;
BINORMAL = mat3(VIEW_MATRIX) * w_binormal; BINORMAL = mat3(VIEW_MATRIX) * w_binormal;
@ -107,3 +143,4 @@ void fragment() {
// Apply PBR // Apply PBR
ALBEDO=vec3(.2); ALBEDO=vec3(.2);
} }

View file

@ -1,8 +1,9 @@
# This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter # This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter
# It allows Scatter to detect the terrain height from Terrain3D # It provides a `Project on Terrain3D` modifier, which allows Scatter
# to detect the terrain height from Terrain3D without using collision.
# Copy this file into /addons/proton_scatter/src/modifiers # Copy this file into /addons/proton_scatter/src/modifiers
# Then uncomment everything below # Then uncomment everything below
# In the editor, add this modifier, then set your Terrain3D node # In the editor, add this modifier to Scatter, then set your Terrain3D node
#@tool #@tool
#extends "base_modifier.gd" #extends "base_modifier.gd"

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cjrjfjmbyseol" uid="uid://dbby47aoknjeb"
path="res://.godot/imported/icon_brush.svg-8886426485f67abe2233686de39952ce.ctex" path="res://.godot/imported/icon_brush.svg-8886426485f67abe2233686de39952ce.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cnytnsydxqdn2" uid="uid://bump6ax7j3rp1"
path="res://.godot/imported/icon_color.svg-b5b52e67ad2f610c27688c5daeb9cd1c.ctex" path="res://.godot/imported/icon_color.svg-b5b52e67ad2f610c27688c5daeb9cd1c.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://ca7l0radlfn5w" uid="uid://8ma1rbcinto3"
path="res://.godot/imported/icon_height_add.svg-2928bbcb35ef4816ead056c5bcf5bdbd.ctex" path="res://.godot/imported/icon_height_add.svg-2928bbcb35ef4816ead056c5bcf5bdbd.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cunkwpgutguvo" uid="uid://5xwklmfyqaha"
path="res://.godot/imported/icon_height_div.svg-982f74e4859453a7d67caa2f6a71b056.ctex" path="res://.godot/imported/icon_height_div.svg-982f74e4859453a7d67caa2f6a71b056.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cs2injw82o1il" uid="uid://cpfvqnfgrw4e"
path="res://.godot/imported/icon_height_flat.svg-e58f6a7038e84631a5f56866c4c671e0.ctex" path="res://.godot/imported/icon_height_flat.svg-e58f6a7038e84631a5f56866c4c671e0.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://tvaw0pb2bdhu" uid="uid://bkqdmfjtug1c7"
path="res://.godot/imported/icon_height_mul.svg-b6b666e20be820f5aa48e7410648290c.ctex" path="res://.godot/imported/icon_height_mul.svg-b6b666e20be820f5aa48e7410648290c.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://c3xbj3i7emxir" uid="uid://wkm8llh72h1m"
path="res://.godot/imported/icon_height_slope.svg-2a8181e8d9f9b74739d6f4a9e62f040d.ctex" path="res://.godot/imported/icon_height_slope.svg-2a8181e8d9f9b74739d6f4a9e62f040d.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://ub12g3jkvivd" uid="uid://fhvnyvqmygfs"
path="res://.godot/imported/icon_height_smooth.svg-83cfb47d64fb9579b212027a5aa50672.ctex" path="res://.godot/imported/icon_height_smooth.svg-83cfb47d64fb9579b212027a5aa50672.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://vyw5rhfpbjku" uid="uid://cbtqb5fq02yx1"
path="res://.godot/imported/icon_height_sub.svg-f01f73a219b6c1858d4bd958d01e8130.ctex" path="res://.godot/imported/icon_height_sub.svg-f01f73a219b6c1858d4bd958d01e8130.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bqktati6gi08q" uid="uid://4qj0x1rs0a83"
path="res://.godot/imported/icon_holes.svg-fadd8eef4df4cdc393621d5ff25aa8e3.ctex" path="res://.godot/imported/icon_holes.svg-fadd8eef4df4cdc393621d5ff25aa8e3.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://js5u88icy324" uid="uid://dkgbiictluyh8"
path="res://.godot/imported/icon_map_add.svg-a13cebbb261c5138d4ca5cbb5df24202.ctex" path="res://.godot/imported/icon_map_add.svg-a13cebbb261c5138d4ca5cbb5df24202.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://c1o6vv5wx3w65" uid="uid://bavktgaibu05s"
path="res://.godot/imported/icon_map_remove.svg-bf5a269f9171f7027b6de1785cc63713.ctex" path="res://.godot/imported/icon_map_remove.svg-bf5a269f9171f7027b6de1785cc63713.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://xg3l0exmqch3" uid="uid://c5004ol65cqti"
path="res://.godot/imported/icon_multimesh.svg-9447b6c5fe1ee9d406fd55dd3c63e196.ctex" path="res://.godot/imported/icon_multimesh.svg-9447b6c5fe1ee9d406fd55dd3c63e196.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://c0sprba3li1x4" uid="uid://dewr4ro7nrg7y"
path="res://.godot/imported/icon_navigation.svg-35e49ee3c403c103a0079d4156b0d168.ctex" path="res://.godot/imported/icon_navigation.svg-35e49ee3c403c103a0079d4156b0d168.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://516qh3duc33g" uid="uid://iiocbe4njv5s"
path="res://.godot/imported/icon_picker.svg-9f872a162ed3e0053283f4bf299ac645.ctex" path="res://.godot/imported/icon_picker.svg-9f872a162ed3e0053283f4bf299ac645.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://wor7v0l4vc6t" uid="uid://73eyg81dnj2e"
path="res://.godot/imported/icon_picker_checked.svg-4e271ac1a29c979a28440c683998675e.ctex" path="res://.godot/imported/icon_picker_checked.svg-4e271ac1a29c979a28440c683998675e.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bx2wxkso083ht" uid="uid://28jdhtmo0tcm"
path="res://.godot/imported/icon_spray.svg-d9864c87d5d420aa9f80c0d3fdc80e87.ctex" path="res://.godot/imported/icon_spray.svg-d9864c87d5d420aa9f80c0d3fdc80e87.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://b03x10iou1m5" uid="uid://cyfcx5hwray0d"
path="res://.godot/imported/icon_terrain3d.svg-39252bb986e607c413d93e00ee31a619.ctex" path="res://.godot/imported/icon_terrain3d.svg-39252bb986e607c413d93e00ee31a619.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://gt2l3qdaivl2" uid="uid://1bsjjcq0cv08"
path="res://.godot/imported/icon_terrain_layer_material.svg-f3d447e84f51556fc60c5f0bd3f57443.ctex" path="res://.godot/imported/icon_terrain_layer_material.svg-f3d447e84f51556fc60c5f0bd3f57443.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://ccywk15q2ad8q" uid="uid://hsoj3vbpdg6d"
path="res://.godot/imported/icon_terrain_material.svg-ea0cde03d29920e042a4043982714cbe.ctex" path="res://.godot/imported/icon_terrain_material.svg-ea0cde03d29920e042a4043982714cbe.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -2,9 +2,10 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bv4f7kijr12yr" uid="uid://cidqhk7wrkcl7"
path="res://.godot/imported/icon_wetness.svg-6c67b1e4e9435c1aa106bee56f000e06.ctex" path="res://.godot/imported/icon_wetness.svg-6c67b1e4e9435c1aa106bee56f000e06.ctex"
metadata={ metadata={
"has_editor_variant": true,
"vram_texture": false "vram_texture": false
} }
@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View file

@ -3,5 +3,5 @@
name="Terrain3D" name="Terrain3D"
description="A high performance, editable terrain system for Godot 4." description="A high performance, editable terrain system for Godot 4."
author="Cory Petkovsek & Roope Palmroos" author="Cory Petkovsek & Roope Palmroos"
version="0.9.1" version="0.9.2-dev"
script="editor/editor.gd" script="editor.gd"

View file

@ -1,6 +1,7 @@
@tool
extends PanelContainer extends PanelContainer
signal placement_changed(index: int)
signal resource_changed(resource: Resource, index: int) signal resource_changed(resource: Resource, index: int)
signal resource_inspected(resource: Resource) signal resource_inspected(resource: Resource)
signal resource_selected signal resource_selected
@ -8,36 +9,55 @@ signal resource_selected
var list: ListContainer var list: ListContainer
var entries: Array[ListEntry] var entries: Array[ListEntry]
var selected_index: int = 0 var selected_index: int = 0
var focus_style: StyleBox
@onready var placement_option: OptionButton = $VBox/PlacementHBox/Options
@onready var placement_pin: Button = $VBox/PlacementHBox/Pinned
@onready var size_slider: HSlider = $VBox/SizeSlider
func _init() -> void: func _ready() -> void:
placement_option.item_selected.connect(_on_placement_selected)
size_slider.value_changed.connect(_on_slider_changed)
list = ListContainer.new() list = ListContainer.new()
var root: VBoxContainer = VBoxContainer.new()
var scroll: ScrollContainer = ScrollContainer.new()
var label: Label = Label.new()
label.set_text("Textures")
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
scroll.set_v_size_flags(SIZE_EXPAND_FILL)
scroll.set_h_size_flags(SIZE_EXPAND_FILL)
list.set_v_size_flags(SIZE_EXPAND_FILL) list.set_v_size_flags(SIZE_EXPAND_FILL)
list.set_h_size_flags(SIZE_EXPAND_FILL) list.set_h_size_flags(SIZE_EXPAND_FILL)
$VBox/ScrollContainer.add_child(list)
scroll.add_child(list)
root.add_child(label)
root.add_child(scroll)
add_child(root)
set_custom_minimum_size(Vector2(256, 448))
# Copy theme from the editor, but since its a tool script, avoid saving icon resources in tscn
func _ready() -> void: if EditorScript.new().get_editor_interface().get_edited_scene_root() != self:
get_child(0).get_child(0).set("theme_override_styles/normal", get_theme_stylebox("bg", "EditorInspectorCategory")) set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel"))
get_child(0).get_child(0).set("theme_override_fonts/font", get_theme_font("bold", "EditorFonts")) $VBox/Label.set("theme_override_styles/normal", get_theme_stylebox("bg", "EditorInspectorCategory"))
get_child(0).get_child(0).set("theme_override_font_sizes/font_size",get_theme_font_size("bold_size", "EditorFonts")) $VBox/Label.set("theme_override_fonts/font", get_theme_font("bold", "EditorFonts"))
set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel")) $VBox/Label.set("theme_override_font_sizes/font_size",get_theme_font_size("bold_size", "EditorFonts"))
placement_pin.icon = get_theme_icon("Pin", "EditorIcons")
placement_pin.text = ""
# Setup style for selected assets
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 _on_placement_selected(index: int) -> void:
emit_signal("placement_changed", index)
func _on_slider_changed(value: float) -> void:
if list:
list.set_entry_size(value)
func move_slider(to_side: bool) -> void:
if to_side and size_slider.get_parent() != $VBox:
size_slider.reparent($VBox)
$VBox.move_child(size_slider, 2)
size_slider.custom_minimum_size = Vector2(0, 0)
elif not to_side and size_slider.get_parent() == $VBox:
size_slider.reparent($VBox/PlacementHBox)
$VBox/PlacementHBox.move_child(size_slider, 2)
size_slider.custom_minimum_size = Vector2(300, 10)
func clear() -> void: func clear() -> void:
@ -49,6 +69,7 @@ func clear() -> void:
func add_item(p_resource: Resource = null) -> void: func add_item(p_resource: Resource = null) -> void:
var entry: ListEntry = ListEntry.new() var entry: ListEntry = ListEntry.new()
entry.focus_style = focus_style
var index: int = entries.size() var index: int = entries.size()
entry.set_edited_resource(p_resource) entry.set_edited_resource(p_resource)
@ -102,23 +123,38 @@ func notify_resource_changed(p_resource: Resource, p_index: int) -> void:
class ListContainer extends Container: class ListContainer extends Container:
var height: float = 0 var height: float = 0
var width: float = 83
func _notification(p_what) -> void:
if p_what == NOTIFICATION_SORT_CHILDREN: func set_entry_size(value: float) -> void:
height = 0 width = clamp(value, 56, 256)
var index: int = 0 redraw()
var separation: float = 4
for c in get_children():
if is_instance_valid(c): func redraw() -> void:
var width: float = size.x / 3 height = 0
c.size = Vector2(width,width) - Vector2(separation, separation) var index: int = 0
c.position = Vector2(index % 3, index / 3) * width + Vector2(separation/3, separation/3) var separation: float = 4
height = max(height, c.position.y + width) var columns: int = 3
index += 1 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(index % columns, index / columns) * width + \
Vector2(separation / columns, separation / columns)
height = max(height, c.position.y + width)
index += 1
func _get_minimum_size() -> Vector2: func _get_minimum_size() -> Vector2:
return Vector2(0, height) return Vector2(0, height)
func _notification(p_what) -> void:
if p_what == NOTIFICATION_SORT_CHILDREN:
redraw()
############################################################## ##############################################################
## class ListEntry ## class ListEntry
@ -143,7 +179,7 @@ class ListEntry extends VBoxContainer:
@onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons") @onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons")
@onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons") @onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons")
@onready var background: StyleBox = get_theme_stylebox("pressed", "Button") @onready var background: StyleBox = get_theme_stylebox("pressed", "Button")
@onready var focus: StyleBox = get_theme_stylebox("focus", "Button") var focus_style: StyleBox
func _ready() -> void: func _ready() -> void:
@ -196,11 +232,11 @@ class ListEntry extends VBoxContainer:
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10) name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10)
if drop_data: if drop_data:
draw_style_box(focus, rect) draw_style_box(focus_style, rect)
if is_hovered: if is_hovered:
draw_rect(rect, Color(1,1,1,0.2)) draw_rect(rect, Color(1,1,1,0.2))
if is_selected: if is_selected:
draw_style_box(focus, rect) draw_style_box(focus_style, rect)
NOTIFICATION_MOUSE_ENTER: NOTIFICATION_MOUSE_ENTER:
is_hovered = true is_hovered = true
name_label.visible = true name_label.visible = true

View file

@ -0,0 +1,69 @@
[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, 136)
offset_right = 256.0
offset_bottom = 128.0
script = ExtResource("1_e23pg")
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="PlacementHBox" type="HBoxContainer" parent="VBox"]
layout_mode = 2
[node name="Label" type="Label" parent="VBox/PlacementHBox"]
layout_mode = 2
text = "Dock Position: "
[node name="Options" type="OptionButton" parent="VBox/PlacementHBox"]
layout_mode = 2
size_flags_horizontal = 3
item_count = 9
selected = 5
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="Pinned" type="Button" parent="VBox/PlacementHBox"]
layout_mode = 2
tooltip_text = "Keep panel visible even if Terrain3D is not selected. Useful for keeping dock floating."
toggle_mode = true
text = "P"
flat = true
[node name="Label" type="Label" parent="VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 16
text = "Textures"
horizontal_alignment = 1
vertical_alignment = 1
[node name="SizeSlider" type="HSlider" parent="VBox"]
custom_minimum_size = Vector2(100, 10)
layout_mode = 2
min_value = 56.0
max_value = 256.0
value = 83.0
[node name="ScrollContainer" type="ScrollContainer" parent="VBox"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3

View file

@ -1,13 +1,13 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"] [gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]
[ext_resource type="Script" path="res://addons/terrain_3d/editor/components/bake_lod_dialog.gd" id="1_57670"] [ext_resource type="Script" path="res://addons/terrain_3d/src/bake_lod_dialog.gd" id="1_sf76d"]
[node name="bake_lod_dialog" type="ConfirmationDialog"] [node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh" title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36) position = Vector2i(0, 36)
size = Vector2i(400, 115) size = Vector2i(400, 115)
visible = true visible = true
script = ExtResource("1_57670") script = ExtResource("1_sf76d")
[node name="VBoxContainer" type="VBoxContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
anchors_preset = 15 anchors_preset = 15

View file

@ -1,6 +1,6 @@
extends Node extends Node
const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/editor/components/bake_lod_dialog.tscn") 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_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 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: const SET_UP_NAVIGATION_DESCRIPTION: String = "This operation will:

View file

@ -1,7 +1,7 @@
extends Object extends Object
const WINDOW_SCENE: String = "res://addons/terrain_3d/editor/components/channel_packer.tscn" const WINDOW_SCENE: String = "res://addons/terrain_3d/src/channel_packer.tscn"
const TEMPLATE_PATH: String = "res://addons/terrain_3d/editor/components/channel_packer_import_template.txt" const TEMPLATE_PATH: String = "res://addons/terrain_3d/src/channel_packer_import_template.txt"
enum { enum {
IMAGE_ALBEDO, IMAGE_ALBEDO,
@ -198,7 +198,7 @@ func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_
_show_error("Textures must be the same size.") _show_error("Textures must be the same size.")
return return
var output_image: Image = Terrain3D.pack_image(p_rgb_image, p_a_image, p_invert_green) var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image, p_invert_green)
if not output_image: if not output_image:
_show_error("Failed to pack textures.") _show_error("Failed to pack textures.")

View file

@ -1,10 +1,10 @@
extends "res://addons/terrain_3d/editor/components/operation_builder.gd" extends "res://addons/terrain_3d/src/operation_builder.gd"
const PointPicker: Script = preload("res://addons/terrain_3d/editor/components/point_picker.gd") const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd")
func _get_point_picker() -> PointPicker: func _get_point_picker() -> MultiPicker:
return tool_settings.settings["gradient_points"] return tool_settings.settings["gradient_points"]

View file

@ -20,10 +20,6 @@ func _init() -> void:
icon_picker = load(ICON_PICKER) icon_picker = load(ICON_PICKER)
icon_picker_checked = load(ICON_PICKER_CHECKED) icon_picker_checked = load(ICON_PICKER_CHECKED)
var label := Label.new()
label.text = "Points:"
add_child(label)
points.resize(MAX_POINTS) points.resize(MAX_POINTS)
for i in range(MAX_POINTS): for i in range(MAX_POINTS):

View file

@ -1,7 +1,7 @@
extends RefCounted extends RefCounted
const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd") const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd")
var tool_settings: ToolSettings var tool_settings: ToolSettings

View file

@ -1,8 +1,8 @@
extends HBoxContainer extends HBoxContainer
const Baker: Script = preload("res://addons/terrain_3d/editor/components/baker.gd") const Baker: Script = preload("res://addons/terrain_3d/src/baker.gd")
const Packer: Script = preload("res://addons/terrain_3d/editor/components/channel_packer.gd") const Packer: Script = preload("res://addons/terrain_3d/src/channel_packer.gd")
var plugin: EditorPlugin var plugin: EditorPlugin
var menu_button: MenuButton = MenuButton.new() var menu_button: MenuButton = MenuButton.new()

View file

@ -0,0 +1,645 @@
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,
TYPE_MAX,
}
const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd")
const DEFAULT_BRUSH: String = "circle0.exr"
const BRUSH_PATH: String = "res://addons/terrain_3d/brushes"
const PICKER_ICON: String = "res://addons/terrain_3d/icons/icon_picker.svg"
# 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
const ADD_SPACER: int = 0x10
var brush_preview_material: ShaderMaterial
var select_brush_button: Button
var main_list: HBoxContainer
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:
main_list = HBoxContainer.new()
add_child(main_list, true)
## Common Settings
add_brushes(main_list)
add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":50, "unit":"m",
"range":Vector3(2, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER })
add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":10,
"unit":"%", "range":Vector3(1, 100, 1), "flags":ALLOW_LARGER })
add_setting({ "name":"enable", "type":SettingType.CHECKBOX, "list":main_list, "default":true })
add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":50,
"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":0,
"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 })
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
add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list,
"default":0, "unit":"°", "range":Vector3(0, 180, 1) })
add_setting({ "name":"gradient_points", "type":SettingType.MULTI_PICKER, "label":"Points",
"list":main_list, "default":Terrain3DEditor.HEIGHT, "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)
var spacer: Control = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_list.add_child(spacer, true)
## Advanced Settings Menu
advanced_list = create_submenu(main_list, "Advanced", Layout.VERTICAL)
add_setting({ "name":"automatic_regions", "type":SettingType.CHECKBOX, "list":advanced_list,
"default":true })
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) -> Container:
var menu_button: Button = Button.new()
menu_button.set_text(p_button_name)
menu_button.set_toggle_mode(true)
menu_button.set_v_size_flags(SIZE_SHRINK_CENTER)
menu_button.toggled.connect(_on_show_submenu.bind(menu_button))
var submenu: PopupPanel = PopupPanel.new()
submenu.popup_hide.connect(menu_button.set_pressed_no_signal.bind(false))
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)
# Pop up menu on hover, hide on exit
menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button))
menu_button.mouse_exited.connect(_on_show_submenu.bind(false, menu_button))
submenu.mouse_exited.connect(_on_show_submenu.bind(false, menu_button))
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 panel: PopupPanel = p_button.get_child(0)
var panel_rect: Rect2 = Rect2(panel.position, panel.size)
var in_panel: bool = panel_rect.has_point(DisplayServer.mouse_get_position())
if not p_toggled and ( in_button or in_panel ):
return
var popup: PopupPanel = p_button.get_child(0)
var popup_pos: Vector2 = p_button.get_screen_transform().origin
popup.set_visible(p_toggled)
popup_pos.y -= popup.get_size().y
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)
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_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.HEIGHT)
emit_signal("picking", p_type, _on_point_picked.bind(p_name))
func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void:
assert(p_type == Terrain3DEditor.HEIGHT)
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.CHECKBOX:
var checkbox := CheckBox.new()
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.color = Color.WHITE
picker.edit_alpha = false
picker.get_picker().set_color_mode(ColorPicker.MODE_HSV)
picker.color_changed.connect(_on_setting_changed)
var popup: PopupPanel = picker.get_popup()
popup.mouse_exited.connect(Callable(func(p): p.hide()).bind(popup))
pending_children.push_back(picker)
control = picker
SettingType.PICKER:
var button := Button.new()
button.icon = load(PICKER_ICON)
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_value(p_default)
spin_slider.set_suffix(p_suffix)
spin_slider.set_h_size_flags(SIZE_SHRINK_CENTER)
spin_slider.set_v_size_flags(SIZE_SHRINK_CENTER)
spin_slider.set_custom_minimum_size(Vector2(75, 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_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
label.set_custom_minimum_size(Vector2(75, 0))
slider = DoubleSlider.new()
slider.label = label
slider.suffix = p_suffix
slider.setting_changed.connect(_on_setting_changed)
pending_children.push_back(slider)
pending_children.push_back(label)
control = slider
slider.set_max(p_maximum)
slider.set_min(p_minimum)
slider.set_step(p_step)
slider.set_value(p_default)
slider.set_v_size_flags(SIZE_SHRINK_CENTER)
slider.set_h_size_flags(SIZE_SHRINK_END | SIZE_EXPAND)
slider.set_custom_minimum_size(Vector2(60, 10))
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 = Vector2(object.get_min_value(), object.get_max_value())
elif object is ButtonGroup:
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 Range:
object.set_value(p_value)
elif object is DoubleSlider: # Expects p_value is Vector2
object.set_min_value(p_value.x)
object.set_max_value(p_value.y)
elif object is ButtonGroup: # Expects p_value is Array [ "button name", boolean ]
if p_value is Array and p_value.size() == 2:
for button in object.get_buttons():
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
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
#### Sub Class DoubleSlider
class DoubleSlider extends Range:
signal setting_changed(Vector2)
var label: Label
var suffix: String
var grabbed: bool = false
var _max_value: float
# TODO Needs to clamp min and max values. Currently allows max slider to go negative.
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
grabbed = p_event.is_pressed()
set_min_max(p_event.get_position().x)
if p_event is InputEventMouseMotion:
if grabbed:
set_min_max(p_event.get_position().x)
func _notification(p_what: int) -> void:
if p_what == NOTIFICATION_RESIZED:
pass
if p_what == NOTIFICATION_DRAW:
var bg: StyleBox = get_theme_stylebox("slider", "HSlider")
var bg_height: float = bg.get_minimum_size().y
draw_style_box(bg, Rect2(Vector2(0, (size.y - bg_height) / 2), Vector2(size.x, bg_height)))
var grabber: Texture2D = get_theme_icon("grabber", "HSlider")
var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
var h: float = size.y / 2 - grabber.get_size().y / 2
var minpos: Vector2 = Vector2((min_value / _max_value) * size.x - grabber.get_size().x / 2, h)
var maxpos: Vector2 = Vector2((max_value / _max_value) * size.x - grabber.get_size().x / 2, h)
draw_style_box(area, Rect2(Vector2(minpos.x + grabber.get_size().x / 2, (size.y - bg_height) / 2), Vector2(maxpos.x - minpos.x, bg_height)))
draw_texture(grabber, minpos)
draw_texture(grabber, maxpos)
func set_max(p_value: float) -> void:
max_value = p_value
if _max_value == 0:
_max_value = max_value
update_label()
func set_min_max(p_xpos: float) -> void:
var mid_value_normalized: float = ((max_value + min_value) / 2.0) / _max_value
var mid_value: float = size.x * mid_value_normalized
var min_active: bool = p_xpos < mid_value
var xpos_ranged: float = snappedf((p_xpos / size.x) * _max_value, step)
if min_active:
min_value = xpos_ranged
else:
max_value = xpos_ranged
min_value = clamp(min_value, 0, max_value - 10)
max_value = clamp(max_value, min_value + 10, _max_value)
update_label()
emit_signal("setting_changed", Vector2(min_value, max_value))
queue_redraw()
func update_label() -> void:
if label:
label.set_text(str(min_value) + suffix + "/" + str(max_value) + suffix)

View file

@ -24,7 +24,7 @@ var tool_group: ButtonGroup = ButtonGroup.new()
func _init() -> void: func _init() -> void:
set_custom_minimum_size(Vector2(32, 0)) set_custom_minimum_size(Vector2(20, 0))
func _ready() -> void: func _ready() -> void:
@ -59,6 +59,7 @@ func add_tool_button(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.
p_tip: String, p_icon: Texture2D, p_group: ButtonGroup) -> void: p_tip: String, p_icon: Texture2D, p_group: ButtonGroup) -> void:
var button: Button = Button.new() var button: Button = Button.new()
button.set_name(p_tip.to_pascal_case())
button.set_meta("Tool", p_tool) button.set_meta("Tool", p_tool)
button.set_meta("Operation", p_operation) button.set_meta("Operation", p_operation)
button.set_tooltip_text(p_tip) button.set_tooltip_text(p_tip)

View file

@ -3,12 +3,11 @@ extends Node
# Includes # Includes
const Toolbar: Script = preload("res://addons/terrain_3d/editor/components/toolbar.gd") const Toolbar: Script = preload("res://addons/terrain_3d/src/toolbar.gd")
const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd") const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd")
const TerrainTools: Script = preload("res://addons/terrain_3d/editor/components/terrain_tools.gd") const TerrainTools: Script = preload("res://addons/terrain_3d/src/terrain_tools.gd")
const OperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/operation_builder.gd") const OperationBuilder: Script = preload("res://addons/terrain_3d/src/operation_builder.gd")
const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/gradient_operation_builder.gd") const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/src/gradient_operation_builder.gd")
const RING1: String = "res://addons/terrain_3d/editor/brushes/ring1.exr"
const COLOR_RAISE := Color.WHITE const COLOR_RAISE := Color.WHITE
const COLOR_LOWER := Color.BLACK const COLOR_LOWER := Color.BLACK
const COLOR_SMOOTH := Color(0.5, 0, .1) const COLOR_SMOOTH := Color(0.5, 0, .1)
@ -26,6 +25,8 @@ const COLOR_PICK_COLOR := Color.WHITE
const COLOR_PICK_HEIGHT := Color.DARK_RED const COLOR_PICK_HEIGHT := Color.DARK_RED
const COLOR_PICK_ROUGH := Color.ROYAL_BLUE const COLOR_PICK_ROUGH := Color.ROYAL_BLUE
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 plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var toolbar: Toolbar var toolbar: Toolbar
@ -40,14 +41,13 @@ var decal_timer: Timer
var gradient_decals: Array[Decal] var gradient_decals: Array[Decal]
var brush_data: Dictionary var brush_data: Dictionary
var operation_builder: OperationBuilder var operation_builder: OperationBuilder
@onready var picker_texture: ImageTexture = ImageTexture.create_from_image(Image.load_from_file(RING1))
func _enter_tree() -> void: func _enter_tree() -> void:
toolbar = Toolbar.new() toolbar = Toolbar.new()
toolbar.hide() toolbar.hide()
toolbar.connect("tool_changed", _on_tool_changed) toolbar.connect("tool_changed", _on_tool_changed)
toolbar_settings = ToolSettings.new() toolbar_settings = ToolSettings.new()
toolbar_settings.connect("setting_changed", _on_setting_changed) toolbar_settings.connect("setting_changed", _on_setting_changed)
toolbar_settings.connect("picking", _on_picking) toolbar_settings.connect("picking", _on_picking)
@ -61,6 +61,8 @@ func _enter_tree() -> void:
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings) plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_tools) plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_tools)
_on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD)
decal = Decal.new() decal = Decal.new()
add_child(decal) add_child(decal)
decal_timer = Timer.new() decal_timer = Timer.new()
@ -87,126 +89,93 @@ func _exit_tree() -> void:
func set_visible(p_visible: bool) -> void: func set_visible(p_visible: bool) -> void:
visible = p_visible visible = p_visible
toolbar.set_visible(p_visible and plugin.terrain)
terrain_tools.set_visible(p_visible) terrain_tools.set_visible(p_visible)
toolbar.set_visible(p_visible)
if p_visible and plugin.terrain: toolbar_settings.set_visible(p_visible)
p_visible = plugin.editor.get_tool() != Terrain3DEditor.REGION
toolbar_settings.set_visible(p_visible and plugin.terrain)
update_decal() update_decal()
func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void: func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
clear_picking() clear_picking()
if not visible or not plugin.terrain: # Select which settings to show. Options in tool_settings.gd:_ready
return var to_show: PackedStringArray = []
if plugin.editor: match p_tool:
plugin.editor.set_tool(p_tool) Terrain3DEditor.HEIGHT:
plugin.editor.set_operation(p_operation) to_show.push_back("brush")
to_show.push_back("size")
if p_tool != Terrain3DEditor.REGION: to_show.push_back("strength")
# Select which settings to hide. Options:
# size, opactiy, height, slope, color, roughness, (height|color|roughness) picker
var to_hide: PackedStringArray = []
if p_tool == Terrain3DEditor.HEIGHT:
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
if p_operation != Terrain3DEditor.REPLACE:
to_hide.push_back("height")
to_hide.push_back("height picker")
if p_operation != Terrain3DEditor.GRADIENT:
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
elif p_tool == Terrain3DEditor.TEXTURE:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
if p_operation == Terrain3DEditor.REPLACE: if p_operation == Terrain3DEditor.REPLACE:
to_hide.push_back("opacity") to_show.push_back("height")
to_show.push_back("height_picker")
if p_operation == Terrain3DEditor.GRADIENT:
to_show.push_back("gradient_points")
to_show.push_back("drawable")
Terrain3DEditor.TEXTURE:
to_show.push_back("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("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")
elif p_tool == Terrain3DEditor.COLOR: Terrain3DEditor.COLOR:
to_hide.push_back("height") to_show.push_back("brush")
to_hide.push_back("height picker") to_show.push_back("size")
to_hide.push_back("gradient_points") to_show.push_back("strength")
to_hide.push_back("drawable") to_show.push_back("color")
to_hide.push_back("roughness") to_show.push_back("color_picker")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
elif p_tool == Terrain3DEditor.ROUGHNESS: Terrain3DEditor.ROUGHNESS:
to_hide.push_back("height") to_show.push_back("brush")
to_hide.push_back("height picker") to_show.push_back("size")
to_hide.push_back("gradient_points") to_show.push_back("strength")
to_hide.push_back("drawable") to_show.push_back("roughness")
to_hide.push_back("color") to_show.push_back("roughness_picker")
to_hide.push_back("color picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
elif p_tool in [ Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION ]: Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION:
to_hide.push_back("height") to_show.push_back("brush")
to_hide.push_back("height picker") to_show.push_back("size")
to_hide.push_back("gradient_points") to_show.push_back("enable")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("opacity")
toolbar_settings.hide_settings(to_hide) _:
pass
toolbar_settings.set_visible(p_tool != Terrain3DEditor.REGION) # Advanced menu settings
to_show.push_back("automatic_regions")
to_show.push_back("align_to_view")
to_show.push_back("show_cursor_while_painting")
to_show.push_back("gamma")
to_show.push_back("jitter")
toolbar_settings.show_settings(to_show)
operation_builder = null operation_builder = null
if p_operation == Terrain3DEditor.GRADIENT: if p_operation == Terrain3DEditor.GRADIENT:
operation_builder = GradientOperationBuilder.new() operation_builder = GradientOperationBuilder.new()
operation_builder.tool_settings = toolbar_settings operation_builder.tool_settings = toolbar_settings
if plugin.editor:
plugin.editor.set_tool(p_tool)
plugin.editor.set_operation(p_operation)
_on_setting_changed() _on_setting_changed()
plugin.update_region_grid() plugin.update_region_grid()
func _on_setting_changed() -> void: func _on_setting_changed() -> void:
if not visible or not plugin.terrain: if not plugin.asset_dock:
return return
brush_data = { brush_data = toolbar_settings.get_settings()
"size": int(toolbar_settings.get_setting("size")), brush_data["strength"] /= 100.0
"opacity": toolbar_settings.get_setting("opacity") / 100.0, brush_data["texture_index"] = plugin.asset_dock.get_selected_index()
"height": toolbar_settings.get_setting("height"),
"texture_index": plugin.texture_dock.get_selected_index(),
"color": toolbar_settings.get_setting("color"),
"roughness": toolbar_settings.get_setting("roughness"),
"gradient_points": toolbar_settings.get_setting("gradient_points"),
"enable": toolbar_settings.get_setting("enable"),
"automatic_regions": toolbar_settings.get_setting("automatic_regions"),
"align_to_view": toolbar_settings.get_setting("align_to_view"),
"show_cursor_while_painting": toolbar_settings.get_setting("show_cursor_while_painting"),
"gamma": toolbar_settings.get_setting("gamma"),
"jitter": toolbar_settings.get_setting("jitter"),
}
var brush_imgs: Array = toolbar_settings.get_setting("brush")
brush_data["image"] = brush_imgs[0]
brush_data["texture"] = brush_imgs[1]
update_decal() update_decal()
plugin.editor.set_brush_data(brush_data) plugin.editor.set_brush_data(brush_data)
@ -226,7 +195,7 @@ func update_decal() -> void:
else: else:
# Wait for cursor to recenter after right-click before revealing # Wait for cursor to recenter after right-click before revealing
# See https://github.com/godotengine/godot/issues/70098 # See https://github.com/godotengine/godot/issues/70098
await get_tree().create_timer(.05).timeout await get_tree().create_timer(.05).timeout
decal.visible = true decal.visible = true
decal.size = Vector3.ONE * brush_data["size"] decal.size = Vector3.ONE * brush_data["size"]
@ -239,7 +208,7 @@ func update_decal() -> void:
# Set texture and color # Set texture and color
if picking != Terrain3DEditor.TOOL_MAX: if picking != Terrain3DEditor.TOOL_MAX:
decal.texture_albedo = picker_texture decal.texture_albedo = ring_texture
decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing() decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
match picking: match picking:
Terrain3DEditor.HEIGHT: Terrain3DEditor.HEIGHT:
@ -250,7 +219,7 @@ func update_decal() -> void:
decal.modulate = COLOR_PICK_ROUGH decal.modulate = COLOR_PICK_ROUGH
decal.modulate.a = 1.0 decal.modulate.a = 1.0
else: else:
decal.texture_albedo = brush_data["texture"] decal.texture_albedo = brush_data["brush"][1]
match plugin.editor.get_tool(): match plugin.editor.get_tool():
Terrain3DEditor.HEIGHT: Terrain3DEditor.HEIGHT:
match plugin.editor.get_operation(): match plugin.editor.get_operation():
@ -270,7 +239,7 @@ func update_decal() -> void:
decal.modulate = COLOR_SLOPE decal.modulate = COLOR_SLOPE
_: _:
decal.modulate = Color.WHITE decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["opacity"]) decal.modulate.a = max(.3, brush_data["strength"])
Terrain3DEditor.TEXTURE: Terrain3DEditor.TEXTURE:
match plugin.editor.get_operation(): match plugin.editor.get_operation():
Terrain3DEditor.REPLACE: Terrain3DEditor.REPLACE:
@ -278,15 +247,15 @@ func update_decal() -> void:
decal.modulate.a = 1.0 decal.modulate.a = 1.0
Terrain3DEditor.ADD: Terrain3DEditor.ADD:
decal.modulate = COLOR_SPRAY decal.modulate = COLOR_SPRAY
decal.modulate.a = max(.3, brush_data["opacity"]) decal.modulate.a = max(.3, brush_data["strength"])
_: _:
decal.modulate = Color.WHITE decal.modulate = Color.WHITE
Terrain3DEditor.COLOR: Terrain3DEditor.COLOR:
decal.modulate = brush_data["color"].srgb_to_linear()*.5 decal.modulate = brush_data["color"].srgb_to_linear()*.5
decal.modulate.a = max(.3, brush_data["opacity"]) decal.modulate.a = max(.3, brush_data["strength"])
Terrain3DEditor.ROUGHNESS: Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_ROUGHNESS decal.modulate = COLOR_ROUGHNESS
decal.modulate.a = max(.3, brush_data["opacity"]) decal.modulate.a = max(.3, brush_data["strength"])
Terrain3DEditor.AUTOSHADER: Terrain3DEditor.AUTOSHADER:
decal.modulate = COLOR_AUTOSHADER decal.modulate = COLOR_AUTOSHADER
decal.modulate.a = 1.0 decal.modulate.a = 1.0
@ -298,15 +267,15 @@ func update_decal() -> void:
decal.modulate.a = 1.0 decal.modulate.a = 1.0
_: _:
decal.modulate = Color.WHITE decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["opacity"]) decal.modulate.a = max(.3, brush_data["strength"])
decal.size.y = max(1000, decal.size.y) decal.size.y = max(1000, decal.size.y)
decal.albedo_mix = 1.0 decal.albedo_mix = 1.0
decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 ) decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 )
decal_timer.start() decal_timer.start()
for gradient_decal in gradient_decals: for gradient_decal in gradient_decals:
gradient_decal.visible = false gradient_decal.visible = false
if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT: if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT:
var index := 0 var index := 0
for point in brush_data["gradient_points"]: for point in brush_data["gradient_points"]:
@ -320,16 +289,16 @@ func update_decal() -> void:
func _get_gradient_decal(index: int) -> Decal: func _get_gradient_decal(index: int) -> Decal:
if gradient_decals.size() > index: if gradient_decals.size() > index:
return gradient_decals[index] return gradient_decals[index]
var gradient_decal := Decal.new() var gradient_decal := Decal.new()
gradient_decal = Decal.new() gradient_decal = Decal.new()
gradient_decal.texture_albedo = picker_texture gradient_decal.texture_albedo = ring_texture
gradient_decal.modulate = COLOR_SLOPE gradient_decal.modulate = COLOR_SLOPE
gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing() gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
gradient_decal.size.y = 1000. gradient_decal.size.y = 1000.
gradient_decal.cull_mask = decal.cull_mask gradient_decal.cull_mask = decal.cull_mask
add_child(gradient_decal) add_child(gradient_decal)
gradient_decals.push_back(gradient_decal) gradient_decals.push_back(gradient_decal)
return gradient_decal return gradient_decal
@ -351,10 +320,10 @@ func clear_picking() -> void:
func is_picking() -> bool: func is_picking() -> bool:
if picking != Terrain3DEditor.TOOL_MAX: if picking != Terrain3DEditor.TOOL_MAX:
return true return true
if operation_builder and operation_builder.is_picking(): if operation_builder and operation_builder.is_picking():
return true return true
return false return false
@ -368,12 +337,16 @@ func pick(p_global_position: Vector3) -> void:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position) color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position)
Terrain3DEditor.COLOR: Terrain3DEditor.COLOR:
color = plugin.terrain.get_storage().get_color(p_global_position) color = plugin.terrain.get_storage().get_color(p_global_position)
Terrain3DEditor.ANGLE:
color = Color(plugin.terrain.get_storage().get_angle(p_global_position), 0., 0., 1.)
Terrain3DEditor.SCALE:
color = Color(plugin.terrain.get_storage().get_scale(p_global_position), 0., 0., 1.)
_: _:
push_error("Unsupported picking type: ", picking) push_error("Unsupported picking type: ", picking)
return return
picking_callback.call(picking, color, p_global_position) picking_callback.call(picking, color, p_global_position)
picking = Terrain3DEditor.TOOL_MAX picking = Terrain3DEditor.TOOL_MAX
elif operation_builder and operation_builder.is_picking(): elif operation_builder and operation_builder.is_picking():
operation_builder.pick(p_global_position, plugin.terrain) operation_builder.pick(p_global_position, plugin.terrain)

View file

@ -1,7 +1,7 @@
[configuration] [configuration]
entry_symbol = "terrain_3d_init" entry_symbol = "terrain_3d_init"
compatibility_minimum = 4.1 compatibility_minimum = 4.2
[libraries] [libraries]

View file

@ -54,14 +54,14 @@ func start_import(p_value: bool) -> void:
var min_max := Vector2(0, 1) var min_max := Vector2(0, 1)
var img: Image var img: Image
if height_file_name: if height_file_name:
img = Terrain3DStorage.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size) img = Terrain3DUtil.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size)
min_max = Terrain3D.get_min_max(img) min_max = Terrain3DUtil.get_min_max(img)
imported_images[Terrain3DStorage.TYPE_HEIGHT] = img imported_images[Terrain3DStorage.TYPE_HEIGHT] = img
if control_file_name: if control_file_name:
img = Terrain3DStorage.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE) img = Terrain3DUtil.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE)
imported_images[Terrain3DStorage.TYPE_CONTROL] = img imported_images[Terrain3DStorage.TYPE_CONTROL] = img
if color_file_name: if color_file_name:
img = Terrain3DStorage.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE) img = Terrain3DUtil.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE)
imported_images[Terrain3DStorage.TYPE_COLOR] = img imported_images[Terrain3DStorage.TYPE_COLOR] = img
if texture_list.get_texture_count() == 0: if texture_list.get_texture_count() == 0:
material.show_checkered = false material.show_checkered = false

View file

@ -1,54 +1,16 @@
[gd_scene load_steps=8 format=3 uid="uid://blaieaqp413k7"] [gd_scene load_steps=5 format=3 uid="uid://blaieaqp413k7"]
[ext_resource type="Script" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"] [ext_resource type="Script" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"]
[sub_resource type="Terrain3DStorage" id="Terrain3DStorage_5p5ir"] [sub_resource type="Terrain3DStorage" id="Terrain3DStorage_rmuvl"]
version = 0.842
[sub_resource type="Gradient" id="Gradient_5sc5a"] [sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_cjpaa"]
offsets = PackedFloat32Array(0.2, 1)
colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1)
[sub_resource type="FastNoiseLite" id="FastNoiseLite_lp4p7"]
noise_type = 2
frequency = 0.03
cellular_jitter = 3.0
cellular_return_type = 0
domain_warp_enabled = true
domain_warp_type = 1
domain_warp_amplitude = 50.0
domain_warp_fractal_type = 2
domain_warp_fractal_lacunarity = 1.5
domain_warp_fractal_gain = 1.0
[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_vyjp8"]
seamless = true
color_ramp = SubResource("Gradient_5sc5a")
noise = SubResource("FastNoiseLite_lp4p7")
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_f0cwi"]
_shader_parameters = {
"_mouse_layer": 2147483648,
"blend_sharpness": null,
"height_blending": null,
"macro_variation1": null,
"macro_variation2": null,
"noise1_angle": null,
"noise1_offset": null,
"noise1_scale": null,
"noise2_scale": null,
"noise3_scale": null,
"noise_texture": SubResource("NoiseTexture2D_vyjp8")
}
show_checkered = true show_checkered = true
[sub_resource type="Terrain3DTextureList" id="Terrain3DTextureList_4yf1r"] [sub_resource type="Terrain3DTextureList" id="Terrain3DTextureList_yjkn1"]
[node name="Importer" type="Terrain3D"] [node name="Importer" type="Terrain3D"]
storage = SubResource("Terrain3DStorage_5p5ir") storage = SubResource("Terrain3DStorage_rmuvl")
material = SubResource("Terrain3DMaterial_f0cwi") material = SubResource("Terrain3DMaterial_cjpaa")
texture_list = SubResource("Terrain3DTextureList_4yf1r") texture_list = SubResource("Terrain3DTextureList_yjkn1")
script = ExtResource("1_60b8f") script = ExtResource("1_60b8f")
import_position = Vector3(-1024, 0, -1024)
r16_range = Vector2(0, 250)
r16_size = Vector2i(2048, 2048)

View file

@ -0,0 +1,176 @@
@tool
extends Node3D
class_name Terrain3DObjects
const TransformChangedNotifier: Script = preload("res://addons/terrain_3d/utils/transform_changed_notifier.gd")
const CHILD_HELPER_NAME: StringName = &"TransformChangedSignaller"
const CHILD_HELPER_PATH: NodePath = ^"TransformChangedSignaller"
var _editor_interface = null
var _undo_redo = null
var _terrain_id: int
var _offsets: Dictionary # Object ID -> Vector3(X, Y offset relative to terrain height, Z)
var _ignore_transform_change: bool = false
func _exit_tree() -> void:
if not Engine.is_editor_hint() or not _editor_interface:
return
child_entered_tree.disconnect(_on_child_entered_tree)
child_exiting_tree.disconnect(_on_child_exiting_tree)
for child in get_children():
_on_child_exiting_tree(child)
func editor_setup(p_plugin) -> void:
if _editor_interface:
return
_editor_interface = p_plugin.get_editor_interface()
_undo_redo = p_plugin.get_undo_redo()
for child in get_children():
_on_child_entered_tree(child)
child_entered_tree.connect(_on_child_entered_tree)
child_exiting_tree.connect(_on_child_exiting_tree)
func get_terrain() -> Terrain3D:
var terrain := instance_from_id(_terrain_id) as Terrain3D
if not terrain or terrain.is_queued_for_deletion() or not terrain.is_inside_tree():
var terrains: Array[Node] = _editor_interface.get_edited_scene_root().find_children("", "Terrain3D")
if terrains.size() > 0:
terrain = terrains[0]
_terrain_id = terrain.get_instance_id() if terrain else 0
if terrain and terrain.storage and not terrain.storage.maps_edited.is_connected(_on_maps_edited):
terrain.storage.maps_edited.connect(_on_maps_edited)
return terrain
func _get_terrain_height(p_global_position: Vector3) -> float:
var terrain: Terrain3D = get_terrain()
if not terrain or not terrain.storage:
return 0.0
var height: float = terrain.storage.get_height(p_global_position)
if is_nan(height):
return 0.0
return height
func _on_child_entered_tree(p_node: Node) -> void:
if not (p_node is Node3D):
return
assert(p_node.get_parent() == self)
var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH)
if not helper:
helper = TransformChangedNotifier.new()
helper.name = CHILD_HELPER_NAME
p_node.add_child(helper, true, INTERNAL_MODE_BACK)
helper.transform_changed.connect(_on_child_transform_changed.bind(p_node))
assert(p_node.has_node(CHILD_HELPER_PATH))
var id: int = p_node.get_instance_id()
if not _offsets.has(id):
_update_child_offset(p_node)
else:
_update_child_position(p_node)
func _on_child_exiting_tree(p_node: Node) -> void:
if not (p_node is Node3D) or not p_node.has_node(CHILD_HELPER_PATH):
return
var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH)
if helper:
p_node.remove_child(helper)
helper.queue_free()
func _is_node_selected(p_node: Node) -> bool:
var editor_sel = _editor_interface.get_selection()
return editor_sel.get_transformable_selected_nodes().has(p_node)
func _on_child_transform_changed(p_node: Node3D) -> void:
if _ignore_transform_change:
return
var lmb_down := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
if lmb_down and (_is_node_selected(p_node) or _is_node_selected(self)):
# The user may be moving the node using gizmos.
# We should wait until they're done before updating otherwise gizmos + this node conflict.
return
if not _offsets.has(p_node.get_instance_id()):
return
var old_offset: Vector3 = _offsets[p_node.get_instance_id()]
var old_h: float = _get_terrain_height(old_offset)
var old_position: Vector3 = old_offset + Vector3(0, old_h, 0)
var new_position: Vector3 = p_node.global_position
if old_position.is_equal_approx(new_position):
return
var new_h: float = _get_terrain_height(new_position)
var new_offset: Vector3 = new_position - Vector3(0, new_h, 0)
var translate_without_reposition: bool = Input.is_key_pressed(KEY_SHIFT)
var y_changed: bool = not is_equal_approx(old_position.y, p_node.global_position.y)
if not y_changed and not translate_without_reposition:
new_offset.y = old_offset.y
new_position = new_offset + Vector3(0, new_h, 0)
# Make sure that when the user undo's the translation, the offset change gets undone too!
_undo_redo.create_action("Translate", UndoRedo.MERGE_ALL)
_undo_redo.add_do_property(self, &"_ignore_transform_change", true)
_undo_redo.add_undo_property(self, &"_ignore_transform_change", true)
_undo_redo.add_do_property(p_node, &"global_position", new_position)
_undo_redo.add_undo_property(p_node, &"global_position", old_position)
_undo_redo.add_do_method(self, &"_set_offset", p_node.get_instance_id(), new_offset)
_undo_redo.add_undo_method(self, &"_set_offset", p_node.get_instance_id(), old_offset)
_undo_redo.add_do_property(self, &"_ignore_transform_change", false)
_undo_redo.add_undo_property(self, &"_ignore_transform_change", false)
_undo_redo.commit_action()
func _set_offset(p_id: int, p_offset: Vector3) -> void:
_offsets[p_id] = p_offset
# Overwrite current offset stored for node with its current Y position relative to the terrain
func _update_child_offset(p_node: Node3D) -> void:
var position: Vector3 = global_transform * p_node.position
var h: float = _get_terrain_height(position)
var offset: Vector3 = position - Vector3(0, h, 0)
_offsets[p_node.get_instance_id()] = offset
# Overwrite node's current position with terrain height + stored offset for this node
func _update_child_position(p_node: Node3D) -> void:
if not _offsets.has(p_node.get_instance_id()):
return
var position: Vector3 = global_transform * p_node.position
var h: float = _get_terrain_height(position)
var offset: Vector3 = _offsets[p_node.get_instance_id()]
var new_position: Vector3 = global_transform.inverse() * (offset + Vector3(0, h, 0))
if not p_node.position.is_equal_approx(new_position):
p_node.position = new_position
func _on_maps_edited(p_edited_aabb: AABB) -> void:
var edited_area: AABB = p_edited_aabb.grow(1)
edited_area.position.y = -INF
edited_area.end.y = INF
for child in get_children():
var node := child as Node3D
if node && edited_area.has_point(node.global_position):
_update_child_position(node)

View file

@ -0,0 +1,14 @@
@tool
extends Node3D
signal transform_changed
func _ready() -> void:
assert(Engine.is_editor_hint())
set_notify_transform(true)
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED:
transform_changed.emit()

BIN
assets/fonts/Exo2/Exo2.ttf Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more