diff --git a/game/domain/sites/terrain_height_field.gd b/game/domain/sites/terrain_height_field.gd index 610e34a..28079df 100644 --- a/game/domain/sites/terrain_height_field.gd +++ b/game/domain/sites/terrain_height_field.gd @@ -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) diff --git a/game/world/sites/bathymetric_terrain_actor.gd b/game/world/sites/bathymetric_terrain_actor.gd index 874aee4..14222df 100644 --- a/game/world/sites/bathymetric_terrain_actor.gd +++ b/game/world/sites/bathymetric_terrain_actor.gd @@ -78,14 +78,12 @@ func _build_tile(start_column: int, end_column: int, start_row: int, end_row: in 0.0, (corner_min.z + corner_max.z) * 0.5 ) - var vertices := PackedVector3Array() - var normals := PackedVector3Array() + var vertices := _field.cell_block_positions(start_column, end_column, start_row, end_row) + 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 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 diff --git a/tests/unit/run_site_domain_tests.gd b/tests/unit/run_site_domain_tests.gd index de0fdbe..8d838a7 100644 --- a/tests/unit/run_site_domain_tests.gd +++ b/tests/unit/run_site_domain_tests.gd @@ -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 DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd") -const TEST_COUNT := 39 +const TEST_COUNT := 41 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(-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") + 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(is_equal_approx(field.reflectivity_for(SonarMaterial.Value.SAND), 0.45), "sand reflectivity comes from the data")