Building the 64 tiles of the Banc du Vigilant cost about 1.9 s through per-vertex cell_world_position/normal_at_cell calls, leaving the 3 s load budget with too little headroom (2.1-2.9 s measured, one 4.8 s outlier). TerrainHeightField now returns positions and normals of a cell block in one call, with normal_at_cell sharing the same formula; the tile build drops to about 0.5 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
5.8 KiB
GDScript
192 lines
5.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:
|
|
return _normal_at(_data.elevations_m, _clamp_column(column), _clamp_row(row))
|
|
|
|
|
|
## World positions of the cells [start_column..end_column] x [start_row..end_row]
|
|
## (bounds included, clamped to the grid), row-major. Same values as cell_world_position.
|
|
func cell_block_positions(start_column: int, end_column: int, start_row: int, end_row: int) -> PackedVector3Array:
|
|
var first_column := _clamp_column(start_column)
|
|
var last_column := _clamp_column(end_column)
|
|
var first_row := _clamp_row(start_row)
|
|
var last_row := _clamp_row(end_row)
|
|
var elevations := _data.elevations_m
|
|
var columns := _data.columns
|
|
var cell_size := _data.cell_size_m
|
|
var positions := PackedVector3Array()
|
|
positions.resize((last_column - first_column + 1) * (last_row - first_row + 1))
|
|
var index := 0
|
|
for row in range(first_row, last_row + 1):
|
|
var z := _data.origin_z_m + float(row) * cell_size
|
|
var row_start := row * columns
|
|
for column in range(first_column, last_column + 1):
|
|
positions[index] = Vector3(
|
|
_data.origin_x_m + float(column) * cell_size,
|
|
elevations[row_start + column],
|
|
z
|
|
)
|
|
index += 1
|
|
return positions
|
|
|
|
|
|
## Normals of the same cell block, row-major. Same values as normal_at_cell.
|
|
func cell_block_normals(start_column: int, end_column: int, start_row: int, end_row: int) -> PackedVector3Array:
|
|
var first_column := _clamp_column(start_column)
|
|
var last_column := _clamp_column(end_column)
|
|
var first_row := _clamp_row(start_row)
|
|
var last_row := _clamp_row(end_row)
|
|
var elevations := _data.elevations_m
|
|
var normals := PackedVector3Array()
|
|
normals.resize((last_column - first_column + 1) * (last_row - first_row + 1))
|
|
var index := 0
|
|
for row in range(first_row, last_row + 1):
|
|
for column in range(first_column, last_column + 1):
|
|
normals[index] = _normal_at(elevations, column, row)
|
|
index += 1
|
|
return normals
|
|
|
|
|
|
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
|
|
|
|
|
|
## Central differences, one-sided on the grid edges. column and row must be in range.
|
|
func _normal_at(elevations: PackedFloat32Array, column: int, row: int) -> Vector3:
|
|
var columns := _data.columns
|
|
var left := maxi(column - 1, 0)
|
|
var right := mini(column + 1, columns - 1)
|
|
var north := maxi(row - 1, 0)
|
|
var south := mini(row + 1, _data.rows - 1)
|
|
var slope_x := (
|
|
(elevations[row * columns + right] - elevations[row * columns + left])
|
|
/ (float(right - left) * _data.cell_size_m)
|
|
)
|
|
var slope_z := (
|
|
(elevations[south * columns + column] - elevations[north * columns + column])
|
|
/ (float(south - north) * _data.cell_size_m)
|
|
)
|
|
return Vector3(-slope_x, 1.0, -slope_z).normalized()
|
|
|
|
|
|
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)
|