perf(sites): build terrain tiles from bulk field blocks

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>
This commit is contained in:
OlivierandClaude Opus 5.5 committed 2026-10-06 20:55:35 +02:00
1 parent bce5c20dec
commit ce3b75989e
3 files changed
+80 -16

No files matched your search

+62 -9
View File
@@ -81,15 +81,50 @@ func cell_world_position(column: int, row: int) -> Vector3:
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()
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:
@@ -130,6 +165,24 @@ func reflectivity_for(material: int) -> float:
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)