Files
Ransom-s-wake/game/presentation/cockpit/cockpit_practical_lighting.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

137 lines
4.7 KiB
GDScript

class_name CockpitPracticalLighting
extends Node3D
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
const CockpitMaterialPalette = preload(
"res://game/presentation/cockpit/cockpit_material_palette.gd"
)
const CockpitMeshFactory = preload(
"res://game/presentation/cockpit/cockpit_mesh_factory.gd"
)
const CockpitPressureBulkhead = preload(
"res://game/presentation/cockpit/cockpit_pressure_bulkhead.gd"
)
const CAGE_LAMP_SCENE = preload("res://game/presentation/cockpit/assets/models/cage_lamp.glb")
## Flange centre of the right-hand lamp, on the flat end cap of the pressure
## hull beside the porthole (the left lamp mirrors x). The cap is flat there, so
## the lamp is seated square to it, facing the cabin.
const LAMP_MOUNT_OFFSET := Vector3(0.636, 1.106, -1.3)
## Centre of the glass globe in the lamp model: where the light is emitted.
const LAMP_GLOBE_CENTRE := Vector3(0.0, 0.0, 0.064)
## Globe emission (1.5 as exported): lower, so the globe does not bloom into a blob.
const LAMP_GLASS_ENERGY := 1.0
## Tungsten-like caged lamps: the cabin's main light.
const WARM_LAMP_COLOR := Color(1.0, 0.66, 0.4)
const WARM_LAMP_ENERGY := 0.5
## Same dimming ratio as before when the red emergency light takes over.
const WARM_LAMP_CRITICAL_ENERGY := 0.23
## Short range and a steep falloff: each lamp makes a pool on the wall around
## it and leaves the lower and outer hull dark.
const WARM_LAMP_RANGE_M := 1.6
const WARM_LAMP_ATTENUATION := 2.0
## Cabin parts are centimetre-scale: the default bias detaches their shadows.
const WARM_LAMP_SHADOW_BIAS := 0.03
var _materials: CockpitMaterialPalette
var _warm_lights: Array[OmniLight3D] = []
var _emergency_light: OmniLight3D
var _porthole_fill: OmniLight3D
func _init(materials: CockpitMaterialPalette) -> void:
_materials = materials
name = "PracticalLighting"
_add_caged_lamp(-1.0)
_add_caged_lamp(1.0)
_add_fill_light()
_add_emergency_light()
func present(alert_level: DiveAlertLevel.Value) -> void:
_emergency_light.visible = alert_level >= DiveAlertLevel.Value.CRITICAL
for light: OmniLight3D in _warm_lights:
light.light_energy = (
WARM_LAMP_CRITICAL_ENERGY
if alert_level >= DiveAlertLevel.Value.CRITICAL
else WARM_LAMP_ENERGY
)
func practical_light_count() -> int:
return _warm_lights.size()
func _add_caged_lamp(side: float) -> void:
var root := Node3D.new()
root.name = "CagedLamp"
root.position = Vector3(
side * LAMP_MOUNT_OFFSET.x,
LAMP_MOUNT_OFFSET.y,
LAMP_MOUNT_OFFSET.z
)
add_child(root)
var model := CAGE_LAMP_SCENE.instantiate() as Node3D
model.name = "CageLampModel"
root.add_child(model)
# The light sits inside the cage, 3 cm from its bars: casting, the lamp's
# own body would swallow it.
CockpitMeshFactory.disable_shadow_casting(model)
var glass := CockpitMeshFactory.own_surface_material(
model.find_child("CageLamp_Glass", true, false) as MeshInstance3D
)
if glass != null:
glass.emission_energy_multiplier = LAMP_GLASS_ENERGY
var light := OmniLight3D.new()
light.name = "WarmPracticalLight"
light.position = LAMP_GLOBE_CENTRE
light.light_color = WARM_LAMP_COLOR
light.light_energy = WARM_LAMP_ENERGY
light.omni_range = WARM_LAMP_RANGE_M
light.omni_attenuation = WARM_LAMP_ATTENUATION
light.shadow_enabled = true
light.shadow_bias = WARM_LAMP_SHADOW_BIAS
root.add_child(light)
_warm_lights.append(light)
func _add_fill_light() -> void:
# The only cool light in the cabin: sea light entering through the porthole,
# placed just inside the glass so it rims the ring, its bolts and the first
# band of wall, and reaches the dashboard only from behind.
var porthole_fill := OmniLight3D.new()
porthole_fill.name = "PortholeFill"
porthole_fill.position = Vector3(0.0, 0.47, -1.2)
porthole_fill.light_color = Color(0.30, 0.56, 0.78)
porthole_fill.light_energy = 1.0
porthole_fill.omni_range = 1.1
porthole_fill.shadow_enabled = false
# 30 cm behind the glass, it would mirror itself as a hard blue point.
porthole_fill.light_cull_mask &= ~CockpitPressureBulkhead.PORTHOLE_GLASS_RENDER_LAYER_BIT
add_child(porthole_fill)
_porthole_fill = porthole_fill
func fill_light_ignores_porthole_glass() -> bool:
return (
_porthole_fill != null
and (
_porthole_fill.light_cull_mask
& CockpitPressureBulkhead.PORTHOLE_GLASS_RENDER_LAYER_BIT
) == 0
)
func _add_emergency_light() -> void:
_emergency_light = OmniLight3D.new()
_emergency_light.name = "EmergencyLight"
# High and central, so no single dial sits right under it and washes out.
_emergency_light.position = Vector3(0.0, 1.1, -0.95)
_emergency_light.light_color = Color(1.0, 0.04, 0.015)
_emergency_light.light_energy = 1.4
_emergency_light.omni_range = 2.4
_emergency_light.visible = false
add_child(_emergency_light)