Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b1f5fb9
Move Newton deformable ownership out of contrib
ooctipus Sep 25, 2026
5951af4
Remove obsolete contrib deformable imports
ooctipus Sep 25, 2026
d0ef66d
Export the Newton deformable builder hook
ooctipus Sep 25, 2026
a31081d
Move deformable builder preparation into the Newton cloner
ooctipus Sep 25, 2026
ff57eac
Merge develop and preserve the coupling test cleanup
ooctipus Sep 25, 2026
febd05d
Import declared deformables through the Newton clone lifecycle
ooctipus Sep 25, 2026
7d9a42f
Merge commit '49bd35fc265789f85283ded3909289b51c7f4b8a' into refactor…
ooctipus Sep 25, 2026
8258f01
Bind native geometry and poses from clone declarations
ooctipus Sep 25, 2026
bbc37dc
Merge branch 'develop' into refactor/pr5c-newton-deformable
ooctipus Sep 25, 2026
3ef0ca5
Merge branch 'develop' into refactor/pr5c-newton-deformable
ooctipus Sep 25, 2026
41ff554
Preserve tendon target references and declare CI fixture assets
ooctipus Sep 25, 2026
7bdc531
Select CUDA capture mode from Kit runtime presence
ooctipus Sep 25, 2026
cb8e3f8
Preserve headless physics capture without rendering warmup
ooctipus Sep 25, 2026
c7b90b2
Merge branch 'develop' into refactor/pr5c-newton-deformable
ooctipus Sep 25, 2026
238a2a1
Scope deformable defaults to selected particles
ooctipus Sep 25, 2026
7746f95
Name clone-plan mappings by source indices
ooctipus Sep 25, 2026
017acff
Merge develop and align deformable imports with prototype topology
ooctipus Sep 27, 2026
541a5e9
Remove redundant scene binding and clone bookkeeping
ooctipus Sep 27, 2026
73b28fb
Preserve sensor bodies and canonical native articulation paths
ooctipus Sep 27, 2026
a47ddd8
Name native copies without mutating source builders
ooctipus Sep 27, 2026
d78a214
Test OVPhysX publication readiness instead of warmup internals
ooctipus Sep 27, 2026
fd302b3
Inline deformable prototype point transforms
ooctipus Sep 27, 2026
8e2b841
Import render deformables before native replication
ooctipus Sep 27, 2026
f528106
Avoid loading GPU PhysX scenes twice
ooctipus Sep 27, 2026
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
1 change: 0 additions & 1 deletion docs/source/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ The following modules are available in the ``isaaclab_contrib`` extension:
controllers
coupling
custom_coupling
deformable
mdp
rl
sensors
Expand Down
25 changes: 0 additions & 25 deletions docs/source/api/lab_contrib/isaaclab_contrib.deformable.rst

This file was deleted.

2 changes: 1 addition & 1 deletion docs/source/concepts/deformables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use a separate asset and solver; see :doc:`/source/concepts/using_mpm`.

.. note::
All three kinds are under active development. On Newton, deformables are implemented in
:mod:`isaaclab_contrib.deformable` and re-exported through :mod:`isaaclab_newton.assets`.
:mod:`isaaclab_newton.assets`, alongside rigid objects and articulations.
Cable support in particular is experimental: its spawner cfg, asset class, and material
defaults may still change.

Expand Down
10 changes: 4 additions & 6 deletions docs/source/developer-tools/extending_newton_solvers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,12 @@ class invokes them.
- Stores the simulation context, reads gravity from the simulation
configuration, and creates the scene data backend.
- none
* - :meth:`~isaaclab_newton.physics.NewtonManager.create_builder`,
:meth:`~isaaclab_newton.physics.NewtonManager.set_builder`, or
:meth:`~isaaclab_newton.physics.NewtonManager.instantiate_builder_from_stage`
- Creates or imports the ``ModelBuilder``.
* - :meth:`~isaaclab_newton.physics.NewtonManager.create_builder` and
:meth:`~isaaclab_newton.physics.NewtonManager.set_builder`
- The cloner imports declared prototypes, composes worlds, and supplies the completed builder.
- ``_register_builder_attributes()`` (not called by ``set_builder()``)
* - :meth:`~isaaclab_newton.physics.NewtonManager.start_simulation`
- Finalizes the model, then allocates states, reset masks, and Fabric
prims.
- Finalizes the model, then allocates states and reset masks.
- ``_register_builder_attributes()``,
``_prepare_builder_for_finalize()``
* - :meth:`~isaaclab_newton.physics.NewtonManager.initialize_solver`
Expand Down
8 changes: 4 additions & 4 deletions docs/source/experimental-features/bleeding-edge.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ The following features are currently available in ``isaaclab_contrib``:
- Geometric controllers for multirotor attitude, velocity, acceleration, and position tracking
on SO(3) (Lee et al.). Suitable for both trajectory following and RL baselines.
- API reference: :mod:`~isaaclab_contrib.controllers`
* - **Newton VBD Deformable Objects**
- Extended deformable object support using the Newton physics backend with Vertex Block Descent (VBD),
including proxy coupling between MJWarp and VBD.
- API reference: :mod:`~isaaclab_contrib.deformable`, :mod:`~isaaclab_contrib.coupling`
* - **Newton Solver Coupling**
- Proxy coupling between MJWarp and VBD. Deformable assets are implemented in
:mod:`isaaclab_newton.assets`.
- API reference: :mod:`~isaaclab_contrib.coupling`

Contributing
------------
Expand Down
2 changes: 1 addition & 1 deletion docs/source/how-to/run_deformable_object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The deformable object API and schema define/modify functions are shared across b
property and material configuration classes are backend-specific. PhysX simulates soft bodies using the Finite
Element Method (FEM); the Newton experimental backend uses the core VBD solver from
:mod:`isaaclab_newton.physics` with the deformable object integration from
:mod:`isaaclab_contrib.deformable`.
:mod:`isaaclab_newton.assets`.
The volume deformable comprises of two tetrahedral meshes -- a simulation mesh and a collision mesh. The simulation
mesh is used to simulate the deformations of the soft body, while the collision mesh is used to detect collisions
with other objects in the scene. For PhysX-specific details, please check the `PhysX documentation`_.
Expand Down
5 changes: 5 additions & 0 deletions source/isaaclab/changelog.d/deformable-prototype-bindings.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Changed
^^^^^^^

* Bound deformable visual geometry during native import, leaving runtime publication to
attach native point buffers without re-reading the stage.
Original file line number Diff line number Diff line change
Expand Up @@ -514,14 +514,14 @@ def _get_mjwarp_names_from_newton_usd_builder(
) -> dict[Literal["joint", "body"], tuple[str, ...]] | None:
"""Build a lightweight Newton prototype view with MJWarp-style articulation names."""
# NOTE: "dfs" and bodies_follow_joint_ordering=True mirror the defaults of
# Newton's ModelBuilder.add_usd. isaaclab_newton's NewtonManager calls
# add_usd (see instantiate_builder_from_stage) without passing
# Newton's ModelBuilder.add_usd. isaaclab_newton's build_source_builders calls
# add_usd without passing
# joint_ordering/bodies_follow_joint_ordering, so a live Newton backend's
# native order matches this emulation only because both sides currently
# rely on the same Newton library defaults. The "mjwarp" convention's
# same-backend identity path (active backend "newton") returns that live
# order directly, assuming it equals what these hardcoded constants would
# produce. If NewtonManager ever passes explicit ordering arguments to
# produce. If the cloner ever passes explicit ordering arguments to
# add_usd, these constants must be updated in lockstep or MJWarp
# resolution will silently diverge from the live backend.
return _get_names_from_newton_usd_builder(
Expand Down
125 changes: 28 additions & 97 deletions source/isaaclab/isaaclab/scene_data/deformable_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import numpy as np
import warp as wp

from pxr import Gf, Sdf, Usd, UsdGeom
from pxr import Sdf, Usd, UsdGeom

from .. import sim as sim_utils
from ..cloner import ClonePlan
Expand Down Expand Up @@ -48,14 +48,14 @@ class DeformableStageEntry:


def deformable_geometry_batches(
entries: Sequence[DeformableStageEntry], points: wp.array, offsets: Sequence[int]
entries: Sequence[DeformableStageEntry], offsets: Sequence[int], *, device: str
) -> list[tuple[SceneDataFormat.Points | SceneDataFormat.WeightedPoints, dict[str, tuple[int, int]]]]:
"""Bind visual mesh paths to native nodal ranges or static barycentric interpolation tables.

Args:
entries: Declared deformable instances in native body order.
points: Flat native nodal positions [m], including any body padding.
offsets: Native point offset for each entry.
device: Device for static interpolation tables; native points are bound by the producer.

Returns:
Native-format publications and exact visual mesh paths with their output ranges.
Expand All @@ -68,7 +68,7 @@ def deformable_geometry_batches(
for entry, offset in zip(entries, offsets, strict=True):
key = (id(entry.vertices), id(entry.indices), id(entry.vis_vertices))
if key not in prototype_remaps:
if entry.vertex_count == entry.vis_vertex_count and np.array_equal(entry.vertices, entry.vis_vertices):
if entry.vertices is entry.vis_vertices or np.array_equal(entry.vertices, entry.vis_vertices):
prototype_remaps[key] = None
else:
if entry.deformable_type != "volume":
Expand All @@ -85,69 +85,15 @@ def deformable_geometry_batches(
weights.append(prototype_weights)
weighted[entry.vis_mesh_path] = (output_offset, entry.vis_vertex_count)
output_offset += entry.vis_vertex_count
batches = []
if direct:
publication = SceneDataFormat.Points()
publication.points = points
batches.append((publication, direct))
batches = [(SceneDataFormat.Points(), direct)] if direct else []
if weighted:
publication = SceneDataFormat.WeightedPoints()
publication.points = points
publication.indices = wp.array(np.concatenate(indices), dtype=wp.int32, device=points.device)
publication.weights = wp.array(np.concatenate(weights), dtype=wp.float32, device=points.device)
publication.indices = wp.array(np.concatenate(indices), dtype=wp.int32, device=device)
publication.weights = wp.array(np.concatenate(weights), dtype=wp.float32, device=device)
batches.append((publication, weighted))
return batches


def _matrix4d_to_numpy(matrix: Gf.Matrix4d) -> np.ndarray:
"""Convert a USD matrix to a host ``(4, 4)`` float64 array."""
return np.array([[matrix[i][j] for j in range(4)] for i in range(4)], dtype=np.float64)


def _transform_points(matrix: np.ndarray, points: np.ndarray) -> np.ndarray:
"""Apply a USD ``(4, 4)`` transform to ``(N, 3)`` points [m], returning float32.

USD ``Gf.Matrix4d.Transform`` uses row-vector convention (``p @ M``). The numpy
matrix from :func:`_matrix4d_to_numpy` stores ``matrix[i, j] = usd[i][j]``, so the
matching host multiply is ``hom @ matrix``, not ``matrix @ hom``.
"""
if points.size == 0:
return np.empty((0, 3), dtype=np.float32)
ones = np.ones((points.shape[0], 1), dtype=np.float64)
hom = np.concatenate([points.astype(np.float64, copy=False), ones], axis=1)
baked = (hom @ matrix)[:, :3]
return baked.astype(np.float32, copy=False)


def _usd_points_to_numpy(points) -> np.ndarray:
"""Convert USD point arrays to ``(N, 3)`` float32."""
if not points:
return np.empty((0, 3), dtype=np.float32)
return np.asarray(points, dtype=np.float32).reshape(-1, 3)


def _get_applied_schema_names(prim) -> set[str]:
"""Return applied API schema names from composed schemas and explicit ``apiSchemas`` metadata."""
names = set(prim.GetAppliedSchemas())
api_schemas = prim.GetMetadata("apiSchemas")
if isinstance(api_schemas, Sdf.TokenListOp):
names.update(str(token) for token in api_schemas.explicitItems)
return names


def _prim_has_schema(prim, schema_substring: str) -> bool:
"""Return ``True`` if any applied API schema name contains ``schema_substring``."""
return any(schema_substring in name for name in _get_applied_schema_names(prim))


def _mesh_point_count(prim) -> int:
"""Return the number of points authored on a Mesh or TetMesh prim."""
if prim.GetTypeName() not in ("Mesh", "TetMesh"):
return 0
pts = UsdGeom.PointBased(prim).GetPointsAttr().Get()
return len(pts or [])


def _select_visual_mesh(vis_candidates: list, sim_mesh_prim, sim_vertex_count: int):
"""Choose the visual mesh among candidates when several non-sim meshes exist.

Expand All @@ -163,18 +109,13 @@ def _select_visual_mesh(vis_candidates: list, sim_mesh_prim, sim_vertex_count: i

# Rare case that the sim mesh has multiple visual candidates.
sim_parent = sim_mesh_prim.GetParent()
sim_parent_path = sim_parent.GetPath() if sim_parent is not None and sim_parent.IsValid() else None

def _score(prim) -> tuple:
"""Rank visual-mesh candidates; higher tuples are preferred by ``max``."""
name = prim.GetName().lower()
name_bonus = int(any(token in name for token in ("visual", "render", "display", "proxy")))
sibling_bonus = int(
sim_parent_path is not None
and prim.GetParent() is not None
and prim.GetParent().GetPath() == sim_parent_path
)
count_bonus = int(_mesh_point_count(prim) == sim_vertex_count)
sibling_bonus = int(prim.GetParent() == sim_parent)
count_bonus = int(len(UsdGeom.PointBased(prim).GetPointsAttr().Get() or []) == sim_vertex_count)
path = prim.GetPath().pathString
return (sibling_bonus, name_bonus, count_bonus, -path.count("/"), path)

Expand All @@ -198,6 +139,11 @@ def deformable_entry(root_prim: Usd.Prim) -> DeformableStageEntry | None:
)
tet_prims = [prim for prim in mesh_prims if prim.GetTypeName() == "TetMesh"]
mesh_prims = [prim for prim in mesh_prims if prim.GetTypeName() == "Mesh"]
sim_candidates, vis_candidates = [], []
for prim in mesh_prims:
schemas = prim.GetPrimTypeInfo().GetAppliedAPISchemas()
is_sim = any("DeformableSimAPI" in schema for schema in schemas)
(sim_candidates if is_sim else vis_candidates).append(prim)

if tet_prims:
if len(tet_prims) > 1:
Expand All @@ -210,20 +156,12 @@ def deformable_entry(root_prim: Usd.Prim) -> DeformableStageEntry | None:

deformable_type = "volume"
sim_mesh_prim = tet_prims[0]
vis_candidates = [p for p in mesh_prims if not _prim_has_schema(p, "DeformableSimAPI")]
tet_mesh = UsdGeom.TetMesh(sim_mesh_prim)
pts = tet_mesh.GetPointsAttr().Get() or []
raw_tet_indices = tet_mesh.GetTetVertexIndicesAttr().Get() or []
indices = np.array([int(v) for vec4i in raw_tet_indices for v in vec4i], dtype=np.int32)
indices = np.asarray(raw_tet_indices, dtype=np.int32).reshape(-1)
elif mesh_prims:
deformable_type = "surface"
sim_candidates: list = []
vis_candidates: list = []
for prim in mesh_prims:
if _prim_has_schema(prim, "DeformableSimAPI"):
sim_candidates.append(prim)
else:
vis_candidates.append(prim)
sim_mesh_prim = sim_candidates[0] if sim_candidates else mesh_prims[0]
if not sim_candidates:
vis_candidates = []
Expand All @@ -235,39 +173,32 @@ def deformable_entry(root_prim: Usd.Prim) -> DeformableStageEntry | None:
return None

vis_mesh_prim = _select_visual_mesh(vis_candidates, sim_mesh_prim, len(pts))
vis_pts = (
UsdGeom.Mesh(vis_mesh_prim).GetPointsAttr().Get()
if vis_mesh_prim.GetTypeName() == "Mesh"
else UsdGeom.TetMesh(vis_mesh_prim).GetPointsAttr().Get()
)
vis_count = len(vis_pts or [])

xform_cache = UsdGeom.XformCache()
mesh_to_parent_frame = _matrix4d_to_numpy(
xform_cache.GetLocalToWorldTransform(sim_mesh_prim)
* xform_cache.GetLocalToWorldTransform(root_prim.GetParent()).GetInverse()
)
vertices = _transform_points(mesh_to_parent_frame, _usd_points_to_numpy(pts))

vis_mesh_to_parent_frame = _matrix4d_to_numpy(
xform_cache.GetLocalToWorldTransform(vis_mesh_prim)
* xform_cache.GetLocalToWorldTransform(root_prim.GetParent()).GetInverse()
)
vis_vertices = _transform_points(vis_mesh_to_parent_frame, _usd_points_to_numpy(vis_pts or []))
parent_transform = xform_cache.GetLocalToWorldTransform(root_prim.GetParent())
world_to_parent = parent_transform.GetInverse()
mesh_to_parent = np.asarray(xform_cache.GetLocalToWorldTransform(sim_mesh_prim) * world_to_parent)
# USD affine transforms multiply row vectors.
vertices = np.asarray(pts, dtype=np.float32).reshape(-1, 3)
vertices = (vertices @ mesh_to_parent[:3, :3] + mesh_to_parent[3, :3]).astype(np.float32)
vis_vertices = vertices
if vis_mesh_prim != sim_mesh_prim:
vis_pts = UsdGeom.PointBased(vis_mesh_prim).GetPointsAttr().Get()
vis_to_parent = np.asarray(xform_cache.GetLocalToWorldTransform(vis_mesh_prim) * world_to_parent)
vis_vertices = np.asarray(vis_pts or [], dtype=np.float32).reshape(-1, 3)
vis_vertices = (vis_vertices @ vis_to_parent[:3, :3] + vis_to_parent[3, :3]).astype(np.float32)

vis_indices = np.empty(0, dtype=np.int32)
if vis_mesh_prim.GetTypeName() == "Mesh":
vis_indices = np.asarray(UsdGeom.Mesh(vis_mesh_prim).GetFaceVertexIndicesAttr().Get() or [], dtype=np.int32)

parent_transform = xform_cache.GetLocalToWorldTransform(root_prim.GetParent())
rotation = parent_transform.ExtractRotationQuat()
return DeformableStageEntry(
root_path=str(root_path),
sim_mesh_path=str(sim_mesh_prim.GetPath()),
vis_mesh_path=str(vis_mesh_prim.GetPath()),
deformable_type=deformable_type,
vertex_count=len(pts),
vis_vertex_count=vis_count,
vis_vertex_count=len(vis_vertices),
vertices=vertices,
indices=indices,
vis_vertices=vis_vertices,
Expand Down
Loading
Loading