Merge branch 'develop' into 'main'

🐛 Fixed blank collision mask in PlayerWalkSensor

See merge request open-fpsz/open-fpsz!100
This commit is contained in:
Spats 2025-01-11 04:05:53 +00:00
commit d5c4e84725
450 changed files with 14339 additions and 8483 deletions

2
.gitignore vendored
View file

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

View file

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

View file

@ -1,36 +1,37 @@
[configuration]
entry_symbol = "godot_jolt_main"
compatibility_minimum = "4.2"
compatibility_minimum = "4.3"
compatibility_maximum = "4.3"
[libraries]
windows.release.x86_64 = "windows/godot-jolt_windows-x64.dll"
windows.debug.x86_64 = "windows/godot-jolt_windows-x64_editor.dll"
windows.release.single.x86_64 = "windows/godot-jolt_windows-x64.dll"
windows.debug.single.x86_64 = "windows/godot-jolt_windows-x64_editor.dll"
windows.release.x86_32 = "windows/godot-jolt_windows-x86.dll"
windows.debug.x86_32 = "windows/godot-jolt_windows-x86_editor.dll"
windows.release.single.x86_32 = "windows/godot-jolt_windows-x86.dll"
windows.debug.single.x86_32 = "windows/godot-jolt_windows-x86_editor.dll"
linux.release.x86_64 = "linux/godot-jolt_linux-x64.so"
linux.debug.x86_64 = "linux/godot-jolt_linux-x64_editor.so"
linux.release.single.x86_64 = "linux/godot-jolt_linux-x64.so"
linux.debug.single.x86_64 = "linux/godot-jolt_linux-x64_editor.so"
linux.release.x86_32 = "linux/godot-jolt_linux-x86.so"
linux.debug.x86_32 = "linux/godot-jolt_linux-x86_editor.so"
linux.release.single.x86_32 = "linux/godot-jolt_linux-x86.so"
linux.debug.single.x86_32 = "linux/godot-jolt_linux-x86_editor.so"
macos.release = "macos/godot-jolt_macos.framework"
macos.debug = "macos/godot-jolt_macos_editor.framework"
macos.release.single = "macos/godot-jolt_macos.framework"
macos.debug.single = "macos/godot-jolt_macos_editor.framework"
ios.release = "ios/godot-jolt_ios.framework"
ios.debug = "ios/godot-jolt_ios_editor.framework"
ios.release.single = "ios/godot-jolt_ios.framework"
ios.debug.single = "ios/godot-jolt_ios_editor.framework"
android.release.arm64 = "android/libgodot-jolt_android-arm64.so"
android.debug.arm64 = "android/libgodot-jolt_android-arm64_editor.so"
android.release.single.arm64 = "android/libgodot-jolt_android-arm64.so"
android.debug.single.arm64 = "android/libgodot-jolt_android-arm64_editor.so"
android.release.arm32 = "android/libgodot-jolt_android-arm32.so"
android.debug.arm32 = "android/libgodot-jolt_android-arm32_editor.so"
android.release.single.arm32 = "android/libgodot-jolt_android-arm32.so"
android.debug.single.arm32 = "android/libgodot-jolt_android-arm32_editor.so"
android.release.x86_64 = "android/libgodot-jolt_android-x64.so"
android.debug.x86_64 = "android/libgodot-jolt_android-x64_editor.so"
android.release.single.x86_64 = "android/libgodot-jolt_android-x64.so"
android.debug.single.x86_64 = "android/libgodot-jolt_android-x64_editor.so"
android.release.x86_32 = "android/libgodot-jolt_android-x86.so"
android.debug.x86_32 = "android/libgodot-jolt_android-x86_editor.so"
android.release.single.x86_32 = "android/libgodot-jolt_android-x86.so"
android.debug.single.x86_32 = "android/libgodot-jolt_android-x86_editor.so"

View file

@ -17,16 +17,35 @@
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<!--
HACK(mihe): This is to work around a bug in Godot 4.3-beta1, where it treats Framework
bundles the same as XCFramework bundles, and expects there to be an `AvailableLibraries`
entry, which is really only a thing in XCFramework bundles. Note that we also lie about the
binary path having a `.dylib` extension in order for Godot to correctly identify this as a
dynamically linked bundle.
-->
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>godot-jolt_ios.dylib</string>
</dict>
</array>
</dict>
</plist>

View file

@ -17,16 +17,35 @@
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<!--
HACK(mihe): This is to work around a bug in Godot 4.3-beta1, where it treats Framework
bundles the same as XCFramework bundles, and expects there to be an `AvailableLibraries`
entry, which is really only a thing in XCFramework bundles. Note that we also lie about the
binary path having a `.dylib` extension in order for Godot to correctly identify this as a
dynamically linked bundle.
-->
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>godot-jolt_ios_editor.dylib</string>
</dict>
</array>
</dict>
</plist>

View file

@ -17,11 +17,15 @@
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>

View file

@ -6,7 +6,7 @@
<dict>
<key>Resources/Info.plist</key>
<data>
HxpaqrKUN+D+lkF90Phqlb+CZKo=
et0C7sxAlu4eIDcq2ihFQ2BhDSk=
</data>
</dict>
<key>files2</key>
@ -15,7 +15,7 @@
<dict>
<key>hash2</key>
<data>
ZTtvl19PLRzCoTuDRMZ2FnJXIXsLYRhaBFxjroNsLDw=
ZnG0hD4DciikOVWrf1Ai1Qedz9hESuIFvUujZAebHRY=
</data>
</dict>
</dict>

View file

@ -17,11 +17,15 @@
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>

View file

@ -6,7 +6,7 @@
<dict>
<key>Resources/Info.plist</key>
<data>
CpU1kp9qZhdCVqKdSia+981vm40=
oIAzxlQz4Hun6JnLVOu9jafYxGE=
</data>
</dict>
<key>files2</key>
@ -15,7 +15,7 @@
<dict>
<key>hash2</key>
<data>
4CTRXI34pj3JIJzQDUUZTlAagKWQeohPQN20M0VCK4o=
FA6I/u5+Ww0DzXAvawYXs792eum+8Bim8uHBbg98jqY=
</data>
</dict>
</dict>

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60af
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e77
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a8
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a47881
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48

View file

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

View file

@ -1,23 +1,24 @@
<img src="doc/docs/images/terrain3d.png">
![Terrain3D Logo](/doc/docs/images/terrain3d.jpg)
# Terrain3D
A high performance, editable terrain system for Godot 4.
## Features
* Written in C++ as a GDExtension plugin, which works with official engine builds
* Written in C++ as a GDExtension addon, which works with official engine builds
* Can be accessed by GDScript, C#, and any language Godot supports
* Geometric Clipmap Mesh Terrain, as used in The Witcher 3. See [System Architecture](https://terrain3d.readthedocs.io/en/stable/docs/system_architecture.html)
* Up to 16k x 16k in 1k regions (imagine multiple islands without paying for 16k^2 vram)
* Up to 10 Levels of Detail (LODs)
* Up to 32 texture sets using albedo, normal, roughness, height
* Terrains as small as 64x64m up to 65.5x65.5km (4295km^2) in variable sized regions
* Up to 32 textures
* Up to 10 levels of detail
* Foliage instancing
* Sculpting, holes, texture painting, texture detiling, painting colors and wetness
* Supports importing heightmaps from [HTerrain](https://github.com/Zylann/godot_heightmap_plugin/), WorldMachine, Unity, Unreal and any tool that can export a heightmap (raw/r16/exr/+). See [importing data](https://terrain3d.readthedocs.io/en/stable/docs/import_export.html)
* Imports heightmaps from [HTerrain](https://github.com/Zylann/godot_heightmap_plugin/), WorldMachine, Unity, Unreal and any tool that can export a heightmap (raw/r16/exr/+). See [importing data](https://terrain3d.readthedocs.io/en/stable/docs/import_export.html)
See [Project Status](https://terrain3d.readthedocs.io/en/stable/docs/project_status.html) for details.
## Getting Started
1. Read through our [documentation](https://terrain3d.readthedocs.io/en/stable/index.html), starting with [Installation](https://terrain3d.readthedocs.io/en/stable/docs/installation.html).
1. Read the [Installation & Upgrades](https://terrain3d.readthedocs.io/en/stable/docs/installation.html) instructions.
2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) or join our [Discord server](https://tokisan.com/discord).
@ -51,5 +52,5 @@ Please see [CONTRIBUTING.md](https://github.com/TokisanGames/Terrain3D/blob/main
## License
This plugin has been released under the [MIT License](https://github.com/TokisanGames/Terrain3D/blob/main/LICENSE.txt).
This addon has been released under the [MIT License](https://github.com/TokisanGames/Terrain3D/blob/main/LICENSE.txt).

413
addons/terrain_3d/editor.gd Normal file
View file

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

View file

@ -1,41 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]
[ext_resource type="Script" path="res://addons/terrain_3d/editor/components/bake_lod_dialog.gd" id="1_57670"]
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 115)
visible = true
script = ExtResource("1_57670")
[node name="VBoxContainer" 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 = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "LOD:"
[node name="LodBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
max_value = 8.0
value = 4.0
[node name="DescriptionLabel" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2

View file

@ -1,212 +0,0 @@
extends Object
const WINDOW_SCENE: String = "res://addons/terrain_3d/editor/components/channel_packer.tscn"
const TEMPLATE_PATH: String = "res://addons/terrain_3d/editor/components/channel_packer_import_template.txt"
enum {
IMAGE_ALBEDO,
IMAGE_HEIGHT,
IMAGE_NORMAL,
IMAGE_ROUGHNESS,
}
var plugin: EditorPlugin
var editor_interface: EditorInterface
var dialog: AcceptDialog
var save_file_dialog: FileDialog
var open_file_dialog: FileDialog
var invert_green_checkbox: CheckBox
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
func pack_textures_popup() -> void:
if dialog != null:
print("Terrain3DChannelPacker: Cannot open pack tool, dialog already open.")
return
dialog = (load(WINDOW_SCENE) as PackedScene).instantiate()
dialog.confirmed.connect(_on_close_requested)
dialog.canceled.connect(_on_close_requested)
status_label = dialog.find_child("StatusLabel")
invert_green_checkbox = dialog.find_child("InvertGreenChannelCheckBox")
editor_interface = plugin.get_editor_interface()
_init_file_dialogs()
editor_interface.popup_dialog_centered(dialog)
_init_texture_picker(dialog.find_child("AlbedoVBox"), IMAGE_ALBEDO)
_init_texture_picker(dialog.find_child("HeightVBox"), IMAGE_HEIGHT)
_init_texture_picker(dialog.find_child("NormalVBox"), IMAGE_NORMAL)
_init_texture_picker(dialog.find_child("RoughnessVBox"), IMAGE_ROUGHNESS)
var pack_button_path: String = "Panel/MarginContainer/VBoxContainer/PackButton"
(dialog.get_node(pack_button_path) as Button).pressed.connect(_on_pack_button_pressed)
func _on_close_requested() -> void:
last_file_selected_fn = no_op
images = [null, null, null, null]
dialog.queue_free()
dialog = null
func _init_file_dialogs() -> void:
save_file_dialog = FileDialog.new()
save_file_dialog.set_filters(PackedStringArray(["*.png"]))
save_file_dialog.set_file_mode(FileDialog.FILE_MODE_SAVE_FILE)
save_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
save_file_dialog.file_selected.connect(_on_save_file_selected)
open_file_dialog = FileDialog.new()
open_file_dialog.set_filters(PackedStringArray(["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", ".ktx"]))
open_file_dialog.set_file_mode(FileDialog.FILE_MODE_OPEN_FILE)
open_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
dialog.add_child(save_file_dialog)
dialog.add_child(open_file_dialog)
func _init_texture_picker(p_parent: Node, p_image_index: int) -> void:
var line_edit: LineEdit = p_parent.find_child("LineEdit")
var file_pick_button: Button = p_parent.find_child("PickButton")
var clear_button: Button = p_parent.find_child("ClearButton")
var texture_rect: TextureRect = p_parent.find_child("TextureRect")
var texture_button: Button = p_parent.find_child("TextureButton")
var open_fn: Callable = func() -> void:
open_file_dialog.current_path = last_opened_directory
if last_file_selected_fn != no_op:
open_file_dialog.file_selected.disconnect(last_file_selected_fn)
last_file_selected_fn = func(path: String) -> void:
line_edit.text = path
line_edit.caret_column = path.length()
last_opened_directory = path.get_base_dir() + "/"
var image: Image = Image.new()
var code: int = image.load(path)
if code != OK:
_show_error("Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
_show_success("Loaded texture '" + path + "'")
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
open_file_dialog.file_selected.connect(last_file_selected_fn)
open_file_dialog.popup_centered_ratio()
var clear_fn: Callable = func() -> void:
line_edit.text = ""
texture_rect.texture = null
images[p_image_index] = null
# allow user to edit textbox and press enter because Godot's file picker doesn't work 100% of the time
var line_edit_submit_fn: Callable = func(path: String) -> void:
var image: Image = Image.new()
var code: int = image.load(path)
if code != OK:
_show_error("Failed to load texture '" + path + "'")
texture_rect.texture = null
images[p_image_index] = null
else:
texture_rect.texture = ImageTexture.create_from_image(image)
images[p_image_index] = image
line_edit.text_submitted.connect(line_edit_submit_fn)
file_pick_button.pressed.connect(open_fn)
texture_button.pressed.connect(open_fn)
clear_button.pressed.connect(clear_fn)
_set_button_icon(file_pick_button, "Folder")
_set_button_icon(clear_button, "Remove")
func _set_button_icon(p_button: Button, p_icon_name: String) -> void:
var editor_base: Control = editor_interface.get_base_control()
var icon: Texture2D = editor_base.get_theme_icon(p_icon_name, "EditorIcons")
p_button.icon = icon
func _show_error(p_text: String) -> void:
push_error("Terrain3DChannelPacker: " + p_text)
status_label.text = p_text
status_label.add_theme_color_override("font_color", Color(0.9, 0, 0))
func _show_success(p_text: String) -> void:
print("Terrain3DChannelPacker: " + p_text)
status_label.text = p_text
status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14))
func _create_import_file(png_path: String) -> void:
var dst_import_path: String = png_path + ".import"
var file: FileAccess = FileAccess.open(TEMPLATE_PATH, FileAccess.READ)
var template_content: String = file.get_as_text()
file.close()
var import_content: String = template_content.replace("$SOURCE_FILE", png_path)
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_error("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() + "/"
if packing_albedo:
_pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false)
else:
_pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path, invert_green_checkbox.button_pressed)
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")
func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool) -> void:
if p_rgb_image and p_a_image:
if p_rgb_image.get_size() != p_a_image.get_size():
_show_error("Textures must be the same size.")
return
var output_image: Image = Terrain3D.pack_image(p_rgb_image, p_a_image, p_invert_green)
if not output_image:
_show_error("Failed to pack textures.")
return
output_image.save_png(p_dst_path)
editor_interface.get_resource_filesystem().scan_sources()
_create_import_file(p_dst_path)
_show_success("Packed to " + p_dst_path + ".")
else:
_show_error("Failed to load one or more textures.")

View file

@ -1,284 +0,0 @@
extends PanelContainer
signal resource_changed(resource: Resource, index: int)
signal resource_inspected(resource: Resource)
signal resource_selected
var list: ListContainer
var entries: Array[ListEntry]
var selected_index: int = 0
func _init() -> void:
list = ListContainer.new()
var root: VBoxContainer = VBoxContainer.new()
var scroll: ScrollContainer = ScrollContainer.new()
var label: Label = Label.new()
label.set_text("Textures")
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
scroll.set_v_size_flags(SIZE_EXPAND_FILL)
scroll.set_h_size_flags(SIZE_EXPAND_FILL)
list.set_v_size_flags(SIZE_EXPAND_FILL)
list.set_h_size_flags(SIZE_EXPAND_FILL)
scroll.add_child(list)
root.add_child(label)
root.add_child(scroll)
add_child(root)
set_custom_minimum_size(Vector2(256, 448))
func _ready() -> void:
get_child(0).get_child(0).set("theme_override_styles/normal", get_theme_stylebox("bg", "EditorInspectorCategory"))
get_child(0).get_child(0).set("theme_override_fonts/font", get_theme_font("bold", "EditorFonts"))
get_child(0).get_child(0).set("theme_override_font_sizes/font_size",get_theme_font_size("bold_size", "EditorFonts"))
set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel"))
func clear() -> void:
for i in entries:
i.get_parent().remove_child(i)
i.queue_free()
entries.clear()
func add_item(p_resource: Resource = null) -> void:
var entry: ListEntry = ListEntry.new()
var index: int = entries.size()
entry.set_edited_resource(p_resource)
entry.selected.connect(set_selected_index.bind(index))
entry.inspected.connect(notify_resource_inspected)
entry.changed.connect(notify_resource_changed.bind(index))
if p_resource:
entry.set_selected(index == selected_index)
if not p_resource.id_changed.is_connected(set_selected_after_swap):
p_resource.id_changed.connect(set_selected_after_swap)
list.add_child(entry)
entries.push_back(entry)
func set_selected_after_swap(p_old_index: int, p_new_index: int) -> void:
set_selected_index(clamp(p_new_index, 0, entries.size() - 2))
func set_selected_index(p_index: int) -> void:
selected_index = p_index
emit_signal("resource_selected")
for i in entries.size():
var entry: ListEntry = entries[i]
entry.set_selected(i == selected_index)
func get_selected_index() -> int:
return selected_index
func notify_resource_inspected(p_resource: Resource) -> void:
emit_signal("resource_inspected", p_resource)
func notify_resource_changed(p_resource: Resource, p_index: int) -> void:
emit_signal("resource_changed", p_resource, p_index)
if !p_resource:
var last_offset: int = 2
if p_index == entries.size()-2:
last_offset = 3
selected_index = clamp(selected_index, 0, entries.size() - last_offset)
##############################################################
## class ListContainer
##############################################################
class ListContainer extends Container:
var height: float = 0
func _notification(p_what) -> void:
if p_what == NOTIFICATION_SORT_CHILDREN:
height = 0
var index: int = 0
var separation: float = 4
for c in get_children():
if is_instance_valid(c):
var width: float = size.x / 3
c.size = Vector2(width,width) - Vector2(separation, separation)
c.position = Vector2(index % 3, index / 3) * width + Vector2(separation/3, separation/3)
height = max(height, c.position.y + width)
index += 1
func _get_minimum_size() -> Vector2:
return Vector2(0, height)
##############################################################
## class ListEntry
##############################################################
class ListEntry extends VBoxContainer:
signal selected()
signal changed(resource: Terrain3DTexture)
signal inspected(resource: Terrain3DTexture)
var resource: Terrain3DTexture
var drop_data: bool = false
var is_hovered: bool = false
var is_selected: bool = false
var button_clear: TextureButton
var button_edit: TextureButton
var name_label: Label
@onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons")
@onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons")
@onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons")
@onready var background: StyleBox = get_theme_stylebox("pressed", "Button")
@onready var focus: StyleBox = get_theme_stylebox("focus", "Button")
func _ready() -> void:
var icon_size: Vector2 = Vector2(12, 12)
button_clear = TextureButton.new()
button_clear.set_texture_normal(clear_icon)
button_clear.set_custom_minimum_size(icon_size)
button_clear.set_h_size_flags(Control.SIZE_SHRINK_END)
button_clear.set_visible(resource != null)
button_clear.pressed.connect(clear)
add_child(button_clear)
button_edit = TextureButton.new()
button_edit.set_texture_normal(edit_icon)
button_edit.set_custom_minimum_size(icon_size)
button_edit.set_h_size_flags(Control.SIZE_SHRINK_END)
button_edit.set_visible(resource != null)
button_edit.pressed.connect(edit)
add_child(button_edit)
name_label = Label.new()
add_child(name_label, true)
name_label.visible = false
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
name_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
name_label.add_theme_color_override("font_shadow_color", Color.BLACK)
name_label.add_theme_constant_override("shadow_offset_x", 1)
name_label.add_theme_constant_override("shadow_offset_y", 1)
name_label.add_theme_font_size_override("font_size", 15)
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
name_label.text = "Add New"
func _notification(p_what) -> void:
match p_what:
NOTIFICATION_DRAW:
var rect: Rect2 = Rect2(Vector2.ZERO, get_size())
if !resource:
draw_style_box(background, rect)
draw_texture(add_icon, (get_size() / 2) - (add_icon.get_size() / 2))
else:
name_label.text = resource.get_name()
self_modulate = resource.get_albedo_color()
var texture: Texture2D = resource.get_albedo_texture()
if texture:
draw_texture_rect(texture, rect, false)
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10)
if drop_data:
draw_style_box(focus, rect)
if is_hovered:
draw_rect(rect, Color(1,1,1,0.2))
if is_selected:
draw_style_box(focus, rect)
NOTIFICATION_MOUSE_ENTER:
is_hovered = true
name_label.visible = true
queue_redraw()
NOTIFICATION_MOUSE_EXIT:
is_hovered = false
name_label.visible = false
drop_data = false
queue_redraw()
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.is_pressed():
match p_event.get_button_index():
MOUSE_BUTTON_LEFT:
# If `Add new` is clicked
if !resource:
set_edited_resource(Terrain3DTexture.new(), false)
edit()
else:
emit_signal("selected")
MOUSE_BUTTON_RIGHT:
if resource:
edit()
MOUSE_BUTTON_MIDDLE:
if resource:
clear()
func _can_drop_data(p_at_position: Vector2, p_data: Variant) -> bool:
drop_data = false
if typeof(p_data) == TYPE_DICTIONARY:
if p_data.files.size() == 1:
queue_redraw()
drop_data = true
return drop_data
func _drop_data(p_at_position: Vector2, p_data: Variant) -> void:
if typeof(p_data) == TYPE_DICTIONARY:
var res: Resource = load(p_data.files[0])
if res is Terrain3DTexture:
set_edited_resource(res, false)
if res is Texture2D:
var surf: Terrain3DTexture = Terrain3DTexture.new()
surf.set_albedo_texture(res)
set_edited_resource(surf, false)
func set_edited_resource(p_res: Terrain3DTexture, p_no_signal: bool = true) -> void:
resource = p_res
if resource:
resource.setting_changed.connect(_on_texture_changed)
resource.file_changed.connect(_on_texture_changed)
if button_clear:
button_clear.set_visible(resource != null)
queue_redraw()
if !p_no_signal:
emit_signal("changed", resource)
func _on_texture_changed() -> void:
emit_signal("changed", resource)
func set_selected(value: bool) -> void:
is_selected = value
queue_redraw()
func clear() -> void:
if resource:
set_edited_resource(null, false)
func edit() -> void:
emit_signal("selected")
emit_signal("inspected", resource)

View file

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

View file

@ -1,74 +0,0 @@
extends VBoxContainer
signal tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation)
const ICON_REGION_ADD: String = "res://addons/terrain_3d/icons/icon_map_add.svg"
const ICON_REGION_REMOVE: String = "res://addons/terrain_3d/icons/icon_map_remove.svg"
const ICON_HEIGHT_ADD: String = "res://addons/terrain_3d/icons/icon_height_add.svg"
const ICON_HEIGHT_SUB: String = "res://addons/terrain_3d/icons/icon_height_sub.svg"
const ICON_HEIGHT_MUL: String = "res://addons/terrain_3d/icons/icon_height_mul.svg"
const ICON_HEIGHT_DIV: String = "res://addons/terrain_3d/icons/icon_height_div.svg"
const ICON_HEIGHT_FLAT: String = "res://addons/terrain_3d/icons/icon_height_flat.svg"
const ICON_HEIGHT_SLOPE: String = "res://addons/terrain_3d/icons/icon_height_slope.svg"
const ICON_HEIGHT_SMOOTH: String = "res://addons/terrain_3d/icons/icon_height_smooth.svg"
const ICON_PAINT_TEXTURE: String = "res://addons/terrain_3d/icons/icon_brush.svg"
const ICON_SPRAY_TEXTURE: String = "res://addons/terrain_3d/icons/icon_spray.svg"
const ICON_COLOR: String = "res://addons/terrain_3d/icons/icon_color.svg"
const ICON_WETNESS: String = "res://addons/terrain_3d/icons/icon_wetness.svg"
const ICON_AUTOSHADER: String = "res://addons/terrain_3d/icons/icon_terrain_material.svg"
const ICON_HOLES: String = "res://addons/terrain_3d/icons/icon_holes.svg"
const ICON_NAVIGATION: String = "res://addons/terrain_3d/icons/icon_navigation.svg"
var tool_group: ButtonGroup = ButtonGroup.new()
func _init() -> void:
set_custom_minimum_size(Vector2(32, 0))
func _ready() -> void:
tool_group.connect("pressed", _on_tool_selected)
add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.ADD, "Add Region", load(ICON_REGION_ADD), tool_group)
add_tool_button(Terrain3DEditor.REGION, Terrain3DEditor.SUBTRACT, "Delete Region", load(ICON_REGION_REMOVE), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.ADD, "Raise", load(ICON_HEIGHT_ADD), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.SUBTRACT, "Lower", load(ICON_HEIGHT_SUB), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.MULTIPLY, "Expand (Away from 0)", load(ICON_HEIGHT_MUL), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.DIVIDE, "Reduce (Towards 0)", load(ICON_HEIGHT_DIV), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.REPLACE, "Flatten", load(ICON_HEIGHT_FLAT), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.GRADIENT, "Slope", load(ICON_HEIGHT_SLOPE), tool_group)
add_tool_button(Terrain3DEditor.HEIGHT, Terrain3DEditor.AVERAGE, "Smooth", load(ICON_HEIGHT_SMOOTH), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE, "Paint Base Texture", load(ICON_PAINT_TEXTURE), tool_group)
add_tool_button(Terrain3DEditor.TEXTURE, Terrain3DEditor.ADD, "Spray Overlay Texture", load(ICON_SPRAY_TEXTURE), tool_group)
add_tool_button(Terrain3DEditor.AUTOSHADER, Terrain3DEditor.REPLACE, "Autoshader", load(ICON_AUTOSHADER), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.COLOR, Terrain3DEditor.REPLACE, "Paint Color", load(ICON_COLOR), tool_group)
add_tool_button(Terrain3DEditor.ROUGHNESS, Terrain3DEditor.REPLACE, "Paint Wetness", load(ICON_WETNESS), tool_group)
add_child(HSeparator.new())
add_tool_button(Terrain3DEditor.HOLES, Terrain3DEditor.REPLACE, "Create Holes", load(ICON_HOLES), tool_group)
add_tool_button(Terrain3DEditor.NAVIGATION, Terrain3DEditor.REPLACE, "Paint Navigable Area", load(ICON_NAVIGATION), tool_group)
var buttons: Array[BaseButton] = tool_group.get_buttons()
buttons[0].set_pressed(true)
func add_tool_button(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation,
p_tip: String, p_icon: Texture2D, p_group: ButtonGroup) -> void:
var button: Button = Button.new()
button.set_meta("Tool", p_tool)
button.set_meta("Operation", p_operation)
button.set_tooltip_text(p_tip)
button.set_button_icon(p_icon)
button.set_button_group(p_group)
button.set_flat(true)
button.set_toggle_mode(true)
button.set_h_size_flags(SIZE_SHRINK_END)
add_child(button)
func _on_tool_selected(p_button: BaseButton) -> void:
emit_signal("tool_changed", p_button.get_meta("Tool", -1), p_button.get_meta("Operation", -1))

View file

@ -1,379 +0,0 @@
extends Node
#class_name Terrain3DUI Cannot be named until Godot #75388
# Includes
const Toolbar: Script = preload("res://addons/terrain_3d/editor/components/toolbar.gd")
const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd")
const TerrainTools: Script = preload("res://addons/terrain_3d/editor/components/terrain_tools.gd")
const OperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/operation_builder.gd")
const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/gradient_operation_builder.gd")
const RING1: String = "res://addons/terrain_3d/editor/brushes/ring1.exr"
const COLOR_RAISE := Color.WHITE
const COLOR_LOWER := Color.BLACK
const COLOR_SMOOTH := Color(0.5, 0, .1)
const COLOR_EXPAND := Color.ORANGE
const COLOR_REDUCE := Color.BLUE_VIOLET
const COLOR_FLATTEN := Color(0., 0.32, .4)
const COLOR_SLOPE := Color.YELLOW
const COLOR_PAINT := Color.FOREST_GREEN
const COLOR_SPRAY := Color.SEA_GREEN
const COLOR_ROUGHNESS := Color.ROYAL_BLUE
const COLOR_AUTOSHADER := Color.DODGER_BLUE
const COLOR_HOLES := Color.BLACK
const COLOR_NAVIGATION := Color.REBECCA_PURPLE
const COLOR_PICK_COLOR := Color.WHITE
const COLOR_PICK_HEIGHT := Color.DARK_RED
const COLOR_PICK_ROUGH := Color.ROYAL_BLUE
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var toolbar: Toolbar
var toolbar_settings: ToolSettings
var terrain_tools: TerrainTools
var setting_has_changed: bool = false
var visible: bool = false
var picking: int = Terrain3DEditor.TOOL_MAX
var picking_callback: Callable
var decal: Decal
var decal_timer: Timer
var gradient_decals: Array[Decal]
var brush_data: Dictionary
var operation_builder: OperationBuilder
@onready var picker_texture: ImageTexture = ImageTexture.create_from_image(Image.load_from_file(RING1))
func _enter_tree() -> void:
toolbar = Toolbar.new()
toolbar.hide()
toolbar.connect("tool_changed", _on_tool_changed)
toolbar_settings = ToolSettings.new()
toolbar_settings.connect("setting_changed", _on_setting_changed)
toolbar_settings.connect("picking", _on_picking)
toolbar_settings.hide()
terrain_tools = TerrainTools.new()
terrain_tools.plugin = plugin
terrain_tools.hide()
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_tools)
decal = Decal.new()
add_child(decal)
decal_timer = Timer.new()
decal_timer.wait_time = .5
decal_timer.one_shot = true
decal_timer.timeout.connect(Callable(func(node):
if node:
get_tree().create_tween().tween_property(node, "albedo_mix", 0.0, 0.15)).bind(decal))
add_child(decal_timer)
func _exit_tree() -> void:
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar)
plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
toolbar.queue_free()
toolbar_settings.queue_free()
terrain_tools.queue_free()
decal.queue_free()
decal_timer.queue_free()
for gradient_decal in gradient_decals:
gradient_decal.queue_free()
gradient_decals.clear()
func set_visible(p_visible: bool) -> void:
visible = p_visible
toolbar.set_visible(p_visible and plugin.terrain)
terrain_tools.set_visible(p_visible)
if p_visible and plugin.terrain:
p_visible = plugin.editor.get_tool() != Terrain3DEditor.REGION
toolbar_settings.set_visible(p_visible and plugin.terrain)
update_decal()
func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
clear_picking()
if not visible or not plugin.terrain:
return
if plugin.editor:
plugin.editor.set_tool(p_tool)
plugin.editor.set_operation(p_operation)
if p_tool != Terrain3DEditor.REGION:
# Select which settings to hide. Options:
# size, opactiy, height, slope, color, roughness, (height|color|roughness) picker
var to_hide: PackedStringArray = []
if p_tool == Terrain3DEditor.HEIGHT:
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
if p_operation != Terrain3DEditor.REPLACE:
to_hide.push_back("height")
to_hide.push_back("height picker")
if p_operation != Terrain3DEditor.GRADIENT:
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
elif p_tool == Terrain3DEditor.TEXTURE:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
if p_operation == Terrain3DEditor.REPLACE:
to_hide.push_back("opacity")
elif p_tool == Terrain3DEditor.COLOR:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
elif p_tool == Terrain3DEditor.ROUGHNESS:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("slope")
to_hide.push_back("enable")
elif p_tool in [ Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION ]:
to_hide.push_back("height")
to_hide.push_back("height picker")
to_hide.push_back("gradient_points")
to_hide.push_back("drawable")
to_hide.push_back("color")
to_hide.push_back("color picker")
to_hide.push_back("roughness")
to_hide.push_back("roughness picker")
to_hide.push_back("slope")
to_hide.push_back("opacity")
toolbar_settings.hide_settings(to_hide)
toolbar_settings.set_visible(p_tool != Terrain3DEditor.REGION)
operation_builder = null
if p_operation == Terrain3DEditor.GRADIENT:
operation_builder = GradientOperationBuilder.new()
operation_builder.tool_settings = toolbar_settings
_on_setting_changed()
plugin.update_region_grid()
func _on_setting_changed() -> void:
if not visible or not plugin.terrain:
return
brush_data = {
"size": int(toolbar_settings.get_setting("size")),
"opacity": toolbar_settings.get_setting("opacity") / 100.0,
"height": toolbar_settings.get_setting("height"),
"texture_index": plugin.texture_dock.get_selected_index(),
"color": toolbar_settings.get_setting("color"),
"roughness": toolbar_settings.get_setting("roughness"),
"gradient_points": toolbar_settings.get_setting("gradient_points"),
"enable": toolbar_settings.get_setting("enable"),
"automatic_regions": toolbar_settings.get_setting("automatic_regions"),
"align_to_view": toolbar_settings.get_setting("align_to_view"),
"show_cursor_while_painting": toolbar_settings.get_setting("show_cursor_while_painting"),
"gamma": toolbar_settings.get_setting("gamma"),
"jitter": toolbar_settings.get_setting("jitter"),
}
var brush_imgs: Array = toolbar_settings.get_setting("brush")
brush_data["image"] = brush_imgs[0]
brush_data["texture"] = brush_imgs[1]
update_decal()
plugin.editor.set_brush_data(brush_data)
func update_decal() -> void:
var mouse_buttons: int = Input.get_mouse_button_mask()
if not visible or \
not plugin.terrain or \
brush_data.is_empty() or \
mouse_buttons & MOUSE_BUTTON_RIGHT or \
(mouse_buttons & MOUSE_BUTTON_LEFT and not brush_data["show_cursor_while_painting"]) or \
plugin.editor.get_tool() == Terrain3DEditor.REGION:
decal.visible = false
for gradient_decal in gradient_decals:
gradient_decal.visible = false
return
else:
# Wait for cursor to recenter after right-click before revealing
# See https://github.com/godotengine/godot/issues/70098
await get_tree().create_timer(.05).timeout
decal.visible = true
decal.size = Vector3.ONE * brush_data["size"]
if brush_data["align_to_view"]:
var cam: Camera3D = plugin.terrain.get_camera();
if (cam):
decal.rotation.y = cam.rotation.y
else:
decal.rotation.y = 0
# Set texture and color
if picking != Terrain3DEditor.TOOL_MAX:
decal.texture_albedo = picker_texture
decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
match picking:
Terrain3DEditor.HEIGHT:
decal.modulate = COLOR_PICK_HEIGHT
Terrain3DEditor.COLOR:
decal.modulate = COLOR_PICK_COLOR
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_PICK_ROUGH
decal.modulate.a = 1.0
else:
decal.texture_albedo = brush_data["texture"]
match plugin.editor.get_tool():
Terrain3DEditor.HEIGHT:
match plugin.editor.get_operation():
Terrain3DEditor.ADD:
decal.modulate = COLOR_RAISE
Terrain3DEditor.SUBTRACT:
decal.modulate = COLOR_LOWER
Terrain3DEditor.MULTIPLY:
decal.modulate = COLOR_EXPAND
Terrain3DEditor.DIVIDE:
decal.modulate = COLOR_REDUCE
Terrain3DEditor.REPLACE:
decal.modulate = COLOR_FLATTEN
Terrain3DEditor.AVERAGE:
decal.modulate = COLOR_SMOOTH
Terrain3DEditor.GRADIENT:
decal.modulate = COLOR_SLOPE
_:
decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["opacity"])
Terrain3DEditor.TEXTURE:
match plugin.editor.get_operation():
Terrain3DEditor.REPLACE:
decal.modulate = COLOR_PAINT
decal.modulate.a = 1.0
Terrain3DEditor.ADD:
decal.modulate = COLOR_SPRAY
decal.modulate.a = max(.3, brush_data["opacity"])
_:
decal.modulate = Color.WHITE
Terrain3DEditor.COLOR:
decal.modulate = brush_data["color"].srgb_to_linear()*.5
decal.modulate.a = max(.3, brush_data["opacity"])
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_ROUGHNESS
decal.modulate.a = max(.3, brush_data["opacity"])
Terrain3DEditor.AUTOSHADER:
decal.modulate = COLOR_AUTOSHADER
decal.modulate.a = 1.0
Terrain3DEditor.HOLES:
decal.modulate = COLOR_HOLES
decal.modulate.a = 1.0
Terrain3DEditor.NAVIGATION:
decal.modulate = COLOR_NAVIGATION
decal.modulate.a = 1.0
_:
decal.modulate = Color.WHITE
decal.modulate.a = max(.3, brush_data["opacity"])
decal.size.y = max(1000, decal.size.y)
decal.albedo_mix = 1.0
decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 )
decal_timer.start()
for gradient_decal in gradient_decals:
gradient_decal.visible = false
if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT:
var index := 0
for point in brush_data["gradient_points"]:
if point != Vector3.ZERO:
var point_decal: Decal = _get_gradient_decal(index)
point_decal.visible = true
point_decal.position = point
index += 1
func _get_gradient_decal(index: int) -> Decal:
if gradient_decals.size() > index:
return gradient_decals[index]
var gradient_decal := Decal.new()
gradient_decal = Decal.new()
gradient_decal.texture_albedo = picker_texture
gradient_decal.modulate = COLOR_SLOPE
gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
gradient_decal.size.y = 1000.
gradient_decal.cull_mask = decal.cull_mask
add_child(gradient_decal)
gradient_decals.push_back(gradient_decal)
return gradient_decal
func set_decal_rotation(p_rot: float) -> void:
decal.rotation.y = p_rot
func _on_picking(p_type: int, p_callback: Callable) -> void:
picking = p_type
picking_callback = p_callback
update_decal()
func clear_picking() -> void:
picking = Terrain3DEditor.TOOL_MAX
func is_picking() -> bool:
if picking != Terrain3DEditor.TOOL_MAX:
return true
if operation_builder and operation_builder.is_picking():
return true
return false
func pick(p_global_position: Vector3) -> void:
if picking != Terrain3DEditor.TOOL_MAX:
var color: Color
match picking:
Terrain3DEditor.HEIGHT:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_HEIGHT, p_global_position)
Terrain3DEditor.ROUGHNESS:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position)
Terrain3DEditor.COLOR:
color = plugin.terrain.get_storage().get_color(p_global_position)
_:
push_error("Unsupported picking type: ", picking)
return
picking_callback.call(picking, color, p_global_position)
picking = Terrain3DEditor.TOOL_MAX
elif operation_builder and operation_builder.is_picking():
operation_builder.pick(p_global_position, plugin.terrain)

View file

@ -1,282 +0,0 @@
@tool
extends EditorPlugin
#class_name Terrain3DEditorPlugin Cannot be named until Godot #75388
# Includes
const UI: Script = preload("res://addons/terrain_3d/editor/components/ui.gd")
const RegionGizmo: Script = preload("res://addons/terrain_3d/editor/components/region_gizmo.gd")
const TextureDock: Script = preload("res://addons/terrain_3d/editor/components/texture_dock.gd")
var terrain: Terrain3D
var nav_region: NavigationRegion3D
var editor: Terrain3DEditor
var ui: Node # Terrain3DUI see Godot #75388
var texture_dock: TextureDock
var texture_dock_container: CustomControlContainer = CONTAINER_INSPECTOR_BOTTOM
var visible: bool
var region_gizmo: RegionGizmo
var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO
func _enter_tree() -> void:
editor = Terrain3DEditor.new()
ui = UI.new()
ui.plugin = self
add_child(ui)
texture_dock = TextureDock.new()
texture_dock.hide()
texture_dock.resource_changed.connect(_on_texture_dock_resource_changed)
texture_dock.resource_inspected.connect(_on_texture_dock_resource_selected)
texture_dock.resource_selected.connect(ui._on_setting_changed)
region_gizmo = RegionGizmo.new()
add_control_to_container(texture_dock_container, texture_dock)
texture_dock.get_parent().visibility_changed.connect(_on_texture_dock_visibility_changed)
func _exit_tree() -> void:
remove_control_from_container(texture_dock_container, texture_dock)
texture_dock.queue_free()
ui.queue_free()
editor.free()
func _handles(p_object: Object) -> bool:
return p_object is Terrain3D or p_object is NavigationRegion3D
func _edit(p_object: Object) -> void:
if !p_object:
_clear()
if p_object is Terrain3D:
if p_object == terrain:
return
terrain = p_object
editor.set_terrain(terrain)
region_gizmo.set_node_3d(terrain)
terrain.add_gizmo(region_gizmo)
terrain.set_plugin(self)
if not terrain.texture_list_changed.is_connected(_load_textures):
terrain.texture_list_changed.connect(_load_textures)
_load_textures()
if not terrain.storage_changed.is_connected(_load_storage):
terrain.storage_changed.connect(_load_storage)
_load_storage()
else:
terrain = null
if p_object is NavigationRegion3D:
nav_region = p_object
else:
nav_region = null
_update_visibility()
func _make_visible(p_visible: bool) -> void:
visible = p_visible
_update_visibility()
func _update_visibility() -> void:
ui.set_visible(visible)
texture_dock.set_visible(visible and terrain)
if terrain:
update_region_grid()
region_gizmo.set_hidden(not visible or not terrain)
func _clear() -> void:
if is_terrain_valid():
terrain.storage_changed.disconnect(_load_storage)
terrain.clear_gizmos()
terrain = null
editor.set_terrain(null)
ui.clear_picking()
region_gizmo.clear()
func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> int:
if not is_terrain_valid():
return AFTER_GUI_INPUT_PASS
## Handle mouse movement
if p_event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_PASS
## Get mouse location on terrain
# Snap terrain to current camera
terrain.set_camera(p_viewport_camera)
# Detect if viewport is set to half_resolution
# Structure is: Node3DEditorViewportContainer/Node3DEditorViewport/SubViewportContainer/SubViewport/Camera3D
var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true
# Project 2D mouse position to 3D position and direction
var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2
var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos)
# If region tool, grab mouse position without considering height
if editor.get_tool() == Terrain3DEditor.REGION:
var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir)
mouse_global_position = (camera_pos + t * camera_dir)
else:
# Else look for intersection with terrain
var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir)
if intersection_point.z > 3.4e38: # double max
return AFTER_GUI_INPUT_STOP
mouse_global_position = intersection_point
## Update decal
ui.decal.global_position = mouse_global_position
ui.decal.albedo_mix = 1.0
if ui.decal_timer.is_stopped():
ui.update_decal()
else:
ui.decal_timer.start()
## Update region highlight
var region_size = terrain.get_storage().get_region_size()
var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \
/ (region_size * terrain.get_mesh_vertex_spacing()) ).floor()
if current_region_position != region_position:
current_region_position = region_position
update_region_grid()
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and editor.is_operating():
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif p_event is InputEventMouseButton:
ui.update_decal()
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
if p_event.is_pressed():
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_STOP
# If picking
if ui.is_picking():
ui.pick(mouse_global_position)
if not ui.operation_builder or not ui.operation_builder.is_ready():
return AFTER_GUI_INPUT_STOP
# If adjusting regions
if editor.get_tool() == Terrain3DEditor.REGION:
# Skip regions that already exist or don't
var has_region: bool = terrain.get_storage().has_region(mouse_global_position)
var op: int = editor.get_operation()
if ( has_region and op == Terrain3DEditor.ADD) or \
( not has_region and op == Terrain3DEditor.SUBTRACT ):
return AFTER_GUI_INPUT_STOP
# If an automatic operation is ready to go (e.g. gradient)
if ui.operation_builder and ui.operation_builder.is_ready():
ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
# Mouse clicked, start editing
editor.start_operation(mouse_global_position)
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif editor.is_operating():
# Mouse released, save undo data
editor.stop_operation()
return AFTER_GUI_INPUT_STOP
return AFTER_GUI_INPUT_PASS
func is_terrain_valid() -> bool:
var valid: bool = false
if is_instance_valid(terrain):
valid = terrain.get_storage() != null
return valid
func update_texture_dock(p_args: Array) -> void:
texture_dock.clear()
if is_terrain_valid() and terrain.texture_list:
var texture_count: int = terrain.texture_list.get_texture_count()
for i in texture_count:
var texture: Terrain3DTexture = terrain.texture_list.get_texture(i)
texture_dock.add_item(texture)
if texture_count < Terrain3DTextureList.MAX_TEXTURES:
texture_dock.add_item()
func update_region_grid() -> void:
if !region_gizmo.get_node_3d():
return
if is_terrain_valid():
region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION
region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT
region_gizmo.region_position = current_region_position
region_gizmo.region_size = terrain.get_storage().get_region_size() * terrain.get_mesh_vertex_spacing()
region_gizmo.grid = terrain.get_storage().get_region_offsets()
terrain.update_gizmos()
return
region_gizmo.show_rect = false
region_gizmo.region_size = 1024
region_gizmo.grid = [Vector2i.ZERO]
# Signal handlers
func _load_textures() -> void:
if terrain and terrain.texture_list:
if not terrain.texture_list.textures_changed.is_connected(update_texture_dock):
terrain.texture_list.textures_changed.connect(update_texture_dock)
update_texture_dock(Array())
func _load_storage() -> void:
if terrain:
update_region_grid()
func _on_texture_dock_resource_changed(texture: Resource, index: int) -> void:
if is_terrain_valid():
# If removing last entry and its selected, clear inspector
if not texture and index == texture_dock.get_selected_index() and \
texture_dock.get_selected_index() == texture_dock.entries.size() - 2:
get_editor_interface().inspect_object(null)
terrain.get_texture_list().set_texture(index, texture)
call_deferred("_load_storage")
func _on_texture_dock_resource_selected(texture) -> void:
get_editor_interface().inspect_object(texture, "", true)
func _on_texture_dock_visibility_changed() -> void:
if texture_dock.get_parent() != null:
remove_control_from_container(texture_dock_container, texture_dock)
if texture_dock.get_parent() == null:
texture_dock_container = CONTAINER_INSPECTOR_BOTTOM
if get_editor_interface().is_distraction_free_mode_enabled():
texture_dock_container = CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT
add_control_to_container(texture_dock_container, texture_dock)

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