feat(world): render steep tiles on a smooth bicubic surface, sampled in parallel
Tiles holding a slope over 35 degrees are meshed at two vertices per cell on a clamped Catmull-Rom surface that passes through every grid node, so cliffs and ledges lose the facets of the heightfield grid while the collision stays on the grid. The domain field exposes the smooth elevation and a separable, per-phase-weighted block sampler with linear edge extrapolation and a one-cell margin for continuous normals; the actor samples tiles on the worker thread pool and builds meshes afterwards. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7e50f8879
commit
122f3f5800
11 files changed
+314
-19
No files matched your search
@@ -19,7 +19,7 @@ const DIVE_SITE_SCENE = preload("res://game/world/sites/dive_site.tscn")
|
||||
const ZONE1_SITE_PATH := "res://content/sites/zone1_vigilant.tres"
|
||||
const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres"
|
||||
const GROOVE_PROBE_OFFSET_M := 25.0
|
||||
const TEST_COUNT := 41
|
||||
const TEST_COUNT := 44
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
@@ -109,12 +109,21 @@ func _test_actor_matches_field() -> void:
|
||||
tile_vertices[tile_indices[2]]
|
||||
)
|
||||
_expect(first_face.normal.y > 0.0, "tile triangles are wound clockwise from above (front face up)")
|
||||
# The first tile spans cells 0..64 on both axes, so vertex index = row * 65 + column.
|
||||
# The first tile holds the steep bump, so it is rendered subdivided; the second tile
|
||||
# (cells 64..80) is flat and stays on the grid.
|
||||
var subdivision := actor.tile_subdivision(0)
|
||||
_expect(subdivision == BathymetricTerrainActor.STEEP_TILE_SUBDIVISION, "the steep tile is subdivided (%d)" % subdivision)
|
||||
_expect(actor.tile_subdivision(1) == 1, "the flat tile keeps one vertex per cell")
|
||||
# The first tile spans cells 0..64 on both axes: vertex index = (row * n) * (64 n + 1) + column * n.
|
||||
var points_per_row := 64 * subdivision + 1
|
||||
var worst_vertex_error := 0.0
|
||||
for cell: Vector2i in [Vector2i(0, 0), Vector2i(10, 50), Vector2i(64, 64)]:
|
||||
var vertex := first_tile.global_transform * tile_vertices[cell.y * 65 + cell.x]
|
||||
var vertex := first_tile.global_transform * tile_vertices[(cell.y * subdivision) * points_per_row + cell.x * subdivision]
|
||||
worst_vertex_error = maxf(worst_vertex_error, vertex.distance_to(field.cell_world_position(cell.x, cell.y)))
|
||||
_expect(worst_vertex_error < 0.01, "tile vertices sit on their field cells (worst %.4f m)" % worst_vertex_error)
|
||||
_expect(worst_vertex_error < 0.01, "tile vertices on grid nodes sit on their field cells (worst %.4f m)" % worst_vertex_error)
|
||||
var mid_vertex := first_tile.global_transform * tile_vertices[(50 * subdivision + 1) * points_per_row + 10 * subdivision + 1]
|
||||
var mid_expected := field.smooth_elevation_at(mid_vertex.x, mid_vertex.z)
|
||||
_expect(absf(mid_vertex.y - mid_expected) < 0.01, "subdivided vertices sit on the smooth surface (%.3f vs %.3f)" % [mid_vertex.y, mid_expected])
|
||||
var space := actor.get_world_3d().direct_space_state
|
||||
var worst_error := 0.0
|
||||
for cell: Vector2i in [
|
||||
|
||||
@@ -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 := 41
|
||||
const TEST_COUNT := 46
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
@@ -97,6 +97,38 @@ 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")
|
||||
_expect(is_equal_approx(field.smooth_elevation_at(-10.0, 100.0), field.elevation_at(-10.0, 100.0)), "smooth elevation passes through the grid nodes")
|
||||
var plane_data := SiteTerrainData.new()
|
||||
plane_data.columns = 7
|
||||
plane_data.rows = 6
|
||||
plane_data.cell_size_m = 2.0
|
||||
plane_data.origin_x_m = 0.0
|
||||
plane_data.origin_z_m = 0.0
|
||||
var plane_elevations := PackedFloat32Array()
|
||||
var plane_materials := PackedByteArray()
|
||||
for row in 6:
|
||||
for column in 7:
|
||||
plane_elevations.append(-20.0 + 0.5 * float(column) * 2.0 - 0.25 * float(row) * 2.0)
|
||||
plane_materials.append(SonarMaterial.Value.SAND)
|
||||
plane_data.elevations_m = plane_elevations
|
||||
plane_data.materials = plane_materials
|
||||
var plane := TerrainHeightField.from_data(plane_data)
|
||||
_expect(
|
||||
is_equal_approx(plane.smooth_elevation_at(5.0, 3.0), -20.0 + 2.5 - 0.75)
|
||||
and is_equal_approx(plane.smooth_elevation_at(0.5, 0.5), -20.0 + 0.25 - 0.125),
|
||||
"smooth elevation reproduces a plane between nodes, edges included"
|
||||
)
|
||||
var fine := field.cell_block_positions_subdivided(1, 2, 0, 3, 2)
|
||||
var fine_normals := field.cell_block_normals_subdivided(1, 2, 0, 3, 2)
|
||||
_expect(fine.size() == 3 * 7 and fine_normals.size() == 3 * 7, "a subdivided block has cells * 2 + 1 points per axis")
|
||||
var nodes_match := fine.size() == 21
|
||||
for row in 4:
|
||||
for column in range(1, 3):
|
||||
var index := (row * 2) * 3 + (column - 1) * 2
|
||||
if nodes_match and not fine[index].is_equal_approx(field.cell_world_position(column, row)):
|
||||
nodes_match = false
|
||||
_expect(nodes_match, "subdivided block points on the grid nodes equal the cell positions")
|
||||
_expect(fine_normals[0].is_normalized() and fine_normals[10].y > 0.0, "subdivided normals are unit length and point up")
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user