Skip to content

Latest commit

 

History

614 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

OpenUSD VRM Avatar Plugins

CI OpenUSD 26.08 License: Apache-2.0

OpenUSD plugins for VRM avatars.

VRM animation retargeting preview in usdview with Hydra Storm

A VRM scene loaded directly through the file-format plugin, with a VRMA motion file retargeted onto the avatar, previewed in usdview with Hydra Storm.

This repository is an OpenUSD plugin workspace: it separates schema definitions, file-format import, package resolution, and shared GLB container parsing into independently buildable, independently testable components. The v0.9.0 release is the OpenExec foundation: two OpenExec bundles, execMotion and execVrm, evaluate a humanoid through OpenExec as thin wrappers over the motion libraries and agree with the offline bake bit for bit, from the installed product. That brings the workspace to six plugin bundles, twelve shared libraries, and seven CLIs.

The importer reads VRM 0.x and 1.0, normalizes the differences away, and authors a static USD stage. It never evaluates or simulates — that boundary is the project's central design decision, and it is described below.

Built with OpenStrata. usd-vrm-plugins is OpenStrata's first external adopter, and the ost CLI is how this workspace is built, tested, packaged, and released. The record of adopting it — every version from pre-0.3 to 0.22.2, including what broke — is published in docs/reports/ost/. The repo is dual-mode: everything also builds with plain CMake against any OpenUSD install, with no ost involved.

Workspace components

Component Type Role Status
vrmSchema USD schema bundle (usd-schema) VRM typed API schemas + the schema contract Shipped
usdVrmFileFormat SdfFileFormat bundle (usd-fileformat) .vrm parsing, canonicalization, USD authoring Shipped
usdVrmPackageResolver ArPackageResolver bundle (usd-package-resolver) Embedded resource resolution from .vrm Shipped
vrmContainer Plain CMake library GLB parsing + byte-range validation Shipped
usdVrmaFileFormat SdfFileFormat bundle (usd-fileformat) .vrma motion clips → canonical UsdSkelAnimation v0.3.0
vrmRetarget Plain static CMake library Humanoid mapping, rest-pose correction, root-motion policy, pose retargeter v0.4.0
motion_retarget CLI executable Bakes a semantic clip onto a target rig as UsdSkelAnimation v0.4.0
motion_capture CLI executable Replays a recorded capture session into a semantic clip the above consumes unchanged v0.5.0
motionSource Plain static CMake library Format-neutral source skeleton / animation model, the producer-profile contract, and the converter to canonical humanoid motion v0.7.0
motionBvh Plain static CMake library BVH syntax and extraction only — no producer semantics, no default profile v0.7.0
motion_bvh_inspect CLI executable Reports what a BVH file contains — hierarchy, channels in declaration order, frames, and per-column value ranges v0.7.0
motion_bvh_convert CLI executable Converts a BVH file to the avatar-independent semantic clip under an explicitly named profile v0.7.0
execMotion OpenExec bundle Vendor-neutral motion computations over UsdSkelAnimation: sample, filter, root-motion intake, history interpolation and blend v0.9.0
execVrm OpenExec bundle VRM retarget computations over the applied VrmHumanoidAPI, equal to motion_retarget's bake bit for bit v0.9.0
usdVrm Aggregate product name Composed distribution of the workspace Shipped via ost plugin package --workspace --product

usdVrm is not a bundle id — it names the product as a whole. It was the file-format bundle's name until the workspace split; documentation and artifacts that predate that rename use it in the old sense.

The motion layer

The core and the runtime have moved too (2026-09-21), and are consumed. motionCore and motionRuntime are usd-motion-plugins' motionCore, motionSampling and motionRecording; this workspace resolves them as published packages, pinned by digest, and builds neither. Every type this product's motion layer speaks — MotionPose, MotionClip, HumanJoint — is that repository's vocabulary now, under openstrata::motion.

The live inputs have moved (2026-09-21). liveTransport, osc, motionTracking, the three adapters and their record tools are motion-connectors' now, under motionConnector* names, and this repository no longer builds, ships or tests any of them. The generic half of this layer — motionCore, motionRuntime, the generic retarget, motionSource, motionBvh, motion_capture and execMotion — moves to usd-motion-plugins next. This repository keeps VRM and VRMA, VRM semantic resolution and execVrm, and consumes the rest as installed packages (WORKSPACE.md §9, the migration plan). The table below describes the tree as it is today.

motionCore and usdVrmaFileFormat were the v0.3.0 foundation; v0.4.0 added motionRuntime, vrmRetarget, and the motion_retarget CLI, which together make a .vrma clip play back on a real avatar. v0.5.0 adds the observation side — a vendor-neutral LiveCaptureSource, a recorded-trace format, and the motion_capture CLI — which produces the same semantic clip, so a live session is baked by the retarget tool unchanged. The fixed contract is docs/design/MOTION_CONTRACT.md. v0.6.0 supplies the first product-specific input leaf: vrmAdapterVmc decodes VMC Protocol from OSC-over-UDP through frame assembly and VRM bone mapping into the existing LiveCaptureSource; vmc_record records the same wire input for inspection and corpus work, and --export-trace hands what the adapter delivered to motion_capture as a plain capture trace — the product's tools consume a live VMC session without linking the adapter, or knowing it exists. v0.7.0 adds vrmAdapterMocopi and mocopi_record on that same shape, and a body that travels: a rig whose only translating joint is the hips now composes RootMotion, so a live session no longer retargets in place.

v0.7.0 supplies the other half of the input layer, and the first evidence off real hardware. A capture product sends packets and writes files: the packets go through vrmAdapterMocopi, a native UDP path for a wire grammar with no published specification, and the files go through a generic BVH pipeline (motionBvh + motionSource + a declarative producer profile) that is deliberately not that product's importer. The two halves meet at motionCore and nowhere earlier — and when one physical session is observed both ways, they agree to a median 0.084° per bone (report 01). OpenExec evaluation follows: execMotion and execVrm re-evaluate that pipeline, and on the recorded export they agree with the offline bake bit for bit. They shipped in v0.9.0. What comes next: docs/roadmap/.

Component Type Role
usdVrmaFileFormat SdfFileFormat bundle .vrma motion clips → UsdSkelAnimation on a canonical semantic humanoid skeleton
vrmRetarget Plain static CMake library Humanoid mapping, rest-pose correction, root-motion policy, pose retargeter
motion_retarget CLI executable The stage half: reads the rig and the clip, bakes the retargeted UsdSkelAnimation, binds skel:animationSource
execMotion OpenExec bundle Vendor-neutral motion nodes over UsdSkelAnimation: sample, filter, root-motion intake, history interpolation and blend — the OpenExec plan's P0-4 node set
execVrm OpenExec bundle VRM semantics over the applied VrmHumanoidAPI: the target rig, the humanoid map, rest-pose correction, one sample's retarget under the root-motion statements, the bake's joint transforms and the retarget's diagnostics — each a wrapper over vrmRetarget, and equal to motion_retarget's bake bit for bit. Expression and look-at computations follow on the ExecIr track
motionSource · motionBvh Plain static CMake libraries Recorded-file input: BVH syntax, a format-neutral source model, and conversion to canonical humanoid motion under an explicit producer profile
profiles/motion/ Package data One declarative file per producer and export preset. Product names live here rather than in the libraries that read them

.vrm and .vrma are deliberately separate file-format plugins with symmetric structure, and they compose by reference, not subLayer — a subLayer stack cannot express which skeleton a clip applies to. A third binding/assembly layer relates them.

Dependencies

usdVrmFileFormat ───────> vrmSchema
        │
        └───────────────> vrmContainer

usdVrmPackageResolver ──> vrmContainer

usdVrmaFileFormat ──────> vrmContainer, motionCore

motionRuntime ──────────> motionCore
vrmRetarget ────────────> motionCore, motionRuntime
motion_retarget (CLI) ──> vrmRetarget + OpenUSD stage APIs

vrmAdapterVmc ──────────> motionCore, motionRuntime, liveTransport, osc
vrmAdapterMocopi ───────> motionCore, motionRuntime, liveTransport
vrmAdapterVrchatOsc ────> motionCore, liveTransport, osc (no motionRuntime: it
                          stops at an observation, and a pose is what reaches
                          that library)
liveTransport ──────────> nothing — its allowed edge set is empty, not short
osc ────────────────────> nothing — the same, `liveTransport` included
motionTracking ─────────> nothing — the same again, and for a third reason: it
                          maps one vocabulary it owns onto another

motionSource ───────────> motionCore
motionBvh ──────────────> motionSource
motion_bvh_convert ─────> motionBvh, motionSource, OpenUSD stage

                          (planned)
execMotion ─────────────> motionCore, motionRuntime
execVrm ────────────────> vrmSchema, vrmRetarget

Five rules keep those edges honest:

  • vrmSchema depends on no other bundle or library.
  • usdVrmPackageResolver never links the file-format bundle; the importer's dependency on the resolver is runtime-only, never link-time.
  • execVrm reads the schema contract from the stage — never the importer's private API or canonical model.
  • vrmRetarget does not depend on OpenExec. The retarget core is finished and testable before any OpenExec node exists; the nodes are thin wrappers.
  • Adapters depend on the core. The core never depends on an adapter, and motionCore never sees a vendor SDK, a network protocol, or a product name.
  • A file reader knows a format and no semantics; motionSource knows semantics and no format. motionBvh → motionSource never reverses, so a second reader can be added without changing anything above it.
  • Live input and recorded files meet at motionCore and nowhere earlier. An adapter never reaches for a reader, and a reader never reaches for an adapter.

The bundle graph is validated by ost plugin test --workspace, and each consumer adds a binary link check proving what it does and does not import. Full contract: docs/architecture/WORKSPACE.md.

What the importer produces

.vrm is read as a GLB container (via vendored cgltf v1.15) and normalized — VRM 0.x and 1.0 differences are absorbed into a canonical model before any USD is authored — into:

/Asset                     SkelRoot (or Xform when there is no skeleton), kind=component
  customData.vrm.*         sourceFormat / sourceVersion / specVersion / meta / rawExtension
  geo/                     Scope of UsdGeomMesh (one per glTF primitive)
    <Mesh>                 points/normals/st, material binding; skel binding when
                           skinned, else the glTF node transform as xformOp
  mtl/<Material>           UsdShadeMaterial: identity, binding target, VRM semantics
    preview/               UsdShadeNodeGraph holding the UsdPreviewSurface network
  skel/Skeleton            single UsdSkelSkeleton unified across all glTF skins
                           (bind transforms from the inverse bind matrices)
  rig/Humanoid             vrm:humanBones:<bone> joint tokens, typed VrmHumanoidAPI

Every /Asset/rig/* control prim carries typed schema data. The schema types themselves are provided by the vrmSchema bundle; usdVrmFileFormat depends on schema contract version 1 and authors against it. Raw VRM blocks stay in customData as the lossless fallback.

Runtime boundary

Import:   VRM bytes ──> canonical model ──> USD stage
Runtime:  USD stage + vrmSchema ──> OpenExec / DCC / renderer runtime

The importer authors data only:

  • Import is deterministic. The same bytes produce the same stage.
  • LookAt, node constraints, and spring bones are written as typed schema data, never executed.
  • Evaluation and simulation belong to execVrm (planned) or an external runtime.
  • No physics runs at import time.

This keeps import pure, so a runtime can be swapped without touching the importer.

Feature support

VRM 0.x / 1.0 detection and canonicalization, geometry, UsdPreviewSurface materials with the full texture set, MToon source preservation (vrm:mtoon:raw; renderer-specific realization is not implemented), unified skeleton + skinning from inverse bind matrices, skeletal animation, humanoid mapping, front-direction normalization, and a coded diagnostic taxonomy.

Per-feature status is in docs/reference/CAPABILITY_MATRIX.md. Supported platforms, OpenUSD versions, and build requirements are in docs/reference/SUPPORTED_CONFIGURATIONS.md. The schema contract is in plugins/vrmSchema/docs/SCHEMA_CONTRACT.md.

Install

See docs/guides/INSTALL.md for release-artifact, OpenStrata, and from-source installation, verification, and troubleshooting.

Install the components you use from the release artifacts. Each release publishes four member bundles and one aggregate product archive. The three VRM bundles are installed together; usdVrmaFileFormat is independently installable because it has no plugin-bundle dependency. The member bundles are separately addressable, while the aggregate archive keeps the exact workspace closure together. See the install guide for extraction and verification.

Build and test

Whole workspace, with OpenStrata (ost)

Requires ost 0.19+, so requires.bundles and requires.libraries are composed automatically.

# One-time: adopt an OpenUSD install as the cy2026 runtime.
ost runtime pull cy2026 --profile usd --from-usd /path/to/openusd-install

# Validate the bundle graph, then test every bundle in dependency order.
ost plugin test --workspace

A single bundle

ost plugin build plugins/usdVrmFileFormat
ost plugin test  plugins/usdVrmFileFormat            # L0-L5 verification pyramid

ost plugin build plugins/usdVrmaFileFormat
ost plugin test  plugins/usdVrmaFileFormat           # L0-L5 + VRMA golden

# Inspect a real avatar. build/test/run/package compose the manifest's
# requires.bundles closure automatically:
ost plugin run plugins/usdVrmFileFormat \
    -- python plugins/usdVrmFileFormat/tools/inspect_vrm.py avatar.vrm

# `view` / `test-view` are the exception: they load only what --with names, so
# the runtime siblings must be spelled out or the schema apply fails.
ost plugin view plugins/usdVrmFileFormat avatar.vrm \
    --with plugins/vrmSchema --with plugins/usdVrmPackageResolver

With plain CMake (no OpenStrata)

The workspace root composes every bundle:

cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/openusd-install
cmake --build build --config Release
ctest --test-dir build -C Release

The motion layer's suites carry CTest labels, so one layer runs on its own: motion.core, motion.runtime, motion.retarget, motion.cli, motion.integration, motion.openexec and motion.real-corpus (ctest --test-dir build -C Release -L motion.openexec). The OpenExec plan (P0-2) says what each names.

Each bundle also builds standalone against installed sibling packages (find_package(vrmSchema CONFIG REQUIRED)), which is what CI proves; a bundle never reaches sideways into a sibling's source tree.

The built libUsdVrmFileFormat.{dll,so,dylib} lands in plugins/usdVrmFileFormat/lib/; add plugins/usdVrmFileFormat/plugin/resources/usdVrmFileFormat to PXR_PLUGINPATH_NAME and the lib/ dir to your dynamic-loader path to use it.

Clean-install smoke

Verifies the packaged bundles have no build-tree dependency:

python scripts/clean_install_smoke.py               # build + package + extract + smoke
python scripts/clean_install_smoke.py --skip-build   # reuse the current build

It packages the three VRM bundles with ost, extracts them into a fresh directory outside the repo, and runs the assertions in plugins/usdVrmFileFormat/tests/clean_install_smoke.py against that extracted tree: .vrm discovery served from the package, a textured fixture and a corpus avatar open and validate, and an embedded texture resolves straight from the .vrm container. Needs ost + a validated cy2026 runtime.

CI

CI is generated from the support matrix in openstrata.ci.yaml (ost ci generate github). The PR lane (.github/workflows/ost-source-ci.yml) runs seven cells against digest-pinned cy2026 runtimes on hosted Windows / macOS arm64 / Linux:

  • One graph cell (verify: graph), which runs ost plugin test --workspace --graph-only — the WORKSPACE.md §2 dependency-direction gate — before anything is built, in milliseconds.
  • Three workspace cells (kind: workspace), one per OS, which build the root CMake tree and run its CTest suite. This is the behavioral lane: the root tree is the only configuration in which the plain libraries and the CLI tools exist, and its suite also contains every bundle's own tests, so it is the coverage motionCore, motionRuntime, vrmRetarget, vrmContainer, motion_retarget, motion_capture, all four plugin bundles and the whole-workspace usdvrm_baseline gate get.
  • Three bundle cellsusdVrmFileFormat on each OS — which build that bundle standalone (ost plugin build, no root tree in scope), run its pyramid (--up-to 5; Windows is capped at 4), and ost plugin package it. Neither the standalone configure nor packaging is reachable from a workspace cell, and they are per-platform, which is what these three are for.

There were sixteen cells until 2026-08-30 — all four bundles on all three OS. Nine were removed as measured duplicates of the workspace suite; openstrata.ci.yaml carries the evidence and what to re-run before adding them back. There is no scheduled lane any more: its one cell targeted a self-hosted runner that does not exist and had been cancelled weekly since 2026-07-27.

Release artifacts

Pushing a tag vX.Y.Z (matching VERSION, with that version's CHANGELOG.md section finalized) runs .github/workflows/release.yml: it builds on all three OS cells, proves the packaged artifact (packaged-artifact verification, clean-install smoke, digest-reproducible packaging), and assembles a draft GitHub release — per-target lean + debug bundles, a source archive, SHA256SUMS, and notes rendered from CHANGELOG.md via docs/contributing/RELEASE_NOTES_TEMPLATE.md. Publishing the draft is a human decision. Run the workflow manually (workflow_dispatch) for a dry run that creates no release.

usdVrmFileFormat carries a buildInfo.json stamp (commit / toolchain / OpenUSD release and PXR_VERSION / OpenExec components / build type / schema contract version), surfaced by tools/vrm_report.py.

Every bundle is built against OpenUSD 26.08 and nothing else, and against a 26.08 that carries OpenExec. Both are enforced at configure time by cmake/UsdVrmOpenUsd.cmake, for ost and plain-CMake builds alike — see supported configurations.

Documentation

docs/ is organized by responsibility — the same layout open-strata and hydra-merlin use:

docs/architecture/ The binding workspace contract: identities, dependency directions, artifact naming
docs/guides/ How to install
docs/reference/ What is supported, on what
docs/roadmap/ What is planned next (incomplete work only)
docs/releases/ Per-version release records
docs/design/ Why the significant decisions were made
docs/reports/ Evidence from real runs: the ost dogfooding series + the delivery log

Release history is in the CHANGELOG; the release version lives in the single-source VERSION file.

Contributing

Small fixes, documentation updates, tests, and questions are welcome. See CONTRIBUTING.md for the short setup and pull request guide. Please also read the Code of Conduct and Security Policy.

License

Original source and documentation: Apache-2.0 (see LICENSE). Third-party components keep their own licenses; see THIRD_PARTY_NOTICES.md. cgltf v1.15 is vendored under third_party/cgltf with its MIT license.

Local test VRM avatars used during development are not part of this repository and are not redistributed here; mind their individual licenses.