Imports .vrm (VRM 0.x / VRM 1.0) avatars as a normalized OpenUSD stage. The
goal is not a throwaway "viewable USD" but a structure that keeps VRM semantics
trackable and reconnectable to downstream runtimes.
Scaffolded with ost plugin new usd-fileformat usdVrm --extension vrm, then
extended with a real importer.
openstrata.plugin.yaml bundle contract (identity, runtime range, provides, tests)
CMakeLists.txt builds libUsdVrmFileFormat.{dll,dylib,so} into lib/
cmake/Dependencies.cmake fetches cgltf (pinned) at configure time
../../libs/vrmContainer/ GLB header/chunk + immutable byte-view library
src/
UsdVrmFileFormat.{h,cpp} SdfFileFormat entry point (CanRead/Read/WriteToString)
io/ VrmDocumentReader interface + cgltf implementation
model/VrmCanonicalDocument.h parser-independent intermediate model (0.x/1.0 normalized)
usd/UsdVrmAuthorer.{h,cpp} canonical model -> USD scene description
resolver/ ArPackageResolver for .vrm-embedded texture assets
util/ path sanitize/uniquify, glTF->USD transform conversion
plugin/resources/usdVrmFileFormat/plugInfo.json.in target-suffix template for USD plugin registration
plugin/resources/usdVrmFileFormat/plugInfo.json generated/inspectable USD plugin registration
tools/ generate_fixtures.py, vrm_fixture_lib.py, inspect_vrm.py,
package_vrm.py
tests/ python smoke tests + generated fixtures (minimal, vrm0_minimal, multiskin_ibm, names, materials, badext) + invalid.vrm
.vrm bytes ─▶ vrmContainer ─▶ CgltfVrmDocumentReader ─▶ VrmCanonicalDocument ─▶ UsdVrmAuthorer ─▶ USDA
(GLB + byte (cgltf + pxr/base/js) (no glTF/USD types (UsdGeom/
ranges; no USD) across this seam) UsdSkel)
- vrmContainer validates untrusted GLB headers/chunks and buffer-view ranges, exposes immutable non-owning byte views, and owns the stable content-addressed embedded-resource naming shared with the package resolver. Its public API has no OpenUSD types or plugin registration.
- Reader owns all cgltf contact and VRM-extension JSON parsing (via USD's own
pxr/base/js, so there is no external JSON dependency). The plan's nominalVRM.hdependency does not exist as a real library; its role — interpreting the VRM/VRMC_vrm extension blocks — is fulfilled here, which also keeps the third-party ABI surface to just cgltf (compiled into one TU, never exported). - Canonical model absorbs VRM 0.x vs 1.0 structural differences.
- Authorer consumes only the canonical model and emits USDA on a worker
thread (USD's reload path runs
Readunder an outerSdfChangeBlock).
- Coordinate system: glTF and USD are both right-handed, Y-up, metric, so
geometry needs no axis flip. Only conversions: glTF column-major matrices →
USD row-vector
GfMatrix4d(a transpose, done for free by reading the array in order), and UVV := 1 - V. All of it lives inutil/TransformUtil. - Front direction: VRM 0.x avatars face -Z; VRM 1.0 standardized on
+Z. To keep every imported avatar consistent (and matching the current
standard), VRM 0.x is rotated 180° about the up axis at the root (
/Asset) — a single SkelRoot-level transform, so skinning, animation, blend shapes and lookAt all keep working untouched. The source orientation is recorded incustomData(vrm:sourceFrontAxis,vrm:frontAxisNormalized). - One unified skeleton: VRM avatars are commonly split across several glTF
skins referencing a shared joint hierarchy. The reader takes the union of
all skin joints into a single
UsdSkelSkeletonand remaps each mesh'sJOINTS_0into that order. (Importing only the first skin yields a partial skeleton — one test avatar dropped from 128 joints / 51 humanoid bones to 23 / 1.) Bind transforms come from each skin's inverse bind matrices (bind = inverse(IBM)), not the joint node world transforms. - Mesh placement: skinned mesh verts live in skeleton space (the glTF node
transform is ignored, per spec;
geomBindTransformcarries the bind placement). A non-skinned mesh instead gets its glTF node world transform authored as anxformOp:transform, so node-placed accessories don't collapse to the origin. /Assetis aSkelRootwhen a skeleton exists (UsdSkel needs a SkelRoot ancestor enclosing the skinned meshes and skeleton), otherwise a plainXform. It is alwayskind=componentand the stage default prim — a deliberate, documented deviation from the plan's "always Xform".rig/Humanoidis control semantics, not a bone copy: it carries one resolvablevrm:skeletonrelationship to the Skeleton prim plus avrm:humanBones:<bone>token attribute per bone holding the joint path (joints areSkeleton.jointstokens, not prims, so a relationship can't target them directly). Never a duplicated joint hierarchy. These are formalized by a typed, compiledVrmHumanoidAPIapplied schema owned by the siblingvrmSchemabundle: standard VRM bones are schema builtins, non-standard / VRM-0.x-only bones fall back to custom attributes (still lossless).- Typed schemas across the rig: every control prim carries a compiled
applied schema —
VrmExpressionAPI(Expressions),VrmLookAtAPI(LookAt),VrmSpringBoneAPI+VrmColliderAPI(SecondaryMotion), andVrmConstraintAPI(Constraints). All are owned, generated, and registered by thevrmSchemabundle (workspace Phase 1 split); this importer links its typed API viafind_package(vrmSchema). The public schema contract v1, versioning policy, raw-to-typed correspondence table, and validator rules are in../vrmSchema/docs/SCHEMA_CONTRACT.md. - Lossless preservation: VRM
meta/specVersion, the full raw VRM/VRMC_vrm extension block (vrm:rawExtension), and provenance (sourceNodeIndex, …) are kept incustomDataunder avrmnamespace.
The importer build-out is complete and released; the forward work is doc/impl sync, runtime verification coverage, and the OpenExec runtime layer. See the project design & development policy and the per-phase roadmap (P0–P6) for live status.
Implemented: GLB read, version detection, meshes
(points/normals/UV/indices), UsdPreviewSurface materials with textures
(base color, metallic-roughness, normal, emissive, occlusion; wrap modes,
KHR_texture_transform), MToon metadata (vrm:shaderModel + vrm:mtoon:raw),
unified skeleton (bind from inverse bind matrices, topologically ordered) +
skinning binding, humanoid mapping, blend shapes (UsdSkelBlendShape) and VRM
expressions (/Asset/rig/Expressions, morph-target bindings), glTF skeletal
animation (UsdSkelAnimation joint TRS, bound + stage time range),
LookAt (/Asset/rig/LookAt: type + eye joint tokens + preserved curves),
SpringBone (/Asset/rig/SecondaryMotion: spring chains + collider groups,
data only — no simulation), VRM meta + raw-extension preservation, graceful
warnings on unsupported data.
VRM constraints, LookAt, and SpringBone are authored as typed schema data only —
their evaluation/simulation is a separate runtime layer (execVrm, roadmap P4),
never run by this file-format plugin. Not yet: morph-weight (blend-shape) animation
(only joint TRS clips are authored today), full MToon shading (roadmap P5; only
approximated via UsdPreviewSurface + vrm:mtoon:raw metadata today), and KTX2/WebP
image decode.
Embedded PNG/JPEG images are referenced as package-relative assets inside the
source .vrm, e.g. avatar.vrm[images/<hash>.png]. The sibling
usdVrmPackageResolver bundle serves those bytes
directly to OpenUSD/Hio, so normal imports do not depend on an OS temp
extraction cache. It is a runtime dependency: this file-format plugin
authors the package paths but does not link the resolver (WORKSPACE.md §2), so a
session must also load usdVrmPackageResolver for embedded textures to resolve.
Image format is detected by sniffing the magic bytes (some VRM exporters drop
the glTF mimeType).
For portable handoff, run the package tool inside a usdVrm-enabled OpenUSD
environment. It opens the source stage, copies texture files into a package-local
textures/ directory, rewrites UsdUVTexture.inputs:file to relative asset
paths, exports asset.usda, and writes package_report.json as the asset
inventory:
ost plugin run plugins/usdVrmFileFormat -- python plugins/usdVrmFileFormat/tools/package_vrm.py avatar.vrm out/avatarRequires ost 0.16+ (from the repo root):
ost plugin build plugins/usdVrmFileFormat # build libUsdVrmFileFormat into lib/
ost plugin test plugins/usdVrmFileFormat # L0-L6 verification pyramid
python plugins/usdVrmFileFormat/tools/generate_fixtures.py # regenerate the test fixturesOpening an avatar in usdview needs the importer's runtime siblings on the
session plugin path — the typed schemas it applies (vrmSchema) and, for
embedded textures, the package resolver (usdVrmPackageResolver). Compose them
with --with:
ost plugin view plugins/usdVrmFileFormat /path/to/Avatar.vrm \
--with plugins/vrmSchema --with plugins/usdVrmPackageResolverost plugin view / test-view load only the bundles named with --with — unlike
build / test / run, they do not auto-compose the manifest's
requires.bundles closure, so a bare ost plugin view plugins/usdVrmFileFormat <avatar>
opens without vrmSchema and the importer's schema apply fails inside
Sdf.Layer.FindOrOpen (on a non-UTF-8 locale usdview then masks it with a
secondary UnicodeDecodeError). Note also there is no -- before the fixture:
the syntax is ost plugin view <bundle> <fixture>, not run's -- <cmd>.
The typed Vrm*API classes moved to the sibling
plugins/vrmSchema bundle in the workspace Phase 1 split
(see docs/architecture/WORKSPACE.md).
This importer consumes them strictly as an installed package
(find_package(vrmSchema CONFIG REQUIRED) + vrmSchema::vrmSchema); ost 0.15
builds/installs the manifest dependency closure before configuring this bundle.
Schema regeneration lives with the schema:
plugins/vrmSchema/tools/generate_schema.py.