Files
Ransom-s-wake/game/world/sites/bathymetric_terrain_actor.gd
T
OlivierandClaude Opus 5.5 682e1f18e5 perf(sites): validate the site once and hand collision the field array
DiveSite.configure() builds the height field once (TerrainHeightField
validates the data) instead of running the definition validation and
then the field construction again; the composition root relies on the
error returned by configure(). The collision shape takes the field's
row-major elevations directly instead of copying 263k cells one by one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:52:04 +02:00

131 lines
4.1 KiB
GDScript

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()
shape.map_data = _field.elevations_m()
_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)