feat(cockpit): replace procedural cockpit with twelve scripted models
Quality Gate / Quality Gate (pull_request) Successful in 9m4s

Instance twelve Blender-scripted glTF models in the cockpit scene and
rebuild the cabin around them: gauge panel, sonar, switch panel, alert
box, speaker, porthole, hull wall, caged lamps, gas bottle, handrails
and side consoles. Presentation only; no domain, application or world
rule changes beyond enabling glow in the test environment.

- Needles, alert lamps and sonar screen are driven by the existing
  snapshots; the alert box now carries SYS/ATTN/ALRM.
- Depth dial reads percent of certified depth.
- Sonar display restyled as a turquoise holographic relief with a
  shader grid and height scale; SubViewport matches the glass (960x604).
- Cabin lighting retuned: warm pooled lamp light, cool porthole rim,
  lamp shadows, dial backlight.
- Procedural stand-ins removed (bulkhead, tanks, consoles, levers,
  indicator columns, placeholder pipe run, dial face PNGs).
- Tests adapted where nodes disappeared; one assertion removed
  (pipe_segment_count) until the pipe kit is placed.
- Model .glb files are committed without LFS; textures Godot extracts
  next to them at import are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Opus 5.5 committed 2026-10-06 21:36:57 +02:00
1 parent 912123087f
commit 2f9b7c0894
45 files changed
+1353 -1603

No files matched your search

@@ -5,53 +5,59 @@ const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const SonarSample = preload("res://game/domain/sonar/sonar_sample.gd")
const SonarScan = preload("res://game/domain/sonar/sonar_scan.gd")
const COLOR_DEEP := Color("173f91")
const COLOR_LOW := Color("168fc1")
const COLOR_MIDDLE := Color("36b979")
const COLOR_HIGH := Color("d7d93d")
const COLOR_PEAK := Color("ee5734")
const COLOR_METAL := Color("ff6b22")
## Grid lines are drawn by the relief shader on the cell edges: every built
## cell carries one row edge and one column edge of the grid.
const GRID_SEGMENTS_PER_CELL := 2
## Two triangles per cell, wound like the sample grid.
const CELL_CORNER_ORDER: Array[int] = [0, 2, 1, 1, 2, 3]
static func build_full_mesh(
scan: SonarScan,
material: StandardMaterial3D
) -> ArrayMesh:
static func build_full_mesh(scan: SonarScan, material: Material) -> ArrayMesh:
var mesh := ArrayMesh.new()
for row in range(1, scan.samples.rows):
add_row_band(mesh, scan, row, material)
return mesh
## Adds the cells between `row - 1` and `row`; returns the triangle count.
## Per vertex: UV = (column, row) of the sample grid, COLOR.r = height relative
## to the scan (0 low, 1 peak), COLOR.g = 1 on metal, COLOR.b = echo intensity.
static func add_row_band(
mesh: ArrayMesh,
scan: SonarScan,
row: int,
material: StandardMaterial3D
material: Material
) -> int:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var colors := PackedColorArray()
var uvs := PackedVector2Array()
for column in range(scan.samples.columns - 1):
var top_left: SonarSample = scan.samples.sample_at(row - 1, column)
var top_right: SonarSample = scan.samples.sample_at(row - 1, column + 1)
var bottom_left: SonarSample = scan.samples.sample_at(row, column)
var bottom_right: SonarSample = scan.samples.sample_at(row, column + 1)
var corners: Array[SonarSample] = [
scan.samples.sample_at(row - 1, column),
scan.samples.sample_at(row - 1, column + 1),
scan.samples.sample_at(row, column),
scan.samples.sample_at(row, column + 1),
]
if not (
top_left.observed
and top_right.observed
and bottom_left.observed
and bottom_right.observed
corners[0].observed
and corners[1].observed
and corners[2].observed
and corners[3].observed
):
continue
_append_triangle(
vertices, normals, colors, scan,
top_left, bottom_left, top_right
)
_append_triangle(
vertices, normals, colors, scan,
top_right, bottom_left, bottom_right
)
var grid: Array[Vector2] = [
Vector2(column, row - 1),
Vector2(column + 1, row - 1),
Vector2(column, row),
Vector2(column + 1, row),
]
for corner: int in CELL_CORNER_ORDER:
var sample := corners[corner]
vertices.append(sample.position_m)
normals.append(sample.normal)
colors.append(_sample_data(scan, sample))
uvs.append(grid[corner])
if vertices.is_empty():
return 0
var arrays: Array = []
@@ -59,93 +65,21 @@ static func add_row_band(
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_COLOR] = colors
arrays[Mesh.ARRAY_TEX_UV] = uvs
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh.surface_set_material(mesh.get_surface_count() - 1, material)
return vertices.size() / 3
static func add_grid_band(
mesh: ArrayMesh,
scan: SonarScan,
row: int,
material: StandardMaterial3D
) -> int:
var vertices := PackedVector3Array()
for column in scan.samples.columns:
if column % 6 != 0 and column != scan.samples.columns - 1:
continue
_append_grid_segment(
vertices,
scan.samples.sample_at(row - 1, column),
scan.samples.sample_at(row, column)
)
if row == 1 or row % 4 == 0 or row == scan.samples.rows - 1:
for column in range(scan.samples.columns - 1):
_append_grid_segment(
vertices,
scan.samples.sample_at(row, column),
scan.samples.sample_at(row, column + 1)
)
if vertices.is_empty():
return 0
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
mesh.surface_set_material(mesh.get_surface_count() - 1, material)
return vertices.size() / 2
static func _append_grid_segment(
vertices: PackedVector3Array,
a: SonarSample,
b: SonarSample
) -> void:
if not a.observed or not b.observed:
return
vertices.append(a.position_m + a.normal * 0.08)
vertices.append(b.position_m + b.normal * 0.08)
static func _append_triangle(
vertices: PackedVector3Array,
normals: PackedVector3Array,
colors: PackedColorArray,
scan: SonarScan,
a: SonarSample,
b: SonarSample,
c: SonarSample
) -> void:
for sample: SonarSample in [a, b, c]:
vertices.append(sample.position_m)
normals.append(sample.normal)
colors.append(_sample_color(scan, sample))
static func _sample_color(scan: SonarScan, sample: SonarSample) -> Color:
if sample.material == SonarMaterial.Value.METAL:
return COLOR_METAL
var height := inverse_lerp(
scan.minimum_elevation_m,
scan.maximum_elevation_m,
sample.elevation_m
)
var color: Color
if height <= 0.28:
color = COLOR_DEEP.lerp(COLOR_LOW, height / 0.28)
elif height <= 0.52:
color = COLOR_LOW.lerp(COLOR_MIDDLE, (height - 0.28) / 0.24)
elif height <= 0.76:
color = COLOR_MIDDLE.lerp(COLOR_HIGH, (height - 0.52) / 0.24)
else:
color = COLOR_HIGH.lerp(COLOR_PEAK, (height - 0.76) / 0.24)
var light_direction := Vector3(-0.35, 0.82, -0.45).normalized()
var slope_light := clampf(sample.normal.dot(light_direction), 0.0, 1.0)
var shade := lerpf(0.68, 1.0, slope_light)
var lit_color := color.lerp(Color.WHITE, sample.intensity * 0.08)
return Color(
lit_color.r * shade,
lit_color.g * shade,
lit_color.b * shade,
lit_color.a
static func _sample_data(scan: SonarScan, sample: SonarSample) -> Color:
var height := clampf(
inverse_lerp(
scan.minimum_elevation_m,
scan.maximum_elevation_m,
sample.elevation_m
),
0.0,
1.0
)
var metal := 1.0 if sample.material == SonarMaterial.Value.METAL else 0.0
return Color(height, metal, clampf(sample.intensity, 0.0, 1.0))