first commit

This commit is contained in:
Ugric
2026-03-02 02:17:04 +00:00
commit 5d56860f3a
813 changed files with 41799 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
shader_type canvas_item;
void fragment() {
float a = texture(TEXTURE, UV).a;
COLOR = vec4(a, a, a, 1.0);
}

View File

@@ -0,0 +1 @@
uid://6bb66fu1fw8n

View File

@@ -0,0 +1,9 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/texture_editor/display_alpha.gdshader" type="Shader" id=1]
[resource]
render_priority = 0
shader = ExtResource( 1 )

View File

@@ -0,0 +1,9 @@
shader_type canvas_item;
uniform sampler2DArray u_texture_array;
uniform float u_index;
void fragment() {
vec4 col = texture(u_texture_array, vec3(UV.x, UV.y, u_index));
COLOR = vec4(col.a, col.a, col.a, 1.0);
}

View File

@@ -0,0 +1 @@
uid://btcvsvc4f2nhc

View File

@@ -0,0 +1,7 @@
shader_type canvas_item;
void fragment() {
// TODO Have an option to "undo" sRGB, for funzies?
vec4 col = texture(TEXTURE, UV);
COLOR = vec4(col.rgb, 1.0);
}

View File

@@ -0,0 +1 @@
uid://b1wwjr82d2o37

View File

@@ -0,0 +1,6 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/texture_editor/display_color.gdshader" type="Shader" id=1]
[resource]
shader = ExtResource( 1 )

View File

@@ -0,0 +1,9 @@
shader_type canvas_item;
uniform sampler2DArray u_texture_array;
uniform float u_index;
void fragment() {
vec4 col = texture(u_texture_array, vec3(UV.x, UV.y, u_index));
COLOR = vec4(col.rgb, 1.0);
}

View File

@@ -0,0 +1 @@
uid://npob3uj5dgkg

View File

@@ -0,0 +1,28 @@
shader_type canvas_item;
uniform float u_strength = 1.0;
uniform bool u_flip_y = false;
vec3 unpack_normal(vec4 rgba) {
vec3 n = rgba.xzy * 2.0 - vec3(1.0);
// Had to negate Z because it comes from Y in the normal map,
// and OpenGL-style normal maps are Y-up.
n.z *= -1.0;
return n;
}
vec3 pack_normal(vec3 n) {
n.z *= -1.0;
return 0.5 * (n.xzy + vec3(1.0));
}
void fragment() {
vec4 col = texture(TEXTURE, UV);
vec3 n = unpack_normal(col);
n = normalize(mix(n, vec3(-n.x, n.y, -n.z), 0.5 - 0.5 * u_strength));
if (u_flip_y) {
n.z = -n.z;
}
col.rgb = pack_normal(n);
COLOR = vec4(col.rgb, 1.0);
}

View File

@@ -0,0 +1 @@
uid://s7qinobxy30j

View File

@@ -0,0 +1,41 @@
@tool
extends Container
const SEPARATION = 2
func _notification(what: int):
if what == NOTIFICATION_SORT_CHILDREN:
_sort_children2()
# TODO Function with ugly name to workaround a Godot 3.1 issue
# See https://github.com/godotengine/godot/pull/38396
func _sort_children2():
var max_x := size.x - SEPARATION
var pos := Vector2(SEPARATION, SEPARATION)
var line_height := 0
for i in get_child_count():
var child_node = get_child(i)
if not child_node is Control:
continue
var child := child_node as Control
var rect := child.get_rect()
if rect.size.y > line_height:
line_height = rect.size.y
if pos.x + rect.size.x > max_x:
pos.x = SEPARATION
pos.y += line_height + SEPARATION
line_height = rect.size.y
rect.position = pos
fit_child_in_rect(child, rect)
pos.x += rect.size.x + SEPARATION
custom_minimum_size.y = pos.y + line_height

View File

@@ -0,0 +1 @@
uid://d2cf2epylmuo6

View File

@@ -0,0 +1,60 @@
@tool
extends Control
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
#const HT_EmptyTexture = preload("../../icons/empty.png")
const EMPTY_TEXTURE_PATH = "res://addons/zylann.hterrain/tools/icons/empty.png"
signal load_pressed
signal clear_pressed
@onready var _label : Label = $Label
@onready var _texture_rect : TextureRect = $TextureRect
@onready var _buttons = [
$LoadButton,
$ClearButton
]
var _material : Material
var _is_empty := true
func set_label(text: String):
_label.text = text
func set_texture(tex: Texture):
if tex == null:
_texture_rect.texture = load(EMPTY_TEXTURE_PATH)
_texture_rect.material = null
_is_empty = true
else:
_texture_rect.texture = tex
_texture_rect.material = _material
_is_empty = false
func set_texture_tooltip(msg: String):
_texture_rect.tooltip_text = msg
func _on_LoadButton_pressed():
load_pressed.emit()
func _on_ClearButton_pressed():
clear_pressed.emit()
func set_material(mat: Material):
_material = mat
if not _is_empty:
_texture_rect.material = _material
func set_enabled(enabled: bool):
for b in _buttons:
b.disabled = not enabled

View File

@@ -0,0 +1 @@
uid://de8wkty1iyg08

View File

@@ -0,0 +1,29 @@
[gd_scene load_steps=2 format=3 uid="uid://dqgaomu3tr1ym"]
[ext_resource type="Script" uid="uid://de8wkty1iyg08" path="res://addons/zylann.hterrain/tools/texture_editor/set_editor/source_file_item_editor.gd" id="2"]
[node name="SourceFileItem" type="VBoxContainer"]
offset_right = 128.0
offset_bottom = 194.0
script = ExtResource("2")
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Albedo"
[node name="TextureRect" type="TextureRect" parent="."]
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
expand_mode = 1
stretch_mode = 1
[node name="LoadButton" type="Button" parent="."]
layout_mode = 2
text = "Load..."
[node name="ClearButton" type="Button" parent="."]
layout_mode = 2
text = "Clear"
[connection signal="pressed" from="LoadButton" to="." method="_on_LoadButton_pressed"]
[connection signal="pressed" from="ClearButton" to="." method="_on_ClearButton_pressed"]

View File

@@ -0,0 +1,529 @@
@tool
extends AcceptDialog
const HTerrainTextureSet = preload("../../../hterrain_texture_set.gd")
const HT_EditorUtil = preload("../../util/editor_util.gd")
const HT_Util = preload("../../../util/util.gd")
const HT_Logger = preload("../../../util/logger.gd")
const HT_ColorShader = preload("../display_color.gdshader")
const HT_ColorSliceShader = preload("../display_color_slice.gdshader")
const HT_AlphaShader = preload("../display_alpha.gdshader")
const HT_AlphaSliceShader = preload("../display_alpha_slice.gdshader")
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
#const HT_EmptyTexture = preload("../../icons/empty.png")
const EMPTY_TEXTURE_PATH = "res://addons/zylann.hterrain/tools/icons/empty.png"
signal import_selected
@onready var _slots_list : ItemList = $VB/HS/VB/SlotsList
@onready var _albedo_preview : TextureRect = $VB/HS/VB2/GC/AlbedoPreview
@onready var _bump_preview : TextureRect = $VB/HS/VB2/GC/BumpPreview
@onready var _normal_preview : TextureRect = $VB/HS/VB2/GC/NormalPreview
@onready var _roughness_preview : TextureRect = $VB/HS/VB2/GC/RoughnessPreview
@onready var _load_albedo_button : Button = $VB/HS/VB2/GC/LoadAlbedo
@onready var _load_normal_button : Button = $VB/HS/VB2/GC/LoadNormal
@onready var _clear_albedo_button : Button = $VB/HS/VB2/GC/ClearAlbedo
@onready var _clear_normal_button : Button = $VB/HS/VB2/GC/ClearNormal
@onready var _mode_selector : OptionButton = $VB/HS/VB2/GC2/ModeSelector
@onready var _add_slot_button : Button = $VB/HS/VB/HB/AddSlot
@onready var _remove_slot_button : Button = $VB/HS/VB/HB/RemoveSlot
var _texture_set : HTerrainTextureSet
var _undo_redo_manager : EditorUndoRedoManager
var _mode_confirmation_dialog : ConfirmationDialog
var _delete_slot_confirmation_dialog : ConfirmationDialog
var _load_texture_dialog : ConfirmationDialog
var _load_texture_array_dialog : ConfirmationDialog
var _load_texture_type := -1
var _logger = HT_Logger.get_for(self)
func _init():
get_ok_button().hide()
func _ready():
if HT_Util.is_in_edited_scene(self):
return
for id in HTerrainTextureSet.MODE_COUNT:
var mode_name = HTerrainTextureSet.get_import_mode_name(id)
_mode_selector.add_item(mode_name, id)
func setup_dialogs(parent: Node):
var d = HT_EditorUtil.create_open_texture_dialog()
d.file_selected.connect(_on_LoadTextureDialog_file_selected)
_load_texture_dialog = d
add_child(d)
d = HT_EditorUtil.create_open_texture_array_dialog()
d.file_selected.connect(_on_LoadTextureArrayDialog_file_selected)
_load_texture_array_dialog = d
add_child(d)
d = ConfirmationDialog.new()
d.confirmed.connect(_on_ModeConfirmationDialog_confirmed)
# This is ridiculous.
# See https://github.com/godotengine/godot/issues/17460
# d.connect("modal_closed", self, "_on_ModeConfirmationDialog_cancelled")
# d.get_close_button().connect("pressed", self, "_on_ModeConfirmationDialog_cancelled")
# d.get_cancel().connect("pressed", self, "_on_ModeConfirmationDialog_cancelled")
_mode_confirmation_dialog = d
add_child(d)
func _notification(what: int):
if HT_Util.is_in_edited_scene(self):
return
if what == NOTIFICATION_EXIT_TREE:
# Have to check for null in all of them,
# because otherwise it breaks in the scene editor...
if _load_texture_dialog != null:
_load_texture_dialog.queue_free()
if _load_texture_array_dialog != null:
_load_texture_array_dialog.queue_free()
if what == NOTIFICATION_VISIBILITY_CHANGED:
if not visible:
# Cleanup referenced resources
set_texture_set(null)
func set_undo_redo(ur: EditorUndoRedoManager):
_undo_redo_manager = ur
func set_texture_set(texture_set: HTerrainTextureSet):
if _texture_set == texture_set:
return
if _texture_set != null:
_texture_set.changed.disconnect(_on_texture_set_changed)
_texture_set = texture_set
if _texture_set != null:
_texture_set.changed.connect(_on_texture_set_changed)
_update_ui_from_data()
func _on_texture_set_changed():
_update_ui_from_data()
func _update_ui_from_data():
var prev_selected_items = _slots_list.get_selected_items()
_slots_list.clear()
var slots_count := _texture_set.get_slots_count()
for slot_index in slots_count:
_slots_list.add_item("Texture {0}".format([slot_index]))
_set_selected_id(_mode_selector, _texture_set.get_mode())
if _slots_list.get_item_count() > 0:
if len(prev_selected_items) > 0:
var i : int = prev_selected_items[0]
if i >= _slots_list.get_item_count():
i = _slots_list.get_item_count() - 1
_select_slot(i)
else:
_select_slot(0)
else:
_clear_previews()
var max_slots := HTerrainTextureSet.get_max_slots_for_mode(_texture_set.get_mode())
_add_slot_button.disabled = slots_count >= max_slots
_remove_slot_button.disabled = slots_count == 0
var buttons := [
_load_albedo_button,
_load_normal_button,
_clear_albedo_button,
_clear_normal_button
]
if _texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES:
_add_slot_button.visible = true
_remove_slot_button.visible = true
_load_albedo_button.text = "Load..."
_load_normal_button.text = "Load..."
for b in buttons:
b.disabled = (slots_count == 0)
else:
_add_slot_button.visible = false
_remove_slot_button.visible = false
_load_albedo_button.text = "Load Array..."
_load_normal_button.text = "Load Array..."
for b in buttons:
b.disabled = false
static func _set_selected_id(ob: OptionButton, id: int):
for i in ob.get_item_count():
if ob.get_item_id(i) == id:
ob.selected = i
break
func select_slot(slot_index: int):
var count = _texture_set.get_slots_count()
if count > 0:
if slot_index >= count:
slot_index = count - 1
_select_slot(slot_index)
func _clear_previews():
var empty_texture : Texture2D = load(EMPTY_TEXTURE_PATH)
if empty_texture == null:
_logger.error(str("Failed to load empty texture ", EMPTY_TEXTURE_PATH))
_albedo_preview.texture = empty_texture
_bump_preview.texture = empty_texture
_normal_preview.texture = empty_texture
_roughness_preview.texture = empty_texture
_albedo_preview.tooltip_text = _get_resource_path_or_empty(null)
_bump_preview.tooltip_text = _get_resource_path_or_empty(null)
_normal_preview.tooltip_text = _get_resource_path_or_empty(null)
_roughness_preview.tooltip_text = _get_resource_path_or_empty(null)
func _select_slot(slot_index: int):
assert(slot_index >= 0)
assert(slot_index < _texture_set.get_slots_count())
var empty_texture : Texture2D = load(EMPTY_TEXTURE_PATH)
if empty_texture == null:
_logger.error(str("Failed to load empty texture ", EMPTY_TEXTURE_PATH))
if _texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES:
var albedo_tex := \
_texture_set.get_texture(slot_index, HTerrainTextureSet.TYPE_ALBEDO_BUMP)
var normal_tex := \
_texture_set.get_texture(slot_index, HTerrainTextureSet.TYPE_NORMAL_ROUGHNESS)
_albedo_preview.texture = albedo_tex if albedo_tex != null else empty_texture
_bump_preview.texture = albedo_tex if albedo_tex != null else empty_texture
_normal_preview.texture = normal_tex if normal_tex != null else empty_texture
_roughness_preview.texture = normal_tex if normal_tex != null else empty_texture
_albedo_preview.tooltip_text = _get_resource_path_or_empty(albedo_tex)
_bump_preview.tooltip_text = _get_resource_path_or_empty(albedo_tex)
_normal_preview.tooltip_text = _get_resource_path_or_empty(normal_tex)
_roughness_preview.tooltip_text = _get_resource_path_or_empty(normal_tex)
_albedo_preview.material.shader = HT_ColorShader
_bump_preview.material.shader = HT_AlphaShader
_normal_preview.material.shader = HT_ColorShader
_roughness_preview.material.shader = HT_AlphaShader
_albedo_preview.material.set_shader_parameter("u_texture_array", null)
_bump_preview.material.set_shader_parameter("u_texture_array", null)
_normal_preview.material.set_shader_parameter("u_texture_array", null)
_roughness_preview.material.set_shader_parameter("u_texture_array", null)
else:
var albedo_tex := _texture_set.get_texture_array(HTerrainTextureSet.TYPE_ALBEDO_BUMP)
var normal_tex := _texture_set.get_texture_array(HTerrainTextureSet.TYPE_NORMAL_ROUGHNESS)
_albedo_preview.texture = empty_texture
_bump_preview.texture = empty_texture
_normal_preview.texture = empty_texture
_roughness_preview.texture = empty_texture
_albedo_preview.tooltip_text = _get_resource_path_or_empty(albedo_tex)
_bump_preview.tooltip_text = _get_resource_path_or_empty(albedo_tex)
_normal_preview.tooltip_text = _get_resource_path_or_empty(normal_tex)
_roughness_preview.tooltip_text = _get_resource_path_or_empty(normal_tex)
_albedo_preview.material.shader = HT_ColorSliceShader
_bump_preview.material.shader = HT_AlphaSliceShader
_normal_preview.material.shader = \
HT_ColorSliceShader if normal_tex != null else HT_ColorShader
_roughness_preview.material.shader = \
HT_AlphaSliceShader if normal_tex != null else HT_AlphaShader
_albedo_preview.material.set_shader_parameter("u_texture_array", albedo_tex)
_bump_preview.material.set_shader_parameter("u_texture_array", albedo_tex)
_normal_preview.material.set_shader_parameter("u_texture_array", normal_tex)
_roughness_preview.material.set_shader_parameter("u_texture_array", normal_tex)
_albedo_preview.material.set_shader_parameter("u_index", slot_index)
_bump_preview.material.set_shader_parameter("u_index", slot_index)
_normal_preview.material.set_shader_parameter("u_index", slot_index)
_roughness_preview.material.set_shader_parameter("u_index", slot_index)
_slots_list.select(slot_index)
static func _get_resource_path_or_empty(res: Resource) -> String:
if res != null:
return res.resource_path
return "<empty>"
func _on_ImportButton_pressed():
import_selected.emit()
func _on_CloseButton_pressed():
hide()
func _get_undo_redo_for_texture_set() -> UndoRedo:
return _undo_redo_manager.get_history_undo_redo(
_undo_redo_manager.get_object_history_id(_texture_set))
func _on_AddSlot_pressed():
assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES)
var slot_index = _texture_set.get_slots_count()
var ur := _get_undo_redo_for_texture_set()
ur.create_action("HTerrainTextureSet: add slot")
ur.add_do_method(_texture_set.insert_slot.bind(-1))
ur.add_undo_method(_texture_set.remove_slot.bind(slot_index))
ur.commit_action()
func _on_RemoveSlot_pressed():
assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES)
var slot_index = _slots_list.get_selected_items()[0]
var textures = []
for type in HTerrainTextureSet.TYPE_COUNT:
textures.append(_texture_set.get_texture(slot_index, type))
var ur := _get_undo_redo_for_texture_set()
ur.create_action("HTerrainTextureSet: remove slot")
ur.add_do_method(_texture_set.remove_slot.bind(slot_index))
ur.add_undo_method(_texture_set.insert_slot.bind(slot_index))
for type in len(textures):
var texture = textures[type]
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if texture == null:
ur.add_undo_method(_texture_set.set_texture_null.bind(slot_index, type))
else:
ur.add_undo_method(_texture_set.set_texture.bind(slot_index, type, texture))
ur.commit_action()
func _on_SlotsList_item_selected(index: int):
_select_slot(index)
func _open_load_texture_dialog(type: int):
_load_texture_type = type
if _texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES:
_load_texture_dialog.popup_centered_ratio()
else:
_load_texture_array_dialog.popup_centered_ratio()
func _on_LoadAlbedo_pressed():
_open_load_texture_dialog(HTerrainTextureSet.TYPE_ALBEDO_BUMP)
func _on_LoadNormal_pressed():
_open_load_texture_dialog(HTerrainTextureSet.TYPE_NORMAL_ROUGHNESS)
func _set_texture_action(slot_index: int, texture: Texture, type: int):
var prev_texture = _texture_set.get_texture(slot_index, type)
var ur := _get_undo_redo_for_texture_set()
ur.create_action("HTerrainTextureSet: load texture")
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if texture == null:
ur.add_do_method(_texture_set.set_texture_null.bind(slot_index, type))
else:
ur.add_do_method(_texture_set.set_texture.bind(slot_index, type, texture))
ur.add_do_method(self._select_slot.bind(slot_index))
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if prev_texture == null:
ur.add_undo_method(_texture_set.set_texture_null.bind(slot_index, type))
else:
ur.add_undo_method(_texture_set.set_texture.bind(slot_index, type, prev_texture))
ur.add_undo_method(self._select_slot.bind(slot_index))
ur.commit_action()
func _set_texture_array_action(slot_index: int, texture_array: TextureLayered, type: int):
var prev_texture_array = _texture_set.get_texture_array(type)
var ur := _get_undo_redo_for_texture_set()
ur.create_action("HTerrainTextureSet: load texture array")
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if texture_array == null:
ur.add_do_method(_texture_set.set_texture_array_null.bind(type))
# Can't select a slot after this because there won't be any after the array is removed
else:
ur.add_do_method(_texture_set.set_texture_array.bind(type, texture_array))
ur.add_do_method(self._select_slot.bind(slot_index))
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if prev_texture_array == null:
ur.add_undo_method(_texture_set.set_texture_array_null.bind(type))
# Can't select a slot after this because there won't be any after the array is removed
else:
ur.add_undo_method(_texture_set.set_texture_array.bind(type, prev_texture_array))
ur.add_undo_method(self._select_slot.bind(slot_index))
ur.commit_action()
func _on_LoadTextureDialog_file_selected(fpath: String):
assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES)
var texture = load(fpath)
assert(texture != null)
var slot_index : int = _slots_list.get_selected_items()[0]
_set_texture_action(slot_index, texture, _load_texture_type)
func _on_LoadTextureArrayDialog_file_selected(fpath: String):
assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURE_ARRAYS)
var texture_array = load(fpath)
assert(texture_array != null)
# It's possible no slot exists at the moment,
# because there could be no texture array already set.
# The number of slots in the new array might also be different.
# So in this case we'll default to selecting the first slot.
var slot_index := 0
_set_texture_array_action(slot_index, texture_array, _load_texture_type)
func _on_ClearAlbedo_pressed():
var slot_index : int = _slots_list.get_selected_items()[0]
if _texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES:
_set_texture_action(slot_index, null, HTerrainTextureSet.TYPE_ALBEDO_BUMP)
else:
_set_texture_array_action(slot_index, null, HTerrainTextureSet.TYPE_ALBEDO_BUMP)
func _on_ClearNormal_pressed():
var slot_index : int = _slots_list.get_selected_items()[0]
if _texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES:
_set_texture_action(slot_index, null, HTerrainTextureSet.TYPE_NORMAL_ROUGHNESS)
else:
_set_texture_array_action(slot_index, null, HTerrainTextureSet.TYPE_NORMAL_ROUGHNESS)
func _on_ModeSelector_item_selected(index: int):
var id = _mode_selector.get_selected_id()
if id == _texture_set.get_mode():
return
# Early-cancel the change in OptionButton, so we won't need to rely on
# the (inexistent) cancel signal from ConfirmationDialog
_set_selected_id(_mode_selector, _texture_set.get_mode())
if not _texture_set.has_any_textures():
_switch_mode_action()
else:
if _texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES:
_mode_confirmation_dialog.window_title = "Switch to TextureArrays"
_mode_confirmation_dialog.dialog_text = \
"This will unload all textures currently setup. Do you want to continue?"
_mode_confirmation_dialog.popup_centered()
else:
_mode_confirmation_dialog.window_title = "Switch to Textures"
_mode_confirmation_dialog.dialog_text = \
"This will unload all textures currently setup. Do you want to continue?"
_mode_confirmation_dialog.popup_centered()
func _on_ModeConfirmationDialog_confirmed():
_switch_mode_action()
func _switch_mode_action():
var mode := _texture_set.get_mode()
var ur := _get_undo_redo_for_texture_set()
if mode == HTerrainTextureSet.MODE_TEXTURES:
ur.create_action("HTerrainTextureSet: switch to TextureArrays")
ur.add_do_method(_texture_set.set_mode.bind(HTerrainTextureSet.MODE_TEXTURE_ARRAYS))
backup_for_undo(_texture_set, ur)
else:
ur.create_action("HTerrainTextureSet: switch to Textures")
ur.add_do_method(_texture_set.set_mode.bind(HTerrainTextureSet.MODE_TEXTURES))
backup_for_undo(_texture_set, ur)
ur.commit_action()
static func backup_for_undo(texture_set: HTerrainTextureSet, ur: UndoRedo):
var mode := texture_set.get_mode()
ur.add_undo_method(texture_set.clear)
ur.add_undo_method(texture_set.set_mode.bind(mode))
if mode == HTerrainTextureSet.MODE_TEXTURES:
# Backup slots
var slot_count := texture_set.get_slots_count()
var type_textures := []
for type in HTerrainTextureSet.TYPE_COUNT:
var textures := []
for slot_index in slot_count:
textures.append(texture_set.get_texture(slot_index, type))
type_textures.append(textures)
for type in len(type_textures):
var textures = type_textures[type]
for slot_index in len(textures):
ur.add_undo_method(texture_set.insert_slot.bind(slot_index))
var texture = textures[slot_index]
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if texture == null:
ur.add_undo_method(texture_set.set_texture_null.bind(slot_index, type))
else:
ur.add_undo_method(texture_set.set_texture.bind(slot_index, type, texture))
else:
# Backup slots
var type_textures := []
for type in HTerrainTextureSet.TYPE_COUNT:
type_textures.append(texture_set.get_texture_array(type))
for type in len(type_textures):
var texture_array = type_textures[type]
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if texture_array == null:
ur.add_undo_method(texture_set.set_texture_array_null.bind(type))
else:
ur.add_undo_method(texture_set.set_texture_array.bind(type, texture_array))
#func _on_ModeConfirmationDialog_cancelled():
# print("Cancelled")
# _set_selected_id(_mode_selector, _texture_set.get_mode())

View File

@@ -0,0 +1 @@
uid://cfjw5kcur7ns6

View File

@@ -0,0 +1,194 @@
[gd_scene load_steps=9 format=3 uid="uid://c0e7ifnoygvr6"]
[ext_resource type="Script" uid="uid://cfjw5kcur7ns6" path="res://addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_editor.gd" id="1"]
[ext_resource type="Shader" uid="uid://6bb66fu1fw8n" path="res://addons/zylann.hterrain/tools/texture_editor/display_alpha.gdshader" id="2"]
[ext_resource type="Shader" uid="uid://b1wwjr82d2o37" path="res://addons/zylann.hterrain/tools/texture_editor/display_color.gdshader" id="3"]
[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" id="5"]
[sub_resource type="ShaderMaterial" id="1"]
shader = ExtResource("3")
[sub_resource type="ShaderMaterial" id="2"]
shader = ExtResource("2")
[sub_resource type="ShaderMaterial" id="3"]
shader = ExtResource("3")
[sub_resource type="ShaderMaterial" id="4"]
shader = ExtResource("2")
[node name="TextureSetEditor" type="AcceptDialog"]
title = "TextureSet Editor"
size = Vector2i(666, 341)
min_size = Vector2i(652, 320)
script = ExtResource("1")
[node name="VB" type="VBoxContainer" 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 = -18.0
[node name="HS" type="HSplitContainer" parent="VB"]
layout_mode = 2
size_flags_vertical = 3
[node name="VB" type="VBoxContainer" parent="VB/HS"]
layout_mode = 2
[node name="SlotsList" type="ItemList" parent="VB/HS/VB"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_vertical = 3
[node name="HB" type="HBoxContainer" parent="VB/HS/VB"]
layout_mode = 2
[node name="AddSlot" type="Button" parent="VB/HS/VB/HB"]
layout_mode = 2
text = "+"
[node name="Control" type="Control" parent="VB/HS/VB/HB"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RemoveSlot" type="Button" parent="VB/HS/VB/HB"]
layout_mode = 2
text = "-"
[node name="VB2" type="VBoxContainer" parent="VB/HS"]
layout_mode = 2
[node name="GC" type="GridContainer" parent="VB/HS/VB2"]
layout_mode = 2
columns = 4
[node name="AlbedoLabel" type="Label" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "Albedo"
[node name="AlbedoExtraLabel" type="Label" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "+ alpha bump"
[node name="NormalLabel" type="Label" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "Normal"
[node name="NormalExtraLabel" type="Label" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "+ alpha roughness"
[node name="AlbedoPreview" type="TextureRect" parent="VB/HS/VB2/GC"]
material = SubResource("1")
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
expand_mode = 1
stretch_mode = 1
[node name="BumpPreview" type="TextureRect" parent="VB/HS/VB2/GC"]
material = SubResource("2")
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
expand_mode = 1
stretch_mode = 1
[node name="NormalPreview" type="TextureRect" parent="VB/HS/VB2/GC"]
material = SubResource("3")
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
expand_mode = 1
stretch_mode = 1
[node name="RoughnessPreview" type="TextureRect" parent="VB/HS/VB2/GC"]
material = SubResource("4")
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
expand_mode = 1
stretch_mode = 1
[node name="LoadAlbedo" type="Button" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "Load..."
[node name="Spacer" type="Control" parent="VB/HS/VB2/GC"]
layout_mode = 2
[node name="LoadNormal" type="Button" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "Load..."
[node name="Spacer2" type="Control" parent="VB/HS/VB2/GC"]
layout_mode = 2
[node name="ClearAlbedo" type="Button" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "Clear"
[node name="Spacer3" type="Control" parent="VB/HS/VB2/GC"]
layout_mode = 2
[node name="ClearNormal" type="Button" parent="VB/HS/VB2/GC"]
layout_mode = 2
text = "Clear"
[node name="HSeparator" type="Control" parent="VB/HS/VB2"]
custom_minimum_size = Vector2(0, 4)
layout_mode = 2
[node name="GC2" type="HBoxContainer" parent="VB/HS/VB2"]
layout_mode = 2
[node name="Label" type="Label" parent="VB/HS/VB2/GC2"]
layout_mode = 2
text = "Mode"
[node name="ModeSelector" type="OptionButton" parent="VB/HS/VB2/GC2"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Spacer" type="Control" parent="VB/HS/VB2/GC2"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Spacer" type="Control" parent="VB"]
custom_minimum_size = Vector2(0, 4)
layout_mode = 2
[node name="HB" type="HBoxContainer" parent="VB"]
layout_mode = 2
alignment = 1
[node name="ImportButton" type="Button" parent="VB/HB"]
layout_mode = 2
text = "Import..."
[node name="CloseButton" type="Button" parent="VB/HB"]
layout_mode = 2
text = "Close"
[node name="Spacer2" type="Control" parent="VB"]
custom_minimum_size = Vector2(0, 2)
layout_mode = 2
[node name="DialogFitter" parent="." instance=ExtResource("5")]
layout_mode = 3
anchors_preset = 0
offset_left = 8.0
offset_top = 8.0
offset_right = 658.0
offset_bottom = 323.0
[connection signal="item_selected" from="VB/HS/VB/SlotsList" to="." method="_on_SlotsList_item_selected"]
[connection signal="pressed" from="VB/HS/VB/HB/AddSlot" to="." method="_on_AddSlot_pressed"]
[connection signal="pressed" from="VB/HS/VB/HB/RemoveSlot" to="." method="_on_RemoveSlot_pressed"]
[connection signal="pressed" from="VB/HS/VB2/GC/LoadAlbedo" to="." method="_on_LoadAlbedo_pressed"]
[connection signal="pressed" from="VB/HS/VB2/GC/LoadNormal" to="." method="_on_LoadNormal_pressed"]
[connection signal="pressed" from="VB/HS/VB2/GC/ClearAlbedo" to="." method="_on_ClearAlbedo_pressed"]
[connection signal="pressed" from="VB/HS/VB2/GC/ClearNormal" to="." method="_on_ClearNormal_pressed"]
[connection signal="item_selected" from="VB/HS/VB2/GC2/ModeSelector" to="." method="_on_ModeSelector_item_selected"]
[connection signal="pressed" from="VB/HB/ImportButton" to="." method="_on_ImportButton_pressed"]
[connection signal="pressed" from="VB/HB/CloseButton" to="." method="_on_CloseButton_pressed"]

View File

@@ -0,0 +1,920 @@
@tool
extends AcceptDialog
const HTerrainTextureSet = preload("../../../hterrain_texture_set.gd")
const HT_Logger = preload("../../../util/logger.gd")
const HT_EditorUtil = preload("../../util/editor_util.gd")
const HT_Errors = preload("../../../util/errors.gd")
const HT_TextureSetEditor = preload("./texture_set_editor.gd")
const HT_Result = preload("../../util/result.gd")
const HT_Util = preload("../../../util/util.gd")
const HT_PackedTextureUtil = preload("../../packed_textures/packed_texture_util.gd")
const ResourceImporterTexture_Unexposed = preload("../../util/resource_importer_texture.gd")
const ResourceImporterTextureLayered_Unexposed = preload(
"../../util/resource_importer_texture_layered.gd")
const HT_NormalMapPreviewShader = preload("../display_normal.gdshader")
const COMPRESS_RAW = 0
const COMPRESS_LOSSLESS = 1
const COMPRESS_LOSSY = 1
const COMPRESS_VRAM = 2
const COMPRESS_COUNT = 3
const _compress_names = ["Raw", "Lossless", "Lossy", "VRAM"]
# Indexed by HTerrainTextureSet.SRC_TYPE_* constants
const _smart_pick_file_keywords = [
["albedo", "color", "col", "diffuse"],
["bump", "height", "depth", "displacement", "disp"],
["normal", "norm", "nrm"],
["roughness", "rough", "rgh"]
]
signal import_finished
@onready var _texture_editors = [
$Import/HS/VB2/HB/Albedo,
$Import/HS/VB2/HB/Bump,
$Import/HS/VB2/HB/Normal,
$Import/HS/VB2/HB/Roughness
]
@onready var _slots_list : ItemList = $Import/HS/VB/SlotsList
# TODO Some shortcuts to import options were disabled in the GUI because of Godot issues.
# If users want to customize that, they need to do it on the files directly.
#
# There is no script API in Godot to choose the import settings of a generated file.
# They always start with the defaults, and the only implemented case is for the import dock.
# It appeared possible to reverse-engineer and write a .import file as done in HTerrainData,
# however when I tried this with custom importers, Godot stopped importing after scan(),
# and the resources could not load. However, selecting them each and clicking "Reimport"
# did import them fine. Unfortunately, this short-circuits the workflow.
# Since I have no idea what's going on with this reverse-engineering, I had to drop those options.
# Godot needs an API to import specific files and choose settings before the first import.
#
# Godot 4: now we'll really need it, let's enable and we'll see if it works
# when we can test the workflow...
const _WRITE_IMPORT_FILES = true
@onready var _import_mode_selector : OptionButton = $Import/GC/ImportModeSelector
@onready var _compression_selector : OptionButton = $Import/GC/CompressionSelector
@onready var _resolution_spinbox : SpinBox = $Import/GC/ResolutionSpinBox
@onready var _mipmaps_checkbox : CheckBox = $Import/GC/MipmapsCheckbox
@onready var _add_slot_button : Button = $Import/HS/VB/HB/AddSlotButton
@onready var _remove_slot_button : Button = $Import/HS/VB/HB/RemoveSlotButton
@onready var _import_directory_line_edit : LineEdit = $Import/HB2/ImportDirectoryLineEdit
@onready var _normalmap_flip_checkbox : CheckBox = $Import/HS/VB2/HB/Normal/NormalMapFlipY
var _texture_set : HTerrainTextureSet
var _undo_redo_manager : EditorUndoRedoManager
var _logger = HT_Logger.get_for(self)
# This is normally an `EditorFileDialog`. I can't type-hint this one properly,
# because when I test this UI in isolation, I can't use `EditorFileDialog`.
var _load_texture_dialog : ConfirmationDialog
var _load_texture_type : int = -1
var _error_popup : AcceptDialog
var _info_popup : AcceptDialog
var _delete_confirmation_popup : ConfirmationDialog
var _open_dir_dialog : ConfirmationDialog
var _editor_file_system : EditorFileSystem
var _normalmap_material : ShaderMaterial
var _import_mode := HTerrainTextureSet.MODE_TEXTURES
class HT_TextureSetImportEditorSlot:
# Array of strings.
# Can be either path to images, hexadecimal colors starting with #, or empty string for "null".
var texture_paths := []
var flip_normalmap_y := false
func _init():
for i in HTerrainTextureSet.SRC_TYPE_COUNT:
texture_paths.append("")
# Array of HT_TextureSetImportEditorSlot
var _slots_data := []
var _import_settings := {
"mipmaps": true,
"compression": COMPRESS_VRAM,
"resolution": 512
}
func _init():
get_ok_button().hide()
# Default data
_slots_data.clear()
for i in 4:
_slots_data.append(HT_TextureSetImportEditorSlot.new())
func _ready():
if HT_Util.is_in_edited_scene(self):
return
for src_type in len(_texture_editors):
var ed = _texture_editors[src_type]
var typename = HTerrainTextureSet.get_source_texture_type_name(src_type)
ed.set_label(typename.capitalize())
ed.load_pressed.connect(_on_texture_load_pressed.bind(src_type))
ed.clear_pressed.connect(_on_texture_clear_pressed.bind(src_type))
for import_mode in HTerrainTextureSet.MODE_COUNT:
var n = HTerrainTextureSet.get_import_mode_name(import_mode)
_import_mode_selector.add_item(n, import_mode)
for compress_mode in COMPRESS_COUNT:
var n = _compress_names[compress_mode]
_compression_selector.add_item(n, compress_mode)
_normalmap_material = ShaderMaterial.new()
_normalmap_material.shader = HT_NormalMapPreviewShader
_texture_editors[HTerrainTextureSet.SRC_TYPE_NORMAL].set_material(_normalmap_material)
func setup_dialogs(parent: Node):
var d = HT_EditorUtil.create_open_image_dialog()
d.file_selected.connect(_on_LoadTextureDialog_file_selected)
_load_texture_dialog = d
add_child(d)
d = AcceptDialog.new()
d.title = "Import error"
_error_popup = d
add_child(_error_popup)
d = AcceptDialog.new()
d.title = "Info"
_info_popup = d
add_child(_info_popup)
d = ConfirmationDialog.new()
d.confirmed.connect(_on_delete_confirmation_popup_confirmed)
_delete_confirmation_popup = d
add_child(_delete_confirmation_popup)
d = HT_EditorUtil.create_open_dir_dialog()
d.title = "Choose import directory"
d.dir_selected.connect(_on_OpenDirDialog_dir_selected)
_open_dir_dialog = d
add_child(_open_dir_dialog)
_update_ui_from_data()
func _notification(what: int):
if what == NOTIFICATION_EXIT_TREE:
# Have to check for null in all of them,
# because otherwise it breaks in the scene editor...
if _load_texture_dialog != null:
_load_texture_dialog.queue_free()
if _error_popup != null:
_error_popup.queue_free()
if _delete_confirmation_popup != null:
_delete_confirmation_popup.queue_free()
if _open_dir_dialog != null:
_open_dir_dialog.queue_free()
if _info_popup != null:
_info_popup.queue_free()
# TODO Is it still necessary for an import tab?
func set_undo_redo(ur: EditorUndoRedoManager):
_undo_redo_manager = ur
func set_editor_file_system(efs: EditorFileSystem):
_editor_file_system = efs
func set_texture_set(texture_set: HTerrainTextureSet):
if _texture_set == texture_set:
# TODO What if the set was actually modified since?
return
_texture_set = texture_set
_slots_data.clear()
if _texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURES:
var slots_count = _texture_set.get_slots_count()
for slot_index in slots_count:
var slot := HT_TextureSetImportEditorSlot.new()
for type in HTerrainTextureSet.TYPE_COUNT:
var texture = _texture_set.get_texture(slot_index, type)
if texture == null or texture.resource_path == "":
continue
if not texture.resource_path.ends_with(".packed_tex"):
continue
var import_data := _parse_json_file(texture.resource_path)
if import_data.is_empty() or not import_data.has("src"):
continue
var src_types = HTerrainTextureSet.get_src_types_from_type(type)
var src_data = import_data["src"]
if src_data.has("rgb"):
slot.texture_paths[src_types[0]] = src_data["rgb"]
if src_data.has("a"):
slot.texture_paths[src_types[1]] = src_data["a"]
_slots_data.append(slot)
else:
var slots_count := _texture_set.get_slots_count()
for type in HTerrainTextureSet.TYPE_COUNT:
var texture_array := _texture_set.get_texture_array(type)
if texture_array == null or texture_array.resource_path == "":
continue
if not texture_array.resource_path.ends_with(".packed_texarr"):
continue
var import_data := _parse_json_file(texture_array.resource_path)
if import_data.is_empty() or not import_data.has("layers"):
continue
var layers_data = import_data["layers"]
for slot_index in len(layers_data):
var src_data = layers_data[slot_index]
var src_types = HTerrainTextureSet.get_src_types_from_type(type)
while slot_index >= len(_slots_data):
var slot = HT_TextureSetImportEditorSlot.new()
_slots_data.append(slot)
var slot : HT_TextureSetImportEditorSlot = _slots_data[slot_index]
if src_data.has("rgb"):
slot.texture_paths[src_types[0]] = src_data["rgb"]
if src_data.has("a"):
slot.texture_paths[src_types[1]] = src_data["a"]
# TODO If the set doesn't have a file, use terrain path by default?
if texture_set.resource_path != "":
var dir = texture_set.resource_path.get_base_dir()
_import_directory_line_edit.text = dir
_update_ui_from_data()
func _parse_json_file(fpath: String) -> Dictionary:
var f := FileAccess.open(fpath, FileAccess.READ)
if f == null:
var err := FileAccess.get_open_error()
_logger.error("Could not load {0}: {1}".format([fpath, HT_Errors.get_message(err)]))
return {}
var json_text := f.get_as_text()
var json := JSON.new()
var json_err := json.parse(json_text)
if json_err != OK:
_logger.error("Failed to parse {0}: {1}".format([fpath, json.get_error_message()]))
return {}
return json.data
func _update_ui_from_data():
var prev_selected_items := _slots_list.get_selected_items()
_slots_list.clear()
for slot_index in len(_slots_data):
_slots_list.add_item("Texture {0}".format([slot_index]))
_resolution_spinbox.value = _import_settings.resolution
_mipmaps_checkbox.button_pressed = _import_settings.mipmaps
_set_selected_id(_compression_selector, _import_settings.compression)
_set_selected_id(_import_mode_selector, _import_mode)
var has_slots : bool = _slots_list.get_item_count() > 0
for ed in _texture_editors:
ed.set_enabled(has_slots)
_normalmap_flip_checkbox.disabled = not has_slots
if has_slots:
if len(prev_selected_items) > 0:
var i : int = prev_selected_items[0]
if i >= _slots_list.get_item_count():
i = _slots_list.get_item_count() - 1
_select_slot(i)
else:
_select_slot(0)
else:
for type in HTerrainTextureSet.SRC_TYPE_COUNT:
_set_ui_slot_texture_from_path("", type)
var max_slots := HTerrainTextureSet.get_max_slots_for_mode(_import_mode)
_add_slot_button.disabled = (len(_slots_data) >= max_slots)
_remove_slot_button.disabled = (len(_slots_data) == 0)
static func _set_selected_id(ob: OptionButton, id: int):
for i in ob.get_item_count():
if ob.get_item_id(i) == id:
ob.selected = i
break
func _select_slot(slot_index: int):
assert(slot_index >= 0)
assert(slot_index < len(_slots_data))
var slot = _slots_data[slot_index]
for type in HTerrainTextureSet.SRC_TYPE_COUNT:
var im_path : String = slot.texture_paths[type]
_set_ui_slot_texture_from_path(im_path, type)
_slots_list.select(slot_index)
_normalmap_flip_checkbox.button_pressed = slot.flip_normalmap_y
_normalmap_material.set_shader_parameter("u_flip_y", slot.flip_normalmap_y)
func _set_ui_slot_texture_from_path(im_path: String, type: int):
var ed = _texture_editors[type]
if im_path == "":
ed.set_texture(null)
ed.set_texture_tooltip("<empty>")
return
var im : Image
if im_path.begins_with("#") and im_path.find(".") == -1:
# The path is actually a preset for a uniform color.
# This is a feature of packed texture descriptor files.
# Make a small placeholder image.
var color := Color(im_path)
im = Image.create(4, 4, false, Image.FORMAT_RGBA8)
im.fill(color)
else:
# Regular path
im = Image.new()
var err := im.load(im_path)
if err != OK:
_logger.error(str("Unable to load image from ", im_path))
# TODO Different icon for images that can't load?
ed.set_texture(null)
ed.set_texture_tooltip("<empty>")
return
var tex := ImageTexture.create_from_image(im)
ed.set_texture(tex)
ed.set_texture_tooltip(im_path)
func _set_source_image(fpath: String, type: int):
_set_ui_slot_texture_from_path(fpath, type)
var slot_index : int = _slots_list.get_selected_items()[0]
#var prev_path = _texture_set.get_source_image_path(slot_index, type)
var slot : HT_TextureSetImportEditorSlot = _slots_data[slot_index]
slot.texture_paths[type] = fpath
func _set_import_property(key: String, value):
var prev_value = _import_settings[key]
# This is needed, notably because CheckBox emits a signal too when we set it from code...
if prev_value == value:
return
_import_settings[key] = value
func _on_texture_load_pressed(type: int):
_load_texture_type = type
_load_texture_dialog.popup_centered_ratio()
func _on_LoadTextureDialog_file_selected(fpath: String):
_set_source_image(fpath, _load_texture_type)
if _load_texture_type == HTerrainTextureSet.SRC_TYPE_ALBEDO:
_smart_pick_files(fpath)
# Attempts to load source images of other types by looking at how the albedo file was named
func _smart_pick_files(albedo_fpath: String):
var albedo_words = _smart_pick_file_keywords[HTerrainTextureSet.SRC_TYPE_ALBEDO]
var albedo_fname := albedo_fpath.get_file()
var albedo_fname_lower = albedo_fname.to_lower()
var fname_pattern = ""
for albedo_word in albedo_words:
var i = albedo_fname_lower.find(albedo_word, 0)
if i != -1:
fname_pattern = \
albedo_fname.substr(0, i) + "{0}" + albedo_fname.substr(i + len(albedo_word))
break
if fname_pattern == "":
return
var dirpath := albedo_fpath.get_base_dir()
var fnames := _get_files_in_directory(dirpath, _logger)
var types := [
HTerrainTextureSet.SRC_TYPE_BUMP,
HTerrainTextureSet.SRC_TYPE_NORMAL,
HTerrainTextureSet.SRC_TYPE_ROUGHNESS
]
var slot_index : int = _slots_list.get_selected_items()[0]
for type in types:
var slot = _slots_data[slot_index]
if slot.texture_paths[type] != "":
# Already set, don't overwrite unwantedly
continue
var keywords = _smart_pick_file_keywords[type]
for key in keywords:
var expected_fname = fname_pattern.format([key])
var found := false
for i in len(fnames):
var fname : String = fnames[i]
# TODO We should probably ignore extensions?
if fname.to_lower() == expected_fname.to_lower():
var fpath = dirpath.path_join(fname)
_set_source_image(fpath, type)
found = true
break
if found:
break
static func _get_files_in_directory(dirpath: String, logger) -> Array:
var dir := DirAccess.open(dirpath)
var err := DirAccess.get_open_error()
if err != OK:
logger.error("Could not open directory {0}: {1}" \
.format([dirpath, HT_Errors.get_message(err)]))
return []
dir.include_hidden = false
dir.include_navigational = false
err = dir.list_dir_begin()
if err != OK:
logger.error("Could not probe directory {0}: {1}" \
.format([dirpath, HT_Errors.get_message(err)]))
return []
var files := []
var fname := dir.get_next()
while fname != "":
if not dir.current_is_dir():
files.append(fname)
fname = dir.get_next()
return files
func _on_texture_clear_pressed(type: int):
_set_source_image("", type)
func _on_SlotsList_item_selected(index: int):
_select_slot(index)
func _on_ImportModeSelector_item_selected(index: int):
var mode : int = _import_mode_selector.get_item_id(index)
if mode != _import_mode:
#_set_import_property("mode", mode)
_import_mode = mode
_update_ui_from_data()
func _on_CompressionSelector_item_selected(index: int):
var compression : int = _compression_selector.get_item_id(index)
_set_import_property("compression", compression)
func _on_MipmapsCheckbox_toggled(button_pressed: bool):
_set_import_property("mipmaps", button_pressed)
func _on_ResolutionSpinBox_value_changed(value):
_set_import_property("resolution", int(value))
func _on_TextureArrayPrefixLineEdit_text_changed(new_text: String):
_set_import_property("output_prefix", new_text)
func _on_AddSlotButton_pressed():
var i := len(_slots_data)
_slots_data.append(HT_TextureSetImportEditorSlot.new())
_update_ui_from_data()
_select_slot(i)
func _on_RemoveSlotButton_pressed():
if _slots_list.get_item_count() == 0:
return
var selected_item = _slots_list.get_selected_items()[0]
_delete_confirmation_popup.title = "Delete slot {0}".format([selected_item])
_delete_confirmation_popup.dialog_text = "Delete import slot {0}?".format([selected_item])
_delete_confirmation_popup.popup_centered()
func _on_delete_confirmation_popup_confirmed():
var selected_item : int = _slots_list.get_selected_items()[0]
_slots_data.remove_at(selected_item)
_update_ui_from_data()
func _on_CancelButton_pressed():
hide()
func _on_BrowseImportDirectory_pressed():
_open_dir_dialog.popup_centered_ratio()
func _on_ImportDirectoryLineEdit_text_changed(new_text: String):
pass
func _on_OpenDirDialog_dir_selected(dir_path: String):
_import_directory_line_edit.text = dir_path
func _show_error(message: String):
_error_popup.dialog_text = message
_error_popup.popup_centered()
func _on_NormalMapFlipY_toggled(button_pressed: bool):
var slot_index : int = _slots_list.get_selected_items()[0]
var slot : HT_TextureSetImportEditorSlot = _slots_data[slot_index]
slot.flip_normalmap_y = button_pressed
_normalmap_material.set_shader_parameter("u_flip_y", slot.flip_normalmap_y)
# class ButtonDisabler:
# var _button : Button
# func _init(b: Button):
# _button = b
# _button.disabled = true
# func _notification(what: int):
# if what == NOTIFICATION_PREDELETE:
# _button.disabled = false
func _get_undo_redo_for_texture_set() -> UndoRedo:
return _undo_redo_manager.get_history_undo_redo(
_undo_redo_manager.get_object_history_id(_texture_set))
func _on_ImportButton_pressed():
if _texture_set == null:
_show_error("No HTerrainTextureSet selected.")
return
var import_dir := _import_directory_line_edit.text.strip_edges()
var prefix := ""
if _texture_set.resource_path != "":
prefix = _texture_set.resource_path.get_file().get_basename() + "_"
var files_data_result : HT_Result
if _import_mode == HTerrainTextureSet.MODE_TEXTURES:
files_data_result = _generate_packed_images(import_dir, prefix)
else:
files_data_result = _generate_packed_texarray_images(import_dir, prefix)
if not files_data_result.success:
_show_error(files_data_result.get_message())
return
var files_data : Array = files_data_result.value
if len(files_data) == 0:
_show_error("There are no files to save.\nYou must setup at least one slot of textures.")
return
for fd in files_data:
var dir_path : String = fd.path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir_path):
_show_error("The directory {0} could not be found.".format([dir_path]))
return
if _WRITE_IMPORT_FILES:
for fd in files_data:
var import_fpath = fd.path + ".import"
if not HT_Util.write_import_file(fd.import_file_data, import_fpath, _logger):
_show_error("Failed to write file {0}: {1}".format([import_fpath]))
return
if _editor_file_system == null:
_show_error("EditorFileSystem is not setup, can't trigger import system.")
return
# ______
# .-" "-.
# / \
# _ | | _
# ( \ |, .-. .-. ,| / )
# > "=._ | )(__/ \__)( | _.=" <
# (_/"=._"=._ |/ /\ \| _.="_.="\_)
# "=._ (_ ^^ _)"_.="
# "=\__|IIIIII|__/="
# _.="| \IIIIII/ |"=._
# _ _.="_.="\ /"=._"=._ _
# ( \_.="_.=" `--------` "=._"=._/ )
# > _.=" "=._ <
# (_/ \_)
#
# TODO What I need here is a way to trigger the import of specific files!
# It exists, but is not exposed, so I have to rely on a VERY fragile and hacky use of scan()...
# I'm not even sure it works tbh. It's terrible.
# See https://github.com/godotengine/godot-proposals/issues/1615
_editor_file_system.scan()
while _editor_file_system.is_scanning():
_logger.debug("Waiting for scan to complete...")
await get_tree().process_frame
if not is_inside_tree():
# oops?
return
_logger.debug("Scanning complete")
# Looks like import takes place AFTER scanning, so let's yield some more...
for fd in len(files_data) * 2:
_logger.debug("Yielding some more")
await get_tree().process_frame
var failed_resource_paths := []
# Using UndoRedo is mandatory for Godot to consider the resource as modified...
# ...yet if files get deleted, that won't be undoable anyways, but whatever :shrug:
var ur := _get_undo_redo_for_texture_set()
# Check imported textures
if _import_mode == HTerrainTextureSet.MODE_TEXTURES:
for fd in files_data:
var texture : Texture2D = load(fd.path)
if texture == null:
failed_resource_paths.append(fd.path)
continue
fd.texture = texture
else:
for fd in files_data:
var texture_array : TextureLayered = load(fd.path)
if texture_array == null:
failed_resource_paths.append(fd.path)
continue
fd.texture_array = texture_array
if len(failed_resource_paths) > 0:
var failed_list := "\n".join(PackedStringArray(failed_resource_paths))
_show_error("Some resources failed to load:\n" + failed_list)
return
# All is OK, commit action to modify the texture set with imported textures
if _import_mode == HTerrainTextureSet.MODE_TEXTURES:
ur.create_action("HTerrainTextureSet: import textures")
HT_TextureSetEditor.backup_for_undo(_texture_set, ur)
ur.add_do_method(_texture_set.clear)
ur.add_do_method(_texture_set.set_mode.bind(_import_mode))
for i in len(_slots_data):
ur.add_do_method(_texture_set.insert_slot.bind(-1))
for fd in files_data:
ur.add_do_method(_texture_set.set_texture.bind(fd.slot_index, fd.type, fd.texture))
else:
ur.create_action("HTerrainTextureSet: import texture arrays")
HT_TextureSetEditor.backup_for_undo(_texture_set, ur)
ur.add_do_method(_texture_set.clear)
ur.add_do_method(_texture_set.set_mode.bind(_import_mode))
for fd in files_data:
ur.add_do_method(_texture_set.set_texture_array.bind(fd.type, fd.texture_array))
ur.commit_action()
_logger.debug("Done importing")
_info_popup.dialog_text = "Importing complete!"
_info_popup.popup_centered()
import_finished.emit()
class HT_PackedImageInfo:
var path := "" # Where the packed image is saved
var slot_index : int # Slot in texture set, when using individual textures
var type : int # 0:Albedo+Bump, 1:Normal+Roughness
var import_file_data := {} # Data to write into the .import file (when enabled...)
var image : Image
var is_default := false
var texture : Texture2D
var texture_array : TextureLayered
# Shared code between the two import modes
func _generate_packed_images2() -> HT_Result:
var resolution : int = _import_settings.resolution
var images_infos := []
for type in HTerrainTextureSet.TYPE_COUNT:
var src_types := HTerrainTextureSet.get_src_types_from_type(type)
for slot_index in len(_slots_data):
var slot : HT_TextureSetImportEditorSlot = _slots_data[slot_index]
# Albedo or Normal
var src0 : String = slot.texture_paths[src_types[0]]
# Bump or Roughness
var src1 : String = slot.texture_paths[src_types[1]]
if src0 == "":
if src_types[0] == HTerrainTextureSet.SRC_TYPE_ALBEDO:
return HT_Result.new(false,
"Albedo texture is missing in slot {0}".format([slot_index]))
var is_default := (src0 == "" and src1 == "")
if src0 == "":
src0 = HTerrainTextureSet.get_source_texture_default_color_code(src_types[0])
if src1 == "":
src1 = HTerrainTextureSet.get_source_texture_default_color_code(src_types[1])
var pack_sources := {
"rgb": src0,
"a": src1
}
if HTerrainTextureSet.SRC_TYPE_NORMAL in src_types and slot.flip_normalmap_y:
pack_sources["normalmap_flip_y"] = true
var packed_image_result := HT_PackedTextureUtil.generate_image(
pack_sources, resolution, _logger)
if not packed_image_result.success:
return packed_image_result
var packed_image : Image = packed_image_result.value
var fd := HT_PackedImageInfo.new()
fd.slot_index = slot_index
fd.type = type
fd.image = packed_image
fd.is_default = is_default
images_infos.append(fd)
return HT_Result.new(true).with_value(images_infos)
func _generate_packed_images(import_dir: String, prefix: String) -> HT_Result:
var images_infos_result := _generate_packed_images2()
if not images_infos_result.success:
return images_infos_result
var images_infos : Array = images_infos_result.value
for info_index in len(images_infos):
var info : HT_PackedImageInfo = images_infos[info_index]
var type_name := HTerrainTextureSet.get_texture_type_name(info.type)
var fpath := import_dir.path_join(
str(prefix, "slot", info.slot_index, "_", type_name, ".png"))
var err := info.image.save_png(fpath)
if err != OK:
return HT_Result.new(false,
"Could not save image {0}, {1}".format([fpath, HT_Errors.get_message(err)]))
info.path = fpath
info.import_file_data = {
"remap": {
"importer": "texture",
"type": "CompressedTexture2D"
},
"deps": {
"source_file": fpath
},
"params": {
"compress/mode": ResourceImporterTexture_Unexposed.COMPRESS_VRAM_COMPRESSED,
"compress/high_quality": false,
"compress/lossy_quality": 0.7,
"mipmaps/generate": true,
"mipmaps/limit": -1,
"roughness/mode": ResourceImporterTexture_Unexposed.ROUGHNESS_DISABLED,
"process/fix_alpha_border": false
}
}
return HT_Result.new(true).with_value(images_infos)
static func _assemble_texarray_images(images: Array[Image], resolution: Vector2i) -> Image:
# Godot expects some kind of grid. Let's be lazy and do a grid with only one row.
var atlas := Image.create(resolution.x * len(images), resolution.y, false, Image.FORMAT_RGBA8)
for index in len(images):
var image : Image = images[index]
if image.get_size() != resolution:
image.resize(resolution.x, resolution.y, Image.INTERPOLATE_BILINEAR)
atlas.blit_rect(image,
Rect2i(0, 0, image.get_width(), image.get_height()),
Vector2i(index * resolution.x, 0))
return atlas
func _generate_packed_texarray_images(import_dir: String, prefix: String) -> HT_Result:
var images_infos_result := _generate_packed_images2()
if not images_infos_result.success:
return images_infos_result
var individual_images_infos : Array = images_infos_result.value
var resolution : int = _import_settings.resolution
var texarray_images_infos := []
var slot_count := len(_slots_data)
for type in HTerrainTextureSet.TYPE_COUNT:
var texarray_images : Array[Image] = []
texarray_images.resize(slot_count)
var fully_defaulted_slots := 0
for i in slot_count:
var info : HT_PackedImageInfo = individual_images_infos[type * slot_count + i]
if info.type == type:
texarray_images[i] = info.image
if info.is_default:
fully_defaulted_slots += 1
if fully_defaulted_slots == len(texarray_images):
# No need to generate this file at all
continue
var texarray_image := _assemble_texarray_images(texarray_images,
Vector2i(resolution, resolution))
var type_name := HTerrainTextureSet.get_texture_type_name(type)
var fpath := import_dir.path_join(str(prefix, type_name, "_array.png"))
var err := texarray_image.save_png(fpath)
if err != OK:
return HT_Result.new(false,
"Could not save image {0}, {1}".format([fpath, HT_Errors.get_message(err)]))
var texarray_image_info := HT_PackedImageInfo.new()
texarray_image_info.type = type
texarray_image_info.path = fpath
texarray_image_info.import_file_data = {
"remap": {
"importer": "2d_array_texture",
"type": "CompressedTexture2DArray"
},
"deps": {
"source_file": fpath
},
"params": {
"compress/mode": ResourceImporterTextureLayered_Unexposed.COMPRESS_VRAM_COMPRESSED,
"compress/high_quality": false,
"compress/lossy_quality": 0.7,
"mipmaps/generate": true,
"mipmaps/limit": -1,
"process/fix_alpha_border": false,
"slices/horizontal": len(texarray_images),
"slices/vertical": 1
}
}
texarray_images_infos.append(texarray_image_info)
return HT_Result.new(true).with_value(texarray_images_infos)

View File

@@ -0,0 +1 @@
uid://m0rb80ho8ptd

View File

@@ -0,0 +1,218 @@
[gd_scene load_steps=4 format=3 uid="uid://3indvrto5vd5"]
[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" id="1"]
[ext_resource type="PackedScene" uid="uid://dqgaomu3tr1ym" path="res://addons/zylann.hterrain/tools/texture_editor/set_editor/source_file_item_editor.tscn" id="3"]
[ext_resource type="Script" uid="uid://m0rb80ho8ptd" path="res://addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_import_editor.gd" id="4"]
[node name="TextureSetImportEditor" type="AcceptDialog"]
title = "Texture Set Import Tool"
size = Vector2i(652, 623)
min_size = Vector2i(652, 480)
script = ExtResource("4")
[node name="Import" type="VBoxContainer" 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 = -18.0
[node name="HS" type="HSplitContainer" parent="Import"]
layout_mode = 2
size_flags_vertical = 3
[node name="VB" type="VBoxContainer" parent="Import/HS"]
layout_mode = 2
size_flags_vertical = 3
[node name="Label" type="Label" parent="Import/HS/VB"]
visible = false
layout_mode = 2
text = "Slots"
[node name="SlotsList" type="ItemList" parent="Import/HS/VB"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_vertical = 3
item_count = 7
item_0/text = "Item 0"
item_1/text = "Item 1"
item_2/text = "Item 2"
item_3/text = "Item 3"
item_4/text = "Item 4"
item_5/text = "Item 5"
item_6/text = "Item 6"
[node name="HB" type="HBoxContainer" parent="Import/HS/VB"]
layout_mode = 2
[node name="AddSlotButton" type="Button" parent="Import/HS/VB/HB"]
layout_mode = 2
text = "+"
[node name="Control" type="Control" parent="Import/HS/VB/HB"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RemoveSlotButton" type="Button" parent="Import/HS/VB/HB"]
layout_mode = 2
text = "-"
[node name="VB2" type="VBoxContainer" parent="Import/HS"]
layout_mode = 2
[node name="Label" type="Label" parent="Import/HS/VB2"]
visible = false
layout_mode = 2
[node name="HB" type="HBoxContainer" parent="Import/HS/VB2"]
layout_mode = 2
[node name="Albedo" parent="Import/HS/VB2/HB" instance=ExtResource("3")]
layout_mode = 2
[node name="Bump" parent="Import/HS/VB2/HB" instance=ExtResource("3")]
layout_mode = 2
[node name="Normal" parent="Import/HS/VB2/HB" instance=ExtResource("3")]
layout_mode = 2
[node name="NormalMapFlipY" type="CheckBox" parent="Import/HS/VB2/HB/Normal"]
layout_mode = 2
text = "Flip Y"
[node name="Roughness" parent="Import/HS/VB2/HB" instance=ExtResource("3")]
layout_mode = 2
[node name="Control" type="Control" parent="Import/HS/VB2"]
custom_minimum_size = Vector2(0, 4)
layout_mode = 2
[node name="Control2" type="Control" parent="Import/HS/VB2"]
custom_minimum_size = Vector2(0, 4)
layout_mode = 2
size_flags_vertical = 3
[node name="Label3" type="Label" parent="Import/HS/VB2"]
modulate = Color(0.564706, 0.564706, 0.564706, 1)
layout_mode = 2
text = "These images should remain accessible for import to work.
Tip: you can place them in a folder with a `.gdignore` file, so they won't take space in your exported game."
autowrap_mode = 2
[node name="Spacer3" type="Control" parent="Import"]
custom_minimum_size = Vector2(0, 8)
layout_mode = 2
[node name="HSeparator" type="HSeparator" parent="Import"]
layout_mode = 2
[node name="GC" type="GridContainer" parent="Import"]
layout_mode = 2
columns = 4
[node name="Label2" type="Label" parent="Import/GC"]
layout_mode = 2
text = "Import mode: "
[node name="ImportModeSelector" type="OptionButton" parent="Import/GC"]
layout_mode = 2
size_flags_horizontal = 3
[node name="MipmapsCheckbox" type="CheckBox" parent="Import/GC"]
visible = false
layout_mode = 2
text = "Mipmaps"
[node name="Spacer2" type="Control" parent="Import/GC"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Import/GC"]
visible = false
layout_mode = 2
text = "Compression:"
[node name="CompressionSelector" type="OptionButton" parent="Import/GC"]
visible = false
layout_mode = 2
size_flags_horizontal = 3
[node name="FilterCheckBox" type="CheckBox" parent="Import/GC"]
visible = false
layout_mode = 2
text = "Filter"
[node name="Spacer" type="Control" parent="Import/GC"]
layout_mode = 2
[node name="Label3" type="Label" parent="Import/GC"]
layout_mode = 2
text = "Resolution:"
[node name="ResolutionSpinBox" type="SpinBox" parent="Import/GC"]
layout_mode = 2
min_value = 1.0
max_value = 4096.0
value = 1.0
[node name="HB2" type="HBoxContainer" parent="Import"]
layout_mode = 2
[node name="Label2" type="Label" parent="Import/HB2"]
layout_mode = 2
text = "Import directory"
[node name="ImportDirectoryLineEdit" type="LineEdit" parent="Import/HB2"]
layout_mode = 2
size_flags_horizontal = 3
[node name="BrowseImportDirectory" type="Button" parent="Import/HB2"]
layout_mode = 2
text = "..."
[node name="Spacer" type="Control" parent="Import"]
custom_minimum_size = Vector2(0, 8)
layout_mode = 2
[node name="HB" type="HBoxContainer" parent="Import"]
layout_mode = 2
alignment = 1
[node name="ImportButton" type="Button" parent="Import/HB"]
layout_mode = 2
text = "Import to TextureSet"
[node name="CancelButton" type="Button" parent="Import/HB"]
layout_mode = 2
text = "Close"
[node name="Spacer2" type="Control" parent="Import"]
custom_minimum_size = Vector2(0, 8)
layout_mode = 2
[node name="DialogFitter" parent="." instance=ExtResource("1")]
layout_mode = 3
anchors_preset = 0
offset_left = 8.0
offset_top = 8.0
offset_right = 644.0
offset_bottom = 605.0
[connection signal="item_selected" from="Import/HS/VB/SlotsList" to="." method="_on_SlotsList_item_selected"]
[connection signal="pressed" from="Import/HS/VB/HB/AddSlotButton" to="." method="_on_AddSlotButton_pressed"]
[connection signal="pressed" from="Import/HS/VB/HB/RemoveSlotButton" to="." method="_on_RemoveSlotButton_pressed"]
[connection signal="toggled" from="Import/HS/VB2/HB/Normal/NormalMapFlipY" to="." method="_on_NormalMapFlipY_toggled"]
[connection signal="item_selected" from="Import/GC/ImportModeSelector" to="." method="_on_ImportModeSelector_item_selected"]
[connection signal="toggled" from="Import/GC/MipmapsCheckbox" to="." method="_on_MipmapsCheckbox_toggled"]
[connection signal="item_selected" from="Import/GC/CompressionSelector" to="." method="_on_CompressionSelector_item_selected"]
[connection signal="toggled" from="Import/GC/FilterCheckBox" to="." method="_on_FilterCheckBox_toggled"]
[connection signal="value_changed" from="Import/GC/ResolutionSpinBox" to="." method="_on_ResolutionSpinBox_value_changed"]
[connection signal="text_changed" from="Import/HB2/ImportDirectoryLineEdit" to="." method="_on_ImportDirectoryLineEdit_text_changed"]
[connection signal="pressed" from="Import/HB2/BrowseImportDirectory" to="." method="_on_BrowseImportDirectory_pressed"]
[connection signal="pressed" from="Import/HB/ImportButton" to="." method="_on_ImportButton_pressed"]
[connection signal="pressed" from="Import/HB/CancelButton" to="." method="_on_CancelButton_pressed"]
[editable path="Import/HS/VB2/HB/Normal"]

View File

@@ -0,0 +1,134 @@
@tool
extends Control
const HTerrain = preload("../../hterrain.gd")
const HTerrainTextureSet = preload("../../hterrain_texture_set.gd")
const HT_TextureList = preload("./texture_list.gd")
const HT_Logger = preload("../../util/logger.gd")
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
const EMPTY_ICON_TEXTURE_PATH = "res://addons/zylann.hterrain/tools/icons/empty.png"
signal texture_selected(index)
signal edit_pressed(index)
signal import_pressed
@onready var _textures_list: HT_TextureList = $TextureList
@onready var _buttons_container : HBoxContainer = $HBoxContainer
var _terrain : HTerrain = null
var _texture_set : HTerrainTextureSet = null
var _texture_list_need_update := false
var _empty_icon : Texture2D
var _logger = HT_Logger.get_for(self)
func _ready():
_empty_icon = load(EMPTY_ICON_TEXTURE_PATH)
if _empty_icon == null:
_logger.error(str("Failed to load empty icon ", EMPTY_ICON_TEXTURE_PATH))
# Default amount, will be updated when a terrain is assigned
_textures_list.clear()
for i in range(4):
_textures_list.add_item(str(i), _empty_icon)
func set_terrain(terrain: HTerrain):
_terrain = terrain
static func _get_slot_count(terrain: HTerrain) -> int:
var texture_set = terrain.get_texture_set()
if texture_set == null:
return 0
return texture_set.get_slots_count()
func _process(delta: float):
var texture_set = null
if _terrain != null:
texture_set = _terrain.get_texture_set()
if _texture_set != texture_set:
if _texture_set != null:
_texture_set.changed.disconnect(_on_texture_set_changed)
_texture_set = texture_set
if _texture_set != null:
_texture_set.changed.connect(_on_texture_set_changed)
_update_texture_list()
if _texture_list_need_update:
_update_texture_list()
_texture_list_need_update = false
func _on_texture_set_changed():
_texture_list_need_update = true
func _update_texture_list():
_textures_list.clear()
if _terrain == null:
_set_buttons_active(false)
return
var texture_set := _terrain.get_texture_set()
if texture_set == null:
_set_buttons_active(false)
return
_set_buttons_active(true)
var slots_count := texture_set.get_slots_count()
match texture_set.get_mode():
HTerrainTextureSet.MODE_TEXTURES:
for slot_index in slots_count:
var texture := texture_set.get_texture(
slot_index, HTerrainTextureSet.TYPE_ALBEDO_BUMP)
var hint := _get_slot_hint_name(slot_index, _terrain.get_shader_type())
if texture == null:
texture = _empty_icon
_textures_list.add_item(hint, texture)
HTerrainTextureSet.MODE_TEXTURE_ARRAYS:
var texture_array = texture_set.get_texture_array(HTerrainTextureSet.TYPE_ALBEDO_BUMP)
for slot_index in slots_count:
var hint := _get_slot_hint_name(slot_index, _terrain.get_shader_type())
_textures_list.add_item(hint, texture_array, slot_index)
func _set_buttons_active(active: bool):
for i in _buttons_container.get_child_count():
var child = _buttons_container.get_child(i)
if child is Button:
child.disabled = not active
static func _get_slot_hint_name(i: int, stype: String) -> String:
if i == 3 and (stype == HTerrain.SHADER_CLASSIC4 or stype == HTerrain.SHADER_CLASSIC4_LITE):
return "cliff"
return str(i)
func _on_TextureList_item_selected(index: int):
texture_selected.emit(index)
func _on_TextureList_item_activated(index: int):
edit_pressed.emit(index)
func _on_EditButton_pressed():
var selected_slot := _textures_list.get_selected_item()
if selected_slot == -1:
selected_slot = 0
edit_pressed.emit(selected_slot)
func _on_ImportButton_pressed():
import_pressed.emit()

View File

@@ -0,0 +1 @@
uid://dssroq58kx6iw

View File

@@ -0,0 +1,49 @@
[gd_scene load_steps=3 format=3 uid="uid://duelkxj5cpvgg"]
[ext_resource type="Script" uid="uid://dssroq58kx6iw" path="res://addons/zylann.hterrain/tools/texture_editor/texture_editor.gd" id="1"]
[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/texture_editor/texture_list.tscn" id="2"]
[node name="TextureEditor" type="Control"]
offset_right = 352.0
offset_bottom = 104.0
custom_minimum_size = Vector2(100, 0)
size_flags_horizontal = 3
script = ExtResource("1")
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TextureList" parent="." instance=ExtResource("2")]
offset_bottom = -26.0
[node name="HBoxContainer" type="HBoxContainer" parent="."]
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -24.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="EditButton" type="Button" parent="HBoxContainer"]
offset_right = 48.0
offset_bottom = 24.0
text = "Edit..."
[node name="ImportButton" type="Button" parent="HBoxContainer"]
offset_left = 52.0
offset_right = 120.0
offset_bottom = 24.0
text = "Import..."
[node name="Label" type="Label" parent="HBoxContainer"]
offset_left = 124.0
offset_top = 5.0
offset_right = 179.0
offset_bottom = 19.0
text = "Textures"
[connection signal="item_activated" from="TextureList" to="." method="_on_TextureList_item_activated"]
[connection signal="item_selected" from="TextureList" to="." method="_on_TextureList_item_selected"]
[connection signal="pressed" from="HBoxContainer/EditButton" to="." method="_on_EditButton_pressed"]
[connection signal="pressed" from="HBoxContainer/ImportButton" to="." method="_on_ImportButton_pressed"]

View File

@@ -0,0 +1,79 @@
# I needed a custom container for this because textures edited by this plugin are often
# unfit to display in a GUI, they need to go through a shader (either discarding alpha,
# or picking layers of a TextureArray). Unfortunately, ItemList does not have custom item drawing,
# and items cannot have individual shaders.
# I could create new textures just for that but it would be expensive.
@tool
extends ScrollContainer
const HT_TextureListItemScene = preload("./texture_list_item.tscn")
const HT_TextureListItem = preload("./texture_list_item.gd")
signal item_selected(index)
signal item_activated(index)
@onready var _container : Container = $Container
var _selected_item := -1
# TEST
#func _ready():
# add_item("First", load("res://addons/zylann.hterrain_demo/textures/ground/bricks_albedo_bump.png"), 0)
# add_item("Second", load("res://addons/zylann.hterrain_demo/textures/ground/grass_albedo_bump.png"), 0)
# add_item("Third", load("res://addons/zylann.hterrain_demo/textures/ground/leaves_albedo_bump.png"), 0)
# add_item("Fourth", load("res://addons/zylann.hterrain_demo/textures/ground/sand_albedo_bump.png"), 0)
# var texture_array = load("res://tests/texarray/textures/array_albedo_atlas.png")
# add_item("Ninth", texture_array, 2)
# add_item("Sixth", texture_array, 3)
# Note: the texture can be a TextureArray, which does not inherit Texture
func add_item(text: String, texture: Texture, texture_layer: int = 0):
var item : HT_TextureListItem = HT_TextureListItemScene.instantiate()
_container.add_child(item)
item.set_text(text)
item.set_texture(texture, texture_layer)
func get_item_count() -> int:
return _container.get_child_count()
func set_item_texture(index: int, tex: Texture, layer: int = 0):
var child : HT_TextureListItem = _container.get_child(index)
child.set_texture(tex, layer)
func get_selected_item() -> int:
return _selected_item
func clear():
for i in _container.get_child_count():
var child = _container.get_child(i)
if child is Control:
child.queue_free()
_selected_item = -1
func _on_item_selected(item: HT_TextureListItem):
_selected_item = item.get_index()
for i in _container.get_child_count():
var child = _container.get_child(i)
if child is HT_TextureListItem and child != item:
child.set_selected(false, false)
item_selected.emit(_selected_item)
func _on_item_activated(item: HT_TextureListItem):
item_activated.emit(item.get_index())
func _draw():
# TODO Draw same background as Panel
# Draw a background
draw_rect(get_rect(), Color(0,0,0,0.3))

View File

@@ -0,0 +1 @@
uid://c2uqsylbxcci3

View File

@@ -0,0 +1,20 @@
[gd_scene load_steps=3 format=3 uid="uid://dv2cgpfghlnwq"]
[ext_resource type="Script" uid="uid://c2uqsylbxcci3" path="res://addons/zylann.hterrain/tools/texture_editor/texture_list.gd" id="1"]
[ext_resource type="Script" uid="uid://d2cf2epylmuo6" path="res://addons/zylann.hterrain/tools/texture_editor/flow_container.gd" id="2"]
[node name="TextureList" type="ScrollContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
scroll_horizontal_enabled = false
script = ExtResource("1")
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Container" type="Container" parent="."]
offset_right = 800.0
offset_bottom = 82.0
custom_minimum_size = Vector2(0, 82)
size_flags_horizontal = 3
script = ExtResource("2")

View File

@@ -0,0 +1,72 @@
@tool
extends PanelContainer
# Had to use PanelContainer, because due to variable font sizes in the editor,
# the contents of the VBoxContainer can vary in size, and so in height.
# Which means the entire item can have variable size, not just because of DPI.
# In such cases, the hierarchy must be made of containers that grow based on their children.
const HT_ColorMaterial = preload("./display_color_material.tres")
const HT_ColorSliceShader = preload("./display_color_slice.gdshader")
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
#const HT_DummyTexture = preload("../icons/empty.png")
const DUMMY_TEXTURE_PATH = "res://addons/zylann.hterrain/tools/icons/empty.png"
@onready var _texture_rect : TextureRect = $VB/TextureRect
@onready var _label : Label = $VB/Label
var _selected := false
func set_text(text: String):
_label.text = text
func set_texture(texture: Texture, texture_layer: int):
if texture is TextureLayered:
var mat = _texture_rect.material
if mat == null or not (mat is ShaderMaterial):
mat = ShaderMaterial.new()
mat.shader = HT_ColorSliceShader
_texture_rect.material = mat
mat.set_shader_parameter("u_texture_array", texture)
mat.set_shader_parameter("u_index", texture_layer)
_texture_rect.texture = load(DUMMY_TEXTURE_PATH)
else:
_texture_rect.texture = texture
_texture_rect.material = HT_ColorMaterial
func _gui_input(event: InputEvent):
if event is InputEventMouseButton:
if event.pressed:
if event.button_index == MOUSE_BUTTON_LEFT:
grab_focus()
set_selected(true, true)
if event.double_click:
# Don't do this at home.
# I do it here because this script is very related to its container anyways.
get_parent().get_parent()._on_item_activated(self)
func set_selected(selected: bool, notify: bool):
if selected == _selected:
return
_selected = selected
queue_redraw()
if _selected:
_label.modulate = Color(0,0,0)
else:
_label.modulate = Color(1,1,1)
if notify:
get_parent().get_parent()._on_item_selected(self)
func _draw():
var color : Color
if _selected:
color = get_theme_color("accent_color", "Editor")
else:
color = Color(0.0, 0.0, 0.0, 0.5)
# Draw background
draw_rect(Rect2(Vector2(), size), color)

View File

@@ -0,0 +1 @@
uid://bdqqab3vyiy0k

View File

@@ -0,0 +1,25 @@
[gd_scene load_steps=2 format=3 uid="uid://daugk4kdnx6vy"]
[ext_resource type="Script" uid="uid://bdqqab3vyiy0k" path="res://addons/zylann.hterrain/tools/texture_editor/texture_list_item.gd" id="2"]
[node name="TextureListItem" type="PanelContainer"]
custom_minimum_size = Vector2(64, 80)
offset_right = 64.0
offset_bottom = 80.0
focus_mode = 1
script = ExtResource("2")
[node name="VB" type="VBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="TextureRect" type="TextureRect" parent="VB"]
custom_minimum_size = Vector2(60, 60)
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
expand_mode = 1
[node name="Label" type="Label" parent="VB"]
layout_mode = 2
text = "Texture"