Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
- Fix `SolverKamino` contact anchors being shifted off the geometry surface by `ShapeConfig.margin`, which biased friction.
- Fix MJCF joint `damping` attribute being ignored by `SolverFeatherstone`
- Fix `SolverMuJoCo` generated MuJoCo joint names for multi-axis D6 joints to avoid duplicate names
- Fix `SolverMuJoCo` ball-joint frame conversion: `joint_q` and position-target quaternions were applied in the wrong basis when `child_xform` had a non-identity rotation, and `joint_qd` / velocity targets / applied / actuator torques were applied and read back in the wrong basis whenever the ball was away from its rest pose.
- Fix USD import of revolute and D6-angular joint `limit_ke` / `limit_kd` from `mjc:solreflimit` being over-scaled by ~57x
- Fix `ViewerGL` GUI rendering at half size on HiDPI / Retina displays by scaling the ImGui style, fonts, sidebar width, and `log_image` window/tile/spacing constants with pyglet's `window.scale` (with framebuffer-to-window ratio as a fallback). DPI changes are tracked at runtime via the pyglet `on_scale` event so the GUI follows the window across displays with different scaling
- Fix USD import losing authored negative scales on shape and parent xforms, so mirrored primitives and meshes are now imported with the correct signed scale
Expand Down
176 changes: 134 additions & 42 deletions newton/_src/solvers/mujoco/kernels.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ def convert_mj_coords_to_warp_kernel(
joint_dof_dim: wp.array2d[wp.int32],
joint_child: wp.array[wp.int32],
joint_X_p: wp.array[wp.transform],
joint_X_c: wp.array[wp.transform],
body_com: wp.array[wp.vec3],
dof_ref: wp.array[wp.float32],
body_flags: wp.array[wp.int32],
Expand Down Expand Up @@ -664,22 +665,25 @@ def convert_mj_coords_to_warp_kernel(
joint_qd[wqd_i + 4] = w_parent[1]
joint_qd[wqd_i + 5] = w_parent[2]
elif type == JointType.BALL:
# change quaternion order from wxyz to xyzw
rot = quat_wxyz_to_xyzw(
wp.quat(
qpos[worldid, q_i],
qpos[worldid, q_i + 1],
qpos[worldid, q_i + 2],
qpos[worldid, q_i + 3],
)
# Newton uses the parent anchor frame for both qpos and qvel.
# MuJoCo splits them: qpos in the child rest frame, qvel/qfrc in the current (post-qpos) body frame.
Comment thread
eric-heiden marked this conversation as resolved.
q_cj = joint_X_c[joint_id].q
q_mj = quat_wxyz_to_xyzw(
wp.quat(qpos[worldid, q_i + 0], qpos[worldid, q_i + 1], qpos[worldid, q_i + 2], qpos[worldid, q_i + 3])
)
joint_q[wq_i] = rot[0]
joint_q[wq_i + 1] = rot[1]
joint_q[wq_i + 2] = rot[2]
joint_q[wq_i + 3] = rot[3]
for i in range(3):
# convert velocity components
joint_qd[wqd_i + i] = qvel[worldid, qd_i + i]

mj_to_anchor = wp.quat_inverse(q_cj) * q_mj # common to qpos similarity transform and the qvel rotation
r = mj_to_anchor * q_cj
joint_q[wq_i + 0] = r[0]
joint_q[wq_i + 1] = r[1]
joint_q[wq_i + 2] = r[2]
joint_q[wq_i + 3] = r[3]

omega_mj = wp.vec3(qvel[worldid, qd_i + 0], qvel[worldid, qd_i + 1], qvel[worldid, qd_i + 2])
w_newton = wp.quat_rotate(mj_to_anchor, omega_mj)
joint_qd[wqd_i + 0] = w_newton[0]
joint_qd[wqd_i + 1] = w_newton[1]
joint_qd[wqd_i + 2] = w_newton[2]
else:
axis_count = joint_dof_dim[joint_id, 0] + joint_dof_dim[joint_id, 1]
for i in range(axis_count):
Expand All @@ -703,6 +707,7 @@ def convert_warp_coords_to_mj_kernel(
joint_dof_dim: wp.array2d[wp.int32],
joint_child: wp.array[wp.int32],
joint_X_p: wp.array[wp.transform],
joint_X_c: wp.array[wp.transform],
body_com: wp.array[wp.vec3],
dof_ref: wp.array[wp.float32],
mj_q_start: wp.array[wp.int32],
Expand Down Expand Up @@ -769,16 +774,21 @@ def convert_warp_coords_to_mj_kernel(
qvel[worldid, qd_i + 5] = w_body[2]

elif jtype == JointType.BALL:
# change quaternion order from xyzw to wxyz
ball_q = wp.quat(joint_q[wq_i], joint_q[wq_i + 1], joint_q[wq_i + 2], joint_q[wq_i + 3])
ball_q_wxyz = quat_xyzw_to_wxyz(ball_q)
# Inverse of convert_mj_coords_to_warp_kernel.
q_cj = joint_X_c[joint_id].q
r = wp.quat(joint_q[wq_i + 0], joint_q[wq_i + 1], joint_q[wq_i + 2], joint_q[wq_i + 3])
q_mj = q_cj * r * wp.quat_inverse(q_cj)
ball_q_wxyz = quat_xyzw_to_wxyz(q_mj)
qpos[worldid, q_i + 0] = ball_q_wxyz[0]
qpos[worldid, q_i + 1] = ball_q_wxyz[1]
qpos[worldid, q_i + 2] = ball_q_wxyz[2]
qpos[worldid, q_i + 3] = ball_q_wxyz[3]
for i in range(3):
# convert velocity components
qvel[worldid, qd_i + i] = joint_qd[wqd_i + i]

w_newton = wp.vec3(joint_qd[wqd_i + 0], joint_qd[wqd_i + 1], joint_qd[wqd_i + 2])
w_mj = wp.quat_rotate(q_cj * wp.quat_inverse(r), w_newton)
qvel[worldid, qd_i + 0] = w_mj[0]
qvel[worldid, qd_i + 1] = w_mj[1]
qvel[worldid, qd_i + 2] = w_mj[2]
else:
axis_count = joint_dof_dim[joint_id, 0] + joint_dof_dim[joint_id, 1]
for i in range(axis_count):
Expand Down Expand Up @@ -1381,27 +1391,35 @@ def apply_mjc_control_kernel(
mjc_actuator_to_newton_idx: wp.array[wp.int32],
mjc_actuator_to_newton_target_q_idx: wp.array[wp.int32],
mjc_actuator_to_target_q_axis_idx: wp.array[wp.int32],
mjc_actuator_to_newton_ball_jnt: wp.array[wp.int32],
joint_X_c: wp.array[wp.transform],
joint_target_q: wp.array[wp.float32],
joint_target_qd: wp.array[wp.float32],
joint_q: wp.array[wp.float32],
mujoco_ctrl: wp.array[wp.float32],
target_q_per_world: wp.int32,
coords_per_world: wp.int32,
dofs_per_world: wp.int32,
ctrls_per_world: wp.int32,
joints_per_world: wp.int32,
use_coord_layout_targets: bool,
# outputs
mj_ctrl: wp.array2d[wp.float32],
):
"""Apply Newton control inputs to MuJoCo control array.

For JOINT_TARGET (source=0), uses sign encoding in mjc_actuator_to_newton_idx:
- Positive value (>=0): position actuator; the coord-index into
- Positive value (>=0): position actuator; the index into
``joint_target_q`` is read from ``mjc_actuator_to_newton_target_q_idx``.
- Value of -1: unmapped/skip
- Negative value (<=-2): velocity actuator, newton_axis = -(value + 2)

For ball-joint position actuators under the coord layout, the target stored
in ``joint_target_q`` is a quaternion. ``mjc_actuator_to_target_q_axis_idx``
selects axis 0/1/2 of the axis-angle representation; a value of -1 means
"scalar passthrough" (the legacy DOF-layout and all non-ball actuators).
For ball-joint actuators, ``axis_idx >= 0`` selects the angular component to feed MuJoCo.
Position targets are rotated by the per-world child anchor ``q_cj`` (``joint_X_c`` indexed by
``mjc_actuator_to_newton_ball_jnt`` and the current world). Velocity targets read the current
quaternion start from ``mjc_actuator_to_newton_target_q_idx`` and rotate by ``q_cj * r^{-1}``
(mirroring the qpos / qvel bridges in :func:`convert_warp_coords_to_mj_kernel` BALL). The
velocity case reuses the existing target-q lookup slot.

For CTRL_DIRECT (source=1), mjc_actuator_to_newton_idx is the ctrl index.
"""
Expand All @@ -1420,23 +1438,72 @@ def apply_mjc_control_kernel(
if world_target_q < joint_target_q.shape[0]:
mj_ctrl[world, actuator] = joint_target_q[world_target_q]
else:
# Ball-joint quaternion target: feed MuJoCo the matching component
# of the axis-angle 3-vector (matches mjc actuator_length units).
if world_target_q + 3 < joint_target_q.shape[0]:
aa = _target_quat_to_axis_angle(
# Ball-joint position target
# Coord layout stores a 4-float quat (needs log-map); DOF layout stores
# extrinsic ZYX Euler target angles directly at the joint's target-q base.
last_elem = world_target_q + wp.where(use_coord_layout_targets, 3, 2) # check size
assert last_elem < joint_target_q.shape[0]
if not last_elem < joint_target_q.shape[0]:
return
Comment thread
camevor marked this conversation as resolved.

if use_coord_layout_targets:
q_n = wp.quat(
joint_target_q[world_target_q + 0],
joint_target_q[world_target_q + 1],
joint_target_q[world_target_q + 2],
joint_target_q[world_target_q + 3],
)
mj_ctrl[world, actuator] = aa[axis_idx]
else:
angles = wp.vec3(
joint_target_q[world_target_q + 0],
joint_target_q[world_target_q + 1],
joint_target_q[world_target_q + 2],
)
q_n = wp.quat_from_euler(angles, 2, 1, 0)

aa_newton = _target_quat_to_axis_angle(q_n[0], q_n[1], q_n[2], q_n[3])
jnt = mjc_actuator_to_newton_ball_jnt[actuator]
assert jnt >= 0
template_jnt = jnt % joints_per_world
joint_id = world * joints_per_world + template_jnt
q_cj = joint_X_c[joint_id].q
aa_mj = wp.quat_rotate(q_cj, aa_newton)
mj_ctrl[world, actuator] = aa_mj[axis_idx]
elif idx == -1:
return
else:
# Velocity actuator: newton_axis = -(idx + 2)
newton_axis = -(idx + 2)
world_dof = world * dofs_per_world + newton_axis
mj_ctrl[world, actuator] = joint_target_qd[world_dof]
axis_idx = mjc_actuator_to_target_q_axis_idx[actuator]
if axis_idx < 0:
world_dof = world * dofs_per_world + newton_axis
mj_ctrl[world, actuator] = joint_target_qd[world_dof]
else:
# Ball-joint velocity target: rotate into MuJoCo's current child body frame.
qd_start = newton_axis - axis_idx
qd_base = world * dofs_per_world + qd_start
# target_q_idx for ball-velocity points at the coord-indexed q_start of the ball
# quat in joint_q (which is always coord-indexed regardless of layout).
target_q_idx = mjc_actuator_to_newton_target_q_idx[actuator]
q_base = world * coords_per_world + target_q_idx
w_newton = wp.vec3(
joint_target_qd[qd_base + 0],
joint_target_qd[qd_base + 1],
joint_target_qd[qd_base + 2],
)
r = wp.quat(
joint_q[q_base + 0],
joint_q[q_base + 1],
joint_q[q_base + 2],
joint_q[q_base + 3],
)
jnt = mjc_actuator_to_newton_ball_jnt[actuator]
assert jnt >= 0
template_jnt = jnt % joints_per_world
joint_id = world * joints_per_world + template_jnt
q_cj = joint_X_c[joint_id].q
w_mj = wp.quat_rotate(q_cj * wp.quat_inverse(r), w_newton)
mj_ctrl[world, actuator] = w_mj[axis_idx]
else: # CTRL_SOURCE_CTRL_DIRECT
world_ctrl_idx = world * ctrls_per_world + idx
if world_ctrl_idx < mujoco_ctrl.shape[0]:
Expand Down Expand Up @@ -1471,11 +1538,14 @@ def apply_mjc_body_f_kernel(
@wp.kernel
def apply_mjc_qfrc_kernel(
joint_f: wp.array[wp.float32],
joint_q: wp.array[wp.float32],
joint_type: wp.array[wp.int32],
joint_child: wp.array[wp.int32],
body_flags: wp.array[wp.int32],
joint_q_start: wp.array[wp.int32],
joint_qd_start: wp.array[wp.int32],
joint_dof_dim: wp.array2d[wp.int32],
joint_X_c: wp.array[wp.transform],
joints_per_world: int,
mj_qd_start: wp.array[wp.int32],
# outputs
Expand All @@ -1488,10 +1558,11 @@ def apply_mjc_qfrc_kernel(
if qd_i < 0:
return

wqd_i = joint_qd_start[joints_per_world * worldid + jntid]
joint_id = joints_per_world * worldid + jntid
jtype = joint_type[jntid]
dof_count = joint_dof_dim[jntid, 0] + joint_dof_dim[jntid, 1]
wq_i = joint_q_start[joint_id]
wqd_i = joint_qd_start[joint_id]
jtype = joint_type[joint_id]
dof_count = joint_dof_dim[joint_id, 0] + joint_dof_dim[joint_id, 1]

for i in range(dof_count):
qfrc_applied[worldid, qd_i + i] = 0.0
Expand All @@ -1504,9 +1575,14 @@ def apply_mjc_qfrc_kernel(
if jtype == JointType.FREE or jtype == JointType.DISTANCE:
return
elif jtype == JointType.BALL:
qfrc_applied[worldid, qd_i + 0] = joint_f[wqd_i + 0]
qfrc_applied[worldid, qd_i + 1] = joint_f[wqd_i + 1]
qfrc_applied[worldid, qd_i + 2] = joint_f[wqd_i + 2]
# Torque uses the same map as the qvel writeback in convert_warp_coords_to_mj_kernel.
q_cj = joint_X_c[joint_id].q
r = wp.quat(joint_q[wq_i + 0], joint_q[wq_i + 1], joint_q[wq_i + 2], joint_q[wq_i + 3])
tau = wp.vec3(joint_f[wqd_i + 0], joint_f[wqd_i + 1], joint_f[wqd_i + 2])
tau_mj = wp.quat_rotate(q_cj * wp.quat_inverse(r), tau)
qfrc_applied[worldid, qd_i + 0] = tau_mj[0]
qfrc_applied[worldid, qd_i + 1] = tau_mj[1]
qfrc_applied[worldid, qd_i + 2] = tau_mj[2]
else:
for i in range(dof_count):
qfrc_applied[worldid, qd_i + i] = joint_f[wqd_i + i]
Expand Down Expand Up @@ -2862,6 +2938,7 @@ def convert_qfrc_actuator_from_mj_kernel(
joint_qd_start: wp.array[wp.int32],
joint_dof_dim: wp.array2d[wp.int32],
joint_child: wp.array[wp.int32],
joint_X_c: wp.array[wp.transform],
body_com: wp.array[wp.vec3],
mj_q_start: wp.array[wp.int32],
mj_qd_start: wp.array[wp.int32],
Expand All @@ -2873,7 +2950,10 @@ def convert_qfrc_actuator_from_mj_kernel(
Uses the same joint-based DOF mapping as the coordinate conversion
kernels. For free joints the wrench is transformed from MuJoCo's
(origin, body-frame) convention to the CoM/world convention used on the
MuJoCo side of Newton. Ball and other joints are copied directly.
MuJoCo side of Newton. For ball joints, the torque is rotated from
MuJoCo's current child body frame into Newton's parent anchor frame;
see :func:`apply_mjc_qfrc_kernel` for the inverse map. Other joints
are copied directly.
"""
worldid, jntid = wp.tid()

Expand Down Expand Up @@ -2930,8 +3010,20 @@ def convert_qfrc_actuator_from_mj_kernel(
qfrc_actuator[wqd_i + 4] = tau_world[1]
qfrc_actuator[wqd_i + 5] = tau_world[2]
elif jtype == JointType.BALL:
for i in range(3):
qfrc_actuator[wqd_i + i] = mjw_qfrc_actuator[worldid, qd_i + i]
# Inverse of apply_mjc_qfrc_kernel BALL; same map as the qvel readback in convert_mj_coords_to_warp_kernel.
q_cj = joint_X_c[joint_id].q
q_mj = quat_wxyz_to_xyzw(
wp.quat(qpos[worldid, q_i + 0], qpos[worldid, q_i + 1], qpos[worldid, q_i + 2], qpos[worldid, q_i + 3])
)
tau_mj = wp.vec3(
mjw_qfrc_actuator[worldid, qd_i + 0],
mjw_qfrc_actuator[worldid, qd_i + 1],
mjw_qfrc_actuator[worldid, qd_i + 2],
)
tau = wp.quat_rotate(wp.quat_inverse(q_cj) * q_mj, tau_mj)
qfrc_actuator[wqd_i + 0] = tau[0]
qfrc_actuator[wqd_i + 1] = tau[1]
qfrc_actuator[wqd_i + 2] = tau[2]
else:
axis_count = joint_dof_dim[joint_id, 0] + joint_dof_dim[joint_id, 1]
for i in range(axis_count):
Expand Down
Loading
Loading