Quality Gate / Quality Gate (pull_request) Successful in 6m55s
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>
162 lines
5.5 KiB
GDScript
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
|