Quality Gate / Quality Gate (pull_request) Successful in 6m49s
First wreck in the production world layer. A WreckActor (StaticBody3D) instantiates a two-LOD glTF visual, builds a trimesh collision from a separate lighter glTF, and carries the sonar metadata (metal, 0.9 reflectivity) so the bathymetry rays echo it. The sunlit shallows site places the MV Hebat Allah 62 m ahead of the submarine on the floor. The assets derive from a CC BY 4.0 photogrammetry scan by Holger Buss (Sketchfab). The raw 274 MB scan stays outside the repo; the committed GLBs come from a reproducible Blender pipeline under scripts/assets/: skirt cut, 44 m rescale, merge by distance (the scan is a triangle soup), fragment removal, 0.4 m voxel remesh, decimation to 58k/14.5k/2.9k triangles, smart UVs and a 2K albedo rebaked from a luminance-rebuilt texture (the scan's red channel is dead). docs/ASSET_LICENSES.md starts the licence registry required by the design plan and records the attribution owed in the credits. The site suite gains twelve assertions covering sonar metadata, collision, LOD handover and triangle budgets. Godot extracts the embedded albedo next to the glb on import; those PNGs are ignored like the cockpit ones. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
314 lines
11 KiB
GDScript
314 lines
11 KiB
GDScript
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)
|
|
_test_wreck(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 _test_wreck(site: SunlitShallowsEnvironment) -> void:
|
|
var wreck := site.get_node_or_null("HebatAllahWreck") as WreckActor
|
|
_expect(wreck != null, "site places the Hebat Allah wreck")
|
|
if wreck == null:
|
|
return
|
|
_expect(
|
|
int(wreck.get_meta("sonar_material", SonarMaterial.Value.UNKNOWN))
|
|
== SonarMaterial.Value.METAL,
|
|
"wreck echoes as metal on the sonar"
|
|
)
|
|
_expect((wreck.collision_layer & 1) == 1, "wreck sits on the sonar collision layer")
|
|
_expect(
|
|
wreck.position.y < SunlitShallowsEnvironment.SURFACE_LEVEL_M
|
|
and absf(wreck.position.y - SunlitShallowsEnvironment.FLOOR_DEPTH_M) < 3.0,
|
|
"wreck rests on the site floor"
|
|
)
|
|
var collision := wreck.get_node_or_null(WreckActor.COLLISION_SHAPE_NAME) as CollisionShape3D
|
|
_expect(
|
|
collision != null and collision.shape is ConcavePolygonShape3D,
|
|
"wreck has a trimesh collision separate from its visuals"
|
|
)
|
|
if collision != null and collision.shape is ConcavePolygonShape3D:
|
|
var faces := (collision.shape as ConcavePolygonShape3D).get_faces()
|
|
_expect(faces.size() >= 3 * 500, "wreck collision mesh is not empty")
|
|
_expect(faces.size() <= 3 * 6000, "wreck collision mesh stays light")
|
|
var lod0 := wreck.find_child(WreckActor.LOD0_NODE_NAME, true, false) as MeshInstance3D
|
|
var lod1 := wreck.find_child(WreckActor.LOD1_NODE_NAME, true, false) as MeshInstance3D
|
|
_expect(lod0 != null and lod1 != null, "wreck carries two visual LODs")
|
|
if lod0 == null or lod1 == null:
|
|
return
|
|
_expect(
|
|
lod0.visibility_range_end > 0.0
|
|
and is_equal_approx(lod1.visibility_range_begin, lod0.visibility_range_end),
|
|
"wreck LODs hand over at one distance"
|
|
)
|
|
var lod0_triangles := _triangle_count(lod0.mesh)
|
|
var lod1_triangles := _triangle_count(lod1.mesh)
|
|
_expect(
|
|
lod0_triangles > 0 and lod0_triangles <= 120000,
|
|
"wreck LOD0 stays within the triangle budget"
|
|
)
|
|
_expect(lod1_triangles > 0 and lod1_triangles < lod0_triangles / 2, "wreck LOD1 is far lighter")
|
|
_expect(
|
|
lod0.mesh.surface_get_material(0) != null
|
|
or lod0.get_surface_override_material(0) != null,
|
|
"wreck LOD0 carries a material"
|
|
)
|
|
|
|
|
|
func _triangle_count(mesh: Mesh) -> int:
|
|
if mesh == null:
|
|
return 0
|
|
var triangles := 0
|
|
for surface in mesh.get_surface_count():
|
|
var arrays := mesh.surface_get_arrays(surface)
|
|
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
|
if indices.is_empty():
|
|
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
triangles += vertices.size() / 3
|
|
else:
|
|
triangles += indices.size() / 3
|
|
return triangles
|
|
|
|
|
|
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)
|