diff --git a/docs/PORTMASTER.md b/docs/PORTMASTER.md index 2cd6038..70f7c0e 100644 --- a/docs/PORTMASTER.md +++ b/docs/PORTMASTER.md @@ -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. diff --git a/main/main.gd b/main/main.gd index b211dfb..d471851 100644 --- a/main/main.gd +++ b/main/main.gd @@ -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 diff --git a/main/runtime_performance_profile.gd b/main/runtime_performance_profile.gd new file mode 100644 index 0000000..cd6395e --- /dev/null +++ b/main/runtime_performance_profile.gd @@ -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 + ) diff --git a/main/runtime_performance_profile.gd.uid b/main/runtime_performance_profile.gd.uid new file mode 100644 index 0000000..3f9e338 --- /dev/null +++ b/main/runtime_performance_profile.gd.uid @@ -0,0 +1 @@ +uid://8vm104r2264h diff --git a/main/world_pixelation_postprocess.gd b/main/world_pixelation_postprocess.gd index 303fa3e..06b6e1b 100644 --- a/main/world_pixelation_postprocess.gd +++ b/main/world_pixelation_postprocess.gd @@ -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 diff --git a/scripts/build_portmaster.sh b/scripts/build_portmaster.sh index 198cba5..194b8be 100755 --- a/scripts/build_portmaster.sh +++ b/scripts/build_portmaster.sh @@ -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 diff --git a/scripts/portmaster/NETfishing.sh b/scripts/portmaster/NETfishing.sh index 58fbfbd..97b73ef 100755 --- a/scripts/portmaster/NETfishing.sh +++ b/scripts/portmaster/NETfishing.sh @@ -78,17 +78,36 @@ fi NETFISHING_GODOT_OPTIONS=() NETFISHING_GAME_ENVIRONMENT=() -DEVICE_COMPATIBILITY="" -if [[ -r /proc/device-tree/compatible ]]; then - DEVICE_COMPATIBILITY="$(tr '\0' ' ' &2 + NETFISHING_GAME_ENVIRONMENT+=( + "NETFISHING_PERFORMANCE_PROFILE=light" + "NETFISHING_LOW_END=1" + ) NETFISHING_GODOT_OPTIONS+=( --single-window --disable-vsync diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index 0c7fbae..78f4f08 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -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" diff --git a/tests/controller_ui_navigation_validation.gd b/tests/controller_ui_navigation_validation.gd index 7cecbab..6bfe7f7 100644 --- a/tests/controller_ui_navigation_validation.gd +++ b/tests/controller_ui_navigation_validation.gd @@ -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")) diff --git a/tests/light_performance_profile_validation.gd b/tests/light_performance_profile_validation.gd new file mode 100644 index 0000000..2f8b9b9 --- /dev/null +++ b/tests/light_performance_profile_validation.gd @@ -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") + ))) diff --git a/tests/light_performance_profile_validation.gd.uid b/tests/light_performance_profile_validation.gd.uid new file mode 100644 index 0000000..3495021 --- /dev/null +++ b/tests/light_performance_profile_validation.gd.uid @@ -0,0 +1 @@ +uid://c6xqcslinxd1a diff --git a/world/environment/local_storm_cloud_layer.gd b/world/environment/local_storm_cloud_layer.gd index 2ec0008..1b58fb7 100644 --- a/world/environment/local_storm_cloud_layer.gd +++ b/world/environment/local_storm_cloud_layer.gd @@ -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() diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index f3dd9e0..adb0fab 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -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, diff --git a/world/test_world.gd b/world/test_world.gd index 236d8bd..e40c3d3 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -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(): diff --git a/world/water_surface_motion.gd b/world/water_surface_motion.gd index 992f9cb..02e397e 100644 --- a/world/water_surface_motion.gd +++ b/world/water_surface_motion.gd @@ -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(), diff --git a/world/world_time_visual_controller.gd b/world/world_time_visual_controller.gd index 99b0429..e4e2166 100644 --- a/world/world_time_visual_controller.gd +++ b/world/world_time_visual_controller.gd @@ -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),