added terrain3d

This commit is contained in:
derek
2025-03-31 14:14:50 -05:00
parent 27175618c0
commit bd767d2927
148 changed files with 2602 additions and 1381 deletions

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]
[ext_resource type="Script" path="res://addons/terrain_3d/menu/bake_lod_dialog.gd" id="1_sf76d"]
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 155)
visible = true
script = ExtResource("1_sf76d")
[node name="MarginContainer" type="MarginContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 392.0
offset_bottom = 106.0
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "LOD:"
[node name="LodBox" type="SpinBox" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
max_value = 8.0
value = 4.0
[node name="DescriptionLabel" type="Label" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2

View File

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

View File

@@ -0,0 +1 @@
uid://4ulaeevj5jvi

View File

@@ -0,0 +1,463 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Channel Packer for Terrain3D
extends RefCounted
const WINDOW_SCENE: String = "res://addons/terrain_3d/menu/channel_packer.tscn"
const TEMPLATE_PATH: String = "res://addons/terrain_3d/menu/channel_packer_import_template.txt"
const DRAG_DROP_SCRIPT: String = "res://addons/terrain_3d/menu/channel_packer_dragdrop.gd"
enum {
INFO,
WARN,
ERROR,
}
enum {
IMAGE_ALBEDO,
IMAGE_HEIGHT,
IMAGE_NORMAL,
IMAGE_ROUGHNESS
}
var plugin: EditorPlugin
var window: Window
var save_file_dialog: EditorFileDialog
var open_file_dialog: EditorFileDialog
var invert_green_checkbox: CheckBox
var invert_smooth_checkbox: CheckBox
var invert_height_checkbox: CheckBox
var lumin_height_button: Button
var generate_mipmaps_checkbox: CheckBox
var high_quality_checkbox: CheckBox
var align_normals_checkbox: CheckBox
var resize_toggle_checkbox: CheckBox
var resize_option_box: SpinBox
var height_channel: Array[Button]
var height_channel_selected: int = 0
var roughness_channel: Array[Button]
var roughness_channel_selected: int = 0
var last_opened_directory: String
var last_saved_directory: String
var packing_albedo: bool = false
var queue_pack_normal_roughness: bool = false
var images: Array[Image] = [null, null, null, null]
var status_label: Label
var no_op: Callable = func(): pass
var last_file_selected_fn: Callable = no_op
var normal_vector: Vector3
func pack_textures_popup() -> void:
if window != null:
window.show()
window.grab_focus()
window.move_to_center()
return
window = (load(WINDOW_SCENE) as PackedScene).instantiate()
window.close_requested.connect(_on_close_requested)
window.window_input.connect(func(event:InputEvent):
if event is InputEventKey:
if event.pressed and event.keycode == KEY_ESCAPE:
_on_close_requested()
)
window.find_child("CloseButton").pressed.connect(_on_close_requested)
status_label = window.find_child("StatusLabel") as Label
invert_green_checkbox = window.find_child("InvertGreenChannelCheckBox") as CheckBox
invert_smooth_checkbox = window.find_child("InvertSmoothCheckBox") as CheckBox
invert_height_checkbox = window.find_child("ConvertDepthToHeight") as CheckBox
lumin_height_button = window.find_child("LuminanceAsHeightButton") as Button
generate_mipmaps_checkbox = window.find_child("GenerateMipmapsCheckBox") as CheckBox
high_quality_checkbox = window.find_child("HighQualityCheckBox") as CheckBox
align_normals_checkbox = window.find_child("AlignNormalsCheckBox") as CheckBox
resize_toggle_checkbox = window.find_child("ResizeToggle") as CheckBox
resize_option_box = window.find_child("ResizeOptionButton") as SpinBox
height_channel = [
window.find_child("HeightChannelR") as Button,
window.find_child("HeightChannelG") as Button,
window.find_child("HeightChannelB") as Button,
window.find_child("HeightChannelA") as Button
]
roughness_channel = [
window.find_child("RoughnessChannelR") as Button,
window.find_child("RoughnessChannelG") as Button,
window.find_child("RoughnessChannelB") as Button,
window.find_child("RoughnessChannelA") as Button
]
height_channel[0].pressed.connect(func() -> void: height_channel_selected = 0)
height_channel[1].pressed.connect(func() -> void: height_channel_selected = 1)
height_channel[2].pressed.connect(func() -> void: height_channel_selected = 2)
height_channel[3].pressed.connect(func() -> void: height_channel_selected = 3)
roughness_channel[0].pressed.connect(func() -> void: roughness_channel_selected = 0)
roughness_channel[1].pressed.connect(func() -> void: roughness_channel_selected = 1)
roughness_channel[2].pressed.connect(func() -> void: roughness_channel_selected = 2)
roughness_channel[3].pressed.connect(func() -> void: roughness_channel_selected = 3)
plugin.add_child(window)
_init_file_dialogs()
# the dialog disables the parent window "on top" so, restore it after 1 frame to alow the dialog to clear.
var set_on_top_fn: Callable = func(_file: String = "") -> void:
await RenderingServer.frame_post_draw
window.always_on_top = true
save_file_dialog.file_selected.connect(set_on_top_fn)
save_file_dialog.canceled.connect(set_on_top_fn)
open_file_dialog.file_selected.connect(set_on_top_fn)
open_file_dialog.canceled.connect(set_on_top_fn)
_init_texture_picker(window.find_child("AlbedoVBox"), IMAGE_ALBEDO)
_init_texture_picker(window.find_child("HeightVBox"), IMAGE_HEIGHT)
_init_texture_picker(window.find_child("NormalVBox"), IMAGE_NORMAL)
_init_texture_picker(window.find_child("RoughnessVBox"), IMAGE_ROUGHNESS)
(window.find_child("PackButton") as Button).pressed.connect(_on_pack_button_pressed)
func _on_close_requested() -> void:
last_file_selected_fn = no_op
images = [null, null, null, null]
window.queue_free()
window = null
func _init_file_dialogs() -> void:
save_file_dialog = EditorFileDialog.new()
save_file_dialog.set_filters(PackedStringArray(["*.png"]))
save_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_SAVE_FILE)
save_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
save_file_dialog.file_selected.connect(_on_save_file_selected)
save_file_dialog.ok_button_text = "Save"
save_file_dialog.size = Vector2i(550, 550)
#save_file_dialog.transient = false
#save_file_dialog.exclusive = false
#save_file_dialog.popup_window = true
open_file_dialog = EditorFileDialog.new()
open_file_dialog.set_filters(PackedStringArray(
["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", "*.ktx", "*.dds"]))
open_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_OPEN_FILE)
open_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
open_file_dialog.ok_button_text = "Open"
open_file_dialog.size = Vector2i(550, 550)
#open_file_dialog.transient = false
#open_file_dialog.exclusive = false
#open_file_dialog.popup_window = true
window.add_child(save_file_dialog)
window.add_child(open_file_dialog)
func _init_texture_picker(p_parent: Node, p_image_index: int) -> void:
var line_edit: LineEdit = p_parent.find_child("LineEdit") as LineEdit
var file_pick_button: Button = p_parent.find_child("PickButton") as Button
var clear_button: Button = p_parent.find_child("ClearButton") as Button
var texture_rect: TextureRect = p_parent.find_child("TextureRect") as TextureRect
var texture_button: Button = p_parent.find_child("TextureButton") as Button
texture_button.set_script(load(DRAG_DROP_SCRIPT) as GDScript)
var set_channel_fn: Callable = func(used_channels: int) -> void:
var channel_count: int = 4
# enum Image.UsedChannels
match used_channels:
Image.USED_CHANNELS_L, Image.USED_CHANNELS_R: channel_count = 1
Image.USED_CHANNELS_LA, Image.USED_CHANNELS_RG: channel_count = 2
Image.USED_CHANNELS_RGB: channel_count = 3
Image.USED_CHANNELS_RGBA: channel_count = 4
if p_image_index == IMAGE_HEIGHT:
for i in 4:
height_channel[i].visible = i < channel_count
height_channel[0].button_pressed = true
height_channel[0].pressed.emit()
elif p_image_index == IMAGE_ROUGHNESS:
for i in 4:
roughness_channel[i].visible = i < channel_count
roughness_channel[0].button_pressed = true
roughness_channel[0].pressed.emit()
var load_image_fn: Callable = func(path: String):
var image: Image = Image.new()
var error: int = OK
# Special case for dds files
if path.get_extension() == "dds":
image = ResourceLoader.load(path).get_image()
if not image.is_empty():
# if the dds file is loaded, we must clear any mipmaps and
# decompress if needed in order to do per pixel operations.
image.clear_mipmaps()
image.decompress()
else:
error = FAILED
else:
error = image.load(path)
if error != OK:
_show_message(ERROR, "Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
_show_message(INFO, "Loaded texture '" + path + "'")
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
_set_wh_labels(p_image_index, image.get_width(), image.get_height())
if p_image_index == IMAGE_NORMAL:
_set_normal_vector(image)
if p_image_index == IMAGE_HEIGHT or p_image_index == IMAGE_ROUGHNESS:
set_channel_fn.call(image.detect_used_channels())
var os_drop_fn: Callable = func(files: PackedStringArray) -> void:
# OS drag drop holds mouse focus until released,
# Get mouse pos and check directly if inside texture_rect
var rect = texture_button.get_global_rect()
var mouse_position = texture_button.get_global_mouse_position()
if rect.has_point(mouse_position):
if files.size() != 1:
_show_message(ERROR, "Cannot load multiple files")
else:
line_edit.text = files[0]
load_image_fn.call(files[0])
var godot_drop_fn: Callable = func(path: String) -> void:
path = ProjectSettings.globalize_path(path)
line_edit.text = path
load_image_fn.call(path)
var open_fn: Callable = func() -> void:
open_file_dialog.current_path = last_opened_directory
if last_file_selected_fn != no_op:
open_file_dialog.file_selected.disconnect(last_file_selected_fn)
last_file_selected_fn = func(path: String) -> void:
line_edit.text = path
load_image_fn.call(path)
open_file_dialog.file_selected.connect(last_file_selected_fn)
open_file_dialog.popup_centered_ratio()
var line_edit_submit_fn: Callable = func(path: String) -> void:
line_edit.text = path
load_image_fn.call(path)
var clear_fn: Callable = func() -> void:
line_edit.text = ""
texture_rect.texture = null
images[p_image_index] = null
_set_wh_labels(p_image_index, -1, -1)
line_edit.text_submitted.connect(line_edit_submit_fn)
file_pick_button.pressed.connect(open_fn)
texture_button.pressed.connect(open_fn)
clear_button.pressed.connect(clear_fn)
texture_button.dropped.connect(godot_drop_fn)
window.files_dropped.connect(os_drop_fn)
if p_image_index == IMAGE_HEIGHT:
var lumin_fn: Callable = func() -> void:
if !images[IMAGE_ALBEDO]:
_show_message(ERROR, "Albedo Image Required for Operation")
else:
line_edit.text = "Generated Height"
var height_texture: Image = Terrain3DUtil.luminance_to_height(images[IMAGE_ALBEDO])
if height_texture.is_empty():
_show_message(ERROR, "Height Texture Generation error")
# blur the image by resizing down and back..
var w: int = height_texture.get_width()
var h: int = height_texture.get_height()
height_texture.resize(w / 4, h / 4)
height_texture.resize(w, h, Image.INTERPOLATE_CUBIC)
# "Load" the height texture
images[IMAGE_HEIGHT] = height_texture
texture_rect.texture = ImageTexture.create_from_image(images[IMAGE_HEIGHT])
_set_wh_labels(IMAGE_HEIGHT, height_texture.get_width(), height_texture.get_height())
set_channel_fn.call(Image.USED_CHANNELS_R)
_show_message(INFO, "Height Texture generated sucsessfully")
lumin_height_button.pressed.connect(lumin_fn)
plugin.ui.set_button_editor_icon(file_pick_button, "Folder")
plugin.ui.set_button_editor_icon(clear_button, "Remove")
func _set_wh_labels(p_image_index: int, width: int, height: int) -> void:
var w: String = ""
var h: String = ""
if width > 0 and height > 0:
w = "w: " + str(width)
h = "h: " + str(height)
match p_image_index:
0:
window.find_child("AlbedoW").text = w
window.find_child("AlbedoH").text = h
1:
window.find_child("HeightW").text = w
window.find_child("HeightH").text = h
2:
window.find_child("NormalW").text = w
window.find_child("NormalH").text = h
3:
window.find_child("RoughnessW").text = w
window.find_child("RoughnessH").text = h
func _show_message(p_level: int, p_text: String) -> void:
status_label.text = p_text
match p_level:
INFO:
print("Terrain3DChannelPacker: " + p_text)
status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14))
WARN:
push_warning("Terrain3DChannelPacker: " + p_text)
status_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0))
ERROR,_:
push_error("Terrain3DChannelPacker: " + p_text)
status_label.add_theme_color_override("font_color", Color(0.9, 0, 0))
func _create_import_file(png_path: String) -> void:
var dst_import_path: String = png_path + ".import"
var file: FileAccess = FileAccess.open(TEMPLATE_PATH, FileAccess.READ)
var template_content: String = file.get_as_text()
file.close()
template_content = template_content.replace(
"$SOURCE_FILE", png_path).replace(
"$HIGH_QUALITY", str(high_quality_checkbox.button_pressed)).replace(
"$GENERATE_MIPMAPS", str(generate_mipmaps_checkbox.button_pressed)
)
var import_content: String = template_content
file = FileAccess.open(dst_import_path, FileAccess.WRITE)
file.store_string(import_content)
file.close()
func _on_pack_button_pressed() -> void:
packing_albedo = images[IMAGE_ALBEDO] != null and images[IMAGE_HEIGHT] != null
var packing_normal_roughness: bool = images[IMAGE_NORMAL] != null and images[IMAGE_ROUGHNESS] != null
if not packing_albedo and not packing_normal_roughness:
_show_message(WARN, "Please select an albedo and height texture or a normal and roughness texture")
return
if packing_albedo:
save_file_dialog.current_path = last_saved_directory + "packed_albedo_height"
save_file_dialog.title = "Save Packed Albedo/Height Texture"
save_file_dialog.popup_centered_ratio()
if packing_normal_roughness:
queue_pack_normal_roughness = true
return
if packing_normal_roughness:
save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness"
save_file_dialog.title = "Save Packed Normal/Roughness Texture"
save_file_dialog.popup_centered_ratio()
func _on_save_file_selected(p_dst_path) -> void:
last_saved_directory = p_dst_path.get_base_dir() + "/"
var error: int
if packing_albedo:
error = _pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false,
invert_height_checkbox.button_pressed, false, height_channel_selected)
else:
error = _pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path,
invert_green_checkbox.button_pressed, invert_smooth_checkbox.button_pressed,
align_normals_checkbox.button_pressed, roughness_channel_selected)
if error == OK:
EditorInterface.get_resource_filesystem().scan()
if window.visible:
window.hide()
await EditorInterface.get_resource_filesystem().resources_reimported
# wait 1 extra frame, to ensure the UI is responsive.
await RenderingServer.frame_post_draw
window.show()
if queue_pack_normal_roughness:
queue_pack_normal_roughness = false
packing_albedo = false
save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness"
save_file_dialog.title = "Save Packed Normal/Roughness Texture"
save_file_dialog.call_deferred("popup_centered_ratio")
save_file_dialog.call_deferred("grab_focus")
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_width():
for y in normal.get_height():
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_width():
for y in source.get_height():
var old_pixel: Color = source.get_pixel(x, y)
var vector_pixel: Vector3 = Vector3(old_pixel.r, old_pixel.g, old_pixel.b)
vector_pixel *= 2.0
vector_pixel -= Vector3.ONE
vector_pixel = vector_pixel.normalized()
vector_pixel = vector_pixel * mat3
vector_pixel += Vector3.ONE
vector_pixel *= 0.5
var new_pixel: Color = Color(vector_pixel.x, vector_pixel.y, vector_pixel.z, old_pixel.a)
source.set_pixel(x, y, new_pixel)
_set_normal_vector(source, true)
if normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999 && iteration < 3:
++iteration
_align_normals(source, iteration)
func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool,
p_invert_smooth: bool, p_align_normals : bool, p_alpha_channel: int) -> Error:
if p_rgb_image and p_a_image:
if p_rgb_image.get_size() != p_a_image.get_size() and !resize_toggle_checkbox.button_pressed:
_show_message(ERROR, "Textures must be the same size.\nEnable resize to override image dimensions")
return FAILED
if resize_toggle_checkbox.button_pressed:
var size: int = max(128, resize_option_box.value)
p_rgb_image.resize(size, size, Image.INTERPOLATE_CUBIC)
p_a_image.resize(size, size, Image.INTERPOLATE_CUBIC)
if p_align_normals and normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999:
_align_normals(p_rgb_image)
elif p_align_normals:
_show_message(INFO, "Alignment OK, skipping Normal Orthogonalization")
var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image,
p_invert_green, p_invert_smooth, p_alpha_channel)
if not output_image:
_show_message(ERROR, "Failed to pack textures")
return FAILED
if output_image.detect_alpha() != Image.ALPHA_BLEND:
_show_message(WARN, "Warning, Alpha channel empty")
output_image.save_png(p_dst_path)
_create_import_file(p_dst_path)
_show_message(INFO, "Packed to " + p_dst_path + ".")
return OK
else:
_show_message(ERROR, "Failed to load one or more textures")
return FAILED

View File

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

View File

@@ -0,0 +1,530 @@
[gd_scene load_steps=7 format=3 uid="uid://nud6dwjcnj5v"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ysabf"]
bg_color = Color(0.211765, 0.239216, 0.290196, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lcvna"]
bg_color = Color(0.168627, 0.211765, 0.266667, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.270588, 0.435294, 0.580392, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cb0xf"]
bg_color = Color(0.137255, 0.137255, 0.137255, 1)
draw_center = false
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.784314, 0.784314, 0.784314, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"]
[sub_resource type="ButtonGroup" id="ButtonGroup_wnxik"]
[sub_resource type="ButtonGroup" id="ButtonGroup_bs6ki"]
[node name="Window" type="Window"]
title = "Terrain3D Channel Packer"
initial_position = 1
size = Vector2i(583, 856)
wrap_controls = true
always_on_top = true
[node name="PanelContainer" type="PanelContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf")
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="AlbedoHeightPanel" type="PanelContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer"]
layout_mode = 2
[node name="AlbedoVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="AlbedoLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
text = "Albedo texture"
[node name="AlbedoHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="AlbedoWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="AlbedoW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="AlbedoH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="LuminanceAsHeightButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/HBoxContainer2"]
layout_mode = 2
text = " Generate Height from Luminance"
icon_alignment = 2
[node name="HeightVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HeightLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
text = "Height texture"
[node name="HeightHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="HeightWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HeightH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="ConvertDepthToHeight" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Depth to Height"
icon_alignment = 2
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightChannelLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="HeightChannelR" type="Button" parent="PanelContainer/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="PanelContainer/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="PanelContainer/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="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "A"
[node name="NormalRoughnessPanel" type="PanelContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer"]
layout_mode = 2
[node name="NormalVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="NormalLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
text = "Normal texture"
[node name="NormalHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="NormalWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="NormalW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="NormalH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer"]
layout_mode = 2
text = " Convert DirectX to OpenGL"
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="AlignNormalsCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer2"]
layout_mode = 2
text = " Orthoganolise Normals"
[node name="RoughnessVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RoughnessLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
text = "Roughness texture"
[node name="RoughnessHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="RoughnessWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="RoughnessH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="InvertSmoothCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Smoothness to Roughness"
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessChannelLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="RoughnessChannelR" type="Button" parent="PanelContainer/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="PanelContainer/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="PanelContainer/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="PanelContainer/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="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "General Options"
horizontal_alignment = 1
vertical_alignment = 1
[node name="GeneralOptionsHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="ResizeToggle" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = " Resize Packed Image"
[node name="ResizeOptionButton" type="SpinBox" parent="PanelContainer/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="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
[node name="GenerateMipmapsCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
button_pressed = true
text = "Generate Mipmaps"
[node name="HighQualityCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = "Import High Quality"
[node name="PackButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Pack textures as..."
[node name="StatusLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 60)
layout_mode = 2
horizontal_alignment = 1
autowrap_mode = 3
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="CloseButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Close"
[connection signal="toggled" from="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeToggle" to="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeOptionButton" method="set_visible"]

View File

@@ -0,0 +1,17 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Channel Packer Dragdropper for Terrain3D
@tool
extends Button
signal dropped
func _can_drop_data(p_position, p_data) -> bool:
if typeof(p_data) == TYPE_DICTIONARY:
if p_data.files.size() == 1:
match p_data.files[0].get_extension():
"png", "bmp", "exr", "hdr", "jpg", "jpeg", "tga", "svg", "webp", "ktx", "dds":
return true
return false
func _drop_data(p_position, p_data) -> void:
dropped.emit(p_data.files[0])

View File

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

View File

@@ -0,0 +1,32 @@
[remap]
importer="texture"
type="CompressedTexture2D"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="$SOURCE_FILE"
[params]
compress/mode=2
compress/high_quality=$HIGH_QUALITY
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=2
compress/channel_pack=0
mipmaps/generate=$GENERATE_MIPMAPS
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,86 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Directory Setup for Terrain3D
extends Node
const DIRECTORY_SETUP: String = "res://addons/terrain_3d/menu/directory_setup.tscn"
var plugin: EditorPlugin
var dialog: ConfirmationDialog
var select_dir_btn: Button
var selected_dir_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.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")
if plugin.terrain.data_directory:
selected_dir_le.text = plugin.terrain.data_directory
# Icons
plugin.ui.set_button_editor_icon(select_dir_btn, "Folder")
#Signals
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_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

View File

@@ -0,0 +1 @@
uid://0034ukv2mngn

View File

@@ -0,0 +1,48 @@
[gd_scene format=3 uid="uid://by3kr2nqbqr67"]
[node name="DirectorySetup" type="ConfirmationDialog"]
title = "Terrain3D Data Directory Setup"
position = Vector2i(0, 36)
size = Vector2i(750, 330)
visible = true
[node name="Margin" type="MarginContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 742.0
offset_bottom = 281.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="Spacer" type="Control" parent="Margin/VBox"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2

View File

@@ -0,0 +1,83 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Menu for Terrain3D
extends HBoxContainer
const DirectoryWizard: Script = preload("res://addons/terrain_3d/menu/directory_setup.gd")
const Packer: Script = preload("res://addons/terrain_3d/menu/channel_packer.gd")
const Baker: Script = preload("res://addons/terrain_3d/menu/baker.gd")
var plugin: EditorPlugin
var menu_button: MenuButton = MenuButton.new()
var directory_setup: DirectoryWizard = DirectoryWizard.new()
var packer: Packer = Packer.new()
var baker: Baker = Baker.new()
# These are IDs and order must be consistent with add_item and set_disabled IDs
enum {
MENU_DIRECTORY_SETUP,
MENU_PACK_TEXTURES,
MENU_SEPARATOR,
MENU_BAKE_ARRAY_MESH,
MENU_BAKE_OCCLUDER,
MENU_SEPARATOR2,
MENU_SET_UP_NAVIGATION,
MENU_BAKE_NAV_MESH,
}
func _enter_tree() -> void:
directory_setup.plugin = plugin
packer.plugin = plugin
baker.plugin = plugin
add_child(directory_setup)
add_child(baker)
menu_button.text = "Terrain3D"
menu_button.get_popup().add_item("Directory Setup...", MENU_DIRECTORY_SETUP)
menu_button.get_popup().add_item("Pack Textures...", MENU_PACK_TEXTURES)
menu_button.get_popup().add_separator("", MENU_SEPARATOR)
menu_button.get_popup().add_item("Bake ArrayMesh...", MENU_BAKE_ARRAY_MESH)
menu_button.get_popup().add_item("Bake Occluder3D...", MENU_BAKE_OCCLUDER)
menu_button.get_popup().add_separator("", MENU_SEPARATOR2)
menu_button.get_popup().add_item("Set up Navigation...", MENU_SET_UP_NAVIGATION)
menu_button.get_popup().add_item("Bake NavMesh...", MENU_BAKE_NAV_MESH)
menu_button.get_popup().id_pressed.connect(_on_menu_pressed)
menu_button.about_to_popup.connect(_on_menu_about_to_popup)
add_child(menu_button)
func _on_menu_pressed(p_id: int) -> void:
match p_id:
MENU_DIRECTORY_SETUP:
directory_setup.directory_setup_popup()
MENU_PACK_TEXTURES:
packer.pack_textures_popup()
MENU_BAKE_ARRAY_MESH:
baker.bake_mesh_popup()
MENU_BAKE_OCCLUDER:
baker.bake_occluder_popup()
MENU_SET_UP_NAVIGATION:
baker.set_up_navigation_popup()
MENU_BAKE_NAV_MESH:
baker.bake_nav_mesh()
func _on_menu_about_to_popup() -> void:
menu_button.get_popup().set_item_disabled(MENU_DIRECTORY_SETUP, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_PACK_TEXTURES, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_ARRAY_MESH, not plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_OCCLUDER, not plugin.terrain)
if plugin.terrain:
var nav_regions: Array[NavigationRegion3D] = baker.find_terrain_nav_regions(plugin.terrain)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, nav_regions.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, nav_regions.size() != 0)
elif plugin.nav_region:
var terrains: Array[Terrain3D] = baker.find_nav_region_terrains(plugin.nav_region)
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, terrains.size() == 0)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)
else:
menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, true)
menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true)

View File

@@ -0,0 +1 @@
uid://3gxvahogxa10