Add default light profile for PortMaster

This commit is contained in:
Alexander Sellite 2026-08-14 22:12:02 -04:00
parent a62cce6404
commit a3e0830bd1
16 changed files with 454 additions and 42 deletions

View file

@ -140,30 +140,52 @@ following in the installed game:
- A single button never cancels auto-map.
- Holding both bumpers together for 1.25 seconds cancels auto-map.
## Low-end performance profile
## PortMaster performance profile
The launcher keeps the normal game profile on unknown and stronger hardware.
It enables the low-end profile only when the Linux device tree reports one of
the identifiers used by Allwinner H616/H700 XX handhelds:
Every PortMaster launch defaults to the light performance profile, regardless
of device family. This reflects the low-end hardware that makes up most of the
PortMaster ecosystem and keeps the canonical package conservative by default.
- `allwinner,h616`
- `sun50iw9p1`
- `allwinner,sun50i-h700`
The low-end profile passes these Godot options:
The light profile passes these Godot options:
- `--single-window`
- `--disable-vsync`
- `--max-fps 30`
- `--audio-output-latency 40`
It also passes `NETFISHING_LOW_END=1`. The game renders the 3D world at 75%
linear resolution with nearest-neighbor scaling, reducing 3D pixel work by
about 44% while the separately rendered UI retains its canonical resolution.
It also passes `NETFISHING_PERFORMANCE_PROFILE=light` and
`NETFISHING_LOW_END=1`. The game renders the 3D world at 50% linear resolution
with nearest-neighbor scaling, reducing 3D pixel work by 75% while the
separately rendered UI retains its canonical resolution. The light profile
also:
Do not use Godot's low processor mode for this profile. It reduces idle CPU
usage by sleeping between updates and is not a game-performance optimization.
- 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
- replaces procedural sky clouds and 81 moving local cloud patches with one
flat cloud ceiling.
Before adding a device family, record its exact NUL-separated device-tree
`compatible` value from hardware. Do not infer detection from a retail product
name alone.
The normal profile retains the full visual presentation.
To opt a capable device into the normal profile, create the persistent file
`netfishing/conf/performance_profile` containing exactly:
```text
normal
```
On a typical muOS installation, the full path is:
```text
/mnt/mmc/ports/netfishing/conf/performance_profile
```
Set the file to `light`, or remove it, to restore the default. An externally
provided `NETFISHING_PERFORMANCE_PROFILE=normal` or `light` environment value
takes precedence over the persistent file. Invalid values safely fall back to
the light profile and are reported in `netfishing/log.txt`.
Do not use Godot's low processor mode for the light profile. It reduces idle
CPU usage by sleeping between updates and is not a game-performance
optimization.

View file

@ -39,6 +39,9 @@ const PixelationResetOverlayType = preload(
const WorldPixelationPostprocessType = preload(
"res://main/world_pixelation_postprocess.gd"
)
const RuntimePerformanceProfileType = preload(
"res://main/runtime_performance_profile.gd"
)
const NetworkSessionType = preload("res://network/network_session.gd")
const DiscoveryClientType = preload("res://network/discovery_client.gd")
const DedicatedServerConfigType = preload(
@ -235,6 +238,7 @@ var _data_folder_dialog: FileDialog
var _data_folder_picker_generation: int = 0
var _restore_data_setup_after_picker: bool = false
var _application_initialized := false
var _performance_profile: RuntimePerformanceProfileType
var _pending_existing_root_path := ""
var _local_recovery_attempt_id: String = ""
var _dedicated_runtime: bool = false
@ -244,10 +248,17 @@ var _rain_ambience: RainAmbienceType
func _ready() -> void:
_performance_profile = RuntimePerformanceProfileType.from_environment()
_dedicated_runtime = _is_dedicated_server_runtime()
if _dedicated_runtime:
call_deferred("_start_dedicated_server")
return
_world_pixelation.set_light_performance_profile(
_performance_profile.is_light()
)
_test_world.set_light_performance_profile(
_performance_profile.is_light()
)
DisplayServer.window_set_title("NETfishing")
_rain_ambience = RainAmbienceType.new()
_rain_ambience.name = "RainAmbience"
@ -441,6 +452,7 @@ func _initialize_application(dedicated: bool) -> void:
_world_weather,
_player,
Callable(_player, "get_active_gameplay_camera"),
_performance_profile.is_light(),
)
if not _world_time.natural_time_advanced.is_connected(
_on_natural_time_advanced
@ -1324,9 +1336,7 @@ func _apply_world_pixelation(pixel_size: int) -> void:
var root_viewport: Viewport = get_viewport()
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
root_viewport.scaling_3d_scale = (
0.75
if OS.get_environment("NETFISHING_LOW_END") == "1"
else 1.0
_performance_profile.get_world_render_scale()
)
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED

View file

@ -0,0 +1,50 @@
class_name RuntimePerformanceProfile
extends RefCounted
const PROFILE_ENVIRONMENT_VARIABLE: String = (
"NETFISHING_PERFORMANCE_PROFILE"
)
const LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "NETFISHING_LOW_END"
const NORMAL_PROFILE: StringName = &"normal"
const LIGHT_PROFILE: StringName = &"light"
const NORMAL_WORLD_RENDER_SCALE: float = 1.0
const LIGHT_WORLD_RENDER_SCALE: float = 0.5
var _profile_name: StringName = NORMAL_PROFILE
static func from_environment() -> RuntimePerformanceProfile:
return from_name(
StringName(OS.get_environment(PROFILE_ENVIRONMENT_VARIABLE)),
OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1",
)
static func from_name(
profile_name: StringName,
legacy_light_requested: bool = false,
) -> RuntimePerformanceProfile:
var profile := RuntimePerformanceProfile.new()
if profile_name == LIGHT_PROFILE or (
profile_name.is_empty() and legacy_light_requested
):
profile._profile_name = LIGHT_PROFILE
else:
profile._profile_name = NORMAL_PROFILE
return profile
func get_profile_name() -> StringName:
return _profile_name
func is_light() -> bool:
return _profile_name == LIGHT_PROFILE
func get_world_render_scale() -> float:
return (
LIGHT_WORLD_RENDER_SCALE
if is_light()
else NORMAL_WORLD_RENDER_SCALE
)

View file

@ -0,0 +1 @@
uid://8vm104r2264h

View file

@ -5,6 +5,7 @@ extends CanvasLayer
var _pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE
var _gameplay_active: bool = false
var _light_performance_profile: bool = false
func _ready() -> void:
@ -26,6 +27,15 @@ func set_gameplay_active(active: bool) -> void:
_refresh_grid()
func set_light_performance_profile(enabled: bool) -> void:
_light_performance_profile = enabled
_refresh_grid()
func is_light_performance_profile() -> bool:
return _light_performance_profile
func get_grid_size() -> Vector2i:
return PlayerSettings.get_world_grid_size(
_pixel_size,
@ -38,6 +48,7 @@ func _refresh_grid() -> void:
return
var effect_enabled: bool = (
_gameplay_active
and not _light_performance_profile
and _pixel_size != PlayerSettings.MIN_WORLD_PIXEL_SIZE
)
_screen_grid.visible = effect_enabled

View file

@ -140,7 +140,10 @@ The package requires an ARM64 device, two analog sticks, GLIBC 2.28 or newer,
and the \`weston_pkg_0.2\` runtime.
Save data and device-local configuration remain under \`netfishing/conf/\`.
See \`netfishing/licenses/\` for bundled credits and license information.
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.
EOF
(
@ -162,6 +165,10 @@ grep -q '"version": 4' "${STAGE_ROOT}/port.json"
grep -q '"name": "netfishing.zip"' "${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
exit 1

View file

@ -78,17 +78,36 @@ fi
NETFISHING_GODOT_OPTIONS=()
NETFISHING_GAME_ENVIRONMENT=()
DEVICE_COMPATIBILITY=""
if [[ -r /proc/device-tree/compatible ]]; then
DEVICE_COMPATIBILITY="$(tr '\0' ' ' </proc/device-tree/compatible)"
elif [[ -r /sys/firmware/devicetree/base/compatible ]]; then
DEVICE_COMPATIBILITY="$(
tr '\0' ' ' </sys/firmware/devicetree/base/compatible
)"
PERFORMANCE_PROFILE="${NETFISHING_PERFORMANCE_PROFILE:-}"
PROFILE_PATH="$CONFDIR/performance_profile"
if [[ -z "$PERFORMANCE_PROFILE" && -r "$PROFILE_PATH" ]]; then
IFS= read -r PERFORMANCE_PROFILE < "$PROFILE_PATH" || true
PERFORMANCE_PROFILE="${PERFORMANCE_PROFILE%$'\r'}"
fi
case "$DEVICE_COMPATIBILITY" in
*allwinner,h616*|*sun50iw9p1*|*allwinner,sun50i-h700*)
NETFISHING_GAME_ENVIRONMENT+=("NETFISHING_LOW_END=1")
case "$PERFORMANCE_PROFILE" in
normal)
NETFISHING_GAME_ENVIRONMENT+=(
"NETFISHING_PERFORMANCE_PROFILE=normal"
)
;;
light|"")
NETFISHING_GAME_ENVIRONMENT+=(
"NETFISHING_PERFORMANCE_PROFILE=light"
"NETFISHING_LOW_END=1"
)
NETFISHING_GODOT_OPTIONS+=(
--single-window
--disable-vsync
--max-fps 30
--audio-output-latency 40
)
;;
*)
echo "Unknown performance profile '$PERFORMANCE_PROFILE'; using light." >&2
NETFISHING_GAME_ENVIRONMENT+=(
"NETFISHING_PERFORMANCE_PROFILE=light"
"NETFISHING_LOW_END=1"
)
NETFISHING_GODOT_OPTIONS+=(
--single-window
--disable-vsync

View file

@ -30,6 +30,7 @@ readonly -a QUICK_TESTS=(
readonly -a RUNTIME_TESTS=(
"tests/inventory_notepad_art_validation.gd"
"tests/light_performance_profile_validation.gd"
"tests/logbook_runtime_validation.gd"
"tests/player_experience_ui_validation.gd"
"tests/title_credits_validation.gd"

View file

@ -61,8 +61,12 @@ func _validate_low_end_profile_contract() -> void:
var launcher: String = FileAccess.get_file_as_string(
"res://scripts/portmaster/NETfishing.sh"
)
assert(launcher.contains("allwinner,h616"))
assert(launcher.contains("sun50iw9p1"))
assert(launcher.contains("NETFISHING_PERFORMANCE_PROFILE:-"))
assert(launcher.contains("$CONFDIR/performance_profile"))
assert(launcher.contains("normal)"))
assert(launcher.contains("light|\"\")"))
assert(launcher.contains("NETFISHING_PERFORMANCE_PROFILE=normal"))
assert(launcher.contains("NETFISHING_PERFORMANCE_PROFILE=light"))
assert(launcher.contains("NETFISHING_LOW_END=1"))
assert(launcher.contains("--max-fps 30"))
assert(launcher.contains("--audio-output-latency 40"))

View file

@ -0,0 +1,108 @@
extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const RuntimePerformanceProfileType = preload(
"res://main/runtime_performance_profile.gd"
)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_validate_profile_resolution()
OS.set_environment(
RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE,
"light",
)
root.size = Vector2i(640, 480)
var main: Node = MainScene.instantiate()
root.add_child(main)
for _frame: int in 4:
await process_frame
if not bool(main.get("_application_initialized")):
main.call("_activate_selected_data_path", "", true)
for _frame: int in 8:
await process_frame
assert(bool(main.get("_application_initialized")))
_validate_main_profile(main)
_validate_minimal_weather(main)
main.queue_free()
for _frame: int in 4:
await process_frame
OS.unset_environment(
RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE
)
print("Light performance profile validation: PASS")
quit()
func _validate_profile_resolution() -> void:
var normal := RuntimePerformanceProfileType.from_name(&"normal")
var light := RuntimePerformanceProfileType.from_name(&"light")
var legacy := RuntimePerformanceProfileType.from_name(&"", true)
assert(not normal.is_light())
assert(is_equal_approx(normal.get_world_render_scale(), 1.0))
assert(light.is_light())
assert(is_equal_approx(light.get_world_render_scale(), 0.5))
assert(legacy.is_light())
func _validate_main_profile(main: Node) -> void:
var profile: RuntimePerformanceProfile = main.get("_performance_profile")
assert(profile != null and profile.is_light())
assert(is_equal_approx(root.scaling_3d_scale, 0.5))
var pixelation: Node = main.get_node("%WorldPixelationPostprocess")
assert(bool(pixelation.call("is_light_performance_profile")))
pixelation.call("set_gameplay_active", true)
var screen_grid := pixelation.get_node("ScreenGrid") as ColorRect
assert(screen_grid != null and not screen_grid.visible)
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 StandardMaterial3D)
assert(ocean.material_override is StandardMaterial3D)
assert(not pond.material_override is ShaderMaterial)
assert(not ocean.material_override is ShaderMaterial)
assert(not ocean.is_processing())
func _validate_minimal_weather(main: Node) -> void:
var visuals: WorldTimeVisualController = main.get_node(
"%WorldTimeVisualController"
)
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)
var clouds := visuals.get_node("LocalStormClouds") as LocalStormCloudLayer
assert(clouds.get_patch_count() == 1)
var ceiling := clouds.get_node("CloudCeiling") as MeshInstance3D
assert(ceiling != null and ceiling.mesh is PlaneMesh)
assert(ceiling.material_override == null)
assert((ceiling.mesh as PlaneMesh).material is StandardMaterial3D)
visuals.apply_weather_immediately(WorldWeatherService.Weather.RAINY)
assert(rain.emitting and rain.amount_ratio > 0.99)
assert(clouds.visible and clouds.get_storm_amount() > 0.87)
var cloud_material := (
(ceiling.mesh as PlaneMesh).material as StandardMaterial3D
)
assert(cloud_material.albedo_color.a < 0.65)
var test_world := main.get_node("TestWorld") as TestWorld
var runtime_environment := (
test_world.get_world_environment().environment as Environment
)
var sky_material := (
runtime_environment.sky.sky_material as ShaderMaterial
)
assert(is_zero_approx(float(
sky_material.get_shader_parameter("cloud_coverage")
)))
assert(is_zero_approx(float(
sky_material.get_shader_parameter("cloud_opacity")
)))

View file

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

View file

@ -13,6 +13,9 @@ const DISTANCE_FADE_START: float = 240.0
const DISTANCE_FADE_END: float = 300.0
const WRAP_RADIUS: float = FIELD_HALF_EXTENT
const MAXIMUM_OPACITY: float = 1.0
const LIGHT_CEILING_SIZE: float = 520.0
const LIGHT_CEILING_ALTITUDE: float = 20.0
const LIGHT_MAXIMUM_OPACITY: float = 0.72
const BASE_VELOCITY := Vector2(0.825, -0.45)
const BODY_OUTLINE: Array[Vector2] = [
Vector2(-6.9, -0.8),
@ -50,14 +53,25 @@ var _target: Node3D
var _material: ShaderMaterial
var _multimesh: MultiMesh
var _cloud_mesh: MultiMeshInstance3D
var _light_cloud_mesh: MeshInstance3D
var _light_material: StandardMaterial3D
var _drift_offset := Vector2.ZERO
var _storm_amount: float = 0.0
var _light_performance_profile: bool = false
func setup(target: Node3D) -> void:
func setup(
target: Node3D,
light_performance_profile: bool = false,
) -> void:
_target = target
_build_cloud_field()
_update_cloud_transforms()
_light_performance_profile = light_performance_profile
if _light_performance_profile:
_build_light_cloud_ceiling()
_update_light_position()
else:
_build_cloud_field()
_update_cloud_transforms()
set_process(true)
@ -65,8 +79,19 @@ func set_storm_amount(amount: float, color: Color) -> void:
_storm_amount = clampf(amount, 0.0, 1.0)
var should_be_visible: bool = _storm_amount > 0.001
if should_be_visible and not visible:
_update_cloud_transforms()
if _light_performance_profile:
_update_light_position()
else:
_update_cloud_transforms()
visible = should_be_visible
if _light_performance_profile:
if _light_material != null:
var light_color: Color = color
light_color.a = (
_storm_amount * LIGHT_MAXIMUM_OPACITY
)
_light_material.albedo_color = light_color
return
if _material == null:
return
_material.set_shader_parameter("weather_opacity", _storm_amount)
@ -78,12 +103,19 @@ func get_storm_amount() -> float:
func get_patch_count() -> int:
return GRID_AXIS_COUNT * GRID_AXIS_COUNT
return (
1
if _light_performance_profile
else GRID_AXIS_COUNT * GRID_AXIS_COUNT
)
func _process(delta: float) -> void:
if not visible or _target == null or not is_instance_valid(_target):
return
if _light_performance_profile:
_update_light_position()
return
_drift_offset += BASE_VELOCITY * delta
_drift_offset = Vector2(
_wrap_axis(_drift_offset.x),
@ -128,6 +160,37 @@ func _build_cloud_field() -> void:
visible = false
func _build_light_cloud_ceiling() -> void:
_light_material = StandardMaterial3D.new()
_light_material.resource_name = "light_cloud_ceiling"
_light_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
_light_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_light_material.cull_mode = BaseMaterial3D.CULL_DISABLED
_light_material.albedo_color = Color(0.22, 0.25, 0.26, 0.0)
var ceiling_mesh := PlaneMesh.new()
ceiling_mesh.size = Vector2.ONE * LIGHT_CEILING_SIZE
ceiling_mesh.material = _light_material
_light_cloud_mesh = MeshInstance3D.new()
_light_cloud_mesh.name = "CloudCeiling"
_light_cloud_mesh.position.y = LIGHT_CEILING_ALTITUDE
_light_cloud_mesh.mesh = ceiling_mesh
_light_cloud_mesh.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
add_child(_light_cloud_mesh)
visible = false
func _update_light_position() -> void:
if _target == null or not is_instance_valid(_target):
return
global_position = Vector3(
_target.global_position.x,
0.0,
_target.global_position.z,
)
func _build_cloud_body_mesh() -> ArrayMesh:
var outline := PackedVector2Array(BODY_OUTLINE)
var surface_tool := SurfaceTool.new()

View file

@ -13,6 +13,14 @@ const FOLIAGE_MATERIAL_NAMES: Array[StringName] = [
&"leaf_light",
&"leaf_dark",
]
const NORMAL_SALT_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water.tres"
)
const NORMAL_FRESH_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water_fresh.tres"
)
const LIGHT_SALT_WATER_COLOR := Color(0.11, 0.345, 0.435)
const LIGHT_FRESH_WATER_COLOR := Color(0.18, 0.46, 0.50)
@export_group("Owned Nodes")
@export_node_path("MeshInstance3D")
@ -60,6 +68,48 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
func set_light_performance_profile(enabled: bool) -> void:
var pond := get_node_or_null(
^"WaterBodies/Pond/VisualWater"
) as MeshInstance3D
var ocean := get_node_or_null(
^"WaterBodies/Ocean/VisualWater"
) as MeshInstance3D
if pond != null:
pond.material_override = (
_create_light_water_material(
LIGHT_FRESH_WATER_COLOR,
&"light_fresh_water",
)
if enabled
else NORMAL_FRESH_WATER_MATERIAL
)
if ocean != null:
ocean.material_override = (
_create_light_water_material(
LIGHT_SALT_WATER_COLOR,
&"light_salt_water",
)
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 _create_light_water_material(
color: Color,
material_name: StringName,
) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.resource_name = str(material_name)
material.albedo_color = color
material.roughness = 1.0
material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
return material
func get_spawn_surface_triangles(
material_names: Array[StringName],
minimum_global_y: float,

View file

@ -50,6 +50,10 @@ func get_sun() -> DirectionalLight3D:
return _sun
func set_light_performance_profile(enabled: bool) -> void:
_starter_island.set_light_performance_profile(enabled)
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
var waters: Array[FishableWaterRegion] = []
for region: WorldRegion in _get_regions():

View file

@ -35,6 +35,12 @@ func _process(_delta: float) -> void:
)
func set_motion_enabled(enabled: bool) -> void:
set_process(enabled and amplitude > 0.0)
if not enabled:
position.y = _base_height
static func get_default_height_offset() -> float:
return calculate_height_offset(
_current_time_seconds(),

View file

@ -2,6 +2,7 @@ class_name WorldTimeVisualController
extends Node
const UPDATE_INTERVAL_SECONDS: float = 0.1
const LIGHT_UPDATE_INTERVAL_SECONDS: float = 0.25
const SUN_YAW_DEGREES: float = -32.0
const WEATHER_TRANSITION_SECONDS: float = 10.0
const RAIN_EMITTER_OFFSET := Vector3(0.0, 7.0, 0.0)
@ -11,6 +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 RAIN_VELOCITY_MIN: float = 16.0
const RAIN_VELOCITY_MAX: float = 20.0
const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)
@ -92,6 +95,7 @@ var _weather_to: WorldWeatherService.Weather = (
WorldWeatherService.Weather.SUNNY
)
var _weather_transition: float = 1.0
var _light_performance_profile: bool = false
func setup(
@ -101,6 +105,7 @@ func setup(
weather_service: WorldWeatherService = null,
rain_target: Node3D = null,
rain_camera_provider: Callable = Callable(),
light_performance_profile: bool = false,
) -> void:
_time_service = time_service
_weather_service = weather_service
@ -108,6 +113,7 @@ func setup(
_sun = sun
_rain_target = rain_target
_rain_camera_provider = rain_camera_provider
_light_performance_profile = light_performance_profile
if not _prepare_runtime_environment():
set_process(false)
return
@ -135,7 +141,12 @@ func _process(delta: float) -> void:
1.0,
)
_elapsed += delta
if _elapsed < UPDATE_INTERVAL_SECONDS:
var update_interval: float = (
LIGHT_UPDATE_INTERVAL_SECONDS
if _light_performance_profile
else UPDATE_INTERVAL_SECONDS
)
if _elapsed < update_interval:
return
_elapsed = 0.0
_apply_time(_time_service.get_time_hours())
@ -187,10 +198,16 @@ func _prepare_runtime_environment() -> bool:
func _prepare_rain() -> void:
_rain = GPUParticles3D.new()
_rain.name = "LocalRain"
_rain.amount = RAIN_PARTICLE_AMOUNT
_rain.amount = (
LIGHT_RAIN_PARTICLE_AMOUNT
if _light_performance_profile
else RAIN_PARTICLE_AMOUNT
)
_rain.amount_ratio = 0.0
_rain.lifetime = 1.25
_rain.fixed_fps = 30
_rain.fixed_fps = (
LIGHT_RAIN_FIXED_FPS if _light_performance_profile else 30
)
_rain.local_coords = false
_rain.visibility_aabb = RAIN_VISIBILITY_AABB
var process_material := ParticleProcessMaterial.new()
@ -220,7 +237,7 @@ func _prepare_storm_clouds() -> void:
_storm_clouds = LocalStormCloudLayerType.new()
_storm_clouds.name = "LocalStormClouds"
add_child(_storm_clouds)
_storm_clouds.setup(_rain_target)
_storm_clouds.setup(_rain_target, _light_performance_profile)
func _apply_time(time_hours: float) -> void:
@ -259,6 +276,33 @@ func _apply_time(time_hours: float) -> void:
var fog_color: Color = _blended_color(
NIGHT_FOG, DAY_FOG, WARM_FOG, daylight, warmth
)
if _light_performance_profile:
var simple_cloud_color := _blended_color(
NIGHT_CLOUD_SHADOW,
DAY_CLOUD_SHADOW,
WARM_CLOUD_SHADOW,
daylight,
warmth,
)
var simple_overcast: float = _weather_value(
0.0,
0.55,
0.82,
0.0,
)
sky_top = sky_top.lerp(simple_cloud_color, simple_overcast)
sky_horizon = sky_horizon.lerp(
simple_cloud_color.lightened(0.08),
simple_overcast,
)
ground_bottom = ground_bottom.lerp(
simple_cloud_color.darkened(0.12),
simple_overcast * 0.65,
)
ground_horizon = ground_horizon.lerp(
simple_cloud_color,
simple_overcast * 0.75,
)
var foggy_fog_amount: float = _weather_value(0.0, 0.0, 0.0, 1.0)
var rain_fog_amount: float = _weather_value(0.0, 0.0, 1.0, 0.0)
var fog_daylight_amount: float = daylight * foggy_fog_amount
@ -399,6 +443,8 @@ func _apply_water_environment(
foggy_horizon_occlusion: float,
fog_render_color: Color,
) -> void:
if _light_performance_profile:
return
var water_tint := _blended_color(
NIGHT_WATER_TINT,
Color.WHITE,
@ -561,6 +607,15 @@ func _apply_sky_clouds(daylight: float, warmth: float) -> void:
daylight,
warmth,
)
if _light_performance_profile:
_sky_material.set_shader_parameter("cloud_coverage", 0.0)
_sky_material.set_shader_parameter("cloud_opacity", 0.0)
if _storm_clouds != null:
_storm_clouds.set_storm_amount(
_weather_value(0.0, 0.42, 0.88, 0.0),
lower_cloud_color,
)
return
_sky_material.set_shader_parameter(
"cloud_coverage",
_weather_value(0.0, 0.58, 0.985, 0.0),