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)