Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions rtxpy/tests/test_lod.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,25 @@ def test_boundary_shared_at_higher_subsample(self):
f"tile(0,1) min_x={min_x_01}"
)

def test_interior_tile_has_no_skirt(self):
"""Interior tiles (not at terrain edge) should have no skirt. #79"""
terrain = self._make_terrain(256, 256)
rtx = _FakeRTX()
mgr = TerrainLODManager(terrain, tile_size=64,
pixel_spacing_x=1.0, pixel_spacing_y=1.0)
mgr.update(np.array([128, 128, 0]), rtx, force=True)
# Tile (1,1) is fully interior — no edges touch terrain boundary.
# Its mesh should have no skirt (vertex count = grid verts only).
gid = _tile_gid(1, 1)
verts = rtx.geometries[gid][0]
# With +1 overlap and subsample=1, tile covers 65x65 grid
# (64 tile_size + 1 overlap). No skirt → exactly 65*65 verts.
n_verts = len(verts) // 3
assert n_verts == 65 * 65, (
f"Interior tile should have no skirt, got {n_verts} verts "
f"(expected {65 * 65})"
)

def test_get_stats(self):
terrain = self._make_terrain(128, 128)
mgr = TerrainLODManager(terrain, tile_size=64)
Expand Down Expand Up @@ -378,3 +397,33 @@ def test_skirt_z_below_min(self):
new_v, _ = _add_tile_skirt(verts, indices, H, W)
skirt_z = new_v[9 * 3 + 2::3] # z of skirt vertices
assert np.all(skirt_z < 10.0)

def test_no_edges_returns_unchanged(self):
"""edges=all False should return original mesh unchanged. #79"""
H, W = 3, 3
verts = np.zeros(9 * 3, dtype=np.float32)
indices = np.zeros(8 * 3, dtype=np.int32)
new_v, new_i = _add_tile_skirt(
verts, indices, H, W, edges=(False, False, False, False))
np.testing.assert_array_equal(new_v, verts)
np.testing.assert_array_equal(new_i, indices)

def test_partial_edges_fewer_wall_tris(self):
"""Activating only some edges should produce fewer wall tris. #79"""
H, W = 4, 4
n_verts = H * W
n_tris = (H - 1) * (W - 1) * 2
verts = np.zeros(n_verts * 3, dtype=np.float32)
indices = np.zeros(n_tris * 3, dtype=np.int32)
for h in range(H):
for w in range(W):
idx = (h * W + w) * 3
verts[idx] = float(w)
verts[idx + 1] = float(h)
verts[idx + 2] = float(h + w)

_, all_i = _add_tile_skirt(verts, indices, H, W)
_, partial_i = _add_tile_skirt(
verts, indices, H, W, edges=(True, False, False, False))
# Only top edge active → fewer wall triangles
assert len(partial_i) < len(all_i)
54 changes: 42 additions & 12 deletions rtxpy/viewer/terrain_lod.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,16 @@ def _build_tile_mesh(self, tr, tc, lod):
verts[0::3] = verts[0::3] * subsample * self._psx + c0 * self._psx
verts[1::3] = verts[1::3] * subsample * self._psy + r0 * self._psy

# Add edge skirt to hide T-junction cracks between LOD levels
verts, indices = _add_tile_skirt(verts, indices, th, tw)
# Only add skirt on exterior edges (terrain boundary).
# Interior edges shared with adjacent tiles via the +1 overlap
# don't need skirt — overlapping skirt walls cause artifacts.
edges = (
tr == 0, # top
tc == self._n_tile_cols - 1, # right
tr == self._n_tile_rows - 1, # bottom
tc == 0, # left
)
verts, indices = _add_tile_skirt(verts, indices, th, tw, edges=edges)

return verts, indices

Expand All @@ -272,13 +280,20 @@ def is_terrain_lod_gid(gid):
return gid.startswith('terrain_lod_r')


def _add_tile_skirt(vertices, indices, H, W, skirt_depth=None):
"""Add a thin skirt around tile edges.
def _add_tile_skirt(vertices, indices, H, W, skirt_depth=None,
edges=(True, True, True, True)):
"""Add a thin skirt around specified tile edges.

The skirt is deliberately small — just enough to cover gaps at
LOD boundaries. It uses the same algorithm as
``mesh.add_terrain_skirt`` but with a shallower default depth.
Parameters
----------
edges : tuple of bool
``(top, right, bottom, left)`` — which edges get skirt
geometry. Interior tile edges shared with adjacent tiles
should be False to avoid overlapping wall triangles.
"""
if not any(edges):
return vertices, indices

z_vals = vertices[2::3]
z_min = float(np.nanmin(z_vals))
z_max = float(np.nanmax(z_vals))
Expand All @@ -303,14 +318,29 @@ def _add_tile_skirt(vertices, indices, H, W, skirt_depth=None):
skirt_verts[1::3] = vertices[perim * 3 + 1]
skirt_verts[2::3] = skirt_z

idx = np.arange(n_perim, dtype=np.int32)
idx_next = np.roll(idx, -1)
top_a = perim
# Mask: only create wall triangles for active edges.
# Perimeter segments per edge: top W-1, right H-1, bottom W-1, left H-1.
edge_top, edge_right, edge_bottom, edge_left = edges
seg_mask = np.zeros(n_perim, dtype=bool)
off = 0
for active, count in [(edge_top, W - 1), (edge_right, H - 1),
(edge_bottom, W - 1), (edge_left, H - 1)]:
if active:
seg_mask[off:off + count] = True
off += count

active_segs = np.where(seg_mask)[0].astype(np.int32)
if len(active_segs) == 0:
return vertices, indices

idx_next = (active_segs + 1) % n_perim
top_a = perim[active_segs]
top_b = perim[idx_next]
bot_a = (n_orig + idx).astype(np.int32)
bot_a = (n_orig + active_segs).astype(np.int32)
bot_b = (n_orig + idx_next).astype(np.int32)

wall_tris = np.empty(n_perim * 6, dtype=np.int32)
n_active = len(active_segs)
wall_tris = np.empty(n_active * 6, dtype=np.int32)
wall_tris[0::6] = top_a
wall_tris[1::6] = bot_b
wall_tris[2::6] = top_b
Expand Down
Loading