From 57df54d82d8278be9b8e33cfdf6994d2a788a004 Mon Sep 17 00:00:00 2001 From: Voyager Date: Sat, 15 Aug 2026 09:53:30 -0400 Subject: [PATCH] Improve PortMaster performance and controls --- docs/PORTMASTER.md | 26 +++- main/main.gd | 72 ++++++--- scripts/build_portmaster.sh | 21 ++- scripts/portmaster/NETfishing.sh | 6 + scripts/portmaster/port.json | 2 +- tests/art_tools_validation.gd | 7 +- tests/controller_mapping_validation.gd | 5 + tests/light_performance_profile_validation.gd | 147 +++++++++++++++++- ui/game_ui.gd | 24 --- ui/player_menu_pattern.gdshader | 6 +- ui/quick_radial_menu.gd | 16 +- ui/title_water_background.gdshader | 82 +++++----- world/regions/starter_island_region.gd | 36 ++++- world/world_time_visual_controller.gd | 4 +- 14 files changed, 328 insertions(+), 126 deletions(-) diff --git a/docs/PORTMASTER.md b/docs/PORTMASTER.md index 70f7c0e..19144c0 100644 --- a/docs/PORTMASTER.md +++ b/docs/PORTMASTER.md @@ -15,13 +15,17 @@ directory. The templates in `scripts/portmaster/` are authoritative. ## Release contract -- `port.json` uses schema version 4 and names `netfishing.zip`. +- `port.json` uses schema version 4, names `netfishing.zip`, and marks the + self-contained package ready to run. - `NETfishing.sh` starts with `# PORTMASTER: netfishing.zip, NETfishing.sh`. - The executable is `netfishing/NETfishing.aarch64`. - The package declares AArch64, two analog sticks, GLIBC 2.28, and `weston_pkg_0.2.squashfs`. -- The launcher must not start GPTOKEYB. Godot and NETfishing's controller - mapping manager handle controller input directly. +- The launcher starts GPTOKEYB in exit-only mode without a `-c` mapping file. + PortMaster therefore owns only its device-specific force-quit chord while + Godot and NETfishing's controller mapping manager handle gameplay input. +- The launcher calls `pm_platform_helper` for the game executable after + starting GPTOKEYB and calls `pm_finish` after the game exits. - Persistent device data remains under `netfishing/conf/data`, `netfishing/conf/config`, and `netfishing/conf/cache`. - The archive must not contain `conf/`, saves, identities, logs, source files, @@ -69,11 +73,14 @@ Before upgrading an existing installation, preserve `/mnt/mmc/ports/netfishing/conf/` on the device. After installation: 1. Confirm the installed executable and PCK hashes match the staged release. -2. Confirm the installed launcher contains the canonical PortMaster header and - does not contain `GPTOKEYB`. +2. Confirm the installed launcher contains the canonical PortMaster header, + starts GPTOKEYB without a `-c` mapping file, and calls + `pm_platform_helper` for the game executable. 3. Confirm `conf/` was not replaced or removed. 4. Launch the installed port through the normal muOS menu. 5. Verify the displayed game version and controller face-button mapping. +6. Verify PortMaster's force-quit chord exits the game. This is Start+Select on + most devices. Installing the public PortMaster catalog entry can still install an older catalog build until the upstream `netfishing.zip` is updated. A local release @@ -127,6 +134,11 @@ as `SDL_GAMECONTROLLERCONFIG` in the game command after Weston initializes; Weston sources PortMaster's control file internally and otherwise restores the SDL2 value. +Do not pass a `.gptk` controller mapping to GPTOKEYB. Its exit-only process +must retain PortMaster's controller configuration, while the verified Godot +mapping remains scoped to the game command. This prevents duplicate or +translated gameplay events while preserving the system force-quit chord. + Do not call `Input.add_joy_mapping(..., true)` for a recognized connected muOS controller. Updating this virtual controller after connection can stop Godot from delivering its standardized controller events until restart. @@ -162,7 +174,9 @@ also: - disables the additional full-screen world-pixelation pass; - replaces animated depth-aware water shaders with opaque, per-vertex water; - disables ocean surface motion; -- reduces rain to 256 particles simulated at 15 FPS; and +- disables foliage wind; +- keeps the title water and full-window menu patterns static; +- reduces rain to 128 particles simulated at 12 FPS; and - replaces procedural sky clouds and 81 moving local cloud patches with one flat cloud ceiling. diff --git a/main/main.gd b/main/main.gd index d471851..f51d0f7 100644 --- a/main/main.gd +++ b/main/main.gd @@ -259,6 +259,9 @@ func _ready() -> void: _test_world.set_light_performance_profile( _performance_profile.is_light() ) + _configure_presentation_performance_profile( + _performance_profile.is_light() + ) DisplayServer.window_set_title("NETfishing") _rain_ambience = RainAmbienceType.new() _rain_ambience.name = "RainAmbience" @@ -268,28 +271,6 @@ func _ready() -> void: _player, _test_world.get_saltwater_shoreline_mesh(), ) - var menu_pattern_material := ( - _player_menu_backdrop.material as ShaderMaterial - ) - if menu_pattern_material != null: - menu_pattern_material.set_shader_parameter( - "display_scale", - PLAYER_MENU_PATTERN_SCALE, - ) - menu_pattern_material.set_shader_parameter( - "scroll_velocity_pixels", - PLAYER_MENU_PATTERN_SCROLL_VELOCITY, - ) - var shop_pattern_material := _shop_backdrop.material as ShaderMaterial - if shop_pattern_material != null: - shop_pattern_material.set_shader_parameter( - "display_scale", - SHOP_PATTERN_SCALE, - ) - shop_pattern_material.set_shader_parameter( - "scroll_velocity_pixels", - PLAYER_MENU_PATTERN_SCROLL_VELOCITY, - ) get_window().size_changed.connect(_resize_native_overlays) _resize_native_overlays() if not _settings_manager.settings_changed.is_connected( @@ -305,6 +286,53 @@ func _ready() -> void: _show_data_root_setup() +func _configure_presentation_performance_profile(light_profile: bool) -> void: + var title_water_material := _title_background.material as ShaderMaterial + if title_water_material != null: + title_water_material.set_shader_parameter( + "animation_enabled", + not light_profile, + ) + var menu_pattern_material := ( + _player_menu_backdrop.material as ShaderMaterial + ) + if menu_pattern_material != null: + menu_pattern_material.set_shader_parameter( + "animation_enabled", + not light_profile, + ) + menu_pattern_material.set_shader_parameter( + "display_scale", + PLAYER_MENU_PATTERN_SCALE, + ) + menu_pattern_material.set_shader_parameter( + "scroll_velocity_pixels", + ( + Vector2.ZERO + if light_profile + else PLAYER_MENU_PATTERN_SCROLL_VELOCITY + ), + ) + var shop_pattern_material := _shop_backdrop.material as ShaderMaterial + if shop_pattern_material != null: + shop_pattern_material.set_shader_parameter( + "animation_enabled", + not light_profile, + ) + shop_pattern_material.set_shader_parameter( + "display_scale", + SHOP_PATTERN_SCALE, + ) + shop_pattern_material.set_shader_parameter( + "scroll_velocity_pixels", + ( + Vector2.ZERO + if light_profile + else PLAYER_MENU_PATTERN_SCROLL_VELOCITY + ), + ) + + func _is_dedicated_server_runtime() -> bool: if OS.has_feature("dedicated_server"): return true diff --git a/scripts/build_portmaster.sh b/scripts/build_portmaster.sh index 194b8be..5da4b10 100755 --- a/scripts/build_portmaster.sh +++ b/scripts/build_portmaster.sh @@ -144,6 +144,11 @@ PortMaster launches use the light performance profile by default. Put the word \`normal\` in \`netfishing/conf/performance_profile\` to opt a capable device into the normal rendering profile. See \`netfishing/licenses/\` for bundled credits and license information. + +## Controls + +NETfishing reads the handheld controller directly. PortMaster's system +force-quit chord remains available; this is Start+Select on most devices. EOF ( @@ -163,14 +168,26 @@ if [[ "${TOP_LEVELS}" != "${EXPECTED_TOP_LEVELS}" ]]; then fi grep -q '"version": 4' "${STAGE_ROOT}/port.json" grep -q '"name": "netfishing.zip"' "${STAGE_ROOT}/port.json" +grep -q '"rtr": true' "${STAGE_ROOT}/port.json" grep -q '^# PORTMASTER: netfishing.zip, NETfishing.sh$' \ "${STAGE_ROOT}/NETfishing.sh" grep -Fq 'PROFILE_PATH="$CONFDIR/performance_profile"' \ "${STAGE_ROOT}/NETfishing.sh" grep -Fq 'NETFISHING_PERFORMANCE_PROFILE=light' \ "${STAGE_ROOT}/NETfishing.sh" -if grep -q 'GPTOKEYB' "${STAGE_ROOT}/NETfishing.sh"; then - echo "GPTOKEYB must not be enabled for NETfishing." >&2 +grep -Fq '$GPTOKEYB "NETfishing.aarch64" &' \ + "${STAGE_ROOT}/NETfishing.sh" +grep -Fq 'pm_platform_helper "$GAME_EXECUTABLE"' \ + "${STAGE_ROOT}/NETfishing.sh" +grep -Fq 'pm_finish' "${STAGE_ROOT}/NETfishing.sh" +if [[ "$(grep -Ec '^[[:space:]]*\$GPTOKEYB[[:space:]]' \ + "${STAGE_ROOT}/NETfishing.sh")" -ne 1 ]]; then + echo "NETfishing must start exactly one GPTOKEYB exit handler." >&2 + exit 1 +fi +if grep -Eq '\$GPTOKEYB.*[[:space:]]-c([[:space:]]|$)' \ + "${STAGE_ROOT}/NETfishing.sh"; then + echo "GPTOKEYB controller mappings must not be enabled for NETfishing." >&2 exit 1 fi if unzip -Z1 "${ARCHIVE}" | grep -E \ diff --git a/scripts/portmaster/NETfishing.sh b/scripts/portmaster/NETfishing.sh index 97b73ef..527f15f 100755 --- a/scripts/portmaster/NETfishing.sh +++ b/scripts/portmaster/NETfishing.sh @@ -117,6 +117,12 @@ case "$PERFORMANCE_PROFILE" in ;; esac +# Keep NETfishing's native controller input separate from PortMaster's exit +# handling. Without a -c mapping file, GPTOKEYB only watches for the +# device-specific force-quit chord and does not inject gameplay inputs. +$GPTOKEYB "NETfishing.aarch64" & +pm_platform_helper "$GAME_EXECUTABLE" + $ESUDO env CRUSTY_RESOLUTION="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \ "$WESTON_DIR/westonwrap.sh" headless noop kiosk crusty_x11egl \ XDG_DATA_HOME="$CONFDIR/data" \ diff --git a/scripts/portmaster/port.json b/scripts/portmaster/port.json index 98df160..affc601 100644 --- a/scripts/portmaster/port.json +++ b/scripts/portmaster/port.json @@ -20,7 +20,7 @@ "simulation" ], "image": {}, - "rtr": false, + "rtr": true, "exp": false, "runtime": [ "weston_pkg_0.2.squashfs" diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 407711d..5f1f16b 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -216,8 +216,7 @@ func _run() -> void: ) as EmoteRadialMenu assert(quick_menu != null and emote_menu != null) assert(QuickRadialMenu.ACTIONS.size() == QuickRadialMenu.SECTOR_COUNT) - assert(QuickRadialMenu.ACTIONS.has(&"freecam")) - assert(QuickRadialMenu.ACTIONS.has(&"hud")) + assert(QuickRadialMenu.ACTIONS == [&"chat", &"freecam", &"hud"]) game_ui.call("_on_quick_action_selected", &"freecam") assert(player.is_free_camera_active()) var free_camera := player.get_active_gameplay_camera() @@ -544,10 +543,6 @@ func _run() -> void: await process_frame assert(player.hotbar.get_selected_slot() == 1) assert(not service.is_active() and not toolbar.visible) - game_ui.call("_on_quick_action_selected", &"paint") - await process_frame - assert(player.hotbar.get_selected_slot() == 0) - assert(service.is_active() and toolbar.visible) print("Art tools validation: PASS") var session := main.get_node("%NetworkSession") as NetworkSession diff --git a/tests/controller_mapping_validation.gd b/tests/controller_mapping_validation.gd index 16c9ad0..82bce26 100644 --- a/tests/controller_mapping_validation.gd +++ b/tests/controller_mapping_validation.gd @@ -211,12 +211,17 @@ func _validate_portmaster_launcher() -> String: "leftstick:b9", "rightstick:b12", "netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"", + "$GPTOKEYB \"NETfishing.aarch64\" &", + "pm_platform_helper \"$GAME_EXECUTABLE\"", "SDL_GAMECONTROLLERCONFIG=\"$netfishing_controllerconfig\" \\", + "pm_finish", ]: if expected_fragment not in launcher: return "PortMaster launcher omitted " + expected_fragment if "$'\\n'" in launcher: return "PortMaster launcher appends mappings across a Weston-unsafe newline" + if "$GPTOKEYB \"NETfishing.aarch64\" -c" in launcher: + return "PortMaster launcher injects duplicate GPTOKEYB gameplay mappings" return "" diff --git a/tests/light_performance_profile_validation.gd b/tests/light_performance_profile_validation.gd index 2f8b9b9..30911c6 100644 --- a/tests/light_performance_profile_validation.gd +++ b/tests/light_performance_profile_validation.gd @@ -4,6 +4,9 @@ const MainScene: PackedScene = preload("res://main/main.tscn") const RuntimePerformanceProfileType = preload( "res://main/runtime_performance_profile.gd" ) +const FOLIAGE_WIND_SHADER: Shader = preload( + "res://world/materials/foliage_wind.gdshader" +) func _initialize() -> void: @@ -28,12 +31,27 @@ func _run() -> void: assert(bool(main.get("_application_initialized"))) _validate_main_profile(main) _validate_minimal_weather(main) + _stop_audio_players(main) main.queue_free() - for _frame: int in 4: + for _frame: int in 8: await process_frame OS.unset_environment( RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE ) + var normal_main: Node = MainScene.instantiate() + root.add_child(normal_main) + for _frame: int in 4: + await process_frame + if not bool(normal_main.get("_application_initialized")): + normal_main.call("_activate_selected_data_path", "", true) + for _frame: int in 8: + await process_frame + assert(bool(normal_main.get("_application_initialized"))) + _validate_normal_profile(normal_main) + _stop_audio_players(normal_main) + normal_main.queue_free() + for _frame: int in 8: + await process_frame print("Light performance profile validation: PASS") quit() @@ -70,6 +88,131 @@ func _validate_main_profile(main: Node) -> void: assert(not pond.material_override is ShaderMaterial) assert(not ocean.material_override is ShaderMaterial) assert(not ocean.is_processing()) + _validate_ocean_fishing_coverage(main, ocean) + var island := main.get_node( + "TestWorld/Regions/StarterIslandRegion" + ) as StarterIslandRegion + assert(island != null and not island.is_foliage_wind_enabled()) + assert(_count_foliage_wind_overrides( + island.get_node("Terrain/Visual") + ) == 0) + + var title_background := main.get_node("%TitleBackground") as ColorRect + var title_material := title_background.material as ShaderMaterial + assert(title_material != null) + assert(not bool(title_material.get_shader_parameter( + "animation_enabled" + ))) + for backdrop_name: StringName in [ + &"PlayerMenuBackdrop", + &"ShopBackdrop", + ]: + var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect + var backdrop_material := backdrop.material as ShaderMaterial + assert(backdrop_material != null) + assert(not bool(backdrop_material.get_shader_parameter( + "animation_enabled" + ))) + assert( + backdrop_material.get_shader_parameter( + "scroll_velocity_pixels" + ) == Vector2.ZERO + ) + + +func _validate_normal_profile(main: Node) -> void: + var profile: RuntimePerformanceProfile = main.get("_performance_profile") + assert(profile != null and not profile.is_light()) + assert(is_equal_approx(root.scaling_3d_scale, 1.0)) + var pond := main.get_node( + "TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/VisualWater" + ) as MeshInstance3D + var ocean := main.get_node( + "TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/VisualWater" + ) as MeshInstance3D + assert(pond.material_override is ShaderMaterial) + assert(ocean.material_override is ShaderMaterial) + assert(not ocean.is_processing()) + _validate_ocean_fishing_coverage(main, ocean) + var island := main.get_node( + "TestWorld/Regions/StarterIslandRegion" + ) as StarterIslandRegion + assert(island != null and island.is_foliage_wind_enabled()) + assert(_count_foliage_wind_overrides( + island.get_node("Terrain/Visual") + ) > 0) + + var title_background := main.get_node("%TitleBackground") as ColorRect + var title_material := title_background.material as ShaderMaterial + assert(title_material != null) + assert(bool(title_material.get_shader_parameter("animation_enabled"))) + for backdrop_name: StringName in [ + &"PlayerMenuBackdrop", + &"ShopBackdrop", + ]: + var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect + var backdrop_material := backdrop.material as ShaderMaterial + assert(backdrop_material != null) + assert(bool(backdrop_material.get_shader_parameter( + "animation_enabled" + ))) + assert( + backdrop_material.get_shader_parameter( + "scroll_velocity_pixels" + ) != Vector2.ZERO + ) + var visuals: WorldTimeVisualController = main.get_node( + "%WorldTimeVisualController" + ) + var rain := visuals.get_node("LocalRain") as GPUParticles3D + assert(rain.amount == WorldTimeVisualController.RAIN_PARTICLE_AMOUNT) + assert(rain.fixed_fps == 30) + + +func _validate_ocean_fishing_coverage( + main: Node, + ocean: MeshInstance3D, +) -> void: + var ocean_mesh := ocean.mesh as PlaneMesh + assert(ocean_mesh != null) + var shape_node := main.get_node( + "TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/" + + "FishingRegions/OceanFishingRegion/Shape" + ) as CollisionShape3D + assert(shape_node != null) + var fishing_shape := shape_node.shape as BoxShape3D + assert(fishing_shape != null) + assert(fishing_shape.size.is_equal_approx(Vector3( + ocean_mesh.size.x, + 2.0, + ocean_mesh.size.y, + ))) + + +func _count_foliage_wind_overrides(root_node: Node) -> int: + var count: int = 0 + var mesh_instance := root_node as MeshInstance3D + if mesh_instance != null and mesh_instance.mesh != null: + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var material := mesh_instance.get_surface_override_material( + surface_index + ) as ShaderMaterial + if material != null and material.shader == FOLIAGE_WIND_SHADER: + count += 1 + for child: Node in root_node.get_children(): + count += _count_foliage_wind_overrides(child) + return count + + +func _stop_audio_players(root_node: Node) -> void: + if root_node is AudioStreamPlayer: + (root_node as AudioStreamPlayer).stop() + elif root_node is AudioStreamPlayer2D: + (root_node as AudioStreamPlayer2D).stop() + elif root_node is AudioStreamPlayer3D: + (root_node as AudioStreamPlayer3D).stop() + for child: Node in root_node.get_children(): + _stop_audio_players(child) func _validate_minimal_weather(main: Node) -> void: @@ -79,6 +222,8 @@ func _validate_minimal_weather(main: Node) -> void: var rain := visuals.get_node("LocalRain") as GPUParticles3D assert(rain.amount == WorldTimeVisualController.LIGHT_RAIN_PARTICLE_AMOUNT) assert(rain.fixed_fps == WorldTimeVisualController.LIGHT_RAIN_FIXED_FPS) + assert(rain.amount == 128) + assert(rain.fixed_fps == 12) var clouds := visuals.get_node("LocalStormClouds") as LocalStormCloudLayer assert(clouds.get_patch_count() == 1) var ceiling := clouds.get_node("CloudCeiling") as MeshInstance3D diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 27db6e8..f4951b4 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -122,7 +122,6 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural" const SHOP_ANIMALESE_BASE_PITCH: float = 1.08 const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0 const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 -const ART_KIT_ITEM_ID: StringName = &"art_kit" @onready var _canonical_stage: Control = %CanonicalStage @@ -612,20 +611,6 @@ func _on_emote_selected(emote_id: StringName) -> void: func _on_quick_action_selected(action_id: StringName) -> void: match action_id: - &"stuff": - _player_menu.open_section(PlayerMenuType.Section.COOLER) - &"logbook": - _player_menu.open_section(PlayerMenuType.Section.LOGBOOK) - &"fishnet": - _player_menu.open_section(PlayerMenuType.Section.NET) - &"mail": - _player_menu.open_section(PlayerMenuType.Section.MAIL) - &"profile": - _player_menu.open_section(PlayerMenuType.Section.PROFILE) - &"online": - _player_menu.open_section(PlayerMenuType.Section.PLAYERS) - &"paint": - _select_art_kit_hotbar_slot() &"chat": _chat_ui.open_chat() &"freecam": @@ -1116,15 +1101,6 @@ static func get_virtual_mouse_window_bounds(window_size: Vector2) -> Rect2: return Rect2(cursor_margin, bounds_size) -func _select_art_kit_hotbar_slot() -> void: - if _hotbar == null: - return - for slot_index: int in range(PlayerHotbarType.SLOT_COUNT): - if _hotbar.get_item_id(slot_index) == ART_KIT_ITEM_ID: - _hotbar.select_slot(slot_index) - return - - func set_surface_drawing_hotbar_selected(is_selected: bool) -> void: _surface_drawing_hotbar_selected = is_selected _refresh_surface_drawing_activation() diff --git a/ui/player_menu_pattern.gdshader b/ui/player_menu_pattern.gdshader index 71f8fc6..5e2d2a3 100644 --- a/ui/player_menu_pattern.gdshader +++ b/ui/player_menu_pattern.gdshader @@ -4,10 +4,14 @@ uniform sampler2D pattern_texture : source_color, repeat_enable, filter_nearest; uniform vec2 source_tile_size = vec2(128.0, 128.0); uniform float display_scale : hint_range(0.5, 2.0, 0.05) = 0.85; uniform vec2 scroll_velocity_pixels = vec2(-7.0, -5.0); +uniform bool animation_enabled = true; void fragment() { vec2 viewport_pixels = 1.0 / SCREEN_PIXEL_SIZE; vec2 display_tile_size = max(source_tile_size * display_scale, vec2(1.0)); - vec2 moving_pixels = UV * viewport_pixels + TIME * scroll_velocity_pixels; + vec2 moving_pixels = UV * viewport_pixels; + if (animation_enabled) { + moving_pixels += TIME * scroll_velocity_pixels; + } COLOR = texture(pattern_texture, moving_pixels / display_tile_size); } diff --git a/ui/quick_radial_menu.gd b/ui/quick_radial_menu.gd index 8ae1720..f04b295 100644 --- a/ui/quick_radial_menu.gd +++ b/ui/quick_radial_menu.gd @@ -16,30 +16,16 @@ const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" ) const ACTIONS: Array[StringName] = [ - &"stuff", - &"logbook", - &"fishnet", - &"mail", - &"profile", - &"online", - &"paint", &"chat", &"freecam", &"hud", ] const LABELS: Array[String] = [ - "stuff", - "logbook", - "fishnet", - "mail", - "profile", - "online", - "paint", "chat", "freecam", "hide hud", ] -const SECTOR_COUNT: int = 10 +const SECTOR_COUNT: int = 3 var _buttons: Array[BubbleButton] = [] var _is_open: bool = false diff --git a/ui/title_water_background.gdshader b/ui/title_water_background.gdshader index c7faabe..ca12bc6 100644 --- a/ui/title_water_background.gdshader +++ b/ui/title_water_background.gdshader @@ -10,30 +10,52 @@ uniform float wave_speed : hint_range(0.0, 0.5, 0.01) = 0.10; uniform float wave_scale : hint_range(0.5, 8.0, 0.1) = 2.4; uniform float wave_strength : hint_range(0.0, 0.05, 0.001) = 0.012; uniform float caustic_strength : hint_range(0.0, 0.08, 0.001) = 0.018; +uniform bool animation_enabled = true; void fragment() { vec2 grid_size = max(virtual_pixel_density, vec2(1.0)); vec2 pixel_uv = (floor(UV * grid_size) + vec2(0.5)) / grid_size; - float motion = TIME * wave_speed; - - float broad_wave = sin( - pixel_uv.x * wave_scale + motion - ); - float secondary_wave = sin( - pixel_uv.x * 1.1 + pixel_uv.y * 1.6 - motion * 0.65 - ); - float movement_falloff = 1.0 - pixel_uv.y * 0.75; - float depth_displacement = ( - (broad_wave * 0.65 + secondary_wave * 0.35) - * wave_strength - * movement_falloff - ); - float continuous_depth = clamp( - pixel_uv.y - + depth_displacement, - 0.0, - 1.0 - ); + float continuous_depth = pixel_uv.y; + float caustic_level = 0.0; + if (animation_enabled) { + float motion = TIME * wave_speed; + float broad_wave = sin( + pixel_uv.x * wave_scale + motion + ); + float secondary_wave = sin( + pixel_uv.x * 1.1 + pixel_uv.y * 1.6 - motion * 0.65 + ); + float movement_falloff = 1.0 - pixel_uv.y * 0.75; + float depth_displacement = ( + (broad_wave * 0.65 + secondary_wave * 0.35) + * wave_strength + * movement_falloff + ); + continuous_depth = clamp( + pixel_uv.y + + depth_displacement, + 0.0, + 1.0 + ); + float caustic_wave = clamp( + ( + sin( + pixel_uv.x * 4.0 + + pixel_uv.y * 1.2 + + motion * 0.7 + ) + * sin( + pixel_uv.x * 1.7 + - pixel_uv.y * 2.2 + - motion * 0.55 + ) + - 0.25 + ) / 0.75, + 0.0, + 1.0 + ); + caustic_level = floor(caustic_wave * 3.0) / 3.0; + } float depth_steps = max(2.0, floor(color_step_count)); float band_index = floor( continuous_depth * (depth_steps - 1.0) + 0.5 @@ -60,26 +82,6 @@ void fragment() { ); } - float caustic_wave = clamp( - ( - sin( - pixel_uv.x * 4.0 - + pixel_uv.y * 1.2 - + motion * 0.7 - ) - * sin( - pixel_uv.x * 1.7 - - pixel_uv.y * 2.2 - - motion * 0.55 - ) - - 0.25 - ) / 0.75, - 0.0, - 1.0 - ); - float caustic_level = floor( - caustic_wave * 3.0 - ) / 3.0; float upper_water_mask = 1.0 - smoothstep( 0.18, 0.34, diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index adb0fab..477c5d2 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -47,10 +47,13 @@ var fishing_shop_path: NodePath = ( @export_range(0.0, 0.4, 0.005) var foliage_wind_strength: float = 0.36 @export_range(0.0, 4.0, 0.05) var foliage_wind_speed: float = 0.9 +var _foliage_wind_enabled: bool = false + + func _ready() -> void: if rebuild_terrain_collision_on_ready or not has_terrain_collision(): _build_terrain_collision() - _apply_foliage_wind() + _set_foliage_wind_enabled(true) if Engine.is_editor_hint(): update_configuration_warnings() @@ -69,6 +72,7 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D: func set_light_performance_profile(enabled: bool) -> void: + _set_foliage_wind_enabled(not enabled) var pond := get_node_or_null( ^"WaterBodies/Pond/VisualWater" ) as MeshInstance3D @@ -93,9 +97,10 @@ func set_light_performance_profile(enabled: bool) -> void: if enabled else NORMAL_SALT_WATER_MATERIAL ) - var surface_motion := ocean as WaterSurfaceMotion - if surface_motion != null: - surface_motion.set_motion_enabled(not enabled) + + +func is_foliage_wind_enabled() -> bool: + return _foliage_wind_enabled func _create_light_water_material( @@ -254,14 +259,33 @@ func _collect_terrain_mesh_instances(root: Node) -> Array[MeshInstance3D]: return mesh_instances -func _apply_foliage_wind() -> void: +func _set_foliage_wind_enabled(enabled: bool) -> void: + _foliage_wind_enabled = enabled var terrain_root: Node = get_node_or_null(terrain_visual_root_path) if terrain_root == null: return for mesh_instance: MeshInstance3D in _collect_terrain_mesh_instances( terrain_root ): - _apply_foliage_wind_to_mesh(mesh_instance) + if enabled: + _apply_foliage_wind_to_mesh(mesh_instance) + else: + _remove_foliage_wind_from_mesh(mesh_instance) + + +func _remove_foliage_wind_from_mesh(mesh_instance: MeshInstance3D) -> void: + if mesh_instance.mesh == null: + return + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var override_material: Material = ( + mesh_instance.get_surface_override_material(surface_index) + ) + var shader_material := override_material as ShaderMaterial + if ( + shader_material != null + and shader_material.shader == FOLIAGE_WIND_SHADER + ): + mesh_instance.set_surface_override_material(surface_index, null) func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void: diff --git a/world/world_time_visual_controller.gd b/world/world_time_visual_controller.gd index e4e2166..821f7ef 100644 --- a/world/world_time_visual_controller.gd +++ b/world/world_time_visual_controller.gd @@ -12,8 +12,8 @@ const RAIN_VISIBILITY_AABB := AABB( Vector3(27.0, 12.0, 27.0), ) const RAIN_PARTICLE_AMOUNT: int = 2240 -const LIGHT_RAIN_PARTICLE_AMOUNT: int = 256 -const LIGHT_RAIN_FIXED_FPS: int = 15 +const LIGHT_RAIN_PARTICLE_AMOUNT: int = 128 +const LIGHT_RAIN_FIXED_FPS: int = 12 const RAIN_VELOCITY_MIN: float = 16.0 const RAIN_VELOCITY_MAX: float = 20.0 const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)