Files
Ransom-s-wake/game/world/sites/sunlit_shallows_environment.gd
OlivierandClaude Fable 5.1 f0072a3e11
Quality Gate / Quality Gate (pull_request) Successful in 6m55s
Add sunlit shallows site with localized sun shafts
New visual test site under game/world/sites: flat sandy floor at
-40 m, calm surface at 0 m, fixed daytime sun ahead of and above
the submarine. Light shafts come from a shadow-only surface
occluder whose alpha-scissor ripples open only inside a pool a few
tens of metres ahead; a subtractive FogVolume cancels the global
volumetric fog above the water, and an unshaded ceiling mesh shows
the lit underside of the surface inside that pool. A composed
world (sunlit_shallows_world.tscn) mounts the submarine, cockpit
and camera rig in it, with the projectors kept out of the fog.

Cockpit: the porthole glass moves to its own render layer with a
rougher material and the porthole fill light excludes that layer,
so interior lamps no longer mirror as hard specular points.

Quality gate: new integration suite for the site (42 checks), three
glass assertions in the foundation suite, and the Input-access
architecture rule now matches both path separators so the gate runs
on Windows. project.godot raises the volumetric fog froxel grid to
128 so the shafts stay readable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 23:18:56 +02:00

162 lines
5.5 KiB
GDScript

class_name SunlitShallowsEnvironment
extends Node3D
## Visual test site: flat floor, calm water, fixed daytime sun.
## The hour never changes, so the sun transform is a constant and the only
## motion is the slow ripple drift inside the two shaders.
const SURFACE_LEVEL_M := 0.0
const FLOOR_DEPTH_M := -40.0
## Air above the calm surface holds no scattering medium: a subtractive
## FogVolume cancels the global volumetric fog there so the view never shows
## an unshadowed glow above the water.
const AIR_VOLUME_HEIGHT_M := 80.0
const VOLUMETRIC_FOG_DENSITY := 0.035
const SITE_EXTENT_M := 240.0
## Sun sits ahead of the observer (world -Z) and high: shafts travel down
## toward the submarine, as in the cockpit visual target.
const SUN_ROTATION_DEGREES := Vector3(-44.0, 162.0, 0.0)
const NOISE_SEED := 7
## Calm water: ripple drift stays well under 0.05 texture units per second.
const SURFACE_DRIFT_SPEED := 0.012
## Sunlight only breaks through a pool of surface a few tens of metres ahead of
## the submarine (world -Z), so the shafts land in front of the porthole.
const SHAFT_POOL_CENTER_XZ_M := Vector2(-14.0, -31.0)
const SHAFT_POOL_RADIUS_M := 22.0
const SHAFT_POOL_EDGE_M := 12.0
## Coarser ripples give a few broken oblique shafts rather than a dense fan.
const SURFACE_RIPPLE_SCALE := 0.025
## The underside of the surface is drawn dark, lit only inside the pool.
const SURFACE_CEILING_LIT_SCALE := 0.3
const SURFACE_OCCLUDER_SHADER := preload(
"res://game/world/sites/sunlit_shallows_surface_occluder.gdshader"
)
const CAUSTIC_FLOOR_SHADER := preload(
"res://game/world/sites/sunlit_shallows_caustic_floor.gdshader"
)
const SURFACE_CEILING_SHADER := preload(
"res://game/world/sites/sunlit_shallows_surface_ceiling.gdshader"
)
func _ready() -> void:
_fix_sun()
_add_flat_floor()
_add_surface_occluder()
_add_surface_ceiling()
_add_air_fog_cut()
func _fix_sun() -> void:
var sun := get_node_or_null("Sun") as DirectionalLight3D
if sun == null:
return
sun.rotation_degrees = SUN_ROTATION_DEGREES
func _add_flat_floor() -> void:
var plane := PlaneMesh.new()
plane.size = Vector2(SITE_EXTENT_M, SITE_EXTENT_M)
plane.subdivide_width = 1
plane.subdivide_depth = 1
var material := ShaderMaterial.new()
material.shader = CAUSTIC_FLOOR_SHADER
material.set_shader_parameter("caustic_noise", _make_ripple_noise(0.007))
var body := StaticBody3D.new()
body.name = "FlatFloor"
body.position = Vector3(0.0, FLOOR_DEPTH_M, 0.0)
body.set_meta("sonar_material", 1)
body.set_meta("sonar_reflectivity", 0.6)
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = "Mesh"
mesh_instance.mesh = plane
mesh_instance.material_override = material
body.add_child(mesh_instance)
var collision := CollisionShape3D.new()
collision.name = "Collision"
var shape := BoxShape3D.new()
shape.size = Vector3(SITE_EXTENT_M, 1.0, SITE_EXTENT_M)
collision.shape = shape
collision.position = Vector3(0.0, -0.5, 0.0)
body.add_child(collision)
add_child(body)
func _add_surface_occluder() -> void:
var plane := PlaneMesh.new()
plane.size = Vector2(SITE_EXTENT_M, SITE_EXTENT_M)
var material := ShaderMaterial.new()
material.shader = SURFACE_OCCLUDER_SHADER
material.set_shader_parameter("ripple_noise", _make_ripple_noise(0.009))
material.set_shader_parameter("drift_speed", SURFACE_DRIFT_SPEED)
material.set_shader_parameter("ripple_scale", SURFACE_RIPPLE_SCALE)
material.set_shader_parameter("pool_center_xz", SHAFT_POOL_CENTER_XZ_M)
material.set_shader_parameter("pool_radius", SHAFT_POOL_RADIUS_M)
material.set_shader_parameter("pool_edge", SHAFT_POOL_EDGE_M)
var occluder := MeshInstance3D.new()
occluder.name = "CalmSurfaceOccluder"
occluder.position = Vector3(0.0, SURFACE_LEVEL_M, 0.0)
occluder.mesh = plane
occluder.material_override = material
occluder.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
add_child(occluder)
func _add_surface_ceiling() -> void:
var plane := PlaneMesh.new()
plane.size = Vector2(SITE_EXTENT_M * 1.5, SITE_EXTENT_M * 1.5)
var material := ShaderMaterial.new()
material.shader = SURFACE_CEILING_SHADER
material.set_shader_parameter("ripple_noise", _make_ripple_noise(0.009))
material.set_shader_parameter("drift_speed", SURFACE_DRIFT_SPEED)
material.set_shader_parameter("pool_center_xz", SHAFT_POOL_CENTER_XZ_M)
material.set_shader_parameter("pool_radius", SHAFT_POOL_RADIUS_M)
material.set_shader_parameter("lit_scale", SURFACE_CEILING_LIT_SCALE)
var ceiling := MeshInstance3D.new()
ceiling.name = "SurfaceCeiling"
ceiling.position = Vector3(0.0, SURFACE_LEVEL_M, 0.0)
ceiling.mesh = plane
ceiling.material_override = material
ceiling.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
add_child(ceiling)
func _add_air_fog_cut() -> void:
var air := FogVolume.new()
air.name = "AirFogCut"
air.shape = RenderingServer.FOG_VOLUME_SHAPE_BOX
air.size = Vector3(SITE_EXTENT_M * 1.5, AIR_VOLUME_HEIGHT_M, SITE_EXTENT_M * 1.5)
air.position = Vector3(0.0, SURFACE_LEVEL_M + AIR_VOLUME_HEIGHT_M * 0.5, 0.0)
var material := FogMaterial.new()
material.density = -VOLUMETRIC_FOG_DENSITY
material.edge_fade = 0.0
air.material = material
add_child(air)
func _make_ripple_noise(frequency: float) -> NoiseTexture2D:
var noise := FastNoiseLite.new()
noise.seed = NOISE_SEED
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
noise.frequency = frequency
noise.fractal_octaves = 3
noise.fractal_lacunarity = 2.1
noise.fractal_gain = 0.45
var texture := NoiseTexture2D.new()
texture.width = 512
texture.height = 512
texture.seamless = true
texture.generate_mipmaps = true
texture.noise = noise
return texture