Articulation Motion¶
SkeletonMotionstores character poses as a world-space root translation, a world-space root rotation, and parent-local joint rotations, with all rotations represented as WXYZ quaternions.ArticulationMotionstores a sampled articulation trajectory as frame-major generalized configurationsqand generalized tangent velocitiesqd, arranged according to anArticulationCoordinateLayout.
Use this conversion when preparing animation data for robot control, motion tracking, or simulation datasets.
Motion data flow¶
KangEngine normalizes animation files and dataset-specific arrays into
SkeletonMotion. An articulation layout can then map the same motion into
ArticulationMotion. MotionLibrary accepts SkeletonMotion and articulation
motions that can be converted back to that representation, then provides packed
sampling and optional backend-native state generation.
flowchart LR
subgraph Sources["Motion sources"]
direction TB
FILES["<b>BVH / FBX</b>"]
DATASETS["<b>NPZ / NPY / PKL</b><br/>AMASS · other SMPL-family data"]
ARTNPZ["<b>Articulation NPZ</b><br/>q + qd + model signature"]
ROBOT["<b>MJCF / URDF</b>"]
end
subgraph Canonical["Skeleton Animation"]
direction TB
LOADER["<b>Motion loader</b>"]
DATASET_ADAPTER["<b>Dataset adapter</b>"]
SKELETON["<b>SkeletonMotion</b><br/>SkeletonTree + root transform<br/>+ parent-local WXYZ rotations"]
end
subgraph Mapping["Articulation Animation"]
direction TB
DESC["<b>ArticulationDesc</b>"]
LAYOUT["<b>ArticulationCoordinateLayout</b>"]
MAPPER["<b>ArticulationMotionMapper</b><br/>joint decomposition + optional limit clamping"]
ARTLOADER["<b>load_articulation_motion_npz()</b>"]
ARTICULATION["<b>ArticulationMotion</b><br/>q + qd"]
end
subgraph Runtime["Runtime sampling"]
direction TB
LIBRARY["<b>MotionLibrary</b><br/>weighted multi-clip sampling"]
ADAPTER["<b>Optional MotionAdapter</b><br/>PhysX · Newton · MuJoCo"]
PACKED["<b>Packed Torch buffers</b><br/>skeleton frames + optional simulator frame data"]
SAMPLE["<b>MotionSample</b><br/>skeleton pose + optional simulator state"]
end
FILES --> LOADER --> SKELETON
DATASETS --> DATASET_ADAPTER --> SKELETON
ROBOT --> DESC --> LAYOUT
SKELETON --> MAPPER
LAYOUT --> MAPPER
MAPPER --> ARTICULATION
ARTNPZ --> ARTLOADER
LAYOUT --> ARTLOADER
ARTLOADER --> ARTICULATION
ARTICULATION -.->|inspect / export| SKELETON
SKELETON --> LIBRARY
ARTICULATION --> LIBRARY
ADAPTER -.->|configured with| LIBRARY
LIBRARY -->|owns| PACKED
ADAPTER -.->|prepare_motion| PACKED
PACKED -->|sample / interpolate| SAMPLE
ADAPTER -.->|pack_sample| SAMPLE
classDef primary fill:#eaf5e3,stroke:#568b32,stroke-width:3px,color:#28322c
class SKELETON,ARTICULATION,LIBRARY primary
Convert a motion¶
Build the layout from the same ArticulationDesc used by the simulator. The
layout preserves the loader’s BFS or DFS body order.
import kangengine as ke
data = ke.asset.MJCFLoader.load(mjcf_path, order="DFS")
layout = ke.animation.ArticulationCoordinateLayout.from_data(
data=data,
free_root=True,
)
mapper = ke.animation.ArticulationMotionMapper(layout)
result = mapper.to_articulation_motion(skeleton_motion)
articulation_motion = result.motion
q = articulation_motion.q # [frames, layout.nq], float32 NumPy view
qd = articulation_motion.qd # [frames, layout.nv], float32 NumPy view
residual_angles reports, in radians, rotation that joint decomposition could
not represent. When clamp_to_limits=True, the residual also includes error
introduced by limit clamping. Use it to detect unsuitable mappings or motions
outside the robot’s range.
Coordinate layout¶
Each layout.blocks entry describes one joint coordinate block and its offsets
inside q and qd.
Joint type |
|
|
|
|
|---|---|---|---|---|
Fixed |
none |
0 |
none |
0 |
Revolute |
angle |
1 |
angular velocity |
1 |
Prismatic |
position |
1 |
linear velocity |
1 |
Spherical(Ball) |
quaternion |
4 |
angular velocity |
3 |
Free root |
position |
7 |
linear velocity |
6 |
Therefore layout.nq and layout.nv may differ. Quaternion orientations use
four configuration values but have only three tangent velocity coordinates.
The layout also contains a model_signature. Mappers and backend adapters
require matching signatures. load_articulation_motion_npz() instead requires
matching nq and nv; it warns but still loads when only the signature differs.
This permits compatible extensions, but the warning may indicate a different
body order, joint layout, reference pose, or limits.
Convert back for inspection¶
Convert the complete clip back to SkeletonMotion for visualization or BVH
export:
preview_motion = mapper.to_skeleton_motion(articulation_motion)
Single poses can be converted in either direction:
mapped = mapper.to_articulation_coordinates(skeleton_state)
restored_state = mapper.to_skeleton_state(mapped.q)
SkeletonState and SkeletonMotion cannot represent prismatic joints. An
ArticulationMotion containing prismatic joints therefore cannot be converted
back to either representation.
Reference motion library¶
MotionLibrary stores the backend-independent quaternion form: root position,
root rotation, and local joint rotations. It accepts either SkeletonMotion
or ArticulationMotion; the latter is converted to SkeletonMotion for
canonical storage when registered. Consequently, articulation motion containing
prismatic joints cannot be registered.
adapter = ke.adapters.physx.PhysXMotionAdapter(layout)
library = ke.animation.MotionLibrary(
motions=[walk_motion, run_motion],
adapter=adapter,
device="cuda:0",
precompute_kinematics=True,
)
sample = library.sample(
motion_ids=[0, 1],
times=[0.25, 0.75],
loop=True,
)
local_rotation = sample.local_rotations_wxyz
body_state = library.sample_kinematics(sample)
body_position = body_state.body_positions
body_rotation = body_state.body_rotations_wxyz
physx_joint_position = sample.backend_state.joint_positions
physx_joint_velocity = sample.backend_state.joint_velocities
# If using MuJoCoMotionAdapter:
# mujoco_qpos = sample.backend_state.qpos
# mujoco_qvel = sample.backend_state.qvel
# If using NewtonMotionAdapter:
# newton_joint_q = sample.backend_state.joint_q
# newton_joint_qd = sample.backend_state.joint_qd
buffers = library.packed_buffers()
global_position_frames = buffers.global_positions
The library uploads all clips and metadata to packed Torch buffers. It uses
linear root-position interpolation and quaternion SLERP, then asks the optional
adapter to produce simulator-native tensors on the same device. Use
forward_kinematics(sample) when occasional samples need robot-body positions
or rotations. For high-frequency imitation and GPU kernels, construct the
library with precompute_kinematics=True and obtain persistent frame-major
position, rotation, linear-velocity, and angular-velocity tensors through
packed_buffers(). These borrowed tensors can be shared with runtimes such as
Warp without a copy; acquire the view again after adding clips or calling
library.to(device). Source SkeletonMotion objects are not retained by default;
pass keep_source_motions=True only when library.motion(id) is needed for
inspection, editing, or export. The optional adapter also receives the original
input during registration. Newton and MuJoCo adapters can reuse an
ArticulationMotion input’s existing q/qd; the PhysX adapter derives its
logical DOFs from the converted skeleton rotations. Use
sample_motion_ids(...) for weighted clip IDs, sample_times(...) for random
times, and sample_frames(...) for exact stored frames. library.to(device)
moves the packed library without rebuilding clips.
Backend buffers¶
Backend adapters require only the canonical layout. The backend model or live articulation can be checked explicitly before simulation, but it is not part of reference motion storage or sampling.
MuJoCo¶
from kangengine.adapters.mujoco import MuJoCoMotionAdapter
adapter = MuJoCoMotionAdapter(layout)
adapter.validate_model(mj_model) # optional compatibility check
buffers = adapter.pack(articulation_motion)
# buffers.qpos: [frames, layout.nq]
# buffers.qvel: [frames, layout.nv]
restored = adapter.unpack(
buffers.qpos,
buffers.qvel,
fps=articulation_motion.fps(),
)
MuJoCo and canonical quaternions are both WXYZ. The adapter additionally converts canonical free/ball angular velocities into MuJoCo’s local tangent frame.
Newton¶
from kangengine.adapters.newton import NewtonMotionAdapter
adapter = NewtonMotionAdapter(layout)
adapter.validate_model(newton_model) # optional compatibility check
buffers = adapter.pack(articulation_motion)
# buffers.joint_q: [frames, layout.nq]
# buffers.joint_qd: [frames, layout.nv]
The adapter handles Newton’s XYZW quaternion storage. Optional validation also checks grouped D6 offsets and authored axis order. The result remains one reference trajectory; broadcasting it across worlds or selecting a different frame per world belongs to the simulation or training layer.
PhysX¶
from kangengine.adapters.physx import PhysXMotionAdapter
adapter = PhysXMotionAdapter(layout)
adapter.validate_articulation(articulation) # optional compatibility check
buffers = adapter.pack(articulation_motion)
# buffers.root_positions / root_rotations_xyzw
# buffers.root_linear_velocities / root_angular_velocities
# buffers.joint_positions / joint_velocities
PhysX root state is separate from logical joint state. Authored scalar
revolute/prismatic DOFs use the stable canonical order. Native
spherical blocks currently raise NotImplementedError instead of silently
discarding rotation, because KangEngine’s PhysX wrapper does not yet expose a
complete three-axis spherical state.