Refactor fishing surfaces around authored water

This commit is contained in:
Alexander Sellite 2026-08-15 09:52:50 -04:00
parent 9f52620a73
commit 9afa60f6a5
11 changed files with 198 additions and 172 deletions

View file

@ -515,9 +515,10 @@ void fragment() {
water_alpha + shoreline_foam * shoreline_opacity_boost,
0.98
);
// Preserve most of the water layer at exact terrain contact. Fully fading
// it exposed the sand texture as a broken tan seam along the water plane.
water_alpha *= mix(0.82, 1.0, contact_stability);
// The authored water-body height is also the casting authority. Fade the
// visual layer completely at exact terrain contact so dry land cannot look
// fishable while the stable sub-surface water remains visible.
water_alpha *= contact_stability;
float surface_mottle = value_noise(
world_position.xz * surface_mottle_scale

View file

@ -12,7 +12,6 @@
[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" 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"]
@ -35,25 +34,9 @@ size = Vector3(16.1, 4.8, 12.3675)
resource_local_to_scene = true
size = Vector2(10000, 10000)
[sub_resource type="BoxShape3D" id="OceanWestShape"]
[sub_resource type="BoxShape3D" id="OceanFishingShape"]
resource_local_to_scene = true
size = Vector3(20.820662814, 2, 97.979589711)
[sub_resource type="BoxShape3D" id="OceanEastShape"]
resource_local_to_scene = true
size = Vector3(51.439284598, 2, 97.979589711)
[sub_resource type="BoxShape3D" id="OceanNorthShape"]
resource_local_to_scene = true
size = Vector3(58.787753827, 2, 22.045407685)
[sub_resource type="BoxShape3D" id="OceanSouthShape"]
resource_local_to_scene = true
size = Vector3(58.787753827, 2, 22.045407685)
[sub_resource type="BoxShape3D" id="OceanCenterShape"]
resource_local_to_scene = true
size = Vector3(58.787753827, 2, 53.888774341)
size = Vector3(10000, 2, 10000)
[sub_resource type="BoxShape3D" id="OceanWestRecoveryShape"]
resource_local_to_scene = true
@ -161,11 +144,20 @@ shape = SubResource("PondRecoveryShape")
[node name="Ocean" type="Node3D" parent="WaterBodies" unique_id=552655799]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, -0.45, 0)
script = ExtResource("10_water_body")
derive_coverage_from_visual_mesh = true
water_material = ExtResource("13_water")
fishing_depth = 2.0
fish_pool = ExtResource("4_pool")
water_type = 1
location_tags = Array[StringName]([&"coast", &"ocean"])
manage_recovery_coverage = false
fishing_shape_path = NodePath("FishingRegions/OceanFishingRegion/Shape")
fishing_region_path = NodePath("FishingRegions/OceanFishingRegion")
[node name="VisualWater" type="MeshInstance3D" parent="WaterBodies/Ocean" unique_id=1176408937]
material_override = ExtResource("13_water")
mesh = SubResource("OceanWaterMesh")
script = ExtResource("20_surface_motion")
[node name="FishingRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=205598473]
@ -179,25 +171,9 @@ water_type = 1
fish_pool = ExtResource("4_pool")
surface_height_mode = 1
[node name="WestShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=357406062]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -55.613519213, -1, 0)
shape = SubResource("OceanWestShape")
[node name="EastShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=772224738]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39.30420832, -1, 0)
shape = SubResource("OceanEastShape")
[node name="NorthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=646147646]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15.809310892, -1, 37.967091013)
shape = SubResource("OceanNorthShape")
[node name="SouthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=894083854]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15.809310892, -1, -37.967091013)
shape = SubResource("OceanSouthShape")
[node name="CenterShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion"]
position = Vector3(-15.809310892, -1, 0)
shape = SubResource("OceanCenterShape")
[node name="Shape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=357406062]
position = Vector3(0, -1, 0)
shape = SubResource("OceanFishingShape")
[node name="RecoveryRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=463706703]

View file

@ -14,6 +14,12 @@ var surface_size: Vector2 = Vector2(10.0, 10.0):
set(value):
surface_size = Vector2(maxf(value.x, 0.1), maxf(value.y, 0.1))
_sync_owned_nodes()
## Derive fishing coverage from VisualWater's mesh bounds instead of resizing
## the mesh from Surface Size. Use this for imported or shared water surfaces.
@export var derive_coverage_from_visual_mesh: bool = false:
set(value):
derive_coverage_from_visual_mesh = value
_sync_owned_nodes()
## Material applied to the visible water surface.
@export var water_material: Material:
set(value):
@ -44,6 +50,11 @@ var fishing_depth: float = 4.0:
_sync_owned_nodes()
@export_group("Recovery Coverage")
## Keep recovery coverage independent when a water body uses custom volumes.
@export var manage_recovery_coverage: bool = true:
set(value):
manage_recovery_coverage = value
_sync_owned_nodes()
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
var recovery_depth: float = 4.8:
set(value):
@ -75,9 +86,10 @@ func _sync_owned_nodes() -> void:
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
if visual_water != null:
var plane_mesh := visual_water.mesh as PlaneMesh
if plane_mesh != null:
if plane_mesh != null and not derive_coverage_from_visual_mesh:
plane_mesh.size = surface_size
visual_water.material_override = water_material
var surface_footprint: Rect2 = _resolve_surface_footprint(visual_water)
var fishing_shape_node := (
get_node_or_null(fishing_shape_path) as CollisionShape3D
@ -99,34 +111,73 @@ func _sync_owned_nodes() -> void:
# The authored root is the one water-surface height. Keep the fishable
# volume entirely below that plane so visible water and interaction
# can be moved together without a second height to tune.
fishing_shape_node.position.y = -fishing_depth * 0.5
fishing_shape_node.position = Vector3(
surface_footprint.get_center().x,
-fishing_depth * 0.5,
surface_footprint.get_center().y,
)
fishing_shape.size = Vector3(
surface_size.x,
surface_footprint.size.x,
fishing_depth,
surface_size.y
surface_footprint.size.y,
)
var recovery_region := (
get_node_or_null(recovery_region_path) as Area3D
)
if recovery_region != null:
recovery_region.position.y = -recovery_depth * 0.5
var recovery_shape_node := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape_node != null:
var recovery_shape := recovery_shape_node.shape as BoxShape3D
if recovery_shape != null:
recovery_shape.size = Vector3(
surface_size.x,
recovery_depth,
surface_size.y
if manage_recovery_coverage:
var recovery_region := (
get_node_or_null(recovery_region_path) as Area3D
)
if recovery_region != null:
recovery_region.position = Vector3(
surface_footprint.get_center().x,
-recovery_depth * 0.5,
surface_footprint.get_center().y,
)
var recovery_shape_node := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape_node != null:
var recovery_shape := recovery_shape_node.shape as BoxShape3D
if recovery_shape != null:
recovery_shape.size = Vector3(
surface_footprint.size.x,
recovery_depth,
surface_footprint.size.y,
)
if Engine.is_editor_hint():
update_configuration_warnings()
func _resolve_surface_footprint(visual_water: MeshInstance3D) -> Rect2:
var authored_footprint := Rect2(-surface_size * 0.5, surface_size)
if (
not derive_coverage_from_visual_mesh
or visual_water == null
or visual_water.mesh == null
):
return authored_footprint
var mesh_bounds: AABB = visual_water.mesh.get_aabb()
if mesh_bounds.size.x <= 0.0 or mesh_bounds.size.z <= 0.0:
return authored_footprint
var visual_to_body: Transform3D = (
global_transform.affine_inverse() * visual_water.global_transform
)
var minimum := Vector2(INF, INF)
var maximum := Vector2(-INF, -INF)
for corner_index: int in 8:
var corner := mesh_bounds.position + Vector3(
mesh_bounds.size.x if (corner_index & 1) != 0 else 0.0,
mesh_bounds.size.y if (corner_index & 2) != 0 else 0.0,
mesh_bounds.size.z if (corner_index & 4) != 0 else 0.0,
)
var body_corner: Vector3 = visual_to_body * corner
minimum.x = minf(minimum.x, body_corner.x)
minimum.y = minf(minimum.y, body_corner.z)
maximum.x = maxf(maximum.x, body_corner.x)
maximum.y = maxf(maximum.y, body_corner.z)
return Rect2(minimum, maximum - minimum)
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
if not scale.is_equal_approx(Vector3.ONE):
@ -134,28 +185,36 @@ func _get_configuration_warnings() -> PackedStringArray:
"Keep root scale at 1; resize water with Surface Size."
)
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
if visual_water == null or not visual_water.mesh is PlaneMesh:
warnings.append("VisualWater must provide a PlaneMesh.")
if visual_water == null or visual_water.mesh == null:
warnings.append("VisualWater must provide a mesh.")
elif (
not derive_coverage_from_visual_mesh
and not visual_water.mesh is PlaneMesh
):
warnings.append(
"VisualWater must provide a PlaneMesh when coverage is authored by size."
)
var fishing_shape := get_node_or_null(fishing_shape_path) as CollisionShape3D
if fishing_shape == null or not fishing_shape.shape is BoxShape3D:
warnings.append("FishingRegion must provide a BoxShape3D.")
var recovery_region := (
get_node_or_null(recovery_region_path) as PlayerWaterTrigger
)
if recovery_region == null:
warnings.append("RecoveryRegion must use PlayerWaterTrigger.")
elif (
recovery_region.surface_height_mode
!= PlayerWaterTrigger.SurfaceHeightMode.PARENT_GLOBAL_Y
):
warnings.append(
"RecoveryRegion must derive surface height from its parent."
if manage_recovery_coverage:
var recovery_region := (
get_node_or_null(recovery_region_path) as PlayerWaterTrigger
)
var recovery_shape := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
warnings.append("RecoveryRegion must provide a BoxShape3D.")
if recovery_region == null:
warnings.append("RecoveryRegion must use PlayerWaterTrigger.")
elif (
recovery_region.surface_height_mode
!= PlayerWaterTrigger.SurfaceHeightMode.PARENT_GLOBAL_Y
):
warnings.append(
"RecoveryRegion must derive surface height from its parent."
)
var recovery_shape := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
warnings.append("RecoveryRegion must provide a BoxShape3D.")
var fishing_region := get_node_or_null(
fishing_region_path
) as FishableWaterRegion