Render bathymetric sonar surface
This commit is contained in:
1 parent
fdebbafeae
commit
b6f6b53a70
7 files changed
+305
-135
No files matched your search
@@ -388,11 +388,12 @@ mesh = SubResource("KnobMesh")
|
|||||||
material_override = SubResource("BrassMaterial")
|
material_override = SubResource("BrassMaterial")
|
||||||
|
|
||||||
[node name="SonarMonitor" type="Node3D" parent="."]
|
[node name="SonarMonitor" type="Node3D" parent="."]
|
||||||
position = Vector3(0.74, 0.42, -1.18)
|
position = Vector3(0.54, 0.59, -1.04)
|
||||||
rotation_degrees = Vector3(-5, -22, -2)
|
rotation_degrees = Vector3(-5, -42, -2)
|
||||||
|
scale = Vector3(0.6, 0.6, 0.6)
|
||||||
|
|
||||||
[node name="Housing" type="MeshInstance3D" parent="SonarMonitor"]
|
[node name="Housing" type="MeshInstance3D" parent="SonarMonitor"]
|
||||||
scale = Vector3(0.5, 0.34, 0.1)
|
scale = Vector3(0.6, 0.37, 0.1)
|
||||||
mesh = SubResource("UnitBoxMesh")
|
mesh = SubResource("UnitBoxMesh")
|
||||||
material_override = SubResource("PaintedMaterial")
|
material_override = SubResource("PaintedMaterial")
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ var indicator_green: StandardMaterial3D
|
|||||||
var indicator_amber: StandardMaterial3D
|
var indicator_amber: StandardMaterial3D
|
||||||
var indicator_red: StandardMaterial3D
|
var indicator_red: StandardMaterial3D
|
||||||
var sonar_grid: StandardMaterial3D
|
var sonar_grid: StandardMaterial3D
|
||||||
var sonar_trace: StandardMaterial3D
|
|
||||||
var warm_glass: StandardMaterial3D
|
var warm_glass: StandardMaterial3D
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +93,6 @@ func _init() -> void:
|
|||||||
indicator_amber = _emissive_material(Color(1.0, 0.43, 0.035), 2.8)
|
indicator_amber = _emissive_material(Color(1.0, 0.43, 0.035), 2.8)
|
||||||
indicator_red = _emissive_material(Color(0.9, 0.045, 0.018), 2.8)
|
indicator_red = _emissive_material(Color(0.9, 0.045, 0.018), 2.8)
|
||||||
sonar_grid = _emissive_material(Color(0.02, 0.20, 0.22), 0.55)
|
sonar_grid = _emissive_material(Color(0.02, 0.20, 0.22), 0.55)
|
||||||
sonar_trace = _emissive_material(Color(0.12, 0.78, 0.78), 1.8)
|
|
||||||
warm_glass = _warm_glass_material()
|
warm_glass = _warm_glass_material()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func _init(materials: CockpitMaterialPalette) -> void:
|
|||||||
_materials = materials
|
_materials = materials
|
||||||
name = "SonarConsoleDetailing"
|
name = "SonarConsoleDetailing"
|
||||||
_build_bezel()
|
_build_bezel()
|
||||||
_build_graticule()
|
_build_grid()
|
||||||
_build_control_strip()
|
_build_control_strip()
|
||||||
_build_mounting_hardware()
|
_build_mounting_hardware()
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ func _build_control_strip() -> void:
|
|||||||
_control_count += 1
|
_control_count += 1
|
||||||
|
|
||||||
|
|
||||||
func _build_graticule() -> void:
|
func _build_grid() -> void:
|
||||||
for x: float in [-0.145, -0.085, -0.025, 0.035, 0.095]:
|
for x: float in [-0.145, -0.085, -0.025, 0.035, 0.095]:
|
||||||
CockpitMeshFactory.add_box(
|
CockpitMeshFactory.add_box(
|
||||||
"SonarGridVertical",
|
"SonarGridVertical",
|
||||||
@@ -117,24 +117,6 @@ func _build_graticule() -> void:
|
|||||||
Vector3(0.33, 0.0015, 0.002),
|
Vector3(0.33, 0.0015, 0.002),
|
||||||
_materials.sonar_grid
|
_materials.sonar_grid
|
||||||
)
|
)
|
||||||
var trace_points: Array[Vector3] = [
|
|
||||||
Vector3(-0.185, -0.035, 0.080),
|
|
||||||
Vector3(-0.135, -0.020, 0.080),
|
|
||||||
Vector3(-0.085, -0.055, 0.080),
|
|
||||||
Vector3(-0.030, -0.005, 0.080),
|
|
||||||
Vector3(0.025, -0.030, 0.080),
|
|
||||||
Vector3(0.075, 0.030, 0.080),
|
|
||||||
Vector3(0.125, 0.075, 0.080),
|
|
||||||
]
|
|
||||||
for point_index in trace_points.size() - 1:
|
|
||||||
CockpitMeshFactory.add_cylinder_between(
|
|
||||||
"SonarReferenceTrace",
|
|
||||||
self,
|
|
||||||
trace_points[point_index],
|
|
||||||
trace_points[point_index + 1],
|
|
||||||
0.0025,
|
|
||||||
_materials.sonar_trace
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _build_mounting_hardware() -> void:
|
func _build_mounting_hardware() -> void:
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ func built_row_count() -> int:
|
|||||||
return _renderer.built_row_count()
|
return _renderer.built_row_count()
|
||||||
|
|
||||||
|
|
||||||
|
func built_triangle_count() -> int:
|
||||||
|
return _renderer.built_triangle_count()
|
||||||
|
|
||||||
|
|
||||||
|
func built_grid_segment_count() -> int:
|
||||||
|
return _renderer.built_grid_segment_count()
|
||||||
|
|
||||||
|
|
||||||
|
func idle_prompt_visible() -> bool:
|
||||||
|
return _renderer.idle_prompt_visible()
|
||||||
|
|
||||||
|
|
||||||
func status_text() -> String:
|
func status_text() -> String:
|
||||||
return _status_label.text
|
return _status_label.text
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
class_name SonarReliefMeshBuilder
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
static func build_full_mesh(
|
||||||
|
scan: SonarScan,
|
||||||
|
material: StandardMaterial3D
|
||||||
|
) -> ArrayMesh:
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
for row in range(1, scan.samples.rows):
|
||||||
|
add_row_band(mesh, scan, row, material)
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
static func add_row_band(
|
||||||
|
mesh: ArrayMesh,
|
||||||
|
scan: SonarScan,
|
||||||
|
row: int,
|
||||||
|
material: StandardMaterial3D
|
||||||
|
) -> int:
|
||||||
|
var vertices := PackedVector3Array()
|
||||||
|
var normals := PackedVector3Array()
|
||||||
|
var colors := PackedColorArray()
|
||||||
|
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)
|
||||||
|
if not (
|
||||||
|
top_left.observed
|
||||||
|
and top_right.observed
|
||||||
|
and bottom_left.observed
|
||||||
|
and bottom_right.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
|
||||||
|
)
|
||||||
|
if vertices.is_empty():
|
||||||
|
return 0
|
||||||
|
var arrays: Array = []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||||
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||||
|
arrays[Mesh.ARRAY_COLOR] = colors
|
||||||
|
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
|
||||||
|
)
|
||||||
@@ -1,25 +1,24 @@
|
|||||||
class_name BathymetricSonarRenderer
|
class_name BathymetricSonarRenderer
|
||||||
extends SubViewport
|
extends SubViewport
|
||||||
|
|
||||||
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 SonarScan = preload("res://game/domain/sonar/sonar_scan.gd")
|
||||||
|
const SonarReliefMeshBuilder = preload(
|
||||||
const COLOR_DEEP := Color("173f91")
|
"res://game/presentation/sonar/sonar_relief_mesh_builder.gd"
|
||||||
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")
|
|
||||||
|
|
||||||
var _current_mesh_instance: MeshInstance3D
|
var _current_mesh_instance: MeshInstance3D
|
||||||
|
var _current_grid_instance: MeshInstance3D
|
||||||
var _previous_mesh_instance: MeshInstance3D
|
var _previous_mesh_instance: MeshInstance3D
|
||||||
var _contact_root: Node3D
|
var _contact_root: Node3D
|
||||||
|
var _idle_prompt: Label
|
||||||
var _current_material: StandardMaterial3D
|
var _current_material: StandardMaterial3D
|
||||||
|
var _grid_material: StandardMaterial3D
|
||||||
var _previous_material: StandardMaterial3D
|
var _previous_material: StandardMaterial3D
|
||||||
var _current_scan_id: StringName = &""
|
var _current_scan_id: StringName = &""
|
||||||
var _previous_scan_id: StringName = &""
|
var _previous_scan_id: StringName = &""
|
||||||
var _built_current_rows := 0
|
var _built_current_rows := 0
|
||||||
|
var _built_current_triangle_count := 0
|
||||||
|
var _built_current_grid_segment_count := 0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -38,10 +37,14 @@ func present(
|
|||||||
) -> void:
|
) -> void:
|
||||||
if current_scan == null:
|
if current_scan == null:
|
||||||
return
|
return
|
||||||
|
_idle_prompt.visible = false
|
||||||
if current_scan.scan_id != _current_scan_id:
|
if current_scan.scan_id != _current_scan_id:
|
||||||
_current_scan_id = current_scan.scan_id
|
_current_scan_id = current_scan.scan_id
|
||||||
_current_mesh_instance.mesh = ArrayMesh.new()
|
_current_mesh_instance.mesh = ArrayMesh.new()
|
||||||
|
_current_grid_instance.mesh = ArrayMesh.new()
|
||||||
_built_current_rows = 0
|
_built_current_rows = 0
|
||||||
|
_built_current_triangle_count = 0
|
||||||
|
_built_current_grid_segment_count = 0
|
||||||
_rebuild_contacts(current_scan)
|
_rebuild_contacts(current_scan)
|
||||||
_build_current_rows(current_scan, clampi(visible_rows, 0, current_scan.samples.rows))
|
_build_current_rows(current_scan, clampi(visible_rows, 0, current_scan.samples.rows))
|
||||||
_update_contact_visibility(current_scan, visible_rows)
|
_update_contact_visibility(current_scan, visible_rows)
|
||||||
@@ -54,7 +57,7 @@ func present(
|
|||||||
_previous_mesh_instance.visible = previous_opacity > 0.0
|
_previous_mesh_instance.visible = previous_opacity > 0.0
|
||||||
if previous_scan.scan_id != _previous_scan_id:
|
if previous_scan.scan_id != _previous_scan_id:
|
||||||
_previous_scan_id = previous_scan.scan_id
|
_previous_scan_id = previous_scan.scan_id
|
||||||
_previous_mesh_instance.mesh = _build_full_mesh(
|
_previous_mesh_instance.mesh = SonarReliefMeshBuilder.build_full_mesh(
|
||||||
previous_scan,
|
previous_scan,
|
||||||
_previous_material
|
_previous_material
|
||||||
)
|
)
|
||||||
@@ -69,6 +72,18 @@ func built_row_count() -> int:
|
|||||||
return _built_current_rows
|
return _built_current_rows
|
||||||
|
|
||||||
|
|
||||||
|
func built_triangle_count() -> int:
|
||||||
|
return _built_current_triangle_count
|
||||||
|
|
||||||
|
|
||||||
|
func built_grid_segment_count() -> int:
|
||||||
|
return _built_current_grid_segment_count
|
||||||
|
|
||||||
|
|
||||||
|
func idle_prompt_visible() -> bool:
|
||||||
|
return _idle_prompt != null and _idle_prompt.visible
|
||||||
|
|
||||||
|
|
||||||
func visible_contact_count() -> int:
|
func visible_contact_count() -> int:
|
||||||
var count := 0
|
var count := 0
|
||||||
for child: Node in _contact_root.get_children():
|
for child: Node in _contact_root.get_children():
|
||||||
@@ -89,11 +104,16 @@ func _build_render_world() -> void:
|
|||||||
add_child(environment_node)
|
add_child(environment_node)
|
||||||
|
|
||||||
_current_material = _relief_material(false)
|
_current_material = _relief_material(false)
|
||||||
|
_grid_material = _surface_grid_material()
|
||||||
_previous_material = _relief_material(true)
|
_previous_material = _relief_material(true)
|
||||||
_current_mesh_instance = MeshInstance3D.new()
|
_current_mesh_instance = MeshInstance3D.new()
|
||||||
_current_mesh_instance.name = "CurrentRelief"
|
_current_mesh_instance.name = "CurrentRelief"
|
||||||
_current_mesh_instance.mesh = ArrayMesh.new()
|
_current_mesh_instance.mesh = ArrayMesh.new()
|
||||||
add_child(_current_mesh_instance)
|
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 = MeshInstance3D.new()
|
||||||
_previous_mesh_instance.name = "PreviousRelief"
|
_previous_mesh_instance.name = "PreviousRelief"
|
||||||
_previous_mesh_instance.mesh = ArrayMesh.new()
|
_previous_mesh_instance.mesh = ArrayMesh.new()
|
||||||
@@ -112,7 +132,7 @@ func _build_render_world() -> void:
|
|||||||
camera.far = 240.0
|
camera.far = 240.0
|
||||||
camera.position = Vector3(0.0, 65.0, -30.0)
|
camera.position = Vector3(0.0, 65.0, -30.0)
|
||||||
add_child(camera)
|
add_child(camera)
|
||||||
camera.look_at(Vector3(0.0, -10.0, 60.0), Vector3.UP)
|
camera.look_at(Vector3(0.0, -38.0, 58.0), Vector3.UP)
|
||||||
camera.current = true
|
camera.current = true
|
||||||
|
|
||||||
var key_light := DirectionalLight3D.new()
|
var key_light := DirectionalLight3D.new()
|
||||||
@@ -124,6 +144,22 @@ func _build_render_world() -> void:
|
|||||||
fill_light.rotation_degrees = Vector3(-28.0, 145.0, 18.0)
|
fill_light.rotation_degrees = Vector3(-28.0, 145.0, 18.0)
|
||||||
fill_light.light_energy = 0.38
|
fill_light.light_energy = 0.38
|
||||||
add_child(fill_light)
|
add_child(fill_light)
|
||||||
|
_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.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_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:
|
func _relief_material(transparent: bool) -> StandardMaterial3D:
|
||||||
@@ -137,108 +173,31 @@ func _relief_material(transparent: bool) -> StandardMaterial3D:
|
|||||||
return material
|
return material
|
||||||
|
|
||||||
|
|
||||||
|
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 _build_current_rows(scan: SonarScan, visible_rows: int) -> void:
|
func _build_current_rows(scan: SonarScan, visible_rows: int) -> void:
|
||||||
var mesh := _current_mesh_instance.mesh as ArrayMesh
|
var mesh := _current_mesh_instance.mesh as ArrayMesh
|
||||||
|
var grid_mesh := _current_grid_instance.mesh as ArrayMesh
|
||||||
while _built_current_rows < visible_rows:
|
while _built_current_rows < visible_rows:
|
||||||
if _built_current_rows > 0:
|
if _built_current_rows > 0:
|
||||||
_add_row_band(mesh, scan, _built_current_rows, _current_material)
|
_built_current_triangle_count += 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_rows += 1
|
_built_current_rows += 1
|
||||||
|
|
||||||
|
|
||||||
func _build_full_mesh(
|
|
||||||
scan: SonarScan,
|
|
||||||
material: StandardMaterial3D
|
|
||||||
) -> ArrayMesh:
|
|
||||||
var mesh := ArrayMesh.new()
|
|
||||||
for row in range(1, scan.samples.rows):
|
|
||||||
_add_row_band(mesh, scan, row, material)
|
|
||||||
return mesh
|
|
||||||
|
|
||||||
|
|
||||||
func _add_row_band(
|
|
||||||
mesh: ArrayMesh,
|
|
||||||
scan: SonarScan,
|
|
||||||
row: int,
|
|
||||||
material: StandardMaterial3D
|
|
||||||
) -> void:
|
|
||||||
var vertices := PackedVector3Array()
|
|
||||||
var normals := PackedVector3Array()
|
|
||||||
var colors := PackedColorArray()
|
|
||||||
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)
|
|
||||||
if not (
|
|
||||||
top_left.observed
|
|
||||||
and top_right.observed
|
|
||||||
and bottom_left.observed
|
|
||||||
and bottom_right.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
|
|
||||||
)
|
|
||||||
if vertices.is_empty():
|
|
||||||
return
|
|
||||||
var arrays: Array = []
|
|
||||||
arrays.resize(Mesh.ARRAY_MAX)
|
|
||||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
|
||||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
|
||||||
arrays[Mesh.ARRAY_COLOR] = colors
|
|
||||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
||||||
mesh.surface_set_material(mesh.get_surface_count() - 1, material)
|
|
||||||
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _rebuild_contacts(scan: SonarScan) -> void:
|
func _rebuild_contacts(scan: SonarScan) -> void:
|
||||||
for child: Node in _contact_root.get_children():
|
for child: Node in _contact_root.get_children():
|
||||||
_contact_root.remove_child(child)
|
_contact_root.remove_child(child)
|
||||||
@@ -253,9 +212,9 @@ func _rebuild_contacts(scan: SonarScan) -> void:
|
|||||||
marker.set_meta("forward_distance_m", contact.center_m.z)
|
marker.set_meta("forward_distance_m", contact.center_m.z)
|
||||||
var material := StandardMaterial3D.new()
|
var material := StandardMaterial3D.new()
|
||||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
material.albedo_color = COLOR_METAL
|
material.albedo_color = SonarReliefMeshBuilder.COLOR_METAL
|
||||||
material.emission_enabled = true
|
material.emission_enabled = true
|
||||||
material.emission = COLOR_METAL
|
material.emission = SonarReliefMeshBuilder.COLOR_METAL
|
||||||
material.emission_energy_multiplier = 4.0
|
material.emission_energy_multiplier = 4.0
|
||||||
marker.material_override = material
|
marker.material_override = material
|
||||||
_contact_root.add_child(marker)
|
_contact_root.add_child(marker)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const ProductionInputAdapter = preload(
|
|||||||
"res://game/infrastructure/input/production_input_adapter.gd"
|
"res://game/infrastructure/input/production_input_adapter.gd"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TEST_COUNT := 69
|
const TEST_COUNT := 78
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -125,6 +125,28 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
|||||||
_expect(instruments.get_node("%WindowFrame") is MeshInstance3D, "cockpit has a physical porthole frame")
|
_expect(instruments.get_node("%WindowFrame") is MeshInstance3D, "cockpit has a physical porthole frame")
|
||||||
_expect(instruments.get_node("%Dashboard") is Node3D, "cockpit has a physical dashboard")
|
_expect(instruments.get_node("%Dashboard") is Node3D, "cockpit has a physical dashboard")
|
||||||
_expect(instruments.get_node("%SonarScreen") is MeshInstance3D, "cockpit reserves a physical sonar display")
|
_expect(instruments.get_node("%SonarScreen") is MeshInstance3D, "cockpit reserves a physical sonar display")
|
||||||
|
var sonar_monitor := instruments.get_node("SonarMonitor") as Node3D
|
||||||
|
var sonar_screen := instruments.get_node("%SonarScreen") as MeshInstance3D
|
||||||
|
var sonar_housing := instruments.get_node("SonarMonitor/Housing") as MeshInstance3D
|
||||||
|
var cockpit_camera := camera.get_node("Camera") as Camera3D
|
||||||
|
var electrical_bank := detailing.get_node("SideEquipment/ElectricalBank") as Node3D
|
||||||
|
_expect(sonar_monitor.position.x > 0.5, "cockpit sonar is mounted on the right side")
|
||||||
|
_expect(
|
||||||
|
_sonar_screen_faces_camera(sonar_screen, cockpit_camera),
|
||||||
|
"right-side sonar display faces the pilot"
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
_sonar_screen_is_ahead_of_bank(sonar_screen, electrical_bank),
|
||||||
|
"right-side electrical equipment does not hide the sonar display"
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
_sonar_screen_clears_housing(sonar_screen, sonar_housing),
|
||||||
|
"sonar texture clears its housing without depth overlap"
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
instruments.find_children("SonarReferenceTrace", "MeshInstance3D", true).is_empty(),
|
||||||
|
"cockpit sonar does not invent a decorative profile"
|
||||||
|
)
|
||||||
_expect(detailing.pressure_hull_rib_count() >= 6, "cockpit exposes reinforced pressure-hull structure")
|
_expect(detailing.pressure_hull_rib_count() >= 6, "cockpit exposes reinforced pressure-hull structure")
|
||||||
_expect(detailing.pipe_segment_count() >= 13, "cockpit exposes a complete life-support pipe run")
|
_expect(detailing.pipe_segment_count() >= 13, "cockpit exposes a complete life-support pipe run")
|
||||||
_expect(absf(detailing.battery_needle_degrees()) > 100.0, "analog battery gauge follows the dive snapshot")
|
_expect(absf(detailing.battery_needle_degrees()) > 100.0, "analog battery gauge follows the dive snapshot")
|
||||||
@@ -148,6 +170,7 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
|||||||
instruments.sonar_texture() == sonar_display.sonar_texture(),
|
instruments.sonar_texture() == sonar_display.sonar_texture(),
|
||||||
"physical and expanded sonar views share one texture"
|
"physical and expanded sonar views share one texture"
|
||||||
)
|
)
|
||||||
|
_expect(sonar_display.idle_prompt_visible(), "idle sonar explains how to emit a scan")
|
||||||
|
|
||||||
adapter.sonar_scan_requested.emit()
|
adapter.sonar_scan_requested.emit()
|
||||||
var first_scan = sonar_service.current_scan()
|
var first_scan = sonar_service.current_scan()
|
||||||
@@ -155,9 +178,12 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
|||||||
_expect(first_scan.samples.observed_count() > 1000, "reference acquisition captures dense bathymetry")
|
_expect(first_scan.samples.observed_count() > 1000, "reference acquisition captures dense bathymetry")
|
||||||
_expect(first_scan.contacts.size() >= 1, "metal calibration target becomes a distinct contact")
|
_expect(first_scan.contacts.size() >= 1, "metal calibration target becomes a distinct contact")
|
||||||
_expect("RELEVÉ EN COURS" in sonar_display.status_text(), "sonar display reports progressive reveal")
|
_expect("RELEVÉ EN COURS" in sonar_display.status_text(), "sonar display reports progressive reveal")
|
||||||
for frame in 8:
|
_expect(not sonar_display.idle_prompt_visible(), "sonar data replaces the idle instruction")
|
||||||
|
for frame in 18:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
_expect(sonar_display.built_row_count() > 0, "renderer reveals near rows incrementally")
|
_expect(sonar_display.built_row_count() > 0, "renderer reveals near rows incrementally")
|
||||||
|
_expect(sonar_display.built_triangle_count() >= 90, "sonar renders a dense relief surface, not a line")
|
||||||
|
_expect(sonar_display.built_grid_segment_count() > 0, "sonar overlays a perspective grid on the relief")
|
||||||
adapter.sonar_view_toggle_requested.emit()
|
adapter.sonar_view_toggle_requested.emit()
|
||||||
_expect(sonar_display.is_expanded(), "sonar can expand without pausing the dive")
|
_expect(sonar_display.is_expanded(), "sonar can expand without pausing the dive")
|
||||||
adapter.sonar_view_toggle_requested.emit()
|
adapter.sonar_view_toggle_requested.emit()
|
||||||
@@ -276,6 +302,47 @@ func _pressed_key(keycode: Key) -> InputEventKey:
|
|||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
func _sonar_screen_faces_camera(
|
||||||
|
sonar_screen: MeshInstance3D,
|
||||||
|
cockpit_camera: Camera3D
|
||||||
|
) -> bool:
|
||||||
|
if sonar_screen == null or cockpit_camera == null:
|
||||||
|
return false
|
||||||
|
var direction_to_camera := (
|
||||||
|
cockpit_camera.global_position - sonar_screen.global_position
|
||||||
|
).normalized()
|
||||||
|
return sonar_screen.global_basis.z.normalized().dot(direction_to_camera) > 0.85
|
||||||
|
|
||||||
|
|
||||||
|
func _sonar_screen_is_ahead_of_bank(
|
||||||
|
sonar_screen: MeshInstance3D,
|
||||||
|
electrical_bank: Node3D
|
||||||
|
) -> bool:
|
||||||
|
if sonar_screen == null or electrical_bank == null:
|
||||||
|
return false
|
||||||
|
for child: Node in electrical_bank.get_children():
|
||||||
|
if child is MeshInstance3D and child.name.begins_with("JunctionBox"):
|
||||||
|
if sonar_screen.global_position.z <= (child as MeshInstance3D).global_position.z:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _sonar_screen_clears_housing(
|
||||||
|
sonar_screen: MeshInstance3D,
|
||||||
|
sonar_housing: MeshInstance3D
|
||||||
|
) -> bool:
|
||||||
|
if sonar_screen == null or sonar_housing == null:
|
||||||
|
return false
|
||||||
|
var housing_box := sonar_housing.mesh as BoxMesh
|
||||||
|
if housing_box == null:
|
||||||
|
return false
|
||||||
|
var housing_front_z := (
|
||||||
|
sonar_housing.position.z
|
||||||
|
+ housing_box.size.z * sonar_housing.scale.z * 0.5
|
||||||
|
)
|
||||||
|
return sonar_screen.position.z >= housing_front_z + 0.005
|
||||||
|
|
||||||
|
|
||||||
func _binding_for(adapter: ProductionInputAdapter, action_id: StringName) -> String:
|
func _binding_for(adapter: ProductionInputAdapter, action_id: StringName) -> String:
|
||||||
for binding in adapter.binding_snapshots():
|
for binding in adapter.binding_snapshots():
|
||||||
if binding.action_id == action_id:
|
if binding.action_id == action_id:
|
||||||
|
|||||||
Reference in new issue
Block a user