This document freezes the public interpretation contract for the typed VRM
control schemas authored by the usdVrm importer. Downstream tools should read
the typed Vrm*API data first. Raw VRM JSON is preserved as a lossless fallback,
not as the primary runtime interface.
The schema source is schema/schema.usda; generated C++ and
plugin/resources/vrmSchema/generatedSchema.usda are build artifacts derived from
that source.
Current contract version: 1.
New importer output writes:
| Location | Value |
|---|---|
/Asset.customData.vrm:schemaContractVersion |
1 |
vrm_report.py --json compatibility.schemaContractVersion |
1 |
Versioning policy:
| Change type | Policy |
|---|---|
| Add an optional typed attribute or relationship | Allowed within v1 when old readers can ignore it. |
| Add a new optional API schema | Allowed within v1 when existing v1 prims keep their meaning. |
| Change a required path, attribute type, token meaning, relationship target meaning, or array ordering rule | Requires a new contract version. |
| Remove or repurpose an existing v1 property | Not allowed in v1. |
| Move a shipped control prim to a new canonical path | Requires a new contract version unless the old path remains authoritative. |
The validator treats a missing version as an old/exported-stage warning
(VRM270) and an unknown version as a contract error (VRM271).
The v1 contract keeps all VRM control semantics under /Asset/rig:
| Feature | Contract path |
|---|---|
| Humanoid | /Asset/rig/Humanoid |
| Expressions | /Asset/rig/Expressions/<name> |
| LookAt | /Asset/rig/LookAt |
| Spring bones | /Asset/rig/SecondaryMotion/SpringBones/<name> |
| Spring-bone colliders | /Asset/rig/SecondaryMotion/Colliders/<group>/Collider_<n> |
| Constraints | /Asset/rig/Constraints/<name> |
The v1 contract freezes the shipped /Asset/rig/SecondaryMotion/* layout; an
earlier design draft had sketched /Asset/physics/* instead. SpringBone data in
usdVrm is control metadata, not a physics simulation graph, and these paths are
already covered by fixtures, reports, and downstream-facing docs. A future
physics-oriented adapter may publish /Asset/physics/* as an additive view, but
v1 readers should treat /Asset/rig/SecondaryMotion/* as authoritative.
Under /Asset/mtl the contract covers the material prim — its path, its
vrm:shaderModel attribute, its customData.vrm:mtoon:raw fallback, and the
canonical material schemas applied to it
(below) — and nothing below it. The
generated shader network is a rendering realization, so its internal layout
may change within v1:
| In the contract | Not in the contract |
|---|---|
/Asset/mtl/<material> as the binding target |
the shader prims below it |
vrm:shaderModel, customData.vrm:mtoon:raw on that prim |
node names, node count, graph nesting |
VrmMaterialAPI, VrmMToonAPI, VrmTextureInfoAPI:<role> on that prim |
which realization input a canonical value is connected to |
Consumers should reach the surface through UsdShadeMaterial's terminal
(ComputeSurfaceSource()), never by assuming a prim path inside the material.
Since 2026-08-13 the UsdPreviewSurface network lives one level down, in a
/preview UsdShadeNodeGraph, and a MaterialX /mtlx sibling followed on
2026-08-14 (material policy §4);
neither is a contract-version change, because no v1 path moved and no v1
property changed meaning.
vrm:expressionName (added 2026-09-01, additive within v1) carries the
canonical VRM 1.0 expression name. The prim name does not, and cannot: a
source name is an arbitrary string, a USD prim name is an identifier, and this
bundle's producers sanitize independently — usdVrmFileFormat through its own
hashed-fallback table and usdVrmaFileFormat through TfMakeValidIdentifier.
A name outside ASCII, or one that had to take a _2 collision suffix, therefore
lands on a different prim name on the avatar and on the clip that drives it.
For a VRM 1.0 file the canonical name is verbatim what the file spelled. For a
VRM 0.x preset it is the VRM 1.0 name that preset migrates to — 0.x names
its presets with the BlendShapePreset enum, a different vocabulary, and of its
seventeen entries only neutral, angry and blink are spelled the same in
1.0:
| VRM 0.x | VRM 1.0 | VRM 0.x | VRM 1.0 | |
|---|---|---|---|---|
joy |
happy |
a i u e o |
aa ih ou ee oh |
|
sorrow |
sad |
blink_l blink_r |
blinkLeft blinkRight |
|
fun |
relaxed |
lookup lookdown … |
lookUp lookDown … |
A .vrma clip is a VRM 1.0-era file and only ever spells the 1.0 names, so a
0.x avatar keeping its own vocabulary could never be driven by one. A 0.x custom
name (no presetName, or unknown) is not a preset and is carried through
untouched, as is a presetName outside the enum. The raw 0.x block at
/Asset.customData.vrm:rawExtension keeps every original spelling, so the
migration decides the canonical identity and not the record.
The key is unique per stage. Two expressions answering to one name are not
two expressions — a resolver would silently bind whichever it reached first — so
the importer keeps the first declaration and reports the rest as VRM152. This
is reachable in both versions: 1.0 can declare the same name under both
expressions.preset and expressions.custom, and 0.x blendShapeGroups is an
array. The prim names stay distinct through the uniquifier either way; it is the
key that is deduplicated.
| Side | Prim | Carries |
|---|---|---|
avatar (.vrm) |
/Asset/rig/Expressions/<sanitized> |
vrm:expressionName, plus the binds |
clip (.vrma) |
/Animation/Expressions/<sanitized> |
vrm:expressionName, plus the weight |
A consumer joining a clip's expression weight to an avatar's morph and
material-colour binds matches on vrm:expressionName and never on a path.
That is the key ExpressionResolve is specified against
(VRM motion policy §3.3); the
clip half shipped it in v0.8.0 and the avatar half is the prerequisite it was
waiting on.
A stage authored before this attribute existed has expression prims without it. That is a v1-legal old stage, not an error: old readers ignore the attribute and a new reader that finds none has only the sanitized name to work with, which is exactly the situation the attribute exists to end.
Expressions accumulate on their targets, and two that bind different morph
targets still fight when those targets displace the same vertices: an eyelid
driven by blink and by a happy that raises the cheek is driven roughly
twice as far as shut, with nothing in either weight array out of range. VRM 1.0
gives exactly one mechanism for this, and v1 carries it verbatim:
| Attribute | Says |
|---|---|
uniform token vrm:overrideBlink |
What this expression does to blink, blinkLeft, blinkRight while it is on |
uniform token vrm:overrideLookAt |
The same over lookUp, lookDown, lookLeft, lookRight |
uniform token vrm:overrideMouth |
The same over aa, ih, ou, ee, oh |
Each is none, block (the category is off while this expression is on at all)
or blend (the category is attenuated by this expression's own weight). Three
things about that are the contract rather than the implementation:
A category is a set of preset names, not one expression. The override names
"the mouth", never aa — and a custom expression is in no category, because VRM
reserves the preset names. A VRM 0.x rig lands in the same sets, since the
importer migrates presetName to the 1.0 spelling on the way in.
An unauthored attribute is not none. The importer authors one only where
the source file stated it, so a VRM 0.x expression — 0.x has no such field —
carries none of the three rather than three tokens it never said.
The token is carried as spelled. There is no allowedTokens list on these
attributes: a value outside the three reaches a consumer as data, with the
importer's VRM153 beside it, rather than failing schema validation and taking
the rest of the avatar with it. A source value that is not a token at all — a
number, null, an empty string — cannot be authored onto a token attribute, so
it survives in the raw block alone and is reported under the same code. A
consumer that cannot read a token must refuse it out loud — reading it as "no
arbitration" is a face that renders wrong with nothing in the log.
Applying the rule needs a whole sample, because an override is a statement one
expression makes about others, so it belongs to the consumer step
(ExpressionResolve) and never to this layer. Additive within v1: an old reader
ignores all three and gets exactly the behaviour it had before they existed.
Added 2026-09-25 (Product P5 Step 3), additive within v1. Three API schemas
carry a material's source semantics on the UsdShadeMaterial itself, and
every rendering realization — /preview, /mtlx, and any renderer's own —
is generated from them and from nothing else
(material policy §3,
§6):
| Schema | Kind | Carries |
|---|---|---|
VrmMaterialAPI |
single-apply | the glTF material core: baseColorFactor (RGB) and baseColorAlphaFactor, metallicFactor, roughnessFactor, emissiveFactor, emissiveStrength (KHR_materials_emissive_strength), alphaMode, alphaCutoff, doubleSided, unlit (KHR_materials_unlit) |
VrmMToonAPI |
single-apply | every non-texture field of VRMC_materials_mtoon 1.0, spelled as the specification spells it |
VrmTextureInfoAPI:<role> |
multiple-apply | one texture: file, texCoord, wrapS, wrapT, the contribution scalars scale / strength, and KHR_texture_transform under transform: (offset, rotation, scale) |
Five rules are the contract rather than the implementation.
Every property is an inputs: attribute. The names are
inputs:vrm:material:<field>, inputs:vrm:mtoon:<field> and
inputs:vrm:textureInfo:<role>:<field> — the vrm: namespace, inside
UsdShade's inputs:, as UsdLux nests inputs:shaping:*. UsdShade connects
only inputs: and outputs: attributes, so this is the only way a
realization graph can read a canonical value rather than hold a copy of it,
and the only way an animated value — an expression's material-colour bind —
reaches whichever realization the renderer selects. Measured on 26.08 in
Storm: a time-sampled inputs:vrm:material:baseColorFactor drives both
/preview and /mtlx frame by frame through a NodeGraph interface
connection, while the same connection to a plain vrm: attribute is discarded
as an invalid source and the surface draws black with no error.
A fallback is not what a connected realization sees. Each property's
schema fallback is the specification default, and a reader gets it from an
unauthored attribute. A UsdShade connection resolves to an authored value
only, so a graph connected to an unauthored canonical input silently takes
its own shader's default instead. A writer therefore authors every canonical
value a realization connects to. alphaMode has no fallback at all (its
generated C++ token would be OPAQUE, a Windows macro); unauthored means
OPAQUE, as in glTF.
The texture roles are exactly eleven: the glTF core baseColor,
metallicRoughness, normal, occlusion, emissive, and the MToon
shadeMultiply, shadingShift, matcap, rimMultiply,
outlineWidthMultiply, uvAnimationMask — each the specification's texture
name without Texture. The schema declares them as its allowed instance
names; CanApplyAPI refuses any other, but ApplyAPI does not check, so the
validator reports one (VRM224). Whether a role is colour or data is fixed by
the role and is not stored.
Token values are glTF's and VRM's, not a realization's. alphaMode is
OPAQUE, MASK or BLEND; outlineWidthMode is none, worldCoordinates
or screenCoordinates; wrapS / wrapT are repeat, clampToEdge or
mirroredRepeat — not UsdUVTexture's clamp / mirror, which is a
realization's translation. As for the expression overrides, the attributes
carry no allowedTokens list; the validator reports a value outside the set
(VRM225).
vrm:shaderModel stays. It predates VrmMToonAPI and contract v1 cannot
remove it, so it keeps its meaning and the two must agree: a material with
VrmMToonAPI applied says vrm:shaderModel = "MToon" (VRM226). The raw
block at customData.vrm:mtoon:raw stays the lossless fallback, and is never a
runtime API — a value a consumer needs and cannot find in the typed schemas is
a missing field, not a reason to parse JSON.
The importer authors them on every material (P5 Step 4, 2026-09-25):
VrmMaterialAPI always, VrmMToonAPI on an MToon material, and one
VrmTextureInfoAPI instance per texture the material samples, every value
written, the specification defaults included — except a texture's
transform:*, written only when the source states a KHR_texture_transform
(the fallback is the identity, so a reader sees the same mapping; a
realization can tell whether the source said it). /preview and /mtlx are
generated from these attributes alone (Steps 5–6;
material track).
A VRM 1.0 material is a rename: the glTF core into VrmMaterialAPI,
VRMC_materials_mtoon field for field into VrmMToonAPI, each textureInfo
into its role. A VRM 0.x MToon material (shader containing MToon) is
materialProperties[i] — Unity shader property names — and lands in the
same fields, with no version switch in the schema. The conversion is
UniVRM's own 0.x → 1.0 migration
(MigrationMToonMaterial.cs,
MToon10Migrator.cs),
destructive choices included, so a 0.x avatar and the 1.0 file UniVRM migrates
it to carry the same canonical values (mtoon_vrm0.vrm / mtoon_vrm1.vrm
prove it). For such a material the 0.x block is the source of the
VrmMaterialAPI half too: the glTF core beside it is the exporter's fallback,
and older exporters wrote its colours in the wrong space.
Each row's fidelity class is DESIGN_POLICY §6's:
| VRM 0.x | Canonical | Conversion | Fidelity |
|---|---|---|---|
_Color |
material:baseColorFactor, baseColorAlphaFactor |
RGB sRGB → linear; alpha as-is | Normalized |
_MainTex |
textureInfo:baseColor |
— | Normalized |
_MainTex tiling [ox, oy, sx, sy] |
every role's transform:offset = (ox, 1 − oy − sy), transform:scale = (sx, sy), except matcap |
Unity's bottom-left origin to glTF's top-left | Normalized |
_BlendMode 0 / 1 / 2 / 3 |
material:alphaMode OPAQUE / MASK / BLEND / BLEND; mtoon:transparentWithZWrite true for 3 only |
— | Normalized |
_Cutoff |
material:alphaCutoff |
cutout only; otherwise 0.5 | Normalized |
renderQueue |
mtoon:renderQueueOffsetNumber |
ranked among the file's materials of the same mode: transparent, highest queue 0 then −1, −2 … (to −9); with z-write, lowest 0 then +1 … (to +9); opaque and cutout 0 | Approximate — order kept, spacing lost |
_CullMode 0 / 1 / 2 |
material:doubleSided true / true / false |
glTF cannot cull front faces | Normalized; Front is Approximate |
| — | material:unlit = true |
UniVRM marks every MToon material KHR_materials_unlit |
Derived |
_ShadeColor |
mtoon:shadeColorFactor |
sRGB → linear | Normalized |
_ShadeTexture |
textureInfo:shadeMultiply |
absent: _MainTex takes its place (UniVRM's destructive choice — 0.x's GI hid a missing shade texture) |
Normalized; the stand-in is Approximate |
_BumpMap, _BumpScale |
textureInfo:normal, its scale |
— | Normalized |
_ShadeShift, _ShadeToony |
mtoon:shadingShiftFactor, shadingToonyFactor |
with min = shift, max = lerp(1, shift, toony): shift′ = clamp(−(max + min)/2, −1, 1), toony′ = clamp((2 − (max − min))/2, 0, 1) | Normalized |
_IndirectLightIntensity |
mtoon:giEqualizationFactor |
clamp(1 − x, 0, 1) | Approximate — the GI models differ |
_EmissionColor, _EmissionMap |
material:emissiveFactor, textureInfo:emissive |
already linear | Normalized |
_SphereAdd |
textureInfo:matcap; mtoon:matcapFactor white if present, black if not |
no tiling | Approximate — 1.0's MatCap is not 0.x's |
_RimColor, _RimFresnelPower, _RimLift, _RimTexture |
mtoon:parametricRimColorFactor (sRGB → linear), parametricRimFresnelPowerFactor, parametricRimLiftFactor, textureInfo:rimMultiply |
— | Normalized |
_RimLightingMix |
mtoon:rimLightingMixFactor = 1, whatever the source says |
UniVRM's destructive choice: 1.0 merges rim with MatCap | Approximate |
_OutlineWidthMode 0 / 1 / 2 (> 2 is 0) |
mtoon:outlineWidthMode none / worldCoordinates / screenCoordinates |
— | Normalized |
_OutlineWidth |
mtoon:outlineWidthFactor |
world: × 0.01 (cm → m); screen: × 0.005 (percent of half the height → fraction of the height); none: 0 | Normalized |
_OutlineWidthTexture |
textureInfo:outlineWidthMultiply |
— | Normalized |
_OutlineColor |
mtoon:outlineColorFactor |
sRGB → linear | Normalized |
_OutlineColorMode, _OutlineLightingMix |
mtoon:outlineLightingMixFactor |
the mix for MixedLighting (1); 0 for FixedColor (0) | Normalized |
_UvAnimMaskTexture, _UvAnimScrollX, _UvAnimScrollY, _UvAnimRotation |
textureInfo:uvAnimationMask, mtoon:uvAnimationScrollXSpeedFactor, uvAnimationScrollYSpeedFactor (negated: V runs the other way), uvAnimationRotationSpeedFactor (× 2π: turns/s → rad/s) |
— | Normalized |
_LightColorAttenuation, _ReceiveShadowRate / _ReceiveShadowTexture, _ShadingGradeRate / _ShadingGradeTexture, _OutlineScaledMaxDistance, _OutlineCullMode, and Unity's derived state (_SrcBlend, _DstBlend, _ZWrite, _MToonVersion, _DebugMode, keywordMap, tagMap) |
not typed | no 1.0 field; UniVRM drops them too | Lossless in vrm:mtoon:raw only |
Kept from the glTF core, as UniVRM keeps them: metallicFactor,
roughnessFactor, emissiveStrength, the metallicRoughness and occlusion
textures, and any core texture the 0.x block does not name. The one
departure from UniVRM: a property absent from the 0.x block takes the MToon
0.x shader's default (_Color white, _ShadeColor (0.97, 0.81, 0.86),
_ShadeToony 0.9, _IndirectLightIntensity 0.1, _RimFresnelPower 1,
_OutlineWidth 0.5, _CullMode Back, the rest 0), where UniVRM's migration
would take C#'s zero — a black, transparent base colour for a missing
_Color. A 0.x material that is not MToon (VRM_USE_GLTFSHADER, the legacy
VRM/Unlit* shaders) carries its glTF core only and no VrmMToonAPI.
Added 2026-09-27 (Product P5 Step 7), additive within v1. An expression's
material-colour bind names a slot — VRM 1.0's MaterialColorType — and
a slot is a canonical material attribute, never a realization's shader input
(material policy
§6.7). Every writer and evaluator resolves a slot through this one table
(vrmRig/MaterialColorSlots.h carries it in code):
| Slot | Schema | Canonical input | Fourth component |
|---|---|---|---|
color |
VrmMaterialAPI |
inputs:vrm:material:baseColorFactor |
inputs:vrm:material:baseColorAlphaFactor |
emissionColor |
VrmMaterialAPI |
inputs:vrm:material:emissiveFactor |
ignored |
shadeColor |
VrmMToonAPI |
inputs:vrm:mtoon:shadeColorFactor |
ignored |
matcapColor |
VrmMToonAPI |
inputs:vrm:mtoon:matcapFactor |
ignored |
rimColor |
VrmMToonAPI |
inputs:vrm:mtoon:parametricRimColorFactor |
ignored |
outlineColor |
VrmMToonAPI |
inputs:vrm:mtoon:outlineColorFactor |
ignored |
Each row is the glTF or VRMC_materials_mtoon field the VRM 1.0
specification's expression table names. Four rules are the contract:
A slot lands only where its schema is applied. An MToon slot of a glTF PBR material is one the specification calls "Unused"; it has no attribute to land on, and a writer reports it rather than inventing one.
The value is linear, and the rule is the specification's. A resolved slot
is base + Σ weightᵢ · (targetᵢ − base), with base the material's own
authored value. Written as time samples on the Material's canonical input —
by motion_retarget, on an override in its output layer — and nothing is
written below /preview or /mtlx. Both realizations read the slot values
through a NodeGraph interface connection wherever their graph can express the
relation (material policy §11 q12), so an animated slot shows without
regenerating either one; /preview holds a copy only where it has to fold the
value into another (factor × texture in UsdUVTexture.scale).
Each bind names its target by index. A relationship holds a target once
however many times it is added, so an expression binding two slots of one
material — color and emissionColor of a face — cannot pair its binds with
vrm:materialColorTargets by position. vrm:materialColorTargetIndices gives,
per bind, the index of its material in the relationship; the slot and value
arrays are parallel to it. A stage authored before the attribute has none and
pairs by position, which is correct exactly when no material is bound twice.
The validator checks both forms (VRM244).
The slot token is carried as spelled. A slot outside the six reaches a consumer as data; the consumer reports it.
A VRM 0.x blendShapeGroups[].materialValues entry is
{materialName, propertyName, targetValue} in Unity terms. The importer
migrates it the way UniVRM's
MigrationVrmExpression.cs
does — the material is the first whose glTF name is materialName — with one
departure:
| VRM 0.x property | Slot | Target conversion | Fidelity |
|---|---|---|---|
_Color |
color |
RGB sRGB → linear; alpha as-is | Normalized |
_EmissionColor |
emissionColor |
already linear | Normalized |
_ShadeColor |
shadeColor |
RGB sRGB → linear | Normalized |
_RimColor |
rimColor |
RGB sRGB → linear | Normalized |
_OutlineColor |
outlineColor |
RGB sRGB → linear | Normalized |
_MainTex_ST, _MainTex_ST_S, _MainTex_ST_T |
— (a texture transform, §11 q11) | — | Lossless in the raw block only; VRM150 |
| any other property; an undeclared material; a target that is not four numbers | — | — | Lossless in the raw block only; VRM150 |
The departure from UniVRM: its migration passes a 0.x target through
unconverted, while converting the material it lands on (the
0.x MToon table) — so a 0.x
target equal to the material's own _Color would come out lighter than the
material. A 0.x target is a Unity colour, as the material's are, and takes the
same conversion (the user's call, 2026-09-27). mtoon_vrm0.vrm and
mtoon_vrm1.vrm carry the same expression in both versions, and the importer
must author the same binds for each.
The v1 contract uses one token attribute per human bone:
| Property | Meaning |
|---|---|
rel vrm:skeleton |
The UsdSkelSkeleton whose joints tokens are referenced. |
uniform token vrm:humanBones:<bone> |
A joint path token from Skeleton.joints. |
This freezes the shipped per-bone attributes; an earlier design draft had sketched
token[] vrm:humanBoneNames plus rel vrm:humanBoneTargets instead.
UsdSkel joints are tokens inside Skeleton.joints, not prims, so a USD
relationship cannot directly target an individual joint. Standard VRM bones are
schema builtins; non-standard or VRM-0.x-only names remain custom
vrm:humanBones:<name> attributes on the same prim so the mapping stays
lossless.
| API | Applied to | Required typed data | Raw fallback |
|---|---|---|---|
VrmHumanoidAPI |
/Asset/rig/Humanoid |
vrm:skeleton, authored vrm:humanBones:<bone> tokens |
/Asset.customData.vrm:rawExtension |
VrmExpressionAPI |
/Asset/rig/Expressions/<name> |
vrm:expressionName, vrm:expressionType, vrm:isBinary; optional vrm:overrideBlink, vrm:overrideLookAt, vrm:overrideMouth; optional vrm:morphTargets plus parallel vrm:morphTargetWeights; optional vrm:materialColorTargets (each material once) plus parallel vrm:materialColorTargetIndices, vrm:materialColorTypes and vrm:materialColorValues (expression colours) |
/Asset.customData.vrm:rawExtension |
VrmLookAtAPI |
/Asset/rig/LookAt |
vrm:type; optional vrm:skeleton, vrm:leftEye, vrm:rightEye joint tokens |
/Asset/rig/LookAt.customData.vrm:lookAt:raw |
VrmSpringBoneAPI |
/Asset/rig/SecondaryMotion/SpringBones/<name> |
vrm:joints plus parallel vrm:stiffness, vrm:gravityPower, vrm:dragForce, vrm:hitRadius, vrm:gravityDir; optional vrm:center; optional vrm:colliderGroups |
/Asset/rig/SecondaryMotion.customData.vrm:springBone:raw |
VrmColliderAPI |
/Asset/rig/SecondaryMotion/Colliders/<group>/Collider_<n> |
vrm:shape, vrm:node, vrm:offset, vrm:radius; vrm:tail for capsules |
/Asset/rig/SecondaryMotion.customData.vrm:springBone:raw |
VrmConstraintAPI |
/Asset/rig/Constraints/<name> |
vrm:type, vrm:constrained, vrm:source, optional vrm:axis, vrm:weight |
/Asset/rig/Constraints/<name>.customData.vrm:constraint:raw |
VrmMaterialAPI |
/Asset/mtl/<material> (a UsdShadeMaterial only) |
inputs:vrm:material:* — every property a realization connects to is authored |
none on the stage: the glTF core is typed, and what is not (sampler filters, other extensions) lives only in the source file |
VrmMToonAPI |
/Asset/mtl/<material> (a UsdShadeMaterial only), with vrm:shaderModel = "MToon" |
inputs:vrm:mtoon:* — every property a realization connects to is authored |
/Asset/mtl/<material>.customData.vrm:mtoon:raw |
VrmTextureInfoAPI:<role> |
/Asset/mtl/<material> (a UsdShadeMaterial only), one of eleven roles |
inputs:vrm:textureInfo:<role>:file, texCoord; the rest where the source states them |
as for the API that owns the role |
Array ordering is part of the contract: every parallel array listed above uses
the same index order as its relationship or vrm:joints token array — except
the material-colour arrays, which are parallel to
vrm:materialColorTargetIndices (a relationship holds a target once, so it
cannot carry one entry per bind).
| VRM source | Typed/schema destination | Preservation |
|---|---|---|
VRM 1.0 humanoid.humanBones / VRM 0.x humanoid.humanBones[] |
VrmHumanoidAPI per-bone token attrs |
Full VRM block at /Asset.customData.vrm:rawExtension |
VRM 1.0 expressions.preset/custom / VRM 0.x blendShapeMaster.blendShapeGroups |
VrmExpressionAPI expression prims, morph binds, material-color binds (VRM 1.0 materialColorBinds; VRM 0.x colour materialValues, migrated), and the VRM 1.0 overrideBlink / overrideLookAt / overrideMouth tokens |
Full VRM block at /Asset.customData.vrm:rawExtension; a VRM 0.x materialValues entry with no colour slot is diagnostic VRM150 |
VRM 1.0 / 0.x lookAt |
VrmLookAtAPI type and eye joint tokens |
Raw lookAt curves at /Asset/rig/LookAt.customData.vrm:lookAt:raw |
VRM 1.0 springBone / VRM 0.x secondaryAnimation |
VrmSpringBoneAPI and VrmColliderAPI |
Raw spring-bone block at /Asset/rig/SecondaryMotion.customData.vrm:springBone:raw |
VRMC_node_constraint |
VrmConstraintAPI |
Raw constraint block at each constraint prim's customData.vrm:constraint:raw |
| VRM meta/license | /Asset.customData.vrm:meta |
Same location as the readable source of truth |
| glTF material core | VrmMaterialAPI, and VrmTextureInfoAPI for its five textures, on every material |
none on the stage (the source file keeps what is not typed) |
VRM 1.0 VRMC_materials_mtoon |
VrmMToonAPI, and VrmTextureInfoAPI for its six textures; vrm:shaderModel = "MToon" |
/Asset/mtl/<material>.customData.vrm:mtoon:raw (the extension block) |
VRM 0.x materialProperties[i] (MToon) |
The same VrmMaterialAPI / VrmMToonAPI / VrmTextureInfoAPI fields, converted per the 0.x table; vrm:shaderModel = "MToon" |
/Asset/mtl/<material>.customData.vrm:mtoon:raw (the whole entry, Unity names and all) |
| KHR texture transform | VrmTextureInfoAPI:<role>'s transform:*, as glTF states it; each realization graph also carries its own node |
Original material JSON remains under the raw VRM block |
tools/validate_vrm.py validates the stage contract without re-reading the
source .vrm. It fails on any ERROR or FATAL; tools/vrm_report.py merges
those diagnostics with import-time diagnostics.
Schema-contract-specific validator rules:
| Codes | Rule |
|---|---|
VRM270, VRM271 |
/Asset carries a supported schema contract version. |
VRM222-VRM226 |
Canonical material schemas apply only to a UsdShadeMaterial, use one of the eleven texture roles, carry token values from their documented sets, and agree with vrm:shaderModel; a canonical texture asset resolves. |
VRM230-VRM232 |
Humanoid prim applies VrmHumanoidAPI, resolves vrm:skeleton, and each authored bone token names a skeleton joint. |
VRM240-VRM244 |
Expression relationships resolve and all parallel arrays line up with their target relationships — the material-colour arrays with vrm:materialColorTargetIndices where it is authored, each index naming a target. |
VRM245-VRM247 |
LookAt prim applies VrmLookAtAPI; eye tokens resolve when a skeleton relationship is authored. |
VRM250-VRM255 |
Spring-bone joint tokens, collider group targets, parallel arrays, collider API application, and collider shape tokens are valid. |
VRM262-VRM264 |
Constraint prims apply VrmConstraintAPI, use known constraint type tokens, and hierarchical joint tokens resolve. |
The complete diagnostic catalog is in DIAGNOSTICS.md.
These are intentionally outside v1:
| Gap | v1 treatment |
|---|---|
VrmColliderGroupAPI |
Collider groups remain structural scope prims; spring chains target them with vrm:colliderGroups. |
| Expression texture-transform binds | Preserved in the raw VRM block (material policy §11 q11), VRM 1.0 textureTransformBinds and VRM 0.x _MainTex_ST materialValues (VRM150) alike. The typed v1 expression contract covers morph and material-color binds. |
| Human-bone axis metadata | Not authored as per-bone API data in v1. Consumers should use the normalized +Z stage, UsdSkel rest/bind transforms, and raw fallback when they need source-axis detail. |
| Sampler filters | glTF magFilter / minFilter are not typed on VrmTextureInfoAPI; no realization reads them. Preserved in the raw VRM block. |
| Canonical-rest provenance per bone | The stage is already front-normalized and carries vrm:sourceFrontAxis / vrm:frontAxisNormalized; per-bone rest provenance is deferred. |