@@ -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