VRM

A character also leaves Dramatis as a VRM 1.0 file (.vrm), the avatar format VTuber apps, VRChat-style worlds and VRM viewers read. A Dramatis VRM is the GLB export with one more extension, VRMC_vrm, in its JSON: the geometry, the skeleton, the face as morph targets, the materials and the licences are byte for byte the GLB's. VRM only names what is already there: which joint is which human bone, which face units make which expression, where the eyes are, and the licence terms in VRM's own words. The profile is normative: Export format, "What VRM adds".

import { loadHumanPack } from 'tosijs-3d/human-pack'
import { exportCharacterVrm, HM08_PACK_URL } from 'dramatis'

const pack = await loadHumanPack(HM08_PACK_URL)
const vrm = await exportCharacterVrm(character, pack, {
  meta: { name: 'Ada', authors: ['Ada L.'] },
})
// vrm is a Uint8Array: save it as ada.vrm

Checked with @pixiv/three-vrm under three.js and with a structural check of its own (below). Not yet tried in UniVRM, a VTuber app or any VRM viewer. The creator has no VRM button yet: make one with the API.

Also exported: writeCharacterVrm(built) (a built character to VRM, synchronously), vrmFromGlb(glb) (any Dramatis GLB under ue naming in the T-pose, to VRM), vrmProblems(bytes) (the structural check below), and the two tables, VRM_HUMAN_BONES and VRM_PRESETS.

What is in it

Humanoid

UAL's skeleton is a UE mannequin, close to VRM's humanoid already: one joint per VRM bone, 52 in all.

VRM bone Joint
hips, spine, chest, upperChest pelvis, spine_01, spine_02, spine_03
neck, head neck_01, Head
leftShoulder, leftUpperArm, leftLowerArm, leftHand clavicle_l, upperarm_l, lowerarm_l, hand_l
leftThumbMetacarpal, leftThumbProximal, leftThumbDistal thumb_01_l, thumb_02_l, thumb_03_l
leftIndexProximal, ...Intermediate, ...Distal (and Middle, Ring, Little) index_01_l, index_02_l, index_03_l (and middle, ring, pinky)
leftUpperLeg, leftLowerLeg, leftFoot, leftToes thigh_l, calf_l, foot_l, ball_l
the same on the right the same with _r

root (the floor joint) and the _leaf tips are not human bones. VRM's leftEye, rightEye and jaw are not mapped: UAL has no eye or jaw joints, so the eyes and jaw move by face units.

Expressions

Each preset is a weighted set of the 59 face units, bound on every mesh that carries the unit (one unit on the body, the lashes, the brows and the teeth is one unit: they move together). A unit the export lacks is left out; a preset with no units left is left out.

"Left" in a unit's name is the character's left (+X), measured, not assumed: every Left unit moves vertices at x > 0, and LeftEyeturnLeft turns the eye's front towards +X. VRM's blinkLeft and lookLeft are the avatar's own left too (three-vrm sends a target on the avatar's +X to lookLeft), so no side is swapped.

The weights are a first cut, judged from what each unit moves; they have not yet been looked at in a VTuber app (see the spike report, reports/spike-3-vrm.md). They live in src/vrm/expressions.ts as data.

Preset Face units and weights Overrides Why
happy MouthLeftPullUp 0.9, MouthRightPullUp 0.9, LeftCheekUp 0.6, RightCheekUp 0.6, LeftLowerLidUp 0.3, RightLowerLidUp 0.3 blink blend A smile is the mouth corners up; the cheeks and lower lids rise with a real one (the Duchenne smile). Blinking is damped, not blocked, since the lids are already partly up.
angry LeftBrowDown 1, RightBrowDown 1, NoseWrinkler 0.4, LeftLowerLidUp 0.3, RightLowerLidUp 0.3, MouthLeftPullDown 0.5, MouthRightPullDown 0.5, lowerLipUp 0.3 blink blend Brows down and drawn in, the nose wrinkled, a squint, and the mouth pressed and pulled down.
sad LeftInnerBrowUp 1, RightInnerBrowUp 1, LeftUpperLidClosed 0.2, RightUpperLidClosed 0.2, MouthLeftPullDown 0.8, MouthRightPullDown 0.8, lowerLipUp 0.3 blink blend The inner brows up (the grief brow), the upper lids drooping, the mouth corners down and the lower lip pushed up.
relaxed LeftUpperLidClosed 0.35, RightUpperLidClosed 0.35, MouthLeftPullUp 0.3, MouthRightPullUp 0.3 blink blend Soft, half-lowered lids and the hint of a smile.
surprised LeftInnerBrowUp 1, RightInnerBrowUp 1, LeftOuterBrowUp 1, RightOuterBrowUp 1, LeftUpperLidOpen 1, RightUpperLidOpen 1, JawDrop 0.35 blink block, mouth blend The whole brow up, the eyes wide (so no blinking) and the jaw dropped; visemes are damped so speech does not open the mouth past a gape.
aa JawDrop 0.8, UpperLipUp 0.2 none The open vowel: the jaw well down, the upper lip lifted a little off the teeth.
ih JawDrop 0.25, MouthLeftPullSide 0.35, MouthRightPullSide 0.35 none A narrow opening with the lips spread a little.
ou LipsKiss 1, UpperLipForward 0.3, lowerLipForward 0.3, JawDrop 0.15 none Rounded, pushed-forward lips over a small opening.
ee MouthLeftPullSide 0.7, MouthRightPullSide 0.7, UpperLipUp 0.2, JawDrop 0.1 none The lips spread wide over nearly closed teeth.
oh JawDrop 0.5, LipsKiss 0.6 none Rounded lips over a medium opening: between aa and ou.
blink LeftUpperLidClosed 1, RightUpperLidClosed 1 none Both upper lids closed (the profile: blink is both).
blinkLeft LeftUpperLidClosed 1 none The character's left upper lid (Left units move x > 0, the character's left).
blinkRight RightUpperLidClosed 1 none The character's right upper lid.
lookUp LeftEyeUp 1, RightEyeUp 1 none Both eyes up.
lookDown LeftEyeDown 1, RightEyeDown 1 none Both eyes down.
lookLeft LeftEyeturnLeft 1, RightEyeturnLeft 1 none Both eyes to the character's left (+X), which is VRM's lookLeft.
lookRight LeftEyeturnRight 1, RightEyeturnRight 1 none Both eyes to the character's right (-X).

neutral is left out: the rest face is neutral. The remaining units (the tongue's, CheeksPump, ChinLeft and the rest) are still morph targets in the file, by name, for apps that drive units directly.

Look-at

By expression, not by bone: UAL has no eye joints, so the eyes turn by the eye units through lookUp, lookDown, lookLeft and lookRight.

Meta and licences

Field Value
name meta.name, else the character's name
authors meta.authors, else Dramatis user
contactInformation meta.contactInformation, else the creator's URL
licenseUrl https://vrm.dev/licenses/1.0/
otherLicenseUrl the licences page
copyrightInformation the GLB's asset.copyright line: every asset, <title>: <licence>
thirdPartyLicenses one line per asset: title, id, licence and its URL, author, source, and a CC BY credit line word for word
references each asset's source, once

The full entries stay in asset.extras.dramatis.licences too, as in every export.

The use terms are the widest the character's assets allow: anyone may perform as it (avatarPermission: everyone), commercially (commercialUsage: corporation), and redistribute and modify it (allowModificationRedistribution), unless an asset is non-commercial (NC, then personalNonProfit) or no-derivatives (ND, then prohibited). Credit is required when any asset is CC BY (the Monk Skin Tone Scale is, so in practice always), unnecessary otherwise. The content flags (excessively violent or sexual, political or religious, antisocial or hateful use) are left at VRM's default, false: those are the author's to grant, not the assets'. Change them in a VRM editor if you mean to.

How it is checked

Every VRM in the tests (src/vrm/vrm.test.ts, on the committed samples and a fixture) is checked four ways:

  1. vrmProblems, the structural check of VRMC_vrm against the 1.0 schema and against the file: required bones present, no node used twice, each bone's nearest human ancestor is the one VRM's hierarchy wants, every morph bind on a mesh node and a real target with a weight in [0, 1], meta's required fields and enums, look-at ranges. Each failure is shown to fire on a broken file.
  2. The Khronos glTF validator: zero errors. It reports VRMC_vrm as an extension it cannot validate (an info), which is why (1) exists.
  3. The gate-1 profile checks (skeleton, morph targets, licences): a VRM is still a profile GLB.
  4. @pixiv/three-vrm under three.js: it loads with every bone and preset; aa sets JawDrop to 0.8 on the body and the teeth; blinkLeft closes only the left lid; looking at a point on the character's left drives lookLeft and LeftEyeturnLeft; the normalized rig is a T-pose facing +Z, and lowering its left upper arm lowers the raw hand_l.

Not yet checked: UniVRM (Unity), VSeeFace, VRoid Hub or any VTuber app. A sample to try by hand is reports/spike-3-vrm/long-hair.vrm.