Improve PortMaster performance and controls

This commit is contained in:
Alexander Sellite 2026-08-15 09:53:30 -04:00
parent 9afa60f6a5
commit 57df54d82d
14 changed files with 328 additions and 126 deletions

View file

@ -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.

View file

@ -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

View file

@ -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 \

View file

@ -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" \

View file

@ -20,7 +20,7 @@
"simulation"
],
"image": {},
"rtr": false,
"rtr": true,
"exp": false,
"runtime": [
"weston_pkg_0.2.squashfs"

View file

@ -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

View file

@ -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 ""

View file

@ -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

View file

@ -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()

View file

@ -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);
}

View file

@ -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

View file

@ -10,12 +10,15 @@ 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 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
);
@ -28,12 +31,31 @@ void fragment() {
* wave_strength
* movement_falloff
);
float continuous_depth = clamp(
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,

View file

@ -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
):
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:

View file

@ -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)