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:
1 parent
bce5c20dec
commit
ce3b75989e
3 files changed
+80
-16
No files matched your search
@@ -81,15 +81,50 @@ func cell_world_position(column: int, row: int) -> Vector3:
|
|||||||
|
|
||||||
|
|
||||||
func normal_at_cell(column: int, row: int) -> Vector3:
|
func normal_at_cell(column: int, row: int) -> Vector3:
|
||||||
var left := elevation_at_cell(column - 1, row)
|
return _normal_at(_data.elevations_m, _clamp_column(column), _clamp_row(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)
|
## World positions of the cells [start_column..end_column] x [start_row..end_row]
|
||||||
var span_x := float(_clamp_column(column + 1) - _clamp_column(column - 1)) * _data.cell_size_m
|
## (bounds included, clamped to the grid), row-major. Same values as cell_world_position.
|
||||||
var span_z := float(_clamp_row(row + 1) - _clamp_row(row - 1)) * _data.cell_size_m
|
func cell_block_positions(start_column: int, end_column: int, start_row: int, end_row: int) -> PackedVector3Array:
|
||||||
var slope_x := (right - left) / span_x
|
var first_column := _clamp_column(start_column)
|
||||||
var slope_z := (south - north) / span_z
|
var last_column := _clamp_column(end_column)
|
||||||
return Vector3(-slope_x, 1.0, -slope_z).normalized()
|
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:
|
func elevation_at(x_m: float, z_m: float) -> float:
|
||||||
@@ -130,6 +165,24 @@ func reflectivity_for(material: int) -> float:
|
|||||||
return _data.rock_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:
|
func _clamp_column(column: int) -> int:
|
||||||
return clampi(column, 0, _data.columns - 1)
|
return clampi(column, 0, _data.columns - 1)
|
||||||
|
|
||||||
|
|||||||
@@ -78,14 +78,12 @@ func _build_tile(start_column: int, end_column: int, start_row: int, end_row: in
|
|||||||
0.0,
|
0.0,
|
||||||
(corner_min.z + corner_max.z) * 0.5
|
(corner_min.z + corner_max.z) * 0.5
|
||||||
)
|
)
|
||||||
var vertices := PackedVector3Array()
|
var vertices := _field.cell_block_positions(start_column, end_column, start_row, end_row)
|
||||||
var normals := PackedVector3Array()
|
for index in vertices.size():
|
||||||
|
vertices[index] -= centre
|
||||||
|
var normals := _field.cell_block_normals(start_column, end_column, start_row, end_row)
|
||||||
var indices := PackedInt32Array()
|
var indices := PackedInt32Array()
|
||||||
var columns_in_tile := end_column - start_column + 1
|
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 row in range(end_row - start_row):
|
||||||
for column in range(end_column - start_column):
|
for column in range(end_column - start_column):
|
||||||
var top_left := row * columns_in_tile + column
|
var top_left := row * columns_in_tile + column
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
|||||||
const SiteRoute = preload("res://game/domain/sites/site_route.gd")
|
const SiteRoute = preload("res://game/domain/sites/site_route.gd")
|
||||||
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
||||||
|
|
||||||
const TEST_COUNT := 39
|
const TEST_COUNT := 41
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -97,6 +97,19 @@ func _test_height_field_sampling() -> void:
|
|||||||
_expect(field.material_at(-10.4, 100.0) == SonarMaterial.Value.ROCK, "material is the nearest cell")
|
_expect(field.material_at(-10.4, 100.0) == SonarMaterial.Value.ROCK, "material is the nearest cell")
|
||||||
_expect(field.material_at(-6.0, 103.0) == SonarMaterial.Value.SAND, "material of an interior cell")
|
_expect(field.material_at(-6.0, 103.0) == SonarMaterial.Value.SAND, "material of an interior cell")
|
||||||
_expect(field.normal_at_cell(1, 1).is_equal_approx(Vector3(0.0, 1.0, 1.0).normalized()), "normal leans against the downhill direction")
|
_expect(field.normal_at_cell(1, 1).is_equal_approx(Vector3(0.0, 1.0, 1.0).normalized()), "normal leans against the downhill direction")
|
||||||
|
var block_positions := field.cell_block_positions(1, 2, 0, 3)
|
||||||
|
var block_normals := field.cell_block_normals(1, 2, 0, 3)
|
||||||
|
var block_matches := block_positions.size() == 8 and block_normals.size() == 8
|
||||||
|
var normals_match := block_matches
|
||||||
|
for row in 4:
|
||||||
|
for column in range(1, 3):
|
||||||
|
var index := row * 2 + column - 1
|
||||||
|
if block_matches and not block_positions[index].is_equal_approx(field.cell_world_position(column, row)):
|
||||||
|
block_matches = false
|
||||||
|
if normals_match and not block_normals[index].is_equal_approx(field.normal_at_cell(column, row)):
|
||||||
|
normals_match = false
|
||||||
|
_expect(block_matches, "cell block positions match cell_world_position, row-major")
|
||||||
|
_expect(normals_match, "cell block normals match normal_at_cell, edges included")
|
||||||
_expect(not field.contains(-11.0, 100.0) and field.contains(-8.0, 103.0), "contains follows the bounds")
|
_expect(not field.contains(-11.0, 100.0) and field.contains(-8.0, 103.0), "contains follows the bounds")
|
||||||
_expect(is_equal_approx(field.reflectivity_for(SonarMaterial.Value.SAND), 0.45), "sand reflectivity comes from the data")
|
_expect(is_equal_approx(field.reflectivity_for(SonarMaterial.Value.SAND), 0.45), "sand reflectivity comes from the data")
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user