Files
Ransom-s-wake/game/domain/sites/terrain_height_field.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

139 lines
3.8 KiB
GDScript

class_name TerrainHeightField
extends RefCounted
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
var _data: SiteTerrainData
var _minimum_elevation_m := 0.0
var _maximum_elevation_m := 0.0
static func from_data(data: SiteTerrainData) -> TerrainHeightField:
if data == null or not data.validation_error().is_empty():
return null
var field := TerrainHeightField.new()
field._data = data
field._minimum_elevation_m = data.elevations_m[0]
field._maximum_elevation_m = data.elevations_m[0]
for elevation: float in data.elevations_m:
field._minimum_elevation_m = minf(field._minimum_elevation_m, elevation)
field._maximum_elevation_m = maxf(field._maximum_elevation_m, elevation)
return field
func columns() -> int:
return _data.columns
func rows() -> int:
return _data.rows
func cell_size_m() -> float:
return _data.cell_size_m
func origin_m() -> Vector2:
return Vector2(_data.origin_x_m, _data.origin_z_m)
func bounds_m() -> Rect2:
return Rect2(
_data.origin_x_m,
_data.origin_z_m,
float(_data.columns - 1) * _data.cell_size_m,
float(_data.rows - 1) * _data.cell_size_m
)
func contains(x_m: float, z_m: float) -> bool:
return bounds_m().has_point(Vector2(x_m, z_m))
func minimum_elevation_m() -> float:
return _minimum_elevation_m
func maximum_elevation_m() -> float:
return _maximum_elevation_m
## Row-major elevations (index = row * columns + column), the HeightMapShape3D layout.
func elevations_m() -> PackedFloat32Array:
return _data.elevations_m
func elevation_at_cell(column: int, row: int) -> float:
return _data.elevations_m[_data.index_of(_clamp_column(column), _clamp_row(row))]
func material_at_cell(column: int, row: int) -> int:
return _data.materials[_data.index_of(_clamp_column(column), _clamp_row(row))]
func cell_world_position(column: int, row: int) -> Vector3:
return Vector3(
_data.origin_x_m + float(column) * _data.cell_size_m,
elevation_at_cell(column, row),
_data.origin_z_m + float(row) * _data.cell_size_m
)
func normal_at_cell(column: int, row: int) -> Vector3:
var left := elevation_at_cell(column - 1, row)
var right := elevation_at_cell(column + 1, row)
var north := elevation_at_cell(column, row - 1)
var south := elevation_at_cell(column, row + 1)
var span_x := float(_clamp_column(column + 1) - _clamp_column(column - 1)) * _data.cell_size_m
var span_z := float(_clamp_row(row + 1) - _clamp_row(row - 1)) * _data.cell_size_m
var slope_x := (right - left) / span_x
var slope_z := (south - north) / span_z
return Vector3(-slope_x, 1.0, -slope_z).normalized()
func elevation_at(x_m: float, z_m: float) -> float:
var column_f := clampf(
(x_m - _data.origin_x_m) / _data.cell_size_m,
0.0,
float(_data.columns - 1)
)
var row_f := clampf(
(z_m - _data.origin_z_m) / _data.cell_size_m,
0.0,
float(_data.rows - 1)
)
var column := mini(int(floorf(column_f)), _data.columns - 2)
var row := mini(int(floorf(row_f)), _data.rows - 2)
var tx := column_f - float(column)
var tz := row_f - float(row)
var top := lerpf(elevation_at_cell(column, row), elevation_at_cell(column + 1, row), tx)
var bottom := lerpf(
elevation_at_cell(column, row + 1),
elevation_at_cell(column + 1, row + 1),
tx
)
return lerpf(top, bottom, tz)
func material_at(x_m: float, z_m: float) -> int:
var column := int(roundf((x_m - _data.origin_x_m) / _data.cell_size_m))
var row := int(roundf((z_m - _data.origin_z_m) / _data.cell_size_m))
return material_at_cell(column, row)
func reflectivity_for(material: int) -> float:
match material:
SonarMaterial.Value.SAND:
return _data.sand_reflectivity
_:
return _data.rock_reflectivity
func _clamp_column(column: int) -> int:
return clampi(column, 0, _data.columns - 1)
func _clamp_row(row: int) -> int:
return clampi(row, 0, _data.rows - 1)