Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- Warn in `ModelBuilder.add_usd()` when a rigid body prim has a mirrored (negative-determinant) world transform. Improper transforms have no unique rotation decomposition, so imported body and joint frames can acquire a spurious constant rotation (common with mirror-scaled CAD exports); the warning recommends baking the reflection into the mesh geometry before import.
- Add opt-in filtering of static-static, static-kinematic, and kinematic-kinematic contacts during broad-phase collision detection. Set `CollisionPipeline(include_static_kinematic_pairs=False)` to enable filtering; the default preserves existing contact generation. `Model.shape_contact_pairs` remains an unfiltered superset for direct consumers such as `SolverKamino` and hydroelastic SDF setup.
- Add opt-in `body_frame_origin="com"` to `ModelBuilder.add_rod()` and `ModelBuilder.add_rod_graph()` for COM-centered cable capsule body frames.
- Add dedicated gravity for global world `-1` while preserving the single-entry `Model.gravity` array for implicit single-world models and local-only array updates through `Model.set_gravity()`. (#3723)
- Add `sign_method` argument to `Mesh.build_sdf` and `SDF.create_from_mesh` support for a `"normal"` (angle-weighted pseudo-normal) sign strategy, for selecting the inside/outside sign of the baked SDF (`"auto"`, `"parity"`, `"winding"`, or `"normal"`).
- Add `forward_depth_image` output support to `SensorTiledCamera.update()` and `SensorTiledCamera.utils.create_forward_depth_image_output()` for native forward-depth rendering without post-processing `depth_image`.
- Add optional `shear_stiffness`/`shear_damping` and `twist_stiffness`/`twist_damping` controls to `ModelBuilder.add_joint_cable()`, `ModelBuilder.add_rod()`, and `ModelBuilder.add_rod_graph()`; omitted shear defaults to stretch and omitted twist defaults to bend for compatibility.
Expand Down Expand Up @@ -116,6 +117,8 @@
- Fix MJCF imports ignoring material and inline RGBA colors on primitive geoms.
- Fix Style3D solver divergence caused by isolated vertices.
- Fix compiler warnings about overflowing int32 constants when compiling SDF texture and `SensorTiledCamera` kernels.
- Fix `SolverVBD` particles using world 0 gravity in multi-world models instead of their assigned world's gravity. (#3692)
- Fix `SensorIMU` to use per-world gravity for world-local sites not attached to a body.
- Fix USD site import to discover sites beneath non-visual containers, collider prims, and instanceable rigid-body prims independently of `load_visual_shapes`; the reworked traversal also speeds up import of scenes with many nested `Xform` or instance prims.
- Fix `SolverFeatherstone` BALL joints to apply passive `joint_damping` on all three angular DOFs.
- Fix `eval_ik()` and `SolverSemiImplicit` rounding small float32 revolute-joint angles to zero. (#3434)
Expand Down
14 changes: 9 additions & 5 deletions docs/concepts/worlds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,12 @@ Each world can have its own gravity vector, which is useful for simulating diffe
Per-world gravity can be configured at build time via the ``gravity`` argument of :meth:`~newton.ModelBuilder.begin_world`,
or modified at runtime via :meth:`~newton.Model.set_gravity`:

.. note::
Global entities (world index ``-1``) use the gravity of world ``0``.
Keep this in mind when mixing global and world-specific entities with different gravity vectors.
The builder's :attr:`~newton.ModelBuilder.gravity` is the default for new local worlds and the
dedicated gravity for global entities (world index ``-1``). The final element of
:attr:`~newton.Model.gravity` stores this global gravity, so the array has shape
``(world_count + 1,)`` and ``vec3`` elements when the builder contains explicit local
worlds. Legacy implicit single-world models retain one shared entry with shape ``(1,)``;
updates for world ``0`` therefore continue to affect their global entities.

.. testcode::

Expand All @@ -323,12 +326,13 @@ or modified at runtime via :meth:`~newton.Model.set_gravity`:
# Set different gravity for each world
model.set_gravity((0.0, 0.0, -9.81), world=0) # Earth
model.set_gravity((0.0, 0.0, -1.62), world=1) # Moon
model.set_gravity((0.0, 0.0, -3.71), world=-1) # Global entities

print("Gravity shape:", model.gravity.numpy().shape)
print("Gravity shape:", model.gravity.shape)

.. testoutput::

Gravity shape: (2, 3)
Gravity shape: (3,)


.. _World-entity partitioning:
Expand Down
8 changes: 6 additions & 2 deletions newton/_src/sensors/sensor_imu.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def compute_sensor_imu_kernel(
body_world: wp.array[wp.int32],
body_com: wp.array[wp.vec3],
shape_body: wp.array[int],
shape_world: wp.array[wp.int32],
shape_transform: wp.array[wp.transform],
sensor_sites: wp.array[int],
body_q: wp.array[wp.transform],
Expand All @@ -40,12 +41,14 @@ def compute_sensor_imu_kernel(
site_transform = shape_transform[site_idx]

if body_idx < 0:
accelerometer[sensor_idx] = wp.quat_rotate_inv(site_transform.q, -gravity[0])
world_idx = shape_world[site_idx]
world_g = gravity[world_idx]
accelerometer[sensor_idx] = wp.quat_rotate_inv(site_transform.q, -world_g)
gyroscope[sensor_idx] = wp.vec3(0.0)
return

world_idx = body_world[body_idx]
world_g = gravity[wp.max(world_idx, 0)]
world_g = gravity[world_idx]

body_acc = body_qdd[body_idx]

Expand Down Expand Up @@ -191,6 +194,7 @@ def update(self, state: State):
self.model.body_world,
self.model.body_com,
self.model.shape_body,
self.model.shape_world,
self.model.shape_transform,
self.sensor_sites_arr,
state.body_q,
Expand Down
13 changes: 7 additions & 6 deletions newton/_src/sim/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,7 @@ def __init__(
self.up_axis: Axis = Axis.from_any(up_axis)
"""Up axis used by geometry helpers and for resolving default or scalar gravity."""
self._gravity: float | wp.vec3 | None = None
"""Explicitly set gravity; ``None`` means -9.81 along the current :attr:`up_axis`."""
"""Explicit global/default gravity; ``None`` means -9.81 along the current :attr:`up_axis`."""
if gravity is not None:
self._set_gravity(gravity, stacklevel=3)

Expand Down Expand Up @@ -2367,7 +2367,7 @@ def up_vector(self, _):

@property
def gravity(self) -> float | wp.vec3:
"""Default gravity vector [m/s^2], or a deprecated scalar along :attr:`up_axis`."""
"""Global/default gravity vector [m/s^2], or a deprecated scalar along :attr:`up_axis`."""
if np.isscalar(self._gravity):
warnings.warn(_SCALAR_GRAVITY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
return self._gravity
Expand Down Expand Up @@ -12099,12 +12099,13 @@ def _to_wp_array(data, dtype, requires_grad):
# enable ground plane
m.up_axis = self.up_axis

# set gravity - create per-world gravity array for multi-world support
# Explicit local worlds need a trailing global entry. Implicit
# single-world models retain their legacy shared gravity entry.
global_gravity = self._gravity_as_vector()
if self.world_gravity:
# Use per-world gravity from world_gravity list
gravity_vecs = self.world_gravity
gravity_vecs = [*self.world_gravity, global_gravity]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Kamino maps global entities to world 0

ModelKamino.from_newton() rewrites every -1 world index to 0 whenever world_count == 1, including models containing both global and world-0 entities. With global gravity -2 and world-0 gravity -5, constructing Kamino changed body_world from [-1, 0] to [0, 0]; one step produced Z velocities [-0.5, -0.5] instead of [-0.2, -0.5].

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed. Kamino now uses a conversion-only world mapping and leaves model.body_world unchanged. Global bodies keep world -1 and use global gravity.

else:
gravity_vecs = [self._gravity_as_vector()] * self.world_count
gravity_vecs = [global_gravity for _ in range(self.world_count)]
m.gravity = wp.array(
gravity_vecs,
dtype=wp.vec3,
Expand Down
8 changes: 4 additions & 4 deletions newton/_src/sim/inverse_dynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ def __init__(
max_joints_per_articulation: Maximum number of joints in any
articulation, used to size the per-articulation Jacobian
scratch. Matches :attr:`Model.max_joints_per_articulation`.
world_count: Number of simulation worlds, used to size the
constant-zero gravity vector consumed by the Coriolis
compensation pass. Matches :attr:`Model.world_count`.
world_count: Number of local simulation worlds, used to size the
local-and-global constant-zero gravity vector consumed by the
Coriolis compensation pass. Matches :attr:`Model.world_count`.
device: Warp device on which the buffers are allocated.
"""
bc = body_count
Expand Down Expand Up @@ -97,7 +97,7 @@ def __init__(
# Constant-zero inputs (allocated once, never written).
self.zeros_dof = wp.zeros(jdc, dtype=wp.float32, device=device)
self.zeros_body = wp.zeros(bc, dtype=wp.spatial_vector, device=device)
self.zero_gravity = wp.zeros(world_count, dtype=wp.vec3, device=device)
self.zero_gravity = wp.zeros(world_count + 1, dtype=wp.vec3, device=device)


def _rnea_compensation_pass(
Expand Down
38 changes: 27 additions & 11 deletions newton/_src/sim/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,13 @@ def __init__(self, device: Devicelike | None = None):
self.up_axis: int = 2
"""Up axis: 0 for x, 1 for y, 2 for z."""
self.gravity: wp.array[wp.vec3] | None = None
"""Per-world gravity vectors [m/s²], shape [world_count, 3], dtype :class:`vec3`."""
"""Local-world and global gravity vectors [m/s²], dtype :class:`vec3`.

Models with explicit local worlds have shape [world_count + 1], where
the final element is the gravity for global world ``-1``. Legacy
implicit single-world models have shape [1], shared by world ``0``
and global world ``-1``.
"""

self.constraint_mimic_joint0: wp.array[wp.int32] | None = None
"""Follower joint index (``joint0 = coef0 + coef1 * joint1``), shape [constraint_mimic_count], int."""
Expand Down Expand Up @@ -1964,31 +1970,41 @@ def set_gravity(
Set gravity for runtime modification.

Args:
gravity: Gravity vector (3,) or per-world array (world_count, 3).
world: If provided, set gravity only for this world.
gravity: A single gravity vector [m/s²], one vector per local world, or one
vector per local world plus a final global vector. A single vector
updates every local world and the global world. Local-world-only
inputs preserve a distinct global gravity entry.
world: If provided, set gravity only for this world. Use ``-1`` for the
global world.

Note:
Call ``solver.notify_model_changed(ModelFlags.MODEL_PROPERTIES)`` after.

Global entities (particles/bodies not assigned to a specific world) use
gravity from world 0.
"""
gravity_np = np.asarray(gravity, dtype=np.float32)

if world is not None:
if gravity_np.shape != (3,):
raise ValueError("Expected single gravity vector (3,) when world is specified")
if world < 0 or world >= self.world_count:
raise IndexError(f"world {world} out of range [0, {self.world_count})")
if world < -1 or world >= self.world_count:
raise IndexError(f"world {world} out of range; expected -1 or [0, {self.world_count})")
current = self.gravity.numpy()
current[world] = gravity_np
self.gravity.assign(current)
elif gravity_np.ndim == 1:
if gravity_np.shape != (3,):
raise ValueError(f"Expected gravity with shape (3,), got {gravity_np.shape}")
self.gravity.fill_(gravity_np)
else:
if len(gravity_np) != self.world_count:
raise ValueError(f"Expected {self.world_count} gravity vectors, got {len(gravity_np)}")
self.gravity.assign(gravity_np)
local_shape = (self.world_count, 3)
full_shape = (self.gravity.shape[0], 3)
if gravity_np.shape == full_shape:
self.gravity.assign(gravity_np)
elif gravity_np.shape == local_shape:
current = self.gravity.numpy()
current[: self.world_count] = gravity_np

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Legacy gravity updates no longer affect global-only entities

ModelBuilder.finalize() reports world_count == 1 even when all entities belong to world -1. Consequently, set_gravity(..., world=0) and (world_count, 3) updates now modify gravity[0], while those entities read gravity[-1]. In a SemiImplicit reproduction, both calls succeeded but the particle continued accelerating at -9.81. The new Model.gravity shape also makes existing direct assignments with (world_count, 3) arrays incompatible.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed. Implicit single-world models keep the legacy one-entry Model.gravity, so world 0 updates, local arrays, and direct assignments still affect global-only entities.

self.gravity.assign(current)
else:
raise ValueError(f"Expected gravity with shape {local_shape} or {full_shape}, got {gravity_np.shape}")

def _init_collision_pipeline(self, enable_rigid_soft_full_surface_contact: bool = False):
"""
Expand Down
4 changes: 2 additions & 2 deletions newton/_src/solvers/coupled/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ def _coupling_eval_body_gravity_acceleration_kernel(
out: wp.array[wp.vec3],
):
i = wp.tid()
out[i] = gravity[wp.max(body_world[i], 0)]
out[i] = gravity[body_world[i]]


@wp.kernel(enable_backward=False)
Expand All @@ -477,7 +477,7 @@ def _coupling_eval_particle_gravity_acceleration_kernel(
out: wp.array[wp.vec3],
):
i = wp.tid()
out[i] = gravity[wp.max(particle_world[i], 0)]
out[i] = gravity[particle_world[i]]


@wp.func
Expand Down
2 changes: 1 addition & 1 deletion newton/_src/solvers/featherstone/kernels.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ def compute_link_velocity(
m = I_m[0, 0]

world_idx = body_world[child]
world_g = gravity[wp.max(world_idx, 0)]
world_g = gravity[world_idx]
f_g = m * world_g
f_g_s = wp.spatial_vector(f_g, wp.cross(x_com_s, f_g))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def integrate_velocity(

vel_adv = velocities[s.qp_index]
world_idx = particle_world[s.qp_index]
world_g = gravity[wp.max(world_idx, 0)]
world_g = gravity[world_idx]

rho = particle_density[s.qp_index]
vel_adv = wp.where(
Expand Down
7 changes: 4 additions & 3 deletions newton/_src/solvers/kamino/_src/core/conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from .....geometry import ShapeFlags
from .....sim.model import Model
from ....coupled.model_view import ModelView
from ..utils import logger as msg
from .bodies import (
RigidBodiesModel,
Expand Down Expand Up @@ -1030,7 +1031,7 @@ def convert_model_materials(


def convert_rigid_bodies(
model: Model,
model: Model | ModelView,
model_size: SizeKamino,
model_info: ModelKaminoInfo,
) -> RigidBodiesModel:
Expand Down Expand Up @@ -1123,7 +1124,7 @@ def convert_rigid_bodies(


def convert_joints(
model: Model,
model: Model | ModelView,
model_size: SizeKamino,
model_info: ModelKaminoInfo,
) -> JointsModel:
Expand Down Expand Up @@ -1626,7 +1627,7 @@ def register_materials(model: Model, materials_manager: MaterialManager) -> np.n


def convert_geometries(
model: Model,
model: Model | ModelView,
model_size: SizeKamino,
model_bodies: RigidBodiesModel,
materials_manager: MaterialManager,
Expand Down
26 changes: 17 additions & 9 deletions newton/_src/solvers/kamino/_src/core/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,9 +682,11 @@ def from_newton(model: Model | ModelView) -> ModelKamino:
f"ModelKamino.from_newton() requires a newton.Model or ModelView instance, got {type(model).__name__}."
)

# Single-world Newton models may have world index -1 (unassigned).
# Normalize to 0 so downstream world-based grouping works correctly.
# Normalize conversion-only grouping metadata for single-world models;
# body world indices still select Model.gravity directly.
conversion_model = model
if model.world_count == 1:
conversion_model = ModelView(model, "kamino_worlds")
for attr, start_attr in (
("body_world", "body_world_start"),
("joint_world", "joint_world_start"),
Expand All @@ -693,14 +695,20 @@ def from_newton(model: Model | ModelView) -> ModelKamino:
arr = getattr(model, attr)
arr_np = arr.numpy()
if np.any(arr_np < 0):
arr_np[arr_np < 0] = 0
arr.assign(arr_np)
if attr != "body_world":
arr_np = arr_np.copy()
arr_np[arr_np < 0] = 0
setattr(conversion_model, attr, wp.array(arr_np, dtype=wp.int32, device=model.device))
# Update world start indices
arr_start = getattr(model, start_attr)
arr_start_np = arr_start.numpy()
arr_start_np = arr_start.numpy().copy()
arr_start_np[0] = 0
arr_start_np[-2] = arr_start_np[-1]
arr_start.assign(arr_start_np)
setattr(
conversion_model,
start_attr,
wp.array(arr_start_np, dtype=wp.int32, device=model.device),
)

# Initialize materials manager
materials_manager = MaterialManager()
Expand All @@ -727,18 +735,18 @@ def from_newton(model: Model | ModelView) -> ModelKamino:
model_gravity = GravityModel.from_newton(model)

# Bodies
model_bodies = convert_rigid_bodies(model, model_size, model_info)
model_bodies = convert_rigid_bodies(conversion_model, model_size, model_info)

# Joints
model_joints = convert_joints(
model,
conversion_model,
model_size,
model_info,
)

# Geometries
model_geoms = convert_geometries(
model=model,
model=conversion_model,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
model_size=model_size,
model_bodies=model_bodies,
materials_manager=materials_manager,
Expand Down
Loading
Loading