diff --git a/.clang-tidy b/.clang-tidy index b72cc52b..3d0adb17 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -40,5 +40,12 @@ Checks: "bugprone-*, -llvm-header-guard, -llvm-else-after-return, -*-unhandled-self-assignment, + -*-use-enum-class, + -*-use-ranges, + -readability-else-after-return, + -cppcoreguidelines-avoid-c-arrays, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-type-cstyle-cast, + -readability-redundant-declaration, -*-return-braced-init-list" WarningsAsErrors: "*" diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 4123419c..b6294439 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -7,7 +7,7 @@ on: jobs: clang-tidy: name: Run clang-tidy and upload report - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -40,6 +40,10 @@ jobs: id: run shell: bash run: | + sudo apt update + sudo add-apt-repository universe + sudo apt update + sudo apt install libncurses5-dev libtinfo5 set -o pipefail # Collect sources (mirror Makefile exclusions) mapfile -t FILES < <(find src/ \( -name "*.cpp" -o -name "*.hpp" -o -name "*.cc" -o -name "*.cxx" -o -name "*.c" -o -name "*.h" \) \ @@ -54,7 +58,7 @@ jobs: for f in "${FILES[@]}"; do echo "=== ${f} ===" | tee -a clang-tidy.log # Use project-local .clang-tidy implicitly - if ! clang-tidy -p build --quiet --fix-notes -header-filter=^src/ "$f" 2>>clang-tidy-errors.log | tee -a clang-tidy.log; then + if ! sudo clang-tidy -p build -extra-arg=-std=c++17 --quiet --fix-notes -header-filter=^src/ "$f" 2>>clang-tidy-errors.log | tee -a clang-tidy.log; then echo "clang-tidy reported non-zero status for $f" >> clang-tidy-errors.log fi done diff --git a/.gitignore b/.gitignore index 7c5ccee1..3e9301f6 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,5 @@ godot-cpp/CMakeFiles/godot-cpp.dir/gen/src/classes /Testing/ -build-*/ \ No newline at end of file +build-*/ +/.codex diff --git a/.gitmodules b/.gitmodules index 88826361..d0594f7f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,8 @@ [submodule "godot-cpp"] - path = godot-cpp - url = https://github.com/godotengine/godot-cpp.git - branch = 4.3 + path = godot-cpp + url = https://github.com/godotengine/godot-cpp.git + branch = 4.3 [submodule "vendor/gut"] - path = vendor/gut - url = https://github.com/bitwes/Gut.git + path = vendor/gut + url = https://github.com/bitwes/Gut.git + ignore = dirty diff --git a/CMakeLists.txt b/CMakeLists.txt index a80a780d..e3b182e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,13 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.17) SET(CMAKE_CXX_STANDARD 17) +SET(CMAKE_CXX_EXTENSIONS ON) +SET(CMAKE_COLOR_DIAGNOSTICS ON) +OPTION(LIBMASZYNA_DEBUG "If the build should contain custom library logger" OFF) # Do not hard‑force a generator; allow users to select one suitable for cross‑compiling. # Silence unused variable warning when specified from toolchain IF (CMAKE_C_COMPILER) ENDIF () -SET(CMAKE_GENERATOR Ninja) + # Allow selecting platform via -DGODOTCPP_PLATFORM=android|windows|linux|macos|ios|web IF (DEFINED GODOTCPP_PLATFORM AND NOT CMAKE_SYSTEM_NAME) STRING(TOLOWER "${GODOTCPP_PLATFORM}" _req_platform) @@ -15,10 +18,10 @@ IF (DEFINED GODOTCPP_PLATFORM AND NOT CMAKE_SYSTEM_NAME) # 1) Explicit toolchain file provided by user IF (DEFINED ANDROID_NDK_TOOLCHAIN) SET(CMAKE_TOOLCHAIN_FILE "${ANDROID_NDK_TOOLCHAIN}" CACHE FILEPATH "Android NDK toolchain (manual)" FORCE) - # 2) NDK root directory provided by user + # 2) NDK root directory provided by user ELSEIF (DEFINED ANDROID_NDK_ROOT) SET(CMAKE_TOOLCHAIN_FILE "${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake" CACHE FILEPATH "Android NDK toolchain (from ANDROID_NDK_ROOT)" FORCE) - # 3) Environment variables commonly used + # 3) Environment variables commonly used ELSEIF (DEFINED ENV{ANDROID_NDK}) SET(CMAKE_TOOLCHAIN_FILE "$ENV{ANDROID_NDK}/build/cmake/android.toolchain.cmake" CACHE FILEPATH "Android NDK toolchain (from ANDROID_NDK)" FORCE) ELSEIF (DEFINED ENV{ANDROID_NDK_HOME}) @@ -44,21 +47,21 @@ IF (DEFINED GODOTCPP_PLATFORM AND NOT CMAKE_SYSTEM_NAME) ENDIF () ELSEIF (_req_platform STREQUAL "windows") # Windows cross‑compile via MinGW (recommended from non‑Windows hosts) - IF (NOT DEFINED CMAKE_TOOLCHAIN_FILE) + IF (NOT DEFINED CMAKE_TOOLCHAIN_FILE AND NOT CMAKE_HOST_WIN32) # Best effort: if cross compilers are present, set them. This must be before project(). FIND_PROGRAM(MINGW_C "x86_64-w64-mingw32-gcc") FIND_PROGRAM(MINGW_CXX "x86_64-w64-mingw32-g++") IF (MINGW_C AND MINGW_CXX) SET(CMAKE_SYSTEM_NAME Windows CACHE STRING "" FORCE) - SET(CMAKE_C_COMPILER ${MINGW_C} CACHE FILEPATH "" FORCE) + SET(CMAKE_C_COMPILER ${MINGW_C} CACHE FILEPATH "" FORCE) SET(CMAKE_CXX_COMPILER ${MINGW_CXX} CACHE FILEPATH "" FORCE) MESSAGE(STATUS "Configuring cross‑compile for Windows using MinGW (x86_64).") - ELSE() + ELSE () # Fallback: still set system name; user must provide a toolchain or compilers. SET(CMAKE_SYSTEM_NAME Windows CACHE STRING "" FORCE) MESSAGE(WARNING "GODOTCPP_PLATFORM=windows set but no MinGW cross compiler auto‑detected. Provide a toolchain file or set CMAKE_C_COMPILER/CMAKE_CXX_COMPILER.") ENDIF () - ELSE() + ELSEIF (NOT CMAKE_HOST_WIN32) SET(CMAKE_SYSTEM_NAME Windows CACHE STRING "" FORCE) MESSAGE(STATUS "Configuring cross‑compile for Windows using specified toolchain: ${CMAKE_TOOLCHAIN_FILE}") ENDIF () @@ -88,6 +91,12 @@ PROJECT(Maszyna-API-Wrapper LANGUAGES CXX ) +IF (MSVC) + # /utf-8: Set source and execution character sets to UTF-8 + # /bigobj: Increase the number of addressable sections in an .obj file + ADD_COMPILE_OPTIONS(/utf-8 /bigobj) +ENDIF () + FILE(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS "src/*.cpp" "src/*.hpp") ADD_LIBRARY(${LIBNAME} SHARED ${SRC_FILES}) @@ -156,11 +165,28 @@ IF (GODOTCPP_TARGET STREQUAL "template_debug") SET(DEBUG_SUFFIX ".debug") ENDIF () +IF (LIBMASZYNA_DEBUG) + TARGET_COMPILE_DEFINITIONS(${LIBNAME} PRIVATE LIBMASZYNA_DEBUG) +ENDIF () + SET(_OUTPUT_NAME "${LIBNAME}${DEBUG_SUFFIX}${ARCH_SUFFIX}") SET(GODOT_PROJECT_BINARY_DIR "${PROJECT_SOURCE_DIR}/${GODOT_PROJECT_DIR}/bin/libmaszyna/${GODOTCPP_PLATFORM}") SET_TARGET_PROPERTIES(${LIBNAME} PROPERTIES OUTPUT_NAME "${_OUTPUT_NAME}" LIBRARY_OUTPUT_DIRECTORY "${GODOT_PROJECT_BINARY_DIR}") +# For multi-config generators (like Visual Studio), ensure output directory doesn't have a config subfolder +GET_PROPERTY(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +IF (IS_MULTI_CONFIG) + FOREACH (CONFIG ${CMAKE_CONFIGURATION_TYPES}) + STRING(TOUPPER ${CONFIG} CONFIG) + SET_TARGET_PROPERTIES(${LIBNAME} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY_${CONFIG} "${GODOT_PROJECT_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_${CONFIG} "${GODOT_PROJECT_BINARY_DIR}" + ARCHIVE_OUTPUT_DIRECTORY_${CONFIG} "${GODOT_PROJECT_BINARY_DIR}" + ) + ENDFOREACH () +ENDIF () + SET(ADDONS_DEMO_DIR "${PROJECT_SOURCE_DIR}/${GODOT_PROJECT_DIR}/addons") ADD_CUSTOM_COMMAND(TARGET ${LIBNAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "${ADDONS_DEMO_DIR}" diff --git a/Makefile b/Makefile index 738ebcdb..5d0851ad 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ GITREV=$(shell git rev-parse --abbrev-ref HEAD | sed -e 's/[^A-Za-z0-9]//g') DATE=$(shell date +"%Y%m%d") +CMAKE_BUILD_JOBS=$(shell cores=$$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1); if [ "$$cores" -gt 2 ]; then echo $$((cores - 2)); else echo 1; fi) +LIBMASZYNA_DEBUG:="" docs: cd demo && godot --doctool .. --gdextension-docs @@ -14,14 +16,25 @@ cleanup: rm -rf demo/addons/gut +cleanup-build-debug: + rm -rf build-debug + + +cleanup-build-release: + rm -rf build-release + + +cleanup-builds: cleanup-build-debug cleanup-build-release + + compile-debug: - cmake -B build-debug -DGODOTCPP_TARGET=template_debug - cmake --build build-debug + cmake -B build-debug -DGODOTCPP_TARGET=template_debug -DLIBMASZYNA_DEBUG=$(LIBMASZYNA_DEBUG) + cmake --build build-debug --parallel $(CMAKE_BUILD_JOBS) compile-release: cmake -B build-release -DGODOTCPP_TARGET=template_release - cmake --build build-release + cmake --build build-release --parallel $(CMAKE_BUILD_JOBS) compile-all: compile-debug compile-release @@ -30,7 +43,7 @@ compile-all: compile-debug compile-release cross-compile-release: cmake -B build-linux64 \ -DGODOTCPP_TARGET="template_release" - cmake --build build-linux64 + cmake --build build-linux64 --parallel $(CMAKE_BUILD_JOBS) cmake -B build-win64 \ -DGODOTCPP_TARGET="template_release" \ -DGODOTCPP_PLATFORM=windows \ @@ -38,13 +51,13 @@ cross-compile-release: -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \ -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ \ -DCMAKE_SIZEOF_VOID_P=8 - cmake --build build-win64 + cmake --build build-win64 --parallel $(CMAKE_BUILD_JOBS) cross-compile-debug: cmake -B build-linux64 \ - -DGODOTCPP_TARGET="template_debug" - cmake --build build-linux64 + -DGODOTCPP_TARGET="template_debug" -DLIBMASZYNA_DEBUG=ON + cmake --build build-linux64 --parallel $(CMAKE_BUILD_JOBS) cmake -B build-win64 \ -DGODOTCPP_TARGET="template_debug" \ -DGODOTCPP_PLATFORM=windows \ @@ -52,13 +65,13 @@ cross-compile-debug: -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \ -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ \ -DCMAKE_SIZEOF_VOID_P=8 - cmake --build build-win64 + cmake --build build-win64 --parallel $(CMAKE_BUILD_JOBS) release-linux: cmake -B build-linux64 \ -DGODOTCPP_TARGET="template_release" - cmake --build build-linux64 + cmake --build build-linux64 --parallel $(CMAKE_BUILD_JOBS) cd demo && godot --headless --export-release "linux_x86_64" ../bin/linux/reloaded.zip && mv ../bin/linux/reloaded.zip ../bin/linux/reloaded-$(GITREV)-$(DATE)-linux.zip @@ -70,7 +83,7 @@ release-windows: -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \ -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ \ -DCMAKE_SIZEOF_VOID_P=8 - cmake --build build-win64 + cmake --build build-win64 --parallel $(CMAKE_BUILD_JOBS) cd demo && godot --headless --export-release "windows_x86_64" ../bin/windows/reloaded.zip && mv ../bin/windows/reloaded.zip ../bin/windows/reloaded-$(GITREV)-$(DATE)-windows.zip @@ -113,4 +126,4 @@ docker-run-tests: docker-build-tests run-tests: compile - godot --path demo --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/ -gexit \ No newline at end of file + godot --path demo --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/,res://addons/gnd_sfx/tests/ -gexit diff --git a/README.md b/README.md index 66c41d75..c1d675ee 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ git submodule update --init --recursive For build system setup, please take a look at [official Godot Engine documentation for Android development](https://docs.godotengine.org/en/4.3/tutorials/export/exporting_for_android.html) ### Compiling +> [!IMPORTANT] +> If you are using DEBUG macro, you should build CMake project with flag `-DLIBMASZYNA_DEBUG=ON` and then compile it. +> When using make ,add `LIBMASZYNA_DEBUG=ON` to a command line, i.e. `make compile-debug LIBMASZYNA_DEBUG=ON`. You can +> also turn off DEBUG using `LIBMASZYNA_DEBUG=OFF`. + ```bash cmake -B build- \ -DGODOTCPP_TARGET="template_release" @@ -49,6 +54,55 @@ Cross-compiling (for Windows on Linux): -DCMAKE_SIZEOF_VOID_P=8 cmake --build build-win64 ``` + +#### Makefile + +You can use Makefile targets as shortcuts. + +For debug compilation: + +``` +make compile-debug +``` + +or just simply + +``` +make +``` + +For release compilation: + +``` +make compile-release +``` + +For cross compilation linux/windows: + +``` +make cross-compile-debug +``` + +or + +``` +make cross-compile-release +``` + +#### Parallel compilation + +To enable parallel compilation add `--parallel ` argument to cmake calls, for example: + +```bash + cmake -B build-linux64 \ + -DGODOTCPP_TARGET="template_release" + --parallel 4 + cmake --build build-linux64 --parallel 4 +``` + +If you're using `Makefile`, number of parralel jobs will be set automatically to ` - 2` for system with at +least two cores. + ### Compatibility | Plugin Version | Godot Engine version | Windows | Linux | Mac OS | Android | iOS | C++ Standard | MaSzyna Version | @@ -83,7 +137,7 @@ git submodule update --init Then run tests from the command line (if you have `make` installed, you can use shortcut `make run-tests`): ```bash -godot --path demo --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/ -gexit +godot --path demo --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/,res://addons/gnd_sfx/tests/ -gexit ``` diff --git a/addons/libmaszyna/cabin/base_cabin_tool_3d.gd b/addons/libmaszyna/cabin/base_cabin_tool_3d.gd index e03dd4a9..6ed23848 100644 --- a/addons/libmaszyna/cabin/base_cabin_tool_3d.gd +++ b/addons/libmaszyna/cabin/base_cabin_tool_3d.gd @@ -18,6 +18,14 @@ func _process_dirty(delta): func _process_tool(delta): pass +func get_cabin() -> Cabin3D: + var node = get_parent() + while node: + if node is Cabin3D: + return node + node = node.get_parent() + return null + func _process(delta): if _dirty: _dirty = false diff --git a/addons/libmaszyna/cabin/cabin_3d.gd b/addons/libmaszyna/cabin/cabin_3d.gd index 6519a0cb..22648643 100644 --- a/addons/libmaszyna/cabin/cabin_3d.gd +++ b/addons/libmaszyna/cabin/cabin_3d.gd @@ -7,6 +7,8 @@ var _dirty = true var _cabin_ready:bool = false var _e3d_instances:Array[E3DModelInstance] = [] var _e3d_loaded_count:int = 0 +var controller: TrainController + @export_node_path("TrainController") var controller_path:NodePath = NodePath(""): set(x): @@ -18,11 +20,16 @@ var _e3d_loaded_count:int = 0 @export var camera_bound_max = Vector3.ZERO @export var camera_bound_enabled:bool = false @export var driver_position = Vector3.ZERO +@export var sound_bank:SfxBank func get_camera_transform(): return global_transform.translated_local(driver_position) +func is_cabin_ready() -> bool: + return _cabin_ready + func _propagate_train_controller(node: Node, controller: TrainController): + self.controller = controller for child in node.get_children(): _propagate_train_controller(child, controller) if "controller_path" in child: diff --git a/addons/libmaszyna/cabin/cabin_blinker.tscn b/addons/libmaszyna/cabin/cabin_blinker.tscn index af9075cd..e2f04eec 100644 --- a/addons/libmaszyna/cabin/cabin_blinker.tscn +++ b/addons/libmaszyna/cabin/cabin_blinker.tscn @@ -2,9 +2,9 @@ [ext_resource type="Script" uid="uid://ccmsi0wq5sq3s" path="res://addons/libmaszyna/cabin/cabin_blinker.gd" id="1_q1beh"] -[node name="Blinker" type="Node3D" unique_id=212943250] +[node name="Blinker" type="Node3D" unique_id=447458378] script = ExtResource("1_q1beh") -[node name="Timer" type="Timer" parent="." unique_id=114747919] +[node name="Timer" type="Timer" parent="." unique_id=666450899] [connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"] diff --git a/addons/libmaszyna/cabin/cabin_button.gd b/addons/libmaszyna/cabin/cabin_button.gd index 4efa014c..b4063ea7 100644 --- a/addons/libmaszyna/cabin/cabin_button.gd +++ b/addons/libmaszyna/cabin/cabin_button.gd @@ -39,8 +39,7 @@ enum ControllerMode { OnOff, On, Off } if pushed: _target_mesh_rotation = x @export var speed = 10.0 -@export var sound_on:AudioStream -@export var sound_off:AudioStream +@export var sound_event = "cabin_button" @export var sound_max_distance:float = 3.0: set(x): sound_max_distance = x @@ -59,17 +58,24 @@ var _t:float = 0.0 var _enabled:bool = true var _setup_phase:bool = true -var _sound:AudioStreamPlayer3D = AudioStreamPlayer3D.new() +var _sound:SfxPlayer3D = SfxPlayer3D.new() func _ready(): - add_child(_sound) _sound.max_distance = sound_max_distance + _sound.max_tracks = 2 + var cabin = get_cabin() + if cabin: + _sound.bank = cabin.sound_bank connect("pushed_changed", self._on_pushed_changed) controller_changed.connect(_update_state) Console.console_toggled.connect(_on_console_toggled) func _enter_tree(): _setup_phase = true + add_child(_sound) + +func _exit_tree() -> void: + remove_child(_sound) func _on_console_toggled(visible:bool): _enabled = not visible @@ -124,9 +130,10 @@ func _process_tool(delta): _mesh.position = _mesh_original_position + _current_position func _play_sound(): - _sound.stream = sound_on if pushed else sound_off - if _sound.stream: - _sound.play() + if pushed: + _sound.play(sound_event) + else: + _sound.stop(sound_event) func _on_pushed_changed(): if pushed: diff --git a/addons/libmaszyna/cabin/cabin_button.tscn b/addons/libmaszyna/cabin/cabin_button.tscn index 16ed4507..8919bc69 100644 --- a/addons/libmaszyna/cabin/cabin_button.tscn +++ b/addons/libmaszyna/cabin/cabin_button.tscn @@ -2,5 +2,5 @@ [ext_resource type="Script" uid="uid://brys456def41l" path="res://addons/libmaszyna/cabin/cabin_button.gd" id="1_glqim"] -[node name="CabinButton" type="Node3D" unique_id=863207262] +[node name="CabinButton" type="Node3D" unique_id=167396750] script = ExtResource("1_glqim") diff --git a/addons/libmaszyna/cabin/cabin_command.tscn b/addons/libmaszyna/cabin/cabin_command.tscn index dd6d7cee..ee99d5c4 100644 --- a/addons/libmaszyna/cabin/cabin_command.tscn +++ b/addons/libmaszyna/cabin/cabin_command.tscn @@ -1,6 +1,6 @@ -[gd_scene format=3 uid="uid://djl581jijicpq"] +[gd_scene load_steps=2 format=3 uid="uid://djl581jijicpq"] [ext_resource type="Script" uid="uid://dvmp4jpa1j5nw" path="res://addons/libmaszyna/cabin/cabin_command.gd" id="1_8wubd"] -[node name="CabinCommand" type="Node" unique_id=694387515] +[node name="CabinCommand" type="Node"] script = ExtResource("1_8wubd") diff --git a/addons/libmaszyna/cabin/cabin_gauge.tscn b/addons/libmaszyna/cabin/cabin_gauge.tscn index d9cb4db2..527b37d8 100644 --- a/addons/libmaszyna/cabin/cabin_gauge.tscn +++ b/addons/libmaszyna/cabin/cabin_gauge.tscn @@ -2,5 +2,5 @@ [ext_resource type="Script" uid="uid://c4lj08dvqxvhn" path="res://addons/libmaszyna/cabin/cabin_gauge.gd" id="1_5vh3x"] -[node name="CabinGauge" type="Node3D" unique_id=114221161] +[node name="CabinGauge" type="Node3D" unique_id=258192697] script = ExtResource("1_5vh3x") diff --git a/addons/libmaszyna/cabin/cabin_knob.tscn b/addons/libmaszyna/cabin/cabin_knob.tscn index 8eac762b..0ec17caa 100644 --- a/addons/libmaszyna/cabin/cabin_knob.tscn +++ b/addons/libmaszyna/cabin/cabin_knob.tscn @@ -1,6 +1,6 @@ -[gd_scene format=3 uid="uid://0s2ppfkln35s"] +[gd_scene load_steps=2 format=3 uid="uid://0s2ppfkln35s"] [ext_resource type="Script" uid="uid://cwpwb7k4icsh" path="res://addons/libmaszyna/cabin/cabin_knob.gd" id="1_5grf4"] -[node name="CabinKnob" type="Node3D" unique_id=824959808] +[node name="CabinKnob" type="Node3D"] script = ExtResource("1_5grf4") diff --git a/addons/libmaszyna/cabin/cabin_sound_3d.gd b/addons/libmaszyna/cabin/cabin_sound_3d.gd new file mode 100644 index 00000000..e6010a02 --- /dev/null +++ b/addons/libmaszyna/cabin/cabin_sound_3d.gd @@ -0,0 +1,32 @@ +extends BaseCabinTool3D +class_name CabinHasler + +@export var sound_event : StringName = "" +@export var speed_parameter : StringName = "speed" + +var _cabin:Cabin3D +var _sfx:SfxPlayer3D + +func _ready() -> void: + _cabin = get_cabin() + if not _cabin: + return + _sfx = SfxPlayer3D.new() + add_child(_sfx) + _sfx.bank = _cabin.sound_bank + +func _exit_tree() -> void: + if _sfx: + remove_child(_sfx) + _sfx = null + +func _process_tool(delta): + if not _cabin or not _sfx or not _controller: + return + var speed = _controller.state.get("speed", 0.0) + if speed > 0.1 and not _sfx.is_playing(sound_event): + _sfx.play(sound_event, 0.0, {speed_parameter: speed}) + elif speed <= 0.1 and _sfx.is_playing(sound_event): + _sfx.stop(sound_event, 0.0) + elif _sfx.is_playing(sound_event): + _sfx.modulate(sound_event, {speed_parameter: speed}) diff --git a/addons/libmaszyna/cabin/cabin_sound_3d.gd.uid b/addons/libmaszyna/cabin/cabin_sound_3d.gd.uid new file mode 100644 index 00000000..2c210b2f --- /dev/null +++ b/addons/libmaszyna/cabin/cabin_sound_3d.gd.uid @@ -0,0 +1 @@ +uid://cigrhgepj7tlg diff --git a/addons/libmaszyna/cabin/cabin_switch.gd b/addons/libmaszyna/cabin/cabin_switch.gd index 2fd4ea7d..afd159be 100644 --- a/addons/libmaszyna/cabin/cabin_switch.gd +++ b/addons/libmaszyna/cabin/cabin_switch.gd @@ -193,6 +193,7 @@ func _handle_position_change(prev, current) -> int: _controller.send_command(command_set, current) if state_property and _controller: - return _controller.state.get(state_property, current) + var state_value = _controller.state.get(state_property, current) + return state_value else: return current diff --git a/addons/libmaszyna/cabin/cabin_switch.tscn b/addons/libmaszyna/cabin/cabin_switch.tscn index aeee496e..a887f556 100644 --- a/addons/libmaszyna/cabin/cabin_switch.tscn +++ b/addons/libmaszyna/cabin/cabin_switch.tscn @@ -1,6 +1,6 @@ -[gd_scene format=3 uid="uid://cmluhfe2vkxcc"] +[gd_scene load_steps=2 format=3 uid="uid://cmluhfe2vkxcc"] [ext_resource type="Script" uid="uid://3hp38qtyx500" path="res://addons/libmaszyna/cabin/cabin_switch.gd" id="1_526md"] -[node name="CabinSwitch" type="Node3D" unique_id=239665158] +[node name="CabinSwitch" type="Node3D"] script = ExtResource("1_526md") diff --git a/addons/libmaszyna/console/console.gd b/addons/libmaszyna/console/console.gd index 14c13958..5bedfe42 100644 --- a/addons/libmaszyna/console/console.gd +++ b/addons/libmaszyna/console/console.gd @@ -62,6 +62,7 @@ func _ready() -> void: line_edit.anchor_bottom = 0.5 line_edit.placeholder_text = "Enter \"help\" for instructions" line_edit.focus_mode = Control.FOCUS_ALL + line_edit.keep_editing_on_text_submit = true control.add_child(line_edit) line_edit.text_submitted.connect(on_text_entered) line_edit.text_changed.connect(on_line_edit_text_changed) @@ -199,7 +200,7 @@ func set_visible(visible:bool) -> void: if (control.visible): was_paused_already = get_tree().paused get_tree().paused = was_paused_already || pause_enabled - line_edit.grab_focus() + _restore_line_edit_focus() console_opened.emit() console_toggled.emit(true) else: @@ -288,10 +289,10 @@ func on_text_entered(new_text : String) -> void: var arguments := text_split.slice(1) if arguments.size() < console_commands[text_command].required: - print_line("[color=light_coral] ERROR:[/color] Too few arguments! Required < %d >" % console_commands[text_command].required) + print_line("[color=light_coral] ERROR:[/color] Too few arguments! Required < %d >" % console_commands[text_command].required) return elif arguments.size() > console_commands[text_command].arguments.size(): - print_line("[color=light_coral] ERROR:[/color] Too many arguments! < %d > Max" % console_commands[text_command].arguments.size()) + print_line("[color=light_coral] ERROR:[/color] Too many arguments! < %d > Max" % console_commands[text_command].arguments.size()) return # Functions fail to call if passed the incorrect number of arguments, so fill out with blank strings. @@ -299,12 +300,10 @@ func on_text_entered(new_text : String) -> void: console_commands[text_command].function.callv(arguments) else: console_unknown_command.emit(text_command) - print_line("[color=light_coral] ERROR:[/color] Command not found.") + print_line("[color=light_coral] ERROR:[/color] Command not found.") await get_tree().process_frame - #line_edit.edit() - line_edit.grab_focus() - line_edit.grab_click_focus() + _restore_line_edit_focus() func on_line_edit_text_changed(new_text : String) -> void: reset_autocomplete() @@ -357,7 +356,7 @@ func commands_list() -> void: arguments_string += " [color=cornflower_blue]<" + console_commands[command].arguments[i] + ">[/color]" else: arguments_string += " <" + console_commands[command].arguments[i] + ">" - rich_label.append_text(" [color=light_green]%s[/color][color=gray]%s[/color]: %s\n" % [command, arguments_string, description]) + rich_label.append_text(" [color=light_green]%s[/color][color=gray]%s[/color]: %s\n" % [command, arguments_string, description]) rich_label.append_text("\n") @@ -392,3 +391,12 @@ func set_enable_on_release_build(enable : bool): if (!enable_on_release_build): if (!OS.is_debug_build()): disable() + + +func _restore_line_edit_focus() -> void: + if not control.visible: + return + + line_edit.grab_focus() + line_edit.grab_click_focus() + line_edit.edit() diff --git a/addons/libmaszyna/console/developer_console.tscn b/addons/libmaszyna/console/developer_console.tscn index 5a1270f7..519bc03c 100644 --- a/addons/libmaszyna/console/developer_console.tscn +++ b/addons/libmaszyna/console/developer_console.tscn @@ -1,6 +1,6 @@ -[gd_scene format=3 uid="uid://dgm10m7u26drx"] +[gd_scene load_steps=2 format=3 uid="uid://dgm10m7u26drx"] [ext_resource type="Script" uid="uid://bhqscvjsre587" path="res://addons/libmaszyna/console/developer_console.gd" id="1_vako3"] -[node name="DeveloperConsole" type="Node" unique_id=1071361171] +[node name="DeveloperConsole" type="Node"] script = ExtResource("1_vako3") diff --git a/addons/libmaszyna/e3d/colored.gdshader b/addons/libmaszyna/e3d/colored.gdshader deleted file mode 100644 index 4219d810..00000000 --- a/addons/libmaszyna/e3d/colored.gdshader +++ /dev/null @@ -1,7 +0,0 @@ -shader_type spatial; - -instance uniform vec3 albedo_color = vec3(1.0, 1.0, 1.0); - -void fragment() { - ALBEDO = albedo_color; -} diff --git a/addons/libmaszyna/e3d/colored.gdshader.uid b/addons/libmaszyna/e3d/colored.gdshader.uid deleted file mode 100644 index ccb8875a..00000000 --- a/addons/libmaszyna/e3d/colored.gdshader.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bqp4s6e2onl0q diff --git a/addons/libmaszyna/e3d/colored.material b/addons/libmaszyna/e3d/colored.material deleted file mode 100644 index 994062e9..00000000 Binary files a/addons/libmaszyna/e3d/colored.material and /dev/null differ diff --git a/addons/libmaszyna/e3d/e3d_model.gd b/addons/libmaszyna/e3d/e3d_model.gd deleted file mode 100644 index 366eb354..00000000 --- a/addons/libmaszyna/e3d/e3d_model.gd +++ /dev/null @@ -1,8 +0,0 @@ -extends Resource -class_name E3DModel - -@export var name:String = "" -@export var submodels:Array[E3DSubModel] = [] - -func add_child(submodel:E3DSubModel): - submodels.append(submodel) diff --git a/addons/libmaszyna/e3d/e3d_model.gd.uid b/addons/libmaszyna/e3d/e3d_model.gd.uid deleted file mode 100644 index 9a824a46..00000000 --- a/addons/libmaszyna/e3d/e3d_model.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dl5xj23epqumd diff --git a/addons/libmaszyna/e3d/e3d_model_instance.gd b/addons/libmaszyna/e3d/e3d_model_instance.gd deleted file mode 100644 index 89921157..00000000 --- a/addons/libmaszyna/e3d/e3d_model_instance.gd +++ /dev/null @@ -1,67 +0,0 @@ -@tool -extends VisualInstance3D -class_name E3DModelInstance - -signal e3d_loaded - -enum Instancer { - OPTIMIZED, - NODES, - EDITABLE_NODES, -} - -var default_aabb_size: Vector3 = Vector3(1, 1, 1) - -@export var data_path:String = "": - set(x): - if not x == data_path: - data_path = x - _reload() - -@export var model_filename:String = "": - set(x): - if not x == model_filename: - model_filename = x - _reload() - -@export var skins:Array = []: - set(x): - if not x == skins: - skins = x - _reload() - -@export var exclude_node_names:Array = []: - set(x): - if not x == exclude_node_names: - exclude_node_names = x - _reload() - -# Probably instancer should be set project-wide -@export var instancer = Instancer.NODES: - set(x): - if not x == instancer: - instancer = x - _reload() - -var submodels_aabb:AABB = AABB() -var editable_in_editor:bool = false: - set(x): - if not editable_in_editor == x: - editable_in_editor = x - _reload() - -func _get_aabb() -> AABB: - return submodels_aabb - -func _reload(): - E3DModelInstanceManager.reload_instance(self) - -func _notification(what: int) -> void: - match what: - NOTIFICATION_ENTER_TREE: - E3DModelInstanceManager.register_instance(self) - NOTIFICATION_EXIT_TREE: - E3DModelInstanceManager.unregister_instance(self) - -func _ready(): - _reload() diff --git a/addons/libmaszyna/e3d/e3d_model_instance.gd.uid b/addons/libmaszyna/e3d/e3d_model_instance.gd.uid deleted file mode 100644 index 8d006f51..00000000 --- a/addons/libmaszyna/e3d/e3d_model_instance.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dgifxvb2e4hww diff --git a/addons/libmaszyna/e3d/e3d_model_instance_manager.gd b/addons/libmaszyna/e3d/e3d_model_instance_manager.gd deleted file mode 100644 index 67cd64c0..00000000 --- a/addons/libmaszyna/e3d/e3d_model_instance_manager.gd +++ /dev/null @@ -1,50 +0,0 @@ -@tool -extends Node - -signal instances_reloaded - -var _t: float = 0.0 -var _needs_reload: bool = false -var _instances = [] - -func _on_user_setting_changed(section, key): - if section == "e3d" and key == "use_alpha_transparency": - reload_all() - -func _ready(): - UserSettings.game_dir_changed.connect(reload_all) - UserSettings.cache_cleared.connect(reload_all) - UserSettings.models_reload_requested.connect(reload_all) - UserSettings.setting_changed.connect(_on_user_setting_changed) - -func register_instance(instance: E3DModelInstance): - _instances.append(instance) - -func unregister_instance(instance: E3DModelInstance): - var idx = _instances.find(instance) - if idx > -1: - _instances.pop_at(idx) - -func reload_all(): - for instance in _instances: - reload_instance(instance) - instances_reloaded.emit() - -func reload_instance(instance: E3DModelInstance): - if instance.is_inside_tree() and instance.model_filename and instance.data_path: - if instance.data_path and instance.model_filename: - var _do_load = func(): - var model = E3DModelManager.load_model(instance.data_path, instance.model_filename) - if model: - match instance.instancer: - E3DModelInstance.Instancer.NODES: - var _do_instantiate = func(): - E3DNodesInstancer.instantiate(model, instance, instance.editable_in_editor) - #_update_head_display() - instance.e3d_loaded.emit() - SceneryResourceLoader.schedule( - "Creating model %s" % instance.name, _do_instantiate - ) - _: - push_error("Selected instancer is not supported!") - SceneryResourceLoader.schedule("Loading %s" % instance.name, _do_load) diff --git a/addons/libmaszyna/e3d/e3d_model_instance_manager.gd.uid b/addons/libmaszyna/e3d/e3d_model_instance_manager.gd.uid deleted file mode 100644 index 3028a83c..00000000 --- a/addons/libmaszyna/e3d/e3d_model_instance_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bt2jcd405kweg diff --git a/addons/libmaszyna/e3d/e3d_model_manager.gd b/addons/libmaszyna/e3d/e3d_model_manager.gd deleted file mode 100644 index 2700551a..00000000 --- a/addons/libmaszyna/e3d/e3d_model_manager.gd +++ /dev/null @@ -1,55 +0,0 @@ -@tool -extends Node - -const E3D_CACHE_PATH = "user://cache/e3d/res" - -func _ready(): - UserSettings.cache_clear_requested.connect(clear_cache) - -func clear_cache(): - if DirAccess.dir_exists_absolute(E3D_CACHE_PATH): - var err = FileUtils.remove_dir_recursively(E3D_CACHE_PATH) - if err == OK: - print("E3D/RES cache cleared") - else: - push_error("E3D/RES cache error: ", err) - -func _set_owner_recursive(node, new_owner): - if not node == new_owner: - node.owner = new_owner - if node.get_child_count(): - for kid in node.get_children(): - _set_owner_recursive(kid, new_owner) - -func load_model(data_path:String, filename: String) -> E3DModel: - var output:E3DModel - var path = UserSettings.get_maszyna_game_dir() + "/" + data_path + "/" + filename + ".e3d" - - # check users cache - var cached_path = E3D_CACHE_PATH+"/"+data_path+"__"+filename+".res" - var cached_meta_path = cached_path+".meta" - - var is_cache_valid = false - - if FileAccess.file_exists(path): - var orig_hash = str(FileAccess.get_modified_time(path)) - if FileAccess.file_exists(cached_path) and FileAccess.file_exists(cached_meta_path): - var cached_hash = FileAccess.get_file_as_string(cached_meta_path) - is_cache_valid = (cached_hash == orig_hash) - - if is_cache_valid: - output = load(cached_path) - else: - var res = load(path) # load external e3d - if res: - output = res - var cached_dir = cached_path.get_base_dir() - DirAccess.make_dir_recursive_absolute(cached_dir) - ResourceSaver.save(res, cached_path) - var meta = FileAccess.open(cached_meta_path, FileAccess.WRITE) - if meta: - meta.store_string(orig_hash) - meta.close() - else: - push_error("File does not exist: %s" % path) - return output diff --git a/addons/libmaszyna/e3d/e3d_model_manager.gd.uid b/addons/libmaszyna/e3d/e3d_model_manager.gd.uid deleted file mode 100644 index 2cc5200e..00000000 --- a/addons/libmaszyna/e3d/e3d_model_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b2v733o52i40x diff --git a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd deleted file mode 100644 index e013bdee..00000000 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd +++ /dev/null @@ -1,113 +0,0 @@ -@tool -extends Node - -var _colored_material = preload("res://addons/libmaszyna/e3d/colored.material") - -func instantiate(model:E3DModel, target_node:E3DModelInstance, editable:bool = false): - for child in target_node.get_children(true): - target_node.remove_child(child) - child.queue_free() - _do_add_submodels(target_node, target_node, model.submodels, editable) - -# Helper function to traverse and merge AABBs of VisualInstance3D descendants -func _traverse_and_extend(node: Node, combined_aabb: AABB, has_initialized_aabb: bool): - for child in node.get_children(): - if child is VisualInstance3D: - # Get the child's AABB and transform it to world space - var child_aabb:AABB = child.get_aabb() - #var transformed_aabb = child_aabb.transformed(child.global_transform) - var transformed_aabb:AABB = child_aabb - - # Merge into the combined AABB - if not has_initialized_aabb: - combined_aabb.position = transformed_aabb.position - combined_aabb.size = transformed_aabb.size - else: - combined_aabb = combined_aabb.merge(transformed_aabb) - - return _traverse_and_extend(child, combined_aabb, true) - return combined_aabb - -func _do_add_submodels(target_node:E3DModelInstance, parent, submodels, editable:bool): - for submodel in submodels: - var child:Node = _create_submodel_instance(target_node, submodel) - if child: - _update_submodel_material(target_node, child, submodel) - var internal = InternalMode.INTERNAL_MODE_DISABLED if editable else InternalMode.INTERNAL_MODE_BACK - parent.add_child(child, false, internal) - - # IMPORTANT: applying transform **after** adding to the tree - # Applying transform before adding may cause issues (especially on windows) - if submodel.transform: - child.transform = submodel.transform - - if Engine.is_editor_hint(): - child.owner = target_node.owner if editable else target_node - if submodel.submodels: - _do_add_submodels(target_node, child, submodel.submodels, editable) - -func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubModel): - var obj - - if submodel.skip_rendering: - return - - match submodel.submodel_type: - E3DSubModel.SubModelType.TRANSFORM: - obj = Node3D.new() - obj.name = submodel.name - - E3DSubModel.SubModelType.GL_TRIANGLES: - var is_name_excluded = target_node.exclude_node_names.any( - func(name): return submodel.name == name - ) - - if not is_name_excluded: - obj = MeshInstance3D.new() - obj.name = submodel.name - obj.mesh = submodel.mesh - obj.visibility_range_begin = submodel.visibility_range_begin - obj.visibility_range_end = submodel.visibility_range_end - - if obj: - obj.visible = submodel.visible - return obj - -func _update_submodel_material(target_node:E3DModelInstance, subnode:Node3D, submodel:E3DSubModel): - var unprefixed_model_path = "/".join(target_node.data_path.split("/").slice(1)) - if submodel.dynamic_material: - if target_node.skins.size() < submodel.dynamic_material_index + 1: - push_warning("Model %s has no skins set, but submodel requires material #%s" % [target_node.name, submodel.dynamic_material_index]) - else: - var transparency = ( - MaterialManager.Transparency.AlphaScissor - if submodel.material_transparent - else MaterialManager.Transparency.Disabled - ) - - var skin = target_node.skins[submodel.dynamic_material_index] - var material = MaterialManager.get_material(unprefixed_model_path, skin, transparency) - subnode.material_override = material - else: - if submodel.material_colored: - subnode.material_override = _colored_material - subnode.set_instance_shader_parameter("albedo_color", submodel.diffuse_color) - elif submodel.material_name: - var transparency = ( - MaterialManager.Transparency.AlphaScissor - if submodel.material_transparent - else MaterialManager.Transparency.Disabled - ) - subnode.material_override = MaterialManager.get_material( - unprefixed_model_path, - submodel.material_name, - transparency, - false, # model.is_sky, # unshaded if sky - submodel.diffuse_color, - ) - # FIXME: a workaround to tweak sorting for ALPHA materials - if subnode is MeshInstance3D: - var mi:MeshInstance3D = subnode as MeshInstance3D - var _m:BaseMaterial3D = subnode.material_override - if _m and _m.transparency == BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS: - mi.sorting_offset = -1 # FIXME: guessing diff --git a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd.uid b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd.uid deleted file mode 100644 index 74dbe9d0..00000000 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dl44jll5gimwi diff --git a/addons/libmaszyna/e3d/e3d_parser.gd b/addons/libmaszyna/e3d/e3d_parser.gd deleted file mode 100644 index 433ca22f..00000000 --- a/addons/libmaszyna/e3d/e3d_parser.gd +++ /dev/null @@ -1,383 +0,0 @@ -@tool -extends Node - -const MAX_31B = 1 << 31 -const MAX_32B = 1 << 32 - -func u32s(unsigned): - return (unsigned + MAX_31B) % MAX_32B - MAX_31B - -func _read_chunk_header(file): - var chunk_id = file.get_buffer(4).get_string_from_ascii() - var chunk_len = file.get_32() - - return {"id": chunk_id, "len": chunk_len, "data_len": chunk_len-8} - -func unsigned32_to_signed(unsigned): - return (unsigned + MAX_31B) % MAX_32B - MAX_31B - -func _calculate_normals(vertices: Array, indices: Array) -> Array: - var normals = [] - var has_indices = indices.size() > 0 - for i in range(vertices.size()): - normals.append(Vector3.ZERO) # Initialize all normals to zero - - # Loop through each triangle in the mesh - for i in range(0, indices.size(), 3): - # Get the indices of the vertices forming the triangle - - var i1 = indices[i] if has_indices else i - var i2 = indices[i + 1] if has_indices else i+1 - var i3 = indices[i + 2] if has_indices else i+2 - - # Get the vertices of the triangle - var v1 = vertices[i1] - var v2 = vertices[i2] - var v3 = vertices[i3] - - # Calculate the two edges of the triangle - var edge1 = v2 - v1 - var edge2 = v3 - v1 - - # Compute the face normal using the cross product - var normal = edge1.cross(edge2).normalized() * -1 - - # Accumulate the face normal into each vertex's normal - normals[i1] += normal - normals[i2] += normal - normals[i3] += normal - - # Normalize all the accumulated vertex normals - for i in range(normals.size()): - normals[i] = normals[i].normalized() - - return normals - - -func _read_submodel(file, chunk_size: int) -> Dictionary: - var result = {} - result["next_idx"] = unsigned32_to_signed(file.get_32()) - result["first_child_idx"] = unsigned32_to_signed(file.get_32()) - result["type"] = file.get_32() - result["name_idx"] = u32s(file.get_32()) - result["anim"] = unsigned32_to_signed(file.get_32()) - result["flags"] = file.get_32() & 0xFFFF - result["matrix_idx"] = unsigned32_to_signed(file.get_32()) - result["vertex_count"] = u32s(file.get_32()) - result["first_vertex_idx"] = u32s(file.get_32()) - result["material_idx"] = unsigned32_to_signed(file.get_32()) - - result["is_material_colored"] = true if result["material_idx"] == 0 else false - result["lights_on_threshold"] = file.get_float() - result["visibility_light_threshold"] = file.get_float() - file.get_buffer(16) # skip unused RGBA ambient - result["diffuse_color"] = Color( - file.get_float(), file.get_float(), file.get_float(), 1.0 - ) - file.get_float() # unused alpha - file.get_buffer(16) # skip unused RGBA specular - result["selfillum_color"] = Color( - file.get_float(), file.get_float(), file.get_float(), 1.0 - ) - file.get_float() # unused alpha - var _transparent = result["flags"] & 32 - if not _transparent: - result["selfillum_color"].a = 1.0 - result["diffuse_color"].a = 1.0 - - result["gl_lines_size"] = file.get_float() - - result["lod_max_distance"] = file.get_float() - result["lod_min_distance"] = file.get_float() - file.get_buffer(32) # skip attrs for lightg - result["index_count"] = file.get_32() - result["first_index_idx"] = file.get_32() - result["transparent"] = result["flags"] & 0b000001 - file.get_buffer(chunk_size-164) # read to the end of the chunk - result["vertices"] = PackedVector3Array() - result["normals"] = PackedVector3Array() - result["uv"] = PackedVector2Array() - result["indices"] = PackedInt32Array() - return result - -func _read_string0(file: FileAccess): - var output = [] - while true: - var c = file.get_8() - if c == 0: - break - output.append(char(c)) - return "".join(output) - -func _buffer_to_strings(buf) -> Array: - var output = [] - var tmp = [] - for i in range(0, buf.size(), 1): - var c = buf[i] - if c == 0: - output.append("".join(tmp)) - tmp = [] - else: - tmp.append(char(c)) - return output - -func _read_matrix(file: FileAccess): - var o = [] - for i in range(0, 16): - o.append(file.get_float()) - - return Transform3D( - Basis( - Vector3(o[0], o[1], o[2]), - Vector3(o[4], o[5], o[6]), - Vector3(o[8], o[9], o[10]) - ), - Vector3(o[12], o[13], o[14]) - ) - -func _parse_file(file: FileAccess, options:Dictionary): - var e3d0 = _read_chunk_header(file) - var submodels = [] - var submodel_names = [] - var material_names = [] - var submodel_vertices = [] - var submodel_normals = [] - var submodel_uvs = [] - var submodel_indices = [] - var submodel_tangents = [] - var matrices = [] - - while not file.eof_reached(): - var chunk = _read_chunk_header(file) - match chunk.id: - "SUB0": - var submodels_count = chunk.data_len / 256 - for i in range(0, submodels_count, 1): - var sm = _read_submodel(file, 256) - submodels.append(sm) - "SUB1": - var submodels_count = chunk.data_len / 320 - for i in range(0, submodels_count, 1): - var sm = _read_submodel(file, 320) - submodels.append(sm) - "NAM0": - submodel_names = _buffer_to_strings(file.get_buffer(chunk.data_len)) - "TEX0": - material_names = _buffer_to_strings(file.get_buffer(chunk.data_len)) - "TRA0": - var matrix_count = chunk.data_len / 64 - for i in range(0, matrix_count, 1): - matrices.append(_read_matrix(file)) - "IDX1": - var _pos = file.get_position() - for submodel in submodels: - file.seek(_pos + submodel["first_index_idx"]) - var indices = PackedInt32Array() - - for i in range(0, submodel["index_count"]): - indices.append(file.get_8()) - submodel_indices.append(indices) - file.seek(chunk.data_len + _pos) - "IDX2": - var _pos = file.get_position() - for submodel in submodels: - file.seek(_pos + submodel["first_index_idx"] * 2) - var indices = PackedInt32Array() - - for i in range(0, submodel["index_count"]): - indices.append(file.get_16()) - submodel_indices.append(indices) - file.seek(chunk.data_len + _pos) - "IDX4": - var _pos = file.get_position() - for submodel in submodels: - file.seek(_pos + submodel["first_index_idx"] * 4) - var indices = PackedInt32Array() - for i in range(0, submodel["index_count"]): - indices.append(file.get_32()) - submodel_indices.append(indices) - file.seek(chunk.data_len + _pos) - "VNT2": - # 8 x 2 bytes - var _pos = file.get_position() - for submodel in submodels: - file.seek(_pos + submodel["first_vertex_idx"] * 48) - var vertices = PackedVector3Array() - var normals = PackedVector3Array() - var uvs = PackedVector2Array() - var tangents = PackedFloat64Array() - - var bv = [] - var bn = [] - var bt = [] - var bu = [] - - for i in range(0, submodel["vertex_count"]): - var x = file.get_float() - var y = file.get_float() - var z = file.get_float() - var nx = file.get_float() - var ny = file.get_float() - var nz = file.get_float() - var u = file.get_float() - var v = file.get_float() - - # VNT2 contains tangents - var tx = file.get_float() - var ty = file.get_float() - var tz = file.get_float() - var tw = file.get_float() - var _vec = Vector3(x,y,z) - var _norm = Vector3(nx, ny, nz).normalized() - var _uv = Vector2(u, v) - - vertices.append(_vec) - normals.append(_norm) - uvs.append(_uv) - tangents.append_array([tx, ty, tz, tw]) - - submodel_vertices.append(vertices) - submodel_normals.append(normals) - submodel_uvs.append(uvs) - submodel_tangents.append(tangents) - file.seek(chunk.data_len + _pos) - _: - if chunk.id: - push_warning("Skipping unsupported chunk: " + chunk.id) - if chunk.data_len > 0: - file.get_buffer(chunk.data_len) - - var i = 0 - - for submodel in submodels: - var name_idx = submodel["name_idx"] - var tex_idx = submodel["material_idx"] - var mtx_idx = submodel["matrix_idx"] - if submodel_indices.size(): - submodel["indices"] = submodel_indices[i] - else: - submodel["indices"] = [] - - submodel["vertices"] = submodel_vertices[i] - submodel["normals"] = submodel_normals[i] - submodel["tangents"] = submodel_tangents[i] - submodel["uvs"] = submodel_uvs[i] - submodel["name"] = submodel_names[name_idx] if name_idx > -1 else null - submodel["material"] = material_names[tex_idx] if tex_idx > -1 else null - submodel["matrix"] = matrices[mtx_idx] if mtx_idx > -1 else null - i += 1 - return submodels - -func _create_submodel(submodel): - var obj = E3DSubModel.new() - obj.submodel_type = submodel["type"] - - obj.visible = true - obj.skip_rendering = false - - if submodel.get("name"): - obj.name = submodel["name"] - # Model3d.cpp TSubModel::BinInit() from the original exe - if obj.name.begins_with("Light_On"): - obj.visible = false - elif obj.name.to_lower().ends_with("_on"): - obj.visible = false - elif obj.name.to_lower().ends_with("_xon"): - obj.visible = false - elif obj.name == "cien": - obj.visible = false - obj.skip_rendering = true - # end Model3d.cpp TSubModel::BinInit() - - match submodel["type"]: - E3DSubModel.SubModelType.TRANSFORM: - if not obj.name: - obj.name = "banan" - obj.transform = submodel["matrix"] - return obj - - E3DSubModel.SubModelType.GL_TRIANGLES: - var _vc = submodel["vertices"].size() - var _nc = submodel["normals"].size() - var _uc = submodel["uvs"].size() - var _ic = submodel["indices"].size() - var _tc = submodel["tangents"].size() / 4 - - var _matname = submodel["material"].split(":")[0] if submodel["material"] else "" - var _ismatabs = _matname.begins_with("/") - - obj.animation = submodel["anim"] - - if submodel["material_idx"] < 0: - obj.dynamic_material = true - obj.dynamic_material_index = abs(submodel["material_idx"])-1 - - obj.material_name = _matname if _matname else "" - obj.material_transparent = submodel["flags"] & (1 << 5) - obj.material_colored = submodel["is_material_colored"] - obj.visibility_range_begin = sqrt(submodel["lod_min_distance"]) - obj.visibility_range_end = sqrt(submodel["lod_max_distance"]) - - obj.visibility_light = submodel["visibility_light_threshold"] - obj.lights_on_threshold = submodel["lights_on_threshold"] - obj.diffuse_color = submodel["diffuse_color"] - obj.self_illumination = submodel["selfillum_color"] - - if _vc > 0: - var mesh = ArrayMesh.new() - var triangles = [] - triangles.resize(ArrayMesh.ARRAY_MAX) - triangles[ArrayMesh.ARRAY_VERTEX] = submodel["vertices"] - var indices = submodel["indices"] - var ccw_indices = [] - for i in range(0, indices.size(), 3): - ccw_indices.append_array([indices[i], indices[i+2], indices[i+1]]) - submodel["indices"] = ccw_indices - var _normals = _calculate_normals(submodel["vertices"], submodel["indices"]) - if submodel["indices"].size() > 0: - triangles[ArrayMesh.ARRAY_INDEX] = PackedInt32Array(submodel["indices"]) - if submodel["normals"].size() > 0: - triangles[ArrayMesh.ARRAY_NORMAL] = submodel["normals"] - if submodel["tangents"].size() > 0: - triangles[ArrayMesh.ARRAY_TANGENT] = submodel["tangents"] - triangles[ArrayMesh.ARRAY_TEX_UV] = submodel["uvs"] - mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, triangles) - obj.mesh = mesh - obj.transform = submodel["matrix"] - return obj - _: - push_error("FIXME! Unsupported submodel type=%s name=%s" % [ - submodel["type"], - submodel["name"], - ]) - -func parse(file:FileAccess, options={}) -> E3DModel: - var node = E3DModel.new() - var submodels_meta = _parse_file(file, options) - var submodels = [] - - for meta in submodels_meta: - var subnode = _create_submodel(meta) - submodels.append(subnode) - - # apply parent/child relationships - for idx in range(submodels_meta.size()): - var meta = submodels_meta[idx] - var parent = submodels[idx] - if meta["first_child_idx"] > -1: - var child = submodels[meta["first_child_idx"]] - if child: - child.set_parent(parent) - if meta["next_idx"] > -1: - var next = submodels[meta["next_idx"]] - if next: - next.set_parent(submodels[idx].parent) - - # add root nodes to the model - node.add_child(submodels[0]) - var next_idx = submodels_meta[0]["next_idx"] - while next_idx > -1: - node.add_child(submodels[next_idx]) - next_idx = submodels_meta[next_idx]["next_idx"] - - return node diff --git a/addons/libmaszyna/e3d/e3d_parser.gd.uid b/addons/libmaszyna/e3d/e3d_parser.gd.uid deleted file mode 100644 index a8555110..00000000 --- a/addons/libmaszyna/e3d/e3d_parser.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d08el7q5iodf4 diff --git a/addons/libmaszyna/e3d/e3d_resource_format_loader.gd b/addons/libmaszyna/e3d/e3d_resource_format_loader.gd deleted file mode 100644 index d84db004..00000000 --- a/addons/libmaszyna/e3d/e3d_resource_format_loader.gd +++ /dev/null @@ -1,23 +0,0 @@ -@tool -extends ResourceFormatLoader -class_name E3DResourceFormatLoader - -var parser = load("res://addons/libmaszyna/e3d/e3d_parser.gd").new() - -func _get_recognized_extensions(): - return ["e3d"] - -func _handles_type(type): - return type == "Resource" - -func _get_resource_type(path: String): - return "Resource" - -func _load(path: String, original_path: String = "", use_sub_threads: bool = false, cache_mode: int = 0) -> Resource: - var file = FileAccess.open(path, FileAccess.READ) - if not file: - push_error("Failed to open E3D file: %s" % path) - return null - - var model = parser.parse(file) - return model diff --git a/addons/libmaszyna/e3d/e3d_resource_format_loader.gd.uid b/addons/libmaszyna/e3d/e3d_resource_format_loader.gd.uid deleted file mode 100644 index 832424be..00000000 --- a/addons/libmaszyna/e3d/e3d_resource_format_loader.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://duxeawg5078gr diff --git a/addons/libmaszyna/e3d/e3d_submodel.gd b/addons/libmaszyna/e3d/e3d_submodel.gd deleted file mode 100644 index 001139ce..00000000 --- a/addons/libmaszyna/e3d/e3d_submodel.gd +++ /dev/null @@ -1,73 +0,0 @@ -extends Resource -class_name E3DSubModel - -enum SubModelType { - GL_POINTS=0, - GL_LINES, - GL_LINE_STRIP, - GL_LINE_LOOP, - GL_TRIANGLES, - GL_TRIANGLE_STRIP, - GL_TRIANGLE_FAN, - GL_QUADS, - GL_QUAD_STRIP, - GL_POLYGON, - TRANSFORM = 256, - FREE_SPOTLIGHT, - STARS, -} - -enum AnimationType { - NONE=0, - ROTATE_VEC=1, - ROTATE_XYZ, - MOVE, - JUMP_SECONDS, - JUMP_MINUTES, - JUMP_HOURS, - JUMP_HOURS24, - SECONDS, - MINUTES, - HOURS, - HOURS24, - BILLBOARD, - WIND, - SKY, - DIGITAL, - DIGICLK, - UNDEFINED, - IK=256, - IK1=257, - IK2=258, - UNKOWN=-1 -} - -@export var name:String = "" -@export var submodel_type:SubModelType = SubModelType.GL_TRIANGLES -@export var animation = AnimationType.NONE -@export_range(-1.0, 1.0, 0.01) var lights_on_threshold:float = 0.0 -@export_range(-1.0, 1.0, 0.01) var visibility_light:float = 0.0 -@export var visibility_range_begin:float = 0.0 -@export var visibility_range_end:float = 0.0 -@export var diffuse_color:Color = Color.WHITE -@export var self_illumination:Color = Color.WHITE -@export var material_colored:bool = false -@export var dynamic_material:bool = false -@export var dynamic_material_index:int = 0 -@export var material_transparent:bool = false -@export var material_name:String = "" -@export var transform:Transform3D = Transform3D() -@export var mesh:ArrayMesh -@export var submodels:Array[E3DSubModel] = [] -@export var visible:bool = true -@export var skip_rendering:bool = false - -var parent:E3DSubModel - -func add_child(submodel:E3DSubModel): - submodels.append(submodel) - -func set_parent(submodel:E3DSubModel): - parent = submodel - if submodel: - submodel.add_child(self) diff --git a/addons/libmaszyna/e3d/e3d_submodel.gd.uid b/addons/libmaszyna/e3d/e3d_submodel.gd.uid deleted file mode 100644 index f0de4cc3..00000000 --- a/addons/libmaszyna/e3d/e3d_submodel.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://brcex5kook487 diff --git a/addons/libmaszyna/editor/toolbar_e3d_instance.tscn b/addons/libmaszyna/editor/toolbar_e3d_instance.tscn index 36628e5e..1d5c3652 100644 --- a/addons/libmaszyna/editor/toolbar_e3d_instance.tscn +++ b/addons/libmaszyna/editor/toolbar_e3d_instance.tscn @@ -1,8 +1,8 @@ -[gd_scene format=3 uid="uid://unrm26h4mwk4"] +[gd_scene load_steps=2 format=3 uid="uid://unrm26h4mwk4"] [ext_resource type="Script" uid="uid://bpk0y7dkvuqq8" path="res://addons/libmaszyna/editor/toolbar_e3d_instance.gd" id="1_h4jha"] -[node name="ToolbarE3dInstance" type="HBoxContainer" unique_id=1889642562] +[node name="ToolbarE3dInstance" type="HBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -10,7 +10,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_h4jha") -[node name="Editable" type="CheckButton" parent="." unique_id=1047319062] +[node name="Editable" type="CheckButton" parent="."] layout_mode = 2 disabled = true text = "Edit E3D" diff --git a/addons/libmaszyna/editor/user_settings_dock.gd b/addons/libmaszyna/editor/user_settings_dock.gd index fe1e1e70..5f04730d 100644 --- a/addons/libmaszyna/editor/user_settings_dock.gd +++ b/addons/libmaszyna/editor/user_settings_dock.gd @@ -25,15 +25,14 @@ func _on_directory_selector_dialog_dir_selected(dir): func _on_clear_cache_button_button_up(): var fn = func(): - UserSettings.cache_clear_requested.emit() - UserSettings.cache_cleared.emit() + ResourceCache.clear_all() call_func_with_message_window("Clering caches...", "Please wait.\nClearing caches in progress...", fn) func _on_reload_models_button_button_up(): var fn = func(): - UserSettings.models_reload_requested.emit() + E3DModelInstanceManager.reload_all() call_func_with_message_window("Reloading models...", "Please wait.\nModels reloading in progress...", fn) diff --git a/addons/libmaszyna/editor/user_settings_dock.tscn b/addons/libmaszyna/editor/user_settings_dock.tscn index 7a5039f9..ae72c491 100644 --- a/addons/libmaszyna/editor/user_settings_dock.tscn +++ b/addons/libmaszyna/editor/user_settings_dock.tscn @@ -4,7 +4,7 @@ [ext_resource type="Script" uid="uid://bc8likdrai4yp" path="res://addons/libmaszyna/editor/user_settings_check_button.gd" id="2_2cs74"] [ext_resource type="Script" uid="uid://c3o4dhxm72pj1" path="res://addons/libmaszyna/editor/user_settings_dropdown.gd" id="3_hwkpe"] -[node name="Maszyna Settings" type="PanelContainer" unique_id=1678337651] +[node name="Maszyna Settings" type="PanelContainer" unique_id=1999745953] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -12,27 +12,27 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_c24dg") -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=885395452] +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1959812118] layout_mode = 2 -[node name="GameDirSection" type="HBoxContainer" parent="VBoxContainer" unique_id=377977798] +[node name="GameDirSection" type="HBoxContainer" parent="VBoxContainer" unique_id=1683885278] layout_mode = 2 -[node name="Label" type="Label" parent="VBoxContainer/GameDirSection" unique_id=1700115177] +[node name="Label" type="Label" parent="VBoxContainer/GameDirSection" unique_id=1288935028] layout_mode = 2 text = "Game Folder" -[node name="LineEdit" type="LineEdit" parent="VBoxContainer/GameDirSection" unique_id=1672396982] +[node name="LineEdit" type="LineEdit" parent="VBoxContainer/GameDirSection" unique_id=1914735815] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -text = "/home/DoS/Stuff/Games/MaSzyna/" +text = "/home/marcin/Games/Maszyna" -[node name="Browse" type="Button" parent="VBoxContainer/GameDirSection" unique_id=1016640318] +[node name="Browse" type="Button" parent="VBoxContainer/GameDirSection" unique_id=137094385] layout_mode = 2 text = "Browse" -[node name="DirectorySelectorDialog" type="FileDialog" parent="VBoxContainer/GameDirSection" unique_id=223573776] +[node name="DirectorySelectorDialog" type="FileDialog" parent="VBoxContainer/GameDirSection" unique_id=1400514923] unique_name_in_owner = true title = "Open a Directory" size = Vector2i(700, 500) @@ -41,37 +41,21 @@ ok_button_text = "Select Current Folder" file_mode = 2 access = 2 -[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer" unique_id=1942549966] +[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer" unique_id=1186768995] layout_mode = 2 -[node name="ClearCacheButton" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=1384117938] +[node name="ClearCacheButton" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=2141868002] layout_mode = 2 text = "Clear caches" -[node name="ReloadModelsButton" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=123521632] +[node name="ReloadModelsButton" type="Button" parent="VBoxContainer/HBoxContainer2" unique_id=1495593205] layout_mode = 2 text = "Reload models" -[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer" unique_id=1076233621] +[node name="HBoxContainer5" type="HBoxContainer" parent="VBoxContainer" unique_id=1620455733] layout_mode = 2 -[node name="FXAAButton" type="CheckButton" parent="VBoxContainer/HBoxContainer3" unique_id=2048680945] -layout_mode = 2 -button_pressed = true -script = ExtResource("2_2cs74") -section = "render" -key = "fxaa_enabled" -default = true - -[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer3" unique_id=752000470] -layout_mode = 2 -size_flags_horizontal = 3 -text = "FXAA" - -[node name="HBoxContainer5" type="HBoxContainer" parent="VBoxContainer" unique_id=1448764628] -layout_mode = 2 - -[node name="SDFGI" type="CheckButton" parent="VBoxContainer/HBoxContainer5" unique_id=125688743] +[node name="SDFGI" type="CheckButton" parent="VBoxContainer/HBoxContainer5" unique_id=337442404] layout_mode = 2 button_pressed = true script = ExtResource("2_2cs74") @@ -79,15 +63,15 @@ section = "render" key = "sdfgi_enabled" default = true -[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer5" unique_id=1368345439] +[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer5" unique_id=1408733151] layout_mode = 2 size_flags_horizontal = 3 text = "SDFGI" -[node name="HBoxContainer6" type="HBoxContainer" parent="VBoxContainer" unique_id=1447691670] +[node name="HBoxContainer6" type="HBoxContainer" parent="VBoxContainer" unique_id=2081979506] layout_mode = 2 -[node name="SSIL" type="CheckButton" parent="VBoxContainer/HBoxContainer6" unique_id=222514818] +[node name="SSIL" type="CheckButton" parent="VBoxContainer/HBoxContainer6" unique_id=2074041979] layout_mode = 2 button_pressed = true script = ExtResource("2_2cs74") @@ -95,15 +79,15 @@ section = "render" key = "ssil_enabled" default = true -[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer6" unique_id=116580096] +[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer6" unique_id=1909974943] layout_mode = 2 size_flags_horizontal = 3 text = "SSIL" -[node name="HBoxContainer7" type="HBoxContainer" parent="VBoxContainer" unique_id=27069324] +[node name="HBoxContainer7" type="HBoxContainer" parent="VBoxContainer" unique_id=1483466242] layout_mode = 2 -[node name="SSAO" type="CheckButton" parent="VBoxContainer/HBoxContainer7" unique_id=2100588125] +[node name="SSAO" type="CheckButton" parent="VBoxContainer/HBoxContainer7" unique_id=64496364] layout_mode = 2 button_pressed = true script = ExtResource("2_2cs74") @@ -111,15 +95,15 @@ section = "render" key = "ssao_enabled" default = true -[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer7" unique_id=530557135] +[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer7" unique_id=930681462] layout_mode = 2 size_flags_horizontal = 3 text = "SSAO" -[node name="HBoxContainer8" type="HBoxContainer" parent="VBoxContainer" unique_id=912863219] +[node name="HBoxContainer8" type="HBoxContainer" parent="VBoxContainer" unique_id=733081414] layout_mode = 2 -[node name="SSR" type="CheckButton" parent="VBoxContainer/HBoxContainer8" unique_id=566231819] +[node name="SSR" type="CheckButton" parent="VBoxContainer/HBoxContainer8" unique_id=1018378059] layout_mode = 2 button_pressed = true script = ExtResource("2_2cs74") @@ -127,15 +111,15 @@ section = "render" key = "ssr_enabled" default = true -[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer8" unique_id=1908661653] +[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer8" unique_id=406498124] layout_mode = 2 size_flags_horizontal = 3 text = "SSR" -[node name="HBoxContainer4" type="HBoxContainer" parent="VBoxContainer" unique_id=466315222] +[node name="HBoxContainer4" type="HBoxContainer" parent="VBoxContainer" unique_id=324150563] layout_mode = 2 -[node name="VolumetricFog" type="CheckButton" parent="VBoxContainer/HBoxContainer4" unique_id=1551217817] +[node name="VolumetricFog" type="CheckButton" parent="VBoxContainer/HBoxContainer4" unique_id=1462240259] layout_mode = 2 button_pressed = true script = ExtResource("2_2cs74") @@ -143,29 +127,29 @@ section = "render" key = "volumetric_fog_enabled" default = true -[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer4" unique_id=1005592621] +[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer4" unique_id=1062160793] layout_mode = 2 text = "Volumetric Fog " -[node name="HBoxContainer9" type="HBoxContainer" parent="VBoxContainer" unique_id=1465897019] +[node name="HBoxContainer9" type="HBoxContainer" parent="VBoxContainer" unique_id=1102682888] layout_mode = 2 -[node name="E3DAlphaTransparency" type="CheckButton" parent="VBoxContainer/HBoxContainer9" unique_id=1546210739] +[node name="E3DAlphaTransparency" type="CheckButton" parent="VBoxContainer/HBoxContainer9" unique_id=501754166] layout_mode = 2 script = ExtResource("2_2cs74") section = "e3d" key = "use_alpha_transparency" -[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer9" unique_id=1170979303] +[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer9" unique_id=1906550362] layout_mode = 2 text = "E3D Alpha Transparency " -[node name="HBoxContainer11" type="HBoxContainer" parent="VBoxContainer" unique_id=1150743320] +[node name="HBoxContainer11" type="HBoxContainer" parent="VBoxContainer" unique_id=1613833729] layout_mode = 2 -[node name="VSync" type="CheckButton" parent="VBoxContainer/HBoxContainer11" unique_id=942552119] +[node name="VSync" type="CheckButton" parent="VBoxContainer/HBoxContainer11" unique_id=1840412047] layout_mode = 2 button_pressed = true script = ExtResource("2_2cs74") @@ -173,16 +157,39 @@ section = "window" key = "vsync_enabled" default = true -[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer11" unique_id=1448577649] +[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer11" unique_id=469098673] layout_mode = 2 text = "VSync" -[node name="HBoxContainer12" type="HBoxContainer" parent="VBoxContainer" unique_id=22609309] +[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer" unique_id=1087819934] +layout_mode = 2 + +[node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer3" unique_id=2138014315] +layout_mode = 2 +selected = 1 +item_count = 3 +popup/item_0/text = "Disabled" +popup/item_0/id = 0 +popup/item_1/text = "TAA" +popup/item_1/id = 1 +popup/item_2/text = "FXAA" +popup/item_2/id = 2 +script = ExtResource("3_hwkpe") +section = "render" +key = "antialias_mode" +default = 1 + +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer3" unique_id=1991793713] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Antialiasing" + +[node name="HBoxContainer12" type="HBoxContainer" parent="VBoxContainer" unique_id=448965713] layout_mode = 2 -[node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer12" unique_id=1687375827] +[node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer12" unique_id=1202089846] layout_mode = 2 -selected = 3 +selected = 0 item_count = 4 popup/item_0/text = "Disabled" popup/item_0/id = 0 @@ -196,16 +203,16 @@ script = ExtResource("3_hwkpe") section = "render" key = "msaa_3d" -[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer12" unique_id=663124461] +[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer12" unique_id=860265308] layout_mode = 2 text = "MSAA level" -[node name="HBoxContainer10" type="HBoxContainer" parent="VBoxContainer" unique_id=990117879] +[node name="HBoxContainer10" type="HBoxContainer" parent="VBoxContainer" unique_id=1957087656] layout_mode = 2 -[node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer10" unique_id=842794816] +[node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer10" unique_id=1506743323] layout_mode = 2 -selected = 5 +selected = 0 item_count = 6 popup/item_0/text = "Disabled" popup/item_0/id = 0 @@ -223,18 +230,18 @@ script = ExtResource("3_hwkpe") section = "render" key = "anisotropic_filtering_level" -[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer10" unique_id=631822608] +[node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer10" unique_id=276630104] layout_mode = 2 text = "Anisotropic filtering level" -[node name="InfoMessageWindow" type="Window" parent="." unique_id=308881657] +[node name="InfoMessageWindow" type="Window" parent="." unique_id=901209509] unique_name_in_owner = true initial_position = 4 size = Vector2i(300, 100) visible = false unresizable = true -[node name="FlowContainer" type="FlowContainer" parent="InfoMessageWindow" unique_id=496315848] +[node name="FlowContainer" type="FlowContainer" parent="InfoMessageWindow" unique_id=1876184802] anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 @@ -247,7 +254,7 @@ offset_bottom = 11.5 grow_horizontal = 2 grow_vertical = 2 -[node name="Label" type="Label" parent="InfoMessageWindow/FlowContainer" unique_id=1784172189] +[node name="Label" type="Label" parent="InfoMessageWindow/FlowContainer" unique_id=1423308702] layout_mode = 2 size_flags_horizontal = 3 horizontal_alignment = 1 diff --git a/addons/libmaszyna/libmaszyna.gd b/addons/libmaszyna/libmaszyna.gd index 19cb8c7c..e3682a2f 100644 --- a/addons/libmaszyna/libmaszyna.gd +++ b/addons/libmaszyna/libmaszyna.gd @@ -1,14 +1,10 @@ @tool extends EditorPlugin -var e3d_loader = E3DResourceFormatLoader.new() - # Custom nodes var maszyna_environment_node_script = preload("res://addons/libmaszyna/environment/maszyna_environment_node.gd") var maszyna_environment_node_icon = preload("res://addons/libmaszyna/environment/maszyna_environment_node_icon.png") -var e3d_model_instance_script = preload("res://addons/libmaszyna/e3d/e3d_model_instance.gd") -var e3d_model_instance_icon = preload("res://addons/libmaszyna/e3d/e3d_model_instance.png") # Editor plugins @@ -23,15 +19,10 @@ func _enter_tree(): add_custom_project_setting("maszyna/import_model_scale_factor", 1.0, TYPE_FLOAT) - add_autoload_singleton("SceneryResourceLoader", "res://addons/libmaszyna/scenery/scenery_resource_loader.gd") add_autoload_singleton("MaszynaEnvironment", "res://addons/libmaszyna/environment/maszyna_environment.gd") add_autoload_singleton("Console", "res://addons/libmaszyna/console/console.gd") - add_autoload_singleton("E3DParser", "res://addons/libmaszyna/e3d/e3d_parser.gd") - add_autoload_singleton("E3DModelManager", "res://addons/libmaszyna/e3d/e3d_model_manager.gd") - add_autoload_singleton("E3DNodesInstancer", "res://addons/libmaszyna/e3d/e3d_nodes_instancer.gd") - add_autoload_singleton("UserSettings", "res://addons/libmaszyna/settings/user_settings.gd") - add_autoload_singleton("E3DModelInstanceManager", "res://addons/libmaszyna/e3d/e3d_model_instance_manager.gd") add_autoload_singleton("AudioStreamManager", "res://addons/libmaszyna/sound/audio_stream_manager.gd") + add_autoload_singleton("UserSettings","res://addons/libmaszyna/settings/user_settings.gd" ) add_custom_type( "MaszynaEnvironmentNode", @@ -40,38 +31,25 @@ func _enter_tree(): maszyna_environment_node_icon, ) - add_custom_type( - "E3DModelInstance", - "VisualInstance3D", - e3d_model_instance_script, - e3d_model_instance_icon, - ) - - ResourceLoader.add_resource_format_loader(e3d_loader) user_settings_dock = user_settings_dock_scene.instantiate() add_control_to_dock(DOCK_SLOT_RIGHT_UL, user_settings_dock) func _exit_tree(): - remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, e3d_submodel_toolbar_instance) + if e3d_submodel_toolbar_instance: + remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, e3d_submodel_toolbar_instance) + e3d_submodel_toolbar_instance.queue_free() if user_settings_dock: remove_control_from_docks(user_settings_dock) + user_settings_dock.queue_free() - ResourceLoader.remove_resource_format_loader(e3d_loader) - - remove_custom_type("E3DModelInstance") remove_custom_type("MaszynaEnvironmentNode") remove_autoload_singleton("AudioStreamManager") - remove_autoload_singleton("E3DModelInstanceManager") - remove_autoload_singleton("UserSettings") - remove_autoload_singleton("E3DNodesInstancer") - remove_autoload_singleton("E3DModelManager") - remove_autoload_singleton("E3DParser") remove_autoload_singleton("Console") remove_autoload_singleton("MaszynaEnvironment") - remove_autoload_singleton("SceneryResourceLoader") + remove_autoload_singleton("UserSettings") func add_custom_project_setting(name: String, default_value, type: int, hint: int = PROPERTY_HINT_NONE, hint_string: String = "") -> void: diff --git a/addons/libmaszyna/libmaszyna.gdextension b/addons/libmaszyna/libmaszyna.gdextension index f9ec74c7..999b7101 100644 --- a/addons/libmaszyna/libmaszyna.gdextension +++ b/addons/libmaszyna/libmaszyna.gdextension @@ -24,3 +24,7 @@ android.debug.x86_64 = "res://bin/libmaszyna/android/libmaszyna.debug.64.so" android.release.x86_64 = "res://bin/libmaszyna/android/libmaszyna.64.so" android.debug.arm64 = "res://bin/libmaszyna/android/libmaszyna.debug.arm64.so" android.release.arm64 = "res://bin/libmaszyna/android/libmaszyna.arm64.so" + +[icons] + +E3DModelInstance = "res://addons/libmaszyna/e3d/e3d_model_instance.png" \ No newline at end of file diff --git a/addons/libmaszyna/player/player.tscn b/addons/libmaszyna/player/player.tscn index 580aed80..4e721114 100644 --- a/addons/libmaszyna/player/player.tscn +++ b/addons/libmaszyna/player/player.tscn @@ -1,4 +1,4 @@ -[gd_scene format=3 uid="uid://dmhikrkk2qsjl"] +[gd_scene load_steps=4 format=3 uid="uid://dmhikrkk2qsjl"] [ext_resource type="Script" uid="uid://cqet31gklr3sb" path="res://addons/libmaszyna/player/free_camera.gd" id="1_iuonp"] [ext_resource type="Script" uid="uid://dc3kl1adyw5y0" path="res://addons/libmaszyna/player/player.gd" id="1_t4mm8"] @@ -6,15 +6,15 @@ [sub_resource type="SphereShape3D" id="SphereShape3D_mexgx"] radius = 2.0 -[node name="Player" type="Node3D" unique_id=1819896729] +[node name="Player" type="Node3D"] script = ExtResource("1_t4mm8") -[node name="Camera3D" type="Camera3D" parent="." unique_id=2023831069] +[node name="Camera3D" type="Camera3D" parent="."] fov = 45.0 script = ExtResource("1_iuonp") deceleration = 70 -[node name="RailVehicleDetector" type="ShapeCast3D" parent="Camera3D" unique_id=626357363] +[node name="RailVehicleDetector" type="ShapeCast3D" parent="Camera3D"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1.88708) shape = SubResource("SphereShape3D_mexgx") target_position = Vector3(0, 0, 0) diff --git a/addons/libmaszyna/rail_vehicle_3d.gd b/addons/libmaszyna/rail_vehicle_3d.gd index 0200e030..aa453232 100644 --- a/addons/libmaszyna/rail_vehicle_3d.gd +++ b/addons/libmaszyna/rail_vehicle_3d.gd @@ -175,6 +175,7 @@ func _process(delta): if _controller: position += Vector3.FORWARD * delta * _controller.state.get("velocity", 0.0) + func _ready() -> void: _needs_head_display_update = true _dirty = true diff --git a/addons/libmaszyna/scenery/scenery_resource_loader.gd b/addons/libmaszyna/scenery/scenery_resource_loader.gd deleted file mode 100644 index 9eae1247..00000000 --- a/addons/libmaszyna/scenery/scenery_resource_loader.gd +++ /dev/null @@ -1,57 +0,0 @@ -# SceneryResourceLoader singleton - -@tool -extends Node - -signal scenery_loaded - -signal loading_started() -signal loading_finished() -signal loading_request() -signal loading_error(error) - -var current_task:String = "" -var files_to_load:int = 0 -var files_loaded:int = 0 -var enabled:bool = true - -var _queue = [] -var _t:float = 0.0 -var _immediate:bool = false -var _busy:bool = false -const _wait_time = 0.01 - -func reset(): - files_to_load = 0 - files_loaded = 0 - _queue.clear() - -func schedule(title:String, callable: Callable): - _queue.append([title, callable]) - files_to_load += 1 - loading_request.emit() - -func _process(delta): - if not enabled: - return - - _t += delta - if _busy or _t > _wait_time: - _t = 0.0 - if files_loaded < files_to_load: - _busy = true - var x = _queue.pop_front() - if x: - current_task = x[0] - loading_started.emit() - x[1].call() - files_loaded += 1 - loading_finished.emit() - else: - files_loaded = files_to_load - loading_error.emit("Not all files were loaded. Check log.") - _busy = false - - if _busy and files_loaded == files_to_load: - _busy = false - scenery_loaded.emit() diff --git a/addons/libmaszyna/settings/user_settings.gd b/addons/libmaszyna/settings/user_settings.gd index b3216197..619d48f7 100644 --- a/addons/libmaszyna/settings/user_settings.gd +++ b/addons/libmaszyna/settings/user_settings.gd @@ -11,6 +11,12 @@ signal cache_cleared var config_file_path = "user://settings.cfg" var config = ConfigFile.new() var DEFAULTS = { + "e3d": { + #Auto generation features are highly experimental, might be useful for future editor. Only turnable by manually editing settings file + "auto_generate_normal": false, + "auto_generate_metallic": false, + "auto_generate_height": false + }, "maszyna": { "game_dir": ".", }, diff --git a/addons/libmaszyna/sound/audio_stream_manager.gd b/addons/libmaszyna/sound/audio_stream_manager.gd index c5fe335f..ec627a0f 100644 --- a/addons/libmaszyna/sound/audio_stream_manager.gd +++ b/addons/libmaszyna/sound/audio_stream_manager.gd @@ -10,8 +10,8 @@ func _ready(): UserSettings.config_changed.connect(clear_cache) UserSettings.cache_clear_requested.connect(clear_cache) -func get_stream(name:String, loop:bool = false) -> AudioStream: - var stream_key = "%s:%s" % [name, loop] +func get_stream(name:String, loop:bool = false, loop_offset:float = 0.0) -> AudioStream: + var stream_key = "%s:%s:%s" % [name, loop, loop_offset] if not stream_key in _streams: var project_data_dir = UserSettings.get_maszyna_game_dir() var full_path = "%s/sounds/%s.ogg" % [project_data_dir, name.to_lower()] @@ -19,6 +19,7 @@ func get_stream(name:String, loop:bool = false) -> AudioStream: var stream:AudioStreamOggVorbis = AudioStreamOggVorbis.load_from_file(full_path) if stream: stream.loop = loop + stream.loop_offset = loop_offset _streams[stream_key] = stream else: push_error("[%s] file does not exists: %s" % [self, full_path]) diff --git a/addons/libmaszyna/sound/maszyna_audio_stream.gd b/addons/libmaszyna/sound/maszyna_audio_stream.gd index e90b7daf..52f9ca73 100644 --- a/addons/libmaszyna/sound/maszyna_audio_stream.gd +++ b/addons/libmaszyna/sound/maszyna_audio_stream.gd @@ -13,6 +13,13 @@ class_name MaszynaAudioStream if not loop == x: loop = x _real_stream = null + +@export var loop_offset:float = 0.0: + set(x): + if not loop_offset == x: + loop_offset = x + _real_stream = null + var _real_stream:AudioStream @@ -24,7 +31,8 @@ func _get_length() -> float: func _instantiate_playback() -> AudioStreamPlayback: if file_path and not _real_stream: - _real_stream = AudioStreamManager.get_stream(file_path, loop) + _real_stream = AudioStreamManager.get_stream(file_path, loop, loop_offset) + if _real_stream: return _real_stream.instantiate_playback() diff --git a/addons/libmaszyna/sound/train_sound_3d.gd b/addons/libmaszyna/sound/train_sound_3d.gd index 0f480502..1c51baa2 100644 --- a/addons/libmaszyna/sound/train_sound_3d.gd +++ b/addons/libmaszyna/sound/train_sound_3d.gd @@ -1,31 +1,57 @@ -extends AudioStreamPlayer3D +extends SfxPlayer3D class_name TrainSound3D -@export var state_property = "" +enum TriggerMode { TOGGLE, CONTINUOUS } + +var _trigger = TrainSoundTrigger.new() +var _dirty := false + +@export var state_property = "": + set(x): + state_property = x + _trigger.state_property = x +@export var trigger_mode:TriggerMode = TriggerMode.TOGGLE: + set(x): + trigger_mode = x + _trigger.trigger_mode = x +@export var trigger_threshold_min:float = 0.0: + set(x): + trigger_threshold_min = x + _trigger.trigger_threshold_min = x +@export var trigger_threshold_max:float = 1.0: + set(x): + trigger_threshold_max = x + _trigger.trigger_threshold_max = x + +@export var sound_event:StringName = "": + set(x): + sound_event = x + _trigger.sound_event = x +@export var sound_parameter:StringName = "": + set(x): + sound_parameter = x + _trigger.sound_parameter = x + @export_node_path("TrainController") var controller_path = NodePath(""): set(x): - controller_path = x _dirty = true - _train = null - -var _t = 0.0 -var _dirty = false -var _train = null -var _should_play = false - + controller_path = x + if x and is_inside_tree(): + var node = get_node(x) + _trigger.controller_path = _trigger.get_path_to(node) + else: + _trigger.controller_path = NodePath("") + -func _process(_delta): - _t += _delta - if _t > 0.1: - _t = 0.0 - if state_property and _train: - _should_play = true if _train.state.get(state_property, false) else false - if _should_play and not playing: - play() - elif not _should_play and playing: - stop() - if _dirty: - _dirty = false +func _ready() -> void: + var node = get_node_or_null(controller_path) + if node: + _trigger.controller_path = _trigger.get_path_to(node) + +func _enter_tree() -> void: + add_child(_trigger) - if controller_path and not _train: - _train = get_node(controller_path) +func _exit_tree() -> void: + super() + remove_child(_trigger) + diff --git a/addons/libmaszyna/sound/train_sound_trigger.gd b/addons/libmaszyna/sound/train_sound_trigger.gd new file mode 100644 index 00000000..e9fb02e3 --- /dev/null +++ b/addons/libmaszyna/sound/train_sound_trigger.gd @@ -0,0 +1,70 @@ +extends Node +class_name TrainSoundTrigger + +enum TriggerMode { TOGGLE, CONTINUOUS } + +@export var state_property = "" +@export var trigger_mode:TriggerMode = TriggerMode.TOGGLE +@export var trigger_threshold_min := 0.0 +@export var trigger_threshold_max := 1.0 +@export var sound_event:StringName = "" +@export var sound_parameter:StringName = "" + +@export_node_path("TrainController") var controller_path = NodePath(""): + set(x): + controller_path = x + _dirty = true + _train = null + +var _t := 0.0 +var _dirty := false +var _train:TrainController = null +var _should_play := false +var _activated := false +var _timer:Timer +var _sfxplayer + +func _ready() -> void: + _sfxplayer = get_parent() + _timer = Timer.new() + add_child(_timer) + _timer.wait_time = 0.05 + _timer.one_shot = false + _timer.timeout.connect(_check_sound_event) + _timer.start() + +func _exit_tree() -> void: + remove_child(_timer) + +func _check_sound_event(): + if _dirty: + _dirty = false + if controller_path and not _train: + _train = get_node(controller_path) + + if not _sfxplayer: + return + + if state_property and _train: + match trigger_mode: + TriggerMode.TOGGLE: + var value:float = float(_train.state.get(state_property, 0.0)) + _should_play = bool(value) and (value <= trigger_threshold_max and value >= trigger_threshold_min) + if _should_play and not _activated: + _sfxplayer.play(sound_event) + _activated = true + elif not _should_play and _activated: + _sfxplayer.stop(sound_event) + _activated = false + TriggerMode.CONTINUOUS: + if sound_event and sound_parameter: + var value = _train.state.get(state_property, 0.0) as float + _should_play = value <= trigger_threshold_max and value >= trigger_threshold_min + if _should_play and not _activated: + _sfxplayer.play(sound_event, 0.0, {sound_parameter: value}) + _activated = true + elif not _should_play and _activated: + _sfxplayer.stop(sound_event, 0.0) + _activated = false + elif _sfxplayer.is_playing(sound_event): + _sfxplayer.modulate(sound_event, {sound_parameter: value}) diff --git a/addons/libmaszyna/sound/train_sound_trigger.gd.uid b/addons/libmaszyna/sound/train_sound_trigger.gd.uid new file mode 100644 index 00000000..2bc8f3c9 --- /dev/null +++ b/addons/libmaszyna/sound/train_sound_trigger.gd.uid @@ -0,0 +1 @@ +uid://ba86u1wfkr52x diff --git a/ci/docker/entrypoint.sh b/ci/docker/entrypoint.sh index d19a7c2e..1c0491f2 100755 --- a/ci/docker/entrypoint.sh +++ b/ci/docker/entrypoint.sh @@ -16,7 +16,7 @@ cmake -B build-host -DGODOTCPP_TARGET=template_debug || exit 1 cmake --build build-host || exit 1 if [ "$unit_tests" = "true" ]; then echo "Running unit tests..." - (godot --path demo --headless --import || exit 0) && godot --path demo --headless --import && godot --path demo --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/ -gexit -gjunit_xml_file=res://test_results.xml + (godot --path demo --headless --import || exit 0) && godot --path demo --headless --import && godot --path demo --headless -s addons/gut/gut_cmdln.gd -gdir=res://tests/,res://addons/gnd_sfx/tests/ -gexit -gjunit_xml_file=res://test_results.xml echo "Unit tests passed!" exit 0 fi diff --git a/demo/addons/gnd_sfx/plugin.cfg b/demo/addons/gnd_sfx/plugin.cfg new file mode 100644 index 00000000..5054cf07 --- /dev/null +++ b/demo/addons/gnd_sfx/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Godot SFX" +description="Simple/Sound Effects System" +author="GamesNotDeveloped" +version="0.1" +script="plugin.gd" diff --git a/demo/addons/gnd_sfx/plugin.gd b/demo/addons/gnd_sfx/plugin.gd new file mode 100644 index 00000000..720cc9df --- /dev/null +++ b/demo/addons/gnd_sfx/plugin.gd @@ -0,0 +1,15 @@ +@tool +extends EditorPlugin + +var _automation_inspector_plugin: EditorInspectorPlugin + + +func _enter_tree() -> void: + _automation_inspector_plugin = SfxAutomationInspectorPlugin.new() + add_inspector_plugin(_automation_inspector_plugin) + + +func _exit_tree() -> void: + if _automation_inspector_plugin != null: + remove_inspector_plugin(_automation_inspector_plugin) + _automation_inspector_plugin = null diff --git a/demo/addons/gnd_sfx/plugin.gd.uid b/demo/addons/gnd_sfx/plugin.gd.uid new file mode 100644 index 00000000..022a9816 --- /dev/null +++ b/demo/addons/gnd_sfx/plugin.gd.uid @@ -0,0 +1 @@ +uid://ow845h4upkr0 diff --git a/demo/addons/gnd_sfx/sfx_automation.gd b/demo/addons/gnd_sfx/sfx_automation.gd new file mode 100644 index 00000000..1d99927d --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_automation.gd @@ -0,0 +1,47 @@ +@tool +extends Resource +class_name SfxAutomation + +enum CrossfadeMode { + LINEAR, + EQUAL_POWER +} + +@export var crossfade_mode: CrossfadeMode = CrossfadeMode.LINEAR: + set(value): + crossfade_mode = value + emit_changed() + +@export var parameter_name : StringName = "": + set(value): + parameter_name = value + emit_changed() + +@export var tracks : Array[SfxTrack]: + set(value): + tracks = value + +@export var audio_bus : StringName: + set(value): + audio_bus = value + emit_changed() + +@export var min_domain = 0.0: + set(value): + min_domain = value + +@export var max_domain = 1.0: + set(value): + max_domain = value + +@export var pitch_curve: Curve: + set(value): + pitch_curve = value + +@export var phase_locked := false: + set(value): + phase_locked = value + +@export var phase_period := 0.0: + set(value): + phase_period = value diff --git a/demo/addons/gnd_sfx/sfx_automation.gd.uid b/demo/addons/gnd_sfx/sfx_automation.gd.uid new file mode 100644 index 00000000..6f469a76 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_automation.gd.uid @@ -0,0 +1 @@ +uid://bf434i6ru4ch diff --git a/demo/addons/gnd_sfx/sfx_automation_inspector_plugin.gd b/demo/addons/gnd_sfx/sfx_automation_inspector_plugin.gd new file mode 100644 index 00000000..0be37f30 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_automation_inspector_plugin.gd @@ -0,0 +1,180 @@ +@tool +extends EditorInspectorPlugin +class_name SfxAutomationInspectorPlugin + +const BUTTON_TEXT := "Auto Sync Phase + Loops" + + +func _can_handle(object: Object) -> bool: + return object is SfxAutomation + + +func _parse_begin(object: Object) -> void: + var automation := object as SfxAutomation + if automation == null: + return + + var container := VBoxContainer.new() + container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + var button := Button.new() + button.text = BUTTON_TEXT + button.tooltip_text = "Analyze track timing, set phase offsets, and rewrite supported loop offsets." + button.disabled = _count_stream_tracks(automation) < 2 + container.add_child(button) + + var confirmation := ConfirmationDialog.new() + confirmation.title = BUTTON_TEXT + confirmation.dialog_text = "Analyze the current automation and apply phase/loop changes?" + confirmation.ok_button_text = "Analyze" + container.add_child(confirmation) + + var status := Label.new() + status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + status.text = "Requires at least two tracks with AudioStream resources." + if not button.disabled: + status.text = "Analyzes PCM from the current automation and writes phase + loop settings." + container.add_child(status) + + button.pressed.connect(_on_sync_confirmation_requested.bind(confirmation)) + confirmation.confirmed.connect(_on_sync_pressed.bind(automation, button, status)) + add_custom_control(container) + + +func _on_sync_confirmation_requested(confirmation: ConfirmationDialog) -> void: + if confirmation == null: + return + confirmation.popup_centered() + + +func _on_sync_pressed(automation: SfxAutomation, button: Button, status: Label) -> void: + button.disabled = true + status.text = "Analyzing tracks..." + + var result: Dictionary = SfxAutomationSyncAnalyzer.analyze_automation(automation) + if not result.get("ok", false): + status.text = result.get("error", "Auto-sync failed.") + button.disabled = false + return + + var touched_stream_paths := PackedStringArray() + var warnings: PackedStringArray = result.get("warnings", PackedStringArray()) + + automation.phase_locked = true + automation.phase_period = result["phase_period"] + + for track_result in result["track_results"]: + var track: SfxTrack = track_result["track"] + track.phase_offset = track_result["phase_offset"] + + if track_result.has("loop_offset"): + var loop_apply := _apply_loop_offset(track, track_result["loop_offset"]) + if loop_apply.get("ok", false): + var stream_path: String = loop_apply.get("stream_path", "") + if not stream_path.is_empty() and not touched_stream_paths.has(stream_path): + touched_stream_paths.append(stream_path) + else: + var error_text: String = loop_apply.get("error", "") + if not error_text.is_empty(): + warnings.append(error_text) + + automation.emit_changed() + var save_result := _save_automation_owner(automation) + if not save_result.get("ok", false): + var save_error: String = save_result.get("error", "") + if not save_error.is_empty(): + warnings.append(save_error) + + _reimport_streams(touched_stream_paths) + + var summary := "Applied phase sync to %d tracks. phase_period=%.4fs" % [ + result["track_results"].size(), + result["phase_period"], + ] + if not warnings.is_empty(): + summary += "\nWarnings: %s" % [", ".join(warnings)] + status.text = summary + button.disabled = false + + +func _count_stream_tracks(automation: SfxAutomation) -> int: + var count := 0 + for track in automation.tracks: + if track != null and track.stream != null: + count += 1 + return count + + +func _apply_loop_offset(track: SfxTrack, loop_offset: float) -> Dictionary: + if track == null or track.stream == null: + return {"ok": false, "error": "Track is missing an AudioStream."} + if not track.stream.has_method("set_loop") or not track.stream.has_method("set_loop_offset"): + return {"ok": false, "error": "Stream type does not support loop_offset editing."} + + var stream: AudioStream = track.stream + stream.call("set_loop", true) + stream.call("set_loop_offset", loop_offset) + + var stream_path := stream.resource_path + if stream_path.is_empty(): + return {"ok": false, "error": "Stream has no resource_path, so loop_offset could not be persisted."} + + var import_path := "%s.import" % stream_path + if not FileAccess.file_exists(import_path): + return {"ok": false, "error": "Missing import config for %s." % stream_path} + + var config := ConfigFile.new() + var load_error := config.load(import_path) + if load_error != OK: + return {"ok": false, "error": "Could not load %s." % import_path} + + config.set_value("params", "loop", true) + config.set_value("params", "loop_offset", loop_offset) + + var save_error := config.save(import_path) + if save_error != OK: + return {"ok": false, "error": "Could not save %s." % import_path} + + return { + "ok": true, + "stream_path": stream_path, + } + + +func _save_automation_owner(automation: SfxAutomation) -> Dictionary: + var owner_path := _resolve_owner_path(automation.resource_path) + if owner_path.is_empty(): + return {"ok": false, "error": "Automation resource has no save path; save it manually after apply."} + if not owner_path.ends_with(".tres") and not owner_path.ends_with(".res"): + return {"ok": false, "error": "Auto-save currently supports .tres/.res owners only; save the resource manually."} + + var owner_resource := ResourceLoader.load(owner_path, "", ResourceLoader.CACHE_MODE_REUSE) + if owner_resource == null: + return {"ok": false, "error": "Could not reload %s for saving." % owner_path} + + var save_error := ResourceSaver.save(owner_resource, owner_path) + if save_error != OK: + return {"ok": false, "error": "Could not save %s." % owner_path} + return {"ok": true} + + +func _reimport_streams(stream_paths: PackedStringArray) -> void: + if stream_paths.is_empty(): + return + + var filesystem := EditorInterface.get_resource_filesystem() + if filesystem == null: + return + if filesystem.has_method("reimport_files"): + filesystem.call("reimport_files", stream_paths) + elif filesystem.has_method("scan"): + filesystem.call_deferred("scan") + + +func _resolve_owner_path(resource_path: String) -> String: + if resource_path.is_empty(): + return "" + var separator := resource_path.find("::") + if separator == -1: + return resource_path + return resource_path.substr(0, separator) diff --git a/demo/addons/gnd_sfx/sfx_automation_inspector_plugin.gd.uid b/demo/addons/gnd_sfx/sfx_automation_inspector_plugin.gd.uid new file mode 100644 index 00000000..922a0c31 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_automation_inspector_plugin.gd.uid @@ -0,0 +1 @@ +uid://de8yudglryniv diff --git a/demo/addons/gnd_sfx/sfx_automation_sync_analyzer.gd b/demo/addons/gnd_sfx/sfx_automation_sync_analyzer.gd new file mode 100644 index 00000000..46e588c9 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_automation_sync_analyzer.gd @@ -0,0 +1,515 @@ +@tool +extends RefCounted +class_name SfxAutomationSyncAnalyzer + +const ANALYSIS_SAMPLE_RATE := 4000.0 +const MIN_PHASE_PERIOD := 0.05 +const MAX_PHASE_PERIOD := 0.5 +const MIN_TRACK_DURATION := 0.1 +const PERIOD_SEARCH_RATIO := 0.5 +const LOOP_SEARCH_PERIODS := 4.0 +const LOOP_SEARCH_MIN := 0.2 +const LOOP_SEARCH_MAX := 0.75 +const LOOP_WINDOW_PERIODS := 1.5 +const LOOP_WINDOW_MIN := 0.05 +const LOOP_WINDOW_MAX := 0.25 +const PHASE_HINT_WEIGHT := 0.05 +const EARLY_LOOP_WEIGHT := 0.02 +const EARLY_LOOP_ACCEPT_RATIO := 0.98 +const EARLY_LOOP_MIN_SCORE := 0.05 +const LOOP_CYCLE_WEIGHT := 0.2 +const SHARED_LOOP_WEIGHT := 0.35 +const PERIOD_CLUSTER_ABSOLUTE_TOLERANCE := 0.02 +const PERIOD_CLUSTER_RELATIVE_TOLERANCE := 0.08 + + +static func analyze_automation(automation: SfxAutomation) -> Dictionary: + var track_data: Array[Dictionary] = [] + var warnings: PackedStringArray = [] + + for track in automation.tracks: + if track == null or track.stream == null: + continue + + var decoded := _decode_stream_to_mono(track.stream) + if decoded.is_empty(): + warnings.append("Skipped a track with unreadable audio stream.") + continue + + var full_analysis := resample_and_normalize(decoded["samples"], decoded["sample_rate"], ANALYSIS_SAMPLE_RATE) + var active_analysis := trim_samples( + full_analysis["samples"], + full_analysis["sample_rate"], + maxf(track.stream_offset, 0.0) + ) + active_analysis = normalize_samples(active_analysis) + + var active_duration := float(active_analysis.size()) / maxf(full_analysis["sample_rate"], 1.0) + if active_duration < MIN_TRACK_DURATION: + warnings.append("Skipped a track with too little usable audio after stream_offset.") + continue + + track_data.append({ + "track": track, + "stream_duration": track.stream.get_length(), + "analysis_rate": full_analysis["sample_rate"], + "full_samples": full_analysis["samples"], + "active_samples": active_analysis, + "loop_supported": _supports_loop_offset(track.stream), + }) + + if track_data.size() < 2: + return { + "ok": false, + "error": "Need at least two tracks with readable audio to auto-sync.", + "warnings": warnings, + } + + var period_estimates: Array[float] = [] + for data in track_data: + var sample_rate: float = data["analysis_rate"] + var samples: PackedFloat32Array = data["active_samples"] + var max_period := minf(MAX_PHASE_PERIOD, (float(samples.size()) / sample_rate) * PERIOD_SEARCH_RATIO) + var estimate := estimate_period(samples, sample_rate, MIN_PHASE_PERIOD, max_period) + if estimate > 0.0: + period_estimates.append(estimate) + data["local_period"] = estimate + else: + data["local_period"] = 0.0 + + if period_estimates.size() < 2: + return { + "ok": false, + "error": "Could not find a stable shared phase period from the available tracks.", + "warnings": warnings, + } + + track_data.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + var track_a: SfxTrack = a["track"] + var track_b: SfxTrack = b["track"] + return track_a.offset < track_b.offset + ) + + var phase_period := _select_consensus_period(period_estimates) + var phase_offsets := _resolve_track_phase_offsets(track_data, phase_period) + var shared_loop_duration := _select_shared_loop_duration(track_data, phase_period) + var results: Array[Dictionary] = [] + + for index in range(track_data.size()): + var data: Dictionary = track_data[index] + var track: SfxTrack = data["track"] + var phase_offset: float = phase_offsets[index] + + var track_result := { + "track": track, + "phase_offset": phase_offset, + } + + if data["loop_supported"]: + var loop_period: float = data["local_period"] if data["local_period"] > 0.0 else phase_period + track_result["loop_offset"] = estimate_loop_offset( + data["full_samples"], + data["analysis_rate"], + loop_period, + maxf(track.stream_offset, 0.0), + phase_offset, + shared_loop_duration, + data["stream_duration"] + ) + else: + warnings.append("Track %s uses a stream type without editable loop_offset." % [track.stream.resource_path]) + + results.append(track_result) + + return { + "ok": true, + "phase_period": phase_period, + "track_results": results, + "warnings": warnings, + } + + +static func normalize_samples(samples: PackedFloat32Array) -> PackedFloat32Array: + var normalized := PackedFloat32Array() + normalized.resize(samples.size()) + if samples.is_empty(): + return normalized + + var mean := 0.0 + for sample in samples: + mean += sample + mean /= float(samples.size()) + + var peak := 0.0 + for index in range(samples.size()): + var centered := samples[index] - mean + normalized[index] = centered + peak = maxf(peak, absf(centered)) + + if peak <= 0.000001: + return normalized + + for index in range(normalized.size()): + normalized[index] /= peak + return normalized + + +static func resample_and_normalize( + samples: PackedFloat32Array, + input_rate: float, + target_rate: float +) -> Dictionary: + if samples.is_empty(): + return {"samples": PackedFloat32Array(), "sample_rate": target_rate} + + var step := 1 + if input_rate > target_rate and target_rate > 0.0: + step = max(1, int(round(input_rate / target_rate))) + var output_rate := input_rate / float(step) + var resized := PackedFloat32Array() + resized.resize(int(ceil(float(samples.size()) / float(step)))) + + for out_index in range(resized.size()): + var start := out_index * step + var end := min(start + step, samples.size()) + var value := 0.0 + for index in range(start, end): + value += samples[index] + resized[out_index] = value / maxf(float(end - start), 1.0) + + return { + "samples": normalize_samples(resized), + "sample_rate": output_rate, + } + + +static func trim_samples(samples: PackedFloat32Array, sample_rate: float, start_seconds: float) -> PackedFloat32Array: + var start_index := clampi(int(round(start_seconds * sample_rate)), 0, samples.size()) + return slice_samples(samples, start_index, samples.size() - start_index) + + +static func slice_samples(samples: PackedFloat32Array, start: int, length: int) -> PackedFloat32Array: + var safe_start := clampi(start, 0, samples.size()) + var safe_end := clampi(safe_start + max(length, 0), safe_start, samples.size()) + var sliced := PackedFloat32Array() + sliced.resize(safe_end - safe_start) + for index in range(sliced.size()): + sliced[index] = samples[safe_start + index] + return sliced + + +static func estimate_period( + samples: PackedFloat32Array, + sample_rate: float, + min_period_seconds: float, + max_period_seconds: float +) -> float: + if samples.size() < 4 or sample_rate <= 0.0: + return 0.0 + + var min_lag: int = max(1, int(round(min_period_seconds * sample_rate))) + var max_lag: int = min(int(round(max_period_seconds * sample_rate)), (samples.size() / 2) - 1) + if max_lag <= min_lag: + return 0.0 + + var best_lag := min_lag + var best_score := -INF + + for lag in range(min_lag, max_lag + 1): + var overlap := samples.size() - lag + var numerator := 0.0 + var lhs_energy := 0.0 + var rhs_energy := 0.0 + for index in range(overlap): + var lhs := samples[index] + var rhs := samples[index + lag] + numerator += lhs * rhs + lhs_energy += lhs * lhs + rhs_energy += rhs * rhs + var denominator := sqrt(lhs_energy * rhs_energy) + if denominator <= 0.000001: + continue + var score := numerator / denominator + if score > best_score: + best_score = score + best_lag = lag + + return float(best_lag) / sample_rate + + +static func estimate_phase_offset( + reference_samples: PackedFloat32Array, + target_samples: PackedFloat32Array, + sample_rate: float, + period_seconds: float +) -> float: + var period_samples: int = max(1, int(round(period_seconds * sample_rate))) + if reference_samples.is_empty() or target_samples.is_empty() or period_samples <= 1: + return 0.0 + + var window: int = min(reference_samples.size(), target_samples.size(), period_samples * 4) + if window <= 4: + return 0.0 + + var best_lag := 0 + var best_score := -INF + + for lag in range(period_samples): + var numerator := 0.0 + var lhs_energy := 0.0 + var rhs_energy := 0.0 + for index in range(window): + var lhs := reference_samples[index] + var rhs := target_samples[(index + lag) % target_samples.size()] + numerator += lhs * rhs + lhs_energy += lhs * lhs + rhs_energy += rhs * rhs + var denominator := sqrt(lhs_energy * rhs_energy) + if denominator <= 0.000001: + continue + var score := numerator / denominator + if score > best_score: + best_score = score + best_lag = lag + + return float(best_lag) / sample_rate + + +static func estimate_loop_offset( + samples: PackedFloat32Array, + sample_rate: float, + period_seconds: float, + search_start_seconds := 0.0, + phase_offset_seconds := 0.0, + target_loop_duration_seconds := -1.0, + stream_duration_seconds := -1.0 +) -> float: + if samples.is_empty() or sample_rate <= 0.0: + return maxf(search_start_seconds, 0.0) + + var duration_seconds := stream_duration_seconds if stream_duration_seconds > 0.0 else float(samples.size()) / sample_rate + var window_seconds := clampf(period_seconds * LOOP_WINDOW_PERIODS, LOOP_WINDOW_MIN, minf(LOOP_WINDOW_MAX, duration_seconds * 0.25)) + var window_samples: int = max(8, int(round(window_seconds * sample_rate))) + if window_samples >= samples.size(): + return maxf(search_start_seconds, 0.0) + + var tail_start: int = samples.size() - window_samples + var tail_window := slice_samples(samples, tail_start, window_samples) + + var search_start: int = clampi(int(round(search_start_seconds * sample_rate)), 0, max(0, tail_start - window_samples)) + var search_duration := clampf(period_seconds * LOOP_SEARCH_PERIODS, LOOP_SEARCH_MIN, LOOP_SEARCH_MAX) + var search_end: int = clampi( + int(round((search_start_seconds + search_duration) * sample_rate)), + search_start, + max(0, tail_start - window_samples) + ) + + var best_index := search_start + var best_score := -INF + var candidate_scores: Array[Dictionary] = [] + var target_offset_seconds := duration_seconds - target_loop_duration_seconds + + if target_loop_duration_seconds > 0.0: + var target_offset_samples := int(round(target_offset_seconds * sample_rate)) + return clampf(float(target_offset_samples) / sample_rate, 0.0, duration_seconds - window_seconds) + + for candidate in range(search_start, search_end + 1): + var candidate_window := slice_samples(samples, candidate, window_samples) + var seam_score := _normalized_dot(candidate_window, tail_window) + var total_score := seam_score + var candidate_seconds := float(candidate) / sample_rate + var loop_duration := maxf(duration_seconds - candidate_seconds, 0.0) + var duration_delta := INF + if period_seconds > 0.0: + var cycle_distance := _circular_distance(loop_duration, 0.0, period_seconds) + var cycle_score := 1.0 - (cycle_distance / maxf(period_seconds * 0.5, 0.000001)) + total_score += maxf(cycle_score, 0.0) * LOOP_CYCLE_WEIGHT + var circular_distance := _circular_distance(candidate_seconds, phase_offset_seconds, period_seconds) + var phase_score := 1.0 - (circular_distance / maxf(period_seconds * 0.5, 0.000001)) + total_score += maxf(phase_score, 0.0) * PHASE_HINT_WEIGHT + total_score -= candidate_seconds * EARLY_LOOP_WEIGHT + if target_loop_duration_seconds > 0.0: + duration_delta = absf(loop_duration - target_loop_duration_seconds) + var shared_score := 1.0 - (duration_delta / maxf(period_seconds * 0.5, 0.000001)) + total_score += maxf(shared_score, 0.0) * SHARED_LOOP_WEIGHT + candidate_scores.append({ + "index": candidate, + "seconds": candidate_seconds, + "seam_score": seam_score, + "duration_delta": duration_delta, + "score": total_score, + }) + if total_score > best_score: + best_score = total_score + best_index = candidate + + var acceptable_score := maxf(EARLY_LOOP_MIN_SCORE, best_score * EARLY_LOOP_ACCEPT_RATIO) + if target_loop_duration_seconds > 0.0: + var quantized_target_offset: float = round(target_offset_seconds * sample_rate) / sample_rate + return clampf(quantized_target_offset, float(search_start) / sample_rate, float(search_end) / sample_rate) + + for candidate in candidate_scores: + if candidate["score"] >= acceptable_score: + return candidate["seconds"] + + return float(best_index) / sample_rate + + +static func _decode_stream_to_mono(stream: AudioStream) -> Dictionary: + if stream == null or stream.get_length() <= 0.0: + return {} + + var playback := stream.instantiate_playback() + if playback == null: + return {} + + var sample_rate := AudioServer.get_mix_rate() + var frame_count := max(1, int(ceil(stream.get_length() * sample_rate))) + playback.start(0.0) + var mixed: Array = playback.mix_audio(1.0, frame_count) + var samples := PackedFloat32Array() + samples.resize(mixed.size()) + for index in range(mixed.size()): + var frame: Vector2 = mixed[index] + samples[index] = 0.5 * (frame.x + frame.y) + + return { + "samples": samples, + "sample_rate": sample_rate, + } + + +static func _supports_loop_offset(stream: AudioStream) -> bool: + if stream == null: + return false + return stream.has_method("set_loop") and stream.has_method("set_loop_offset") + + +static func _normalized_dot(lhs: PackedFloat32Array, rhs: PackedFloat32Array) -> float: + var size := min(lhs.size(), rhs.size()) + if size == 0: + return -INF + + var numerator := 0.0 + var lhs_energy := 0.0 + var rhs_energy := 0.0 + for index in range(size): + var lhs_value := lhs[index] + var rhs_value := rhs[index] + numerator += lhs_value * rhs_value + lhs_energy += lhs_value * lhs_value + rhs_energy += rhs_value * rhs_value + + var denominator := sqrt(lhs_energy * rhs_energy) + if denominator <= 0.000001: + return -INF + return numerator / denominator + + +static func _circular_distance(lhs: float, rhs: float, period: float) -> float: + if period <= 0.0: + return 0.0 + var delta := fposmod(lhs - rhs, period) + return minf(delta, period - delta) + + +static func _median(values: Array[float]) -> float: + if values.is_empty(): + return 0.0 + var sorted := values.duplicate() + sorted.sort() + var middle := sorted.size() / 2 + if sorted.size() % 2 == 1: + return sorted[middle] + return 0.5 * (sorted[middle - 1] + sorted[middle]) + + +static func _select_consensus_period(values: Array[float]) -> float: + if values.is_empty(): + return 0.0 + if values.size() <= 2: + return _median(values) + + var best_cluster: Array[float] = [] + var best_variance := INF + var best_center := INF + + for seed in values: + var tolerance := maxf(PERIOD_CLUSTER_ABSOLUTE_TOLERANCE, absf(seed) * PERIOD_CLUSTER_RELATIVE_TOLERANCE) + var cluster: Array[float] = [] + for value in values: + if absf(value - seed) <= tolerance: + cluster.append(value) + + if cluster.is_empty(): + continue + + var center := _median(cluster) + var variance := 0.0 + for value in cluster: + variance += pow(value - center, 2.0) + variance /= float(cluster.size()) + + if cluster.size() > best_cluster.size(): + best_cluster = cluster + best_variance = variance + best_center = center + continue + + if cluster.size() == best_cluster.size(): + if variance < best_variance - 0.000001: + best_cluster = cluster + best_variance = variance + best_center = center + continue + if absf(variance - best_variance) <= 0.000001 and center < best_center: + best_cluster = cluster + best_variance = variance + best_center = center + + if best_cluster.is_empty(): + return _median(values) + return _median(best_cluster) + + +static func _resolve_track_phase_offsets(track_data: Array[Dictionary], phase_period: float) -> Array[float]: + var phase_offsets: Array[float] = [] + if track_data.is_empty(): + return phase_offsets + + phase_offsets.resize(track_data.size()) + phase_offsets[0] = 0.0 + + for index in range(1, track_data.size()): + var previous: Dictionary = track_data[index - 1] + var current: Dictionary = track_data[index] + var relative_offset := estimate_phase_offset( + previous["active_samples"], + current["active_samples"], + previous["analysis_rate"], + phase_period + ) + phase_offsets[index] = fposmod(phase_offsets[index - 1] + relative_offset, phase_period) + + return phase_offsets + + +static func _select_shared_loop_duration(track_data: Array[Dictionary], phase_period: float = 0.0) -> float: + if track_data.is_empty(): + return 0.0 + + var shortest_duration := INF + for data in track_data: + var duration: float = data["stream_duration"] if data.has("stream_duration") else 0.0 + if duration > 0.0: + shortest_duration = minf(shortest_duration, duration) + + if shortest_duration == INF: + return 0.0 + + if phase_period > 0.0: + var cycles := floorf(shortest_duration / phase_period) + if cycles > 0: + return cycles * phase_period + + return shortest_duration diff --git a/demo/addons/gnd_sfx/sfx_automation_sync_analyzer.gd.uid b/demo/addons/gnd_sfx/sfx_automation_sync_analyzer.gd.uid new file mode 100644 index 00000000..ca6f9e0a --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_automation_sync_analyzer.gd.uid @@ -0,0 +1 @@ +uid://bb1y8nlc4p04r diff --git a/demo/addons/gnd_sfx/sfx_bank.gd b/demo/addons/gnd_sfx/sfx_bank.gd new file mode 100644 index 00000000..4f0d652d --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_bank.gd @@ -0,0 +1,20 @@ +@tool +extends Resource +class_name SfxBank + +@export var events: Array[SfxEvent] = []: + set(x): + events = x + _rebuild_events_cache() + + +var _events_cache = {} + +func _rebuild_events_cache(): + _events_cache = {} + for event in events: + if event.name: + _events_cache[event.name] = event + +func get_event(event_name: StringName) -> SfxEvent: + return _events_cache.get(event_name) diff --git a/demo/addons/gnd_sfx/sfx_bank.gd.uid b/demo/addons/gnd_sfx/sfx_bank.gd.uid new file mode 100644 index 00000000..2608a541 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_bank.gd.uid @@ -0,0 +1 @@ +uid://bbamgfxaoiofj diff --git a/demo/addons/gnd_sfx/sfx_event.gd b/demo/addons/gnd_sfx/sfx_event.gd new file mode 100644 index 00000000..89fd38cb --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_event.gd @@ -0,0 +1,55 @@ +@tool +extends Resource +class_name SfxEvent + +@export var tracks: Array[SfxTrack] = []: + set(value): + tracks = value + emit_changed() + +@export var polyphony_enabled := true: + set(value): + polyphony_enabled = value + emit_changed() + +@export var adsr_enabled := false: + set(value): + adsr_enabled = value + emit_changed() + +@export_range(0.0, 30.0, 0.01) var attack := 0.0: + set(value): + attack = value + emit_changed() + +@export_range(0.0, 30.0, 0.01) var decay := 0.0: + set(value): + decay = value + emit_changed() + +@export_range(0.0, 1.0, 0.01) var sustain := 1.0: + set(value): + sustain = value + emit_changed() + +@export_range(0.0, 30.0, 0.01) var release := 0.0: + set(value): + release = value + emit_changed() + +@export var name: StringName = "": + set(value): + name = value + emit_changed() + +@export var automations: Array[SfxAutomation] = []: + set(value): + automations = value + emit_changed() + + +func get_automation(automation_name:StringName) -> SfxAutomation: + for automation in automations: + if automation.parameter_name == automation_name: + return automation + return null diff --git a/demo/addons/gnd_sfx/sfx_event.gd.uid b/demo/addons/gnd_sfx/sfx_event.gd.uid new file mode 100644 index 00000000..0480f1e3 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_event.gd.uid @@ -0,0 +1 @@ +uid://bgxtc44e5rb0l diff --git a/demo/addons/gnd_sfx/sfx_generator_playback.gd b/demo/addons/gnd_sfx/sfx_generator_playback.gd new file mode 100644 index 00000000..6676e6f3 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_generator_playback.gd @@ -0,0 +1,15 @@ +@tool +extends Resource +class_name SfxGeneratorPlayback + + +func create_state(playback: AudioStreamGeneratorPlayback, track): + return null + + +func update(state, context: Dictionary) -> void: + pass + + +func cleanup(state) -> void: + pass diff --git a/demo/addons/gnd_sfx/sfx_generator_playback.gd.uid b/demo/addons/gnd_sfx/sfx_generator_playback.gd.uid new file mode 100644 index 00000000..1f947ce4 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_generator_playback.gd.uid @@ -0,0 +1 @@ +uid://bwwkwbu1tx1qb diff --git a/demo/addons/gnd_sfx/sfx_playback_runtime.gd b/demo/addons/gnd_sfx/sfx_playback_runtime.gd new file mode 100644 index 00000000..dc24d908 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_playback_runtime.gd @@ -0,0 +1,1094 @@ +extends RefCounted +class_name SfxPlaybackRuntime + + +class EventInstance: + var event: SfxEvent + var event_name: StringName = &"" + var playback_time := 0.0 + var release_time := 0.0 + var parameters: Dictionary = {} + var status: StringName = &"stopped" + var adsr_stage: StringName = &"idle" + var adsr_elapsed := 0.0 + var release_start_gain := 0.0 + var triggered_event_tracks: Array[SfxTrack] = [] + var triggered_sustain_tracks: Array[SfxTrack] = [] + var triggered_automation_tracks := {} + + +class ActiveVoice: + var player + var event_instance: EventInstance + var track: SfxTrack + var stream: AudioStream + var stream_start_position := 0.0 + var stream_end_position := 0.0 + var automation: SfxAutomation + var automation_name: StringName = &"" + var generator_playback: SfxGeneratorPlayback + var generator_state + var generator_stream_playback: AudioStreamGeneratorPlayback + var stopping := false + var stop_elapsed := 0.0 + var stop_fade_duration := 0.0 + var player_token := 0 + var creation_order := 0 + var track_adsr_stage: StringName = &"idle" + var track_adsr_elapsed := 0.0 + var track_release_start_gain := 0.0 + + +signal finished +signal process_requirement_changed(required: bool) + +var events: Array[SfxEvent] = [] +var _players: Array = [] +var _active_voices: Array[ActiveVoice] = [] +var _instances: Dictionary = {} +var _player_tokens := {} +var _voice_creation_counter := 0 + + +func set_events(value: Array[SfxEvent]) -> void: + events = value + + +func set_players(players: Array) -> void: + _players = players + _notify_process_requirement_changed() + + +func clear() -> void: + var had_activity := not _active_voices.is_empty() or not _instances.is_empty() + for voice in _active_voices: + _cleanup_voice(voice) + for player in _players: + _reset_player(player, true) + _active_voices.clear() + _instances.clear() + _notify_process_requirement_changed() + if had_activity: + finished.emit() + + +func handle_player_finished(player) -> void: + if not is_instance_valid(player): + return + if player.playing: + return + var token := int(_player_tokens.get(player, 0)) + var voice := _find_voice_by_player_token(player, token) + if voice == null: + return + _release_voice(_find_active_voice_index(player)) + + +func update(delta: float) -> void: + var active_instances: Array[EventInstance] = [] + for raw_instances in _instances.values(): + for raw_instance in raw_instances: + var instance := raw_instance as EventInstance + if instance != null: + active_instances.append(instance) + + for instance in active_instances: + _update_instance(instance, delta) + + for index in range(_active_voices.size() - 1, -1, -1): + _update_voice(index, delta) + + _collect_finished_instances() + _notify_process_requirement_changed() + + +func play(event: SfxEvent, offset := 0.0, parameters: Dictionary = {}) -> void: + if event == null: + return + + var event_name := event.name + if not event.polyphony_enabled: + for instance in _get_instances_for_event(event_name): + _stop_event_instance(instance, true) + + var instance := EventInstance.new() + instance.event = event + instance.event_name = event_name + instance.playback_time = maxf(offset, 0.0) + instance.parameters = parameters.duplicate(true) + instance.status = &"playing" + _enter_attack(instance) + _register_instance(instance) + + _refresh_event_tracks(instance, -1.0, instance.playback_time) + _refresh_automation_tracks(instance, true, {}) + _notify_process_requirement_changed() + + +func seek(event_name: StringName, offset: float) -> void: + var instance := _get_latest_instance(event_name) + if instance == null: + return + + var rebuilt_parameters: Dictionary = instance.parameters.duplicate(true) + var adsr_stage: StringName = instance.adsr_stage + var adsr_elapsed: float = instance.adsr_elapsed + var release_time: float = instance.release_time + var release_start_gain: float = instance.release_start_gain + var was_releasing: bool = instance.status == &"releasing" + + _stop_event_instance(instance, true) + + var rebuilt_instance := EventInstance.new() + rebuilt_instance.event = instance.event + rebuilt_instance.event_name = event_name + rebuilt_instance.playback_time = maxf(offset, 0.0) + rebuilt_instance.release_time = release_time + rebuilt_instance.parameters = rebuilt_parameters + rebuilt_instance.status = &"releasing" if was_releasing else &"playing" + rebuilt_instance.adsr_stage = adsr_stage + rebuilt_instance.adsr_elapsed = adsr_elapsed + rebuilt_instance.release_start_gain = release_start_gain + rebuilt_instance.triggered_sustain_tracks = instance.triggered_sustain_tracks.duplicate() + _register_instance(rebuilt_instance) + + _refresh_event_tracks(rebuilt_instance, -1.0, rebuilt_instance.playback_time) + if was_releasing: + _refresh_sustain_tracks(rebuilt_instance, -1.0, rebuilt_instance.release_time) + _refresh_automation_tracks(rebuilt_instance, true, {}) + _notify_process_requirement_changed() + + +func modulate(event_name: StringName, parameters: Dictionary) -> void: + var instance := _get_latest_instance(event_name) + if instance == null: + return + + var previous_values := {} + for key in parameters.keys(): + previous_values[key] = instance.parameters.get(key) + instance.parameters[key] = parameters[key] + + _refresh_automation_tracks(instance, false, previous_values) + for voice in _active_voices: + if voice.event_instance == instance and voice.automation != null: + _apply_voice_state(voice) + _notify_process_requirement_changed() + + +func set_parameters(parameters: Dictionary) -> void: + for event_name in parameters.keys(): + var event_parameters = parameters[event_name] + if event_parameters is Dictionary: + modulate(StringName(event_name), event_parameters) + + +func stop(event_name_or_immediate = null, immediate: bool = false) -> void: + var event_name := &"" + if event_name_or_immediate is bool: + immediate = event_name_or_immediate + elif event_name_or_immediate != null: + event_name = StringName(event_name_or_immediate) + + if event_name: + var instance := _get_latest_stoppable_instance(event_name) + if instance != null: + _stop_event_instance(instance, immediate) + else: + var instances_to_stop: Array[EventInstance] = [] + for raw_instances in _instances.values(): + for raw_instance in raw_instances: + var instance := raw_instance as EventInstance + if instance != null: + instances_to_stop.append(instance) + for instance in instances_to_stop: + _stop_event_instance(instance, immediate) + + _collect_finished_instances() + _notify_process_requirement_changed() + + +func play_automation(event: SfxEvent, automation_name: StringName, value: float = 0.0, restart: bool = false) -> void: + if event == null: + return + + if restart or not is_playing(event.name): + play(event, 0.0, {automation_name: value}) + return + + modulate(event.name, {automation_name: value}) + + +func stop_automation(event: SfxEvent, _automation_name: StringName, immediate: bool = false) -> void: + if event == null: + return + stop(event.name, immediate) + + +func is_playing(event_name: StringName) -> bool: + return not _get_instances_for_event(event_name).is_empty() + + +func requires_process() -> bool: + return not _active_voices.is_empty() or not _instances.is_empty() + + +func _update_instance(instance: EventInstance, delta: float) -> void: + if instance == null: + return + + var previous_time := instance.playback_time + var previous_release_time := instance.release_time + if instance.status == &"playing": + instance.playback_time += delta + _refresh_event_tracks(instance, previous_time, instance.playback_time) + elif instance.status == &"releasing": + instance.release_time += delta + _refresh_sustain_tracks(instance, previous_release_time, instance.release_time) + + _update_adsr(instance, delta) + + +func _refresh_event_tracks(instance: EventInstance, previous_time: float, current_time: float) -> void: + for track in instance.event.tracks: + if track == null: + continue + if track.trigger_mode != SfxTrack.TriggerMode.TRIGGER_TIMELINE: + continue + if instance.triggered_event_tracks.has(track): + continue + if current_time < track.offset: + continue + if previous_time >= 0.0 and previous_time > current_time: + continue + if _start_voice(instance, track): + instance.triggered_event_tracks.append(track) + + +func _refresh_sustain_tracks(instance: EventInstance, previous_time: float, current_time: float) -> void: + for track in instance.event.tracks: + if track == null: + continue + if track.trigger_mode != SfxTrack.TriggerMode.TRIGGER_SUSTAIN: + continue + if instance.triggered_sustain_tracks.has(track): + continue + if current_time < track.offset: + continue + if previous_time >= 0.0 and previous_time > current_time: + continue + if _start_voice(instance, track): + instance.triggered_sustain_tracks.append(track) + + +func _refresh_automation_tracks(instance: EventInstance, initial: bool, previous_values: Dictionary) -> void: + for automation in instance.event.automations: + if automation == null: + continue + + var automation_name := automation.parameter_name + if String(automation_name).is_empty(): + continue + + var current_value = instance.parameters.get(automation_name, automation.min_domain) + var previous_value = previous_values.get(automation_name, null) + if initial or previous_value == null: + previous_value = automation.min_domain if automation.max_domain >= automation.min_domain else automation.max_domain + + if not instance.triggered_automation_tracks.has(automation_name): + instance.triggered_automation_tracks[automation_name] = [] + + var triggered_tracks: Array = instance.triggered_automation_tracks[automation_name] + for track in automation.tracks: + if track == null: + continue + if track.trigger_mode != SfxTrack.TriggerMode.TRIGGER_TIMELINE: + continue + if triggered_tracks.has(track): + continue + if not _automation_threshold_crossed(automation, previous_value, current_value, track.offset, initial): + continue + + if _start_voice(instance, track, automation): + triggered_tracks.append(track) + + +func _automation_threshold_crossed(automation: SfxAutomation, previous_value: float, current_value: float, threshold: float, initial: bool) -> bool: + var ascending: bool = automation.max_domain >= automation.min_domain + if initial: + return current_value >= threshold if ascending else current_value <= threshold + if ascending: + return previous_value < threshold and current_value >= threshold + return previous_value > threshold and current_value <= threshold + + +func _update_adsr(instance: EventInstance, delta: float) -> void: + if not instance.event.adsr_enabled: + instance.adsr_stage = &"sustain" + instance.adsr_elapsed = 0.0 + return + + instance.adsr_elapsed += delta + + match instance.adsr_stage: + &"attack": + if instance.event.attack <= 0.0 or instance.adsr_elapsed >= instance.event.attack: + instance.adsr_stage = &"decay" + instance.adsr_elapsed = 0.0 + &"decay": + if instance.event.decay <= 0.0 or instance.adsr_elapsed >= instance.event.decay: + instance.adsr_stage = &"sustain" + instance.adsr_elapsed = 0.0 + &"release": + if instance.event.release <= 0.0 or instance.adsr_elapsed >= instance.event.release: + instance.adsr_stage = &"stopped" + instance.adsr_elapsed = 0.0 + _stop_event_instance(instance, true) + + +func _enter_attack(instance: EventInstance) -> void: + if not instance.event.adsr_enabled: + instance.adsr_stage = &"sustain" + instance.adsr_elapsed = 0.0 + return + + if instance.event.attack > 0.0: + instance.adsr_stage = &"attack" + elif instance.event.decay > 0.0: + instance.adsr_stage = &"decay" + else: + instance.adsr_stage = &"sustain" + instance.adsr_elapsed = 0.0 + instance.release_start_gain = 0.0 + + +func _current_adsr_gain(instance: EventInstance) -> float: + if instance == null: + return 1.0 + if not instance.event.adsr_enabled: + return 1.0 + + match instance.adsr_stage: + &"attack": + if instance.event.attack <= 0.0: + return 1.0 + return clampf(instance.adsr_elapsed / instance.event.attack, 0.0, 1.0) + &"decay": + if instance.event.decay <= 0.0: + return clampf(instance.event.sustain, 0.0, 1.0) + return lerpf(1.0, clampf(instance.event.sustain, 0.0, 1.0), clampf(instance.adsr_elapsed / instance.event.decay, 0.0, 1.0)) + &"release": + if instance.event.release <= 0.0: + return 0.0 + return lerpf(instance.release_start_gain, 0.0, clampf(instance.adsr_elapsed / instance.event.release, 0.0, 1.0)) + &"stopped": + return 0.0 + _: + return clampf(instance.event.sustain, 0.0, 1.0) + + +func _enter_track_attack(voice: ActiveVoice) -> void: + if voice == null or voice.track == null or not voice.track.adsr_enabled: + voice.track_adsr_stage = &"sustain" + voice.track_adsr_elapsed = 0.0 + voice.track_release_start_gain = 0.0 + return + + if voice.track.attack > 0.0: + voice.track_adsr_stage = &"attack" + elif voice.track.decay > 0.0: + voice.track_adsr_stage = &"decay" + else: + voice.track_adsr_stage = &"sustain" + voice.track_adsr_elapsed = 0.0 + voice.track_release_start_gain = 0.0 + + +func _update_track_adsr(voice: ActiveVoice, delta: float) -> void: + if voice == null or voice.track == null: + return + if not voice.track.adsr_enabled: + voice.track_adsr_stage = &"sustain" + voice.track_adsr_elapsed = 0.0 + return + + voice.track_adsr_elapsed += delta + + match voice.track_adsr_stage: + &"attack": + if voice.track.attack <= 0.0 or voice.track_adsr_elapsed >= voice.track.attack: + voice.track_adsr_stage = &"decay" + voice.track_adsr_elapsed = 0.0 + &"decay": + if voice.track.decay <= 0.0 or voice.track_adsr_elapsed >= voice.track.decay: + voice.track_adsr_stage = &"sustain" + voice.track_adsr_elapsed = 0.0 + &"release": + if voice.track.release <= 0.0 or voice.track_adsr_elapsed >= voice.track.release: + voice.track_adsr_stage = &"stopped" + voice.track_adsr_elapsed = 0.0 + + +func _current_track_adsr_gain(voice: ActiveVoice) -> float: + if voice == null or voice.track == null or not voice.track.adsr_enabled: + return 1.0 + + match voice.track_adsr_stage: + &"attack": + if voice.track.attack <= 0.0: + return 1.0 + return clampf(voice.track_adsr_elapsed / voice.track.attack, 0.0, 1.0) + &"decay": + if voice.track.decay <= 0.0: + return clampf(voice.track.sustain, 0.0, 1.0) + return lerpf(1.0, clampf(voice.track.sustain, 0.0, 1.0), clampf(voice.track_adsr_elapsed / voice.track.decay, 0.0, 1.0)) + &"release": + if voice.track.release <= 0.0: + return 0.0 + return lerpf(voice.track_release_start_gain, 0.0, clampf(voice.track_adsr_elapsed / voice.track.release, 0.0, 1.0)) + &"stopped": + return 0.0 + _: + return clampf(voice.track.sustain, 0.0, 1.0) + + +func _begin_track_release(voice: ActiveVoice) -> bool: + if voice == null or voice.track == null or not voice.track.adsr_enabled: + return false + if voice.track_adsr_stage == &"release" or voice.track_adsr_stage == &"stopped": + return true + voice.track_release_start_gain = _current_track_adsr_gain(voice) + voice.track_adsr_stage = &"release" + voice.track_adsr_elapsed = 0.0 + return voice.track.release > 0.0 + + +func _find_active_voice_index(player) -> int: + for index in range(_active_voices.size()): + if _active_voices[index].player == player: + return index + return -1 + + +func _find_voice(instance: EventInstance, track: SfxTrack, automation_name: StringName = &"") -> ActiveVoice: + for voice in _active_voices: + if voice.event_instance == instance and voice.track == track and voice.automation_name == automation_name: + return voice + return null + + +func _find_voice_by_player_token(player, token: int) -> ActiveVoice: + for voice in _active_voices: + if voice.player == player and voice.player_token == token: + return voice + return null + + +func _get_instances_for_event(event_name: StringName) -> Array[EventInstance]: + if not _instances.has(event_name): + return [] + var instances: Array[EventInstance] = [] + for raw_instance in _instances[event_name]: + var instance := raw_instance as EventInstance + if instance != null: + instances.append(instance) + return instances + + +func _get_latest_instance(event_name: StringName) -> EventInstance: + var instances: Array[EventInstance] = _get_instances_for_event(event_name) + if instances.is_empty(): + return null + return instances[instances.size() - 1] + + +func _get_latest_stoppable_instance(event_name: StringName) -> EventInstance: + var instances: Array[EventInstance] = _get_instances_for_event(event_name) + for index in range(instances.size() - 1, -1, -1): + var instance: EventInstance = instances[index] + if instance != null and instance.status == &"playing": + return instance + for index in range(instances.size() - 1, -1, -1): + var instance: EventInstance = instances[index] + if instance != null: + return instance + return null + + +func _register_instance(instance: EventInstance) -> void: + if instance == null: + return + if not _instances.has(instance.event_name): + _instances[instance.event_name] = [] + var instances: Array = _instances[instance.event_name] + instances.append(instance) + _instances[instance.event_name] = instances + + +func _remove_instance(instance: EventInstance) -> void: + if instance == null: + return + if not _instances.has(instance.event_name): + return + var instances: Array = _instances[instance.event_name] + var index := instances.find(instance) + if index != -1: + instances.remove_at(index) + if instances.is_empty(): + _instances.erase(instance.event_name) + else: + _instances[instance.event_name] = instances + + +func _has_instance(instance: EventInstance) -> bool: + if instance == null: + return false + return _get_instances_for_event(instance.event_name).has(instance) + + +func _get_available_player(): + for player in _players: + if _find_active_voice_index(player) == -1: + return player + return null + + +func _find_voice_to_steal() -> ActiveVoice: + var oldest_releasing: ActiveVoice = null + var oldest_global: ActiveVoice = null + for voice in _active_voices: + if voice == null: + continue + if oldest_global == null or voice.creation_order < oldest_global.creation_order: + oldest_global = voice + if voice.event_instance != null and voice.event_instance.status == &"releasing": + if oldest_releasing == null or voice.creation_order < oldest_releasing.creation_order: + oldest_releasing = voice + return oldest_releasing if oldest_releasing != null else oldest_global + + +func _acquire_player_for_new_voice(): + var player = _get_available_player() + if player != null: + return player + + var victim := _find_voice_to_steal() + if victim == null or not is_instance_valid(victim.player): + return null + + var stolen_player = victim.player + _stop_voice(_find_active_voice_index(stolen_player)) + if _find_active_voice_index(stolen_player) != -1: + return null + return stolen_player + + +func _resolve_audio_bus(track: SfxTrack, automation: SfxAutomation = null) -> StringName: + if track != null and not String(track.audio_bus).is_empty(): + return track.audio_bus + if automation != null and not String(automation.audio_bus).is_empty(): + return automation.audio_bus + return &"Master" + + +func _get_curve_duration(curve: Curve) -> float: + if curve == null: + return 0.0 + return maxf(curve.max_domain - curve.min_domain, 0.0) + + +func _sample_curve_gain(curve: Curve, elapsed: float) -> float: + if curve == null: + return 1.0 + + var duration := _get_curve_duration(curve) + if duration <= 0.0: + return clampf(curve.sample(curve.max_domain), 0.0, 1.0) + + + var sample_position := curve.min_domain + clampf(elapsed, 0.0, duration) + + return clampf(curve.sample(sample_position), 0.0, 1.0) + + +func _sample_time_fade_out_curve(curve: Curve, remaining: float) -> float: + if curve == null: + return 1.0 + + var duration := _get_curve_duration(curve) + if duration <= 0.0: + return clampf(curve.sample(curve.max_domain), 0.0, 1.0) + + var clamped_remaining := clampf(remaining, 0.0, duration) + var sample_position := curve.min_domain + (duration - clamped_remaining) + return clampf(curve.sample(sample_position), 0.0, 1.0) + + +func _sample_automation_curve(curve: Curve, track: SfxTrack, value: float, default_value: float) -> float: + if curve == null: + return 1.0 + + var sample_position := clampf(value - track.offset, curve.min_domain, curve.max_domain) + return curve.sample(sample_position) + + +func _sample_automation_domain_curve(curve: Curve, value: float, default_value: float) -> float: + if curve == null: + return 1.0 + + var sample_position := clampf(value, curve.min_domain, curve.max_domain) + return curve.sample(sample_position) + + +func _sample_automation_fade_out_curve(curve: Curve, track: SfxTrack, value: float, default_value: float) -> float: + if curve == null: + return 1.0 + + if track.length > 0.0: + var duration := _get_curve_duration(curve) + if duration <= 0.0: + return clampf(curve.sample(curve.max_domain), 0.0, 1.0) + + var fade_end := track.offset + track.length + var remaining := maxf(fade_end - value, 0.0) + var clamped_remaining := clampf(remaining, 0.0, duration) + var sample_position := curve.min_domain + (duration - clamped_remaining) + return curve.sample(sample_position) + + var local_offset := track.offset + + var local_value := clampf(value - local_offset, curve.min_domain, curve.max_domain) + var sample_position := curve.min_domain + (curve.max_domain - local_value) + return curve.sample(sample_position) + + +func _is_generator_voice(voice: ActiveVoice) -> bool: + return voice.generator_playback != null and voice.generator_stream_playback != null + + +func _set_player_gain(player, gain: float) -> void: + player.volume_db = linear_to_db(maxf(gain, 0.0001)) + + +func _build_generator_context(voice: ActiveVoice, delta: float) -> Dictionary: + var playback_position := 0.0 + if is_instance_valid(voice.player): + playback_position = voice.player.get_playback_position() + + var automation_value = null + if voice.automation != null: + automation_value = voice.event_instance.parameters.get(voice.automation_name, voice.automation.min_domain) + + return { + "delta": delta, + "playback_position": playback_position, + "event_name": voice.event_instance.event_name, + "track": voice.track, + "player": voice.player, + "stream_playback": voice.generator_stream_playback, + "event_time": voice.event_instance.playback_time, + "parameters": voice.event_instance.parameters, + "automation_name": voice.automation_name, + "automation_value": automation_value, + } + + +func _pump_generator_voice(voice: ActiveVoice, delta: float) -> void: + if not _is_generator_voice(voice): + return + voice.generator_playback.update(voice.generator_state, _build_generator_context(voice, delta)) + + +func _build_track_stream(track: SfxTrack) -> AudioStream: + if track == null or track.stream == null: + return null + + if track.stream is AudioStreamGenerator: + if track.generator_playback == null: + push_error("AudioStreamGenerator track requires generator_playback") + return null + var stream_copy = track.stream.duplicate(true) + return stream_copy as AudioStream if stream_copy != null else track.stream + + return track.stream + + +func _resolve_voice_start_position(instance: EventInstance, track: SfxTrack, automation: SfxAutomation = null) -> float: + var start_position := maxf(track.stream_offset, 0.0) + if automation == null: + if track.trigger_mode == SfxTrack.TriggerMode.TRIGGER_SUSTAIN: + return start_position + start_position += maxf(instance.playback_time - track.offset, 0.0) + return start_position + + +func _resolve_phase_locked_automation_start_position(instance: EventInstance, track: SfxTrack, automation: SfxAutomation, stream: AudioStream) -> float: + var start_position := maxf(track.stream_offset, 0.0) + if instance == null or automation == null or stream == null: + return start_position + if not automation.phase_locked or automation.phase_period <= 0.0: + return start_position + + var stream_length := maxf(stream.get_length(), 0.0) + var available_length := maxf(stream_length - start_position, 0.0) + if available_length <= 0.0: + return start_position + + var phase := fposmod(instance.playback_time + track.phase_offset, automation.phase_period) + if phase > available_length: + phase = fposmod(phase, available_length) + return start_position + phase + + +func _resolve_local_track_time(voice: ActiveVoice, playback_position: float) -> float: + return maxf(playback_position - voice.stream_start_position, 0.0) + + +func _resolve_remaining_track_time(voice: ActiveVoice, playback_position: float) -> float: + if voice.stream == null: + return 0.0 + var end_position := voice.stream_end_position + if end_position <= 0.0: + end_position = maxf(voice.stream.get_length(), 0.0) + if end_position <= 0.0: + return 0.0 + return maxf(end_position - playback_position, 0.0) + + +func _resolve_voice_end_position(track: SfxTrack, stream: AudioStream) -> float: + if stream == null: + return 0.0 + + var stream_length := maxf(stream.get_length(), 0.0) + if stream_length <= 0.0: + return 0.0 + + var end_position := stream_length + if track.length > 0.0: + end_position = minf(maxf(track.stream_offset, 0.0) + track.length, stream_length) + return end_position + + +func _start_voice(instance: EventInstance, track: SfxTrack, automation: SfxAutomation = null) -> bool: + var automation_name := &"" + if automation != null: + automation_name = automation.parameter_name + var existing_voice := _find_voice(instance, track, automation_name) + if existing_voice != null: + _apply_voice_state(existing_voice) + return true + else: + var time_voice := _find_voice(instance, track) + if time_voice != null: + return true + + var player = _acquire_player_for_new_voice() + if player == null: + return false + + var stream := _build_track_stream(track) + if stream == null: + return false + + var start_position := _resolve_voice_start_position(instance, track, automation) + if automation != null: + start_position = _resolve_phase_locked_automation_start_position(instance, track, automation, stream) + var stream_length := maxf(stream.get_length(), 0.0) + if stream_length > 0.0: + start_position = clampf(start_position, 0.0, stream_length) + var end_position := _resolve_voice_end_position(track, stream) + if end_position > 0.0: + start_position = minf(start_position, end_position) + + player.stream = stream + player.bus = _resolve_audio_bus(track, automation) + player.pitch_scale = 1.0 + var player_token := int(_player_tokens.get(player, 0)) + 1 + _player_tokens[player] = player_token + player.play(start_position) + + var voice := ActiveVoice.new() + voice.player = player + voice.event_instance = instance + voice.track = track + voice.stream = stream + voice.stream_start_position = start_position + voice.stream_end_position = end_position + voice.automation = automation + voice.automation_name = automation_name + voice.player_token = player_token + voice.creation_order = _voice_creation_counter + _voice_creation_counter += 1 + _enter_track_attack(voice) + + if stream is AudioStreamGenerator: + voice.generator_playback = track.generator_playback + voice.generator_stream_playback = player.get_stream_playback() as AudioStreamGeneratorPlayback + if voice.generator_stream_playback == null: + push_error("Failed to get AudioStreamGeneratorPlayback for generator track") + _reset_player(player, true) + return false + voice.generator_state = voice.generator_playback.create_state(voice.generator_stream_playback, track) + + _active_voices.append(voice) + _apply_voice_state(voice) + _pump_generator_voice(voice, 0.0) + return true + + +func _resolve_automation_raw_gain(voice: ActiveVoice) -> float: + if voice == null or voice.automation == null or voice.event_instance == null: + return 1.0 + + var automation_value = float(voice.event_instance.parameters.get(voice.automation_name, voice.automation.min_domain)) + var fade_in := clampf(_sample_automation_curve(voice.track.fade_in_curve, voice.track, automation_value, 1.0), 0.0, 1.0) + var fade_out := clampf(_sample_automation_fade_out_curve(voice.track.fade_out_curve, voice.track, automation_value, 1.0), 0.0, 1.0) + + if voice.automation.crossfade_mode == SfxAutomation.CrossfadeMode.EQUAL_POWER: + return sqrt(fade_in) * sqrt(fade_out) + return fade_in * fade_out + + +func _resolve_automation_track_gain(voice: ActiveVoice) -> float: + var raw_gain := _resolve_automation_raw_gain(voice) + if voice == null or voice.automation == null or voice.event_instance == null: + return raw_gain + + if voice.automation.crossfade_mode == SfxAutomation.CrossfadeMode.EQUAL_POWER: + var power_sum := 0.0 + for candidate in _active_voices: + if candidate == null: + continue + if candidate.automation == null: + continue + if candidate.event_instance != voice.event_instance: + continue + if candidate.automation_name != voice.automation_name: + continue + var candidate_gain := _resolve_automation_raw_gain(candidate) + power_sum += candidate_gain * candidate_gain + if power_sum > 1.0: + return raw_gain / sqrt(power_sum) + return raw_gain + + var sum_raw := 0.0 + for candidate in _active_voices: + if candidate == null: + continue + if candidate.automation == null: + continue + if candidate.event_instance != voice.event_instance: + continue + if candidate.automation_name != voice.automation_name: + continue + sum_raw += _resolve_automation_raw_gain(candidate) + + if sum_raw > 1.0: + return raw_gain / sum_raw + return raw_gain + + +func _apply_voice_state(voice: ActiveVoice) -> void: + if voice == null or not is_instance_valid(voice.player): + return + + var track_gain := 1.0 + var pitch := 1.0 + if voice.automation: + var automation_value = float(voice.event_instance.parameters.get(voice.automation_name, voice.automation.min_domain)) + track_gain = _resolve_automation_track_gain(voice) + pitch = _sample_automation_curve(voice.track.pitch_curve, voice.track, automation_value, 1.0) + pitch *= _sample_automation_domain_curve(voice.automation.pitch_curve, automation_value, 1.0) + else: + var playback_position := 0.0 + if voice.player.playing: + playback_position = voice.player.get_playback_position() + var local_track_time := _resolve_local_track_time(voice, playback_position) + var remaining_track_time := _resolve_remaining_track_time(voice, playback_position) + track_gain = clampf(_sample_curve_gain(voice.track.fade_in_curve, local_track_time), 0.0, 1.0) + if voice.stopping: + var stop_remaining := maxf(voice.stop_fade_duration - voice.stop_elapsed, 0.0) + track_gain *= clampf(_sample_time_fade_out_curve(voice.track.fade_out_curve, stop_remaining), 0.0, 1.0) + else: + track_gain *= clampf(_sample_time_fade_out_curve(voice.track.fade_out_curve, remaining_track_time), 0.0, 1.0) + pitch = _sample_curve_gain(voice.track.pitch_curve, local_track_time) + + _set_player_gain(voice.player, clampf(_current_adsr_gain(voice.event_instance), 0.0, 1.0) * clampf(_current_track_adsr_gain(voice), 0.0, 1.0) * track_gain) + voice.player.pitch_scale = maxf(pitch, 0.01) + + +func _cleanup_voice(voice: ActiveVoice) -> void: + if voice != null and voice.generator_playback != null: + voice.generator_playback.cleanup(voice.generator_state) + + +func _voice_owns_player(voice: ActiveVoice) -> bool: + if voice == null or not is_instance_valid(voice.player): + return false + return int(_player_tokens.get(voice.player, 0)) == voice.player_token + + +func _release_voice(index: int) -> void: + if index == -1: + return + + var voice := _active_voices[index] + _cleanup_voice(voice) + if _voice_owns_player(voice): + voice.player.volume_db = 0.0 + voice.player.pitch_scale = 1.0 + _active_voices.remove_at(index) + + if _active_voices.is_empty() and _instances.is_empty(): + finished.emit() + + +func _stop_voice(index: int) -> void: + if index == -1: + return + + var voice := _active_voices[index] + var owns_player := _voice_owns_player(voice) + _release_voice(index) + if owns_player: + _reset_player(voice.player, true) + + +func _reset_player(player, clear_stream := false) -> void: + if not is_instance_valid(player): + return + player.stop() + player.pitch_scale = 1.0 + player.volume_db = 0.0 + if clear_stream: + player.stream = null + + +func _update_voice(index: int, delta: float) -> bool: + if index < 0 or index >= _active_voices.size(): + return false + + var voice := _active_voices[index] + if voice.event_instance == null or not _has_instance(voice.event_instance): + _stop_voice(index) + return false + + if not is_instance_valid(voice.player) or not voice.player.playing: + _release_voice(index) + return false + + if voice.stopping: + voice.stop_elapsed += delta + if voice.stop_fade_duration <= 0.0 or voice.stop_elapsed >= voice.stop_fade_duration: + _stop_voice(index) + return false + + _update_track_adsr(voice, delta) + if voice.track_adsr_stage == &"stopped": + _stop_voice(index) + return false + + if voice.automation == null and voice.stream_end_position > 0.0 and voice.player.get_playback_position() >= voice.stream_end_position: + _stop_voice(index) + return false + + _apply_voice_state(voice) + _pump_generator_voice(voice, delta) + return true + + +func _stop_event_instance(instance: EventInstance, immediate: bool) -> void: + if instance == null: + return + + if immediate: + _stop_instance_voices(instance) + _remove_instance(instance) + if _active_voices.is_empty() and _instances.is_empty(): + finished.emit() + return + + if instance.status == &"releasing": + return + + instance.status = &"releasing" + instance.release_time = 0.0 + instance.release_start_gain = _current_adsr_gain(instance) + if instance.event.adsr_enabled and instance.event.release > 0.0: + instance.adsr_stage = &"release" + else: + instance.adsr_stage = &"sustain" + _begin_instance_voice_stop(instance) + instance.adsr_elapsed = 0.0 + _refresh_sustain_tracks(instance, -1.0, instance.release_time) + + +func _collect_finished_instances() -> void: + var finished_instances: Array[EventInstance] = [] + for raw_instances in _instances.values(): + for raw_instance in raw_instances: + var instance := raw_instance as EventInstance + if instance == null: + continue + + if instance.status == &"releasing" and instance.adsr_stage == &"stopped": + finished_instances.append(instance) + continue + + if instance.status == &"playing": + continue + + if instance.status == &"releasing" and not _instance_has_pending_release_activity(instance): + finished_instances.append(instance) + continue + + for instance in finished_instances: + _remove_instance(instance) + + if _active_voices.is_empty() and _instances.is_empty(): + finished.emit() + + +func _notify_process_requirement_changed() -> void: + process_requirement_changed.emit(requires_process()) + + +func _instance_has_pending_release_activity(instance: EventInstance) -> bool: + if instance == null: + return false + for voice in _active_voices: + if voice.event_instance == instance: + return true + return false + + +func _stop_instance_voices(instance: EventInstance) -> void: + for index in range(_active_voices.size() - 1, -1, -1): + var voice := _active_voices[index] + if voice.event_instance == instance: + _stop_voice(index) + + +func _begin_instance_voice_stop(instance: EventInstance) -> void: + for index in range(_active_voices.size() - 1, -1, -1): + var voice := _active_voices[index] + if voice.event_instance != instance: + continue + if not _begin_voice_stop(voice): + _stop_voice(index) + + +func _begin_voice_stop(voice: ActiveVoice) -> bool: + if voice == null or voice.automation != null: + return false + if _begin_track_release(voice): + _apply_voice_state(voice) + return true + var fade_duration := _get_curve_duration(voice.track.fade_out_curve) + if fade_duration <= 0.0: + return false + voice.stopping = true + voice.stop_elapsed = 0.0 + voice.stop_fade_duration = fade_duration + _apply_voice_state(voice) + return true diff --git a/demo/addons/gnd_sfx/sfx_playback_runtime.gd.uid b/demo/addons/gnd_sfx/sfx_playback_runtime.gd.uid new file mode 100644 index 00000000..ab9cacab --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_playback_runtime.gd.uid @@ -0,0 +1 @@ +uid://c2xe7k1k4kbup diff --git a/demo/addons/gnd_sfx/sfx_player.gd b/demo/addons/gnd_sfx/sfx_player.gd new file mode 100644 index 00000000..b6632b8a --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_player.gd @@ -0,0 +1,337 @@ +@tool +extends Node +class_name SfxPlayer + +const PLAYBACK_NONE_OPTION := "" + +signal finished + +var _runtime = SfxPlaybackRuntime.new() + +@export var bank: SfxBank: + set(value): + bank = value + _events_changed() + +@export var max_tracks: int = 10: + set(value): + max_tracks = value + sync_values(true) + +@export var max_polyphony: int = 1: + set(value): + max_polyphony = value + sync_values() + +@export_group("Playback", "playback") +@export var playback_enabled: bool = false: + set(value): + playback_enabled = value + sync_values() + +@export var playback_effect: StringName = "": + set(value): + if value == PLAYBACK_NONE_OPTION: + value = &"" + playback_effect = value + _sanitize_playback_selection() + _notify_playback_property_list_changed() + sync_values() + +@export var playback_automation: StringName = "": + set(value): + if value == PLAYBACK_NONE_OPTION: + value = &"" + playback_automation = value + sync_values() + +@export var playback_automation_value: float = 0.0: + set(value): + playback_automation_value = value + sync_values() + +var _preview_enabled: bool = false +var _preview_effect: StringName = &"" +var _preview_automation: StringName = &"" +var _preview_automation_value: float = 0.0 +var _watched_events: Array[SfxEvent] = [] +var _watched_automations: Array[SfxAutomation] = [] +var _players: Array = [] + + +func _enter_tree() -> void: + _reset_playback_preview_state() + if Engine.is_editor_hint(): + playback_enabled = false + _notify_playback_property_list_changed() + + +func _ready() -> void: + _ensure_runtime_connections() + _connect_playback_resource_watchers() + sync_values(true) + _update_process_state() + + +func _exit_tree() -> void: + _disconnect_playback_resource_watchers() + + +func _process(delta: float) -> void: + _runtime.update(delta) + + +func _events_changed() -> void: + _disconnect_playback_resource_watchers() + _connect_playback_resource_watchers() + _sanitize_playback_selection() + _notify_playback_property_list_changed() + + +func sync_values(rebuild := false) -> void: + _ensure_runtime_connections() + if rebuild: + _runtime.clear() + for player in _players: + if is_instance_valid(player): + remove_child(player) + player.queue_free() + _players = [] + var index := 0 + while index < max_tracks: + var player := AudioStreamPlayer.new() + _players.append(player) + add_child(player) + player.finished.connect(_on_player_finished.bind(player)) + index += 1 + _runtime.set_players(_players) + + for player in _players: + player.max_polyphony = max_polyphony + + _sync_editor_playback() + _update_process_state() + + +func play(event_name: StringName, offset_or_parameters = null, parameters: Dictionary = {}) -> void: + if not bank: + return + + var offset := 0.0 + if offset_or_parameters is Dictionary: + parameters = offset_or_parameters + elif offset_or_parameters != null: + offset = float(offset_or_parameters) + + var event := bank.get_event(event_name) + if event != null: + _runtime.play(event, offset, parameters) + + +func seek(event_name: StringName, offset: float) -> void: + _runtime.seek(event_name, offset) + + +func modulate(event_name: StringName, parameters: Dictionary) -> void: + _runtime.modulate(event_name, parameters) + + +func set_parameters(parameters: Dictionary) -> void: + _runtime.set_parameters(parameters) + + +func stop(event_name_or_immediate = null, immediate: bool = false) -> void: + _runtime.stop(event_name_or_immediate, immediate) + + +func is_playing(event_name: StringName) -> bool: + return _runtime.is_playing(event_name) + + +func play_automation(event_name: StringName, automation_name: StringName, value: float = 0.0, restart: bool = false) -> void: + if not bank: + return + + var event := bank.get_event(event_name) + if event != null: + _runtime.play_automation(event, automation_name, value, restart) + + +func stop_automation(event_name: StringName, automation_name: StringName, immediate: bool = false) -> void: + if not bank: + return + + var event := bank.get_event(event_name) + if event != null: + _runtime.stop_automation(event, automation_name, immediate) + + +func _ensure_runtime_connections() -> void: + if not _runtime.process_requirement_changed.is_connected(_on_runtime_process_requirement_changed): + _runtime.process_requirement_changed.connect(_on_runtime_process_requirement_changed) + if not _runtime.finished.is_connected(_on_runtime_finished): + _runtime.finished.connect(_on_runtime_finished) + + +func _on_runtime_process_requirement_changed(required: bool) -> void: + if Engine.is_editor_hint(): + _update_process_state() + return + set_process(required) + + +func _on_runtime_finished() -> void: + finished.emit() + + +func _on_player_finished(player: AudioStreamPlayer) -> void: + _runtime.handle_player_finished(player) + + +func _sync_editor_playback() -> void: + if not Engine.is_editor_hint() or not is_inside_tree(): + return + + var config_changed := ( + playback_enabled != _preview_enabled + or playback_effect != _preview_effect + or playback_automation != _preview_automation + ) + var value_changed := not is_equal_approx(playback_automation_value, _preview_automation_value) + + if not config_changed and not value_changed: + return + + if not playback_enabled or String(playback_effect).is_empty(): + _runtime.clear() + _store_playback_preview_state() + return + + if config_changed: + _runtime.clear() + play(playback_effect, 0.0, _build_preview_parameters()) + _store_playback_preview_state() + return + + if value_changed and not String(playback_automation).is_empty(): + modulate(playback_effect, {playback_automation: playback_automation_value}) + _store_playback_preview_state() + + +func _build_preview_parameters() -> Dictionary: + if String(playback_automation).is_empty(): + return {} + return {playback_automation: playback_automation_value} + + +func _store_playback_preview_state() -> void: + _preview_enabled = playback_enabled + _preview_effect = playback_effect + _preview_automation = playback_automation + _preview_automation_value = playback_automation_value + + +func _reset_playback_preview_state() -> void: + _preview_enabled = false + _preview_effect = &"" + _preview_automation = &"" + _preview_automation_value = 0.0 + + +func _update_process_state() -> void: + if Engine.is_editor_hint(): + set_process(_runtime.requires_process()) + + +func _validate_property(property: Dictionary) -> void: + if property.name == "playback_effect": + property.hint = PROPERTY_HINT_ENUM + property.hint_string = _build_playback_effect_hint() + elif property.name == "playback_automation": + property.hint = PROPERTY_HINT_ENUM + property.hint_string = _build_playback_automation_hint() + + +func _build_playback_effect_hint() -> String: + if not bank: + return "" + + var options := PackedStringArray([PLAYBACK_NONE_OPTION]) + for event in bank.events: + if event == null or String(event.name).is_empty(): + continue + options.append(String(event.name)) + return ",".join(options) + + +func _build_playback_automation_hint() -> String: + var options := PackedStringArray([PLAYBACK_NONE_OPTION]) + var event := _find_playback_event() + if event == null: + return ",".join(options) + + for automation in event.automations: + if automation == null or String(automation.parameter_name).is_empty(): + continue + options.append(String(automation.parameter_name)) + return ",".join(options) + + +func _find_playback_event() -> SfxEvent: + if not bank or not playback_effect: + return null + return bank.get_event(playback_effect) + + +func _sanitize_playback_selection() -> void: + var event := _find_playback_event() + if not String(playback_effect).is_empty() and event == null: + playback_effect = &"" + playback_automation = &"" + return + + if String(playback_automation).is_empty() or event == null: + return + + for automation in event.automations: + if automation != null and automation.parameter_name == playback_automation: + return + playback_automation = &"" + + +func _notify_playback_property_list_changed() -> void: + if Engine.is_editor_hint(): + notify_property_list_changed() + + +func _connect_playback_resource_watchers() -> void: + if not Engine.is_editor_hint() or not bank: + return + + for event in bank.events: + if event == null or _watched_events.has(event): + continue + event.changed.connect(_on_playback_source_changed) + _watched_events.append(event) + + for automation in event.automations: + if automation == null or _watched_automations.has(automation): + continue + automation.changed.connect(_on_playback_source_changed) + _watched_automations.append(automation) + + +func _disconnect_playback_resource_watchers() -> void: + for event in _watched_events: + if is_instance_valid(event) and event.changed.is_connected(_on_playback_source_changed): + event.changed.disconnect(_on_playback_source_changed) + for automation in _watched_automations: + if is_instance_valid(automation) and automation.changed.is_connected(_on_playback_source_changed): + automation.changed.disconnect(_on_playback_source_changed) + _watched_events.clear() + _watched_automations.clear() + + +func _on_playback_source_changed() -> void: + _sanitize_playback_selection() + sync_values() diff --git a/demo/addons/gnd_sfx/sfx_player.gd.uid b/demo/addons/gnd_sfx/sfx_player.gd.uid new file mode 100644 index 00000000..a734b92c --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_player.gd.uid @@ -0,0 +1 @@ +uid://lj2sjiebi4m0 diff --git a/demo/addons/gnd_sfx/sfx_player_3d.gd b/demo/addons/gnd_sfx/sfx_player_3d.gd new file mode 100644 index 00000000..7b984f4a --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_player_3d.gd @@ -0,0 +1,365 @@ +@tool +extends Node3D +class_name SfxPlayer3D + +const PLAYBACK_NONE_OPTION := "" + +signal finished + +var _runtime = SfxPlaybackRuntime.new() + +@export var bank: SfxBank: + set(value): + bank = value + _events_changed() + +@export var max_tracks: int = 10: + set(value): + max_tracks = value + sync_values(true) + +@export var attenuation_model: AudioStreamPlayer3D.AttenuationModel = AudioStreamPlayer3D.ATTENUATION_INVERSE_DISTANCE: + set(value): + attenuation_model = value + sync_values() + +@export_range(0.01, 100.0, 0.01) var unit_size: float = 10: + set(value): + unit_size = value + sync_values() + +@export var max_distance: int = 0: + set(value): + max_distance = value + sync_values() + +@export var max_polyphony: int = 1: + set(value): + max_polyphony = value + sync_values() + +@export_range(0.0, 3.0) var panning_strength: float = 1.0: + set(value): + panning_strength = value + sync_values() + +@export_group("Playback", "playback") +@export var playback_enabled: bool = false: + set(value): + playback_enabled = value + sync_values() + +@export var playback_effect: StringName = "": + set(value): + if value == PLAYBACK_NONE_OPTION: + value = &"" + playback_effect = value + _sanitize_playback_selection() + _notify_playback_property_list_changed() + sync_values() + +@export var playback_automation: StringName = "": + set(value): + if value == PLAYBACK_NONE_OPTION: + value = &"" + playback_automation = value + sync_values() + +@export var playback_automation_value: float = 0.0: + set(value): + playback_automation_value = value + sync_values() + +var _preview_enabled: bool = false +var _preview_effect: StringName = &"" +var _preview_automation: StringName = &"" +var _preview_automation_value: float = 0.0 +var _watched_events: Array[SfxEvent] = [] +var _watched_automations: Array[SfxAutomation] = [] +var _players: Array = [] + + +func _enter_tree() -> void: + _reset_playback_preview_state() + if Engine.is_editor_hint(): + playback_enabled = false + _notify_playback_property_list_changed() + + +func _ready() -> void: + _ensure_runtime_connections() + _connect_playback_resource_watchers() + sync_values(true) + _update_process_state() + + +func _events_changed() -> void: + _disconnect_playback_resource_watchers() + _connect_playback_resource_watchers() + _sanitize_playback_selection() + _notify_playback_property_list_changed() + + +func _exit_tree() -> void: + _disconnect_playback_resource_watchers() + + +func _process(delta: float) -> void: + _runtime.update(delta) + + +func sync_values(rebuild := false) -> void: + _ensure_runtime_connections() + if rebuild: + _runtime.clear() + for player in _players: + if is_instance_valid(player): + remove_child(player) + player.queue_free() + _players = [] + var index := 0 + while index < max_tracks: + var player := AudioStreamPlayer3D.new() + _players.append(player) + add_child(player) + player.finished.connect(_on_player_finished.bind(player)) + index += 1 + _runtime.set_players(_players) + + for player in _players: + player.max_polyphony = max_polyphony + player.max_distance = max_distance + player.panning_strength = panning_strength + player.attenuation_model = attenuation_model + player.unit_size = unit_size + + _sync_editor_playback() + _update_process_state() + + +func play(event_name: StringName, offset_or_parameters = null, parameters: Dictionary = {}) -> void: + if not bank: + return + + var offset := 0.0 + if offset_or_parameters is Dictionary: + parameters = offset_or_parameters + elif offset_or_parameters != null: + offset = float(offset_or_parameters) + + var event := bank.get_event(event_name) + if event != null: + _runtime.play(event, offset, parameters) + + +func seek(event_name: StringName, offset: float) -> void: + _runtime.seek(event_name, offset) + + +func modulate(event_name: StringName, parameters: Dictionary) -> void: + _runtime.modulate(event_name, parameters) + + +func set_parameters(parameters: Dictionary) -> void: + _runtime.set_parameters(parameters) + + +func stop(event_name_or_immediate = null, immediate: bool = false) -> void: + _runtime.stop(event_name_or_immediate, immediate) + + +func is_playing(event_name: StringName) -> bool: + return _runtime.is_playing(event_name) + + +func play_automation(event_name: StringName, automation_name: StringName, value: float = 0.0, restart: bool = false) -> void: + if not bank: + return + + var event := bank.get_event(event_name) + if event != null: + _runtime.play_automation(event, automation_name, value, restart) + + +func stop_automation(event_name: StringName, automation_name: StringName, immediate: bool = false) -> void: + if not bank: + return + + var event := bank.get_event(event_name) + if event != null: + _runtime.stop_automation(event, automation_name, immediate) + + +func _ensure_runtime_connections() -> void: + if not _runtime.process_requirement_changed.is_connected(_on_runtime_process_requirement_changed): + _runtime.process_requirement_changed.connect(_on_runtime_process_requirement_changed) + if not _runtime.finished.is_connected(_on_runtime_finished): + _runtime.finished.connect(_on_runtime_finished) + + +func _on_runtime_process_requirement_changed(required: bool) -> void: + if Engine.is_editor_hint(): + _update_process_state() + return + set_process(required) + + +func _on_runtime_finished() -> void: + finished.emit() + + +func _on_player_finished(player: AudioStreamPlayer3D) -> void: + _runtime.handle_player_finished(player) + + +func _sync_editor_playback() -> void: + if not Engine.is_editor_hint() or not is_inside_tree(): + return + + var config_changed := ( + playback_enabled != _preview_enabled + or playback_effect != _preview_effect + or playback_automation != _preview_automation + ) + var value_changed := not is_equal_approx(playback_automation_value, _preview_automation_value) + + if not config_changed and not value_changed: + return + + if not playback_enabled or String(playback_effect).is_empty(): + #_runtime.clear() + if _preview_enabled and _preview_effect: + stop(_preview_effect) + else: + _runtime.clear() + _store_playback_preview_state() + return + + if config_changed: + _runtime.clear() + play(playback_effect, 0.0, _build_preview_parameters()) + _store_playback_preview_state() + return + + if value_changed and not String(playback_automation).is_empty(): + modulate(playback_effect, {playback_automation: playback_automation_value}) + _store_playback_preview_state() + + +func _build_preview_parameters() -> Dictionary: + if String(playback_automation).is_empty(): + return {} + return {playback_automation: playback_automation_value} + + +func _store_playback_preview_state() -> void: + _preview_enabled = playback_enabled + _preview_effect = playback_effect + _preview_automation = playback_automation + _preview_automation_value = playback_automation_value + + +func _reset_playback_preview_state() -> void: + _preview_enabled = false + _preview_effect = &"" + _preview_automation = &"" + _preview_automation_value = 0.0 + + +func _update_process_state() -> void: + if Engine.is_editor_hint(): + set_process(_runtime.requires_process()) + + +func _validate_property(property: Dictionary) -> void: + if property.name == "playback_effect": + property.hint = PROPERTY_HINT_ENUM + property.hint_string = _build_playback_effect_hint() + elif property.name == "playback_automation": + property.hint = PROPERTY_HINT_ENUM + property.hint_string = _build_playback_automation_hint() + + +func _build_playback_effect_hint() -> String: + if not bank: + return "" + + var options := PackedStringArray([PLAYBACK_NONE_OPTION]) + for event in bank.events: + if event == null or String(event.name).is_empty(): + continue + options.append(String(event.name)) + return ",".join(options) + + +func _build_playback_automation_hint() -> String: + var options := PackedStringArray([PLAYBACK_NONE_OPTION]) + var event := _find_playback_event() + if event == null: + return ",".join(options) + + for automation in event.automations: + if automation == null or String(automation.parameter_name).is_empty(): + continue + options.append(String(automation.parameter_name)) + return ",".join(options) + + +func _find_playback_event() -> SfxEvent: + if not bank or not playback_effect: + return null + return bank.get_event(playback_effect) + + +func _sanitize_playback_selection() -> void: + var event := _find_playback_event() + if not String(playback_effect).is_empty() and event == null: + playback_effect = &"" + playback_automation = &"" + return + + if String(playback_automation).is_empty() or event == null: + return + + for automation in event.automations: + if automation != null and automation.parameter_name == playback_automation: + return + playback_automation = &"" + + +func _notify_playback_property_list_changed() -> void: + if Engine.is_editor_hint(): + notify_property_list_changed() + + +func _connect_playback_resource_watchers() -> void: + if not Engine.is_editor_hint() or not bank: + return + + for event in bank.events: + if event == null or _watched_events.has(event): + continue + event.changed.connect(_on_playback_source_changed) + _watched_events.append(event) + + for automation in event.automations: + if automation == null or _watched_automations.has(automation): + continue + automation.changed.connect(_on_playback_source_changed) + _watched_automations.append(automation) + + +func _disconnect_playback_resource_watchers() -> void: + for event in _watched_events: + if is_instance_valid(event) and event.changed.is_connected(_on_playback_source_changed): + event.changed.disconnect(_on_playback_source_changed) + for automation in _watched_automations: + if is_instance_valid(automation) and automation.changed.is_connected(_on_playback_source_changed): + automation.changed.disconnect(_on_playback_source_changed) + _watched_events.clear() + _watched_automations.clear() + + +func _on_playback_source_changed() -> void: + _sanitize_playback_selection() + sync_values() diff --git a/demo/addons/gnd_sfx/sfx_player_3d.gd.uid b/demo/addons/gnd_sfx/sfx_player_3d.gd.uid new file mode 100644 index 00000000..f2da9070 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_player_3d.gd.uid @@ -0,0 +1 @@ +uid://uo24v0se8gsf diff --git a/demo/addons/gnd_sfx/sfx_track.gd b/demo/addons/gnd_sfx/sfx_track.gd new file mode 100644 index 00000000..d6957850 --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_track.gd @@ -0,0 +1,25 @@ +@tool +extends Resource +class_name SfxTrack + +enum TriggerMode { + TRIGGER_TIMELINE, + TRIGGER_SUSTAIN +} + +@export var stream : AudioStream +@export var generator_playback: SfxGeneratorPlayback +@export var offset := 0.0 +@export var length := 0.0 +@export var stream_offset := 0.0 +@export var phase_offset := 0.0 +@export var adsr_enabled := false +@export_range(0.0, 30.0, 0.01) var attack := 0.0 +@export_range(0.0, 30.0, 0.01) var decay := 0.0 +@export_range(0.0, 1.0, 0.01) var sustain := 1.0 +@export_range(0.0, 30.0, 0.01) var release := 0.0 +@export var fade_in_curve: Curve +@export var fade_out_curve: Curve +@export var audio_bus : StringName +@export var pitch_curve: Curve +@export var trigger_mode: TriggerMode = TriggerMode.TRIGGER_TIMELINE diff --git a/demo/addons/gnd_sfx/sfx_track.gd.uid b/demo/addons/gnd_sfx/sfx_track.gd.uid new file mode 100644 index 00000000..b36874df --- /dev/null +++ b/demo/addons/gnd_sfx/sfx_track.gd.uid @@ -0,0 +1 @@ +uid://boicinkvs7i0h diff --git a/demo/addons/gnd_sfx/tests/test_sfx_automation_sync_analyzer.gd b/demo/addons/gnd_sfx/tests/test_sfx_automation_sync_analyzer.gd new file mode 100644 index 00000000..f17a03b1 --- /dev/null +++ b/demo/addons/gnd_sfx/tests/test_sfx_automation_sync_analyzer.gd @@ -0,0 +1,186 @@ +extends MaszynaGutTest + + +func test_estimate_period_detects_tick_train() -> void: + var samples := SfxAutomationSyncAnalyzer.normalize_samples(_make_tick_signal(2000.0, 2.0, 0.2, 0.0)) + var period := SfxAutomationSyncAnalyzer.estimate_period(samples, 2000.0, 0.1, 0.3) + + assert_almost_eq(period, 0.2, 0.005, "Autocorrelation should recover the shared tick period") + + +func test_estimate_phase_offset_aligns_shifted_tick_train() -> void: + var reference := SfxAutomationSyncAnalyzer.normalize_samples(_make_tick_signal(2000.0, 2.0, 0.2, 0.0)) + var shifted := SfxAutomationSyncAnalyzer.normalize_samples(_make_tick_signal(2000.0, 2.0, 0.2, 0.05)) + var offset := SfxAutomationSyncAnalyzer.estimate_phase_offset(reference, shifted, 2000.0, 0.2) + + assert_almost_eq(offset, 0.05, 0.005, "Cross-correlation should recover the target phase lag") + + +func test_estimate_loop_offset_finds_earliest_seam_after_leadin() -> void: + var samples := SfxAutomationSyncAnalyzer.normalize_samples(_make_loop_candidate_signal(2000.0, 2.0, 0.2, 0.03)) + var loop_offset := SfxAutomationSyncAnalyzer.estimate_loop_offset(samples, 2000.0, 0.2, 0.03) + + assert_almost_eq(loop_offset, 0.03, 0.01, "Loop offset should land on the first seam that matches the stream tail") + + +func test_select_consensus_period_prefers_dominant_cluster_over_global_median() -> void: + var estimates: Array[float] = [0.33548752834467, 0.49936507936508, 0.50011337868481, 0.22922902494331] + var period := SfxAutomationSyncAnalyzer._select_consensus_period(estimates) + + assert_almost_eq(period, 0.499739229024945, 0.002, "Consensus period should follow the dominant cluster, not outliers") + + +func test_resolve_track_phase_offsets_accumulates_adjacent_pair_offsets() -> void: + var phase_offsets := SfxAutomationSyncAnalyzer._resolve_track_phase_offsets( + [ + { + "active_samples": SfxAutomationSyncAnalyzer.normalize_samples(_make_tick_signal(2000.0, 2.0, 0.5, 0.0)), + "analysis_rate": 2000.0, + }, + { + "active_samples": SfxAutomationSyncAnalyzer.normalize_samples(_make_tick_signal(2000.0, 2.0, 0.5, 0.1)), + "analysis_rate": 2000.0, + }, + { + "active_samples": SfxAutomationSyncAnalyzer.normalize_samples(_make_tick_signal(2000.0, 2.0, 0.5, 0.2)), + "analysis_rate": 2000.0, + }, + ], + 0.5 + ) + + assert_eq(phase_offsets.size(), 3, "Sequential phase resolution should return one offset per track") + assert_almost_eq(phase_offsets[0], 0.0, 0.005, "The first track should remain the phase anchor") + assert_almost_eq(phase_offsets[1], 0.1, 0.005, "The second track should preserve its adjacent lag") + assert_almost_eq(phase_offsets[2], 0.2, 0.005, "The third track should accumulate from the previous track, not re-anchor to the first") + + +func test_estimate_loop_offset_prefers_cycle_stable_seam_when_multiple_matches_exist() -> void: + var samples := SfxAutomationSyncAnalyzer.normalize_samples( + _make_multi_seam_loop_candidate_signal(2000.0, 1.0, 0.2, [0.03, 0.2]) + ) + var loop_offset := SfxAutomationSyncAnalyzer.estimate_loop_offset(samples, 2000.0, 0.2, 0.03, 0.0, 0.8) + + assert_almost_eq(loop_offset, 0.2, 0.01, "Loop offset should prefer a seam that keeps the loop duration phase-stable") + + +func test_estimate_loop_offset_prefers_target_shared_loop_duration() -> void: + var samples := SfxAutomationSyncAnalyzer.normalize_samples( + _make_multi_seam_loop_candidate_signal(2000.0, 1.0, 0.2, [0.03, 0.1]) + ) + var loop_offset := SfxAutomationSyncAnalyzer.estimate_loop_offset(samples, 2000.0, 0.2, 0.03, 0.0, 0.9) + + assert_almost_eq(loop_offset, 0.1, 0.01, "Loop offset should prefer the seam that matches the shared loop duration") + + +func test_select_shared_loop_duration_uses_shortest_track() -> void: + var shared_duration := SfxAutomationSyncAnalyzer._select_shared_loop_duration( + [ + { + "stream_duration": 2.01, + }, + { + "stream_duration": 1.993, + }, + { + "stream_duration": 2.016, + }, + ] + ) + + assert_almost_eq(shared_duration, 1.993, 0.001, "Shared loop duration should match the shortest track, because other loops can only be shortened") + + +func test_select_shared_loop_duration_is_multiple_of_phase_period() -> void: + var shared_duration := SfxAutomationSyncAnalyzer._select_shared_loop_duration( + [ + {"stream_duration": 2.1}, + {"stream_duration": 2.2}, + ], + 0.4 + ) + # Shortest is 2.1. Multiples of 0.4 are 0.4, 0.8, 1.2, 1.6, 2.0. + # Largest multiple <= 2.1 is 2.0. + assert_almost_eq(shared_duration, 2.0, 0.001, "Shared loop duration should be a multiple of phase_period") + + +func test_estimate_loop_offset_ignores_search_limit_for_target_duration() -> void: + # Track duration 5.0, target loop 2.0. Needs loop_offset 3.0. + # Default search limit (LOOP_SEARCH_MAX) is 0.75. + var loop_offset := SfxAutomationSyncAnalyzer.estimate_loop_offset( + PackedFloat32Array([0,0,0,0,0,0,0,0,0,0]), # dummy samples + 10.0, + 0.1, # period + 0.0, # search_start + 0.0, # phase_offset + 2.0, # target_loop_duration + 5.0 # stream_duration + ) + assert_almost_eq(loop_offset, 3.0, 0.01, "Should honor target duration even if it requires large offset") + + +func _make_tick_signal(sample_rate: float, duration_seconds: float, period_seconds: float, phase_seconds: float) -> PackedFloat32Array: + var sample_count: int = int(round(sample_rate * duration_seconds)) + var period_samples: int = max(1, int(round(sample_rate * period_seconds))) + var phase_samples: int = int(round(sample_rate * phase_seconds)) + var samples := PackedFloat32Array() + samples.resize(sample_count) + + for center in range(phase_samples, sample_count, period_samples): + for pulse_index in range(5): + var sample_index := center + pulse_index + if sample_index >= sample_count: + break + samples[sample_index] = 1.0 - (float(pulse_index) * 0.2) + + return samples + + +func _make_loop_candidate_signal( + sample_rate: float, + duration_seconds: float, + period_seconds: float, + loop_offset_seconds: float +) -> PackedFloat32Array: + var sample_count: int = int(round(sample_rate * duration_seconds)) + var loop_offset_samples: int = int(round(sample_rate * loop_offset_seconds)) + var window_samples: int = int(round(sample_rate * min(period_seconds * 1.5, 0.25))) + var tail_start: int = sample_count - window_samples + var samples := PackedFloat32Array() + samples.resize(sample_count) + + for index in range(sample_count): + samples[index] = 0.15 * sin(float(index) * 0.071) + + for index in range(window_samples): + var value := sin(float(index) * 0.11) + (0.35 * cos(float(index) * 0.037)) + samples[tail_start + index] = value + samples[loop_offset_samples + index] = value + + return samples + + +func _make_multi_seam_loop_candidate_signal( + sample_rate: float, + duration_seconds: float, + period_seconds: float, + loop_offsets_seconds: Array[float] +) -> PackedFloat32Array: + var sample_count: int = int(round(sample_rate * duration_seconds)) + var window_samples: int = int(round(sample_rate * min(period_seconds * 1.5, 0.25))) + var tail_start: int = sample_count - window_samples + var samples := PackedFloat32Array() + samples.resize(sample_count) + + for index in range(sample_count): + samples[index] = 0.12 * sin(float(index) * 0.053) + + for index in range(window_samples): + var value := cos(float(index) * 0.09) + (0.28 * sin(float(index) * 0.041)) + samples[tail_start + index] = value + for loop_offset_seconds in loop_offsets_seconds: + var loop_offset_samples: int = int(round(sample_rate * loop_offset_seconds)) + samples[loop_offset_samples + index] = value + + return samples + diff --git a/demo/addons/gnd_sfx/tests/test_sfx_automation_sync_analyzer.gd.uid b/demo/addons/gnd_sfx/tests/test_sfx_automation_sync_analyzer.gd.uid new file mode 100644 index 00000000..c8e644c4 --- /dev/null +++ b/demo/addons/gnd_sfx/tests/test_sfx_automation_sync_analyzer.gd.uid @@ -0,0 +1 @@ +uid://bqyolxhiqr48t diff --git a/demo/addons/gnd_sfx/tests/test_sfx_playback_runtime.gd b/demo/addons/gnd_sfx/tests/test_sfx_playback_runtime.gd new file mode 100644 index 00000000..71a2ab05 --- /dev/null +++ b/demo/addons/gnd_sfx/tests/test_sfx_playback_runtime.gd @@ -0,0 +1,806 @@ +extends MaszynaGutTest + +var runtime: SfxPlaybackRuntime +var players: Array = [] + + +func before_each() -> void: + runtime = SfxPlaybackRuntime.new() + players = [] + for _i in range(4): + var player := AudioStreamPlayer.new() + players.append(player) + add_child_autoqfree(player) + runtime.set_players(players) + + +func after_each() -> void: + runtime.clear() + players.clear() + + +func test_time_offset_track_starts_only_after_threshold() -> void: + var event := SfxEvent.new() + event.name = &"timed" + event.tracks = [_make_track(0.5)] + + runtime.play(event) + assert_eq(runtime._active_voices.size(), 0, "Track should not start before crossing time offset") + + runtime.update(0.6) + assert_eq(runtime._active_voices.size(), 1, "Track should start after time threshold is crossed") + + +func test_seek_rebuilds_event_tracks_for_new_offset() -> void: + var event := SfxEvent.new() + event.name = &"seekable" + event.tracks = [_make_track(1.0)] + + runtime.play(event) + assert_eq(runtime._active_voices.size(), 0) + + runtime.seek(event.name, 1.1) + assert_eq(runtime._active_voices.size(), 1, "Seek should activate tracks whose offset is already behind playback time") + + +func test_stream_offset_is_separate_from_timeline_offset() -> void: + var event := SfxEvent.new() + event.name = &"stream_offset" + var track := _make_track(1.0) + track.stream_offset = 0.25 + event.tracks = [track] + + runtime.play(event, 1.5) + assert_eq(runtime._active_voices.size(), 1) + + var start_position := runtime._resolve_voice_start_position(runtime._get_latest_instance(event.name), track) + assert_eq(start_position, 0.75, "Time tracks should add elapsed event time on top of stream_offset") + + +func test_time_track_length_limits_playback_segment() -> void: + var event := SfxEvent.new() + event.name = &"time_length" + var track := _make_track(0.0, 25.0) + track.stream_offset = 14.0 + track.length = 4.0 + track.fade_out_curve = _make_linear_curve(0.0, 4.0, 1.0, 0.0) + event.tracks = [track] + + runtime.play(event) + var voice: SfxPlaybackRuntime.ActiveVoice = runtime._active_voices[0] + + assert_eq(voice.stream_end_position, 18.0, "Track length should cap playback at stream_offset + length") + assert_eq(runtime._resolve_remaining_track_time(voice, 14.0), 4.0, "Remaining time should be measured against the shortened segment") + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 4.0), 1.0, "Fade-out should start at the beginning of the shortened tail") + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 0.0), 0.0, "Fade-out should end at silence at the shortened segment end") + + +func test_time_track_curves_use_local_time_and_remaining_time() -> void: + var event := SfxEvent.new() + event.name = &"curve_domains" + var track := _make_track(0.0, 25.0) + track.stream_offset = 14.0 + track.fade_in_curve = _make_linear_curve(0.0, 4.0, 0.0, 1.0) + track.fade_out_curve = _make_linear_curve(0.0, 4.0, 1.0, 0.0) + event.tracks = [track] + + runtime.play(event) + var voice: SfxPlaybackRuntime.ActiveVoice = runtime._active_voices[0] + + assert_eq(runtime._resolve_local_track_time(voice, voice.stream_start_position), 0.0, "Fade-in domain should start at local track time") + assert_eq(runtime._resolve_remaining_track_time(voice, voice.stream.get_length() - 4.0), 4.0, "Fade-out domain should count remaining playback time") + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 4.0), 1.0, "Fade-out should stay at full gain when four seconds remain") + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 0.0), 0.0, "Fade-out should reach silence at the end of playback") + + +func test_time_track_fade_out_curve_does_not_require_reversed_shape() -> void: + var track := _make_track(0.0, 25.0) + track.stream_offset = 14.0 + track.fade_out_curve = _make_linear_curve(0.0, 4.0, 1.0, 0.0) + + assert_eq(track.stream.get_length(), 25.0) + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 11.0), 1.0, "Playback should start audible before the last four seconds") + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 4.0), 1.0, "Fade-out should start when four seconds remain") + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 2.0), 0.5, "Fade-out should interpolate naturally toward the end") + assert_eq(runtime._sample_time_fade_out_curve(track.fade_out_curve, 0.0), 0.0, "Fade-out should end at silence") + + +func test_automation_tracks_start_after_parameter_threshold_crossing() -> void: + var event := SfxEvent.new() + event.name = &"engine" + event.automations = [_make_automation(&"rpm", 0.0, 1000.0, 500.0)] + + runtime.play(event, 0.0, {"rpm": 250.0}) + assert_eq(runtime._active_voices.size(), 0, "Automation track should not start below offset threshold") + + runtime.modulate(event.name, {"rpm": 750.0}) + assert_eq(runtime._active_voices.size(), 1, "Automation track should start after threshold crossing") + + +func test_automation_tracks_use_stream_offset_without_parameter_delta() -> void: + var event := SfxEvent.new() + event.name = &"automation_stream_offset" + var automation := _make_automation(&"rpm", 0.0, 1000.0, 500.0) + var track: SfxTrack = automation.tracks[0] + track.stream_offset = 0.4 + event.automations = [automation] + + runtime.play(event, 0.0, {"rpm": 800.0}) + assert_eq(runtime._active_voices.size(), 1) + + var start_position := runtime._resolve_voice_start_position(runtime._get_latest_instance(event.name), track, automation) + assert_eq(start_position, 0.4, "Automation tracks should start from stream_offset only") + + +func test_phase_locked_automation_tracks_use_event_clock_for_start_position() -> void: + var event := SfxEvent.new() + event.name = &"phase_locked" + var automation := _make_automation(&"speed", 0.0, 200.0, 10.0) + automation.phase_locked = true + automation.phase_period = 2.0 + var track: SfxTrack = automation.tracks[0] + track.stream = _make_test_wav(4.0) + track.stream_offset = 0.25 + event.automations = [automation] + + runtime.play(event, 1.25, {"speed": 20.0}) + assert_eq(runtime._active_voices.size(), 1) + + var expected_start := 1.5 + var voice: SfxPlaybackRuntime.ActiveVoice = runtime._active_voices[0] + assert_almost_eq(voice.stream_start_position, expected_start, 0.0001, "Phase-locked automation tracks should derive start position from event time") + + +func test_phase_locked_automation_tracks_apply_phase_offset() -> void: + var event := SfxEvent.new() + event.name = &"phase_offset" + var automation := _make_automation(&"speed", 0.0, 200.0, 10.0) + automation.phase_locked = true + automation.phase_period = 2.0 + var track: SfxTrack = automation.tracks[0] + track.stream = _make_test_wav(4.0) + track.phase_offset = 0.5 + event.automations = [automation] + + runtime.play(event, 1.25, {"speed": 20.0}) + assert_eq(runtime._active_voices.size(), 1) + + var voice: SfxPlaybackRuntime.ActiveVoice = runtime._active_voices[0] + assert_almost_eq(voice.stream_start_position, 1.75, 0.0001, "Track phase_offset should shift the shared automation phase") + + +func test_automation_curves_are_shifted_by_track_offset() -> void: + var track := _make_track(460.0) + track.fade_in_curve = _make_linear_curve(0.0, 100.0, 0.0, 1.0) + + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 460.0, 1.0), 0.0, "Fade-in should start at the track offset") + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 510.0, 1.0), 0.5, "Curve midpoint should land offset units later in automation space") + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 560.0, 1.0), 1.0, "Curve max domain should also be shifted by the track offset") + + +func test_automation_pitch_curve_uses_full_automation_domain() -> void: + var automation := _make_automation(&"rpm", 0.0, 1000.0, 460.0) + automation.pitch_curve = _make_linear_curve(0.0, 1000.0, 1.0, 2.0) + var track: SfxTrack = automation.tracks[0] + track.pitch_curve = _make_linear_curve(0.0, 100.0, 1.0, 1.5) + + assert_eq(runtime._sample_automation_domain_curve(automation.pitch_curve, 0.0, 1.0), 1.0, "Automation pitch curve should start at the automation domain start") + assert_eq(runtime._sample_automation_domain_curve(automation.pitch_curve, 500.0, 1.0), 1.5, "Automation pitch curve should sample directly from automation_value") + assert_eq(runtime._sample_automation_domain_curve(automation.pitch_curve, 1000.0, 1.0), 2.0, "Automation pitch curve should reach the end of its domain") + assert_eq(runtime._sample_automation_curve(track.pitch_curve, track, 460.0, 1.0), 1.0, "Track pitch curve should still be local to track offset") + assert_eq(runtime._sample_automation_curve(track.pitch_curve, track, 560.0, 1.0), 1.5, "Track pitch curve should still use local automation value") + + +func test_automation_fade_out_curve_is_reversed_in_local_track_domain() -> void: + var track := _make_track(310.0) + track.fade_in_curve = _make_linear_curve(0.0, 90.0, 0.0, 1.0) + track.fade_out_curve = _make_linear_curve(0.0, 90.0, 1.0, 0.0) + + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 310.0, 1.0), 0.0, "Fade-in should start silent at the threshold") + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 355.0, 1.0), 0.5, "Fade-in should use local automation value") + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 400.0, 1.0), 1.0, "Fade-in should reach full gain at the end of the local domain") + assert_eq(runtime._sample_automation_fade_out_curve(track.fade_out_curve, track, 310.0, 1.0), 0.0, "Fade-out should read the end of the curve at the threshold") + assert_eq(runtime._sample_automation_fade_out_curve(track.fade_out_curve, track, 355.0, 1.0), 0.5, "Fade-out midpoint should mirror local automation space") + assert_eq(runtime._sample_automation_fade_out_curve(track.fade_out_curve, track, 400.0, 1.0), 1.0, "Fade-out should not zero the track at the end of the local domain") + + +func test_automation_length_shifts_fade_out_curve_to_track_end() -> void: + var track := _make_track(405.0) + track.length = 285.0 + track.fade_in_curve = _make_linear_curve(0.0, 90.0, 0.0, 1.0) + track.fade_out_curve = _make_linear_curve(0.0, 90.0, 1.0, 0.0) + + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 600.0, 1.0), 1.0, "Fade-in should already be at full gain by the time fade-out starts") + assert_eq(runtime._sample_automation_fade_out_curve(track.fade_out_curve, track, 600.0, 1.0), 1.0, "Fade-out should start one curve duration before offset + length") + assert_eq(runtime._sample_automation_fade_out_curve(track.fade_out_curve, track, 645.0, 1.0), 0.5, "Fade-out should interpolate across the last curve duration before track end") + assert_eq(runtime._sample_automation_fade_out_curve(track.fade_out_curve, track, 690.0, 1.0), 0.0, "Fade-out should end exactly at offset + length") + + +func test_automation_length_uses_last_curve_window_before_track_end() -> void: + var track := _make_track(760.0) + track.length = 180.0 + track.fade_in_curve = _make_linear_curve(0.0, 90.0, 0.0, 1.0) + track.fade_out_curve = _make_linear_curve(0.0, 90.0, 1.0, 0.0) + + assert_eq(runtime._sample_automation_curve(track.fade_in_curve, track, 856.0, 1.0), 1.0, "Fade-in should already be fully open at 856") + assert_gt(runtime._sample_automation_fade_out_curve(track.fade_out_curve, track, 856.0, 1.0), 0.9, "Fade-out should only be slightly attenuated at 856") + + +func test_stop_with_release_keeps_instance_alive_until_adsr_finishes() -> void: + var event := SfxEvent.new() + event.name = &"released" + event.adsr_enabled = true + event.release = 0.2 + event.tracks = [_make_track(0.0)] + + runtime.play(event) + assert_true(runtime._instances.has(event.name)) + assert_eq(runtime._active_voices.size(), 1) + + runtime.stop(event.name, false) + assert_true(runtime._instances.has(event.name), "Event instance should stay alive during release") + + runtime.update(0.1) + assert_true(runtime._instances.has(event.name), "Release should still be in progress") + + runtime.update(0.2) + assert_false(runtime._instances.has(event.name), "Event instance should be removed after release") + assert_eq(runtime._active_voices.size(), 0, "Voices should be stopped after release") + + +func test_polyphony_enabled_allows_multiple_instances_of_same_event() -> void: + var event := SfxEvent.new() + event.name = &"poly" + event.polyphony_enabled = true + event.tracks = [_make_track(0.0)] + + runtime.play(event) + runtime.play(event) + + assert_eq(runtime._get_instances_for_event(event.name).size(), 2, "Polyphonic event should keep multiple instances alive") + assert_eq(runtime._active_voices.size(), 2, "Each polyphonic instance should allocate its own voice") + + +func test_polyphony_disabled_replaces_existing_instance() -> void: + var event := SfxEvent.new() + event.name = &"mono" + event.polyphony_enabled = false + event.tracks = [_make_track(0.0)] + + runtime.play(event) + runtime.play(event) + + assert_eq(runtime._get_instances_for_event(event.name).size(), 1, "Monophonic event should replace the previous instance") + assert_eq(runtime._active_voices.size(), 1, "Monophonic event should keep only one active voice") + + +func test_stop_targets_latest_instance_for_polyphonic_event() -> void: + var event := SfxEvent.new() + event.name = &"poly_stop" + event.polyphony_enabled = true + event.tracks = [_make_track(0.0)] + + runtime.play(event) + runtime.play(event) + assert_eq(runtime._get_instances_for_event(event.name).size(), 2) + + runtime.stop(event.name, true) + + assert_eq(runtime._get_instances_for_event(event.name).size(), 1, "Named stop should only remove the newest polyphonic instance") + assert_eq(runtime._active_voices.size(), 1, "One older polyphonic voice should remain active") + + +func test_is_playing_tracks_instance_lifecycle() -> void: + var event := SfxEvent.new() + event.name = &"playing_state" + event.tracks = [_make_track(0.0)] + + assert_false(runtime.is_playing(event.name)) + + runtime.play(event) + assert_true(runtime.is_playing(event.name)) + + runtime.stop(event.name, true) + assert_false(runtime.is_playing(event.name)) + + +func test_sustain_track_does_not_start_during_initial_playback() -> void: + var event := SfxEvent.new() + event.name = &"sustain_wait" + var sustain_track := _make_track(0.0) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [sustain_track] + + runtime.play(event) + + assert_eq(runtime._active_voices.size(), 0, "Sustain tracks should not start on play") + assert_true(runtime._instances.has(event.name), "Event instance should remain active until stopped") + + +func test_stop_with_release_triggers_sustain_track_once() -> void: + var event := SfxEvent.new() + event.name = &"sustain_once" + event.adsr_enabled = true + event.release = 0.5 + var sustain_track := _make_track(0.0) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [sustain_track] + + runtime.play(event) + runtime.stop(event.name, false) + + assert_eq(runtime._active_voices.size(), 1, "Sustain track should start on non-immediate stop") + + runtime.stop(event.name, false) + assert_eq(runtime._active_voices.size(), 1, "Repeated non-immediate stop should not retrigger sustain track") + + +func test_sustain_track_offset_uses_release_clock_and_stream_offset() -> void: + var event := SfxEvent.new() + event.name = &"sustain_offset" + var sustain_track := _make_track(0.15, 1.0) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + sustain_track.stream_offset = 0.3 + event.tracks = [sustain_track] + + runtime.play(event) + runtime.stop(event.name, false) + assert_eq(runtime._active_voices.size(), 0, "Delayed sustain track should not be queued after stop") + assert_false(runtime._instances.has(event.name), "Instance should be collected when only delayed sustain remains") + + +func test_sustain_track_start_position_does_not_inherit_event_playback_time() -> void: + var event := SfxEvent.new() + event.name = &"sustain_stream_start" + var sustain_track := _make_track(0.0, 1.0) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + sustain_track.stream_offset = 0.1 + event.tracks = [sustain_track] + + runtime.play(event) + runtime.update(0.25) + runtime.stop(event.name, false) + + var voice: SfxPlaybackRuntime.ActiveVoice = runtime._active_voices[0] + assert_eq(voice.stream_start_position, 0.1, "Sustain track should start from stream_offset, not from elapsed event playback time") + + +func test_immediate_stop_does_not_trigger_sustain_track() -> void: + var event := SfxEvent.new() + event.name = &"sustain_immediate" + var sustain_track := _make_track(0.0) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [sustain_track] + + runtime.play(event) + runtime.stop(event.name, true) + + assert_false(runtime._instances.has(event.name), "Immediate stop should remove the event instance") + assert_eq(runtime._active_voices.size(), 0, "Immediate stop should not start sustain tracks") + + +func test_stop_without_adsr_keeps_instance_alive_until_sustain_track_finishes() -> void: + var event := SfxEvent.new() + event.name = &"sustain_no_adsr" + var sustain_track := _make_track(0.0, 0.2) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [sustain_track] + + runtime.play(event) + runtime.stop(event.name, false) + + assert_true(runtime._instances.has(event.name), "Non-immediate stop without ADSR should keep the instance alive for sustain playback") + assert_eq(runtime._active_voices.size(), 1, "Sustain track should still start without ADSR") + + runtime._players[0].stop() + runtime.handle_player_finished(runtime._players[0]) + runtime.update(0.0) + + assert_false(runtime._instances.has(event.name), "Instance should be removed after sustain playback ends") + assert_eq(runtime._active_voices.size(), 0, "No voices should remain after sustain playback ends") + + +func test_stop_without_adsr_stops_existing_timeline_loop_before_starting_sustain_track() -> void: + var event := SfxEvent.new() + event.name = &"sustain_cuts_loop" + var timeline_track := _make_track(0.0, 1.0) + timeline_track.stream = _make_test_wav(1.0, true) + var sustain_track := _make_track(0.0, 0.2) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [timeline_track, sustain_track] + + runtime.play(event) + assert_eq(runtime._active_voices.size(), 1, "Timeline track should start on play") + assert_eq(runtime._active_voices[0].track, timeline_track, "Initial voice should belong to the looping timeline track") + + runtime.stop(event.name, false) + + assert_eq(runtime._active_voices.size(), 1, "Only the sustain track should remain after non-ADSR stop") + assert_eq(runtime._active_voices[0].track, sustain_track, "Non-ADSR stop should cut the old loop and start the sustain track") + + +func test_stop_without_adsr_uses_track_fade_out_curve_for_existing_voice() -> void: + var event := SfxEvent.new() + event.name = &"sustain_fades_loop" + var timeline_track := _make_track(0.0, 1.0) + timeline_track.stream = _make_test_wav(1.0, true) + timeline_track.fade_out_curve = _make_linear_curve(0.0, 0.2, 1.0, 0.0) + var sustain_track := _make_track(0.0, 0.2) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [timeline_track, sustain_track] + + runtime.play(event) + runtime.stop(event.name, false) + + assert_eq(runtime._active_voices.size(), 2, "Looping timeline voice should fade out while sustain track starts") + assert_true(runtime._active_voices[0].track == timeline_track or runtime._active_voices[1].track == timeline_track) + + runtime.update(0.1) + var fading_voice := runtime._find_voice(runtime._get_latest_instance(event.name), timeline_track) + assert_not_null(fading_voice, "Timeline voice should still exist halfway through its stop fade") + assert_almost_eq(db_to_linear(fading_voice.player.volume_db), 0.5, 0.1, "Timeline voice should follow the fade_out_curve during non-ADSR stop") + + runtime.update(0.11) + assert_null(runtime._find_voice(runtime._get_latest_instance(event.name), timeline_track), "Timeline voice should be removed after its fade_out_curve duration") + + +func test_stop_without_event_adsr_uses_track_adsr_release_for_looping_voice() -> void: + var event := SfxEvent.new() + event.name = &"track_adsr_release" + var timeline_track := _make_track(0.0, 1.0) + timeline_track.stream = _make_test_wav(1.0, true) + timeline_track.adsr_enabled = true + timeline_track.release = 0.2 + var sustain_track := _make_track(0.0, 0.2) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [timeline_track, sustain_track] + + runtime.play(event) + runtime.stop(event.name, false) + + assert_eq(runtime._active_voices.size(), 2, "Looping voice should release while sustain track starts") + + runtime.update(0.1) + var fading_voice := runtime._find_voice(runtime._get_latest_instance(event.name), timeline_track) + assert_not_null(fading_voice, "Timeline voice should still exist halfway through track ADSR release") + assert_almost_eq(db_to_linear(fading_voice.player.volume_db), 0.5, 0.1, "Track ADSR release should attenuate the looping voice immediately after stop") + + runtime.update(0.11) + assert_null(runtime._find_voice(runtime._get_latest_instance(event.name), timeline_track), "Timeline voice should end after its track ADSR release") + + +func test_track_adsr_sustain_applies_while_voice_is_held() -> void: + var event := SfxEvent.new() + event.name = &"track_adsr_hold" + var track := _make_track(0.0, 1.0) + track.adsr_enabled = true + track.attack = 0.1 + track.decay = 0.1 + track.sustain = 0.25 + event.tracks = [track] + + runtime.play(event) + runtime.update(0.1) + runtime.update(0.1) + runtime.update(0.05) + + assert_eq(runtime._active_voices.size(), 1) + assert_almost_eq(_player_linear_gain(0), 0.25, 0.1, "Track ADSR sustain should hold the voice below unity even before stop") + + +func test_horn_like_overlap_stop_starts_sustain_track() -> void: + var local_runtime := SfxPlaybackRuntime.new() + var local_players: Array = [] + for _i in range(2): + var player := AudioStreamPlayer.new() + local_players.append(player) + add_child_autoqfree(player) + local_runtime.set_players(local_players) + + var event := SfxEvent.new() + event.name = &"horn_like" + + var start_track := _make_track(0.0, 0.36) + var loop_track := _make_track(0.2, 1.0) + loop_track.stream = _make_test_wav(1.0, true) + var sustain_track := _make_track(0.0, 0.3) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + + event.tracks = [start_track, loop_track, sustain_track] + + local_runtime.play(event) + local_runtime.update(0.25) + assert_eq(local_runtime._active_voices.size(), 2, "Horn overlap should have start and loop voices active before stop") + + local_runtime.stop(event.name, false) + assert_eq(local_runtime._active_voices.size(), 1, "Sustain track should replace overlapping horn voices on stop") + + var sustain_voice := local_runtime._find_voice(local_runtime._get_latest_instance(event.name), sustain_track) + assert_not_null(sustain_voice, "Horn-like sustain track should start during overlap stop") + assert_true(sustain_voice.player.playing, "Sustain voice should still be playing after overlapping voices are stopped") + + +func test_new_voice_steals_oldest_releasing_voice_before_delaying_start() -> void: + var local_runtime := SfxPlaybackRuntime.new() + var local_players: Array = [] + for _i in range(2): + var player := AudioStreamPlayer.new() + local_players.append(player) + add_child_autoqfree(player) + local_runtime.set_players(local_players) + + var releasing_event := SfxEvent.new() + releasing_event.name = &"releasing" + var releasing_track := _make_track(0.0, 1.0) + releasing_track.stream = _make_test_wav(1.0, true) + releasing_track.fade_out_curve = _make_linear_curve(0.0, 0.5, 1.0, 0.0) + var releasing_sustain := _make_track(0.0, 0.25) + releasing_sustain.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + releasing_event.tracks = [releasing_track, releasing_sustain] + + var held_event := SfxEvent.new() + held_event.name = &"held" + var held_track := _make_track(0.0, 1.0) + held_track.stream = _make_test_wav(1.0, true) + held_event.tracks = [held_track] + + var newcomer_event := SfxEvent.new() + newcomer_event.name = &"newcomer" + newcomer_event.tracks = [_make_track(0.0, 0.2)] + + local_runtime.play(releasing_event) + local_runtime.play(held_event) + local_runtime.stop(releasing_event.name, false) + + assert_eq(local_runtime._active_voices.size(), 2, "Releasing sustain and held voice should occupy the full pool before the steal") + assert_null(local_runtime._find_voice(local_runtime._get_latest_instance(releasing_event.name), releasing_track), "Stop should already evict the older releasing loop when sustain starts at the limit") + assert_not_null(local_runtime._find_voice(local_runtime._get_latest_instance(releasing_event.name), releasing_sustain), "Sustain should be the remaining releasing voice before the next play") + + local_runtime.play(newcomer_event) + + assert_eq(local_runtime._active_voices.size(), 2, "New voice should start immediately by stealing a slot") + assert_not_null(local_runtime._find_voice(local_runtime._get_latest_instance(newcomer_event.name), newcomer_event.tracks[0]), "Newcomer voice should exist right after play") + assert_null(local_runtime._find_voice(local_runtime._get_latest_instance(releasing_event.name), releasing_sustain), "Remaining releasing voice should be stolen before touching non-releasing voices") + assert_not_null(local_runtime._find_voice(local_runtime._get_latest_instance(held_event.name), held_track), "Non-releasing voice should not be stolen while a releasing one exists") + + +func test_new_voice_steals_oldest_global_voice_when_no_releasing_voice_exists() -> void: + var local_runtime := SfxPlaybackRuntime.new() + var local_players: Array = [] + for _i in range(2): + var player := AudioStreamPlayer.new() + local_players.append(player) + add_child_autoqfree(player) + local_runtime.set_players(local_players) + + var first_event := SfxEvent.new() + first_event.name = &"first" + var first_track := _make_track(0.0, 1.0) + first_track.stream = _make_test_wav(1.0, true) + first_event.tracks = [first_track] + + var second_event := SfxEvent.new() + second_event.name = &"second" + var second_track := _make_track(0.0, 1.0) + second_track.stream = _make_test_wav(1.0, true) + second_event.tracks = [second_track] + + var newcomer_event := SfxEvent.new() + newcomer_event.name = &"newest" + newcomer_event.tracks = [_make_track(0.0, 0.2)] + + local_runtime.play(first_event) + local_runtime.play(second_event) + local_runtime.play(newcomer_event) + + assert_eq(local_runtime._active_voices.size(), 2, "New voice should steal instead of waiting for a free player") + assert_null(local_runtime._find_voice(local_runtime._get_latest_instance(first_event.name), first_track), "Oldest global voice should be stolen first") + assert_not_null(local_runtime._find_voice(local_runtime._get_latest_instance(second_event.name), second_track), "More recent global voice should remain active") + assert_not_null(local_runtime._find_voice(local_runtime._get_latest_instance(newcomer_event.name), newcomer_event.tracks[0]), "New voice should start immediately") + + +func test_rapid_replay_with_non_immediate_stop_does_not_delay_new_starts_at_track_limit() -> void: + var local_runtime := SfxPlaybackRuntime.new() + var local_players: Array = [] + for _i in range(2): + var player := AudioStreamPlayer.new() + local_players.append(player) + add_child_autoqfree(player) + local_runtime.set_players(local_players) + + var event := SfxEvent.new() + event.name = &"rapid" + event.polyphony_enabled = true + var loop_track := _make_track(0.0, 1.0) + loop_track.stream = _make_test_wav(1.0, true) + loop_track.fade_out_curve = _make_linear_curve(0.0, 0.5, 1.0, 0.0) + var sustain_track := _make_track(0.0, 0.2) + sustain_track.trigger_mode = SfxTrack.TriggerMode.TRIGGER_SUSTAIN + event.tracks = [loop_track, sustain_track] + + local_runtime.play(event) + local_runtime.stop(event.name, false) + local_runtime.play(event) + + var instances := local_runtime._get_instances_for_event(event.name) + assert_eq(instances.size(), 2, "Polyphonic replay should keep the releasing instance while starting a new one") + assert_eq(local_runtime._active_voices.size(), 2, "Replay should reuse a slot immediately instead of delaying the new start") + assert_not_null(local_runtime._find_voice(instances[1], loop_track), "Newest instance should start its loop immediately") + assert_not_null(local_runtime._find_voice(instances[0], sustain_track), "Existing sustain should remain audible if it is not the stolen voice") + + +func test_automation_equal_power_crossfade_applies_sqrt() -> void: + var event := SfxEvent.new() + event.name = &"equal_power" + var automation := _make_automation(&"param", 0.0, 1.0, 0.0) + automation.crossfade_mode = SfxAutomation.CrossfadeMode.EQUAL_POWER + var track := automation.tracks[0] + track.fade_in_curve = _make_linear_curve(0.0, 1.0, 0.0, 1.0) + event.automations = [automation] + + runtime.play(event, 0.0, {&"param": 1.0}) + + var player = runtime._players[0] + # gain=1.0, sqrt(1.0)=1.0 + assert_almost_eq(db_to_linear(player.volume_db), 1.0, 0.01, "EQUAL_POWER mode should apply sqrt to track gain") + + +func test_linear_crossfade_normalizes_overlapping_automation_tracks() -> void: + var event := SfxEvent.new() + event.name = &"linear_overlap" + var automation := SfxAutomation.new() + automation.parameter_name = &"param" + automation.min_domain = 0.0 + automation.max_domain = 1.0 + automation.crossfade_mode = SfxAutomation.CrossfadeMode.LINEAR + var first_track := _make_track(0.0) + first_track.fade_in_curve = _make_constant_curve(1.0) + first_track.fade_out_curve = _make_linear_curve(0.0, 1.0, 1.0, 0.0) + var second_track := _make_track(0.0) + second_track.fade_in_curve = _make_linear_curve(0.0, 1.0, 0.0, 1.0) + second_track.fade_out_curve = _make_constant_curve(1.0) + automation.tracks = [first_track, second_track] + event.automations = [automation] + + runtime.play(event, 0.0, {&"param": 0.5}) + + assert_eq(runtime._active_voices.size(), 2, "Both overlapping automation tracks should be active") + assert_almost_eq(_player_linear_gain(0), 0.5, 0.01, "The first track should be normalized to half gain at midpoint") + assert_almost_eq(_player_linear_gain(1), 0.5, 0.01, "The second track should be normalized to half gain at midpoint") + assert_almost_eq(_sum_player_linear_gain([0, 1]), 1.0, 0.01, "Linear overlap should preserve total amplitude") + + +func test_equal_power_crossfade_normalizes_group_power() -> void: + var event := SfxEvent.new() + event.name = &"equal_power_overlap" + var automation := SfxAutomation.new() + automation.parameter_name = &"param" + automation.min_domain = 0.0 + automation.max_domain = 1.0 + automation.crossfade_mode = SfxAutomation.CrossfadeMode.EQUAL_POWER + var first_track := _make_track(0.0) + first_track.fade_in_curve = _make_constant_curve(1.0) + first_track.fade_out_curve = _make_linear_curve(0.0, 1.0, 1.0, 0.0) + var second_track := _make_track(0.0) + second_track.fade_in_curve = _make_linear_curve(0.0, 1.0, 0.0, 1.0) + second_track.fade_out_curve = _make_constant_curve(1.0) + automation.tracks = [first_track, second_track] + event.automations = [automation] + + runtime.play(event, 0.0, {&"param": 0.5}) + + assert_eq(runtime._active_voices.size(), 2, "Both overlapping automation tracks should be active") + assert_almost_eq(_player_linear_gain(0), sqrt(0.5), 0.01, "Equal-power midpoint should keep each voice at sqrt(0.5)") + assert_almost_eq(_player_linear_gain(1), sqrt(0.5), 0.01, "Equal-power midpoint should mirror the matching voice gain") + assert_almost_eq(_sum_player_power([0, 1]), 1.0, 0.02, "Equal-power overlap should preserve total power") + assert_gt(_player_linear_gain(0), 0.5, "Equal-power should differ from linear normalization at midpoint") + + +func test_automation_crossfade_normalization_is_scoped_per_event_instance_and_automation() -> void: + var event_a := SfxEvent.new() + event_a.name = &"scope_a" + var automation_a := SfxAutomation.new() + automation_a.parameter_name = &"param" + automation_a.min_domain = 0.0 + automation_a.max_domain = 1.0 + automation_a.crossfade_mode = SfxAutomation.CrossfadeMode.LINEAR + var a_first := _make_track(0.0) + a_first.fade_in_curve = _make_constant_curve(1.0) + a_first.fade_out_curve = _make_linear_curve(0.0, 1.0, 1.0, 0.0) + var a_second := _make_track(0.0) + a_second.fade_in_curve = _make_linear_curve(0.0, 1.0, 0.0, 1.0) + a_second.fade_out_curve = _make_constant_curve(1.0) + automation_a.tracks = [a_first, a_second] + event_a.automations = [automation_a] + + var event_b := SfxEvent.new() + event_b.name = &"scope_b" + var automation_b := SfxAutomation.new() + automation_b.parameter_name = &"param" + automation_b.min_domain = 0.0 + automation_b.max_domain = 1.0 + automation_b.crossfade_mode = SfxAutomation.CrossfadeMode.LINEAR + var b_track := _make_track(0.0) + b_track.fade_in_curve = _make_constant_curve(1.0) + b_track.fade_out_curve = _make_constant_curve(1.0) + automation_b.tracks = [b_track] + event_b.automations = [automation_b] + + runtime.play(event_a, 0.0, {&"param": 0.5}) + runtime.play(event_b, 0.0, {&"param": 0.5}) + + assert_eq(runtime._active_voices.size(), 3, "The control event should add one independent automation voice") + assert_almost_eq(_sum_player_linear_gain([0, 1]), 1.0, 0.01, "Normalization should only apply within the overlapping event instance") + assert_almost_eq(_player_linear_gain(2), 1.0, 0.01, "An independent event instance should keep its own full gain") + + +func _make_track(offset: float, duration_seconds := 0.5) -> SfxTrack: + var track := SfxTrack.new() + track.stream = _make_test_wav(duration_seconds) + track.offset = offset + return track + + +func _make_automation(name: StringName, min_domain: float, max_domain: float, offset: float) -> SfxAutomation: + var automation := SfxAutomation.new() + automation.parameter_name = name + automation.min_domain = min_domain + automation.max_domain = max_domain + automation.tracks = [_make_track(offset)] + return automation + + +func _make_test_wav(duration_seconds := 0.5, loop := false) -> AudioStreamWAV: + var stream := AudioStreamWAV.new() + stream.format = AudioStreamWAV.FORMAT_8_BITS + stream.mix_rate = 44100 + stream.stereo = false + stream.loop_mode = AudioStreamWAV.LOOP_FORWARD if loop else AudioStreamWAV.LOOP_DISABLED + var sample_count := int(round(44100.0 * duration_seconds)) + var audio_data := PackedByteArray() + audio_data.resize(sample_count) + for index in range(sample_count): + audio_data[index] = 128 + stream.data = audio_data + return stream + + +func _make_linear_curve(x0: float, x1: float, y0: float, y1: float) -> Curve: + var curve := Curve.new() + curve.min_domain = minf(x0, x1) + curve.max_domain = maxf(x0, x1) + curve.min_value = minf(y0, y1) + curve.max_value = maxf(y0, y1) + curve.add_point(Vector2(x0, y0)) + curve.add_point(Vector2(x1, y1)) + return curve + + +func _make_constant_curve(value: float) -> Curve: + var curve := Curve.new() + curve.min_domain = 0.0 + curve.max_domain = 1.0 + curve.min_value = value + curve.max_value = value + curve.add_point(Vector2(0.0, value)) + curve.add_point(Vector2(1.0, value)) + return curve + + +func _player_linear_gain(index: int) -> float: + return db_to_linear(runtime._players[index].volume_db) + + +func _sum_player_linear_gain(indices: Array[int]) -> float: + var total := 0.0 + for index in indices: + total += _player_linear_gain(index) + return total + + +func _sum_player_power(indices: Array[int]) -> float: + var total := 0.0 + for index in indices: + var gain := _player_linear_gain(index) + total += gain * gain + return total diff --git a/demo/addons/gnd_sfx/tests/test_sfx_playback_runtime.gd.uid b/demo/addons/gnd_sfx/tests/test_sfx_playback_runtime.gd.uid new file mode 100644 index 00000000..57a06d49 --- /dev/null +++ b/demo/addons/gnd_sfx/tests/test_sfx_playback_runtime.gd.uid @@ -0,0 +1 @@ +uid://d2nv261p3dq3l diff --git a/demo/addons/gnd_skydome/EditorAccess.gd b/demo/addons/gnd_skydome/EditorAccess.gd new file mode 100644 index 00000000..2b9655b5 --- /dev/null +++ b/demo/addons/gnd_skydome/EditorAccess.gd @@ -0,0 +1,19 @@ +extends RefCounted + + +static func get_editor_viewport_3d(index := 0): + return EditorInterface.get_editor_viewport_3d(index) + + +static func get_editor_camera_3d(index := 0): + var viewport = get_editor_viewport_3d(index) + if viewport == null: + return null + return viewport.get_camera_3d() + + +static func get_editor_viewport_size(index := 0) -> Vector2: + var viewport = get_editor_viewport_3d(index) + if viewport == null: + return Vector2.ZERO + return viewport.get_visible_rect().size diff --git a/demo/addons/gnd_skydome/EditorAccess.gd.uid b/demo/addons/gnd_skydome/EditorAccess.gd.uid new file mode 100644 index 00000000..5c334de5 --- /dev/null +++ b/demo/addons/gnd_skydome/EditorAccess.gd.uid @@ -0,0 +1 @@ +uid://tnpek0x1oo16 diff --git a/demo/addons/gnd_skydome/Skydome.gd b/demo/addons/gnd_skydome/Skydome.gd new file mode 100644 index 00000000..2a8a1848 --- /dev/null +++ b/demo/addons/gnd_skydome/Skydome.gd @@ -0,0 +1,1349 @@ +@tool +class_name Skydome +extends Node + +signal time_changed(day: int, time: float) +signal day_changed(day: int) + +const SUN_SHAFTS_EFFECT_SCRIPT := preload("res://addons/gnd_skydome/SunShaftsCompositorEffect.gd") +const FILMIC_SKY_SHADER := preload("res://addons/gnd_skydome/filmic_procedural_sky.gdshader") +const EDITOR_ACCESS_SCRIPT_PATH := "res://addons/gnd_skydome/EditorAccess.gd" + +var _environment: Environment +var _rendered_day: int = 180 +var _rendered_time: float = 12.0 +var _time_transition_active: bool = false +var _time_transition_wrapped: bool = false +var _time_transition_target_total_hours: float = 0.0 +var _time_transition_target_unwrapped_time: float = 0.0 +var _time_transition_speed_hours_per_second: float = 0.0 +var _last_cloud_total_hours: float = 0.0 +var _cloud_motion_time: float = 0.0 +var _cloud_evolution_time: float = 0.0 +var _sky_material: ShaderMaterial +var _compositor_effect: CompositorEffect +var _light: DirectionalLight3D +var _is_ready: bool = false +var _is_daytime: bool = true +var _day_blend: float = 1.0 +var _sunset_blend: float = 0.0 +var _cloud_texture_a: Texture2D +var _cloud_texture_b: Texture2D +var _camera: Camera3D +var _viewport_size: Vector2 = Vector2.ZERO +var _editor_access = null + +func _error(x): + push_error("[Skydome] "+x) + +func _success(x): + print_rich("[color=green][Skydome][/color] "+x) + +@export_node_path("DirectionalLight3D") var directional_light_path: NodePath: + set(v): + directional_light_path = v + _refresh() +@export_node_path("WorldEnvironment") var world_environment_path: NodePath: + set(v): + world_environment_path = v + _refresh() + +@export_group("Time & Date") +@export_range(1, 365) var day_of_year: int = 180: + set(v): + day_of_year = v + _request_time_update() +@export_range(0.0, 24.0) var time_of_day: float = 12.0: + set(v): + time_of_day = v + _request_time_update() +@export_range(-90.0, 90.0) var latitude: float = 50.0: + set(v): + latitude = v + _update_sun_transform() +@export var time_transition_duration: float = 1.0 + +@export_group("Performance") +@export var shader_high_quality_sky: bool = true: + set(v): + shader_high_quality_sky = v + _set_shader_param("high_quality_sky", v) + +@export_group("Sunset", "sunset") +@export_subgroup("Light", "sunset_light") +@export var sunset_light_color: Color = Color(1.0, 0.45, 0.15): + set(v): + sunset_light_color = v + _update_sun_transform() +@export_subgroup("Sky", "shader") +@export var shader_sunset_bottom_color: Color = Color(1.0, 0.5, 0.2, 1): + set(v): shader_sunset_bottom_color = v; _set_shader_param("sunset_bottom_color", v) +@export var shader_sunset_horizon_color: Color = Color(0.8, 0.2, 0.05, 1): + set(v): shader_sunset_horizon_color = v; _set_shader_param("sunset_horizon_color", v) +@export var shader_sunset_zenith_color: Color = Color(0.4, 0.3, 0.5, 1): + set(v): shader_sunset_zenith_color = v; _set_shader_param("sunset_zenith_color", v) +@export var shader_sunset_cloud_color: Color = Color(1.0, 0.4, 0.15, 1): + set(v): shader_sunset_cloud_color = v; _set_shader_param("sunset_cloud_color", v) + +@export_group("Day", "day") +@export_subgroup("Light", "day_light") +@export var day_light_energy: float = 1.28: + set(v): + day_light_energy = v + _update_sun_transform() +@export var day_light_color: Color = Color(1.0, 0.93, 0.85): + set(v): + day_light_color = v + _update_sun_transform() +@export_subgroup("Sky", "shader") +@export var shader_lower_sky_color: Color = Color(0.655, 0.706, 0.79, 1): + set(v): + shader_lower_sky_color = v + _set_shader_param("lower_sky_color", v) +@export var shader_horizon_color: Color = Color(0.832, 0.86, 0.886, 1): + set(v): + shader_horizon_color = v + _set_shader_param("horizon_color", v) +@export var shader_zenith_color: Color = Color(0.2373352, 0.4190016, 0.7890625, 1): + set(v): + shader_zenith_color = v + _set_shader_param("zenith_color", v) +@export var shader_sky_energy: float = 1.0: + set(v): + shader_sky_energy = v + _set_shader_param("sky_energy", v) +@export var shader_horizon_height: float = 0.05: + set(v): + shader_horizon_height = v + _set_shader_param("horizon_height", v) +@export var shader_horizon_softness: float = 0.24: + set(v): + shader_horizon_softness = v + _set_shader_param("horizon_softness", v) +@export var shader_zenith_curve: float = 0.405: + set(v): + shader_zenith_curve = v + _set_shader_param("zenith_curve", v) +@export var shader_horizon_glow_strength: float = 1.004: + set(v): + shader_horizon_glow_strength = v + _set_shader_param("horizon_glow_strength", v) +@export_subgroup("Rainbow", "rainbow") +@export_range(0.0, 2.0, 0.001) var rainbow_intensity: float = 0.0: + set(v): + rainbow_intensity = clampf(v, 0.0, 2.0) + _set_shader_param("rainbow_intensity", rainbow_intensity) +@export_range(0.0, 2.0, 0.001) var rainbow_secondary_intensity: float = 0.35: + set(v): + rainbow_secondary_intensity = clampf(v, 0.0, 2.0) + _set_shader_param("rainbow_secondary_intensity", rainbow_secondary_intensity) +@export_subgroup("Ambient", "day_ambient") +@export var day_ambient_color: Color = Color(0.91, 0.85, 0.69): + set(v): day_ambient_color = v; _update_sun_transform() +@export var day_ambient_energy: float = 1.5: + set(v): day_ambient_energy = v; _update_sun_transform() +@export var day_ambient_sky_contribution: float = 0.25: + set(v): day_ambient_sky_contribution = v; _update_sun_transform() +@export_subgroup("Fog", "day_fog") +@export var day_fog_color: Color = Color(1.0, 0.95, 0.91): + set(v): day_fog_color = v; _update_sun_transform() +@export var day_fog_density: float = 0.005: + set(v): day_fog_density = v; _update_sun_transform() +@export var day_fog_sky_affect: float = 0.15: + set(v): day_fog_sky_affect = v; _update_sun_transform() +@export var day_fog_distance_begin: float = 2.6: + set(v): day_fog_distance_begin = v; _update_sun_transform() +@export var day_fog_distance: float = 470.0: + set(v): day_fog_distance = v; _update_sun_transform() +@export_subgroup("Volumetric fog", "day_vol_fog") +@export var day_vol_fog_albedo: Color = Color(0.77, 0.74, 0.7): + set(v): day_vol_fog_albedo = v; _update_sun_transform() +@export var day_vol_fog_density: float = 0.001: + set(v): day_vol_fog_density = v; _update_sun_transform() +@export var day_vol_fog_sky_affect: float = 0.5: + set(v): day_vol_fog_sky_affect = v; _update_sun_transform() +@export var day_vol_fog_length: float = 8.0: + set(v): day_vol_fog_length = v; _update_sun_transform() +@export var day_vol_fog_ambient_inject: float = 0.04: + set(v): day_vol_fog_ambient_inject = v; _update_sun_transform() + +@export_group("Night", "night") +@export_subgroup("Light", "night_light") +@export var night_light_color: Color = Color(0.6, 0.8, 1.0): + set(v): + night_light_color = v + _update_sun_transform() +@export var night_light_energy: float = 0.2: + set(v): + night_light_energy = v + _update_sun_transform() +@export_subgroup("Sky", "shader") +@export var shader_night_lower_sky_color: Color = Color(0.03, 0.05, 0.09, 1): + set(v): + shader_night_lower_sky_color = v + _set_shader_param("night_lower_sky_color", v) +@export var shader_night_horizon_color: Color = Color(0.03, 0.05, 0.09, 1): + set(v): + shader_night_horizon_color = v + _set_shader_param("night_horizon_color", v) +@export var shader_night_zenith_color: Color = Color(0.069, 0.08, 0.109, 1.0): + set(v): + shader_night_zenith_color = v + _set_shader_param("night_zenith_color", v) +@export var shader_night_sky_energy: float = 0.3: + set(v): + shader_night_sky_energy = v + _set_shader_param("night_sky_energy", v) +@export var shader_stars_color: Color = Color(1.0, 1.0, 1.0, 1): + set(v): + shader_stars_color = v + _set_shader_param("stars_color", v) +@export var shader_stars_energy: float = 2.0: + set(v): + shader_stars_energy = v + _set_shader_param("stars_energy", v) +@export var shader_stars_size_min: float = 0.01: + set(v): + shader_stars_size_min = v + _set_shader_param("stars_size_min", v) +@export var shader_stars_size_max: float = 0.03: + set(v): + shader_stars_size_max = v + _set_shader_param("stars_size_max", v) +@export var shader_stars_edge_softness: float = 0.25: + set(v): + shader_stars_edge_softness = v + _set_shader_param("stars_edge_softness", v) +@export_subgroup("Ambient", "night_ambient") +@export var night_ambient_color: Color = Color(0.02, 0.03, 0.06): + set(v): night_ambient_color = v; _update_sun_transform() +@export var night_ambient_energy: float = 0.1: + set(v): night_ambient_energy = v; _update_sun_transform() +@export var night_ambient_sky_contribution: float = 0.8: + set(v): night_ambient_sky_contribution = v; _update_sun_transform() +@export_subgroup("Fog", "night_fog") +@export var night_fog_color: Color = Color(0.04, 0.06, 0.12): + set(v): night_fog_color = v; _update_sun_transform() +@export var night_fog_density: float = 0.02: + set(v): night_fog_density = v; _update_sun_transform() +@export var night_fog_sky_affect: float = 0.15: + set(v): night_fog_sky_affect = v; _update_sun_transform() +@export var night_fog_distance_begin: float = 2.6: + set(v): night_fog_distance_begin = v; _update_sun_transform() +@export var night_fog_distance: float = 200.0: + set(v): night_fog_distance = v; _update_sun_transform() +@export_subgroup("Volumetric fog", "night_vol_fog") +@export var night_vol_fog_albedo: Color = Color(0.15, 0.18, 0.25): + set(v): night_vol_fog_albedo = v; _update_sun_transform() +@export var night_vol_fog_density: float = 0.05: + set(v): night_vol_fog_density = v; _update_sun_transform() +@export var night_vol_fog_ambient_inject: float = 0.1: + set(v): night_vol_fog_ambient_inject = v; _update_sun_transform() +@export var night_vol_fog_sky_affect: float = 0.5: + set(v): night_vol_fog_sky_affect = v; _update_sun_transform() +@export var night_vol_fog_length: float = 3.0: + set(v): night_vol_fog_length = v; _update_sun_transform() + +@export_group("Clouds", "clouds") +@export var clouds_coverage: float = 0.25: + set(v): + clouds_coverage = clampf(v, 0.0, 1.0) + _set_shader_param("cloud_coverage", clouds_coverage) + _update_sun_transform() +@export var clouds_opacity: float = 0.85: + set(v): + clouds_opacity = v + _set_shader_param("cloud_opacity", v) +@export var clouds_softness: float = 0.2: + set(v): + clouds_softness = v + _set_shader_param("cloud_softness", v) +@export_range(0.0, 1.0, 0.001) var clouds_light_energy_overcast: float = 0.5: + set(v): + clouds_light_energy_overcast = clampf(v, 0.0, 1.0) + _update_sun_transform() +@export_subgroup("Generator", "clouds_generator") +@export var clouds_generator_seed_a: int = 10: + set(v): + clouds_generator_seed_a = v + _init_sky() +@export var clouds_generator_seed_b: int = 100: + set(v): + clouds_generator_seed_b = v + _init_sky() +@export var clouds_generator_frequency_a: float = 1.0: + set(v): + clouds_generator_frequency_a = v + _init_sky() +@export var clouds_generator_frequency_b: float = 0.8: + set(v): + clouds_generator_frequency_b = v + _init_sky() + +@export_subgroup("Colors", "clouds_color") +@export var clouds_color_light: Color = Color(1, 0.98, 0.95, 1): + set(v): + clouds_color_light = v + _set_shader_param("cloud_light_color", v) +@export var clouds_color_shadow: Color = Color(0.4, 0.45, 0.55, 1.0): + set(v): + clouds_color_shadow = v + _set_shader_param("cloud_shadow_color", v) +@export_subgroup("Shadow", "clouds_shadow") +@export var clouds_shadow_angular_distance_clear: float = 0.5: + set(v): + clouds_shadow_angular_distance_clear = maxf(v, 0.0) + clouds_shadow_angular_distance_overcast = maxf(clouds_shadow_angular_distance_overcast, clouds_shadow_angular_distance_clear) + _update_sun_transform() +@export var clouds_shadow_angular_distance_overcast: float = 15.0: + set(v): + clouds_shadow_angular_distance_overcast = maxf(v, clouds_shadow_angular_distance_clear) + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var clouds_shadow_opacity_clear: float = 1.0: + set(v): + clouds_shadow_opacity_clear = clampf(v, 0.0, 1.0) + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var clouds_shadow_opacity_overcast: float = 0.8: + set(v): + clouds_shadow_opacity_overcast = clampf(v, 0.0, 1.0) + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var clouds_shadow_soften_start: float = 0.28: + set(v): + clouds_shadow_soften_start = clampf(v, 0.0, 1.0) + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var clouds_shadow_soften_end: float = 0.9: + set(v): + clouds_shadow_soften_end = clampf(v, 0.0, 1.0) + _update_sun_transform() +@export_subgroup("Motion", "clouds") +@export var clouds_time_scale: float = 6.0: + set(v): + clouds_time_scale = v + _update_cloud_time() +@export var clouds_wind_direction: Vector2 = Vector2(0.8, 0.3): + set(v): + clouds_wind_direction = v + _update_cloud_wind() +@export var clouds_wind_strength: float = 1.0: + set(v): + clouds_wind_strength = maxf(v, 0.0) + _update_cloud_wind() +@export var clouds_wind_speed_multiplier: float = 1.0: + set(v): + clouds_wind_speed_multiplier = v + _update_cloud_wind() +@export var clouds_motion_scale: float = 0.12: + set(v): + clouds_motion_scale = v + _set_shader_param("cloud_motion_scale", v) +@export var clouds_evolution_speed: float = 0.04 +@export var clouds_evolution_strength: float = 0.18: + set(v): + clouds_evolution_strength = v + _set_shader_param("cloud_evolution_strength", v) +@export var clouds_evolution_scale: float = 0.018: + set(v): + clouds_evolution_scale = v + _set_shader_param("cloud_evolution_scale", v) +@export var clouds_scroll_a: Vector2 = Vector2(0.0012, 0.00015): + set(v): + clouds_scroll_a = v + _set_shader_param("cloud_scroll_a", v) +@export var clouds_scroll_b: Vector2 = Vector2(-0.0018, 0.0004): + set(v): + clouds_scroll_b = v + _set_shader_param("cloud_scroll_b", v) +@export_subgroup("Size and shape", "clouds") +@export var clouds_scale_a: Vector2 = Vector2(0.045, 0.055): + set(v): + clouds_scale_a = v + _set_shader_param("cloud_scale_a", v) +@export var clouds_scale_b: Vector2 = Vector2(0.082, 0.125): + set(v): + clouds_scale_b = v + _set_shader_param("cloud_scale_b", v) +@export_subgroup("Advanced", "clouds") +@export var clouds_plane_height: float = 0.187: + set(v): + clouds_plane_height = v + _set_shader_param("cloud_plane_height", v) +@export var clouds_plane_curve: float = 0.595: + set(v): + clouds_plane_curve = v + _set_shader_param("cloud_plane_curve", v) +@export var clouds_warp_strength: float = 0.053: + set(v): + clouds_warp_strength = v + _set_shader_param("cloud_warp_strength", v) +@export var clouds_horizon_fade: float = 0.481: + set(v): + clouds_horizon_fade = v + _set_shader_param("cloud_horizon_fade", v) +@export var clouds_top_fade: float = 0.118: + set(v): + clouds_top_fade = v + _set_shader_param("cloud_top_fade", v) +@export var clouds_forward_scatter: float = 1.5: + set(v): + clouds_forward_scatter = v + _set_shader_param("cloud_forward_scatter", v) +@export var clouds_backscatter: float = 0.390: + set(v): + clouds_backscatter = v + _set_shader_param("cloud_backscatter", v) +@export var clouds_sun_occlusion: float = 0.406: + set(v): + clouds_sun_occlusion = v + _set_shader_param("sun_cloud_occlusion", v) + +@export_group("Sun", "sun") +@export var sun_day_color: Color = Color(1, 0.98, 0.9, 1): + set(v): + sun_day_color = v + _set_shader_param("sun_color", v) +@export var sun_sunset_color: Color = Color(1.0, 0.4, 0.1, 1): + set(v): sun_sunset_color = v; _set_shader_param("sunset_sun_color", v) +@export var sun_disk_size: float = 0.04: + set(v): + sun_disk_size = v + _set_shader_param("sun_disk_size", v) +@export_range(0.0, 50.0, 0.01) var sun_seasonal_size_variation: float = 30.0: + set(v): + sun_seasonal_size_variation = v + _set_shader_param("sun_seasonal_size_variation", v) +@export var sun_disk_softness: float = 0.6: + set(v): + sun_disk_softness = v + _set_shader_param("sun_disk_softness", v) +@export var sun_disk_strength: float = 0.6: + set(v): + sun_disk_strength = v + _set_shader_param("sun_disk_strength", v) +@export var sun_halo_size: float = 0.2: + set(v): + sun_halo_size = v + _set_shader_param("sun_halo_size", v) +@export var sun_halo_strength: float = 0.5: + set(v): + sun_halo_strength = v + _set_shader_param("sun_halo_strength", v) +@export var sun_atmosphere_size: float = 0.4: + set(v): + sun_atmosphere_size = v + _set_shader_param("sun_atmosphere_size", v) +@export var sun_atmosphere_strength: float = 0.2: + set(v): + sun_atmosphere_strength = v + _set_shader_param("sun_atmosphere_strength", v) +@export var sun_energy_scale: float = 0.8: + set(v): + sun_energy_scale = v + _set_shader_param("sun_energy_scale", v) + +@export_group("Moon", "moon") +@export var moon_texture: Texture2D: + set(v): moon_texture = v; _set_shader_param("moon_texture", v) +@export var moon_color: Color = Color(0.9, 0.95, 1.0, 1): + set(v): + moon_color = v + _set_shader_param("moon_color", v) +@export var moon_size: float = 1.0: + set(v): + moon_size = v + _set_shader_param("moon_size", v) +@export var moon_glow_strength: float = 0.1: + set(v): + moon_glow_strength = v + _set_shader_param("moon_glow_strength", v) +@export var moon_eclipse_size: float = 0.8: + set(v): + moon_eclipse_size = v + _set_shader_param("moon_eclipse_size", v) +@export_range(0.1, 4.0) var moon_glow_size: float = 1.0: + set(v): + moon_glow_size = v + _set_shader_param("moon_glow_size", v) + +@export_group("Sunshafts", "sunshafts") +@export var sunshafts_enabled: bool = true: + set(v): + sunshafts_enabled = v + _update_effect() +@export var sunshafts_distance: float = 3000.0 +@export var sunshafts_moon_color: Color = Color(0.6, 0.7, 1.0, 1.0): + set(v): + sunshafts_moon_color = v + _update_effect() +@export var sunshafts_shaft_color: Color = Color(0.718, 0.637, 0.379, 1): + set(v): + sunshafts_shaft_color = v + _update_effect() +@export var sunshafts_density: float = 0.485: + set(v): + sunshafts_density = v + _update_effect() +@export var sunshafts_bright_threshold: float = 0.182: + set(v): + sunshafts_bright_threshold = v + _update_effect() +@export var sunshafts_weight: float = 0.0355: + set(v): + sunshafts_weight = v + _update_effect() +@export var sunshafts_decay: float = 0.93: + set(v): + sunshafts_decay = v + _update_effect() +@export var sunshafts_exposure: float = 1.59: + set(v): + sunshafts_exposure = v + _update_effect() +@export var sunshafts_max_radius: float = 1.377: + set(v): + sunshafts_max_radius = v + _update_effect() +@export_subgroup("Performance", "sunshafts_perf") +@export_range(4, 100) var sunshafts_perf_sample_count: int = 8: + set(v): + sunshafts_perf_sample_count = v + _update_effect() +@export_range(0.0, 2.0, 0.001) var sunshafts_perf_dither_strength: float = 1.4: + set(v): + sunshafts_perf_dither_strength = v + _update_effect() + +@export_group("Storm") +@export_range(0.0, 2.0, 0.01) var storm_flash_sky_energy: float = 0.18: + set(v): + storm_flash_sky_energy = maxf(v, 0.0) + _update_sun_transform() +@export_range(0.0, 2.0, 0.01) var storm_flash_night_sky_energy: float = 0.08: + set(v): + storm_flash_night_sky_energy = maxf(v, 0.0) + _update_sun_transform() +@export_range(0.0, 10.0, 0.01) var storm_flash_light_energy_clear: float = 0.9: + set(v): + storm_flash_light_energy_clear = maxf(v, 0.0) + _update_sun_transform() +@export_range(0.0, 10.0, 0.01) var storm_flash_light_energy_overcast: float = 5.6: + set(v): + storm_flash_light_energy_overcast = maxf(v, 0.0) + _update_sun_transform() +@export var storm_flash_light_color: Color = Color(0.83, 0.89, 1.0, 1.0): + set(v): + storm_flash_light_color = v + _update_sun_transform() +@export var storm_flash_ambient_color: Color = Color(0.76, 0.82, 0.96, 1.0): + set(v): + storm_flash_ambient_color = v + _update_sun_transform() +@export_range(0.0, 2.0, 0.01) var storm_flash_ambient_energy_base: float = 0.28: + set(v): + storm_flash_ambient_energy_base = maxf(v, 0.0) + _update_sun_transform() +@export_range(0.0, 2.0, 0.01) var storm_flash_ambient_energy_overcast_boost: float = 0.35: + set(v): + storm_flash_ambient_energy_overcast_boost = maxf(v, 0.0) + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var storm_flash_ambient_blend: float = 0.4: + set(v): + storm_flash_ambient_blend = clampf(v, 0.0, 1.0) + _update_sun_transform() +@export var storm_flash_fog_color: Color = Color(0.74, 0.82, 0.96, 1.0): + set(v): + storm_flash_fog_color = v + _update_sun_transform() +@export var storm_flash_volumetric_color: Color = Color(0.58, 0.66, 0.82, 1.0): + set(v): + storm_flash_volumetric_color = v + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var storm_flash_volumetric_blend: float = 0.35: + set(v): + storm_flash_volumetric_blend = clampf(v, 0.0, 1.0) + _update_sun_transform() + +@export_group("Fog") +@export_range(0.0, 1.0, 0.001) var fog_density: float = 0.0: + set(v): + var next := clampf(v, 0.0, 1.0) + if absf(fog_density - next) <= 0.0001: + return + fog_density = next + _update_sun_transform() + _update_effect() +@export_range(0.0, 1.0, 0.001) var fog_sky_affect_intensity: float = 1.0: + set(v): + fog_sky_affect_intensity = v + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var vol_fog_sky_affect_intensity: float = 1.0: + set(v): + vol_fog_sky_affect_intensity = v + _update_sun_transform() +@export_range(0.0, 1.0, 0.001) var storm_fog_emission_scale: float = 1.0: + set(v): + storm_fog_emission_scale = v + _update_sun_transform() + _update_effect() +@export_range(0.0, 1.0, 0.001) var lightning_flash: float = 0.0: + set(v): + var next := clampf(v, 0.0, 1.0) + if absf(lightning_flash - next) <= 0.0001: + return + lightning_flash = next + _update_sun_transform() + _update_effect() + +@export_group("Debug") +@export var force_refresh: bool = false: + set(v): + if v: + _refresh() + force_refresh = false +@export_range(0.0, 1.0) var moon_phase_debug: float + +@export_group("Sky Shader: Atmosphere") +@export var shader_atmosphere_horizon_level: float = -0.035: + set(v): + shader_atmosphere_horizon_level = clampf(v, -0.2, 0.2) + _set_shader_param("atmosphere_horizon_level", shader_atmosphere_horizon_level) +@export var shader_atmosphere_height: float = 0.24: + set(v): + shader_atmosphere_height = clampf(v, 0.02, 0.6) + _set_shader_param("atmosphere_height", shader_atmosphere_height) +@export var shader_atmosphere_density: float = 0.46: + set(v): + shader_atmosphere_density = clampf(v, 0.0, 2.0) + _set_shader_param("atmosphere_density", shader_atmosphere_density) +@export var shader_atmosphere_sun_scatter: float = 0.34: + set(v): + shader_atmosphere_sun_scatter = clampf(v, 0.0, 2.0) + _set_shader_param("atmosphere_sun_scatter", shader_atmosphere_sun_scatter) +@export var shader_atmosphere_sunset_boost: float = 1.35: + set(v): + shader_atmosphere_sunset_boost = clampf(v, 0.0, 3.0) + _set_shader_param("atmosphere_sunset_boost", shader_atmosphere_sunset_boost) + +@export_group("Sky Shader: GI (SDFGI Fill)") +@export var gi_day_tint: Color = Color(0.8, 0.75, 0.7, 1.0): + set(v): + gi_day_tint = v + _update_sun_transform() +@export var gi_day_energy: float = 0.6: + set(v): + gi_day_energy = v + _update_sun_transform() +@export var gi_night_tint: Color = Color(0.2, 0.4, 0.8, 1.0): + set(v): + gi_night_tint = v + _update_sun_transform() +@export var gi_night_energy: float = 2.0: + set(v): + gi_night_energy = v + _update_sun_transform() + + + +func _ready() -> void: + _rendered_day = day_of_year + _rendered_time = time_of_day + _is_ready = true + + _refresh() + set_process(true) + + +func _exit_tree(): + _remove_sunshafts_compositor_effect() + +func _process(_delta: float) -> void: + _advance_time_transition(_delta) + _process_sunshafts() + + +func apply_now() -> void: + _init_sky() + _request_time_update(true) + _update_effect() + + +func apply_wind_now() -> void: + _update_cloud_wind() + +func _refresh() -> void: + if not is_inside_tree(): + return + + _camera = _find_active_camera() + _environment = _get_environment() + _light = _get_directional_light() + + _remove_sunshafts_compositor_effect() + _install_sunshafts_compositor_effect() + _init_sky() + _update_sun_transform() + _update_cloud_time() + _update_cloud_wind() + _update_effect() + + _success("(Re)Initialized") + + +func _get_final_cloud_density() -> float: + var base_cloud_density := clouds_coverage + var current_cloud_overcast_intensity := 0.0 + var cloud_overcast_mix := clampf(current_cloud_overcast_intensity, 0.0, 1.0) + if base_cloud_density <= 0.0001: + return 0.0 + return lerpf(base_cloud_density, maxf(base_cloud_density, 0.88), cloud_overcast_mix) + + +func _apply_cloud_light_response(light: DirectionalLight3D) -> void: + if not light: + return + var final_cloud_density := _get_final_cloud_density() + var soften_start := minf(clouds_shadow_soften_start, clouds_shadow_soften_end) + var soften_end := maxf(clouds_shadow_soften_start, clouds_shadow_soften_end) + var shadow_soften := smoothstep(soften_start, soften_end, final_cloud_density) + light.light_angular_distance = lerpf( + clouds_shadow_angular_distance_clear, + clouds_shadow_angular_distance_overcast, + shadow_soften + ) + light.shadow_opacity = lerpf( + clouds_shadow_opacity_clear, + clouds_shadow_opacity_overcast, + shadow_soften + ) + + + +func _get_directional_light() -> DirectionalLight3D: + if not is_inside_tree(): return null + if not directional_light_path.is_empty(): + return get_node_or_null(directional_light_path) as DirectionalLight3D + else: + return null + +func _get_world_environment() -> WorldEnvironment: + if not world_environment_path.is_empty(): + return get_node_or_null(world_environment_path) + return null + +func _get_environment() -> Environment: + var world_environment := _get_world_environment() + if world_environment: + return world_environment.environment + return null + + +func _get_editor_access(): + if _editor_access != null: + return _editor_access + if not Engine.is_editor_hint(): + return null + _editor_access = load(EDITOR_ACCESS_SCRIPT_PATH) + return _editor_access + +func _get_compositor() -> Compositor: + var we := _get_world_environment() + if we and "compositor" in we: + if we.compositor: return we.compositor + + if Engine.is_editor_hint(): + var editor_camera = _get_editor_access().get_editor_camera_3d(0) if _get_editor_access() != null else null + if editor_camera and "compositor" in editor_camera: + return editor_camera.compositor + return null + + var vp := get_viewport() + if vp: + var cam := vp.get_camera_3d() + if cam and "compositor" in cam: + return cam.compositor + + var camera := _find_active_camera() + return camera.compositor if camera else null + +func _set_compositor(compositor: Compositor): + var we := _get_world_environment() + if we and "compositor" in we: + we.compositor = compositor + return + + if Engine.is_editor_hint(): + var editor_camera = _get_editor_access().get_editor_camera_3d(0) if _get_editor_access() != null else null + if editor_camera and "compositor" in editor_camera: + editor_camera.compositor = compositor + return + + var vp := get_viewport() + if vp: + var cam := vp.get_camera_3d() + if cam and "compositor" in cam: + cam.compositor = compositor + return + + var camera := _find_active_camera() + if camera and "compositor" in camera: + camera.compositor = compositor +func _init_sky() -> void: + if not _environment: + return + + _environment.sky = Sky.new() + _environment.background_mode = Environment.BG_SKY + + if not _sky_material: + _sky_material = ShaderMaterial.new() + _sky_material.shader = FILMIC_SKY_SHADER + + if not _cloud_texture_a: + _cloud_texture_a = NoiseTexture2D.new() + _cloud_texture_a.seamless = true + var noise := FastNoiseLite.new() + _cloud_texture_a.noise = noise + + if not _cloud_texture_b: + _cloud_texture_b = NoiseTexture2D.new() + _cloud_texture_b.seamless = true + var noise := FastNoiseLite.new() + _cloud_texture_b.noise = noise + + _cloud_texture_a.noise.seed = clouds_generator_seed_a + _cloud_texture_a.noise.frequency = clouds_generator_frequency_a * 0.01 + _cloud_texture_b.noise.seed = clouds_generator_seed_b + _cloud_texture_b.noise.frequency = clouds_generator_frequency_b * 0.01 + + _environment.sky.sky_material = _sky_material + + if not _compositor_effect: + _install_sunshafts_compositor_effect() + + _sync_sky_shader_params() + _reset_cloud_time_tracking() + + +func _set_shader_param(param_name: String, value: Variant) -> void: + if _sky_material: + _sky_material.set_shader_parameter(param_name, value) + +func _sync_sky_shader_params() -> void: + if not _sky_material: + return + + _sky_material.set_shader_parameter("lower_sky_color", shader_lower_sky_color) + _sky_material.set_shader_parameter("horizon_color", shader_horizon_color) + _sky_material.set_shader_parameter("zenith_color", shader_zenith_color) + _sky_material.set_shader_parameter("sky_energy", shader_sky_energy) + _sky_material.set_shader_parameter("horizon_height", shader_horizon_height) + _sky_material.set_shader_parameter("horizon_softness", shader_horizon_softness) + _sky_material.set_shader_parameter("zenith_curve", shader_zenith_curve) + _sky_material.set_shader_parameter("horizon_glow_strength", shader_horizon_glow_strength) + _sky_material.set_shader_parameter("atmosphere_horizon_level", shader_atmosphere_horizon_level) + _sky_material.set_shader_parameter("atmosphere_height", shader_atmosphere_height) + _sky_material.set_shader_parameter("atmosphere_density", shader_atmosphere_density) + _sky_material.set_shader_parameter("atmosphere_sun_scatter", shader_atmosphere_sun_scatter) + _sky_material.set_shader_parameter("atmosphere_sunset_boost", shader_atmosphere_sunset_boost) + _sky_material.set_shader_parameter("rainbow_intensity", rainbow_intensity) + _sky_material.set_shader_parameter("rainbow_secondary_intensity", rainbow_secondary_intensity) + _sky_material.set_shader_parameter("high_quality_sky", shader_high_quality_sky) + + _sky_material.set_shader_parameter("sunset_bottom_color", shader_sunset_bottom_color) + _sky_material.set_shader_parameter("sunset_horizon_color", shader_sunset_horizon_color) + _sky_material.set_shader_parameter("sunset_zenith_color", shader_sunset_zenith_color) + _sky_material.set_shader_parameter("sunset_cloud_color", shader_sunset_cloud_color) + _sky_material.set_shader_parameter("sunset_sun_color", sun_sunset_color) + + _sky_material.set_shader_parameter("night_lower_sky_color", shader_night_lower_sky_color) + _sky_material.set_shader_parameter("night_horizon_color", shader_night_horizon_color) + _sky_material.set_shader_parameter("night_zenith_color", shader_night_zenith_color) + _sky_material.set_shader_parameter("night_sky_energy", shader_night_sky_energy) + _sky_material.set_shader_parameter("stars_color", shader_stars_color) + _sky_material.set_shader_parameter("stars_energy", shader_stars_energy) + _sky_material.set_shader_parameter("stars_size_min", shader_stars_size_min) + _sky_material.set_shader_parameter("stars_size_max", shader_stars_size_max) + _sky_material.set_shader_parameter("stars_edge_softness", shader_stars_edge_softness) + + _sky_material.set_shader_parameter("sun_color", sun_day_color) + _sky_material.set_shader_parameter("sun_disk_size", sun_disk_size) + _sky_material.set_shader_parameter("sun_seasonal_size_variation", sun_seasonal_size_variation) + _sky_material.set_shader_parameter("sun_disk_softness", sun_disk_softness) + _sky_material.set_shader_parameter("sun_disk_strength", sun_disk_strength) + _sky_material.set_shader_parameter("sun_halo_size", sun_halo_size) + _sky_material.set_shader_parameter("sun_halo_strength", sun_halo_strength) + _sky_material.set_shader_parameter("sun_atmosphere_size", sun_atmosphere_size) + _sky_material.set_shader_parameter("sun_atmosphere_strength", sun_atmosphere_strength) + _sky_material.set_shader_parameter("sun_energy_scale", sun_energy_scale) + + _sky_material.set_shader_parameter("moon_color", moon_color) + _sky_material.set_shader_parameter("moon_size", moon_size) + _sky_material.set_shader_parameter("moon_glow_strength", moon_glow_strength) + _sky_material.set_shader_parameter("moon_eclipse_size", moon_eclipse_size) + _sky_material.set_shader_parameter("moon_glow_size", moon_glow_size) + _sky_material.set_shader_parameter("moon_texture", moon_texture) + + _sky_material.set_shader_parameter("cloud_tex_a", _cloud_texture_a) + _sky_material.set_shader_parameter("cloud_tex_b", _cloud_texture_b) + _sky_material.set_shader_parameter("cloud_scroll_a", clouds_scroll_a) + _sky_material.set_shader_parameter("cloud_scroll_b", clouds_scroll_b) + _sky_material.set_shader_parameter("cloud_scale_a", clouds_scale_a) + _sky_material.set_shader_parameter("cloud_scale_b", clouds_scale_b) + _sky_material.set_shader_parameter("cloud_plane_height", clouds_plane_height) + _sky_material.set_shader_parameter("cloud_plane_curve", clouds_plane_curve) + _sky_material.set_shader_parameter("cloud_warp_strength", clouds_warp_strength) + _sky_material.set_shader_parameter("cloud_coverage", clouds_coverage) + _sky_material.set_shader_parameter("cloud_softness", clouds_softness) + _sky_material.set_shader_parameter("cloud_opacity", clouds_opacity) + _sky_material.set_shader_parameter("cloud_horizon_fade", clouds_horizon_fade) + _sky_material.set_shader_parameter("cloud_top_fade", clouds_top_fade) + _sky_material.set_shader_parameter("cloud_light_color", clouds_color_light) + _sky_material.set_shader_parameter("cloud_shadow_color", clouds_color_shadow) + _sky_material.set_shader_parameter("cloud_forward_scatter", clouds_forward_scatter) + _sky_material.set_shader_parameter("cloud_backscatter", clouds_backscatter) + _sky_material.set_shader_parameter("sun_cloud_occlusion", clouds_sun_occlusion) + + _sky_material.set_shader_parameter("cloud_time", _get_cloud_time_value()) + _sky_material.set_shader_parameter("cloud_motion_time", _cloud_motion_time) + _sky_material.set_shader_parameter("cloud_evolution_time", _cloud_evolution_time) + _sky_material.set_shader_parameter("cloud_motion_scale", clouds_motion_scale) + _sky_material.set_shader_parameter("cloud_evolution_strength", clouds_evolution_strength) + _sky_material.set_shader_parameter("cloud_evolution_scale", clouds_evolution_scale) + _apply_cloud_wind_params() + +func _get_cloud_time_value() -> float: + return ((float(_rendered_day - 1) * 24.0) + _rendered_time) * clouds_time_scale + + +func _get_rendered_total_hours() -> float: + return float(_rendered_day) * 24.0 + _rendered_time + + +func _reset_cloud_time_tracking() -> void: + _last_cloud_total_hours = _get_rendered_total_hours() + _set_shader_param("cloud_motion_time", _cloud_motion_time) + _set_shader_param("cloud_evolution_time", _cloud_evolution_time) + + +func _update_cloud_time(current_total_hours: float = INF) -> void: + if is_inf(current_total_hours): + current_total_hours = _get_rendered_total_hours() + var delta_hours := current_total_hours - _last_cloud_total_hours + _last_cloud_total_hours = current_total_hours + var delta_world_seconds := delta_hours * 3600.0 + + if absf(delta_world_seconds) > 0.0001: + _cloud_motion_time += delta_world_seconds * _get_cloud_wind_speed() * clouds_motion_scale + _cloud_evolution_time += delta_world_seconds * clouds_evolution_speed + + _set_shader_param("cloud_time", _get_cloud_time_value()) + _set_shader_param("cloud_motion_time", _cloud_motion_time) + _set_shader_param("cloud_evolution_time", _cloud_evolution_time) + + +func _get_cloud_wind_speed() -> float: + return maxf(clouds_wind_strength, 0.0) * clouds_wind_speed_multiplier + + +func _get_cloud_wind_direction() -> Vector2: + if clouds_wind_direction.length_squared() <= 0.000001: + return Vector2(1.0, 0.0) + return clouds_wind_direction.normalized() + + +func _apply_cloud_wind_params() -> void: + _sky_material.set_shader_parameter("cloud_wind_direction", _get_cloud_wind_direction()) + _sky_material.set_shader_parameter("cloud_wind_speed", _get_cloud_wind_speed()) + + +func _update_cloud_wind() -> void: + if _sky_material: + _apply_cloud_wind_params() + + +func _request_time_update(snap: bool = false) -> void: + if not is_inside_tree(): + return + var target_hours := float(day_of_year) * 24.0 + time_of_day + var current_hours := float(_rendered_day) * 24.0 + _rendered_time + var same_day_wrap := day_of_year == _rendered_day and absf(time_of_day - _rendered_time) > 12.0 + + if Engine.is_editor_hint() or time_transition_duration <= 0.0 or snap: + _stop_time_transition() + _apply_total_hours(target_hours) + return + + if same_day_wrap: + var wrapped_target_time := _rendered_time + _get_wrapped_time_delta(_rendered_time, time_of_day) + var wrapped_delta := wrapped_target_time - _rendered_time + if absf(wrapped_delta) <= 0.0001: + _stop_time_transition() + _apply_wrapped_time_of_day(wrapped_target_time) + return + _time_transition_wrapped = true + _time_transition_target_unwrapped_time = wrapped_target_time + _time_transition_speed_hours_per_second = wrapped_delta / time_transition_duration + _time_transition_active = true + return + + var total_delta := target_hours - current_hours + if absf(total_delta) <= 0.0001: + _stop_time_transition() + _apply_total_hours(target_hours) + return + _time_transition_wrapped = false + _time_transition_target_total_hours = target_hours + _time_transition_speed_hours_per_second = total_delta / time_transition_duration + _time_transition_active = true + + +func _get_wrapped_time_delta(from_time: float, to_time: float) -> float: + return wrapf((to_time - from_time) + 12.0, 0.0, 24.0) - 12.0 + + +func _advance_time_transition(delta: float) -> void: + if not _time_transition_active or delta <= 0.0: + return + + var step := absf(_time_transition_speed_hours_per_second) * delta + if _time_transition_wrapped: + var next_unwrapped_time := move_toward(_rendered_time, _time_transition_target_unwrapped_time, step) + _apply_wrapped_time_of_day(next_unwrapped_time) + if absf(next_unwrapped_time - _time_transition_target_unwrapped_time) <= 0.0001: + _stop_time_transition() + return + + var current_hours := float(_rendered_day) * 24.0 + _rendered_time + var next_total_hours := move_toward(current_hours, _time_transition_target_total_hours, step) + _apply_total_hours(next_total_hours) + if absf(next_total_hours - _time_transition_target_total_hours) <= 0.0001: + _stop_time_transition() + + +func _stop_time_transition() -> void: + _time_transition_active = false + _time_transition_wrapped = false + _time_transition_speed_hours_per_second = 0.0 + + +func _apply_wrapped_time_of_day(unwrapped_time: float) -> void: + _rendered_day = day_of_year + _rendered_time = wrapf(unwrapped_time, 0.0, 24.0) + _update_sun_transform() + _update_cloud_time(float(day_of_year) * 24.0 + unwrapped_time) + time_changed.emit(_rendered_day, _rendered_time) + +func _apply_total_hours(total_hours: float) -> void: + var day_new = int(floor(total_hours / 24.0)) + var new_time = fmod(total_hours, 24.0) + if day_new != _rendered_day: + day_changed.emit(day_new) + _rendered_day = day_new + _rendered_time = new_time + + _update_sun_transform() + _update_cloud_time(total_hours) + time_changed.emit(_rendered_day, _rendered_time) + +func _update_sun_transform() -> void: + if not is_inside_tree(): + return + + var light = _get_directional_light() + var day_current = float(_rendered_day) + _rendered_time / 24.0 + var moon_phase = fmod( day_current / 29.53, 1.0) + moon_phase_debug = moon_phase + + var theta_sun = deg_to_rad(360.0 / 365.0 * ( day_current + 10.0)) + var declination_sun = deg_to_rad(-23.45) * cos(theta_sun) + + var theta_moon = theta_sun - moon_phase * TAU + var declination_moon = deg_to_rad(-23.45) * cos(theta_moon) + deg_to_rad(5.14) * sin(theta_moon) + + var hour_angle = deg_to_rad(15.0 * (_rendered_time - 12.0)) + var lat_rad = deg_to_rad(latitude) + + var get_dir = func(ha: float, dec: float) -> Vector3: + var y = sin(lat_rad) * sin(dec) + cos(lat_rad) * cos(dec) * cos(ha) + var x = -cos(dec) * sin(ha) + var z = sin(lat_rad) * cos(dec) * cos(ha) - cos(lat_rad) * sin(dec) + return Vector3(x, y, z).normalized() + + + var sun_dir = get_dir.call(hour_angle, declination_sun) + var moon_hour_angle = hour_angle - moon_phase * TAU + var moon_dir = get_dir.call(moon_hour_angle, declination_moon) + + var sidereal_time = deg_to_rad( day_current * 360.0 + _rendered_time * 15.0) + var celestial_basis = Basis() + celestial_basis = celestial_basis.rotated(Vector3.RIGHT, lat_rad - PI / 2.0) + celestial_basis = celestial_basis.rotated(Vector3.UP, -sidereal_time) + + _set_shader_param("custom_sun_dir", sun_dir) + _set_shader_param("custom_moon_dir", moon_dir) + _set_shader_param("celestial_matrix", celestial_basis) + _set_shader_param("rendered_day_of_year", float(_rendered_day)) + _set_shader_param("rendered_time_of_day", _rendered_time) + _set_shader_param("observer_latitude_deg", latitude) + + var dir_to_basis = func(dir: Vector3) -> Basis: + var up = Vector3.UP + if abs(dir.y) > 0.999: + up = Vector3.RIGHT + var right = up.cross(dir).normalized() + var new_up = dir.cross(right).normalized() + return Basis(right, new_up, dir) + + var s_alt = sun_dir.y + var m_alt = moon_dir.y + + _day_blend = smoothstep(-0.2, 0.3, s_alt) + _sunset_blend = smoothstep(-0.2, 0.05, s_alt) * (1.0 - smoothstep(0.1, 0.4, s_alt)) + + var sun_energy = day_light_energy * smoothstep(-0.05, 0.08, s_alt) + var moon_energy = night_light_energy * smoothstep(0.0, 0.05, m_alt) * (1.0 - smoothstep(-0.1, 0.0, s_alt)) + + if sun_energy >= moon_energy: + if light: + light.global_transform.basis = dir_to_basis.call(sun_dir) + light.light_color = day_light_color.lerp(sunset_light_color, _sunset_blend) + light.light_energy = sun_energy + _is_daytime = true + else: + if light: + light.global_transform.basis = dir_to_basis.call(moon_dir) + light.light_color = night_light_color + light.light_energy = moon_energy + _is_daytime = false + + _set_shader_param("gi_tint", gi_night_tint.lerp(gi_day_tint, _day_blend)) + _set_shader_param("gi_energy_multiplier", lerp(gi_night_energy, gi_day_energy, _day_blend) + _sunset_blend * 0.5) + + if _environment: + var env = _environment + env.ambient_light_color = night_ambient_color.lerp( day_ambient_color, _day_blend) + env.ambient_light_energy = lerp( night_ambient_energy, day_ambient_energy, pow(_day_blend, 0.5)) + _sunset_blend * 0.8 + + var fog_day_mix = night_fog_color.lerp( day_fog_color, _day_blend) + env.fog_light_color = fog_day_mix.lerp(sunset_light_color, _sunset_blend * 0.5) + env.fog_density = lerp( night_fog_density, day_fog_density, _day_blend) + env.fog_sky_affect = lerp( night_fog_sky_affect, day_fog_sky_affect, _day_blend) + env.fog_depth_begin = lerp( night_fog_distance_begin, day_fog_distance_begin, _day_blend) + env.fog_depth_end = lerp(night_fog_distance, day_fog_distance, _day_blend) + + var vol_day_mix = night_vol_fog_albedo.lerp( day_vol_fog_albedo, _day_blend) + env.volumetric_fog_albedo = vol_day_mix.lerp(sunset_light_color, _sunset_blend * 0.3) + + var current_vol_fog_density = lerp( night_vol_fog_density, day_vol_fog_density, _day_blend) + env.volumetric_fog_density = current_vol_fog_density + + env.volumetric_fog_sky_affect = lerp( night_vol_fog_sky_affect, day_vol_fog_sky_affect, _day_blend) + env.volumetric_fog_length = lerp( night_vol_fog_length, day_vol_fog_length, _day_blend) + env.volumetric_fog_ambient_inject = lerp( night_vol_fog_ambient_inject, day_vol_fog_ambient_inject, _day_blend) + + _apply_state_params(env, light) + else: + _apply_state_params(null, light) + _apply_cloud_light_response(light) + + +func _apply_state_params(env: Environment, light: DirectionalLight3D) -> void: + var current_lightning_flash := clampf(lightning_flash, 0.0, 1.0) + var current_storm_fog_emission_scale := clampf(storm_fog_emission_scale, 0.0, 1.0) + + var current_cloud_overcast_intensity := 0.0 + var base_fog_density := lerpf(night_fog_density, day_fog_density, _day_blend) + var base_vol_fog_density := lerpf(night_vol_fog_density, day_vol_fog_density, _day_blend) + + var fog_density_boost := clampf(fog_density, 0.0, 1.0) + var vol_fog_density_boost := clampf(fog_density * 0.05, 0.0, 1.0) + var current_fog_density := clampf(base_fog_density + fog_density_boost, 0.0, 1.5) + var current_vol_fog_density := clampf(base_vol_fog_density + vol_fog_density_boost, 0.0, 1.0) + + var cloud_mix := clampf(current_cloud_overcast_intensity * 0.8, 0.0, 1.0) + var final_cloud_density := _get_final_cloud_density() + var sky_overcast := clampf(maxf(final_cloud_density, cloud_mix), 0.0, 1.0) + var overcast_cooling := clampf(sky_overcast * 0.78, 0.0, 1.0) + + _set_shader_param("cloud_coverage", final_cloud_density) + _set_shader_param("cloud_opacity", lerpf(clouds_opacity, maxf(clouds_opacity, 0.95), clampf(sky_overcast * 0.85, 0.0, 1.0))) + _set_shader_param("cloud_shadow_color", clouds_color_shadow) + _set_shader_param("cloud_light_color", clouds_color_light.lerp(Color(0.66, 0.7, 0.78, 1.0), clampf(sky_overcast * 0.35, 0.0, 1.0))) + _set_shader_param("sun_cloud_occlusion", clampf(clouds_sun_occlusion + sky_overcast * 0.42, 0.0, 0.98)) + _set_shader_param("sky_energy", maxf(0.02, shader_sky_energy * (1.0 - sky_overcast * 0.46) + current_lightning_flash * storm_flash_sky_energy)) + _set_shader_param("night_sky_energy", maxf(0.02, shader_night_sky_energy * (1.0 - sky_overcast * 0.34) + current_lightning_flash * storm_flash_night_sky_energy)) + _set_shader_param("stars_energy", maxf(0.0, shader_stars_energy * (1.0 - cloud_mix * 0.98))) + _set_shader_param("moon_color", moon_color.lerp(Color(0.045, 0.05, 0.06, 1.0), cloud_mix * 0.96)) + _set_shader_param("moon_size", lerpf(moon_size, moon_size * 0.72, cloud_mix * 0.85)) + _set_shader_param("moon_glow_strength", maxf(0.0, moon_glow_strength * (1.0 - cloud_mix * 0.96))) + + if light: + light.light_energy += current_lightning_flash * lerpf(storm_flash_light_energy_clear, storm_flash_light_energy_overcast, sky_overcast) + light.light_color = light.light_color.lerp(Color(0.58, 0.62, 0.68, 1.0), overcast_cooling * 0.94) + light.light_color = light.light_color.lerp(storm_flash_light_color, current_lightning_flash * 0.8) + + if env == null: + return + + env.ambient_light_color = env.ambient_light_color.lerp(Color(0.38, 0.41, 0.46, 1.0), overcast_cooling * 0.82) + env.ambient_light_energy *= maxf(0.18, 1.0 - (sky_overcast * 0.24)) + + env.fog_density = current_fog_density + env.fog_sky_affect = lerpf(env.fog_sky_affect, 1.0, fog_sky_affect_intensity * fog_density_boost) + + env.volumetric_fog_density = current_vol_fog_density + + env.volumetric_fog_sky_affect = lerpf(env.volumetric_fog_sky_affect, 1.0, vol_fog_sky_affect_intensity * fog_density_boost) + env.volumetric_fog_length = maxf(2.0, env.volumetric_fog_length * (1.0 - fog_density_boost * 0.8)) + + var base_emission := Color(0, 0, 0) + var volumetric_emission := base_emission * current_storm_fog_emission_scale + if current_lightning_flash > 0.0: + volumetric_emission = volumetric_emission.lerp(storm_flash_volumetric_color * (current_lightning_flash * 0.55), current_lightning_flash * storm_flash_volumetric_blend) + env.volumetric_fog_emission = volumetric_emission + + if current_lightning_flash > 0.0: + env.ambient_light_color = env.ambient_light_color.lerp(storm_flash_ambient_color, current_lightning_flash * storm_flash_ambient_blend) + env.ambient_light_energy += current_lightning_flash * (storm_flash_ambient_energy_base + sky_overcast * storm_flash_ambient_energy_overcast_boost) + env.fog_light_color = env.fog_light_color.lerp(storm_flash_fog_color, current_lightning_flash * 0.7) + env.volumetric_fog_albedo = env.volumetric_fog_albedo.lerp(storm_flash_volumetric_color, current_lightning_flash * storm_flash_volumetric_blend) + + +func _get_all_compositors() -> Array[Compositor]: + var compositors: Array[Compositor] = [] + + var we := _get_world_environment() + if we and "compositor" in we and we.compositor: + compositors.append(we.compositor) + + if Engine.is_editor_hint(): + var editor_camera = _get_editor_access().get_editor_camera_3d(0) if _get_editor_access() != null else null + if editor_camera and "compositor" in editor_camera and editor_camera.compositor: + if not compositors.has(editor_camera.compositor): + compositors.append(editor_camera.compositor) + + var vp := get_viewport() + if vp: + var cam := vp.get_camera_3d() + if cam and "compositor" in cam and cam.compositor: + if not compositors.has(cam.compositor): + compositors.append(cam.compositor) + + var camera := _find_active_camera() + if camera and "compositor" in camera and camera.compositor: + if not compositors.has(camera.compositor): + compositors.append(camera.compositor) + + return compositors + +func _install_sunshafts_compositor_effect() -> void: + _remove_sunshafts_compositor_effect() + + if not sunshafts_enabled: + return + + var compositor := _get_compositor() + if not compositor: + var has_target := false + if _get_world_environment(): + has_target = true + elif Engine.is_editor_hint(): + var editor_camera = _get_editor_access().get_editor_camera_3d(0) if _get_editor_access() != null else null + if editor_camera: + has_target = true + else: + var vp := get_viewport() + if vp and vp.get_camera_3d(): has_target = true + elif _find_active_camera(): has_target = true + + if has_target: + compositor = Compositor.new() + _set_compositor(compositor) + else: + return + + if not compositor.resource_path.is_empty(): + compositor = compositor.duplicate(true) as Compositor + _set_compositor(compositor) + + _compositor_effect = SUN_SHAFTS_EFFECT_SCRIPT.new() + _compositor_effect.set("sun_visible", true) + var effects = compositor.compositor_effects + effects.insert(0, _compositor_effect) + compositor.compositor_effects = effects + _success("Installed sunshafts compositor effect") + + +func _remove_sunshafts_compositor_effect() -> void: + for comp in _get_all_compositors(): + var effects = comp.compositor_effects + var changed = false + var i := effects.size() - 1 + while i >= 0: + if effects[i] != null and effects[i].get_script() == SUN_SHAFTS_EFFECT_SCRIPT: + effects.remove_at(i) + changed = true + i -= 1 + if changed: + comp.compositor_effects = effects + + _compositor_effect = null + +func _update_effect() -> void: + if not sunshafts_enabled: + if _compositor_effect: + _remove_sunshafts_compositor_effect() + return + + if not _compositor_effect: + _install_sunshafts_compositor_effect() + + if not _compositor_effect: + return + + _viewport_size = _get_active_viewport_size() + _camera = _find_active_camera() + _light = _get_directional_light() + + var current_base_color = sunshafts_moon_color.lerp(sunshafts_shaft_color, _day_blend) + + var cloud_occlusion := clampf(_get_final_cloud_density() * 0.85, 0.0, 0.96) + var shafts_visibility := 1.0 - cloud_occlusion + + _compositor_effect.set("shaft_color", current_base_color.lerp(Color(0.72, 0.74, 0.78, 1.0), cloud_occlusion * 0.5)) + _compositor_effect.set("density", sunshafts_density * shafts_visibility * lerpf(0.7, 1.0, _day_blend)) + _compositor_effect.set("bright_threshold", sunshafts_bright_threshold) + _compositor_effect.set("weight", sunshafts_weight * shafts_visibility * lerpf(1.5, 1.0, _day_blend)) + _compositor_effect.set("decay", sunshafts_decay) + _compositor_effect.set("exposure", sunshafts_exposure * shafts_visibility * lerpf(1.3, 1.0, _day_blend)) + _compositor_effect.set("max_radius", sunshafts_max_radius) + _compositor_effect.set("sample_count", sunshafts_perf_sample_count) + _compositor_effect.set("dither_strength", sunshafts_perf_dither_strength) + +func _process_sunshafts() -> void: + if sunshafts_enabled and _compositor_effect and _camera and _light: + var sun_dir = _light.global_transform.basis.z.normalized() + var sun_world_pos = _camera.global_position + (sun_dir * sunshafts_distance) + var screen_pos = _camera.unproject_position(sun_world_pos) + _compositor_effect.set("sun_screen_uv", Vector2(screen_pos.x / _viewport_size.x, screen_pos.y / _viewport_size.y)) + +func _find_active_camera() -> Camera3D: + if not is_inside_tree(): return null + if Engine.is_editor_hint(): + var editor_access = _get_editor_access() + if editor_access != null: + return editor_access.get_editor_camera_3d(0) + var vp = get_viewport() + return vp.get_camera_3d() if vp else null + +func _get_active_viewport_size() -> Vector2: + if not is_inside_tree(): return Vector2.ZERO + if Engine.is_editor_hint(): + var editor_access = _get_editor_access() + if editor_access != null: + return editor_access.get_editor_viewport_size(0) + var vp = get_viewport() + return vp.get_visible_rect().size if vp else Vector2.ZERO + + +func get_day_blend() -> float: + return _day_blend diff --git a/demo/addons/gnd_skydome/Skydome.gd.uid b/demo/addons/gnd_skydome/Skydome.gd.uid new file mode 100644 index 00000000..6f3d82ad --- /dev/null +++ b/demo/addons/gnd_skydome/Skydome.gd.uid @@ -0,0 +1 @@ +uid://blkilnimp0sck diff --git a/demo/addons/gnd_skydome/SunShaftsCompositorEffect.gd b/demo/addons/gnd_skydome/SunShaftsCompositorEffect.gd new file mode 100644 index 00000000..4e3cdafb --- /dev/null +++ b/demo/addons/gnd_skydome/SunShaftsCompositorEffect.gd @@ -0,0 +1,344 @@ +@tool +class_name SunShaftsCompositorEffect +extends CompositorEffect + +const SHADER_CODE := """ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(set = 0, binding = 0) uniform sampler2D source_color; +layout(set = 0, binding = 1) uniform sampler2D source_depth; +layout(rgba16f, set = 0, binding = 2) uniform image2D color_image; + +layout(push_constant, std430) uniform Params { + vec2 raster_size; + vec2 sun_uv; + vec4 shaft_color; + vec4 shaft_params_a; // x: density, y: threshold, z: weight, w: decay + vec4 shaft_params_b; // x: visible, y: max_radius, z: exposure, w: unused + vec4 perf_params; // x: sample_count, y: dither_strength + vec4 debug_params; +} params; + +float luminance(vec3 c) { + return dot(c, vec3(0.2126, 0.7152, 0.0722)); +} + +float interleaved_gradient_noise(vec2 uv) { + vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189); + return fract(magic.z * fract(dot(uv, magic.xy))); +} + +void main() { + ivec2 pixel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = ivec2(params.raster_size); + if (pixel.x >= size.x || pixel.y >= size.y) { + return; + } + + vec4 base = texelFetch(source_color, pixel, 0); + + if (params.shaft_params_b.x < 0.5) { + imageStore(color_image, pixel, base); + return; + } + + vec2 uv = (vec2(pixel) + vec2(0.5)) / params.raster_size; + vec2 delta = params.sun_uv - uv; + float dist = length(delta); + + float radial_falloff = smoothstep(params.shaft_params_b.y, 0.0, dist); + if (radial_falloff <= 0.0) { + imageStore(color_image, pixel, base); + return; + } + + if (params.debug_params.x > 0.0) { + base.rgb = mix(base.rgb, params.debug_params.yzw, clamp(params.debug_params.x, 0.0, 1.0)); + } + + int sample_count = max(4, int(params.perf_params.x)); + float dither = interleaved_gradient_noise(vec2(pixel)) * params.perf_params.y; + vec2 step_uv = delta * (params.shaft_params_a.x / float(sample_count)); + vec2 sample_uv = uv + step_uv * dither; + + float illumination_decay = 1.0; + float accumulation = 0.0; + float threshold = params.shaft_params_a.y; + + for (int i = 0; i < sample_count; i++) { + if (sample_uv.x >= 0.0 && sample_uv.x <= 1.0 && sample_uv.y >= 0.0 && sample_uv.y <= 1.0) { + ivec2 sample_pixel = ivec2(sample_uv * params.raster_size); + + // MAGIC FIX: Only sample if depth == 0.0 (Sky in Reverse-Z) + float depth = texelFetch(source_depth, sample_pixel, 0).r; + if (depth <= 0.00001) { + vec3 sample_color = texelFetch(source_color, sample_pixel, 0).rgb; + float lum = luminance(sample_color); + float bright = smoothstep(threshold, threshold + 0.5, lum); + accumulation += bright * illumination_decay * params.shaft_params_a.z; + } + } + + sample_uv += step_uv; + illumination_decay *= params.shaft_params_a.w; + } + + vec3 shafts = params.shaft_color.rgb * accumulation * params.shaft_params_b.z * radial_falloff; + base.rgb += shafts; + imageStore(color_image, pixel, base); +} +""" + +var _sun_screen_uv := Vector2(0.5, 0.5) +var _sun_visible := false +var _shaft_color := Color(1.0, 0.9, 0.72, 1.0) +var _density := 0.92 +var _bright_threshold := 0.7 +var _weight := 0.028 +var _decay := 0.95 +var _exposure := 1.1 +var _max_radius := 0.9 +var _sample_count := 20 +var _dither_strength := 1.0 +var _debug_overlay_strength := 0.0 +var _debug_overlay_color := Color(1.0, 0.0, 1.0, 1.0) + +@export var sun_screen_uv := Vector2(0.5, 0.5): + set(value): + _mutex.lock() + _sun_screen_uv = value + _mutex.unlock() + get: + return _sun_screen_uv + +@export var sun_visible := false: + set(value): + _mutex.lock() + _sun_visible = value + _mutex.unlock() + get: + return _sun_visible + +@export var shaft_color: Color = Color(1.0, 0.9, 0.72, 1.0): + set(value): + _mutex.lock() + _shaft_color = value + _mutex.unlock() + get: + return _shaft_color + +@export_range(0.0, 1.0, 0.001) var density: float = 0.92: + set(value): + _mutex.lock() + _density = clampf(value, 0.0, 1.0) + _mutex.unlock() + get: + return _density + +@export_range(0.0, 10.0, 0.001) var bright_threshold: float = 0.7: + set(value): + _mutex.lock() + _bright_threshold = clampf(value, 0.0, 10.0) + _mutex.unlock() + get: + return _bright_threshold + +@export_range(0.0, 0.2, 0.0005) var weight: float = 0.028: + set(value): + _mutex.lock() + _weight = maxf(value, 0.0) + _mutex.unlock() + get: + return _weight + +@export_range(0.0, 1.0, 0.001) var decay: float = 0.95: + set(value): + _mutex.lock() + _decay = clampf(value, 0.0, 1.0) + _mutex.unlock() + get: + return _decay + +@export_range(0.0, 8.0, 0.01) var exposure: float = 1.1: + set(value): + _mutex.lock() + _exposure = maxf(value, 0.0) + _mutex.unlock() + get: + return _exposure + +@export_range(0.0, 2.0, 0.001) var max_radius: float = 0.9: + set(value): + _mutex.lock() + _max_radius = maxf(value, 0.0) + _mutex.unlock() + get: + return _max_radius + +@export_range(4, 100) var sample_count: int = 20: + set(value): + _mutex.lock() + _sample_count = max(4, value) + _mutex.unlock() + get: + return _sample_count + +@export_range(0.0, 2.0, 0.001) var dither_strength: float = 1.0: + set(value): + _mutex.lock() + _dither_strength = maxf(value, 0.0) + _mutex.unlock() + get: + return _dither_strength + +@export_group("Debug") +@export_range(0.0, 1.0, 0.001) var debug_overlay_strength: float = 0.0: + set(value): + _mutex.lock() + _debug_overlay_strength = clampf(value, 0.0, 1.0) + _mutex.unlock() + get: + return _debug_overlay_strength + +@export var debug_overlay_color: Color = Color(1.0, 0.0, 1.0, 1.0): + set(value): + _mutex.lock() + _debug_overlay_color = value + _mutex.unlock() + get: + return _debug_overlay_color + +var rd: RenderingDevice +var shader: RID +var pipeline: RID +var sampler: RID +var _mutex := Mutex.new() + +func _init() -> void: + effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT + access_resolved_color = true + access_resolved_depth = true + enabled = true + rd = RenderingServer.get_rendering_device() + _initialize_shader() + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + if rd != null: + if sampler.is_valid(): + rd.free_rid(sampler) + if pipeline.is_valid(): + rd.free_rid(pipeline) + if shader.is_valid(): + rd.free_rid(shader) + +func _initialize_shader() -> void: + if rd == null: + return + var shader_file := RDShaderSource.new() + shader_file.language = RenderingDevice.SHADER_LANGUAGE_GLSL + shader_file.source_compute = SHADER_CODE + var shader_spirv := rd.shader_compile_spirv_from_source(shader_file) + if shader_spirv.compile_error_compute != "": + push_error(shader_spirv.compile_error_compute) + return + shader = rd.shader_create_from_spirv(shader_spirv) + if not shader.is_valid(): + return + var sampler_state := RDSamplerState.new() + sampler_state.mag_filter = RenderingDevice.SAMPLER_FILTER_LINEAR + sampler_state.min_filter = RenderingDevice.SAMPLER_FILTER_LINEAR + sampler_state.repeat_u = RenderingDevice.SAMPLER_REPEAT_MODE_CLAMP_TO_EDGE + sampler_state.repeat_v = RenderingDevice.SAMPLER_REPEAT_MODE_CLAMP_TO_EDGE + sampler = rd.sampler_create(sampler_state) + pipeline = rd.compute_pipeline_create(shader) + +func _render_callback(callback_type: int, render_data: RenderData) -> void: + if rd == null or not pipeline.is_valid(): + return + if callback_type != EFFECT_CALLBACK_TYPE_POST_TRANSPARENT: + return + + var render_scene_buffers := render_data.get_render_scene_buffers() as RenderSceneBuffersRD + if render_scene_buffers == null: + return + var size := render_scene_buffers.get_internal_size() + if size.x == 0 or size.y == 0: + return + + var params := _copy_params() + var x_groups := int((size.x - 1) / 8) + 1 + var y_groups := int((size.y - 1) / 8) + 1 + + var view_count := render_scene_buffers.get_view_count() + + for view in range(view_count): + var source_color: RID + if render_scene_buffers.has_texture("render_buffers", "resolved_color"): + source_color = render_scene_buffers.get_texture("render_buffers", "resolved_color") + else: + source_color = render_scene_buffers.get_color_layer(view) + + # GET THE DEPTH LAYER EXACTLY FOR THIS VIEW + var source_depth := render_scene_buffers.get_depth_layer(view) + var color_image := render_scene_buffers.get_color_layer(view) + + if not color_image.is_valid() or not source_color.is_valid() or not source_depth.is_valid(): + continue + + var push_constant := PackedFloat32Array([ + float(size.x), float(size.y), + params["sun_uv"].x, params["sun_uv"].y, + params["color"].r, params["color"].g, params["color"].b, params["color"].a, + params["density"], params["threshold"], params["weight"], params["decay"], + 1.0 if params["visible"] else 0.0, params["max_radius"], params["exposure"], 0.0, + float(params["sample_count"]), params["dither_strength"], 0.0, 0.0, + params["debug_overlay_strength"], params["debug_overlay_color"].r, params["debug_overlay_color"].g, params["debug_overlay_color"].b + ]) + + var source_uniform := RDUniform.new() + source_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_SAMPLER_WITH_TEXTURE + source_uniform.binding = 0 + source_uniform.add_id(sampler) + source_uniform.add_id(source_color) + + var depth_uniform := RDUniform.new() + depth_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_SAMPLER_WITH_TEXTURE + depth_uniform.binding = 1 + depth_uniform.add_id(sampler) + depth_uniform.add_id(source_depth) + + var target_uniform := RDUniform.new() + target_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE + target_uniform.binding = 2 + target_uniform.add_id(color_image) + + var uniform_set := UniformSetCacheRD.get_cache(shader, 0, [source_uniform, depth_uniform, target_uniform]) + var compute_list := rd.compute_list_begin() + rd.compute_list_bind_compute_pipeline(compute_list, pipeline) + rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0) + rd.compute_list_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4) + rd.compute_list_dispatch(compute_list, x_groups, y_groups, 1) + rd.compute_list_end() + +func _copy_params() -> Dictionary: + _mutex.lock() + var params := { + "sun_uv": _sun_screen_uv, + "visible": _sun_visible, + "color": _shaft_color, + "density": _density, + "threshold": _bright_threshold, + "weight": _weight, + "decay": _decay, + "exposure": _exposure, + "max_radius": _max_radius, + "sample_count": _sample_count, + "dither_strength": _dither_strength, + "debug_overlay_strength": _debug_overlay_strength, + "debug_overlay_color": _debug_overlay_color, + } + _mutex.unlock() + return params diff --git a/demo/addons/gnd_skydome/SunShaftsCompositorEffect.gd.uid b/demo/addons/gnd_skydome/SunShaftsCompositorEffect.gd.uid new file mode 100644 index 00000000..ad256604 --- /dev/null +++ b/demo/addons/gnd_skydome/SunShaftsCompositorEffect.gd.uid @@ -0,0 +1 @@ +uid://pw1ljvn4g7cy diff --git a/demo/addons/gnd_skydome/filmic_procedural_sky.gdshader b/demo/addons/gnd_skydome/filmic_procedural_sky.gdshader new file mode 100644 index 00000000..288e3b00 --- /dev/null +++ b/demo/addons/gnd_skydome/filmic_procedural_sky.gdshader @@ -0,0 +1,448 @@ +shader_type sky; + +uniform sampler2D cloud_tex_a : source_color, filter_linear, repeat_enable; +uniform sampler2D cloud_tex_b : source_color, filter_linear, repeat_enable; +uniform sampler2D moon_tex : source_color, filter_linear_mipmap, repeat_disable; + +group_uniforms DaySky; + uniform vec3 lower_sky_color : source_color = vec3(0.66, 0.71, 0.8); + uniform vec3 horizon_color : source_color = vec3(0.82, 0.86, 0.89); + uniform vec3 zenith_color : source_color = vec3(0.12, 0.24, 0.48); + uniform float sky_energy : hint_range(0.0, 10.0) = 1.0; + uniform float horizon_height : hint_range(-0.4, 0.5) = 0.05; + uniform float horizon_softness : hint_range(0.02, 1.0) = 0.24; + uniform float zenith_curve : hint_range(0.1, 8.0) = 1.65; + uniform float horizon_glow_strength : hint_range(0.0, 4.0) = 0.5; + +group_uniforms Atmosphere; + uniform float atmosphere_horizon_level : hint_range(-0.2, 0.2) = -0.035; + uniform float atmosphere_height : hint_range(0.02, 0.6) = 0.24; + uniform float atmosphere_density : hint_range(0.0, 2.0) = 0.46; + uniform float atmosphere_sun_scatter : hint_range(0.0, 2.0) = 0.34; + uniform float atmosphere_sunset_boost : hint_range(0.0, 3.0) = 1.35; + +group_uniforms Rainbow; + uniform float rainbow_intensity : hint_range(0.0, 2.0) = 0.0; + uniform float rainbow_secondary_intensity : hint_range(0.0, 2.0) = 0.35; + +group_uniforms Sunset; + uniform vec3 sunset_bottom_color : source_color = vec3(1.0, 0.5, 0.2); + uniform vec3 sunset_horizon_color : source_color = vec3(0.8, 0.2, 0.05); + uniform vec3 sunset_zenith_color : source_color = vec3(0.4, 0.3, 0.5); + uniform vec3 sunset_cloud_color : source_color = vec3(1.0, 0.4, 0.15); + uniform vec3 sunset_sun_color : source_color = vec3(1.0, 0.4, 0.1); + +group_uniforms NightSky; + uniform vec3 night_lower_sky_color : source_color = vec3(0.03, 0.05, 0.09); + uniform vec3 night_horizon_color : source_color = vec3(0.06, 0.1, 0.18); + uniform vec3 night_zenith_color : source_color = vec3(0.01, 0.015, 0.03); + uniform float night_sky_energy : hint_range(0.0, 10.0) = 1.0; + uniform vec3 stars_color : source_color = vec3(1.0, 1.0, 1.0); + uniform float stars_energy : hint_range(0.0, 50.0) = 2.0; + uniform float stars_size_min : hint_range(0.005, 0.25) = 0.05; + uniform float stars_size_max : hint_range(0.005, 0.4) = 0.14; + uniform float stars_edge_softness : hint_range(0.25, 4.0) = 1.5; + +group_uniforms GI; + uniform vec3 gi_tint : source_color = vec3(1.0, 1.0, 1.0); + uniform float gi_energy_multiplier : hint_range(0.0, 10.0) = 1.0; + +group_uniforms Sun; + uniform vec3 sun_color : source_color = vec3(1.0, 0.95, 0.8); + uniform float sun_disk_size : hint_range(0.0, 2.0) = 1.0; + uniform float sun_disk_softness : hint_range(0.0, 1.0) = 0.5; + uniform float sun_disk_strength : hint_range(0.0, 20.0) = 5.0; + uniform float sun_seasonal_size_variation : hint_range(0.0, 10.0) = 1.0; + uniform float sun_halo_size : hint_range(0.0, 2.0) = 1.0; + uniform float sun_halo_strength : hint_range(0.0, 10.0) = 1.5; + uniform float sun_atmosphere_size : hint_range(0.0, 4.0) = 1.0; + uniform float sun_atmosphere_strength : hint_range(0.0, 10.0) = 1.0; + uniform float sun_energy_scale : hint_range(0.0, 10.0) = 1.0; + +group_uniforms Moon; + uniform vec3 moon_color : source_color = vec3(0.9, 0.95, 1.0); + uniform float moon_size : hint_range(0.0, 2.0) = 1.0; + uniform float moon_glow_strength : hint_range(0.0, 10.0) = 1.0; + uniform float moon_eclipse_size : hint_range(0.0, 4.0) = 2.5; + uniform float moon_glow_size : hint_range(0.1, 4.0) = 1.0; + +uniform bool high_quality_sky = true; + +group_uniforms Clouds; + uniform vec2 cloud_scroll_a = vec2(0.0012, 0.00015); + uniform vec2 cloud_scroll_b = vec2(-0.0018, 0.0004); + uniform vec2 cloud_scale_a = vec2(0.045, 0.055); + uniform vec2 cloud_scale_b = vec2(0.082, 0.125); + uniform float cloud_plane_height : hint_range(0.02, 4.0) = 0.25; + uniform float cloud_plane_curve : hint_range(0.0, 1.0) = 0.15; + uniform float cloud_warp_strength : hint_range(0.0, 1.0) = 0.14; + uniform float cloud_coverage : hint_range(0.0, 1.0) = 0.5; + uniform float cloud_softness : hint_range(0.01, 1.0) = 0.2; + uniform float cloud_opacity : hint_range(0.0, 1.0) = 0.6; + uniform float cloud_horizon_fade : hint_range(0.0, 1.0) = 0.3; + uniform float cloud_top_fade : hint_range(0.0, 1.0) = 0.2; + uniform vec3 cloud_light_color : source_color = vec3(1.0, 0.98, 0.95); + uniform vec3 cloud_shadow_color : source_color = vec3(0.4, 0.45, 0.55); + uniform float cloud_forward_scatter : hint_range(0.0, 4.0) = 1.5; + uniform float cloud_backscatter : hint_range(0.0, 1.0) = 0.2; + uniform float sun_cloud_occlusion : hint_range(0.0, 1.0) = 0.8; + +uniform vec3 custom_sun_dir = vec3(0.0, 1.0, 0.0); +uniform vec3 custom_moon_dir = vec3(0.0, -1.0, 0.0); +uniform mat3 celestial_matrix = mat3(1.0); +uniform float rendered_day_of_year = 180.0; +uniform float rendered_time_of_day = 12.0; +uniform float observer_latitude_deg = 21.0; +uniform float cloud_time = 0.0; +uniform vec2 cloud_wind_direction = vec2(0.8, 0.3); +uniform float cloud_wind_speed = 1.0; +uniform float cloud_motion_time = 0.0; +uniform float cloud_motion_scale = 0.12; +uniform float cloud_evolution_time = 0.0; +uniform float cloud_evolution_strength : hint_range(0.0, 1.0) = 0.18; +uniform float cloud_evolution_scale : hint_range(0.001, 0.25) = 0.018; + +const float ASTRONOMICAL_UNIT_KM = 149597870.7; +const float SOLAR_RADIUS_KM = 695700.0; +const float EARTH_EQUATORIAL_RADIUS_KM = 6378.137; +const float EARTH_POLAR_RADIUS_KM = 6356.752314245; +const float EARTH_ORBITAL_ECCENTRICITY = 0.0167086; +const float EARTH_SIDEREAL_YEAR_DAYS = 365.256363004; +const float EARTH_PERIHELION_DAY = 3.0; + +vec2 dir_to_cloud_plane_uv(vec3 dir, float plane_height, float curve) { + float denom = max(dir.y + plane_height, 0.05); + vec2 uv = dir.xz / denom; + float dist = length(uv); + uv *= 1.0 + dist * curve; + return uv; +} + +float saturate(float value) { return clamp(value, 0.0, 1.0); } + +float hash(vec3 p) { + p = fract(p * vec3(123.34, 456.21, 876.45)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float get_observer_radius_km(float latitude_rad) { + float cos_lat = cos(latitude_rad); + float sin_lat = sin(latitude_rad); + float equatorial_num = EARTH_EQUATORIAL_RADIUS_KM * EARTH_EQUATORIAL_RADIUS_KM * cos_lat; + float polar_num = EARTH_POLAR_RADIUS_KM * EARTH_POLAR_RADIUS_KM * sin_lat; + float equatorial_den = EARTH_EQUATORIAL_RADIUS_KM * cos_lat; + float polar_den = EARTH_POLAR_RADIUS_KM * sin_lat; + return sqrt( + (equatorial_num * equatorial_num + polar_num * polar_num) / + (equatorial_den * equatorial_den + polar_den * polar_den) + ); +} + +float get_solar_disk_scale(float sun_alt) { + float day_current = rendered_day_of_year + rendered_time_of_day / 24.0; + float mean_anomaly = TAU * mod((day_current - EARTH_PERIHELION_DAY) / EARTH_SIDEREAL_YEAR_DAYS, 1.0); + + // Seasonal size variation (Physics = 0.0167 amplitude). We scale the amplitude aggressively with the parameter. + // When variation is 1.0, it behaves like physics (~3.3% change). + // When it is 10.0, the amplitude becomes 0.167 (meaning sun is ~33% larger in winter!) + float dist_factor = 1.0 + (EARTH_ORBITAL_ECCENTRICITY * sun_seasonal_size_variation) * cos(mean_anomaly); + + // Cinematic "Moon Illusion" magnification. + // Real sun doesn't grow, but perceived size increases at horizon. + // We use a smooth curve that doesn't feel like a "squeezed ellipsoid" transition. + float sun_alt_clamped = saturate(sun_alt); + float atmospheric_magnification = 0.45; // Max 45% larger + float magnification_curve = pow(1.0 - sun_alt_clamped, 2.5); + float atmospheric_scale = 1.0 + atmospheric_magnification * magnification_curve; + + return dist_factor * atmospheric_scale; +} + +// FAST RAINBOW APPROXIMATION +vec3 get_fast_rainbow_color(float angle_deg, float center_deg, float width_deg) { + float t = (angle_deg - center_deg) / width_deg; + if (abs(t) > 1.0) return vec3(0.0); + + // Spectral color approximation + vec3 c = clamp(vec3( + smoothstep(0.0, 0.4, t) * (1.0 - smoothstep(0.7, 1.0, t)), // Red/Orange + smoothstep(0.3, 0.6, t) * (1.0 - smoothstep(0.8, 0.9, t)), // Green + smoothstep(0.6, 0.9, t) // Blue/Violet + ), 0.0, 1.0); + + // Shape of the lobe + float envelope = exp(-4.0 * t * t); + return c * envelope; +} + +void sky() { + vec3 dir = normalize(EYEDIR); + + float sun_alt = custom_sun_dir.y; + + // ALIGNED WITH GDSCRIPT FOR BETTER DAWN/DUSK + float day_blend = smoothstep(-0.2, 0.3, sun_alt); + float sunset_blend = smoothstep(-0.2, 0.05, sun_alt) * (1.0 - smoothstep(0.1, 0.4, sun_alt)); + + float horizon_t = smoothstep(horizon_height - horizon_softness, horizon_height + horizon_softness, dir.y); + float zenith_t = pow(saturate(max(dir.y, 0.0)), zenith_curve); + + // Day + vec3 day_base = mix(lower_sky_color, horizon_color, horizon_t); + day_base = mix(day_base, zenith_color, zenith_t); + float day_glow = pow(1.0 - saturate(abs(dir.y - horizon_height) * 2.5), 4.0); + day_base += horizon_color * day_glow * horizon_glow_strength; + + // Sunset + vec3 sunset_base = mix(sunset_bottom_color, sunset_horizon_color, horizon_t); + sunset_base = mix(sunset_base, sunset_zenith_color, zenith_t); + float sunset_glow = pow(1.0 - saturate(abs(dir.y - horizon_height) * 2.5), 4.0); + sunset_base += sunset_horizon_color * sunset_glow * horizon_glow_strength * 1.5; + + // Mix Day and Sunset + day_base = mix(day_base, sunset_base, sunset_blend); + day_base *= sky_energy; + + // Night + vec3 night_base = mix(night_lower_sky_color, night_horizon_color, horizon_t); + night_base = mix(night_base, night_zenith_color, zenith_t); + float night_glow = pow(1.0 - saturate(abs(dir.y - horizon_height) * 2.5), 4.0); + night_base += night_horizon_color * night_glow * horizon_glow_strength; + night_base *= night_sky_energy; + + vec3 final_color = mix(night_base, day_base, day_blend); + float sun_dot = dot(dir, custom_sun_dir); + + if (!high_quality_sky) { + float sun_disk = smoothstep(0.998, 1.0, sun_dot); + final_color += sun_color * sun_disk * 5.0; + } else { + + vec3 active_sun_color = mix(sun_color, sunset_sun_color, sunset_blend); + + vec3 celestial_dir = celestial_matrix * dir; + if (day_blend < 0.99 && stars_energy > 0.001) { + vec3 star_p = celestial_dir * 250.0; + vec3 star_i = floor(star_p); + vec3 star_f = fract(star_p); + float star_h = fract(sin(dot(star_i, vec3(12.9898, 78.233, 45.164))) * 43758.5453); + vec3 star_pos = vec3(fract(star_h * 34.34), fract(star_h * 12.34), fract(star_h * 45.34)); + float star_d = length(star_f - star_pos); + float star_size = mix(stars_size_min, stars_size_max, fract(star_h * 11.11)); + float star_aa = max(fwidth(star_d) * stars_edge_softness, 0.003); + float star_core = 1.0 - smoothstep(star_size - star_aa, star_size + star_aa, star_d); + float star_intensity = star_core * smoothstep(0.95, 0.96, star_h); + float twinkle = mix(0.3, 1.0, sin(cloud_time * 3.0 + star_h * 100.0) * 0.5 + 0.5); + float sun_star_occlusion = smoothstep(0.92, 0.9985, sun_dot); + float stars = star_intensity * twinkle * 8.0 * pow(1.0 - zenith_t, 2.0) * (1.0 - sun_star_occlusion); + final_color += stars_color * stars * stars_energy * (1.0 - day_blend); + } + + vec2 cloud_uv = dir_to_cloud_plane_uv(dir, cloud_plane_height, cloud_plane_curve); + float cloud_mask = 0.0; + float cloud_shape = 0.0; + float evolution_a = 0.0; + float evolution_b = 0.0; + + if (cloud_opacity > 0.001) { + vec2 wind_dir = normalize(max(abs(cloud_wind_direction.x) + abs(cloud_wind_direction.y), 0.0001) * cloud_wind_direction); + float motion_time = cloud_motion_time; + vec2 scroll_a = wind_dir * length(cloud_scroll_a); + vec2 scroll_b = wind_dir * length(cloud_scroll_b) * 1.35; + + if (high_quality_sky) { + vec2 warp = texture(cloud_tex_b, cloud_uv * 0.01).rg * 2.0 - 1.0; + cloud_uv += warp * cloud_warp_strength; + + vec2 evolution_uv = cloud_uv * cloud_evolution_scale; + vec2 evolution_flow_a = vec2(0.011, -0.007) * cloud_evolution_time; + vec2 evolution_flow_b = vec2(-0.009, 0.013) * cloud_evolution_time; + evolution_a = texture(cloud_tex_a, evolution_uv + evolution_flow_a).r; + evolution_b = texture(cloud_tex_b, evolution_uv * 1.83 + evolution_flow_b).r; + vec2 evolution_warp = (vec2(evolution_a, evolution_b) - 0.5) * cloud_evolution_strength; + cloud_uv += evolution_warp; + } + + float cloud_a = texture(cloud_tex_a, cloud_uv * cloud_scale_a + scroll_a * motion_time).r; + float cloud_b = texture(cloud_tex_b, cloud_uv * cloud_scale_b + scroll_b * motion_time).r; + + if (high_quality_sky) { + float breakup = texture(cloud_tex_b, cloud_uv * 0.04).r; + cloud_shape = mix(cloud_a, cloud_b, 0.5); + cloud_shape = mix(cloud_shape, cloud_shape * breakup, 0.4); + } else { + cloud_shape = mix(cloud_a, cloud_b, 0.5); + } + + float evolution_signed = 0.0; + if (high_quality_sky) { + evolution_signed = (mix(evolution_a, evolution_b, 0.45) * 2.0 - 1.0) * cloud_evolution_strength; + } + float evolved_coverage = clamp(cloud_coverage + evolution_signed * (cloud_softness * 1.4 + 0.08), 0.0, 1.0); + + if (evolved_coverage >= 0.9999) { + cloud_mask = 1.0; + } else if (evolved_coverage > 0.0001) { + float cloud_threshold = 1.0 - evolved_coverage; + cloud_mask = smoothstep(cloud_threshold - cloud_softness, cloud_threshold + cloud_softness, cloud_shape); + } + + float horizon_mask = smoothstep(-0.1, cloud_horizon_fade, dir.y); + float top_mask = 1.0 - smoothstep(1.0 - cloud_top_fade, 1.0, max(dir.y, 0.0)); + cloud_mask *= horizon_mask * top_mask * cloud_opacity; + } + + float sky_visibility = 1.0 - (cloud_mask * sun_cloud_occlusion); + float horizon_air = 1.0 - smoothstep(atmosphere_horizon_level, atmosphere_horizon_level + atmosphere_height, dir.y); + horizon_air = pow(saturate(horizon_air), 1.35); + + float atmosphere_light_blend = smoothstep(-0.08, 0.12, sun_alt); + float low_sun = smoothstep(-0.08, -0.005, sun_alt) * (1.0 - smoothstep(0.06, 0.30, sun_alt)); + float azimuth_alignment = 0.0; + float dir_xz_len = length(dir.xz); + float sun_xz_len = length(custom_sun_dir.xz); + if (dir_xz_len > 0.0001 && sun_xz_len > 0.0001) { + azimuth_alignment = saturate(dot(dir.xz / dir_xz_len, custom_sun_dir.xz / sun_xz_len)); + } + + vec3 haze_day_color = mix(horizon_color, sunset_horizon_color, sunset_blend * 0.9); + vec3 haze_color = mix(night_horizon_color, haze_day_color, atmosphere_light_blend); + float haze_mix = saturate(horizon_air * atmosphere_density * mix(0.04, 0.78, atmosphere_light_blend) * mix(1.0, atmosphere_sunset_boost, sunset_blend)); + final_color = mix(final_color, haze_color, haze_mix * sky_visibility); + final_color += active_sun_color * pow(azimuth_alignment, 6.0) * horizon_air * low_sun * atmosphere_sun_scatter * sky_visibility; + + vec3 antisolar_dir = -custom_sun_dir; + float scattering_angle_deg = degrees(acos(clamp(dot(dir, antisolar_dir), -1.0, 1.0))); + float sun_altitude_deg = degrees(asin(clamp(sun_alt, -1.0, 1.0))); + float rainbow_day_visibility = day_blend * smoothstep(0.0, 2.0, sun_altitude_deg); + float rainbow_altitude_visibility = 1.0 - smoothstep(38.0, 42.0, sun_altitude_deg); + float rainbow_horizon_visibility = smoothstep(-0.02, 0.08, dir.y); + float rainbow_sky_visibility = smoothstep(0.15, 0.95, sky_visibility); + float rainbow_visibility = rainbow_intensity * rainbow_day_visibility * rainbow_altitude_visibility * rainbow_horizon_visibility * rainbow_sky_visibility; + if (rainbow_visibility > 0.001) { + vec3 primary_rainbow = get_fast_rainbow_color(scattering_angle_deg, 42.0, 1.5); + vec3 secondary_rainbow = get_fast_rainbow_color(scattering_angle_deg, 51.0, 3.0); + float alexander_band = smoothstep(43.0, 46.5, scattering_angle_deg) * (1.0 - smoothstep(49.5, 52.5, scattering_angle_deg)); + final_color *= 1.0 - alexander_band * rainbow_visibility * 0.05; + final_color += (primary_rainbow + secondary_rainbow * rainbow_secondary_intensity) * rainbow_visibility * 2.5; + } + + // Sun + vec3 sun_dir_norm = custom_sun_dir; + vec3 sun_right = normalize(cross(vec3(0.0, 1.0, 0.0), sun_dir_norm)); + if (length(sun_right) < 0.001) sun_right = vec3(1.0, 0.0, 0.0); + vec3 sun_up = cross(sun_dir_norm, sun_right); + + float sun_x = dot(dir, sun_right); + float sun_y = dot(dir, sun_up); + + // Vertical flattening (Atmospheric Refraction) + float sun_flattening = 1.0 + 0.3 * pow(1.0 - saturate(sun_alt * 4.0 + 0.1), 3.0); + float sun_r2 = sun_x * sun_x + sun_y * sun_y * (sun_flattening * sun_flattening); + float flattened_sun_dot = sqrt(max(0.0, 1.0 - sun_r2)); + if (sun_dot < 0.0) flattened_sun_dot = -flattened_sun_dot; // Prevent appearing on the opposite hemisphere + + // Dynamic atmospheric blur at the horizon + float horizon_blur_factor = pow(1.0 - saturate(sun_alt * 5.0 + 0.1), 2.5); + + float disk_radius = 0.015 * sun_disk_size * get_solar_disk_scale(sun_alt); + // Increase softness massively at the horizon (up to 8x softer) + float disk_soft = disk_radius * sun_disk_softness * (1.0 + horizon_blur_factor * 8.0); + float disk = smoothstep(1.0 - disk_radius - disk_soft, 1.0 - disk_radius, flattened_sun_dot); + + float sun_glow_visibility = smoothstep(-0.10, 0.04, sun_alt); + sun_glow_visibility = pow(sun_glow_visibility, 0.6); + + // Also expand the halo slightly at the horizon to merge with the blurred disk + float active_halo_size = sun_halo_size * (1.0 + horizon_blur_factor * 1.5); + float halo = pow(saturate(sun_dot), 1.0 / (0.08 * active_halo_size)) * sun_halo_strength * sun_glow_visibility; + float atmosphere = pow(saturate(sun_dot), 1.0 / (0.35 * sun_atmosphere_size)) * sun_atmosphere_strength * sun_glow_visibility; + + vec3 sun_body = active_sun_color * sun_energy_scale; + + // Sun extinction at horizon (solid drawing) + float sun_extinction = mix(1.0, 0.2, pow(1.0 - saturate(sun_alt * 5.0 + 0.1), 2.0)); + final_color += sun_body * disk * sky_visibility * sun_disk_strength * sun_extinction; + final_color += sun_body * halo * sky_visibility; + final_color += sun_body * atmosphere * sky_visibility; + + // Moon + float moon_dot = dot(dir, custom_moon_dir); + float m_radius = 0.015 * moon_size; + vec3 final_moon_color = vec3(0.0); + float moon_alpha = 0.0; + + if (moon_dot > 1.0 - m_radius * 2.0) { + vec3 up = abs(custom_moon_dir.y) < 0.999 ? vec3(0,1,0) : vec3(1,0,0); + vec3 right = normalize(cross(up, custom_moon_dir)); + up = cross(custom_moon_dir, right); + + float x = dot(dir, right) / m_radius; + float y = dot(dir, up) / m_radius; + float r2 = x*x + y*y; + + if (r2 < 1.0) { + float moon_mask_val = smoothstep(1.0, 0.9, r2); + float z = -sqrt(1.0 - r2); + vec3 N = normalize(x * right + y * up + z * custom_moon_dir); + + float N_dot_L = dot(N, custom_sun_dir); + float phase_light = smoothstep(-0.1, 0.1, N_dot_L); + + float earth_shadow_radius = m_radius * moon_eclipse_size; + float umbra_dist = length(dir + custom_sun_dir); + float umbra = smoothstep(earth_shadow_radius * 0.8, earth_shadow_radius, umbra_dist); + + float light_intensity = phase_light * mix(0.05, 1.0, umbra); + + vec2 moon_uv = vec2(x, y) * 0.5 + 0.5; + vec3 tex_color = texture(moon_tex, moon_uv).rgb; + float tex_val = mix(1.0, tex_color.r, step(0.01, tex_color.r + tex_color.g + tex_color.b)); + + vec3 earthshine_color = moon_color * 0.1; + vec3 bright_color = moon_color * 5.0; + + //final_moon_color = mix(earthshine_color, bright_color, light_intensity) * tex_val * 10.0; + final_moon_color = mix(earthshine_color, bright_color, light_intensity) * tex_val; + + // W noc oswietlony fragment 100%, nieoswietlony 25% + // W dzien oswietlony fragment ok. 60-80% (leciutko przebija niebo), nieoswietlony 0% (niewidoczny calkiem) + float lit_alpha = clamp(1.0 - day_blend * 0.3, 0.0, 1.0); + float unlit_alpha = 0.25 * (1.0 - day_blend); + moon_alpha = mix(unlit_alpha, lit_alpha, light_intensity) * moon_mask_val; + } + } + + float moon_illumination = smoothstep(-0.5, 0.5, dot(custom_sun_dir, -custom_moon_dir)); + float glow_intensity = pow(saturate(moon_dot), 1.0 / (0.1 * moon_glow_size)) * max(0.0, moon_glow_strength); + vec3 glow_color = moon_color * glow_intensity * (0.1 + 0.9 * moon_illumination) * (1.0 - day_blend * 0.8); + + final_color = mix(final_color, final_moon_color, moon_alpha * sky_visibility); + //final_color += glow_color * sky_visibility * 10.0; + final_color += glow_color * sky_visibility; + + // Clouds + vec3 active_light_dir = mix(custom_moon_dir, custom_sun_dir, day_blend); + float cloud_dot = dot(dir, active_light_dir); + float silver_lining = pow(saturate(cloud_dot), 12.0) * cloud_forward_scatter; + float backscatter_val = pow(1.0 - saturate(cloud_dot), 2.0) * cloud_backscatter; + float cloud_density_tint = smoothstep(0.45, 1.0, saturate(cloud_coverage)); + vec3 toned_cloud_light_color = mix(vec3(1.0), cloud_light_color, cloud_density_tint); + vec3 toned_cloud_shadow_color = mix(cloud_shadow_color, cloud_shadow_color * vec3(0.8, 0.82, 0.88), cloud_density_tint); + + vec3 cloud_sun_light = mix(sun_color, sunset_cloud_color, sunset_blend) * sun_energy_scale; + vec3 cloud_moon_light = moon_color * 0.1; + vec3 active_light_color = mix(cloud_moon_light, cloud_sun_light, day_blend) * toned_cloud_light_color; + vec3 cloud_col = mix(toned_cloud_shadow_color * mix(0.02, 1.0, day_blend), active_light_color, saturate(0.1 + silver_lining - backscatter_val)); + + final_color = mix(final_color, cloud_col, cloud_mask); + } + + if (AT_CUBEMAP_PASS) { + vec3 gi_base = mix(night_base, day_base, day_blend); + final_color = gi_base * gi_tint * gi_energy_multiplier; + } + + COLOR = final_color; +} diff --git a/demo/addons/gnd_skydome/filmic_procedural_sky.gdshader.uid b/demo/addons/gnd_skydome/filmic_procedural_sky.gdshader.uid new file mode 100644 index 00000000..d4c339e9 --- /dev/null +++ b/demo/addons/gnd_skydome/filmic_procedural_sky.gdshader.uid @@ -0,0 +1 @@ +uid://ck1w2pkpwin11 diff --git a/demo/addons/gnd_skydome/plugin.cfg b/demo/addons/gnd_skydome/plugin.cfg new file mode 100644 index 00000000..a5b19791 --- /dev/null +++ b/demo/addons/gnd_skydome/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Godot Skydome" +description="Reusable skydome with optional wind globals and dynamic sunshafts compositor management." +author="GamesNotDeveloped" +version="0.1" +script="plugin.gd" diff --git a/demo/addons/gnd_skydome/plugin.gd b/demo/addons/gnd_skydome/plugin.gd new file mode 100644 index 00000000..a1ab6cdb --- /dev/null +++ b/demo/addons/gnd_skydome/plugin.gd @@ -0,0 +1,5 @@ +@tool +extends EditorPlugin + +func _enter_tree() -> void: + pass diff --git a/demo/addons/gnd_skydome/plugin.gd.uid b/demo/addons/gnd_skydome/plugin.gd.uid new file mode 100644 index 00000000..d9a89b8d --- /dev/null +++ b/demo/addons/gnd_skydome/plugin.gd.uid @@ -0,0 +1 @@ +uid://coqvcd47pfkqa diff --git a/demo/addons/gnd_weather/PuddleSurface3D.gd b/demo/addons/gnd_weather/PuddleSurface3D.gd new file mode 100644 index 00000000..1e297a0e --- /dev/null +++ b/demo/addons/gnd_weather/PuddleSurface3D.gd @@ -0,0 +1,180 @@ +@tool +extends Node3D +class_name PuddleSurface3D + +const PUDDLE_SURFACE_SHADER := preload("res://addons/gnd_weather/puddle_surface.gdshader") +const PROBE_OFFSETS: Array[Vector2] = [ + Vector2.ZERO, + Vector2(-0.35, -0.35), + Vector2(0.35, -0.35), + Vector2(-0.35, 0.35), + Vector2(0.35, 0.35), +] + +@export var mask_texture: Texture2D +@export var mesh: Mesh: + set(value): + mesh = value + if is_inside_tree(): + _ensure_render_resources() + _sync_render_state() +@export_range(0.0, 0.2, 0.001) var surface_height_offset: float = 0.078 +@export_range(0.0, 500.0, 0.1) var visibility_range_begin: float = 6.0 +@export_range(0.0, 500.0, 0.1) var visibility_range_end: float = 10.0 +@export_range(0.1, 5.0, 0.05) var probe_interval_sec: float = 0.4 +@export_range(0.0, 2.0, 0.01) var probe_height: float = 0.12 +@export_range(0.1, 20.0, 0.05) var rain_smoothing_speed: float = 10.35 +@export_range(0.0, 1.0, 0.01) var rain_ripple_threshold: float = 0.31 +@export_range(0.0, 1.0, 0.01) var specular_wet: float = 0.75 +@export_range(0.0, 0.1, 0.0005) var refraction_strength: float = 0.014 +@export_range(0.0, 1.0, 0.01) var refraction_mix: float = 1.0 +@export_range(0.0, 1.0, 0.01) var surface_roughness: float = 0.04 +@export_range(0.0, 1.0, 0.01) var ripple_roughness_reduction: float = 0.02 +@export_range(0.1, 4.0, 0.01) var ripple_speed: float = 0.67 +@export_range(0.1, 10.0, 0.01) var ripple_scale: float = 0.1 +@export_range(0.0, 5.0, 0.01) var ripple_max_radius: float = 1.0 +@export_range(0.0, 2.0, 0.01) var ripple_intensity: float = 0.9 +@export_range(0.0, 1.0, 0.01) var edge_foam_strength: float = 0.08 +@export_range(0.0, 8.0, 0.01) var normal_strength: float = 1.0 +@export_range(0.0, 1.0, 0.01) var puddle_alpha_cutoff: float = 0.07 + +var _probe_timer := 0.0 +var _target_rain_strength := 0.0 +var _current_rain_strength := 0.0 + +var _material: ShaderMaterial +var _instance_rid: RID + + +func _ready() -> void: + set_notify_transform(true) + _ensure_render_resources() + _sync_render_state() + _probe_timer = 0.0 + set_process(true) + + +func _exit_tree() -> void: + if _instance_rid.is_valid(): + RenderingServer.free_rid(_instance_rid) + _instance_rid = RID() + + +func _process(delta: float) -> void: + _probe_timer -= delta + if _probe_timer <= 0.0: + _probe_timer = maxf(probe_interval_sec, 0.1) + _sample_local_rain() + + _current_rain_strength = move_toward( + _current_rain_strength, + _target_rain_strength, + maxf(rain_smoothing_speed, 0.1) * delta + ) + _sync_render_state() + + +func _notification(what: int) -> void: + if what == NOTIFICATION_TRANSFORM_CHANGED: + _sync_render_state() + elif what == NOTIFICATION_ENTER_WORLD: + _sync_render_state() + + +func _ensure_render_resources() -> void: + if _material == null: + _material = ShaderMaterial.new() + _material.shader = PUDDLE_SURFACE_SHADER + + if not _instance_rid.is_valid(): + _instance_rid = RenderingServer.instance_create() + RenderingServer.instance_geometry_set_material_override(_instance_rid, _material.get_rid()) + + var render_mesh := _get_render_mesh() + if render_mesh != null: + RenderingServer.instance_set_base(_instance_rid, render_mesh.get_rid()) + else: + RenderingServer.instance_set_base(_instance_rid, RID()) + + +func _sync_render_state() -> void: + if _material == null or not _instance_rid.is_valid(): + return + + var render_mesh := _get_render_mesh() + if render_mesh == null: + RenderingServer.instance_set_base(_instance_rid, RID()) + return + + RenderingServer.instance_set_base(_instance_rid, render_mesh.get_rid()) + + var world_3d := get_world_3d() + if world_3d != null: + RenderingServer.instance_set_scenario(_instance_rid, world_3d.scenario) + + var render_transform := global_transform.translated_local(Vector3(0.0, surface_height_offset, 0.0)) + RenderingServer.instance_set_transform(_instance_rid, render_transform) + + _material.set_shader_parameter("mask_texture", mask_texture) + _material.set_shader_parameter("rain_strength", _current_rain_strength) + _material.set_shader_parameter("rain_ripple_threshold", rain_ripple_threshold) + _material.set_shader_parameter("puddle_alpha_cutoff", puddle_alpha_cutoff) + _material.set_shader_parameter("visibility_range_begin", visibility_range_begin) + _material.set_shader_parameter("visibility_range_end", maxf(visibility_range_end, visibility_range_begin + 0.001)) + _material.set_shader_parameter("refraction_strength", refraction_strength) + _material.set_shader_parameter("refraction_mix", refraction_mix) + _material.set_shader_parameter("surface_roughness", clampf(surface_roughness, 0.01, 0.18)) + _material.set_shader_parameter("ripple_roughness_reduction", ripple_roughness_reduction) + _material.set_shader_parameter("specular_strength", specular_wet) + _material.set_shader_parameter("ripple_intensity", ripple_intensity) + _material.set_shader_parameter("ripple_scale", ripple_scale) + _material.set_shader_parameter("ripple_speed", ripple_speed) + _material.set_shader_parameter("ripple_max_radius", ripple_max_radius) + _material.set_shader_parameter("edge_foam_strength", edge_foam_strength) + _material.set_shader_parameter("normal_strength", normal_strength) + + +func _sample_local_rain() -> void: + if mesh == null: + _target_rain_strength = 0.0 + return + var world_3d := get_world_3d() + if world_3d == null: + _target_rain_strength = 0.0 + return + + var weather_state := WeatherServer.get_weather_state(world_3d) + var base_strength: float = clampf(float(weather_state.get("global_precipitation", 0.0)), 0.0, 1.0) + var total := 0.0 + for probe_offset in PROBE_OFFSETS: + total += WeatherServer.get_rain_participation_strength( + world_3d, + _get_probe_world_position(probe_offset), + base_strength + ) + _target_rain_strength = clampf(total / float(PROBE_OFFSETS.size()), 0.0, 1.0) + + +func _get_probe_world_position(offset: Vector2) -> Vector3: + var basis := global_transform.basis.orthonormalized() + var footprint := _get_probe_footprint_size() + var half_width := footprint.x * 0.5 + var half_depth := footprint.y * 0.5 + var world_position := global_transform.origin + world_position += basis.x * (offset.x * half_width) + world_position += basis.z * (offset.y * half_depth) + world_position.y += probe_height + surface_height_offset + return world_position + + +func _get_render_mesh() -> Mesh: + return mesh + + +func _get_probe_footprint_size() -> Vector2: + if mesh == null: + return Vector2(0.1, 0.1) + var aabb := mesh.get_aabb() + var width := maxf(absf(aabb.size.x), 0.1) + var depth := maxf(absf(aabb.size.z), 0.1) + return Vector2(width, depth) diff --git a/demo/addons/gnd_weather/PuddleSurface3D.gd.uid b/demo/addons/gnd_weather/PuddleSurface3D.gd.uid new file mode 100644 index 00000000..59438ce2 --- /dev/null +++ b/demo/addons/gnd_weather/PuddleSurface3D.gd.uid @@ -0,0 +1 @@ +uid://n6h2ydalytac diff --git a/demo/addons/gnd_weather/RainVolume.gd b/demo/addons/gnd_weather/RainVolume.gd new file mode 100644 index 00000000..645eb5f1 --- /dev/null +++ b/demo/addons/gnd_weather/RainVolume.gd @@ -0,0 +1,193 @@ +@tool +class_name RainVolume +extends VisualInstance3D + +enum VolumeShape { + BOX, +} + +@export_group("Rain") +@export var volume_enabled: bool = true: + set(value): + if volume_enabled == value: + return + volume_enabled = value + _notify_weather_server_changed() +@export var volume_priority: int = 0: + set(value): + if volume_priority == value: + return + volume_priority = value + _notify_weather_server_changed() +@export_range(-10.0, 10.0, 0.01) var precipitation_delta: float = -1.0: + set(value): + var next_value := clampf(value, -10.0, 10.0) + if is_equal_approx(precipitation_delta, next_value): + return + precipitation_delta = next_value + _notify_weather_server_changed() +@export_range(0.0, 2.0, 0.01) var precipitation_multiplier: float = 1.0: + set(value): + var next_value := maxf(value, 0.0) + if is_equal_approx(precipitation_multiplier, next_value): + return + precipitation_multiplier = next_value + _notify_weather_server_changed() +@export_range(0.0, 4.0, 0.01) var lightning_multiplier: float = 1.0: + set(value): + var next_value := maxf(value, 0.0) + if is_equal_approx(lightning_multiplier, next_value): + return + lightning_multiplier = next_value + _notify_weather_server_changed() + +@export_group("Volume") +@export var shape: VolumeShape = VolumeShape.BOX: + set(value): + if shape == value: + return + shape = value + _notify_weather_server_changed() +@export var size: Vector3 = Vector3(4.0, 2.0, 4.0): + set(value): + var next_size := Vector3(absf(value.x), absf(value.y), absf(value.z)) + if size.is_equal_approx(next_size): + return + size = next_size + _refresh_cached_volume_shape() + _notify_weather_server_changed() +@export_range(0.0, 8.0, 0.01) var edge_feather: float = 0.6: + set(value): + var next_value := maxf(value, 0.0) + if is_equal_approx(edge_feather, next_value): + return + edge_feather = next_value + _refresh_cached_volume_shape() + _notify_weather_server_changed() + +var _registered_world: World3D +var _registered_volume_rid: RID +var _cached_half_size: Vector3 = Vector3(2.0, 1.0, 2.0) +var _cached_outer_half_size: Vector3 = Vector3(2.6, 1.6, 2.6) +var _cached_edge_feather: float = 0.6 + + +func _notification(what: int) -> void: + if what == NOTIFICATION_READY: + set_notify_transform(true) + _refresh_cached_volume_shape() + + if what == NOTIFICATION_ENTER_WORLD: + _register_in_weather_server() + elif what == NOTIFICATION_EXIT_WORLD: + _unregister_from_weather_server() + elif what == NOTIFICATION_TRANSFORM_CHANGED: + _notify_weather_server_changed() + + +func is_rain_volume_enabled() -> bool: + return volume_enabled + + +func get_precipitation_delta() -> float: + return clampf(precipitation_delta, -1.0, 1.0) + + +func get_precipitation_multiplier() -> float: + return maxf(precipitation_multiplier, 0.0) + + +func get_lightning_multiplier() -> float: + return maxf(lightning_multiplier, 0.0) + + +func contains_world_position(world_position: Vector3) -> bool: + if shape != VolumeShape.BOX: + return false + + var local_position := global_transform.affine_inverse() * world_position + return ( + absf(local_position.x) <= _cached_half_size.x + and absf(local_position.y) <= _cached_half_size.y + and absf(local_position.z) <= _cached_half_size.z + ) + + +func get_precipitation_blend(world_position: Vector3) -> float: + if shape != VolumeShape.BOX: + return 0.0 + + var local_position := global_transform.affine_inverse() * world_position + var local_abs := Vector3( + absf(local_position.x), + absf(local_position.y), + absf(local_position.z) + ) + if ( + local_abs.x <= _cached_half_size.x + and local_abs.y <= _cached_half_size.y + and local_abs.z <= _cached_half_size.z + ): + return 1.0 + + if _cached_edge_feather <= 0.0001: + return 0.0 + + var feather_distance := Vector3( + _cached_outer_half_size.x - local_abs.x, + _cached_outer_half_size.y - local_abs.y, + _cached_outer_half_size.z - local_abs.z + ) + var distance_to_outer_edge := minf(feather_distance.x, minf(feather_distance.y, feather_distance.z)) + if distance_to_outer_edge <= 0.0: + return 0.0 + + var t := clampf(distance_to_outer_edge / _cached_edge_feather, 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) + + +func _get_aabb() -> AABB: + var local_size := _get_safe_size() + return AABB(-local_size * 0.5, local_size) + + +func _register_in_weather_server() -> void: + var world_3d := get_world_3d() + var volume_rid := get_instance() + if world_3d == null or not volume_rid.is_valid(): + return + + _registered_world = world_3d + _registered_volume_rid = volume_rid + WeatherServer.add_rain_volume(world_3d, volume_rid, self) + + +func _unregister_from_weather_server() -> void: + if _registered_world == null or not _registered_volume_rid.is_valid(): + return + + WeatherServer.remove_rain_volume(_registered_world, _registered_volume_rid) + _registered_world = null + _registered_volume_rid = RID() + + +func _get_half_size() -> Vector3: + return _get_safe_size() * 0.5 + + +func _get_safe_size() -> Vector3: + return Vector3(maxf(size.x, 0.001), maxf(size.y, 0.001), maxf(size.z, 0.001)) + + +func _refresh_cached_volume_shape() -> void: + _cached_half_size = _get_safe_size() * 0.5 + _cached_edge_feather = maxf(edge_feather, 0.0) + var feather_offset := Vector3.ONE * _cached_edge_feather + _cached_outer_half_size = _cached_half_size + feather_offset + + +func _notify_weather_server_changed() -> void: + var world_3d := _registered_world + if world_3d == null: + return + WeatherServer.mark_rain_volumes_changed(world_3d) diff --git a/demo/addons/gnd_weather/RainVolume.gd.uid b/demo/addons/gnd_weather/RainVolume.gd.uid new file mode 100644 index 00000000..f68fbe63 --- /dev/null +++ b/demo/addons/gnd_weather/RainVolume.gd.uid @@ -0,0 +1 @@ +uid://4xov8ndpybam diff --git a/demo/addons/gnd_weather/RainVolumeGizmoPlugin.gd b/demo/addons/gnd_weather/RainVolumeGizmoPlugin.gd new file mode 100644 index 00000000..093b31b0 --- /dev/null +++ b/demo/addons/gnd_weather/RainVolumeGizmoPlugin.gd @@ -0,0 +1,165 @@ +@tool +class_name RainVolumeGizmoPlugin +extends EditorNode3DGizmoPlugin + +const HANDLE_NAMES := [ + "-X", + "+X", + "-Y", + "+Y", + "-Z", + "+Z", +] + +const HANDLE_AXES := [ + Vector3.RIGHT, + Vector3.RIGHT, + Vector3.UP, + Vector3.UP, + Vector3.BACK, + Vector3.BACK, +] + +const HANDLE_SIGNS := [-1.0, 1.0, -1.0, 1.0, -1.0, 1.0] + +const LINE_MATERIAL_NAME := "rain_volume_lines" +const HANDLE_MATERIAL_NAME := "rain_volume_handles" + +var undo_redo: EditorUndoRedoManager + + +func _init() -> void: + create_material(LINE_MATERIAL_NAME, Color(0.42, 0.76, 1.0, 0.85), false, true) + create_handle_material(HANDLE_MATERIAL_NAME, false) + + +func _get_gizmo_name() -> String: + return "RainVolume" + + +func _has_gizmo(for_node_3d: Node3D) -> bool: + return for_node_3d is RainVolume + + +func _redraw(gizmo: EditorNode3DGizmo) -> void: + gizmo.clear() + + var rain_volume := gizmo.get_node_3d() as RainVolume + if rain_volume == null: + return + + var half_size := rain_volume.size * 0.5 + var corners := [ + Vector3(-half_size.x, -half_size.y, -half_size.z), + Vector3(half_size.x, -half_size.y, -half_size.z), + Vector3(half_size.x, half_size.y, -half_size.z), + Vector3(-half_size.x, half_size.y, -half_size.z), + Vector3(-half_size.x, -half_size.y, half_size.z), + Vector3(half_size.x, -half_size.y, half_size.z), + Vector3(half_size.x, half_size.y, half_size.z), + Vector3(-half_size.x, half_size.y, half_size.z), + ] + var edge_pairs := [ + [0, 1], [1, 2], [2, 3], [3, 0], + [4, 5], [5, 6], [6, 7], [7, 4], + [0, 4], [1, 5], [2, 6], [3, 7], + ] + + var lines := PackedVector3Array() + for pair in edge_pairs: + lines.append(corners[pair[0]]) + lines.append(corners[pair[1]]) + + var handles := PackedVector3Array([ + Vector3(-half_size.x, 0.0, 0.0), + Vector3(half_size.x, 0.0, 0.0), + Vector3(0.0, -half_size.y, 0.0), + Vector3(0.0, half_size.y, 0.0), + Vector3(0.0, 0.0, -half_size.z), + Vector3(0.0, 0.0, half_size.z), + ]) + var handle_ids := PackedInt32Array([0, 1, 2, 3, 4, 5]) + + gizmo.add_lines(lines, get_material(LINE_MATERIAL_NAME, gizmo), false) + gizmo.add_collision_segments(lines) + gizmo.add_handles(handles, get_material(HANDLE_MATERIAL_NAME, gizmo), handle_ids, false) + + +func _get_handle_name(gizmo: EditorNode3DGizmo, handle_id: int, _secondary: bool) -> String: + if handle_id < 0 or handle_id >= HANDLE_NAMES.size(): + return "" + return HANDLE_NAMES[handle_id] + + +func _get_handle_value(gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> Variant: + var rain_volume := gizmo.get_node_3d() as RainVolume + if rain_volume == null: + return {} + return { + "size": rain_volume.size, + "position": rain_volume.position, + } + + +func _set_handle(gizmo: EditorNode3DGizmo, handle_id: int, _secondary: bool, camera: Camera3D, screen_pos: Vector2) -> void: + var rain_volume := gizmo.get_node_3d() as RainVolume + if rain_volume == null or handle_id < 0 or handle_id >= HANDLE_AXES.size(): + return + + var axis_local: Vector3 = HANDLE_AXES[handle_id] + var sign: float = HANDLE_SIGNS[handle_id] + var axis_world: Vector3 = (rain_volume.global_transform.basis * axis_local).normalized() + if axis_world.length_squared() <= 0.0001: + return + + var center_world := rain_volume.global_transform.origin + var axis_extent := maxf(rain_volume.size.length() * 4.0, 8.0) + var ray_origin := camera.project_ray_origin(screen_pos) + var ray_direction := camera.project_ray_normal(screen_pos) + var closest_points := Geometry3D.get_closest_points_between_segments( + center_world - axis_world * axis_extent, + center_world + axis_world * axis_extent, + ray_origin, + ray_origin + ray_direction * 4096.0 + ) + if closest_points.size() < 2: + return + + var local_point: Vector3 = rain_volume.global_transform.affine_inverse() * closest_points[0] + var distance_along_axis: float = axis_local.dot(local_point) + var target_half_extent: float = maxf(distance_along_axis * sign, 0.05) + var new_size: Vector3 = rain_volume.size + var axis_index: int = handle_id / 2 + var current_half_extent: float = rain_volume.size[axis_index] * 0.5 + var face_delta: float = target_half_extent - current_half_extent + var min_full_extent := 0.1 + face_delta = maxf(face_delta, min_full_extent - rain_volume.size[axis_index]) + new_size[axis_index] = rain_volume.size[axis_index] + face_delta + + var local_center_offset: Vector3 = axis_local * (sign * face_delta * 0.5) + var new_position: Vector3 = rain_volume.position + rain_volume.transform.basis * local_center_offset + + rain_volume.size = new_size + rain_volume.position = new_position + + +func _commit_handle(gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool, restore: Variant, cancel: bool) -> void: + var rain_volume := gizmo.get_node_3d() as RainVolume + if rain_volume == null or typeof(restore) != TYPE_DICTIONARY: + return + + var restore_size: Vector3 = restore.get("size", rain_volume.size) + var restore_position: Vector3 = restore.get("position", rain_volume.position) + if cancel: + rain_volume.size = restore_size + rain_volume.position = restore_position + return + + if undo_redo == null: + return + undo_redo.create_action("Resize RainVolume") + undo_redo.add_do_property(rain_volume, "size", rain_volume.size) + undo_redo.add_do_property(rain_volume, "position", rain_volume.position) + undo_redo.add_undo_property(rain_volume, "size", restore_size) + undo_redo.add_undo_property(rain_volume, "position", restore_position) + undo_redo.commit_action() diff --git a/demo/addons/gnd_weather/RainVolumeGizmoPlugin.gd.uid b/demo/addons/gnd_weather/RainVolumeGizmoPlugin.gd.uid new file mode 100644 index 00000000..03fd7906 --- /dev/null +++ b/demo/addons/gnd_weather/RainVolumeGizmoPlugin.gd.uid @@ -0,0 +1 @@ +uid://bqnrftwnfny5j diff --git a/demo/addons/gnd_weather/WeatherNode.gd b/demo/addons/gnd_weather/WeatherNode.gd new file mode 100644 index 00000000..ae544574 --- /dev/null +++ b/demo/addons/gnd_weather/WeatherNode.gd @@ -0,0 +1,1270 @@ +@tool +class_name WeatherNode +extends Node3D + +signal thunder(strength: float) +signal rain_strength_changed(strength: float) +signal rain_local_strength_changed(strength: float) +signal wind_changed(speed: float, direction: Vector2) + +const RAIN_STREAK_SHADER := preload("./rain_streak.gdshader") +const NEAR_FIELD_NAME := "RainNear" +const MID_FIELD_NAME := "RainMid" +const RAIN_FIELD_RUNTIME_REFRESH_INTERVAL_MSEC := 250 +const RAIN_FIELD_COUNT_REDUCTION_SPACING_SCALE := 2.0 +const RAIN_FIELD_WIDTH_SCALE := 1.5 +const LIGHTNING_ROLL_INTERVAL_SEC := 0.1 +const GND_WIND_DIRECTION_SETTING := "shader_globals/gnd_wind_direction/value" +const GND_WIND_SPEED_SETTING := "shader_globals/gnd_wind_speed/value" +const GND_WIND_STRENGTH_SETTING := "shader_globals/gnd_wind_strength/value" + +@export_group("Nodes") +@export_node_path("Node") var skydome_path: NodePath +@export_node_path("WorldEnvironment") var world_environment_path: NodePath: + set(value): + world_environment_path = value + if is_inside_tree(): + _refresh_environment_cache() + +@export_group("Weather") +@export_range(0.0, 1.0, 0.001) var precipitation_intensity: float = 0.0: + set(value): + precipitation_intensity = clampf(value, 0.0, 1.0) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.001) var cloud_density: float = 0.0: + set(value): + cloud_density = clampf(value, 0.0, 1.0) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.001) var cloud_overcast_intensity: float = 0.0: + set(value): + cloud_overcast_intensity = clampf(value, 0.0, 1.0) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.001) var storm_intensity: float = 0.0: + set(value): + storm_intensity = clampf(value, 0.0, 1.0) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.001) var storm_fog_intensity: float = 0.0: + set(value): + storm_fog_intensity = clampf(value, 0.0, 1.0) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() + +@export_group("Wind") +@export_range(0.0, 16.0, 0.01) var precipitation_wind_strength: float = 4.0: + set(value): + precipitation_wind_strength = maxf(value, 0.0) + if is_inside_tree(): + _push_weather_server_settings() + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.01) var wind_influence: float = 0.35 + +@export_group("Rain") +@export var follow_height: float = 7.5 +@export var near_emission_extents: Vector3 = Vector3(4.6, 3.0, 4.6) +@export var mid_emission_extents: Vector3 = Vector3(4.2, 2.8, 4.2) +@export_range(0.1, 8.0, 0.01) var rain_mesh_density: float = 1.0: + set(value): + rain_mesh_density = maxf(value, 0.1) + _invalidate_rain_field_state_cache() + _refresh_editor_preview() +@export_range(1.0, 80.0, 0.1) var base_fall_speed: float = 26.0 +@export_range(0.5, 6.0, 0.05) var rain_streak_alpha_curve_exponent: float = 2.4 +@export_range(0.1, 4.0, 0.01) var near_layer_speed_multiplier: float = 1.0 +@export_range(0.1, 4.0, 0.01) var mid_layer_speed_multiplier: float = 0.78 +@export_range(0.0, 1.0, 0.01) var near_rain_blur: float = 0.74: + set(value): + near_rain_blur = clampf(value, 0.0, 1.0) + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.01) var near_rain_glow: float = 0.28: + set(value): + near_rain_glow = clampf(value, 0.0, 1.0) + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.01) var mid_rain_blur: float = 0.22: + set(value): + mid_rain_blur = clampf(value, 0.0, 1.0) + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.01) var mid_rain_glow: float = 0.1: + set(value): + mid_rain_glow = clampf(value, 0.0, 1.0) + _sync_rain_materials() + _refresh_editor_preview() +@export var mid_layer_enabled: bool = true +@export_range(0.0, 1.0, 0.01) var sheltered_volumetric_emission_scale: float = 0.0: + set(value): + sheltered_volumetric_emission_scale = clampf(value, 0.0, 1.0) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export var near_rain_color: Color = Color(0.72, 0.74, 0.76, 0.08): + set(value): + near_rain_color = value + _sync_rain_materials() + _refresh_editor_preview() +@export var mid_rain_color: Color = Color(0.66, 0.68, 0.72, 0.055): + set(value): + mid_rain_color = value + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.01) var visual_intensity = 0.50: + set(value): + visual_intensity = clampf(value, 0.0, 1.0) + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.01) var rain_specular = 0.50: + set(value): + rain_specular = clampf(value, 0.0, 1.0) + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.0, 1.0, 0.01) var rain_roughness = 0.18: + set(value): + rain_roughness = clampf(value, 0.0, 1.0) + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.1, 50.0, 0.1) var rain_specular_fade_start = 2.0: + set(value): + rain_specular_fade_start = value + _sync_rain_materials() + _refresh_editor_preview() +@export_range(0.1, 100.0, 0.1) var rain_specular_fade_end = 8.0: + set(value): + rain_specular_fade_end = value + _sync_rain_materials() + _refresh_editor_preview() + +@export_group("Rain Field") +@export_range(0.1, 4.0, 0.05) var near_field_spacing: float = 0.8 +@export_range(0.1, 4.0, 0.05) var mid_field_spacing: float = 1.35 +@export_range(0.0, 1.0, 0.01) var near_field_jitter: float = 0.38 +@export_range(0.0, 1.0, 0.01) var mid_field_jitter: float = 0.48 + +@export_group("Rain Probes") +@export_range(1, 256, 1) var rain_probe_max_count: int = 24: + set(value): + rain_probe_max_count = maxi(value, 1) + if is_inside_tree(): + _push_rain_probe_config() + _refresh_editor_preview() +@export_range(0.1, 100.0, 0.1) var rain_probe_distance: float = 8.0: + set(value): + rain_probe_distance = maxf(value, 0.1) + if is_inside_tree(): + _push_rain_probe_config() + _refresh_editor_preview() + +@export_group("Storm") +@export var lightning_enabled: bool = true: + set(value): + lightning_enabled = value + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.1, 30.0, 0.1) var lightning_min_interval: float = 3.2: + set(value): + lightning_min_interval = maxf(value, 0.1) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.1, 30.0, 0.1) var lightning_max_interval: float = 9.5: + set(value): + lightning_max_interval = maxf(value, 0.1) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.1, 20.0, 0.01) var lightning_flash_decay: float = 4.8: + set(value): + lightning_flash_decay = maxf(value, 0.1) + if is_inside_tree(): + _push_weather_server_settings() + _apply_weather_state(true) + _refresh_editor_preview() +@export_range(0.1, 20.0, 0.1) var thunder_lock_min_duration: float = 2.4: + set(value): + thunder_lock_min_duration = maxf(value, 0.1) + if thunder_lock_max_duration < thunder_lock_min_duration: + thunder_lock_max_duration = thunder_lock_min_duration +@export_range(0.1, 20.0, 0.1) var thunder_lock_max_duration: float = 4.8: + set(value): + thunder_lock_max_duration = maxf(value, thunder_lock_min_duration) +@export_range(0.1, 8.0, 0.01) var storm_intensity_exponent: float = 2.2: + set(value): + storm_intensity_exponent = maxf(value, 0.1) + if is_inside_tree(): + _apply_weather_state(true) + _refresh_editor_preview() +@export var storm_intensity_curve: Curve: + set(value): + storm_intensity_curve = value + if is_inside_tree(): + _apply_weather_state(true) + _refresh_editor_preview() + +var _near_rain_field: MultiMeshInstance3D +var _mid_rain_field: MultiMeshInstance3D +var _near_rain_debug_material: StandardMaterial3D +var _mid_rain_debug_material: StandardMaterial3D +var _rain_mesh_debug_preview_enabled: bool = false +var _environment: Environment +var _current_global_precipitation: float = 0.0 +var _current_cloud_density: float = 0.0 +var _current_cloud_overcast_intensity_input: float = 0.0 +var _current_storm_intensity_input: float = 0.0 +var _current_storm_fog_intensity_input: float = 0.0 +var _current_local_precipitation: float = 0.0 +var _current_storm_factor: float = 0.0 +var _current_lightning_flash: float = 0.0 +var _current_shelter_factor: float = 0.0 +var _current_local_emission_scale: float = 1.0 +var _lightning_activity: float = 0.0 +var _lightning_roll_timer: float = 0.0 +var _thunder_locked: bool = false +var _thunder_unlock_timer: SceneTreeTimer +var _lightning_rng := RandomNumberGenerator.new() + +var _last_emitted_rain_strength: float = -1.0 +var _last_emitted_local_rain_strength: float = -1.0 +var _last_emitted_wind_speed: float = -1.0 +var _last_emitted_wind_direction: Vector2 = Vector2(INF, INF) +var _editor_preview_camera_id: int = 0 +var _editor_preview_camera_transform: Transform3D = Transform3D.IDENTITY +var _editor_preview_camera_transform_valid: bool = false +var _rain_mesh_debug_near_tint: Color = Color.BLACK +var _rain_mesh_debug_mid_tint: Color = Color.BLACK +var _near_rain_field_state_cache: Dictionary = {} +var _mid_rain_field_state_cache: Dictionary = {} + + +func _notification(what: int) -> void: + if what == NOTIFICATION_ENTER_WORLD: + _refresh_environment_cache() + if Engine.is_editor_hint(): + call_deferred("_refresh_editor_preview") + elif what == NOTIFICATION_POST_ENTER_TREE: + if Engine.is_editor_hint(): + call_deferred("_refresh_editor_preview") + + +func _ready() -> void: + _refresh_environment_cache() + _lightning_rng.randomize() + _push_weather_server_settings() + _push_rain_probe_config() + _ensure_rain_field_nodes() + _apply_weather_state(true) + set_process(true) + + +func _exit_tree() -> void: + _invalidate_rain_field_state_cache() + _clear_thunder_lock() + WeatherServer.clear_weather_observer_sample(get_world_3d()) + WeatherServer.clear_weather_state(get_world_3d()) + WeatherServer.clear_visible_rain_participation_cache(get_world_3d(), get_instance_id()) + WeatherServer.clear_visible_rain_probe_field_config(get_world_3d(), get_instance_id()) + WeatherServer.clear_rain_render_field_cache(get_world_3d(), get_instance_id()) + var skydome := _get_skydome() + if skydome: + skydome.clouds_coverage = 0.0 + #skydome.cloud_overcast_intensity = -1.0 + #skydome.cloud_shadow_intensity = 0.0 + skydome.fog_density = 0.0 + #skydome.local_emission_scale = 1.0 + skydome.lightning_flash = 0.0 + + +func _process(delta: float) -> void: + _update_follow_position() + _update_weather_observer() + WeatherServer.update_weather_state(get_world_3d(), delta) + _sync_weather_state() + _emit_current_wind_changed() + _update_lightning(delta) + _update_rain_rendering() + _push_weather_state() + + +func set_precipitation_intensity(value: float) -> void: + precipitation_intensity = value + + +func set_cloud_density(value: float) -> void: + cloud_density = value + + +func set_cloud_overcast_intensity(value: float) -> void: + cloud_overcast_intensity = value + + +func set_storm_intensity(value: float) -> void: + storm_intensity = value + + +func set_storm_fog_intensity(value: float) -> void: + storm_fog_intensity = value + + +func apply_wind_controls(strength_ratio: float, direction: Vector2) -> void: + var normalized_direction := direction.normalized() + var gnd_speed := lerpf(0.15, 3.0, strength_ratio) + var gnd_strength := lerpf(0.4, 5.0, strength_ratio) + + ProjectSettings.set_setting(GND_WIND_DIRECTION_SETTING, normalized_direction) + ProjectSettings.set_setting(GND_WIND_SPEED_SETTING, gnd_speed) + ProjectSettings.set_setting(GND_WIND_STRENGTH_SETTING, gnd_strength) + RenderingServer.global_shader_parameter_set("gnd_wind_direction", normalized_direction) + RenderingServer.global_shader_parameter_set("gnd_wind_speed", gnd_speed) + RenderingServer.global_shader_parameter_set("gnd_wind_strength", gnd_strength) + + var skydome := _get_skydome() + if skydome: + skydome.clouds_wind_direction = normalized_direction + skydome.clouds_wind_strength = gnd_speed + skydome.apply_wind_now() + + apply_now() + + +func apply_now() -> void: + _sync_weather_state(true) + _emit_current_wind_changed() + _apply_weather_state(true) + + +func _refresh_editor_preview() -> void: + if not Engine.is_editor_hint() or not is_inside_tree(): + return + _invalidate_rain_field_state_cache() + _push_weather_server_settings() + _push_rain_probe_config() + _apply_weather_state(true) + + +func get_effective_precipitation_intensity() -> float: + return _current_local_precipitation + + +func get_precipitation_strength_at_position(world_position: Vector3) -> float: + return WeatherServer.get_rain_participation_strength( + get_world_3d(), + world_position, + _get_global_precipitation_setting() + ) + + +func get_storm_factor(precipitation_override: float = -1.0) -> float: + if precipitation_override < 0.0: + return _current_storm_factor + return _evaluate_storm_intensity_response(_current_storm_intensity_input) + + +func _apply_weather_state(force: bool = false) -> void: + _ensure_rain_field_nodes() + _update_follow_position() + _update_weather_observer() + _sync_weather_state(force) + _update_lightning(0.0, force) + _update_rain_rendering() + _push_weather_state(force) + + +func _get_skydome() -> Node: + if not skydome_path.is_empty(): + return get_node_or_null(skydome_path) + + var root := get_tree().current_scene + if root != null: + return root.find_child("Skydome", true, false) + return null + + +func _get_world_environment() -> WorldEnvironment: + if not world_environment_path.is_empty(): + return get_node_or_null(world_environment_path) as WorldEnvironment + + return null + + +func _get_environment() -> Environment: + return _environment + + +func _refresh_environment_cache() -> void: + var env_node := _get_world_environment() + if env_node != null: + _environment = env_node.environment + return + + var world_3d := get_world_3d() + _environment = world_3d.environment if world_3d != null else null + + +func _push_rain_probe_config() -> void: + WeatherServer.configure_visible_rain_probe_field( + get_world_3d(), + get_instance_id(), + rain_probe_max_count, + rain_probe_distance + ) + _invalidate_rain_field_state_cache() + + +func _push_weather_server_settings() -> void: + WeatherServer.configure_weather_state( + get_world_3d(), + precipitation_intensity, + cloud_density, + cloud_overcast_intensity, + storm_intensity, + storm_fog_intensity, + precipitation_wind_strength, + sheltered_volumetric_emission_scale, + lightning_enabled, + lightning_min_interval, + lightning_max_interval, + lightning_flash_decay + ) + + +func _update_weather_observer() -> void: + var follow_target := _get_follow_target() + if follow_target == null: + WeatherServer.clear_weather_observer_sample(get_world_3d()) + return + + WeatherServer.set_weather_observer_sample(get_world_3d(), follow_target.global_position) + + +func _sync_weather_state(force: bool = false) -> void: + var state := _get_server_weather_state(force) + if state.is_empty(): + return + _apply_weather_state_snapshot(state) + + +func _apply_weather_state_snapshot(state: Dictionary) -> void: + _current_global_precipitation = clampf(float(state.get("global_precipitation", precipitation_intensity)), 0.0, 1.0) + _current_cloud_density = clampf(float(state.get("cloud_density", cloud_density)), 0.0, 1.0) + _current_cloud_overcast_intensity_input = clampf(float(state.get("cloud_overcast_intensity_input", cloud_overcast_intensity)), 0.0, 1.0) + _current_storm_intensity_input = clampf(float(state.get("storm_intensity_input", storm_intensity)), 0.0, 1.0) + _current_storm_fog_intensity_input = clampf(float(state.get("storm_fog_intensity_input", storm_fog_intensity)), 0.0, 1.0) + _current_local_precipitation = clampf(float(state.get("local_precipitation", _current_global_precipitation)), 0.0, 1.0) + _current_storm_factor = clampf(float(state.get("storm_factor", 0.0)), 0.0, 1.0) + _current_shelter_factor = clampf(float(state.get("shelter_factor", 0.0)), 0.0, 1.0) + _current_local_emission_scale = clampf(float(state.get("local_emission_scale", 1.0)), 0.0, 1.0) + + +func _get_server_weather_state(force_refresh: bool = false) -> Dictionary: + var world_3d := get_world_3d() + if world_3d == null: + return {} + + if force_refresh: + _push_weather_server_settings() + WeatherServer.update_weather_state(world_3d, 0.0) + + return WeatherServer.get_weather_state(world_3d) + + +func _get_global_precipitation_setting() -> float: + return clampf(precipitation_intensity, 0.0, 1.0) + + +func _evaluate_storm_intensity_response(intensity: float) -> float: + var clamped_intensity := clampf(intensity, 0.0, 1.0) + if storm_intensity_curve != null: + return clampf(storm_intensity_curve.sample_baked(clamped_intensity), 0.0, 1.0) + return clampf(pow(clamped_intensity, storm_intensity_exponent), 0.0, 1.0) + + +func _update_lightning(delta: float, force: bool = false) -> void: + var lightning_multiplier := 1.0 + var follow_target := _get_follow_target() + if follow_target != null: + lightning_multiplier = WeatherServer.get_rain_lightning_multiplier(get_world_3d(), follow_target.global_position) + + var storm_response := _evaluate_storm_intensity_response(_current_storm_intensity_input) + _lightning_activity = clampf(storm_response * lightning_multiplier, 0.0, 1.0) + + if delta > 0.0: + _current_lightning_flash = move_toward(_current_lightning_flash, 0.0, delta * lightning_flash_decay) + elif force and (not lightning_enabled or _lightning_activity <= 0.02): + _current_lightning_flash = 0.0 + + if not lightning_enabled or _lightning_activity <= 0.02: + _lightning_roll_timer = 0.0 + if force: + _current_lightning_flash = 0.0 + return + + if delta <= 0.0: + return + + _lightning_roll_timer += delta + var flash_interval := lerpf(lightning_max_interval, lightning_min_interval, _lightning_activity) + flash_interval = clampf(flash_interval, 0.1, 60.0) + var flash_chance := clampf(LIGHTNING_ROLL_INTERVAL_SEC / flash_interval, 0.0, 1.0) + while _lightning_roll_timer >= LIGHTNING_ROLL_INTERVAL_SEC: + _lightning_roll_timer -= LIGHTNING_ROLL_INTERVAL_SEC + if _lightning_rng.randf() <= flash_chance: + _trigger_lightning_pulse(_lightning_activity) + + +func _trigger_lightning_pulse(activity: float) -> void: + var clamped_activity := clampf(activity, 0.0, 1.0) + var strength_roll := _lightning_rng.randf() + var min_strength := lerpf(0.03, 0.22, clamped_activity) + var max_strength := lerpf(0.35, 1.0, clamped_activity) + var flash_strength := lerpf(min_strength, max_strength, pow(strength_roll, 1.35)) + _current_lightning_flash = maxf(_current_lightning_flash, flash_strength) + if not _thunder_locked: + thunder.emit(clampf(flash_strength, 0.0, 1.0)) + _start_thunder_lock() + + +func _start_thunder_lock() -> void: + _thunder_locked = true + _disconnect_thunder_unlock_timer() + var lock_duration := _lightning_rng.randf_range(thunder_lock_min_duration, thunder_lock_max_duration) + _thunder_unlock_timer = get_tree().create_timer(lock_duration) + _thunder_unlock_timer.timeout.connect(_on_thunder_unlock_timeout, CONNECT_ONE_SHOT) + + +func _disconnect_thunder_unlock_timer() -> void: + if _thunder_unlock_timer == null: + return + if _thunder_unlock_timer.timeout.is_connected(_on_thunder_unlock_timeout): + _thunder_unlock_timer.timeout.disconnect(_on_thunder_unlock_timeout) + _thunder_unlock_timer = null + + +func _clear_thunder_lock() -> void: + _thunder_locked = false + _disconnect_thunder_unlock_timer() + + +func _on_thunder_unlock_timeout() -> void: + _thunder_locked = false + _thunder_unlock_timer = null + + +func _get_follow_target() -> Node3D: + if Engine.is_editor_hint() and _editor_preview_camera_id != 0: + var editor_camera := instance_from_id(_editor_preview_camera_id) as Camera3D + if editor_camera != null: + return editor_camera + + var viewport := get_viewport() + if viewport != null: + var camera := viewport.get_camera_3d() + if camera != null: + return camera + return null + + +func _ensure_rain_field_nodes() -> void: + if _near_rain_field == null: + _near_rain_field = _ensure_rain_field_node(NEAR_FIELD_NAME, near_rain_color) + if _mid_rain_field == null: + _mid_rain_field = _ensure_rain_field_node(MID_FIELD_NAME, mid_rain_color) + _sync_rain_materials() + + +func _ensure_rain_field_node(node_name: String, tint: Color) -> MultiMeshInstance3D: + var existing_node := get_node_or_null(node_name) + if existing_node != null and not (existing_node is MultiMeshInstance3D): + remove_child(existing_node) + existing_node.queue_free() + + var rain_field := get_node_or_null(node_name) as MultiMeshInstance3D + if rain_field != null: + return rain_field + + rain_field = MultiMeshInstance3D.new() + rain_field.name = node_name + rain_field.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + + var multimesh := MultiMesh.new() + multimesh.transform_format = MultiMesh.TRANSFORM_3D + multimesh.use_custom_data = true + multimesh.instance_count = 0 + multimesh.visible_instance_count = 0 + + var quad := QuadMesh.new() + quad.size = Vector2(0.01, 3.5) + + var material := ShaderMaterial.new() + material.shader = RAIN_STREAK_SHADER + material.set_shader_parameter("tint", tint) + material.set_shader_parameter("intensity", visual_intensity) + material.set_shader_parameter("roughness", rain_roughness) + material.set_shader_parameter("specular", rain_specular) + material.set_shader_parameter("specular_fade_start", rain_specular_fade_start) + material.set_shader_parameter("specular_fade_end", rain_specular_fade_end) + quad.material = material + + multimesh.mesh = quad + rain_field.multimesh = multimesh + add_child(rain_field) + return rain_field + + +func _sync_rain_materials() -> void: + _set_rain_field_tint(_near_rain_field, near_rain_color) + _set_rain_field_tint(_mid_rain_field, mid_rain_color) + + var near_mat := _get_rain_field_material(_near_rain_field) + if near_mat != null: + near_mat.set_shader_parameter("intensity", visual_intensity) + near_mat.set_shader_parameter("roughness", rain_roughness) + near_mat.set_shader_parameter("specular", rain_specular) + near_mat.set_shader_parameter("specular_fade_start", rain_specular_fade_start) + near_mat.set_shader_parameter("specular_fade_end", rain_specular_fade_end) + + var mid_mat := _get_rain_field_material(_mid_rain_field) + if mid_mat != null: + mid_mat.set_shader_parameter("intensity", visual_intensity) + mid_mat.set_shader_parameter("roughness", rain_roughness) + mid_mat.set_shader_parameter("specular", rain_specular) + mid_mat.set_shader_parameter("specular_fade_start", rain_specular_fade_start) + mid_mat.set_shader_parameter("specular_fade_end", rain_specular_fade_end) + +func set_rain_mesh_debug_preview(enabled: bool, near_tint: Color, mid_tint: Color) -> void: + _ensure_rain_field_nodes() + var changed := ( + _rain_mesh_debug_preview_enabled != enabled + or not _rain_mesh_debug_near_tint.is_equal_approx(near_tint) + or not _rain_mesh_debug_mid_tint.is_equal_approx(mid_tint) + ) + if not changed: + return + + _rain_mesh_debug_preview_enabled = enabled + _rain_mesh_debug_near_tint = near_tint + _rain_mesh_debug_mid_tint = mid_tint + if enabled: + _ensure_rain_debug_materials(near_tint, mid_tint) + if _near_rain_field != null: + _near_rain_field.material_override = _near_rain_debug_material + if _mid_rain_field != null: + _mid_rain_field.material_override = _mid_rain_debug_material + else: + if _near_rain_field != null: + _near_rain_field.material_override = null + if _mid_rain_field != null: + _mid_rain_field.material_override = null + + if Engine.is_editor_hint() and is_inside_tree(): + _refresh_editor_preview() + + +func set_editor_preview_camera(camera: Camera3D) -> void: + var next_camera_id := camera.get_instance_id() if camera != null else 0 + var next_transform := camera.global_transform if camera != null else Transform3D.IDENTITY + var changed := _editor_preview_camera_id != next_camera_id + if not changed and camera != null: + changed = ( + not _editor_preview_camera_transform_valid + or not _editor_preview_camera_transform.is_equal_approx(next_transform) + ) + elif not changed and camera == null: + changed = _editor_preview_camera_transform_valid + + if not changed: + return + + _editor_preview_camera_id = next_camera_id + _editor_preview_camera_transform = next_transform + _editor_preview_camera_transform_valid = camera != null + if Engine.is_editor_hint() and is_inside_tree(): + _refresh_editor_preview() + + +func _ensure_rain_debug_materials(near_tint: Color, mid_tint: Color) -> void: + if _near_rain_debug_material == null: + _near_rain_debug_material = StandardMaterial3D.new() + _near_rain_debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + _near_rain_debug_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + _near_rain_debug_material.no_depth_test = false + _near_rain_debug_material.cull_mode = BaseMaterial3D.CULL_DISABLED + if _mid_rain_debug_material == null: + _mid_rain_debug_material = StandardMaterial3D.new() + _mid_rain_debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + _mid_rain_debug_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + _mid_rain_debug_material.no_depth_test = false + _mid_rain_debug_material.cull_mode = BaseMaterial3D.CULL_DISABLED + + _near_rain_debug_material.albedo_color = near_tint + _near_rain_debug_material.emission_enabled = true + _near_rain_debug_material.emission = near_tint + _mid_rain_debug_material.albedo_color = mid_tint + _mid_rain_debug_material.emission_enabled = true + _mid_rain_debug_material.emission = mid_tint + + +func _set_rain_field_tint(rain_field: MultiMeshInstance3D, tint: Color) -> void: + var material := _get_rain_field_material(rain_field) + if material == null: + return + material.set_shader_parameter("tint", tint) + + +func _get_rain_field_material(rain_field: MultiMeshInstance3D) -> ShaderMaterial: + if rain_field == null or rain_field.multimesh == null: + return null + var quad := rain_field.multimesh.mesh as QuadMesh + if quad == null: + return null + return quad.material as ShaderMaterial + + +func _get_rain_field_mesh(rain_field: MultiMeshInstance3D) -> QuadMesh: + if rain_field == null or rain_field.multimesh == null: + return null + return rain_field.multimesh.mesh as QuadMesh + + +func _update_follow_position() -> void: + var follow_target := _get_follow_target() + if follow_target == null: + return + + global_position = follow_target.global_position + Vector3(0.0, follow_height, 0.0) + + +func _update_rain_rendering() -> void: + var follow_target := _get_follow_target() + var global_intensity := _current_global_precipitation + var local_intensity := _current_local_precipitation + var render_intensity := maxf(global_intensity, local_intensity) + _emit_rain_strength_changed(global_intensity) + _emit_local_rain_strength_changed(local_intensity) + + if follow_target == null or render_intensity <= 0.0: + _clear_rain_field_layer(_near_rain_field) + _clear_rain_field_layer(_mid_rain_field) + return + + var rain_direction := _get_rain_direction(_get_wind_speed(), render_intensity) + var near_layer_intensity: float = _get_layer_intensity(render_intensity, false) + var mid_layer_intensity: float = _get_layer_intensity(render_intensity, true) + var sample_y: float = follow_target.global_position.y + 1.0 + + var camera := follow_target as Camera3D + if camera == null: + _clear_rain_field_layer(_near_rain_field) + _clear_rain_field_layer(_mid_rain_field) + return + + var view_transform := camera.global_transform + var rain_basis: Basis = _get_rain_field_basis(rain_direction, view_transform.basis) + var near_card_height: float = _get_rain_field_card_height(near_emission_extents, near_layer_intensity, false) + var near_travel_distance: float = _get_rain_field_travel_distance(near_card_height, render_intensity) + var near_spacing: float = _get_rain_field_spacing(near_field_spacing, camera) + var near_state: Dictionary = _get_rain_render_field_state_cached( + &"near", + camera, + view_transform, + follow_target.global_position, + sample_y, + _get_rain_field_center_y(follow_target.global_position.y, near_card_height), + global_intensity, + near_emission_extents, + near_spacing, + near_field_jitter, + rain_direction, + _get_rain_field_motion_half_span(near_card_height, near_travel_distance) + ) + _update_rain_field_layer( + _near_rain_field, + near_state, + render_intensity, + near_layer_intensity, + false, + rain_basis, + near_layer_speed_multiplier, + near_emission_extents, + near_spacing + ) + + if not mid_layer_enabled: + _clear_rain_field_layer(_mid_rain_field) + return + + var mid_card_height: float = _get_rain_field_card_height(mid_emission_extents, mid_layer_intensity, true) + var mid_travel_distance: float = _get_rain_field_travel_distance(mid_card_height, render_intensity) + var mid_spacing: float = _get_rain_field_spacing(mid_field_spacing, camera) + var mid_state: Dictionary = _get_rain_render_field_state_cached( + &"mid", + camera, + view_transform, + follow_target.global_position, + sample_y, + _get_rain_field_center_y(follow_target.global_position.y, mid_card_height), + global_intensity, + mid_emission_extents, + mid_spacing, + mid_field_jitter, + rain_direction, + _get_rain_field_motion_half_span(mid_card_height, mid_travel_distance) + ) + _update_rain_field_layer( + _mid_rain_field, + mid_state, + render_intensity, + mid_layer_intensity, + true, + rain_basis, + mid_layer_speed_multiplier, + mid_emission_extents, + mid_spacing + ) + + +func _update_rain_field_layer( + rain_field: MultiMeshInstance3D, + field_state: Dictionary, + rain_intensity: float, + layer_intensity: float, + is_mid_layer: bool, + rain_basis: Basis, + speed_multiplier: float, + extents: Vector3, + field_spacing: float +) -> void: + if rain_field == null or rain_field.multimesh == null: + return + + var multimesh := rain_field.multimesh + var positions: PackedVector3Array = field_state.get("positions", PackedVector3Array()) + var custom_data: PackedColorArray = field_state.get("custom_data", PackedColorArray()) + var visible_count: int = int(field_state.get("count", 0)) + + if rain_intensity <= 0.0 or visible_count <= 0: + multimesh.visible_instance_count = 0 + return + + if multimesh.instance_count != positions.size(): + multimesh.instance_count = positions.size() + + var card_height: float = _get_rain_field_card_height(extents, layer_intensity, is_mid_layer) + _update_rain_field_visuals(rain_field, rain_intensity, layer_intensity, is_mid_layer, speed_multiplier, card_height, extents, field_spacing) + + var coverage: float = _get_rain_field_density_coverage(layer_intensity, is_mid_layer) + var write_index: int = 0 + for index in range(visible_count): + var instance_position: Vector3 = positions[index] - global_position + var instance_custom: Color = custom_data[index] + if instance_custom.a > coverage: + continue + var variation_scale: float = lerpf(0.85, 1.15, instance_custom.b) + var instance_basis: Basis = rain_basis.scaled(Vector3.ONE * variation_scale) + multimesh.set_instance_transform(write_index, Transform3D(instance_basis, instance_position)) + multimesh.set_instance_custom_data(write_index, instance_custom) + write_index += 1 + + multimesh.visible_instance_count = write_index + + +func _clear_rain_field_layer(rain_field: MultiMeshInstance3D) -> void: + if rain_field == null or rain_field.multimesh == null: + return + rain_field.multimesh.visible_instance_count = 0 + + +func _invalidate_rain_field_state_cache() -> void: + _near_rain_field_state_cache = {} + _mid_rain_field_state_cache = {} + + +func _get_rain_render_field_state_cached( + layer_key: StringName, + camera: Camera3D, + view_transform: Transform3D, + view_origin: Vector3, + sample_y: float, + layer_center_y: float, + base_strength: float, + half_extents: Vector3, + cell_spacing: float, + jitter_ratio: float, + rain_direction: Vector3, + rain_motion_half_span: float +) -> Dictionary: + if Engine.is_editor_hint(): + return WeatherServer.get_rain_render_field_state( + get_world_3d(), + get_instance_id(), + layer_key, + view_transform, + camera, + view_origin, + sample_y, + layer_center_y, + base_strength, + half_extents, + cell_spacing, + jitter_ratio, + rain_direction, + rain_motion_half_span + ) + + var cache := _get_rain_render_field_state_cache(layer_key) + var volume_revision: int = WeatherServer.get_rain_volume_revision(get_world_3d()) + var needs_refresh := not bool(cache.get("ready", false)) + + if not needs_refresh: + var last_view_transform: Transform3D = cache.get("view_transform", Transform3D.IDENTITY) + var last_volume_revision: int = int(cache.get("volume_revision", -1)) + var last_base_strength: float = float(cache.get("base_strength", -1.0)) + var last_sample_y: float = float(cache.get("sample_y", INF)) + var last_layer_center_y: float = float(cache.get("layer_center_y", INF)) + var last_cell_spacing: float = float(cache.get("cell_spacing", -1.0)) + var last_rain_direction: Vector3 = cache.get("rain_direction", Vector3.ZERO) + var last_rain_motion_half_span: float = float(cache.get("rain_motion_half_span", -1.0)) + var last_half_extents: Vector3 = cache.get("half_extents", Vector3.ZERO) + + needs_refresh = ( + last_volume_revision != volume_revision + or not last_view_transform.is_equal_approx(view_transform) + or absf(last_base_strength - base_strength) > 0.0001 + or absf(last_sample_y - sample_y) > 0.0001 + or absf(last_layer_center_y - layer_center_y) > 0.0001 + or absf(last_cell_spacing - cell_spacing) > 0.0001 + or not last_rain_direction.is_equal_approx(rain_direction) + or absf(last_rain_motion_half_span - rain_motion_half_span) > 0.0001 + or not last_half_extents.is_equal_approx(half_extents) + ) + if not needs_refresh: + return cache.get("state", {}) + + var current_time_msec: int = Time.get_ticks_msec() + var last_refresh_time_msec: int = int(cache.get("last_refresh_time_msec", 0)) + if last_refresh_time_msec > 0 and current_time_msec - last_refresh_time_msec < RAIN_FIELD_RUNTIME_REFRESH_INTERVAL_MSEC: + return cache.get("state", {}) + + var field_state: Dictionary = WeatherServer.get_rain_render_field_state( + get_world_3d(), + get_instance_id(), + layer_key, + view_transform, + camera, + view_origin, + sample_y, + layer_center_y, + base_strength, + half_extents, + cell_spacing, + jitter_ratio, + rain_direction, + rain_motion_half_span + ) + _store_rain_render_field_state_cache(layer_key, { + "ready": true, + "state": field_state, + "last_refresh_time_msec": current_time_msec, + "view_transform": view_transform, + "volume_revision": volume_revision, + "base_strength": base_strength, + "sample_y": sample_y, + "layer_center_y": layer_center_y, + "cell_spacing": cell_spacing, + "rain_direction": rain_direction, + "rain_motion_half_span": rain_motion_half_span, + "half_extents": half_extents, + }) + return field_state + + +func _get_rain_render_field_state_cache(layer_key: StringName) -> Dictionary: + return _near_rain_field_state_cache if layer_key == &"near" else _mid_rain_field_state_cache + + +func _store_rain_render_field_state_cache(layer_key: StringName, cache: Dictionary) -> void: + if layer_key == &"near": + _near_rain_field_state_cache = cache + return + _mid_rain_field_state_cache = cache + + +func _emit_rain_strength_changed(strength: float) -> void: + var clamped_strength := clampf(strength, 0.0, 1.0) + if absf(_last_emitted_rain_strength - clamped_strength) <= 0.0001: + return + _last_emitted_rain_strength = clamped_strength + rain_strength_changed.emit(clamped_strength) + + +func _emit_local_rain_strength_changed(strength: float) -> void: + var clamped_strength := clampf(strength, 0.0, 1.0) + if absf(_last_emitted_local_rain_strength - clamped_strength) <= 0.0001: + return + _last_emitted_local_rain_strength = clamped_strength + rain_local_strength_changed.emit(clamped_strength) + + +func _emit_current_wind_changed() -> void: + var world_3d := get_world_3d() + if world_3d == null: + return + + var speed := maxf(WeatherServer.get_final_wind_speed(world_3d), 0.0) + var direction := WeatherServer.get_final_wind_direction(world_3d) + if direction.length_squared() <= 0.0001: + direction = Vector2(0.8, 0.3) + direction = direction.normalized() + + if absf(_last_emitted_wind_speed - speed) <= 0.0001 and _last_emitted_wind_direction.is_equal_approx(direction): + return + + _last_emitted_wind_speed = speed + _last_emitted_wind_direction = direction + wind_changed.emit(speed, direction) + + +func _smooth_factor(value: float, start: float, end: float) -> float: + var t := clampf((value - start) / maxf(end - start, 0.0001), 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) + + +func _get_rain_direction(wind_speed_value: float, rain_intensity: float) -> Vector3: + var wind_dir := _get_wind_direction() + var wind_tilt := maxf(wind_speed_value * wind_influence, 0.0) + var rain_tilt_factor := _get_rain_tilt_factor(rain_intensity) + var lateral_strength := minf( + pow(wind_tilt, 1.1) * lerpf(0.05, 0.14, rain_tilt_factor), + 0.32 + ) + return Vector3(wind_dir.x * lateral_strength, -1.0, wind_dir.y * lateral_strength).normalized() + + +func _get_rain_tilt_factor(rain_intensity: float) -> float: + var timing_intensity := clampf(rain_intensity, 0.0, 1.0) + if timing_intensity <= 0.5: + return 0.0 + var remapped_t := (timing_intensity - 0.5) / 0.5 + return pow(clampf(remapped_t, 0.0, 1.0), 2.4) + + +func _get_layer_intensity(intensity: float, is_mid_layer: bool) -> float: + if is_mid_layer: + return _smooth_factor(intensity, 0.28, 0.9) + return _smooth_factor(intensity, 0.0, 0.42) + + +func _get_rain_field_basis(rain_direction: Vector3, camera_basis: Basis) -> Basis: + var down_axis := rain_direction.normalized() + var camera_forward := (-camera_basis.z).normalized() + var right_axis := camera_forward.cross(down_axis).normalized() + if right_axis.length_squared() <= 0.0001: + right_axis = camera_basis.x.normalized() + if right_axis.length_squared() <= 0.0001: + right_axis = Vector3.RIGHT + var normal_axis := right_axis.cross(down_axis).normalized() + if normal_axis.length_squared() <= 0.0001: + normal_axis = Vector3.FORWARD + return Basis(right_axis, down_axis, normal_axis).orthonormalized() + + +func _get_rain_field_density_coverage(layer_intensity: float, is_mid_layer: bool) -> float: + return 1.0 + + +func _update_rain_field_visuals( + rain_field: MultiMeshInstance3D, + rain_intensity: float, + layer_intensity: float, + is_mid_layer: bool, + speed_multiplier: float, + card_height: float, + extents: Vector3, + field_spacing: float +) -> void: + var mesh := _get_rain_field_mesh(rain_field) + var material := _get_rain_field_material(rain_field) + if mesh == null or material == null: + return + + var base_color := mid_rain_color if is_mid_layer else near_rain_color + var target_width := _get_rain_field_visual_width(layer_intensity, is_mid_layer, field_spacing) + mesh.size = Vector2(target_width, card_height) + + var effective_color := Color( + base_color.r, + base_color.g, + base_color.b, + base_color.a + ) + material.set_shader_parameter("intensity", visual_intensity) + material.set_shader_parameter("roughness", rain_roughness) + material.set_shader_parameter("specular", rain_specular) + material.set_shader_parameter("specular_fade_start", rain_specular_fade_start) + material.set_shader_parameter("specular_fade_end", rain_specular_fade_end) + material.set_shader_parameter("tint", effective_color) + material.set_shader_parameter("intensity_alpha", clampf(rain_intensity, 0.0, 1.0)) + material.set_shader_parameter("blur_amount", mid_rain_blur if is_mid_layer else near_rain_blur) + material.set_shader_parameter("glow_amount", mid_rain_glow if is_mid_layer else near_rain_glow) + material.set_shader_parameter("width_softness", lerpf(0.14, 0.24, layer_intensity)) + material.set_shader_parameter("tail_softness", lerpf(0.24, 0.62, layer_intensity)) + material.set_shader_parameter("center_bias", lerpf(0.34, 0.68, layer_intensity)) + var timing_intensity := clampf(rain_intensity, 0.0, 1.0) + var flow_intensity := pow(timing_intensity, 0.7) + material.set_shader_parameter( + "flow_speed", + (base_fall_speed / 26.0) * lerpf(18.0, 7.5, flow_intensity) * maxf(speed_multiplier, 0.1) + ) + material.set_shader_parameter( + "spawn_rate", + lerpf(0.08, 2.6, pow(timing_intensity, 1.4)) + ) + material.set_shader_parameter( + "spawn_duration", + _get_rain_field_spawn_duration(timing_intensity, is_mid_layer) + ) + material.set_shader_parameter( + "travel_distance", + _get_rain_field_travel_distance(card_height, timing_intensity) + ) + material.set_shader_parameter("respawn_spread", _get_rain_field_respawn_spread(field_spacing)) + material.set_shader_parameter( + "streak_length_scale", + lerpf(0.001, 1.0, pow(timing_intensity, 2.1)) + ) + material.set_shader_parameter( + "spawn_probability", + _get_rain_field_spawn_probability(timing_intensity, is_mid_layer) + ) + + var local_height: float = maxf(card_height + 6.0, 8.0) + rain_field.custom_aabb = AABB( + Vector3(-extents.x - 4.0, -local_height * 0.5, -extents.z - 4.0), + Vector3((extents.x + 4.0) * 2.0, local_height, (extents.z + 4.0) * 2.0) + ) + + +func _get_rain_field_visual_width(layer_intensity: float, is_mid_layer: bool, field_spacing: float) -> float: + var streak_width := ( + lerpf(0.006, 0.0105, layer_intensity) + if not is_mid_layer + else lerpf(0.005, 0.009, layer_intensity) + ) * RAIN_FIELD_WIDTH_SCALE + if not _rain_mesh_debug_preview_enabled: + return streak_width + + var debug_width_scale := 0.82 if not is_mid_layer else 0.74 + return maxf(streak_width, field_spacing * debug_width_scale) + + +func _get_rain_field_respawn_spread(field_spacing: float) -> float: + if _rain_mesh_debug_preview_enabled: + return 0.0 + return minf(field_spacing * 0.14, 0.08) + + +func _get_rain_field_spawn_probability(layer_intensity: float, is_mid_layer: bool) -> float: + var intensity := clampf(layer_intensity, 0.0, 1.0) + if is_mid_layer: + return pow(intensity, 5.0) + return pow(intensity, 4.0) + + +func _get_rain_field_spawn_duration(layer_intensity: float, is_mid_layer: bool) -> float: + var intensity := clampf(layer_intensity, 0.0, 1.0) + if is_mid_layer: + return lerpf(0.08, 0.42, pow(intensity, 1.2)) + return lerpf(0.06, 0.36, pow(intensity, 1.05)) + + +func _get_rain_field_travel_distance(card_height: float, rain_intensity: float) -> float: + var timing_intensity := clampf(rain_intensity, 0.0, 1.0) + return lerpf(card_height * 0.14, card_height * 1.05, pow(timing_intensity, 1.3)) + + +func _get_rain_field_motion_half_span(card_height: float, travel_distance: float) -> float: + return maxf(card_height + travel_distance, 0.0) * 0.5 + + +func _get_rain_field_card_height(extents: Vector3, layer_intensity: float, is_mid_layer: bool) -> float: + var vertical_extent := 2.2 if is_mid_layer else 2.0 + return maxf(follow_height + extents.y * vertical_extent, 6.0) + + +func _get_rain_field_center_y(follow_y: float, card_height: float) -> float: + return follow_y + maxf(card_height * 0.5 - 0.9, 0.0) + + +func _get_rain_field_spacing(base_spacing: float, camera: Camera3D = null) -> float: + var spacing := maxf( + base_spacing + * RAIN_FIELD_COUNT_REDUCTION_SPACING_SCALE + / sqrt(maxf(rain_mesh_density * 4.0, 0.1)), + 0.1 + ) + var probe_spacing := -1.0 + if camera != null: + probe_spacing = WeatherServer.get_configured_visible_rain_probe_spacing( + get_world_3d(), + get_instance_id(), + camera + ) + if probe_spacing > 0.0: + spacing = minf(spacing, probe_spacing * RAIN_FIELD_COUNT_REDUCTION_SPACING_SCALE) + return maxf(spacing, 0.1) + + +func _get_wind_direction() -> Vector2: + return WeatherServer.get_final_wind_direction(get_world_3d()) + + +func _get_wind_speed() -> float: + return WeatherServer.get_final_wind_speed(get_world_3d()) + + +func _push_weather_state(force: bool = false) -> void: + var skydome := _get_skydome() + if skydome == null: + return + + var state := _get_server_weather_state(force) + if state.is_empty(): + return + + var current_cloud_density := clampf(float(state.get("cloud_density", _current_cloud_density)), 0.0, 1.0) + var current_cloud_overcast_intensity := clampf(float(state.get("cloud_overcast_intensity_input", _current_cloud_overcast_intensity_input)), 0.0, 1.0) + var current_fog_intensity := clampf(float(state.get("storm_fog_intensity_input", _current_storm_fog_intensity_input)), 0.0, 1.0) + var current_cloud_shadow_intensity := clampf(float(state.get("storm_factor", _current_storm_factor)), 0.0, 1.0) + var local_emission_scale := clampf(float(state.get("local_emission_scale", _current_local_emission_scale)), 0.0, 1.0) + var final_wind_speed := maxf(float(state.get("final_wind_speed", _get_wind_speed())), 0.0) + var final_wind_direction := state.get("final_wind_direction", _get_wind_direction()) as Vector2 + if final_wind_direction.length_squared() <= 0.0001: + final_wind_direction = Vector2(0.8, 0.3) + final_wind_direction = final_wind_direction.normalized() + + skydome.clouds_coverage = current_cloud_density + skydome.clouds_wind_direction = final_wind_direction + skydome.clouds_wind_strength = final_wind_speed + skydome.apply_wind_now() + #skydome.cloud_overcast_intensity = current_cloud_overcast_intensity + #skydome.cloud_shadow_intensity = current_cloud_shadow_intensity + skydome.fog_density = current_fog_intensity + #skydome.local_emission_scale = local_emission_scale + skydome.lightning_flash = _current_lightning_flash diff --git a/demo/addons/gnd_weather/WeatherNode.gd.uid b/demo/addons/gnd_weather/WeatherNode.gd.uid new file mode 100644 index 00000000..96c32fe5 --- /dev/null +++ b/demo/addons/gnd_weather/WeatherNode.gd.uid @@ -0,0 +1 @@ +uid://lgnwh1qwjbmt diff --git a/demo/addons/gnd_weather/WeatherServer.gd b/demo/addons/gnd_weather/WeatherServer.gd new file mode 100644 index 00000000..456df841 --- /dev/null +++ b/demo/addons/gnd_weather/WeatherServer.gd @@ -0,0 +1,1553 @@ +class_name WeatherServer +extends RefCounted + +const WIND_DIRECTION_GLOBAL := &"gnd_wind_direction" +const WIND_SPEED_GLOBAL := &"gnd_wind_speed" +const WIND_TIME_GLOBAL := &"gnd_wind_time" +const WIND_DIRECTION_SETTING := &"shader_globals/gnd_wind_direction" +const WIND_SPEED_SETTING := &"shader_globals/gnd_wind_speed" +const WIND_STRENGTH_SETTING := &"shader_globals/gnd_wind_strength" +const WIND_TIME_SETTING := &"shader_globals/gnd_wind_time" +const WIND_TURBULENCE_SETTING := &"shader_globals/gnd_wind_turbulence" +const WIND_PATTERN_SETTING := &"shader_globals/gnd_wind_pattern" +const WIND_DIRECTION_VALUE_SETTING := &"shader_globals/gnd_wind_direction/value" +const WIND_SPEED_VALUE_SETTING := &"shader_globals/gnd_wind_speed/value" +const WIND_STRENGTH_VALUE_SETTING := &"shader_globals/gnd_wind_strength/value" +const WIND_TIME_VALUE_SETTING := &"shader_globals/gnd_wind_time/value" +const WIND_TURBULENCE_VALUE_SETTING := &"shader_globals/gnd_wind_turbulence/value" +const WIND_PATTERN_VALUE_SETTING := &"shader_globals/gnd_wind_pattern/value" +const VISIBLE_RAIN_PROBE_REFERENCE_DISTANCE := 8.0 +const RAIN_FIELD_FEATHER_RENDER_CUTOFF := 0.24 +const RAIN_PROBE_SUPPRESSIVE_FEATHER_BLEND_START := 0.28 +const RAIN_PROBE_SUPPRESSIVE_FEATHER_BLEND_END := 0.94 +const RAIN_FIELD_LOWER_CULL_SAMPLE_FACTORS := [0.18, 0.42] +const RAIN_FIELD_RESAMPLE_INTERVAL_MSEC := 250 +const RAIN_FIELD_FOOTPRINT_OFFSETS := [ + Vector3.ZERO, + Vector3(1.0, 0.0, 1.0), + Vector3(1.0, 0.0, -1.0), + Vector3(-1.0, 0.0, 1.0), + Vector3(-1.0, 0.0, -1.0), +] +const WIND_GUST_PRIMARY_RATE := 0.055 +const WIND_GUST_SECONDARY_RATE := 0.11 +const WIND_GUST_MACRO_RATE := 0.21 + +static var _rain_volumes_by_world: Dictionary = {} +static var _rain_volume_revision_by_world: Dictionary = {} +static var _visible_rain_probe_fields_by_world: Dictionary = {} +static var _visible_rain_probe_configs_by_world: Dictionary = {} +static var _weather_state_by_world: Dictionary = {} +static var _rain_render_fields_by_world: Dictionary = {} + + +static func _make_default_weather_state() -> Dictionary: + return { + "precipitation_intensity": 0.0, + "cloud_density": 0.0, + "cloud_overcast_intensity": 0.0, + "storm_intensity": 0.0, + "storm_fog_intensity": 0.0, + "precipitation_wind_strength": 4.0, + "sheltered_volumetric_emission_scale": 0.0, + "lightning_enabled": true, + "lightning_min_interval": 3.2, + "lightning_max_interval": 9.5, + "lightning_flash_decay": 4.8, + "observer_position": Vector3.ZERO, + "has_observer_sample": false, + "global_precipitation": 0.0, + "local_precipitation": 0.0, + "storm_factor": 0.0, + "shelter_factor": 0.0, + "local_emission_scale": 1.0, + "wind_time": 0.0, + } + + +static func _ensure_weather_state(world_3d: World3D) -> Dictionary: + if world_3d == null: + return {} + + var world_id := world_3d.get_instance_id() + var state: Dictionary = _weather_state_by_world.get(world_id, {}) + if state.is_empty(): + state = _make_default_weather_state() + _weather_state_by_world[world_id] = state + return state + + +static func _store_weather_state(world_3d: World3D, state: Dictionary) -> void: + if world_3d == null: + return + _weather_state_by_world[world_3d.get_instance_id()] = state + + +static func _refresh_weather_state(world_3d: World3D, state: Dictionary) -> void: + if world_3d == null or state.is_empty(): + return + + var next_global := clampf(float(state.get("precipitation_intensity", 0.0)), 0.0, 1.0) + var next_local := next_global + if bool(state.get("has_observer_sample", false)): + next_local = get_rain_participation_strength( + world_3d, + state.get("observer_position", Vector3.ZERO), + next_global + ) + + var next_storm := clampf(float(state.get("storm_intensity", 0.0)), 0.0, 1.0) + var next_shelter := 0.0 + if next_global > 0.0001 and next_local < next_global: + next_shelter = clampf((next_global - next_local) / next_global, 0.0, 1.0) + + state["global_precipitation"] = next_global + state["local_precipitation"] = next_local + state["storm_factor"] = next_storm + state["shelter_factor"] = next_shelter + state["local_emission_scale"] = lerpf( + 1.0, + clampf(float(state.get("sheltered_volumetric_emission_scale", 0.0)), 0.0, 1.0), + next_shelter + ) + + +static func _get_weather_controlled_wind_speed_for_state(state: Dictionary, fallback: float = 1.0) -> float: + var base_speed := get_global_wind_speed(fallback) + if state.is_empty(): + return base_speed + var precipitation_boost := clampf(float(state.get("precipitation_intensity", 0.0)), 0.0, 1.0) * maxf(float(state.get("precipitation_wind_strength", 4.0)), 0.0) + return maxf(base_speed + precipitation_boost, 0.0) + + +static func add_rain_volume(world_3d: World3D, volume_rid: RID, volume: RainVolume) -> void: + if world_3d == null or not volume_rid.is_valid() or volume == null: + return + + var world_id := world_3d.get_instance_id() + var world_bucket: Dictionary = _rain_volumes_by_world.get(world_id, {}) + world_bucket[volume_rid.get_id()] = volume + _rain_volumes_by_world[world_id] = world_bucket + _notify_rain_volumes_changed(world_3d) + + +static func remove_rain_volume(world_3d: World3D, volume_rid: RID) -> void: + if world_3d == null or not volume_rid.is_valid(): + return + + var world_id := world_3d.get_instance_id() + var world_bucket: Dictionary = _rain_volumes_by_world.get(world_id, {}) + if world_bucket.is_empty(): + return + + world_bucket.erase(volume_rid.get_id()) + if world_bucket.is_empty(): + _rain_volumes_by_world.erase(world_id) + else: + _rain_volumes_by_world[world_id] = world_bucket + _notify_rain_volumes_changed(world_3d) + + +static func mark_rain_volumes_changed(world_3d: World3D) -> void: + _notify_rain_volumes_changed(world_3d) + + +static func get_rain_volume_revision(world_3d: World3D) -> int: + if world_3d == null: + return 0 + + return int(_rain_volume_revision_by_world.get(world_3d.get_instance_id(), 0)) + + +static func configure_visible_rain_probe_field( + world_3d: World3D, + cache_key: int, + max_probes: int, + distance: float +) -> void: + if world_3d == null or cache_key < 0: + return + + var world_id := world_3d.get_instance_id() + var world_bucket: Dictionary = _visible_rain_probe_configs_by_world.get(world_id, {}) + world_bucket[cache_key] = { + "max_probes": maxi(max_probes, 1), + "distance": maxf(distance, 0.1), + } + _visible_rain_probe_configs_by_world[world_id] = world_bucket + clear_visible_rain_participation_cache(world_3d, cache_key) + + +static func clear_visible_rain_probe_field_config(world_3d: World3D, cache_key: int) -> void: + if world_3d == null or cache_key < 0: + return + + var world_id := world_3d.get_instance_id() + var world_bucket: Dictionary = _visible_rain_probe_configs_by_world.get(world_id, {}) + if world_bucket.is_empty(): + return + + world_bucket.erase(cache_key) + if world_bucket.is_empty(): + _visible_rain_probe_configs_by_world.erase(world_id) + else: + _visible_rain_probe_configs_by_world[world_id] = world_bucket + + clear_visible_rain_participation_cache(world_3d, cache_key) + + +static func clear_weather_state(world_3d: World3D) -> void: + if world_3d == null: + return + + var world_id := world_3d.get_instance_id() + _weather_state_by_world.erase(world_id) + _rain_volume_revision_by_world.erase(world_id) + _apply_weather_controlled_wind(null) + + +static func configure_weather_state( + world_3d: World3D, + precipitation_intensity: float, + cloud_density: float, + cloud_overcast_intensity: float, + storm_intensity: float, + storm_fog_intensity: float, + precipitation_wind_strength: float, + sheltered_volumetric_emission_scale: float, + lightning_enabled: bool, + lightning_min_interval: float, + lightning_max_interval: float, + lightning_flash_decay: float +) -> void: + var state := _ensure_weather_state(world_3d) + if state.is_empty(): + return + + state["precipitation_intensity"] = clampf(precipitation_intensity, 0.0, 1.0) + state["cloud_density"] = clampf(cloud_density, 0.0, 1.0) + state["cloud_overcast_intensity"] = clampf(cloud_overcast_intensity, 0.0, 1.0) + state["storm_intensity"] = clampf(storm_intensity, 0.0, 1.0) + state["storm_fog_intensity"] = clampf(storm_fog_intensity, 0.0, 1.0) + state["precipitation_wind_strength"] = maxf(precipitation_wind_strength, 0.0) + state["sheltered_volumetric_emission_scale"] = clampf(sheltered_volumetric_emission_scale, 0.0, 1.0) + state["lightning_enabled"] = lightning_enabled + state["lightning_min_interval"] = maxf(lightning_min_interval, 0.1) + state["lightning_max_interval"] = maxf(lightning_max_interval, 0.1) + state["lightning_flash_decay"] = maxf(lightning_flash_decay, 0.1) + _refresh_weather_state(world_3d, state) + _store_weather_state(world_3d, state) + _apply_weather_controlled_wind(world_3d) + + +static func set_weather_observer_sample(world_3d: World3D, world_position: Vector3) -> void: + var state := _ensure_weather_state(world_3d) + if state.is_empty(): + return + state["observer_position"] = world_position + state["has_observer_sample"] = true + _refresh_weather_state(world_3d, state) + _store_weather_state(world_3d, state) + + +static func clear_weather_observer_sample(world_3d: World3D) -> void: + var state := _ensure_weather_state(world_3d) + if state.is_empty(): + return + state["observer_position"] = Vector3.ZERO + state["has_observer_sample"] = false + _refresh_weather_state(world_3d, state) + _store_weather_state(world_3d, state) + + +static func update_weather_state(world_3d: World3D, delta: float) -> void: + var state := _ensure_weather_state(world_3d) + if state.is_empty(): + return + _refresh_weather_state(world_3d, state) + if delta > 0.0: + state["wind_time"] = float(state.get("wind_time", 0.0)) + delta * _get_weather_controlled_wind_speed_for_state(state, 1.0) + _store_weather_state(world_3d, state) + _apply_weather_controlled_wind(world_3d) + + +static func get_weather_state(world_3d: World3D) -> Dictionary: + if world_3d == null: + return {} + var state: Dictionary = _weather_state_by_world.get(world_3d.get_instance_id(), {}) + if state.is_empty(): + return {} + return { + "global_precipitation": float(state.get("global_precipitation", 0.0)), + "cloud_density": float(state.get("cloud_density", 0.0)), + "cloud_overcast_intensity_input": float(state.get("cloud_overcast_intensity", 0.0)), + "storm_intensity_input": float(state.get("storm_intensity", 0.0)), + "storm_fog_intensity_input": float(state.get("storm_fog_intensity", 0.0)), + "local_precipitation": float(state.get("local_precipitation", 0.0)), + "storm_factor": float(state.get("storm_factor", 0.0)), + "lightning_flash": 0.0, + "shelter_factor": float(state.get("shelter_factor", 0.0)), + "local_emission_scale": float(state.get("local_emission_scale", 1.0)), + "final_wind_speed": get_final_wind_speed(world_3d), + "final_wind_direction": get_final_wind_direction(world_3d), + } + + +static func ensure_wind_project_settings() -> void: + var dirty := false + + if not ProjectSettings.has_setting(String(WIND_DIRECTION_SETTING)): + ProjectSettings.set_setting(String(WIND_DIRECTION_SETTING), { + "type": "vec2", + "value": Vector2(0.8, 0.3), + }) + dirty = true + + if not ProjectSettings.has_setting(String(WIND_SPEED_SETTING)): + ProjectSettings.set_setting(String(WIND_SPEED_SETTING), { + "type": "float", + "value": 1.0, + }) + dirty = true + + if not ProjectSettings.has_setting(String(WIND_STRENGTH_SETTING)): + ProjectSettings.set_setting(String(WIND_STRENGTH_SETTING), { + "type": "float", + "value": 4.0, + }) + dirty = true + + if not ProjectSettings.has_setting(String(WIND_TIME_SETTING)): + ProjectSettings.set_setting(String(WIND_TIME_SETTING), { + "type": "float", + "value": 0.0, + }) + dirty = true + + if not ProjectSettings.has_setting(String(WIND_TURBULENCE_SETTING)): + ProjectSettings.set_setting(String(WIND_TURBULENCE_SETTING), { + "type": "float", + "value": 1.0, + }) + dirty = true + + if not ProjectSettings.has_setting(String(WIND_PATTERN_SETTING)): + ProjectSettings.set_setting(String(WIND_PATTERN_SETTING), { + "type": "sampler2D", + "value": "res://grass/wind_pattern.png", + }) + dirty = true + + if dirty and Engine.is_editor_hint(): + ProjectSettings.save() + + +static func get_global_wind_direction(fallback: Vector2 = Vector2(0.8, 0.3)) -> Vector2: + ensure_wind_project_settings() + var direction := fallback + if ProjectSettings.has_setting(String(WIND_DIRECTION_VALUE_SETTING)): + direction = ProjectSettings.get_setting(String(WIND_DIRECTION_VALUE_SETTING), direction) + if direction.length_squared() <= 0.0001: + return Vector2(0.8, 0.3) + return direction.normalized() + + +static func get_global_wind_speed(fallback: float = 1.0) -> float: + ensure_wind_project_settings() + if ProjectSettings.has_setting(String(WIND_SPEED_VALUE_SETTING)): + return maxf(float(ProjectSettings.get_setting(String(WIND_SPEED_VALUE_SETTING), fallback)), 0.0) + return maxf(fallback, 0.0) + + +static func get_precipitation_wind_strength(fallback: float = 4.0) -> float: + ensure_wind_project_settings() + if ProjectSettings.has_setting(String(WIND_STRENGTH_VALUE_SETTING)): + return maxf(float(ProjectSettings.get_setting(String(WIND_STRENGTH_VALUE_SETTING), fallback)), 0.0) + return maxf(fallback, 0.0) + + +static func get_global_wind_turbulence(fallback: float = 1.0) -> float: + ensure_wind_project_settings() + if ProjectSettings.has_setting(String(WIND_TURBULENCE_VALUE_SETTING)): + return maxf(float(ProjectSettings.get_setting(String(WIND_TURBULENCE_VALUE_SETTING), fallback)), 0.0) + return maxf(fallback, 0.0) + + +static func get_weather_controlled_wind_speed(world_3d: World3D, fallback: float = 1.0) -> float: + if world_3d == null: + return get_global_wind_speed(fallback) + return _get_weather_controlled_wind_speed_for_state( + _weather_state_by_world.get(world_3d.get_instance_id(), {}), + fallback + ) + + +static func get_final_wind_speed(world_3d: World3D, fallback: float = 1.0) -> float: + var base_speed := get_weather_controlled_wind_speed(world_3d, fallback) + if base_speed <= 0.0001: + return 0.0 + + var wind_time := get_weather_controlled_wind_time(world_3d) + var turbulence := get_global_wind_turbulence(1.0) + var strength_ratio := clampf(base_speed / 6.0, 0.0, 1.0) + var modulation_depth := turbulence * lerpf(0.05, 0.26, strength_ratio) + + var primary_noise := _sample_wind_noise_1d(wind_time * WIND_GUST_PRIMARY_RATE) + var secondary_noise := _sample_wind_noise_1d(wind_time * WIND_GUST_SECONDARY_RATE + 13.7) + var macro_noise := _sample_wind_noise_1d(wind_time * WIND_GUST_MACRO_RATE + 47.3) + + var gust := pow(maxf(primary_noise, 0.0), 1.7) * 0.7 + pow(maxf(secondary_noise, 0.0), 2.1) * 0.3 + var lull := pow(maxf(-macro_noise, 0.0), 1.35) + var wind_factor := 1.0 + gust * modulation_depth - lull * modulation_depth * 0.55 + + return maxf(base_speed * maxf(wind_factor, 0.1), 0.0) + + +static func get_final_wind_direction(world_3d: World3D, fallback: Vector2 = Vector2(0.8, 0.3)) -> Vector2: + return get_global_wind_direction(fallback) + + +static func get_weather_controlled_wind_time(world_3d: World3D) -> float: + if world_3d == null: + return 0.0 + var state: Dictionary = _weather_state_by_world.get(world_3d.get_instance_id(), {}) + if state.is_empty(): + return 0.0 + return float(state.get("wind_time", 0.0)) + + +static func _sample_wind_noise_1d(position: float) -> float: + var cell := floori(position) + var local := position - float(cell) + var smooth := local * local * (3.0 - 2.0 * local) + return lerpf(_hash_wind_noise(cell), _hash_wind_noise(cell + 1), smooth) + + +static func _hash_wind_noise(index: int) -> float: + var value := sin(float(index) * 12.9898 + 78.233) * 43758.5453 + return (value - floor(value)) * 2.0 - 1.0 + + +static func get_configured_visible_rain_probe_field_layout( + world_3d: World3D, + cache_key: int, + camera: Camera3D +) -> Dictionary: + if world_3d == null or cache_key < 0: + return {} + + var world_id := world_3d.get_instance_id() + var world_bucket: Dictionary = _visible_rain_probe_configs_by_world.get(world_id, {}) + var config: Dictionary = world_bucket.get(cache_key, {}) + if config.is_empty(): + return {} + + return _build_visible_rain_probe_field_layout( + camera, + int(config.get("max_probes", 24)), + float(config.get("distance", 8.0)) + ) + + +static func get_configured_visible_rain_probe_spacing( + world_3d: World3D, + cache_key: int, + camera: Camera3D +) -> float: + var layout: Dictionary = get_configured_visible_rain_probe_field_layout(world_3d, cache_key, camera) + return _get_visible_rain_probe_layout_spacing(layout) + + +static func get_registered_visible_rain_probe_positions( + world_3d: World3D, + view_transform: Transform3D, + camera: Camera3D +) -> PackedVector3Array: + if world_3d == null: + return PackedVector3Array() + + var world_id := world_3d.get_instance_id() + var world_bucket: Dictionary = _visible_rain_probe_configs_by_world.get(world_id, {}) + if world_bucket.is_empty(): + return PackedVector3Array() + + var positions := PackedVector3Array() + for cache_key in world_bucket.keys(): + var layout: Dictionary = get_configured_visible_rain_probe_field_layout(world_3d, int(cache_key), camera) + if layout.is_empty(): + continue + + var field_positions := get_visible_rain_probe_positions( + view_transform, + camera, + int(layout.get("probe_columns", 1)), + int(layout.get("probe_rows", 1)), + int(layout.get("probe_depth_slices", 1)), + float(layout.get("near_depth", 0.5)), + float(layout.get("far_depth", 1.0)), + float(layout.get("field_scale", 1.0)) + ) + positions.append_array(field_positions) + + return positions + + +static func get_rain_participation_strength( + world_3d: World3D, + world_position: Vector3, + base_strength: float +) -> float: + return get_rain_participation_strength_for_volumes( + _get_active_rain_volumes(world_3d), + world_position, + base_strength + ) + + +static func get_rain_lightning_multiplier( + world_3d: World3D, + world_position: Vector3 +) -> float: + return get_rain_lightning_multiplier_for_volumes( + _get_active_rain_volumes(world_3d), + world_position + ) + + +static func get_rain_lightning_multiplier_for_volumes( + volumes: Array, + world_position: Vector3 +) -> float: + return _get_rain_lightning_multiplier_for_sorted_volumes(_sort_rain_volumes(volumes), world_position) + + +static func _get_rain_lightning_multiplier_for_sorted_volumes( + sorted_volumes: Array, + world_position: Vector3 +) -> float: + var multiplier := 1.0 + for volume in sorted_volumes: + var blend: float = volume.get_precipitation_blend(world_position) + if blend <= 0.0: + continue + + multiplier *= lerpf(1.0, volume.get_lightning_multiplier(), blend) + return maxf(multiplier, 0.0) + + +static func get_rain_participation_strength_for_volumes( + volumes: Array, + world_position: Vector3, + base_strength: float +) -> float: + return _get_rain_participation_strength_for_sorted_volumes(_sort_rain_volumes(volumes), world_position, base_strength) + + +static func _get_rain_participation_strength_for_sorted_volumes( + sorted_volumes: Array, + world_position: Vector3, + base_strength: float +) -> float: + var intensity: float = clampf(base_strength, 0.0, 1.0) + for volume in sorted_volumes: + var blend: float = volume.get_precipitation_blend(world_position) + if blend <= 0.0: + continue + + var precipitation_delta: float = volume.get_precipitation_delta() * blend + var precipitation_multiplier: float = lerpf(1.0, volume.get_precipitation_multiplier(), blend) + intensity = clampf((intensity + precipitation_delta) * precipitation_multiplier, 0.0, 1.0) + return intensity + + +static func _get_rain_probe_render_strength_for_sorted_volumes( + sorted_volumes: Array, + world_position: Vector3, + base_strength: float +) -> float: + var intensity: float = clampf(base_strength, 0.0, 1.0) + for volume in sorted_volumes: + var blend: float = volume.get_precipitation_blend(world_position) + if blend <= 0.0: + continue + + var precipitation_multiplier: float = volume.get_precipitation_multiplier() + var full_intensity: float = clampf( + (intensity + volume.get_precipitation_delta()) * precipitation_multiplier, + 0.0, + 1.0 + ) + var effective_blend: float = blend + if full_intensity < intensity - 0.0001 and blend < 1.0: + effective_blend = _remap_probe_render_suppressive_blend(blend) + + var precipitation_delta: float = volume.get_precipitation_delta() * effective_blend + var blended_multiplier: float = lerpf(1.0, precipitation_multiplier, effective_blend) + intensity = clampf((intensity + precipitation_delta) * blended_multiplier, 0.0, 1.0) + return intensity + + +static func _get_rain_participation_strength_for_volumes_footprint_min( + sorted_volumes: Array, + world_position: Vector3, + base_strength: float, + footprint_half_extent: float +) -> float: + var min_intensity := 1.0 + + for sample_offset_scale in RAIN_FIELD_FOOTPRINT_OFFSETS: + min_intensity = minf( + min_intensity, + _get_rain_probe_render_strength_for_sorted_volumes( + sorted_volumes, + world_position + sample_offset_scale * footprint_half_extent, + base_strength + ) + ) + if min_intensity <= 0.001: + return 0.0 + + return clampf(min_intensity, 0.0, 1.0) + + +static func get_rain_render_field_state( + world_3d: World3D, + cache_key: int, + layer_key: StringName, + view_transform: Transform3D, + camera: Camera3D, + view_origin: Vector3, + sample_y: float, + layer_center_y: float, + base_strength: float, + half_extents: Vector3, + cell_spacing: float, + jitter_ratio: float, + rain_direction: Vector3, + rain_motion_half_span: float +) -> Dictionary: + if world_3d == null or cache_key < 0 or layer_key == &"": + return { + "count": 0, + "positions": PackedVector3Array(), + "custom_data": PackedColorArray(), + } + + var clamped_strength: float = clampf(base_strength, 0.0, 1.0) + var active_volumes: Array = _get_active_rain_volumes(world_3d) + if clamped_strength <= 0.001 and active_volumes.is_empty(): + clear_rain_render_field_cache(world_3d, cache_key) + return { + "count": 0, + "positions": PackedVector3Array(), + "custom_data": PackedColorArray(), + } + + var spacing: float = maxf(cell_spacing, 0.1) + var safe_half_extents := Vector3( + maxf(absf(half_extents.x), spacing * 0.5), + maxf(absf(half_extents.y), 0.1), + maxf(absf(half_extents.z), spacing * 0.5) + ) + var radius_x: int = maxi(1, int(ceil(safe_half_extents.x / spacing))) + var radius_z: int = maxi(1, int(ceil(safe_half_extents.z / spacing))) + var columns: int = radius_x * 2 + 1 + var rows: int = radius_z * 2 + 1 + var max_count: int = columns * rows + var cache: Dictionary = _ensure_rain_render_field_cache(world_3d, cache_key, layer_key, max_count) + var positions: PackedVector3Array = cache.get("positions", PackedVector3Array()) + var custom_data: PackedColorArray = cache.get("custom_data", PackedColorArray()) + var last_refresh_time_msec: int = int(cache.get("last_refresh_time_msec", 0)) + var cached_active_count: int = int(cache.get("active_count", 0)) + var current_time_msec: int = Time.get_ticks_msec() + if ( + last_refresh_time_msec > 0 + and current_time_msec - last_refresh_time_msec < RAIN_FIELD_RESAMPLE_INTERVAL_MSEC + ): + return { + "count": cached_active_count, + "positions": positions, + "custom_data": custom_data, + } + + var jitter_amount: float = clampf(jitter_ratio, 0.0, 1.0) * spacing * 0.5 + var normalized_rain_direction := rain_direction.normalized() + var snapped_grid_x: int = int(floor(view_origin.x / spacing)) + var snapped_grid_z: int = int(floor(view_origin.z / spacing)) + var active_count: int = 0 + var probe_layout: Dictionary = {} + var probe_values := PackedFloat32Array() + + if camera != null: + probe_layout = get_configured_visible_rain_probe_field_layout(world_3d, cache_key, camera) + if not probe_layout.is_empty(): + probe_values = _get_visible_rain_probe_values_for_layout( + world_3d, + cache_key, + view_transform, + camera, + clamped_strength, + active_volumes, + probe_layout + ) + + for row_index in range(rows): + var grid_z: int = snapped_grid_z + row_index - radius_z + for column_index in range(columns): + var grid_x: int = snapped_grid_x + column_index - radius_x + var jitter_x: float = _get_rain_field_jitter(grid_x, grid_z, 17) * jitter_amount + var jitter_z: float = _get_rain_field_jitter(grid_x, grid_z, 43) * jitter_amount + var world_x: float = float(grid_x) * spacing + jitter_x + var world_z: float = float(grid_z) * spacing + jitter_z + if absf(world_x - view_origin.x) > safe_half_extents.x + spacing: + continue + if absf(world_z - view_origin.z) > safe_half_extents.z + spacing: + continue + + var instance_position := Vector3(world_x, layer_center_y, world_z) + var intensity: float = _get_rain_participation_strength_for_volumes_footprint_min( + active_volumes, + instance_position, + clamped_strength, + spacing * 0.45 + ) + var feather_mask := _get_rain_field_feather_render_mask_for_volumes( + active_volumes, + instance_position, + clamped_strength, + spacing * 0.45 + ) + if rain_motion_half_span > 0.0: + for sample_factor in RAIN_FIELD_LOWER_CULL_SAMPLE_FACTORS: + var lower_sample_position := instance_position + normalized_rain_direction * (rain_motion_half_span * float(sample_factor)) + intensity = minf( + intensity, + _get_rain_participation_strength_for_volumes_footprint_min( + active_volumes, + lower_sample_position, + clamped_strength, + spacing * 0.45 + ) + ) + feather_mask = minf( + feather_mask, + _get_rain_field_feather_render_mask_for_volumes( + active_volumes, + lower_sample_position, + clamped_strength, + spacing * 0.45 + ) + ) + if not probe_layout.is_empty() and not probe_values.is_empty(): + var probe_intensity := _sample_visible_rain_probe_layout_footprint_min( + view_transform, + probe_layout, + instance_position, + spacing * 0.45, + probe_values + ) + if probe_intensity >= 0.0: + intensity = minf(intensity, probe_intensity) + if rain_motion_half_span > 0.0: + for sample_factor in RAIN_FIELD_LOWER_CULL_SAMPLE_FACTORS: + var lower_probe_sample_position := instance_position + normalized_rain_direction * (rain_motion_half_span * float(sample_factor)) + var lower_probe_intensity := _sample_visible_rain_probe_layout_footprint_min( + view_transform, + probe_layout, + lower_probe_sample_position, + spacing * 0.45, + probe_values + ) + if lower_probe_intensity >= 0.0: + intensity = minf(intensity, lower_probe_intensity) + if intensity <= 0.001 or feather_mask <= RAIN_FIELD_FEATHER_RENDER_CUTOFF: + continue + + positions[active_count] = instance_position + custom_data[active_count] = Color( + _get_rain_field_phase(grid_x, grid_z), + _get_rain_field_instance_alpha(intensity) * _normalize_rain_field_feather_mask(feather_mask), + _get_rain_field_variation(grid_x, grid_z), + _hash_to_unit_float(grid_x, grid_z, 313) + ) + active_count += 1 + + cache["positions"] = positions + cache["custom_data"] = custom_data + cache["active_count"] = active_count + cache["last_refresh_time_msec"] = current_time_msec + _store_rain_render_field_cache(world_3d, cache_key, layer_key, cache) + return { + "count": active_count, + "positions": positions, + "custom_data": custom_data, + } + + +static func get_visible_rain_participation_strength( + world_3d: World3D, + cache_key: int, + view_transform: Transform3D, + camera: Camera3D, + base_strength: float, + probe_columns: int, + probe_rows: int, + probe_depth_slices: int, + near_depth: float, + far_depth: float, + field_scale: float, + refresh_budget: int +) -> float: + var state: Dictionary = get_visible_rain_probe_field_state( + world_3d, + cache_key, + view_transform, + camera, + base_strength, + probe_columns, + probe_rows, + probe_depth_slices, + near_depth, + far_depth, + field_scale, + refresh_budget + ) + return float(state.get("strength", 0.0)) + + +static func get_configured_visible_rain_participation_strength( + world_3d: World3D, + cache_key: int, + view_transform: Transform3D, + camera: Camera3D, + base_strength: float +) -> float: + var state := get_configured_visible_rain_probe_field_state( + world_3d, + cache_key, + view_transform, + camera, + base_strength + ) + return float(state.get("strength", 0.0)) + + +static func get_visible_rain_probe_field_state( + world_3d: World3D, + cache_key: int, + view_transform: Transform3D, + camera: Camera3D, + base_strength: float, + probe_columns: int, + probe_rows: int, + probe_depth_slices: int, + near_depth: float, + far_depth: float, + field_scale: float, + refresh_budget: int +) -> Dictionary: + if world_3d == null or cache_key < 0: + return { + "strength": 0.0, + "nearest_depth": 0.0, + "has_visible_rain": false, + } + + var clamped_strength: float = clampf(base_strength, 0.0, 1.0) + var active_volumes: Array = _get_active_rain_volumes(world_3d) + if clamped_strength <= 0.001 and active_volumes.is_empty(): + clear_visible_rain_participation_cache(world_3d, cache_key) + return { + "strength": 0.0, + "nearest_depth": 0.0, + "has_visible_rain": false, + } + + var columns: int = maxi(probe_columns, 1) + var rows: int = maxi(probe_rows, 1) + var depth_slices: int = maxi(probe_depth_slices, 1) + var probe_count: int = columns * rows * depth_slices + var budget: int = clampi(refresh_budget, 1, probe_count) + var cache: Dictionary = _ensure_visible_rain_probe_field_cache(world_3d, cache_key, probe_count) + var values: PackedFloat32Array = cache.get("values", PackedFloat32Array()) + var cursor: int = int(cache.get("cursor", 0)) + var ready: bool = bool(cache.get("ready", false)) + + if not ready: + budget = probe_count + + for offset in range(budget): + var probe_index: int = (cursor + offset) % probe_count + var probe_position: Vector3 = _get_visible_rain_probe_world_position( + view_transform, + camera, + columns, + rows, + depth_slices, + near_depth, + far_depth, + field_scale, + probe_index + ) + values[probe_index] = _get_rain_probe_render_strength_for_sorted_volumes( + active_volumes, + probe_position, + clamped_strength + ) + + cache["values"] = values + cache["cursor"] = (cursor + budget) % probe_count + cache["ready"] = true + _store_visible_rain_probe_field_cache(world_3d, cache_key, cache) + return _get_visible_rain_probe_field_state( + values, + columns * rows, + depth_slices, + near_depth, + far_depth + ) + + +static func get_configured_visible_rain_probe_field_state( + world_3d: World3D, + cache_key: int, + view_transform: Transform3D, + camera: Camera3D, + base_strength: float +) -> Dictionary: + var layout: Dictionary = get_configured_visible_rain_probe_field_layout(world_3d, cache_key, camera) + if layout.is_empty(): + return { + "strength": 0.0, + "nearest_depth": 0.0, + "has_visible_rain": false, + } + + return get_visible_rain_probe_field_state( + world_3d, + cache_key, + view_transform, + camera, + base_strength, + int(layout.get("probe_columns", 1)), + int(layout.get("probe_rows", 1)), + int(layout.get("probe_depth_slices", 1)), + float(layout.get("near_depth", 0.5)), + float(layout.get("far_depth", 1.0)), + float(layout.get("field_scale", 1.0)), + int(layout.get("refresh_budget", 1)) + ) + + +static func get_visible_rain_probe_positions( + view_transform: Transform3D, + camera: Camera3D, + probe_columns: int, + probe_rows: int, + probe_depth_slices: int, + near_depth: float, + far_depth: float, + field_scale: float +) -> PackedVector3Array: + var columns: int = maxi(probe_columns, 1) + var rows: int = maxi(probe_rows, 1) + var depth_slices: int = maxi(probe_depth_slices, 1) + var probe_count: int = columns * rows * depth_slices + var positions := PackedVector3Array() + positions.resize(probe_count) + + for probe_index in range(probe_count): + positions[probe_index] = _get_visible_rain_probe_world_position( + view_transform, + camera, + columns, + rows, + depth_slices, + near_depth, + far_depth, + field_scale, + probe_index + ) + + return positions + + +static func clear_visible_rain_participation_cache(world_3d: World3D, cache_key: int) -> void: + if world_3d == null or cache_key < 0: + return + + var world_id: int = world_3d.get_instance_id() + var world_fields: Dictionary = _visible_rain_probe_fields_by_world.get(world_id, {}) + if world_fields.is_empty(): + return + + world_fields.erase(cache_key) + if world_fields.is_empty(): + _visible_rain_probe_fields_by_world.erase(world_id) + else: + _visible_rain_probe_fields_by_world[world_id] = world_fields + + +static func clear_rain_render_field_cache(world_3d: World3D, cache_key: int) -> void: + if world_3d == null or cache_key < 0: + return + + var world_id: int = world_3d.get_instance_id() + var world_fields: Dictionary = _rain_render_fields_by_world.get(world_id, {}) + if world_fields.is_empty(): + return + + var cache_prefix: String = "%s:" % [cache_key] + var stale_keys: Array[String] = [] + for field_cache_id in world_fields.keys(): + var field_cache_key: String = str(field_cache_id) + if field_cache_key.begins_with(cache_prefix): + stale_keys.append(field_cache_key) + + for field_cache_key in stale_keys: + world_fields.erase(field_cache_key) + + if world_fields.is_empty(): + _rain_render_fields_by_world.erase(world_id) + else: + _rain_render_fields_by_world[world_id] = world_fields + + +static func _collect_rain_volumes_at_position(world_3d: World3D, world_position: Vector3) -> Array: + var volumes: Array = [] + for volume in _get_active_rain_volumes(world_3d): + if volume.get_precipitation_blend(world_position) > 0.0: + volumes.append(volume) + return volumes + + +static func _get_active_rain_volumes(world_3d: World3D) -> Array: + if world_3d == null: + return [] + + var world_id: int = world_3d.get_instance_id() + var world_bucket: Dictionary = _rain_volumes_by_world.get(world_id, {}) + if world_bucket.is_empty(): + return [] + + var stale_ids: Array[int] = [] + var volumes: Array = [] + for volume_id in world_bucket.keys(): + var volume := world_bucket[volume_id] as RainVolume + if not is_instance_valid(volume): + stale_ids.append(volume_id) + continue + if not volume.is_inside_tree() or volume.get_world_3d() != world_3d: + stale_ids.append(volume_id) + continue + if not volume.is_rain_volume_enabled(): + continue + volumes.append(volume) + + if not stale_ids.is_empty(): + for volume_id in stale_ids: + world_bucket.erase(volume_id) + if world_bucket.is_empty(): + _rain_volumes_by_world.erase(world_id) + else: + _rain_volumes_by_world[world_id] = world_bucket + + return _sort_rain_volumes(volumes) + + +static func _notify_rain_volumes_changed(world_3d: World3D) -> void: + if world_3d == null: + return + + var world_id := world_3d.get_instance_id() + _rain_volume_revision_by_world[world_id] = int(_rain_volume_revision_by_world.get(world_id, 0)) + 1 + _visible_rain_probe_fields_by_world.erase(world_id) + _rain_render_fields_by_world.erase(world_id) + + +static func _sort_rain_volumes(volumes: Array) -> Array: + var sorted_volumes: Array = [] + for volume in volumes: + var rain_volume := volume as RainVolume + if rain_volume == null: + continue + if not is_instance_valid(rain_volume): + continue + if not rain_volume.is_rain_volume_enabled(): + continue + sorted_volumes.append(rain_volume) + + sorted_volumes.sort_custom(func(a: RainVolume, b: RainVolume) -> bool: + if a.volume_priority == b.volume_priority: + return a.get_instance_id() < b.get_instance_id() + return a.volume_priority < b.volume_priority + ) + return sorted_volumes + + +static func _ensure_visible_rain_probe_field_cache(world_3d: World3D, cache_key: int, probe_count: int) -> Dictionary: + var world_id: int = world_3d.get_instance_id() + var world_fields: Dictionary = _visible_rain_probe_fields_by_world.get(world_id, {}) + var cache: Dictionary = world_fields.get(cache_key, {}) + var current_count: int = int(cache.get("count", -1)) + var values: PackedFloat32Array = cache.get("values", PackedFloat32Array()) + + if current_count != probe_count or values.size() != probe_count: + values = PackedFloat32Array() + values.resize(probe_count) + cache = { + "count": probe_count, + "cursor": 0, + "ready": false, + "values": values, + } + world_fields[cache_key] = cache + _visible_rain_probe_fields_by_world[world_id] = world_fields + + return cache + + +static func _apply_weather_controlled_wind(world_3d: World3D) -> void: + ensure_wind_project_settings() + RenderingServer.global_shader_parameter_set(StringName(WIND_DIRECTION_GLOBAL), get_final_wind_direction(world_3d)) + RenderingServer.global_shader_parameter_set( + StringName(WIND_SPEED_GLOBAL), + get_final_wind_speed(world_3d) + ) + RenderingServer.global_shader_parameter_set( + StringName(WIND_TIME_GLOBAL), + get_weather_controlled_wind_time(world_3d) + ) + + +static func _store_visible_rain_probe_field_cache(world_3d: World3D, cache_key: int, cache: Dictionary) -> void: + var world_id: int = world_3d.get_instance_id() + var world_fields: Dictionary = _visible_rain_probe_fields_by_world.get(world_id, {}) + world_fields[cache_key] = cache + _visible_rain_probe_fields_by_world[world_id] = world_fields + + +static func _ensure_rain_render_field_cache( + world_3d: World3D, + cache_key: int, + layer_key: StringName, + max_count: int +) -> Dictionary: + var world_id: int = world_3d.get_instance_id() + var world_fields: Dictionary = _rain_render_fields_by_world.get(world_id, {}) + var field_cache_id: String = _make_rain_render_field_cache_id(cache_key, layer_key) + var cache: Dictionary = world_fields.get(field_cache_id, {}) + var current_count: int = int(cache.get("max_count", -1)) + var positions: PackedVector3Array = cache.get("positions", PackedVector3Array()) + var custom_data: PackedColorArray = cache.get("custom_data", PackedColorArray()) + + if current_count != max_count or positions.size() != max_count or custom_data.size() != max_count: + positions = PackedVector3Array() + positions.resize(max_count) + custom_data = PackedColorArray() + custom_data.resize(max_count) + cache = { + "max_count": max_count, + "active_count": 0, + "last_refresh_time_msec": 0, + "positions": positions, + "custom_data": custom_data, + } + world_fields[field_cache_id] = cache + _rain_render_fields_by_world[world_id] = world_fields + + return cache + + +static func _store_rain_render_field_cache( + world_3d: World3D, + cache_key: int, + layer_key: StringName, + cache: Dictionary +) -> void: + var world_id: int = world_3d.get_instance_id() + var world_fields: Dictionary = _rain_render_fields_by_world.get(world_id, {}) + world_fields[_make_rain_render_field_cache_id(cache_key, layer_key)] = cache + _rain_render_fields_by_world[world_id] = world_fields + + +static func _make_rain_render_field_cache_id(cache_key: int, layer_key: StringName) -> String: + return "%s:%s" % [cache_key, String(layer_key)] + + + + +static func _build_visible_rain_probe_field_layout( + camera: Camera3D, + max_probes: int, + distance: float +) -> Dictionary: + var safe_max_probes: int = maxi(max_probes, 1) + var safe_far_depth: float = maxf(distance, 0.1) + var near_depth: float = minf(0.1, safe_far_depth) + var axis_count: int = maxi(int(ceil(pow(float(safe_max_probes), 1.0 / 3.0))), 1) + var probe_columns: int = axis_count + var probe_rows: int = axis_count + var probe_depth_slices: int = axis_count + var probe_count: int = probe_columns * probe_rows * probe_depth_slices + + while probe_count > safe_max_probes: + if probe_columns >= probe_rows and probe_columns >= probe_depth_slices and probe_columns > 1: + probe_columns -= 1 + elif probe_rows >= probe_depth_slices and probe_rows > 1: + probe_rows -= 1 + elif probe_depth_slices > 1: + probe_depth_slices -= 1 + else: + break + probe_count = probe_columns * probe_rows * probe_depth_slices + + return { + "max_probes": safe_max_probes, + "distance": safe_far_depth, + "probe_columns": probe_columns, + "probe_rows": probe_rows, + "probe_depth_slices": probe_depth_slices, + "near_depth": near_depth, + "far_depth": safe_far_depth, + "field_scale": 1.0, + "refresh_budget": probe_columns * probe_rows * probe_depth_slices, + } + + +static func _get_visible_rain_probe_layout_spacing(layout: Dictionary) -> float: + if layout.is_empty(): + return -1.0 + + var distance: float = maxf(float(layout.get("distance", 0.1)), 0.1) + var near_depth: float = clampf(float(layout.get("near_depth", 0.1)), 0.0, distance) + var depth_span: float = maxf(distance - near_depth, 0.1) + var field_scale: float = maxf(float(layout.get("field_scale", 1.0)), 0.01) + var half_extent: float = maxf(distance * field_scale * 0.5, 0.05) + var columns: int = maxi(int(layout.get("probe_columns", 1)), 1) + var rows: int = maxi(int(layout.get("probe_rows", 1)), 1) + var depth_slices: int = maxi(int(layout.get("probe_depth_slices", 1)), 1) + var lateral_spacing: float = (half_extent * 2.0) / maxf(float(columns - 1), 1.0) + var vertical_spacing: float = (half_extent * 2.0) / maxf(float(rows - 1), 1.0) + var depth_spacing: float = depth_span / maxf(float(depth_slices - 1), 1.0) + return maxf(minf(lateral_spacing, minf(vertical_spacing, depth_spacing)), 0.1) + + +static func _get_visible_rain_probe_world_position( + view_transform: Transform3D, + camera: Camera3D, + probe_columns: int, + probe_rows: int, + probe_depth_slices: int, + near_depth: float, + far_depth: float, + field_scale: float, + probe_index: int +) -> Vector3: + var slice_index: int = probe_index / (probe_columns * probe_rows) + var plane_index: int = probe_index % (probe_columns * probe_rows) + var row_index: int = plane_index / probe_columns + var column_index: int = plane_index % probe_columns + + var depth_t: float = 0.0 if probe_depth_slices <= 1 else float(slice_index) / float(probe_depth_slices - 1) + var u: float = 0.0 if probe_columns <= 1 else lerpf(-1.0, 1.0, float(column_index) / float(probe_columns - 1)) + var v: float = 0.0 if probe_rows <= 1 else lerpf(1.0, -1.0, float(row_index) / float(probe_rows - 1)) + + var safe_near_depth: float = minf(maxf(near_depth, 0.0), far_depth) + var depth_span: float = maxf(far_depth - safe_near_depth, 0.1) + var center_depth: float = safe_near_depth + depth_span * 0.5 + var half_extent: float = maxf(far_depth * field_scale * 0.5, 0.05) + var forward: Vector3 = -view_transform.basis.z + var right: Vector3 = view_transform.basis.x + var up: Vector3 = view_transform.basis.y + return ( + view_transform.origin + + forward * center_depth + + right * (u * half_extent) + + up * (v * half_extent) + + forward * ((depth_t - 0.5) * depth_span) + ) + + +static func _get_visible_rain_probe_half_extents(camera: Camera3D, depth: float, field_scale: float) -> Vector2: + var half_extent: float = maxf(depth * field_scale * 0.5, 0.05) + return Vector2(half_extent, half_extent) + + +static func _get_visible_rain_probe_values_for_layout( + world_3d: World3D, + cache_key: int, + view_transform: Transform3D, + camera: Camera3D, + base_strength: float, + active_volumes: Array, + layout: Dictionary +) -> PackedFloat32Array: + var columns: int = maxi(int(layout.get("probe_columns", 1)), 1) + var rows: int = maxi(int(layout.get("probe_rows", 1)), 1) + var depth_slices: int = maxi(int(layout.get("probe_depth_slices", 1)), 1) + var probe_count: int = columns * rows * depth_slices + var cache: Dictionary = _ensure_visible_rain_probe_field_cache(world_3d, cache_key, probe_count) + var values: PackedFloat32Array = cache.get("values", PackedFloat32Array()) + var near_depth: float = float(layout.get("near_depth", 0.5)) + var far_depth: float = float(layout.get("far_depth", 1.0)) + var field_scale: float = float(layout.get("field_scale", 1.0)) + + for probe_index in range(probe_count): + var probe_position: Vector3 = _get_visible_rain_probe_world_position( + view_transform, + camera, + columns, + rows, + depth_slices, + near_depth, + far_depth, + field_scale, + probe_index + ) + values[probe_index] = _get_rain_probe_render_strength_for_sorted_volumes( + active_volumes, + probe_position, + base_strength + ) + + cache["values"] = values + cache["cursor"] = 0 + cache["ready"] = true + _store_visible_rain_probe_field_cache(world_3d, cache_key, cache) + return values + + +static func _sample_visible_rain_probe_layout_nearest( + view_transform: Transform3D, + layout: Dictionary, + world_position: Vector3, + values: PackedFloat32Array +) -> float: + if layout.is_empty() or values.is_empty(): + return -1.0 + + var columns: int = maxi(int(layout.get("probe_columns", 1)), 1) + var rows: int = maxi(int(layout.get("probe_rows", 1)), 1) + var depth_slices: int = maxi(int(layout.get("probe_depth_slices", 1)), 1) + var near_depth: float = maxf(float(layout.get("near_depth", 0.1)), 0.0) + var far_depth: float = maxf(float(layout.get("far_depth", near_depth)), near_depth) + var field_scale: float = maxf(float(layout.get("field_scale", 1.0)), 0.01) + var half_extents: Vector2 = _get_visible_rain_probe_half_extents(null, far_depth, field_scale) + var forward: Vector3 = -view_transform.basis.z + var right: Vector3 = view_transform.basis.x + var up: Vector3 = view_transform.basis.y + var sample_offset: Vector3 = world_position - view_transform.origin + var sample_depth: float = forward.dot(sample_offset) + if sample_depth < near_depth or sample_depth > far_depth: + return -1.0 + + var sample_x: float = right.dot(sample_offset) + var sample_y: float = up.dot(sample_offset) + if absf(sample_x) > half_extents.x or absf(sample_y) > half_extents.y: + return -1.0 + + var column_t: float = 0.0 if columns <= 1 else clampf(sample_x / maxf(half_extents.x * 2.0, 0.0001) + 0.5, 0.0, 1.0) + var row_t: float = 0.0 if rows <= 1 else clampf(0.5 - sample_y / maxf(half_extents.y * 2.0, 0.0001), 0.0, 1.0) + var depth_t: float = 0.0 if depth_slices <= 1 else clampf( + (sample_depth - near_depth) / maxf(far_depth - near_depth, 0.0001), + 0.0, + 1.0 + ) + var column_index: int = int(round(column_t * float(columns - 1))) + var row_index: int = int(round(row_t * float(rows - 1))) + var slice_index: int = int(round(depth_t * float(depth_slices - 1))) + var probe_index: int = slice_index * columns * rows + row_index * columns + column_index + if probe_index < 0 or probe_index >= values.size(): + return -1.0 + return values[probe_index] + + +static func _sample_visible_rain_probe_layout_footprint_min( + view_transform: Transform3D, + layout: Dictionary, + world_position: Vector3, + footprint_half_extent: float, + values: PackedFloat32Array +) -> float: + var min_value := INF + var has_sample := false + + for sample_offset_scale in RAIN_FIELD_FOOTPRINT_OFFSETS: + var sample_value := _sample_visible_rain_probe_layout_nearest( + view_transform, + layout, + world_position + sample_offset_scale * footprint_half_extent, + values + ) + if sample_value < 0.0: + continue + min_value = minf(min_value, sample_value) + has_sample = true + if min_value <= 0.001: + return 0.0 + + if not has_sample: + return -1.0 + return min_value + + +static func _get_rain_field_feather_render_mask_for_volumes( + sorted_volumes: Array, + world_position: Vector3, + base_strength: float, + footprint_half_extent: float +) -> float: + var min_mask := 1.0 + + for sample_offset_scale in RAIN_FIELD_FOOTPRINT_OFFSETS: + min_mask = minf( + min_mask, + _get_rain_field_feather_render_mask_at_position( + sorted_volumes, + world_position + sample_offset_scale * footprint_half_extent, + base_strength + ) + ) + if min_mask <= 0.0: + return 0.0 + + return clampf(min_mask, 0.0, 1.0) + + +static func _remap_probe_render_suppressive_blend(blend: float) -> float: + return _smoothstepf( + clampf(blend, 0.0, 1.0), + RAIN_PROBE_SUPPRESSIVE_FEATHER_BLEND_START, + RAIN_PROBE_SUPPRESSIVE_FEATHER_BLEND_END + ) + + +static func _get_rain_field_feather_render_mask_at_position( + sorted_volumes: Array, + world_position: Vector3, + base_strength: float +) -> float: + var intensity: float = clampf(base_strength, 0.0, 1.0) + var render_mask := 1.0 + + for volume in sorted_volumes: + var blend: float = volume.get_precipitation_blend(world_position) + if blend <= 0.0: + continue + + var full_intensity: float = clampf( + (intensity + volume.get_precipitation_delta()) * volume.get_precipitation_multiplier(), + 0.0, + 1.0 + ) + var blended_intensity: float = clampf( + (intensity + volume.get_precipitation_delta() * blend) + * lerpf(1.0, volume.get_precipitation_multiplier(), blend), + 0.0, + 1.0 + ) + if full_intensity < intensity - 0.0001: + var suppression_strength := clampf( + (intensity - full_intensity) / maxf(intensity, 0.0001), + 0.0, + 1.0 + ) + var feather_visibility := 1.0 - smoothstep(0.08, 0.62, blend) + render_mask *= lerpf(1.0, feather_visibility, suppression_strength) + + intensity = blended_intensity + + return clampf(render_mask, 0.0, 1.0) + + +static func _get_visible_rain_probe_field_max(values: PackedFloat32Array) -> float: + var visible_intensity: float = 0.0 + for value in values: + visible_intensity = maxf(visible_intensity, value) + return visible_intensity + + +static func _get_visible_rain_probe_field_state( + values: PackedFloat32Array, + plane_probe_count: int, + probe_depth_slices: int, + near_depth: float, + far_depth: float +) -> Dictionary: + var visible_intensity: float = 0.0 + var nearest_visible_depth: float = 0.0 + var has_visible_rain: bool = false + + for probe_index in range(values.size()): + var value: float = values[probe_index] + if value <= 0.001: + continue + + visible_intensity = maxf(visible_intensity, value) + var depth: float = _get_visible_rain_probe_depth( + plane_probe_count, + probe_depth_slices, + near_depth, + far_depth, + probe_index + ) + if not has_visible_rain or depth < nearest_visible_depth: + nearest_visible_depth = depth + has_visible_rain = true + + return { + "strength": visible_intensity, + "nearest_depth": nearest_visible_depth, + "has_visible_rain": has_visible_rain, + } + + +static func _get_visible_rain_probe_depth( + plane_probe_count: int, + probe_depth_slices: int, + near_depth: float, + far_depth: float, + probe_index: int +) -> float: + var safe_plane_probe_count: int = maxi(plane_probe_count, 1) + var safe_depth_slices: int = maxi(probe_depth_slices, 1) + var slice_index: int = 0 if safe_depth_slices <= 1 else probe_index / safe_plane_probe_count + slice_index = clampi(slice_index, 0, safe_depth_slices - 1) + var depth_t: float = 0.0 if safe_depth_slices <= 1 else float(slice_index) / float(safe_depth_slices - 1) + return lerpf(maxf(near_depth, 0.1), maxf(far_depth, near_depth), depth_t) + + +static func _get_rain_field_jitter(grid_x: int, grid_z: int, seed: int) -> float: + return _hash_to_unit_float(grid_x, grid_z, seed) * 2.0 - 1.0 + + +static func _get_rain_field_phase(grid_x: int, grid_z: int) -> float: + return _hash_to_unit_float(grid_x, grid_z, 101) + + +static func _get_rain_field_instance_alpha(intensity: float) -> float: + return pow(clampf(intensity, 0.0, 1.0), 1.65) + + +static func _normalize_rain_field_feather_mask(feather_mask: float) -> float: + return clampf( + (feather_mask - RAIN_FIELD_FEATHER_RENDER_CUTOFF) / maxf(1.0 - RAIN_FIELD_FEATHER_RENDER_CUTOFF, 0.0001), + 0.0, + 1.0 + ) + + +static func _smoothstepf(value: float, start: float, end: float) -> float: + var t := clampf((value - start) / maxf(end - start, 0.0001), 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) + + +static func _get_rain_field_variation(grid_x: int, grid_z: int) -> float: + return _hash_to_unit_float(grid_x, grid_z, 211) + + +static func _hash_to_unit_float(grid_x: int, grid_z: int, seed: int) -> float: + var hash_value: int = int(grid_x * 73856093) ^ int(grid_z * 19349663) ^ int(seed * 83492791) + hash_value = int(((hash_value << 13) ^ hash_value) & 0x7fffffff) + return float(hash_value % 1000000) / 999999.0 diff --git a/demo/addons/gnd_weather/WeatherServer.gd.uid b/demo/addons/gnd_weather/WeatherServer.gd.uid new file mode 100644 index 00000000..bd4f7f2a --- /dev/null +++ b/demo/addons/gnd_weather/WeatherServer.gd.uid @@ -0,0 +1 @@ +uid://bd5rvim4puk06 diff --git a/demo/addons/gnd_weather/plugin.cfg b/demo/addons/gnd_weather/plugin.cfg new file mode 100644 index 00000000..7f08b5b6 --- /dev/null +++ b/demo/addons/gnd_weather/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GND Weather" +description="Weather runtime systems: rain volumes, weather node, and weather server." +author="GamesNotDeveloped" +version="0.1" +script="plugin.gd" diff --git a/demo/addons/gnd_weather/plugin.gd b/demo/addons/gnd_weather/plugin.gd new file mode 100644 index 00000000..928a666d --- /dev/null +++ b/demo/addons/gnd_weather/plugin.gd @@ -0,0 +1,311 @@ +@tool +extends EditorPlugin + +const RAIN_VOLUME_GIZMO_PLUGIN_SCRIPT := preload("res://addons/gnd_weather/RainVolumeGizmoPlugin.gd") +const PROBE_BUTTON_TEXT := "Weather Probes" +const RAIN_MESHES_BUTTON_TEXT := "Rain Meshes" +const PROBE_REFRESH_INTERVAL := 0.2 +const RAIN_MESH_SYNC_INTERVAL := 1.0 +const PROBE_WORLD_SCALE_MIN := 0.03 +const PROBE_WORLD_SCALE_MAX := 0.18 +const PROBE_WORLD_SCALE_FACTOR := 0.015 +const PROBE_COLOR := Color(0.25, 0.78, 1.0, 0.92) +const RAIN_MESH_NEAR_COLOR := Color(0.18, 1.0, 0.72, 0.36) +const RAIN_MESH_MID_COLOR := Color(1.0, 0.82, 0.2, 0.3) + +var _rain_volume_gizmo_plugin: RainVolumeGizmoPlugin +var _weather_probes_button: CheckBox +var _rain_meshes_button: CheckBox +var _probe_refresh_timer: Timer +var _probe_mesh: SphereMesh +var _probe_material: StandardMaterial3D +var _probe_instance_rids: Array[RID] = [] +var _preview_weather_node_ids: Array[int] = [] +var _debug_rain_mesh_node_ids: Array[int] = [] +var _rain_mesh_sync_accumulator: float = 0.0 + + +func _enter_tree() -> void: + WeatherServer.ensure_wind_project_settings() + _rain_volume_gizmo_plugin = RAIN_VOLUME_GIZMO_PLUGIN_SCRIPT.new() + _rain_volume_gizmo_plugin.undo_redo = get_undo_redo() + add_node_3d_gizmo_plugin(_rain_volume_gizmo_plugin) + _weather_probes_button = CheckBox.new() + _weather_probes_button.text = PROBE_BUTTON_TEXT + _weather_probes_button.tooltip_text = "Show weather rain probes in the 3D editor viewport." + _weather_probes_button.toggled.connect(_on_weather_probes_toggled) + add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, _weather_probes_button) + _rain_meshes_button = CheckBox.new() + _rain_meshes_button.text = RAIN_MESHES_BUTTON_TEXT + _rain_meshes_button.tooltip_text = "Show rain mesh spawn positions in the 3D editor viewport." + _rain_meshes_button.toggled.connect(_on_rain_meshes_toggled) + add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, _rain_meshes_button) + _probe_refresh_timer = Timer.new() + _probe_refresh_timer.wait_time = PROBE_REFRESH_INTERVAL + _probe_refresh_timer.one_shot = false + _probe_refresh_timer.timeout.connect(_on_probe_refresh_timeout) + add_child(_probe_refresh_timer, false, INTERNAL_MODE_FRONT) + _probe_refresh_timer.start() + set_force_draw_over_forwarding_enabled() + + +func _exit_tree() -> void: + _clear_probe_preview() + _clear_rain_mesh_preview() + _clear_registered_probe_configs() + if _probe_refresh_timer != null: + _probe_refresh_timer.stop() + _probe_refresh_timer.queue_free() + _probe_refresh_timer = null + if _weather_probes_button != null: + remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, _weather_probes_button) + _weather_probes_button.queue_free() + _weather_probes_button = null + if _rain_meshes_button != null: + remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, _rain_meshes_button) + _rain_meshes_button.queue_free() + _rain_meshes_button = null + if _rain_volume_gizmo_plugin != null: + remove_node_3d_gizmo_plugin(_rain_volume_gizmo_plugin) + _rain_volume_gizmo_plugin = null + + +func _handles(object: Object) -> bool: + return object is Node3D + + +func _forward_3d_draw_over_viewport(overlay: Control) -> void: + return + + +func _on_weather_probes_toggled(_enabled: bool) -> void: + if _weather_probes_button != null and _weather_probes_button.button_pressed: + _sync_debug_preview(true) + else: + _clear_probe_preview() + _update_refresh_timer_state() + if not _is_any_debug_preview_enabled(): + _clear_registered_probe_configs() + + +func _on_rain_meshes_toggled(_enabled: bool) -> void: + if _rain_meshes_button != null and _rain_meshes_button.button_pressed: + _rain_mesh_sync_accumulator = RAIN_MESH_SYNC_INTERVAL + _sync_debug_preview(true) + else: + _clear_rain_mesh_preview() + _update_refresh_timer_state() + if not _is_any_debug_preview_enabled(): + _clear_registered_probe_configs() + + +func _on_probe_refresh_timeout() -> void: + _sync_weather_node_editor_preview_cameras(_get_editor_camera()) + if not _is_any_debug_preview_enabled(): + return + _rain_mesh_sync_accumulator += PROBE_REFRESH_INTERVAL + _sync_debug_preview(false) + + +func _get_editor_camera() -> Camera3D: + var editor_viewport := get_editor_interface().get_editor_viewport_3d(0) + if editor_viewport == null: + return null + return editor_viewport.get_camera_3d() + + +func _sync_debug_preview(force_rain_mesh_sync: bool = false) -> void: + var camera := _get_editor_camera() + _sync_weather_node_editor_preview_cameras(camera) + + if _rain_meshes_button != null and _rain_meshes_button.button_pressed: + if force_rain_mesh_sync or _rain_mesh_sync_accumulator >= RAIN_MESH_SYNC_INTERVAL: + _sync_rain_mesh_debug_preview() + _rain_mesh_sync_accumulator = 0.0 + else: + _clear_rain_mesh_preview() + _rain_mesh_sync_accumulator = 0.0 + + if camera == null: + _clear_probe_preview() + return + + var world_3d := camera.get_world_3d() + if world_3d == null: + _clear_probe_preview() + return + + _sync_registered_probe_configs(world_3d) + if _weather_probes_button != null and _weather_probes_button.button_pressed: + _sync_probe_preview(world_3d, camera) + else: + _clear_probe_preview() + + +func _sync_probe_preview(world_3d: World3D, camera: Camera3D) -> void: + var probe_positions := WeatherServer.get_registered_visible_rain_probe_positions(world_3d, camera.global_transform, camera) + _ensure_probe_mesh_resources() + _ensure_instance_count(_probe_instance_rids, probe_positions.size(), _probe_mesh.get_rid(), RID(), world_3d) + + for index in range(probe_positions.size()): + var probe_position: Vector3 = probe_positions[index] + var probe_instance := _probe_instance_rids[index] + if not probe_instance.is_valid(): + continue + + var distance_to_camera := camera.global_position.distance_to(probe_position) + var probe_scale := clampf(distance_to_camera * PROBE_WORLD_SCALE_FACTOR, PROBE_WORLD_SCALE_MIN, PROBE_WORLD_SCALE_MAX) + var transform := Transform3D(Basis().scaled(Vector3.ONE * probe_scale), probe_position) + RenderingServer.instance_set_transform(probe_instance, transform) + RenderingServer.instance_set_scenario(probe_instance, world_3d.scenario) + + +func _sync_rain_mesh_debug_preview() -> void: + var active_weather_node_ids: Array[int] = [] + for weather_node in _get_edited_weather_nodes(): + if weather_node == null: + continue + active_weather_node_ids.append(weather_node.get_instance_id()) + weather_node.set_rain_mesh_debug_preview(true, RAIN_MESH_NEAR_COLOR, RAIN_MESH_MID_COLOR) + + for weather_node_id in _debug_rain_mesh_node_ids: + if active_weather_node_ids.has(weather_node_id): + continue + var stale_node := instance_from_id(weather_node_id) as WeatherNode + if stale_node != null: + stale_node.set_rain_mesh_debug_preview(false, RAIN_MESH_NEAR_COLOR, RAIN_MESH_MID_COLOR) + + _debug_rain_mesh_node_ids = active_weather_node_ids + + +func _ensure_probe_mesh_resources() -> void: + if _probe_mesh != null and _probe_material != null: + return + + _probe_material = StandardMaterial3D.new() + _probe_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + _probe_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + _probe_material.no_depth_test = false + _probe_material.albedo_color = PROBE_COLOR + _probe_material.emission_enabled = true + _probe_material.emission = PROBE_COLOR + + _probe_mesh = SphereMesh.new() + _probe_mesh.radius = 0.5 + _probe_mesh.height = 1.0 + _probe_mesh.radial_segments = 8 + _probe_mesh.rings = 4 + _probe_mesh.material = _probe_material + + +func _ensure_instance_count( + instance_rids: Array[RID], + target_count: int, + mesh_rid: RID, + material_rid: RID, + world_3d: World3D +) -> void: + while instance_rids.size() < target_count: + var debug_instance := RenderingServer.instance_create() + RenderingServer.instance_set_base(debug_instance, mesh_rid) + if material_rid.is_valid(): + RenderingServer.instance_geometry_set_material_override(debug_instance, material_rid) + RenderingServer.instance_set_scenario(debug_instance, world_3d.scenario) + instance_rids.append(debug_instance) + + while instance_rids.size() > target_count: + var debug_instance := instance_rids.pop_back() + if debug_instance.is_valid(): + RenderingServer.free_rid(debug_instance) + + +func _clear_probe_preview() -> void: + for probe_instance in _probe_instance_rids: + if probe_instance.is_valid(): + RenderingServer.free_rid(probe_instance) + _probe_instance_rids.clear() + + +func _clear_rain_mesh_preview() -> void: + for weather_node_id in _debug_rain_mesh_node_ids: + var weather_node := instance_from_id(weather_node_id) as WeatherNode + if weather_node != null: + weather_node.set_rain_mesh_debug_preview(false, RAIN_MESH_NEAR_COLOR, RAIN_MESH_MID_COLOR) + _debug_rain_mesh_node_ids.clear() + + +func _clear_registered_probe_configs() -> void: + var camera := _get_editor_camera() + if camera == null: + _preview_weather_node_ids.clear() + return + + var world_3d := camera.get_world_3d() + if world_3d == null: + _preview_weather_node_ids.clear() + return + + for weather_node_id in _preview_weather_node_ids: + WeatherServer.clear_visible_rain_probe_field_config(world_3d, weather_node_id) + _preview_weather_node_ids.clear() + + +func _sync_registered_probe_configs(world_3d: World3D) -> void: + var active_weather_node_ids: Array[int] = [] + for weather_node in _get_edited_weather_nodes(): + if weather_node == null: + continue + + var weather_node_id: int = weather_node.get_instance_id() + active_weather_node_ids.append(weather_node_id) + WeatherServer.configure_visible_rain_probe_field( + world_3d, + weather_node_id, + weather_node.rain_probe_max_count, + weather_node.rain_probe_distance + ) + + for weather_node_id in _preview_weather_node_ids: + if active_weather_node_ids.has(weather_node_id): + continue + WeatherServer.clear_visible_rain_probe_field_config(world_3d, weather_node_id) + + _preview_weather_node_ids = active_weather_node_ids + + +func _update_refresh_timer_state() -> void: + if _probe_refresh_timer == null: + return + if _is_any_debug_preview_enabled(): + _probe_refresh_timer.start() + else: + _probe_refresh_timer.start() + + +func _is_any_debug_preview_enabled() -> bool: + return ( + (_weather_probes_button != null and _weather_probes_button.button_pressed) + or (_rain_meshes_button != null and _rain_meshes_button.button_pressed) + ) + + +func _get_edited_weather_nodes() -> Array: + var root := get_editor_interface().get_edited_scene_root() + if root == null: + return [] + + var weather_nodes: Array = [] + if root is WeatherNode: + weather_nodes.append(root) + + for child in root.find_children("*", "WeatherNode", true, false): + weather_nodes.append(child) + + return weather_nodes + + +func _sync_weather_node_editor_preview_cameras(camera: Camera3D) -> void: + for weather_node in _get_edited_weather_nodes(): + if weather_node == null: + continue + weather_node.set_editor_preview_camera(camera) diff --git a/demo/addons/gnd_weather/plugin.gd.uid b/demo/addons/gnd_weather/plugin.gd.uid new file mode 100644 index 00000000..df50e2e9 --- /dev/null +++ b/demo/addons/gnd_weather/plugin.gd.uid @@ -0,0 +1 @@ +uid://dwyh8djvrdnuw diff --git a/demo/addons/gnd_weather/puddle_surface.gdshader b/demo/addons/gnd_weather/puddle_surface.gdshader new file mode 100644 index 00000000..1390f145 --- /dev/null +++ b/demo/addons/gnd_weather/puddle_surface.gdshader @@ -0,0 +1,189 @@ +shader_type spatial; + +render_mode depth_draw_opaque, cull_disabled, diffuse_burley, specular_schlick_ggx; + +uniform sampler2D mask_texture : source_color, filter_linear_mipmap, repeat_disable; +uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap, repeat_disable; +uniform sampler2D depth_texture : hint_depth_texture, filter_linear_mipmap, repeat_disable; +uniform vec4 shallow_color : source_color = vec4(0.11, 0.15, 0.13, 1.0); +uniform vec4 deep_color : source_color = vec4(0.03, 0.06, 0.07, 1.0); +uniform float rain_strength : hint_range(0.0, 1.0) = 0.0; +uniform float rain_ripple_threshold : hint_range(0.0, 1.0) = 0.12; +uniform float rain_wave_threshold : hint_range(0.0, 1.0) = 0.45; +uniform float puddle_alpha_cutoff : hint_range(0.0, 1.0) = 0.07; +uniform float visibility_range_begin : hint_range(0.0, 500.0) = 18.0; +uniform float visibility_range_end : hint_range(0.0, 500.0) = 28.0; +uniform float depth_absorption : hint_range(0.0, 8.0) = 1.35; +uniform float refraction_strength : hint_range(0.0, 0.1) = 0.014; +uniform float refraction_mix : hint_range(0.0, 1.0) = 1.0; +uniform float fresnel_strength : hint_range(0.0, 1.0) = 0.72; +uniform float fresnel_power : hint_range(0.5, 8.0) = 5.0; +uniform float surface_roughness : hint_range(0.0, 1.0) = 0.04; +uniform float ripple_roughness_reduction : hint_range(0.0, 1.0) = 0.02; +uniform float specular_strength : hint_range(0.0, 1.0) = 0.65; +uniform float wave_intensity : hint_range(0.0, 2.0) = 0.08; +uniform float wave_scale : hint_range(0.1, 20.0) = 1.6; +uniform float wave_speed : hint_range(0.1, 20.0) = 0.35; +uniform float secondary_wave_intensity : hint_range(0.0, 2.0) = 0.05; +uniform float secondary_wave_scale : hint_range(0.1, 20.0) = 2.8; +uniform float secondary_wave_speed : hint_range(0.1, 20.0) = 0.55; +uniform float ripple_intensity : hint_range(0.0, 2.0) = 0.18; +uniform float ripple_scale : hint_range(0.1, 10.0) = 1.2; +uniform float ripple_speed : hint_range(0.1, 4.0) = 0.9; +uniform float ripple_max_radius : hint_range(0.0, 5.0) = 1.0; +uniform float edge_foam_strength : hint_range(0.0, 1.0) = 0.08; +uniform float normal_strength : hint_range(0.0, 8.0) = 1.4; + +const float HASHSCALE1 = 0.1031; +const vec3 HASHSCALE3 = vec3(0.1031, 0.1030, 0.0973); + + +float sample_mask_alpha(vec2 uv) { + return texture(mask_texture, uv).a; +} + + +float linear_depth(float nonlinear_depth, mat4 inv_projection_matrix) { + #if CURRENT_RENDERER == RENDERER_COMPATIBILITY + nonlinear_depth = nonlinear_depth * 2.0 - 1.0; + #endif + return 1.0 / (nonlinear_depth * inv_projection_matrix[2].w + inv_projection_matrix[3].w); +} + + +float hash12(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * HASHSCALE1); + p3 += dot(p3, p3.yzx + 19.19); + return fract((p3.x + p3.y) * p3.z); +} + + +vec2 hash22(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * HASHSCALE3); + p3 += dot(p3, p3.yzx + 19.19); + return fract((p3.xx + p3.yz) * p3.zy); +} + + +float ripple_height(vec2 uv, float time_value) { + float rain_mask = smoothstep(rain_ripple_threshold, 1.0, rain_strength); + if (rain_mask <= 0.0001) { + return 0.0; + } + + vec2 field_uv = uv / max(ripple_scale, 0.001) * 0.85; + vec2 cell_origin = floor(field_uv); + float height = 0.0; + + for (float j = -1.0; j <= 1.0; j += 1.0) { + for (float i = -1.0; i <= 1.0; i += 1.0) { + vec2 cell = cell_origin + vec2(i, j); + vec2 rnd = hash22(cell); + vec2 center = cell + rnd; + float age = fract(time_value * ripple_speed * mix(0.85, 1.15, rnd.x) + hash12(cell)); + float radius = mix(0.02, 0.18 + max(ripple_max_radius, 0.0) * 0.54, age); + vec2 delta = field_uv - center; + float dist = length(delta); + float band = dist - radius; + float envelope = exp(-11.0 * abs(band)) * (1.0 - age) * mix(0.8, 1.15, rnd.y); + float ring = sin(band * 34.0 - age * 4.5) * envelope; + height += ring; + } + } + + return height * ripple_intensity * rain_mask * 0.12; +} + + +float primary_wave_height(vec2 uv, float time_value) { + float height = 0.0; + height += sin(dot(uv, normalize(vec2(1.0, 0.24))) * wave_scale * 10.0 + time_value * wave_speed); + height += sin(dot(uv, normalize(vec2(-0.45, 1.0))) * wave_scale * 7.0 - time_value * wave_speed * 0.82) * 0.65; + return height * wave_intensity * 0.02; +} + + +float secondary_wave_height(vec2 uv, float time_value) { + float height = 0.0; + height += sin(dot(uv, normalize(vec2(0.18, 1.0))) * secondary_wave_scale * 13.0 + time_value * secondary_wave_speed); + height += cos(dot(uv, normalize(vec2(-1.0, 0.16))) * secondary_wave_scale * 9.0 - time_value * secondary_wave_speed * 1.17) * 0.6; + return height * secondary_wave_intensity * 0.012; +} + + +float water_height(vec2 uv, float time_value) { + float mask = sample_mask_alpha(uv); + if (mask <= 0.001) { + return 0.0; + } + float waves = primary_wave_height(uv, time_value) + secondary_wave_height(uv, time_value); + float ripples = ripple_height(uv, time_value); + return (waves + ripples) * mask; +} + + +void fragment() { + float mask = sample_mask_alpha(UV); + if (mask < puddle_alpha_cutoff) { + discard; + } + + float fragment_depth = linear_depth(FRAGCOORD.z, INV_PROJECTION_MATRIX); + float visibility = 1.0 - smoothstep(visibility_range_begin, visibility_range_end, fragment_depth); + float fade_noise = hash12(floor(UV * 192.0)); + if (visibility <= 0.0 || fade_noise > visibility) { + discard; + } + + float t = TIME; + float eps = 0.004; + float h_l = water_height(UV + vec2(-eps, 0.0), t); + float h_r = water_height(UV + vec2(eps, 0.0), t); + float h_d = water_height(UV + vec2(0.0, -eps), t); + float h_u = water_height(UV + vec2(0.0, eps), t); + + vec3 base_normal = normalize(NORMAL); + float slope_strength = max(normal_strength, 0.0) * 3.2; + vec3 tangent_normal = normalize(vec3((h_l - h_r) * slope_strength, (h_d - h_u) * slope_strength, 0.55)); + mat3 tbn = mat3(normalize(TANGENT), normalize(BINORMAL), base_normal); + vec3 view_normal = normalize(tbn * tangent_normal); + NORMAL = view_normal; + + float scene_depth = linear_depth(texture(depth_texture, SCREEN_UV).x, INV_PROJECTION_MATRIX); + float surface_depth = -VERTEX.z; + float water_depth = max(scene_depth - surface_depth, 0.0); + float depth_mix = 1.0 - exp(-water_depth * depth_absorption); + + float x_mult = VIEWPORT_SIZE.y / max(VIEWPORT_SIZE.x, 0.0001); + vec3 tanx = BINORMAL * tangent_normal.x * x_mult; + vec3 tany = TANGENT * tangent_normal.y; + vec2 distortion = (tanx + tany).xy * refraction_strength * water_depth / max(scene_depth, 0.001); + vec2 refracted_uv = clamp(SCREEN_UV + distortion, vec2(0.001), vec2(0.999)); + float refracted_depth = linear_depth(texture(depth_texture, refracted_uv).x, INV_PROJECTION_MATRIX); + if (refracted_depth < surface_depth) { + refracted_uv = SCREEN_UV; + } + vec3 scene_color = textureLod(screen_texture, refracted_uv, 0.0).rgb; + vec3 absorption_tint = mix(shallow_color.rgb, deep_color.rgb, depth_mix); + float alpha_tint = mix(shallow_color.a, deep_color.a, depth_mix); + vec3 volume_absorption = mix(vec3(1.0), max(absorption_tint, vec3(0.001)), alpha_tint); + vec3 absorbed_refraction = scene_color * pow(volume_absorption, vec3(water_depth * depth_absorption)); + vec3 transmission = mix(absorption_tint, absorbed_refraction, refraction_mix); + + float edge_mask = 1.0 - smoothstep(puddle_alpha_cutoff, min(puddle_alpha_cutoff + 0.18, 1.0), mask); + float shore_fade = edge_mask * (1.0 - smoothstep(0.0, 0.22, water_depth)); + float ripple_energy = clamp(abs(h_l - h_r) + abs(h_d - h_u), 0.0, 1.0); + float ndotv = clamp(abs(dot(view_normal, normalize(VIEW))), 0.0, 1.0); + float fresnel = pow(1.0 - ndotv, fresnel_power) * fresnel_strength; + float edge_blend = shore_fade * (0.75 + edge_foam_strength * 0.25); + transmission = mix(transmission, scene_color, edge_blend * 0.2); + + ALBEDO = vec3(0.0); + EMISSION = transmission * (1.0 - fresnel); + METALLIC = 0.0; + SPECULAR = specular_strength * (1.0 - edge_blend * 0.9); + + float roughness = surface_roughness; + roughness += edge_blend * 0.16; + ROUGHNESS = clamp(roughness, 0.03, 0.32); +} diff --git a/demo/addons/gnd_weather/puddle_surface.gdshader.uid b/demo/addons/gnd_weather/puddle_surface.gdshader.uid new file mode 100644 index 00000000..828ea2a0 --- /dev/null +++ b/demo/addons/gnd_weather/puddle_surface.gdshader.uid @@ -0,0 +1 @@ +uid://dsiia6rs1y4gv diff --git a/demo/addons/gnd_weather/rain_streak.gdshader b/demo/addons/gnd_weather/rain_streak.gdshader new file mode 100644 index 00000000..29700b3c --- /dev/null +++ b/demo/addons/gnd_weather/rain_streak.gdshader @@ -0,0 +1,109 @@ +shader_type spatial; +render_mode blend_premul_alpha, depth_draw_never, cull_disabled; + +uniform vec4 tint : source_color = vec4(0.84, 0.9, 1.0, 0.12); +uniform float width_softness : hint_range(0.01, 0.8) = 0.22; +uniform float tail_softness : hint_range(0.01, 1.0) = 0.55; +uniform float center_bias : hint_range(0.0, 1.0) = 0.62; +uniform float flow_speed : hint_range(0.0, 8.0) = 1.0; +uniform float travel_distance : hint_range(0.0, 32.0) = 8.0; +uniform float respawn_spread : hint_range(0.0, 4.0) = 0.35; +uniform float streak_length_scale : hint_range(0.05, 2.0) = 1.0; +uniform float intensity_alpha : hint_range(0.0, 1.0) = 1.0; +uniform float blur_amount : hint_range(0.0, 1.0) = 0.0; +uniform float glow_amount : hint_range(0.0, 1.0) = 0.0; +uniform float intensity : hint_range(0.0, 1.0) = 0.47; +uniform float specular : hint_range(0.0, 1.0) = 0.5; +uniform float roughness : hint_range(0.0, 1.0) = 0.1; +uniform float specular_fade_start : hint_range(0.0, 50.0) = 2.0; +uniform float specular_fade_end : hint_range(0.1, 100.0) = 8.0; + +varying vec4 instance_custom_data; +varying float streak_flow; + +float hash11(float p) { + return fract(sin(p) * 43758.5453123); +} + +void vertex() { + instance_custom_data = INSTANCE_CUSTOM; + float variation = instance_custom_data.b; + float detail = instance_custom_data.a; + float speed = TIME * flow_speed * mix(0.75, 1.35, variation); + streak_flow = fract(instance_custom_data.r + speed); + float cycle = floor(instance_custom_data.r + speed); + float offset_x = (hash11(instance_custom_data.a * 97.13 + cycle * 13.71) - 0.5) * respawn_spread; + float offset_z = (hash11(instance_custom_data.r * 83.17 + cycle * 7.91 + instance_custom_data.a * 19.37) - 0.5) * respawn_spread; + VERTEX.y += (streak_flow - 0.5) * travel_distance; + VERTEX.x += offset_x + sin((streak_flow + detail) * 6.28318) * mix(0.0, 0.012, detail); + VERTEX.z += offset_z; +} + +void fragment() { + float instance_alpha = clamp(instance_custom_data.g, 0.0, 1.0); + float variation = instance_custom_data.b; + float detail = instance_custom_data.a; + float primary_trail = UV.y; + float secondary_trail = fract(UV.y * 1.35 + instance_custom_data.r * 0.7 + TIME * flow_speed * 0.18); + + float center_shift = mix(-0.18, 0.18, detail); + float centered_x = abs((UV.x - 0.5) + center_shift * (1.0 - UV.y)) * 2.0; + float width_mask = 1.0 - smoothstep(0.0, mix(width_softness * 0.7, width_softness * 1.35, detail), centered_x); + width_mask *= 1.0 - smoothstep(0.78, 1.0, centered_x); + + float primary_length = mix(0.24, 0.52, variation) * streak_length_scale; + float primary_head_size = mix(0.02, 0.05, detail); + float primary_tail = (1.0 - smoothstep(0.0, primary_length, primary_trail)) * smoothstep(0.0, 0.012, primary_trail); + float primary_head_glow = 1.0 - smoothstep(0.0, primary_head_size, primary_trail); + float primary_streak = max(primary_tail * 0.9, primary_head_glow); + + float secondary_length = mix(0.1, 0.28, detail) * streak_length_scale; + float secondary_tail = (1.0 - smoothstep(0.0, secondary_length, secondary_trail)) * smoothstep(0.0, 0.02, secondary_trail); + float secondary_head_glow = 1.0 - smoothstep(0.0, mix(0.025, 0.055, variation), secondary_trail); + float secondary_streak = max(secondary_tail * mix(0.35, 0.7, detail), secondary_head_glow * 0.65); + + float streak_mask = max(primary_streak, secondary_streak * mix(0.2, 0.45, variation)); + float body_end_min = mix(0.28, 0.46, detail) * streak_length_scale; + float body_end_max = mix(0.58, 0.82, variation) * streak_length_scale; + float body_fade = smoothstep(0.0, 0.04, UV.y) * (1.0 - smoothstep(body_end_min, body_end_max, UV.y)); + float breakup = 0.65 + 0.35 * sin( + UV.y * mix(16.0, 28.0, detail) + + instance_custom_data.r * 17.0 + + UV.x * mix(6.0, 14.0, variation) + ); + float core_alpha = width_mask * body_fade * streak_mask * breakup; + float blur_width = mix(width_softness * 1.35, width_softness * 3.1, blur_amount); + float blur_width_mask = 1.0 - smoothstep(0.0, mix(blur_width * 0.95, blur_width * 1.55, detail), centered_x); + blur_width_mask *= 1.0 - smoothstep(0.96, 1.36 + blur_amount * 0.3, centered_x); + float blur_body_start = 0.08 + blur_amount * 0.08; + float blur_body_end_min = body_end_min * mix(0.92, 0.74, blur_amount); + float blur_body_end_max = body_end_max * mix(1.04, 1.2, blur_amount); + float blur_body_fade = smoothstep(0.0, blur_body_start, UV.y) * (1.0 - smoothstep(blur_body_end_min, blur_body_end_max, UV.y)); + float blur_breakup = mix(1.0, breakup, 0.35); + float blur_shape = blur_width_mask * blur_body_fade * pow(max(streak_mask, 0.0), 0.7) * blur_breakup; + float blur_alpha = blur_shape * glow_amount; + float alpha = core_alpha + blur_alpha; + if (alpha < 0.00001) { + discard; + } + float color_alpha = clamp(intensity * 0.5, 0.1, 0.3); + float opacity_alpha = clamp(intensity * 0.5, 0.1, 0.4); + float blur_visibility = mix(1.0, 0.42, blur_amount); + float blur_color_visibility = mix(1.0, 0.58, blur_amount); + ALBEDO = tint.rgb * ((core_alpha * intensity + blur_alpha * 0.18) * color_alpha * blur_color_visibility); + ALPHA = (core_alpha * intensity + blur_alpha * 0.1) * opacity_alpha * blur_visibility; + ROUGHNESS = roughness; + + // Z-buffer / Distance fade: + // Fade specular intensity based on distance from the camera using inspector parameters. + float dist = length(VERTEX); + float specular_fade = 1.0 - smoothstep(specular_fade_start, specular_fade_end, dist); + SPECULAR = specular * specular_fade; + EMISSION = vec3(1.0, 1.0, 1.0) * intensity * 0.005 * specular_fade; + + // Curve the normal to make the streak act like a cylinder rather than a flat mirror. + // Softened the multiplier from 2.5 to 1.8 to prevent side-lights from catching too aggressively. + vec3 local_normal = normalize(vec3((UV.x - 0.5) * 1.8, 0.0, 1.0)); + NORMAL = normalize((VIEW_MATRIX * vec4(local_normal, 0.0)).xyz); +} + diff --git a/demo/addons/gnd_weather/rain_streak.gdshader.uid b/demo/addons/gnd_weather/rain_streak.gdshader.uid new file mode 100644 index 00000000..6385b034 --- /dev/null +++ b/demo/addons/gnd_weather/rain_streak.gdshader.uid @@ -0,0 +1 @@ +uid://jnv3yp1t6m3i diff --git a/demo/demo_3d.gd b/demo/demo_3d.gd index 4ca6c213..1df0858b 100644 --- a/demo/demo_3d.gd +++ b/demo/demo_3d.gd @@ -2,6 +2,7 @@ extends Node3D @onready var world = $WorldEnvironment + func _auto_user_settings_visibility(): var game_dir = UserSettings.get_maszyna_game_dir() $UserSettingsPanel.visible = not game_dir or FileAccess.file_exists(game_dir) @@ -14,8 +15,22 @@ func _update_render_settings(): world3d.environment.ssao_enabled = UserSettings.get_setting("render", "ssao_enabled", true) world3d.environment.ssil_enabled = UserSettings.get_setting("render", "ssil_enabled", true) world3d.environment.ssr_enabled = UserSettings.get_setting("render", "ssr_enabled", true) + + var antialias_mode = UserSettings.get_setting("render", "antialias_mode") + match antialias_mode: + 0: + viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED + viewport.use_taa = false + 1: + viewport.use_taa = true + viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED + 2: + viewport.use_taa = false + viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA + + viewport.anisotropic_filtering_level = UserSettings.get_setting("render", "anisotropic_filtering_level", 2) - viewport.screen_space_aa = UserSettings.get_setting("render", "screen_space_aa", Viewport.SCREEN_SPACE_AA_FXAA) + viewport.msaa_3d = UserSettings.get_setting("render", "msaa_3d", 2) viewport.fsr_sharpness = UserSettings.get_setting("render", "fsr_sharpness", 0.2) world3d.environment.glow_enabled = true @@ -30,9 +45,6 @@ func _ready(): UserSettings.game_dir_changed.connect(_auto_user_settings_visibility) UserSettings.config_changed.connect(_update_render_settings) _update_render_settings() - SceneryResourceLoader.enabled = false - SceneryResourceLoader.loading_request.connect(_on_loading_started) - SceneryResourceLoader.scenery_loaded.connect(_on_loading_finished) func _on_loading_started(): $LoadingLabel.visible = true @@ -52,7 +64,3 @@ func _input(event): func _on_user_settings_panel_visibility_changed(): var game_dir = UserSettings.get_maszyna_game_dir() $UserSettingsPanel/VBoxContainer/GameDirNotSet.visible = not game_dir or FileAccess.file_exists(game_dir) - - -func _on_loading_screen_fadein_finished() -> void: - SceneryResourceLoader.enabled = true diff --git a/demo/demo_3d.tscn b/demo/demo_3d.tscn index 8ebafc41..e2b5a427 100644 --- a/demo/demo_3d.tscn +++ b/demo/demo_3d.tscn @@ -3,55 +3,199 @@ [ext_resource type="Script" uid="uid://bjjgkykr4c718" path="res://demo_3d.gd" id="1_ut343"] [ext_resource type="PackedScene" uid="uid://dgm10m7u26drx" path="res://addons/libmaszyna/console/developer_console.tscn" id="2_ig825"] [ext_resource type="Material" uid="uid://b7qto250huuby" path="res://materials/custom_demo_ground.tres" id="2_imrpy"] -[ext_resource type="PackedScene" uid="uid://1djubq5jy2wx" path="res://loading_screen/loading_screen.tscn" id="2_n5hjg"] -[ext_resource type="Script" uid="uid://b0r5vydn7ve4l" path="res://addons/libmaszyna/environment/maszyna_environment_node.gd" id="2_py6pt"] -[ext_resource type="Shader" uid="uid://cer53we0fnelj" path="res://addons/libmaszyna/environment/sky.gdshader" id="2_ydn4r"] -[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="4_drlsl"] +[ext_resource type="Script" uid="uid://blkilnimp0sck" path="res://addons/gnd_skydome/Skydome.gd" id="2_m0qth"] +[ext_resource type="Script" uid="uid://lgnwh1qwjbmt" path="res://addons/gnd_weather/WeatherNode.gd" id="3_qg2pq"] [ext_resource type="PackedScene" uid="uid://cpnfrm4urr642" path="res://addons/libmaszyna/editor/user_settings_dock.tscn" id="4_dyl8i"] [ext_resource type="PackedScene" uid="uid://gr0lw1vyf240" path="res://hud/debug_hud.tscn" id="5_vl0kb"] +[ext_resource type="Shader" uid="uid://ck1w2pkpwin11" path="res://addons/gnd_skydome/filmic_procedural_sky.gdshader" id="9_esmlu"] [ext_resource type="PackedScene" uid="uid://bcxtf08c2yqx4" path="res://vehicles/sm42/sm_42.tscn" id="11_8gidn"] [ext_resource type="PackedScene" uid="uid://dqwt31s3qnglp" path="res://vehicles/impuls/impuls.tscn" id="12_hsov8"] [ext_resource type="PackedScene" uid="uid://xh8isp1j28uj" path="res://vehicles/tem2/tem2.tscn" id="13_fe5ju"] [ext_resource type="PackedScene" uid="uid://dmhikrkk2qsjl" path="res://addons/libmaszyna/player/player.tscn" id="13_nxmm0"] - -[sub_resource type="ShaderMaterial" id="ShaderMaterial_s18ef"] -shader = ExtResource("2_ydn4r") -shader_parameter/sky_offset = Vector2(0.49, 0.005) -shader_parameter/exposure = 0.85 -shader_parameter/tint_color = Color(1, 1, 1, 1) -shader_parameter/sun_color = Color(10, 8, 1, 1) -shader_parameter/sun_sunset_color = Color(10, 0, 0, 1) -shader_parameter/sun_size = 0.2 -shader_parameter/sun_blur = 10.0 -shader_parameter/sky_scale = Vector2(0.547, 0.865) - -[sub_resource type="Sky" id="Sky_pybda"] -sky_material = SubResource("ShaderMaterial_s18ef") +[ext_resource type="Script" uid="uid://4xov8ndpybam" path="res://addons/gnd_weather/RainVolume.gd" id="13_rrc65"] +[ext_resource type="Script" uid="uid://lj2sjiebi4m0" path="res://addons/gnd_sfx/sfx_player.gd" id="16_lsqkb"] +[ext_resource type="Resource" uid="uid://wi3rnp4pryi0" path="res://vehicles/sm42/sm42_sound_bank.tres" id="17_t705k"] + +[sub_resource type="FastNoiseLite" id="FastNoiseLite_rrc65"] +seed = 10 + +[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_du3lc"] +noise = SubResource("FastNoiseLite_rrc65") +seamless = true + +[sub_resource type="FastNoiseLite" id="FastNoiseLite_l04v0"] +seed = 100 +frequency = 0.008 + +[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_lsqkb"] +noise = SubResource("FastNoiseLite_l04v0") +seamless = true + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_t705k"] +shader = ExtResource("9_esmlu") +shader_parameter/cloud_tex_a = SubResource("NoiseTexture2D_du3lc") +shader_parameter/cloud_tex_b = SubResource("NoiseTexture2D_lsqkb") +shader_parameter/lower_sky_color = Color(0.655, 0.706, 0.79, 1) +shader_parameter/horizon_color = Color(0.832, 0.86, 0.886, 1) +shader_parameter/zenith_color = Color(0.2373352, 0.4190016, 0.7890625, 1) +shader_parameter/sky_energy = 1.0 +shader_parameter/horizon_height = 0.05 +shader_parameter/horizon_softness = 0.24 +shader_parameter/zenith_curve = 0.405 +shader_parameter/horizon_glow_strength = 1.004 +shader_parameter/atmosphere_horizon_level = -0.035 +shader_parameter/atmosphere_height = 0.24 +shader_parameter/atmosphere_density = 0.46 +shader_parameter/atmosphere_sun_scatter = 0.34 +shader_parameter/atmosphere_sunset_boost = 1.35 +shader_parameter/rainbow_intensity = 0.05 +shader_parameter/rainbow_secondary_intensity = 0.35 +shader_parameter/sunset_bottom_color = Color(1, 0.5, 0.2, 1) +shader_parameter/sunset_horizon_color = Color(0.8, 0.2, 0.05, 1) +shader_parameter/sunset_zenith_color = Color(0.4, 0.3, 0.5, 1) +shader_parameter/sunset_cloud_color = Color(1, 0.4, 0.15, 1) +shader_parameter/sunset_sun_color = Color(1, 0.4, 0.1, 1) +shader_parameter/night_lower_sky_color = Color(0.03, 0.05, 0.09, 1) +shader_parameter/night_horizon_color = Color(0.03, 0.05, 0.09, 1) +shader_parameter/night_zenith_color = Color(0.069, 0.08, 0.109, 1) +shader_parameter/night_sky_energy = 0.3 +shader_parameter/stars_color = Color(1, 1, 1, 1) +shader_parameter/stars_energy = 2.0 +shader_parameter/stars_size_min = 0.01 +shader_parameter/stars_size_max = 0.03 +shader_parameter/stars_edge_softness = 0.25 +shader_parameter/gi_tint = Color(0.8, 0.75, 0.7, 1) +shader_parameter/gi_energy_multiplier = 0.6000000000000001 +shader_parameter/sun_color = Color(1, 0.98, 0.9, 1) +shader_parameter/sun_disk_size = 0.04 +shader_parameter/sun_disk_softness = 4.76 +shader_parameter/sun_disk_strength = 2.445 +shader_parameter/sun_seasonal_size_variation = 30.0 +shader_parameter/sun_halo_size = 0.2 +shader_parameter/sun_halo_strength = 0.5 +shader_parameter/sun_atmosphere_size = 0.4 +shader_parameter/sun_atmosphere_strength = 0.2 +shader_parameter/sun_energy_scale = 0.425 +shader_parameter/moon_color = Color(0.9, 0.95, 1, 1) +shader_parameter/moon_size = 1.0 +shader_parameter/moon_glow_strength = 0.1 +shader_parameter/moon_eclipse_size = 0.8 +shader_parameter/moon_glow_size = 1.0 +shader_parameter/high_quality_sky = true +shader_parameter/cloud_scroll_a = Vector2(0.0012, 0.00015) +shader_parameter/cloud_scroll_b = Vector2(-0.0018, 0.0004) +shader_parameter/cloud_scale_a = Vector2(0.045, 0.055) +shader_parameter/cloud_scale_b = Vector2(0.082, 0.125) +shader_parameter/cloud_plane_height = 0.187 +shader_parameter/cloud_plane_curve = 0.595 +shader_parameter/cloud_warp_strength = 0.053 +shader_parameter/cloud_coverage = 0.0 +shader_parameter/cloud_softness = 0.2 +shader_parameter/cloud_opacity = 0.85 +shader_parameter/cloud_horizon_fade = 0.481 +shader_parameter/cloud_top_fade = 0.118 +shader_parameter/cloud_light_color = Color(1, 0.98, 0.95, 1) +shader_parameter/cloud_shadow_color = Color(0.4, 0.45, 0.55, 1) +shader_parameter/cloud_forward_scatter = 1.5 +shader_parameter/cloud_backscatter = 0.39 +shader_parameter/sun_cloud_occlusion = 0.406 +shader_parameter/custom_sun_dir = Vector3(-0.5873196, 0.7375705, 0.33323497) +shader_parameter/custom_moon_dir = Vector3(0.95412827, -0.27332854, 0.12219128) +shader_parameter/celestial_matrix = Basis(0.18841238, 0.5924319, -0.78327847, 0, 0.79756284, 0.60323584, 0.98209, -0.1136571, 0.15027072) +shader_parameter/rendered_day_of_year = 158.0 +shader_parameter/rendered_time_of_day = 14.638 +shader_parameter/observer_latitude_deg = 52.898 +shader_parameter/cloud_time = 22695.828 +shader_parameter/cloud_wind_direction = Vector2(0.9363292, 0.35112345) +shader_parameter/cloud_wind_speed = 3.285126723193237 +shader_parameter/cloud_motion_time = 0.0 +shader_parameter/cloud_motion_scale = 0.12 +shader_parameter/cloud_evolution_time = 0.0 +shader_parameter/cloud_evolution_strength = 0.18 +shader_parameter/cloud_evolution_scale = 0.018 + +[sub_resource type="Sky" id="Sky_h5mje"] +sky_material = SubResource("ShaderMaterial_t705k") [sub_resource type="Environment" id="Environment_jrns0"] background_mode = 2 -sky = SubResource("Sky_pybda") -tonemap_mode = 3 +sky = SubResource("Sky_h5mje") +ambient_light_color = Color(0.91, 0.85, 0.69, 1) +ambient_light_energy = 1.5 tonemap_white = 6.44 ssr_enabled = true ssao_enabled = true ssil_enabled = true sdfgi_enabled = true +sdfgi_min_cell_size = 0.488281 glow_enabled = true +glow_intensity = 0.9 +glow_bloom = 0.08 +glow_hdr_threshold = 0.29 fog_enabled = true -fog_sky_affect = 0.4 -volumetric_fog_enabled = true -volumetric_fog_density = 0.029 -volumetric_fog_anisotropy = 0.9 -volumetric_fog_length = 0.5 -volumetric_fog_detail_spread = 5.598199 -volumetric_fog_sky_affect = 0.405 +fog_mode = 1 +fog_light_color = Color(1, 0.95, 0.91, 1) +fog_sun_scatter = 0.4 +fog_density = 0.005 +fog_sky_affect = 0.15 +fog_depth_curve = 1.1487 +fog_depth_begin = 2.6 +fog_depth_end = 470.0 +volumetric_fog_density = 0.001 +volumetric_fog_albedo = Color(0.77, 0.74, 0.7, 1) +volumetric_fog_anisotropy = 0.49 +volumetric_fog_length = 8.0 +volumetric_fog_detail_spread = 3.95852 +volumetric_fog_ambient_inject = 0.04 +volumetric_fog_sky_affect = 0.5 +volumetric_fog_temporal_reprojection_amount = 0.99 adjustment_enabled = true -[node name="Demo3D" type="Node3D" unique_id=598546957] +[sub_resource type="Compositor" id="Compositor_ss847"] + +[node name="Demo3D" type="Node3D" unique_id=553602174] script = ExtResource("1_ut343") -[node name="LoadingLabel" type="VBoxContainer" parent="." unique_id=337717684] +[node name="Skydome" type="Node" parent="." unique_id=1448665126] +script = ExtResource("2_m0qth") +directional_light_path = NodePath("../DirectionalLight3D") +world_environment_path = NodePath("../WorldEnvironment") +day_of_year = 158 +time_of_day = 14.638 +latitude = 52.898 +day_light_energy = 2.0 +rainbow_intensity = 0.05 +clouds_coverage = 0.0 +clouds_wind_direction = Vector2(0.9363292, 0.35112345) +clouds_wind_strength = 3.285126723193237 +sun_disk_softness = 4.76 +sun_disk_strength = 2.445 +sun_energy_scale = 0.425 +moon_phase_debug = 0.3711451631109606 +metadata/_custom_type_script = "uid://blkilnimp0sck" + +[node name="WeatherNode" type="Node3D" parent="." unique_id=1247061759] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 21.04, 4.959, -18.089) +script = ExtResource("3_qg2pq") +skydome_path = NodePath("../Skydome") +world_environment_path = NodePath("../WorldEnvironment") +precipitation_intensity = 0.608 +storm_fog_intensity = 0.055 +follow_height = 3.0 +rain_mesh_density = 0.1 +near_rain_blur = 1.0 +near_rain_glow = 1.0 +near_rain_color = Color(0.72156864, 0.7411765, 0.7607843, 0.8392157) +mid_rain_color = Color(0.65882355, 0.6784314, 0.72156864, 0.4745098) +visual_intensity = 1.0 +rain_specular = 0.1 +rain_roughness = 0.6 +near_field_spacing = 1.0 +mid_field_spacing = 3.0 +rain_probe_distance = 1.0 +metadata/_custom_type_script = "uid://lgnwh1qwjbmt" + +[node name="LoadingLabel" type="VBoxContainer" parent="." unique_id=282874346] visible = false anchors_preset = 3 anchor_left = 1.0 @@ -63,18 +207,17 @@ offset_top = -40.0 grow_horizontal = 0 grow_vertical = 0 -[node name="Label" type="Label" parent="LoadingLabel" unique_id=1515757586] +[node name="Label" type="Label" parent="LoadingLabel" unique_id=2034556169] layout_mode = 2 theme_override_colors/font_shadow_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_font_sizes/font_size = 36 text = "Loading..." -[node name="LoadingScreen" parent="." unique_id=785219485 instance=ExtResource("2_n5hjg")] +[node name="DeveloperConsole" parent="." unique_id=1773385475 instance=ExtResource("2_ig825")] +visible = null -[node name="DeveloperConsole" parent="." unique_id=495569075 instance=ExtResource("2_ig825")] - -[node name="UserSettingsPanel" type="PanelContainer" parent="." unique_id=1242102357] +[node name="UserSettingsPanel" type="PanelContainer" parent="." unique_id=256009570] visible = false anchors_preset = 5 anchor_left = 0.5 @@ -86,10 +229,10 @@ offset_bottom = 206.0 grow_horizontal = 2 size_flags_horizontal = 3 -[node name="VBoxContainer" type="VBoxContainer" parent="UserSettingsPanel" unique_id=1324972957] +[node name="VBoxContainer" type="VBoxContainer" parent="UserSettingsPanel" unique_id=1667366111] layout_mode = 2 -[node name="GameDirNotSet" type="Label" parent="UserSettingsPanel/VBoxContainer" unique_id=811810530] +[node name="GameDirNotSet" type="Label" parent="UserSettingsPanel/VBoxContainer" unique_id=1443012784] layout_mode = 2 theme_override_colors/font_color = Color(1, 0.832245, 0.0664062, 1) theme_override_colors/font_shadow_color = Color(0, 0, 0, 1) @@ -97,34 +240,28 @@ theme_override_font_sizes/font_size = 26 text = "MaSzyna game folder data is not set or invalid." horizontal_alignment = 1 -[node name="Maszyna Settings" parent="UserSettingsPanel/VBoxContainer" unique_id=1669941723 instance=ExtResource("4_dyl8i")] +[node name="Maszyna Settings" parent="UserSettingsPanel/VBoxContainer" unique_id=2059956236 instance=ExtResource("4_dyl8i")] layout_mode = 2 -[node name="DebugHUD" parent="." unique_id=1282244848 instance=ExtResource("5_vl0kb")] +[node name="DebugHUD" parent="." unique_id=1382872080 instance=ExtResource("5_vl0kb")] player_path = NodePath("../Player") -[node name="MaszynaEnvironmentNode" type="Node" parent="." unique_id=1228891371] -script = ExtResource("2_py6pt") -world_environment = NodePath("../WorldEnvironment") -sky_texture_offset = Vector2(0.49, 0.005) -sky_texture_scale = Vector2(0.547, 0.865) -sky_energy = 0.85 - -[node name="Player" parent="." unique_id=423486079 instance=ExtResource("13_nxmm0")] +[node name="Player" parent="." unique_id=1342508122 instance=ExtResource("13_nxmm0")] transform = Transform3D(-0.543175, 0, 0.83962, 0, 1, 0, -0.83962, 0, -0.543175, 21.04, 1.959, -18.089) controlled_vehicle_path = NodePath("../SM42-099") -[node name="Ground" type="CSGBox3D" parent="." unique_id=1903537021] +[node name="Ground" type="CSGBox3D" parent="." unique_id=1495808372] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.069304, -5.63989) material_override = ExtResource("2_imrpy") size = Vector3(5000, 0.1, 5000) -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=786641912] -transform = Transform3D(-0.846557, 0.01871, 0.531969, 0.393589, 0.694835, 0.601906, -0.358369, 0.718925, -0.595582, 5.83027, -6.14961, 0) -light_color = Color(0.929688, 0.848771, 0.817108, 1) -light_energy = 2.34 +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=2092751958] +transform = Transform3D(0.4934839, 0.6415056, -0.5873196, 0, 0.67527014, 0.7375705, 0.86975497, -0.36397916, 0.33323497, 0, 0, 0) +light_color = Color(1, 0.93, 0.85, 1) +light_energy = 2.0 light_indirect_energy = 2.0 light_volumetric_fog_energy = 11.811 +light_angular_distance = 0.5 shadow_enabled = true directional_shadow_split_1 = 0.032 directional_shadow_split_2 = 0.1 @@ -132,21 +269,14 @@ directional_shadow_split_3 = 0.3 directional_shadow_blend_splits = true directional_shadow_max_distance = 300.0 -[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=2040205312] +[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1272131009] environment = SubResource("Environment_jrns0") +compositor = SubResource("Compositor_ss847") -[node name="SM42-099" parent="." unique_id=194333198 instance=ExtResource("11_8gidn")] +[node name="SM42-099" parent="." unique_id=1125812614 instance=ExtResource("11_8gidn")] transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, -16.3318, 0, -13.7746) -[node name="Impuls" parent="." unique_id=2111644575 instance=ExtResource("12_hsov8")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.021647027, -0.019251347, -4.3020163) - -[node name="TEM2" parent="." unique_id=803954038 instance=ExtResource("13_fe5ju")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.3788, 0, 0) - -[node name="Dekoracje" type="Node3D" parent="." unique_id=1893813040] - -[node name="Dworzec Olkusz" type="VisualInstance3D" parent="Dekoracje" unique_id=228197153] +[node name="RainVolume" type="VisualInstance3D" parent="SM42-099" unique_id=516118781] unique_name_in_owner = false process_mode = 0 process_priority = 0 @@ -155,180 +285,79 @@ process_thread_group = 0 physics_interpolation_mode = 0 auto_translate_mode = 0 editor_description = "" -transform = Transform3D(1, 0, 0, 0, 0.999979, -0.00651009, 0, 0.00651009, 0.999979, 23.6762, -0.0722218, 3.12076) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.10034084, 2.8718324, -2.527668) rotation_edit_mode = 0 rotation_order = 2 top_level = false visible = true visibility_parent = NodePath("") layers = 1 -script = ExtResource("4_drlsl") +script = ExtResource("13_rrc65") +size = Vector3(2.869945, 4.0781183, 3.0760903) +edge_feather = 0.2 +metadata/_custom_type_script = "uid://4xov8ndpybam" + +[node name="Impuls" parent="." unique_id=1642739350 instance=ExtResource("12_hsov8")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0308414, 0, -4.30589) + +[node name="TEM2" parent="." unique_id=1971513606 instance=ExtResource("13_fe5ju")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.3788, 0, 0) + +[node name="Dekoracje" type="Node3D" parent="." unique_id=1593258065] + +[node name="Dworzec Olkusz" type="E3DModelInstance" parent="Dekoracje" unique_id=1961918976] data_path = "models/stacje/" model_filename = "dworzec_olkusz" skins = ["dworzec_olkusz_lhs"] +transform = Transform3D(1, 0, 0, 0, 0.999979, -0.00651009, 0, 0.00651009, 0.999979, 23.6762, -0.0722218, 3.12076) -[node name="Dworzec Sosnowiec" type="VisualInstance3D" parent="Dekoracje" unique_id=1386451553] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-0.996005, -0.000581314, -0.0892923, 0, 0.999979, -0.00651009, 0.0892942, -0.00648409, -0.995984, 21.3481, 0.108946, 32.9385) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="Dworzec Sosnowiec" type="E3DModelInstance" parent="Dekoracje" unique_id=239443790] data_path = "models/stacje/" model_filename = "dworzec_sosnowiec_porabka" skins = ["dworzec_sosnowiec_porabka"] +transform = Transform3D(-0.996005, -0.000581314, -0.0892923, 0, 0.999979, -0.00651009, 0.0892942, -0.00648409, -0.995984, 21.3481, 0.108946, 32.9385) -[node name="Nastawnia Sochaczew" type="VisualInstance3D" parent="Dekoracje" unique_id=2113648551] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(0.995214, -0.000636179, -0.0977197, -2.73898e-10, 0.999979, -0.00651009, 0.0977218, 0.00647893, 0.995193, 49.2187, -0.199109, 49.3024) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="Nastawnia Sochaczew" type="E3DModelInstance" parent="Dekoracje" unique_id=977137201] data_path = "models/sochaczew/" model_filename = "nastawnia" skins = ["dworzec_sosnowiec_porabka"] +transform = Transform3D(0.995214, -0.000636179, -0.0977197, -2.73898e-10, 0.999979, -0.00651009, 0.0977218, 0.00647893, 0.995193, 49.2187, -0.199109, 49.3024) -[node name="Szyb Kleofas" type="VisualInstance3D" parent="Dekoracje" unique_id=723025036] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-0.00991323, -0.00650977, -0.99993, -3.31827e-10, 0.999979, -0.00651009, 0.999951, -6.45372e-05, -0.00991302, -48.9397, -0.199109, 49.3024) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="Szyb Kleofas" type="E3DModelInstance" parent="Dekoracje" unique_id=1502784411] data_path = "models/przemysl/kopalnie" model_filename = "szyb_kleofas_gigant" skins = ["dworzec_sosnowiec_porabka"] +transform = Transform3D(-0.00991323, -0.00650977, -0.99993, -3.31827e-10, 0.999979, -0.00651009, 0.999951, -6.45372e-05, -0.00991302, -48.9397, -0.199109, 49.3024) -[node name="KWK Staszic" type="VisualInstance3D" parent="Dekoracje" unique_id=230588431] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(0.991685, 0.000837795, 0.128689, -7.01784e-10, 0.999979, -0.00651009, -0.128692, 0.00645596, 0.991664, 69.2926, -0.199109, 92.8501) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="KWK Staszic" type="E3DModelInstance" parent="Dekoracje" unique_id=869329264] data_path = "models/przemysl/kopalnie/kwk_staszic" model_filename = "szyb_ii" +transform = Transform3D(0.991685, 0.000837795, 0.128689, -7.01784e-10, 0.999979, -0.00651009, -0.128692, 0.00645596, 0.991664, 69.2926, -0.199109, 92.8501) -[node name="Tabliczka PKP" type="VisualInstance3D" parent="Dekoracje" unique_id=2055300724] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-0.989323, -0.000948765, -0.145735, -8.28882e-10, 0.999979, -0.0065101, 0.145738, -0.00644059, -0.989302, 16.5195, -0.199109, -17.7848) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="Tabliczka PKP" type="E3DModelInstance" parent="Dekoracje" unique_id=1843243574] data_path = "models/otoczenie/obiekty_stacyjne" model_filename = "terenpkp" +transform = Transform3D(-0.989323, -0.000948765, -0.145735, -8.28882e-10, 0.999979, -0.0065101, 0.145738, -0.00644059, -0.989302, 16.5195, -0.199109, -17.7848) -[node name="Parkomat" type="VisualInstance3D" parent="Dekoracje" unique_id=969128519] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-0.997577, -0.000452873, -0.0695637, -5.57518e-10, 0.999979, -0.0065101, 0.0695652, -0.00649432, -0.997556, 18.1062, -0.0388117, -10.9422) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="Parkomat" type="E3DModelInstance" parent="Dekoracje" unique_id=1329807292] data_path = "models/miejskie" model_filename = "parkomat_rozi_macius9551" +transform = Transform3D(-0.997577, -0.000452873, -0.0695637, -5.57518e-10, 0.999979, -0.0065101, 0.0695652, -0.00649432, -0.997556, 18.1062, -0.0388117, -10.9422) -[node name="Skrzynka" type="VisualInstance3D" parent="Dekoracje" unique_id=204687938] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(0.999866, 0.000106457, 0.0163529, -4.68966e-10, 0.999979, -0.0065101, -0.0163532, 0.00650922, 0.999845, 21.1062, -0.0161638, -10.6909) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="Skrzynka" type="E3DModelInstance" parent="Dekoracje" unique_id=1717139619] data_path = "models/miejskie" model_filename = "skrzynka_tel_rozi_macius9551" +transform = Transform3D(0.999866, 0.000106457, 0.0163529, -4.68966e-10, 0.999979, -0.0065101, -0.0163532, 0.00650922, 0.999845, 21.1062, -0.0161638, -10.6909) -[node name="Klamoty" type="VisualInstance3D" parent="Dekoracje" unique_id=2091251360] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(0.999866, 0.000106457, 0.0163529, -4.68966e-10, 0.999979, -0.0065101, -0.0163532, 0.00650922, 0.999845, -26.1908, -0.0161638, -13.3744) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_drlsl") +[node name="Klamoty" type="E3DModelInstance" parent="Dekoracje" unique_id=263717604] data_path = "models/kolejowe" model_filename = "klamoty_naprawcze" +transform = Transform3D(0.999866, 0.000106457, 0.0163529, -4.68966e-10, 0.999979, -0.0065101, -0.0163532, 0.00650922, 0.999845, -26.1908, -0.0161638, -13.3744) + +[node name="SfxPlayer" type="Node" parent="." unique_id=734802449] +script = ExtResource("16_lsqkb") +bank = ExtResource("17_t705k") +playback_automation_value = 0.1 +metadata/_custom_type_script = "uid://lj2sjiebi4m0" -[connection signal="fadein_finished" from="LoadingScreen" to="." method="_on_loading_screen_fadein_finished"] [connection signal="visibility_changed" from="UserSettingsPanel" to="." method="_on_user_settings_panel_visibility_changed"] diff --git a/demo/examples/cabin_demo.tscn b/demo/examples/cabin_demo.tscn index 4837badc..5efd5f22 100644 --- a/demo/examples/cabin_demo.tscn +++ b/demo/examples/cabin_demo.tscn @@ -1,4 +1,4 @@ -[gd_scene format=3 uid="uid://b2gqnpyxsbpkv"] +[gd_scene load_steps=12 format=3 uid="uid://b2gqnpyxsbpkv"] [ext_resource type="PackedScene" uid="uid://c5hi8nsm1d2hb" path="res://vehicles/sm42/sm_42v_1.tscn" id="1_b8jw8"] [ext_resource type="PackedScene" uid="uid://dgm10m7u26drx" path="res://addons/libmaszyna/console/developer_console.tscn" id="1_ebs3d"] @@ -55,14 +55,15 @@ volumetric_fog_sky_affect = 0.405 volumetric_fog_temporal_reprojection_amount = 0.99 adjustment_enabled = true -[node name="CabinDemo" type="Node3D" unique_id=562475351] +[node name="CabinDemo" type="Node3D"] -[node name="SM42v1" parent="." unique_id=83480242 instance=ExtResource("1_b8jw8")] +[node name="SM42v1" parent="." instance=ExtResource("1_b8jw8")] -[node name="SM42Cabin" parent="." unique_id=759671070 instance=ExtResource("8_1ye4a")] +[node name="SM42Cabin" parent="." instance=ExtResource("8_1ye4a")] controller_path = NodePath("../SM42v1") -[node name="Buda" type="VisualInstance3D" parent="." unique_id=1882743326] +[node name="Buda" type="VisualInstance3D" parent="."] +_import_path = NodePath("") unique_name_in_owner = false process_mode = 0 process_priority = 0 @@ -83,11 +84,11 @@ data_path = "/dynamic/pkp/sm42_v1" model_filename = "6da" skins = ["6d-907"] -[node name="Environment" type="Node" parent="." unique_id=200797671] +[node name="Environment" type="Node" parent="."] -[node name="DeveloperConsole" parent="Environment" unique_id=808068084 instance=ExtResource("1_ebs3d")] +[node name="DeveloperConsole" parent="Environment" instance=ExtResource("1_ebs3d")] -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Environment" unique_id=1925994841] +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Environment"] transform = Transform3D(-0.666529, 0.334317, 0.666311, 0.728995, 0.479221, 0.488788, -0.1559, 0.811529, -0.56313, 5.83027, -6.14961, 0) light_color = Color(0.929688, 0.848771, 0.817108, 1) light_energy = 3.414 @@ -100,19 +101,19 @@ directional_shadow_split_3 = 0.3 directional_shadow_blend_splits = true directional_shadow_max_distance = 300.0 -[node name="WorldEnvironment" type="WorldEnvironment" parent="Environment" unique_id=1182985648] +[node name="WorldEnvironment" type="WorldEnvironment" parent="Environment"] environment = SubResource("Environment_y62w3") -[node name="Camera3D" type="Camera3D" parent="Environment" unique_id=2145021518] +[node name="Camera3D" type="Camera3D" parent="Environment"] transform = Transform3D(0.937281, -0.0875449, 0.337402, 0, 0.967948, 0.251151, -0.348575, -0.235399, 0.907239, 0.785838, 3.45703, 3.02634) script = ExtResource("3_xr1tx") velocity_multiplier = 0.2 -[node name="MaszynaEnvironmentNode" type="Node" parent="Environment" unique_id=579365825] +[node name="MaszynaEnvironmentNode" type="Node" parent="Environment"] script = ExtResource("4_n50b0") world_environment = NodePath("../WorldEnvironment") sky_texture_offset = Vector2(0.49, 0.005) sky_texture_scale = Vector2(0.547, 0.865) sky_energy = 0.575 -[node name="LoadingScreen" parent="." unique_id=306278624 instance=ExtResource("8_ac5ka")] +[node name="LoadingScreen" parent="." instance=ExtResource("8_ac5ka")] diff --git a/demo/examples/custom_powered_train_part.tscn b/demo/examples/custom_powered_train_part.tscn index b26f7a7c..2e251d60 100644 --- a/demo/examples/custom_powered_train_part.tscn +++ b/demo/examples/custom_powered_train_part.tscn @@ -1,10 +1,10 @@ -[gd_scene format=3 uid="uid://dhir05gi7u6by"] +[gd_scene load_steps=4 format=3 uid="uid://dhir05gi7u6by"] [ext_resource type="Script" uid="uid://xht2bkb58aan" path="res://examples/powered_train_part_example.gd" id="1_1fn40"] [ext_resource type="PackedScene" uid="uid://dgm10m7u26drx" path="res://addons/libmaszyna/console/developer_console.tscn" id="1_vsgvw"] [ext_resource type="PackedScene" uid="uid://clt6kosjdfdfx" path="res://examples/example_train.tscn" id="2_ep4dj"] -[node name="CustomPoweredTrainPart" type="Control" unique_id=1645023659] +[node name="CustomPoweredTrainPart" type="Control"] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -12,11 +12,11 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="DeveloperConsole" parent="." unique_id=1066644971 instance=ExtResource("1_vsgvw")] +[node name="DeveloperConsole" parent="." instance=ExtResource("1_vsgvw")] visible = true -[node name="train1" parent="." unique_id=1618340066 instance=ExtResource("2_ep4dj")] +[node name="train1" parent="." instance=ExtResource("2_ep4dj")] train_id = "train1" -[node name="PoweredTrainPart" type="GenericTrainPart" parent="train1" unique_id=1580505920] +[node name="PoweredTrainPart" type="GenericTrainPart" parent="train1"] script = ExtResource("1_1fn40") diff --git a/demo/examples/custom_train_part.tscn b/demo/examples/custom_train_part.tscn index 3317d149..e1b2831c 100644 --- a/demo/examples/custom_train_part.tscn +++ b/demo/examples/custom_train_part.tscn @@ -1,10 +1,10 @@ -[gd_scene format=3 uid="uid://cvf3h3bnqo1my"] +[gd_scene load_steps=4 format=3 uid="uid://cvf3h3bnqo1my"] [ext_resource type="Script" uid="uid://bc88k5e1tpbqj" path="res://examples/custom_train_part.gd" id="1_wmqlj"] [ext_resource type="PackedScene" uid="uid://dgm10m7u26drx" path="res://addons/libmaszyna/console/developer_console.tscn" id="2_l7ubh"] [ext_resource type="PackedScene" uid="uid://clt6kosjdfdfx" path="res://examples/example_train.tscn" id="2_qm6os"] -[node name="CustomTrainPart" type="Control" unique_id=859153966] +[node name="CustomTrainPart" type="Control"] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -12,11 +12,11 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="DeveloperConsole" parent="." unique_id=966677997 instance=ExtResource("2_l7ubh")] +[node name="DeveloperConsole" parent="." instance=ExtResource("2_l7ubh")] visible = true -[node name="train1" parent="." unique_id=693549522 instance=ExtResource("2_qm6os")] +[node name="train1" parent="." instance=ExtResource("2_qm6os")] train_id = "train1" -[node name="CustomTrainPart" type="GenericTrainPart" parent="train1" unique_id=884497581] +[node name="CustomTrainPart" type="GenericTrainPart" parent="train1"] script = ExtResource("1_wmqlj") diff --git a/demo/examples/example_train.tscn b/demo/examples/example_train.tscn index 196bc0da..402dbe5c 100644 --- a/demo/examples/example_train.tscn +++ b/demo/examples/example_train.tscn @@ -1,7 +1,7 @@ [gd_scene format=3 uid="uid://clt6kosjdfdfx"] -[node name="ExampleTrain" type="TrainController" unique_id=180205831] +[node name="ExampleTrain" type="TrainController"] train_id = "exampletrain" -[node name="Brake" type="TrainBrake" parent="." unique_id=2067197281] +[node name="Brake" type="TrainBrake" parent="."] process_priority = 1 diff --git a/demo/examples/mover_demo.tscn b/demo/examples/mover_demo.tscn index 5c3345ac..c510b99b 100644 --- a/demo/examples/mover_demo.tscn +++ b/demo/examples/mover_demo.tscn @@ -1,4 +1,4 @@ -[gd_scene format=3 uid="uid://djqacw65swdqc"] +[gd_scene load_steps=7 format=3 uid="uid://djqacw65swdqc"] [ext_resource type="Script" uid="uid://ccuksauqx4qmn" path="res://examples/mover_demo.gd" id="1_hbfy1"] [ext_resource type="PackedScene" uid="uid://dgm10m7u26drx" path="res://addons/libmaszyna/console/developer_console.tscn" id="2_j25qa"] @@ -7,7 +7,7 @@ [ext_resource type="PackedScene" uid="uid://laujatulc22" path="res://hud/mover_switches.tscn" id="5_vg5os"] [ext_resource type="PackedScene" uid="uid://c5hi8nsm1d2hb" path="res://vehicles/sm42/sm_42v_1.tscn" id="6_7bum4"] -[node name="Demo" type="Control" unique_id=1843221546] +[node name="Demo" type="Control"] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -18,9 +18,9 @@ size_flags_horizontal = 3 size_flags_vertical = 3 script = ExtResource("1_hbfy1") -[node name="DeveloperConsole" parent="." unique_id=956064767 instance=ExtResource("2_j25qa")] +[node name="DeveloperConsole" parent="." instance=ExtResource("2_j25qa")] -[node name="UI" type="VBoxContainer" parent="." unique_id=91857460] +[node name="UI" type="VBoxContainer" parent="."] layout_mode = 1 anchors_preset = -1 anchor_right = 1.0 @@ -31,21 +31,21 @@ grow_vertical = 2 size_flags_horizontal = 3 size_flags_vertical = 3 -[node name="Header" type="HBoxContainer" parent="UI" unique_id=711074471] +[node name="Header" type="HBoxContainer" parent="UI"] layout_mode = 2 size_flags_horizontal = 3 -[node name="TrainName" type="Label" parent="UI/Header" unique_id=794720092] +[node name="TrainName" type="Label" parent="UI/Header"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 0 -[node name="BatteryLabel" type="Label" parent="UI/Header" unique_id=798195162] +[node name="BatteryLabel" type="Label" parent="UI/Header"] layout_mode = 2 size_flags_horizontal = 0 text = "Battery" -[node name="BatteryProgressBar" type="ProgressBar" parent="UI/Header" unique_id=437801902] +[node name="BatteryProgressBar" type="ProgressBar" parent="UI/Header"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 @@ -54,53 +54,53 @@ tooltip_text = "Battery" max_value = 110.0 show_percentage = false -[node name="BatteryValue" type="Label" parent="UI/Header" unique_id=829232401] +[node name="BatteryValue" type="Label" parent="UI/Header"] unique_name_in_owner = true layout_mode = 2 -[node name="Title" type="Label" parent="UI/Header" unique_id=976416075] +[node name="Title" type="Label" parent="UI/Header"] layout_mode = 2 size_flags_horizontal = 10 text = "MaSzyna MOVER TESTER" horizontal_alignment = 2 -[node name="DebugPanels" type="HFlowContainer" parent="UI" unique_id=105784666] +[node name="DebugPanels" type="HFlowContainer" parent="UI"] layout_mode = 2 -[node name="DebugDoor" parent="UI/DebugPanels" unique_id=21064788 instance=ExtResource("3_pdcrj")] +[node name="DebugDoor" parent="UI/DebugPanels" instance=ExtResource("3_pdcrj")] unique_name_in_owner = true layout_mode = 2 title = "Door" -[node name="DebugEngine" parent="UI/DebugPanels" unique_id=1691294425 instance=ExtResource("3_pdcrj")] +[node name="DebugEngine" parent="UI/DebugPanels" instance=ExtResource("3_pdcrj")] unique_name_in_owner = true layout_mode = 2 title = "Engine" -[node name="DebugSecurity" parent="UI/DebugPanels" unique_id=245666883 instance=ExtResource("3_pdcrj")] +[node name="DebugSecurity" parent="UI/DebugPanels" instance=ExtResource("3_pdcrj")] unique_name_in_owner = true layout_mode = 2 title = "Security" -[node name="DebugBrake" parent="UI/DebugPanels" unique_id=916046018 instance=ExtResource("3_pdcrj")] +[node name="DebugBrake" parent="UI/DebugPanels" instance=ExtResource("3_pdcrj")] unique_name_in_owner = true layout_mode = 2 title = "Brake" -[node name="DebugTrain" parent="UI/DebugPanels" unique_id=183917546 instance=ExtResource("3_pdcrj")] +[node name="DebugTrain" parent="UI/DebugPanels" instance=ExtResource("3_pdcrj")] unique_name_in_owner = true layout_mode = 2 title = "Train" -[node name="Gauges" parent="UI" unique_id=896597401 instance=ExtResource("4_4bpxo")] +[node name="Gauges" parent="UI" instance=ExtResource("4_4bpxo")] layout_mode = 2 train_controller = NodePath("../../SM42") -[node name="MoverSwitches" parent="UI" unique_id=394670291 instance=ExtResource("5_vg5os")] +[node name="MoverSwitches" parent="UI" instance=ExtResource("5_vg5os")] layout_mode = 2 train_controller = NodePath("../../SM42") -[node name="SM42" parent="." unique_id=1178158982 instance=ExtResource("6_7bum4")] +[node name="SM42" parent="." instance=ExtResource("6_7bum4")] train_id = "sm42" [connection signal="mover_initialized" from="SM42" to="." method="_on_sm_42_mover_initialized"] diff --git a/demo/export_presets.cfg b/demo/export_presets.cfg index edb66190..4a0e9fb1 100644 --- a/demo/export_presets.cfg +++ b/demo/export_presets.cfg @@ -8,7 +8,7 @@ dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" -exclude_filter="addons/gut/*,examples/*,tests/*" +exclude_filter="addons/gut/*,addons/gnd_sfx/tests/*,examples/*,tests/*" export_path="" patches=PackedStringArray() encryption_include_filters="" @@ -50,7 +50,7 @@ dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" -exclude_filter="addons/gut/*,examples/*,tests/*" +exclude_filter="addons/gut/*,addons/gnd_sfx/tests/*,examples/*,tests/*" export_path="" patches=PackedStringArray() encryption_include_filters="" @@ -92,7 +92,7 @@ dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" -exclude_filter="addons/gut/*,examples/*,tests/*" +exclude_filter="addons/gut/*,addons/gnd_sfx/tests/*,examples/*,tests/*" export_path="../build/maszyna_x86_32.exe" patches=PackedStringArray() encryption_include_filters="" @@ -159,7 +159,7 @@ dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" -exclude_filter="addons/gut/*,examples/*,tests/*" +exclude_filter="addons/gut/*,addons/gnd_sfx/tests/*,examples/*,tests/*" export_path="../build/windows/maszyna_x86_64.exe" patches=PackedStringArray() encryption_include_filters="" diff --git a/demo/hud/button.tscn b/demo/hud/button.tscn index afb34a78..901f0b43 100644 --- a/demo/hud/button.tscn +++ b/demo/hud/button.tscn @@ -1,8 +1,8 @@ -[gd_scene format=3 uid="uid://dy8wkx8wr4gjf"] +[gd_scene load_steps=2 format=3 uid="uid://dy8wkx8wr4gjf"] [ext_resource type="Script" uid="uid://eerusu12lp80" path="res://hud/button.gd" id="1_e1dhy"] -[node name="DebugButton" type="Button" unique_id=1689562325] +[node name="DebugButton" type="Button"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -10,5 +10,6 @@ grow_horizontal = 2 grow_vertical = 2 disabled = true script = ExtResource("1_e1dhy") +covnert_argument_to_bool = null [connection signal="pressed" from="." to="." method="_on_pressed"] diff --git a/demo/hud/debug_hud.tscn b/demo/hud/debug_hud.tscn index 038085c2..589cc618 100644 --- a/demo/hud/debug_hud.tscn +++ b/demo/hud/debug_hud.tscn @@ -1,10 +1,10 @@ -[gd_scene format=3 uid="uid://gr0lw1vyf240"] +[gd_scene load_steps=4 format=3 uid="uid://gr0lw1vyf240"] [ext_resource type="PackedScene" uid="uid://laujatulc22" path="res://hud/mover_switches.tscn" id="1_77c1y"] [ext_resource type="Script" uid="uid://cig7pvfw83ix4" path="res://hud/debug_hud.gd" id="1_ht8he"] [ext_resource type="PackedScene" uid="uid://e8tfpu8xl6ps" path="res://hud/mover_gauges.tscn" id="2_hy386"] -[node name="DebugHUD" type="HBoxContainer" unique_id=40650741] +[node name="DebugHUD" type="HBoxContainer"] anchors_preset = 12 anchor_top = 1.0 anchor_right = 1.0 @@ -15,9 +15,9 @@ grow_vertical = 0 size_flags_horizontal = 3 script = ExtResource("1_ht8he") -[node name="MoverSwitches" parent="." unique_id=1818962873 instance=ExtResource("1_77c1y")] +[node name="MoverSwitches" parent="." instance=ExtResource("1_77c1y")] layout_mode = 2 -[node name="Gauges" parent="." unique_id=157992799 instance=ExtResource("2_hy386")] +[node name="Gauges" parent="." instance=ExtResource("2_hy386")] layout_mode = 2 size_flags_horizontal = 3 diff --git a/demo/hud/gauge.tscn b/demo/hud/gauge.tscn index 4c0ace7b..30b4c5bb 100644 --- a/demo/hud/gauge.tscn +++ b/demo/hud/gauge.tscn @@ -1,4 +1,4 @@ -[gd_scene format=3 uid="uid://1v37c7y4cwcx"] +[gd_scene load_steps=7 format=3 uid="uid://1v37c7y4cwcx"] [ext_resource type="Script" uid="uid://bocq0tt1rcbtg" path="res://hud/gauge.gd" id="1_l2s17"] [ext_resource type="Shader" uid="uid://b755nh40kmdqg" path="res://hud/gauge.gdshader" id="1_rywa7"] @@ -15,21 +15,21 @@ shader_parameter/feather = -0.05 [sub_resource type="CanvasTexture" id="CanvasTexture_883b5"] -[node name="Gauge" type="Control" unique_id=926326915] +[node name="Gauge" type="Control"] layout_mode = 3 anchors_preset = 0 size_flags_horizontal = 3 size_flags_vertical = 3 script = ExtResource("1_l2s17") -[node name="Background" type="TextureRect" parent="." unique_id=1435227005] +[node name="Background" type="TextureRect" parent="."] material = SubResource("ShaderMaterial_xghqt") layout_mode = 0 offset_right = 144.0 offset_bottom = 139.0 texture = SubResource("CanvasTexture_e7i56") -[node name="Arrow" type="TextureRect" parent="." unique_id=1718395539] +[node name="Arrow" type="TextureRect" parent="."] modulate = Color(0, 0, 0, 1) self_modulate = Color(0, 0, 0, 1) material = SubResource("CanvasItemMaterial_0mu1h") @@ -41,7 +41,7 @@ offset_bottom = 71.0 rotation = 4.712389 texture = SubResource("CanvasTexture_883b5") -[node name="Label" type="Label" parent="." unique_id=1273729468] +[node name="Label" type="Label" parent="."] layout_mode = 0 offset_left = 4.0 offset_top = 123.0 diff --git a/demo/hud/knob.tscn b/demo/hud/knob.tscn index ac63c188..f46cc290 100644 --- a/demo/hud/knob.tscn +++ b/demo/hud/knob.tscn @@ -1,16 +1,16 @@ -[gd_scene format=3 uid="uid://ddag4vhhuox0t"] +[gd_scene load_steps=2 format=3 uid="uid://ddag4vhhuox0t"] [ext_resource type="Script" uid="uid://b285sg5jqauaj" path="res://hud/knob.gd" id="1_an08j"] -[node name="DebugKnob" type="HBoxContainer" unique_id=700336505] +[node name="DebugKnob" type="HBoxContainer"] offset_right = 40.0 offset_bottom = 40.0 script = ExtResource("1_an08j") -[node name="Label" type="Label" parent="." unique_id=1349015093] +[node name="Label" type="Label" parent="."] layout_mode = 2 -[node name="SpinBox" type="SpinBox" parent="." unique_id=2135562369] +[node name="SpinBox" type="SpinBox" parent="."] layout_mode = 2 max_value = 1.0 step = 0.1 diff --git a/demo/hud/light.tscn b/demo/hud/light.tscn index 8ddf0968..96127845 100644 --- a/demo/hud/light.tscn +++ b/demo/hud/light.tscn @@ -1,4 +1,4 @@ -[gd_scene format=3 uid="uid://c4f4ti7sfn87w"] +[gd_scene load_steps=3 format=3 uid="uid://c4f4ti7sfn87w"] [ext_resource type="Script" uid="uid://digl5wc1yynj3" path="res://hud/light.gd" id="1_cukq6"] @@ -6,7 +6,7 @@ resource_local_to_scene = true bg_color = Color(0, 0, 0, 1) -[node name="Light" type="VBoxContainer" unique_id=1483459749] +[node name="Light" type="VBoxContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -16,10 +16,10 @@ size_flags_horizontal = 3 size_flags_vertical = 3 script = ExtResource("1_cukq6") -[node name="PanelContainer" type="PanelContainer" parent="." unique_id=129842950] +[node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_jsypt") -[node name="Label" type="Label" parent="PanelContainer" unique_id=1356407088] +[node name="Label" type="Label" parent="PanelContainer"] layout_mode = 2 horizontal_alignment = 1 diff --git a/demo/hud/mover_gauges.tscn b/demo/hud/mover_gauges.tscn index 9ed1f8bd..3f74ab8d 100644 --- a/demo/hud/mover_gauges.tscn +++ b/demo/hud/mover_gauges.tscn @@ -1,108 +1,108 @@ -[gd_scene format=3 uid="uid://e8tfpu8xl6ps"] +[gd_scene load_steps=4 format=3 uid="uid://e8tfpu8xl6ps"] [ext_resource type="PackedScene" uid="uid://1v37c7y4cwcx" path="res://hud/gauge.tscn" id="1_irtlh"] [ext_resource type="Script" uid="uid://cvieq2vslauaq" path="res://hud/mover_gauges.gd" id="1_mxntq"] [ext_resource type="PackedScene" uid="uid://c4f4ti7sfn87w" path="res://hud/light.tscn" id="2_rwq8g"] -[node name="Gauges" type="HFlowContainer" unique_id=628728530] +[node name="Gauges" type="HFlowContainer"] offset_right = 960.0 offset_bottom = 332.0 size_flags_vertical = 3 script = ExtResource("1_mxntq") -[node name="EngineRPM" parent="." unique_id=2036432339 instance=ExtResource("1_irtlh")] +[node name="EngineRPM" parent="." instance=ExtResource("1_irtlh")] layout_mode = 2 size_flags_vertical = 1 start_angle = 120.0 label = "Engine RPM" -[node name="EngineCurrent" parent="." unique_id=474736536 instance=ExtResource("1_irtlh")] +[node name="EngineCurrent" parent="." instance=ExtResource("1_irtlh")] layout_mode = 2 start_angle = 120.0 label = "Engine Current" -[node name="OilPressure" parent="." unique_id=1389412149 instance=ExtResource("1_irtlh")] +[node name="OilPressure" parent="." instance=ExtResource("1_irtlh")] layout_mode = 2 start_angle = 120.0 label = "Oil Pressure" -[node name="BrakeCylinderPressure" parent="." unique_id=214601039 instance=ExtResource("1_irtlh")] +[node name="BrakeCylinderPressure" parent="." instance=ExtResource("1_irtlh")] layout_mode = 2 start_angle = 120.0 label = "Brake Cyl Pressure" -[node name="BrakePipePressure" parent="." unique_id=1735120391 instance=ExtResource("1_irtlh")] +[node name="BrakePipePressure" parent="." instance=ExtResource("1_irtlh")] layout_mode = 2 start_angle = 140.0 label = "Brake Pipe Pressure" -[node name="SpringBrakePressure" parent="." unique_id=1340296834 instance=ExtResource("1_irtlh")] +[node name="SpringBrakePressure" parent="." instance=ExtResource("1_irtlh")] layout_mode = 2 start_angle = 120.0 label = "Spring Brake Cyl. Pressure" -[node name="Speed" parent="." unique_id=1908446303 instance=ExtResource("1_irtlh")] +[node name="Speed" parent="." instance=ExtResource("1_irtlh")] layout_mode = 2 start_angle = 120.0 label = "Speed" -[node name="VBoxContainer2" type="VBoxContainer" parent="." unique_id=123911513] +[node name="VBoxContainer2" type="VBoxContainer" parent="."] layout_mode = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=468218971] +[node name="VBoxContainer" type="VBoxContainer" parent="."] custom_minimum_size = Vector2(300, 0) layout_mode = 2 -[node name="Lights1" type="HBoxContainer" parent="VBoxContainer" unique_id=492284379] +[node name="Lights1" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 -[node name="SecurityLight" parent="VBoxContainer/Lights1" unique_id=1574934773 instance=ExtResource("2_rwq8g")] +[node name="SecurityLight" parent="VBoxContainer/Lights1" instance=ExtResource("2_rwq8g")] unique_name_in_owner = true layout_mode = 2 color_active = Color(0.988281, 0.138977, 0, 1) label = "CA" blink_interval = 0.25 -[node name="SHPLight" parent="VBoxContainer/Lights1" unique_id=2090500678 instance=ExtResource("2_rwq8g")] +[node name="SHPLight" parent="VBoxContainer/Lights1" instance=ExtResource("2_rwq8g")] unique_name_in_owner = true layout_mode = 2 color_active = Color(0.988281, 0.138977, 0, 1) label = "SHP" blink_interval = 0.25 -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" unique_id=195715084] +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 -[node name="DoorsLocked" parent="VBoxContainer/HBoxContainer" unique_id=595586585 instance=ExtResource("2_rwq8g")] +[node name="DoorsLocked" parent="VBoxContainer/HBoxContainer" instance=ExtResource("2_rwq8g")] unique_name_in_owner = true layout_mode = 2 color_active = Color(0.90625, 0.488525, 0, 1) label = "Doors Locked" -[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer" unique_id=974512066] +[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 -[node name="LeftDoorsOpenLight" parent="VBoxContainer/HBoxContainer2" unique_id=1239865602 instance=ExtResource("2_rwq8g")] +[node name="LeftDoorsOpenLight" parent="VBoxContainer/HBoxContainer2" instance=ExtResource("2_rwq8g")] unique_name_in_owner = true layout_mode = 2 color_active = Color(0, 0.679688, 0.090271, 1) label = "Left Doors Open" -[node name="RightDoorsOpenLight" parent="VBoxContainer/HBoxContainer2" unique_id=1136386519 instance=ExtResource("2_rwq8g")] +[node name="RightDoorsOpenLight" parent="VBoxContainer/HBoxContainer2" instance=ExtResource("2_rwq8g")] unique_name_in_owner = true layout_mode = 2 color_active = Color(0, 0.679688, 0.090271, 1) label = "Right Doors Open" -[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer" unique_id=220468951] +[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 -[node name="SpringBrakeEnabled" parent="VBoxContainer/HBoxContainer3" unique_id=1537137261 instance=ExtResource("2_rwq8g")] +[node name="SpringBrakeEnabled" parent="VBoxContainer/HBoxContainer3" instance=ExtResource("2_rwq8g")] layout_mode = 2 color_active = Color(0, 0.679688, 0.090271, 1) label = "Spring brake enabled" -[node name="SpringBrakeActive" parent="VBoxContainer/HBoxContainer3" unique_id=1999119484 instance=ExtResource("2_rwq8g")] +[node name="SpringBrakeActive" parent="VBoxContainer/HBoxContainer3" instance=ExtResource("2_rwq8g")] layout_mode = 2 color_active = Color(0, 0.679688, 0.090271, 1) label = "Spring brake active" diff --git a/demo/hud/mover_switches.tscn b/demo/hud/mover_switches.tscn index 3c14d16b..52af049a 100644 --- a/demo/hud/mover_switches.tscn +++ b/demo/hud/mover_switches.tscn @@ -1,99 +1,109 @@ -[gd_scene format=3 uid="uid://laujatulc22"] +[gd_scene load_steps=5 format=3 uid="uid://laujatulc22"] [ext_resource type="Script" uid="uid://cyuyraig8yxc" path="res://hud/mover_switches.gd" id="1_khm1i"] [ext_resource type="PackedScene" uid="uid://cslby72tp46ig" path="res://hud/switch.tscn" id="1_vladt"] [ext_resource type="PackedScene" uid="uid://dy8wkx8wr4gjf" path="res://hud/button.tscn" id="2_qbcao"] [ext_resource type="PackedScene" uid="uid://ddag4vhhuox0t" path="res://hud/knob.tscn" id="3_j1mtt"] -[node name="MoverSwitches" type="HBoxContainer" unique_id=339320426] +[node name="MoverSwitches" type="HBoxContainer"] size_flags_vertical = 3 script = ExtResource("1_khm1i") -[node name="General" type="HFlowContainer" parent="." unique_id=1285486844] +[node name="General" type="HFlowContainer" parent="."] layout_mode = 2 -[node name="Label" type="Label" parent="General" unique_id=60872803] +[node name="Label" type="Label" parent="General"] layout_mode = 2 text = "General" -[node name="Battery2" parent="General" unique_id=1031787286 instance=ExtResource("1_vladt")] +[node name="Battery2" parent="General" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Battery" state_property = "battery_enabled" command = "battery" -[node name="HBoxContainer2" type="HBoxContainer" parent="General" unique_id=251685054] +[node name="HBoxContainer2" type="HBoxContainer" parent="General"] layout_mode = 2 -[node name="Forward" parent="General/HBoxContainer2" unique_id=1483744598 instance=ExtResource("2_qbcao")] +[node name="Forward" parent="General/HBoxContainer2" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Forward" command = "direction_increase" +covnert_argument_to_bool = false -[node name="Reverse" parent="General/HBoxContainer2" unique_id=2091739000 instance=ExtResource("2_qbcao")] +[node name="Reverse" parent="General/HBoxContainer2" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Backward" command = "direction_decrease" +covnert_argument_to_bool = false -[node name="HBoxContainer" type="HBoxContainer" parent="General" unique_id=852991503] +[node name="HBoxContainer" type="HBoxContainer" parent="General"] layout_mode = 2 -[node name="MainIncrease" parent="General/HBoxContainer" unique_id=76479498 instance=ExtResource("2_qbcao")] +[node name="MainIncrease" parent="General/HBoxContainer" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Main +" command = "main_controller_increase" +covnert_argument_to_bool = false -[node name="MainDecrease" parent="General/HBoxContainer" unique_id=1555303172 instance=ExtResource("2_qbcao")] +[node name="MainDecrease" parent="General/HBoxContainer" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Main -" command = "main_controller_decrease" +covnert_argument_to_bool = false -[node name="MainCtrlPos" type="Label" parent="General/HBoxContainer" unique_id=326791865] +[node name="MainCtrlPos" type="Label" parent="General/HBoxContainer"] unique_name_in_owner = true layout_mode = 2 -[node name="HBoxContainer3" type="HBoxContainer" parent="General" unique_id=1356500360] +[node name="HBoxContainer3" type="HBoxContainer" parent="General"] layout_mode = 2 -[node name="LightsIncrease" parent="General/HBoxContainer3" unique_id=1937226329 instance=ExtResource("2_qbcao")] +[node name="LightsIncrease" parent="General/HBoxContainer3" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Light selector +" command = "increase_light_selector_position" +covnert_argument_to_bool = false -[node name="LightsDecrease" parent="General/HBoxContainer3" unique_id=1702639036 instance=ExtResource("2_qbcao")] +[node name="LightsDecrease" parent="General/HBoxContainer3" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Light selector -" command = "decrease_light_selector_position" +covnert_argument_to_bool = false -[node name="HBoxContainer4" type="HBoxContainer" parent="General" unique_id=1681608652] +[node name="HBoxContainer4" type="HBoxContainer" parent="General"] layout_mode = 2 -[node name="SpringBrakeActivate" parent="General/HBoxContainer4" unique_id=262499799 instance=ExtResource("2_qbcao")] +[node name="SpringBrakeActivate" parent="General/HBoxContainer4" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Spring brake: On" command = "set_spring_brake_active" command_argument = "true" +covnert_argument_to_bool = true -[node name="SpringBrakeDeactivate" parent="General/HBoxContainer4" unique_id=526765782 instance=ExtResource("2_qbcao")] +[node name="SpringBrakeDeactivate" parent="General/HBoxContainer4" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Spring brake: Off" command = "set_spring_brake_active" command_argument = "false" +covnert_argument_to_bool = true -[node name="HBoxContainer5" type="HBoxContainer" parent="General" unique_id=2055977280] +[node name="HBoxContainer5" type="HBoxContainer" parent="General"] layout_mode = 2 -[node name="SpringBrakeEnable" parent="General/HBoxContainer5" unique_id=1817438112 instance=ExtResource("2_qbcao")] +[node name="SpringBrakeEnable" parent="General/HBoxContainer5" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Spring brake: Enable" command = "set_spring_brake_enabled" command_argument = "true" +covnert_argument_to_bool = true -[node name="SpringBrakeDisable" parent="General/HBoxContainer5" unique_id=110025536 instance=ExtResource("2_qbcao")] +[node name="SpringBrakeDisable" parent="General/HBoxContainer5" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Spring brake: Disable" command = "set_spring_brake_enabled" command_argument = "false" +covnert_argument_to_bool = true [node name="HBoxContainer6" type="HBoxContainer" parent="General"] layout_mode = 2 @@ -111,140 +121,143 @@ command = "buffer_decouple" [node name="Security" type="HFlowContainer" parent="." unique_id=176203822] layout_mode = 2 -[node name="Label" type="Label" parent="Security" unique_id=1347421778] +[node name="Label" type="Label" parent="Security"] layout_mode = 2 text = "Security" -[node name="CA_SHP_Reset" parent="Security" unique_id=946022722 instance=ExtResource("1_vladt")] +[node name="CA_SHP_Reset" parent="Security" instance=ExtResource("1_vladt")] layout_mode = 2 label = "CA/SHP" type = 1 command = "security_acknowledge" -[node name="Diesel" type="HFlowContainer" parent="." unique_id=1978544548] +[node name="Diesel" type="HFlowContainer" parent="."] layout_mode = 2 -[node name="Label" type="Label" parent="Diesel" unique_id=860134821] +[node name="Label" type="Label" parent="Diesel"] layout_mode = 2 text = "Diesel" -[node name="OilPump" parent="Diesel" unique_id=147082081 instance=ExtResource("1_vladt")] +[node name="OilPump" parent="Diesel" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Oil Pump" type = 1 state_property = "oil_pump_active" command = "oil_pump" -[node name="FuelPump" parent="Diesel" unique_id=1508457149 instance=ExtResource("1_vladt")] +[node name="FuelPump" parent="Diesel" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Fuel Pump" type = 1 state_property = "fuel_pump_active" command = "fuel_pump" -[node name="Main" parent="Diesel" unique_id=1647544102 instance=ExtResource("1_vladt")] +[node name="Main" parent="Diesel" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Main" type = 0 state_property = "main_switch_enabled" command = "main_switch" -[node name="Brakes" type="HFlowContainer" parent="." unique_id=285370667] +[node name="Brakes" type="HFlowContainer" parent="."] layout_mode = 2 -[node name="Label" type="Label" parent="Brakes" unique_id=2073071209] +[node name="Label" type="Label" parent="Brakes"] layout_mode = 2 size_flags_horizontal = 3 text = "Brakes" -[node name="VBoxContainer" type="VBoxContainer" parent="Brakes" unique_id=269252679] +[node name="VBoxContainer" type="VBoxContainer" parent="Brakes"] layout_mode = 2 -[node name="EPFuse" parent="Brakes/VBoxContainer" unique_id=1679341982 instance=ExtResource("1_vladt")] +[node name="EPFuse" parent="Brakes/VBoxContainer" instance=ExtResource("1_vladt")] layout_mode = 2 label = "EP Fuse" state_property = "dcemued/ep_fuse" command = "switch_ep_fuse" -[node name="EPLevel" parent="Brakes/VBoxContainer" unique_id=538369926 instance=ExtResource("3_j1mtt")] +[node name="EPLevel" parent="Brakes/VBoxContainer" instance=ExtResource("3_j1mtt")] layout_mode = 2 label = "EP Level" step = 0.05 state_property = "dcemued/ep_force" command = "set_ep_brake_force" -[node name="Drive" parent="Brakes/VBoxContainer" unique_id=1247507865 instance=ExtResource("2_qbcao")] +[node name="Drive" parent="Brakes/VBoxContainer" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Drive" command = "brake_level_set_position" command_argument = "drive" +covnert_argument_to_bool = false -[node name="Full" parent="Brakes/VBoxContainer" unique_id=1745993826 instance=ExtResource("2_qbcao")] +[node name="Full" parent="Brakes/VBoxContainer" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Full" command = "brake_level_set_position" command_argument = "full" +covnert_argument_to_bool = false -[node name="Emergency" parent="Brakes/VBoxContainer" unique_id=1616408736 instance=ExtResource("2_qbcao")] +[node name="Emergency" parent="Brakes/VBoxContainer" instance=ExtResource("2_qbcao")] layout_mode = 2 text = "Emergency" command = "brake_level_set_position" command_argument = "emergency" +covnert_argument_to_bool = false -[node name="Level" parent="Brakes/VBoxContainer" unique_id=610189844 instance=ExtResource("3_j1mtt")] +[node name="Level" parent="Brakes/VBoxContainer" instance=ExtResource("3_j1mtt")] layout_mode = 2 label = "Level" step = 0.05 state_property = "brake_controller_position_normalized" command = "brake_level_set" -[node name="BrakeLevel" type="SpinBox" parent="Brakes/VBoxContainer" unique_id=178796767] +[node name="BrakeLevel" type="SpinBox" parent="Brakes/VBoxContainer"] layout_mode = 2 max_value = 1.0 step = 0.01 -[node name="Releaser" parent="Brakes/VBoxContainer" unique_id=786192513 instance=ExtResource("1_vladt")] +[node name="Releaser" parent="Brakes/VBoxContainer" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Releaser" type = 1 command = "brake_releaser" -[node name="Doors" type="HFlowContainer" parent="." unique_id=1748786254] +[node name="Doors" type="HFlowContainer" parent="."] layout_mode = 2 -[node name="Label" type="Label" parent="Doors" unique_id=6458345] +[node name="Label" type="Label" parent="Doors"] layout_mode = 2 text = "Doors" -[node name="LeftDoorsOpen" parent="Doors" unique_id=2079426602 instance=ExtResource("1_vladt")] +[node name="LeftDoorsOpen" parent="Doors" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Open left doors" command = "doors_left" -[node name="RightDoorsOpen" parent="Doors" unique_id=274404426 instance=ExtResource("1_vladt")] +[node name="RightDoorsOpen" parent="Doors" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Open right doors" command = "doors_right" -[node name="LeftDoorsPermit" parent="Doors" unique_id=236065208 instance=ExtResource("1_vladt")] +[node name="LeftDoorsPermit" parent="Doors" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Permit left doors" state_property = "doors/left/open_permit" command = "doors_left_permit" -[node name="RightDoorsPermit" parent="Doors" unique_id=2065874538 instance=ExtResource("1_vladt")] +[node name="RightDoorsPermit" parent="Doors" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Permit right doors" state_property = "doors/right/open_permit" command = "doors_right_permit" -[node name="DoorLock" parent="Doors" unique_id=1730048993 instance=ExtResource("1_vladt")] +[node name="DoorLock" parent="Doors" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Lock doors" state_property = "doors/lock_enabled" command = "doors_lock" -[node name="DoorStepPermit" parent="Doors" unique_id=657163167 instance=ExtResource("1_vladt")] +[node name="DoorStepPermit" parent="Doors" instance=ExtResource("1_vladt")] layout_mode = 2 label = "Permit Step" state_property = "doors/step_enabled" diff --git a/demo/hud/panel.tscn b/demo/hud/panel.tscn index 575c4368..84b08248 100644 --- a/demo/hud/panel.tscn +++ b/demo/hud/panel.tscn @@ -1,7 +1,7 @@ -[gd_scene format=3 uid="uid://br6moafqr8ykn"] +[gd_scene load_steps=4 format=3 uid="uid://br6moafqr8ykn"] [ext_resource type="Script" uid="uid://bkwi32wa1iq70" path="res://hud/panel.gd" id="1_7xna6"] -[ext_resource type="Texture2D" uid="uid://cerha8bjqn01p" path="res://icons/search.png" id="2_s0ly2"] +[ext_resource type="Texture2D" uid="uid://b4mi7h8ej3xqn" path="res://icons/search.png" id="2_s0ly2"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kvppn"] content_margin_left = 10.0 @@ -15,7 +15,7 @@ border_width_right = 1 border_width_bottom = 1 border_color = Color(0.160034, 0.223886, 0.359375, 1) -[node name="DebugPanel" type="VBoxContainer" unique_id=1830241259] +[node name="DebugPanel" type="VBoxContainer"] custom_minimum_size = Vector2(0, 400) offset_right = 258.0 offset_bottom = 369.0 @@ -23,35 +23,35 @@ size_flags_horizontal = 3 size_flags_vertical = 3 script = ExtResource("1_7xna6") -[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=593828238] +[node name="HBoxContainer" type="HBoxContainer" parent="."] layout_mode = 2 -[node name="Title" type="Label" parent="HBoxContainer" unique_id=2018966999] +[node name="Title" type="Label" parent="HBoxContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -[node name="TextureRect" type="TextureRect" parent="HBoxContainer" unique_id=2003395112] +[node name="TextureRect" type="TextureRect" parent="HBoxContainer"] layout_mode = 2 texture = ExtResource("2_s0ly2") stretch_mode = 3 -[node name="Filter" type="LineEdit" parent="HBoxContainer" unique_id=2094806861] +[node name="Filter" type="LineEdit" parent="HBoxContainer"] unique_name_in_owner = true custom_minimum_size = Vector2(120, 0) layout_mode = 2 size_flags_horizontal = 8 size_flags_vertical = 4 -[node name="HFlowContainer" type="HFlowContainer" parent="HBoxContainer" unique_id=94551818] +[node name="HFlowContainer" type="HFlowContainer" parent="HBoxContainer"] layout_mode = 2 -[node name="DebugPanel" type="PanelContainer" parent="." unique_id=31482797] +[node name="DebugPanel" type="PanelContainer" parent="."] layout_mode = 2 size_flags_vertical = 3 theme_override_styles/panel = SubResource("StyleBoxFlat_kvppn") -[node name="RichTextLabel" type="RichTextLabel" parent="DebugPanel" unique_id=282098891] +[node name="RichTextLabel" type="RichTextLabel" parent="DebugPanel"] layout_mode = 2 theme_override_font_sizes/normal_font_size = 12 diff --git a/demo/hud/switch.tscn b/demo/hud/switch.tscn index bea98030..4fc9d4ce 100644 --- a/demo/hud/switch.tscn +++ b/demo/hud/switch.tscn @@ -1,18 +1,18 @@ -[gd_scene format=3 uid="uid://cslby72tp46ig"] +[gd_scene load_steps=2 format=3 uid="uid://cslby72tp46ig"] [ext_resource type="Script" uid="uid://1nofjpxw2wvq" path="res://hud/switch.gd" id="1_7hfja"] -[node name="DebugSwitch" type="HBoxContainer" unique_id=118575775] +[node name="DebugSwitch" type="HBoxContainer"] offset_right = 106.0 offset_bottom = 24.0 size_flags_horizontal = 3 script = ExtResource("1_7hfja") -[node name="Label" type="Label" parent="." unique_id=1711133923] +[node name="Label" type="Label" parent="."] layout_mode = 2 size_flags_horizontal = 3 -[node name="Switch" type="CheckButton" parent="." unique_id=1943980002] +[node name="Switch" type="CheckButton" parent="."] layout_mode = 2 size_flags_horizontal = 10 disabled = true diff --git a/demo/loading_screen/loading_screen.gd b/demo/loading_screen/loading_screen.gd index bf948109..efe1edf7 100644 --- a/demo/loading_screen/loading_screen.gd +++ b/demo/loading_screen/loading_screen.gd @@ -1,29 +1,20 @@ extends PanelContainer -signal fadein_finished @export var autoclose_after_load:bool = true +const SCENE_PATH: String = "res://demo_3d.tscn" +var progress: Array[float] = [] func _ready(): + ResourceLoader.load_threaded_request(SCENE_PATH) visible = true $VBoxContainer.visible = true - SceneryResourceLoader.loading_request.connect(_do_update) - SceneryResourceLoader.loading_finished.connect(_do_update) - SceneryResourceLoader.scenery_loaded.connect(_on_scenery_loaded) - $AnimationPlayer.play("fade_in") -func _do_update(): - $%ProgressBar.value = SceneryResourceLoader.files_loaded - $%ProgressBar.max_value = SceneryResourceLoader.files_to_load - $%Message.text = SceneryResourceLoader.current_task - -func _on_scenery_loaded(): - SceneryResourceLoader.reset() - _do_update() - - if visible and autoclose_after_load: - $AnimationPlayer.play("dissolve") - - -func _on_animation_player_animation_finished(anim_name: StringName) -> void: - if anim_name == "fade_in": - fadein_finished.emit() +func _process(delta: float) -> void: + var status = ResourceLoader.load_threaded_get_status(SCENE_PATH, progress) + match status: + ResourceLoader.THREAD_LOAD_IN_PROGRESS: + var pct = progress[0] * 100 + $%ProgressBar.value = pct + ResourceLoader.THREAD_LOAD_LOADED: + var scene = ResourceLoader.load_threaded_get(SCENE_PATH) + get_tree().change_scene_to_packed(scene) diff --git a/demo/loading_screen/loading_screen.tscn b/demo/loading_screen/loading_screen.tscn index 83426b1c..6d1ea5e5 100644 --- a/demo/loading_screen/loading_screen.tscn +++ b/demo/loading_screen/loading_screen.tscn @@ -229,20 +229,22 @@ _data = { &"fade_in": SubResource("Animation_6yp64") } -[node name="LoadingScreen" type="PanelContainer" unique_id=1258855528] +[node name="LoadingScreen" type="PanelContainer" unique_id=974424848] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +offset_top = -78.0 +offset_bottom = 78.0 grow_horizontal = 2 grow_vertical = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_6lv5a") script = ExtResource("1_xq1lc") -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=523311916] +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=2055655165] layout_mode = 2 alignment = 1 -[node name="TextureRect" type="TextureRect" parent="VBoxContainer" unique_id=1501613550] +[node name="TextureRect" type="TextureRect" parent="VBoxContainer" unique_id=157059998] custom_minimum_size = Vector2(0, 600) layout_mode = 2 size_flags_horizontal = 4 @@ -250,18 +252,18 @@ size_flags_vertical = 4 texture = ExtResource("2_fyl6c") stretch_mode = 5 -[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer" unique_id=422310860] +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer" unique_id=1069824234] layout_mode = 2 theme_override_constants/margin_left = 100 theme_override_constants/margin_right = 100 -[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MarginContainer" unique_id=1624064189] +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MarginContainer" unique_id=679494584] custom_minimum_size = Vector2(0, 200) layout_mode = 2 size_flags_vertical = 3 size_flags_stretch_ratio = 2.0 -[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/MarginContainer/VBoxContainer" unique_id=1319813138] +[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/MarginContainer/VBoxContainer" unique_id=1015951774] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 4 @@ -269,18 +271,18 @@ theme_override_styles/background = SubResource("StyleBoxFlat_i6sv4") theme_override_styles/fill = SubResource("StyleBoxFlat_bd6b5") value = 25.0 -[node name="Message" type="Label" parent="VBoxContainer/MarginContainer/VBoxContainer" unique_id=2795491] +[node name="Message" type="Label" parent="VBoxContainer/MarginContainer/VBoxContainer" unique_id=1307755428] unique_name_in_owner = true layout_mode = 2 theme_override_constants/outline_size = 6 horizontal_alignment = 1 -[node name="Black" type="ColorRect" parent="." unique_id=1575392648] +[node name="Black" type="ColorRect" parent="." unique_id=227052382] z_index = 1 layout_mode = 2 color = Color(0, 0, 0, 0) -[node name="Background" type="TextureRect" parent="." unique_id=141237006] +[node name="Background" type="TextureRect" parent="." unique_id=954012219] modulate = Color(0.991164, 0.991164, 0.991164, 1) z_index = -1 material = SubResource("ShaderMaterial_wi7ft") @@ -289,7 +291,7 @@ texture = ExtResource("4_eev04") expand_mode = 5 stretch_mode = 5 -[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1474635995] +[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=142772226] libraries/ = SubResource("AnimationLibrary_pdtk4") [connection signal="animation_finished" from="AnimationPlayer" to="." method="_on_animation_player_animation_finished"] diff --git a/demo/project.godot b/demo/project.godot index 5dbf7989..f3845139 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -16,7 +16,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true config/name="MaSzyna Reloaded" config/description="A port of MaSzyna Train Simulator" -run/main_scene="res://demo_3d.tscn" +run/main_scene="uid://1djubq5jy2wx" run/print_header=false config/features=PackedStringArray("4.6", "Forward Plus") boot_splash/bg_color=Color(0, 0, 0, 1) @@ -29,20 +29,13 @@ run/disable_stderr.release=true [autoload] -MaszynaModelManager="*res://addons/libmaszyna/e3d/e3d_model_manager.gd" -SceneryResourceLoader="*uid://bgkb12fiqj2y2" MaszynaEnvironment="*uid://3nkx0u4dgdws" Console="*uid://d046w0soh2iou" -E3DParser="*uid://d08el7q5iodf4" -E3DModelManager="*uid://b2v733o52i40x" -E3DNodesInstancer="*uid://dl44jll5gimwi" -UserSettings="*uid://dd2pj6a7hturn" -E3DModelInstanceManager="*uid://bt2jcd405kweg" AudioStreamManager="*uid://cp8sgfk334adm" +UserSettings="*uid://dd2pj6a7hturn" [debug] -settings/stdout/verbose_stdout=true file_logging/log_path="user://logs/app.log" [display] @@ -50,9 +43,14 @@ file_logging/log_path="user://logs/app.log" window/size/mode=3 window/size/initial_position_type=3 +[editor_overrides] + +text_editor/behavior/indent/type=1 +text_editor/behavior/indent/size=4 + [editor_plugins] -enabled=PackedStringArray("res://addons/gut/plugin.cfg", "res://addons/libmaszyna/plugin.cfg") +enabled=PackedStringArray("res://addons/gnd_sfx/plugin.cfg", "res://addons/gnd_skydome/plugin.cfg", "res://addons/gnd_weather/plugin.cfg", "res://addons/gut/plugin.cfg", "res://addons/libmaszyna/plugin.cfg") [input] @@ -156,10 +154,19 @@ change_vehicle={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194336,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +horn1={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} +horn2={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} [rendering] -rendering_device/driver.windows="d3d12" textures/vram_compression/import_etc2_astc=true anti_aliasing/quality/msaa_3d=1 environment/volumetric_fog/volume_size=96 @@ -172,3 +179,27 @@ sky_texture={ "type": "sampler2D", "value": "" } +gnd_wind_direction={ +"type": "vec2", +"value": Vector2(0.8, 0.3) +} +gnd_wind_speed={ +"type": "float", +"value": 1.0 +} +gnd_wind_strength={ +"type": "float", +"value": 4.0 +} +gnd_wind_time={ +"type": "float", +"value": 0.0 +} +gnd_wind_turbulence={ +"type": "float", +"value": 1.0 +} +gnd_wind_pattern={ +"type": "sampler2D", +"value": "res://grass/wind_pattern.png" +} diff --git a/demo/sounds/hasler.tres b/demo/sounds/hasler.tres new file mode 100644 index 00000000..3f9d3f7c --- /dev/null +++ b/demo/sounds/hasler.tres @@ -0,0 +1,89 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://cg7f3nd65aevu"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_5p8xl"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_n0wvt"] +[ext_resource type="Curve" uid="uid://184cdj31umgt" path="res://sounds/hasler_fade_in_curve.tres" id="3_1be0m"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_5x8j1"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="3_n0wvt"] +[ext_resource type="Curve" uid="uid://ct72vfwtf0ufy" path="res://sounds/hasler_fade_out_curve.tres" id="4_7gox4"] + +[sub_resource type="AudioStream" id="AudioStream_n0wvt"] +script = ExtResource("3_n0wvt") +file_path = "20786_hasler_10_km" +loop = true +loop_offset = 0.5138321995464853 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_1be0m"] +script = ExtResource("3_5x8j1") +stream = SubResource("AudioStream_n0wvt") +offset = 1.0 +length = 15.03 +phase_offset = 0.863 +fade_in_curve = ExtResource("3_1be0m") +fade_out_curve = ExtResource("4_7gox4") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_0a7ww"] +script = ExtResource("3_n0wvt") +file_path = "20786_hasler_20_km" +loop = true +loop_offset = 0.5178231292517007 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_qhkib"] +script = ExtResource("3_5x8j1") +stream = SubResource("AudioStream_0a7ww") +offset = 15.0 +length = 35.0 +phase_offset = 0.22947845804988665 +fade_in_curve = ExtResource("3_1be0m") +fade_out_curve = ExtResource("4_7gox4") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_7a7ww"] +script = ExtResource("3_n0wvt") +file_path = "20786_hasler_50_km" +loop = true +loop_offset = 0.49412698412698414 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_sfxe8"] +script = ExtResource("3_5x8j1") +stream = SubResource("AudioStream_7a7ww") +offset = 45.0 +length = 55.0 +phase_offset = 0.995 +fade_in_curve = ExtResource("3_1be0m") +fade_out_curve = ExtResource("4_7gox4") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_qhkib"] +script = ExtResource("3_n0wvt") +file_path = "20786_hasler_100_km" +loop = true +loop_offset = 0.5170748299319728 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_li3hn"] +script = ExtResource("3_5x8j1") +stream = SubResource("AudioStream_qhkib") +offset = 95.0 +phase_offset = 0.3485827664399092 +fade_in_curve = ExtResource("3_1be0m") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="Resource" id="Resource_7gox4"] +script = ExtResource("1_5p8xl") +crossfade_mode = 1 +parameter_name = &"speed" +tracks = Array[ExtResource("3_5x8j1")]([SubResource("Resource_1be0m"), SubResource("Resource_qhkib"), SubResource("Resource_sfxe8"), SubResource("Resource_li3hn")]) +phase_locked = true +phase_period = 0.4997392290249434 +metadata/_custom_type_script = "uid://bf434i6ru4ch" + +[resource] +script = ExtResource("2_n0wvt") +name = &"hasler" +automations = Array[ExtResource("1_5p8xl")]([SubResource("Resource_7gox4")]) +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/sounds/hasler_fade_in_curve.tres b/demo/sounds/hasler_fade_in_curve.tres new file mode 100644 index 00000000..6ee57a7b --- /dev/null +++ b/demo/sounds/hasler_fade_in_curve.tres @@ -0,0 +1,6 @@ +[gd_resource type="Curve" format=3 uid="uid://184cdj31umgt"] + +[resource] +_limits = [0.0, 1.0, 0.0, 5.0] +_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(5, 1), 0.0, 0.0, 0, 0] +point_count = 2 diff --git a/demo/sounds/hasler_fade_out_curve.tres b/demo/sounds/hasler_fade_out_curve.tres new file mode 100644 index 00000000..fd666a91 --- /dev/null +++ b/demo/sounds/hasler_fade_out_curve.tres @@ -0,0 +1,6 @@ +[gd_resource type="Curve" format=3 uid="uid://ct72vfwtf0ufy"] + +[resource] +_limits = [0.0, 1.0, 0.0, 5.0] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(5, 0), 0.0, 0.0, 0, 0] +point_count = 2 diff --git a/demo/tests/test_console_focus.gd b/demo/tests/test_console_focus.gd new file mode 100644 index 00000000..d1f6f38c --- /dev/null +++ b/demo/tests/test_console_focus.gd @@ -0,0 +1,31 @@ +extends MaszynaGutTest + +var console: Node + + +func before_each() -> void: + console = load("res://addons/libmaszyna/console/console.gd").new() + add_child_autoqfree(console) + await wait_idle_frames(2) + + +func test_line_edit_keeps_editing_on_submit() -> void: + assert_true(console.line_edit.keep_editing_on_text_submit, "Console input should keep editing after text submit") + + +func test_submit_keeps_focus_and_editing_state() -> void: + console.set_visible(true) + await wait_idle_frames(1) + + assert_true(console.is_visible(), "Console should be visible after opening") + assert_eq(get_viewport().gui_get_focus_owner(), console.line_edit, "Console input should own focus when opened") + assert_true(console.line_edit.is_editing(), "Console input should enter editing mode when opened") + + console.line_edit.text = "help" + console.on_text_entered("help") + await wait_idle_frames(2) + + assert_true(console.is_visible(), "Console should stay visible after command submission") + assert_eq(get_viewport().gui_get_focus_owner(), console.line_edit, "Console input should keep focus after command submission") + assert_true(console.line_edit.is_editing(), "Console input should stay in editing mode after command submission") + assert_eq(console.line_edit.text, "", "Console input should be cleared after command submission") diff --git a/demo/tests/test_console_focus.gd.uid b/demo/tests/test_console_focus.gd.uid new file mode 100644 index 00000000..9eaf89e6 --- /dev/null +++ b/demo/tests/test_console_focus.gd.uid @@ -0,0 +1 @@ +uid://inqw7u7qpj3w diff --git a/demo/vehicles/impuls/impuls.tscn b/demo/vehicles/impuls/impuls.tscn index 7419e2a3..fd08cb2a 100644 --- a/demo/vehicles/impuls/impuls.tscn +++ b/demo/vehicles/impuls/impuls.tscn @@ -1,7 +1,6 @@ -[gd_scene format=3 uid="uid://dqwt31s3qnglp"] +[gd_scene load_steps=6 format=3 uid="uid://dqwt31s3qnglp"] [ext_resource type="Script" uid="uid://bmsduesjycv2v" path="res://addons/libmaszyna/rail_vehicle_3d.gd" id="1_4abai"] -[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="1_pes3o"] [ext_resource type="Material" uid="uid://bokq2xuaxcfw8" path="res://vehicles/impuls/impuls_head_display.tres" id="2_iawif"] [ext_resource type="PackedScene" uid="uid://cno8d0tylykmu" path="res://vehicles/impuls/impuls_cabin_a.tscn" id="2_tivfr"] @@ -11,9 +10,9 @@ size = Vector3(5.11954, 4.72223, 21.2475) [sub_resource type="BoxShape3D" id="BoxShape3D_3bq8j"] size = Vector3(5.11954, 4.72223, 21.2475) -[node name="Impuls" type="Node3D" unique_id=741729485] +[node name="Impuls" type="Node3D"] -[node name="Impuls-024a" type="Node3D" parent="." unique_id=62371356] +[node name="Impuls-024a" type="Node3D" parent="."] script = ExtResource("1_4abai") cabin_scene = ExtResource("2_tivfr") cabin_rotate_180deg = true @@ -22,201 +21,81 @@ head_display_e3d_path = NodePath("Impuls-024a") head_display_material = ExtResource("2_iawif") head_display_node_path = NodePath("Impuls-024a/banan/banan/tablice_relacyjne") -[node name="Impuls-024a" type="VisualInstance3D" parent="Impuls-024a" unique_id=170562242] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") +[node name="Impuls-024a" type="E3DModelInstance" parent="Impuls-024a"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "main/36wea-a_kd" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] exclude_node_names = ["cien"] - -[node name="Impuls-024a_LowPolyCab" type="VisualInstance3D" parent="Impuls-024a" unique_id=361100598] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") + +[node name="Impuls-024a_LowPolyCab" type="E3DModelInstance" parent="Impuls-024a"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "low_poly_int/int_36wea_kd-a" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] exclude_node_names = ["cien"] - -[node name="People_A" type="VisualInstance3D" parent="Impuls-024a" unique_id=117299797] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") + +[node name="People_A" type="E3DModelInstance" parent="Impuls-024a"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "loads/36wea-a_passengers" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] +transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -[node name="Area3D-A" type="Area3D" parent="Impuls-024a" unique_id=186197418] +[node name="Area3D-A" type="Area3D" parent="Impuls-024a"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.326183) monitoring = false -[node name="CollisionShape3D" type="CollisionShape3D" parent="Impuls-024a/Area3D-A" unique_id=2121185379] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Impuls-024a/Area3D-A"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0671234, 1.97558, -0.231311) shape = SubResource("BoxShape3D_tsk56") -[node name="Impuls-024b" type="Node3D" parent="." unique_id=1161615865] +[node name="Impuls-024b" type="Node3D" parent="."] script = ExtResource("1_4abai") head_display_e3d_path = NodePath("Impuls-024b") head_display_material = ExtResource("2_iawif") head_display_node_path = NodePath("Impuls-024b/banan/tablice_relacyjne") -[node name="Impuls-024b" type="VisualInstance3D" parent="Impuls-024b" unique_id=873606672] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 18.525) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") +[node name="Impuls-024b" type="E3DModelInstance" parent="Impuls-024b"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "main/31wea-b_kd" skins = ["36wea-019b,1", "36wea-019b,2", "36wea-019b,3", "36wea-019c,4"] exclude_node_names = ["cien"] +transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 18.525) -[node name="Impuls-024b_LowPolyInt" type="VisualInstance3D" parent="Impuls-024b" unique_id=887032148] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 18.525) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") +[node name="Impuls-024b_LowPolyInt" type="E3DModelInstance" parent="Impuls-024b"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "low_poly_int/int_36wea_kd-b" skins = ["36wea-019b,1", "36wea-019b,2", "36wea-019b,3", "36wea-019c,4"] exclude_node_names = ["cien"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 18.525) -[node name="People_B" type="VisualInstance3D" parent="Impuls-024b" unique_id=1981720913] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 1.74846e-07, 0, 1, 0, -1.74846e-07, 0, 1, 0, 0, 18.525) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") +[node name="People_B" type="E3DModelInstance" parent="Impuls-024b"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "loads/36wea-b_passengers" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] +transform = Transform3D(1, 0, 1.74846e-07, 0, 1, 0, -1.74846e-07, 0, 1, 0, 0, 18.525) -[node name="Impuls-024c" type="Node3D" parent="." unique_id=1550835407] +[node name="Impuls-024c" type="Node3D" parent="."] script = ExtResource("1_4abai") head_display_e3d_path = NodePath("Impuls-024c") head_display_material = ExtResource("2_iawif") head_display_node_path = NodePath("Impuls-024c/banan/tablice_relacyjne") -[node name="Impuls-024c" type="VisualInstance3D" parent="Impuls-024c" unique_id=457893452] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 34.4186) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") +[node name="Impuls-024c" type="E3DModelInstance" parent="Impuls-024c"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "main/31wea-c_kd" skins = ["36wea-019b,1", "36wea-019c,2", "36wea-019c,3", "36wea-019c,4"] exclude_node_names = ["cien"] - -[node name="Impuls-024c_LowPolyInt" type="VisualInstance3D" parent="Impuls-024c" unique_id=1503305875] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 34.4186) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") + +[node name="Impuls-024c_LowPolyInt" type="E3DModelInstance" parent="Impuls-024c"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "low_poly_int/int_36wea_kd-b" skins = ["36wea-019b,1", "36wea-019c,2", "36wea-019c,3", "36wea-019c,4"] exclude_node_names = ["cien"] +transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 34.4186) -[node name="Impuls-024d" type="Node3D" parent="." unique_id=1037242744] +[node name="Impuls-024d" type="Node3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 52.9294) script = ExtResource("1_4abai") cabin_scene = ExtResource("2_tivfr") @@ -225,75 +104,29 @@ head_display_e3d_path = NodePath("Impuls-024d") head_display_material = ExtResource("2_iawif") head_display_node_path = NodePath("Impuls-024d/banan/tablice_relacyjne") -[node name="Impuls-024d" type="VisualInstance3D" parent="Impuls-024d" unique_id=1769520630] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") +[node name="Impuls-024d" type="E3DModelInstance" parent="Impuls-024d"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "main/31wea-d_kd" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] exclude_node_names = ["cien"] - -[node name="Impuls-024d_LowPolyCab" type="VisualInstance3D" parent="Impuls-024d" unique_id=288601517] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") + +[node name="Impuls-024d_LowPolyCab" type="E3DModelInstance" parent="Impuls-024d"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "low_poly_int/int_36wea_kd-c" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] exclude_node_names = ["cien"] +transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -[node name="People_A2" type="VisualInstance3D" parent="Impuls-024d" unique_id=323616169] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_pes3o") +[node name="People_A2" type="E3DModelInstance" parent="Impuls-024d"] data_path = "/dynamic/pkp/impuls_v1" model_filename = "loads/36wea-a_passengers" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] -[node name="Area3D-A" type="Area3D" parent="Impuls-024d" unique_id=566175330] +[node name="Area3D-A" type="Area3D" parent="Impuls-024d"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.326183) monitoring = false -[node name="CollisionShape3D" type="CollisionShape3D" parent="Impuls-024d/Area3D-A" unique_id=2052501684] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Impuls-024d/Area3D-A"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0671234, 1.97558, -0.231311) shape = SubResource("BoxShape3D_3bq8j") diff --git a/demo/vehicles/impuls/impuls_cabin_a.tscn b/demo/vehicles/impuls/impuls_cabin_a.tscn index d41102e7..2c70bf42 100644 --- a/demo/vehicles/impuls/impuls_cabin_a.tscn +++ b/demo/vehicles/impuls/impuls_cabin_a.tscn @@ -1,13 +1,13 @@ -[gd_scene format=3 uid="uid://cno8d0tylykmu"] +[gd_scene load_steps=3 format=3 uid="uid://cno8d0tylykmu"] [ext_resource type="Script" uid="uid://76jsq4dvtdbf" path="res://vehicles/impuls/impuls_cabin_a.gd" id="1_fa3e3"] -[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="2_ytbcc"] -[node name="ImpulsCabin" type="Node3D" unique_id=303337587] +[node name="ImpulsCabin" type="Node3D"] script = ExtResource("1_fa3e3") driver_position = Vector3(-0.509006, 2.62038, 8.10148) -[node name="E3DModelInstance" type="VisualInstance3D" parent="." unique_id=95679097] +[node name="E3DModelInstance" type="E3DModelInstance" parent="."] +_import_path = NodePath("") unique_name_in_owner = false process_mode = 0 process_priority = 0 @@ -23,11 +23,11 @@ top_level = false visible = true visibility_parent = NodePath("") layers = 1 -script = ExtResource("2_ytbcc") data_path = "/dynamic/pkp/impuls_v1" model_filename = "kabina_a_kd" -[node name="E3DModelInstance3" type="VisualInstance3D" parent="." unique_id=88048388] +[node name="E3DModelInstance3" type="E3DModelInstance" parent="."] +_import_path = NodePath("") unique_name_in_owner = false process_mode = 0 process_priority = 0 @@ -43,7 +43,6 @@ top_level = false visible = true visibility_parent = NodePath("") layers = 1 -script = ExtResource("2_ytbcc") data_path = "/dynamic/pkp/impuls_v1" model_filename = "low_poly_int/int_36wea_kd-a" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] diff --git a/demo/vehicles/sm42/horns.gd b/demo/vehicles/sm42/horns.gd new file mode 100644 index 00000000..f8d4139f --- /dev/null +++ b/demo/vehicles/sm42/horns.gd @@ -0,0 +1,9 @@ +extends GenericTrainPart + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + register_command("horn", set_horn) + + +func set_horn(p_state) -> void: + self.update_state({"horn": p_state}) diff --git a/demo/vehicles/sm42/horns.gd.uid b/demo/vehicles/sm42/horns.gd.uid new file mode 100644 index 00000000..f7724db4 --- /dev/null +++ b/demo/vehicles/sm42/horns.gd.uid @@ -0,0 +1 @@ +uid://bd7e6ncgjphe7 diff --git a/demo/vehicles/sm42/sm42_sound_bank.tres b/demo/vehicles/sm42/sm42_sound_bank.tres new file mode 100644 index 00000000..e9cb7e7e --- /dev/null +++ b/demo/vehicles/sm42/sm42_sound_bank.tres @@ -0,0 +1,18 @@ +[gd_resource type="Resource" script_class="SfxBank" format=3 uid="uid://wi3rnp4pryi0"] + +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="1_3tkg2"] +[ext_resource type="Resource" uid="uid://xehc2ntq4f3w" path="res://vehicles/sm42/sounds/engine.tres" id="2_6tffg"] +[ext_resource type="Resource" uid="uid://caakm2jf2vlo6" path="res://vehicles/sm42/sounds/horn1.tres" id="3_0quvj"] +[ext_resource type="Resource" uid="uid://b5n7saj0mfejt" path="res://vehicles/sm42/sounds/horn2.tres" id="4_42g83"] +[ext_resource type="Resource" uid="uid://bc8ysdcqr22rb" path="res://vehicles/sm42/sounds/oil_pump.tres" id="5_n3jdp"] +[ext_resource type="Resource" uid="uid://cg7f3nd65aevu" path="res://sounds/hasler.tres" id="6_rjfok"] +[ext_resource type="Resource" uid="uid://bqiprpnqmu7iw" path="res://vehicles/sm42/sounds/alerter_light.tres" id="7_6tffg"] +[ext_resource type="Script" uid="uid://bbamgfxaoiofj" path="res://addons/gnd_sfx/sfx_bank.gd" id="7_55hqn"] +[ext_resource type="Resource" uid="uid://dr6q0ep567inn" path="res://vehicles/sm42/sounds/buzzer.tres" id="8_42g83"] +[ext_resource type="Resource" uid="uid://cqep24078h1i1" path="res://vehicles/sm42/sounds/cabin_button.tres" id="9_n3jdp"] +[ext_resource type="Resource" uid="uid://dg8rxskk1n38y" path="res://vehicles/sm42/sounds/cabin_switch.tres" id="10_n3jdp"] + +[resource] +script = ExtResource("7_55hqn") +events = Array[ExtResource("1_3tkg2")]([ExtResource("2_6tffg"), ExtResource("3_0quvj"), ExtResource("4_42g83"), ExtResource("5_n3jdp"), ExtResource("6_rjfok"), ExtResource("7_6tffg"), ExtResource("8_42g83"), ExtResource("9_n3jdp"), ExtResource("10_n3jdp")]) +metadata/_custom_type_script = "uid://bbamgfxaoiofj" diff --git a/demo/vehicles/sm42/sm_42.tscn b/demo/vehicles/sm42/sm_42.tscn index 8b852858..25353729 100644 --- a/demo/vehicles/sm42/sm_42.tscn +++ b/demo/vehicles/sm42/sm_42.tscn @@ -1,61 +1,33 @@ [gd_scene format=3 uid="uid://bcxtf08c2yqx4"] -[ext_resource type="Script" uid="uid://bmsduesjycv2v" path="res://addons/libmaszyna/rail_vehicle_3d.gd" id="1_iiqbf"] -[ext_resource type="PackedScene" uid="uid://do3s7mkbpv7es" path="res://vehicles/sm42/sm_42_cabin.tscn" id="2_htu7b"] +[ext_resource type="Script" uid="uid://bmsduesjycv2v" path="res://addons/libmaszyna/rail_vehicle_3d.gd" id="1_liqvc"] +[ext_resource type="PackedScene" uid="uid://do3s7mkbpv7es" path="res://vehicles/sm42/sm_42_cabin.tscn" id="2_clokw"] [ext_resource type="PackedScene" uid="uid://c5hi8nsm1d2hb" path="res://vehicles/sm42/sm_42v_1.tscn" id="3_vuhlb"] -[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="4_c88oy"] +[ext_resource type="Script" uid="uid://uo24v0se8gsf" path="res://addons/gnd_sfx/sfx_player_3d.gd" id="4_n7ime"] +[ext_resource type="Resource" uid="uid://wi3rnp4pryi0" path="res://vehicles/sm42/sm42_sound_bank.tres" id="5_liqvc"] +[ext_resource type="Script" uid="uid://ba86u1wfkr52x" path="res://addons/libmaszyna/sound/train_sound_trigger.gd" id="6_clokw"] -[sub_resource type="BoxShape3D" id="BoxShape3D_uljbo"] -size = Vector3(4.5332, 4.72223, 17.0549) +[sub_resource type="BoxShape3D" id="BoxShape3D_liqvc"] +size = Vector3(4.0637207, 3.6567383, 12.690292) -[node name="SM42-099" type="Node3D" unique_id=1967816975] -transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -script = ExtResource("1_iiqbf") +[node name="SM42-099" type="Node3D" unique_id=1097141795] +transform = Transform3D(-1, 0, -1.509958e-07, 0, 1, 0, 1.509958e-07, 0, -1, 0, 0, 0) +script = ExtResource("1_liqvc") controller_path = NodePath("SM42v1") -cabin_scene = ExtResource("2_htu7b") +cabin_scene = ExtResource("2_clokw") low_poly_cabin_path = NodePath("SM42-099-LowPolyCab") +metadata/_custom_type_script = "uid://bmsduesjycv2v" [node name="SM42v1" parent="." unique_id=111298649 instance=ExtResource("3_vuhlb")] +mass = 174000.0 -[node name="SM42-099" type="VisualInstance3D" parent="." unique_id=853366110] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_c88oy") +[node name="SM42-099" type="E3DModelInstance" parent="." unique_id=853366110] data_path = "/dynamic/pkp/sm42_v1" model_filename = "6da" skins = ["6d-907"] exclude_node_names = ["cien"] -[node name="SM42-099-LowPolyCab" type="VisualInstance3D" parent="." unique_id=1411080143] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_c88oy") +[node name="SM42-099-LowPolyCab" type="E3DModelInstance" parent="." unique_id=1411080143] data_path = "/dynamic/pkp/sm42_v1" model_filename = "6da_interior" skins = ["6d-907"] @@ -65,115 +37,85 @@ exclude_node_names = ["cien"] monitoring = false [node name="CollisionShape3D" type="CollisionShape3D" parent="Area3D" unique_id=1381651767] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.53125, 1.97558, 0.243691) -shape = SubResource("BoxShape3D_uljbo") - -[node name="401W-1" type="VisualInstance3D" parent="." unique_id=1515864927] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -70.1831) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_c88oy") +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.3839112, 2.1833436, 0.27777177) +shape = SubResource("BoxShape3D_liqvc") + +[node name="401W-1" type="E3DModelInstance" parent="." unique_id=1515864927] data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] exclude_node_names = ["cien"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -70.1831) -[node name="401W-2" type="VisualInstance3D" parent="." unique_id=589335020] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -56.1651) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_c88oy") +[node name="401W-2" type="E3DModelInstance" parent="." unique_id=589335020] data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] exclude_node_names = ["cien"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -56.1651) -[node name="401W-3" type="VisualInstance3D" parent="." unique_id=1577934693] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -42.1591) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_c88oy") +[node name="401W-3" type="E3DModelInstance" parent="." unique_id=1577934693] data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] exclude_node_names = ["cien"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -42.1591) -[node name="401W-4" type="VisualInstance3D" parent="." unique_id=229000746] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -28.1431) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_c88oy") +[node name="401W-4" type="E3DModelInstance" parent="." unique_id=229000746] data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] exclude_node_names = ["cien"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -28.1431) -[node name="401W-5" type="VisualInstance3D" parent="." unique_id=1494701951] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -14.1341) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("4_c88oy") +[node name="401W-5" type="E3DModelInstance" parent="." unique_id=1494701951] data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] exclude_node_names = ["cien"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.17445e-06, 0, -14.1341) + +[node name="SfxPlayer3D" type="Node3D" parent="." unique_id=1646618549] +transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, -0.058641274, 1.3523579, 0.6670667) +script = ExtResource("4_n7ime") +bank = ExtResource("5_liqvc") +attenuation_model = 1 +unit_size = 20.0 +max_distance = 100 +playback_effect = &"horn1" +playback_automation_value = 489.0 +metadata/_custom_type_script = "uid://uo24v0se8gsf" + +[node name="OilPump" type="Node" parent="SfxPlayer3D" unique_id=132705259] +script = ExtResource("6_clokw") +state_property = "oil_pump_active" +sound_event = &"oil_pump" +controller_path = NodePath("../../SM42v1") +metadata/_custom_type_script = "uid://ba86u1wfkr52x" + +[node name="Engine" type="Node" parent="SfxPlayer3D" unique_id=381118470] +script = ExtResource("6_clokw") +state_property = "engine_rpm" +trigger_mode = 1 +trigger_threshold_min = 10.0 +trigger_threshold_max = 10000.0 +sound_event = &"engine" +sound_parameter = &"rpm" +controller_path = NodePath("../../SM42v1") +metadata/_custom_type_script = "uid://ba86u1wfkr52x" + +[node name="Horn1" type="Node" parent="SfxPlayer3D" unique_id=209300178] +script = ExtResource("6_clokw") +state_property = "horn" +trigger_threshold_min = -1.0 +trigger_threshold_max = 0.0 +sound_event = &"horn1" +controller_path = NodePath("../../SM42v1") +metadata/_custom_type_script = "uid://ba86u1wfkr52x" + +[node name="Horn2" type="Node" parent="SfxPlayer3D" unique_id=875255125] +script = ExtResource("6_clokw") +state_property = "horn" +sound_event = &"horn2" +controller_path = NodePath("../../SM42v1") +metadata/_custom_type_script = "uid://ba86u1wfkr52x" diff --git a/demo/vehicles/sm42/sm_42_cabin.gd b/demo/vehicles/sm42/sm_42_cabin.gd index 2354d4cb..0d60ae2f 100644 --- a/demo/vehicles/sm42/sm_42_cabin.gd +++ b/demo/vehicles/sm42/sm_42_cabin.gd @@ -5,6 +5,6 @@ func _on_czuwak_blink(state): $Lights/CzuwakOmni2.enabled = state $Lights/CzuwakOmni3.enabled = state if state: - $AlerterLightOn.play() + $Sounds/AlerterLight.play("alerter_light") else: - $AlerterLightOff.play() + $Sounds/AlerterLight.stop("alerter_light") diff --git a/demo/vehicles/sm42/sm_42_cabin.tscn b/demo/vehicles/sm42/sm_42_cabin.tscn index 5602e2c8..ec81a419 100644 --- a/demo/vehicles/sm42/sm_42_cabin.tscn +++ b/demo/vehicles/sm42/sm_42_cabin.tscn @@ -1,9 +1,10 @@ [gd_scene format=3 uid="uid://do3s7mkbpv7es"] -[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="1_6k4j1"] [ext_resource type="Script" uid="uid://fi23qlrohins" path="res://vehicles/sm42/sm_42_cabin.gd" id="1_bffp8"] +[ext_resource type="Resource" uid="uid://wi3rnp4pryi0" path="res://vehicles/sm42/sm42_sound_bank.tres" id="2_mu6ha"] [ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="7_lbg33"] [ext_resource type="Script" uid="uid://blbw51qwvmbue" path="res://addons/libmaszyna/sound/train_sound_3d.gd" id="15_df5xj"] +[ext_resource type="Script" uid="uid://uo24v0se8gsf" path="res://addons/gnd_sfx/sfx_player_3d.gd" id="17_4hnq2"] [ext_resource type="PackedScene" uid="uid://djl581jijicpq" path="res://addons/libmaszyna/cabin/cabin_command.tscn" id="66_epgva"] [ext_resource type="Script" uid="uid://c4lj08dvqxvhn" path="res://addons/libmaszyna/cabin/cabin_gauge.gd" id="67_fryen"] [ext_resource type="PackedScene" uid="uid://cfj82gn1nfl36" path="res://addons/libmaszyna/cabin/cabin_gauge.tscn" id="68_g4w65"] @@ -13,94 +14,9 @@ [ext_resource type="PackedScene" uid="uid://cmluhfe2vkxcc" path="res://addons/libmaszyna/cabin/cabin_switch.tscn" id="72_a07r6"] [ext_resource type="Script" uid="uid://brxp2te1tebgk" path="res://addons/libmaszyna/cabin/cabin_spot_light_3d.gd" id="73_lxu5b"] [ext_resource type="Script" uid="uid://bw876lucal3mo" path="res://addons/libmaszyna/cabin/cabin_omni_light_3d.gd" id="74_q8ngl"] -[ext_resource type="Texture2D" uid="uid://cycwf1avry6mk" path="res://vehicles/sm42/czuwak_projector.png" id="75_p8gfg"] +[ext_resource type="Texture2D" uid="uid://wf2pbnu62pgl" path="res://vehicles/sm42/czuwak_projector.png" id="75_p8gfg"] [ext_resource type="PackedScene" uid="uid://gp6wbsgcq48k" path="res://addons/libmaszyna/cabin/cabin_blinker.tscn" id="76_djwot"] -[sub_resource type="AudioStream" id="AudioStream_hwnyj"] -script = ExtResource("7_lbg33") -file_path = "sm42czuwak" -loop = true - -[sub_resource type="AudioStream" id="AudioStream_ii5lq"] -script = ExtResource("7_lbg33") -file_path = "20786_203e_light_shp_start" - -[sub_resource type="AudioStream" id="AudioStream_xhr6w"] -script = ExtResource("7_lbg33") -file_path = "20786_203e_light_shp_stop" - -[sub_resource type="AudioStream" id="AudioStream_ffwa0"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_left_white_light_2_on" - -[sub_resource type="AudioStream" id="AudioStream_hgbvq"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_left_white_light_2_of" - -[sub_resource type="AudioStream" id="AudioStream_burd0"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_lights_cabin_2_on" - -[sub_resource type="AudioStream" id="AudioStream_pmvef"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_lights_cabin_2_of" - -[sub_resource type="AudioStream" id="AudioStream_2hxr7"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_lights_cabin_2_on" - -[sub_resource type="AudioStream" id="AudioStream_3t6f6"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_lights_cabin_2_of" - -[sub_resource type="AudioStream" id="AudioStream_ym8rb"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_lights_cabin_2_on" - -[sub_resource type="AudioStream" id="AudioStream_qeyam"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_switch_lights_cabin_2_of" - -[sub_resource type="AudioStream" id="AudioStream_tir1f"] -script = ExtResource("7_lbg33") -file_path = "20786_4e_119_button_shp_1_on" - -[sub_resource type="AudioStream" id="AudioStream_h2c58"] -script = ExtResource("7_lbg33") -file_path = "20786_4e_119_button_shp_1_of" - -[sub_resource type="AudioStream" id="AudioStream_5cke3"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_button_releaser_2_on" - -[sub_resource type="AudioStream" id="AudioStream_05d8o"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_button_releaser_2_of" - -[sub_resource type="AudioStream" id="AudioStream_p1s5s"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_button_ws_1_on" - -[sub_resource type="AudioStream" id="AudioStream_4cuxf"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_button_ws_1_of" - -[sub_resource type="AudioStream" id="AudioStream_54m4i"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_button_ws_of_2_on" - -[sub_resource type="AudioStream" id="AudioStream_4kcv5"] -script = ExtResource("7_lbg33") -file_path = "20786_203E_button_ws_of_2_of" - -[sub_resource type="AudioStream" id="AudioStream_bfhgf"] -script = ExtResource("7_lbg33") -file_path = "20786_203e_button_radio_3_on" - -[sub_resource type="AudioStream" id="AudioStream_gspxv"] -script = ExtResource("7_lbg33") -file_path = "20786_203e_button_radio_3_of" - [sub_resource type="AudioStream" id="AudioStream_gg4oe"] script = ExtResource("7_lbg33") file_path = "20786_reverser_6d_inc_v2" @@ -169,85 +85,88 @@ file_path = "20786_control_6d_2" script = ExtResource("7_lbg33") file_path = "20786_control_6d_6" -[node name="SM42Cabin" type="Node3D" unique_id=1273857723] +[node name="SM42Cabin" type="Node3D" unique_id=1915165861] script = ExtResource("1_bffp8") camera_bound_min = Vector3(-1.65, 1.8, -3.4) camera_bound_max = Vector3(1.65, 2.05, -2.83) camera_bound_enabled = true driver_position = Vector3(-0.99, 3.454, -3.311) +sound_bank = ExtResource("2_mu6ha") -[node name="Cabin" type="VisualInstance3D" parent="." unique_id=159772609] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("1_6k4j1") +[node name="Cabin" type="E3DModelInstance" parent="." unique_id=115171064] data_path = "/dynamic/pkp/sm42_v1" model_filename = "6da_kabina" -[node name="Buzzer" type="AudioStreamPlayer3D" parent="." unique_id=942524699] -transform = Transform3D(0.999125, -0.0128957, 0.0397967, 0, 0.951302, 0.30826, -0.0418339, -0.30799, 0.950469, -0.988872, 2.80983, 2.49681) -stream = SubResource("AudioStream_hwnyj") +[node name="Sounds" type="Node3D" parent="." unique_id=2006517475] + +[node name="Hasler" type="Node3D" parent="Sounds" unique_id=1746132504] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.9841678, 3.1604133, -1.9004567) +script = ExtResource("15_df5xj") +state_property = "speed" +trigger_mode = 1 +trigger_threshold_min = 0.01 +trigger_threshold_max = 300.0 +sound_event = &"hasler" +sound_parameter = &"speed" +bank = ExtResource("2_mu6ha") +max_tracks = 4 +metadata/_custom_type_script = "uid://blbw51qwvmbue" + +[node name="Buzzer" type="Node3D" parent="Sounds" unique_id=503665757] +transform = Transform3D(0.999125, -0.0128957, 0.0397967, 0, 0.951302, 0.30826, -0.0418339, -0.30799, 0.950469, -0.32159233, 2.80983, -2.6323152) script = ExtResource("15_df5xj") state_property = "beeping" - -[node name="AlerterLightOn" type="AudioStreamPlayer3D" parent="." unique_id=809716656] -transform = Transform3D(0.991105, -0.0399184, 0.126957, 0, 0.953956, 0.299946, -0.133085, -0.297278, 0.94547, 0.704235, 3.06481, 2.01449) -stream = SubResource("AudioStream_ii5lq") - -[node name="AlerterLightOff" type="AudioStreamPlayer3D" parent="." unique_id=523314564] -transform = Transform3D(0.991105, -0.0399184, 0.126957, 0, 0.953956, 0.299946, -0.133085, -0.297278, 0.94547, 0.704235, 3.06481, 2.01449) -stream = SubResource("AudioStream_xhr6w") - -[node name="Commands" type="Node" parent="." unique_id=2005708606] - -[node name="BrakeLevelSet_Drive" parent="Commands" unique_id=229069845 instance=ExtResource("66_epgva")] +sound_event = &"buzzer" +bank = ExtResource("2_mu6ha") +max_tracks = 1 +playback_effect = &"buzzer" +metadata/_custom_type_script = "uid://blbw51qwvmbue" + +[node name="AlerterLight" type="Node3D" parent="Sounds" unique_id=118834087] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.03002578, 3.3033729, -1.8377038) +script = ExtResource("17_4hnq2") +bank = ExtResource("2_mu6ha") +max_tracks = 2 +metadata/_custom_type_script = "uid://uo24v0se8gsf" + +[node name="Commands" type="Node" parent="." unique_id=1967708882] + +[node name="BrakeLevelSet_Drive" parent="Commands" unique_id=507523159 instance=ExtResource("66_epgva")] action_name = "brake_level_drive" command = "brake_level_set_position" command_param = "drive" -[node name="Gauges" type="Node3D" parent="." unique_id=1005883781] +[node name="Gauges" type="Node3D" parent="." unique_id=1901864646] transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -[node name="EngineRPM" type="Node3D" parent="Gauges" unique_id=2023033448] +[node name="EngineRPM" type="Node3D" parent="Gauges" unique_id=692405195] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0178885, 3.15643, 1.98876) script = ExtResource("67_fryen") mesh_rotation = Vector3(0, 0.151, 0) state_property = "engine_rpm" target_mesh_path = NodePath("../../Cabin/banan/podloga_glowna/obrot01") -[node name="Speed" type="Node3D" parent="Gauges" unique_id=896816523] +[node name="Speed" type="Node3D" parent="Gauges" unique_id=712761867] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0178885, 3.15643, 1.98876) script = ExtResource("67_fryen") mesh_rotation = Vector3(0, 2.4, 0) state_property = "speed" target_mesh_path = NodePath("../../Cabin/banan/hasler/speedknob") -[node name="OilPressure" type="Node3D" parent="Gauges" unique_id=543191871] +[node name="OilPressure" type="Node3D" parent="Gauges" unique_id=828496323] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.422279, 3.02141, 2.00855) script = ExtResource("67_fryen") mesh_rotation = Vector3(0, -200, 0) state_property = "oil_pump_pressure" target_mesh_path = NodePath("../../Cabin/banan/podloga_glowna/cyl04") -[node name="PrzewodGlowny" parent="Gauges" unique_id=517930769 instance=ExtResource("68_g4w65")] +[node name="PrzewodGlowny" parent="Gauges" unique_id=357696114 instance=ExtResource("68_g4w65")] max_value = 10.0 mesh_rotation = Vector3(0, 270, 0) state_property = "pipe_pressure" target_mesh_path = NodePath("../../Cabin/banan/podloga_glowna/cyl03") -[node name="BrakeCyl" parent="Gauges" unique_id=917435695 instance=ExtResource("68_g4w65")] +[node name="BrakeCyl" parent="Gauges" unique_id=851700422 instance=ExtResource("68_g4w65")] max_value = 10.0 max_angle = 272.0 mesh_rotation = Vector3(0, 272, 0) @@ -255,104 +174,90 @@ state_property = "brake_air_pressure" max_state_property = "brake_tank_volume" target_mesh_path = NodePath("../../Cabin/banan/podloga_glowna/cyl06") -[node name="Switches" type="Node3D" parent="." unique_id=1310351452] +[node name="Switches" type="Node3D" parent="." unique_id=1635056921] transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -[node name="DeviesLight" parent="Switches" unique_id=582197935 instance=ExtResource("69_feumc")] +[node name="DeviesLight" parent="Switches" unique_id=1038443810 instance=ExtResource("69_feumc")] transform = Transform3D(0.999609, 0.0164182, -0.0226356, 0, 0.809485, 0.587141, 0.027963, -0.586911, 0.809168, -0.131001, 2.99968, 2.2417) mesh_path = NodePath("../../Cabin/banan/podloga_glowna/pulpit_duzy/przodreflprawy24") command = "devices_light" state_property = "devices_light_enabled" mesh_rotation = Vector3(0, -85, 0) -sound_on = SubResource("AudioStream_ffwa0") -sound_off = SubResource("AudioStream_hgbvq") +sound_event = "cabin_switch" action = "devices_light_toggle" -[node name="RoofLight" parent="Switches" unique_id=1111851228 instance=ExtResource("69_feumc")] +[node name="RoofLight" parent="Switches" unique_id=1261216337 instance=ExtResource("69_feumc")] transform = Transform3D(0.999889, 0.00358736, -0.0144371, 0, 0.970488, 0.241149, 0.0148761, -0.241122, 0.970381, -0.0800086, 2.99968, 2.24417) mesh_path = NodePath("../../Cabin/banan/podloga_glowna/pulpit_duzy/przodreflprawy22") command = "roof_light" state_property = "roof_light_enabled" mesh_rotation = Vector3(0, -85, 0) -sound_on = SubResource("AudioStream_burd0") -sound_off = SubResource("AudioStream_pmvef") +sound_event = "cabin_switch" action = "cabin_light_toggle" -[node name="FuelPump" parent="Switches" unique_id=1266178667 instance=ExtResource("69_feumc")] +[node name="FuelPump" parent="Switches" unique_id=1806107420 instance=ExtResource("69_feumc")] transform = Transform3D(0.996349, -0.0256081, 0.0814447, 0, 0.953956, 0.299946, -0.0853757, -0.298851, 0.950473, 0.219126, 2.99231, 2.34069) monostable = true mesh_path = NodePath("../../Cabin/banan/podloga_glowna/pulpit_duzy/pompka_paliwa") command = "fuel_pump" state_property = "fuel_pump_active" mesh_rotation = Vector3(0, -85, 0) -sound_on = SubResource("AudioStream_2hxr7") -sound_off = SubResource("AudioStream_3t6f6") +sound_event = "cabin_switch" action = "fuel_pump_toggle" -[node name="OilPump" parent="Switches" unique_id=1012996107 instance=ExtResource("69_feumc")] +[node name="OilPump" parent="Switches" unique_id=149823012 instance=ExtResource("69_feumc")] transform = Transform3D(0.999157, 0.0166647, -0.0375098, 0, 0.913869, 0.406009, 0.0410451, -0.405667, 0.913099, 0.0726701, 3.01034, 2.33282) monostable = true mesh_path = NodePath("../../Cabin/banan/podloga_glowna/pulpit_duzy/przodreflprawy17") command = "oil_pump" state_property = "oil_pump_active" mesh_rotation = Vector3(0, -85, 0) -sound_on = SubResource("AudioStream_ym8rb") -sound_off = SubResource("AudioStream_qeyam") +sound_event = "cabin_switch" action = "oil_pump_toggle" -[node name="SecurityAcknowledge" parent="Switches" unique_id=1621161711 instance=ExtResource("69_feumc")] +[node name="SecurityAcknowledge" parent="Switches" unique_id=1988610137 instance=ExtResource("69_feumc")] transform = Transform3D(0.999976, -0.00325316, 0.00613007, 0, 0.883321, 0.468769, -0.0069398, -0.468757, 0.8833, 0.803523, 2.97298, 2.4885) monostable = true mesh_path = NodePath("../../Cabin/banan/podloga_glowna/czuw_shp") command = "security_acknowledge" mesh_position = Vector3(0, 0, -0.006) -sound_on = SubResource("AudioStream_tir1f") -sound_off = SubResource("AudioStream_h2c58") action = "security_acknowledge" -[node name="BrakeReleaser" parent="Switches" unique_id=1917296538 instance=ExtResource("69_feumc")] +[node name="BrakeReleaser" parent="Switches" unique_id=361997116 instance=ExtResource("69_feumc")] transform = Transform3D(0.983577, -0.0735428, 0.164826, 0, 0.913221, 0.407465, -0.180489, -0.400773, 0.898223, 0.662463, 2.95732, 2.4515) monostable = true mesh_path = NodePath("../../Cabin/banan/podloga_glowna/odl") command = "brake_releaser" mesh_position = Vector3(0, 0, -0.004) -sound_on = SubResource("AudioStream_5cke3") -sound_off = SubResource("AudioStream_05d8o") action = "brake_release" -[node name="Start" parent="Switches" unique_id=268113647 instance=ExtResource("69_feumc")] +[node name="Start" parent="Switches" unique_id=1414292914 instance=ExtResource("69_feumc")] transform = Transform3D(0.996416, 0.0396522, -0.0747185, 0, 0.883321, 0.468768, 0.0845882, -0.467088, 0.880155, -0.0449095, 2.94281, 2.08425) monostable = true mesh_path = NodePath("../../Cabin/banan/podloga_glowna/start") command = "main_switch" controller_mode = 1 mesh_position = Vector3(0, 0, -0.004) -sound_on = SubResource("AudioStream_p1s5s") -sound_off = SubResource("AudioStream_4cuxf") action = "main_switch_on" -[node name="Stop" parent="Switches" unique_id=1289664625 instance=ExtResource("69_feumc")] +[node name="Stop" parent="Switches" unique_id=1992841207 instance=ExtResource("69_feumc")] transform = Transform3D(0.999936, -0.00277359, 0.0109576, 0, 0.969427, 0.245382, -0.0113032, -0.245366, 0.969365, 0.0232396, 2.96006, 2.08687) monostable = true mesh_path = NodePath("../../Cabin/banan/podloga_glowna/stop") command = "main_switch" controller_mode = 2 mesh_position = Vector3(0, 0, -0.004) -sound_on = SubResource("AudioStream_54m4i") -sound_off = SubResource("AudioStream_4kcv5") action = "main_switch_off" -[node name="RadioToggle" parent="Switches" unique_id=2014769954 instance=ExtResource("69_feumc")] +[node name="RadioToggle" parent="Switches" unique_id=53097585 instance=ExtResource("69_feumc")] transform = Transform3D(0.995566, -0.00607763, 0.093871, 0, 0.997911, 0.0646092, -0.0940675, -0.0643227, 0.993486, 0.489931, 2.79296, 2.62626) mesh_path = NodePath("../../Cabin/banan/radio/swr-wl") command = "radio" state_property = "radio_enabled" mesh_position = Vector3(0, 0, -0.004) -sound_on = SubResource("AudioStream_bfhgf") -sound_off = SubResource("AudioStream_gspxv") action = "radio_toggle" -[node name="Reverser" type="Node3D" parent="Switches" unique_id=1690432205] +[node name="Reverser" type="Node3D" parent="Switches" unique_id=1187736882] transform = Transform3D(0.998289, 0.0302963, -0.0500164, 0, 0.855324, 0.518093, 0.0584765, -0.517206, 0.853861, 0.747439, 2.89025, 2.30261) script = ExtResource("70_sdo5w") switch_min_position = -1 @@ -367,7 +272,7 @@ sound_neutral_position_stream = SubResource("AudioStream_k7rjm") action_increase = "direction_increase" action_decrease = "direction_decrease" -[node name="Zasadniczy" type="Node3D" parent="Switches" unique_id=1026839427] +[node name="Zasadniczy" type="Node3D" parent="Switches" unique_id=2019918712] transform = Transform3D(-0.22677, 0, 0.973948, 0, 1, 0, -0.973948, 0, -0.22677, 1.26591, 2.97955, 2.3353) script = ExtResource("71_h1qpq") mesh_path = NodePath("../../Cabin/banan/podloga_glowna/zasadniczy") @@ -377,7 +282,7 @@ mesh_rotation = Vector3(0, 120, 0) action_increase = "brake_level_increase" action_decrease = "brake_level_decrease" -[node name="MainController" parent="Switches" unique_id=975265843 instance=ExtResource("72_a07r6")] +[node name="MainController" parent="Switches" unique_id=1347101638 instance=ExtResource("72_a07r6")] transform = Transform3D(0.999877, -0.00636055, 0.0143167, 0, 0.913869, 0.406009, -0.015666, -0.405959, 0.913757, 0.629557, 2.7611, 2.36947) switch_max_position = 11 mesh_path = NodePath("../../Cabin/banan/podloga_glowna/pomocniczy/nastawnik") @@ -391,17 +296,29 @@ sound_override = Array[AudioStream]([SubResource("AudioStream_cq0wk"), SubResour action_increase = "main_controller_increase" action_decrease = "main_controller_decrease" -[node name="Battery" parent="Switches" unique_id=828418078 instance=ExtResource("69_feumc")] +[node name="Battery" parent="Switches" unique_id=660648759 instance=ExtResource("69_feumc")] transform = Transform3D(0.999722, 0.00711453, -0.0225018, 0, 0.953477, 0.301467, 0.0235997, -0.301383, 0.953211, -0.491953, 2.9605, 2.31912) command = "battery" state_property = "battery_enabled" action = "battery_toggle" controller_path = NodePath("../../../SM42v1") -[node name="Lights" type="Node3D" parent="." unique_id=454038932] +[node name="Horn" type="Node3D" parent="Switches" unique_id=1683144414] +script = ExtResource("70_sdo5w") +switch_min_position = -1 +automatic_reset = true +mesh_path = NodePath("../../Cabin/banan/podloga_glowna/horn") +command_set = "horn" +state_property = "horn" +mesh_rotation = Vector3(0, -20, 0) +action_increase = "horn1" +action_decrease = "horn2" +metadata/_custom_type_script = "uid://3hp38qtyx500" + +[node name="Lights" type="Node3D" parent="." unique_id=348672108] transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) -[node name="RoofLight" type="SpotLight3D" parent="Lights" unique_id=55981619] +[node name="RoofLight" type="SpotLight3D" parent="Lights" unique_id=413726375] transform = Transform3D(0.671019, 0.739562, 0.0527488, 8.19564e-08, -0.0711437, 0.997466, 0.741441, -0.669318, -0.0477388, 0.0180681, 3.9919, 2.8865) light_color = Color(0.960938, 0.881759, 0.75824, 1) light_energy = 0.411 @@ -416,7 +333,7 @@ script = ExtResource("73_lxu5b") state_property = "roof_light_enabled" light_energy_on = 0.411 -[node name="RadioPowerLed" type="OmniLight3D" parent="Lights" unique_id=901208068] +[node name="RadioPowerLed" type="OmniLight3D" parent="Lights" unique_id=2101402398] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.487651, 2.78811, 2.62152) light_color = Color(0, 0.738281, 0.121986, 1) light_energy = 0.007 @@ -425,7 +342,7 @@ script = ExtResource("74_q8ngl") state_property = "radio_powered" light_energy_on = 0.05 -[node name="RadioPowerLed2" type="OmniLight3D" parent="Lights" unique_id=1702426315] +[node name="RadioPowerLed2" type="OmniLight3D" parent="Lights" unique_id=291512366] transform = Transform3D(0.999945, 0.000907595, -0.0104695, 0.00153279, 0.973012, 0.230748, 0.0103964, -0.230751, 0.972957, 0.715, 3.009, 2.036) light_color = Color(0.344238, 0.631599, 0.9375, 1) light_energy = 0.007 @@ -438,7 +355,7 @@ script = ExtResource("74_q8ngl") state_property = "radio_powered" light_energy_on = 0.002 -[node name="DeviceLight1" type="SpotLight3D" parent="Lights" unique_id=220143743] +[node name="DeviceLight1" type="SpotLight3D" parent="Lights" unique_id=1287725166] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0104857, -0.15218, 0.988297, -0.000696268, -0.988352, -0.152181, -0.0886833, 3.20948, 1.94735) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -451,7 +368,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight2" type="SpotLight3D" parent="Lights" unique_id=1035238683] +[node name="DeviceLight2" type="SpotLight3D" parent="Lights" unique_id=752508636] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0104857, -0.15218, 0.988297, -0.000696268, -0.988352, -0.152181, 0.0841957, 3.20948, 1.94752) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -464,7 +381,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight3" type="SpotLight3D" parent="Lights" unique_id=1171350953] +[node name="DeviceLight3" type="SpotLight3D" parent="Lights" unique_id=510377640] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0104857, -0.15218, 0.988297, -0.000696268, -0.988352, -0.152181, 0.376934, 3.24047, 1.93445) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -477,7 +394,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight5" type="SpotLight3D" parent="Lights" unique_id=1306658441] +[node name="DeviceLight5" type="SpotLight3D" parent="Lights" unique_id=981025760] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0104857, -0.15218, 0.988297, -0.000696268, -0.988352, -0.152181, 0.521681, 3.24047, 1.93445) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -490,7 +407,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight4" type="SpotLight3D" parent="Lights" unique_id=1631062914] +[node name="DeviceLight4" type="SpotLight3D" parent="Lights" unique_id=393566833] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0104857, -0.15218, 0.988297, -0.000696268, -0.988352, -0.152181, 0.229699, 3.21728, 1.93445) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -503,7 +420,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight6" type="SpotLight3D" parent="Lights" unique_id=699677396] +[node name="DeviceLight6" type="SpotLight3D" parent="Lights" unique_id=154932506] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0104857, -0.15218, 0.988297, -0.000696268, -0.988352, -0.152181, 0.670892, 3.21882, 1.93445) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -516,7 +433,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight11" type="SpotLight3D" parent="Lights" unique_id=1625272036] +[node name="DeviceLight11" type="SpotLight3D" parent="Lights" unique_id=896249208] transform = Transform3D(-0.999945, -0.000907594, 0.0104694, -0.0104856, 0.15218, -0.988297, -0.000696268, -0.988352, -0.152181, 0.991721, 3.09631, 1.92514) light_color = Color(0.0111694, 0.714844, 0.0221643, 1) light_energy = 0.002 @@ -529,7 +446,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight7" type="SpotLight3D" parent="Lights" unique_id=1208334636] +[node name="DeviceLight7" type="SpotLight3D" parent="Lights" unique_id=667390765] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0103584, -0.253071, 0.967392, -0.00177152, -0.967447, -0.253066, -0.21773, 3.21163, 1.94631) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -542,7 +459,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight8" type="SpotLight3D" parent="Lights" unique_id=1211477804] +[node name="DeviceLight8" type="SpotLight3D" parent="Lights" unique_id=316533651] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0103584, -0.253071, 0.967392, -0.00177152, -0.967447, -0.253066, -0.322201, 3.21163, 1.94631) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -555,7 +472,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight9" type="SpotLight3D" parent="Lights" unique_id=1056717777] +[node name="DeviceLight9" type="SpotLight3D" parent="Lights" unique_id=1298845471] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0103584, -0.253071, 0.967392, -0.00177152, -0.967447, -0.253066, -0.440302, 3.21163, 1.94631) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -568,7 +485,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="DeviceLight10" type="SpotLight3D" parent="Lights" unique_id=1406303147] +[node name="DeviceLight10" type="SpotLight3D" parent="Lights" unique_id=1719353422] transform = Transform3D(0.999945, 0.000907607, -0.0104695, 0.0103584, -0.253071, 0.967392, -0.00177152, -0.967447, -0.253066, -0.546264, 3.21163, 1.94631) light_color = Color(0.808594, 0.518647, 0.06633, 1) light_energy = 0.007 @@ -581,7 +498,7 @@ script = ExtResource("73_lxu5b") state_property = "devices_light_enabled" light_energy_on = 0.002 -[node name="CzuwakOmni1" type="SpotLight3D" parent="Lights" unique_id=866179476] +[node name="CzuwakOmni1" type="SpotLight3D" parent="Lights" unique_id=1001943059] transform = Transform3D(0.900154, 0.155718, -0.406786, -0.0599979, -0.880682, -0.469892, -0.43142, 0.447382, -0.783407, 0.689291, 3.31268, 1.88082) light_color = Color(0.960938, 0.506832, 0.349091, 1) light_energy = 0.2 @@ -596,7 +513,7 @@ spot_angle = 69.32 script = ExtResource("73_lxu5b") light_energy_on = 0.2 -[node name="CzuwakOmni2" type="SpotLight3D" parent="Lights" unique_id=435179726] +[node name="CzuwakOmni2" type="SpotLight3D" parent="Lights" unique_id=1425611392] transform = Transform3D(0.868601, -0.116307, 0.481669, 0.134029, -0.880682, -0.454352, 0.477041, 0.459208, -0.749373, -0.68228, 3.31988, 1.87836) light_color = Color(0.960938, 0.506832, 0.349091, 1) light_energy = 0.2 @@ -611,7 +528,7 @@ spot_angle = 69.32 script = ExtResource("73_lxu5b") light_energy_on = 0.2 -[node name="CzuwakOmni3" type="SpotLight3D" parent="Lights" unique_id=1818599812] +[node name="CzuwakOmni3" type="SpotLight3D" parent="Lights" unique_id=478649024] transform = Transform3D(0.739002, 0.0915089, -0.667459, 8.04663e-07, 0.990732, 0.135831, 0.673703, -0.10038, 0.732153, 0.0450159, 3.88586, 3.70396) light_color = Color(0.960938, 0.506832, 0.349091, 1) light_energy = 0.2 @@ -626,7 +543,7 @@ spot_angle = 69.32 script = ExtResource("73_lxu5b") light_energy_on = 0.2 -[node name="Czuwak" parent="Lights" unique_id=469053793 instance=ExtResource("76_djwot")] +[node name="Czuwak" parent="Lights" unique_id=2034864414 instance=ExtResource("76_djwot")] transform = Transform3D(0.997974, -0.0455378, 0.0444316, 0, 0.698359, 0.715747, -0.0636228, -0.714297, 0.696945, -0.0587103, 3.1026, 2.18974) state_property = "blinking" target_path = NodePath("../../Cabin/banan/podloga_glowna/czuwak_on") diff --git a/demo/vehicles/sm42/sm_42v_1.tscn b/demo/vehicles/sm42/sm_42v_1.tscn index c96b9463..eb9f546b 100644 --- a/demo/vehicles/sm42/sm_42v_1.tscn +++ b/demo/vehicles/sm42/sm_42v_1.tscn @@ -2,6 +2,7 @@ [ext_resource type="Script" uid="uid://bbn3nd8ts8yjx" path="res://vehicles/sm42/sm_42v_1.gd" id="1_vib8j"] [ext_resource type="Script" uid="uid://b6080q3nwa3xs" path="res://vehicles/sm42/cabin_interior_lights.gd" id="2_qmqy7"] +[ext_resource type="Script" uid="uid://bd7e6ncgjphe7" path="res://vehicles/sm42/horns.gd" id="3_3nj8v"] [sub_resource type="WWListItem" id="WWListItem_tqpff"] rpm = 496.0 @@ -118,7 +119,7 @@ cabin_a/head_light = true cabin_a/left/white_signal = true cabin_a/right/white_signal = true -[node name="SM42v1" type="TrainController" unique_id=2029372200] +[node name="SM42v1" type="TrainController" unique_id=1310060421] train_id = "sm42v1" type_name = "6d" mass = 74000.0 @@ -128,7 +129,7 @@ axle_arrangement = "Bo'Bo'" battery_voltage = 110.0 script = ExtResource("1_vib8j") -[node name="Brake" type="TrainBrake" parent="." unique_id=588434163] +[node name="Brake" type="TrainBrake" parent="." unique_id=1613880884] valve/type = 20 friction_elements_per_axle = 4 brake_force/max = 85.0 @@ -146,13 +147,13 @@ compressor/speed = 0.057 compressor/power = 3 rig_effectiveness = 0.85 -[node name="StonkaDieselEngine" type="TrainDieselElectricEngine" parent="." unique_id=1998930943] +[node name="StonkaDieselEngine" type="TrainDieselElectricEngine" parent="." unique_id=106755602] oil_pump/pressure_minimum = 0.15 maximum_traction_force = 228000.0 wwlist = Array[WWListItem]([SubResource("WWListItem_tqpff"), SubResource("WWListItem_083b8"), SubResource("WWListItem_0r5q0"), SubResource("WWListItem_vjhrb"), SubResource("WWListItem_6sfix"), SubResource("WWListItem_jxcvy"), SubResource("WWListItem_q4qj4"), SubResource("WWListItem_p5gw0"), SubResource("WWListItem_laio0"), SubResource("WWListItem_qppya"), SubResource("WWListItem_65frr"), SubResource("WWListItem_e4rmd")]) motor_param_table = Array[MotorParameter]([SubResource("MotorParameter_210ku"), SubResource("MotorParameter_ewrnx")]) -[node name="TrainSecuritySystem" type="TrainSecuritySystem" parent="." unique_id=888453964] +[node name="TrainSecuritySystem" type="TrainSecuritySystem" parent="." unique_id=1639274733] aware_system/active = true aware_delay = 60.0 emergency_brake/delay = 2.5 @@ -160,7 +161,7 @@ sound_signal_delay = 2.5 shp_magnet_distance = 3.0 ca_max_hold_time = 1.0 -[node name="TrainDoors" type="TrainDoors" parent="." unique_id=1972367549] +[node name="TrainDoors" type="TrainDoors" parent="." unique_id=1415281539] open/time = 3.0 max_shift = 3.0 open/method = 2 @@ -172,21 +173,24 @@ has_lock = true max_shift_plug = 3.0 platform/max_shift = 3.0 -[node name="CabinInteriorLights" type="GenericTrainPart" parent="." unique_id=1668337540] +[node name="CabinInteriorLights" type="GenericTrainPart" parent="." unique_id=1624545150] script = ExtResource("2_qmqy7") -[node name="TrainLightning" type="TrainLighting" parent="." unique_id=1232836093] +[node name="TrainLightning" type="TrainLighting" parent="." unique_id=1894046249] lights/wrap_selector = true lights/list = Array[LightListItem]([SubResource("LightListItem_ss4tc")]) light/source = 4 source/generator/engine = 0 -[node name="TrainElectroPneumaticDynamicBrake" type="TrainElectroPneumaticDynamicBrake" parent="." unique_id=444721737] +[node name="TrainElectroPneumaticDynamicBrake" type="TrainElectroPneumaticDynamicBrake" parent="." unique_id=921716875] -[node name="TrainSpringBrake" type="TrainSpringBrake" parent="." unique_id=1394463130] +[node name="TrainSpringBrake" type="TrainSpringBrake" parent="." unique_id=1112748059] -[node name="TrainLoad" type="TrainLoad" parent="." unique_id=829257758] +[node name="TrainLoad" type="TrainLoad" parent="." unique_id=1149444873] -[node name="TrainBuffer" type="TrainBuffCoupl" parent="."] +[node name="TrainBuffer" type="TrainBuffCoupl" parent="." unique_id=1888287505] + +[node name="Horns" type="GenericTrainPart" parent="." unique_id=52939850] +script = ExtResource("3_3nj8v") [connection signal="selector_position_changed" from="TrainLightning" to="." method="_on_train_lightning_selector_position_changed"] diff --git a/demo/vehicles/sm42/sounds/alerter_light.tres b/demo/vehicles/sm42/sounds/alerter_light.tres new file mode 100644 index 00000000..f2e9b417 --- /dev/null +++ b/demo/vehicles/sm42/sounds/alerter_light.tres @@ -0,0 +1,33 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://bqiprpnqmu7iw"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_p41s5"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_mo85r"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_jg1lv"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="4_vpw8n"] + +[sub_resource type="AudioStream" id="AudioStream_55hqn"] +script = ExtResource("4_vpw8n") +file_path = "20786_203e_light_shp_start" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_btci3"] +script = ExtResource("3_jg1lv") +stream = SubResource("AudioStream_55hqn") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_42g83"] +script = ExtResource("4_vpw8n") +file_path = "20786_203e_light_shp_stop" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_n3jdp"] +script = ExtResource("3_jg1lv") +stream = SubResource("AudioStream_42g83") +trigger_mode = 1 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_mo85r") +tracks = Array[ExtResource("3_jg1lv")]([SubResource("Resource_btci3"), SubResource("Resource_n3jdp")]) +name = &"alerter_light" +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/sm42/sounds/buzzer.tres b/demo/vehicles/sm42/sounds/buzzer.tres new file mode 100644 index 00000000..8679d15c --- /dev/null +++ b/demo/vehicles/sm42/sounds/buzzer.tres @@ -0,0 +1,25 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://dr6q0ep567inn"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_kxg8o"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_ukiyr"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_6v0sr"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="4_dbhld"] + +[sub_resource type="AudioStream" id="AudioStream_n3jdp"] +script = ExtResource("4_dbhld") +file_path = "sm42czuwak" +loop = true +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_rjfok"] +script = ExtResource("3_6v0sr") +stream = SubResource("AudioStream_n3jdp") +adsr_enabled = true +release = 0.2 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_ukiyr") +tracks = Array[ExtResource("3_6v0sr")]([SubResource("Resource_rjfok")]) +name = &"buzzer" +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/sm42/sounds/cabin_button.tres b/demo/vehicles/sm42/sounds/cabin_button.tres new file mode 100644 index 00000000..0fe49b4b --- /dev/null +++ b/demo/vehicles/sm42/sounds/cabin_button.tres @@ -0,0 +1,33 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://cqep24078h1i1"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_yvdjv"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_svqy3"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_40qsa"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="4_qkqtl"] + +[sub_resource type="AudioStream" id="AudioStream_rjfok"] +script = ExtResource("4_qkqtl") +file_path = "20786_203E_button_ws_1_on" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_btci3"] +script = ExtResource("3_40qsa") +stream = SubResource("AudioStream_rjfok") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_hn4yp"] +script = ExtResource("4_qkqtl") +file_path = "20786_203E_button_ws_1_of" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_au8bn"] +script = ExtResource("3_40qsa") +stream = SubResource("AudioStream_hn4yp") +trigger_mode = 1 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_svqy3") +tracks = Array[ExtResource("3_40qsa")]([SubResource("Resource_btci3"), SubResource("Resource_au8bn")]) +name = &"cabin_button" +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/sm42/sounds/cabin_switch.tres b/demo/vehicles/sm42/sounds/cabin_switch.tres new file mode 100644 index 00000000..b8fc2fa8 --- /dev/null +++ b/demo/vehicles/sm42/sounds/cabin_switch.tres @@ -0,0 +1,37 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://dg8rxskk1n38y"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_yvdjv"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_svqy3"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_40qsa"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="4_qkqtl"] + +[sub_resource type="AudioStream" id="AudioStream_rjfok"] +script = ExtResource("4_qkqtl") +file_path = "20786_203E_switch_lights_cabin_2_on" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_btci3"] +script = ExtResource("3_40qsa") +stream = SubResource("AudioStream_rjfok") +adsr_enabled = true +release = 0.1 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_hn4yp"] +script = ExtResource("4_qkqtl") +file_path = "20786_203E_switch_lights_cabin_2_of" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_au8bn"] +script = ExtResource("3_40qsa") +stream = SubResource("AudioStream_hn4yp") +adsr_enabled = true +attack = 0.5 +trigger_mode = 1 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_svqy3") +tracks = Array[ExtResource("3_40qsa")]([SubResource("Resource_btci3"), SubResource("Resource_au8bn")]) +name = &"cabin_switch" +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/sm42/sounds/engine.tres b/demo/vehicles/sm42/sounds/engine.tres new file mode 100644 index 00000000..2d1e524f --- /dev/null +++ b/demo/vehicles/sm42/sounds/engine.tres @@ -0,0 +1,131 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://xehc2ntq4f3w"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_e5ld8"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_65bpq"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_6jtqw"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="3_70rhc"] +[ext_resource type="Curve" uid="uid://dphavxwg7ojon" path="res://vehicles/sm42/sounds/fade_in_curve.tres" id="3_wdfhh"] +[ext_resource type="Curve" uid="uid://snoluwsvvhmu" path="res://vehicles/sm42/sounds/fade_out_curve.tres" id="4_lui7t"] + +[sub_resource type="Curve" id="Curve_lui7t"] +_limits = [0.0, 2.0, 400.0, 1000.0] +_data = [Vector2(400, 0.84480596), 0.0, 0.0, 0, 0, Vector2(1000, 1.0563203), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="AudioStream" id="AudioStream_idle_1"] +script = ExtResource("3_70rhc") +file_path = "697_sm42-idle-1" +loop = true +loop_offset = 0.00124716553287981 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_cgl6l"] +script = ExtResource("3_6jtqw") +stream = SubResource("AudioStream_idle_1") +offset = 405.0 +length = 285.0 +fade_in_curve = ExtResource("3_wdfhh") +fade_out_curve = ExtResource("4_lui7t") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_idle_2"] +script = ExtResource("3_70rhc") +file_path = "697_sm42-idle-2" +loop = true +loop_offset = 9.261201814058955 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_3yqc7"] +script = ExtResource("3_6jtqw") +stream = SubResource("AudioStream_idle_2") +offset = 510.0 +length = 280.0 +fade_in_curve = ExtResource("3_wdfhh") +fade_out_curve = ExtResource("4_lui7t") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_idle_3"] +script = ExtResource("3_70rhc") +file_path = "697_sm42-idle-3" +loop = true +loop_offset = 6.663605442176871 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_k2yml"] +script = ExtResource("3_6jtqw") +stream = SubResource("AudioStream_idle_3") +offset = 610.0 +length = 330.0 +fade_in_curve = ExtResource("3_wdfhh") +fade_out_curve = ExtResource("4_lui7t") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_idle_4"] +script = ExtResource("3_70rhc") +file_path = "697_sm42-idle-4" +loop = true +loop_offset = 5.977414965986394 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_exrhq"] +script = ExtResource("3_6jtqw") +stream = SubResource("AudioStream_idle_4") +offset = 760.0 +length = 330.0 +fade_in_curve = ExtResource("3_wdfhh") +fade_out_curve = ExtResource("4_lui7t") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_idle_5"] +script = ExtResource("3_70rhc") +file_path = "697_sm42-idle-5" +loop = true +loop_offset = 2.192766439909297 +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_fgxdh"] +script = ExtResource("3_6jtqw") +stream = SubResource("AudioStream_idle_5") +offset = 910.0 +length = 180.0 +fade_in_curve = ExtResource("3_wdfhh") +fade_out_curve = ExtResource("4_lui7t") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="Resource" id="Resource_4aa5h"] +script = ExtResource("1_e5ld8") +parameter_name = &"rpm" +tracks = Array[ExtResource("3_6jtqw")]([SubResource("Resource_cgl6l"), SubResource("Resource_3yqc7"), SubResource("Resource_k2yml"), SubResource("Resource_exrhq"), SubResource("Resource_fgxdh")]) +max_domain = 2000.0 +pitch_curve = SubResource("Curve_lui7t") +metadata/_custom_type_script = "uid://bf434i6ru4ch" + +[sub_resource type="Curve" id="Curve_3yqc7"] +_limits = [0.0, 1.0, 0.0, 6.0] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(6, 0), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="Curve" id="Curve_wdfhh"] +_limits = [0.0, 2.0, 400.0, 1000.0] +_data = [Vector2(426.24997, 0.9261576), 0.0, 0.0006427791, 0, 0, Vector2(1000, 1.0563203), 0.00035060642, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="AudioStream" id="AudioStream_start"] +script = ExtResource("3_70rhc") +file_path = "697_sm42-start" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_thgt3"] +script = ExtResource("3_6jtqw") +stream = SubResource("AudioStream_start") +stream_offset = 14.0 +fade_out_curve = SubResource("Curve_3yqc7") +pitch_curve = SubResource("Curve_wdfhh") +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_65bpq") +tracks = Array[ExtResource("3_6jtqw")]([SubResource("Resource_thgt3")]) +name = &"engine" +automations = Array[ExtResource("1_e5ld8")]([SubResource("Resource_4aa5h")]) +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/sm42/sounds/fade_in_curve.tres b/demo/vehicles/sm42/sounds/fade_in_curve.tres new file mode 100644 index 00000000..927d7ee5 --- /dev/null +++ b/demo/vehicles/sm42/sounds/fade_in_curve.tres @@ -0,0 +1,6 @@ +[gd_resource type="Curve" format=3 uid="uid://dphavxwg7ojon"] + +[resource] +_limits = [0.0, 1.0, 0.0, 90.0] +_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(90, 1), 0.0, 0.0, 0, 0] +point_count = 2 diff --git a/demo/vehicles/sm42/sounds/fade_out_curve.tres b/demo/vehicles/sm42/sounds/fade_out_curve.tres new file mode 100644 index 00000000..8f8d7b8d --- /dev/null +++ b/demo/vehicles/sm42/sounds/fade_out_curve.tres @@ -0,0 +1,6 @@ +[gd_resource type="Curve" format=3 uid="uid://snoluwsvvhmu"] + +[resource] +_limits = [0.0, 1.0, 0.0, 90.0] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(90, 0), 0.0, 0.0, 0, 0] +point_count = 2 diff --git a/demo/vehicles/sm42/sounds/horn1.tres b/demo/vehicles/sm42/sounds/horn1.tres new file mode 100644 index 00000000..cbef4a5d --- /dev/null +++ b/demo/vehicles/sm42/sounds/horn1.tres @@ -0,0 +1,54 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://caakm2jf2vlo6"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_wirw6"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_6lyxy"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_1n36j"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="4_x84c7"] + +[sub_resource type="AudioStream" id="AudioStream_horn3_start"] +script = ExtResource("4_x84c7") +file_path = "697_sm42-horn3-start" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_ey1vs"] +script = ExtResource("3_1n36j") +stream = SubResource("AudioStream_horn3_start") +length = 0.36 +adsr_enabled = true +release = 0.18 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_horn3_trwa"] +script = ExtResource("4_x84c7") +file_path = "697_sm42-horn3-trwa" +loop = true +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_fqfhb"] +script = ExtResource("3_1n36j") +stream = SubResource("AudioStream_horn3_trwa") +offset = 0.2 +adsr_enabled = true +attack = 0.1 +release = 0.18 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_horn3_stop"] +script = ExtResource("4_x84c7") +file_path = "697_sm42-horn3-stop" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_olrsv"] +script = ExtResource("3_1n36j") +stream = SubResource("AudioStream_horn3_stop") +adsr_enabled = true +attack = 0.1 +trigger_mode = 1 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_6lyxy") +tracks = Array[ExtResource("3_1n36j")]([SubResource("Resource_ey1vs"), SubResource("Resource_fqfhb"), SubResource("Resource_olrsv")]) +release = 0.25 +name = &"horn1" +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/sm42/sounds/horn2.tres b/demo/vehicles/sm42/sounds/horn2.tres new file mode 100644 index 00000000..f0913aff --- /dev/null +++ b/demo/vehicles/sm42/sounds/horn2.tres @@ -0,0 +1,55 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://b5n7saj0mfejt"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_76hph"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_mfj00"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_uhadh"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="4_a61is"] + +[sub_resource type="AudioStream" id="AudioStream_horn1_start"] +script = ExtResource("4_a61is") +file_path = "697_sm42-horn1-start" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_klw4o"] +script = ExtResource("3_uhadh") +stream = SubResource("AudioStream_horn1_start") +length = 0.36 +attack = 0.2 +decay = 0.05 +release = 0.05 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_horn1_trwa"] +script = ExtResource("4_a61is") +file_path = "697_sm42-horn1-trwa" +loop = true +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_3vuhd"] +script = ExtResource("3_uhadh") +stream = SubResource("AudioStream_horn1_trwa") +offset = 0.2 +adsr_enabled = true +attack = 0.1 +release = 0.1 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[sub_resource type="AudioStream" id="AudioStream_horn1_stop"] +script = ExtResource("4_a61is") +file_path = "697_sm42-horn1-stop" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_qy2v8"] +script = ExtResource("3_uhadh") +stream = SubResource("AudioStream_horn1_stop") +adsr_enabled = true +attack = 0.01 +trigger_mode = 1 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_mfj00") +tracks = Array[ExtResource("3_uhadh")]([SubResource("Resource_klw4o"), SubResource("Resource_3vuhd"), SubResource("Resource_qy2v8")]) +release = 0.25 +name = &"horn2" +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/sm42/sounds/oil_pump.tres b/demo/vehicles/sm42/sounds/oil_pump.tres new file mode 100644 index 00000000..f4898ca7 --- /dev/null +++ b/demo/vehicles/sm42/sounds/oil_pump.tres @@ -0,0 +1,26 @@ +[gd_resource type="Resource" script_class="SfxEvent" format=3 uid="uid://bc8ysdcqr22rb"] + +[ext_resource type="Script" uid="uid://bf434i6ru4ch" path="res://addons/gnd_sfx/sfx_automation.gd" id="1_2mers"] +[ext_resource type="Script" uid="uid://bgxtc44e5rb0l" path="res://addons/gnd_sfx/sfx_event.gd" id="2_1wvjm"] +[ext_resource type="Script" uid="uid://boicinkvs7i0h" path="res://addons/gnd_sfx/sfx_track.gd" id="3_40hr8"] +[ext_resource type="Script" uid="uid://cmmb3s07th3xp" path="res://addons/libmaszyna/sound/maszyna_audio_stream.gd" id="4_pcnbq"] + +[sub_resource type="AudioStream" id="AudioStream_start"] +script = ExtResource("4_pcnbq") +file_path = "697_sm42-start" +metadata/_custom_type_script = "uid://cmmb3s07th3xp" + +[sub_resource type="Resource" id="Resource_k06g3"] +script = ExtResource("3_40hr8") +stream = SubResource("AudioStream_start") +length = 13.0 +stream_offset = 1.0 +metadata/_custom_type_script = "uid://boicinkvs7i0h" + +[resource] +script = ExtResource("2_1wvjm") +tracks = Array[ExtResource("3_40hr8")]([SubResource("Resource_k06g3")]) +adsr_enabled = true +release = 0.2 +name = &"oil_pump" +metadata/_custom_type_script = "uid://bgxtc44e5rb0l" diff --git a/demo/vehicles/tem2/tem2.tscn b/demo/vehicles/tem2/tem2.tscn index 702af66d..97473648 100644 --- a/demo/vehicles/tem2/tem2.tscn +++ b/demo/vehicles/tem2/tem2.tscn @@ -2,7 +2,6 @@ [ext_resource type="Script" uid="uid://bmsduesjycv2v" path="res://addons/libmaszyna/rail_vehicle_3d.gd" id="1_ra284"] [ext_resource type="PackedScene" uid="uid://dwcx6807obsgm" path="res://vehicles/tem2/tem2_cabin.tscn" id="2_nt307"] -[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="2_q4wpc"] [ext_resource type="Script" uid="uid://bbn3nd8ts8yjx" path="res://vehicles/sm42/sm_42v_1.gd" id="4_tujep"] [sub_resource type="BoxShape3D" id="BoxShape3D_kol4m"] @@ -129,65 +128,35 @@ saturation_current_multiplier = 183.3 voltage_constant = 2000.0 saturation_current = 49.0 -[node name="TEM2" type="Node3D" unique_id=347305240] +[node name="TEM2" type="Node3D" unique_id=1789571545] script = ExtResource("1_ra284") controller_path = NodePath("TEM2Controller") cabin_scene = ExtResource("2_nt307") cabin_rotate_180deg = true low_poly_cabin_path = NodePath("TEM2_LowPolyCab") -[node name="TEM2" type="VisualInstance3D" parent="." unique_id=1879275085] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("2_q4wpc") +[node name="TEM2" type="E3DModelInstance" parent="." unique_id=1876231323] data_path = "/dynamic/pkp/tem2_v2" model_filename = "tem2-122a" skins = ["tem2-122"] exclude_node_names = ["cien"] - -[node name="TEM2_LowPolyCab" type="VisualInstance3D" parent="." unique_id=124864942] -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0, 0, 0) -rotation_edit_mode = 0 -rotation_order = 2 -top_level = false -visible = true -visibility_parent = NodePath("") -layers = 1 -script = ExtResource("2_q4wpc") + +[node name="TEM2_LowPolyCab" type="E3DModelInstance" parent="." unique_id=1160136315] data_path = "/dynamic/pkp/tem2_v2" model_filename = "lowpoly/int_tem2" skins = ["tem2-122"] exclude_node_names = ["cien"] +transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0, 0, 0) -[node name="Area3D" type="Area3D" parent="." unique_id=440063590] +[node name="Area3D" type="Area3D" parent="." unique_id=776248700] monitoring = false -[node name="CollisionShape3D" type="CollisionShape3D" parent="Area3D" unique_id=2078926479] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Area3D" unique_id=19832897] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.53125, 1.97558, 0.243691) shape = SubResource("BoxShape3D_kol4m") -[node name="TEM2Controller" type="TrainController" parent="." unique_id=183030337] +[node name="TEM2Controller" type="TrainController" parent="." unique_id=580738422] train_id = "tem2" type_name = "tem2" mass = 116000.0 @@ -197,7 +166,7 @@ axle_arrangement = "Bo'Bo'" battery_voltage = 110.0 script = ExtResource("4_tujep") -[node name="Brake" type="TrainBrake" parent="TEM2Controller" unique_id=341250150] +[node name="Brake" type="TrainBrake" parent="TEM2Controller" unique_id=1980341126] valve/type = 20 friction_elements_per_axle = 4 brake_force/max = 85.0 @@ -215,14 +184,14 @@ compressor/speed = 0.057 compressor/power = 3 rig_effectiveness = 0.85 -[node name="DieselEngine" type="TrainDieselElectricEngine" parent="TEM2Controller" unique_id=605863155] +[node name="DieselEngine" type="TrainDieselElectricEngine" parent="TEM2Controller" unique_id=1770523098] oil_pump/pressure_minimum = 0.16 oil_pump/pressure_maximum = 0.3 maximum_traction_force = 228000.0 wwlist = Array[WWListItem]([SubResource("WWListItem_w7fy5"), SubResource("WWListItem_j7m6u"), SubResource("WWListItem_w1qdg"), SubResource("WWListItem_wk0d4"), SubResource("WWListItem_ndnpm"), SubResource("WWListItem_ye3f5"), SubResource("WWListItem_ljbq7"), SubResource("WWListItem_wnj0o"), SubResource("WWListItem_qwjcl"), SubResource("WWListItem_4li30"), SubResource("WWListItem_or38i"), SubResource("WWListItem_lwfur")]) motor_param_table = Array[MotorParameter]([SubResource("MotorParameter_l4x0r"), SubResource("MotorParameter_374xt")]) -[node name="TrainSecuritySystem" type="TrainSecuritySystem" parent="TEM2Controller" unique_id=627307967] +[node name="TrainSecuritySystem" type="TrainSecuritySystem" parent="TEM2Controller" unique_id=912812858] aware_system/active = true aware_delay = 60.0 emergency_brake/delay = 2.5 diff --git a/demo/vehicles/tem2/tem2_cabin.tscn b/demo/vehicles/tem2/tem2_cabin.tscn index c862e680..1348ed3f 100644 --- a/demo/vehicles/tem2/tem2_cabin.tscn +++ b/demo/vehicles/tem2/tem2_cabin.tscn @@ -1,13 +1,13 @@ -[gd_scene format=3 uid="uid://dwcx6807obsgm"] +[gd_scene load_steps=3 format=3 uid="uid://dwcx6807obsgm"] [ext_resource type="Script" uid="uid://bhoad6heiil6t" path="res://vehicles/tem2/tem2_cabin.gd" id="1_w5uim"] -[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="2_6c6uo"] -[node name="Tem2Cabin" type="Node3D" unique_id=1618443441] +[node name="Tem2Cabin" type="Node3D"] script = ExtResource("1_w5uim") driver_position = Vector3(-1.04938, 3.26692, -5.38975) -[node name="E3DModelInstance" type="VisualInstance3D" parent="." unique_id=1541046040] +[node name="E3DModelInstance" type="E3DModelInstance" parent="."] +_import_path = NodePath("") unique_name_in_owner = false process_mode = 0 process_priority = 0 @@ -23,6 +23,5 @@ top_level = false visible = true visibility_parent = NodePath("") layers = 1 -script = ExtResource("2_6c6uo") data_path = "/dynamic/pkp/tem2_v2" model_filename = "kabina-tem2a" diff --git a/doc_classes/E3DModel.xml b/doc_classes/E3DModel.xml new file mode 100644 index 00000000..bb8b1730 --- /dev/null +++ b/doc_classes/E3DModel.xml @@ -0,0 +1,19 @@ + + + + Resource representing a parsed MaSzyna E3D model. + + + [E3DModel] is a resource that stores the complete hierarchical structure of a MaSzyna [code].e3d[/code] model. It acts as a container for the root [E3DSubModel] components. This resource is designed to be serialized into Godot's native [code].res[/code] format by the [ResourceCache] to optimize subsequent loading times by avoiding repeated binary parsing. + + + + + + The internal name of the model, usually extracted from the E3D file header. + + + A list of top-level [E3DSubModel] resources that form the root of the model's hierarchy. + + + diff --git a/doc_classes/E3DModelInstance.xml b/doc_classes/E3DModelInstance.xml new file mode 100644 index 00000000..bec08d47 --- /dev/null +++ b/doc_classes/E3DModelInstance.xml @@ -0,0 +1,55 @@ + + + + A 3D node that represents an instance of a MaSzyna E3D model in the scene. + + + [E3DModelInstance] is the primary node used to display MaSzyna models within Godot. It takes a model filename and a data path, loads the corresponding [E3DModel] resource via the [E3DModelManager], and instantiates its geometry into the scene tree using the selected [enum Instancer] strategy. It handles complex model features like hierarchical submodels, seasonal textures, and dynamic material updates. + + + + + + The relative directory path within the MaSzyna game directory where the model and its textures are located. + + + The default size used for the Axis-Aligned Bounding Box (AABB) if the model geometry is not yet loaded or is empty. + + + If [code]true[/code], the nodes created during instantiation will be set as editable in the Godot editor, allowing for manual adjustment of submodel properties. + + + A list of submodel names that should be excluded from the instantiation process. + + + The strategy used to convert the [E3DModel] resource into scene tree nodes. + + + The filename of the [code].e3d[/code] model to load (without the extension). + + + A list of alternative texture sets or skins that can be applied to the model. + + + The calculated bounding box encompassing all submodels within this instance. + + + + + + Emitted when the model has been successfully loaded and its node hierarchy has been instantiated. + + + + + + Standard instancer that creates a hierarchy of [MeshInstance3D] and [Node3D] objects. + + + Similar to [code]INSTANCER_NODES[/code], but specifically configures nodes to be editable within the Godot editor. + + + Experimental instancer designed for higher performance with reduced node overhead. + + + diff --git a/doc_classes/E3DModelInstanceManager.xml b/doc_classes/E3DModelInstanceManager.xml new file mode 100644 index 00000000..7e67bca3 --- /dev/null +++ b/doc_classes/E3DModelInstanceManager.xml @@ -0,0 +1,48 @@ + + + + Global manager for tracking and updating active E3DModelInstance nodes. + + + [E3DModelInstanceManager] acts as a registry for all [E3DModelInstance] nodes currently active in the scene. It provides centralized control for batch operations, such as reloading all active models when global settings (like texture quality or game directory) change. This manager ensures that model instances remain synchronized with the underlying [E3DModel] resources and the [MaterialManager]. + + + + + + + + + Registers an [E3DModelInstance] with the manager. This is typically called automatically when a model instance enters the scene tree. + + + + + + Triggers a reload for all currently registered [E3DModelInstance] nodes. Useful for updating the visual representation of all models simultaneously. + + + + + + + Forces a specific [E3DModelInstance] to re-instantiate its geometry and re-apply its materials from the [E3DModel] resource. + + + + + + + Removes an [E3DModelInstance] from the manager's registry. This is typically called automatically when a model instance leaves the scene tree. + + + + + + + + Emitted when a model instance has been successfully reloaded and its node hierarchy has been updated. + + + + diff --git a/doc_classes/E3DModelManager.xml b/doc_classes/E3DModelManager.xml new file mode 100644 index 00000000..a711611c --- /dev/null +++ b/doc_classes/E3DModelManager.xml @@ -0,0 +1,21 @@ + + + + High-level manager for loading and caching E3D model resources. + + + [E3DModelManager] handles the loading of MaSzyna [code].e3d[/code] models from the filesystem. It resolves paths based on the configured MaSzyna game directory and utilizes [ResourceCache] to provide efficient memory and persistent disk caching. This ensures that models are only parsed once and can be quickly reloaded from Godot's native resource format on subsequent requests. + + + + + + + + + + Loads the specified model from the given [param data_path] and [param file_name]. The method first checks [ResourceCache] for a cached version (both in memory and on disk). If no cached version exists, it loads the original [code].e3d[/code] file, parses it, and automatically saves the result to the persistent cache for future use. + + + + diff --git a/doc_classes/E3DNodesInstancer.xml b/doc_classes/E3DNodesInstancer.xml new file mode 100644 index 00000000..4e84d4c4 --- /dev/null +++ b/doc_classes/E3DNodesInstancer.xml @@ -0,0 +1,22 @@ + + + + Utility class for converting E3DModel resources into a hierarchy of Godot nodes. + + + [E3DNodesInstancer] provides functionality to take an [E3DModel] resource and instantiate its structure as real [Node3D] objects in the scene tree. It recursively traverses the [E3DSubModel] hierarchy, creating [MeshInstance3D] nodes for parts with geometry and [Node3D] nodes for transformation-only parts. It also applies materials using the [MaterialManager] and configures node properties like visibility and transformations to match the MaSzyna model definition. + + + + + + + + + + + Instantiates the provided [param model] as a child hierarchy under [param target_node]. If [param editable] is [code]true[/code], the created nodes are owned by the current scene, allowing them to be visible and editable in the Godot editor. + + + + diff --git a/doc_classes/E3DParser.xml b/doc_classes/E3DParser.xml new file mode 100644 index 00000000..220e3415 --- /dev/null +++ b/doc_classes/E3DParser.xml @@ -0,0 +1,20 @@ + + + + Low-level parser for the MaSzyna E3D binary model format. + + + [E3DParser] is responsible for reading and decoding the binary data structure of MaSzyna [code].e3d[/code] files. It traverses the file's hierarchical node structure, extracting geometry, transformations, and material metadata to build a complete [E3DModel] resource tree. This class is primarily used internally by [E3DResourceFormatLoader]. + + + + + + + + + Reads from the provided [FileAccess] handle and returns a fully constructed [E3DModel] representing the data in the file. + + + + diff --git a/doc_classes/E3DResourceFormatLoader.xml b/doc_classes/E3DResourceFormatLoader.xml new file mode 100644 index 00000000..080d2275 --- /dev/null +++ b/doc_classes/E3DResourceFormatLoader.xml @@ -0,0 +1,11 @@ + + + + Resource loader for MaSzyna E3D model files. + + + [E3DResourceFormatLoader] is a specialized [ResourceFormatLoader] that allows Godot's [ResourceLoader] to recognize and load [code].e3d[/code] files. When a [code].e3d[/code] file is requested, this loader uses the internal [E3DParser] to read the binary data and construct a corresponding [E3DModel] resource hierarchy. This enables native integration of MaSzyna models into the Godot editor and runtime. + + + + diff --git a/doc_classes/E3DSubModel.xml b/doc_classes/E3DSubModel.xml new file mode 100644 index 00000000..48c610b4 --- /dev/null +++ b/doc_classes/E3DSubModel.xml @@ -0,0 +1,180 @@ + + + + Represents a single subcomponent of an E3D model, containing geometry and metadata. + + + [E3DSubModel] is a resource that stores the data for a specific part of an [E3DModel]. It contains the geometry (as an [ArrayMesh]), transformation matrices, material properties, and animation logic specific to the MaSzyna E3D format. Submodels can be nested, allowing for complex hierarchical structures where child submodels inherit the transformations of their parents. + + + + + + The type of animation logic applied to this submodel (e.g., rotating wheels, moving clock hands, or wind effects). + + + The base color of the submodel's material. + + + If [code]true[/code], the submodel uses a dynamic material that can change based on the environment or seasonal settings. + + + The index mapping for the dynamic material state. + + + The ambient light level threshold at which the submodel's self-illumination becomes active. + + + If [code]true[/code], the diffuse color is applied to the material. + + + The name of the [code].mat[/code] file associated with this submodel. + + + The transformation matrix applied to the UV coordinates of the material. + + + If [code]true[/code], the material is treated as having transparency (either alpha blended or alpha scissored). + + + The [ArrayMesh] containing the vertex data (positions, normals, UVs, colors) for this submodel. + + + The name of the submodel as defined in the E3D file. + + + The emissive color of the submodel, used for lights and glowing parts. + + + If [code]true[/code], the submodel is excluded from the rendering pass but remains part of the model hierarchy and animation logic. + + + The geometric primitive type or special node type of the submodel. + + + A list of child [E3DSubModel] resources that are nested under this submodel. + + + The local transformation matrix of the submodel relative to its parent. + + + Factor for visibility based on ambient light conditions. + + + The distance from the camera at which the submodel starts becoming visible. + + + The distance from the camera at which the submodel becomes invisible. + + + The general visibility state of the submodel. + + + + + Render as points. + + + Render as individual lines. + + + Render as a continuous line strip. + + + Render as a closed line loop. + + + Render as individual triangles (Standard). + + + Render as a triangle strip. + + + Render as a triangle fan. + + + Render as individual quadrilaterals. + + + Render as a quad strip. + + + Render as a generic polygon. + + + Special node type representing a pure transformation without geometry. + + + Special node type for a light source. + + + Special node type for star rendering. + + + No animation applied. + + + Rotation around a specific vector. + + + Rotation around X, Y, or Z axes based on parameters. + + + Translation along an axis. + + + Stepped rotation for clock seconds. + + + Stepped rotation for clock minutes. + + + Stepped rotation for clock hours (12h). + + + Stepped rotation for clock hours (24h). + + + Continuous rotation for clock seconds. + + + Continuous rotation for clock minutes. + + + Continuous rotation for clock hours (12h). + + + Continuous rotation for clock hours (24h). + + + Always faces the camera. + + + Swaying animation based on wind simulation. + + + Special behavior for sky-related submodels. + + + Digital display segments animation. + + + Digital clock specific animation. + + + Placeholder for undefined animation logic. + + + Inverse Kinematics animation. + + + IK variant 1. + + + IK variant 2. + + + Unknown animation type. + + + diff --git a/doc_classes/GameLog.xml b/doc_classes/GameLog.xml index f8f2bf2e..5ffe1a2f 100644 --- a/doc_classes/GameLog.xml +++ b/doc_classes/GameLog.xml @@ -1,5 +1,5 @@ - + Application-wide logger for general purpose. Use it to write messages to the game's debug console. For development-only logs, prefer Godot's built-in functions such as push_error(), push_warning(), print(), and print_rich(). diff --git a/doc_classes/MaterialManager.xml b/doc_classes/MaterialManager.xml index efaf1400..3d60e9eb 100644 --- a/doc_classes/MaterialManager.xml +++ b/doc_classes/MaterialManager.xml @@ -4,11 +4,34 @@ A manager class for loading and caching materials and textures from MaSzyna models. - [MaterialManager] is responsible for loading MaSzyna materials and textures, converting them to Godot's [StandardMaterial3D] format, and providing caching for performance optimization. It handles texture loading, normal map processing, and transparency detection. The class is thread-safe with mutex-protected caching and supports seasonal texture variations through the [MaszynaMaterial] resource type. + [MaterialManager] is responsible for loading MaSzyna materials and textures, converting them to Godot's [StandardMaterial3D] format, and providing caching for performance optimization. It handles texture loading, normal map processing, and transparency detection. The class is thread-safe and utilizes [ResourceCache] for both memory and persistent disk caching. + + + + + Generates a grayscale heightmap [Image] from the provided albedo texture. It converts the image to the [code]L8[/code] format and applies contrast adjustments to make it suitable for height mapping. + + + + + + + + Generates a metallic map [Image] by thresholding the brightness of the provided albedo. Pixels with brightness values above the [param threshold] are treated as metallic (white), while others are treated as non-metallic (black). + + + + + + + + Generates a normal map [Image] from the albedo texture using a Sobel-like filter. It first generates an internal heightmap and then converts it to a normal map with the specified [param strength]. + + @@ -17,7 +40,7 @@ - Gets or creates a [StandardMaterial3D] for the specified model and material. Results are cached for performance. Automatically loads albedo and normal textures, detects alpha channel, and applies the transparency mode. If [param is_sky] is true, configures the material with unshaded rendering. + Gets or creates a [StandardMaterial3D] for the specified model and material. Results are cached in memory for performance. The method automatically loads albedo and normal textures, performs transparency detection (alpha channel check), and applies the requested [param transparent] mode. If [param is_sky] is true, the material is configured for unshaded rendering. @@ -25,13 +48,14 @@ + Searches for a material file ([code].mat[/code]) associated with the given model and material name across multiple possible directories (local model folder, global textures folder, etc.) and returns the first valid absolute path found. - Loads a texture from the given absolute or relative path. This is a convenience method that calls [method load_texture] with an empty model path. + Loads a texture from the given path. This is a convenience method that calls [method load_texture] with an empty model path. @@ -39,14 +63,7 @@ - - - - - - - - Loads a texture for a submodel, searching in submodel-specific directories. Used when a model has child models with their own textures. + Parses a MaSzyna material file ([code].mat[/code]) and returns its representation as a [MaszynaMaterial] resource. @@ -54,7 +71,7 @@ - Loads a texture from the model's material directory. If [param global] is true, also searches in the global textures directory. Returns a placeholder texture if the file cannot be found. + Searches for and loads a texture (preferring [code].dds[/code] format). This method utilizes [ResourceCache] to manage memory and persistent disk caching. If the texture is not found, a fallback placeholder is returned. diff --git a/doc_classes/MaterialParser.xml b/doc_classes/MaterialParser.xml index 09496aef..7d93d9c6 100644 --- a/doc_classes/MaterialParser.xml +++ b/doc_classes/MaterialParser.xml @@ -11,13 +11,12 @@ - + - - - + + - Parses a MaSzyna material file and returns a [MaszynaMaterial] resource. The [param material_manager] is used to resolve the full file path. If the material file cannot be found, returns a material with the [param material_path] as the albedo texture. + Parses a MaSzyna material file and returns a [MaszynaMaterial] resource. The [param material_manager] is used to resolve the full file path. If the material file cannot be found, returns a material with the [param material_name] as the albedo texture. diff --git a/doc_classes/ResourceCache.xml b/doc_classes/ResourceCache.xml new file mode 100644 index 00000000..0c4e2ef4 --- /dev/null +++ b/doc_classes/ResourceCache.xml @@ -0,0 +1,70 @@ + + + + Global singleton for memory and persistent disk caching of resources. + + + [ResourceCache] provides a centralized, thread-safe system for caching resources used by the MaSzyna API Wrapper. It maintains an in-memory dictionary for fast access and a persistent disk cache in [code]user://cache/[/code] for long-term storage. Resources are identified by their original source paths and organized into subdirectories based on their type. Disk cache files are stored as Godot-native [code].res[/code] files with MD5-hashed filenames to ensure cross-platform compatibility and avoid path length issues. + + + + + + + + + Clears both the memory cache and the persistent disk cache for the specified subdirectory. + + + + + + Clears the entire memory cache and all persistent disk cache subdirectories. + + + + + + + + Attempts to retrieve a resource from the cache. It first checks the memory cache, and if not found, attempts to load it from the persistent disk cache. Returns an invalid [Ref] if the resource is not cached. + + + + + + + + Returns [code]true[/code] if the resource exists in either the memory cache or the persistent disk cache. + + + + + + + + Removes the specified resource from both the memory cache and the persistent disk cache. + + + + + + + + + Stores the given resource in the memory cache and serializes it to the persistent disk cache in the specified subdirectory. + + + + + + Cache directory for 3D models (stored in [code]user://cache/models/[/code]). + + + Cache directory for textures and images (stored in [code]user://cache/textures/[/code]). + + + Cache directory for audio resources (stored in [code]user://cache/sounds/[/code]). + + + diff --git a/doc_classes/TrainBuffCoupl.xml b/doc_classes/TrainBuffCoupl.xml index 9e574f39..48c2d59e 100644 --- a/doc_classes/TrainBuffCoupl.xml +++ b/doc_classes/TrainBuffCoupl.xml @@ -122,4 +122,4 @@ Indicates presence of 3x400V power. - \ No newline at end of file + diff --git a/doc_classes/TrainSystem.xml b/doc_classes/TrainSystem.xml index 86a16a43..54f88e21 100644 --- a/doc_classes/TrainSystem.xml +++ b/doc_classes/TrainSystem.xml @@ -1,5 +1,5 @@ - + Handles trains registry and communication between trains and train systems diff --git a/src/core/GameLog.hpp b/src/core/GameLog.hpp index 5e79b0d8..434c4c7d 100644 --- a/src/core/GameLog.hpp +++ b/src/core/GameLog.hpp @@ -7,7 +7,7 @@ #include #include -#ifdef LIBMASZYNA_DEBUG_ENABLED +#ifdef LIBMASZYNA_DEBUG #if defined(_MSC_VER) // MSVC doesn't require special handling. #define DEBUG(msg, ...) \ @@ -28,8 +28,8 @@ namespace godot { - class GameLog : public RefCounted { - GDCLASS(GameLog, RefCounted); + class GameLog : public Object { + GDCLASS(GameLog, Object); public: static const char *LOG_UPDATED_SIGNAL; diff --git a/src/core/GenericTrainPart.cpp b/src/core/GenericTrainPart.cpp index c7361d12..16ef6596 100644 --- a/src/core/GenericTrainPart.cpp +++ b/src/core/GenericTrainPart.cpp @@ -7,6 +7,7 @@ namespace godot { void GenericTrainPart::_bind_methods() { ClassDB::bind_method(D_METHOD("get_train_controller_node"), &GenericTrainPart::get_train_controller_node); ClassDB::bind_method(D_METHOD("get_train_state"), &GenericTrainPart::get_train_state); + ClassDB::bind_method(D_METHOD("update_state"), &GenericTrainPart::update_state); BIND_VIRTUAL_METHOD(GenericTrainPart, _process_train_part, 2); BIND_VIRTUAL_METHOD(GenericTrainPart, _get_train_part_state, 1); } @@ -19,6 +20,10 @@ namespace godot { Dictionary GenericTrainPart::_get_train_part_state() { return internal_state; }; + void GenericTrainPart::update_state(const Dictionary &state) { + internal_state.merge(state, true); + train_controller_node->get_state().merge(internal_state, true); + } void GenericTrainPart::_process_mover(const double delta) { call("_process_train_part", delta); // FIXME: this should not be called each frame, but only when state changes diff --git a/src/core/GenericTrainPart.hpp b/src/core/GenericTrainPart.hpp index 23946e1f..2f624484 100644 --- a/src/core/GenericTrainPart.hpp +++ b/src/core/GenericTrainPart.hpp @@ -16,6 +16,7 @@ namespace godot { void _do_fetch_state_from_mover(TMoverParameters *mover, Dictionary &state) override; void _do_fetch_config_from_mover(TMoverParameters *mover, Dictionary &config) override; void _do_process_mover(TMoverParameters *mover, double delta) override; + void update_state(const Dictionary &state); public: TrainController *get_train_controller_node(); diff --git a/src/core/ResourceCache.cpp b/src/core/ResourceCache.cpp new file mode 100644 index 00000000..21bc8dc6 --- /dev/null +++ b/src/core/ResourceCache.cpp @@ -0,0 +1,198 @@ +#include "ResourceCache.hpp" +#include "models/e3d/E3DModel.hpp" + +#include +#include +#include +#include +#include +#include + +namespace godot { + + ResourceCache *ResourceCache::singleton = nullptr; + + void ResourceCache::_bind_methods() { + ClassDB::bind_static_method(get_class_static(), D_METHOD("has", "path", "dir"), &ResourceCache::has); + ClassDB::bind_static_method(get_class_static(), D_METHOD("get", "path", "dir"), &ResourceCache::get); + ClassDB::bind_static_method(get_class_static(), D_METHOD("set", "path", "resource", "dir"), &ResourceCache::set); + ClassDB::bind_static_method(get_class_static(), D_METHOD("remove", "path", "dir"), &ResourceCache::remove); + ClassDB::bind_static_method(get_class_static(), D_METHOD("clear", "dir"), &ResourceCache::clear); + ClassDB::bind_static_method(get_class_static(), D_METHOD("clear_all"), &ResourceCache::clear_all); + + BIND_ENUM_CONSTANT(RESOURCE_CACHE_DIR_MODELS); + BIND_ENUM_CONSTANT(RESOURCE_CACHE_DIR_TEXTURES); + BIND_ENUM_CONSTANT(RESOURCE_CACHE_DIR_SOUNDS); + } + + ResourceCache::ResourceCache() { + singleton = this; + _mutex.instantiate(); + } + + ResourceCache::~ResourceCache() { + if (singleton == this) { + singleton = nullptr; + } + Array keys = _cache.keys(); + for (int i = 0; i < keys.size(); i++) { + Ref res = _cache[keys.get(i)]; + if (res.is_valid()) { + if (E3DModel *model = cast_to(res.ptr())) { + model->clear(); + } + } + } + _cache.clear(); + _mutex.unref(); + } + + String ResourceCache::_get_dir_name(const ResourceCacheDir p_dir) { + switch (p_dir) { + case RESOURCE_CACHE_DIR_MODELS: + return "models"; + case RESOURCE_CACHE_DIR_TEXTURES: + return "textures"; + case RESOURCE_CACHE_DIR_SOUNDS: + return "sounds"; + default: + return "misc"; + } + } + + String ResourceCache::_get_cache_path(const String &p_path, const ResourceCacheDir p_dir) { + String cache_dir = "user://cache/" + _get_dir_name(p_dir); + return cache_dir + "/" + p_path.md5_text() + ".res"; + } + + bool ResourceCache::has(const String &p_path, const ResourceCacheDir p_dir) { + if (singleton == nullptr) { + return false; + } + + singleton->_mutex->lock(); + if (singleton->_cache.has(p_path)) { + singleton->_mutex->unlock(); + return true; + } + singleton->_mutex->unlock(); + + return FileAccess::file_exists(_get_cache_path(p_path, p_dir)); + } + + Ref ResourceCache::get(const String &p_path, const ResourceCacheDir p_dir) { + if (singleton == nullptr) { + return Ref(); + } + + singleton->_mutex->lock(); + if (singleton->_cache.has(p_path)) { + Ref res = singleton->_cache.get(p_path, Ref()); + singleton->_mutex->unlock(); + return res; + } + singleton->_mutex->unlock(); + + const String cache_path = _get_cache_path(p_path, p_dir); + if (FileAccess::file_exists(cache_path)) { + UtilityFunctions::print_verbose( + "[ResourceCache] Loading from disk: " + cache_path + " (original: " + p_path + ")"); + Ref res = ResourceLoader::get_singleton()->load(cache_path); + if (res.is_valid()) { + singleton->_mutex->lock(); + singleton->_cache.set(p_path, res); + singleton->_mutex->unlock(); + return res; + } + } + + return Ref(); + } + + void ResourceCache::set(const String &p_path, const Ref &p_resource, const ResourceCacheDir p_dir) { + if (singleton == nullptr || p_resource.is_null()) { + return; + } + + singleton->_mutex->lock(); + singleton->_cache.set(p_path, p_resource); + singleton->_mutex->unlock(); + + const String cache_path = _get_cache_path(p_path, p_dir); + const String cache_dir = cache_path.get_base_dir(); + + if (!DirAccess::dir_exists_absolute(cache_dir)) { + DirAccess::make_dir_recursive_absolute(cache_dir); + } + + const Error err = ResourceSaver::get_singleton()->save(p_resource, cache_path); + if (err != OK) { + UtilityFunctions::push_error("[ResourceCache] Failed to save cache: " + cache_path); + } else { + UtilityFunctions::print_verbose("[ResourceCache] Saved to disk: " + cache_path); + } + } + + void ResourceCache::remove(const String &p_path, const ResourceCacheDir p_dir) { + if (singleton == nullptr) { + return; + } + + singleton->_mutex->lock(); + singleton->_cache.erase(p_path); + singleton->_mutex->unlock(); + + const String cache_path = _get_cache_path(p_path, p_dir); + if (FileAccess::file_exists(cache_path)) { + DirAccess::remove_absolute(cache_path); + } + } + + void ResourceCache::clear(const ResourceCacheDir p_dir) { + if (singleton == nullptr) { + return; + } + + singleton->_mutex->lock(); + singleton->_cache.clear(); + singleton->_mutex->unlock(); + + const String _dir_name = _get_dir_name(p_dir); + const String cache_dir = "user://cache/" + _dir_name; + if (DirAccess::dir_exists_absolute(cache_dir)) { + const Ref dir = DirAccess::open(cache_dir); + if (dir.is_valid()) { + UtilityFunctions::print_verbose("[ResourceCache] Clearing cache for " + _dir_name + "..."); + dir->list_dir_begin(); + String file_name = dir->get_next(); + while (file_name != "") { + if (!dir->current_is_dir()) { + dir->remove(file_name); + } + file_name = dir->get_next(); + } + } + } + } + + void ResourceCache::clear_all() { + if (singleton == nullptr) { + return; + } + + singleton->_mutex->lock(); + singleton->_cache.clear(); + singleton->_mutex->unlock(); + + if (DirAccess::dir_exists_absolute("user://cache")) { + clear(RESOURCE_CACHE_DIR_MODELS); + clear(RESOURCE_CACHE_DIR_TEXTURES); + clear(RESOURCE_CACHE_DIR_SOUNDS); + } + } + + ResourceCache *ResourceCache::get_singleton() { + return singleton; + } + +} // namespace godot diff --git a/src/core/ResourceCache.hpp b/src/core/ResourceCache.hpp new file mode 100644 index 00000000..e640b003 --- /dev/null +++ b/src/core/ResourceCache.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace godot { + +class ResourceCache : public Object { + GDCLASS(ResourceCache, Object) + +public: + enum ResourceCacheDir { + RESOURCE_CACHE_DIR_MODELS, + RESOURCE_CACHE_DIR_TEXTURES, + RESOURCE_CACHE_DIR_SOUNDS + }; + +private: + static ResourceCache *singleton; + Dictionary _cache; + Ref _mutex; + + static String _get_dir_name(ResourceCacheDir p_dir); + static String _get_cache_path(const String &p_path, ResourceCacheDir p_dir); + +protected: + static void _bind_methods(); + +public: + ResourceCache(); + ~ResourceCache() override; + + static bool has(const String &p_path, ResourceCacheDir p_dir); + static Ref get(const String &p_path, ResourceCacheDir p_dir); + static void set(const String &p_path, const Ref &p_resource, ResourceCacheDir p_dir); + static void remove(const String &p_path, ResourceCacheDir p_dir); + static void clear(ResourceCacheDir p_dir); + static void clear_all(); + + static ResourceCache *get_singleton(); +}; + +} // namespace godot + +VARIANT_ENUM_CAST(ResourceCache::ResourceCacheDir); diff --git a/src/core/TrainController.cpp b/src/core/TrainController.cpp index fd107abb..49cbb7d9 100644 --- a/src/core/TrainController.cpp +++ b/src/core/TrainController.cpp @@ -8,6 +8,15 @@ #include namespace godot { + TrainController::~TrainController() { + state.clear(); + config.clear(); + internal_state.clear(); + if (mover != nullptr) { + delete mover; + mover = nullptr; + } + } const char *TrainController::MOVER_CONFIG_CHANGED_SIGNAL = "mover_config_changed"; const char *TrainController::MOVER_INITIALIZED_SIGNAL = "mover_initialized"; diff --git a/src/core/TrainController.hpp b/src/core/TrainController.hpp index c1c18380..54f31bfb 100644 --- a/src/core/TrainController.hpp +++ b/src/core/TrainController.hpp @@ -14,6 +14,8 @@ namespace godot { class TrainController : public Node { GDCLASS(TrainController, Node) + public: + ~TrainController() override; private: TMoverParameters *mover{}; double initial_velocity = 0.0; diff --git a/src/core/TrainSystem.cpp b/src/core/TrainSystem.cpp index f26bfac9..9056d81e 100644 --- a/src/core/TrainSystem.cpp +++ b/src/core/TrainSystem.cpp @@ -29,6 +29,11 @@ namespace godot { ClassDB::bind_method(D_METHOD("log", "train_id", "loglevel", "line"), &TrainSystem::log); } + TrainSystem::~TrainSystem() { + commands.clear(); + trains.clear(); + } + int TrainSystem::get_train_count() const { return static_cast(trains.size()); } diff --git a/src/core/TrainSystem.hpp b/src/core/TrainSystem.hpp index 3b728458..11a037ca 100644 --- a/src/core/TrainSystem.hpp +++ b/src/core/TrainSystem.hpp @@ -11,16 +11,17 @@ namespace godot { class TrainController; - class TrainSystem : public RefCounted { - GDCLASS(TrainSystem, RefCounted); + class TrainSystem : public Object { + GDCLASS(TrainSystem, Object); private: std::map trains; Dictionary commands; public: + ~TrainSystem() override; static TrainSystem *get_instance() { - return dynamic_cast(godot::Engine::get_singleton()->get_singleton("TrainSystem")); + return dynamic_cast(Engine::get_singleton()->get_singleton("TrainSystem")); } void register_train(const String &train_id, TrainController *train); diff --git a/src/core/utils.hpp b/src/core/utils.hpp index 70bf107f..c5905fda 100644 --- a/src/core/utils.hpp +++ b/src/core/utils.hpp @@ -25,7 +25,6 @@ namespace libmaszyna::utils { std::string enum_to_string(); template Type_ interpolate(Type_ const &First, Type_ const &Second, double const Factor) { - return static_cast((First * (1.0 - Factor)) + (Second * Factor)); } @@ -50,7 +49,7 @@ namespace libmaszyna::utils { * @param callbackFn Callback function taking no arguments */ ObservableValue(const T& initialValue, std::function callbackFn) - : value_(initialValue), callback_([fn = std::move(callbackFn)](const T&) { if (fn) fn(); }) {} + : value_(initialValue), callback_([fn = std::move(callbackFn)](const T&) { if (fn) { fn(); } }) {} /** * ObservableValue watches any change to the variable it declares and calls the provided callback as it changes diff --git a/src/engines/TrainDieselEngine.cpp b/src/engines/TrainDieselEngine.cpp index 0aad8e15..9545921f 100644 --- a/src/engines/TrainDieselEngine.cpp +++ b/src/engines/TrainDieselEngine.cpp @@ -6,6 +6,10 @@ #include namespace godot { + TrainDieselEngine::~TrainDieselEngine() { + wwlist.clear(); + } + void TrainDieselEngine::_bind_methods() { BIND_PROPERTY(Variant::FLOAT, "oil_min_pressure", "oil_pump/pressure_minimum", &TrainDieselEngine::set_oil_min_pressure, &TrainDieselEngine::get_oil_min_pressure, "oil_min_pressure"); BIND_PROPERTY(Variant::FLOAT, "oil_max_pressure", "oil_pump/pressure_maximum", &TrainDieselEngine::set_oil_max_pressure, &TrainDieselEngine::get_oil_max_pressure, "oil_max_pressure"); diff --git a/src/engines/TrainDieselEngine.hpp b/src/engines/TrainDieselEngine.hpp index 238ce566..6c1ab3d8 100644 --- a/src/engines/TrainDieselEngine.hpp +++ b/src/engines/TrainDieselEngine.hpp @@ -9,6 +9,8 @@ namespace godot { class TrainDieselEngine : public TrainEngine { GDCLASS(TrainDieselEngine, TrainEngine) + public: + ~TrainDieselEngine() override; private: static void _bind_methods(); MAKE_MEMBER_GS(float, oil_min_pressure, 0.0); diff --git a/src/engines/TrainEngine.cpp b/src/engines/TrainEngine.cpp index 87a49bc1..b754cbaf 100644 --- a/src/engines/TrainEngine.cpp +++ b/src/engines/TrainEngine.cpp @@ -4,7 +4,10 @@ #include namespace godot { - class TrainController; + TrainEngine::~TrainEngine() { + motor_param_table.clear(); + } + void TrainEngine::_bind_methods() { ClassDB::bind_method(D_METHOD("main_switch", "enabled"), &TrainEngine::main_switch); BIND_PROPERTY_W_HINT_RES_ARRAY(Variant::ARRAY, "motor_param_table", "motor_param_table", &TrainEngine::set_motor_param_table, &TrainEngine::get_motor_param_table, "motor_param_table", PROPERTY_HINT_TYPE_STRING, "MotorParameter"); diff --git a/src/engines/TrainEngine.hpp b/src/engines/TrainEngine.hpp index a4fafe27..dbf7226c 100644 --- a/src/engines/TrainEngine.hpp +++ b/src/engines/TrainEngine.hpp @@ -9,6 +9,7 @@ namespace godot { class TrainEngine : public TrainPart { GDCLASS(TrainEngine, TrainPart) public: + ~TrainEngine() override; enum EngineType { NONE, DUMB, diff --git a/src/lighting/TrainLighting.cpp b/src/lighting/TrainLighting.cpp index b9a830a4..98416cd7 100644 --- a/src/lighting/TrainLighting.cpp +++ b/src/lighting/TrainLighting.cpp @@ -2,6 +2,14 @@ namespace godot { const char *TrainLighting::SELECTOR_POSITION_CHANGED_SIGNAL = "selector_position_changed"; + TrainLighting::TrainLighting() { + train_controller_node = nullptr; + } + + TrainLighting::~TrainLighting() { + light_position_list.clear(); + } + void TrainLighting::_bind_methods() {BIND_PROPERTY(Variant::COLOR, "head_light_color", "head_light/color", &TrainLighting::set_head_light_color, &TrainLighting::get_head_light_color, "color"); BIND_PROPERTY(Variant::FLOAT, "head_light_dimmed_multiplier", "head_light/dimmed_multiplier", &TrainLighting::set_dimming_multiplier, &TrainLighting::get_dimming_multiplier, "multiplier"); BIND_PROPERTY(Variant::FLOAT, "head_light_normal_multiplier", "head_light/normal_multiplier", &TrainLighting::set_normal_multiplier, &TrainLighting::get_normal_multiplier, "multiplier"); @@ -31,16 +39,20 @@ namespace godot { mover->LightsWrap = wrap_light_selector; mover->LightsDefPos = default_selector_position; mover->LightPower = 0; //LightPower is used there but declared in the Param section in the .fiz file - mover->LightPowerSource.SourceType = _controller->power_source_map.at(light_source); - mover->AlterLightPowerSource.SourceType = _controller->power_source_map.at(alternative_light_source); + if (train_controller_node != nullptr) { + mover->LightPowerSource.SourceType = train_controller_node->power_source_map.at(light_source); + mover->AlterLightPowerSource.SourceType = train_controller_node->power_source_map.at(alternative_light_source); + } mover->LightsPos = selector_position; } void TrainLighting::_do_fetch_state_from_mover(TMoverParameters *mover, Dictionary &state) { ASSERT_MOVER(mover); - state["light_position"] = mover->LightsPosNo; - state["light_power"] = mover->LightPower; - state["power_source"] = _controller->tpower_source_map.at(mover->LightPowerSource.SourceType); + state.set("light_position", mover->LightsPosNo); + state.set("light_power", mover->LightPower); + if (train_controller_node != nullptr) { + state.set("power_source", train_controller_node->tpower_source_map.at(mover->LightPowerSource.SourceType)); + } } void TrainLighting::_do_fetch_config_from_mover(TMoverParameters *mover, Dictionary &config) { diff --git a/src/lighting/TrainLighting.hpp b/src/lighting/TrainLighting.hpp index 90baeccb..0b429d7c 100644 --- a/src/lighting/TrainLighting.hpp +++ b/src/lighting/TrainLighting.hpp @@ -10,9 +10,12 @@ namespace godot { class TrainLighting : public TrainPart { GDCLASS(TrainLighting, TrainPart); + public: + TrainLighting(); + ~TrainLighting() override; + private: static void _bind_methods(); - const TrainController *_controller = memnew(TrainController); TypedArray light_position_list; protected: void _do_update_internal_mover(TMoverParameters *mover) override; diff --git a/src/load/TrainLoad.cpp b/src/load/TrainLoad.cpp index d60baf3f..4a256cf2 100644 --- a/src/load/TrainLoad.cpp +++ b/src/load/TrainLoad.cpp @@ -1,6 +1,12 @@ #include "TrainLoad.hpp" namespace godot { + TrainLoad::~TrainLoad() { + load_list.clear(); + accepted_loads.clear(); + minimum_load_offsets.clear(); + } + void TrainLoad::_bind_methods() { BIND_PROPERTY_W_HINT_RES_ARRAY(Variant::ARRAY, "load_list", "load_list", &TrainLoad::set_load_list, &TrainLoad::get_load_list, "load_list", PROPERTY_HINT_ARRAY_TYPE, "LoadListItem") BIND_PROPERTY_W_HINT(Variant::INT, "load_unit", "load_unit", &TrainLoad::set_load_unit, &TrainLoad::get_load_unit, "load_unit", PROPERTY_HINT_ENUM, "Tons,Pieces"); diff --git a/src/load/TrainLoad.hpp b/src/load/TrainLoad.hpp index 48af005c..81dc57c1 100644 --- a/src/load/TrainLoad.hpp +++ b/src/load/TrainLoad.hpp @@ -6,6 +6,8 @@ namespace godot { class TrainLoad : public TrainPart { GDCLASS(TrainLoad, TrainPart) + public: + ~TrainLoad() override; private: static void _bind_methods(); TypedArray load_list; diff --git a/src/macros.hpp b/src/macros.hpp index 77f6989a..d714a609 100644 --- a/src/macros.hpp +++ b/src/macros.hpp @@ -1,7 +1,6 @@ #ifndef MACROS_HPP #define MACROS_HPP #include "core/utils.hpp" - /** * Macro for generating private members with their setters and getters * @param type Member type @@ -19,6 +18,23 @@ public: void set_##name(const type &value) { \ name = value; \ } +/** + * Macro for generating private members with their setters and getters without any default value + * @param type Member type + * @param name Member name + * @param default_value Default value + */ +#define MAKE_MEMBER_GS_NO_DEF(type, name) \ +private: \ + type name; \ + \ +public: \ + const type &get_##name() const { \ + return name; \ + } \ + void set_##name(const type &value) { \ + name = value; \ + } /** * Macro for generating private synchronized members with their setters and getters. Those members will ALWAYS be @@ -51,13 +67,48 @@ private: type name = default_value; \ \ public: \ - type get_##name() const { \ + type get_##name() const { \ return name; \ } \ void set_##name(type value) { \ name = value; \ } +/** + * Macro for generating private members with their setters and getters without a const reference + * @param type Member type + * @param name Member name + * @param default_value Default value + */ +#define MAKE_MEMBER_OBSERVABLE_GS(type, name) \ +private: \ + libmaszyna::utils::ObservableValue name; \ + \ +public: \ + type get_##name() const { \ + return name.get(); \ + } \ + void set_##name(const type &value) { \ + name = value; \ + } + +/** + * Macro for generating private members with their setters and getters without a const reference + * @param type Member type + * @param name Member name + * @param default_value Default value + */ +#define MAKE_MEMBER_OBSERVABLE_GS_NR(type, name) \ +private: \ + libmaszyna::utils::ObservableValue name; \ + \ +public: \ + type get_##name() const { \ + return name.get(); \ + } \ + void set_##name(const type value) { \ + name = value; \ + } /** * Generates Godot setters and getters with the desired property using the following: @@ -147,40 +198,4 @@ public: PROPERTY_USAGE_DEFAULT, "TypedArray<" + String(p_hint_resource_name) + ">"), \ "set_" + String(p_name), "get_" + String(p_name)); -/** - * Macro for generating private members with their setters and getters without a const reference - * @param type Member type - * @param name Member name - * @param default_value Default value - */ -#define MAKE_MEMBER_OBSERVABLE_GS(type, name) \ -private: \ - libmaszyna::utils::ObservableValue name; \ - \ -public: \ - type get_##name() const { \ - return name.get(); \ - } \ - void set_##name(const type &value) { \ - name = value; \ - } - -/** - * Macro for generating private members with their setters and getters without a const reference - * @param type Member type - * @param name Member name - * @param default_value Default value - */ -#define MAKE_MEMBER_OBSERVABLE_GS_NR(type, name) \ -private: \ - libmaszyna::utils::ObservableValue name; \ - \ -public: \ - type get_##name() const { \ - return name.get(); \ - } \ - void set_##name(const type value) { \ - name = value; \ - } - -#endif // MACROS_HPP \ No newline at end of file +#endif // MACROS_HPP diff --git a/src/models/MaterialManager.cpp b/src/models/MaterialManager.cpp index ca5d37ea..68b64384 100644 --- a/src/models/MaterialManager.cpp +++ b/src/models/MaterialManager.cpp @@ -1,11 +1,13 @@ #include "MaterialManager.hpp" #include "MaterialParser.hpp" +#include "core/ResourceCache.hpp" #include "godot_cpp/classes/config_file.hpp" #include "resources/material/MaszynaMaterial.hpp" #include #include #include +#include #include #include #include @@ -21,57 +23,35 @@ namespace godot { &MaterialManager::get_material, DEFVAL(Transparency::Disabled), DEFVAL(false), DEFVAL(Color(1, 1, 1))); ClassDB::bind_method(D_METHOD("get_texture", "texture_path"), &MaterialManager::get_texture); ClassDB::bind_method(D_METHOD("load_texture", "model_path", "material_name"), &MaterialManager::load_texture); - ClassDB::bind_method( - D_METHOD("load_submodel_texture", "model_path", "material_name"), - &MaterialManager::load_submodel_texture); - ClassDB::bind_method(D_METHOD("_clear_cache"), &MaterialManager::_clear_cache); - ClassDB::bind_method(D_METHOD("_on_config_changed"), &MaterialManager::_on_config_changed); + + ClassDB::bind_static_method( + get_class_static(), D_METHOD("generate_heightmap_from_albedo", "albedo"), + &MaterialManager::generate_heightmap_from_albedo); + ClassDB::bind_static_method( + get_class_static(), D_METHOD("generate_normal_from_albedo", "albedo", "strength"), + &MaterialManager::generate_normal_from_albedo, DEFVAL(1.0f)); + ClassDB::bind_static_method( + get_class_static(), D_METHOD("generate_metallic_from_albedo", "albedo", "threshold"), + &MaterialManager::generate_metallic_from_albedo, DEFVAL(0.5f)); + BIND_ENUM_CONSTANT(Disabled); BIND_ENUM_CONSTANT(Alpha); BIND_ENUM_CONSTANT(AlphaScissor); } - MaterialManager::MaterialManager() : game_dir(""), parser(memnew(MaterialParser)), user_settings_node(nullptr) { + MaterialManager::~MaterialManager() { + _materials.clear(); + _unknown_material.unref(); + _unknown_texture.unref(); + mutex.unref(); + parser.unref(); + } + + MaterialManager::MaterialManager() { mutex.instantiate(); - _transparency_codes[0] = "0"; - _transparency_codes[1] = "a"; - _transparency_codes[2] = "s"; + parser.instantiate(); UtilityFunctions::print("[MaterialManager] Initializing MaterialManager..."); _clear_cache(); - - // Try to find UserSettings if it's already registered as a singleton - if (Engine::get_singleton()->has_singleton("UserSettings")) { - UtilityFunctions::print_verbose("[MaterialManager] Engine has UserSettings singleton"); - user_settings_node = Engine::get_singleton()->get_singleton("UserSettings"); - if (const Variant result = user_settings_node->call("get_maszyna_game_dir"); - result.get_type() == Variant::STRING) { - game_dir = result; - UtilityFunctions::print_verbose("[MaterialManager] Project data dir: " + game_dir); - } else { - UtilityFunctions::push_warning("[MaterialManager] Cannot access MaSzyna game directory via UserSettings singleton. Falling back to manual reading settings file..."); - } - } else { - UtilityFunctions::push_warning("[MaterialManager] Cannot access UserSettings singleton. Falling back to manual reading settings file..."); - } - - if (game_dir == "") { - Ref _config = memnew(ConfigFile); - _config.instantiate(); - _config->load("user://settings.cfg"); - game_dir = _config->get_value("maszyna", "game_dir"); - } - - if (user_settings_node != nullptr) { - if (Object *mm_singleton = Engine::get_singleton()->get_singleton("MaterialManager"); - mm_singleton != nullptr) { - user_settings_node->connect("config_changed", Callable(mm_singleton, "_on_config_changed")); - user_settings_node->connect("cache_clear_requested", Callable(mm_singleton, "_clear_cache")); - } - } - } - - MaterialManager::~MaterialManager() { - mutex.unref(); } String MaterialManager::get_material_path(const String &model_name, const String &material_name) { @@ -84,10 +64,10 @@ namespace godot { game_dir + "/textures/" + model_name + "/" + material_name + ".mat", game_dir + "/" + material_name + ".mat", game_dir + "/" + "textures/" + material_name + ".mat", - }; for (String _file: _possible_paths) { + UtilityFunctions::print_verbose("[MaterialManager] Trying to load material: " + _file); if (FileAccess::file_exists(_file)) { return _file; } @@ -99,40 +79,52 @@ namespace godot { void MaterialManager::_clear_cache() { mutex->lock(); - _textures.clear(); _materials.clear(); - use_alpha_transparency = false; - if (user_settings_node != nullptr) { - if (const Variant v = user_settings_node->call("get_setting", "e3d", "use_alpha_transparency", false); - v.get_type() != Variant::NIL) { - use_alpha_transparency = static_cast(v); + ResourceCache::clear(ResourceCache::RESOURCE_CACHE_DIR_TEXTURES); + + Ref _config; + _config.instantiate(); + if (_config->load("user://settings.cfg") == OK) { + use_alpha_transparency = _config->get_value("e3d", "use_alpha_transparency", false); + auto_generate_normal = _config->get_value("e3d", "auto_generate_normal", false); + auto_generate_metallic = _config->get_value("e3d", "auto_generate_metallic", false); + auto_generate_height = _config->get_value("e3d", "auto_generate_height", false); + if (OS::get_singleton()->has_feature("release") && !OS::get_singleton()->has_feature("editor")) { + game_dir = "."; + } else { + game_dir = _config->get_value("maszyna", "game_dir", "."); } + } else { + use_alpha_transparency = false; + auto_generate_normal = false; + auto_generate_metallic = false; + auto_generate_height = false; + game_dir = "."; } - + _config.unref(); + UtilityFunctions::print_verbose("[MaterialManager] Config loaded. Project data dir: " + game_dir); mutex->unlock(); } - - void MaterialManager::_on_config_changed() { - _clear_cache(); - } - - Ref MaterialManager::load_material(const String &model_path, const String &material_name) { if (parser.is_null()) { parser.instantiate(); } - UtilityFunctions::print_verbose("[MaterialManager] Loading material: " + material_name); - return parser->parse(this, model_path, material_name); + return parser->parse(get_material_path(model_path, material_name), material_name); } Ref MaterialManager::get_material( const String &model_path, const String &material_path, Transparency transparent, const bool is_sky, const Color &diffuse_color) { - const String _code = model_path + String("/") + material_path + ":t=" + _transparency_codes[transparent] + - ":s=" + (is_sky ? "1" : "0"); + + const char *t_code = "0"; + if (transparent >= 0 && transparent < 3) { + t_code = _transparency_codes[transparent]; + } + + const String _code = model_path + String("/") + material_path + ":t=" + t_code + ":s=" + (is_sky ? "1" : "0"); mutex->lock(); if (_materials.has(_code)) { Ref m = _materials.get(_code, ""); @@ -150,7 +142,53 @@ namespace godot { UtilityFunctions::print_verbose("[MaterialManager] Material is valid: " + material_path); if (const String _albedo = _mmat->get_albedo_texture_path(); !_albedo.is_empty()) { UtilityFunctions::print_verbose("[MaterialManager] Albedo is not empty: " + _albedo); - _m->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, load_texture(model_path, _albedo.split(":").get(0))); + const Ref albedo_tex = load_texture(model_path, _albedo.split(":").get(0)); + _m->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, albedo_tex); + + if (albedo_tex.is_valid()) { + const Ref albedo_img = albedo_tex->get_image(); + if (albedo_img.is_valid()) { + if (auto_generate_metallic) { + const Ref metallic_img = generate_metallic_from_albedo(albedo_img); + if (metallic_img.is_valid()) { + UtilityFunctions::print_verbose( + "[MaterialManager] Auto-generated and applied metallic texture for: " + + material_path); + _m->set_texture( + BaseMaterial3D::TEXTURE_METALLIC, + ImageTexture::create_from_image(metallic_img)); + _m->set_metallic(1.0); + } + } + + // Auto-generate Normal if not explicitly provided + if (auto_generate_normal && _mmat->get_normal_texture_path().is_empty()) { + const Ref normal_img = generate_normal_from_albedo(albedo_img); + if (normal_img.is_valid()) { + UtilityFunctions::print_verbose( + "[MaterialManager] Auto-generated and applied normal texture for: " + + material_path); + _m->set_texture( + BaseMaterial3D::TEXTURE_NORMAL, ImageTexture::create_from_image(normal_img)); + _m->set_feature(StandardMaterial3D::FEATURE_NORMAL_MAPPING, true); + } + } + + // Auto-generate Height + if (auto_generate_height) { + const Ref height_img = generate_heightmap_from_albedo(albedo_img); + if (height_img.is_valid()) { + UtilityFunctions::print_verbose( + "[MaterialManager] Auto-generated and applied height texture for: " + + material_path); + _m->set_texture( + BaseMaterial3D::TEXTURE_HEIGHTMAP, ImageTexture::create_from_image(height_img)); + _m->set_feature(StandardMaterial3D::FEATURE_HEIGHT_MAPPING, true); + _m->set_heightmap_scale(0.01); + } + } + } + } } else { // possibly "COLORED" material UtilityFunctions::print_verbose("[MaterialManager] Albedo is empty: " + material_path); @@ -173,7 +211,7 @@ namespace godot { } } - _mmat->apply_to_material(*_m); + _mmat->apply_to_material(_m.ptr()); } else { UtilityFunctions::push_warning("[MaterialManager] Material is not valid: " + material_path); } @@ -235,7 +273,7 @@ namespace godot { "[MaterialManager] Loading texture for: " + model_path + ", " + material_name + "..."); if (_unknown_texture.is_null()) { - _unknown_texture.instantiate(); // Create new ImageTexture + _unknown_texture.instantiate(); // Create a new ImageTexture } static bool fallback_loaded = false; @@ -279,14 +317,11 @@ namespace godot { return _unknown_texture; } - mutex->lock(); - if (_textures.has(final_path)) { - Ref t = _textures.get(final_path, ""); - mutex->unlock(); - return t; + Ref cached_res = ResourceCache::get(final_path, ResourceCache::RESOURCE_CACHE_DIR_TEXTURES); + if (cached_res.is_valid()) { + return cached_res; } - mutex->unlock(); const Ref file = FileAccess::open(final_path, FileAccess::READ); if (!file.is_valid()) { UtilityFunctions::push_error("Error opening texture file: " + final_path); @@ -298,15 +333,57 @@ namespace godot { const Ref im = memnew(Image); if (const Error res = im->load_dds_from_buffer(buffer); res == OK) { Ref tex = ImageTexture::create_from_image(im); - mutex->lock(); - _textures.set(final_path, tex); - mutex->unlock(); + ResourceCache::set(final_path, tex, ResourceCache::RESOURCE_CACHE_DIR_TEXTURES); return tex; } + return _unknown_texture; } - Ref MaterialManager::load_submodel_texture(const String &model_path, const String &material_name) { - return load_texture(model_path, material_name); + + Ref MaterialManager::generate_heightmap_from_albedo(const Ref &p_albedo) { + if (p_albedo.is_null()) { + return nullptr; + } + Ref height = p_albedo->duplicate(); + if (height->is_compressed()) { + height->decompress(); + } + height->convert(Image::FORMAT_L8); + height->adjust_bcs(1.0, 0.5, 1.0); // Reduce contrast to make it less "weird" + return height; + } + + Ref MaterialManager::generate_normal_from_albedo(const Ref &p_albedo, const float p_strength) { + Ref height = generate_heightmap_from_albedo(p_albedo); + if (height.is_null()) { + return nullptr; + } + height->bump_map_to_normal_map(p_strength); + return height; + } + + Ref MaterialManager::generate_metallic_from_albedo(const Ref &p_albedo, float p_threshold) { + if (p_albedo.is_null()) { + return nullptr; + } + + Ref meta = p_albedo->duplicate(); + if (meta->is_compressed()) { + meta->decompress(); + } + meta->convert(Image::FORMAT_L8); + + for (int y = 0; y < meta->get_height(); y++) { + for (int x = 0; x < meta->get_width(); x++) { + const Color c = meta->get_pixel(x, y); + if (c.r > p_threshold) { + meta->set_pixel(x, y, Color(1, 1, 1)); + } else { + meta->set_pixel(x, y, Color(0, 0, 0)); + } + } + } + return meta; } } // namespace godot diff --git a/src/models/MaterialManager.hpp b/src/models/MaterialManager.hpp index 5cc5efb2..9693bdbc 100644 --- a/src/models/MaterialManager.hpp +++ b/src/models/MaterialManager.hpp @@ -1,5 +1,6 @@ #pragma once +#include "MaterialParser.hpp" #include "resources/material/MaszynaMaterial.hpp" #include @@ -8,29 +9,25 @@ #include namespace godot { - class MaterialParser; class MaterialManager : public Object { GDCLASS(MaterialManager, Object) - - public: - enum Transparency { Disabled, Alpha, AlphaScissor }; - private: - String game_dir; + String game_dir = ""; Ref mutex; Ref parser; Ref _unknown_material; Ref _unknown_texture; - Object *user_settings_node; - Dictionary _textures; + Object *user_settings_node = nullptr; Dictionary _materials; - bool use_alpha_transparency; + bool use_alpha_transparency = false; + bool auto_generate_normal = false; + bool auto_generate_metallic = false; + bool auto_generate_height = false; - const char *_transparency_codes[3]; + const char *_transparency_codes[3] = {"0", "a", "s"}; void _clear_cache(); - void _on_config_changed(); protected: static void _bind_methods(); @@ -38,7 +35,7 @@ namespace godot { public: MaterialManager(); ~MaterialManager() override; - + enum Transparency { Disabled, Alpha, AlphaScissor }; String get_material_path(const String &model_name, const String &material_name); Ref load_material(const String &model_path, const String &material_name); @@ -47,7 +44,10 @@ namespace godot { bool is_sky = false, const Color &diffuse_color = Color(1.0, 1.0, 1.0)); Ref get_texture(const String &texture_path); Ref load_texture(const String &model_path, const String &material_name); - Ref load_submodel_texture(const String &model_path, const String &material_name); + + static Ref generate_heightmap_from_albedo(const Ref &p_albedo); + static Ref generate_normal_from_albedo(const Ref &p_albedo, float p_strength = 1.0f); + static Ref generate_metallic_from_albedo(const Ref &p_albedo, float p_threshold = 0.5f); }; } // namespace godot diff --git a/src/models/MaterialParser.cpp b/src/models/MaterialParser.cpp index f558c2aa..54307b90 100644 --- a/src/models/MaterialParser.cpp +++ b/src/models/MaterialParser.cpp @@ -6,13 +6,14 @@ namespace godot { void MaterialParser::_bind_methods() { - ClassDB::bind_method(D_METHOD("parse", "material_manager", "model_path", "material_path"), &MaterialParser::parse); + ClassDB::bind_method(D_METHOD("parse", "material_path", "material_name"), &MaterialParser::parse); } - Ref MaterialParser::parse(MaterialManager *material_manager, const String &model_path, const String &material_path) { + Ref MaterialParser::parse(const String &material_path, const String &material_name) const { + UtilityFunctions::print_verbose("[MaterialParser] Parsing material: " + material_path); Ref material = memnew(MaszynaMaterial); - const String final_path = material_manager->get_material_path(model_path, material_path); - if (const Ref file = FileAccess::open(final_path, FileAccess::READ); file.is_valid()) { + if (const Ref file = FileAccess::open(material_path, FileAccess::READ); file.is_valid()) { + UtilityFunctions::print_verbose("[MaterialParser] Opening material file: " + material_path); MaszynaParser *parser = memnew(MaszynaParser); parser->initialize(file->get_buffer(static_cast(file->get_length()))); const Dictionary data = {}; @@ -62,12 +63,12 @@ namespace godot { auto _t2 = data.get("texture_normalmap", data.get("texture2", "")); if (_t1.get_type() == Variant::ARRAY) { - UtilityFunctions::push_warning("[MaterialParser]: More than 1 texture parameters (texture_diffuse, " + final_path + ") are not supported!"); + UtilityFunctions::push_warning("[MaterialParser]: More than 1 texture parameters (texture_diffuse, " + material_path + ") are not supported!"); _t1 = _t1.get(0); } if (_t2.get_type() == Variant::ARRAY) { - UtilityFunctions::push_warning("[MaterialParser]: More than 1 texture parameters (texture_normalmap, " + final_path + ") are not supported!"); + UtilityFunctions::push_warning("[MaterialParser]: More than 1 texture parameters (texture_normalmap, " + material_path + ") are not supported!"); _t2 = _t2.get(0); } @@ -75,7 +76,7 @@ namespace godot { material->set_normal_texture_path(_t2); memdelete(parser); } else { - material->set_albedo_texture_path(material_path); + material->set_albedo_texture_path(material_name); } return material; } diff --git a/src/models/MaterialParser.hpp b/src/models/MaterialParser.hpp index 2fe52391..39ec2496 100644 --- a/src/models/MaterialParser.hpp +++ b/src/models/MaterialParser.hpp @@ -1,10 +1,9 @@ #pragma once -#include "models/MaterialManager.hpp" + #include "resources/material/MaszynaMaterial.hpp" #include namespace godot { - class MaszynaParser; class MaterialParser: public RefCounted { GDCLASS(MaterialParser, RefCounted) private: @@ -13,6 +12,6 @@ namespace godot { protected: static void _bind_methods(); public: - Ref parse(MaterialManager *material_manager, const String &model_path, const String &material_path); + Ref parse(const String &material_path, const String &material_name) const; }; } //namespace godot \ No newline at end of file diff --git a/src/models/e3d/E3DModel.cpp b/src/models/e3d/E3DModel.cpp new file mode 100644 index 00000000..c0a51897 --- /dev/null +++ b/src/models/e3d/E3DModel.cpp @@ -0,0 +1,26 @@ +#include "E3DModel.hpp" + +namespace godot { + E3DModel::~E3DModel() { + clear(); + } + + void E3DModel::clear() { + for (int i = 0; i < submodels.size(); i++) { + Ref sm = submodels.get(i); + if (sm.is_valid()) { + sm->clear(); + } + } + submodels.clear(); + } + + void E3DModel::_bind_methods() { + BIND_PROPERTY(Variant::STRING, "name", "name", &E3DModel::set_name, &E3DModel::get_name, "p_name"); + BIND_PROPERTY_W_HINT_RES_ARRAY(Variant::ARRAY, "submodels", "submodels", &E3DModel::set_submodels, &E3DModel::get_submodels, "p_submodels", PROPERTY_HINT_ARRAY_TYPE, "E3DSubModel"); + } + + void E3DModel::add_child(const Ref &sub_model) { + submodels.append(sub_model); + } +} // namespace godot diff --git a/src/models/e3d/E3DModel.hpp b/src/models/e3d/E3DModel.hpp new file mode 100644 index 00000000..d119dc98 --- /dev/null +++ b/src/models/e3d/E3DModel.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "E3DSubModel.hpp" +#include +#include + +namespace godot { + class E3DModel: public Resource { + GDCLASS(E3DModel, Resource) + public: + ~E3DModel() override; + protected: + static void _bind_methods(); + MAKE_MEMBER_GS_NR(String, name, ""); + MAKE_MEMBER_GS_NR(TypedArray, submodels, TypedArray()); + + void add_child(const Ref& sub_model); + void clear(); + }; +} // namespace godot \ No newline at end of file diff --git a/src/models/e3d/E3DModelManager.cpp b/src/models/e3d/E3DModelManager.cpp new file mode 100644 index 00000000..248906fa --- /dev/null +++ b/src/models/e3d/E3DModelManager.cpp @@ -0,0 +1,62 @@ +#include "E3DModelManager.hpp" +#include "core/ResourceCache.hpp" + +#include +#include +#include +#include + +namespace godot { + + void E3DModelManager::_set_owner_recursive(Node *node, Node *new_owner) { + if (node != new_owner) { + node->set_owner(new_owner); + } + + if (node->get_child_count() > 0) { + for (int i = 0; i < node->get_child_count(); ++i) { + _set_owner_recursive(node->get_child(i), node); + } + } + } + + void E3DModelManager::_bind_methods() { + ClassDB::bind_method(D_METHOD("load_model", "data_path", "file_name"), &E3DModelManager::load_model); + } + + E3DModelManager::E3DModelManager() : user_settings_node(nullptr) { + Ref _config; + _config.instantiate(); + if (_config->load("user://settings.cfg") == OK) { + if (OS::get_singleton()->has_feature("release") && !OS::get_singleton()->has_feature("editor")) { + game_dir = "."; + } else { + game_dir = _config->get_value("maszyna", "game_dir", "."); + } + } else { + game_dir = "."; + } + // Will be set when entering the SceneTree + } + + Ref E3DModelManager::load_model(const String &data_path, const String &file_name) { + Ref model; + const String path = game_dir + "/" + data_path + "/" + file_name + ".e3d"; + + Ref cached_res = ResourceCache::get(path, ResourceCache::RESOURCE_CACHE_DIR_MODELS); + if (cached_res.is_valid()) { + return cached_res; + } + + if (FileAccess::file_exists(path)) { + if (const Ref res = ResourceLoader::get_singleton()->load(path); res.is_valid()) { + model = res; + ResourceCache::set(path, model, ResourceCache::RESOURCE_CACHE_DIR_MODELS); + } + } else { + UtilityFunctions::push_warning("File does not exist: " + path); + } + + return model; + } +} // namespace godot diff --git a/src/models/e3d/E3DModelManager.hpp b/src/models/e3d/E3DModelManager.hpp new file mode 100644 index 00000000..e7bf892e --- /dev/null +++ b/src/models/e3d/E3DModelManager.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "E3DModel.hpp" +#include "macros.hpp" + +#include + +#include + +namespace godot { + class E3DModelManager: public Node { + GDCLASS(E3DModelManager, Node) + private: + String game_dir; + Object *user_settings_node; + static void _set_owner_recursive(Node *node, Node *new_owner); + protected: + static void _bind_methods(); + public: + E3DModelManager(); + Ref load_model(const String &data_path, const String &file_name); + }; +} //namespace godot \ No newline at end of file diff --git a/src/models/e3d/E3DResourceFormatLoader.cpp b/src/models/e3d/E3DResourceFormatLoader.cpp new file mode 100644 index 00000000..e8c4a3dd --- /dev/null +++ b/src/models/e3d/E3DResourceFormatLoader.cpp @@ -0,0 +1,36 @@ +#include "E3DResourceFormatLoader.hpp" +#include + +namespace godot { + + void E3DResourceFormatLoader::_bind_methods() {} + + PackedStringArray E3DResourceFormatLoader::_get_recognized_extensions() const { + PackedStringArray arr; + arr.push_back("e3d"); + return arr; + } + + bool E3DResourceFormatLoader::_handles_type(const StringName &p_type) const { + return p_type == StringName("E3DModel"); + } + + String E3DResourceFormatLoader::_get_resource_type(const String &p_path) const { + return "E3DModel"; + } + + Variant E3DResourceFormatLoader::_load( + const String &p_path, const String &p_original_path, const bool p_use_sub_threads, const int32_t p_cache_mode) const { + const Ref file = FileAccess::open(p_path, FileAccess::READ); + if (!file.is_valid()) { + UtilityFunctions::push_error("Failed to open E3D file: " + p_path); + return Variant(); + } + + E3DParser *parser = memnew(E3DParser); + Ref model = parser->parse(file); + memdelete(parser); + return model; + // return ResourceFormatLoader::_load(p_path, p_original_path, p_use_sub_threads, p_cache_mode); + } +} // namespace godot diff --git a/src/models/e3d/E3DResourceFormatLoader.hpp b/src/models/e3d/E3DResourceFormatLoader.hpp new file mode 100644 index 00000000..9761b9aa --- /dev/null +++ b/src/models/e3d/E3DResourceFormatLoader.hpp @@ -0,0 +1,16 @@ +#pragma once +#include "./parsers/e3d_parser.hpp" +#include + +namespace godot { + class E3DResourceFormatLoader: public ResourceFormatLoader { + GDCLASS(E3DResourceFormatLoader, ResourceFormatLoader) + protected: + static void _bind_methods(); + public: + PackedStringArray _get_recognized_extensions() const override; + bool _handles_type(const StringName &p_type) const override; + String _get_resource_type(const String &p_path) const override; + Variant _load(const String &p_path, const String &p_original_path, bool p_use_sub_threads, int32_t p_cache_mode) const override; + }; +} // namespace godot diff --git a/src/models/e3d/E3DSubModel.cpp b/src/models/e3d/E3DSubModel.cpp new file mode 100644 index 00000000..495274b6 --- /dev/null +++ b/src/models/e3d/E3DSubModel.cpp @@ -0,0 +1,109 @@ +#include "E3DSubModel.hpp" + +namespace godot { + E3DSubModel::~E3DSubModel() { + clear(); + } + + void E3DSubModel::clear() { + for (int i = 0; i < submodels.size(); i++) { + Ref sm = submodels.get(i); + if (sm.is_valid()) { + sm->clear(); + } + } + submodels.clear(); + mesh.unref(); + parent = nullptr; + } + + void E3DSubModel::_bind_methods() { + // Enums + BIND_ENUM_CONSTANT(GL_POINTS) + BIND_ENUM_CONSTANT(GL_LINES) + BIND_ENUM_CONSTANT(GL_LINE_STRIP) + BIND_ENUM_CONSTANT(GL_LINE_LOOP) + BIND_ENUM_CONSTANT(GL_TRIANGLES) + BIND_ENUM_CONSTANT(GL_TRIANGLE_STRIP) + BIND_ENUM_CONSTANT(GL_TRIANGLE_FAN) + BIND_ENUM_CONSTANT(GL_QUADS) + BIND_ENUM_CONSTANT(GL_QUAD_STRIP) + BIND_ENUM_CONSTANT(GL_POLYGON) + BIND_ENUM_CONSTANT(TRANSFORM) + BIND_ENUM_CONSTANT(FREE_SPOTLIGHT) + BIND_ENUM_CONSTANT(STARS) + + BIND_ENUM_CONSTANT(NONE) + BIND_ENUM_CONSTANT(ROTATE_VEC) + BIND_ENUM_CONSTANT(ROTATE_XYZ) + BIND_ENUM_CONSTANT(MOVE) + BIND_ENUM_CONSTANT(JUMP_SECONDS) + BIND_ENUM_CONSTANT(JUMP_MINUTES) + BIND_ENUM_CONSTANT(JUMP_HOURS) + BIND_ENUM_CONSTANT(JUMP_HOURS24) + BIND_ENUM_CONSTANT(SECONDS) + BIND_ENUM_CONSTANT(MINUTES) + BIND_ENUM_CONSTANT(HOURS) + BIND_ENUM_CONSTANT(HOURS24) + BIND_ENUM_CONSTANT(BILLBOARD) + BIND_ENUM_CONSTANT(WIND) + BIND_ENUM_CONSTANT(SKY) + BIND_ENUM_CONSTANT(DIGITAL) + BIND_ENUM_CONSTANT(DIGICLK) + BIND_ENUM_CONSTANT(UNDEFINED) + BIND_ENUM_CONSTANT(IK) + BIND_ENUM_CONSTANT(IK1) + BIND_ENUM_CONSTANT(IK2) + BIND_ENUM_CONSTANT(UNKNOWN) + + // Properties + BIND_PROPERTY(Variant::STRING, "name", "name", &E3DSubModel::set_name, &E3DSubModel::get_name, "p_name"); + + BIND_PROPERTY_W_HINT(Variant::INT, "submodel_type", "submodel_type", + &E3DSubModel::set_submodel_type, &E3DSubModel::get_submodel_type, "p_submodel_type", + PROPERTY_HINT_ENUM, "GL_POINTS,GL_LINES,GL_LINE_STRIP,GL_LINE_LOOP,GL_TRIANGLES,GL_TRIANGLE_STRIP,GL_TRIANGLE_FAN,GL_QUADS,GL_QUAD_STRIP,GL_POLYGON,TRANSFORM,FREE_SPOTLIGHT,STARS"); + + BIND_PROPERTY_W_HINT(Variant::INT, "animation", "animation", + &E3DSubModel::set_animation, &E3DSubModel::get_animation, "p_animation", + PROPERTY_HINT_ENUM, "NONE,ROTATE_VEC,ROTATE_XYZ,MOVE,JUMP_SECONDS,JUMP_MINUTES,JUMP_HOURS,JUMP_HOURS24,SECONDS,MINUTES,HOURS,HOURS24,BILLBOARD,WIND,SKY,DIGITAL,DIGICLK,UNDEFINED,IK,IK1,IK2,UNKNOWN"); + + BIND_PROPERTY(Variant::FLOAT, "lights_on_threshold", "lights_on_threshold", &E3DSubModel::set_lights_on_threshold, &E3DSubModel::get_lights_on_threshold, "p_lights_on_threshold"); + BIND_PROPERTY(Variant::FLOAT, "visibility_light", "visibility_light", &E3DSubModel::set_visibility_light, &E3DSubModel::get_visibility_light, "p_visibility_light"); + BIND_PROPERTY(Variant::FLOAT, "visibility_range_begin", "visibility_range_begin", &E3DSubModel::set_visibility_range_begin, &E3DSubModel::get_visibility_range_begin, "p_visibility_range_begin"); + BIND_PROPERTY(Variant::FLOAT, "visibility_range_end", "visibility_range_end", &E3DSubModel::set_visibility_range_end, &E3DSubModel::get_visibility_range_end, "p_visibility_range_end"); + + BIND_PROPERTY(Variant::COLOR, "diffuse_color", "diffuse_color", &E3DSubModel::set_diffuse_color, &E3DSubModel::get_diffuse_color, "p_diffuse_color"); + BIND_PROPERTY(Variant::COLOR, "self_illumination", "self_illumination", &E3DSubModel::set_self_illumination, &E3DSubModel::get_self_illumination, "p_self_illumination"); + + BIND_PROPERTY(Variant::BOOL, "material_colored", "material_colored", &E3DSubModel::set_material_colored, &E3DSubModel::get_material_colored, "p_material_colored"); + BIND_PROPERTY(Variant::BOOL, "dynamic_material", "dynamic_material", &E3DSubModel::set_dynamic_material, &E3DSubModel::get_dynamic_material, "p_dynamic_material"); + BIND_PROPERTY(Variant::INT, "dynamic_material_index", "dynamic_material_index", &E3DSubModel::set_dynamic_material_index, &E3DSubModel::get_dynamic_material_index, "p_dynamic_material_index"); + BIND_PROPERTY(Variant::BOOL, "material_transparent", "material_transparent", &E3DSubModel::set_material_transparent, &E3DSubModel::get_material_transparent, "p_material_transparent"); + BIND_PROPERTY(Variant::STRING, "material_name", "material_name", &E3DSubModel::set_material_name, &E3DSubModel::get_material_name, "p_material_name"); + + BIND_PROPERTY(Variant::TRANSFORM3D, "transform", "transform", &E3DSubModel::set_transform, &E3DSubModel::get_transform, "p_transform"); + BIND_PROPERTY(Variant::TRANSFORM3D, "material_transform", "material_transform", &E3DSubModel::set_material_transform, &E3DSubModel::get_material_transform, "p_material_transform"); + + BIND_PROPERTY_W_HINT(Variant::OBJECT, "mesh", "mesh", + &E3DSubModel::set_mesh, &E3DSubModel::get_mesh, "p_mesh", + PROPERTY_HINT_RESOURCE_TYPE, "ArrayMesh"); + + BIND_PROPERTY_W_HINT_RES_ARRAY(Variant::ARRAY, "submodels", "submodels", + &E3DSubModel::set_submodels, &E3DSubModel::get_submodels, "p_submodels", + PROPERTY_HINT_ARRAY_TYPE, "E3DSubModel"); + + BIND_PROPERTY(Variant::BOOL, "visible", "visible", &E3DSubModel::set_visible, &E3DSubModel::get_visible, "p_visible"); + BIND_PROPERTY(Variant::BOOL, "skip_rendering", "skip_rendering", &E3DSubModel::set_skip_rendering, &E3DSubModel::get_skip_rendering, "p_skip_rendering"); + } + + void E3DSubModel::add_child(const Ref &p_sub_model) { + submodels.append(p_sub_model); + } + + void E3DSubModel::set_parent(E3DSubModel *p_sub_model) { + parent = p_sub_model; + if (p_sub_model != nullptr) { + p_sub_model->add_child(this); + } + } +} //namespace godot diff --git a/src/models/e3d/E3DSubModel.hpp b/src/models/e3d/E3DSubModel.hpp new file mode 100644 index 00000000..a21feaa2 --- /dev/null +++ b/src/models/e3d/E3DSubModel.hpp @@ -0,0 +1,89 @@ +#pragma once +#include "macros.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace godot { + class E3DSubModel: public Resource { + GDCLASS(E3DSubModel, Resource) + public: + ~E3DSubModel() override; + protected: + static void _bind_methods(); + E3DSubModel* parent = nullptr; + public: + enum SubModelType { + GL_POINTS, + GL_LINES, + GL_LINE_STRIP, + GL_LINE_LOOP, + GL_TRIANGLES, + GL_TRIANGLE_STRIP, + GL_TRIANGLE_FAN, + GL_QUADS, + GL_QUAD_STRIP, + GL_POLYGON, + TRANSFORM = 256, + FREE_SPOTLIGHT, + STARS + }; + + enum AnimationType { + NONE = 0, + ROTATE_VEC, + ROTATE_XYZ, + MOVE, + JUMP_SECONDS, + JUMP_MINUTES, + JUMP_HOURS, + JUMP_HOURS24, + SECONDS, + MINUTES, + HOURS, + HOURS24, + BILLBOARD, + WIND, + SKY, + DIGITAL, + DIGICLK, + UNDEFINED, + IK=256, + IK1, + IK2, + UNKNOWN=-1 + }; + + MAKE_MEMBER_GS_NR(String, name, "") + MAKE_MEMBER_GS_NR(SubModelType, submodel_type, GL_TRIANGLES) + MAKE_MEMBER_GS_NR(AnimationType, animation, NONE) + MAKE_MEMBER_GS_NR(float, lights_on_threshold, 0.0) + MAKE_MEMBER_GS_NR(float, visibility_light, 0.0) + MAKE_MEMBER_GS_NR(float, visibility_range_begin, 0.0) + MAKE_MEMBER_GS_NR(float, visibility_range_end, 0.0) + MAKE_MEMBER_GS_NR(Color, diffuse_color, Color(1.0, 1.0, 1.0, 1.0)) + MAKE_MEMBER_GS_NR(Color, self_illumination, Color(1.0, 1.0, 1.0, 1.0)) + MAKE_MEMBER_GS_NR(bool, material_colored, false) + MAKE_MEMBER_GS_NR(bool, dynamic_material, false) + MAKE_MEMBER_GS_NR(int, dynamic_material_index, 0) + MAKE_MEMBER_GS_NR(bool, material_transparent, false) + MAKE_MEMBER_GS_NR(String, material_name, "") + MAKE_MEMBER_GS_NR(Transform3D, transform, Transform3D()) + MAKE_MEMBER_GS_NR(Transform3D, material_transform, Transform3D()) + MAKE_MEMBER_GS_NR(Ref, mesh, Ref()) + MAKE_MEMBER_GS_NR(TypedArray, submodels, TypedArray()) + MAKE_MEMBER_GS_NR(bool, visible, true) + MAKE_MEMBER_GS_NR(bool, skip_rendering, false) + + void add_child(const Ref& p_sub_model); + void set_parent(E3DSubModel* p_sub_model); + void clear(); + }; +} //namespace godot + +VARIANT_ENUM_CAST(E3DSubModel::AnimationType) +VARIANT_ENUM_CAST(E3DSubModel::SubModelType) diff --git a/src/models/e3d/instance/E3DModelInstance.cpp b/src/models/e3d/instance/E3DModelInstance.cpp new file mode 100644 index 00000000..3da0ec2c --- /dev/null +++ b/src/models/e3d/instance/E3DModelInstance.cpp @@ -0,0 +1,107 @@ +#include "E3DModelInstance.hpp" +#include "E3DModelInstanceManager.hpp" +#include "E3DNodesInstancer.hpp" +#include + +namespace godot { + const char *E3DModelInstance::E3D_LOADED_SIGNAL = "e3d_loaded"; + + E3DModelInstance::E3DModelInstance() + : data_path(String(), [this] { _reload(); }) + , model_filename(String(), [this] { _reload(); }) + , skins(Array(), [this] { _reload(); }) + , exclude_node_names(Array(), [this] { _reload(); }) + , instancer(INSTANCER_NODES, [this] { _reload(); }) + , submodels_aabb(AABB(), [this] { _reload(); }) + , editable_in_editor(false, [this] { _reload(); }) + { + _mutex.instantiate(); + } + + E3DModelInstance::~E3DModelInstance() { + _mutex.unref(); + } + + E3DModelInstanceManager *E3DModelInstance::_get_manager() const { + return cast_to(Engine::get_singleton()->get_singleton("E3DModelInstanceManager")); + } + + void E3DModelInstance::_bind_methods() { + ADD_SIGNAL(MethodInfo(E3D_LOADED_SIGNAL)); + BIND_PROPERTY(Variant::VECTOR3, "default_aabb_size", "default_aabb_size", &E3DModelInstance::set_default_aabb_size, &E3DModelInstance::get_default_aabb_size, "default_aabb_size"); + BIND_PROPERTY(Variant::STRING, "data_path", "data_path", &E3DModelInstance::set_data_path, &E3DModelInstance::get_data_path, "data_path"); + BIND_PROPERTY(Variant::STRING, "model_filename", "model_filename", &E3DModelInstance::set_model_filename, &E3DModelInstance::get_model_filename, "model_filename"); + BIND_PROPERTY_ARRAY("skins", "skins", &E3DModelInstance::set_skins, &E3DModelInstance::get_skins, "skins"); + BIND_PROPERTY_ARRAY("exclude_node_names", "exclude_node_names", &E3DModelInstance::set_exclude_node_names, &E3DModelInstance::get_exclude_node_names, "exclude_node_names"); + BIND_PROPERTY_W_HINT(Variant::INT, "instancer", "instancer", &E3DModelInstance::set_instancer, &E3DModelInstance::get_instancer, "instancer", PROPERTY_HINT_ENUM, "Optimized,Nodes,Editable nodes"); + BIND_PROPERTY(Variant::AABB, "submodels_aabb", "submodels_aabb", &E3DModelInstance::set_submodels_aabb, &E3DModelInstance::get_submodels_aabb, "submodels_aabb"); + BIND_PROPERTY(Variant::BOOL, "editable_in_editor", "editable_in_editor", &E3DModelInstance::set_editable_in_editor, &E3DModelInstance::get_editable_in_editor, "editable_in_editor"); + + BIND_ENUM_CONSTANT(INSTANCER_NODES); + BIND_ENUM_CONSTANT(INSTANCER_EDITABLE_NODES); + BIND_ENUM_CONSTANT(INSTANCER_OPTIMIZED); + + ClassDB::bind_method(D_METHOD("_instantiate_children", "model"), &E3DModelInstance::_instantiate_children); + ClassDB::bind_method(D_METHOD("_deferred_reload"), &E3DModelInstance::_deferred_reload); + ClassDB::bind_method(D_METHOD("_flush_pending_model"), &E3DModelInstance::_flush_pending_model); + } + + void E3DModelInstance::_notification(const int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + UtilityFunctions::print_verbose("[E3DModelInstance] Entering tree"); + _reload(); + if (E3DModelInstanceManager *manager = _get_manager()) { + manager->register_instance(this); + } + } break; + + case NOTIFICATION_EXIT_TREE: { + UtilityFunctions::print_verbose("[E3DModelInstance] Exiting tree"); + if (E3DModelInstanceManager *manager = _get_manager()) { + manager->unregister_instance(this); + } + } break; + default: ; + } + } + + void E3DModelInstance::_reload() { + if (_is_dirty) { + return; + } + _is_dirty = true; + call_deferred("_deferred_reload"); + } + + void E3DModelInstance::_deferred_reload() { + _is_dirty = false; + if (const E3DModelInstanceManager *manager = _get_manager()) { + manager->reload_instance(this); + } + } + + void E3DModelInstance::_instantiate_children(const Ref &p_model) { + _mutex->lock(); + const bool is_scheduled = _pending_model_scheduled; + _pending_model = p_model; + _pending_model_scheduled = true; + _mutex->unlock(); + + if (is_scheduled) { + return; + } + + call_deferred("_flush_pending_model"); + } + + void E3DModelInstance::_flush_pending_model() { + _mutex->lock(); + const Ref model = _pending_model; + _pending_model_scheduled = false; + _mutex->unlock(); + + E3DNodesInstancer::instantiate(model, this, get_editable_in_editor()); + emit_signal(E3D_LOADED_SIGNAL); + } +}//namespace godot diff --git a/src/models/e3d/instance/E3DModelInstance.hpp b/src/models/e3d/instance/E3DModelInstance.hpp new file mode 100644 index 00000000..940d5c77 --- /dev/null +++ b/src/models/e3d/instance/E3DModelInstance.hpp @@ -0,0 +1,51 @@ +#pragma once +#include "./core/utils.hpp" +#include "E3DModelInstanceManager.hpp" +#include "macros.hpp" +#include "models/e3d/E3DModel.hpp" + +#include +#include + +namespace godot { + class E3DNodesInstancer; // forward declaration + class E3DModelInstance: public VisualInstance3D { + GDCLASS(E3DModelInstance, VisualInstance3D) + private: + void _reload(); + void _deferred_reload(); + void _flush_pending_model(); + E3DModelInstanceManager *_get_manager() const; + bool _is_dirty = false; + bool _pending_model_scheduled = false; + Ref _mutex; + Ref _pending_model; + protected: + static void _bind_methods(); + public: + static const char *E3D_LOADED_SIGNAL; + enum Instancer { + INSTANCER_OPTIMIZED, + INSTANCER_NODES, + INSTANCER_EDITABLE_NODES, + }; + + //@TODO: Maybe remove this? + void _notification(int p_what); + E3DModelInstance(); + ~E3DModelInstance() override; + MAKE_MEMBER_GS(Vector3, default_aabb_size, Vector3(1, 1, 1)) + // Observable values that trigger _reload() when changed + MAKE_MEMBER_OBSERVABLE_GS(String, data_path); + MAKE_MEMBER_OBSERVABLE_GS(String, model_filename); + MAKE_MEMBER_OBSERVABLE_GS(Array, skins); + MAKE_MEMBER_OBSERVABLE_GS(Array, exclude_node_names); + MAKE_MEMBER_OBSERVABLE_GS_NR(Instancer, instancer) + MAKE_MEMBER_OBSERVABLE_GS(AABB, submodels_aabb) + MAKE_MEMBER_OBSERVABLE_GS(bool, editable_in_editor) + + void _instantiate_children(const Ref &p_model); + }; +} //namespace godot + +VARIANT_ENUM_CAST(E3DModelInstance::Instancer) diff --git a/src/models/e3d/instance/E3DModelInstanceManager.cpp b/src/models/e3d/instance/E3DModelInstanceManager.cpp new file mode 100644 index 00000000..62ca1d9d --- /dev/null +++ b/src/models/e3d/instance/E3DModelInstanceManager.cpp @@ -0,0 +1,93 @@ +#include "E3DModelInstance.hpp" +#include "E3DModelInstanceManager.hpp" +#include "E3DNodesInstancer.hpp" +#include "models/e3d/E3DModelManager.hpp" + +#include +#include +#include +#include + +namespace godot { + const char *E3DModelInstanceManager::INSTANCES_RELOADED_SIGNAL = "instances_reloaded"; + void E3DModelInstanceManager::_bind_methods() { + ADD_SIGNAL(MethodInfo(INSTANCES_RELOADED_SIGNAL, PropertyInfo(Variant::OBJECT, "instance", PROPERTY_HINT_RESOURCE_TYPE, "E3DModelInstance"))); + ClassDB::bind_method(D_METHOD("reload_all"), &E3DModelInstanceManager::reload_all); + ClassDB::bind_method(D_METHOD("register_instance", "instance"), &E3DModelInstanceManager::register_instance); + ClassDB::bind_method(D_METHOD("unregister_instance", "instance"), &E3DModelInstanceManager::unregister_instance); + ClassDB::bind_method(D_METHOD("reload_instance", "instance"), &E3DModelInstanceManager::reload_instance); + } + + E3DModelInstanceManager::E3DModelInstanceManager() : model_manager(memnew(E3DModelManager)) {} + + E3DModelInstanceManager::~E3DModelInstanceManager() { + if (model_manager != nullptr) { + memdelete(model_manager); + model_manager = nullptr; + } + _instances.clear(); + } + + void E3DModelInstanceManager::reload_all() const { + for (E3DModelInstance *instance: _instances) { + if (instance != nullptr) { + reload_instance(instance); + } + } + } + + void E3DModelInstanceManager::register_instance(E3DModelInstance *instance) { + if (instance == nullptr) { + return; + } + + if (std::find(_instances.begin(), _instances.end(), instance) == _instances.end()) { + _instances.push_back(instance); + } + } + + void E3DModelInstanceManager::unregister_instance(E3DModelInstance *instance) { + if (instance == nullptr) { + return; + } + + if (const auto it = std::remove(_instances.begin(), _instances.end(), instance); it != _instances.end()) { + _instances.erase(it, _instances.end()); + } + } + + void E3DModelInstanceManager::reload_instance(E3DModelInstance *instance) const { + if (instance == nullptr) { + return; + } + + const SceneTree *tree = nullptr; + if (instance->is_inside_tree()) { + tree = instance->get_tree(); + } + + const Engine *singleton = Engine::get_singleton(); + + if (tree == nullptr) { + Object *ml = (singleton != nullptr) ? singleton->get_main_loop() : nullptr; + tree = cast_to(ml); + } + + if (tree == nullptr) { + UtilityFunctions::push_error("E3DModelInstanceManager::reload_instance: no SceneTree available"); + return; + } + + const Ref model = model_manager->load_model(instance->get_data_path(), instance->get_model_filename()); + + switch (instance->get_instancer()) { + case E3DModelInstance::INSTANCER_NODES: { + UtilityFunctions::print_verbose("[E3DModelInstanceManager] Instantiating nodes for ", instance->get_name()); + instance->call_deferred("_instantiate_children", model); + } break; + default: + UtilityFunctions::push_warning("[E3DModelInstanceManager] Unsupported instancer type for ", instance->get_name()); + break; + } + } +} // namespace godot diff --git a/src/models/e3d/instance/E3DModelInstanceManager.hpp b/src/models/e3d/instance/E3DModelInstanceManager.hpp new file mode 100644 index 00000000..64161e37 --- /dev/null +++ b/src/models/e3d/instance/E3DModelInstanceManager.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "models/e3d/E3DModelManager.hpp" + +#include +#include +#include + +namespace godot { + class E3DModelInstance; // forward declaration + class E3DModelInstanceManager: public Node { + GDCLASS(E3DModelInstanceManager, Node) + protected: + static void _bind_methods(); + E3DModelManager *model_manager; + std::vector _instances; + public: + E3DModelInstanceManager(); + ~E3DModelInstanceManager() override; + static const char *INSTANCES_RELOADED_SIGNAL; + void reload_all() const; + void register_instance(E3DModelInstance *instance); + void unregister_instance(E3DModelInstance *instance); + void reload_instance(E3DModelInstance *instance) const; + }; +} //namespace godot diff --git a/src/models/e3d/instance/E3DNodesInstancer.cpp b/src/models/e3d/instance/E3DNodesInstancer.cpp new file mode 100644 index 00000000..90c88e88 --- /dev/null +++ b/src/models/e3d/instance/E3DNodesInstancer.cpp @@ -0,0 +1,167 @@ +#include "E3DNodesInstancer.hpp" +#include +#include +#include + +#include "models/MaterialManager.hpp" +#include +#include +#include + +namespace godot { + Ref E3DNodesInstancer::_colored_material; + Node3D * E3DNodesInstancer::_create_submodel_instance(const E3DModelInstance &p_target_node, const E3DSubModel &submodel) { + if (submodel.get_skip_rendering()) { + return nullptr; + } + + switch (submodel.get_submodel_type()) { + case E3DSubModel::TRANSFORM: { + Node3D *obj = memnew(Node3D); + obj->set_name(StringName(submodel.get_name())); + obj->set_visible(submodel.get_visible()); + return obj; + } + + case E3DSubModel::GL_TRIANGLES: { + if (const bool is_name_excluded = p_target_node.get_exclude_node_names().has(submodel.get_name()); !is_name_excluded) { + MeshInstance3D *mesh_instance = memnew(MeshInstance3D); + mesh_instance->set_name(StringName(submodel.get_name())); + mesh_instance->set_mesh(submodel.get_mesh()); + mesh_instance->set_visibility_range_begin(submodel.get_visibility_range_begin()); + mesh_instance->set_visibility_range_end(submodel.get_visibility_range_end()); + mesh_instance->set_visible(submodel.get_visible()); + _update_submodel_material(p_target_node, *mesh_instance, submodel); + return mesh_instance; + } + } + default:; + } + return nullptr; + } + + void E3DNodesInstancer::_do_add_submodels( + const E3DModelInstance &p_target_node, Node3D *parent, const TypedArray &submodels, + const bool editable) { + if (parent == nullptr) { + return; + } + for (const Variant &i: submodels) { + Ref submodel_ref = i; + + const E3DSubModel *submodel = submodel_ref.ptr(); + if (submodel == nullptr) { + continue; + } + + Node3D *child = _create_submodel_instance(p_target_node, *submodel); + if (child == nullptr) { + continue; + } + + const InternalMode internal = editable ? Node::INTERNAL_MODE_DISABLED : Node::INTERNAL_MODE_BACK; + parent->add_child(child, false, internal); + + // Apply transform AFTER adding to the tree (important especially on Windows) + child->set_transform(submodel->get_transform()); + + if ((Engine::get_singleton() != nullptr) && Engine::get_singleton()->is_editor_hint()) { + if (Node *owner = editable ? p_target_node.get_owner() : nullptr; + owner != nullptr) { + child->set_owner(owner); + } + } + + if (TypedArray children = submodel->get_submodels(); children.size() > 0) { + _do_add_submodels(p_target_node, child, children, editable); + } + } + } + + void E3DNodesInstancer::_update_submodel_material( + const E3DModelInstance &p_target_node, Node3D &subnode, const E3DSubModel &submodel) { + const String unprefixed_model_path = String("/").join(p_target_node.get_data_path().split("/").slice(1)); + MeshInstance3D *mesh_instance = cast_to(&subnode); + if (mesh_instance == nullptr) { + return; + } + + MaterialManager *mm = cast_to(Engine::get_singleton()->get_singleton("MaterialManager")); + if (mm == nullptr) { + return; + } + + String material_name = submodel.get_material_name(); + if (submodel.get_dynamic_material()) { + const Array skins = p_target_node.get_skins(); + if (const int idx = submodel.get_dynamic_material_index(); skins.size() > idx) { + material_name = skins.get(idx); + } else { + UtilityFunctions::push_warning( "Model " + p_target_node.get_name() + " has less skins than dynamic material index " + String::num_int64(idx)); + } + } + + MaterialManager::Transparency transparency = MaterialManager::Disabled; + if (submodel.get_material_transparent()) { + transparency = MaterialManager::Alpha; + } + + Ref mat; + if (submodel.get_material_colored()) { + mat = get_colored_material(); + if (mat.is_valid()) { + const Ref sm = mat; + sm->set_albedo(submodel.get_diffuse_color()); + } + } else { + mat = mm->get_material(unprefixed_model_path, material_name, transparency, false, submodel.get_diffuse_color()); + } + + if (mat.is_valid()) { + mesh_instance->set_surface_override_material(0, mat); + } + } + + void E3DNodesInstancer::_bind_methods() { + ClassDB::bind_static_method( + get_class_static(), D_METHOD("instantiate", "model", "target_node", "editable"), + &E3DNodesInstancer::instantiate); + } + + void E3DNodesInstancer::cleanup() { + _colored_material.unref(); + } + + Ref E3DNodesInstancer::get_colored_material() { + if (!_colored_material.is_valid()) { + if ((Engine::get_singleton() != nullptr) && !Engine::get_singleton()->is_editor_hint()) { + const String path = "res://addons/libmaszyna/e3d/colored.material"; + if (const Ref res = ResourceLoader::get_singleton()->load(path, "Material", ResourceLoader::CACHE_MODE_REUSE); res.is_valid()) { + _colored_material = res; + } + } + } + return _colored_material; + } + + void E3DNodesInstancer::instantiate(const Ref &p_model, E3DModelInstance *p_target_node, const bool editable) { + if (p_target_node == nullptr) { + return; + } + + const int32_t child_count = p_target_node->get_child_count(true); + for (int32_t i = child_count - 1; i >= 0; --i) { + Node *child = p_target_node->get_child(i, true); + if (child == nullptr) { + continue; + } + + p_target_node->remove_child(child); + child->queue_free(); + } + + if (p_model.is_valid()) { + _do_add_submodels(*p_target_node, p_target_node, p_model->get_submodels(), editable); + } + } +} // namespace godot diff --git a/src/models/e3d/instance/E3DNodesInstancer.hpp b/src/models/e3d/instance/E3DNodesInstancer.hpp new file mode 100644 index 00000000..d5e12b8c --- /dev/null +++ b/src/models/e3d/instance/E3DNodesInstancer.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "models/e3d/instance/E3DModelInstance.hpp" +#include "models/e3d/E3DModel.hpp" +#include +#include +#include + +namespace godot { + class E3DNodesInstancer : public Node { + GDCLASS(E3DNodesInstancer, Node) + private: + static Ref _colored_material; + static Node3D *_create_submodel_instance(const E3DModelInstance &p_target_node, const E3DSubModel &submodel); + static void _do_add_submodels( + const E3DModelInstance &p_target_node, Node3D *parent, const TypedArray &submodels, + bool editable); + static void _update_submodel_material(const E3DModelInstance &p_target_node, Node3D &subnode, const E3DSubModel &submodel); + protected: + static void _bind_methods(); + public: + static void cleanup(); + static Ref get_colored_material(); + static void instantiate(const Ref &p_model, E3DModelInstance *p_target_node, bool editable = false); + }; +} //namespace godot diff --git a/src/parsers/e3d_parser.cpp b/src/parsers/e3d_parser.cpp new file mode 100644 index 00000000..3717a466 --- /dev/null +++ b/src/parsers/e3d_parser.cpp @@ -0,0 +1,473 @@ +#include "parsers/e3d_parser.hpp" +#include +#include + +#include +#include +#include + +namespace godot { + void E3DParser::_bind_methods() { + ClassDB::bind_method(D_METHOD("parse", "file"), &E3DParser::parse); + } + + E3DParser::ChunkHeader E3DParser::_read_chunk_header(const Ref &p_file) { + if (!p_file.is_valid()) { + return {}; + } + const String chunk_id = p_file->get_buffer(4).get_string_from_ascii(); + const uint32_t chunk_len = p_file->get_32(); + return {chunk_id, chunk_len, chunk_len - 8}; + } + + int E3DParser::u32s(const uint32_t value) const { + return static_cast((static_cast(value) + MAX_31B) % MAX_32B - MAX_31B); // NOLINT(*-math-missing-parentheses) + } + + PackedVector3Array + E3DParser::_calculate_normals(const PackedVector3Array &vertices, const PackedInt32Array &indices) { + PackedVector3Array normals; + const bool has_indices = indices.size() > 0; + for (int i = 0; i < vertices.size(); i++) { + normals.append(Vector3(0, 0, 0)); + } + + for (int i = 0; i < indices.size(); i += 3) { + Indices _indices; + Vertices _vertices; + Edges _edges; + if (has_indices) { + _indices.i1 = indices.get(i); + _indices.i2 = indices.get(i + 1); + _indices.i3 = indices.get(i + 2); + } else { + _indices.i1 = i; + _indices.i2 = i + 1; + _indices.i3 = i + 2; + } + + _vertices.v1 = vertices.get(_indices.i1); + _vertices.v2 = vertices.get(_indices.i2); + _vertices.v3 = vertices.get(_indices.i3); + + _edges.e1 = _vertices.v2 - _vertices.v1; + _edges.e2 = _vertices.v3 - _vertices.v1; + + const Vector3 normal = _edges.e1.cross(_edges.e2).normalized(); + + normals.set(_indices.i1, normals.get(_indices.i1) + normal); + normals.set(_indices.i2, normals.get(_indices.i2) + normal); + normals.set(_indices.i3, normals.get(_indices.i3) + normal); + } + + for (int i = 0; i < normals.size(); i++) { + normals.set(i, normals.get(i).normalized()); + } + + return normals; + } + + E3DParser::SubModelData E3DParser::_read_submodel(const Ref &p_file, const int chunk_size) const { + SubModelData result; + result.next_idx = u32s(p_file->get_32()); + result.first_child_idx = u32s(p_file->get_32()); + result.type = p_file->get_32(); + result.name_idx = u32s(p_file->get_32()); + result.anim = u32s(p_file->get_32()); + result.flags = p_file->get_32() & 0xFFFF; + result.matrix_idx = u32s(p_file->get_32()); + result.vertex_count = u32s(p_file->get_32()); + result.first_vertex_idx = u32s(p_file->get_32()); + result.material_idx = u32s(p_file->get_32()); + result.is_material_colored = (result.material_idx == 0); + result.lights_on_threshold = p_file->get_float(); + result.visibility_light_threshold = p_file->get_float(); + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_buffer(16); // skip unused RGBA ambient + Color diffuse_color = Color(p_file->get_float(), p_file->get_float(), p_file->get_float(), 1.0); + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_float(); // skip unused alpha + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_buffer(16); // skip unused RGBA specular + Color selfillum_color = Color(p_file->get_float(), p_file->get_float(), p_file->get_float(), 1.0); + + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_float(); // skip unused alpha + if (const auto _transparent = result.flags & 32; _transparent == 0u) { + selfillum_color.a = 1.0; + diffuse_color.a = 1.0; + } + + result.selfillum_color = selfillum_color; + result.diffuse_color = diffuse_color; + result.gl_lines_size = p_file->get_float(); + + result.lod_max_distance = p_file->get_float(); + result.lod_min_distance = p_file->get_float(); + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_buffer(32); // skip attrs for lighting + result.index_count = p_file->get_32(); + result.first_index_idx = p_file->get_32(); + result.transparent = result.flags & 0b000001; + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_buffer(chunk_size - 164); // read to the end of the chunk + result.vertices = PackedVector3Array(); + result.normals = PackedVector3Array(); + result.uvs = PackedVector2Array(); + result.indices = PackedInt32Array(); + return result; + } + + std::vector E3DParser::_parse_file(const Ref &p_file) const { + ChunkHeader chunk_header = _read_chunk_header(p_file); + std::vector submodels; + TypedArray submodel_names; + TypedArray material_names; + TypedArray matrices; + while (!p_file->eof_reached()) { + const ChunkHeader chunk = _read_chunk_header(p_file); + if (chunk.id == "SUB0") { + const int submodels_count = static_cast(chunk.data_len) / 256; + for (int i = 0; i < submodels_count; i++) { + submodels.emplace_back(_read_submodel(p_file, 256)); + } + } else if (chunk.id == "SUB1") { + const int submodels_count = static_cast(chunk.data_len) / 320; + for (int i = 0; i < submodels_count; i++) { + submodels.emplace_back(_read_submodel(p_file, 320)); + } + } else if (chunk.id == "NAM0") { + submodel_names = _buffer_to_strings(p_file->get_buffer(chunk.data_len)); + } else if (chunk.id == "TEX0") { + material_names = _buffer_to_strings(p_file->get_buffer(chunk.data_len)); + } else if (chunk.id == "TRA0") { + const int matrix_count = static_cast(chunk.data_len) / 64; + for (int i = 0; i < matrix_count; i++) { + matrices.append(_read_matrix(p_file)); + } + } else if (chunk.id == "IDX1") { + const uint64_t pos = p_file->get_position(); + for (SubModelData &submodel: submodels) { + if (submodel.index_count <= 0) { + continue; + } + p_file->seek(pos + submodel.first_index_idx); + PackedInt32Array indices; + for (int j = 0; j < submodel.index_count; j++) { + indices.append(p_file->get_8()); + } + + submodel.indices.append_array(indices); + } + + p_file->seek(pos + chunk.data_len); + } else if (chunk.id == "IDX2") { + const uint64_t pos = p_file->get_position(); + for (SubModelData &submodel: submodels) { + p_file->seek(pos + (static_cast(submodel.first_index_idx) * 2)); + PackedInt32Array indices; + for (int j = 0; j < submodel.index_count; j++) { + indices.append(p_file->get_16()); + } + + submodel.indices.append_array(indices); + } + + p_file->seek(pos + chunk.data_len); + } else if (chunk.id == "IDX4") { + const uint64_t pos = p_file->get_position(); + for (SubModelData &submodel: submodels) { + p_file->seek(pos + (static_cast(submodel.first_index_idx) * 4)); + PackedInt32Array indices; + for (int j = 0; j < submodel.index_count; j++) { + indices.append(p_file->get_32()); + } + + submodel.indices.append_array(indices); + } + + p_file->seek(pos + chunk.data_len); + } else if (chunk.id == "VNT2") { + const uint64_t pos = p_file->get_position(); + for (SubModelData &submodel: submodels) { + p_file->seek(pos + (static_cast(submodel.first_vertex_idx) * 48)); + + PackedVector3Array vertices; + PackedVector3Array normals; + PackedVector2Array uvs; + PackedFloat64Array tangents; + + for (int j = 0; j < submodel.vertex_count; ++j) { + const float x = p_file->get_float(); + const float y = p_file->get_float(); + const float z = p_file->get_float(); + const float nx = p_file->get_float(); + const float ny = p_file->get_float(); + const float nz = p_file->get_float(); + const float u = p_file->get_float(); + const float v = p_file->get_float(); + + const float tx = p_file->get_float(); + const float ty = p_file->get_float(); + const float tz = p_file->get_float(); + const float tw = p_file->get_float(); + + Vector3 vertice(x, y, z); + Vector3 normal(nx, ny, nz); + Vector2 uv(u, v); + + vertices.append(vertice); + normals.append(normal); + uvs.append(uv); + tangents.push_back(tx); + tangents.push_back(ty); + tangents.push_back(tz); + tangents.push_back(tw); + } + submodel.vertices = vertices; + submodel.normals = normals; + submodel.uvs = uvs; + submodel.tangents = tangents; + } + p_file->seek(pos + chunk.data_len); + } else { + if (!chunk.id.is_empty()) { + UtilityFunctions::push_warning("Skipping unsupported chunk: " + chunk.id); + } + + if (chunk.data_len > 0) { + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_buffer(chunk.data_len); + } + } + } + + for (SubModelData& submodel : submodels) { + //might be better to use operator[] + + if (submodel.name_idx >= 0 && submodel.name_idx < submodel_names.size()) { + submodel.name = String(submodel_names.get(submodel.name_idx)); + } + + if (submodel.material_idx >= 0 && submodel.material_idx < material_names.size()) { + submodel.material = String(material_names.get(submodel.material_idx)); + } + + if (submodel.matrix_idx >= 0 && submodel.matrix_idx < matrices.size()) { + submodel.matrix = Transform3D(matrices.get(submodel.matrix_idx)); + } + } + + return submodels; + } + + TypedArray E3DParser::_buffer_to_strings(const PackedByteArray &p_buffer) { + TypedArray output; + PackedStringArray tmp; + for (const unsigned char i: p_buffer) { + if (i == 0) { + output.append(String("").join(tmp)); + tmp.clear(); + } else { + tmp.append(String::chr(i)); + } + } + + return output; + } + + Transform3D E3DParser::_read_matrix(const Ref &p_file) { + float m[16]; + for (float &row: m) { + row = p_file->get_float(); + } + + return Transform3D( + Basis( + Vector3(m[0], m[1], m[2]), + Vector3(m[4], m[5], m[6]), + Vector3(m[8], m[9], m[10]) + ), + Vector3(m[12], m[13], m[14]) + ); + } + + Ref E3DParser::_create_submodel(SubModelData &p_submodel) { + UtilityFunctions::print_verbose("[E3DParser] Creating submodel from data: " + p_submodel.name); + Ref submodel; + submodel.instantiate(); + + const std::unordered_map typeMap = { + {0, E3DSubModel::SubModelType::GL_POINTS}, {1, E3DSubModel::SubModelType::GL_LINES}, + {2, E3DSubModel::SubModelType::GL_LINE_STRIP}, {3, E3DSubModel::SubModelType::GL_LINE_LOOP}, + {4, E3DSubModel::SubModelType::GL_TRIANGLES}, {5, E3DSubModel::SubModelType::GL_TRIANGLE_STRIP}, + {6, E3DSubModel::SubModelType::GL_TRIANGLE_FAN}, {7, E3DSubModel::SubModelType::GL_QUADS}, + {8, E3DSubModel::SubModelType::GL_QUAD_STRIP}, {9, E3DSubModel::SubModelType::GL_POLYGON}, + {256, E3DSubModel::SubModelType::TRANSFORM}, {257, E3DSubModel::SubModelType::FREE_SPOTLIGHT}, + {258, E3DSubModel::SubModelType::STARS}}; + + const std::unordered_map animMap = { + {1, E3DSubModel::AnimationType::NONE}, {2, E3DSubModel::AnimationType::ROTATE_VEC}, + {3, E3DSubModel::AnimationType::ROTATE_XYZ}, {4, E3DSubModel::AnimationType::MOVE}, + {5, E3DSubModel::AnimationType::JUMP_SECONDS}, {6, E3DSubModel::AnimationType::JUMP_MINUTES}, + {5, E3DSubModel::AnimationType::JUMP_HOURS}, {6, E3DSubModel::AnimationType::JUMP_HOURS24}, + {7, E3DSubModel::AnimationType::SECONDS}, {8, E3DSubModel::AnimationType::MINUTES}, + {9, E3DSubModel::AnimationType::HOURS}, {10, E3DSubModel::AnimationType::HOURS24}, + {11, E3DSubModel::AnimationType::BILLBOARD}, {12, E3DSubModel::AnimationType::WIND}, + {13, E3DSubModel::AnimationType::SKY}, {14, E3DSubModel::AnimationType::DIGITAL}, + {15, E3DSubModel::AnimationType::DIGICLK}, {16, E3DSubModel::AnimationType::UNDEFINED}, + {256, E3DSubModel::AnimationType::IK}, {257, E3DSubModel::AnimationType::IK1}, + {258, E3DSubModel::AnimationType::IK2}, {-1, E3DSubModel::AnimationType::UNKNOWN}}; + + if (const std::unordered_map::const_iterator type_it = + typeMap.find(static_cast(p_submodel.type)); + type_it != typeMap.end()) { + submodel->set_submodel_type(type_it->second); + } else { + UtilityFunctions::push_warning("Unknown submodel type: " + String::num_int64(p_submodel.type)); + submodel->set_submodel_type(E3DSubModel::SubModelType::GL_TRIANGLES); + } + + submodel->set_visible(true); + submodel->set_skip_rendering(false); + const String _submodel_name = p_submodel.name; + if (!_submodel_name.is_empty()) { + submodel->set_name(_submodel_name); + + if (_submodel_name.begins_with("Light_On")) { + submodel->set_visible(false); + } else if (_submodel_name.to_lower().ends_with("_on")) { + submodel->set_visible(false); + } else if (_submodel_name.to_lower().ends_with("_xon")) { + submodel->set_visible(false); + } else if (_submodel_name == "cien") { + submodel->set_visible(false); + submodel->set_skip_rendering(true); + } + } + + switch (p_submodel.type) { + case E3DSubModel::SubModelType::TRANSFORM: + if (_submodel_name.is_empty()) { + submodel->set_name("banan"); + } + + submodel->set_transform(p_submodel.matrix); + return submodel; + case E3DSubModel::SubModelType::GL_TRIANGLES: { + const int64_t vertices_count = p_submodel.vertices.size(); + const String _mat_name = p_submodel.material != "" ? p_submodel.material.split(":").get(0) : ""; + if (const std::unordered_map::const_iterator anim_it = + animMap.find(p_submodel.anim); + anim_it != animMap.end()) { + submodel->set_animation(anim_it->second); + } else { + submodel->set_animation(E3DSubModel::AnimationType::NONE); + } + + if (p_submodel.material_idx < 0) { + submodel->set_dynamic_material(true); + submodel->set_dynamic_material_index(abs(p_submodel.material_idx) - 1); + } + + submodel->set_material_name(_mat_name); + submodel->set_material_transparent((p_submodel.flags & (1 << 5)) != 0); + submodel->set_material_colored(p_submodel.is_material_colored); + submodel->set_visibility_range_begin(std::sqrt(p_submodel.lod_min_distance)); + submodel->set_visibility_range_end(std::sqrt(p_submodel.lod_max_distance)); + submodel->set_visibility_light(p_submodel.visibility_light_threshold); + submodel->set_lights_on_threshold(p_submodel.lights_on_threshold); + submodel->set_diffuse_color(p_submodel.diffuse_color); + submodel->set_self_illumination(p_submodel.selfillum_color); + + if (vertices_count > 0) { + Ref mesh; + mesh.instantiate(); + Array triangles; + triangles.resize(ArrayMesh::ARRAY_MAX); + triangles.set(ArrayMesh::ARRAY_VERTEX, p_submodel.vertices); + const PackedInt32Array _indices = p_submodel.indices; + PackedInt32Array ccw_indices; + for (int i = 0; i < _indices.size(); i += 3) { + int32_t _i1 = static_cast(_indices.get(i)); + int32_t _i2 = static_cast(_indices.get(i + 1)); + int32_t _i3 = static_cast(_indices.get(i + 2)); + ccw_indices.append_array(PackedInt32Array({_i1, _i3, _i2})); + } + + p_submodel.indices = ccw_indices; + if (p_submodel.normals.is_empty()) { + p_submodel.normals = _calculate_normals(p_submodel.vertices, p_submodel.indices); + } + + if (p_submodel.indices.size() > 0) { + triangles.set(ArrayMesh::ARRAY_INDEX, p_submodel.indices); + } + + if (p_submodel.normals.size() > 0) { + triangles.set(ArrayMesh::ARRAY_NORMAL, p_submodel.normals); + } + + if (p_submodel.tangents.size() > 0) { + triangles.set(ArrayMesh::ARRAY_TANGENT, p_submodel.tangents); + } + + triangles.set(ArrayMesh::ARRAY_TEX_UV, p_submodel.uvs); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, triangles); + submodel->set_mesh(mesh); + } + + submodel->set_transform(p_submodel.matrix); + return submodel; + } + default: + UtilityFunctions::push_error("Unsupported submodel (name: " + p_submodel.name + ", type: " + String::num(p_submodel.type) + ")"); //@TODO Display type name based on p_sumbodel.type + return submodel; + } + } + + Ref E3DParser::parse(const Ref &p_file) const { + UtilityFunctions::print_verbose("[E3DParser] Parsing " + p_file->get_path()); + // Build a list of submodels first using a simple vector to avoid Variant conversions. + std::vector> submodels; + std::vector submodels_meta = _parse_file(p_file); + submodels.reserve(submodels_meta.size()); + + for (SubModelData &i: submodels_meta) { + submodels.push_back(_create_submodel(i)); + } + + // Track parentage to avoid duplication in E3DModel + std::vector has_parent(submodels.size(), false); + + // Apply parent/child relationships using references to actual stored elements + for (size_t i = 0; i < submodels_meta.size(); i++) { + const SubModelData &meta = submodels_meta.at(i); + const Ref &parent = submodels.at(i); + + if (meta.first_child_idx > -1 && static_cast(meta.first_child_idx) < submodels.size()) { + int child_idx = meta.first_child_idx; + while (child_idx > -1 && static_cast(child_idx) < submodels.size()) { + const Ref &child = submodels.at(child_idx); + child->set_parent(parent.ptr()); + if (child_idx >= 0 && static_cast(child_idx) < has_parent.size()) { + has_parent.at(child_idx) = true; + } + child_idx = submodels_meta.at(child_idx).next_idx; + } + } + } + + // Create the model and add only root-level submodels + Ref model; + UtilityFunctions::print_verbose("[E3DParser] Creating model instance for " + p_file->get_path()); + model.instantiate(); + for (size_t i = 0; i < submodels.size(); i++) { + if (!has_parent.at(i)) { + model->add_child(submodels.at(i)); + } + } + + return model; + } +} // namespace godot diff --git a/src/parsers/e3d_parser.hpp b/src/parsers/e3d_parser.hpp new file mode 100644 index 00000000..40dcbdd3 --- /dev/null +++ b/src/parsers/e3d_parser.hpp @@ -0,0 +1,88 @@ +#pragma once +#include "models/e3d/E3DModel.hpp" +#include +#include +#include + +#include + +namespace godot { + class E3DParser : public Node { + GDCLASS(E3DParser, Node) + + public: + Ref parse(const Ref &p_file) const; + + protected: + static void _bind_methods(); + + private: + const int64_t MAX_31B = 1LL << 31; + const int64_t MAX_32B = 1LL << 32; + + struct ChunkHeader { + String id; + uint32_t len; + uint32_t data_len; + }; + + struct Indices { + uint32_t i1; + uint32_t i2; + uint32_t i3; + }; + + struct Vertices { + Vector3 v1; + Vector3 v2; + Vector3 v3; + }; + + struct Edges { + Vector3 e1; + Vector3 e2; + }; + + struct SubModelData { + int next_idx; + int first_child_idx; + uint32_t type; + int name_idx; + String name; + int anim; + uint32_t flags; + int matrix_idx; + String material; + int vertex_count; + int first_vertex_idx; + int material_idx; + bool is_material_colored; + float lights_on_threshold; + float visibility_light_threshold; + Color diffuse_color; + Color selfillum_color; + float gl_lines_size; + float lod_max_distance; + float lod_min_distance; + uint32_t index_count; + uint32_t first_index_idx; + uint32_t transparent; + PackedVector3Array vertices; + PackedVector3Array normals; + PackedVector2Array uvs; + PackedInt32Array indices; + PackedFloat64Array tangents; + Transform3D matrix; + }; + + static ChunkHeader _read_chunk_header(const Ref &p_file); + int u32s(uint32_t value) const; + static PackedVector3Array + _calculate_normals(const PackedVector3Array &vertices, const PackedInt32Array &indices); + SubModelData _read_submodel(const Ref &p_file, int chunk_size) const; + std::vector _parse_file(const Ref &p_file) const; + static TypedArray _buffer_to_strings(const PackedByteArray &p_buffer); + static Transform3D _read_matrix(const Ref &p_file); + static Ref _create_submodel(SubModelData &p_submodel); + }; +} // namespace godot diff --git a/src/parsers/maszyna_parser.cpp b/src/parsers/maszyna_parser.cpp index 36896006..35a3aab1 100644 --- a/src/parsers/maszyna_parser.cpp +++ b/src/parsers/maszyna_parser.cpp @@ -25,10 +25,18 @@ namespace godot { meta.emplace_back(); } - void MaszynaParser::initialize(const PackedByteArray &buffer) { + MaszynaParser::~MaszynaParser() { + handlers.clear(); + parameters.clear(); + meta.clear(); + buffer.clear(); + mutex.unref(); + } + + void MaszynaParser::initialize(const PackedByteArray &p_buffer) { mutex->lock(); - this->buffer = buffer; - length = static_cast(buffer.size()); + this->buffer = p_buffer; + length = static_cast(p_buffer.size()); cursor = 0; mutex->unlock(); } diff --git a/src/parsers/maszyna_parser.hpp b/src/parsers/maszyna_parser.hpp index 6e457341..4a15999e 100644 --- a/src/parsers/maszyna_parser.hpp +++ b/src/parsers/maszyna_parser.hpp @@ -27,7 +27,8 @@ namespace godot { public: MaszynaParser(); - void initialize(const PackedByteArray &buffer); + ~MaszynaParser() override; + void initialize(const PackedByteArray &p_buffer); int64_t get8(); String get_line(); bool eof_reached() const; diff --git a/src/register_types.cpp b/src/register_types.cpp index 486c1f0a..4cb2d2dd 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -4,6 +4,7 @@ #include "buffers/TrainBuffCoupl.hpp" #include "core/GameLog.hpp" #include "core/GenericTrainPart.hpp" +#include "core/ResourceCache.hpp" #include "core/TrainController.hpp" #include "core/TrainPart.hpp" #include "core/TrainSystem.hpp" @@ -15,6 +16,8 @@ #include "engines/TrainEngine.hpp" #include "lighting/TrainLighting.hpp" #include "load/TrainLoad.hpp" +#include "models/MaterialManager.hpp" +#include "models/MaterialParser.hpp" #include "parsers/maszyna_parser.hpp" #include "register_types.h" #include "resources/engines/MotorParameter.hpp" @@ -22,20 +25,24 @@ #include "resources/lighting/LightListItem.hpp" #include "resources/load/LoadListItem.hpp" #include "resources/material/MaszynaMaterial.hpp" -#include "models/MaterialManager.hpp" -#include "models/MaterialParser.hpp" #include "systems/TrainSecuritySystem.hpp" #include #include -#include #include +#include #include +#include +#include +#include using namespace godot; TrainSystem *train_system_singleton = nullptr; +ResourceCache *resource_cache_singleton = nullptr; GameLog *game_log_singleton = nullptr; MaterialManager* material_manager_singleton = nullptr; +Ref e3d_loader_singleton; +E3DModelInstanceManager *e3d_model_instance_manager_singleton = nullptr; static bool is_doctool_mode() { const PackedStringArray args = OS::get_singleton()->get_cmdline_args(); @@ -82,14 +89,33 @@ void initialize_libmaszyna_module(const ModuleInitializationLevel p_level) { GDREGISTER_CLASS(LoadListItem) GDREGISTER_CLASS(TrainBuffCoupl) + // E3D + GDREGISTER_CLASS(E3DModel); + GDREGISTER_CLASS(E3DSubModel); + GDREGISTER_CLASS(E3DModelManager); + GDREGISTER_CLASS(E3DModelInstanceManager); + GDREGISTER_CLASS(E3DModelInstance); + GDREGISTER_CLASS(E3DNodesInstancer); + GDREGISTER_CLASS(E3DParser); + GDREGISTER_CLASS(E3DResourceFormatLoader); + + // Core + GDREGISTER_CLASS(ResourceCache) + if (!is_doctool_mode()) { train_system_singleton = memnew(TrainSystem); + resource_cache_singleton = memnew(ResourceCache); game_log_singleton = memnew(GameLog); material_manager_singleton = memnew(MaterialManager); + e3d_model_instance_manager_singleton = memnew(E3DModelInstanceManager); + e3d_loader_singleton.instantiate(); Engine::get_singleton()->register_singleton("TrainSystem", train_system_singleton); + Engine::get_singleton()->register_singleton("ResourceCache", resource_cache_singleton); Engine::get_singleton()->register_singleton("GameLog", game_log_singleton); Engine::get_singleton()->register_singleton("MaterialManager", material_manager_singleton); + Engine::get_singleton()->register_singleton("E3DModelInstanceManager", e3d_model_instance_manager_singleton); + ResourceLoader::get_singleton()->add_resource_format_loader(e3d_loader_singleton); } } } @@ -100,20 +126,59 @@ void uninitialize_libmaszyna_module(const ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } - - if (!is_doctool_mode()) { + if (Engine *_singleton = Engine::get_singleton(); !is_doctool_mode() && _singleton != nullptr) { if (train_system_singleton != nullptr) { - Engine::get_singleton()->unregister_singleton("TrainSystem"); + _singleton->unregister_singleton("TrainSystem"); + } + + if (resource_cache_singleton != nullptr) { + _singleton->unregister_singleton("ResourceCache"); } if (game_log_singleton != nullptr) { - Engine::get_singleton()->unregister_singleton("GameLog"); + _singleton->unregister_singleton("GameLog"); } if (material_manager_singleton != nullptr) { - Engine::get_singleton()->unregister_singleton("MaterialManager"); + _singleton->unregister_singleton("MaterialManager"); + } + + if (_singleton->has_singleton("E3DModelInstanceManager")) { + _singleton->unregister_singleton("E3DModelInstanceManager"); } } + + if (train_system_singleton != nullptr) { + memdelete(train_system_singleton); + train_system_singleton = nullptr; + } + + if (resource_cache_singleton != nullptr) { + memdelete(resource_cache_singleton); + resource_cache_singleton = nullptr; + } + + if (game_log_singleton != nullptr) { + memdelete(game_log_singleton); + game_log_singleton = nullptr; + } + + if (material_manager_singleton != nullptr) { + memdelete(material_manager_singleton); + material_manager_singleton = nullptr; + } + + if (e3d_model_instance_manager_singleton != nullptr) { + memdelete(e3d_model_instance_manager_singleton); + e3d_model_instance_manager_singleton = nullptr; + } + + if (e3d_loader_singleton.is_valid()) { + ResourceLoader::get_singleton()->remove_resource_format_loader(e3d_loader_singleton); + e3d_loader_singleton.unref(); + } + + E3DNodesInstancer::cleanup(); } extern "C" { diff --git a/src/resources/material/MaszynaMaterial.cpp b/src/resources/material/MaszynaMaterial.cpp index 05ee6413..af7e75e3 100644 --- a/src/resources/material/MaszynaMaterial.cpp +++ b/src/resources/material/MaszynaMaterial.cpp @@ -51,4 +51,4 @@ namespace godot { return _tex; } -} \ No newline at end of file +} //namespace godot \ No newline at end of file diff --git a/src/systems/TrainSecuritySystem.cpp b/src/systems/TrainSecuritySystem.cpp index f004ebfa..1e886bba 100644 --- a/src/systems/TrainSecuritySystem.cpp +++ b/src/systems/TrainSecuritySystem.cpp @@ -91,7 +91,7 @@ namespace godot { void TrainSecuritySystem::security_acknowledge(const bool p_enabled) { TMoverParameters *mover = get_mover(); ASSERT_MOVER(mover); - if (enabled) { + if (p_enabled) { mover->SecuritySystem.acknowledge_press(); } else { mover->SecuritySystem.acknowledge_release();