OpenUSD plugins for VRM avatars.
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-pluginsis OpenStrata's first external adopter, and theostCLI 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 noostinvolved.
| 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 core and the runtime have moved too (2026-09-21), and are consumed.
motionCoreandmotionRuntimeareusd-motion-plugins'motionCore,motionSamplingandmotionRecording; 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, underopenstrata::motion.The live inputs have moved (2026-09-21).
liveTransport,osc,motionTracking, the three adapters and their record tools aremotion-connectors' now, undermotionConnector*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_captureandexecMotion— moves tousd-motion-pluginsnext. This repository keeps VRM and VRMA, VRM semantic resolution andexecVrm, 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.
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:
vrmSchemadepends on no other bundle or library.usdVrmPackageResolvernever links the file-format bundle; the importer's dependency on the resolver is runtime-only, never link-time.execVrmreads the schema contract from the stage — never the importer's private API or canonical model.vrmRetargetdoes 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
motionCorenever sees a vendor SDK, a network protocol, or a product name. - A file reader knows a format and no semantics;
motionSourceknows semantics and no format.motionBvh → motionSourcenever reverses, so a second reader can be added without changing anything above it. - Live input and recorded files meet at
motionCoreand 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.
.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.
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.
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.
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;
usdVrmaFileFormatis 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.
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 --workspaceost 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/usdVrmPackageResolverThe 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 ReleaseThe 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.
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 buildIt 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 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 runsost 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 coveragemotionCore,motionRuntime,vrmRetarget,vrmContainer,motion_retarget,motion_capture, all four plugin bundles and the whole-workspaceusdvrm_baselinegate get. - Three bundle cells —
usdVrmFileFormaton 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), andost plugin packageit. 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.
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.
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.
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.
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.
