Add production bathymetric sonar slice
This commit is contained in:
1 parent
a18e82eb44
commit
7613daee84
28 files changed
+1825
-87
No files matched your search
@@ -0,0 +1,68 @@
|
||||
class_name SonarDisplay
|
||||
extends Control
|
||||
|
||||
const SonarScan = preload("res://game/domain/sonar/sonar_scan.gd")
|
||||
const SonarScanState = preload("res://game/domain/sonar/sonar_scan_state.gd")
|
||||
const BathymetricSonarRendererScript = preload(
|
||||
"res://game/presentation/sonar/sonar_relief_renderer.gd"
|
||||
)
|
||||
|
||||
@onready var _renderer: Node = %SonarReliefRenderer
|
||||
@onready var _expanded_panel: PanelContainer = %ExpandedPanel
|
||||
@onready var _expanded_view: TextureRect = %ExpandedView
|
||||
@onready var _status_label: Label = %StatusLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_expanded_view.texture = _renderer.render_texture()
|
||||
_expanded_panel.visible = false
|
||||
|
||||
|
||||
func present(
|
||||
current_scan: SonarScan,
|
||||
previous_scan: SonarScan,
|
||||
visible_rows: int,
|
||||
previous_opacity: float
|
||||
) -> void:
|
||||
if current_scan == null:
|
||||
_status_label.text = "SONAR PRÊT · F POUR ÉMETTRE"
|
||||
return
|
||||
_renderer.present(current_scan, previous_scan, visible_rows, previous_opacity)
|
||||
_status_label.text = "%s · %.0f %% · %.0f–%.0f m · %d CONTACT(S)" % [
|
||||
_state_text(current_scan.state),
|
||||
current_scan.reveal_progress * 100.0,
|
||||
current_scan.minimum_elevation_m,
|
||||
current_scan.maximum_elevation_m,
|
||||
_renderer.visible_contact_count(),
|
||||
]
|
||||
|
||||
|
||||
func toggle_expanded() -> void:
|
||||
_expanded_panel.visible = not _expanded_panel.visible
|
||||
|
||||
|
||||
func is_expanded() -> bool:
|
||||
return _expanded_panel.visible
|
||||
|
||||
|
||||
func sonar_texture() -> Texture2D:
|
||||
return _renderer.render_texture()
|
||||
|
||||
|
||||
func built_row_count() -> int:
|
||||
return _renderer.built_row_count()
|
||||
|
||||
|
||||
func status_text() -> String:
|
||||
return _status_label.text
|
||||
|
||||
|
||||
func _state_text(state: SonarScanState.Value) -> String:
|
||||
match state:
|
||||
SonarScanState.Value.REVEALING:
|
||||
return "RELEVÉ EN COURS"
|
||||
SonarScanState.Value.RECOVERY:
|
||||
return "RÉCUPÉRATION"
|
||||
SonarScanState.Value.READY:
|
||||
return "RELEVÉ DISPONIBLE"
|
||||
return "CAPTURE"
|
||||
@@ -0,0 +1,80 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://game/presentation/sonar/sonar_display.gd" id="1_display"]
|
||||
[ext_resource type="Script" path="res://game/presentation/sonar/sonar_relief_renderer.gd" id="2_renderer"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="ExpandedStyle"]
|
||||
bg_color = Color(0.008, 0.02, 0.025, 0.98)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(0.18, 0.68, 0.7, 1)
|
||||
content_margin_left = 10.0
|
||||
content_margin_top = 10.0
|
||||
content_margin_right = 10.0
|
||||
content_margin_bottom = 10.0
|
||||
|
||||
[node name="SonarDisplay" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_display")
|
||||
|
||||
[node name="SonarReliefRenderer" type="SubViewport" parent="."]
|
||||
unique_name_in_owner = true
|
||||
size = Vector2i(960, 540)
|
||||
render_target_update_mode = 4
|
||||
script = ExtResource("2_renderer")
|
||||
|
||||
[node name="ExpandedPanel" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -530.0
|
||||
offset_top = -326.0
|
||||
offset_right = 530.0
|
||||
offset_bottom = 326.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme_override_styles/panel = SubResource("ExpandedStyle")
|
||||
|
||||
[node name="Layout" type="VBoxContainer" parent="ExpandedPanel"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Title" type="Label" parent="ExpandedPanel/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.42, 0.9, 0.91, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "SONAR BATHYMÉTRIQUE · CAP VERS LE HAUT"
|
||||
|
||||
[node name="ExpandedView" type="TextureRect" parent="ExpandedPanel/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(1038, 556)
|
||||
layout_mode = 2
|
||||
texture_filter = 1
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="ExpandedPanel/Layout"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.7, 0.82, 0.78, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
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"
|
||||
@@ -0,0 +1,295 @@
|
||||
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")
|
||||
|
||||
var _current_mesh_instance: MeshInstance3D
|
||||
var _previous_mesh_instance: MeshInstance3D
|
||||
var _contact_root: Node3D
|
||||
var _current_material: StandardMaterial3D
|
||||
var _previous_material: StandardMaterial3D
|
||||
var _current_scan_id: StringName = &""
|
||||
var _previous_scan_id: StringName = &""
|
||||
var _built_current_rows := 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
size = Vector2i(960, 540)
|
||||
render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
positional_shadow_atlas_size = 1024
|
||||
own_world_3d = true
|
||||
_build_render_world()
|
||||
|
||||
|
||||
func present(
|
||||
current_scan: SonarScan,
|
||||
previous_scan: SonarScan,
|
||||
visible_rows: int,
|
||||
previous_opacity: float
|
||||
) -> void:
|
||||
if current_scan == null:
|
||||
return
|
||||
if current_scan.scan_id != _current_scan_id:
|
||||
_current_scan_id = current_scan.scan_id
|
||||
_current_mesh_instance.mesh = ArrayMesh.new()
|
||||
_built_current_rows = 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)
|
||||
|
||||
if previous_scan == null:
|
||||
_previous_scan_id = &""
|
||||
_previous_mesh_instance.mesh = ArrayMesh.new()
|
||||
_previous_mesh_instance.visible = false
|
||||
else:
|
||||
_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_scan,
|
||||
_previous_material
|
||||
)
|
||||
_previous_material.albedo_color.a = clampf(previous_opacity, 0.0, 1.0)
|
||||
|
||||
|
||||
func render_texture() -> Texture2D:
|
||||
return get_texture()
|
||||
|
||||
|
||||
func built_row_count() -> int:
|
||||
return _built_current_rows
|
||||
|
||||
|
||||
func visible_contact_count() -> int:
|
||||
var count := 0
|
||||
for child: Node in _contact_root.get_children():
|
||||
if child is Node3D and (child as Node3D).visible:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
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_node.environment = environment
|
||||
add_child(environment_node)
|
||||
|
||||
_current_material = _relief_material(false)
|
||||
_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)
|
||||
_previous_mesh_instance = MeshInstance3D.new()
|
||||
_previous_mesh_instance.name = "PreviousRelief"
|
||||
_previous_mesh_instance.mesh = ArrayMesh.new()
|
||||
_previous_mesh_instance.visible = false
|
||||
add_child(_previous_mesh_instance)
|
||||
|
||||
_contact_root = Node3D.new()
|
||||
_contact_root.name = "Contacts"
|
||||
add_child(_contact_root)
|
||||
_add_submarine_marker()
|
||||
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "SonarCamera"
|
||||
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
||||
camera.size = 116.0
|
||||
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.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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
func _build_current_rows(scan: SonarScan, visible_rows: int) -> void:
|
||||
var mesh := _current_mesh_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_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)
|
||||
child.queue_free()
|
||||
for contact in scan.contacts:
|
||||
var marker := MeshInstance3D.new()
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(4.0, 2.25, 4.0)
|
||||
marker.mesh = box
|
||||
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 = COLOR_METAL
|
||||
material.emission_enabled = true
|
||||
material.emission = COLOR_METAL
|
||||
material.emission_energy_multiplier = 4.0
|
||||
marker.material_override = material
|
||||
_contact_root.add_child(marker)
|
||||
|
||||
|
||||
func _update_contact_visibility(scan: SonarScan, visible_rows: int) -> void:
|
||||
var maximum_visible_distance := -INF
|
||||
if visible_rows > 0:
|
||||
var row_ratio := float(visible_rows - 1) / float(scan.samples.rows - 1)
|
||||
maximum_visible_distance = lerpf(
|
||||
scan.minimum_range_m,
|
||||
scan.maximum_range_m,
|
||||
row_ratio
|
||||
)
|
||||
for child: Node in _contact_root.get_children():
|
||||
if child is Node3D:
|
||||
(child as Node3D).visible = (
|
||||
float(child.get_meta("forward_distance_m", INF))
|
||||
<= maximum_visible_distance
|
||||
)
|
||||
|
||||
|
||||
func _add_submarine_marker() -> void:
|
||||
var marker := MeshInstance3D.new()
|
||||
marker.name = "SubmarineMarker"
|
||||
var box := BoxMesh.new()
|
||||
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
|
||||
add_child(marker)
|
||||
Reference in new issue
Block a user