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") ## Grid lines are drawn by the relief shader on the cell edges: every built ## cell carries one row edge and one column edge of the grid. const GRID_SEGMENTS_PER_CELL := 2 ## Two triangles per cell, wound like the sample grid. const CELL_CORNER_ORDER: Array[int] = [0, 2, 1, 1, 2, 3] static func build_full_mesh(scan: SonarScan, material: Material) -> ArrayMesh: var mesh := ArrayMesh.new() for row in range(1, scan.samples.rows): add_row_band(mesh, scan, row, material) return mesh ## Adds the cells between `row - 1` and `row`; returns the triangle count. ## Per vertex: UV = (column, row) of the sample grid, COLOR.r = height relative ## to the scan (0 low, 1 peak), COLOR.g = 1 on metal, COLOR.b = echo intensity. static func add_row_band( mesh: ArrayMesh, scan: SonarScan, row: int, material: Material ) -> int: var vertices := PackedVector3Array() var normals := PackedVector3Array() var colors := PackedColorArray() var uvs := PackedVector2Array() for column in range(scan.samples.columns - 1): var corners: Array[SonarSample] = [ scan.samples.sample_at(row - 1, column), scan.samples.sample_at(row - 1, column + 1), scan.samples.sample_at(row, column), scan.samples.sample_at(row, column + 1), ] if not ( corners[0].observed and corners[1].observed and corners[2].observed and corners[3].observed ): continue var grid: Array[Vector2] = [ Vector2(column, row - 1), Vector2(column + 1, row - 1), Vector2(column, row), Vector2(column + 1, row), ] for corner: int in CELL_CORNER_ORDER: var sample := corners[corner] vertices.append(sample.position_m) normals.append(sample.normal) colors.append(_sample_data(scan, sample)) uvs.append(grid[corner]) 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 arrays[Mesh.ARRAY_TEX_UV] = uvs 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 _sample_data(scan: SonarScan, sample: SonarSample) -> Color: var height := clampf( inverse_lerp( scan.minimum_elevation_m, scan.maximum_elevation_m, sample.elevation_m ), 0.0, 1.0 ) var metal := 1.0 if sample.material == SonarMaterial.Value.METAL else 0.0 return Color(height, metal, clampf(sample.intensity, 0.0, 1.0))