class_name BathymetricTerrainActor extends StaticBody3D const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd") const TERRAIN_SHADER = preload("res://game/world/sites/seabed_terrain.gdshader") const TILE_CELLS := 64 const VISIBILITY_RANGE_END_M := 260.0 const VISIBILITY_RANGE_MARGIN_M := 40.0 var _field: TerrainHeightField var _material: ShaderMaterial var _tiles_root: Node3D var _collision: CollisionShape3D func configure(field: TerrainHeightField) -> String: if field == null: return "terrain field is missing" _clear() _field = field _material = ShaderMaterial.new() _material.shader = TERRAIN_SHADER _build_tiles() _build_collision() return "" func height_field() -> TerrainHeightField: return _field func tile_count() -> int: return 0 if _tiles_root == null else _tiles_root.get_child_count() func sonar_material_at(world_position: Vector3) -> int: return _field.material_at(world_position.x, world_position.z) func sonar_reflectivity_at(world_position: Vector3) -> float: return _field.reflectivity_for(sonar_material_at(world_position)) func _clear() -> void: if _tiles_root != null: remove_child(_tiles_root) _tiles_root.free() _tiles_root = null if _collision != null: remove_child(_collision) _collision.free() _collision = null func _build_tiles() -> void: _tiles_root = Node3D.new() _tiles_root.name = "Tiles" add_child(_tiles_root) var last_column := _field.columns() - 1 var last_row := _field.rows() - 1 var start_row := 0 while start_row < last_row: var end_row := mini(start_row + TILE_CELLS, last_row) var start_column := 0 while start_column < last_column: var end_column := mini(start_column + TILE_CELLS, last_column) _tiles_root.add_child(_build_tile(start_column, end_column, start_row, end_row)) start_column = end_column start_row = end_row func _build_tile(start_column: int, end_column: int, start_row: int, end_row: int) -> MeshInstance3D: var corner_min := _field.cell_world_position(start_column, start_row) var corner_max := _field.cell_world_position(end_column, end_row) var centre := Vector3( (corner_min.x + corner_max.x) * 0.5, 0.0, (corner_min.z + corner_max.z) * 0.5 ) var vertices := PackedVector3Array() var normals := PackedVector3Array() var indices := PackedInt32Array() var columns_in_tile := end_column - start_column + 1 for row in range(start_row, end_row + 1): for column in range(start_column, end_column + 1): vertices.append(_field.cell_world_position(column, row) - centre) normals.append(_field.normal_at_cell(column, row)) for row in range(end_row - start_row): for column in range(end_column - start_column): var top_left := row * columns_in_tile + column var top_right := top_left + 1 var bottom_left := top_left + columns_in_tile var bottom_right := bottom_left + 1 indices.append_array([top_left, top_right, bottom_left]) indices.append_array([top_right, bottom_right, bottom_left]) var arrays: Array = [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = vertices arrays[Mesh.ARRAY_NORMAL] = normals arrays[Mesh.ARRAY_INDEX] = indices var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) var instance := MeshInstance3D.new() instance.name = "Tile_%d_%d" % [start_column / TILE_CELLS, start_row / TILE_CELLS] instance.mesh = mesh instance.material_override = _material instance.position = centre instance.visibility_range_end = VISIBILITY_RANGE_END_M instance.visibility_range_end_margin = VISIBILITY_RANGE_MARGIN_M instance.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF return instance func _build_collision() -> void: var shape := HeightMapShape3D.new() shape.map_width = _field.columns() shape.map_depth = _field.rows() var heights := PackedFloat32Array() heights.resize(_field.columns() * _field.rows()) for row in _field.rows(): for column in _field.columns(): heights[row * _field.columns() + column] = _field.elevation_at_cell(column, row) shape.map_data = heights _collision = CollisionShape3D.new() _collision.name = "Collision" _collision.shape = shape var bounds := _field.bounds_m() _collision.position = Vector3( bounds.position.x + bounds.size.x * 0.5, 0.0, bounds.position.y + bounds.size.y * 0.5 ) _collision.scale = Vector3(_field.cell_size_m(), 1.0, _field.cell_size_m()) add_child(_collision)