Render bathymetric sonar surface

This commit is contained in:
chindit committed 2026-10-06 07:59:50 +02:00
1 parent fdebbafeae
commit b6f6b53a70
7 files changed
+305 -135

No files matched your search

+12
View File
@@ -53,6 +53,18 @@ func built_row_count() -> int:
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:
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
)
+67 -108
View File
@@ -1,25 +1,24 @@
class_name BathymetricSonarRenderer
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 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")
const SonarReliefMeshBuilder = preload(
"res://game/presentation/sonar/sonar_relief_mesh_builder.gd"
)
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_scan_id: StringName = &""
var _previous_scan_id: StringName = &""
var _built_current_rows := 0
var _built_current_triangle_count := 0
var _built_current_grid_segment_count := 0
func _ready() -> void:
@@ -38,10 +37,14 @@ func present(
) -> void:
if current_scan == null:
return
_idle_prompt.visible = false
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)
_build_current_rows(current_scan, clampi(visible_rows, 0, current_scan.samples.rows))
_update_contact_visibility(current_scan, visible_rows)
@@ -54,7 +57,7 @@ func present(
_previous_mesh_instance.visible = previous_opacity > 0.0
if previous_scan.scan_id != _previous_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_material
)
@@ -69,6 +72,18 @@ func built_row_count() -> int:
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:
var count := 0
for child: Node in _contact_root.get_children():
@@ -89,11 +104,16 @@ func _build_render_world() -> void:
add_child(environment_node)
_current_material = _relief_material(false)
_grid_material = _surface_grid_material()
_previous_material = _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()
@@ -112,7 +132,7 @@ func _build_render_world() -> void:
camera.far = 240.0
camera.position = Vector3(0.0, 65.0, -30.0)
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
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.light_energy = 0.38
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:
@@ -137,108 +173,31 @@ func _relief_material(transparent: bool) -> StandardMaterial3D:
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:
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:
_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
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:
for child: Node in _contact_root.get_children():
_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)
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = COLOR_METAL
material.albedo_color = SonarReliefMeshBuilder.COLOR_METAL
material.emission_enabled = true
material.emission = COLOR_METAL
material.emission = SonarReliefMeshBuilder.COLOR_METAL
material.emission_energy_multiplier = 4.0
marker.material_override = material
_contact_root.add_child(marker)