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 )