SOMA ships the following model assets in assets/, loaded at layer __init__ time (callers typically do not touch them directly):
SOMA_template_rig.usda-- required rig source (joint hierarchy, bind/T-pose, bind-shape, skinning weights). This is the v0027 SOMA template with procedural twist joints.SOMA_procedural_transforms.json-- portable procedural-control definition for the v0027 twist setup. The filename is intentionally unsuffixed; the schema version is inside the file.SOMA_neutral.npz-- PCA shape model, mesh topology, UVs, LOD maps, semantic segments, and metadata.example_animation.usd-- canonical example motion for the body and hand demos.- Per-backend model folders -- native identity models for
mhr,smpl/smplx,anny,garment, each with OBJ pairs used to compute the mesh correspondence to SOMA topology.
.npz files use np.savez with allow_pickle=False. SOMA native unit is centimeters, up axis +Y, forward axis +Z. Values below use V for the mid-LOD body vertex count (18056) and J for full-body joint count (78).
UsdSkel file holding the canonical body rig. Loaded by {py:func}~soma.io.load_lod_rig_from_usd during SOMALayer.__init__. This is the source of truth for the rig; the slim SOMA_neutral.npz no longer stores the rig fallback fields. Procedural-control topology and parameter metadata are loaded from SOMA_procedural_transforms.json.
The checked-in template is the v0027 SOMA template with procedural twist joints and updated skin weights. By default, SOMALayer keeps the expanded template skeleton and the public pose input remains the current 77 controllable SOMA joints. Passing enable_procedural_transforms=False derives the legacy 78-joint public rig in memory by pruning procedural/auxiliary joints and aggregating each removed joint's skinning weights to its nearest kept parent. SOMA_procedural_transforms.json defines procedural topology, rotation extraction, and sparse parameter matrices.
Keys supplied by the USD:
joint_names,joint_parent_idsbind_pose_world,bind_pose_local,t_pose_world,t_pose_localbind_shapeskinning_weights_{data,indices,indptr,shape}- Optionally
face_vert_indices,face_vert_counts,uv_data(when the skin mesh carries polygon + UV data)
Keys not covered by the USD (still loaded from SOMA_neutral.npz):
- Shape PCA (
mean,shapedirs,eigenvalues) - Mesh topology (
triangles,triangles_low, LOD maps) - Semantic segments (
segment_*) mirror_vert_indices, UV primvars stored as npz keys
Structure (top-level UsdSkelRoot at /OUTPUT):
/OUTPUT/c_skeleton_grp/Root--UsdSkelSkeletonwith joint hierarchy,bindTransforms,restTransforms/OUTPUT/c_skeleton_grp/Root/Animation--UsdSkelAnimation/OUTPUT/c_geometry_grp/MainMesh/Meshes/c_skin_mid-- the mid-LOD skin mesh (default_skin_mesh_nameforSOMALayer(..., lod="mid"))/OUTPUT/c_geometry_grp/MainMesh/Meshes/c_skin_lo-- the low-LOD skin mesh/OUTPUT/c_geometry_grp/MainMesh/Meshes/c_skin_xlo-- the extra-low-LOD skin mesh
The skin mesh name is resolved by load_lod_rig_from_usd, which prefers LOD-specific names such as c_skin_mid, c_skin_lo, and c_skin_xlo.
Procedural mode uses a single SOMA-owned procedural parameter transform with compiled rotation and translation matrices loaded from SOMA_procedural_transforms.json: forearm and shin twist come from hand and foot twist, while upper arm and thigh twist use reverse start/end compensation. The SOMA path is sidecar-driven and owns translation generation. rotation_extraction in the JSON selects local_x_euler, local_x_swing_twist, aligned_x_swing_twist, or per-procedural-joint mixes of those extractors. The checked-in SOMA sidecar uses aligned_x_swing_twist globally. local_x_euler reads the configured SOMA local-X twist channel from local Euler angles. local_x_swing_twist extracts the same configured channel by projecting a half-angle stabilized source quaternion onto its SOMA twist axis. The current pose-channel convention maps arm local-X twist to axis X, left leg local-X twist to negative axis Y, and right leg local-X twist to positive axis Y. The JSON translation matrix places twist helpers along the fitted public segment, preserving identity and body-part stretch instead of trusting independently fitted twist translations. Body pose correctives are supported only with procedural transforms and can be disabled by constructing SOMALayer(correctives_model_path=None).
Declarative procedural-control definition loaded by SOMALayer from
data_root. It is the authoritative source for supported extraction modes,
public 78-joint derivation from the 110-joint template, twist segments, sparse
rotation and translation parameter matrices, rotation extraction policy, and
evaluation order. SOMALayer requires this sidecar unless
enable_procedural_transforms=False; there is no
Python hard-coded SOMA twist topology or extraction-mode fallback. See
Procedural Control Format for the schema and
non-Python consumer plan.
Single-package PCA shape model + topology data for the neutral full-body mesh (gender: neutral, right-handed). Produced by the asset pipeline; its metadata field is a JSON string with model version, provenance, training-source information, and the asset split contract. Runtime rig data comes from SOMA_template_rig.usda, which is required for the slim NPZ.
| Key | Shape | Dtype | Description |
|---|---|---|---|
mean |
(V, 3) |
f32 | Mean (neutral) vertex positions in native cm. |
shapedirs |
(K, V*3) |
f32 | PCA shape directions flattened per vertex, K = 128. |
eigenvalues |
(K,) |
f32 | PCA eigenvalues; SOMAIdentityModel scales coeffs by sqrt(eigenvalues) before linear reconstruction. |
| Key | Shape | Dtype | Description |
|---|---|---|---|
triangles |
(T, 3) |
i32 | Triangle faces for the mid-LOD mesh; T = 36108. |
face_vert_indices |
(sum(counts),) |
i32 | USD-style flat per-face vertex-index stream (ngon-compatible). |
face_vert_counts |
(F,) |
i32 | USD-style per-face vertex count (3 or 4); F = 18054. |
mirror_vert_indices |
(V,) |
i32 | Per-vertex left/right mirror index. mirror_vert_indices[i] is the vertex symmetric to i across the midline. |
The slim asset no longer stores joint_names, joint_parent_ids, bind_pose_world, bind_pose_local, t_pose_world, t_pose_local, bind_shape, or skinning_weights_{data,indices,indptr,shape}. These arrays are loaded from SOMA_template_rig.usda.
The current SOMA_neutral.npz retains historical input reference orientations.
Both SOMALayer and SOMAHandLayer can use them offline. Set a default once:
from soma import SOMALayer
layer = SOMALayer(
reference_pose={"version": "v0.1.0"},
)
layer.prepare_identity(identity_coeffs)
out = layer.pose(rotations, pose2rot=False)forward() uses the same default. A call-time tensor or dictionary overrides it
for that call; omitted or None references inherit it. Without a constructor
reference, the current template is used.
out = layer.pose(
rotations,
pose2rot=False,
reference_pose={"version": "v0.3.0"},
)Dictionaries use get_reference_pose() arguments. Constructor dictionaries are
resolved once; call-time dictionaries resolve on each call. Stored references
follow the layer's device and dtype. To get a tensor directly, use
layer.get_reference_pose(version="v0.3.0"). The default data key is t_pose_world.
Version lookup selects the newest stored revision at or before the requested
version. Here, "v0.3.0" and "0.3.0" both select t_pose_world__v0.2.0.
SemVer precedence applies; build metadata is ignored. Requests newer than the
bundle use its latest known revision. Missing keys or requests before the first
revision raise KeyError. Invalid or conflicting selectors raise errors.
list_reference_poses() lists available entries. A full NPZ key (reference_id)
or source hash (asset_revision) selects an exact entry instead of a version.
Use one selector at a time. The exact alias pre-v0.1.0 selects the early
reference used to train Kimodo, with its original asset hash retained:
layer = SOMALayer(reference_pose={"alias": "pre-v0.1.0"})To convert rotation matrices once between references, use either layer's
convert_reference() method:
converted = layer.convert_reference(
rotations,
from_ref={"alias": "pre-v0.1.0"},
to_ref={"version": "v0.3.0"},
)Both references accept tensors or lookup dictionaries. Input and output are
(B, 77, 3, 3) matrices for the body or (B, 25, 3, 3) for a hand. Conversion
preserves absolute local rotations, device, dtype and gradients. It does not
change layer state or use the constructor default. No identity preparation is
needed. Pose the result using the target reference and pose2rot=False.
The getter returns a fresh tensor on the layer's device and dtype:
| Layer | Reference shape and frame | Input rotations |
|---|---|---|
SOMALayer |
(78, 3, 3), body world, including identity virtual Root |
77 joints, excluding Root |
SOMAHandLayer |
(25, 3, 3), hand world (current wrist bind frame), wrist first |
25 joints, including wrist |
Custom tensors use the same order and frame. They may also contain (J, 4, 4)
transforms; only rotation blocks are used. Rotations must be finite and in SO(3)
(absolute tolerance 1e-4). The body's virtual Root must be identity; the hand's
wrist reference need not be. Tensor gradients are preserved.
absolute_pose=True bypasses the constructor default. Combining it with an
explicit call-time reference_pose is an error.
Reference selection changes how rotations are interpreted. Geometry, bind transforms and identity models remain current; this does not reproduce an entire old model. Historical translations and internal twist joints are not included. Assets without history still accept custom tensors.
Only changes add snapshots. The bundle stores v0.1.0, v0.2.0 and an earlier asset-hash reference. Both layers read this shared history; hands select their joints and convert orientations into the current wrist bind frame.
| NPZ key | Contents |
|---|---|
t_pose_world__v0.1.0 |
(78, 3, 3) float32 public world rotations |
t_pose_world__v0.1.0__joint_names |
(78,) Unicode names, Root first |
t_pose_world__v0.1.0__parent_ids |
(78,) integer parent indices, Root parent 0 |
reference_pose_history_metadata |
Unicode JSON: schema version 2, default reference ID, conventions and entry metadata |
Other snapshots use the same suffixes. Unpublished sources use
t_pose_world__sha256_<source-asset-hash>. Each metadata entry records its key,
data key, version or source hash, provenance and orientation checksum.
The default reference ID describes the stored history; it does not change the
runtime template.
Three UV sets (st, st1, st2) preserved from the source USD. Each uses faceVarying interpolation: indices are flat per-face-corner lookups into uv_coord_*.
| Key | Shape | Dtype | Description |
|---|---|---|---|
uv_coord_st / uv_coord_st1 / uv_coord_st2 |
(U, 2) |
f32 | UV coordinates; U varies per set (~19.6k). |
uv_indices_st / uv_indices_st1 / uv_indices_st2 |
(sum(counts),) |
i32 | Per-face-corner UV index lookup. |
uv_interp_st / uv_interp_st1 / uv_interp_st2 |
scalar | <U11 |
Always faceVarying. |
A ~1:4 vertex subset for faster inference (SOMALayer(..., lod="low"); legacy alias low_lod=True). The low-LOD mesh is a strict vertex-index subset of the mid-LOD mesh.
| Key | Shape | Dtype | Description |
|---|---|---|---|
lod_mid_to_low |
(V_lo,) |
i32 | Mid-LOD vertex indices that survive in the low-LOD mesh. V_lo = 4505. |
triangles_low |
(T_lo, 3) |
i32 | Low-LOD triangles in low-LOD vertex index space; T_lo = 9006. |
face_vert_indices_low |
(sum,) |
i32 | Flat per-face vertex-index stream for low-LOD. |
face_vert_counts_low |
(F_lo,) |
i32 | Per-face vertex count for low-LOD; F_lo = 4524. |
SOMALayer(..., lod="xlo") returns 612 body vertices with the same SOMA skeleton and identity backends, but loads the extra-low mesh topology, bind vertices, skinning weights, and UVs from the xlo mesh in assets/SOMA_template_rig.usda. Unlike lod="low", xlo is not a strict vertex-index subset of the mid mesh. Runtime identity shapes and pose correctives are transferred from the mid SOMA bind geometry to xlo with barycentric interpolation. Identity-dependent skeleton fitting still uses the low-LOD mesh from the same v0027 USD internally, because direct xlo fitting is too sparse around limbs for stable joint placement across random body shapes.
The expected asset is the v0027 SOMA USD template, stored under the canonical SOMA_template_rig.usda filename. The loader auto-detects skinned LOD meshes using names such as c_skin_mid, c_skin_lo, and c_skin_xlo.
Each entry is a flat int32 array of vertex IDs into the mid-LOD mesh. Useful for masking/filtering (e.g. excluding inner geometry from PoseInversion).
| Key | Description |
|---|---|
segment_head |
Head (face + skull surface). |
segment_feet |
Feet. |
segment_torso |
Torso. |
segment_mouth_bag |
Inner mouth geometry (excluded from pose inversion). |
segment_eye_bags |
Inner eye geometry (excluded from pose inversion). |
segment_between_toes |
Inter-toe geometry. |
segment_hair |
Hair strands. |
segment_haircap |
Hair cap. |
segment_armpits |
Armpits. |
| Key | Shape | Dtype | Description |
|---|---|---|---|
metadata |
scalar | unicode | JSON string: model_version, gender, units, up_axis, forward_axis, handedness, asset_contract, and provenance (training sources, PCA config, rig source). |
The example contains only a standalone /Animation SkelAnimation prim, with
no skeleton definition, bind/rest transforms, meshes, or external dependencies.
It authors only the 78 public joints, including Root. Twist-joint transforms are
generated by procedural evaluation.
It is a Y-up UsdSkel animation in centimeters at 24 time codes per
second, with a playback range of 0 through 930. Its joint paths identify the
animated joints; rotations are absolute local rotations. Consumers must sample
the playback range, including interpolated and held frames, rather than treating
the sparse authored keys as evenly spaced frames.
The general reader soma.io.load_usd_animation_sequence loads joint paths,
local transforms, scales, sample times, and stage unit/axis/timing metadata.
It supports animation-only files without requiring a rig or SOMA joint layout.
soma.io.load_soma_animation maps joint names to the requested order and
converts translations to meters; world_space=True composes authored ancestors
before selecting joints.
Both SOMALayer and SOMAHandLayer expose
load_motion(path, *, absolute_pose=False, reference_pose=None), returning
(poses, translation, fps). Body poses contain 77 rotation matrices; hand poses
contain 25, including wrist rotation in the selected representation. Translations use the layer's output
units. The hand loader accepts full-body and hand-only USD animations with this
hand's joint names and hierarchy, including cropped hand paths with a common
body prefix. Animated wrist ancestors are composed; gaps within an authored
ancestor chain are rejected. No separate wrist transform is needed.
Reference selection matches pose(): an explicit tensor or
get_reference_pose lookup dictionary, otherwise the constructor default,
otherwise the template T-pose. Absolute output bypasses the default and rejects
an explicit reference. Use the same options when loading and posing:
options = {"reference_pose": {"version": "v0.2.0"}}
# Or: options = {"absolute_pose": True}
poses, translation, fps = layer.load_motion("motion.usd", **options)
layer.prepare_identity(identity_coeffs)
body_out = layer.pose(poses, transl=translation, pose2rot=False, **options)
# For SOMAHandLayer, pass global_translation=translation instead of transl.These APIs require no prepared identity for loading and preserve the animation's
frame rate. Absolute means parent-local rotations, with world rotation at the
hand's root wrist; relative output follows the selected reference convention.
The formerly distributed example_animation.npy has been retired; the current
0.3 asset bundle ships the USD instead. Legacy NPY motion input is no longer
supported; provide a USD animation to the demos and diagnostics.
Each non-SOMA identity backend (mhr, smpl, smplx, anny, garment) has its own subdirectory under data_root/ holding a pair of OBJ meshes that define the mesh correspondence to SOMA topology (plus, for some backends, the backend's native identity model).
Every backend folder contains:
base_body.obj(orbase_body_lod{1,6}.objfor MHR, ormean.objfor Garment) -- the backend's native-topology mesh in its own native frame and unit.SOMA_wrap.obj(orSOMA_wrap_lod1.obj) -- the SOMA-topology mesh wrapped onto the native mesh's surface.
Together these define a surface-to-surface correspondence: vertex i of SOMA_wrap.obj sits on (or very near) the native mesh's surface, at the same material point across every pose / shape. At __init__ time, each identity model calls _setup_topology_transfer[_with_blending](V_native, F_native, V_soma, F_soma, ...) to precompute the barycentric transfer that maps a shaped native mesh back into SOMA topology. For backends that leave a head region not covered by the wrap (MHR, SMPL*), a Laplacian blend at the boundary smooths the transition.
The OBJ meshes are loaded with trimesh.load(..., maintain_order=True, process=False) so vertex ordering is preserved -- do not reprocess them with any tool that reorders vertices.
| Folder | Backend class | Checked-in native model | OBJ correspondence pair |
|---|---|---|---|
MHR/ |
MHRIdentityModel |
mhr_model_lod{1,6}.pt (TorchScript) |
base_body_lod{1,6}.obj + SOMA_wrap_lod1.obj |
SMPL/ |
SMPLIdentityModel (type smpl) |
-- (see below) | base_body.obj + SOMA_wrap.obj |
SMPLX/ |
SMPLIdentityModel (type smplx) |
-- (see below) | base_body.obj + SOMA_wrap.obj |
Anny/ |
AnnyIdentityModel |
(loaded from the anny Python package at runtime) |
base_body.obj + SOMA_wrap.obj |
GarmentMeasurements/ |
GarmentMeasurementIdentityModel |
-- (see below) | mean.obj + SOMA_wrap.obj |
The SMPL / SMPL-X identity models are not redistributed with SOMA. To use those backends, download the official models and place them in the corresponding folder:
| Backend | Expected file(s) | Source |
|---|---|---|
smpl |
SMPL/SMPL_NEUTRAL.pkl |
SMPL |
smplx |
SMPLX/SMPLX_NEUTRAL.pkl |
SMPL-X |
SMPLIdentityModel raises FileNotFoundError at __init__ with a pointer to these filenames if the files are missing. An explicit path can be passed via identity_model_kwargs={"model_path": ...}.
The garment backend expects GarmentMeasurements/point.npz, which must be generated locally from the publicly available point.pca binary distributed with the upstream GarmentMeasurements repo. Convert with tools/convert_gm_pca_to_npz.py:
git clone https://github.com/mbotsch/GarmentMeasurements
python tools/convert_gm_pca_to_npz.py ./GarmentMeasurements/data/pca/point.pca assets/GarmentMeasurements/point.npz