Upgraded Terrain3D to v0.9.3a-beta

This commit is contained in:
Rob Kelly 2024-11-17 12:47:37 -07:00
parent c99c470b00
commit 81e3f6ce93
44 changed files with 1993 additions and 831 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Cory Petkovsek, Roope Palmroos, and Contributors.
Copyright (c) 2024 Cory Petkovsek, Roope Palmroos, and Contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,4 +1,4 @@
<img src="doc/docs/images/terrain3d.png">
![Terrain3D Logo](/doc/docs/images/terrain3d.jpg)
# Terrain3D
A high performance, editable terrain system for Godot 4.
@ -7,7 +7,7 @@ A high performance, editable terrain system for Godot 4.
* Written in C++ as a GDExtension addon, which works with official engine builds
* Can be accessed by GDScript, C#, and any language Godot supports
* Geometric Clipmap Mesh Terrain, as used in The Witcher 3. See [System Architecture](https://terrain3d.readthedocs.io/en/stable/docs/system_architecture.html)
* Up to 16k x 16k in 1k regions (imagine multiple islands without paying for 16k^2 vram)
* Terrains as small as 64x64m up to 65.5x65.5km (4295km^2) in variable sized regions
* Up to 32 textures
* Up to 10 levels of detail
* Foliage instancing

View File

@ -7,29 +7,36 @@ extends EditorPlugin
const UI: Script = preload("res://addons/terrain_3d/src/ui.gd")
const RegionGizmo: Script = preload("res://addons/terrain_3d/src/region_gizmo.gd")
const ASSET_DOCK: String = "res://addons/terrain_3d/src/asset_dock.tscn"
const PS_DOCK_POSITION: String = "terrain3d/config/dock_position"
const PS_DOCK_PINNED: String = "terrain3d/config/dock_pinned"
var modifier_ctrl: bool
var modifier_alt: bool
var modifier_shift: bool
var _last_modifiers: int = 0
var _input_mode: int = 0 # -1: camera move, 0: none, 1: operating
var terrain: Terrain3D
var _last_terrain: Terrain3D
var nav_region: NavigationRegion3D
var editor: Terrain3DEditor
var editor_settings: EditorSettings
var ui: Node # Terrain3DUI see Godot #75388
var asset_dock: PanelContainer
var region_gizmo: RegionGizmo
var visible: bool
var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO
# Track negative input (CTRL)
var _negative_input: bool = false
# Track state prior to pressing CTRL: -1 not tracked, 0 false, 1 true
var _prev_enable_state: int = -1
var godot_editor_window: Window # The Godot Editor window
func _init() -> void:
# Get the Godot Editor window. Structure is root:Window/EditorNode/Base Control
godot_editor_window = EditorInterface.get_base_control().get_parent().get_parent()
godot_editor_window.focus_entered.connect(_on_godot_focus_entered)
func _enter_tree() -> void:
editor = Terrain3DEditor.new()
setup_editor_settings()
ui = UI.new()
ui.plugin = self
add_child(ui)
@ -41,7 +48,7 @@ func _enter_tree() -> void:
asset_dock = load(ASSET_DOCK).instantiate()
asset_dock.initialize(self)
func _exit_tree() -> void:
asset_dock.remove_dock(true)
asset_dock.queue_free()
@ -49,25 +56,46 @@ func _exit_tree() -> void:
editor.free()
scene_changed.disconnect(_on_scene_changed)
godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered)
func _on_godot_focus_entered() -> void:
_read_input()
ui.update_decal()
## EditorPlugin selection function call chain isn't consistent. Here's the map of calls:
## Assume we handle Terrain3D and NavigationRegion3D
# Click Terrain3D: _handles(Terrain3D), _make_visible(true), _edit(Terrain3D)
# Deselect: _make_visible(false), _edit(null)
# Click other node: _handles(OtherNode)
# Click NavRegion3D: _handles(NavReg3D), _make_visible(true), _edit(NavReg3D)
# Click NavRegion3D, Terrain3D: _handles(Terrain3D), _edit(Terrain3D)
# Click Terrain3D, NavRegion3D: _handles(NavReg3D), _edit(NavReg3D)
func _handles(p_object: Object) -> bool:
if p_object is Terrain3D:
return true
elif p_object is NavigationRegion3D and is_instance_valid(_last_terrain):
return true
# Terrain3DObjects requires access to EditorUndoRedoManager. The only way to make sure it
# always has it, is to pass it in here. _edit is NOT called if the node is cut and pasted.
if p_object is Terrain3DObjects:
elif p_object is Terrain3DObjects:
p_object.editor_setup(self)
elif p_object is Node3D and p_object.get_parent() is Terrain3DObjects:
p_object.get_parent().editor_setup(self)
if is_instance_valid(_last_terrain) and _last_terrain.is_inside_tree() and p_object is NavigationRegion3D:
return true
return false
func _make_visible(p_visible: bool, p_redraw: bool = false) -> void:
if p_visible and is_selected():
ui.set_visible(true)
asset_dock.update_dock()
else:
ui.set_visible(false)
func _edit(p_object: Object) -> void:
if !p_object:
_clear()
@ -77,39 +105,41 @@ func _edit(p_object: Object) -> void:
return
terrain = p_object
_last_terrain = terrain
terrain.set_plugin(self)
terrain.set_editor(editor)
editor.set_terrain(terrain)
region_gizmo.set_node_3d(terrain)
terrain.add_gizmo(region_gizmo)
terrain.set_plugin(self)
ui.set_visible(true)
terrain.set_meta("_edit_lock_", true)
# Deprecated 0.9.3 - Remove 1.0
if terrain.storage:
ui.terrain_menu.directory_setup.directory_setup_popup()
# Connect to new Assets resource
# Get alerted when a new asset list is loaded
if not terrain.assets_changed.is_connected(asset_dock.update_assets):
terrain.assets_changed.connect(asset_dock.update_assets)
asset_dock.update_assets()
# Connect to new Storage resource
if not terrain.storage_changed.is_connected(_load_storage):
terrain.storage_changed.connect(_load_storage)
_load_storage()
# Get alerted when the region map changes
if not terrain.data.region_map_changed.is_connected(update_region_grid):
terrain.data.region_map_changed.connect(update_region_grid)
update_region_grid()
else:
_clear()
if is_instance_valid(_last_terrain) and _last_terrain.is_inside_tree():
if is_terrain_valid(_last_terrain):
if p_object is NavigationRegion3D:
ui.set_visible(true, true)
nav_region = p_object
else:
nav_region = null
func _make_visible(p_visible: bool, p_redraw: bool = false) -> void:
visible = p_visible
ui.set_visible(visible)
update_region_grid()
asset_dock.update_dock(visible)
func _clear() -> void:
if is_terrain_valid():
terrain.storage_changed.disconnect(_load_storage)
if terrain.data.region_map_changed.is_connected(update_region_grid):
terrain.data.region_map_changed.disconnect(update_region_grid)
terrain.clear_gizmos()
terrain = null
@ -124,29 +154,12 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
if not is_terrain_valid():
return AFTER_GUI_INPUT_PASS
## Track negative input (CTRL)
if p_event is InputEventKey and not p_event.echo and p_event.keycode == KEY_CTRL:
if p_event.is_pressed():
_negative_input = true
_prev_enable_state = int(ui.toolbar_settings.get_setting("enable"))
ui.toolbar_settings.set_setting("enable", false)
else:
_negative_input = false
ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
_prev_enable_state = -1
_read_input(p_event)
## Handle mouse movement
if p_event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_PASS
## Setup active camera & viewport
if _prev_enable_state >= 0 and not Input.is_key_pressed(KEY_CTRL):
_negative_input = false
ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
_prev_enable_state = -1
## Setup for active camera & viewport
# Snap terrain to current camera
terrain.set_camera(p_viewport_camera)
@ -169,88 +182,131 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
else:
# Else look for intersection with terrain
var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir)
if intersection_point.z > 3.4e38 or is_nan(intersection_point.z): # max double or nan
if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan
return AFTER_GUI_INPUT_STOP
mouse_global_position = intersection_point
## Update decal
ui.decal.global_position = mouse_global_position
ui.decal.albedo_mix = 1.0
if ui.decal_timer.is_stopped():
ui.update_decal()
else:
ui.decal_timer.start()
## Update region highlight
var region_size = terrain.get_storage().get_region_size()
var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \
/ (region_size * terrain.get_mesh_vertex_spacing()) ).floor()
if current_region_position != region_position:
current_region_position = region_position
update_region_grid()
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and editor.is_operating():
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif p_event is InputEventMouseButton:
ui.update_decal()
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
if p_event.is_pressed():
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_STOP
# If picking
if ui.is_picking():
ui.pick(mouse_global_position)
if not ui.operation_builder or not ui.operation_builder.is_ready():
return AFTER_GUI_INPUT_STOP
# If adjusting regions
if editor.get_tool() == Terrain3DEditor.REGION:
# Skip regions that already exist or don't
var has_region: bool = terrain.get_storage().has_region(mouse_global_position)
var op: int = editor.get_operation()
if ( has_region and op == Terrain3DEditor.ADD) or \
( not has_region and op == Terrain3DEditor.SUBTRACT ):
return AFTER_GUI_INPUT_STOP
# If an automatic operation is ready to go (e.g. gradient)
if ui.operation_builder and ui.operation_builder.is_ready():
ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
# Mouse clicked, start editing
editor.start_operation(mouse_global_position)
if _input_mode != -1: # Not cam rotation
## Update region highlight
var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \
/ (terrain.get_region_size() * terrain.get_vertex_spacing()) ).floor()
if current_region_position != region_position:
current_region_position = region_position
update_region_grid()
if _input_mode > 0 and editor.is_operating():
# Inject pressure - Relies on C++ set_brush_data() using same dictionary instance
ui.brush_data["mouse_pressure"] = p_event.pressure
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif editor.is_operating():
# Mouse released, save undo data
editor.stop_operation()
return AFTER_GUI_INPUT_PASS
ui.update_decal()
if p_event is InputEventMouseButton and _input_mode > 0:
if p_event.is_pressed():
# If picking
if ui.is_picking():
ui.pick(mouse_global_position)
if not ui.operation_builder or not ui.operation_builder.is_ready():
return AFTER_GUI_INPUT_STOP
# If adjusting regions
if editor.get_tool() == Terrain3DEditor.REGION:
# Skip regions that already exist or don't
var has_region: bool = terrain.data.has_regionp(mouse_global_position)
var op: int = editor.get_operation()
if ( has_region and op == Terrain3DEditor.ADD) or \
( not has_region and op == Terrain3DEditor.SUBTRACT ):
return AFTER_GUI_INPUT_STOP
# If an automatic operation is ready to go (e.g. gradient)
if ui.operation_builder and ui.operation_builder.is_ready():
ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
# Mouse clicked, start editing
editor.start_operation(mouse_global_position)
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
# _input_apply released, save undo data
elif editor.is_operating():
editor.stop_operation()
return AFTER_GUI_INPUT_STOP
return AFTER_GUI_INPUT_PASS
func _read_input(p_event: InputEvent = null) -> void:
## Determine if user is moving camera or applying
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) or \
p_event is InputEventMouseButton and p_event.is_released() and \
p_event.get_button_index() == MOUSE_BUTTON_LEFT:
_input_mode = 1
else:
_input_mode = 0
func _load_storage() -> void:
if terrain:
update_region_grid()
match get_setting("editors/3d/navigation/navigation_scheme", 0):
2, 1: # Modo, Maya
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \
( Input.is_key_pressed(KEY_ALT) and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) ):
_input_mode = -1
if p_event is InputEventMouseButton and p_event.is_released() and \
( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \
( Input.is_key_pressed(KEY_ALT) and p_event.get_button_index() == MOUSE_BUTTON_LEFT )):
ui.last_rmb_time = Time.get_ticks_msec()
0, _: # Godot
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \
Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE):
_input_mode = -1
if p_event is InputEventMouseButton and p_event.is_released() and \
( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \
p_event.get_button_index() == MOUSE_BUTTON_MIDDLE ):
ui.last_rmb_time = Time.get_ticks_msec()
if _input_mode < 0:
return
## Determine modifiers pressed
modifier_shift = Input.is_key_pressed(KEY_SHIFT)
modifier_ctrl = Input.is_key_pressed(KEY_CTRL)
# Keybind enum: Alt,Space,Meta,Capslock
var alt_key: int
match get_setting("terrain3d/config/alt_key_bind", 0):
3: alt_key = KEY_CAPSLOCK
2: alt_key = KEY_META
1: alt_key = KEY_SPACE
0, _: alt_key = KEY_ALT
modifier_alt = Input.is_key_pressed(alt_key)
# Return if modifiers haven't changed AND brush_data has them;
# modifiers disappear from brush_data when clicking asset_dock (Why?)
var current_mods: int = int(modifier_shift) | int(modifier_ctrl) << 1 | int(modifier_alt) << 2
if _last_modifiers == current_mods and ui.brush_data.has("modifier_shift"):
return
_last_modifiers = current_mods
ui.brush_data["modifier_shift"] = modifier_shift
ui.brush_data["modifier_ctrl"] = modifier_ctrl
ui.brush_data["modifier_alt"] = modifier_alt
ui.update_modifiers()
func update_region_grid() -> void:
if not region_gizmo:
return
region_gizmo.set_hidden(not ui.visible)
region_gizmo.set_hidden(not visible)
if is_terrain_valid():
region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION
region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT
region_gizmo.region_position = current_region_position
region_gizmo.region_size = terrain.get_storage().get_region_size() * terrain.get_mesh_vertex_spacing()
region_gizmo.grid = terrain.get_storage().get_region_offsets()
region_gizmo.region_size = terrain.get_region_size() * terrain.get_vertex_spacing()
region_gizmo.grid = terrain.get_data().get_region_locations()
terrain.update_gizmos()
return
@ -278,22 +334,80 @@ func is_terrain_valid(p_terrain: Terrain3D = null) -> bool:
t = p_terrain
else:
t = terrain
if is_instance_valid(t) and t.is_inside_tree() and t.get_storage():
if is_instance_valid(t) and t.is_inside_tree() and t.data:
return true
return false
func is_selected() -> bool:
var selected: Array[Node] = get_editor_interface().get_selection().get_selected_nodes()
var selected: Array[Node] = EditorInterface.get_selection().get_selected_nodes()
for node in selected:
if node.get_instance_id() == _last_terrain.get_instance_id():
return true
if ( is_instance_valid(_last_terrain) and node.get_instance_id() == _last_terrain.get_instance_id() ) or \
node is Terrain3D:
return true
return false
func select_terrain() -> void:
if is_instance_valid(_last_terrain) and is_terrain_valid(_last_terrain) and not is_selected():
var es: EditorSelection = get_editor_interface().get_selection()
var es: EditorSelection = EditorInterface.get_selection()
es.clear()
es.add_node(_last_terrain)
## Editor Settings
func setup_editor_settings() -> void:
editor_settings = EditorInterface.get_editor_settings()
if not editor_settings.has_setting("terrain3d/config/alt_key_bind"):
editor_settings.set("terrain3d/config/alt_key_bind", 0)
var property_info = {
"name": "terrain3d/config/alt_key_bind",
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "Alt,Space,Meta,Capslock"
}
editor_settings.add_property_info(property_info)
_cleanup_old_settings()
# Remove or rename old settings
func _cleanup_old_settings() -> void:
# Rename deprecated settings - Remove in 1.0
var value: Variant
var rename_arr := [ "terrain3d/config/dock_slot", "terrain3d/config/dock_tile_size",
"terrain3d/config/dock_floating", "terrain3d/config/dock_always_on_top",
"terrain3d/config/dock_window_size", "terrain3d/config/dock_window_position", ]
for es: String in rename_arr:
if editor_settings.has_setting(es):
value = editor_settings.get_setting(es)
editor_settings.erase(es)
editor_settings.set_setting(es.replace("/config/dock_", "/dock/"), value)
# Special handling
var es: String = "terrain3d/tool_settings/slope"
if editor_settings.has_setting(es):
value = editor_settings.get_setting(es)
if typeof(value) == TYPE_FLOAT:
editor_settings.erase(es)
func set_setting(p_str: String, p_value: Variant) -> void:
editor_settings.set_setting(p_str, p_value)
func get_setting(p_str: String, p_default: Variant) -> Variant:
if editor_settings.has_setting(p_str):
return editor_settings.get_setting(p_str)
else:
return p_default
func has_setting(p_str: String) -> bool:
return editor_settings.has_setting(p_str)
func erase_setting(p_str: String) -> void:
editor_settings.erase(p_str)

View File

@ -10,7 +10,7 @@
# 1. Click import. The output window and console will report when finished.
# 1. Clear the script from your Terrain3D node, and save your scene.
#
# The instance transforms are now stored in your Storage resource.
# The instance transforms are now stored in your region files.
#
# Use clear_instances to erase all instances that match the assign_mesh_id.
#

View File

@ -1,30 +1,41 @@
// This shader is the minimum needed to allow the terrain to function, without any texturing.
shader_type spatial;
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform;
// Private uniforms
uniform float _region_size = 1024.0;
uniform float _region_texel_size = 0.0009765625; // = 1/1024
uniform float _mesh_vertex_spacing = 1.0;
uniform float _mesh_vertex_density = 1.0; // = 1/_mesh_vertex_spacing
uniform int _region_map_size = 16;
uniform int _region_map[256];
uniform vec2 _region_offsets[256];
uniform float _vertex_spacing = 1.0;
uniform float _vertex_density = 1.0; // = 1/_vertex_spacing
uniform int _region_map_size = 32;
uniform int _region_map[1024];
uniform vec2 _region_locations[1024];
uniform sampler2DArray _height_maps : repeat_disable;
uniform usampler2DArray _control_maps : repeat_disable;
uniform sampler2DArray _color_maps : source_color, filter_linear_mipmap_anisotropic, repeat_disable;
uniform sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2D noise_texture : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
uniform float _texture_uv_scale_array[32];
uniform float _texture_uv_rotation_array[32];
uniform float _texture_detile_array[32];
uniform vec4 _texture_color_array[32];
uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
uniform uint _mouse_layer = 0x80000000u; // Layer 32
// Public uniforms
uniform float vertex_normals_distance : hint_range(0, 1024) = 128.0;
// Varyings & Types
varying flat vec3 v_vertex; // World coordinate vertex location
varying flat vec3 v_camera_pos;
varying float v_vertex_xz_dist;
varying flat ivec3 v_region;
varying flat vec2 v_uv_offset;
varying flat vec2 v_uv2_offset;
varying vec3 v_normal;
varying float v_region_border_mask;
////////////////////////
// Vertex
@ -33,26 +44,22 @@ varying flat vec2 v_uv2_offset;
// Takes in UV world space coordinates, returns ivec3 with:
// XY: (0 to _region_size) coordinates within a region
// Z: layer index used for texturearrays, -1 if not in a region
ivec3 get_region_uv(vec2 uv) {
uv *= _region_texel_size;
ivec2 pos = ivec2(floor(uv)) + (_region_map_size / 2);
int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size);
ivec3 get_region_uv(const vec2 uv) {
ivec2 pos = ivec2(floor(uv * _region_texel_size)) + (_region_map_size / 2);
int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
return ivec3(ivec2((uv - _region_offsets[layer_index]) * _region_size), layer_index);
return ivec3(ivec2(mod(uv,_region_size)), layer_index);
}
// Takes in UV2 region space coordinates, returns vec3 with:
// XY: (0 to 1) coordinates within a region
// Z: layer index used for texturearrays, -1 if not in a region
vec3 get_region_uv2(vec2 uv) {
// Vertex function added half a texel to UV2, to center the UV's. vertex(), fragment() and get_height()
// call this with reclaimed versions of UV2, so to keep the last row/column within the correct
// window, take back the half pixel before the floor().
ivec2 pos = ivec2(floor(uv - vec2(_region_texel_size * 0.5))) + (_region_map_size / 2);
int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size);
vec3 get_region_uv2(const vec2 uv2) {
// Remove Texel Offset to ensure correct region index.
ivec2 pos = ivec2(floor(uv2 - vec2(_region_texel_size * 0.5))) + (_region_map_size / 2);
int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
// The return value is still texel-centered.
return vec3(uv - _region_offsets[layer_index], float(layer_index));
return vec3(uv2 - _region_locations[layer_index], float(layer_index));
}
// 1 lookup
@ -66,63 +73,75 @@ float get_height(vec2 uv) {
}
void vertex() {
// Get camera pos in world vertex coords
v_camera_pos = INV_VIEW_MATRIX[3].xyz;
// Get vertex of flat plane in world coordinates and set world UV
vec3 vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
// Camera distance to vertex on flat plane
v_vertex_xz_dist = length(v_vertex.xz - v_camera_pos.xz);
// UV coordinates in world space. Values are 0 to _region_size within regions
UV = round(vertex.xz * _mesh_vertex_density);
UV = round(v_vertex.xz * _vertex_density);
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size));
// Discard vertices for Holes. 1 lookup
ivec3 region = get_region_uv(UV);
uint control = texelFetch(_control_maps, region, 0).r;
v_region = get_region_uv(UV);
uint control = texelFetch(_control_maps, v_region, 0).r;
bool hole = bool(control >>2u & 0x1u);
// Show holes to all cameras except mouse camera (on exactly 1 layer)
if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
(hole || (_background_mode == 0u && region.z < 0)) ) {
VERTEX.x = 0./0.;
} else {
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
UV2 = (UV + vec2(0.5)) * _region_texel_size;
// Get final vertex location and save it
(hole || (_background_mode == 0u && (get_region_uv(UV - _region_texel_size) & v_region).z < 0))) {
VERTEX.x = 0. / 0.;
} else {
// Set final vertex height & calculate vertex normals. 3 lookups.
VERTEX.y = get_height(UV2);
v_vertex.y = VERTEX.y;
v_normal = vec3(
v_vertex.y - get_height(UV2 + vec2(_region_texel_size, 0)),
_vertex_spacing,
v_vertex.y - get_height(UV2 + vec2(0, _region_texel_size))
);
// Due to a bug caused by the GPUs linear interpolation across edges of region maps,
// mask region edges and use vertex normals only across region boundaries.
v_region_border_mask = mod(UV.x + 2.5, _region_size) - fract(UV.x) < 5.0 || mod(UV.y + 2.5, _region_size) - fract(UV.y) < 5.0 ? 1. : 0.;
}
// Transform UVs to local to avoid poor precision during varying interpolation.
v_uv_offset = MODEL_MATRIX[3].xz * _mesh_vertex_density;
v_uv_offset = MODEL_MATRIX[3].xz * _vertex_density;
UV -= v_uv_offset;
v_uv2_offset = v_uv_offset * _region_texel_size;
UV2 -= v_uv2_offset;
// Convert model space to view space w/ skip_vertex_transform render mode
VERTEX = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
VERTEX = (VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz);
TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz);
}
////////////////////////
// Fragment
////////////////////////
// 3 lookups
// 0 - 3 lookups
vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) {
// Get the height of the current vertex
float height = get_height(uv);
// Get the heights to the right and in front, but because of hardware
// interpolation on the edges of the heightmaps, the values are off
// causing the normal map to look weird. So, near the edges of the map
// get the heights to the left or behind instead. Hacky solution that
// reduces the artifact, but doesn't fix it entirely. See #185.
float u, v;
if(mod(uv.y*_region_size, _region_size) > _region_size-2.) {
v = get_height(uv + vec2(0, -_region_texel_size)) - height;
} else {
v = height - get_height(uv + vec2(0, _region_texel_size));
}
if(mod(uv.x*_region_size, _region_size) > _region_size-2.) {
u = get_height(uv + vec2(-_region_texel_size, 0)) - height;
float u, v, height;
vec3 normal;
// Use vertex normals within radius of vertex_normals_distance, and along region borders.
if (v_region_border_mask > 0.5 || v_vertex_xz_dist < vertex_normals_distance) {
normal = normalize(v_normal);
} else {
height = get_height(uv);
u = height - get_height(uv + vec2(_region_texel_size, 0));
v = height - get_height(uv + vec2(0, _region_texel_size));
normal = normalize(vec3(u, _vertex_spacing, v));
}
vec3 normal = vec3(u, _mesh_vertex_spacing, v);
normal = normalize(normal);
tangent = cross(normal, vec3(0, 0, 1));
binormal = cross(normal, tangent);
return normal;
@ -143,4 +162,3 @@ void fragment() {
// Apply PBR
ALBEDO=vec3(.2);
}

View File

@ -61,8 +61,8 @@
#warning += """No Terrain3D node found"""
#return
#
#if not _terrain.storage:
#warning += """Terrain3D storage is not initialized"""
#if not _terrain.data:
#warning += """Terrain3DData is not initialized"""
#return
#
## Get global transform
@ -70,8 +70,8 @@
#var gt_inverse := gt.affine_inverse()
#for i in transforms.list.size():
#var location: Vector3 = (gt * transforms.list[i]).origin
#var height: float = _terrain.storage.get_height(location)
#var normal: Vector3 = _terrain.storage.get_normal(location)
#var height: float = _terrain.data.get_height(location)
#var normal: Vector3 = _terrain.data.get_normal(location)
#
#if align_with_collision_normal and not is_nan(normal.x):
#transforms.list[i].basis.y = normal

BIN
addons/terrain_3d/icons/picker.svg (Stored with Git LFS)

Binary file not shown.

View File

@ -1,38 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c11ip32w7ln4v"
path="res://.godot/imported/picker.svg-0ed48f8d7e66014d2aac4b303bc65df6.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/terrain_3d/icons/picker.svg"
dest_files=["res://.godot/imported/picker.svg-0ed48f8d7e66014d2aac4b303bc65df6.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

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

View File

@ -6,12 +6,13 @@ signal confirmation_closed
signal confirmation_confirmed
signal confirmation_canceled
const PS_DOCK_SLOT: String = "terrain3d/config/dock_slot"
const PS_DOCK_TILE_SIZE: String = "terrain3d/config/dock_tile_size"
const PS_DOCK_FLOATING: String = "terrain3d/config/dock_floating"
const PS_DOCK_PINNED: String = "terrain3d/config/dock_always_on_top"
const PS_DOCK_WINDOW_POSITION: String = "terrain3d/config/dock_window_position"
const PS_DOCK_WINDOW_SIZE: String = "terrain3d/config/dock_window_size"
const ES_DOCK_SLOT: String = "terrain3d/dock/slot"
const ES_DOCK_TILE_SIZE: String = "terrain3d/dock/tile_size"
const ES_DOCK_FLOATING: String = "terrain3d/dock/floating"
const ES_DOCK_PINNED: String = "terrain3d/dock/always_on_top"
const ES_DOCK_WINDOW_POSITION: String = "terrain3d/dock/window_position"
const ES_DOCK_WINDOW_SIZE: String = "terrain3d/dock/window_size"
const ES_DOCK_TAB: String = "terrain3d/dock/tab"
var texture_list: ListContainer
var mesh_list: ListContainer
@ -40,10 +41,6 @@ enum {
}
var state: int = HIDDEN
var window: Window
var _godot_editor_window: Window # The main Godot Editor window
var _godot_last_state: Window.Mode = Window.MODE_FULLSCREEN
enum {
POS_LEFT_UL = 0,
POS_LEFT_BL = 1,
@ -59,17 +56,15 @@ enum {
var slot: int = POS_RIGHT_BR
var _initialized: bool = false
var plugin: EditorPlugin
var editor_settings: EditorSettings
var window: Window
var _godot_last_state: Window.Mode = Window.MODE_FULLSCREEN
func initialize(p_plugin: EditorPlugin) -> void:
if p_plugin:
plugin = p_plugin
# Get editor window. Structure is root:Window/EditorNode/Base Control
_godot_editor_window = plugin.get_editor_interface().get_base_control().get_parent().get_parent()
_godot_last_state = _godot_editor_window.mode
_godot_last_state = plugin.godot_editor_window.mode
placement_opt = $Box/Buttons/PlacementOpt
pinned_btn = $Box/Buttons/Pinned
floating_btn = $Box/Buttons/Floating
@ -93,7 +88,6 @@ func initialize(p_plugin: EditorPlugin) -> void:
asset_container.add_child(mesh_list)
_current_list = texture_list
editor_settings = EditorInterface.get_editor_settings()
load_editor_settings()
# Connect signals
@ -103,7 +97,7 @@ func initialize(p_plugin: EditorPlugin) -> void:
placement_opt.item_selected.connect(set_slot)
floating_btn.pressed.connect(make_dock_float)
pinned_btn.toggled.connect(_on_pin_changed)
pinned_btn.visible = false
pinned_btn.visible = ( window != null )
size_slider.value_changed.connect(_on_slider_changed)
plugin.ui.toolbar.tool_changed.connect(_on_tool_changed)
@ -111,7 +105,7 @@ func initialize(p_plugin: EditorPlugin) -> void:
textures_btn.add_theme_font_size_override("font_size", 16 * EditorInterface.get_editor_scale())
_initialized = true
update_dock(plugin.visible)
update_dock()
update_layout()
@ -122,7 +116,7 @@ func _ready() -> void:
# Setup styles
set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel"))
# Avoid saving icon resources in tscn when editing w/ a tool script
if plugin.get_editor_interface().get_edited_scene_root() != self:
if EditorInterface.get_edited_scene_root() != self:
pinned_btn.icon = get_theme_icon("Pin", "EditorIcons")
pinned_btn.text = ""
floating_btn.icon = get_theme_icon("MakeFloating", "EditorIcons")
@ -154,7 +148,7 @@ func set_slot(p_slot: int) -> void:
placement_opt.selected = slot
save_editor_settings()
plugin.select_terrain()
update_dock(plugin.visible)
update_dock()
func remove_dock(p_force: bool = false) -> void:
@ -167,50 +161,42 @@ func remove_dock(p_force: bool = false) -> void:
state = HIDDEN
# If windowed and destination is not window or final exit, otherwise leave
elif state == WINDOWED and p_force:
if not window:
return
elif state == WINDOWED and p_force and window:
var parent: Node = get_parent()
if parent:
parent.remove_child(self)
_godot_editor_window.mouse_entered.disconnect(_on_godot_window_entered)
_godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered)
_godot_editor_window.focus_exited.disconnect(_on_godot_focus_exited)
window.hide()
window.queue_free()
window = null
plugin.godot_editor_window.mouse_entered.disconnect(_on_godot_window_entered)
plugin.godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered)
plugin.godot_editor_window.focus_exited.disconnect(_on_godot_focus_exited)
window.hide()
window.queue_free()
window = null
floating_btn.button_pressed = false
floating_btn.visible = true
pinned_btn.visible = false
placement_opt.visible = true
state = HIDDEN
update_dock(plugin.visible) # return window to side/bottom
update_dock() # return window to side/bottom
func update_dock(p_visible: bool) -> void:
func update_dock() -> void:
if not _initialized or window:
return
update_assets()
if not _initialized:
return
if window:
return
elif floating_btn.button_pressed:
# No window, but floating button pressed, occurs when from editor settings
make_dock_float()
return
# Move dock to new destination
remove_dock()
# Add dock to new destination
# Sidebar
if slot < POS_BOTTOM:
state = SIDEBAR
plugin.add_control_to_dock(slot, self)
# Bottom
elif slot == POS_BOTTOM:
state = BOTTOM
plugin.add_control_to_bottom_panel(self, "Terrain3D")
if p_visible:
plugin.make_bottom_panel_item_visible(self)
plugin.make_bottom_panel_item_visible(self)
func update_layout() -> void:
if not _initialized:
@ -257,6 +243,8 @@ func update_thumbnails() -> void:
_last_thumb_update_time = Time.get_ticks_msec()
for mesh_asset in mesh_list.entries:
mesh_asset.queue_redraw()
## Dock Button handlers
@ -282,7 +270,9 @@ func _on_textures_pressed() -> void:
textures_btn.button_pressed = true
meshes_btn.button_pressed = false
texture_list.set_selected_id(texture_list.selected_id)
plugin.get_editor_interface().edit_node(plugin.terrain)
if plugin.is_terrain_valid():
EditorInterface.edit_node(plugin.terrain)
save_editor_settings()
func _on_meshes_pressed() -> void:
@ -293,14 +283,16 @@ func _on_meshes_pressed() -> void:
meshes_btn.button_pressed = true
textures_btn.button_pressed = false
mesh_list.set_selected_id(mesh_list.selected_id)
plugin.get_editor_interface().edit_node(plugin.terrain)
if plugin.is_terrain_valid():
EditorInterface.edit_node(plugin.terrain)
update_thumbnails()
save_editor_settings()
func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
if p_tool == Terrain3DEditor.INSTANCER:
_on_meshes_pressed()
elif p_tool == Terrain3DEditor.TEXTURE:
elif p_tool in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]:
_on_textures_pressed()
@ -320,24 +312,34 @@ func update_assets() -> void:
_current_list.update_asset_list()
## Window Management
func make_dock_float() -> void:
# If already created (eg from editor Make Floating)
# If not already created (eg from editor panel 'Make Floating' button)
if not window:
remove_dock()
create_window()
state = WINDOWED
visible = true # Asset dock contents are hidden when popping out of the bottom!
pinned_btn.visible = true
floating_btn.visible = false
placement_opt.visible = false
window.title = "Terrain3D Asset Dock"
window.always_on_top = pinned_btn.button_pressed
window.close_requested.connect(remove_dock.bind(true))
visible = true # Is hidden when pops off of bottom. ??
_godot_editor_window.grab_focus()
window.window_input.connect(_on_window_input)
window.focus_exited.connect(save_editor_settings)
window.mouse_exited.connect(save_editor_settings)
window.size_changed.connect(save_editor_settings)
plugin.godot_editor_window.mouse_entered.connect(_on_godot_window_entered)
plugin.godot_editor_window.focus_entered.connect(_on_godot_focus_entered)
plugin.godot_editor_window.focus_exited.connect(_on_godot_focus_exited)
plugin.godot_editor_window.grab_focus()
update_assets()
save_editor_settings()
func create_window() -> void:
@ -348,79 +350,80 @@ func create_window() -> void:
mc.add_child(self)
window.add_child(mc)
window.set_transient(false)
window.set_size(get_setting(PS_DOCK_WINDOW_SIZE, Vector2i(512, 512)))
window.set_position(get_setting(PS_DOCK_WINDOW_POSITION, Vector2i(704, 284)))
window.set_size(plugin.get_setting(ES_DOCK_WINDOW_SIZE, Vector2i(512, 512)))
window.set_position(plugin.get_setting(ES_DOCK_WINDOW_POSITION, Vector2i(704, 284)))
plugin.add_child(window)
window.show()
window.window_input.connect(_on_window_input)
window.focus_exited.connect(_on_window_focus_exited)
_godot_editor_window.mouse_entered.connect(_on_godot_window_entered)
_godot_editor_window.focus_entered.connect(_on_godot_focus_entered)
_godot_editor_window.focus_exited.connect(_on_godot_focus_exited)
func clamp_window_position() -> void:
if window and window.visible:
var bounds: Vector2i
if EditorInterface.get_editor_settings().get_setting("interface/editor/single_window_mode"):
bounds = EditorInterface.get_base_control().size
else:
bounds = DisplayServer.screen_get_position(window.current_screen)
bounds += DisplayServer.screen_get_size(window.current_screen)
var margin: int = 40
window.position.x = clamp(window.position.x, -window.size.x + 2*margin, bounds.x - margin)
window.position.y = clamp(window.position.y, 25, bounds.y - margin)
func _on_window_input(event: InputEvent) -> void:
# Capture CTRL+S when doc focused to save scene)
# Capture CTRL+S when doc focused to save scene
if event is InputEventKey and event.keycode == KEY_S and event.pressed and event.is_command_or_control_pressed():
save_editor_settings()
plugin.get_editor_interface().save_scene()
func _on_window_focus_exited() -> void:
# Capture window position w/o other changes
save_editor_settings()
EditorInterface.save_scene()
func _on_godot_window_entered() -> void:
if is_instance_valid(window) and window.has_focus():
_godot_editor_window.grab_focus()
plugin.godot_editor_window.grab_focus()
func _on_godot_focus_entered() -> void:
# If asset dock is windowed, and Godot was minimized, and now is not, restore asset dock window
if is_instance_valid(window):
if _godot_last_state == Window.MODE_MINIMIZED and _godot_editor_window.mode != Window.MODE_MINIMIZED:
if _godot_last_state == Window.MODE_MINIMIZED and plugin.godot_editor_window.mode != Window.MODE_MINIMIZED:
window.show()
_godot_last_state = _godot_editor_window.mode
_godot_editor_window.grab_focus()
_godot_last_state = plugin.godot_editor_window.mode
plugin.godot_editor_window.grab_focus()
func _on_godot_focus_exited() -> void:
if is_instance_valid(window) and _godot_editor_window.mode == Window.MODE_MINIMIZED:
if is_instance_valid(window) and plugin.godot_editor_window.mode == Window.MODE_MINIMIZED:
window.hide()
_godot_last_state = _godot_editor_window.mode
_godot_last_state = plugin.godot_editor_window.mode
## Manage Editor Settings
func get_setting(p_str: String, p_default: Variant) -> Variant:
if editor_settings.has_setting(p_str):
return editor_settings.get_setting(p_str)
else:
return p_default
func load_editor_settings() -> void:
floating_btn.button_pressed = get_setting(PS_DOCK_FLOATING, false)
pinned_btn.button_pressed = get_setting(PS_DOCK_PINNED, true)
size_slider.value = get_setting(PS_DOCK_TILE_SIZE, 83)
set_slot(get_setting(PS_DOCK_SLOT, POS_BOTTOM))
floating_btn.button_pressed = plugin.get_setting(ES_DOCK_FLOATING, false)
pinned_btn.button_pressed = plugin.get_setting(ES_DOCK_PINNED, true)
size_slider.value = plugin.get_setting(ES_DOCK_TILE_SIZE, 83)
_on_slider_changed(size_slider.value)
# Window pos/size set on window creation in update_dock
update_dock(plugin.visible)
set_slot(plugin.get_setting(ES_DOCK_SLOT, POS_BOTTOM))
if floating_btn.button_pressed:
make_dock_float()
# TODO Don't save tab until thumbnail generation more reliable
#if plugin.get_setting(ES_DOCK_TAB, 0) == 1:
# _on_meshes_pressed()
func save_editor_settings() -> void:
if not _initialized:
return
editor_settings.set_setting(PS_DOCK_SLOT, slot)
editor_settings.set_setting(PS_DOCK_TILE_SIZE, size_slider.value)
editor_settings.set_setting(PS_DOCK_FLOATING, floating_btn.button_pressed)
editor_settings.set_setting(PS_DOCK_PINNED, pinned_btn.button_pressed)
clamp_window_position()
plugin.set_setting(ES_DOCK_SLOT, slot)
plugin.set_setting(ES_DOCK_TILE_SIZE, size_slider.value)
plugin.set_setting(ES_DOCK_FLOATING, floating_btn.button_pressed)
plugin.set_setting(ES_DOCK_PINNED, pinned_btn.button_pressed)
# TODO Don't save tab until thumbnail generation more reliable
# plugin.set_setting(ES_DOCK_TAB, 0 if _current_list == texture_list else 1)
if window:
editor_settings.set_setting(PS_DOCK_WINDOW_SIZE, window.size)
editor_settings.set_setting(PS_DOCK_WINDOW_POSITION, window.position)
plugin.set_setting(ES_DOCK_WINDOW_SIZE, window.size)
plugin.set_setting(ES_DOCK_WINDOW_POSITION, window.position)
##############################################################
@ -527,7 +530,8 @@ class ListContainer extends Container:
plugin.select_terrain()
# Select Paint tool if clicking a texture
if type == Terrain3DAssets.TYPE_TEXTURE and plugin.editor.get_tool() != Terrain3DEditor.TEXTURE:
if type == Terrain3DAssets.TYPE_TEXTURE and \
not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]:
var paint_btn: Button = plugin.ui.toolbar.get_node_or_null("PaintBaseTexture")
if paint_btn:
paint_btn.set_pressed(true)
@ -545,7 +549,7 @@ class ListContainer extends Container:
func _on_resource_inspected(p_resource: Resource) -> void:
await get_tree().create_timer(.01).timeout
plugin.get_editor_interface().edit_resource(p_resource)
EditorInterface.edit_resource(p_resource)
func _on_resource_changed(p_resource: Resource, p_id: int) -> void:
@ -575,7 +579,7 @@ class ListContainer extends Container:
# If removing an entry, clear inspector
if not p_resource:
plugin.get_editor_interface().inspect_object(null)
EditorInterface.inspect_object(null)
# If null resource, remove last
if not p_resource:
@ -694,7 +698,7 @@ class ListEntry extends VBoxContainer:
else:
name_label.text = "Add Mesh"
func _notification(p_what) -> void:
match p_what:
NOTIFICATION_DRAW:

View File

@ -5,37 +5,39 @@
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 115)
size = Vector2i(400, 155)
visible = true
script = ExtResource("1_sf76d")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="MarginContainer" type="MarginContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
offset_right = 392.0
offset_bottom = 106.0
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "LOD:"
[node name="LodBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
[node name="LodBox" type="SpinBox" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
max_value = 8.0
value = 4.0
[node name="DescriptionLabel" type="Label" parent="VBoxContainer"]
[node name="DescriptionLabel" type="Label" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2

View File

@ -44,11 +44,14 @@ func bake_mesh_popup() -> void:
if plugin.terrain:
bake_method = _bake_mesh
bake_lod_dialog.description = BAKE_MESH_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog)
EditorInterface.popup_dialog_centered(bake_lod_dialog)
func _bake_mesh() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_NEAREST)
if plugin.terrain.data.get_region_count() == 0:
push_error("Terrain3D has no active regions to bake")
return
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DData.HEIGHT_FILTER_NEAREST)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
@ -65,7 +68,7 @@ func _bake_mesh() -> void:
undo.add_do_method(plugin.terrain, &"add_child", mesh_instance, true)
undo.add_undo_method(plugin.terrain, &"remove_child", mesh_instance)
undo.add_do_property(mesh_instance, &"owner", plugin.terrain.owner)
undo.add_do_property(mesh_instance, &"owner", EditorInterface.get_edited_scene_root())
undo.add_do_reference(mesh_instance)
else:
@ -86,11 +89,14 @@ func bake_occluder_popup() -> void:
if plugin.terrain:
bake_method = _bake_occluder
bake_lod_dialog.description = BAKE_OCCLUDER_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(bake_lod_dialog)
EditorInterface.popup_dialog_centered(bake_lod_dialog)
func _bake_occluder() -> void:
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DStorage.HEIGHT_FILTER_MINIMUM)
if plugin.terrain.data.get_region_count() == 0:
push_error("Terrain3D has no active regions to bake")
return
var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DData.HEIGHT_FILTER_MINIMUM)
if !mesh:
push_error("Failed to bake mesh from Terrain3D")
return
@ -113,7 +119,7 @@ func _bake_occluder() -> void:
undo.add_do_method(plugin.terrain, &"add_child", occluder_instance, true)
undo.add_undo_method(plugin.terrain, &"remove_child", occluder_instance)
undo.add_do_property(occluder_instance, &"owner", plugin.terrain.owner)
undo.add_do_property(occluder_instance, &"owner", EditorInterface.get_edited_scene_root())
undo.add_do_reference(occluder_instance)
else:
@ -153,7 +159,7 @@ func find_nav_region_terrains(p_nav_region: NavigationRegion3D) -> Array[Terrain
func find_terrain_nav_regions(p_terrain: Terrain3D) -> Array[NavigationRegion3D]:
var result: Array[NavigationRegion3D] = []
var root: Node = plugin.get_editor_interface().get_edited_scene_root()
var root: Node = EditorInterface.get_edited_scene_root()
if not root:
return result
for nav_region in root.find_children("", "NavigationRegion3D", true, true):
@ -169,6 +175,9 @@ func bake_nav_mesh() -> void:
print("Terrain3DNavigation: Finished baking 1 NavigationMesh.")
elif plugin.terrain:
if plugin.terrain.data.get_region_count() == 0:
push_error("Terrain3D has no active regions to bake")
return
# A Terrain3D is selected. There are potentially multiple navmeshes to bake and we need to
# find them all. (The multiple navmesh use-case is likely on very large scenes with lots of
# geometry. Each navmesh in this case would define its own, non-overlapping, baking AABB, to
@ -329,11 +338,17 @@ func set_up_navigation_popup() -> void:
if plugin.terrain:
bake_method = _set_up_navigation
confirm_dialog.dialog_text = SET_UP_NAVIGATION_DESCRIPTION
plugin.get_editor_interface().popup_dialog_centered(confirm_dialog)
EditorInterface.popup_dialog_centered(confirm_dialog)
func _set_up_navigation() -> void:
assert(plugin.terrain)
if plugin.terrain == EditorInterface.get_edited_scene_root():
push_error("Terrain3D Navigation setup not possible if Terrain3D node is scene root")
return
if plugin.terrain.data.get_region_count() == 0:
push_error("Terrain3D has no active regions")
return
var terrain: Terrain3D = plugin.terrain
var nav_region := NavigationRegion3D.new()
@ -348,7 +363,7 @@ func _set_up_navigation() -> void:
undo_redo.add_do_reference(nav_region)
undo_redo.commit_action()
plugin.get_editor_interface().inspect_object(nav_region)
EditorInterface.inspect_object(nav_region)
assert(plugin.nav_region == nav_region)
bake_nav_mesh()

View File

@ -1,21 +1,38 @@
extends Object
extends RefCounted
const WINDOW_SCENE: String = "res://addons/terrain_3d/src/channel_packer.tscn"
const TEMPLATE_PATH: String = "res://addons/terrain_3d/src/channel_packer_import_template.txt"
const DRAG_DROP_SCRIPT: String = "res://addons/terrain_3d/src/channel_packer_dragdrop.gd"
enum {
IMAGE_ALBEDO,
IMAGE_HEIGHT,
IMAGE_NORMAL,
IMAGE_ROUGHNESS,
INFO,
WARN,
ERROR,
}
enum {
IMAGE_ALBEDO,
IMAGE_HEIGHT,
IMAGE_NORMAL,
IMAGE_ROUGHNESS
}
var plugin: EditorPlugin
var editor_interface: EditorInterface
var dialog: AcceptDialog
var save_file_dialog: FileDialog
var open_file_dialog: FileDialog
var window: Window
var save_file_dialog: EditorFileDialog
var open_file_dialog: EditorFileDialog
var invert_green_checkbox: CheckBox
var invert_smooth_checkbox: CheckBox
var invert_height_checkbox: CheckBox
var lumin_height_button: Button
var generate_mipmaps_checkbox: CheckBox
var high_quality_checkbox: CheckBox
var align_normals_checkbox: CheckBox
var resize_toggle_checkbox: CheckBox
var resize_option_box: SpinBox
var height_channel: Array[Button]
var height_channel_selected: int = 0
var roughness_channel: Array[Button]
var roughness_channel_selected: int = 0
var last_opened_directory: String
var last_saved_directory: String
var packing_albedo: bool = false
@ -24,133 +41,283 @@ var images: Array[Image] = [null, null, null, null]
var status_label: Label
var no_op: Callable = func(): pass
var last_file_selected_fn: Callable = no_op
var normal_vector: Vector3
func pack_textures_popup() -> void:
if dialog != null:
print("Terrain3DChannelPacker: Cannot open pack tool, dialog already open.")
if window != null:
window.show()
window.move_to_foreground()
window.move_to_center()
return
dialog = (load(WINDOW_SCENE) as PackedScene).instantiate()
dialog.confirmed.connect(_on_close_requested)
dialog.canceled.connect(_on_close_requested)
status_label = dialog.find_child("StatusLabel")
invert_green_checkbox = dialog.find_child("InvertGreenChannelCheckBox")
editor_interface = plugin.get_editor_interface()
_init_file_dialogs()
editor_interface.popup_dialog_centered(dialog)
_init_texture_picker(dialog.find_child("AlbedoVBox"), IMAGE_ALBEDO)
_init_texture_picker(dialog.find_child("HeightVBox"), IMAGE_HEIGHT)
_init_texture_picker(dialog.find_child("NormalVBox"), IMAGE_NORMAL)
_init_texture_picker(dialog.find_child("RoughnessVBox"), IMAGE_ROUGHNESS)
window = (load(WINDOW_SCENE) as PackedScene).instantiate()
window.close_requested.connect(_on_close_requested)
window.window_input.connect(func(event:InputEvent):
if event is InputEventKey:
if event.pressed and event.keycode == KEY_ESCAPE:
_on_close_requested()
)
window.find_child("CloseButton").pressed.connect(_on_close_requested)
status_label = window.find_child("StatusLabel") as Label
invert_green_checkbox = window.find_child("InvertGreenChannelCheckBox") as CheckBox
invert_smooth_checkbox = window.find_child("InvertSmoothCheckBox") as CheckBox
invert_height_checkbox = window.find_child("ConvertDepthToHeight") as CheckBox
lumin_height_button = window.find_child("LuminanceAsHeightButton") as Button
generate_mipmaps_checkbox = window.find_child("GenerateMipmapsCheckBox") as CheckBox
high_quality_checkbox = window.find_child("HighQualityCheckBox") as CheckBox
align_normals_checkbox = window.find_child("AlignNormalsCheckBox") as CheckBox
resize_toggle_checkbox = window.find_child("ResizeToggle") as CheckBox
resize_option_box = window.find_child("ResizeOptionButton") as SpinBox
height_channel = [
window.find_child("HeightChannelR") as Button,
window.find_child("HeightChannelG") as Button,
window.find_child("HeightChannelB") as Button,
window.find_child("HeightChannelA") as Button
]
roughness_channel = [
window.find_child("RoughnessChannelR") as Button,
window.find_child("RoughnessChannelG") as Button,
window.find_child("RoughnessChannelB") as Button,
window.find_child("RoughnessChannelA") as Button
]
height_channel[0].pressed.connect(func() -> void: height_channel_selected = 0)
height_channel[1].pressed.connect(func() -> void: height_channel_selected = 1)
height_channel[2].pressed.connect(func() -> void: height_channel_selected = 2)
height_channel[3].pressed.connect(func() -> void: height_channel_selected = 3)
roughness_channel[0].pressed.connect(func() -> void: roughness_channel_selected = 0)
roughness_channel[1].pressed.connect(func() -> void: roughness_channel_selected = 1)
roughness_channel[2].pressed.connect(func() -> void: roughness_channel_selected = 2)
roughness_channel[3].pressed.connect(func() -> void: roughness_channel_selected = 3)
plugin.add_child(window)
_init_file_dialogs()
# the dialog disables the parent window "on top" so, restore it after 1 frame to alow the dialog to clear.
var set_on_top_fn: Callable = func(_file: String = "") -> void:
await RenderingServer.frame_post_draw
window.always_on_top = true
save_file_dialog.file_selected.connect(set_on_top_fn)
save_file_dialog.canceled.connect(set_on_top_fn)
open_file_dialog.file_selected.connect(set_on_top_fn)
open_file_dialog.canceled.connect(set_on_top_fn)
_init_texture_picker(window.find_child("AlbedoVBox"), IMAGE_ALBEDO)
_init_texture_picker(window.find_child("HeightVBox"), IMAGE_HEIGHT)
_init_texture_picker(window.find_child("NormalVBox"), IMAGE_NORMAL)
_init_texture_picker(window.find_child("RoughnessVBox"), IMAGE_ROUGHNESS)
var pack_button_path: String = "Panel/MarginContainer/VBoxContainer/PackButton"
(dialog.get_node(pack_button_path) as Button).pressed.connect(_on_pack_button_pressed)
(window.get_node(pack_button_path) as Button).pressed.connect(_on_pack_button_pressed)
func _on_close_requested() -> void:
last_file_selected_fn = no_op
images = [null, null, null, null]
dialog.queue_free()
dialog = null
window.queue_free()
window = null
func _init_file_dialogs() -> void:
save_file_dialog = FileDialog.new()
save_file_dialog = EditorFileDialog.new()
save_file_dialog.set_filters(PackedStringArray(["*.png"]))
save_file_dialog.set_file_mode(FileDialog.FILE_MODE_SAVE_FILE)
save_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
save_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_SAVE_FILE)
save_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
save_file_dialog.file_selected.connect(_on_save_file_selected)
open_file_dialog = FileDialog.new()
open_file_dialog.set_filters(PackedStringArray(["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", ".ktx"]))
open_file_dialog.set_file_mode(FileDialog.FILE_MODE_OPEN_FILE)
open_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
dialog.add_child(save_file_dialog)
dialog.add_child(open_file_dialog)
save_file_dialog.ok_button_text = "Save"
save_file_dialog.size = Vector2i(550, 550)
#save_file_dialog.transient = false
#save_file_dialog.exclusive = false
#save_file_dialog.popup_window = true
open_file_dialog = EditorFileDialog.new()
open_file_dialog.set_filters(PackedStringArray(
["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", "*.ktx", "*.dds"]))
open_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_OPEN_FILE)
open_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
open_file_dialog.ok_button_text = "Open"
open_file_dialog.size = Vector2i(550, 550)
#open_file_dialog.transient = false
#open_file_dialog.exclusive = false
#open_file_dialog.popup_window = true
window.add_child(save_file_dialog)
window.add_child(open_file_dialog)
func _init_texture_picker(p_parent: Node, p_image_index: int) -> void:
var line_edit: LineEdit = p_parent.find_child("LineEdit")
var file_pick_button: Button = p_parent.find_child("PickButton")
var clear_button: Button = p_parent.find_child("ClearButton")
var texture_rect: TextureRect = p_parent.find_child("TextureRect")
var texture_button: Button = p_parent.find_child("TextureButton")
var line_edit: LineEdit = p_parent.find_child("LineEdit") as LineEdit
var file_pick_button: Button = p_parent.find_child("PickButton") as Button
var clear_button: Button = p_parent.find_child("ClearButton") as Button
var texture_rect: TextureRect = p_parent.find_child("TextureRect") as TextureRect
var texture_button: Button = p_parent.find_child("TextureButton") as Button
texture_button.set_script(load(DRAG_DROP_SCRIPT) as GDScript)
var set_channel_fn: Callable = func(used_channels: int) -> void:
var channel_count: int = 4
# enum Image.UsedChannels
match used_channels:
Image.USED_CHANNELS_L, Image.USED_CHANNELS_R: channel_count = 1
Image.USED_CHANNELS_LA, Image.USED_CHANNELS_RG: channel_count = 2
Image.USED_CHANNELS_RGB: channel_count = 3
Image.USED_CHANNELS_RGBA: channel_count = 4
if p_image_index == IMAGE_HEIGHT:
for i in 4:
height_channel[i].visible = i < channel_count
height_channel[0].button_pressed = true
height_channel[0].pressed.emit()
elif p_image_index == IMAGE_ROUGHNESS:
for i in 4:
roughness_channel[i].visible = i < channel_count
roughness_channel[0].button_pressed = true
roughness_channel[0].pressed.emit()
var load_image_fn: Callable = func(path: String):
var image: Image = Image.new()
var error: int = OK
# Special case for dds files
if path.get_extension() == "dds":
image = ResourceLoader.load(path).get_image()
if not image.is_empty():
# if the dds file is loaded, we must clear any mipmaps and
# decompress if needed in order to do per pixel operations.
image.clear_mipmaps()
image.decompress()
else:
error = FAILED
else:
error = image.load(path)
if error != OK:
_show_message(ERROR, "Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
_show_message(INFO, "Loaded texture '" + path + "'")
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
_set_wh_labels(p_image_index, image.get_width(), image.get_height())
if p_image_index == IMAGE_NORMAL:
_set_normal_vector(image)
if p_image_index == IMAGE_HEIGHT or p_image_index == IMAGE_ROUGHNESS:
set_channel_fn.call(image.detect_used_channels())
var os_drop_fn: Callable = func(files: PackedStringArray) -> void:
# OS drag drop holds mouse focus until released,
# Get mouse pos and check directly if inside texture_rect
var rect = texture_button.get_global_rect()
var mouse_position = texture_button.get_global_mouse_position()
if rect.has_point(mouse_position):
if files.size() != 1:
_show_message(ERROR, "Cannot load multiple files")
else:
line_edit.text = files[0]
load_image_fn.call(files[0])
var godot_drop_fn: Callable = func(path: String) -> void:
path = ProjectSettings.globalize_path(path)
line_edit.text = path
load_image_fn.call(path)
var open_fn: Callable = func() -> void:
open_file_dialog.current_path = last_opened_directory
if last_file_selected_fn != no_op:
open_file_dialog.file_selected.disconnect(last_file_selected_fn)
last_file_selected_fn = func(path: String) -> void:
line_edit.text = path
line_edit.caret_column = path.length()
last_opened_directory = path.get_base_dir() + "/"
var image: Image = Image.new()
var code: int = image.load(path)
if code != OK:
_show_error("Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
_show_success("Loaded texture '" + path + "'")
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
load_image_fn.call(path)
open_file_dialog.file_selected.connect(last_file_selected_fn)
open_file_dialog.popup_centered_ratio()
var line_edit_submit_fn: Callable = func(path: String) -> void:
line_edit.text = path
load_image_fn.call(path)
var clear_fn: Callable = func() -> void:
line_edit.text = ""
texture_rect.texture = null
images[p_image_index] = null
# allow user to edit textbox and press enter because Godot's file picker doesn't work 100% of the time
var line_edit_submit_fn: Callable = func(path: String) -> void:
var image: Image = Image.new()
var code: int = image.load(path)
if code != OK:
_show_error("Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
_set_wh_labels(p_image_index, -1, -1)
line_edit.text_submitted.connect(line_edit_submit_fn)
file_pick_button.pressed.connect(open_fn)
texture_button.pressed.connect(open_fn)
clear_button.pressed.connect(clear_fn)
_set_button_icon(file_pick_button, "Folder")
_set_button_icon(clear_button, "Remove")
texture_button.dropped.connect(godot_drop_fn)
window.files_dropped.connect(os_drop_fn)
if p_image_index == IMAGE_HEIGHT:
var lumin_fn: Callable = func() -> void:
if !images[IMAGE_ALBEDO]:
_show_message(ERROR, "Albedo Image Required for Operation")
else:
line_edit.text = "Generated Height"
var height_texture: Image = Terrain3DUtil.luminance_to_height(images[IMAGE_ALBEDO])
if height_texture.is_empty():
_show_message(ERROR, "Height Texture Generation error")
# blur the image by resizing down and back..
var w: int = height_texture.get_width()
var h: int = height_texture.get_height()
height_texture.resize(w / 4, h / 4)
height_texture.resize(w, h, Image.INTERPOLATE_CUBIC)
# "Load" the height texture
images[IMAGE_HEIGHT] = height_texture
texture_rect.texture = ImageTexture.create_from_image(images[IMAGE_HEIGHT])
_set_wh_labels(IMAGE_HEIGHT, height_texture.get_width(), height_texture.get_height())
set_channel_fn.call(Image.USED_CHANNELS_R)
_show_message(INFO, "Height Texture generated sucsessfully")
lumin_height_button.pressed.connect(lumin_fn)
plugin.ui.set_button_editor_icon(file_pick_button, "Folder")
plugin.ui.set_button_editor_icon(clear_button, "Remove")
func _set_button_icon(p_button: Button, p_icon_name: String) -> void:
var editor_base: Control = editor_interface.get_base_control()
var icon: Texture2D = editor_base.get_theme_icon(p_icon_name, "EditorIcons")
p_button.icon = icon
func _set_wh_labels(p_image_index: int, width: int, height: int) -> void:
var w: String = ""
var h: String = ""
if width > 0 and height > 0:
w = "w: " + str(width)
h = "h: " + str(height)
match p_image_index:
0:
window.find_child("AlbedoW").text = w
window.find_child("AlbedoH").text = h
1:
window.find_child("HeightW").text = w
window.find_child("HeightH").text = h
2:
window.find_child("NormalW").text = w
window.find_child("NormalH").text = h
3:
window.find_child("RoughnessW").text = w
window.find_child("RoughnessH").text = h
func _show_error(p_text: String) -> void:
push_error("Terrain3DChannelPacker: " + p_text)
func _show_message(p_level: int, p_text: String) -> void:
status_label.text = p_text
status_label.add_theme_color_override("font_color", Color(0.9, 0, 0))
func _show_success(p_text: String) -> void:
print("Terrain3DChannelPacker: " + p_text)
status_label.text = p_text
status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14))
match p_level:
INFO:
print("Terrain3DChannelPacker: " + p_text)
status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14))
WARN:
push_warning("Terrain3DChannelPacker: " + p_text)
status_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0))
ERROR,_:
push_error("Terrain3DChannelPacker: " + p_text)
status_label.add_theme_color_override("font_color", Color(0.9, 0, 0))
func _create_import_file(png_path: String) -> void:
var dst_import_path: String = png_path + ".import"
var file: FileAccess = FileAccess.open(TEMPLATE_PATH, FileAccess.READ)
var template_content: String = file.get_as_text()
file.close()
var import_content: String = template_content.replace("$SOURCE_FILE", png_path)
template_content = template_content.replace(
"$SOURCE_FILE", png_path).replace(
"$HIGH_QUALITY", str(high_quality_checkbox.button_pressed)).replace(
"$GENERATE_MIPMAPS", str(generate_mipmaps_checkbox.button_pressed)
)
var import_content: String = template_content
file = FileAccess.open(dst_import_path, FileAccess.WRITE)
file.store_string(import_content)
file.close()
@ -159,11 +326,10 @@ func _create_import_file(png_path: String) -> void:
func _on_pack_button_pressed() -> void:
packing_albedo = images[IMAGE_ALBEDO] != null and images[IMAGE_HEIGHT] != null
var packing_normal_roughness: bool = images[IMAGE_NORMAL] != null and images[IMAGE_ROUGHNESS] != null
if not packing_albedo and not packing_normal_roughness:
_show_error("Please select an albedo and height texture or a normal and roughness texture.")
_show_message(WARN, "Please select an albedo and height texture or a normal and roughness texture")
return
if packing_albedo:
save_file_dialog.current_path = last_saved_directory + "packed_albedo_height"
save_file_dialog.title = "Save Packed Albedo/Height Texture"
@ -179,34 +345,119 @@ func _on_pack_button_pressed() -> void:
func _on_save_file_selected(p_dst_path) -> void:
last_saved_directory = p_dst_path.get_base_dir() + "/"
var error: int
if packing_albedo:
_pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false)
error = _pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false,
invert_height_checkbox.button_pressed, false, height_channel_selected)
else:
_pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path, invert_green_checkbox.button_pressed)
error = _pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path,
invert_green_checkbox.button_pressed, invert_smooth_checkbox.button_pressed,
align_normals_checkbox.button_pressed, roughness_channel_selected)
if error == OK:
EditorInterface.get_resource_filesystem().scan()
if window.visible:
window.hide()
await EditorInterface.get_resource_filesystem().resources_reimported
# wait 1 extra frame, to ensure the UI is responsive.
await RenderingServer.frame_post_draw
window.show()
if queue_pack_normal_roughness:
queue_pack_normal_roughness = false
packing_albedo = false
save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness"
save_file_dialog.title = "Save Packed Normal/Roughness Texture"
save_file_dialog.call_deferred("popup_centered_ratio")
save_file_dialog.call_deferred("move_to_foreground")
func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool) -> void:
func _alignment_basis(normal: Vector3) -> Basis:
var up: Vector3 = Vector3(0, 0, 1)
var v: Vector3 = normal.cross(up)
var c: float = normal.dot(up)
var k: float = 1.0 / (1.0 + c)
var vxy: float = v.x * v.y * k
var vxz: float = v.x * v.z * k
var vyz: float = v.y * v.z * k
return Basis(Vector3(v.x * v.x * k + c, vxy - v.z, vxz + v.y),
Vector3(vxy + v.z, v.y * v.y * k + c, vyz - v.x),
Vector3(vxz - v.y, vyz + v.x, v.z * v.z * k + c)
)
func _set_normal_vector(source: Image, quiet: bool = false) -> void:
# Calculate texture normal sum direction
var normal: Image = source
var sum: Color = Color(0.0, 0.0, 0.0, 0.0)
for x in normal.get_height():
for y in normal.get_width():
sum += normal.get_pixel(x, y)
var div: float = normal.get_height() * normal.get_width()
sum /= Color(div, div, div)
sum *= 2.0
sum -= Color(1.0, 1.0, 1.0)
normal_vector = Vector3(sum.r, sum.g, sum.b).normalized()
if normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999 && !quiet:
_show_message(WARN, "Normal Texture Not Orthoganol to UV plane.\nFor Compatability with Detiling and Rotation, Select Orthoganolize Normals")
func _align_normals(source: Image, iteration: int = 0) -> void:
# generate matrix to re-align the normalmap
var mat3: Basis = _alignment_basis(normal_vector)
# re-align the normal map pixels
for x in source.get_height():
for y in source.get_width():
var old_pixel: Color = source.get_pixel(x, y)
var vector_pixel: Vector3 = Vector3(old_pixel.r, old_pixel.g, old_pixel.b)
vector_pixel *= 2.0
vector_pixel -= Vector3.ONE
vector_pixel = vector_pixel.normalized()
vector_pixel = vector_pixel * mat3
vector_pixel += Vector3.ONE
vector_pixel *= 0.5
var new_pixel: Color = Color(vector_pixel.x, vector_pixel.y, vector_pixel.z, old_pixel.a)
source.set_pixel(x, y, new_pixel)
_set_normal_vector(source, true)
if normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999 && iteration < 3:
++iteration
_align_normals(source, iteration)
func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool,
p_invert_smooth: bool, p_align_normals : bool, p_alpha_channel: int) -> Error:
if p_rgb_image and p_a_image:
if p_rgb_image.get_size() != p_a_image.get_size():
_show_error("Textures must be the same size.")
return
var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image, p_invert_green)
if p_rgb_image.get_size() != p_a_image.get_size() and !resize_toggle_checkbox.button_pressed:
_show_message(ERROR, "Textures must be the same size.\nEnable resize to override image dimensions")
return FAILED
if resize_toggle_checkbox.button_pressed:
var size: int = max(128, resize_option_box.value)
p_rgb_image.resize(size, size, Image.INTERPOLATE_CUBIC)
p_a_image.resize(size, size, Image.INTERPOLATE_CUBIC)
if p_align_normals and normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999:
_align_normals(p_rgb_image)
elif p_align_normals:
_show_message(INFO, "Alignment OK, skipping Normal Orthogonalization")
var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image,
p_invert_green, p_invert_smooth, p_alpha_channel)
if not output_image:
_show_error("Failed to pack textures.")
return
_show_message(ERROR, "Failed to pack textures")
return FAILED
if output_image.detect_used_channels() != 5:
_show_message(ERROR, "Packing Error, Alpha Channel empty")
return FAILED
output_image.save_png(p_dst_path)
editor_interface.get_resource_filesystem().scan_sources()
_create_import_file(p_dst_path)
_show_success("Packed to " + p_dst_path + ".")
_show_message(INFO, "Packed to " + p_dst_path + ".")
return OK
else:
_show_error("Failed to load one or more textures.")
_show_message(ERROR, "Failed to load one or more textures")
return FAILED

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=5 format=3 uid="uid://nud6dwjcnj5v"]
[gd_scene load_steps=7 format=3 uid="uid://nud6dwjcnj5v"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ysabf"]
bg_color = Color(0.211765, 0.239216, 0.290196, 1)
@ -30,21 +30,21 @@ corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"]
[node name="AcceptDialog" type="AcceptDialog"]
[sub_resource type="ButtonGroup" id="ButtonGroup_wnxik"]
[sub_resource type="ButtonGroup" id="ButtonGroup_bs6ki"]
[node name="Window" type="Window"]
title = "Terrain3D Channel Packer"
initial_position = 1
size = Vector2i(660, 900)
visible = true
ok_button_text = "Close"
size = Vector2i(680, 835)
unresizable = true
always_on_top = true
[node name="Panel" type="Panel" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf")
@ -54,10 +54,10 @@ layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 4.0
offset_top = 4.0
offset_right = -1.0
offset_bottom = -53.0
offset_left = 5.0
offset_top = 5.0
offset_right = -5.0
offset_bottom = 5.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 5
@ -67,12 +67,12 @@ theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
layout_mode = 2
size_flags_vertical = 0
theme_override_constants/separation = 10
[node name="AlbedoHeightPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 250)
custom_minimum_size = Vector2(0, 290)
layout_mode = 2
mouse_filter = 1
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel"]
@ -147,6 +147,27 @@ grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="AlbedoWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="AlbedoW" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="AlbedoH" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="LuminanceAsHeightButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/HBoxContainer2"]
layout_mode = 2
text = " Generate Height from Luminance"
icon_alignment = 2
[node name="HeightVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
@ -204,8 +225,63 @@ grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="HeightWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightW" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HeightH" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="ConvertDepthToHeight" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Depth to Height"
icon_alignment = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightChannelLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="HeightChannelR" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_wnxik")
text = "R"
[node name="HeightChannelB" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "G"
[node name="HeightChannelG" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "B"
[node name="HeightChannelA" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "A"
[node name="NormalRoughnessPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 280)
custom_minimum_size = Vector2(0, 290)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
@ -281,9 +357,33 @@ grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
[node name="NormalWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
text = "Convert DirectX to OpenGL"
alignment = 1
[node name="NormalW" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="NormalH" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer"]
layout_mode = 2
text = " Convert DirectX to OpenGL"
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="AlignNormalsCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer2"]
layout_mode = 2
text = " Orthoganolise Normals"
[node name="RoughnessVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
@ -342,18 +442,112 @@ grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="NormalRoughnessHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
[node name="RoughnessWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessW" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="RoughnessH" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="InvertSmoothCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Smoothness to Roughness"
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessChannelLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="RoughnessChannelR" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "R"
[node name="RoughnessChannelG" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "G"
[node name="RoughnessChannelB" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "B"
[node name="RoughnessChannelA" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "A"
[node name="GeneralOptionsLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "General Options"
horizontal_alignment = 1
vertical_alignment = 1
[node name="GeneralOptionsHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 35)
layout_mode = 2
alignment = 1
[node name="ResizeToggle" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = " Resize Packed Image"
[node name="ResizeOptionButton" type="SpinBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
visible = false
layout_mode = 2
tooltip_text = "A value of 0 disables resizing."
min_value = 128.0
max_value = 4096.0
step = 128.0
value = 1024.0
[node name="VSeparator" type="VSeparator" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
[node name="GenerateMipmapsCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
button_pressed = true
text = "Generate Mipmaps"
[node name="HighQualityCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = "Import High Quality"
[node name="PackButton" type="Button" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Pack textures as..."
[node name="StatusLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 10)
custom_minimum_size = Vector2(0, 60)
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "Use this to create a packed Albedo + Height texture and/or a packed Normal + Roughness texture.
horizontal_alignment = 1
autowrap_mode = 3
You can then use these textures with Terrain3D."
autowrap_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="CloseButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Close"
[connection signal="toggled" from="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeToggle" to="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeOptionButton" method="set_visible"]

View File

@ -0,0 +1,15 @@
@tool
extends Button
signal dropped
func _can_drop_data(p_position, p_data) -> bool:
if typeof(p_data) == TYPE_DICTIONARY:
if p_data.files.size() == 1:
match p_data.files[0].get_extension():
"png", "bmp", "exr", "hdr", "jpg", "jpeg", "tga", "svg", "webp", "ktx", "dds":
return true
return false
func _drop_data(p_position, p_data) -> void:
dropped.emit(p_data.files[0])

View File

@ -14,12 +14,12 @@ source_file="$SOURCE_FILE"
[params]
compress/mode=2
compress/high_quality=false
compress/high_quality=$HIGH_QUALITY
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=2
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=$GENERATE_MIPMAPS
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
@ -29,4 +29,4 @@ process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
detect_3d/compress_to=1

View File

@ -0,0 +1,116 @@
extends Node
const DIRECTORY_SETUP: String = "res://addons/terrain_3d/src/directory_setup.tscn"
var plugin: EditorPlugin
var dialog: ConfirmationDialog
var select_dir_btn: Button
var selected_dir_le: LineEdit
var select_upg_btn: Button
var upgrade_file_le: LineEdit
var editor_file_dialog: EditorFileDialog
func _init() -> void:
editor_file_dialog = EditorFileDialog.new()
editor_file_dialog.set_filters(PackedStringArray(["*.res"]))
editor_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_SAVE_FILE)
editor_file_dialog.access = EditorFileDialog.ACCESS_RESOURCES
editor_file_dialog.ok_button_text = "Open"
editor_file_dialog.title = "Open a folder or file"
editor_file_dialog.file_selected.connect(_on_file_selected)
editor_file_dialog.dir_selected.connect(_on_dir_selected)
editor_file_dialog.size = Vector2i(850, 550)
editor_file_dialog.transient = false
editor_file_dialog.exclusive = false
editor_file_dialog.popup_window = true
add_child(editor_file_dialog)
func directory_setup_popup() -> void:
dialog = load(DIRECTORY_SETUP).instantiate()
dialog.hide()
# Nodes
select_dir_btn = dialog.get_node("Margin/VBox/DirHBox/SelectDir")
selected_dir_le = dialog.get_node("Margin/VBox/DirHBox/LineEdit")
select_upg_btn = dialog.get_node("Margin/VBox/UpgradeHBox/SelectResFile")
upgrade_file_le = dialog.get_node("Margin/VBox/UpgradeHBox/LineEdit")
upgrade_file_le.text = ""
if plugin.terrain.data_directory:
selected_dir_le.text = plugin.terrain.data_directory
if plugin.terrain.storage:
upgrade_file_le.text = plugin.terrain.storage.get_path()
# Icons
plugin.ui.set_button_editor_icon(select_upg_btn, "Folder")
plugin.ui.set_button_editor_icon(select_dir_btn, "Folder")
#Signals
select_upg_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_FILE))
select_dir_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_DIR))
dialog.confirmed.connect(_on_close_requested)
dialog.canceled.connect(_on_close_requested)
dialog.get_ok_button().pressed.connect(_on_ok_pressed)
# Popup
EditorInterface.popup_dialog_centered(dialog)
func _on_close_requested() -> void:
dialog.queue_free()
dialog = null
func _on_select_file_pressed(file_mode: EditorFileDialog.FileMode) -> void:
editor_file_dialog.file_mode = file_mode
editor_file_dialog.popup_centered()
func _on_dir_selected(path: String) -> void:
selected_dir_le.text = path
func _on_file_selected(path: String) -> void:
upgrade_file_le.text = path
func _on_ok_pressed() -> void:
if not plugin.terrain:
push_error("Not connected terrain. Click the Terrain3D node first")
return
if selected_dir_le.text.is_empty():
push_error("No data directory specified")
return
if not DirAccess.dir_exists_absolute(selected_dir_le.text):
push_error("Directory doesn't exist: ", selected_dir_le.text)
return
# Check if directory empty of terrain files
var data_found: bool = false
var files: Array = DirAccess.get_files_at(selected_dir_le.text)
for file in files:
if file.begins_with("terrain3d") || file.ends_with(".res"):
data_found = true
break
print("Setting terrain directory: ", selected_dir_le.text)
plugin.terrain.data_directory = selected_dir_le.text
if not upgrade_file_le.text.is_empty():
if data_found:
push_warning("Target directory already has terrain data. Specify an empty directory to upgrade")
return
if not FileAccess.file_exists(upgrade_file_le.text):
push_error("File doesn't exist: ", upgrade_file_le.text)
return
if not plugin.terrain.storage or \
( plugin.terrain.storage and plugin.terrain.storage.get_path() != upgrade_file_le.text):
print("Loading storage file: ", upgrade_file_le.text)
plugin.terrain.set_storage(load(upgrade_file_le.text))
if plugin.terrain.storage:
print("Begining upgrade of: ", upgrade_file_le.text)
plugin.terrain.split_storage()

View File

@ -0,0 +1,66 @@
[gd_scene format=3 uid="uid://by3kr2nqbqr67"]
[node name="DirectorySetup" type="ConfirmationDialog"]
title = "Terrain3D Data Directory Setup"
position = Vector2i(0, 36)
size = Vector2i(750, 574)
visible = true
[node name="Margin" type="MarginContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 742.0
offset_bottom = 525.0
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBox" type="VBoxContainer" parent="Margin"]
layout_mode = 2
[node name="Instructions" type="Label" parent="Margin/VBox"]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
text = "Terrain3D now stores data in a directory instead of a single file. Each region is stored in a separate file named `terrain3d[-_]##[-_]##.res`. For instance, the region at location (-1, 1) would be named `terrain3d-01_01.res`. Enable Terrain3D / Debug / Show Region Labels for a visual display."
autowrap_mode = 3
[node name="DirectoryLabel" type="Label" parent="Margin/VBox"]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
text = "
Specify the directory to store your data. Any existing region files will be loaded."
autowrap_mode = 3
[node name="DirHBox" type="HBoxContainer" parent="Margin/VBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Margin/VBox/DirHBox"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Data directory"
[node name="SelectDir" type="Button" parent="Margin/VBox/DirHBox"]
layout_mode = 2
[node name="UpgradeLabel" type="Label" parent="Margin/VBox"]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
text = "
If you wish to upgrade a storage file from v0.8.4 - v0.9.2, specify it below. Data will be stored in the directory above upon save. The original file will not be touched."
autowrap_mode = 3
[node name="UpgradeHBox" type="HBoxContainer" parent="Margin/VBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Margin/VBox/UpgradeHBox"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Storage .res to upgrade"
[node name="SelectResFile" type="Button" parent="Margin/VBox/UpgradeHBox"]
layout_mode = 2
[node name="Spacer" type="Control" parent="Margin/VBox"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2

View File

@ -0,0 +1,124 @@
@tool
class_name DoubleSlider
extends Control
signal value_changed(Vector2)
var label: Label
var suffix: String
var grabbed_handle: int = 0 # -1 left, 0 none, 1 right
var min_value: float = 0.0
var max_value: float = 100.0
var step: float = 1.0
var range := Vector2(0, 100)
func _ready() -> void:
update_label()
func set_min(p_value: float) -> void:
min_value = p_value
if range.x <= min_value:
range.x = min_value
set_value(range)
update_label()
func get_min() -> float:
return min_value
func set_max(p_value: float) -> void:
max_value = p_value
if range.y == 0 or range.y >= max_value:
range.y = max_value
set_value(range)
update_label()
func get_max() -> float:
return max_value
func set_step(p_step: float) -> void:
step = p_step
func get_step() -> float:
return step
func set_value(p_range: Vector2) -> void:
range.x = clamp(p_range.x, min_value, max_value)
range.y = clamp(p_range.y, min_value, max_value)
if range.y < range.x:
var tmp: float = range.x
range.x = range.y
range.y = tmp
update_label()
emit_signal("value_changed", Vector2(range.x, range.y))
queue_redraw()
func get_value() -> Vector2:
return range
func update_label() -> void:
if label:
label.set_text(str(range.x) + suffix + "/" + str(range.y) + suffix)
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
if p_event.is_pressed():
var mid_point = (range.x + range.y) / 2.0
var xpos: float = p_event.get_position().x * 2.0
if xpos >= mid_point:
grabbed_handle = 1
else:
grabbed_handle = -1
set_slider(p_event.get_position().x)
else:
grabbed_handle = 0
if p_event is InputEventMouseMotion:
if grabbed_handle != 0:
set_slider(p_event.get_position().x)
func set_slider(p_xpos: float) -> void:
if grabbed_handle == 0:
return
var xpos_step: float = clamp(snappedf((p_xpos / size.x) * max_value, step), min_value, max_value)
if(grabbed_handle < 0):
range.x = xpos_step
else:
range.y = xpos_step
set_value(range)
func _notification(p_what: int) -> void:
if p_what == NOTIFICATION_DRAW:
# Draw background bar
var bg: StyleBox = get_theme_stylebox("slider", "HSlider")
var bg_height: float = bg.get_minimum_size().y
var mid_y: float = (size.y - bg_height) / 2.0
draw_style_box(bg, Rect2(Vector2(0, mid_y), Vector2(size.x, bg_height)))
# Draw foreground bar
var handle: Texture2D = get_theme_icon("grabber", "HSlider")
var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
var h: float = size.y / 2 - handle.get_size().y / 2
var startx: float = (range.x / max_value) * size.x
var endx: float = (range.y / max_value) * size.x #- startx
draw_style_box(area, Rect2(Vector2(startx, mid_y), Vector2(endx - startx, bg_height)))
# Draw handles, slightly in so they don't get on the outside edges
var handle_pos: Vector2
handle_pos.x = clamp(startx - handle.get_size().x/2, -5, size.x)
handle_pos.y = clamp(endx - handle.get_size().x/2, 0, size.x - 10)
draw_texture(handle, Vector2(handle_pos.x, -mid_y))
draw_texture(handle, Vector2(handle_pos.y, -mid_y))

View File

@ -5,7 +5,6 @@ signal pressed
signal value_changed
const ICON_PICKER: String = "res://addons/terrain_3d/icons/picker.svg"
const ICON_PICKER_CHECKED: String = "res://addons/terrain_3d/icons/picker_checked.svg"
const MAX_POINTS: int = 2
@ -16,8 +15,8 @@ var points: PackedVector3Array
var picking_index: int = -1
func _init() -> void:
icon_picker = load(ICON_PICKER)
func _enter_tree() -> void:
icon_picker = get_theme_icon("ColorPick", "EditorIcons")
icon_picker_checked = load(ICON_PICKER_CHECKED)
points.resize(MAX_POINTS)

View File

@ -32,7 +32,7 @@ func _redraw() -> void:
if show_rect:
var modulate: Color = main_color if !use_secondary_color else secondary_color
if abs(region_position.x) > 8 or abs(region_position.y) > 8:
if abs(region_position.x) > Terrain3DData.REGION_MAP_SIZE*.5 or abs(region_position.y) > Terrain3DData.REGION_MAP_SIZE*.5:
modulate = Color.GRAY
draw_rect(Vector2(region_size,region_size)*.5 + rect_position, region_size, selection_material, modulate)
@ -44,7 +44,7 @@ func _redraw() -> void:
draw_rect(Vector2(region_size,region_size)*.5 + grid_tile_position, region_size, material, grid_color)
draw_rect(Vector2.ZERO, region_size * 16.0, material, border_color)
draw_rect(Vector2.ZERO, region_size * Terrain3DData.REGION_MAP_SIZE, material, border_color)
func draw_rect(p_pos: Vector2, p_size: float, p_material: StandardMaterial3D, p_modulate: Color) -> void:

View File

@ -1,68 +1,72 @@
extends HBoxContainer
const Baker: Script = preload("res://addons/terrain_3d/src/baker.gd")
const DirectoryWizard: Script = preload("res://addons/terrain_3d/src/directory_setup.gd")
const Packer: Script = preload("res://addons/terrain_3d/src/channel_packer.gd")
const Baker: Script = preload("res://addons/terrain_3d/src/baker.gd")
var plugin: EditorPlugin
var menu_button: MenuButton = MenuButton.new()
var baker: Baker = Baker.new()
var directory_setup: DirectoryWizard = DirectoryWizard.new()
var packer: Packer = Packer.new()
var baker: Baker = Baker.new()
# These are IDs and order must be consistent with add_item and set_disabled IDs
enum {
MENU_DIRECTORY_SETUP,
MENU_PACK_TEXTURES,
MENU_SEPARATOR,
MENU_BAKE_ARRAY_MESH,
MENU_BAKE_OCCLUDER,
MENU_BAKE_NAV_MESH,
MENU_SEPARATOR,
MENU_SEPARATOR2,
MENU_SET_UP_NAVIGATION,
MENU_PACK_TEXTURES,
MENU_BAKE_NAV_MESH,
}
func _enter_tree() -> void:
baker.plugin = plugin
directory_setup.plugin = plugin
packer.plugin = plugin
baker.plugin = plugin
add_child(directory_setup)
add_child(baker)
menu_button.text = "Terrain3D Tools"
menu_button.get_popup().add_item("Bake ArrayMesh", MENU_BAKE_ARRAY_MESH)
menu_button.get_popup().add_item("Bake Occluder3D", MENU_BAKE_OCCLUDER)
menu_button.get_popup().add_item("Bake NavMesh", MENU_BAKE_NAV_MESH)
menu_button.get_popup().add_item("Directory Setup...", MENU_DIRECTORY_SETUP)
menu_button.get_popup().add_item("Pack Textures...", MENU_PACK_TEXTURES)
menu_button.get_popup().add_separator("", MENU_SEPARATOR)
menu_button.get_popup().add_item("Set up Navigation", MENU_SET_UP_NAVIGATION)
menu_button.get_popup().add_separator("", MENU_SEPARATOR)
menu_button.get_popup().add_item("Pack Textures", MENU_PACK_TEXTURES)
menu_button.get_popup().add_item("Bake ArrayMesh...", MENU_BAKE_ARRAY_MESH)
menu_button.get_popup().add_item("Bake Occluder3D...", MENU_BAKE_OCCLUDER)
menu_button.get_popup().add_separator("", MENU_SEPARATOR2)
menu_button.get_popup().add_item("Set up Navigation...", MENU_SET_UP_NAVIGATION)
menu_button.get_popup().add_item("Bake NavMesh...", MENU_BAKE_NAV_MESH)
menu_button.get_popup().id_pressed.connect(_on_menu_pressed)
menu_button.about_to_popup.connect(_on_menu_about_to_popup)
add_child(menu_button)
func _exit_tree() -> void:
# TODO: If packer isn't freed, Godot complains about ObjectDB instances leaked and
# resources still in use at exit. Figure out why.
packer.free()
func _on_menu_pressed(p_id: int) -> void:
match p_id:
MENU_DIRECTORY_SETUP:
directory_setup.directory_setup_popup()
MENU_PACK_TEXTURES:
packer.pack_textures_popup()
MENU_BAKE_ARRAY_MESH:
baker.bake_mesh_popup()
MENU_BAKE_OCCLUDER:
baker.bake_occluder_popup()
MENU_BAKE_NAV_MESH:
baker.bake_nav_mesh()
MENU_SET_UP_NAVIGATION:
baker.set_up_navigation_popup()
MENU_PACK_TEXTURES:
packer.pack_textures_popup()
MENU_BAKE_NAV_MESH:
baker.bake_nav_mesh()
func _on_menu_about_to_popup() -> void:
menu_button.get_popup().set_item_disabled(MENU_DIRECTORY_SETUP, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_PACK_TEXTURES, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_ARRAY_MESH, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_OCCLUDER, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_PACK_TEXTURES, not plugin.terrain)
if plugin.terrain:
var nav_regions: Array[NavigationRegion3D] = baker.find_terrain_nav_regions(plugin.terrain)

View File

@ -17,13 +17,14 @@ enum SettingType {
PICKER,
MULTI_PICKER,
SLIDER,
LABEL,
TYPE_MAX,
}
const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd")
const DEFAULT_BRUSH: String = "circle0.exr"
const BRUSH_PATH: String = "res://addons/terrain_3d/brushes"
const PICKER_ICON: String = "res://addons/terrain_3d/icons/picker.svg"
const ES_TOOL_SETTINGS: String = "terrain3d/tool_settings/"
# Add settings flags
const NONE: int = 0x0
@ -31,13 +32,14 @@ const ALLOW_LARGER: int = 0x1
const ALLOW_SMALLER: int = 0x2
const ALLOW_OUT_OF_BOUNDS: int = 0x3 # LARGER|SMALLER
const NO_LABEL: int = 0x4
const ADD_SEPARATOR: int = 0x8
const ADD_SPACER: int = 0x10
const ADD_SEPARATOR: int = 0x8 # Add a vertical line before this entry
const ADD_SPACER: int = 0x10 # Add a space before this entry
const NO_SAVE: int = 0x20 # Don't save this in EditorSettings
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var brush_preview_material: ShaderMaterial
var select_brush_button: Button
var main_list: HBoxContainer
var main_list: HFlowContainer
var advanced_list: VBoxContainer
var height_list: VBoxContainer
var scale_list: VBoxContainer
@ -47,21 +49,26 @@ var settings: Dictionary = {}
func _ready() -> void:
main_list = HBoxContainer.new()
# Remove old editor settings
for setting in ["lift_floor", "flatten_peaks", "lift_flatten", "automatic_regions"]:
plugin.erase_setting(ES_TOOL_SETTINGS + setting)
# Setup buttons
main_list = HFlowContainer.new()
add_child(main_list, true)
## Common Settings
add_brushes(main_list)
add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":50, "unit":"m",
"range":Vector3(2, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER })
add_setting({ "name":"instructions", "label":"Click the terrain to add a region. CTRL+Click to remove. Or select another tool on the left.",
"type":SettingType.LABEL, "list":main_list, "flags":NO_LABEL|NO_SAVE })
add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":20, "unit":"m",
"range":Vector3(0.1, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER })
add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":10,
add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":33,
"unit":"%", "range":Vector3(1, 100, 1), "flags":ALLOW_LARGER })
add_setting({ "name":"enable", "type":SettingType.CHECKBOX, "list":main_list, "default":true })
add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":50,
add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":20,
"unit":"m", "range":Vector3(-500, 500, 0.1), "flags":ALLOW_OUT_OF_BOUNDS })
add_setting({ "name":"height_picker", "type":SettingType.PICKER, "list":main_list,
"default":Terrain3DEditor.HEIGHT, "flags":NO_LABEL })
@ -71,14 +78,21 @@ func _ready() -> void:
add_setting({ "name":"color_picker", "type":SettingType.PICKER, "list":main_list,
"default":Terrain3DEditor.COLOR, "flags":NO_LABEL })
add_setting({ "name":"roughness", "type":SettingType.SLIDER, "list":main_list, "default":0,
add_setting({ "name":"roughness", "type":SettingType.SLIDER, "list":main_list, "default":-65,
"unit":"%", "range":Vector3(-100, 100, 1), "flags":ADD_SEPARATOR })
add_setting({ "name":"roughness_picker", "type":SettingType.PICKER, "list":main_list,
"default":Terrain3DEditor.ROUGHNESS, "flags":NO_LABEL })
add_setting({ "name":"enable_texture", "label":"Texture", "type":SettingType.CHECKBOX,
"list":main_list, "default":true })
"list":main_list, "default":true, "flags":ADD_SEPARATOR })
add_setting({ "name":"margin", "type":SettingType.SLIDER, "list":main_list, "default":0,
"unit":"", "range":Vector3(-50, 50, 1), "flags":ALLOW_OUT_OF_BOUNDS })
# Slope painting filter
add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list, "default":Vector2(0, 90),
"unit":"°", "range":Vector3(0, 90, 1), "flags":ADD_SEPARATOR })
add_setting({ "name":"enable_angle", "label":"Angle", "type":SettingType.CHECKBOX,
"list":main_list, "default":true, "flags":ADD_SEPARATOR })
add_setting({ "name":"angle", "type":SettingType.SLIDER, "list":main_list, "default":0,
@ -88,18 +102,16 @@ func _ready() -> void:
add_setting({ "name":"dynamic_angle", "label":"Dynamic", "type":SettingType.CHECKBOX,
"list":main_list, "default":false, "flags":ADD_SPACER })
add_setting({ "name":"enable_scale", "label":"Scale ±", "type":SettingType.CHECKBOX,
add_setting({ "name":"enable_scale", "label":"Scale", "type":SettingType.CHECKBOX,
"list":main_list, "default":true, "flags":ADD_SEPARATOR })
add_setting({ "name":"scale", "label":"±", "type":SettingType.SLIDER, "list":main_list, "default":0,
"unit":"%", "range":Vector3(-60, 80, 20), "flags":NO_LABEL })
add_setting({ "name":"scale_picker", "type":SettingType.PICKER, "list":main_list,
"default":Terrain3DEditor.SCALE, "flags":NO_LABEL })
## Slope
add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list,
"default":0, "unit":"°", "range":Vector3(0, 180, 1) })
## Slope sculpting brush
add_setting({ "name":"gradient_points", "type":SettingType.MULTI_PICKER, "label":"Points",
"list":main_list, "default":Terrain3DEditor.HEIGHT, "flags":ADD_SEPARATOR })
"list":main_list, "default":Terrain3DEditor.SCULPT, "flags":ADD_SEPARATOR })
add_setting({ "name":"drawable", "type":SettingType.CHECKBOX, "list":main_list, "default":false,
"flags":ADD_SEPARATOR })
settings["drawable"].toggled.connect(_on_drawable_toggled)
@ -123,9 +135,9 @@ func _ready() -> void:
"default":0, "unit":"°", "range":Vector3(0, 360, 1) })
add_setting({ "name":"random_spin", "type":SettingType.SLIDER, "list":rotation_list, "default":360,
"unit":"°", "range":Vector3(0, 360, 1) })
add_setting({ "name":"fixed_angle", "label":"Fixed Angle (From Y)", "type":SettingType.SLIDER, "list":rotation_list,
add_setting({ "name":"fixed_tilt", "label":"Fixed Tilt", "type":SettingType.SLIDER, "list":rotation_list,
"default":0, "unit":"°", "range":Vector3(-85, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS })
add_setting({ "name":"random_angle", "label":"Random Angle ±", "type":SettingType.SLIDER, "list":rotation_list,
add_setting({ "name":"random_tilt", "label":"Random Tilt ±", "type":SettingType.SLIDER, "list":rotation_list,
"default":10, "unit":"°", "range":Vector3(0, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS })
add_setting({ "name":"align_to_normal", "type":SettingType.CHECKBOX, "list":rotation_list, "default":false })
@ -139,14 +151,17 @@ func _ready() -> void:
#add_setting({ "name":"blend_mode", "type":SettingType.OPTION, "list":color_list, "default":0,
#"range":Vector3(0, 3, 1) })
if DisplayServer.is_touchscreen_available():
add_setting({ "name":"remove", "label":"Invert", "type":SettingType.CHECKBOX, "list":main_list, "default":false, "flags":ADD_SEPARATOR })
var spacer: Control = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_list.add_child(spacer, true)
## Advanced Settings Menu
advanced_list = create_submenu(main_list, "Advanced", Layout.VERTICAL)
add_setting({ "name":"automatic_regions", "type":SettingType.CHECKBOX, "list":advanced_list,
"default":true })
advanced_list = create_submenu(main_list, "", Layout.VERTICAL, false)
add_setting({ "name":"auto_regions", "label":"Add regions while sculpting", "type":SettingType.CHECKBOX,
"list":advanced_list, "default":true })
add_setting({ "name":"align_to_view", "type":SettingType.CHECKBOX, "list":advanced_list,
"default":true })
add_setting({ "name":"show_cursor_while_painting", "type":SettingType.CHECKBOX, "list":advanced_list,
@ -158,23 +173,44 @@ func _ready() -> void:
"unit":"%", "range":Vector3(0, 100, 1) })
func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout) -> Container:
func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true) -> Container:
var menu_button: Button = Button.new()
menu_button.set_text(p_button_name)
if p_button_name.is_empty():
menu_button.icon = get_theme_icon("GuiTabMenuHl", "EditorIcons")
else:
menu_button.set_text(p_button_name)
menu_button.set_toggle_mode(true)
menu_button.set_v_size_flags(SIZE_SHRINK_CENTER)
menu_button.toggled.connect(_on_show_submenu.bind(menu_button))
var submenu: PopupPanel = PopupPanel.new()
submenu.popup_hide.connect(menu_button.set_pressed_no_signal.bind(false))
submenu.popup_hide.connect(menu_button.set_pressed.bind(false))
var panel_style: StyleBox = get_theme_stylebox("panel", "PopupMenu").duplicate()
panel_style.set_content_margin_all(10)
submenu.set("theme_override_styles/panel", panel_style)
submenu.add_to_group("terrain3d_submenus")
# Pop up menu on hover, hide on exit
menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button))
submenu.mouse_exited.connect(_on_show_submenu.bind(false, menu_button))
if p_hover_pop:
menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button))
submenu.mouse_entered.connect(func(): submenu.set_meta("mouse_entered", true))
submenu.mouse_exited.connect(func():
# On mouse_exit, hide popup unless LineEdit focused
var focused_element: Control = submenu.gui_get_focus_owner()
if not focused_element is LineEdit:
_on_show_submenu(false, menu_button)
submenu.set_meta("mouse_entered", false)
return
focused_element.focus_exited.connect(func():
# Close submenu once lineedit loses focus
if not submenu.get_meta("mouse_entered"):
_on_show_submenu(false, menu_button)
submenu.set_meta("mouse_entered", false)
)
)
var sublist: Container
match(p_layout):
@ -200,20 +236,21 @@ func _on_show_submenu(p_toggled: bool, p_button: Button) -> void:
# Hide menu if mouse is not in button or panel
var button_rect: Rect2 = Rect2(p_button.get_screen_transform().origin, p_button.get_global_rect().size)
var in_button: bool = button_rect.has_point(DisplayServer.mouse_get_position())
var panel: PopupPanel = p_button.get_child(0)
var panel_rect: Rect2 = Rect2(panel.position, panel.size)
var in_panel: bool = panel_rect.has_point(DisplayServer.mouse_get_position())
if not p_toggled and ( in_button or in_panel ):
var popup: PopupPanel = p_button.get_child(0)
var popup_rect: Rect2 = Rect2(popup.position, popup.size)
var in_popup: bool = popup_rect.has_point(DisplayServer.mouse_get_position())
if not p_toggled and ( in_button or in_popup ):
return
# Hide all submenus before possibly enabling the current one
get_tree().call_group("terrain3d_submenus", "set_visible", false)
var popup: PopupPanel = p_button.get_child(0)
var popup_pos: Vector2 = p_button.get_screen_transform().origin
popup.set_visible(p_toggled)
popup_pos.y -= popup.get_size().y
var popup_pos: Vector2 = p_button.get_screen_transform().origin
popup_pos.y -= popup.size.y
if popup.get_child_count()>0 and popup.get_child(0) == advanced_list:
popup_pos.x -= popup.size.x - p_button.size.x
popup.set_position(popup_pos)
func add_brushes(p_parent: Control) -> void:
var brush_list: GridContainer = create_submenu(p_parent, "Brush", Layout.GRID)
@ -231,6 +268,8 @@ func add_brushes(p_parent: Control) -> void:
if !dir.current_is_dir() and file_name.ends_with(".exr"):
var img: Image = Image.load_from_file(BRUSH_PATH + "/" + file_name)
img = Terrain3DUtil.black_to_alpha(img)
if img.get_width() < 1024 and img.get_height() < 1024:
img.resize(1024, 1024, Image.INTERPOLATE_CUBIC)
var tex: ImageTexture = ImageTexture.create_from_image(img)
var btn: Button = Button.new()
@ -270,10 +309,11 @@ func add_brushes(p_parent: Control) -> void:
select_brush_button = brush_list.get_parent().get_parent()
# Optionally erase the main brush button text and replace it with the texture
# select_brush_button.set_button_icon(default_brush_btn.get_button_icon())
# select_brush_button.set_custom_minimum_size(Vector2.ONE * 36)
# select_brush_button.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER)
# select_brush_button.set_expand_icon(true)
select_brush_button.set_text("")
select_brush_button.set_button_icon(default_brush_btn.get_button_icon())
select_brush_button.set_custom_minimum_size(Vector2.ONE * 36)
select_brush_button.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER)
select_brush_button.set_expand_icon(true)
func _on_brush_hover(p_hovering: bool, p_button: Button) -> void:
@ -307,13 +347,12 @@ func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position:
func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void:
assert(p_type == Terrain3DEditor.HEIGHT)
assert(p_type == Terrain3DEditor.SCULPT)
emit_signal("picking", p_type, _on_point_picked.bind(p_name))
func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void:
assert(p_type == Terrain3DEditor.HEIGHT)
assert(p_type == Terrain3DEditor.SCULPT)
var point: Vector3 = p_global_position
point.y = p_color.r
settings[p_name].add_point(point)
@ -342,9 +381,22 @@ func add_setting(p_args: Dictionary) -> void:
var pending_children: Array[Control]
match p_type:
SettingType.LABEL:
var label := Label.new()
label.set_text(p_label)
pending_children.push_back(label)
control = label
SettingType.CHECKBOX:
var checkbox := CheckBox.new()
checkbox.set_pressed_no_signal(p_default)
if !(p_flags & NO_SAVE):
checkbox.set_pressed_no_signal(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default))
checkbox.toggled.connect( (
func(value, path):
plugin.set_setting(path, value)
).bind(ES_TOOL_SETTINGS + p_name) )
else:
checkbox.set_pressed_no_signal(p_default)
checkbox.pressed.connect(_on_setting_changed)
pending_children.push_back(checkbox)
control = checkbox
@ -352,19 +404,24 @@ func add_setting(p_args: Dictionary) -> void:
SettingType.COLOR_SELECT:
var picker := ColorPickerButton.new()
picker.set_custom_minimum_size(Vector2(100, 25))
picker.color = Color.WHITE
picker.edit_alpha = false
picker.get_picker().set_color_mode(ColorPicker.MODE_HSV)
if !(p_flags & NO_SAVE):
picker.set_pick_color(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default))
picker.color_changed.connect( (
func(value, path):
plugin.set_setting(path, value)
).bind(ES_TOOL_SETTINGS + p_name) )
else:
picker.set_pick_color(p_default)
picker.color_changed.connect(_on_setting_changed)
var popup: PopupPanel = picker.get_popup()
popup.mouse_exited.connect(Callable(func(p): p.hide()).bind(popup))
pending_children.push_back(picker)
control = picker
SettingType.PICKER:
var button := Button.new()
button.set_v_size_flags(SIZE_SHRINK_CENTER)
button.icon = load(PICKER_ICON)
button.icon = get_theme_icon("ColorPick", "EditorIcons")
button.tooltip_text = "Pick value from the Terrain"
button.pressed.connect(_on_pick.bind(p_default))
pending_children.push_back(button)
@ -397,10 +454,9 @@ func add_setting(p_args: Dictionary) -> void:
spin_slider.set_max(p_maximum)
spin_slider.set_min(p_minimum)
spin_slider.set_step(p_step)
spin_slider.set_value(p_default)
spin_slider.set_suffix(p_suffix)
spin_slider.set_v_size_flags(SIZE_SHRINK_CENTER)
spin_slider.set_custom_minimum_size(Vector2(75, 0))
spin_slider.set_custom_minimum_size(Vector2(65, 0))
# Create horizontal slider linked to the above box
slider = HSlider.new()
@ -409,27 +465,38 @@ func add_setting(p_args: Dictionary) -> void:
slider.set_allow_greater(true)
if p_flags & ALLOW_SMALLER:
slider.set_allow_lesser(true)
pending_children.push_back(slider)
pending_children.push_back(spin_slider)
control = spin_slider
else: # DOUBLE_SLIDER
var label := Label.new()
label.set_custom_minimum_size(Vector2(75, 0))
label.set_custom_minimum_size(Vector2(60, 0))
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT)
slider = DoubleSlider.new()
slider.label = label
slider.suffix = p_suffix
slider.setting_changed.connect(_on_setting_changed)
slider.value_changed.connect(_on_setting_changed)
pending_children.push_back(slider)
pending_children.push_back(label)
control = slider
slider.set_max(p_maximum)
slider.set_min(p_minimum)
slider.set_max(p_maximum)
slider.set_step(p_step)
slider.set_value(p_default)
slider.set_v_size_flags(SIZE_SHRINK_CENTER)
slider.set_custom_minimum_size(Vector2(60, 10))
slider.set_custom_minimum_size(Vector2(50, 10))
if !(p_flags & NO_SAVE):
slider.set_value(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default))
slider.value_changed.connect( (
func(value, path):
plugin.set_setting(path, value)
).bind(ES_TOOL_SETTINGS + p_name) )
else:
slider.set_value(p_default)
control.name = p_name.to_pascal_case()
settings[p_name] = control
@ -491,8 +558,8 @@ func get_setting(p_setting: String) -> Variant:
var width: float = clamp( (1 + count_digits(value)) * 19., 50, 80) * clamp(EditorInterface.get_editor_scale(), .9, 2)
object.set_custom_minimum_size(Vector2(width, 0))
elif object is DoubleSlider:
value = Vector2(object.get_min_value(), object.get_max_value())
elif object is ButtonGroup:
value = object.get_value()
elif object is ButtonGroup: # "brush"
var img: Image = object.get_pressed_button().get_meta("image")
var tex: Texture2D = object.get_pressed_button().get_button_icon()
value = [ img, tex ]
@ -509,11 +576,10 @@ func get_setting(p_setting: String) -> Variant:
func set_setting(p_setting: String, p_value: Variant) -> void:
var object: Object = settings.get(p_setting)
if object is Range:
if object is DoubleSlider: # Expects p_value is Vector2
object.set_value(p_value)
elif object is Range:
object.set_value(p_value)
elif object is DoubleSlider: # Expects p_value is Vector2
object.set_min_value(p_value.x)
object.set_max_value(p_value.y)
elif object is ButtonGroup: # Expects p_value is Array [ "button name", boolean ]
if p_value is Array and p_value.size() == 2:
for button in object.get_buttons():
@ -523,6 +589,7 @@ func set_setting(p_setting: String, p_value: Variant) -> void:
object.button_pressed = p_value
elif object is ColorPickerButton:
object.color = p_value
plugin.set_setting(ES_TOOL_SETTINGS + p_setting, p_value) # Signal doesn't fire on CPB
elif object is MultiPicker: # Expects p_value is PackedVector3Array
object.points = p_value
_on_setting_changed(object)
@ -548,13 +615,12 @@ func _on_setting_changed(p_data: Variant = null) -> void:
if p_data is Button and p_data.get_parent().get_parent() is PopupPanel:
if p_data.get_parent().name == "BrushList":
# Optionally Set selected brush texture in main brush button
# p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon())
p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon())
# Hide popup
p_data.get_parent().get_parent().set_visible(false)
# Hide label
if p_data.get_child_count() > 0:
p_data.get_child(0).visible = false
emit_signal("setting_changed")
@ -567,7 +633,6 @@ func _get_brush_preview_material() -> ShaderMaterial:
if !brush_preview_material:
brush_preview_material = ShaderMaterial.new()
var shader: Shader = Shader.new()
var code: String = "shader_type canvas_item;\n"
code += "varying vec4 v_vertex_color;\n"
code += "void vertex() {\n"
@ -578,13 +643,11 @@ func _get_brush_preview_material() -> ShaderMaterial:
code += " COLOR.a *= pow(tex.r, 0.666);\n"
code += " COLOR.rgb = v_vertex_color.rgb;\n"
code += "}\n"
shader.set_code(code)
brush_preview_material.set_shader(shader)
return brush_preview_material
# Counts digits of a number including negative sign, decimal points, and up to 3 decimals
func count_digits(p_value: float) -> int:
var count: int = 1
@ -605,75 +668,3 @@ func count_digits(p_value: float) -> int:
count += 1
return count
#### Sub Class DoubleSlider
class DoubleSlider extends Range:
signal setting_changed(Vector2)
var label: Label
var suffix: String
var grabbed: bool = false
var _max_value: float
# TODO Needs to clamp min and max values. Currently allows max slider to go negative.
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
grabbed = p_event.is_pressed()
set_min_max(p_event.get_position().x)
if p_event is InputEventMouseMotion:
if grabbed:
set_min_max(p_event.get_position().x)
func _notification(p_what: int) -> void:
if p_what == NOTIFICATION_RESIZED:
pass
if p_what == NOTIFICATION_DRAW:
var bg: StyleBox = get_theme_stylebox("slider", "HSlider")
var bg_height: float = bg.get_minimum_size().y
draw_style_box(bg, Rect2(Vector2(0, (size.y - bg_height) / 2), Vector2(size.x, bg_height)))
var grabber: Texture2D = get_theme_icon("grabber", "HSlider")
var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
var h: float = size.y / 2 - grabber.get_size().y / 2
var minpos: Vector2 = Vector2((min_value / _max_value) * size.x - grabber.get_size().x / 2, h)
var maxpos: Vector2 = Vector2((max_value / _max_value) * size.x - grabber.get_size().x / 2, h)
draw_style_box(area, Rect2(Vector2(minpos.x + grabber.get_size().x / 2, (size.y - bg_height) / 2), Vector2(maxpos.x - minpos.x, bg_height)))
draw_texture(grabber, minpos)
draw_texture(grabber, maxpos)
func set_max(p_value: float) -> void:
max_value = p_value
if _max_value == 0:
_max_value = max_value
update_label()
func set_min_max(p_xpos: float) -> void:
var mid_value_normalized: float = ((max_value + min_value) / 2.0) / _max_value
var mid_value: float = size.x * mid_value_normalized
var min_active: bool = p_xpos < mid_value
var xpos_ranged: float = snappedf((p_xpos / size.x) * _max_value, step)
if min_active:
min_value = xpos_ranged
else:
max_value = xpos_ranged
min_value = clamp(min_value, 0, max_value - 10)
max_value = clamp(max_value, min_value + 10, _max_value)
update_label()
emit_signal("setting_changed", Vector2(min_value, max_value))
queue_redraw()
func update_label() -> void:
if label:
label.set_text(str(min_value) + suffix + "/" + str(max_value) + suffix)

View File

@ -1,14 +1,11 @@
extends VBoxContainer
extends VFlowContainer
signal tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation)
const ICON_REGION_ADD: String = "res://addons/terrain_3d/icons/region_add.svg"
const ICON_REGION_REMOVE: String = "res://addons/terrain_3d/icons/region_remove.svg"
const ICON_HEIGHT_ADD: String = "res://addons/terrain_3d/icons/height_add.svg"
const ICON_HEIGHT_SUB: String = "res://addons/terrain_3d/icons/height_sub.svg"
const ICON_HEIGHT_MUL: String = "res://addons/terrain_3d/icons/height_mul.svg"
const ICON_HEIGHT_DIV: String = "res://addons/terrain_3d/icons/height_div.svg"
const ICON_HEIGHT_FLAT: String = "res://addons/terrain_3d/icons/height_flat.svg"
const ICON_HEIGHT_SLOPE: String = "res://addons/terrain_3d/icons/height_slope.svg"
const ICON_HEIGHT_SMOOTH: String = "res://addons/terrain_3d/icons/height_smooth.svg"
@ -21,57 +18,126 @@ const ICON_HOLES: String = "res://addons/terrain_3d/icons/holes.svg"
const ICON_NAVIGATION: String = "res://addons/terrain_3d/icons/navigation.svg"
const ICON_INSTANCER: String = "res://addons/terrain_3d/icons/multimesh.svg"
var tool_group: ButtonGroup = ButtonGroup.new()
var add_tool_group: ButtonGroup = ButtonGroup.new()
var sub_tool_group: ButtonGroup = ButtonGroup.new()
func _init() -> void:
set_custom_minimum_size(Vector2(20, 0))
func _ready() -> void:
tool_group.connect("pressed", _on_tool_selected)
add_tool_group.connect("pressed", _on_tool_selected)
sub_tool_group.connect("pressed", _on_tool_selected)
add_tool_button({ "tool":Terrain3DEditor.REGION,
"add_text":"Add Region", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_REGION_ADD,
"sub_text":"Remove Region", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_REGION_REMOVE })
add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.ADD, "Add Region", load(ICON_REGION_ADD), tool_group)
add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.SUBTRACT, "Remove Region", load(ICON_REGION_REMOVE), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.ADD, "Raise", load(ICON_HEIGHT_ADD), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.SUBTRACT, "Lower", load(ICON_HEIGHT_SUB), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.MULTIPLY, "Expand (Away from 0)", load(ICON_HEIGHT_MUL), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.DIVIDE, "Reduce (Towards 0)", load(ICON_HEIGHT_DIV), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.REPLACE, "Flatten", load(ICON_HEIGHT_FLAT), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.GRADIENT, "Slope", load(ICON_HEIGHT_SLOPE), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.AVERAGE, "Smooth", load(ICON_HEIGHT_SMOOTH), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE, "Paint Base Texture", load(ICON_PAINT_TEXTURE), tool_group)
add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.ADD, "Spray Overlay Texture", load(ICON_SPRAY_TEXTURE), tool_group)
add_tool_button(Terrain3DEditor.AUTOSHADER, Terrain3DEditor.REPLACE, "Autoshader", load(ICON_AUTOSHADER), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.COLOR, Terrain3DEditor.REPLACE, "Paint Color", load(ICON_COLOR), tool_group)
add_tool_button(Terrain3DEditor.ROUGHNESS, Terrain3DEditor.REPLACE, "Paint Wetness", load(ICON_WETNESS), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.HOLES, Terrain3DEditor.REPLACE, "Create Holes", load(ICON_HOLES), tool_group)
add_tool_button(Terrain3DEditor.NAVIGATION, Terrain3DEditor.REPLACE, "Paint Navigable Area", load(ICON_NAVIGATION), tool_group)
add_tool_button(Terrain3DEditor.INSTANCER, Terrain3DEditor.ADD, "Instance Meshes", load(ICON_INSTANCER), tool_group)
add_tool_button({ "tool":Terrain3DEditor.SCULPT,
"add_text":"Raise", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_ADD,
"sub_text":"Lower", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_SUB })
var buttons: Array[BaseButton] = tool_group.get_buttons()
add_tool_button({ "tool":Terrain3DEditor.SCULPT,
"add_text":"Smooth", "add_op":Terrain3DEditor.AVERAGE, "add_icon":ICON_HEIGHT_SMOOTH })
add_tool_button({ "tool":Terrain3DEditor.HEIGHT,
"add_text":"Height", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_FLAT,
"sub_text":"Zero", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_FLAT })
add_tool_button({ "tool":Terrain3DEditor.SCULPT,
"add_text":"Slope", "add_op":Terrain3DEditor.GRADIENT, "add_icon":ICON_HEIGHT_SLOPE })
add_child(HSeparator.new())
add_tool_button({ "tool":Terrain3DEditor.TEXTURE,
"add_text":"Paint Base Texture", "add_op":Terrain3DEditor.REPLACE, "add_icon":ICON_PAINT_TEXTURE })
add_tool_button({ "tool":Terrain3DEditor.TEXTURE,
"add_text":"Spray Overlay Texture", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_SPRAY_TEXTURE })
add_tool_button({ "tool":Terrain3DEditor.AUTOSHADER,
"add_text":"Enable Autoshader", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_AUTOSHADER,
"sub_text":"Disable Autoshader", "sub_op":Terrain3DEditor.SUBTRACT })
add_child(HSeparator.new())
add_tool_button({ "tool":Terrain3DEditor.COLOR,
"add_text":"Paint Color", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_COLOR,
"sub_text":"Remove Color", "sub_op":Terrain3DEditor.SUBTRACT })
add_tool_button({ "tool":Terrain3DEditor.ROUGHNESS,
"add_text":"Paint Wetness", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_WETNESS,
"sub_text":"Remove Wetness", "sub_op":Terrain3DEditor.SUBTRACT })
add_child(HSeparator.new())
add_tool_button({ "tool":Terrain3DEditor.HOLES,
"add_text":"Add Holes", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HOLES,
"sub_text":"Remove Holes", "sub_op":Terrain3DEditor.SUBTRACT })
add_tool_button({ "tool":Terrain3DEditor.NAVIGATION,
"add_text":"Paint Navigable Area", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_NAVIGATION,
"sub_text":"Remove Navigable Area", "sub_op":Terrain3DEditor.SUBTRACT })
add_tool_button({ "tool":Terrain3DEditor.INSTANCER,
"add_text":"Instance Meshes", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_INSTANCER,
"sub_text":"Remove Meshes", "sub_op":Terrain3DEditor.SUBTRACT })
# Select first button
var buttons: Array[BaseButton] = add_tool_group.get_buttons()
buttons[0].set_pressed(true)
show_add_buttons(true)
func add_tool_button(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation,
p_tip: String, p_icon: Texture2D, p_group: ButtonGroup) -> void:
var button: Button = Button.new()
button.set_name(p_tip.to_pascal_case())
button.set_meta("Tool", p_tool)
button.set_meta("Operation", p_operation)
button.set_tooltip_text(p_tip)
button.set_button_icon(p_icon)
button.set_button_group(p_group)
func add_tool_button(p_params: Dictionary) -> void:
# Additive button
var button := Button.new()
button.set_name(p_params.get("add_text", "blank").to_pascal_case())
button.set_meta("Tool", p_params.get("tool", 0))
button.set_meta("Operation", p_params.get("add_op", 0))
button.set_meta("ID", add_tool_group.get_buttons().size() + 1)
button.set_tooltip_text(p_params.get("add_text", "blank"))
button.set_button_icon(load(p_params.get("add_icon")))
button.set_flat(true)
button.set_toggle_mode(true)
button.set_h_size_flags(SIZE_SHRINK_END)
add_child(button)
button.set_button_group(p_params.get("group", add_tool_group))
add_child(button, true)
# Subtractive button
var button2: Button
if p_params.has("sub_text"):
button2 = Button.new()
button2.set_name(p_params.get("sub_text", "blank").to_pascal_case())
button2.set_meta("Tool", p_params.get("tool", 0))
button2.set_meta("Operation", p_params.get("sub_op", 0))
button2.set_meta("ID", button.get_meta("ID"))
button2.set_tooltip_text(p_params.get("sub_text", "blank"))
button2.set_button_icon(load(p_params.get("sub_icon", p_params.get("add_icon"))))
button2.set_flat(true)
button2.set_toggle_mode(true)
button2.set_h_size_flags(SIZE_SHRINK_END)
else:
button2 = button.duplicate()
button2.set_button_group(p_params.get("group", sub_tool_group))
add_child(button2, true)
func show_add_buttons(p_enable: bool) -> void:
for button in add_tool_group.get_buttons():
button.visible = p_enable
for button in sub_tool_group.get_buttons():
button.visible = !p_enable
func _on_tool_selected(p_button: BaseButton) -> void:
emit_signal("tool_changed", p_button.get_meta("Tool", -1), p_button.get_meta("Operation", -1))
# Select same tool on negative bar
var group: ButtonGroup = p_button.get_button_group()
var change_group: ButtonGroup = add_tool_group if group == sub_tool_group else sub_tool_group
var id: int = p_button.get_meta("ID", -2)
for button in change_group.get_buttons():
button.set_pressed_no_signal(button.get_meta("ID", -1) == id)
emit_signal("tool_changed", p_button.get_meta("Tool", Terrain3DEditor.TOOL_MAX), p_button.get_meta("Operation", Terrain3DEditor.OP_MAX))

View File

@ -5,34 +5,38 @@ extends Node
# Includes
const Toolbar: Script = preload("res://addons/terrain_3d/src/toolbar.gd")
const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd")
const TerrainTools: Script = preload("res://addons/terrain_3d/src/terrain_tools.gd")
const TerrainMenu: Script = preload("res://addons/terrain_3d/src/terrain_menu.gd")
const OperationBuilder: Script = preload("res://addons/terrain_3d/src/operation_builder.gd")
const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/src/gradient_operation_builder.gd")
const COLOR_RAISE := Color.WHITE
const COLOR_LOWER := Color.BLACK
const COLOR_LOWER := Color(.02, .02, .02)
const COLOR_SMOOTH := Color(0.5, 0, .1)
const COLOR_EXPAND := Color.ORANGE
const COLOR_REDUCE := Color.BLUE_VIOLET
const COLOR_FLATTEN := Color(0., 0.32, .4)
const COLOR_LIFT := Color.ORANGE
const COLOR_FLATTEN := Color.BLUE_VIOLET
const COLOR_HEIGHT := Color(0., 0.32, .4)
const COLOR_SLOPE := Color.YELLOW
const COLOR_PAINT := Color.FOREST_GREEN
const COLOR_SPRAY := Color.SEA_GREEN
const COLOR_PAINT := Color.DARK_GREEN
const COLOR_SPRAY := Color.PALE_GREEN
const COLOR_ROUGHNESS := Color.ROYAL_BLUE
const COLOR_AUTOSHADER := Color.DODGER_BLUE
const COLOR_HOLES := Color.BLACK
const COLOR_NAVIGATION := Color.REBECCA_PURPLE
const COLOR_NAVIGATION := Color(.15, .0, .255)
const COLOR_INSTANCER := Color.CRIMSON
const COLOR_PICK_COLOR := Color.WHITE
const COLOR_PICK_HEIGHT := Color.DARK_RED
const COLOR_PICK_ROUGH := Color.ROYAL_BLUE
const OP_NONE: int = 0x0
const OP_POSITIVE_ONLY: int = 0x01
const OP_NEGATIVE_ONLY: int = 0x02
const RING1: String = "res://addons/terrain_3d/brushes/ring1.exr"
@onready var ring_texture := ImageTexture.create_from_image(Terrain3DUtil.black_to_alpha(Image.load_from_file(RING1)))
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var toolbar: Toolbar
var toolbar_settings: ToolSettings
var terrain_tools: TerrainTools
var tool_settings: ToolSettings
var terrain_menu: TerrainMenu
var setting_has_changed: bool = false
var visible: bool = false
var picking: int = Terrain3DEditor.TOOL_MAX
@ -42,6 +46,16 @@ var decal_timer: Timer
var gradient_decals: Array[Decal]
var brush_data: Dictionary
var operation_builder: OperationBuilder
var last_tool: Terrain3DEditor.Tool
var last_operation: Terrain3DEditor.Operation
var last_rmb_time: int = 0 # Set in editor.gd
# Compatibility decals, indices; 0 = main brush, 1 = slope point A, 2 = slope point B
var editor_decal_position: Array[Vector2]
var editor_decal_rotation: Array[float]
var editor_decal_size: Array[float]
var editor_decal_color: Array[Color]
var editor_decal_visible: Array[bool]
func _enter_tree() -> void:
@ -49,18 +63,19 @@ func _enter_tree() -> void:
toolbar.hide()
toolbar.connect("tool_changed", _on_tool_changed)
toolbar_settings = ToolSettings.new()
toolbar_settings.connect("setting_changed", _on_setting_changed)
toolbar_settings.connect("picking", _on_picking)
toolbar_settings.hide()
tool_settings = ToolSettings.new()
tool_settings.connect("setting_changed", _on_setting_changed)
tool_settings.connect("picking", _on_picking)
tool_settings.plugin = plugin
tool_settings.hide()
terrain_tools = TerrainTools.new()
terrain_tools.plugin = plugin
terrain_tools.hide()
terrain_menu = TerrainMenu.new()
terrain_menu.plugin = plugin
terrain_menu.hide()
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_tools)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, tool_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_menu)
_on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD)
@ -77,10 +92,10 @@ func _enter_tree() -> void:
func _exit_tree() -> void:
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar)
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, tool_settings)
toolbar.queue_free()
toolbar_settings.queue_free()
terrain_tools.queue_free()
tool_settings.queue_free()
terrain_menu.queue_free()
decal.queue_free()
decal_timer.queue_free()
for gradient_decal in gradient_decals:
@ -88,14 +103,27 @@ func _exit_tree() -> void:
gradient_decals.clear()
func set_visible(p_visible: bool) -> void:
visible = p_visible
terrain_tools.set_visible(p_visible)
toolbar.set_visible(p_visible)
toolbar_settings.set_visible(p_visible)
update_decal()
func set_visible(p_visible: bool, p_menu_only: bool = false) -> void:
terrain_menu.set_visible(p_visible)
if p_menu_only:
toolbar.set_visible(false)
tool_settings.set_visible(false)
else:
visible = p_visible
toolbar.set_visible(p_visible)
tool_settings.set_visible(p_visible)
update_decal()
if(plugin.editor):
if(p_visible):
await get_tree().create_timer(.01).timeout # Won't work, otherwise.
_on_tool_changed(last_tool, last_operation)
else:
plugin.editor.set_tool(Terrain3DEditor.TOOL_MAX)
plugin.editor.set_operation(Terrain3DEditor.OP_MAX)
func set_menu_visibility(p_list: Control, p_visible: bool) -> void:
if p_list:
p_list.get_parent().get_parent().visible = p_visible
@ -103,33 +131,45 @@ func set_menu_visibility(p_list: Control, p_visible: bool) -> void:
func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
clear_picking()
set_menu_visibility(toolbar_settings.advanced_list, true)
set_menu_visibility(toolbar_settings.scale_list, false)
set_menu_visibility(toolbar_settings.rotation_list, false)
set_menu_visibility(toolbar_settings.height_list, false)
set_menu_visibility(toolbar_settings.color_list, false)
set_menu_visibility(tool_settings.advanced_list, true)
set_menu_visibility(tool_settings.scale_list, false)
set_menu_visibility(tool_settings.rotation_list, false)
set_menu_visibility(tool_settings.height_list, false)
set_menu_visibility(tool_settings.color_list, false)
# Select which settings to show. Options in tool_settings.gd:_ready
var to_show: PackedStringArray = []
match p_tool:
Terrain3DEditor.REGION:
to_show.push_back("instructions")
to_show.push_back("remove")
set_menu_visibility(tool_settings.advanced_list, false)
Terrain3DEditor.SCULPT:
to_show.push_back("brush")
to_show.push_back("size")
to_show.push_back("strength")
if p_operation in [Terrain3DEditor.ADD, Terrain3DEditor.SUBTRACT]:
to_show.push_back("remove")
elif p_operation == Terrain3DEditor.GRADIENT:
to_show.push_back("gradient_points")
to_show.push_back("drawable")
Terrain3DEditor.HEIGHT:
to_show.push_back("brush")
to_show.push_back("size")
to_show.push_back("strength")
if p_operation == Terrain3DEditor.REPLACE:
to_show.push_back("height")
to_show.push_back("height_picker")
if p_operation == Terrain3DEditor.GRADIENT:
to_show.push_back("gradient_points")
to_show.push_back("drawable")
Terrain3DEditor.TEXTURE:
to_show.push_back("height")
to_show.push_back("height_picker")
Terrain3DEditor.TEXTURE:
to_show.push_back("brush")
to_show.push_back("size")
to_show.push_back("enable_texture")
if p_operation == Terrain3DEditor.ADD:
to_show.push_back("strength")
to_show.push_back("slope")
to_show.push_back("enable_angle")
to_show.push_back("angle")
to_show.push_back("angle_picker")
@ -144,6 +184,10 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
to_show.push_back("strength")
to_show.push_back("color")
to_show.push_back("color_picker")
to_show.push_back("slope")
to_show.push_back("enable_texture")
to_show.push_back("margin")
to_show.push_back("remove")
Terrain3DEditor.ROUGHNESS:
to_show.push_back("brush")
@ -151,52 +195,59 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
to_show.push_back("strength")
to_show.push_back("roughness")
to_show.push_back("roughness_picker")
to_show.push_back("slope")
to_show.push_back("enable_texture")
to_show.push_back("margin")
to_show.push_back("remove")
Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION:
to_show.push_back("brush")
to_show.push_back("size")
to_show.push_back("enable")
to_show.push_back("remove")
Terrain3DEditor.INSTANCER:
to_show.push_back("size")
to_show.push_back("strength")
to_show.push_back("enable")
set_menu_visibility(toolbar_settings.height_list, true)
to_show.push_back("slope")
set_menu_visibility(tool_settings.height_list, true)
to_show.push_back("height_offset")
to_show.push_back("random_height")
set_menu_visibility(toolbar_settings.scale_list, true)
set_menu_visibility(tool_settings.scale_list, true)
to_show.push_back("fixed_scale")
to_show.push_back("random_scale")
set_menu_visibility(toolbar_settings.rotation_list, true)
set_menu_visibility(tool_settings.rotation_list, true)
to_show.push_back("fixed_spin")
to_show.push_back("random_spin")
to_show.push_back("fixed_angle")
to_show.push_back("random_angle")
to_show.push_back("fixed_tilt")
to_show.push_back("random_tilt")
to_show.push_back("align_to_normal")
set_menu_visibility(toolbar_settings.color_list, true)
set_menu_visibility(tool_settings.color_list, true)
to_show.push_back("vertex_color")
to_show.push_back("random_darken")
to_show.push_back("random_hue")
to_show.push_back("remove")
_:
pass
# Advanced menu settings
to_show.push_back("automatic_regions")
to_show.push_back("auto_regions")
to_show.push_back("align_to_view")
to_show.push_back("show_cursor_while_painting")
to_show.push_back("gamma")
to_show.push_back("jitter")
toolbar_settings.show_settings(to_show)
tool_settings.show_settings(to_show)
operation_builder = null
if p_operation == Terrain3DEditor.GRADIENT:
operation_builder = GradientOperationBuilder.new()
operation_builder.tool_settings = toolbar_settings
operation_builder.tool_settings = tool_settings
if plugin.editor:
plugin.editor.set_tool(p_tool)
plugin.editor.set_operation(p_operation)
plugin.editor.set_operation(_modify_operation(p_operation))
last_tool = p_tool
last_operation = p_operation
_on_setting_changed()
plugin.update_region_grid()
@ -205,31 +256,67 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
func _on_setting_changed() -> void:
if not plugin.asset_dock:
return
brush_data = toolbar_settings.get_settings()
brush_data = tool_settings.get_settings()
brush_data["asset_id"] = plugin.asset_dock.get_current_list().get_selected_id()
update_decal()
plugin.editor.set_brush_data(brush_data)
plugin.editor.set_operation(_modify_operation(plugin.editor.get_operation()))
func update_modifiers() -> void:
toolbar.show_add_buttons(not plugin.modifier_ctrl)
if plugin.modifier_shift and not plugin.modifier_ctrl:
plugin.editor.set_tool(Terrain3DEditor.SCULPT)
plugin.editor.set_operation(Terrain3DEditor.AVERAGE)
else:
plugin.editor.set_tool(last_tool)
if plugin.modifier_ctrl:
plugin.editor.set_operation(_modify_operation(last_operation))
else:
plugin.editor.set_operation(last_operation)
func _modify_operation(p_operation: Terrain3DEditor.Operation) -> Terrain3DEditor.Operation:
var remove_checked: bool = false
if DisplayServer.is_touchscreen_available():
var removable_tools := [Terrain3DEditor.REGION, Terrain3DEditor.SCULPT, Terrain3DEditor.HEIGHT, Terrain3DEditor.AUTOSHADER,
Terrain3DEditor.HOLES, Terrain3DEditor.INSTANCER, Terrain3DEditor.NAVIGATION,
Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS]
remove_checked = brush_data.get("remove", false) && plugin.editor.get_tool() in removable_tools
if plugin.modifier_ctrl or remove_checked:
return _invert_operation(p_operation, OP_NEGATIVE_ONLY)
return _invert_operation(p_operation, OP_POSITIVE_ONLY)
func _invert_operation(p_operation: Terrain3DEditor.Operation, flags: int = OP_NONE) -> Terrain3DEditor.Operation:
if p_operation == Terrain3DEditor.ADD and ! (flags & OP_POSITIVE_ONLY):
return Terrain3DEditor.SUBTRACT
elif p_operation == Terrain3DEditor.SUBTRACT and ! (flags & OP_NEGATIVE_ONLY):
return Terrain3DEditor.ADD
return p_operation
func update_decal() -> void:
var mouse_buttons: int = Input.get_mouse_button_mask()
# If not a state that should show the decal, hide everything and return
if not visible or \
not plugin.terrain or \
brush_data.is_empty() or \
mouse_buttons & MOUSE_BUTTON_RIGHT or \
(mouse_buttons & MOUSE_BUTTON_LEFT and not brush_data["show_cursor_while_painting"]) or \
plugin.editor.get_tool() == Terrain3DEditor.REGION:
decal.visible = false
for gradient_decal in gradient_decals:
gradient_decal.visible = false
return
else:
# Wait for cursor to recenter after right-click before revealing
not plugin.terrain or \
plugin._input_mode < 0 or \
# Wait for cursor to recenter after moving camera before revealing
# See https://github.com/godotengine/godot/issues/70098
await get_tree().create_timer(.05).timeout
decal.visible = true
Time.get_ticks_msec() - last_rmb_time <= 30 or \
brush_data.is_empty() or \
plugin.editor.get_tool() == Terrain3DEditor.REGION or \
(plugin._input_mode > 0 and not brush_data["show_cursor_while_painting"]):
decal.visible = false
for gradient_decal in gradient_decals:
gradient_decal.visible = false
return
decal.size = Vector3.ONE * brush_data["size"]
decal.position = plugin.mouse_global_position
decal.visible = true
decal.size = Vector3.ONE * maxf(brush_data["size"], .5)
if brush_data["align_to_view"]:
var cam: Camera3D = plugin.terrain.get_camera();
if (cam):
@ -240,7 +327,7 @@ func update_decal() -> void:
# Set texture and color
if picking != Terrain3DEditor.TOOL_MAX:
decal.texture_albedo = ring_texture
decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing()
match picking:
Terrain3DEditor.HEIGHT:
decal.modulate = COLOR_PICK_HEIGHT
@ -252,62 +339,66 @@ func update_decal() -> void:
else:
decal.texture_albedo = brush_data["brush"][1]
match plugin.editor.get_tool():
Terrain3DEditor.HEIGHT:
Terrain3DEditor.SCULPT:
match plugin.editor.get_operation():
Terrain3DEditor.ADD:
decal.modulate = COLOR_RAISE
if plugin.modifier_alt:
decal.modulate = COLOR_LIFT
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
else:
decal.modulate = COLOR_RAISE
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.SUBTRACT:
decal.modulate = COLOR_LOWER
Terrain3DEditor.MULTIPLY:
decal.modulate = COLOR_EXPAND
Terrain3DEditor.DIVIDE:
decal.modulate = COLOR_REDUCE
Terrain3DEditor.REPLACE:
decal.modulate = COLOR_FLATTEN
if plugin.modifier_alt:
decal.modulate = COLOR_FLATTEN
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
else:
decal.modulate = COLOR_LOWER
decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .5
Terrain3DEditor.AVERAGE:
decal.modulate = COLOR_SMOOTH
decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .2
Terrain3DEditor.GRADIENT:
decal.modulate = COLOR_SLOPE
_:
decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["strength"] * .01)
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.HEIGHT:
decal.modulate = COLOR_HEIGHT
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.TEXTURE:
match plugin.editor.get_operation():
Terrain3DEditor.REPLACE:
decal.modulate = COLOR_PAINT
decal.modulate.a = 1.0
decal.modulate.a = .7
Terrain3DEditor.SUBTRACT:
decal.modulate = COLOR_PAINT
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.ADD:
decal.modulate = COLOR_SPRAY
decal.modulate.a = max(.3, brush_data["strength"] * .01)
_:
decal.modulate = Color.WHITE
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.COLOR:
decal.modulate = brush_data["color"].srgb_to_linear()*.5
decal.modulate.a = max(.3, brush_data["strength"] * .01)
decal.modulate = brush_data["color"].srgb_to_linear()
decal.modulate.a *= clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_ROUGHNESS
decal.modulate.a = max(.3, brush_data["strength"] * .01)
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.AUTOSHADER:
decal.modulate = COLOR_AUTOSHADER
decal.modulate.a = 1.0
decal.modulate.a = .7
Terrain3DEditor.HOLES:
decal.modulate = COLOR_HOLES
decal.modulate.a = 1.0
decal.modulate.a = .85
Terrain3DEditor.NAVIGATION:
decal.modulate = COLOR_NAVIGATION
decal.modulate.a = 1.0
decal.modulate.a = .85
Terrain3DEditor.INSTANCER:
decal.texture_albedo = ring_texture
decal.modulate = COLOR_INSTANCER
decal.modulate.a = 1.0
_:
decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["strength"] * .01)
decal.size.y = max(1000, decal.size.y)
decal.albedo_mix = 1.0
decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 )
decal_timer.start()
for gradient_decal in gradient_decals:
gradient_decal.visible = false
@ -320,6 +411,8 @@ func update_decal() -> void:
point_decal.position = point
index += 1
update_compatibility_decal()
func _get_gradient_decal(index: int) -> Decal:
if gradient_decals.size() > index:
@ -329,7 +422,7 @@ func _get_gradient_decal(index: int) -> Decal:
gradient_decal = Decal.new()
gradient_decal.texture_albedo = ring_texture
gradient_decal.modulate = COLOR_SLOPE
gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing()
gradient_decal.size.y = 1000.
gradient_decal.cull_mask = decal.cull_mask
add_child(gradient_decal)
@ -338,6 +431,61 @@ func _get_gradient_decal(index: int) -> Decal:
return gradient_decal
func update_compatibility_decal() -> void:
if not plugin.terrain.is_compatibility_mode():
return
# Verify setup
if editor_decal_position.size() != 3:
editor_decal_position.resize(3)
editor_decal_rotation.resize(3)
editor_decal_size.resize(3)
editor_decal_color.resize(3)
editor_decal_visible.resize(3)
decal_timer.timeout.connect(func():
var mat_rid: RID = plugin.terrain.material.get_material_rid()
editor_decal_visible[0] = false
RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible)
)
# Update compatibility decal
var mat_rid: RID = plugin.terrain.material.get_material_rid()
if decal.visible:
editor_decal_position[0] = Vector2(decal.global_position.x, decal.global_position.z)
editor_decal_rotation[0] = decal.rotation.y
editor_decal_size[0] = brush_data.get("size")
editor_decal_color[0] = decal.modulate
editor_decal_visible[0] = decal.visible
RenderingServer.material_set_param(
mat_rid, "_editor_decal_0", decal.texture_albedo.get_rid()
)
if gradient_decals.size() >= 1:
editor_decal_position[1] = Vector2(gradient_decals[0].global_position.x,
gradient_decals[0].global_position.z)
editor_decal_rotation[1] = gradient_decals[0].rotation.y
editor_decal_size[1] = 10.0
editor_decal_color[1] = gradient_decals[0].modulate
editor_decal_visible[1] = gradient_decals[0].visible
RenderingServer.material_set_param(
mat_rid, "_editor_decal_1", gradient_decals[0].texture_albedo.get_rid()
)
if gradient_decals.size() >= 2:
editor_decal_position[2] = Vector2(gradient_decals[1].global_position.x,
gradient_decals[1].global_position.z)
editor_decal_rotation[2] = gradient_decals[1].rotation.y
editor_decal_size[2] = 10.0
editor_decal_color[2] = gradient_decals[1].modulate
editor_decal_visible[2] = gradient_decals[1].visible
RenderingServer.material_set_param(
mat_rid, "_editor_decal_2", gradient_decals[1].texture_albedo.get_rid()
)
RenderingServer.material_set_param(mat_rid, "_editor_decal_position", editor_decal_position)
RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation)
RenderingServer.material_set_param(mat_rid, "_editor_decal_size", editor_decal_size)
RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color)
RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible)
func set_decal_rotation(p_rot: float) -> void:
decal.rotation.y = p_rot
@ -366,16 +514,16 @@ func pick(p_global_position: Vector3) -> void:
if picking != Terrain3DEditor.TOOL_MAX:
var color: Color
match picking:
Terrain3DEditor.HEIGHT:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_HEIGHT, p_global_position)
Terrain3DEditor.HEIGHT, Terrain3DEditor.SCULPT:
color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_HEIGHT, p_global_position)
Terrain3DEditor.ROUGHNESS:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position)
color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_COLOR, p_global_position)
Terrain3DEditor.COLOR:
color = plugin.terrain.get_storage().get_color(p_global_position)
color = plugin.terrain.data.get_color(p_global_position)
Terrain3DEditor.ANGLE:
color = Color(plugin.terrain.get_storage().get_angle(p_global_position), 0., 0., 1.)
color = Color(plugin.terrain.data.get_control_angle(p_global_position), 0., 0., 1.)
Terrain3DEditor.SCALE:
color = Color(plugin.terrain.get_storage().get_scale(p_global_position), 0., 0., 1.)
color = Color(plugin.terrain.data.get_control_scale(p_global_position), 0., 0., 1.)
_:
push_error("Unsupported picking type: ", picking)
return
@ -385,3 +533,6 @@ func pick(p_global_position: Vector3) -> void:
elif operation_builder and operation_builder.is_picking():
operation_builder.pick(p_global_position, plugin.terrain)
func set_button_editor_icon(p_button: Button, p_icon_name: String) -> void:
p_button.icon = EditorInterface.get_base_control().get_theme_icon(p_icon_name, "EditorIcons")

View File

@ -12,64 +12,83 @@ func reset_settings(p_value) -> void:
height_file_name = ""
control_file_name = ""
color_file_name = ""
import_position = Vector3.ZERO
import_offset = 0.0
destination_directory = ""
import_position = Vector2i.ZERO
height_offset = 0.0
import_scale = 1.0
r16_range = Vector2(0, 1)
r16_size = Vector2i(1024, 1024)
storage = null
material = null
assets = null
func reset_terrain(p_value) -> void:
if p_value:
storage = null
data_directory = ""
func update_heights(p_value) -> void:
if p_value and storage:
storage.update_height_range()
if p_value and data:
data.update_height_range()
@export_group("Import File")
@export_global_file var height_file_name: String = ""
@export_global_file var control_file_name: String = ""
@export_global_file var color_file_name: String = ""
@export var import_position: Vector3 = Vector3.ZERO
@export var import_position: Vector2i = Vector2i(0, 0) : set = set_import_position
@export var import_scale: float = 1.0
@export var import_offset: float = 0.0
@export var height_offset: float = 0.0
@export var r16_range: Vector2 = Vector2(0, 1)
@export var r16_size: Vector2i = Vector2i(1024, 1024)
@export var r16_size: Vector2i = Vector2i(1024, 1024) : set = set_r16_size
@export var run_import: bool = false : set = start_import
@export_dir var destination_directory: String = ""
@export var save_to_disk: bool = false : set = save_data
func set_import_position(p_value: Vector2i) -> void:
import_position.x = clamp(p_value.x, -8192, 8192)
import_position.y = clamp(p_value.y, -8192, 8192)
func set_r16_size(p_value: Vector2i) -> void:
r16_size.x = clamp(p_value.x, 0, 16384)
r16_size.y = clamp(p_value.y, 0, 16384)
func start_import(p_value: bool) -> void:
if p_value:
print("Terrain3DImporter: Importing files:\n\t%s\n\t%s\n\t%s" % [ height_file_name, control_file_name, color_file_name])
if not storage:
storage = Terrain3DStorage.new()
var imported_images: Array[Image]
imported_images.resize(Terrain3DStorage.TYPE_MAX)
imported_images.resize(Terrain3DRegion.TYPE_MAX)
var min_max := Vector2(0, 1)
var img: Image
if height_file_name:
img = Terrain3DUtil.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size)
min_max = Terrain3DUtil.get_min_max(img)
imported_images[Terrain3DStorage.TYPE_HEIGHT] = img
imported_images[Terrain3DRegion.TYPE_HEIGHT] = img
if control_file_name:
img = Terrain3DUtil.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE)
imported_images[Terrain3DStorage.TYPE_CONTROL] = img
imported_images[Terrain3DRegion.TYPE_CONTROL] = img
if color_file_name:
img = Terrain3DUtil.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE)
imported_images[Terrain3DStorage.TYPE_COLOR] = img
imported_images[Terrain3DRegion.TYPE_COLOR] = img
if assets.get_texture_count() == 0:
material.show_checkered = false
material.show_colormap = true
storage.import_images(imported_images, import_position, import_offset, import_scale)
var pos := Vector3(import_position.x, 0, import_position.y)
data.import_images(imported_images, pos, height_offset, import_scale)
print("Terrain3DImporter: Import finished")
func save_data(p_value: bool) -> void:
if destination_directory.is_empty():
push_error("Set destination directory first")
return
data.save_directory(destination_directory)
@export_group("Export File")
enum { TYPE_HEIGHT, TYPE_CONTROL, TYPE_COLOR }
@export_enum("Height:0", "Control:1", "Color:2") var map_type: int = TYPE_HEIGHT
@ -77,6 +96,6 @@ enum { TYPE_HEIGHT, TYPE_CONTROL, TYPE_COLOR }
@export var run_export: bool = false : set = start_export
func start_export(p_value: bool) -> void:
var err: int = storage.export_image(file_name_out, map_type)
var err: int = data.export_image(file_name_out, map_type)
print("Terrain3DImporter: Export error status: ", err, " ", error_string(err))

View File

@ -1,16 +1,63 @@
[gd_scene load_steps=5 format=3 uid="uid://blaieaqp413k7"]
[gd_scene load_steps=9 format=3 uid="uid://blaieaqp413k7"]
[ext_resource type="Script" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"]
[sub_resource type="Terrain3DStorage" id="Terrain3DStorage_rmuvl"]
[sub_resource type="Gradient" id="Gradient_88f3t"]
offsets = PackedFloat32Array(0.2, 1)
colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1)
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_cjpaa"]
[sub_resource type="FastNoiseLite" id="FastNoiseLite_muvel"]
noise_type = 2
frequency = 0.03
cellular_jitter = 3.0
cellular_return_type = 0
domain_warp_enabled = true
domain_warp_type = 1
domain_warp_amplitude = 50.0
domain_warp_fractal_type = 2
domain_warp_fractal_lacunarity = 1.5
domain_warp_fractal_gain = 1.0
[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_ve0yk"]
seamless = true
color_ramp = SubResource("Gradient_88f3t")
noise = SubResource("FastNoiseLite_muvel")
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_p55u0"]
_shader_parameters = {
"blend_sharpness": 0.87,
"height_blending": true,
"macro_variation1": Color(1, 1, 1, 1),
"macro_variation2": Color(1, 1, 1, 1),
"noise1_angle": 0.0,
"noise1_offset": Vector2(0.5, 0.5),
"noise1_scale": 0.04,
"noise2_scale": 0.076,
"noise3_scale": 0.225,
"noise_texture": SubResource("NoiseTexture2D_ve0yk"),
"vertex_normals_distance": 128.0
}
show_checkered = true
[sub_resource type="Terrain3DAssets" id="Terrain3DAssets_gbxcd"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_8rvqy"]
cull_mode = 2
vertex_color_use_as_albedo = true
backlight_enabled = true
backlight = Color(0.5, 0.5, 0.5, 1)
[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_7je72"]
height_offset = 0.5
density = 10.0
material_override = SubResource("StandardMaterial3D_8rvqy")
generated_type = 1
[sub_resource type="Terrain3DAssets" id="Terrain3DAssets_op32e"]
mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_7je72")])
[node name="Importer" type="Terrain3D"]
storage = SubResource("Terrain3DStorage_rmuvl")
material = SubResource("Terrain3DMaterial_cjpaa")
assets = SubResource("Terrain3DAssets_gbxcd")
material = SubResource("Terrain3DMaterial_p55u0")
assets = SubResource("Terrain3DAssets_op32e")
mesh_lods = 8
top_level = true
script = ExtResource("1_60b8f")
metadata/_edit_lock_ = true

View File

@ -42,22 +42,22 @@ func editor_setup(p_plugin) -> void:
func get_terrain() -> Terrain3D:
var terrain := instance_from_id(_terrain_id) as Terrain3D
if not terrain or terrain.is_queued_for_deletion() or not terrain.is_inside_tree():
var terrains: Array[Node] = EditorInterface.get_edited_scene_root().find_children("", "Terrain3D")
var terrains: Array[Node] = Engine.get_singleton(&"EditorInterface").get_edited_scene_root().find_children("", "Terrain3D")
if terrains.size() > 0:
terrain = terrains[0]
_terrain_id = terrain.get_instance_id() if terrain else 0
if terrain and terrain.storage and not terrain.storage.maps_edited.is_connected(_on_maps_edited):
terrain.storage.maps_edited.connect(_on_maps_edited)
if terrain and terrain.data and not terrain.data.maps_edited.is_connected(_on_maps_edited):
terrain.data.maps_edited.connect(_on_maps_edited)
return terrain
func _get_terrain_height(p_global_position: Vector3) -> float:
var terrain: Terrain3D = get_terrain()
if not terrain or not terrain.storage:
if not terrain or not terrain.data:
return 0.0
var height: float = terrain.storage.get_height(p_global_position)
var height: float = terrain.data.get_height(p_global_position)
if is_nan(height):
return 0.0
return height
@ -105,7 +105,7 @@ func _on_child_exiting_tree(p_node: Node) -> void:
func _is_node_selected(p_node: Node) -> bool:
var editor_sel = EditorInterface.get_selection()
var editor_sel = Engine.get_singleton(&"EditorInterface").get_selection()
return editor_sel.get_transformable_selected_nodes().has(p_node)

View File

@ -1,6 +1,5 @@
[gd_scene load_steps=56 format=3 uid="uid://bm2o3mex10v11"]
[gd_scene load_steps=55 format=3 uid="uid://bm2o3mex10v11"]
[ext_resource type="Terrain3DStorage" uid="uid://bu1gewgsgc5hm" path="res://levels/debug_level/terrain_3d_storage.res" id="1_nlsu2"]
[ext_resource type="Texture2D" path="res://assets/textures/grass_fairway/grass_fairway_albedo.dds" id="2_e4m27"]
[ext_resource type="Texture2D" path="res://assets/textures/grass_rough/grass_rough_albedo.dds" id="4_p0awb"]
[ext_resource type="Texture2D" path="res://assets/textures/grass_rough/grass_rough_normal.dds" id="4_yhjhv"]
@ -51,30 +50,33 @@ _shader_parameters = {
"noise3_scale": 0.021,
"noise_texture": SubResource("NoiseTexture2D_sb318"),
"vertex_normals_distance": 128.0,
"world_noise_blend_far": 1.0,
"world_noise_blend_near": 0.48,
"world_noise_height": 74.0,
"world_noise_lod_distance": 16384.0,
"world_noise_max_octaves": 8,
"world_noise_min_octaves": 2,
"world_noise_offset": Vector3(0, -8, 0),
"world_noise_region_blend": 0.33,
"world_noise_scale": 5.0
}
world_background = 2
texture_filtering = 1
auto_shader = true
show_region_grid = true
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_2u1w6"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_l1sla"]
transparency = 4
cull_mode = 2
vertex_color_use_as_albedo = true
backlight_enabled = true
backlight = Color(0.5, 0.5, 0.5, 1)
distance_fade_mode = 1
distance_fade_min_distance = 960.0
distance_fade_max_distance = 480.0
[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_8g62o"]
name = "palm_tree"
[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_iaq2t"]
height_offset = 0.5
density = 10.0
material_override = SubResource("StandardMaterial3D_2u1w6")
material_override = SubResource("StandardMaterial3D_l1sla")
generated_type = 1
[sub_resource type="Terrain3DTextureAsset" id="Terrain3DTextureAsset_kocfk"]
@ -105,7 +107,7 @@ normal_texture = ExtResource("9_rrguh")
uv_scale = 0.02
[sub_resource type="Terrain3DAssets" id="Terrain3DAssets_eq5uw"]
mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_8g62o")])
mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_iaq2t")])
texture_list = Array[Terrain3DTextureAsset]([SubResource("Terrain3DTextureAsset_kocfk"), SubResource("Terrain3DTextureAsset_vmhlw"), SubResource("Terrain3DTextureAsset_k6h8c"), SubResource("Terrain3DTextureAsset_13d2a")])
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_h8tes"]
@ -252,9 +254,11 @@ point_count = 5
[node name="TestLevel" type="Node3D"]
[node name="Terrain3D" type="Terrain3D" parent="."]
storage = ExtResource("1_nlsu2")
data_directory = "res://levels/debug_level/terrain_3d_data"
material = SubResource("Terrain3DMaterial_woy2k")
assets = SubResource("Terrain3DAssets_eq5uw")
show_grid = true
metadata/_edit_lock_ = true
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_k6wwx")

BIN
levels/debug_level/terrain_3d_data/terrain3d-01_00.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
levels/debug_level/terrain_3d_data/terrain3d_00_00.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
levels/debug_level/terrain_3d_storage.res (Stored with Git LFS)

Binary file not shown.