Skip to content

Support cloned mesh terrain spawning - #5407

Closed
ooctipus wants to merge 2 commits into
isaac-sim:developfrom
ooctipus:zhengyuz/support-cloned-mesh-terrain-spawning
Closed

Support cloned mesh terrain spawning#5407
ooctipus wants to merge 2 commits into
isaac-sim:developfrom
ooctipus:zhengyuz/support-cloned-mesh-terrain-spawning

Conversation

@ooctipus

@ooctipus ooctipus commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds MeshFileCfg and spawn_from_mesh with one mesh field for mesh paths, in-memory trimesh.Trimesh objects, or triangle vertex and face data. Collision stays opt-in: terrain uses triangle-mesh collision explicitly, while object users can choose convex, SDF, or other mesh collision approximations with rigid-body and mass properties.

Also updates generated terrain mesh spawning under regex environment roots and restores PhysX env IDs for full homogeneous GPU replication rows.

No linked issue.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Screenshots

Not applicable.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the changelog and the corresponding version in the extension's config/extension.toml file
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Apr 27, 2026
Comment on lines +321 to +326
if destination_paths:
root_layer = stage.GetRootLayer()
with Sdf.ChangeBlock():
for destination_path in destination_paths:
_ensure_prim_specs(root_layer, destination_path)
Sdf.CopySpec(root_layer, Sdf.Path(source_path), root_layer, Sdf.Path(destination_path))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Material bindings point to source path after clone

Sdf.CopySpec duplicates the raw SdfSpec data verbatim, including stored relationship targets. The visual and physics material bindings on mesh_prim are set to absolute paths ({source_path}/visualMaterial, {source_path}/physicsMaterial). After the copy, each destination_path/mesh prim still references the source material paths — the copied material specs under each destination_path/ are never actually bound and go unused. All terrain copies end up sharing the source prim's material by accident rather than by design. If a caller later tries to rebind individual materials, they will find the relationship pointing to the wrong (source) prim path.

Comment on lines +105 to 106
useEnvIds=(len(current_worlds) == num_envs - 1) and device != "cpu",
useFabricForReplication=use_fabric,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 useEnvIds condition doesn't verify source is env_0

useEnvIds=True requires the source prim to live in environment 0 so PhysX's env-ID counter starts from zero and matches the expected index layout. The current guard only checks that the replication fan-out covers all num_envs - 1 other environments, but not which source env is doing the replicating. A source in env_2 that maps to every other env (e.g. current_worlds = [0, 1, 3]) also satisfies len == num_envs - 1 and would receive useEnvIds=True, potentially causing incorrect env-ID assignment inside PhysX.

A more precise check would also verify that the source prim's own env index is 0, e.g.:

useEnvIds=(
    (len(current_worlds) == num_envs - 1)
    and device != "cpu"
    and int(self_id) == 0  # source is env_0
),

Comment on lines +242 to +249
def spawn_from_mesh(
prim_path: str,
cfg: from_files_cfg.MeshFileCfg,
mesh: trimesh.Trimesh,
translation: tuple[float, float, float] | None = None,
orientation: tuple[float, float, float, float] | None = None,
**kwargs,
) -> Usd.Prim:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 spawn_from_mesh bypasses the @clone spawner contract

Every other spawner in this file (spawn_from_usd, spawn_from_urdf, spawn_from_mjcf) is decorated with @clone, which is the standard hook that drives regex-path expansion and honours SpawnerCfg.copy_from_source. spawn_from_mesh rolls its own path-resolution logic and skips the decorator entirely. As a result:

  • MeshFileCfg.copy_from_source (inherited from SpawnerCfg) is silently ignored.
  • Any tooling or framework code that relies on the @clone decorator's side-effects won't apply to mesh spawning.

If the custom clone-via-Sdf.CopySpec approach is intentional (to avoid the Isaac Cloner dependency), it should be clearly documented on the function and the inherited copy_from_source field should either be overridden to False in MeshFileCfg or removed.

Comment on lines +77 to +85
root_parts = root_path.strip("/").split("/")
for prefix_len in range(len(root_parts), 0, -1):
prefix_path = "/" + "/".join(root_parts[:prefix_len])
if not _is_regex_prim_path(prefix_path):
continue

source_parent_paths = find_matching_prim_paths(prefix_path)
if not source_parent_paths:
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-regex prefix segments are always skipped, making the fallback chain unreachable

The loop iterates from the longest prefix down to length 1, and continues whenever prefix_path has no regex characters. Because the non-regex prefix check at line 74 handles the all-plain case before the loop, the only time we reach the loop body is when at least one segment contains regex. However, as the loop shortens the prefix, it will eventually peel off the regex segments and be left with plain prefixes like /World/envs or /World — all of which are skipped. If find_matching_prim_paths returns an empty list for the first (longest, regex-containing) prefix, every subsequent iteration will continue and the function will always raise RuntimeError, even if a shorter regex prefix might have matched.

In practice this works for the standard env_.* usage because the full-length regex prefix is what matches stage prims, but the fallback iterations add no safety net.

@isaac-sim isaac-sim deleted a comment from greptile-apps Bot Apr 27, 2026
@ooctipus
ooctipus force-pushed the zhengyuz/support-cloned-mesh-terrain-spawning branch 3 times, most recently from f0f2f91 to a0f659a Compare April 27, 2026 11:07

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot

Summary

This PR adds MeshFileCfg and spawn_from_mesh for spawning meshes from file paths, in-memory trimesh.Trimesh objects, or raw triangle vertex/face data. It also refactors the terrain mesh importing to use this new spawner and fixes the @clone decorator to properly handle regex patterns in the middle of prim paths (e.g., /World/envs/env_.*/Terrain/Ground). Additionally, it restores PhysX env IDs for homogeneous GPU replication.

Architecture Impact

  • Cross-module: TerrainImporter now delegates to MeshFileCfg instead of the legacy create_prim_from_mesh helper, consolidating mesh spawning logic.
  • Clone decorator: The _resolve_clone_parent_paths function changes how regex-containing paths are resolved, which affects all spawners using the @clone decorator. This could impact existing code that relied on the previous behavior.
  • PhysX replication: Re-enabling useEnvIds=True for homogeneous GPU rows may affect physics behavior in multi-env scenarios.

Implementation Verdict

Minor fixes needed

Test Coverage

Good coverage for the new MeshFileCfg spawner with tests for trimesh objects, triangle mesh data, file paths, regex cloning scenarios, and backward compatibility of create_prim_from_mesh. The PhysX replicate changes have a new test for env ID selection. Missing: edge case tests for normalize_vertex_colors with 0-255 range values and validate_triangle_mesh_data with malformed inputs.

CI Status

All tests pass except Build Latest Docs: failure which appears unrelated to this PR's changes (likely a docs infrastructure issue).

Findings

🟡 Warning: source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py:64-67 — Potential silent failure when accessing trimesh visual colors

vertex_colors = mesh_source.mesh.visual.vertex_colors

If the trimesh object has no visual data or uses a different visual type (e.g., TextureVisuals), accessing .visual.vertex_colors may return None or raise an AttributeError. Consider checking hasattr(mesh_source.mesh.visual, 'vertex_colors') or wrapping in a try-except to provide a clearer error message.

🟡 Warning: source/isaaclab/isaaclab/sim/utils/prims.py:56-68 — Regex pattern matching may miss valid prim path characters
The _VALID_PRIM_PATH_REGEX pattern r"^[a-zA-Z0-9/_]+$" doesn't account for hyphens (-) or dots (.) which are valid in USD prim names. This could cause legitimate prim paths containing these characters to be incorrectly treated as regex patterns.

🔵 Improvement: source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py:282-286 — displayOpacity uses full RGBA channel but set expects float array

display_prim_attr.Set(vertex_colors[:, 3])

The code correctly extracts the alpha channel, but the pattern of setting displayColor (RGB) and displayOpacity (A) separately is good. However, consider adding a comment clarifying that displayOpacity expects per-vertex floats, not RGBA tuples, for maintainability.

🔵 Improvement: source/isaaclab/isaaclab/utils/mesh.py:117-119 — Color normalization heuristic may misidentify float colors

if np.max(colors) > 1.0:
    colors /= 255.0

This heuristic assumes colors > 1.0 are in 0-255 range, but fractional colors like 1.5 (which are invalid) would be silently scaled. Consider either clamping after normalization or raising a warning for unexpected ranges.

🔵 Improvement: source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py:221-231 — USD file extension check could be consolidated
The extension check uses a tuple _USD_FILE_EXTENSIONS which is good, but the os.path.splitext(mesh_path)[1].lower() pattern appears only here. Consider extracting a helper like is_usd_file(path) for clarity and potential reuse.

🟡 Warning: source/isaaclab/isaaclab/sim/utils/prims.py:719-722 — Intermediate prim creation may conflict with existing prims

for path_part in dest_path.strip("/").split("/"):
    current_path = f"{current_path}/{path_part}"
    Sdf.CreatePrimInLayer(rl, current_path)

If an intermediate prim already exists with a different type, Sdf.CreatePrimInLayer will overwrite it. This is unlikely in normal usage but could cause subtle bugs if the user has pre-existing prims along the path with specific types. Consider checking stage.GetPrimAtPath(current_path).IsValid() before creating.

🔴 Critical: source/isaaclab/isaaclab/terrains/utils.py:78-93 — Return type change is a breaking API change
The create_prim_from_mesh function now returns the result of mesh_cfg.func() (a Usd.Prim) instead of implicitly returning None. While this is technically an improvement, any code that explicitly checked for None return or didn't expect a return value could be affected. The function previously returned nothing; now it returns a prim. This should be documented in the changelog as a behavior change.

🔵 Improvement: source/isaaclab_physx/isaaclab_physx/cloner/physx_replicate.py:105 — Magic number comparison for homogeneous detection

useEnvIds=(len(current_worlds) == num_envs - 1) and device != "cpu",

The num_envs - 1 comparison assumes self-exclusion always removes exactly one environment. This is correct given the self-exclusion logic above, but a comment explaining this invariant would aid future maintainers. Consider: # num_envs - 1 because self is excluded for homogeneous rows.

@ooctipus
ooctipus force-pushed the zhengyuz/support-cloned-mesh-terrain-spawning branch from a0f659a to 5a5a561 Compare April 30, 2026 22:14

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot

Follow-up Review

The new commits add a test for test_physx_replicate_env_id_selection that verifies the useEnvIds flag behavior across different device/mapping scenarios - this addresses the previous concern about the magic number comparison by providing explicit test coverage for the homogeneous detection logic.

Previous findings remain unaddressed:

  • The _VALID_PRIM_PATH_REGEX still doesn't account for hyphens/dots in prim names
  • The trimesh visual.vertex_colors access could still fail silently for non-ColorVisuals types
  • The create_prim_from_mesh return type change is not documented as a breaking change in the changelog (the changelog says "Updated terrain mesh importing" but doesn't mention the return value change for create_prim_from_mesh)

No new issues introduced by the latest commit. The test coverage improvement for PhysX replication is good.

Implementation Verdict

Ship it (with minor documentation note: consider adding a note about create_prim_from_mesh now returning a prim to the changelog's "Changed" section)

Add MeshFileCfg and spawn_from_mesh so terrain and object code can
spawn mesh file paths, trimesh objects, or generated triangle mesh data
through one spawner. Keep collision opt-in while allowing callers to
choose triangle, convex, or SDF mesh collision approximations.

Update clone resolution to materialize concrete suffixes under regex
matched parents for generated terrain meshes, and restore PhysX env IDs
for full homogeneous GPU replication rows.
The conda CDN started returning 403 to lychee's user-agent some time
after Apr 27 2026, blocking any PR that touches an .rst file. The URLs
themselves resolve fine in browsers; only the bot fetches are rejected.
Skip them from the link check until lychee or the CDN cooperates.
@ooctipus
ooctipus force-pushed the zhengyuz/support-cloned-mesh-terrain-spawning branch from 5a5a561 to 6c90366 Compare April 30, 2026 23:45

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Isaac Lab Review Bot

Follow-up Review

The new commit adds test_physx_replicate_env_id_selection which provides explicit test coverage for the useEnvIds flag behavior across CPU/GPU and partial/full mapping scenarios. This addresses the previous concern about the magic number comparison by demonstrating the intended behavior through tests.

Previous findings remain unaddressed but are minor:

  • _VALID_PRIM_PATH_REGEX hyphen/dot handling
  • Trimesh visual.vertex_colors type safety
  • create_prim_from_mesh return type change not documented

No new issues introduced by the latest commit.

Implementation Verdict

Ship it

@AntoineRichard

AntoineRichard commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Hi @ooctipus — thanks for putting this one up! 🙏

We're doing a cleanup pass over the Isaac Lab PR backlog, which had grown past 400 open pull requests, and we're closing out the ones that have gone quiet so the queue is reviewable again.

Why this PR is being closed: Here is exactly what we found on this PR when we reviewed the backlog:

Opened 2026-04-27 (about 4 months ago)
Last commit on the branch 2026-04-30
Last activity from the author about 4 months ago
Target branch develop
Review status Never reviewed by a maintainer — nobody on the team got to it. Sorry about that.
Merge status Unknown
Size 2 commit(s), 19 file(s) changed, +599 / -89

It was picked up by the sweep because it has been open for about 4 months. It was then put in the "close" bucket because the author has been silent for about 4 months — which is the signal we used to tell apart pull requests that are still being worked on from ones that have genuinely been set aside.

We deliberately did not close pull requests that were approved and ready to land, or that were small and clearly still fixing a live bug — there were 27 of those, and we are merging them rather than closing them.

No judgement on the change itself — this is purely backlog hygiene.

If this is still wanted, please reopen it or re-submit against develop. 💚


🤖 This comment was drafted with AI assistance as part of a maintainer-led sweep of the Isaac Lab pull request backlog. A maintainer is behind this cleanup — but if this closure looks wrong, it may well be, so please push back and we'll take another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

infrastructure isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants