feat: add rain puddles and stabilize shoreline water
This commit is contained in:
parent
eeef99793d
commit
3a61056ce5
49 changed files with 1033 additions and 1852 deletions
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://cbk4xpalei5v3
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
30
main/main.gd
30
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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://eosavaa6wu1e
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
84
tests/rain_puddle_runtime_validation.gd
Normal file
84
tests/rain_puddle_runtime_validation.gd
Normal file
|
|
@ -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)
|
||||
1
tests/rain_puddle_runtime_validation.gd.uid
Normal file
1
tests/rain_puddle_runtime_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://jo5lqk2n8bik
|
||||
|
|
@ -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()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://bwvhgpbkb00l1
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://c73frygpfhred
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://5dauni8hb2lv
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
645
world/rain_puddle_presentation.gd
Normal file
645
world/rain_puddle_presentation.gd
Normal file
|
|
@ -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)
|
||||
1
world/rain_puddle_presentation.gd.uid
Normal file
1
world/rain_puddle_presentation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://lhwc7dc1pivp
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://b0c3o2vy76fg3
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://bj704rvwwh77b
|
||||
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://c8hk3eojujyax
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://dhp7ctspabll
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue