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,248 @@
|
||||
extends SceneTree
|
||||
|
||||
## Checks the sunlit shallows visual site keeps its three promises:
|
||||
## a flat floor, a calm shadow-only water surface, and a daytime sun whose
|
||||
## hour never changes.
|
||||
|
||||
const SCENE_PATH := "res://game/world/sites/sunlit_shallows_environment.tscn"
|
||||
const WORLD_SCENE_PATH := (
|
||||
"res://game/composition/vertical_slice/sunlit_shallows_world.tscn"
|
||||
)
|
||||
|
||||
var _failures: Array[String] = []
|
||||
var _checks := 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var scene := load(SCENE_PATH) as PackedScene
|
||||
_expect(scene != null, "scene loads")
|
||||
if scene == null:
|
||||
_finish()
|
||||
return
|
||||
|
||||
var site := scene.instantiate() as SunlitShallowsEnvironment
|
||||
_expect(site != null, "scene root is SunlitShallowsEnvironment")
|
||||
if site == null:
|
||||
_finish()
|
||||
return
|
||||
root.add_child(site)
|
||||
await process_frame
|
||||
|
||||
_test_fixed_daytime_sun(site)
|
||||
_test_flat_floor(site)
|
||||
_test_calm_surface_occluder(site)
|
||||
_test_scattering_environment(site)
|
||||
_test_air_fog_cut(site)
|
||||
_test_surface_ceiling(site)
|
||||
|
||||
site.queue_free()
|
||||
await process_frame
|
||||
_test_composed_world()
|
||||
_finish()
|
||||
|
||||
|
||||
func _test_composed_world() -> void:
|
||||
var world_scene := load(WORLD_SCENE_PATH) as PackedScene
|
||||
_expect(world_scene != null, "composed world scene loads")
|
||||
if world_scene == null:
|
||||
return
|
||||
var world := world_scene.instantiate() as Node3D
|
||||
_expect(
|
||||
world.get_node_or_null("Environment") is SunlitShallowsEnvironment,
|
||||
"composed world uses the sunlit shallows environment"
|
||||
)
|
||||
var submarine := world.get_node_or_null("SubmarineActor") as Node3D
|
||||
_expect(submarine != null, "composed world has a submarine")
|
||||
if submarine != null:
|
||||
_expect(
|
||||
submarine.position.y < SunlitShallowsEnvironment.SURFACE_LEVEL_M
|
||||
and submarine.position.y > SunlitShallowsEnvironment.FLOOR_DEPTH_M,
|
||||
"submarine starts between surface and floor"
|
||||
)
|
||||
_expect(
|
||||
submarine.get_node_or_null("CockpitInterior") != null
|
||||
and submarine.get_node_or_null("CockpitCameraRig") != null,
|
||||
"submarine carries cockpit and camera rig"
|
||||
)
|
||||
for projector_name: String in ["LeftProjector", "RightProjector"]:
|
||||
var projector := submarine.get_node_or_null(projector_name) as SpotLight3D
|
||||
_expect(projector != null, "%s exists" % projector_name)
|
||||
if projector != null:
|
||||
_expect(
|
||||
projector.light_volumetric_fog_energy < 1.0,
|
||||
"%s does not wash out the sun shafts in the fog" % projector_name
|
||||
)
|
||||
world.free()
|
||||
|
||||
|
||||
func _test_fixed_daytime_sun(site: SunlitShallowsEnvironment) -> void:
|
||||
var sun := site.get_node_or_null("Sun") as DirectionalLight3D
|
||||
_expect(sun != null, "sun is a DirectionalLight3D")
|
||||
if sun == null:
|
||||
return
|
||||
_expect(
|
||||
sun.rotation_degrees.is_equal_approx(
|
||||
SunlitShallowsEnvironment.SUN_ROTATION_DEGREES
|
||||
),
|
||||
"sun hour is the fixed constant"
|
||||
)
|
||||
var sun_direction := -sun.global_transform.basis.z
|
||||
_expect(sun_direction.y < -0.6, "sun shines downward from above the porthole (daytime)")
|
||||
_expect(
|
||||
sun_direction.z > 0.15,
|
||||
"sun shines toward the observer (rays come from ahead, world -Z)"
|
||||
)
|
||||
_expect(sun.shadow_enabled, "sun casts shadows (needed for light shafts)")
|
||||
_expect(
|
||||
sun.light_volumetric_fog_energy >= 1.0,
|
||||
"sun injects energy into the volumetric fog"
|
||||
)
|
||||
|
||||
|
||||
func _test_flat_floor(site: SunlitShallowsEnvironment) -> void:
|
||||
var floor_mesh := site.get_node_or_null("FlatFloor/Mesh") as MeshInstance3D
|
||||
_expect(floor_mesh != null, "floor mesh exists")
|
||||
if floor_mesh == null:
|
||||
return
|
||||
var vertices := floor_mesh.mesh.get_faces()
|
||||
var flat := true
|
||||
for vertex: Vector3 in vertices:
|
||||
if not is_zero_approx(vertex.y):
|
||||
flat = false
|
||||
break
|
||||
_expect(flat, "floor mesh is flat")
|
||||
_expect(
|
||||
is_equal_approx(
|
||||
floor_mesh.global_position.y,
|
||||
SunlitShallowsEnvironment.FLOOR_DEPTH_M
|
||||
),
|
||||
"floor sits at the site depth"
|
||||
)
|
||||
_expect(
|
||||
floor_mesh.material_override is ShaderMaterial,
|
||||
"floor uses the caustic shader material"
|
||||
)
|
||||
var collision := site.get_node_or_null("FlatFloor/Collision") as CollisionShape3D
|
||||
_expect(collision != null and collision.shape is BoxShape3D, "floor has a box collider")
|
||||
|
||||
|
||||
func _test_calm_surface_occluder(site: SunlitShallowsEnvironment) -> void:
|
||||
var occluder := site.get_node_or_null("CalmSurfaceOccluder") as MeshInstance3D
|
||||
_expect(occluder != null, "surface occluder exists")
|
||||
if occluder == null:
|
||||
return
|
||||
_expect(
|
||||
occluder.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY,
|
||||
"surface occluder is shadow-only"
|
||||
)
|
||||
_expect(
|
||||
is_equal_approx(
|
||||
occluder.global_position.y,
|
||||
SunlitShallowsEnvironment.SURFACE_LEVEL_M
|
||||
),
|
||||
"surface occluder sits at water level"
|
||||
)
|
||||
var material := occluder.material_override as ShaderMaterial
|
||||
_expect(material != null, "surface occluder uses a shader material")
|
||||
if material == null:
|
||||
return
|
||||
var drift: Variant = material.get_shader_parameter("drift_speed")
|
||||
_expect(
|
||||
drift is float and drift > 0.0 and drift < 0.05,
|
||||
"surface drift is slow (calm water)"
|
||||
)
|
||||
_expect(
|
||||
material.get_shader_parameter("ripple_noise") is NoiseTexture2D,
|
||||
"surface ripple noise is assigned"
|
||||
)
|
||||
var pool_center: Variant = material.get_shader_parameter("pool_center_xz")
|
||||
_expect(
|
||||
pool_center is Vector2 and pool_center.y < -20.0 and pool_center.y > -120.0,
|
||||
"light pool sits a few tens of metres ahead (world -Z)"
|
||||
)
|
||||
var pool_radius: Variant = material.get_shader_parameter("pool_radius")
|
||||
_expect(
|
||||
pool_radius is float and pool_radius > 5.0 and pool_radius < 60.0,
|
||||
"light pool radius is local, not the whole surface"
|
||||
)
|
||||
|
||||
|
||||
func _test_scattering_environment(site: SunlitShallowsEnvironment) -> void:
|
||||
var world_environment := site.get_node_or_null("WorldEnvironment") as WorldEnvironment
|
||||
_expect(world_environment != null, "world environment exists")
|
||||
if world_environment == null:
|
||||
return
|
||||
var environment := world_environment.environment
|
||||
_expect(environment.volumetric_fog_enabled, "volumetric fog is enabled")
|
||||
_expect(environment.volumetric_fog_anisotropy > 0.5, "fog scatters forward")
|
||||
_expect(environment.fog_enabled, "depth fog is enabled")
|
||||
|
||||
|
||||
func _test_surface_ceiling(site: SunlitShallowsEnvironment) -> void:
|
||||
var ceiling := site.get_node_or_null("SurfaceCeiling") as MeshInstance3D
|
||||
_expect(ceiling != null, "visible surface ceiling exists")
|
||||
if ceiling == null:
|
||||
return
|
||||
_expect(
|
||||
is_equal_approx(ceiling.global_position.y, SunlitShallowsEnvironment.SURFACE_LEVEL_M),
|
||||
"surface ceiling sits at water level"
|
||||
)
|
||||
_expect(
|
||||
ceiling.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
|
||||
"surface ceiling leaves shadowing to the occluder"
|
||||
)
|
||||
var ceiling_material := ceiling.material_override as ShaderMaterial
|
||||
var occluder := site.get_node_or_null("CalmSurfaceOccluder") as MeshInstance3D
|
||||
var occluder_material: ShaderMaterial = null if occluder == null else occluder.material_override
|
||||
_expect(
|
||||
ceiling_material != null
|
||||
and occluder_material != null
|
||||
and ceiling_material.get_shader_parameter("pool_center_xz")
|
||||
== occluder_material.get_shader_parameter("pool_center_xz")
|
||||
and ceiling_material.get_shader_parameter("pool_radius")
|
||||
== occluder_material.get_shader_parameter("pool_radius"),
|
||||
"surface ceiling lights the same pool the occluder opens"
|
||||
)
|
||||
|
||||
|
||||
func _test_air_fog_cut(site: SunlitShallowsEnvironment) -> void:
|
||||
var air := site.get_node_or_null("AirFogCut") as FogVolume
|
||||
_expect(air != null, "air fog cut exists")
|
||||
if air == null:
|
||||
return
|
||||
var bottom := air.global_position.y - air.size.y * 0.5
|
||||
_expect(
|
||||
is_equal_approx(bottom, SunlitShallowsEnvironment.SURFACE_LEVEL_M),
|
||||
"air fog cut starts at the water surface"
|
||||
)
|
||||
var material := air.material as FogMaterial
|
||||
_expect(material != null and material.density < 0.0, "air fog cut subtracts fog")
|
||||
var world_environment := site.get_node("WorldEnvironment") as WorldEnvironment
|
||||
_expect(
|
||||
material != null and is_equal_approx(
|
||||
-material.density,
|
||||
world_environment.environment.volumetric_fog_density
|
||||
),
|
||||
"air fog cut cancels the global volumetric density exactly"
|
||||
)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
_failures.append(label)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if _failures.is_empty():
|
||||
print("Sunlit shallows environment: %d passed, 0 failed." % _checks)
|
||||
quit(0)
|
||||
else:
|
||||
for failure: String in _failures:
|
||||
push_error("sunlit shallows environment: %s" % failure)
|
||||
print("Sunlit shallows environment: %d failed." % _failures.size())
|
||||
quit(1)
|
||||
Reference in new issue
Block a user