Add sunlit shallows site with localized sun shafts
Quality Gate / Quality Gate (pull_request) Successful in 6m55s
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>
This commit is contained in:
1 parent
2c1ea50aba
commit
f0072a3e11
14 files changed
+666
-2
No files matched your search
@@ -0,0 +1,161 @@
|
||||
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
|
||||
Reference in new issue
Block a user