Export format

Normative. This page is the profile every Dramatis GLB and VRM export follows, version 1. The writer (writeCharacterGlb), the three.js adapter (characterObject) and the gate-1 harness (test/gate1/) are all built to it. Where it says MUST, a file that does otherwise is a bug in the writer; the harness fails it. The same names are in code, in src/contracts/profile.ts (UE_JOINTS, MIXAMO_NAMES, FACE_UNITS, checkExportExtras), and a test pins them to tosijs-3d.

Where today's writer falls short of it (each is a known bug, tracked on the board, and listed on Export under "Not yet"):

The file

Units, axes, root

Nodes and meshes

One mesh per part of the character, each its own node, the node and the mesh sharing a name:

Name What
body The skin, with covered skin removed (coverBody), not hidden
eyes Both eyeballs
eyelashes, teeth, tongue, brows, beard When the character has them
hair The hair, whichever kind
garment_<asset> Each worn garment, by its asset name (garment_toigo_harem_pants)

Names are ASCII letters, digits and _. Each mesh has one primitive per material (usually one), mode 4 (triangles), indexed, with POSITION, NORMAL, TEXCOORD_0 (when textured), JOINTS_0 and WEIGHTS_0. The order of the meshes is the order above, garments by asset name.

Skeleton

# ue parent mixamo
0 root none root
1 pelvis root mixamorig:Hips
2 spine_01 pelvis mixamorig:Spine
3 spine_02 spine_01 mixamorig:Spine1
4 spine_03 spine_02 mixamorig:Spine2
5 neck_01 spine_03 mixamorig:Neck
6 Head neck_01 mixamorig:Head
7 clavicle_l spine_03 mixamorig:LeftShoulder
8 upperarm_l clavicle_l mixamorig:LeftArm
9 lowerarm_l upperarm_l mixamorig:LeftForeArm
10 hand_l lowerarm_l mixamorig:LeftHand
11 thumb_01_l hand_l mixamorig:LeftHandThumb1
12 thumb_02_l thumb_01_l mixamorig:LeftHandThumb2
13 thumb_03_l thumb_02_l mixamorig:LeftHandThumb3
14 thumb_04_leaf_l thumb_03_l mixamorig:LeftHandThumb4
15 index_01_l hand_l mixamorig:LeftHandIndex1
16 index_02_l index_01_l mixamorig:LeftHandIndex2
17 index_03_l index_02_l mixamorig:LeftHandIndex3
18 index_04_leaf_l index_03_l mixamorig:LeftHandIndex4
19 middle_01_l hand_l mixamorig:LeftHandMiddle1
20 middle_02_l middle_01_l mixamorig:LeftHandMiddle2
21 middle_03_l middle_02_l mixamorig:LeftHandMiddle3
22 middle_04_leaf_l middle_03_l mixamorig:LeftHandMiddle4
23 ring_01_l hand_l mixamorig:LeftHandRing1
24 ring_02_l ring_01_l mixamorig:LeftHandRing2
25 ring_03_l ring_02_l mixamorig:LeftHandRing3
26 ring_04_leaf_l ring_03_l mixamorig:LeftHandRing4
27 pinky_01_l hand_l mixamorig:LeftHandPinky1
28 pinky_02_l pinky_01_l mixamorig:LeftHandPinky2
29 pinky_03_l pinky_02_l mixamorig:LeftHandPinky3
30 pinky_04_leaf_l pinky_03_l mixamorig:LeftHandPinky4
31 thigh_l pelvis mixamorig:LeftUpLeg
32 calf_l thigh_l mixamorig:LeftLeg
33 foot_l calf_l mixamorig:LeftFoot
34 ball_l foot_l mixamorig:LeftToeBase
35 ball_leaf_l ball_l mixamorig:LeftToe_End
36 clavicle_r spine_03 mixamorig:RightShoulder
37 upperarm_r clavicle_r mixamorig:RightArm
38 lowerarm_r upperarm_r mixamorig:RightForeArm
39 hand_r lowerarm_r mixamorig:RightHand
40 thumb_01_r hand_r mixamorig:RightHandThumb1
41 thumb_02_r thumb_01_r mixamorig:RightHandThumb2
42 thumb_03_r thumb_02_r mixamorig:RightHandThumb3
43 thumb_04_leaf_r thumb_03_r mixamorig:RightHandThumb4
44 index_01_r hand_r mixamorig:RightHandIndex1
45 index_02_r index_01_r mixamorig:RightHandIndex2
46 index_03_r index_02_r mixamorig:RightHandIndex3
47 index_04_leaf_r index_03_r mixamorig:RightHandIndex4
48 middle_01_r hand_r mixamorig:RightHandMiddle1
49 middle_02_r middle_01_r mixamorig:RightHandMiddle2
50 middle_03_r middle_02_r mixamorig:RightHandMiddle3
51 middle_04_leaf_r middle_03_r mixamorig:RightHandMiddle4
52 ring_01_r hand_r mixamorig:RightHandRing1
53 ring_02_r ring_01_r mixamorig:RightHandRing2
54 ring_03_r ring_02_r mixamorig:RightHandRing3
55 ring_04_leaf_r ring_03_r mixamorig:RightHandRing4
56 pinky_01_r hand_r mixamorig:RightHandPinky1
57 pinky_02_r pinky_01_r mixamorig:RightHandPinky2
58 pinky_03_r pinky_02_r mixamorig:RightHandPinky3
59 pinky_04_leaf_r pinky_03_r mixamorig:RightHandPinky4
60 thigh_r pelvis mixamorig:RightUpLeg
61 calf_r thigh_r mixamorig:RightLeg
62 foot_r calf_r mixamorig:RightFoot
63 ball_r foot_r mixamorig:RightToeBase
64 ball_leaf_r ball_r mixamorig:RightToe_End

Skinning

Rest pose: the T-pose

The bind pose is a T-pose: arms straight out along ±X, palms down, legs straight and slightly apart, as UAL's mannequin stands. Every joint's rest rotation is UAL's own joint frame (the bone along the joint's local +Y, as Blender exports it); only the translations are the character's.

Why, decided for version 1:

  1. UAL clips then play by name, unretargeted. A clip's rotation keys are relative to its skeleton's rest frames. With the same frames, they mean the same thing on our skeleton, so a three.js AnimationMixer or a Godot AnimationPlayer plays UAL's clips on an export as is. This is the gate's test (below).
  2. VRM requires a T-pose, and Unity's Humanoid and Unreal's IK Retargeter both prefer one: their auto-mapping assumes it.
  3. UAL is a T-pose already (measured from UAL1.glb: the hands at the shoulders' height, 0.74 m out). MakeHuman's own rest holds the arms angled down, so the export re-poses MakeHuman's bind to the T-pose; that is linear-blend skinning of the baked mesh, done once at export.

restPose: 'source' (an option, not the default) keeps MakeHuman's rest instead, with UAL's frames re-aimed along its bones. Clips then need retargeting; it exists for tools that want the sculpted rest.

Playing UAL clips

A UAL clip's translation keys hold the mannequin's bone lengths, so played on another body they stretch it. Play its rotation tracks on every joint and its translation tracks on root and pelvis only (ideally scaling the pelvis's by the ratio of the hips' heights). That is what characterClip in dramatis/three does (its translationScale is the ratio), and the gate's clip check plays the clip through it.

Morph targets: the face units

The 59 units, in order:

LeftBrowDown, RightBrowDown, LeftOuterBrowUp, RightOuterBrowUp, LeftInnerBrowUp, RightInnerBrowUp, NoseWrinkler, LeftUpperLidOpen, RightUpperLidOpen, LeftUpperLidClosed, RightUpperLidClosed, LeftLowerLidUp, RightLowerLidUp, LeftEyeDown, RightEyeDown, LeftEyeUp, RightEyeUp, LeftEyeturnRight, RightEyeturnRight, LeftEyeturnLeft, RightEyeturnLeft, LeftCheekUp, RightCheekUp, CheeksPump, CheeksSuck, NasolabialDeepener, ChinLeft, ChinRight, ChinDown, ChinForward, lowerLipUp, lowerLipDown, lowerLipBackward, lowerLipForward, UpperLipUp, UpperLipBackward, UpperLipForward, UpperLipStretched, JawDrop, JawDropStretched, LipsKiss, MouthMoveLeft, MouthMoveRight, MouthLeftPullUp, MouthRightPullUp, MouthLeftPullSide, MouthRightPullSide, MouthLeftPullDown, MouthRightPullDown, MouthLeftPlatysma, MouthRightPlatysma, TongueOut, TongueUshape, TongueUp, TongueDown, TongueLeft, TongueRight, TonguePointUp, TonguePointDown

Materials

Licences in the file

Every asset in the file is listed, with its source and licence, in asset.extras.dramatis. asset.copyright repeats it as one line for tools that show only that: <title>: <licence> per asset, joined with ; . A CC BY asset's credit line (attribution) is copied word for word.

asset.extras.dramatis MUST match this schema (checkExportExtras is the same schema in code):

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://dramatis.tosijs.net/schema/export-extras-1.json",
  "type": "object",
  "required": ["format", "version", "generator", "naming", "restPose",
               "faceUnits", "character", "licences"],
  "properties": {
    "format": { "const": "dramatis-export" },
    "version": { "const": 1 },
    "generator": { "type": "string", "minLength": 1,
                   "description": "dramatis@<version>" },
    "naming": { "enum": ["ue", "mixamo"] },
    "restPose": { "enum": ["t-pose", "source"] },
    "faceUnits": { "type": "array", "uniqueItems": true,
                   "items": { "$ref": "#/$defs/faceUnit" } },
    "character": { "type": "string" },
    "licences": {
      "type": "array", "minItems": 1,
      "items": { "$ref": "#/$defs/licence" },
      "contains": { "properties": { "kind": { "const": "base" } } }
    }
  },
  "$defs": {
    "faceUnit": { "type": "string",
                  "description": "one of the 59 face units above" },
    "licence": {
      "type": "object",
      "required": ["asset", "title", "kind", "licence", "licenceUrl",
                   "source", "author"],
      "properties": {
        "asset": { "type": "string", "minLength": 1,
                   "description": "<source>:<kind>/<name>, e.g. makehuman:base/hm08" },
        "title": { "type": "string", "minLength": 1 },
        "kind": { "enum": ["base", "eyes", "eyelashes", "teeth", "tongue",
                           "brows", "beard", "hair", "garment", "texture",
                           "animation", "skin-tone", "other"] },
        "licence": { "type": "string", "minLength": 1,
                     "description": "SPDX id: CC0-1.0, CC-BY-4.0, ..." },
        "licenceUrl": { "type": "string", "minLength": 1 },
        "source": { "type": "string", "minLength": 1 },
        "author": { "type": "string", "minLength": 1 },
        "attribution": { "type": "string", "minLength": 1,
                         "description": "required for any CC BY licence" },
        "modified": { "type": "boolean" }
      }
    }
  }
}

An asset with no entry in the licence manifest is never exported: buildCharacter throws UnlicensedAssetError instead.

What VRM adds

A VRM export (exportCharacterVrm, .vrm) is this profile, always ue naming and the T-pose, plus the VRMC_vrm 1.0 extension (in extensionsUsed):

How it is checked

bun test test/gate1 runs six checks on every GLB in samples/out/: the validator (zero errors); the skeleton's names, order and parents; the morph targets' names; the licence block; three.js's GLTFLoader loading it under Bun; and UAL's Walk_Loop (Quaternius, CC0) binding by name to the major joints (spine, neck, head, each limb to the hand and foot) and moving them. With nothing in samples/out/, every check reports NOT RUN. Each check is shown able to fail on a hand-made bad file (test/gate1/fixtures.test.ts).

The engine imports are spike 1's (tools/engines/, reports/spike-1.md): Godot 4.5, Babylon.js 9.33 (headless) and Blender 5.2 have passed on every sample; Unity and Unreal 5 have not been run. Each engine's guide says what was checked there.