Skip to content

Commit c40dfdc

Browse files
authored
Merge pull request #84 from makepath/issue-79-skirt
Skip skirt on interior LOD tile edges
2 parents 1141979 + 8dcd1a3 commit c40dfdc

File tree

2 files changed

+91
-12
lines changed

2 files changed

+91
-12
lines changed

rtxpy/tests/test_lod.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,25 @@ def test_boundary_shared_at_higher_subsample(self):
316316
f"tile(0,1) min_x={min_x_01}"
317317
)
318318

319+
def test_interior_tile_has_no_skirt(self):
320+
"""Interior tiles (not at terrain edge) should have no skirt. #79"""
321+
terrain = self._make_terrain(256, 256)
322+
rtx = _FakeRTX()
323+
mgr = TerrainLODManager(terrain, tile_size=64,
324+
pixel_spacing_x=1.0, pixel_spacing_y=1.0)
325+
mgr.update(np.array([128, 128, 0]), rtx, force=True)
326+
# Tile (1,1) is fully interior — no edges touch terrain boundary.
327+
# Its mesh should have no skirt (vertex count = grid verts only).
328+
gid = _tile_gid(1, 1)
329+
verts = rtx.geometries[gid][0]
330+
# With +1 overlap and subsample=1, tile covers 65x65 grid
331+
# (64 tile_size + 1 overlap). No skirt → exactly 65*65 verts.
332+
n_verts = len(verts) // 3
333+
assert n_verts == 65 * 65, (
334+
f"Interior tile should have no skirt, got {n_verts} verts "
335+
f"(expected {65 * 65})"
336+
)
337+
319338
def test_get_stats(self):
320339
terrain = self._make_terrain(128, 128)
321340
mgr = TerrainLODManager(terrain, tile_size=64)
@@ -378,3 +397,33 @@ def test_skirt_z_below_min(self):
378397
new_v, _ = _add_tile_skirt(verts, indices, H, W)
379398
skirt_z = new_v[9 * 3 + 2::3] # z of skirt vertices
380399
assert np.all(skirt_z < 10.0)
400+
401+
def test_no_edges_returns_unchanged(self):
402+
"""edges=all False should return original mesh unchanged. #79"""
403+
H, W = 3, 3
404+
verts = np.zeros(9 * 3, dtype=np.float32)
405+
indices = np.zeros(8 * 3, dtype=np.int32)
406+
new_v, new_i = _add_tile_skirt(
407+
verts, indices, H, W, edges=(False, False, False, False))
408+
np.testing.assert_array_equal(new_v, verts)
409+
np.testing.assert_array_equal(new_i, indices)
410+
411+
def test_partial_edges_fewer_wall_tris(self):
412+
"""Activating only some edges should produce fewer wall tris. #79"""
413+
H, W = 4, 4
414+
n_verts = H * W
415+
n_tris = (H - 1) * (W - 1) * 2
416+
verts = np.zeros(n_verts * 3, dtype=np.float32)
417+
indices = np.zeros(n_tris * 3, dtype=np.int32)
418+
for h in range(H):
419+
for w in range(W):
420+
idx = (h * W + w) * 3
421+
verts[idx] = float(w)
422+
verts[idx + 1] = float(h)
423+
verts[idx + 2] = float(h + w)
424+
425+
_, all_i = _add_tile_skirt(verts, indices, H, W)
426+
_, partial_i = _add_tile_skirt(
427+
verts, indices, H, W, edges=(True, False, False, False))
428+
# Only top edge active → fewer wall triangles
429+
assert len(partial_i) < len(all_i)

rtxpy/viewer/terrain_lod.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,16 @@ def _build_tile_mesh(self, tr, tc, lod):
252252
verts[0::3] = verts[0::3] * subsample * self._psx + c0 * self._psx
253253
verts[1::3] = verts[1::3] * subsample * self._psy + r0 * self._psy
254254

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

258266
return verts, indices
259267

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

274282

275-
def _add_tile_skirt(vertices, indices, H, W, skirt_depth=None):
276-
"""Add a thin skirt around tile edges.
283+
def _add_tile_skirt(vertices, indices, H, W, skirt_depth=None,
284+
edges=(True, True, True, True)):
285+
"""Add a thin skirt around specified tile edges.
277286
278-
The skirt is deliberately small — just enough to cover gaps at
279-
LOD boundaries. It uses the same algorithm as
280-
``mesh.add_terrain_skirt`` but with a shallower default depth.
287+
Parameters
288+
----------
289+
edges : tuple of bool
290+
``(top, right, bottom, left)`` — which edges get skirt
291+
geometry. Interior tile edges shared with adjacent tiles
292+
should be False to avoid overlapping wall triangles.
281293
"""
294+
if not any(edges):
295+
return vertices, indices
296+
282297
z_vals = vertices[2::3]
283298
z_min = float(np.nanmin(z_vals))
284299
z_max = float(np.nanmax(z_vals))
@@ -303,14 +318,29 @@ def _add_tile_skirt(vertices, indices, H, W, skirt_depth=None):
303318
skirt_verts[1::3] = vertices[perim * 3 + 1]
304319
skirt_verts[2::3] = skirt_z
305320

306-
idx = np.arange(n_perim, dtype=np.int32)
307-
idx_next = np.roll(idx, -1)
308-
top_a = perim
321+
# Mask: only create wall triangles for active edges.
322+
# Perimeter segments per edge: top W-1, right H-1, bottom W-1, left H-1.
323+
edge_top, edge_right, edge_bottom, edge_left = edges
324+
seg_mask = np.zeros(n_perim, dtype=bool)
325+
off = 0
326+
for active, count in [(edge_top, W - 1), (edge_right, H - 1),
327+
(edge_bottom, W - 1), (edge_left, H - 1)]:
328+
if active:
329+
seg_mask[off:off + count] = True
330+
off += count
331+
332+
active_segs = np.where(seg_mask)[0].astype(np.int32)
333+
if len(active_segs) == 0:
334+
return vertices, indices
335+
336+
idx_next = (active_segs + 1) % n_perim
337+
top_a = perim[active_segs]
309338
top_b = perim[idx_next]
310-
bot_a = (n_orig + idx).astype(np.int32)
339+
bot_a = (n_orig + active_segs).astype(np.int32)
311340
bot_b = (n_orig + idx_next).astype(np.int32)
312341

313-
wall_tris = np.empty(n_perim * 6, dtype=np.int32)
342+
n_active = len(active_segs)
343+
wall_tris = np.empty(n_active * 6, dtype=np.int32)
314344
wall_tris[0::6] = top_a
315345
wall_tris[1::6] = bot_b
316346
wall_tris[2::6] = top_b

0 commit comments

Comments
 (0)