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

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:46:46 +02:00
1 parent 912123087f
commit 2c1ea50aba
45 files changed
+1353 -1603

No files matched your search

+4
View File
@@ -61,6 +61,10 @@ func built_grid_segment_count() -> int:
return _renderer.built_grid_segment_count()
func depth_scale_texts() -> PackedStringArray:
return _renderer.depth_scale_texts()
func idle_prompt_visible() -> bool:
return _renderer.idle_prompt_visible()
+2 -2
View File
@@ -27,7 +27,7 @@ script = ExtResource("1_display")
[node name="SonarReliefRenderer" type="SubViewport" parent="."]
unique_name_in_owner = true
size = Vector2i(960, 540)
size = Vector2i(960, 604)
render_target_update_mode = 4
script = ExtResource("2_renderer")
@@ -77,4 +77,4 @@ text = "SONAR PRÊT · F POUR ÉMETTRE"
[node name="Legend" type="Label" parent="ExpandedPanel/Layout"]
layout_mode = 2
theme_override_colors/font_color = Color(0.58, 0.7, 0.71, 1)
text = "BLEU PROFOND · VERT/JAUNE HAUTEUR · ORANGE ANOMALIE · NOIR INCONNU · TAB FERMER"
text = "TURQUOISE SOMBRE PROFOND · CYAN CLAIR HAUTEUR · ORANGE ANOMALIE · NOIR INCONNU · TAB FERMER"
@@ -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))
@@ -5,15 +5,32 @@ const SonarScan = preload("res://game/domain/sonar/sonar_scan.gd")
const SonarReliefMeshBuilder = preload(
"res://game/presentation/sonar/sonar_relief_mesh_builder.gd"
)
const SonarReliefStyle = preload("res://game/presentation/sonar/sonar_relief_style.gd")
## Matches the glass of the cockpit sonar monitor (0.690 × 0.434, ratio 1.59).
const RENDER_SIZE := Vector2i(960, 604)
## Ground width framed by the orthographic camera, independent of the ratio.
const VIEW_WIDTH_M := 178.0
## Table-like oblique view from behind the vehicle. The aim point sits right
## of the scan axis so the relief leaves the right edge to the height scale.
const CAMERA_AIM_POINT := Vector3(14.0, -46.0, 50.0)
const CAMERA_ELEVATION_DEGREES := 44.0
const CAMERA_DISTANCE_M := 120.0
const IDLE_PROMPT_SIZE := Vector2(600.0, 64.0)
## Height scale drawn at the right edge of the screen, in viewport pixels.
const SCALE_BAR_RECT := Rect2(920.0, 70.0, 12.0, 464.0)
const SCALE_TICK_COUNT := 3
const SCALE_LABEL_SIZE := Vector2(112.0, 40.0)
const SCALE_FONT_SIZE := 34
var _current_mesh_instance: MeshInstance3D
var _current_grid_instance: MeshInstance3D
var _previous_mesh_instance: MeshInstance3D
var _contact_root: Node3D
var _idle_prompt: Label
var _current_material: StandardMaterial3D
var _grid_material: StandardMaterial3D
var _previous_material: StandardMaterial3D
var _current_material: ShaderMaterial
var _previous_material: ShaderMaterial
var _depth_scale: TextureRect
var _scale_labels: Array[Label] = []
var _current_scan_id: StringName = &""
var _previous_scan_id: StringName = &""
var _built_current_rows := 0
@@ -22,9 +39,8 @@ var _built_current_grid_segment_count := 0
func _ready() -> void:
size = Vector2i(960, 540)
size = RENDER_SIZE
render_target_update_mode = SubViewport.UPDATE_ALWAYS
positional_shadow_atlas_size = 1024
own_world_3d = true
_build_render_world()
@@ -41,11 +57,11 @@ func present(
if current_scan.scan_id != _current_scan_id:
_current_scan_id = current_scan.scan_id
_current_mesh_instance.mesh = ArrayMesh.new()
_current_grid_instance.mesh = ArrayMesh.new()
_built_current_rows = 0
_built_current_triangle_count = 0
_built_current_grid_segment_count = 0
_rebuild_contacts(current_scan)
_update_depth_scale(current_scan)
_build_current_rows(current_scan, clampi(visible_rows, 0, current_scan.samples.rows))
_update_contact_visibility(current_scan, visible_rows)
@@ -61,7 +77,7 @@ func present(
previous_scan,
_previous_material
)
_previous_material.albedo_color.a = clampf(previous_opacity, 0.0, 1.0)
SonarReliefStyle.set_opacity(_previous_material, previous_opacity)
func render_texture() -> Texture2D:
@@ -84,6 +100,16 @@ func idle_prompt_visible() -> bool:
return _idle_prompt != null and _idle_prompt.visible
## Elevation labels of the height scale, top (highest) to bottom; empty until
## a scan has been presented.
func depth_scale_texts() -> PackedStringArray:
var texts := PackedStringArray()
if _depth_scale != null and _depth_scale.visible:
for label: Label in _scale_labels:
texts.append(label.text)
return texts
func visible_contact_count() -> int:
var count := 0
for child: Node in _contact_root.get_children():
@@ -96,24 +122,17 @@ func _build_render_world() -> void:
var environment_node := WorldEnvironment.new()
var environment := Environment.new()
environment.background_mode = Environment.BG_COLOR
environment.background_color = Color("020407")
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
environment.ambient_light_color = Color(0.18, 0.31, 0.34)
environment.ambient_light_energy = 0.42
environment.background_color = SonarReliefStyle.COLOR_BACKGROUND
environment.ambient_light_source = Environment.AMBIENT_SOURCE_DISABLED
environment_node.environment = environment
add_child(environment_node)
_current_material = _relief_material(false)
_grid_material = _surface_grid_material()
_previous_material = _relief_material(true)
_current_material = SonarReliefStyle.relief_material(false)
_previous_material = SonarReliefStyle.relief_material(true)
_current_mesh_instance = MeshInstance3D.new()
_current_mesh_instance.name = "CurrentRelief"
_current_mesh_instance.mesh = ArrayMesh.new()
add_child(_current_mesh_instance)
_current_grid_instance = MeshInstance3D.new()
_current_grid_instance.name = "CurrentReliefGrid"
_current_grid_instance.mesh = ArrayMesh.new()
add_child(_current_grid_instance)
_previous_mesh_instance = MeshInstance3D.new()
_previous_mesh_instance.name = "PreviousRelief"
_previous_mesh_instance.mesh = ArrayMesh.new()
@@ -128,72 +147,88 @@ func _build_render_world() -> void:
var camera := Camera3D.new()
camera.name = "SonarCamera"
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
camera.size = 116.0
camera.keep_aspect = Camera3D.KEEP_WIDTH
camera.size = VIEW_WIDTH_M
camera.far = 240.0
camera.position = Vector3(0.0, 65.0, -30.0)
var elevation := deg_to_rad(CAMERA_ELEVATION_DEGREES)
camera.position = CAMERA_AIM_POINT + CAMERA_DISTANCE_M * Vector3(
0.0, sin(elevation), -cos(elevation)
)
add_child(camera)
camera.look_at(Vector3(0.0, -38.0, 58.0), Vector3.UP)
camera.look_at(CAMERA_AIM_POINT, Vector3.UP)
camera.current = true
var key_light := DirectionalLight3D.new()
key_light.rotation_degrees = Vector3(-52.0, -28.0, 0.0)
key_light.light_energy = 1.25
key_light.shadow_enabled = true
add_child(key_light)
var fill_light := DirectionalLight3D.new()
fill_light.rotation_degrees = Vector3(-28.0, 145.0, 18.0)
fill_light.light_energy = 0.38
add_child(fill_light)
_build_depth_scale()
_build_idle_prompt()
func _build_idle_prompt() -> void:
_idle_prompt = Label.new()
_idle_prompt.name = "IdlePrompt"
_idle_prompt.position = Vector2(180.0, 238.0)
_idle_prompt.size = Vector2(600.0, 64.0)
_idle_prompt.position = (Vector2(RENDER_SIZE) - IDLE_PROMPT_SIZE) * 0.5
_idle_prompt.size = IDLE_PROMPT_SIZE
_idle_prompt.text = "SONAR PRÊT · F POUR ÉMETTRE"
_idle_prompt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_idle_prompt.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_idle_prompt.add_theme_font_size_override("font_size", 26)
_idle_prompt.add_theme_color_override("font_color", Color(0.38, 0.88, 0.86))
_idle_prompt.add_theme_color_override("font_color", SonarReliefStyle.COLOR_MIDDLE)
_idle_prompt.add_theme_color_override("font_outline_color", Color(0.0, 0.04, 0.05))
_idle_prompt.add_theme_constant_override("outline_size", 6)
add_child(_idle_prompt)
func _relief_material(transparent: bool) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.vertex_color_use_as_albedo = true
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.cull_mode = BaseMaterial3D.CULL_DISABLED
if transparent:
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.albedo_color = Color(1.0, 1.0, 1.0, 0.0)
return material
## Height legend: the relief palette is relative to the scan, so the bar shows
## the real elevations of its two ends and of the steps in between.
func _build_depth_scale() -> void:
var gradient := GradientTexture2D.new()
gradient.gradient = SonarReliefStyle.height_gradient()
gradient.fill_from = Vector2(0.0, 1.0)
gradient.fill_to = Vector2(0.0, 0.0)
gradient.width = int(SCALE_BAR_RECT.size.x)
gradient.height = int(SCALE_BAR_RECT.size.y)
_depth_scale = TextureRect.new()
_depth_scale.name = "DepthScale"
_depth_scale.texture = gradient
_depth_scale.position = SCALE_BAR_RECT.position
_depth_scale.size = SCALE_BAR_RECT.size
_depth_scale.visible = false
add_child(_depth_scale)
for tick in SCALE_TICK_COUNT:
var label := Label.new()
label.size = SCALE_LABEL_SIZE
label.position = Vector2(
-label.size.x - 8.0,
SCALE_BAR_RECT.size.y * tick / float(SCALE_TICK_COUNT - 1) - label.size.y * 0.5
)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", SCALE_FONT_SIZE)
label.add_theme_color_override("font_color", SonarReliefStyle.COLOR_MIDDLE)
_depth_scale.add_child(label)
_scale_labels.append(label)
func _surface_grid_material() -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.albedo_color = Color(0.32, 0.95, 0.91, 0.34)
material.emission_enabled = true
material.emission = Color(0.18, 0.78, 0.76)
material.emission_energy_multiplier = 1.35
return material
func _update_depth_scale(scan: SonarScan) -> void:
for tick in _scale_labels.size():
var elevation := lerpf(
scan.maximum_elevation_m,
scan.minimum_elevation_m,
tick / float(_scale_labels.size() - 1)
)
_scale_labels[tick].text = "%.0f m" % elevation
_depth_scale.visible = true
func _build_current_rows(scan: SonarScan, visible_rows: int) -> void:
var mesh := _current_mesh_instance.mesh as ArrayMesh
var grid_mesh := _current_grid_instance.mesh as ArrayMesh
while _built_current_rows < visible_rows:
if _built_current_rows > 0:
_built_current_triangle_count += SonarReliefMeshBuilder.add_row_band(
var triangles := SonarReliefMeshBuilder.add_row_band(
mesh, scan, _built_current_rows, _current_material
)
_built_current_grid_segment_count += SonarReliefMeshBuilder.add_grid_band(
grid_mesh, scan, _built_current_rows, _grid_material
_built_current_triangle_count += triangles
_built_current_grid_segment_count += (
triangles / 2 * SonarReliefMeshBuilder.GRID_SEGMENTS_PER_CELL
)
_built_current_rows += 1
@@ -210,13 +245,9 @@ func _rebuild_contacts(scan: SonarScan) -> void:
marker.position = contact.center_m + Vector3.UP * 1.4
marker.visible = false
marker.set_meta("forward_distance_m", contact.center_m.z)
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = SonarReliefMeshBuilder.COLOR_METAL
material.emission_enabled = true
material.emission = SonarReliefMeshBuilder.COLOR_METAL
material.emission_energy_multiplier = 4.0
marker.material_override = material
marker.material_override = SonarReliefStyle.marker_material(
SonarReliefStyle.COLOR_METAL
)
_contact_root.add_child(marker)
@@ -244,11 +275,7 @@ func _add_submarine_marker() -> void:
box.size = Vector3(3.2, 1.1, 5.8)
marker.mesh = box
marker.position = Vector3(0.0, 0.0, 1.0)
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = Color.WHITE
material.emission_enabled = true
material.emission = Color.WHITE
material.emission_energy_multiplier = 2.4
marker.material_override = material
marker.material_override = SonarReliefStyle.marker_material(
SonarReliefStyle.COLOR_VEHICLE
)
add_child(marker)
@@ -0,0 +1,157 @@
class_name SonarReliefStyle
extends RefCounted
## Single owner of the holographic look of the sonar relief: palette, grid
## spacing, line widths and intensities. Display only: nothing here is a
## gameplay value.
## Near-black table the hologram floats on. Black also means "unknown".
const COLOR_BACKGROUND := Color("010507")
## Height ramp, relative to the scan: dark teal low, cyan mid, pale on peaks.
const COLOR_DEEP := Color("06404a")
const COLOR_MIDDLE := Color("17b8c4")
const COLOR_PEAK := Color("d8fbff")
## Metal returns keep one warm accent so an anomaly never reads as terrain.
const COLOR_METAL := Color("ff7a2a")
const COLOR_VEHICLE := Color("f2ffff")
## Grid lines follow the sample grid: one minor line every N cells.
const GRID_MINOR_CELLS := 2.0
const GRID_MAJOR_CELLS := 6.0
const GRID_MINOR_WIDTH_PX := 1.2
const GRID_MAJOR_WIDTH_PX := 2.2
const GRID_MINOR_INTENSITY := 0.30
const GRID_MAJOR_INTENSITY := 0.80
## Iso-height contours, in fractions of the scan's height span.
const CONTOUR_STEP := 0.125
const CONTOUR_WIDTH_PX := 1.0
const CONTOUR_INTENSITY := 0.20
## Translucent surface under the grid and the rim that outlines ridges.
const SURFACE_INTENSITY := 0.50
const RIM_INTENSITY := 0.35
const RIM_POWER := 2.5
## How much brighter the peaks' lines are than the deep ones.
const PEAK_LINE_BOOST := 0.55
const _SHADER_CODE := """
shader_type spatial;
render_mode unshaded, cull_disabled, depth_draw_always;
uniform vec3 background : source_color;
uniform vec3 color_deep : source_color;
uniform vec3 color_middle : source_color;
uniform vec3 color_peak : source_color;
uniform vec3 color_metal : source_color;
uniform float grid_minor_cells;
uniform float grid_major_cells;
uniform float grid_minor_width_px;
uniform float grid_major_width_px;
uniform float grid_minor_intensity;
uniform float grid_major_intensity;
uniform float contour_step;
uniform float contour_width_px;
uniform float contour_intensity;
uniform float surface_intensity;
uniform float rim_intensity;
uniform float rim_power;
uniform float peak_line_boost;
uniform float opacity = 1.0;
// Anti-aliased line on every multiple of `spacing`, `width_px` wide on screen.
// Fades out when lines would be closer than a few pixels, so it never shimmers.
float grid_line(float coordinate, float spacing, float width_px) {
float scaled = coordinate / spacing;
float pixel = max(fwidth(scaled), 1e-5);
float distance_px = abs(fract(scaled - 0.5) - 0.5) / pixel;
float line = 1.0 - smoothstep(width_px * 0.5 - 0.5, width_px * 0.5 + 0.5, distance_px);
return line * clamp((1.0 / pixel - 2.0) / 3.0, 0.0, 1.0);
}
void fragment() {
// UV carries (column, row) of the sample grid; COLOR.r the relative height,
// COLOR.g the metal flag, COLOR.b the echo intensity.
float height = clamp(COLOR.r, 0.0, 1.0);
vec3 ramp = mix(color_deep, color_middle, smoothstep(0.0, 0.55, height));
ramp = mix(ramp, color_peak, smoothstep(0.55, 1.0, height));
// The translucent surface stays in the teal-to-cyan range so peaks do not
// wash out to grey; only the lines reach the pale peak colour.
vec3 fill = mix(color_deep, color_middle, smoothstep(0.0, 0.85, height));
float metal = step(0.5, COLOR.g);
ramp = mix(ramp, color_metal, metal);
fill = mix(fill, color_metal, metal);
float minor = max(
grid_line(UV.x, grid_minor_cells, grid_minor_width_px),
grid_line(UV.y, grid_minor_cells, grid_minor_width_px)
);
float major = max(
grid_line(UV.x, grid_major_cells, grid_major_width_px),
grid_line(UV.y, grid_major_cells, grid_major_width_px)
);
float contour = grid_line(height, contour_step, contour_width_px);
float rim = pow(1.0 - abs(dot(normalize(NORMAL), normalize(VIEW))), rim_power);
float lines = max(minor * grid_minor_intensity, major * grid_major_intensity);
lines = max(lines, contour * contour_intensity);
lines *= 1.0 + peak_line_boost * height;
float surface = surface_intensity * (0.22 + 0.78 * height) * (0.8 + 0.2 * COLOR.b);
vec3 glow = fill * (surface + rim * rim_intensity) + ramp * lines;
ALBEDO = background + glow;
//FADE ALPHA = opacity;
}
"""
static var _opaque_shader: Shader
static var _fading_shader: Shader
## The current scan is opaque: depth-tested, so ridges hide what lies behind
## them and nothing depends on draw order. The previous scan fades out with the
## same shader, alpha-blended and still writing depth.
static func relief_material(fading: bool) -> ShaderMaterial:
if _opaque_shader == null:
_opaque_shader = Shader.new()
_opaque_shader.code = _SHADER_CODE
_fading_shader = Shader.new()
_fading_shader.code = _SHADER_CODE.replace("//FADE ", "")
var material := ShaderMaterial.new()
material.shader = _fading_shader if fading else _opaque_shader
material.set_shader_parameter("background", COLOR_BACKGROUND)
material.set_shader_parameter("color_deep", COLOR_DEEP)
material.set_shader_parameter("color_middle", COLOR_MIDDLE)
material.set_shader_parameter("color_peak", COLOR_PEAK)
material.set_shader_parameter("color_metal", COLOR_METAL)
material.set_shader_parameter("grid_minor_cells", GRID_MINOR_CELLS)
material.set_shader_parameter("grid_major_cells", GRID_MAJOR_CELLS)
material.set_shader_parameter("grid_minor_width_px", GRID_MINOR_WIDTH_PX)
material.set_shader_parameter("grid_major_width_px", GRID_MAJOR_WIDTH_PX)
material.set_shader_parameter("grid_minor_intensity", GRID_MINOR_INTENSITY)
material.set_shader_parameter("grid_major_intensity", GRID_MAJOR_INTENSITY)
material.set_shader_parameter("contour_step", CONTOUR_STEP)
material.set_shader_parameter("contour_width_px", CONTOUR_WIDTH_PX)
material.set_shader_parameter("contour_intensity", CONTOUR_INTENSITY)
material.set_shader_parameter("surface_intensity", SURFACE_INTENSITY)
material.set_shader_parameter("rim_intensity", RIM_INTENSITY)
material.set_shader_parameter("rim_power", RIM_POWER)
material.set_shader_parameter("peak_line_boost", PEAK_LINE_BOOST)
return material
static func set_opacity(material: ShaderMaterial, opacity: float) -> void:
material.set_shader_parameter("opacity", clampf(opacity, 0.0, 1.0))
static func marker_material(color: Color) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
return material
## Colour of a relative height (0 low, 1 peak), for the depth scale legend.
static func height_gradient() -> Gradient:
var gradient := Gradient.new()
gradient.offsets = PackedFloat32Array([0.0, 0.55, 1.0])
gradient.colors = PackedColorArray([COLOR_DEEP, COLOR_MIDDLE, COLOR_PEAK])
return gradient