diff --git a/addons/straywild_shoreline_baker/plugin.cfg b/addons/straywild_shoreline_baker/plugin.cfg deleted file mode 100644 index c3f4e47..0000000 --- a/addons/straywild_shoreline_baker/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="straywild Shoreline Auto-Baker" -description="Rebuilds configured static shoreline ribbons after authored terrain reimports." -author="woofmeow" -version="1.0.0" -script="shoreline_auto_baker.gd" diff --git a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd b/addons/straywild_shoreline_baker/shoreline_auto_baker.gd deleted file mode 100644 index 18da4af..0000000 --- a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd +++ /dev/null @@ -1,66 +0,0 @@ -@tool -extends EditorPlugin - -const TERRAIN_RESOURCE := ( - "res://art/exported/environment/terrain/starter_island.glb" -) -const REGION_SCENE := "res://world/regions/starter_island_region.tscn" - -var _bake_pending := false - - -func _enter_tree() -> void: - var filesystem := get_editor_interface().get_resource_filesystem() - if not filesystem.resources_reimported.is_connected(_on_resources_reimported): - filesystem.resources_reimported.connect(_on_resources_reimported) - - -func _exit_tree() -> void: - var filesystem := get_editor_interface().get_resource_filesystem() - if filesystem.resources_reimported.is_connected(_on_resources_reimported): - filesystem.resources_reimported.disconnect(_on_resources_reimported) - - -func _on_resources_reimported(resources: PackedStringArray) -> void: - if not resources.has(TERRAIN_RESOURCE) or _bake_pending: - return - _bake_pending = true - call_deferred("_bake_configured_shorelines") - - -func _bake_configured_shorelines() -> void: - _bake_pending = false - var packed_region := ResourceLoader.load( - REGION_SCENE, - "PackedScene", - ResourceLoader.CACHE_MODE_REPLACE, - ) as PackedScene - if packed_region == null: - push_error("Shoreline auto-bake could not load %s." % REGION_SCENE) - return - var region := packed_region.instantiate() as Node3D - add_child(region) - var baker := region.get_node_or_null( - "ShorelineRibbonBaker" - ) as ShorelineRibbonBaker - if baker == null: - push_error("Shoreline auto-bake found no configured baker.") - region.queue_free() - return - var results := baker.rebuild_all() - for result: Dictionary in results: - if result.get("skipped", false): - continue - var output_path: String = result.get("output_path", "") - if not output_path.is_empty(): - ResourceLoader.load( - output_path, - "ArrayMesh", - ResourceLoader.CACHE_MODE_REPLACE, - ) - print( - "Shoreline auto-bake: %s, %d loops, %d triangles" - % [output_path, result.loop_count, result.triangle_count] - ) - region.queue_free() - get_editor_interface().get_resource_filesystem().scan() diff --git a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid b/addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid deleted file mode 100644 index a143fcb..0000000 --- a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cbk4xpalei5v3 diff --git a/docs/ATTRIBUTION.md b/docs/ATTRIBUTION.md index d8676f0..e4ab55b 100644 --- a/docs/ATTRIBUTION.md +++ b/docs/ATTRIBUTION.md @@ -309,9 +309,9 @@ revision-specific inventory. ## Generated resources -Godot `.import` sidecars and deterministic shoreline `.tres` meshes are derived -repository resources, not original artwork. `.godot/imported/`, editor caches, -captures, exports, and temporary test data are not tracked. +Godot `.import` sidecars and other deterministic generated resources are +derived repository resources, not original artwork. `.godot/imported/`, editor +caches, captures, exports, and temporary test data are not tracked. ## Release gate diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a5ebc03..25d3549 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -233,12 +233,10 @@ materials local to the scene. ### Shoreline presentation -The visible shoreline treatment is currently produced by the water materials. -The older smooth-ribbon baker and generated starter-island ribbon resource are -retained as development history/tooling, but the ribbon mesh is disabled and -is not part of the current rendered shoreline. Do not treat a ribbon rebuild -as a normal terrain-authoring requirement or re-enable the mesh as incidental -cleanup. +The visible shoreline treatment is produced entirely by the water materials. +It does not require supplemental shoreline meshes or generation tooling. Keep +shoreline foam and edge-effect work in the water materials rather than adding +terrain-authoring requirements. ### Facial-feature textures diff --git a/docs/PORTMASTER.md b/docs/PORTMASTER.md index 262f930..374ca22 100644 --- a/docs/PORTMASTER.md +++ b/docs/PORTMASTER.md @@ -235,7 +235,7 @@ profile also: - keeps the title water and full-window menu patterns static; - replaces the animated cooler water and decorative bubbles with flat water; - disables decorative title-screen fish and bubbles; -- retains title and dusk music while omitting rain and shoreline ambience; +- retains title and dusk music while omitting rain ambience; - reduces rain to 48 particles simulated at 8 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 20120d6..6cacbca 100644 --- a/main/main.gd +++ b/main/main.gd @@ -119,6 +119,9 @@ const NetworkWorldWeatherServiceType = preload( "res://network/network_world_weather_service.gd" ) const RainAmbienceType = preload("res://world/rain_ambience.gd") +const RainPuddlePresentationType = preload( + "res://world/rain_puddle_presentation.gd" +) const PlayerJobServiceType = preload("res://jobs/player_job_service.gd") const NetworkJobServiceType = preload("res://network/network_job_service.gd") const GatherableCatalogType = preload( @@ -309,8 +312,8 @@ var _home_entry_transition_active: bool = false var _home_entry_walk_tween: Tween var _observed_cancel_input_frame: int = -1 -@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience var _rain_ambience: RainAmbienceType +var _rain_puddles: RainPuddlePresentationType func _ready() -> void: @@ -337,11 +340,10 @@ func _ready() -> void: _rain_ambience.name = "RainAmbience" add_child(_rain_ambience) _rain_ambience.configure(_world_weather) + _rain_puddles = RainPuddlePresentationType.new() + _rain_puddles.name = "RainPuddlePresentation" + add_child(_rain_puddles) _configure_audio_performance_profile(_performance_profile.is_light()) - _shoreline_ambience.configure( - _player, - _test_world.get_saltwater_shoreline_mesh(), - ) if not _settings_manager.settings_changed.is_connected( _apply_runtime_settings ): @@ -376,10 +378,8 @@ func _configure_audio_performance_profile(light_profile: bool) -> void: _new_game_music.stream = _load_optional_audio_stream(NEW_GAME_MUSIC_PATH) _dusk_music.stream = _load_optional_audio_stream(DUSK_MUSIC_PATH) if light_profile: - _shoreline_ambience.set_audio_enabled(false) _rain_ambience.set_audio_enabled(false) return - _shoreline_ambience.set_audio_enabled(true) _rain_ambience.set_audio_enabled(true) @@ -439,7 +439,6 @@ func _disable_dedicated_presentation_tree() -> void: NodePath("TitleMusic"), NodePath("NewGameMusic"), NodePath("DuskMusic"), - NodePath("ShorelineAmbience"), ]: var audio_node: Node = get_node_or_null(audio_path) if audio_node == null: @@ -674,6 +673,12 @@ func _initialize_application(dedicated: bool) -> void: Callable(_player, "get_active_gameplay_camera"), _performance_profile.is_light(), ) + _rain_puddles.configure( + _test_world, + _world_weather, + _player, + _performance_profile.is_light(), + ) if not _world_weather.weather_changed.is_connected( _on_world_weather_changed ): @@ -1926,8 +1931,8 @@ func _set_gameplay_active(active: bool) -> void: _end_shop_npc_conversation(true) _cancel_home_entry_transition() _cancel_home_exit_transition(true) - _shoreline_ambience.set_active(active) _rain_ambience.set_active(active) + _rain_puddles.set_active(active) if active and not was_gameplay_started: var current_hour: float = _world_time.get_time_hours() _dusk_music_played_for_natural_day = ( @@ -2641,10 +2646,7 @@ func _apply_world( _test_world.get_safe_respawn_points(), Callable(_test_world, "get_water_recovery_position"), ) - _shoreline_ambience.configure( - _player, - _test_world.get_saltwater_shoreline_mesh(), - ) + _rain_puddles.refresh_world() return true @@ -3037,7 +3039,7 @@ func _on_local_home_space_changed( var inside_rv: bool = String(space_id).begins_with("rv:") _world_time_visuals.set_local_home_interior_active(inside_rv) _rain_ambience.set_suppressed(inside_rv) - _shoreline_ambience.set_suppressed(inside_rv) + _rain_puddles.set_suppressed(inside_rv) _surface_drawings_root.visible = not inside_rv _world_gatherables_root.visible = not inside_rv _refresh_active_hotbar_item() diff --git a/main/main.tscn b/main/main.tscn index aeea011..a86500f 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -49,7 +49,6 @@ [ext_resource type="Script" uid="uid://llj46fx0oco7" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"] [ext_resource type="Script" uid="uid://daqtje84gqgu0" path="res://jobs/player_job_service.gd" id="52_player_jobs"] [ext_resource type="Script" uid="uid://drlyytr2o3uw8" path="res://network/network_job_service.gd" id="53_network_jobs"] -[ext_resource type="Script" uid="uid://b0c3o2vy76fg3" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"] [ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"] [ext_resource type="Script" path="res://settings/keyboard_mouse_mapping_manager.gd" id="keyboard_mouse_mapping"] [ext_resource type="Script" uid="uid://d0jv8n2nfqia0" path="res://network/discovery_client.gd" id="57_discovery"] @@ -353,14 +352,6 @@ bus = &"Music" unique_name_in_owner = true bus = &"Music" -[node name="ShorelineAmbience" type="Node" parent="." unique_id=1948037177] -unique_name_in_owner = true -script = ExtResource("54_shoreline_ambience") - -[node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience" unique_id=193530658] -unique_name_in_owner = true -bus = &"Environment" - [node name="WorldPixelationPostprocess" parent="." unique_id=344378000 instance=ExtResource("16_world_pixelation")] unique_name_in_owner = true diff --git a/project.godot b/project.godot index 727a49a..559f8f4 100644 --- a/project.godot +++ b/project.godot @@ -24,10 +24,6 @@ TextureSamplingPolicy="*res://main/texture_sampling_policy.gd" window/stretch/aspect="expand" -[editor_plugins] - -enabled=PackedStringArray("res://addons/straywild_shoreline_baker/plugin.cfg") - [gui] timers/tooltip_delay_sec=0.0 diff --git a/scripts/bake_shoreline_ribbons.gd b/scripts/bake_shoreline_ribbons.gd deleted file mode 100644 index c030579..0000000 --- a/scripts/bake_shoreline_ribbons.gd +++ /dev/null @@ -1,42 +0,0 @@ -extends SceneTree - -const REGION_SCENE := preload("res://world/regions/starter_island_region.tscn") -func _initialize() -> void: - call_deferred("_bake") - - -func _bake() -> void: - var started := Time.get_ticks_usec() - var region := REGION_SCENE.instantiate() - root.add_child(region) - var baker := region.get_node_or_null("ShorelineRibbonBaker") as ShorelineRibbonBaker - if baker == null: - push_error("The map does not contain a configured ShorelineRibbonBaker.") - quit(1) - return - var results := baker.rebuild_all() - if results.size() != baker.water_bodies.size(): - quit(1) - return - for result: Dictionary in results: - if result.get("skipped", false): - print( - "Shoreline %s: type=%s skipped intentionally" - % [result["output_path"], WaterType.label(result["water_type"])] - ) - continue - print( - "Shoreline %s: type=%s segments=%d loops=%d raw=%d simplified=%d smoothed=%d triangles=%d" - % [ - result["output_path"], - WaterType.label(result["water_type"]), - result["segment_count"], - result["loop_count"], - result["raw_point_count"], - result["simplified_point_count"], - result["smoothed_point_count"], - result["triangle_count"], - ] - ) - print("Shoreline bake completed in %.2f ms" % ((Time.get_ticks_usec() - started) / 1000.0)) - quit() diff --git a/scripts/bake_shoreline_ribbons.gd.uid b/scripts/bake_shoreline_ribbons.gd.uid deleted file mode 100644 index 7b789be..0000000 --- a/scripts/bake_shoreline_ribbons.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://eosavaa6wu1e diff --git a/scripts/build_portmaster.sh b/scripts/build_portmaster.sh index cb4c101..da7dabc 100755 --- a/scripts/build_portmaster.sh +++ b/scripts/build_portmaster.sh @@ -233,7 +233,7 @@ selected data root. Device-local configuration remains under PortMaster launches use the light performance profile by default. Put the word \`normal\` in \`straywild/conf/performance_profile\` to opt a capable device into the normal rendering profile. PortMaster packages retain title and world -music while omitting rain and shoreline ambience in both profiles. See +music while omitting rain ambience in both profiles. See \`straywild/licenses/\` for bundled credits and license information. ## Controls diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index f71cb60..64cfb31 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -45,7 +45,6 @@ readonly -a QUICK_TESTS=( "tests/player_gathering_animation_validation.gd" "tests/player_experience_validation.gd" "tests/profile_title_validation.gd" - "tests/shoreline_ambience_validation.gd" "tests/surface_drawing_validation.gd" "tests/surface_drawing_performance_validation.gd" "tests/surface_drawing_texture_renderer_validation.gd" diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 8ae45bf..1ac593d 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -505,7 +505,7 @@ func _run() -> void: "trailblazer", )) var remote_transform: Transform3D = player.global_transform - remote_transform.origin += Vector3(0.8, 0.0, 0.0) + remote_transform.origin += Vector3(20.0, 0.0, 0.0) var remote_avatar := spawn_service.spawn_remote_player( NAMEPLATE_TEST_PEER_ID, remote_transform, @@ -534,6 +534,29 @@ func _run() -> void: assert(remote_plate.visible) assert((remote_nameplate.get("name") as Label).text == "remote stray") assert((remote_nameplate.get("title") as Label).text == "trailblazer") + chat_ui.call( + "_show_speech_bubble", + NAMEPLATE_TEST_PEER_ID, + "distant hello", + "", + ) + await process_frame + chat_ui.call("_update_speech") + var remote_speech_entries: Dictionary = chat_ui.get("_speech") + var remote_speech: Dictionary = remote_speech_entries.get( + NAMEPLATE_TEST_PEER_ID, + {}, + ) + var remote_speech_bubble := remote_speech.get("bubble") as PanelContainer + assert(remote_speech_bubble != null and remote_speech_bubble.visible) + assert(is_equal_approx( + remote_speech_bubble.scale.x, + ChatUI.speech_scale_for_distance( + remote_avatar.global_position.distance_to(player.global_position) + ), + )) + assert(remote_speech_bubble.scale.x < 1.0) + chat_ui.call("_on_peer_removed", NAMEPLATE_TEST_PEER_ID) spawn_service.remove_peer(NAMEPLATE_TEST_PEER_ID) registry.remove_peer(NAMEPLATE_TEST_PEER_ID) chat_ui.call("_update_nameplates") diff --git a/tests/fish_catalog_content_validation.gd b/tests/fish_catalog_content_validation.gd index 82f5604..2c635c6 100644 --- a/tests/fish_catalog_content_validation.gd +++ b/tests/fish_catalog_content_validation.gd @@ -524,21 +524,16 @@ func _validate_starter_water_bodies() -> void: assert(ocean.water_type == WaterType.Type.SALT_WATER) assert(pond.fish_pool == PondPool) assert(ocean.fish_pool == OceanPool) - assert(region.get_node_or_null("ShorelineRibbons/Pond") == null) - var shoreline_geometry := ( - region.get_node_or_null("ShorelineRibbons/Ocean") as MeshInstance3D - ) - assert(shoreline_geometry != null) - assert(not shoreline_geometry.visible) - assert(shoreline_geometry.material_override == null) var pond_material := ( region.get_node("WaterBodies/Pond/VisualWater") as MeshInstance3D ).material_override as ShaderMaterial var ocean_material := ( region.get_node("WaterBodies/Ocean/VisualWater") as MeshInstance3D ).material_override as ShaderMaterial - assert(not bool(pond_material.get_shader_parameter("tide_effect_enabled"))) - assert(bool(ocean_material.get_shader_parameter("tide_effect_enabled"))) + assert(pond_material != null) + assert(ocean_material != null) + assert(float(ocean_material.get_shader_parameter("shoreline_foam_depth")) > 0.0) + assert(float(ocean_material.get_shader_parameter("shoreline_foam_strength")) > 0.0) region.free() diff --git a/tests/light_performance_profile_validation.gd b/tests/light_performance_profile_validation.gd index ada8f93..2a06fc9 100644 --- a/tests/light_performance_profile_validation.gd +++ b/tests/light_performance_profile_validation.gd @@ -131,9 +131,6 @@ func _validate_normal_profile(main: Node) -> void: assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null) assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null) assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null) - assert( - (main.get_node("%WavesAudio") as AudioStreamPlayer).stream != null - ) assert( (main.get_node("RainAmbience") as AudioStreamPlayer).stream != null ) diff --git a/tests/profile_multiplayer_validation.gd b/tests/profile_multiplayer_validation.gd index c279721..b4ff5d0 100644 --- a/tests/profile_multiplayer_validation.gd +++ b/tests/profile_multiplayer_validation.gd @@ -40,6 +40,15 @@ func _validate_voice_distance_attenuation() -> void: ChatUI.animalese_volume_offset_db_for_distance(24.0), -80.0, )) + assert(is_equal_approx(ChatUI.speech_scale_for_distance(0.0), 1.0)) + assert(is_equal_approx(ChatUI.speech_scale_for_distance(4.0), 1.0)) + var middle_distance_scale := ChatUI.speech_scale_for_distance(14.0) + assert(middle_distance_scale < 1.0) + assert(middle_distance_scale > ChatUI.SPEECH_MINIMUM_SCALE) + assert(is_equal_approx( + ChatUI.speech_scale_for_distance(24.0), + ChatUI.SPEECH_MINIMUM_SCALE, + )) func _run_host() -> void: diff --git a/tests/rain_puddle_runtime_validation.gd b/tests/rain_puddle_runtime_validation.gd new file mode 100644 index 0000000..6456276 --- /dev/null +++ b/tests/rain_puddle_runtime_validation.gd @@ -0,0 +1,84 @@ +extends SceneTree + +const MainScene: PackedScene = preload("res://main/main.tscn") + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + root.size = Vector2i(1280, 720) + 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"))) + assert(bool(main.call( + "_apply_world", + WorldLayout.GENERATED, + PlayerSaveManager.DEFAULT_WORLD_SEED, + true, + ))) + var weather := main.get_node( + "%WorldWeatherService" + ) as WorldWeatherService + assert(weather != null) + weather.apply_authoritative_snapshot( + WorldWeatherService.Weather.RAINY, + 60.0, + ) + main.call("_set_gameplay_active", true) + await physics_frame + await physics_frame + + var puddles := main.get_node( + "RainPuddlePresentation" + ) as RainPuddlePresentation + assert(puddles != null) + assert(not puddles.is_light_performance_profile()) + assert(puddles.is_processing()) + puddles.set_suppressed(true) + assert(not puddles.is_processing()) + puddles.set_suppressed(false) + assert(puddles.is_processing()) + puddles.set_active(false) + assert(not puddles.is_processing()) + puddles.set_active(true) + assert(puddles.is_processing()) + for _tick: int in 18: + puddles.call("_process", 0.25) + assert(puddles.get_active_puddle_count() > 0) + var visual_root := puddles.get_node("PuddleVisuals") as Node3D + assert(visual_root != null) + for puddle_visual: Node in visual_root.get_children(): + assert(puddle_visual is MeshInstance3D) + assert(puddle_visual.get_child_count() == 0) + + weather.apply_authoritative_snapshot( + WorldWeatherService.Weather.SUNNY, + 60.0, + ) + puddles.call("_process", 30.0) + assert(puddles.get_active_puddle_count() == 0) + _stop_audio_players(main) + main.queue_free() + for _frame: int in 8: + await process_frame + print("Rain puddle runtime validation: PASS") + quit() + + +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) diff --git a/tests/rain_puddle_runtime_validation.gd.uid b/tests/rain_puddle_runtime_validation.gd.uid new file mode 100644 index 0000000..2937f9c --- /dev/null +++ b/tests/rain_puddle_runtime_validation.gd.uid @@ -0,0 +1 @@ +uid://jo5lqk2n8bik diff --git a/tests/shoreline_ambience_validation.gd b/tests/shoreline_ambience_validation.gd deleted file mode 100644 index 78829f1..0000000 --- a/tests/shoreline_ambience_validation.gd +++ /dev/null @@ -1,50 +0,0 @@ -extends SceneTree - -const ShorelineAmbienceType := preload("res://world/shoreline_ambience.gd") -const WavesStream := preload("res://audio/ambience/waves.wav") - - -func _initialize() -> void: - call_deferred("_run") - - -func _run() -> void: - var waves := WavesStream as AudioStreamWAV - assert(waves != null) - - var segments: Array[PackedVector2Array] = [ - PackedVector2Array([Vector2.ZERO, Vector2(10.0, 0.0)]), - ] - assert(is_equal_approx( - ShorelineAmbienceType.distance_to_segments(Vector2(5.0, 3.0), segments), - 3.0, - )) - assert(is_equal_approx( - ShorelineAmbienceType.distance_to_segments(Vector2(-4.0, 0.0), segments), - 4.0, - )) - - var controller := ShorelineAmbienceType.new() as ShorelineAmbience - var runtime_audio := AudioStreamPlayer.new() - runtime_audio.name = "WavesAudio" - runtime_audio.unique_name_in_owner = true - runtime_audio.stream = WavesStream - runtime_audio.bus = &"SFX" - controller.add_child(runtime_audio) - runtime_audio.owner = controller - root.add_child(controller) - await process_frame - assert(runtime_audio.bus == &"Environment") - assert(runtime_audio.stream == null) - controller.set_audio_enabled(true) - assert(controller.near_distance < controller.far_distance) - assert(controller.near_volume_db > controller.far_volume_db) - var runtime_waves := runtime_audio.stream as AudioStreamWAV - assert(runtime_waves.loop_mode == AudioStreamWAV.LOOP_FORWARD) - assert(runtime_waves.loop_begin == 0) - assert(runtime_waves.loop_end == 1044956) - controller.free() - await process_frame - - print("Shoreline ambience validation: PASS") - quit() diff --git a/tests/shoreline_ambience_validation.gd.uid b/tests/shoreline_ambience_validation.gd.uid deleted file mode 100644 index 4b7987b..0000000 --- a/tests/shoreline_ambience_validation.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwvhgpbkb00l1 diff --git a/tests/shoreline_ribbon_validation.gd b/tests/shoreline_ribbon_validation.gd deleted file mode 100644 index e502d70..0000000 --- a/tests/shoreline_ribbon_validation.gd +++ /dev/null @@ -1,97 +0,0 @@ -extends SceneTree - -const ShorelineMesh: ArrayMesh = preload( - "res://world/generated/shorelines/starter_ocean_shoreline.tres" -) -const GEOMETRY_EPSILON := 0.0001 -const CROSSING_NEIGHBORHOOD := 32 - - -func _initialize() -> void: - call_deferred("_run") - - -func _run() -> void: - assert(ShorelineMesh.get_surface_count() == 1) - var arrays := ShorelineMesh.surface_get_arrays(0) - var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array - var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array - assert(not vertices.is_empty()) - assert(vertices.size() % 2 == 0) - assert(indices.size() == vertices.size() * 3) - var point_count: int = int(float(vertices.size()) / 2.0) - var land_edge := PackedVector2Array() - var water_edge := PackedVector2Array() - for index: int in point_count: - var land := vertices[index * 2] - var water := vertices[index * 2 + 1] - land_edge.append(Vector2(land.x, land.z)) - water_edge.append(Vector2(water.x, water.z)) - var ribbon_width := land.distance_to(water) - assert(ribbon_width >= 0.34) - assert(ribbon_width <= 1.5) - var crossing_count := ( - _count_local_crossings(water_edge) - + _count_local_crossings(land_edge) - ) - if crossing_count > 0: - push_error( - "Shoreline ribbon contains %d local edge crossings." - % crossing_count - ) - quit(1) - return - print("Shoreline ribbon validation: PASS") - quit() - - -func _count_local_crossings(points: PackedVector2Array) -> int: - var crossing_count := 0 - for first: int in points.size(): - var first_next := (first + 1) % points.size() - var checked_neighbors := mini( - CROSSING_NEIGHBORHOOD, - points.size() - 2, - ) - for offset: int in range(2, checked_neighbors + 1): - var second := (first + offset) % points.size() - var second_next := (second + 1) % points.size() - if second_next == first: - continue - if _segments_cross( - points[first], - points[first_next], - points[second], - points[second_next], - ): - crossing_count += 1 - return crossing_count - - -func _segments_cross( - a: Vector2, - b: Vector2, - c: Vector2, - d: Vector2, -) -> bool: - if ( - maxf(a.x, b.x) < minf(c.x, d.x) - GEOMETRY_EPSILON - or maxf(c.x, d.x) < minf(a.x, b.x) - GEOMETRY_EPSILON - or maxf(a.y, b.y) < minf(c.y, d.y) - GEOMETRY_EPSILON - or maxf(c.y, d.y) < minf(a.y, b.y) - GEOMETRY_EPSILON - ): - return false - var first_direction := b - a - var second_direction := d - c - var denominator := first_direction.cross(second_direction) - if absf(denominator) <= GEOMETRY_EPSILON: - return false - var offset := c - a - var first_amount := offset.cross(second_direction) / denominator - var second_amount := offset.cross(first_direction) / denominator - return ( - first_amount > GEOMETRY_EPSILON - and first_amount < 1.0 - GEOMETRY_EPSILON - and second_amount > GEOMETRY_EPSILON - and second_amount < 1.0 - GEOMETRY_EPSILON - ) diff --git a/tests/shoreline_ribbon_validation.gd.uid b/tests/shoreline_ribbon_validation.gd.uid deleted file mode 100644 index 7989d89..0000000 --- a/tests/shoreline_ribbon_validation.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c73frygpfhred diff --git a/tests/world_weather_validation.gd b/tests/world_weather_validation.gd index f3c41a3..beb3541 100644 --- a/tests/world_weather_validation.gd +++ b/tests/world_weather_validation.gd @@ -19,6 +19,9 @@ const FishAvailabilityType = preload("res://fish/fish_availability.gd") const FishableWaterRegionType = preload( "res://world/fishable_water_region.gd" ) +const RainPuddlePresentationType = preload( + "res://world/rain_puddle_presentation.gd" +) func _initialize() -> void: @@ -34,6 +37,7 @@ func _run() -> void: _validate_fishing_weather_seams() _validate_fishing_spot_context() _validate_weather_presentation() + _validate_rain_puddle_surface_rules() print("World weather validation: PASS") quit() @@ -285,6 +289,53 @@ func _validate_fishing_spot_context() -> void: weather.free() +func _validate_rain_puddle_surface_rules() -> void: + var dry_flat := PackedVector3Array([ + Vector3(-1.0, 0.0, -1.0), + Vector3(2.0, 0.0, -1.0), + Vector3(-1.0, 0.0, 2.0), + ]) + assert(RainPuddlePresentationType.is_puddle_surface_eligible( + dry_flat, + -0.25, + )) + var water_edge := PackedVector3Array([ + Vector3(-1.0, 0.0, -1.0), + Vector3(2.0, 0.0, -1.0), + Vector3(-1.0, -0.2, 2.0), + ]) + assert(not RainPuddlePresentationType.is_puddle_surface_eligible( + water_edge, + -0.25, + )) + var steep_ground := PackedVector3Array([ + Vector3(-1.0, 0.0, -1.0), + Vector3(2.0, 0.0, -1.0), + Vector3(-1.0, 2.0, -0.75), + ]) + assert(not RainPuddlePresentationType.is_puddle_surface_eligible( + steep_ground, + -0.25, + )) + var center := Vector3(4.0, 1.0, -3.0) + var radii := Vector2(1.0, 0.5) + assert(RainPuddlePresentationType.is_point_inside_puddle( + Vector3(4.75, 1.0, -3.0), + center, + radii, + )) + assert(not RainPuddlePresentationType.is_point_inside_puddle( + Vector3(5.1, 1.0, -3.0), + center, + radii, + )) + assert(not RainPuddlePresentationType.is_point_inside_puddle( + Vector3(4.0, 2.1, -3.0), + center, + radii, + )) + + func _validate_weather_presentation() -> void: var world_root := Node3D.new() root.add_child(world_root) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 17b3330..4cf88a0 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -39,6 +39,7 @@ const NAMEPLATE_SPEECH_GAP: float = 5.0 const ANIMALESE_FULL_VOLUME_DISTANCE: float = 4.0 const ANIMALESE_SILENT_DISTANCE: float = 24.0 const ANIMALESE_SILENT_VOLUME_DB: float = -80.0 +const SPEECH_MINIMUM_SCALE: float = 0.60 const ROLEPLAY_FONT_SLANT: float = -0.18 const MAX_EDITOR_GIVE_BALANCE: int = 1_000_000_000_000 const MOBILE_COMPACT_WIDTH: float = 620.0 @@ -1357,6 +1358,19 @@ static func animalese_volume_offset_db_for_distance(distance: float) -> float: return maxf(linear_to_db(amplitude), ANIMALESE_SILENT_VOLUME_DB) +static func speech_scale_for_distance(distance: float) -> float: + if not is_finite(distance) or distance >= ANIMALESE_SILENT_DISTANCE: + return SPEECH_MINIMUM_SCALE + if distance <= ANIMALESE_FULL_VOLUME_DISTANCE: + return 1.0 + var distance_weight := inverse_lerp( + ANIMALESE_FULL_VOLUME_DISTANCE, + ANIMALESE_SILENT_DISTANCE, + distance, + ) + return lerpf(1.0, SPEECH_MINIMUM_SCALE, distance_weight) + + func _on_history(messages: Array) -> void: for peer_id: int in _speech.keys(): var state: Dictionary = _speech[peer_id] @@ -1841,6 +1855,10 @@ func _update_speech() -> void: bubble.hide() continue var world_position := avatar.get_nameplate_anchor_position() + var bubble_scale := speech_scale_for_distance( + avatar.global_position.distance_to(_player.global_position) + ) + bubble.scale = Vector2.ONE * bubble_scale if camera.is_position_behind(world_position): bubble.hide() continue @@ -1860,18 +1878,20 @@ func _update_speech() -> void: ): bubble.hide() continue + var visual_size := bubble.size * bubble_scale + var visual_pointer_height := SPEECH_POINTER_HEIGHT * bubble_scale var desired := screen_position - Vector2( - bubble.size.x * 0.5, - bubble.size.y + SPEECH_POINTER_HEIGHT, + visual_size.x * 0.5, + visual_size.y + visual_pointer_height, ) bubble.position = Vector2( - clampf(desired.x, 8.0, viewport_size.x - bubble.size.x - 8.0), + clampf(desired.x, 8.0, viewport_size.x - visual_size.x - 8.0), clampf( desired.y, 8.0, viewport_size.y - - bubble.size.y - - SPEECH_POINTER_HEIGHT + - visual_size.y + - visual_pointer_height - 8.0, ), ) @@ -1879,7 +1899,7 @@ func _update_speech() -> void: if pointer != null: pointer.position = Vector2( clampf( - screen_position.x - bubble.position.x, + (screen_position.x - bubble.position.x) / bubble_scale, SPEECH_POINTER_HALF_WIDTH + 12.0, bubble.size.x - SPEECH_POINTER_HALF_WIDTH - 12.0, ), diff --git a/world/generated/shorelines/starter_ocean_shoreline.tres b/world/generated/shorelines/starter_ocean_shoreline.tres deleted file mode 100644 index 627b08e..0000000 --- a/world/generated/shorelines/starter_ocean_shoreline.tres +++ /dev/null @@ -1,14 +0,0 @@ -[gd_resource type="ArrayMesh" format=4 uid="uid://dmgps6rhbdjlw"] - -[resource] -_surfaces = [{ -"aabb": AABB(-27.86245, -0.432, -26.431866, 54.402885, 1e-05, 51.728455), -"attribute_data": PackedByteArray("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"), -"format": 34359742487, -"index_count": 6588, -"index_data": PackedByteArray("AAACAAEAAQACAAMAAgAEAAMAAwAEAAUABAAGAAUABQAGAAcABgAIAAcABwAIAAkACAAKAAkACQAKAAsACgAMAAsACwAMAA0ADAAOAA0ADQAOAA8ADgAQAA8ADwAQABEAEAASABEAEQASABMAEgAUABMAEwAUABUAFAAWABUAFQAWABcAFgAYABcAFwAYABkAGAAaABkAGQAaABsAGgAcABsAGwAcAB0AHAAeAB0AHQAeAB8AHgAgAB8AHwAgACEAIAAiACEAIQAiACMAIgAkACMAIwAkACUAJAAmACUAJQAmACcAJgAoACcAJwAoACkAKAAqACkAKQAqACsAKgAsACsAKwAsAC0ALAAuAC0ALQAuAC8ALgAwAC8ALwAwADEAMAAyADEAMQAyADMAMgA0ADMAMwA0ADUANAA2ADUANQA2ADcANgA4ADcANwA4ADkAOAA6ADkAOQA6ADsAOgA8ADsAOwA8AD0APAA+AD0APQA+AD8APgBAAD8APwBAAEEAQABCAEEAQQBCAEMAQgBEAEMAQwBEAEUARABGAEUARQBGAEcARgBIAEcARwBIAEkASABKAEkASQBKAEsASgBMAEsASwBMAE0ATABOAE0ATQBOAE8ATgBQAE8ATwBQAFEAUABSAFEAUQBSAFMAUgBUAFMAUwBUAFUAVABWAFUAVQBWAFcAVgBYAFcAVwBYAFkAWABaAFkAWQBaAFsAWgBcAFsAWwBcAF0AXABeAF0AXQBeAF8AXgBgAF8AXwBgAGEAYABiAGEAYQBiAGMAYgBkAGMAYwBkAGUAZABmAGUAZQBmAGcAZgBoAGcAZwBoAGkAaABqAGkAaQBqAGsAagBsAGsAawBsAG0AbABuAG0AbQBuAG8AbgBwAG8AbwBwAHEAcAByAHEAcQByAHMAcgB0AHMAcwB0AHUAdAB2AHUAdQB2AHcAdgB4AHcAdwB4AHkAeAB6AHkAeQB6AHsAegB8AHsAewB8AH0AfAB+AH0AfQB+AH8AfgCAAH8AfwCAAIEAgACCAIEAgQCCAIMAggCEAIMAgwCEAIUAhACGAIUAhQCGAIcAhgCIAIcAhwCIAIkAiACKAIkAiQCKAIsAigCMAIsAiwCMAI0AjACOAI0AjQCOAI8AjgCQAI8AjwCQAJEAkACSAJEAkQCSAJMAkgCUAJMAkwCUAJUAlACWAJUAlQCWAJcAlgCYAJcAlwCYAJkAmACaAJkAmQCaAJsAmgCcAJsAmwCcAJ0AnACeAJ0AnQCeAJ8AngCgAJ8AnwCgAKEAoACiAKEAoQCiAKMAogCkAKMAowCkAKUApACmAKUApQCmAKcApgCoAKcApwCoAKkAqACqAKkAqQCqAKsAqgCsAKsAqwCsAK0ArACuAK0ArQCuAK8ArgCwAK8ArwCwALEAsACyALEAsQCyALMAsgC0ALMAswC0ALUAtAC2ALUAtQC2ALcAtgC4ALcAtwC4ALkAuAC6ALkAuQC6ALsAugC8ALsAuwC8AL0AvAC+AL0AvQC+AL8AvgDAAL8AvwDAAMEAwADCAMEAwQDCAMMAwgDEAMMAwwDEAMUAxADGAMUAxQDGAMcAxgDIAMcAxwDIAMkAyADKAMkAyQDKAMsAygDMAMsAywDMAM0AzADOAM0AzQDOAM8AzgDQAM8AzwDQANEA0ADSANEA0QDSANMA0gDUANMA0wDUANUA1ADWANUA1QDWANcA1gDYANcA1wDYANkA2ADaANkA2QDaANsA2gDcANsA2wDcAN0A3ADeAN0A3QDeAN8A3gDgAN8A3wDgAOEA4ADiAOEA4QDiAOMA4gDkAOMA4wDkAOUA5ADmAOUA5QDmAOcA5gDoAOcA5wDoAOkA6ADqAOkA6QDqAOsA6gDsAOsA6wDsAO0A7ADuAO0A7QDuAO8A7gDwAO8A7wDwAPEA8ADyAPEA8QDyAPMA8gD0APMA8wD0APUA9AD2APUA9QD2APcA9gD4APcA9wD4APkA+AD6APkA+QD6APsA+gD8APsA+wD8AP0A/AD+AP0A/QD+AP8A/gAAAf8A/wAAAQEBAAECAQEBAQECAQMBAgEEAQMBAwEEAQUBBAEGAQUBBQEGAQcBBgEIAQcBBwEIAQkBCAEKAQkBCQEKAQsBCgEMAQsBCwEMAQ0BDAEOAQ0BDQEOAQ8BDgEQAQ8BDwEQAREBEAESAREBEQESARMBEgEUARMBEwEUARUBFAEWARUBFQEWARcBFgEYARcBFwEYARkBGAEaARkBGQEaARsBGgEcARsBGwEcAR0BHAEeAR0BHQEeAR8BHgEgAR8BHwEgASEBIAEiASEBIQEiASMBIgEkASMBIwEkASUBJAEmASUBJQEmAScBJgEoAScBJwEoASkBKAEqASkBKQEqASsBKgEsASsBKwEsAS0BLAEuAS0BLQEuAS8BLgEwAS8BLwEwATEBMAEyATEBMQEyATMBMgE0ATMBMwE0ATUBNAE2ATUBNQE2ATcBNgE4ATcBNwE4ATkBOAE6ATkBOQE6ATsBOgE8ATsBOwE8AT0BPAE+AT0BPQE+AT8BPgFAAT8BPwFAAUEBQAFCAUEBQQFCAUMBQgFEAUMBQwFEAUUBRAFGAUUBRQFGAUcBRgFIAUcBRwFIAUkBSAFKAUkBSQFKAUsBSgFMAUsBSwFMAU0BTAFOAU0BTQFOAU8BTgFQAU8BTwFQAVEBUAFSAVEBUQFSAVMBUgFUAVMBUwFUAVUBVAFWAVUBVQFWAVcBVgFYAVcBVwFYAVkBWAFaAVkBWQFaAVsBWgFcAVsBWwFcAV0BXAFeAV0BXQFeAV8BXgFgAV8BXwFgAWEBYAFiAWEBYQFiAWMBYgFkAWMBYwFkAWUBZAFmAWUBZQFmAWcBZgFoAWcBZwFoAWkBaAFqAWkBaQFqAWsBagFsAWsBawFsAW0BbAFuAW0BbQFuAW8BbgFwAW8BbwFwAXEBcAFyAXEBcQFyAXMBcgF0AXMBcwF0AXUBdAF2AXUBdQF2AXcBdgF4AXcBdwF4AXkBeAF6AXkBeQF6AXsBegF8AXsBewF8AX0BfAF+AX0BfQF+AX8BfgGAAX8BfwGAAYEBgAGCAYEBgQGCAYMBggGEAYMBgwGEAYUBhAGGAYUBhQGGAYcBhgGIAYcBhwGIAYkBiAGKAYkBiQGKAYsBigGMAYsBiwGMAY0BjAGOAY0BjQGOAY8BjgGQAY8BjwGQAZEBkAGSAZEBkQGSAZMBkgGUAZMBkwGUAZUBlAGWAZUBlQGWAZcBlgGYAZcBlwGYAZkBmAGaAZkBmQGaAZsBmgGcAZsBmwGcAZ0BnAGeAZ0BnQGeAZ8BngGgAZ8BnwGgAaEBoAGiAaEBoQGiAaMBogGkAaMBowGkAaUBpAGmAaUBpQGmAacBpgGoAacBpwGoAakBqAGqAakBqQGqAasBqgGsAasBqwGsAa0BrAGuAa0BrQGuAa8BrgGwAa8BrwGwAbEBsAGyAbEBsQGyAbMBsgG0AbMBswG0AbUBtAG2AbUBtQG2AbcBtgG4AbcBtwG4AbkBuAG6AbkBuQG6AbsBugG8AbsBuwG8Ab0BvAG+Ab0BvQG+Ab8BvgHAAb8BvwHAAcEBwAHCAcEBwQHCAcMBwgHEAcMBwwHEAcUBxAHGAcUBxQHGAccBxgHIAccBxwHIAckByAHKAckByQHKAcsBygHMAcsBywHMAc0BzAHOAc0BzQHOAc8BzgHQAc8BzwHQAdEB0AHSAdEB0QHSAdMB0gHUAdMB0wHUAdUB1AHWAdUB1QHWAdcB1gHYAdcB1wHYAdkB2AHaAdkB2QHaAdsB2gHcAdsB2wHcAd0B3AHeAd0B3QHeAd8B3gHgAd8B3wHgAeEB4AHiAeEB4QHiAeMB4gHkAeMB4wHkAeUB5AHmAeUB5QHmAecB5gHoAecB5wHoAekB6AHqAekB6QHqAesB6gHsAesB6wHsAe0B7AHuAe0B7QHuAe8B7gHwAe8B7wHwAfEB8AHyAfEB8QHyAfMB8gH0AfMB8wH0AfUB9AH2AfUB9QH2AfcB9gH4AfcB9wH4AfkB+AH6AfkB+QH6AfsB+gH8AfsB+wH8Af0B/AH+Af0B/QH+Af8B/gEAAv8B/wEAAgECAAICAgECAQICAgMCAgIEAgMCAwIEAgUCBAIGAgUCBQIGAgcCBgIIAgcCBwIIAgkCCAIKAgkCCQIKAgsCCgIMAgsCCwIMAg0CDAIOAg0CDQIOAg8CDgIQAg8CDwIQAhECEAISAhECEQISAhMCEgIUAhMCEwIUAhUCFAIWAhUCFQIWAhcCFgIYAhcCFwIYAhkCGAIaAhkCGQIaAhsCGgIcAhsCGwIcAh0CHAIeAh0CHQIeAh8CHgIgAh8CHwIgAiECIAIiAiECIQIiAiMCIgIkAiMCIwIkAiUCJAImAiUCJQImAicCJgIoAicCJwIoAikCKAIqAikCKQIqAisCKgIsAisCKwIsAi0CLAIuAi0CLQIuAi8CLgIwAi8CLwIwAjECMAIyAjECMQIyAjMCMgI0AjMCMwI0AjUCNAI2AjUCNQI2AjcCNgI4AjcCNwI4AjkCOAI6AjkCOQI6AjsCOgI8AjsCOwI8Aj0CPAI+Aj0CPQI+Aj8CPgJAAj8CPwJAAkECQAJCAkECQQJCAkMCQgJEAkMCQwJEAkUCRAJGAkUCRQJGAkcCRgJIAkcCRwJIAkkCSAJKAkkCSQJKAksCSgJMAksCSwJMAk0CTAJOAk0CTQJOAk8CTgJQAk8CTwJQAlECUAJSAlECUQJSAlMCUgJUAlMCUwJUAlUCVAJWAlUCVQJWAlcCVgJYAlcCVwJYAlkCWAJaAlkCWQJaAlsCWgJcAlsCWwJcAl0CXAJeAl0CXQJeAl8CXgJgAl8CXwJgAmECYAJiAmECYQJiAmMCYgJkAmMCYwJkAmUCZAJmAmUCZQJmAmcCZgJoAmcCZwJoAmkCaAJqAmkCaQJqAmsCagJsAmsCawJsAm0CbAJuAm0CbQJuAm8CbgJwAm8CbwJwAnECcAJyAnECcQJyAnMCcgJ0AnMCcwJ0AnUCdAJ2AnUCdQJ2AncCdgJ4AncCdwJ4AnkCeAJ6AnkCeQJ6AnsCegJ8AnsCewJ8An0CfAJ+An0CfQJ+An8CfgKAAn8CfwKAAoECgAKCAoECgQKCAoMCggKEAoMCgwKEAoUChAKGAoUChQKGAocChgKIAocChwKIAokCiAKKAokCiQKKAosCigKMAosCiwKMAo0CjAKOAo0CjQKOAo8CjgKQAo8CjwKQApECkAKSApECkQKSApMCkgKUApMCkwKUApUClAKWApUClQKWApcClgKYApcClwKYApkCmAKaApkCmQKaApsCmgKcApsCmwKcAp0CnAKeAp0CnQKeAp8CngKgAp8CnwKgAqECoAKiAqECoQKiAqMCogKkAqMCowKkAqUCpAKmAqUCpQKmAqcCpgKoAqcCpwKoAqkCqAKqAqkCqQKqAqsCqgKsAqsCqwKsAq0CrAKuAq0CrQKuAq8CrgKwAq8CrwKwArECsAKyArECsQKyArMCsgK0ArMCswK0ArUCtAK2ArUCtQK2ArcCtgK4ArcCtwK4ArkCuAK6ArkCuQK6ArsCugK8ArsCuwK8Ar0CvAK+Ar0CvQK+Ar8CvgLAAr8CvwLAAsECwALCAsECwQLCAsMCwgLEAsMCwwLEAsUCxALGAsUCxQLGAscCxgLIAscCxwLIAskCyALKAskCyQLKAssCygLMAssCywLMAs0CzALOAs0CzQLOAs8CzgLQAs8CzwLQAtEC0ALSAtEC0QLSAtMC0gLUAtMC0wLUAtUC1ALWAtUC1QLWAtcC1gLYAtcC1wLYAtkC2ALaAtkC2QLaAtsC2gLcAtsC2wLcAt0C3ALeAt0C3QLeAt8C3gLgAt8C3wLgAuEC4ALiAuEC4QLiAuMC4gLkAuMC4wLkAuUC5ALmAuUC5QLmAucC5gLoAucC5wLoAukC6ALqAukC6QLqAusC6gLsAusC6wLsAu0C7ALuAu0C7QLuAu8C7gLwAu8C7wLwAvEC8ALyAvEC8QLyAvMC8gL0AvMC8wL0AvUC9AL2AvUC9QL2AvcC9gL4AvcC9wL4AvkC+AL6AvkC+QL6AvsC+gL8AvsC+wL8Av0C/AL+Av0C/QL+Av8C/gIAA/8C/wIAAwEDAAMCAwEDAQMCAwMDAgMEAwMDAwMEAwUDBAMGAwUDBQMGAwcDBgMIAwcDBwMIAwkDCAMKAwkDCQMKAwsDCgMMAwsDCwMMAw0DDAMOAw0DDQMOAw8DDgMQAw8DDwMQAxEDEAMSAxEDEQMSAxMDEgMUAxMDEwMUAxUDFAMWAxUDFQMWAxcDFgMYAxcDFwMYAxkDGAMaAxkDGQMaAxsDGgMcAxsDGwMcAx0DHAMeAx0DHQMeAx8DHgMgAx8DHwMgAyEDIAMiAyEDIQMiAyMDIgMkAyMDIwMkAyUDJAMmAyUDJQMmAycDJgMoAycDJwMoAykDKAMqAykDKQMqAysDKgMsAysDKwMsAy0DLAMuAy0DLQMuAy8DLgMwAy8DLwMwAzEDMAMyAzEDMQMyAzMDMgM0AzMDMwM0AzUDNAM2AzUDNQM2AzcDNgM4AzcDNwM4AzkDOAM6AzkDOQM6AzsDOgM8AzsDOwM8Az0DPAM+Az0DPQM+Az8DPgNAAz8DPwNAA0EDQANCA0EDQQNCA0MDQgNEA0MDQwNEA0UDRANGA0UDRQNGA0cDRgNIA0cDRwNIA0kDSANKA0kDSQNKA0sDSgNMA0sDSwNMA00DTANOA00DTQNOA08DTgNQA08DTwNQA1EDUANSA1EDUQNSA1MDUgNUA1MDUwNUA1UDVANWA1UDVQNWA1cDVgNYA1cDVwNYA1kDWANaA1kDWQNaA1sDWgNcA1sDWwNcA10DXANeA10DXQNeA18DXgNgA18DXwNgA2EDYANiA2EDYQNiA2MDYgNkA2MDYwNkA2UDZANmA2UDZQNmA2cDZgNoA2cDZwNoA2kDaANqA2kDaQNqA2sDagNsA2sDawNsA20DbANuA20DbQNuA28DbgNwA28DbwNwA3EDcANyA3EDcQNyA3MDcgN0A3MDcwN0A3UDdAN2A3UDdQN2A3cDdgN4A3cDdwN4A3kDeAN6A3kDeQN6A3sDegN8A3sDewN8A30DfAN+A30DfQN+A38DfgOAA38DfwOAA4EDgAOCA4EDgQOCA4MDggOEA4MDgwOEA4UDhAOGA4UDhQOGA4cDhgOIA4cDhwOIA4kDiAOKA4kDiQOKA4sDigOMA4sDiwOMA40DjAOOA40DjQOOA48DjgOQA48DjwOQA5EDkAOSA5EDkQOSA5MDkgOUA5MDkwOUA5UDlAOWA5UDlQOWA5cDlgOYA5cDlwOYA5kDmAOaA5kDmQOaA5sDmgOcA5sDmwOcA50DnAOeA50DnQOeA58DngOgA58DnwOgA6EDoAOiA6EDoQOiA6MDogOkA6MDowOkA6UDpAOmA6UDpQOmA6cDpgOoA6cDpwOoA6kDqAOqA6kDqQOqA6sDqgOsA6sDqwOsA60DrAOuA60DrQOuA68DrgOwA68DrwOwA7EDsAOyA7EDsQOyA7MDsgO0A7MDswO0A7UDtAO2A7UDtQO2A7cDtgO4A7cDtwO4A7kDuAO6A7kDuQO6A7sDugO8A7sDuwO8A70DvAO+A70DvQO+A78DvgPAA78DvwPAA8EDwAPCA8EDwQPCA8MDwgPEA8MDwwPEA8UDxAPGA8UDxQPGA8cDxgPIA8cDxwPIA8kDyAPKA8kDyQPKA8sDygPMA8sDywPMA80DzAPOA80DzQPOA88DzgPQA88DzwPQA9ED0APSA9ED0QPSA9MD0gPUA9MD0wPUA9UD1APWA9UD1QPWA9cD1gPYA9cD1wPYA9kD2APaA9kD2QPaA9sD2gPcA9sD2wPcA90D3APeA90D3QPeA98D3gPgA98D3wPgA+ED4APiA+ED4QPiA+MD4gPkA+MD4wPkA+UD5APmA+UD5QPmA+cD5gPoA+cD5wPoA+kD6APqA+kD6QPqA+sD6gPsA+sD6wPsA+0D7APuA+0D7QPuA+8D7gPwA+8D7wPwA/ED8APyA/ED8QPyA/MD8gP0A/MD8wP0A/UD9AP2A/UD9QP2A/cD9gP4A/cD9wP4A/kD+AP6A/kD+QP6A/sD+gP8A/sD+wP8A/0D/AP+A/0D/QP+A/8D/gMABP8D/wMABAEEAAQCBAEEAQQCBAMEAgQEBAMEAwQEBAUEBAQGBAUEBQQGBAcEBgQIBAcEBwQIBAkECAQKBAkECQQKBAsECgQMBAsECwQMBA0EDAQOBA0EDQQOBA8EDgQQBA8EDwQQBBEEEAQSBBEEEQQSBBMEEgQUBBMEEwQUBBUEFAQWBBUEFQQWBBcEFgQYBBcEFwQYBBkEGAQaBBkEGQQaBBsEGgQcBBsEGwQcBB0EHAQeBB0EHQQeBB8EHgQgBB8EHwQgBCEEIAQiBCEEIQQiBCMEIgQkBCMEIwQkBCUEJAQmBCUEJQQmBCcEJgQoBCcEJwQoBCkEKAQqBCkEKQQqBCsEKgQsBCsEKwQsBC0ELAQuBC0ELQQuBC8ELgQwBC8ELwQwBDEEMAQyBDEEMQQyBDMEMgQ0BDMEMwQ0BDUENAQ2BDUENQQ2BDcENgQ4BDcENwQ4BDkEOAQ6BDkEOQQ6BDsEOgQ8BDsEOwQ8BD0EPAQ+BD0EPQQ+BD8EPgRABD8EPwRABEEEQARCBEEEQQRCBEMEQgREBEMEQwREBEUERARGBEUERQRGBEcERgRIBEcERwRIBEkESARKBEkESQRKBEsESgRMBEsESwRMBE0ETAROBE0ETQROBE8ETgRQBE8ETwRQBFEEUARSBFEEUQRSBFMEUgRUBFMEUwRUBFUEVARWBFUEVQRWBFcEVgRYBFcEVwRYBFkEWARaBFkEWQRaBFsEWgRcBFsEWwRcBF0EXAReBF0EXQReBF8EXgRgBF8EXwRgBGEEYARiBGEEYQRiBGMEYgRkBGMEYwRkBGUEZARmBGUEZQRmBGcEZgRoBGcEZwRoBGkEaARqBGkEaQRqBGsEagRsBGsEawRsBG0EbARuBG0EbQRuBG8EbgRwBG8EbwRwBHEEcARyBHEEcQRyBHMEcgR0BHMEcwR0BHUEdAR2BHUEdQR2BHcEdgR4BHcEdwR4BHkEeAR6BHkEeQR6BHsEegR8BHsEewR8BH0EfAR+BH0EfQR+BH8EfgSABH8EfwSABIEEgASCBIEEgQSCBIMEggSEBIMEgwSEBIUEhASGBIUEhQSGBIcEhgSIBIcEhwSIBIkEiASKBIkEiQSKBIsEigSMBIsEiwSMBI0EjASOBI0EjQSOBI8EjgSQBI8EjwSQBJEEkASSBJEEkQSSBJMEkgSUBJMEkwSUBJUElASWBJUElQSWBJcElgSYBJcElwSYBJkEmASaBJkEmQSaBJsEmgScBJsEmwScBJ0EnASeBJ0EnQSeBJ8EngSgBJ8EnwSgBKEEoASiBKEEoQSiBKMEogSkBKMEowSkBKUEpASmBKUEpQSmBKcEpgSoBKcEpwSoBKkEqASqBKkEqQSqBKsEqgSsBKsEqwSsBK0ErASuBK0ErQSuBK8ErgSwBK8ErwSwBLEEsASyBLEEsQSyBLMEsgS0BLMEswS0BLUEtAS2BLUEtQS2BLcEtgS4BLcEtwS4BLkEuAS6BLkEuQS6BLsEugS8BLsEuwS8BL0EvAS+BL0EvQS+BL8EvgTABL8EvwTABMEEwATCBMEEwQTCBMMEwgTEBMMEwwTEBMUExATGBMUExQTGBMcExgTIBMcExwTIBMkEyATKBMkEyQTKBMsEygTMBMsEywTMBM0EzATOBM0EzQTOBM8EzgTQBM8EzwTQBNEE0ATSBNEE0QTSBNME0gTUBNME0wTUBNUE1ATWBNUE1QTWBNcE1gTYBNcE1wTYBNkE2ATaBNkE2QTaBNsE2gTcBNsE2wTcBN0E3ATeBN0E3QTeBN8E3gTgBN8E3wTgBOEE4ATiBOEE4QTiBOME4gTkBOME4wTkBOUE5ATmBOUE5QTmBOcE5gToBOcE5wToBOkE6ATqBOkE6QTqBOsE6gTsBOsE6wTsBO0E7ATuBO0E7QTuBO8E7gTwBO8E7wTwBPEE8ATyBPEE8QTyBPME8gT0BPME8wT0BPUE9AT2BPUE9QT2BPcE9gT4BPcE9wT4BPkE+AT6BPkE+QT6BPsE+gT8BPsE+wT8BP0E/AT+BP0E/QT+BP8E/gQABf8E/wQABQEFAAUCBQEFAQUCBQMFAgUEBQMFAwUEBQUFBAUGBQUFBQUGBQcFBgUIBQcFBwUIBQkFCAUKBQkFCQUKBQsFCgUMBQsFCwUMBQ0FDAUOBQ0FDQUOBQ8FDgUQBQ8FDwUQBREFEAUSBREFEQUSBRMFEgUUBRMFEwUUBRUFFAUWBRUFFQUWBRcFFgUYBRcFFwUYBRkFGAUaBRkFGQUaBRsFGgUcBRsFGwUcBR0FHAUeBR0FHQUeBR8FHgUgBR8FHwUgBSEFIAUiBSEFIQUiBSMFIgUkBSMFIwUkBSUFJAUmBSUFJQUmBScFJgUoBScFJwUoBSkFKAUqBSkFKQUqBSsFKgUsBSsFKwUsBS0FLAUuBS0FLQUuBS8FLgUwBS8FLwUwBTEFMAUyBTEFMQUyBTMFMgU0BTMFMwU0BTUFNAU2BTUFNQU2BTcFNgU4BTcFNwU4BTkFOAU6BTkFOQU6BTsFOgU8BTsFOwU8BT0FPAU+BT0FPQU+BT8FPgVABT8FPwVABUEFQAVCBUEFQQVCBUMFQgVEBUMFQwVEBUUFRAVGBUUFRQVGBUcFRgVIBUcFRwVIBUkFSAVKBUkFSQVKBUsFSgVMBUsFSwVMBU0FTAVOBU0FTQVOBU8FTgVQBU8FTwVQBVEFUAVSBVEFUQVSBVMFUgVUBVMFUwVUBVUFVAVWBVUFVQVWBVcFVgVYBVcFVwVYBVkFWAVaBVkFWQVaBVsFWgVcBVsFWwVcBV0FXAVeBV0FXQVeBV8FXgVgBV8FXwVgBWEFYAViBWEFYQViBWMFYgVkBWMFYwVkBWUFZAVmBWUFZQVmBWcFZgVoBWcFZwVoBWkFaAVqBWkFaQVqBWsFagVsBWsFawVsBW0FbAVuBW0FbQVuBW8FbgVwBW8FbwVwBXEFcAVyBXEFcQVyBXMFcgV0BXMFcwV0BXUFdAV2BXUFdQV2BXcFdgV4BXcFdwV4BXkFeAV6BXkFeQV6BXsFegV8BXsFewV8BX0FfAV+BX0FfQV+BX8FfgWABX8FfwWABYEFgAWCBYEFgQWCBYMFggWEBYMFgwWEBYUFhAWGBYUFhQWGBYcFhgWIBYcFhwWIBYkFiAWKBYkFiQWKBYsFigWMBYsFiwWMBY0FjAWOBY0FjQWOBY8FjgWQBY8FjwWQBZEFkAWSBZEFkQWSBZMFkgWUBZMFkwWUBZUFlAWWBZUFlQWWBZcFlgWYBZcFlwWYBZkFmAWaBZkFmQWaBZsFmgWcBZsFmwWcBZ0FnAWeBZ0FnQWeBZ8FngWgBZ8FnwWgBaEFoAWiBaEFoQWiBaMFogWkBaMFowWkBaUFpAWmBaUFpQWmBacFpgWoBacFpwWoBakFqAWqBakFqQWqBasFqgWsBasFqwWsBa0FrAWuBa0FrQWuBa8FrgWwBa8FrwWwBbEFsAWyBbEFsQWyBbMFsgW0BbMFswW0BbUFtAW2BbUFtQW2BbcFtgW4BbcFtwW4BbkFuAW6BbkFuQW6BbsFugW8BbsFuwW8Bb0FvAW+Bb0FvQW+Bb8FvgXABb8FvwXABcEFwAXCBcEFwQXCBcMFwgXEBcMFwwXEBcUFxAXGBcUFxQXGBccFxgXIBccFxwXIBckFyAXKBckFyQXKBcsFygXMBcsFywXMBc0FzAXOBc0FzQXOBc8FzgXQBc8FzwXQBdEF0AXSBdEF0QXSBdMF0gXUBdMF0wXUBdUF1AXWBdUF1QXWBdcF1gXYBdcF1wXYBdkF2AXaBdkF2QXaBdsF2gXcBdsF2wXcBd0F3AXeBd0F3QXeBd8F3gXgBd8F3wXgBeEF4AXiBeEF4QXiBeMF4gXkBeMF4wXkBeUF5AXmBeUF5QXmBecF5gXoBecF5wXoBekF6AXqBekF6QXqBesF6gXsBesF6wXsBe0F7AXuBe0F7QXuBe8F7gXwBe8F7wXwBfEF8AXyBfEF8QXyBfMF8gX0BfMF8wX0BfUF9AX2BfUF9QX2BfcF9gX4BfcF9wX4BfkF+AX6BfkF+QX6BfsF+gX8BfsF+wX8Bf0F/AX+Bf0F/QX+Bf8F/gUABv8F/wUABgEGAAYCBgEGAQYCBgMGAgYEBgMGAwYEBgUGBAYGBgUGBQYGBgcGBgYIBgcGBwYIBgkGCAYKBgkGCQYKBgsGCgYMBgsGCwYMBg0GDAYOBg0GDQYOBg8GDgYQBg8GDwYQBhEGEAYSBhEGEQYSBhMGEgYUBhMGEwYUBhUGFAYWBhUGFQYWBhcGFgYYBhcGFwYYBhkGGAYaBhkGGQYaBhsGGgYcBhsGGwYcBh0GHAYeBh0GHQYeBh8GHgYgBh8GHwYgBiEGIAYiBiEGIQYiBiMGIgYkBiMGIwYkBiUGJAYmBiUGJQYmBicGJgYoBicGJwYoBikGKAYqBikGKQYqBisGKgYsBisGKwYsBi0GLAYuBi0GLQYuBi8GLgYwBi8GLwYwBjEGMAYyBjEGMQYyBjMGMgY0BjMGMwY0BjUGNAY2BjUGNQY2BjcGNgY4BjcGNwY4BjkGOAY6BjkGOQY6BjsGOgY8BjsGOwY8Bj0GPAY+Bj0GPQY+Bj8GPgZABj8GPwZABkEGQAZCBkEGQQZCBkMGQgZEBkMGQwZEBkUGRAZGBkUGRQZGBkcGRgZIBkcGRwZIBkkGSAZKBkkGSQZKBksGSgZMBksGSwZMBk0GTAZOBk0GTQZOBk8GTgZQBk8GTwZQBlEGUAZSBlEGUQZSBlMGUgZUBlMGUwZUBlUGVAZWBlUGVQZWBlcGVgZYBlcGVwZYBlkGWAZaBlkGWQZaBlsGWgZcBlsGWwZcBl0GXAZeBl0GXQZeBl8GXgZgBl8GXwZgBmEGYAZiBmEGYQZiBmMGYgZkBmMGYwZkBmUGZAZmBmUGZQZmBmcGZgZoBmcGZwZoBmkGaAZqBmkGaQZqBmsGagZsBmsGawZsBm0GbAZuBm0GbQZuBm8GbgZwBm8GbwZwBnEGcAZyBnEGcQZyBnMGcgZ0BnMGcwZ0BnUGdAZ2BnUGdQZ2BncGdgZ4BncGdwZ4BnkGeAZ6BnkGeQZ6BnsGegZ8BnsGewZ8Bn0GfAZ+Bn0GfQZ+Bn8GfgaABn8GfwaABoEGgAaCBoEGgQaCBoMGggaEBoMGgwaEBoUGhAaGBoUGhQaGBocGhgaIBocGhwaIBokGiAaKBokGiQaKBosGigaMBosGiwaMBo0GjAaOBo0GjQaOBo8GjgaQBo8GjwaQBpEGkAaSBpEGkQaSBpMGkgaUBpMGkwaUBpUGlAaWBpUGlQaWBpcGlgaYBpcGlwaYBpkGmAaaBpkGmQaaBpsGmgacBpsGmwacBp0GnAaeBp0GnQaeBp8GngagBp8GnwagBqEGoAaiBqEGoQaiBqMGogakBqMGowakBqUGpAamBqUGpQamBqcGpgaoBqcGpwaoBqkGqAaqBqkGqQaqBqsGqgasBqsGqwasBq0GrAauBq0GrQauBq8GrgawBq8GrwawBrEGsAayBrEGsQayBrMGsga0BrMGswa0BrUGtAa2BrUGtQa2BrcGtga4BrcGtwa4BrkGuAa6BrkGuQa6BrsGuga8BrsGuwa8Br0GvAa+Br0GvQa+Br8GvgbABr8GvwbABsEGwAbCBsEGwQbCBsMGwgbEBsMGwwbEBsUGxAbGBsUGxQbGBscGxgbIBscGxwbIBskGyAbKBskGyQbKBssGygbMBssGywbMBs0GzAbOBs0GzQbOBs8GzgbQBs8GzwbQBtEG0AbSBtEG0QbSBtMG0gbUBtMG0wbUBtUG1AbWBtUG1QbWBtcG1gbYBtcG1wbYBtkG2AbaBtkG2QbaBtsG2gbcBtsG2wbcBt0G3AbeBt0G3QbeBt8G3gbgBt8G3wbgBuEG4AbiBuEG4QbiBuMG4gbkBuMG4wbkBuUG5AbmBuUG5QbmBucG5gboBucG5wboBukG6AbqBukG6QbqBusG6gbsBusG6wbsBu0G7AbuBu0G7QbuBu8G7gbwBu8G7wbwBvEG8AbyBvEG8QbyBvMG8gb0BvMG8wb0BvUG9Ab2BvUG9Qb2BvcG9gb4BvcG9wb4BvkG+Ab6BvkG+Qb6BvsG+gb8BvsG+wb8Bv0G/Ab+Bv0G/Qb+Bv8G/gYAB/8G/wYABwEHAAcCBwEHAQcCBwMHAgcEBwMHAwcEBwUHBAcGBwUHBQcGBwcHBgcIBwcHBwcIBwkHCAcKBwkHCQcKBwsHCgcMBwsHCwcMBw0HDAcOBw0HDQcOBw8HDgcQBw8HDwcQBxEHEAcSBxEHEQcSBxMHEgcUBxMHEwcUBxUHFAcWBxUHFQcWBxcHFgcYBxcHFwcYBxkHGAcaBxkHGQcaBxsHGgccBxsHGwccBx0HHAceBx0HHQceBx8HHgcgBx8HHwcgByEHIAciByEHIQciByMHIgckByMHIwckByUHJAcmByUHJQcmBycHJgcoBycHJwcoBykHKAcqBykHKQcqBysHKgcsBysHKwcsBy0HLAcuBy0HLQcuBy8HLgcwBy8HLwcwBzEHMAcyBzEHMQcyBzMHMgc0BzMHMwc0BzUHNAc2BzUHNQc2BzcHNgc4BzcHNwc4BzkHOAc6BzkHOQc6BzsHOgc8BzsHOwc8Bz0HPAc+Bz0HPQc+Bz8HPgdABz8HPwdAB0EHQAdCB0EHQQdCB0MHQgdEB0MHQwdEB0UHRAdGB0UHRQdGB0cHRgdIB0cHRwdIB0kHSAdKB0kHSQdKB0sHSgdMB0sHSwdMB00HTAdOB00HTQdOB08HTgdQB08HTwdQB1EHUAdSB1EHUQdSB1MHUgdUB1MHUwdUB1UHVAdWB1UHVQdWB1cHVgdYB1cHVwdYB1kHWAdaB1kHWQdaB1sHWgdcB1sHWwdcB10HXAdeB10HXQdeB18HXgdgB18HXwdgB2EHYAdiB2EHYQdiB2MHYgdkB2MHYwdkB2UHZAdmB2UHZQdmB2cHZgdoB2cHZwdoB2kHaAdqB2kHaQdqB2sHagdsB2sHawdsB20HbAduB20HbQduB28HbgdwB28HbwdwB3EHcAdyB3EHcQdyB3MHcgd0B3MHcwd0B3UHdAd2B3UHdQd2B3cHdgd4B3cHdwd4B3kHeAd6B3kHeQd6B3sHegd8B3sHewd8B30HfAd+B30HfQd+B38HfgeAB38HfweAB4EHgAeCB4EHgQeCB4MHggeEB4MHgweEB4UHhAeGB4UHhQeGB4cHhgeIB4cHhweIB4kHiAeKB4kHiQeKB4sHigeMB4sHiweMB40HjAeOB40HjQeOB48HjgeQB48HjweQB5EHkAeSB5EHkQeSB5MHkgeUB5MHkweUB5UHlAeWB5UHlQeWB5cHlgeYB5cHlweYB5kHmAeaB5kHmQeaB5sHmgecB5sHmwecB50HnAeeB50HnQeeB58HngegB58HnwegB6EHoAeiB6EHoQeiB6MHogekB6MHowekB6UHpAemB6UHpQemB6cHpgeoB6cHpweoB6kHqAeqB6kHqQeqB6sHqgesB6sHqwesB60HrAeuB60HrQeuB68HrgewB68HrwewB7EHsAeyB7EHsQeyB7MHsge0B7MHswe0B7UHtAe2B7UHtQe2B7cHtge4B7cHtwe4B7kHuAe6B7kHuQe6B7sHuge8B7sHuwe8B70HvAe+B70HvQe+B78HvgfAB78HvwfAB8EHwAfCB8EHwQfCB8MHwgfEB8MHwwfEB8UHxAfGB8UHxQfGB8cHxgfIB8cHxwfIB8kHyAfKB8kHyQfKB8sHygfMB8sHywfMB80HzAfOB80HzQfOB88HzgfQB88HzwfQB9EH0AfSB9EH0QfSB9MH0gfUB9MH0wfUB9UH1AfWB9UH1QfWB9cH1gfYB9cH1wfYB9kH2AfaB9kH2QfaB9sH2gfcB9sH2wfcB90H3AfeB90H3QfeB98H3gfgB98H3wfgB+EH4AfiB+EH4QfiB+MH4gfkB+MH4wfkB+UH5AfmB+UH5QfmB+cH5gfoB+cH5wfoB+kH6AfqB+kH6QfqB+sH6gfsB+sH6wfsB+0H7AfuB+0H7QfuB+8H7gfwB+8H7wfwB/EH8AfyB/EH8QfyB/MH8gf0B/MH8wf0B/UH9Af2B/UH9Qf2B/cH9gf4B/cH9wf4B/kH+Af6B/kH+Qf6B/sH+gf8B/sH+wf8B/0H/Af+B/0H/Qf+B/8H/gcACP8H/wcACAEIAAgCCAEIAQgCCAMIAggECAMIAwgECAUIBAgGCAUIBQgGCAcIBggICAcIBwgICAkICAgKCAkICQgKCAsICggMCAsICwgMCA0IDAgOCA0IDQgOCA8IDggQCA8IDwgQCBEIEAgSCBEIEQgSCBMIEggUCBMIEwgUCBUIFAgWCBUIFQgWCBcIFggYCBcIFwgYCBkIGAgaCBkIGQgaCBsIGggcCBsIGwgcCB0IHAgeCB0IHQgeCB8IHgggCB8IHwggCCEIIAgiCCEIIQgiCCMIIggkCCMIIwgkCCUIJAgmCCUIJQgmCCcIJggoCCcIJwgoCCkIKAgqCCkIKQgqCCsIKggsCCsIKwgsCC0ILAguCC0ILQguCC8ILggwCC8ILwgwCDEIMAgyCDEIMQgyCDMIMgg0CDMIMwg0CDUINAg2CDUINQg2CDcINgg4CDcINwg4CDkIOAg6CDkIOQg6CDsIOgg8CDsIOwg8CD0IPAg+CD0IPQg+CD8IPghACD8IPwhACEEIQAhCCEEIQQhCCEMIQghECEMIQwhECEUIRAhGCEUIRQhGCEcIRghICEcIRwhICEkISAhKCEkISQhKCEsISghMCEsISwhMCE0ITAhOCE0ITQhOCE8ITghQCE8ITwhQCFEIUAhSCFEIUQhSCFMIUghUCFMIUwhUCFUIVAhWCFUIVQhWCFcIVghYCFcIVwhYCFkIWAhaCFkIWQhaCFsIWghcCFsIWwhcCF0IXAheCF0IXQheCF8IXghgCF8IXwhgCGEIYAhiCGEIYQhiCGMIYghkCGMIYwhkCGUIZAhmCGUIZQhmCGcIZghoCGcIZwhoCGkIaAhqCGkIaQhqCGsIaghsCGsIawhsCG0IbAhuCG0IbQhuCG8IbghwCG8IbwhwCHEIcAhyCHEIcQhyCHMIcgh0CHMIcwh0CHUIdAh2CHUIdQh2CHcIdgh4CHcIdwh4CHkIeAh6CHkIeQh6CHsIegh8CHsIewh8CH0IfAh+CH0IfQh+CH8IfgiACH8IfwiACIEIgAiCCIEIgQiCCIMIggiECIMIgwiECIUIhAiGCIUIhQiGCIcIhgiICIcIhwiICIkIiAiKCIkIiQiKCIsIigiMCIsIiwiMCI0IjAiOCI0IjQiOCI8IjgiQCI8IjwiQCJEIkAiSCJEIkQiSCJMIkggAAJMIkwgAAAEA"), -"primitive": 3, -"uv_scale": Vector4(0, 0, 0, 0), -"vertex_count": 2196, -"vertex_data": PackedByteArray("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qQmcQRsv3b6xxLTBq2qVQRsv3b7zI6zBX/ScQRsv3b7x37PBYVWWQRsv3b4zP6vBFd+dQRsv3b4x+7LBF0CXQRsv3b5zWqrBy8meQRsv3b5xFrLBRSmYQRsv3b40d6nBMMSfQRsv3b5FIrHBqQ2ZQRsv3b6xkKjBJcKgQRsv3b4RIrDBivGZQRsv3b6jpqfBfa6hQRsv3b5bL6/BLNaaQRsv3b7Qu6bBH5OiQRsv3b6IRK7Bz7qbQRsv3b790KXBwHejQRsv3b63Wa3Bcp+cQRsv3b4q5qTBY1ykQRsv3b7kbqzBE4SdQRsv3b5X+6PBBkGlQRsv3b4PhKvB/3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+/") -}] diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index 9c7568d..6ba1786 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -126,7 +126,6 @@ const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = ( @onready var _diggable_beach: DiggableArea3D = %DiggableBeach @onready var _ocean: WaterBodyAuthoring = %OceanWater @onready var _fresh_water_root: Node3D = %FreshWaterBodies -@onready var _shoreline_reference: MeshInstance3D = %ShorelineReference var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED var _light_performance_profile := false @@ -146,7 +145,6 @@ func _ready() -> void: _validate_prop_catalog() _validate_biome_catalog() _configure_static_water() - _build_shoreline_reference() _set_foliage_wind_enabled(not _light_performance_profile) @@ -199,10 +197,6 @@ func get_rv_upgrade_shop() -> RVUpgradeInteractionType: return _rv_upgrade_shop -func get_saltwater_shoreline_mesh() -> MeshInstance3D: - return _shoreline_reference - - func get_prop_catalog() -> TerrainPropCatalog: return prop_catalog @@ -218,10 +212,13 @@ func get_biome_at(coordinate: Vector2i) -> StringName: func set_light_performance_profile(enabled: bool) -> void: _light_performance_profile = enabled _set_foliage_wind_enabled(not enabled) - _apply_water_materials() var surface_motion := _get_ocean_surface_motion() if surface_motion != null: - surface_motion.set_motion_enabled(not enabled) + # Reset the old shader material before _apply_water_materials() swaps it. + surface_motion.set_motion_enabled(false) + _apply_water_materials() + if surface_motion != null and not enabled: + surface_motion.set_motion_enabled(true) func set_foliage_wind_strength_multiplier(multiplier: float) -> void: @@ -1921,42 +1918,6 @@ func _add_prop_collision( body.add_child(collision) -func _build_shoreline_reference() -> void: - var half := get_playable_half_extents() - var width := 0.05 - var vertices := PackedVector3Array() - var indices := PackedInt32Array() - var corners := [ - Vector3(-half.x, WATER_HEIGHT, -half.y), - Vector3(half.x, WATER_HEIGHT, -half.y), - Vector3(half.x, WATER_HEIGHT, half.y), - Vector3(-half.x, WATER_HEIGHT, half.y), - ] - for index: int in 4: - var start: Vector3 = corners[index] - var finish: Vector3 = corners[(index + 1) % 4] - var direction := (finish - start).normalized() - var perpendicular := Vector3(-direction.z, 0.0, direction.x) * width - var base := vertices.size() - vertices.append_array(PackedVector3Array([ - start - perpendicular, - start + perpendicular, - finish + perpendicular, - finish - perpendicular, - ])) - indices.append_array(PackedInt32Array([ - base, base + 1, base + 2, - base, base + 2, base + 3, - ])) - var arrays := [] - arrays.resize(Mesh.ARRAY_MAX) - arrays[Mesh.ARRAY_VERTEX] = vertices - arrays[Mesh.ARRAY_INDEX] = indices - var mesh := ArrayMesh.new() - mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) - _shoreline_reference.mesh = mesh - - func _append_surface_triangles( result: Array[PackedVector3Array], mesh_instance: MeshInstance3D, diff --git a/world/generation/generated_world_region.tscn b/world/generation/generated_world_region.tscn index b4b3dba..03446a0 100644 --- a/world/generation/generated_world_region.tscn +++ b/world/generation/generated_world_region.tscn @@ -112,8 +112,3 @@ unique_name_in_owner = true [node name="Decorations" type="Node3D" parent="."] unique_name_in_owner = true - -[node name="ShorelineReference" type="MeshInstance3D" parent="."] -unique_name_in_owner = true -visible = false -cast_shadow = 0 diff --git a/world/materials/shoreline_tide.gdshader b/world/materials/shoreline_tide.gdshader deleted file mode 100644 index f8a58e0..0000000 --- a/world/materials/shoreline_tide.gdshader +++ /dev/null @@ -1,71 +0,0 @@ -shader_type spatial; -render_mode blend_mix, depth_draw_never, cull_disabled, unshaded; - -uniform vec4 lead_color : source_color = vec4(0.780, 0.960, 0.930, 1.0); -uniform vec4 trail_color : source_color = vec4(0.500, 0.840, 0.820, 1.0); -uniform float ribbon_width : hint_range(0.2, 2.0, 0.01) = 1.35; -uniform float land_inset : hint_range(0.0, 0.4, 0.01) = 0.12; -uniform float tide_speed : hint_range(0.1, 3.0, 0.01) = 1.42; -uniform float tide_distance : hint_range(0.0, 0.6, 0.01) = 0.42; -uniform float lead_width : hint_range(0.01, 0.2, 0.005) = 0.14; -uniform float trail_width : hint_range(0.02, 0.4, 0.005) = 0.32; -uniform float lead_strength : hint_range(0.0, 1.0, 0.01) = 0.88; -uniform float trail_strength : hint_range(0.0, 1.0, 0.01) = 0.52; -uniform float phase_scale : hint_range(0.001, 0.2, 0.001) = 0.035; -uniform float phase_strength : hint_range(0.0, 2.0, 0.01) = 0.55; - -varying vec3 world_position; - - -float hash_21(vec2 point) { - return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453); -} - - -float value_noise(vec2 point) { - vec2 cell = floor(point); - vec2 local = fract(point); - vec2 blend = local * local * (3.0 - 2.0 * local); - float bottom = mix(hash_21(cell), hash_21(cell + vec2(1.0, 0.0)), blend.x); - float top = mix( - hash_21(cell + vec2(0.0, 1.0)), - hash_21(cell + vec2(1.0, 1.0)), - blend.x - ); - return mix(bottom, top, blend.y); -} - - -float soft_line(float coordinate, float center, float width) { - float normalized_width = max(width / ribbon_width, 0.001); - float derivative_width = min(fwidth(coordinate) * 1.25, normalized_width * 0.65); - return 1.0 - smoothstep( - max(normalized_width * 0.25 - derivative_width, 0.0), - normalized_width + derivative_width, - abs(coordinate - center) - ); -} - - -void vertex() { - world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; -} - - -void fragment() { - float phase = ( - value_noise(world_position.xz * phase_scale) * 2.0 - 1.0 - ) * phase_strength; - float ebb = sin(TIME * tide_speed + phase) * 0.5 + 0.5; - float shoreline = land_inset / ribbon_width; - float lead_position = shoreline + ebb * tide_distance / ribbon_width; - float trail_position = lead_position + max(tide_distance * 0.72, 0.10) / ribbon_width; - float lead = soft_line(UV.y, lead_position, lead_width); - float trail = soft_line(UV.y, trail_position, trail_width); - float edge_fade = smoothstep(0.0, 0.055, UV.y) * (1.0 - smoothstep(0.90, 1.0, UV.y)); - float lead_alpha = lead * lead_strength; - float trail_alpha = trail * trail_strength; - float combined_alpha = max(lead_alpha, trail_alpha) * edge_fade; - ALBEDO = mix(trail_color.rgb, lead_color.rgb, lead_alpha); - ALPHA = combined_alpha; -} diff --git a/world/materials/shoreline_tide.gdshader.uid b/world/materials/shoreline_tide.gdshader.uid deleted file mode 100644 index e8e5d36..0000000 --- a/world/materials/shoreline_tide.gdshader.uid +++ /dev/null @@ -1 +0,0 @@ -uid://5dauni8hb2lv diff --git a/world/materials/shoreline_tide.tres b/world/materials/shoreline_tide.tres deleted file mode 100644 index 65d2fa8..0000000 --- a/world/materials/shoreline_tide.tres +++ /dev/null @@ -1,19 +0,0 @@ -[gd_resource type="ShaderMaterial" load_steps=2 format=3] - -[ext_resource type="Shader" path="res://world/materials/shoreline_tide.gdshader" id="1_shader"] - -[resource] -render_priority = 1 -shader = ExtResource("1_shader") -shader_parameter/lead_color = Color(0.78, 0.96, 0.93, 1) -shader_parameter/trail_color = Color(0.5, 0.84, 0.82, 1) -shader_parameter/ribbon_width = 1.35 -shader_parameter/land_inset = 0.12 -shader_parameter/tide_speed = 1.42 -shader_parameter/tide_distance = 0.42 -shader_parameter/lead_width = 0.14 -shader_parameter/trail_width = 0.32 -shader_parameter/lead_strength = 0.88 -shader_parameter/trail_strength = 0.52 -shader_parameter/phase_scale = 0.035 -shader_parameter/phase_strength = 0.55 diff --git a/world/materials/stylized_water.gdshader b/world/materials/stylized_water.gdshader index 669f649..61e5622 100644 --- a/world/materials/stylized_water.gdshader +++ b/world/materials/stylized_water.gdshader @@ -1,35 +1,27 @@ shader_type spatial; render_mode blend_mix, depth_draw_never, cull_back, unshaded, fog_disabled; +// Use one depth sample directly behind this water pixel. Do not turn this into +// a neighbourhood filter: samples from adjacent cliff faces are what caused +// the former wedges and folding at corners. uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest; uniform vec4 shore_color : source_color = vec4(0.475, 0.788, 0.796, 1.0); uniform vec4 shallow_color : source_color = vec4(0.227, 0.604, 0.663, 1.0); uniform vec4 deep_color : source_color = vec4(0.110, 0.345, 0.435, 1.0); -uniform vec4 tide_wash_color : source_color = vec4(0.360, 0.720, 0.735, 1.0); +uniform vec4 shoreline_foam_color : source_color = vec4(0.580, 0.850, 0.860, 1.0); uniform float shallow_depth : hint_range(0.1, 3.0, 0.01) = 0.70; uniform float deep_depth : hint_range(0.5, 12.0, 0.05) = 3.60; -uniform float shore_depth_width : hint_range(0.02, 1.0, 0.01) = 0.16; uniform float shore_alpha : hint_range(0.1, 1.0, 0.01) = 0.44; uniform float shallow_alpha : hint_range(0.1, 1.0, 0.01) = 0.60; uniform float deep_alpha : hint_range(0.1, 1.0, 0.01) = 0.82; -uniform bool tide_effect_enabled = true; -uniform float tide_speed : hint_range(0.1, 3.0, 0.01) = 1.42; -uniform float tide_distance : hint_range(0.0, 0.6, 0.01) = 0.30; -uniform float tide_wash_width : hint_range(0.05, 1.0, 0.01) = 0.38; -uniform float tide_wash_strength : hint_range(0.0, 0.3, 0.01) = 0.11; -uniform float tide_wash_alpha_shift : hint_range(0.0, 0.15, 0.005) = 0.045; - -uniform float phase_noise_scale : hint_range(0.001, 0.5, 0.001) = 0.055; -uniform float phase_noise_strength : hint_range(0.0, 2.0, 0.01) = 0.68; -uniform float contour_corner_variation : hint_range(0.0, 0.15, 0.005) = 0.045; -uniform float contour_smoothing_pixels : hint_range(0.0, 2.5, 0.05) = 1.0; -uniform float contour_smoothing_strength : hint_range(0.0, 1.0, 0.01) = 0.72; -uniform float contour_depth_reject : hint_range(0.05, 2.0, 0.01) = 0.60; -uniform float shoreline_horizontal_reach : hint_range(0.05, 1.5, 0.01) = 0.32; +// WaterSurfaceMotion supplies the inverse of its visual bob here. Shore +// coloration and foam therefore use the physical waterline rather than a +// surface that moves a few centimetres through the terrain every frame. +uniform float stable_surface_height_offset = 0.0; uniform float contact_fade_depth : hint_range(0.0, 0.1, 0.001) = 0.018; uniform float open_water_drift_scale : hint_range(0.001, 0.3, 0.001) = 0.018; uniform vec2 open_water_drift_speed = vec2(0.010, -0.007); @@ -41,14 +33,17 @@ uniform float surface_mark_strength : hint_range(0.0, 0.3, 0.005) = 0.12; uniform float surface_mottle_scale : hint_range(0.01, 1.0, 0.01) = 0.11; uniform vec2 surface_mottle_speed = vec2(-0.018, 0.013); uniform float surface_mottle_strength : hint_range(0.0, 0.2, 0.005) = 0.07; +uniform float shoreline_foam_speed : hint_range(0.1, 3.0, 0.01) = 1.42; +uniform float shoreline_foam_phase_noise_scale : hint_range(0.001, 0.5, 0.001) = 0.055; +uniform float shoreline_foam_phase_noise_strength : hint_range(0.0, 2.0, 0.01) = 0.68; uniform vec2 shoreline_mark_density = vec2(4.60, 3.20); uniform vec2 shoreline_mark_drift = vec2(0.085, -0.060); -uniform float shoreline_mark_depth : hint_range(0.1, 2.0, 0.01) = 0.95; -uniform float shoreline_mark_strength : hint_range(0.0, 1.0, 0.01) = 0.82; -uniform float shoreline_base_fill : hint_range(0.0, 1.0, 0.01) = 0.32; -uniform float shoreline_opacity_boost : hint_range(0.0, 0.5, 0.01) = 0.26; +uniform float shoreline_foam_depth : hint_range(0.1, 2.0, 0.01) = 0.55; +uniform float shoreline_foam_strength : hint_range(0.0, 1.0, 0.01) = 0.95; +uniform float shoreline_base_fill : hint_range(0.0, 1.0, 0.01) = 0.58; +uniform float shoreline_opacity_boost : hint_range(0.0, 0.5, 0.01) = 0.38; uniform float surface_highlight_night_floor : hint_range(0.0, 1.0, 0.01) = 0.72; -uniform float shoreline_minimum_reach : hint_range(0.1, 1.0, 0.01) = 0.48; +uniform float shoreline_minimum_reach : hint_range(0.1, 1.0, 0.01) = 0.22; uniform float distant_alpha_start : hint_range(10.0, 2000.0, 1.0) = 260.0; uniform float distant_alpha_end : hint_range(20.0, 5000.0, 1.0) = 1400.0; @@ -171,40 +166,6 @@ vec3 reconstruct_world_position( } -float sample_water_depth( - vec2 screen_uv, - float water_surface_height, - mat4 inverse_projection, - mat4 inverse_view -) { - float raw_depth = texture(depth_texture, screen_uv).r; - vec3 scene_world_position = reconstruct_world_position( - screen_uv, - raw_depth, - inverse_projection, - inverse_view - ); - return water_surface_height - scene_world_position.y; -} - - -float contour_neighbor_weight( - float center_depth, - float neighbor_depth, - float rejection_distance -) { - if (neighbor_depth <= 0.0) { - return 0.0; - } - float safe_rejection = max(rejection_distance, 0.001); - return 1.0 - smoothstep( - safe_rejection * 0.35, - safe_rejection, - abs(neighbor_depth - center_depth) - ); -} - - void vertex() { vec4 world_vertex = MODEL_MATRIX * vec4(VERTEX, 1.0); vec4 view_vertex = VIEW_MATRIX * world_vertex; @@ -221,151 +182,32 @@ void vertex() { void fragment() { float raw_depth = texture(depth_texture, SCREEN_UV).r; - vec3 scene_world_position = reconstruct_world_position( - SCREEN_UV, - raw_depth, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float water_depth = max(world_position.y - scene_world_position.y, 0.0); - water_depth = min(water_depth, deep_depth * 4.0); - - float contour_depth = water_depth; - float contour_feature_support = 1.0; - if (tide_effect_enabled) { - vec2 contour_texel = ( - vec2(1.0) / vec2(textureSize(depth_texture, 0)) - * contour_smoothing_pixels - ); - float contour_depth_sum = water_depth; - float contour_weight_sum = 1.0; - float left_depth = sample_water_depth( - SCREEN_UV - vec2(contour_texel.x, 0.0), - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float right_depth = sample_water_depth( - SCREEN_UV + vec2(contour_texel.x, 0.0), - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float upper_depth = sample_water_depth( - SCREEN_UV - vec2(0.0, contour_texel.y), - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float lower_depth = sample_water_depth( - SCREEN_UV + vec2(0.0, contour_texel.y), - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float upper_left_depth = sample_water_depth( - SCREEN_UV - contour_texel, - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float upper_right_depth = sample_water_depth( - SCREEN_UV + vec2(contour_texel.x, -contour_texel.y), - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float lower_left_depth = sample_water_depth( - SCREEN_UV + vec2(-contour_texel.x, contour_texel.y), - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float lower_right_depth = sample_water_depth( - SCREEN_UV + contour_texel, - world_position.y, - INV_PROJECTION_MATRIX, - INV_VIEW_MATRIX - ); - float left_weight = contour_neighbor_weight( - water_depth, left_depth, contour_depth_reject - ); - float right_weight = contour_neighbor_weight( - water_depth, right_depth, contour_depth_reject - ); - float upper_weight = contour_neighbor_weight( - water_depth, upper_depth, contour_depth_reject - ); - float lower_weight = contour_neighbor_weight( - water_depth, lower_depth, contour_depth_reject - ); - float diagonal_scale = 0.70710678; - float upper_left_weight = contour_neighbor_weight( - water_depth, upper_left_depth, contour_depth_reject - ) * diagonal_scale; - float upper_right_weight = contour_neighbor_weight( - water_depth, upper_right_depth, contour_depth_reject - ) * diagonal_scale; - float lower_left_weight = contour_neighbor_weight( - water_depth, lower_left_depth, contour_depth_reject - ) * diagonal_scale; - float lower_right_weight = contour_neighbor_weight( - water_depth, lower_right_depth, contour_depth_reject - ) * diagonal_scale; - contour_depth_sum += left_depth * left_weight; - contour_depth_sum += right_depth * right_weight; - contour_depth_sum += upper_depth * upper_weight; - contour_depth_sum += lower_depth * lower_weight; - contour_depth_sum += upper_left_depth * upper_left_weight; - contour_depth_sum += upper_right_depth * upper_right_weight; - contour_depth_sum += lower_left_depth * lower_left_weight; - contour_depth_sum += lower_right_depth * lower_right_weight; - float neighbor_support = ( - left_weight - + right_weight - + upper_weight - + lower_weight - + upper_left_weight - + upper_right_weight - + lower_left_weight - + lower_right_weight - ); - contour_weight_sum += neighbor_support; - float smoothed_contour_depth = contour_depth_sum / contour_weight_sum; - contour_depth = mix( - water_depth, - smoothed_contour_depth, - contour_smoothing_strength - ); - contour_feature_support = smoothstep(0.75, 1.75, neighbor_support); + // The ocean is a single flat surface. Its visible surf must not be inferred + // from neighbouring depth-buffer pixels: that is camera dependent at cliffs + // and corners, and previously created flickering triangular shoreline masks. + // Use only the terrain sample directly behind this pixel for depth coloration + // and foam. Clear depth is sky/open water, not an imaginary zero-depth shore. + bool has_scene_depth = raw_depth < 0.99999; + float water_depth = deep_depth * 4.0; + if (has_scene_depth) { + vec3 scene_world_position = reconstruct_world_position( + SCREEN_UV, + raw_depth, + INV_PROJECTION_MATRIX, + INV_VIEW_MATRIX + ); + float stable_surface_height = ( + world_position.y + stable_surface_height_offset + ); + water_depth = max(stable_surface_height - scene_world_position.y, 0.0); } - - vec2 scene_world_dx = dFdx(scene_world_position.xz); - vec2 scene_world_dy = dFdy(scene_world_position.xz); - float horizontal_dx = max(length(scene_world_dx), 0.0001); - float horizontal_dy = max(length(scene_world_dy), 0.0001); - float depth_gradient = length(vec2( - dFdx(water_depth) / horizontal_dx, - dFdy(water_depth) / horizontal_dy - )); - float estimated_shore_distance = min( - water_depth / max(depth_gradient, 0.001), - shoreline_horizontal_reach * 4.0 - ); - float proximity_center = shoreline_horizontal_reach * 0.86; - float proximity_filter_width = max( - fwidth(estimated_shore_distance) * 1.5, - shoreline_horizontal_reach * 0.12 - ); - float shoreline_proximity = 1.0 - smoothstep( - max(proximity_center - proximity_filter_width, 0.0), - proximity_center + proximity_filter_width, - estimated_shore_distance - ); - float contact_filter_width = max(fwidth(water_depth) * 1.5, 0.004); + water_depth = min(water_depth, deep_depth * 4.0); + // A very thin alpha threshold is visibly unstable at oblique angles, even + // with a correct centre depth sample. Blend the final contact over a broad, + // fixed physical range rather than toggling it across a single screen pixel. float contact_stability = smoothstep( - max(contact_fade_depth - contact_filter_width, 0.0), - contact_fade_depth + contact_filter_width, + 0.0, + max(contact_fade_depth, 0.10), water_depth ); @@ -376,36 +218,67 @@ void fragment() { float water_alpha = mix(shore_alpha, shallow_alpha, shallow_mix); water_alpha = mix(water_alpha, deep_alpha, deep_mix); - if (tide_effect_enabled) { - vec2 phase_coordinates = world_position.xz * phase_noise_scale; - float phase_variation = ( - value_noise(phase_coordinates) - + value_noise(phase_coordinates * 0.47 + vec2(9.7, 3.1)) * 0.5 - - 0.75 - ) * phase_noise_strength; - float corner_variation = ( - value_noise(phase_coordinates * 1.73 + vec2(4.2, 13.6)) * 2.0 - 1.0 - ) * contour_corner_variation; - float ebb = sin(TIME * tide_speed + phase_variation) * 0.5 + 0.5; - float lead_threshold = shore_depth_width + ebb * tide_distance + corner_variation; - float tide_wash = 1.0 - smoothstep( - lead_threshold, - lead_threshold + max(tide_wash_width, 0.001), - contour_depth - ); - tide_wash *= contour_feature_support; - tide_wash *= shoreline_proximity; - tide_wash *= 1.0 - smoothstep(shallow_depth, deep_depth, water_depth); - water_color = mix( - water_color, - tide_wash_color.rgb, - tide_wash * tide_wash_strength - ); - water_alpha = min( - water_alpha - + tide_wash * tide_wash_alpha_shift, - 0.96 - ); + // Foam is driven by the exact same stable, one-pixel terrain depth as the + // base shoreline. Its breakup pattern lives in world coordinates, so camera + // motion cannot bend, fold, or slide it across unrelated terrain at corners. + float shoreline_foam = 0.0; + if (has_scene_depth) { + vec2 phase_coordinates = ( + world_position.xz * shoreline_foam_phase_noise_scale + ); + float shoreline_phase = ( + value_noise(phase_coordinates) + + value_noise( + phase_coordinates * 0.47 + vec2(9.7, 3.1) + ) * 0.5 + - 0.75 + ) * shoreline_foam_phase_noise_strength; + float shoreline_cycle = ( + sin(TIME * shoreline_foam_speed + shoreline_phase) * 0.5 + 0.5 + ); + float animated_shoreline_depth = shoreline_foam_depth * mix( + shoreline_minimum_reach, + 1.0, + shoreline_cycle + ); + float shoreline_mask = 1.0 - smoothstep( + max(animated_shoreline_depth * 0.08, 0.01), + max(animated_shoreline_depth, 0.02), + water_depth + ); + shoreline_mask *= 1.0 - smoothstep( + shallow_depth, + deep_depth, + water_depth + ); + float shoreline_marks = max( + blocky_mark_layer( + world_position.xz * shoreline_mark_density + + TIME * shoreline_mark_drift, + 17.0, + 0.04 + ), + blocky_mark_layer( + world_position.xz * shoreline_mark_density * 1.37 + - TIME * shoreline_mark_drift.yx * 0.73, + 41.0, + 0.08 + ) + ); + shoreline_foam = shoreline_mask * mix( + shoreline_base_fill, + 1.0, + shoreline_marks + ); + water_color = mix( + water_color, + shoreline_foam_color.rgb, + shoreline_foam * shoreline_foam_strength + ); + water_alpha = min( + water_alpha + shoreline_foam * shoreline_opacity_boost, + 0.98 + ); } vec2 flowing_grid = ( @@ -464,57 +337,6 @@ void fragment() { surface_mark * surface_mark_strength * mark_visibility ); - vec2 shoreline_phase_coordinates = world_position.xz * phase_noise_scale; - float shoreline_phase = ( - value_noise(shoreline_phase_coordinates) - + value_noise( - shoreline_phase_coordinates * 0.47 + vec2(9.7, 3.1) - ) * 0.5 - - 0.75 - ) * phase_noise_strength; - float shoreline_cycle = ( - sin(TIME * tide_speed + shoreline_phase) * 0.5 + 0.5 - ); - float animated_shoreline_depth = shoreline_mark_depth * mix( - shoreline_minimum_reach, - 1.0, - shoreline_cycle - ); - float shoreline_mask = 1.0 - smoothstep( - max(animated_shoreline_depth * 0.08, 0.01), - max(animated_shoreline_depth, 0.02), - contour_depth - ); - float shoreline_depth_visibility = 1.0 - smoothstep( - shallow_depth, - deep_depth, - water_depth - ); - shoreline_mask *= shoreline_depth_visibility; - float shoreline_marks = max( - blocky_mark_layer( - world_position.xz * shoreline_mark_density - + TIME * shoreline_mark_drift, - 17.0, - 0.04 - ), - blocky_mark_layer( - world_position.xz * shoreline_mark_density * 1.37 - - TIME * shoreline_mark_drift.yx * 0.73, - 41.0, - 0.08 - ) - ); - float shoreline_foam = shoreline_mask * mix( - shoreline_base_fill, - 1.0, - shoreline_marks - ); - shoreline_foam *= shoreline_proximity; - water_alpha = min( - water_alpha + shoreline_foam * shoreline_opacity_boost, - 0.98 - ); // Keep the water tint visually continuous across authored terrain contact. // Fully fading here exposes the raised sand edge as a broken tan seam; the // fishing resolver, rather than this presentation shader, owns castability. @@ -551,7 +373,7 @@ void fragment() { ); float visible_highlight = max( surface_highlight, - shoreline_foam * shoreline_mark_strength + shoreline_foam * shoreline_foam_strength ); water_color = mix( water_color, diff --git a/world/materials/stylized_water.tres b/world/materials/stylized_water.tres index 7f4671a..ae99df2 100644 --- a/world/materials/stylized_water.tres +++ b/world/materials/stylized_water.tres @@ -8,26 +8,12 @@ shader = ExtResource("1_water_shader") shader_parameter/shore_color = Color(0.47451, 0.788235, 0.796078, 1) shader_parameter/shallow_color = Color(0.227451, 0.603922, 0.662745, 1) shader_parameter/deep_color = Color(0.109804, 0.345098, 0.435294, 1) -shader_parameter/tide_wash_color = Color(0.360784, 0.721569, 0.733333, 1) +shader_parameter/shoreline_foam_color = Color(0.580392, 0.85098, 0.858824, 1) shader_parameter/shallow_depth = 0.7 shader_parameter/deep_depth = 3.6 -shader_parameter/shore_depth_width = 0.1 shader_parameter/shore_alpha = 0.68 shader_parameter/shallow_alpha = 0.78 shader_parameter/deep_alpha = 0.9 -shader_parameter/tide_effect_enabled = true -shader_parameter/tide_speed = 1.42 -shader_parameter/tide_distance = 0.11 -shader_parameter/tide_wash_width = 0.16 -shader_parameter/tide_wash_strength = 0.11 -shader_parameter/tide_wash_alpha_shift = 0.045 -shader_parameter/phase_noise_scale = 0.055 -shader_parameter/phase_noise_strength = 0.68 -shader_parameter/contour_corner_variation = 0.025 -shader_parameter/contour_smoothing_pixels = 1.35 -shader_parameter/contour_smoothing_strength = 0.88 -shader_parameter/contour_depth_reject = 0.6 -shader_parameter/shoreline_horizontal_reach = 0.32 shader_parameter/contact_fade_depth = 0.018 shader_parameter/open_water_drift_scale = 0.018 shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007) @@ -39,10 +25,13 @@ shader_parameter/surface_mark_strength = 0.12 shader_parameter/surface_mottle_scale = 0.11 shader_parameter/surface_mottle_speed = Vector2(-0.018, 0.013) shader_parameter/surface_mottle_strength = 0.07 +shader_parameter/shoreline_foam_speed = 1.42 +shader_parameter/shoreline_foam_phase_noise_scale = 0.055 +shader_parameter/shoreline_foam_phase_noise_strength = 0.68 shader_parameter/shoreline_mark_density = Vector2(4.6, 3.2) shader_parameter/shoreline_mark_drift = Vector2(0.085, -0.06) -shader_parameter/shoreline_mark_depth = 0.55 -shader_parameter/shoreline_mark_strength = 0.95 +shader_parameter/shoreline_foam_depth = 0.55 +shader_parameter/shoreline_foam_strength = 0.95 shader_parameter/shoreline_base_fill = 0.58 shader_parameter/shoreline_opacity_boost = 0.38 shader_parameter/surface_highlight_night_floor = 0.72 diff --git a/world/materials/stylized_water_fresh.tres b/world/materials/stylized_water_fresh.tres index 5fe4c8f..07bbfd3 100644 --- a/world/materials/stylized_water_fresh.tres +++ b/world/materials/stylized_water_fresh.tres @@ -8,26 +8,12 @@ shader = ExtResource("1_water_shader") shader_parameter/shore_color = Color(0.47451, 0.788235, 0.796078, 1) shader_parameter/shallow_color = Color(0.227451, 0.603922, 0.662745, 1) shader_parameter/deep_color = Color(0.109804, 0.345098, 0.435294, 1) -shader_parameter/tide_wash_color = Color(0.360784, 0.721569, 0.733333, 1) +shader_parameter/shoreline_foam_color = Color(0.580392, 0.85098, 0.858824, 1) shader_parameter/shallow_depth = 0.7 shader_parameter/deep_depth = 3.6 -shader_parameter/shore_depth_width = 0.1 shader_parameter/shore_alpha = 0.68 shader_parameter/shallow_alpha = 0.78 shader_parameter/deep_alpha = 0.9 -shader_parameter/tide_effect_enabled = false -shader_parameter/tide_speed = 1.42 -shader_parameter/tide_distance = 0.3 -shader_parameter/tide_wash_width = 0.38 -shader_parameter/tide_wash_strength = 0.11 -shader_parameter/tide_wash_alpha_shift = 0.045 -shader_parameter/phase_noise_scale = 0.055 -shader_parameter/phase_noise_strength = 0.68 -shader_parameter/contour_corner_variation = 0.045 -shader_parameter/contour_smoothing_pixels = 1.35 -shader_parameter/contour_smoothing_strength = 0.88 -shader_parameter/contour_depth_reject = 0.6 -shader_parameter/shoreline_horizontal_reach = 0.32 shader_parameter/contact_fade_depth = 0.018 shader_parameter/open_water_drift_scale = 0.018 shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007) @@ -39,10 +25,13 @@ shader_parameter/surface_mark_strength = 0.12 shader_parameter/surface_mottle_scale = 0.11 shader_parameter/surface_mottle_speed = Vector2(-0.018, 0.013) shader_parameter/surface_mottle_strength = 0.07 +shader_parameter/shoreline_foam_speed = 1.42 +shader_parameter/shoreline_foam_phase_noise_scale = 0.055 +shader_parameter/shoreline_foam_phase_noise_strength = 0.68 shader_parameter/shoreline_mark_density = Vector2(4.6, 3.2) shader_parameter/shoreline_mark_drift = Vector2(0.085, -0.06) -shader_parameter/shoreline_mark_depth = 0.55 -shader_parameter/shoreline_mark_strength = 0.95 +shader_parameter/shoreline_foam_depth = 0.55 +shader_parameter/shoreline_foam_strength = 0.95 shader_parameter/shoreline_base_fill = 0.58 shader_parameter/shoreline_opacity_boost = 0.38 shader_parameter/surface_highlight_night_floor = 0.72 diff --git a/world/rain_puddle_presentation.gd b/world/rain_puddle_presentation.gd new file mode 100644 index 0000000..82c2a39 --- /dev/null +++ b/world/rain_puddle_presentation.gd @@ -0,0 +1,645 @@ +class_name RainPuddlePresentation +extends Node3D + +## Client-only rain puddles. These are deliberately visual-only: they have no +## collision, water-region, fishing, persistence, or networking components. + +const PUDDLE_SURFACE_MATERIALS: Array[StringName] = [ + &"grass_lite", + &"grass", + &"sand", +] +const TERRAIN_COLLISION_MASK: int = 1 +const MAX_PUDDLES: int = 14 +const SURFACE_CELL_SIZE: float = 8.0 +const SURFACE_SEARCH_CELL_RADIUS: int = 3 +const SURFACE_CLEARANCE_ABOVE_WATER: float = 0.08 +const MINIMUM_SURFACE_UP_DOT: float = 0.985 +const MINIMUM_PLAYER_DISTANCE: float = 2.5 +const MAXIMUM_PLAYER_DISTANCE: float = 17.0 +const RELEASE_DISTANCE: float = 23.0 +# Puddle radius is intentionally generous: the presentation should read from +# the normal gameplay camera rather than as tiny ground specks. +const MINIMUM_PUDDLE_RADIUS: float = 0.78 +const MAXIMUM_PUDDLE_RADIUS: float = 2.76 +const MINIMUM_PUDDLE_SPACING: float = 4.35 +const MAXIMUM_PLAYER_HEIGHT_OFFSET: float = 1.0 +const SPAWN_INTERVAL_SECONDS: float = 0.18 +const FADE_IN_ALPHA_PER_SECOND: float = 0.85 +const FADE_OUT_ALPHA_PER_SECOND: float = 0.055 +const SPLASH_COOLDOWN_SECONDS: float = 0.34 +const SPLASH_COLOR := Color(0.42, 0.76, 0.80, 0.92) +const PUDDLE_COLOR := Color(0.055, 0.18, 0.225, 0.42) + + +class PuddleState: + var visual: MeshInstance3D + var material: StandardMaterial3D + var center: Vector3 = Vector3.ZERO + var radii: Vector2 = Vector2.ONE + var rotation_y: float = 0.0 + var alpha: float = 0.0 + var active: bool = false + var player_was_inside: bool = false + var splash_cooldown: float = 0.0 + + +var _test_world: TestWorld +var _weather_service: WorldWeatherService +var _local_player: Player +var _light_performance_profile: bool = false +var _is_active: bool = false +var _suppressed: bool = false +var _surface_cache_ready: bool = false +var _surface_triangles: Array[PackedVector3Array] = [] +var _surface_triangle_indices_by_cell: Dictionary = {} +var _puddles: Array[PuddleState] = [] +var _random := RandomNumberGenerator.new() +var _spawn_accumulator: float = 0.0 +var _player_was_grounded: bool = false +var _visual_root: Node3D +var _splash_root: Node3D +var _puddle_mesh: CylinderMesh + + +func _ready() -> void: + set_process(false) + + +func _exit_tree() -> void: + if ( + _weather_service != null + and _weather_service.weather_changed.is_connected( + _on_weather_changed + ) + ): + _weather_service.weather_changed.disconnect(_on_weather_changed) + + +func configure( + test_world: TestWorld, + weather_service: WorldWeatherService, + local_player: Player, + light_performance_profile: bool, +) -> void: + if ( + _weather_service != null + and _weather_service.weather_changed.is_connected( + _on_weather_changed + ) + ): + _weather_service.weather_changed.disconnect(_on_weather_changed) + _test_world = test_world + _weather_service = weather_service + _local_player = local_player + _light_performance_profile = light_performance_profile + _random.seed = _seed_for_world() + if ( + _weather_service != null + and not _weather_service.weather_changed.is_connected( + _on_weather_changed + ) + ): + _weather_service.weather_changed.connect(_on_weather_changed) + if not _light_performance_profile: + _prepare_visual_pool() + refresh_world() + _refresh_processing() + + +func set_active(active: bool) -> void: + if _is_active == active: + return + _is_active = active + if not _is_active: + _clear_puddles() + _refresh_processing() + + +func set_suppressed(suppressed: bool) -> void: + if _suppressed == suppressed: + return + _suppressed = suppressed + if _suppressed: + _clear_puddles() + _refresh_processing() + + +func refresh_world() -> void: + _surface_cache_ready = false + _surface_triangles.clear() + _surface_triangle_indices_by_cell.clear() + _spawn_accumulator = 0.0 + _player_was_grounded = false + _random.seed = _seed_for_world() + _clear_puddles() + _refresh_processing() + + +func is_light_performance_profile() -> bool: + return _light_performance_profile + + +func get_active_puddle_count() -> int: + var count: int = 0 + for puddle: PuddleState in _puddles: + if puddle.active and puddle.alpha > 0.0: + count += 1 + return count + + +func _process(delta: float) -> void: + if not _can_present_puddles(): + return + var player_position: Vector3 = _local_player.global_position + var is_raining: bool = _weather_service.is_raining() + if is_raining: + _ensure_surface_cache() + _release_distant_puddles(player_position) + _fade_puddles_in(delta) + _spawn_accumulator += delta + while _spawn_accumulator >= SPAWN_INTERVAL_SECONDS: + _spawn_accumulator -= SPAWN_INTERVAL_SECONDS + if not _spawn_puddle_near(player_position): + break + else: + _fade_puddles_out(delta) + _update_player_splashes(player_position, delta) + _refresh_processing() + + +func _on_weather_changed( + _weather: WorldWeatherService.Weather, + _seconds_remaining: float, +) -> void: + if _weather_service != null and _weather_service.is_raining(): + _spawn_accumulator = SPAWN_INTERVAL_SECONDS + _refresh_processing() + + +func _can_present_puddles() -> bool: + return ( + not _light_performance_profile + and _is_active + and not _suppressed + and _test_world != null + and _test_world.is_world_ready() + and _weather_service != null + and _local_player != null + and _local_player.is_local_control_enabled() + ) + + +func _refresh_processing() -> void: + var should_process: bool = ( + _can_present_puddles() + and ( + _weather_service.is_raining() + or _has_active_puddles() + ) + ) + set_process(should_process) + + +func _has_active_puddles() -> bool: + for puddle: PuddleState in _puddles: + if puddle.active: + return true + return false + + +func _prepare_visual_pool() -> void: + if _visual_root != null: + return + _visual_root = Node3D.new() + _visual_root.name = "PuddleVisuals" + add_child(_visual_root) + _splash_root = Node3D.new() + _splash_root.name = "PuddleSplashes" + add_child(_splash_root) + _puddle_mesh = CylinderMesh.new() + _puddle_mesh.top_radius = 1.0 + _puddle_mesh.bottom_radius = 1.0 + _puddle_mesh.height = 0.012 + _puddle_mesh.radial_segments = 12 + _puddle_mesh.rings = 1 + for _index: int in MAX_PUDDLES: + var material := StandardMaterial3D.new() + material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + material.cull_mode = BaseMaterial3D.CULL_DISABLED + material.roughness = 0.18 + material.metallic = 0.0 + var visual := MeshInstance3D.new() + visual.name = "RainPuddle" + visual.mesh = _puddle_mesh + visual.material_override = material + visual.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + visual.visible = false + _visual_root.add_child(visual) + var puddle := PuddleState.new() + puddle.visual = visual + puddle.material = material + _puddles.append(puddle) + + +func _ensure_surface_cache() -> void: + if _surface_cache_ready or _test_world == null: + return + _surface_cache_ready = true + var saltwater_height: float = _test_world.get_saltwater_surface_height() + if is_nan(saltwater_height): + return + var candidate_triangles: Array[PackedVector3Array] = ( + _test_world.get_spawn_surface_triangles( + PUDDLE_SURFACE_MATERIALS, + saltwater_height + SURFACE_CLEARANCE_ABOVE_WATER, + MINIMUM_SURFACE_UP_DOT, + ) + ) + for triangle: PackedVector3Array in candidate_triangles: + if not is_puddle_surface_eligible(triangle, saltwater_height): + continue + var triangle_index: int = _surface_triangles.size() + _surface_triangles.append(triangle) + var center: Vector3 = ( + triangle[0] + triangle[1] + triangle[2] + ) / 3.0 + var cell: Vector2i = _surface_cell_for(center) + var triangle_indices: Array = ( + _surface_triangle_indices_by_cell.get(cell, []) as Array + ) + triangle_indices.append(triangle_index) + _surface_triangle_indices_by_cell[cell] = triangle_indices + + +func _spawn_puddle_near(player_position: Vector3) -> bool: + if _active_puddle_count() >= MAX_PUDDLES: + return false + var puddle: PuddleState = _first_inactive_puddle() + if puddle == null: + return false + for _attempt: int in 24: + var triangle_index: int = _random_nearby_triangle_index( + player_position + ) + if triangle_index < 0 or triangle_index >= _surface_triangles.size(): + return false + var triangle: PackedVector3Array = _surface_triangles[triangle_index] + var position: Vector3 = _random_point_in_triangle(triangle) + var horizontal_distance: float = Vector2( + position.x, + position.z, + ).distance_to(Vector2(player_position.x, player_position.z)) + if ( + horizontal_distance < MINIMUM_PLAYER_DISTANCE + or horizontal_distance > MAXIMUM_PLAYER_DISTANCE + or _is_candidate_blocked(position) + or _is_near_existing_puddle(position) + ): + continue + var maximum_radius: float = minf( + MAXIMUM_PUDDLE_RADIUS, + _minimum_triangle_edge_distance(position, triangle) * 0.65, + ) + if maximum_radius < MINIMUM_PUDDLE_RADIUS: + continue + var radius_x: float = _random.randf_range( + MINIMUM_PUDDLE_RADIUS, + maximum_radius, + ) + var radius_z: float = radius_x * _random.randf_range(0.58, 0.86) + _activate_puddle( + puddle, + position, + Vector2(radius_x, radius_z), + _random.randf_range(0.0, TAU), + ) + return true + return false + + +func _active_puddle_count() -> int: + var count: int = 0 + for puddle: PuddleState in _puddles: + if puddle.active: + count += 1 + return count + + +func _first_inactive_puddle() -> PuddleState: + for puddle: PuddleState in _puddles: + if not puddle.active: + return puddle + return null + + +func _random_nearby_triangle_index(player_position: Vector3) -> int: + var base_cell: Vector2i = _surface_cell_for(player_position) + for _attempt: int in 16: + var cell := base_cell + Vector2i( + _random.randi_range( + -SURFACE_SEARCH_CELL_RADIUS, + SURFACE_SEARCH_CELL_RADIUS, + ), + _random.randi_range( + -SURFACE_SEARCH_CELL_RADIUS, + SURFACE_SEARCH_CELL_RADIUS, + ), + ) + var triangle_indices: Array = ( + _surface_triangle_indices_by_cell.get(cell, []) as Array + ) + if not triangle_indices.is_empty(): + return int(triangle_indices[_random.randi_range( + 0, + triangle_indices.size() - 1, + )]) + return -1 + + +func _activate_puddle( + puddle: PuddleState, + center: Vector3, + radii: Vector2, + rotation_y: float, +) -> void: + puddle.center = center + puddle.radii = radii + puddle.rotation_y = rotation_y + puddle.alpha = 0.0 + puddle.active = true + puddle.player_was_inside = false + puddle.splash_cooldown = 0.0 + puddle.visual.global_position = center + Vector3.UP * 0.012 + puddle.visual.rotation = Vector3(0.0, rotation_y, 0.0) + puddle.visual.scale = Vector3(radii.x, 1.0, radii.y) + _apply_puddle_alpha(puddle) + puddle.visual.visible = true + + +func _fade_puddles_in(delta: float) -> void: + for puddle: PuddleState in _puddles: + if not puddle.active: + continue + puddle.alpha = minf( + 1.0, + puddle.alpha + FADE_IN_ALPHA_PER_SECOND * delta, + ) + _apply_puddle_alpha(puddle) + + +func _fade_puddles_out(delta: float) -> void: + for puddle: PuddleState in _puddles: + if not puddle.active: + continue + puddle.alpha = maxf( + 0.0, + puddle.alpha - FADE_OUT_ALPHA_PER_SECOND * delta, + ) + if puddle.alpha <= 0.0: + _deactivate_puddle(puddle) + continue + _apply_puddle_alpha(puddle) + + +func _release_distant_puddles(player_position: Vector3) -> void: + for puddle: PuddleState in _puddles: + if ( + puddle.active + and Vector2(puddle.center.x, puddle.center.z).distance_to( + Vector2(player_position.x, player_position.z) + ) > RELEASE_DISTANCE + ): + _deactivate_puddle(puddle) + + +func _deactivate_puddle(puddle: PuddleState) -> void: + puddle.active = false + puddle.alpha = 0.0 + puddle.player_was_inside = false + puddle.splash_cooldown = 0.0 + if puddle.visual != null: + puddle.visual.visible = false + + +func _clear_puddles() -> void: + for puddle: PuddleState in _puddles: + _deactivate_puddle(puddle) + if _splash_root == null: + return + for splash: Node in _splash_root.get_children(): + splash.queue_free() + + +func _apply_puddle_alpha(puddle: PuddleState) -> void: + if puddle.material == null: + return + var color := PUDDLE_COLOR + color.a *= puddle.alpha + puddle.material.albedo_color = color + + +func _is_candidate_blocked(position: Vector3) -> bool: + if not is_inside_tree(): + return false + var world: World3D = get_world_3d() + if world == null: + return false + var query := PhysicsRayQueryParameters3D.create( + position + Vector3.UP * 0.05, + position + Vector3.UP * 1.75, + TERRAIN_COLLISION_MASK, + ) + query.collide_with_areas = false + query.collide_with_bodies = true + return not world.direct_space_state.intersect_ray(query).is_empty() + + +func _is_near_existing_puddle(position: Vector3) -> bool: + var horizontal_position := Vector2(position.x, position.z) + for puddle: PuddleState in _puddles: + if not puddle.active: + continue + var minimum_distance: float = ( + MINIMUM_PUDDLE_SPACING + maxf(puddle.radii.x, puddle.radii.y) + ) + if horizontal_position.distance_to( + Vector2(puddle.center.x, puddle.center.z) + ) < minimum_distance: + return true + return false + + +func _update_player_splashes( + player_position: Vector3, + delta: float, +) -> void: + var grounded: bool = _local_player.is_on_floor() + for puddle: PuddleState in _puddles: + if not puddle.active or puddle.alpha <= 0.08: + continue + puddle.splash_cooldown = maxf( + 0.0, + puddle.splash_cooldown - delta, + ) + var is_inside: bool = is_point_inside_puddle( + player_position, + puddle.center, + puddle.radii, + puddle.rotation_y, + ) + var should_splash: bool = ( + is_inside + and puddle.splash_cooldown <= 0.0 + and ( + not puddle.player_was_inside + or (not _player_was_grounded and grounded) + ) + ) + if should_splash: + _emit_splash(player_position, puddle.center.y) + puddle.splash_cooldown = SPLASH_COOLDOWN_SECONDS + puddle.player_was_inside = is_inside + _player_was_grounded = grounded + + +func _emit_splash(player_position: Vector3, surface_height: float) -> void: + if _splash_root == null or not _splash_root.visible: + return + var particles := CPUParticles3D.new() + particles.name = "RainPuddleSplash" + particles.global_position = Vector3( + player_position.x, + surface_height + 0.04, + player_position.z, + ) + particles.amount = 7 + particles.lifetime = 0.38 + particles.one_shot = true + particles.local_coords = true + particles.explosiveness = 1.0 + particles.direction = Vector3.UP + particles.spread = 48.0 + particles.gravity = Vector3(0.0, -3.2, 0.0) + particles.initial_velocity_min = 0.65 + particles.initial_velocity_max = 1.15 + particles.scale_amount_min = 0.7 + particles.scale_amount_max = 1.0 + var material := StandardMaterial3D.new() + material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + material.albedo_color = SPLASH_COLOR + material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + material.roughness = 1.0 + var droplet_mesh := BoxMesh.new() + droplet_mesh.size = Vector3(0.028, 0.04, 0.028) + droplet_mesh.material = material + particles.mesh = droplet_mesh + _splash_root.add_child(particles) + particles.emitting = true + var cleanup := create_tween() + cleanup.tween_interval(0.5) + cleanup.tween_callback(particles.queue_free) + + +func _seed_for_world() -> int: + if _test_world == null: + return 1 + return max(_test_world.get_generation_seed(), 1) ^ 0x5261696e + + +func _surface_cell_for(position: Vector3) -> Vector2i: + return Vector2i( + floori(position.x / SURFACE_CELL_SIZE), + floori(position.z / SURFACE_CELL_SIZE), + ) + + +func _random_point_in_triangle(triangle: PackedVector3Array) -> Vector3: + var first: float = _random.randf() + var second: float = _random.randf() + if first + second > 1.0: + first = 1.0 - first + second = 1.0 - second + return triangle[0] + (triangle[1] - triangle[0]) * first + ( + triangle[2] - triangle[0] + ) * second + + +func _minimum_triangle_edge_distance( + position: Vector3, + triangle: PackedVector3Array, +) -> float: + var horizontal_position := Vector2(position.x, position.z) + return minf( + _horizontal_distance_to_segment( + horizontal_position, + Vector2(triangle[0].x, triangle[0].z), + Vector2(triangle[1].x, triangle[1].z), + ), + minf( + _horizontal_distance_to_segment( + horizontal_position, + Vector2(triangle[1].x, triangle[1].z), + Vector2(triangle[2].x, triangle[2].z), + ), + _horizontal_distance_to_segment( + horizontal_position, + Vector2(triangle[2].x, triangle[2].z), + Vector2(triangle[0].x, triangle[0].z), + ), + ), + ) + + +static func is_puddle_surface_eligible( + triangle: PackedVector3Array, + saltwater_height: float, +) -> bool: + if triangle.size() != 3 or is_nan(saltwater_height): + return false + for vertex: Vector3 in triangle: + if vertex.y <= saltwater_height + SURFACE_CLEARANCE_ABOVE_WATER: + return false + var cross: Vector3 = (triangle[1] - triangle[0]).cross( + triangle[2] - triangle[0] + ) + return ( + cross.length_squared() > 0.0000001 + and absf(cross.normalized().dot(Vector3.UP)) >= MINIMUM_SURFACE_UP_DOT + ) + + +static func is_point_inside_puddle( + world_position: Vector3, + center: Vector3, + radii: Vector2, + rotation_y: float = 0.0, +) -> bool: + if radii.x <= 0.0 or radii.y <= 0.0: + return false + if absf(world_position.y - center.y) > MAXIMUM_PLAYER_HEIGHT_OFFSET: + return false + var offset := Vector2( + world_position.x - center.x, + world_position.z - center.z, + ).rotated(-rotation_y) + offset = Vector2(offset.x / radii.x, offset.y / radii.y) + return offset.length_squared() <= 1.0 + + +static func _horizontal_distance_to_segment( + point: Vector2, + start: Vector2, + end: Vector2, +) -> float: + var segment: Vector2 = end - start + var segment_length_squared: float = segment.length_squared() + if segment_length_squared <= 0.0000001: + return point.distance_to(start) + var ratio: float = clampf( + (point - start).dot(segment) / segment_length_squared, + 0.0, + 1.0, + ) + return point.distance_to(start + segment * ratio) diff --git a/world/rain_puddle_presentation.gd.uid b/world/rain_puddle_presentation.gd.uid new file mode 100644 index 0000000..6fff927 --- /dev/null +++ b/world/rain_puddle_presentation.gd.uid @@ -0,0 +1 @@ +uid://lhwc7dc1pivp diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index 5352c39..130da68 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -107,8 +107,10 @@ func get_rv_upgrade_shop() -> RVUpgradeInteractionType: return get_node_or_null(rv_upgrade_shop_path) as RVUpgradeInteractionType -func get_saltwater_shoreline_mesh() -> MeshInstance3D: - return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D +func _get_ocean_surface_motion() -> WaterSurfaceMotion: + return get_node_or_null( + ^"WaterBodies/Ocean/VisualWater" + ) as WaterSurfaceMotion func set_light_performance_profile(enabled: bool) -> void: @@ -116,9 +118,7 @@ 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 + var ocean := _get_ocean_surface_motion() if pond != null: pond.material_override = ( _create_light_water_material( @@ -129,6 +129,9 @@ func set_light_performance_profile(enabled: bool) -> void: else NORMAL_FRESH_WATER_MATERIAL ) if ocean != null: + # Reset the old shader material before replacing it with the lite material. + # Otherwise its last bob offset could leak into the next normal-profile use. + ocean.set_motion_enabled(false) ocean.material_override = ( _create_light_water_material( LIGHT_SALT_WATER_COLOR, @@ -137,20 +140,15 @@ 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) - - + if not enabled: + ocean.set_motion_enabled(true) func set_foliage_wind_strength_multiplier(multiplier: float) -> void: _foliage_wind_strength_multiplier = maxf(multiplier, 0.0) _set_foliage_wind_enabled(_foliage_wind_enabled) func set_water_motion_strength_multiplier(multiplier: float) -> void: - var ocean := get_node_or_null( - ^"WaterBodies/Ocean/VisualWater" - ) as WaterSurfaceMotion + var ocean := _get_ocean_surface_motion() if ocean != null: ocean.set_motion_strength_multiplier(multiplier) diff --git a/world/regions/starter_island_region.tscn b/world/regions/starter_island_region.tscn index 6f7f10b..3da63eb 100644 --- a/world/regions/starter_island_region.tscn +++ b/world/regions/starter_island_region.tscn @@ -11,11 +11,8 @@ [ext_resource type="PackedScene" path="res://world/interactables/player_storage_box.tscn" id="8_storage"] [ext_resource type="Script" uid="uid://dtcgnxl1kmdu1" path="res://world/water_body_authoring.gd" id="10_water_body"] [ext_resource type="Material" path="res://world/materials/stylized_water.tres" id="13_water"] -[ext_resource type="ArrayMesh" path="res://world/generated/shorelines/starter_ocean_shoreline.tres" id="17_ocean_shoreline"] [ext_resource type="Material" path="res://world/materials/stylized_water_fresh.tres" id="19_fresh_water"] [ext_resource type="Script" uid="uid://drdjm443o35tc" path="res://world/water_surface_motion.gd" id="20_surface_motion"] -[ext_resource type="Script" path="res://world/water/shoreline_ribbon_baker.gd" id="21_shoreline_baker"] -[ext_resource type="Script" path="res://world/water/shoreline_ribbon_config.gd" id="22_shoreline_config"] [ext_resource type="Script" path="res://world/digging/diggable_area_3d.gd" id="23_diggable_area"] [ext_resource type="Script" path="res://world/gathering/gatherable_anchor_set_3d.gd" id="24_gatherable_anchors"] [ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="25_decor_shop"] @@ -60,29 +57,6 @@ size = Vector3(48, 5, 18) resource_local_to_scene = true size = Vector3(48, 5, 18) -[sub_resource type="Resource" id="ShorelineRibbonConfigPond"] -script = ExtResource("22_shoreline_config") -terrain_source = NodePath("../Terrain/Visual/starter_island") -water_height = 2.51 -generation_bounds = Rect2(-17.45, -4.08375, 16.1, 12.3675) -water_reference = Vector2(-9.4, 2.1) -water_is_inside = true -output_resource_path = "res://world/generated/shorelines/starter_pond_shoreline.tres" - -[sub_resource type="Resource" id="ShorelineRibbonConfigOcean"] -script = ExtResource("22_shoreline_config") -terrain_source = NodePath("../Terrain/Visual/starter_island") -water_type = 1 -water_height = -0.45 -generation_bounds = Rect2(-42, -50, 108, 100) -water_reference = Vector2(0, 0) -water_is_inside = false -output_resource_path = "res://world/generated/shorelines/starter_ocean_shoreline.tres" -simplification_tolerance = 0.55 -smoothing_iterations = 3 -resample_spacing = 0.16 -corner_rounding_distance = 2.4 - [node name="StarterIslandRegion" type="Node3D" unique_id=1830415700] script = ExtResource("1_region") region_id = &"starter_island" @@ -142,17 +116,6 @@ position = Vector3(-19.4381, 4.4016, 0.5559) [node name="TreeWestNorth" type="Marker3D" parent="GatherableAnchors/ReachableTreeTrunks"] position = Vector3(-17.4368, 4.3886, 1.3864) -[node name="ShorelineRibbonBaker" type="Node" parent="."] -script = ExtResource("21_shoreline_baker") -water_bodies = Array[ExtResource("22_shoreline_config")]([SubResource("ShorelineRibbonConfigPond"), SubResource("ShorelineRibbonConfigOcean")]) - -[node name="ShorelineRibbons" type="Node3D" parent="."] - -[node name="Ocean" type="MeshInstance3D" parent="ShorelineRibbons"] -visible = false -cast_shadow = 0 -mesh = ExtResource("17_ocean_shoreline") - [node name="WaterBodies" type="Node3D" parent="." unique_id=1417012172] [node name="Pond" type="Node3D" parent="WaterBodies" unique_id=995477450] diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index ac5b2ff..b19175d 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -30,10 +30,6 @@ func get_player_storage() -> PlayerStorageInteraction: return null -func get_saltwater_shoreline_mesh() -> MeshInstance3D: - return null - - func set_light_performance_profile(_enabled: bool) -> void: pass diff --git a/world/shoreline_ambience.gd b/world/shoreline_ambience.gd deleted file mode 100644 index 9bda5f6..0000000 --- a/world/shoreline_ambience.gd +++ /dev/null @@ -1,200 +0,0 @@ -class_name ShorelineAmbience -extends Node - -const WAVES_STREAM_PATH: String = "res://audio/ambience/waves.wav" - -@export_range(0.0, 20.0, 0.1) var near_distance: float = 2.0 -@export_range(1.0, 100.0, 0.5) var far_distance: float = 24.0 -@export_range(-40.0, 12.0, 0.5) var near_volume_db: float = 1.0 -@export_range(-80.0, 0.0, 0.5) var far_volume_db: float = -18.0 -@export_range(0.05, 1.0, 0.05) var distance_update_interval: float = 0.2 -@export_range(0.1, 20.0, 0.1) var volume_smoothing_speed: float = 4.0 - -@onready var _waves_audio: AudioStreamPlayer = %WavesAudio - -var _listener: Node3D -var _shoreline_segments: Array[PackedVector2Array] = [] -var _distance_update_remaining: float = 0.0 -var _target_volume_db: float = -80.0 -var _is_active := false -var _audio_enabled: bool = false -var _suppressed: bool = false - - -func _ready() -> void: - _waves_audio.bus = &"Environment" - _waves_audio.stream = null - _waves_audio.volume_db = -80.0 - set_process(false) - - -func set_audio_enabled(enabled: bool) -> void: - if _audio_enabled == enabled: - return - _audio_enabled = enabled - if not _audio_enabled: - _waves_audio.stop() - _waves_audio.stream = null - set_process(false) - return - if not ResourceLoader.exists(WAVES_STREAM_PATH, "AudioStream"): - return - _waves_audio.stream = load(WAVES_STREAM_PATH) as AudioStream - _configure_audio_loop(_waves_audio.stream) - if _is_active: - set_process(true) - _update_target_volume() - _waves_audio.volume_db = _target_volume_db - if _waves_audio.stream != null: - _waves_audio.play() - - -func configure(listener: Node3D, shoreline_mesh: MeshInstance3D) -> void: - _listener = listener - _shoreline_segments = _extract_shoreline_segments(shoreline_mesh) - _distance_update_remaining = 0.0 - if _shoreline_segments.is_empty(): - push_warning("Saltwater shoreline ambience has no coastline geometry.") - - -func set_active(active: bool) -> void: - _is_active = active - set_process(active and _audio_enabled and not _suppressed) - if active and not _suppressed: - if not _audio_enabled: - return - _distance_update_remaining = 0.0 - _update_target_volume() - _waves_audio.volume_db = _target_volume_db - if not _waves_audio.playing and _waves_audio.stream != null: - _waves_audio.play() - else: - _waves_audio.stop() - _waves_audio.volume_db = -80.0 - - -func set_suppressed(suppressed: bool) -> void: - if _suppressed == suppressed: - return - _suppressed = suppressed - set_process(_is_active and _audio_enabled and not suppressed) - if suppressed: - _waves_audio.stop() - _waves_audio.volume_db = -80.0 - elif _is_active and _audio_enabled: - _distance_update_remaining = 0.0 - _update_target_volume() - _waves_audio.volume_db = _target_volume_db - if _waves_audio.stream != null: - _waves_audio.play() - - -func _process(delta: float) -> void: - if not _is_active: - return - _distance_update_remaining -= delta - if _distance_update_remaining <= 0.0: - _distance_update_remaining = distance_update_interval - _update_target_volume() - _waves_audio.volume_db = lerpf( - _waves_audio.volume_db, - _target_volume_db, - 1.0 - exp(-volume_smoothing_speed * delta), - ) - - -func _update_target_volume() -> void: - if _listener == null or _shoreline_segments.is_empty(): - _target_volume_db = -80.0 - return - var listener_position := Vector2( - _listener.global_position.x, - _listener.global_position.z, - ) - var distance := distance_to_segments(listener_position, _shoreline_segments) - var blend := clampf( - inverse_lerp(near_distance, maxf(near_distance + 0.01, far_distance), distance), - 0.0, - 1.0, - ) - _target_volume_db = lerpf(near_volume_db, far_volume_db, smoothstep(0.0, 1.0, blend)) - - -func _extract_shoreline_segments( - shoreline_mesh: MeshInstance3D, -) -> Array[PackedVector2Array]: - var segments: Array[PackedVector2Array] = [] - var seen_segments: Dictionary = {} - if shoreline_mesh == null or shoreline_mesh.mesh == null: - return segments - for surface_index in shoreline_mesh.mesh.get_surface_count(): - var arrays := shoreline_mesh.mesh.surface_get_arrays(surface_index) - var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] - var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX] - if indices.is_empty(): - continue - for index_offset in range(0, indices.size() - 2, 3): - var triangle := PackedVector2Array() - for corner in 3: - var vertex := shoreline_mesh.to_global( - vertices[indices[index_offset + corner]] - ) - triangle.append(Vector2(vertex.x, vertex.z)) - _append_unique_segment(segments, seen_segments, triangle[0], triangle[1]) - _append_unique_segment(segments, seen_segments, triangle[1], triangle[2]) - _append_unique_segment(segments, seen_segments, triangle[2], triangle[0]) - return segments - - -func _append_unique_segment( - segments: Array[PackedVector2Array], - seen_segments: Dictionary, - start: Vector2, - end: Vector2, -) -> void: - if start.distance_squared_to(end) <= 0.000001: - return - # The baked ribbon repeats shared triangle edges. Quantized canonical keys - # keep only distinct segments without an O(n²) startup pass. - var first := start - var second := end - if first.x > second.x or (is_equal_approx(first.x, second.x) and first.y > second.y): - first = end - second = start - var key := "%d,%d:%d,%d" % [ - roundi(first.x * 10000.0), - roundi(first.y * 10000.0), - roundi(second.x * 10000.0), - roundi(second.y * 10000.0), - ] - if seen_segments.has(key): - return - seen_segments[key] = true - segments.append(PackedVector2Array([start, end])) - - -static func distance_to_segments( - point: Vector2, - segments: Array[PackedVector2Array], -) -> float: - var closest_squared := INF - for segment in segments: - if segment.size() < 2: - continue - var closest := Geometry2D.get_closest_point_to_segment( - point, - segment[0], - segment[1], - ) - closest_squared = minf(closest_squared, point.distance_squared_to(closest)) - return sqrt(closest_squared) if closest_squared < INF else INF - - -func _configure_audio_loop(stream: AudioStream) -> void: - var wav := stream as AudioStreamWAV - if wav == null: - return - wav.loop_mode = AudioStreamWAV.LOOP_FORWARD - if wav.loop_end <= wav.loop_begin: - wav.loop_begin = 0 - wav.loop_end = int(round(wav.get_length() * wav.mix_rate)) diff --git a/world/shoreline_ambience.gd.uid b/world/shoreline_ambience.gd.uid deleted file mode 100644 index 3188ec7..0000000 --- a/world/shoreline_ambience.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b0c3o2vy76fg3 diff --git a/world/test_world.gd b/world/test_world.gd index b036626..64dd157 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -188,10 +188,6 @@ func get_fishable_water_regions() -> Array[FishableWaterRegion]: ) -func get_saltwater_shoreline_mesh() -> MeshInstance3D: - return _active_region.get_saltwater_shoreline_mesh() - - func get_spawn_surface_triangles( material_names: Array[StringName], minimum_global_y: float, diff --git a/world/water/shoreline_ribbon_baker.gd b/world/water/shoreline_ribbon_baker.gd deleted file mode 100644 index a88b6e4..0000000 --- a/world/water/shoreline_ribbon_baker.gd +++ /dev/null @@ -1,139 +0,0 @@ -@tool -class_name ShorelineRibbonBaker -extends Node - -@export var water_bodies: Array[ShorelineRibbonConfig] = [] -@export_enum("Off", "Raw", "Simplified", "Smoothed") var debug_path_stage := 0 -@export_tool_button("Rebuild Shoreline Ribbons") -var rebuild_shorelines: Callable = rebuild_all - - -func rebuild_all() -> Array[Dictionary]: - var results: Array[Dictionary] = [] - for configuration: ShorelineRibbonConfig in water_bodies: - var result := _rebuild(configuration) - if result.is_empty(): - return [] - results.append(result) - _update_debug_display(results) - return results - - -func _rebuild(configuration: ShorelineRibbonConfig) -> Dictionary: - if configuration == null: - push_error("Shoreline ribbon configuration is missing.") - return {} - if configuration.water_type != WaterType.Type.SALT_WATER: - return { - "skipped": true, - "water_type": configuration.water_type, - "output_path": configuration.output_resource_path, - } - var source := get_node_or_null(configuration.terrain_source) as Node3D - if source == null: - push_error( - "Shoreline terrain source was not found: %s" - % configuration.terrain_source - ) - return {} - if configuration.output_resource_path.is_empty(): - push_error("Shoreline output resource path is empty.") - return {} - var faces := _terrain_faces(source) - if faces.is_empty(): - push_error("Configured shoreline terrain source exposes no triangles.") - return {} - var result := ShorelineRibbonGenerator.generate( - faces, - configuration.water_height, - configuration.generation_bounds, - configuration.water_reference, - configuration.water_is_inside, - configuration.simplification_tolerance, - configuration.smoothing_iterations, - configuration.resample_spacing, - configuration.corner_rounding_distance, - ) - var output_directory := configuration.output_resource_path.get_base_dir() - DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_directory)) - var save_error := ResourceSaver.save( - result["mesh"], configuration.output_resource_path - ) - if save_error != OK: - push_error( - "Failed to save %s: %s" - % [configuration.output_resource_path, error_string(save_error)] - ) - return {} - result["output_path"] = configuration.output_resource_path - result["water_height"] = configuration.water_height - result["water_type"] = configuration.water_type - result["skipped"] = false - return result - - -func _update_debug_display(results: Array[Dictionary]) -> void: - var previous := get_node_or_null("_ShorelinePathDebug") - if previous != null: - previous.queue_free() - if not Engine.is_editor_hint() or debug_path_stage == 0: - return - var debug_mesh := ImmediateMesh.new() - var debug_material := StandardMaterial3D.new() - debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - debug_material.albedo_color = Color(1.0, 0.35, 0.65, 1.0) - var stage_key: String = [ - "", "debug_raw_paths", "debug_simplified_paths", "debug_smoothed_paths" - ][debug_path_stage] - for result: Dictionary in results: - if result.get("skipped", false): - continue - var height := float(result["water_height"]) + 0.06 - for path_data: Dictionary in result[stage_key]: - var points: PackedVector2Array = path_data["points"] - if points.size() < 2: - continue - debug_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP, debug_material) - for point: Vector2 in points: - debug_mesh.surface_add_vertex(Vector3(point.x, height, point.y)) - if path_data["closed"]: - debug_mesh.surface_add_vertex(Vector3(points[0].x, height, points[0].y)) - debug_mesh.surface_end() - var debug_instance := MeshInstance3D.new() - debug_instance.name = "_ShorelinePathDebug" - debug_instance.mesh = debug_mesh - add_child(debug_instance) - - -func _terrain_faces(source: Node3D) -> PackedVector3Array: - var result := PackedVector3Array() - if source is CollisionShape3D: - var collision_shape := source as CollisionShape3D - var concave_shape := collision_shape.shape as ConcavePolygonShape3D - if concave_shape == null: - return result - for vertex: Vector3 in concave_shape.get_faces(): - result.append(_to_map_space(source, vertex)) - return result - if source is MeshInstance3D: - var mesh_instance := source as MeshInstance3D - if mesh_instance.mesh == null: - return result - for surface: int in mesh_instance.mesh.get_surface_count(): - var arrays := mesh_instance.mesh.surface_get_arrays(surface) - var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array - var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array - if indices.is_empty(): - for vertex: Vector3 in vertices: - result.append(_to_map_space(source, vertex)) - else: - for index: int in indices: - result.append(_to_map_space(source, vertices[index])) - return result - - -func _to_map_space(source: Node3D, vertex: Vector3) -> Vector3: - var map_root := get_parent() as Node3D - if map_root == null: - return source.to_global(vertex) - return map_root.to_local(source.to_global(vertex)) diff --git a/world/water/shoreline_ribbon_baker.gd.uid b/world/water/shoreline_ribbon_baker.gd.uid deleted file mode 100644 index 2991de4..0000000 --- a/world/water/shoreline_ribbon_baker.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bj704rvwwh77b diff --git a/world/water/shoreline_ribbon_config.gd b/world/water/shoreline_ribbon_config.gd deleted file mode 100644 index 682bfed..0000000 --- a/world/water/shoreline_ribbon_config.gd +++ /dev/null @@ -1,15 +0,0 @@ -class_name ShorelineRibbonConfig -extends Resource - -@export_node_path("CollisionShape3D", "MeshInstance3D") var terrain_source: NodePath -@export var water_type: WaterType.Type = WaterType.Type.FRESH_WATER -@export var water_height := 0.0 -@export var generation_bounds := Rect2() -@export var water_reference := Vector2.ZERO -@export var water_is_inside := true -@export_file("*.tres") var output_resource_path := "" -@export_group("Optional Smoothing Overrides") -@export var simplification_tolerance := -1.0 -@export_range(-1, 4, 1) var smoothing_iterations := -1 -@export var resample_spacing := -1.0 -@export var corner_rounding_distance := -1.0 diff --git a/world/water/shoreline_ribbon_config.gd.uid b/world/water/shoreline_ribbon_config.gd.uid deleted file mode 100644 index 6a563e3..0000000 --- a/world/water/shoreline_ribbon_config.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c8hk3eojujyax diff --git a/world/water/shoreline_ribbon_generator.gd b/world/water/shoreline_ribbon_generator.gd deleted file mode 100644 index 9708b06..0000000 --- a/world/water/shoreline_ribbon_generator.gd +++ /dev/null @@ -1,636 +0,0 @@ -class_name ShorelineRibbonGenerator -extends RefCounted - -const WATER_PLANE_EPSILON := 0.001 -const ENDPOINT_MERGE_TOLERANCE := 0.03 -const LOOP_CLOSURE_TOLERANCE := 0.06 -const MINIMUM_FRAGMENT_LENGTH := 2.0 -const SIMPLIFICATION_TOLERANCE := 0.55 -const SMOOTHING_ITERATIONS := 3 -const RESAMPLE_SPACING := 0.16 -const CORNER_ROUNDING_DISTANCE := 2.4 -const CORNER_ROUNDING_START_DEGREES := 24.0 -const CORNER_ROUNDING_FULL_DEGREES := 78.0 -const MAXIMUM_JOIN_SCALE := 1.0 -const RIBBON_REACH_GROWTH_PER_METER := 0.52 -const RIBBON_WIDTH := 1.35 -const LAND_INSET := 0.12 -const SURFACE_OFFSET := 0.018 - - -static func generate( - faces: PackedVector3Array, - water_height: float, - bounds: Rect2, - water_reference: Vector2, - water_is_inside: bool, - simplification_override := -1.0, - smoothing_iterations_override := -1, - resample_spacing_override := -1.0, - corner_rounding_override := -1.0 -) -> Dictionary: - var simplification := ( - SIMPLIFICATION_TOLERANCE - if simplification_override < 0.0 - else simplification_override - ) - var smoothing_iterations := ( - SMOOTHING_ITERATIONS - if smoothing_iterations_override < 0 - else smoothing_iterations_override - ) - var resample_spacing := ( - RESAMPLE_SPACING - if resample_spacing_override < 0.0 - else resample_spacing_override - ) - var corner_rounding := ( - CORNER_ROUNDING_DISTANCE - if corner_rounding_override < 0.0 - else corner_rounding_override - ) - var segments := _extract_segments(faces, water_height, bounds) - var raw_paths := _stitch_segments(segments) - var processed_paths: Array[Dictionary] = [] - var simplified_paths: Array[Dictionary] = [] - var raw_point_count := 0 - var simplified_point_count := 0 - var smoothed_point_count := 0 - for path_data: Dictionary in raw_paths: - var raw_points: PackedVector2Array = path_data["points"] - var closed: bool = path_data["closed"] - if _path_length(raw_points, closed) < MINIMUM_FRAGMENT_LENGTH: - continue - var simplified := _simplify(raw_points, closed, simplification) - simplified_paths.append({"points": simplified, "closed": closed}) - var rounded := _round_sharp_corners( - simplified, - closed, - corner_rounding, - ) - var smoothed := _chaikin(rounded, closed, smoothing_iterations) - var resampled := _resample(smoothed, closed, resample_spacing) - if resampled.size() < (3 if closed else 2): - continue - raw_point_count += raw_points.size() - simplified_point_count += simplified.size() - smoothed_point_count += resampled.size() - processed_paths.append({"points": resampled, "closed": closed}) - var mesh := _build_mesh( - processed_paths, - water_height, - water_reference, - water_is_inside - ) - return { - "mesh": mesh, - "segment_count": segments.size(), - "loop_count": processed_paths.size(), - "raw_point_count": raw_point_count, - "simplified_point_count": simplified_point_count, - "smoothed_point_count": smoothed_point_count, - "triangle_count": _mesh_triangle_count(mesh), - "debug_raw_paths": raw_paths, - "debug_simplified_paths": simplified_paths, - "debug_smoothed_paths": processed_paths, - } - - -static func _extract_segments( - faces: PackedVector3Array, - water_height: float, - bounds: Rect2 -) -> Array[PackedVector2Array]: - var segments: Array[PackedVector2Array] = [] - for index: int in range(0, faces.size(), 3): - var triangle: Array[Vector3] = [ - faces[index], faces[index + 1], faces[index + 2] - ] - var hits := PackedVector2Array() - for edge: int in 3: - var a := triangle[edge] - var b := triangle[(edge + 1) % 3] - var distance_a := a.y - water_height - var distance_b := b.y - water_height - if ( - absf(distance_a) <= WATER_PLANE_EPSILON - and absf(distance_b) <= WATER_PLANE_EPSILON - ): - continue - if not ( - distance_a * distance_b < 0.0 - or absf(distance_a) <= WATER_PLANE_EPSILON - or absf(distance_b) <= WATER_PLANE_EPSILON - ): - continue - var denominator := distance_a - distance_b - var amount := ( - 0.0 - if absf(denominator) <= WATER_PLANE_EPSILON - else distance_a / denominator - ) - var hit3 := a.lerp(b, clampf(amount, 0.0, 1.0)) - var hit := Vector2(hit3.x, hit3.z) - if bounds.has_point(hit) and not _contains_near(hits, hit): - hits.append(hit) - if ( - hits.size() == 2 - and hits[0].distance_to(hits[1]) > WATER_PLANE_EPSILON - ): - segments.append(hits) - return segments - - -static func _stitch_segments( - segments: Array[PackedVector2Array] -) -> Array[Dictionary]: - var point_by_key: Dictionary = {} - var adjacency: Dictionary = {} - var unused_edges: Dictionary = {} - for segment: PackedVector2Array in segments: - var a_key := _point_key(segment[0]) - var b_key := _point_key(segment[1]) - if a_key == b_key: - continue - point_by_key[a_key] = segment[0] - point_by_key[b_key] = segment[1] - if not adjacency.has(a_key): - adjacency[a_key] = [] - if not adjacency.has(b_key): - adjacency[b_key] = [] - var edge_key := _edge_key(a_key, b_key) - if unused_edges.has(edge_key): - continue - (adjacency[a_key] as Array).append(b_key) - (adjacency[b_key] as Array).append(a_key) - unused_edges[edge_key] = true - - var starts: Array = [] - for key: Vector2i in adjacency: - if (adjacency[key] as Array).size() != 2: - starts.append(key) - for key: Vector2i in adjacency: - if not starts.has(key): - starts.append(key) - - var paths: Array[Dictionary] = [] - for start: Vector2i in starts: - while _has_unused_neighbor(start, adjacency, unused_edges): - var walked := _walk_path(start, point_by_key, adjacency, unused_edges) - if (walked["points"] as PackedVector2Array).size() >= 2: - paths.append(walked) - return paths - - -static func _walk_path( - start: Vector2i, - point_by_key: Dictionary, - adjacency: Dictionary, - unused_edges: Dictionary -) -> Dictionary: - var points := PackedVector2Array() - var previous := Vector2i(2147483647, 2147483647) - var current := start - var closed := false - var guard := unused_edges.size() + 2 - while guard > 0: - guard -= 1 - points.append(point_by_key[current]) - var next_key := Vector2i(2147483647, 2147483647) - for candidate: Vector2i in adjacency[current]: - if candidate == previous and (adjacency[current] as Array).size() > 1: - continue - if unused_edges.get(_edge_key(current, candidate), false): - next_key = candidate - break - if next_key.x == 2147483647: - for candidate: Vector2i in adjacency[current]: - if unused_edges.get(_edge_key(current, candidate), false): - next_key = candidate - break - if next_key.x == 2147483647: - break - unused_edges[_edge_key(current, next_key)] = false - previous = current - current = next_key - if current == start: - closed = true - break - return {"points": points, "closed": closed} - - -static func _simplify( - points: PackedVector2Array, - closed: bool, - tolerance: float -) -> PackedVector2Array: - if points.size() <= (4 if closed else 2): - return points - if not closed: - return _rdp_open(points, tolerance) - var split_a := 0 - var split_b := 1 - var greatest_distance := 0.0 - for a: int in points.size(): - for b: int in range(a + 1, points.size()): - var distance := points[a].distance_squared_to(points[b]) - if distance > greatest_distance: - greatest_distance = distance - split_a = a - split_b = b - var first_arc := _closed_arc(points, split_a, split_b) - var second_arc := _closed_arc(points, split_b, split_a) - var first_result := _rdp_open(first_arc, tolerance) - var second_result := _rdp_open(second_arc, tolerance) - var result := PackedVector2Array() - for index: int in first_result.size() - 1: - result.append(first_result[index]) - for index: int in second_result.size() - 1: - result.append(second_result[index]) - return result if result.size() >= 4 else points - - -static func _rdp_open( - points: PackedVector2Array, - tolerance: float -) -> PackedVector2Array: - if points.size() <= 2: - return points - var greatest_distance := 0.0 - var split_index := 0 - for index: int in range(1, points.size() - 1): - var distance := _point_segment_distance( - points[index], points[0], points[points.size() - 1] - ) - if distance > greatest_distance: - greatest_distance = distance - split_index = index - if greatest_distance <= tolerance: - return PackedVector2Array([points[0], points[points.size() - 1]]) - var left := _rdp_open(points.slice(0, split_index + 1), tolerance) - var right := _rdp_open(points.slice(split_index), tolerance) - var result := PackedVector2Array() - for index: int in left.size() - 1: - result.append(left[index]) - result.append_array(right) - return result - - -static func _closed_arc( - points: PackedVector2Array, - start: int, - finish: int -) -> PackedVector2Array: - var result := PackedVector2Array() - var index := start - result.append(points[index]) - while index != finish: - index = (index + 1) % points.size() - result.append(points[index]) - return result - - -static func _chaikin( - points: PackedVector2Array, - closed: bool, - iterations: int -) -> PackedVector2Array: - var result := points - for _iteration: int in iterations: - var next := PackedVector2Array() - if not closed: - next.append(result[0]) - var edge_count := result.size() if closed else result.size() - 1 - for index: int in edge_count: - var a := result[index] - var b := result[(index + 1) % result.size()] - next.append(a.lerp(b, 0.25)) - next.append(a.lerp(b, 0.75)) - if not closed: - next.append(result[result.size() - 1]) - result = next - return result - - -static func _round_sharp_corners( - points: PackedVector2Array, - closed: bool, - rounding_distance: float, -) -> PackedVector2Array: - if rounding_distance <= WATER_PLANE_EPSILON or points.size() < 3: - return points - var result := PackedVector2Array() - for index: int in points.size(): - if not closed and (index == 0 or index == points.size() - 1): - result.append(points[index]) - continue - var previous := points[(index - 1 + points.size()) % points.size()] - var point := points[index] - var following := points[(index + 1) % points.size()] - var incoming_vector := point - previous - var outgoing_vector := following - point - var incoming_length := incoming_vector.length() - var outgoing_length := outgoing_vector.length() - if ( - incoming_length <= WATER_PLANE_EPSILON - or outgoing_length <= WATER_PLANE_EPSILON - ): - result.append(point) - continue - var incoming := incoming_vector / incoming_length - var outgoing := outgoing_vector / outgoing_length - var turn_degrees := rad_to_deg( - acos(clampf(incoming.dot(outgoing), -1.0, 1.0)) - ) - var corner_weight := smoothstep( - CORNER_ROUNDING_START_DEGREES, - CORNER_ROUNDING_FULL_DEGREES, - turn_degrees, - ) - var cut_distance := minf( - rounding_distance * corner_weight, - minf(incoming_length, outgoing_length) * 0.44, - ) - if cut_distance <= WATER_PLANE_EPSILON: - result.append(point) - continue - var entry := point - incoming * cut_distance - var exit := point + outgoing * cut_distance - var curve_steps := maxi(3, ceili(cut_distance / 0.22)) - for step: int in curve_steps + 1: - var amount := float(step) / float(curve_steps) - var first := entry.lerp(point, amount) - var second := point.lerp(exit, amount) - var rounded_point := first.lerp(second, amount) - if ( - result.is_empty() - or result[result.size() - 1].distance_to(rounded_point) - > WATER_PLANE_EPSILON - ): - result.append(rounded_point) - return result - - -static func _resample( - points: PackedVector2Array, - closed: bool, - spacing: float -) -> PackedVector2Array: - var total_length := _path_length(points, closed) - if total_length <= spacing: - return points - var count := maxi(roundi(total_length / spacing), 3 if closed else 2) - var actual_spacing := total_length / float(count if closed else count - 1) - var result := PackedVector2Array() - var edge := 0 - var edge_start_distance := 0.0 - var edge_length := points[0].distance_to(points[1]) - for sample: int in count: - var target := actual_spacing * sample - while target > edge_start_distance + edge_length and edge < points.size() - 1: - edge_start_distance += edge_length - edge += 1 - if edge >= points.size() - 1: - edge_length = points[edge].distance_to(points[0]) if closed else 0.0 - else: - edge_length = points[edge].distance_to(points[edge + 1]) - var next_index := (edge + 1) % points.size() - var amount := ( - 0.0 - if edge_length <= WATER_PLANE_EPSILON - else (target - edge_start_distance) / edge_length - ) - result.append(points[edge].lerp(points[next_index], clampf(amount, 0.0, 1.0))) - return result - - -static func _build_mesh( - paths: Array[Dictionary], - water_height: float, - water_reference: Vector2, - water_is_inside: bool -) -> ArrayMesh: - var vertices := PackedVector3Array() - var normals := PackedVector3Array() - var uvs := PackedVector2Array() - var indices := PackedInt32Array() - for path_data: Dictionary in paths: - var points: PackedVector2Array = path_data["points"] - var closed: bool = path_data["closed"] - var closed_water_side := ( - _closed_path_water_side(points, water_is_inside) - if closed - else 0.0 - ) - var base_index := vertices.size() - var water_normals := PackedVector2Array() - var join_scales := PackedFloat32Array() - var water_reaches := PackedFloat32Array() - for index: int in points.size(): - var previous := points[(index - 1 + points.size()) % points.size()] - var following := points[(index + 1) % points.size()] - if not closed: - previous = points[maxi(index - 1, 0)] - following = points[mini(index + 1, points.size() - 1)] - var point := points[index] - var incoming := previous.direction_to(point) - var outgoing := point.direction_to(following) - if incoming.is_zero_approx(): - incoming = outgoing - if outgoing.is_zero_approx(): - outgoing = incoming - var incoming_normal := Vector2(-incoming.y, incoming.x) - var outgoing_normal := Vector2(-outgoing.y, outgoing.x) - var water_normal := incoming_normal + outgoing_normal - if water_normal.is_zero_approx(): - water_normal = outgoing_normal - water_normal = water_normal.normalized() - if closed: - water_normal *= closed_water_side - else: - var toward_reference := point.direction_to(water_reference) - if ( - (water_is_inside and water_normal.dot(toward_reference) < 0.0) - or (not water_is_inside and water_normal.dot(toward_reference) > 0.0) - ): - water_normal = -water_normal - var join_scale := minf( - 1.0 / maxf(absf(water_normal.dot(outgoing_normal)), 0.55), - MAXIMUM_JOIN_SCALE, - ) - var water_reach := _safe_water_reach( - incoming, - outgoing, - water_normal, - previous.distance_to(point), - point.distance_to(following), - (RIBBON_WIDTH - LAND_INSET) * join_scale, - ) - water_normals.append(water_normal) - join_scales.append(join_scale) - water_reaches.append(water_reach) - water_reaches = _smooth_water_reaches( - points, - water_reaches, - closed, - ) - var path_distance := 0.0 - for index: int in points.size(): - if index > 0: - path_distance += points[index - 1].distance_to(points[index]) - var point := points[index] - var water_normal := water_normals[index] - var join_scale := join_scales[index] - var land_point := ( - point - water_normal * LAND_INSET * join_scale - ) - var water_point := ( - point - + water_normal * water_reaches[index] - ) - vertices.append(Vector3(land_point.x, water_height + SURFACE_OFFSET, land_point.y)) - vertices.append(Vector3(water_point.x, water_height + SURFACE_OFFSET, water_point.y)) - normals.append(Vector3.UP) - normals.append(Vector3.UP) - uvs.append(Vector2(path_distance, 0.0)) - uvs.append(Vector2(path_distance, 1.0)) - var edge_count := points.size() if closed else points.size() - 1 - for index: int in edge_count: - var next := (index + 1) % points.size() - var a := base_index + index * 2 - var b := a + 1 - var c := base_index + next * 2 - var d := c + 1 - indices.append_array(PackedInt32Array([a, c, b, b, c, d])) - var arrays := [] - arrays.resize(Mesh.ARRAY_MAX) - arrays[Mesh.ARRAY_VERTEX] = vertices - arrays[Mesh.ARRAY_NORMAL] = normals - arrays[Mesh.ARRAY_TEX_UV] = uvs - arrays[Mesh.ARRAY_INDEX] = indices - var mesh := ArrayMesh.new() - if not vertices.is_empty(): - mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) - return mesh - - -static func _safe_water_reach( - incoming: Vector2, - outgoing: Vector2, - water_normal: Vector2, - incoming_length: float, - outgoing_length: float, - desired_reach: float, -) -> float: - var turn_cross := incoming.cross(outgoing) - var water_side := incoming.cross(water_normal) - if turn_cross * water_side <= 0.0: - return desired_reach - var turn_angle := acos(clampf(incoming.dot(outgoing), -1.0, 1.0)) - if turn_angle <= WATER_PLANE_EPSILON: - return desired_reach - var radius := minf(incoming_length, outgoing_length) / maxf( - 2.0 * sin(turn_angle * 0.5), - WATER_PLANE_EPSILON, - ) - return minf(desired_reach, maxf(radius * 0.58, 0.24)) - - -static func _smooth_water_reaches( - points: PackedVector2Array, - reaches: PackedFloat32Array, - closed: bool, -) -> PackedFloat32Array: - var result := reaches.duplicate() - if result.size() < 2: - return result - for _pass: int in 3: - var forward_start := 0 if closed else 1 - for index: int in range(forward_start, result.size()): - var previous := (index - 1 + result.size()) % result.size() - var allowed := ( - result[previous] - + points[previous].distance_to(points[index]) - * RIBBON_REACH_GROWTH_PER_METER - ) - result[index] = minf(result[index], allowed) - var backward_start := result.size() - 1 if closed else result.size() - 2 - for index: int in range(backward_start, -1, -1): - var following := (index + 1) % result.size() - var allowed := ( - result[following] - + points[index].distance_to(points[following]) - * RIBBON_REACH_GROWTH_PER_METER - ) - result[index] = minf(result[index], allowed) - return result - - -static func _closed_path_water_side( - points: PackedVector2Array, - water_is_inside: bool, -) -> float: - var signed_area_twice := 0.0 - for index: int in points.size(): - signed_area_twice += points[index].cross( - points[(index + 1) % points.size()] - ) - var interior_side := 1.0 if signed_area_twice >= 0.0 else -1.0 - return interior_side if water_is_inside else -interior_side - - -static func _point_key(point: Vector2) -> Vector2i: - return Vector2i( - roundi(point.x / ENDPOINT_MERGE_TOLERANCE), - roundi(point.y / ENDPOINT_MERGE_TOLERANCE) - ) - - -static func _edge_key(a: Vector2i, b: Vector2i) -> String: - if a.x < b.x or (a.x == b.x and a.y <= b.y): - return "%d:%d|%d:%d" % [a.x, a.y, b.x, b.y] - return "%d:%d|%d:%d" % [b.x, b.y, a.x, a.y] - - -static func _has_unused_neighbor( - key: Vector2i, - adjacency: Dictionary, - unused_edges: Dictionary -) -> bool: - for neighbor: Vector2i in adjacency[key]: - if unused_edges.get(_edge_key(key, neighbor), false): - return true - return false - - -static func _contains_near(points: PackedVector2Array, point: Vector2) -> bool: - for existing: Vector2 in points: - if existing.distance_to(point) <= WATER_PLANE_EPSILON: - return true - return false - - -static func _point_segment_distance(point: Vector2, a: Vector2, b: Vector2) -> float: - var segment := b - a - if segment.length_squared() <= WATER_PLANE_EPSILON: - return point.distance_to(a) - var amount := clampf((point - a).dot(segment) / segment.length_squared(), 0.0, 1.0) - return point.distance_to(a + segment * amount) - - -static func _path_length(points: PackedVector2Array, closed: bool) -> float: - var result := 0.0 - for index: int in points.size() - 1: - result += points[index].distance_to(points[index + 1]) - if closed and points.size() > 2: - result += points[points.size() - 1].distance_to(points[0]) - return result - - -static func _mesh_triangle_count(mesh: ArrayMesh) -> int: - if mesh.get_surface_count() == 0: - return 0 - var arrays := mesh.surface_get_arrays(0) - return floori( - float((arrays[Mesh.ARRAY_INDEX] as PackedInt32Array).size()) / 3.0 - ) diff --git a/world/water/shoreline_ribbon_generator.gd.uid b/world/water/shoreline_ribbon_generator.gd.uid deleted file mode 100644 index 2edffe8..0000000 --- a/world/water/shoreline_ribbon_generator.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dhp7ctspabll diff --git a/world/water_surface_motion.gd b/world/water_surface_motion.gd index d8dbd4d..0957646 100644 --- a/world/water_surface_motion.gd +++ b/world/water_surface_motion.gd @@ -5,6 +5,9 @@ const DEFAULT_AMPLITUDE: float = 0.055 const DEFAULT_CYCLE_SECONDS: float = 7.0 const DEFAULT_SECONDARY_SWELL: float = 0.18 const DEFAULT_PHASE_OFFSET: float = 0.0 +const STABLE_SURFACE_HEIGHT_OFFSET_PARAMETER: StringName = ( + &"stable_surface_height_offset" +) @export_range(0.0, 0.2, 0.005, "suffix:m") var amplitude: float = ( DEFAULT_AMPLITUDE @@ -23,29 +26,32 @@ var _motion_strength_multiplier: float = 1.0 func _ready() -> void: _base_height = position.y + _apply_height_offset(0.0) set_process(amplitude > 0.0) func _process(_delta: float) -> void: - position.y = _base_height + calculate_height_offset( - _current_time_seconds(), - get_effective_amplitude(), - cycle_seconds, - secondary_swell, - phase_offset, + _apply_height_offset( + calculate_height_offset( + _current_time_seconds(), + get_effective_amplitude(), + cycle_seconds, + secondary_swell, + phase_offset, + ) ) func set_motion_enabled(enabled: bool) -> void: set_process(enabled and amplitude > 0.0) if not enabled: - position.y = _base_height + _apply_height_offset(0.0) func set_motion_strength_multiplier(multiplier: float) -> void: _motion_strength_multiplier = maxf(multiplier, 0.0) if _motion_strength_multiplier <= 0.0: - position.y = _base_height + _apply_height_offset(0.0) func get_motion_strength_multiplier() -> float: @@ -56,6 +62,25 @@ func get_effective_amplitude() -> float: return amplitude * _motion_strength_multiplier +func _apply_height_offset(height_offset: float) -> void: + position.y = _base_height + height_offset + # Headless worlds have no visible shoreline to correct. Skip the visual + # material update there and keep dedicated/headless worlds presentation-free. + if DisplayServer.get_name() == "headless": + return + # The surface can still bob visually, but all shoreline shader thresholds + # should remain at the authored waterline. Only the active ocean uses this + # moving material, so publishing the offset on that material is sufficient. + var shader_material := material_override as ShaderMaterial + if shader_material == null: + shader_material = get_active_material(0) as ShaderMaterial + if shader_material != null: + shader_material.set_shader_parameter( + STABLE_SURFACE_HEIGHT_OFFSET_PARAMETER, + -height_offset + ) + + static func get_default_height_offset() -> float: return calculate_height_offset( _current_time_seconds(),