Skip to content

dh.rig

Extension: .dh-rig
Type ID: dh.rig

A rig definition maps DigitalHeaven standard bone names to model-specific bone names. This lets the system understand your armature regardless of what naming convention the original model uses (Mixamo, Blender, custom, etc.).

PropertyTypeRequiredDescription
$type"dh.rig"noType identifier
namestringnoDisplay name
descriptionstringnoDescription
inheritsstringnoBarcode of the rig to inherit from
bonesobjectyesMaps standard bone names → model bone names
customBonesobjectnoMaps custom (non-standard) bone names → model bone names
eyeRotationLimitsobjectnoEye rotation limits as local Euler angles (see Eye Look-At)
jawRotationLimitsobjectnoThe Jaw bone’s open pose as a local Euler angle (see Jaw)
viewPosition[x, y, z]noFirst-person camera offset in local space (see View Position)
visemesobjectnoMaps standard viseme keys to blendshape names (see Visemes)
eyelidsobjectnoEyelid blendshape names for blink tracking (see Eyelids)

A rig describes the model: which bones it has, how far its eyes can turn, which shapes close its lids. How the avatar behaves with them (whether it looks around and blinks, how shy or excited it is, how fast it blinks) belongs to the avatar, as a dh.eyes component.

The type is inferred from the file extension, so $type is not needed in source files. The compiler adds it automatically during builds.

Mapping a Mixamo-rigged model:

humanoid.dh-rig
{
"name": "Mayu Humanoid Rig",
"bones": {
"Hips": "mixamorig:Hips",
"Spine": "mixamorig:Spine",
"Chest": "mixamorig:Spine1",
"UpperChest": "mixamorig:Spine2",
"Neck": "mixamorig:Neck",
"Head": "mixamorig:Head",
"LeftEye": "mixamorig:LeftEye",
"RightEye": "mixamorig:RightEye",
"LeftShoulder": "mixamorig:LeftShoulder",
"LeftUpperArm": "mixamorig:LeftArm",
"LeftLowerArm": "mixamorig:LeftForeArm",
"LeftHand": "mixamorig:LeftHand",
"RightShoulder": "mixamorig:RightShoulder",
"RightUpperArm": "mixamorig:RightArm",
"RightLowerArm": "mixamorig:RightForeArm",
"RightHand": "mixamorig:RightHand",
"LeftUpperLeg": "mixamorig:LeftUpLeg",
"LeftLowerLeg": "mixamorig:LeftLeg",
"LeftFoot": "mixamorig:LeftFoot",
"LeftToes": "mixamorig:LeftToeBase",
"RightUpperLeg": "mixamorig:RightUpLeg",
"RightLowerLeg": "mixamorig:RightLeg",
"RightFoot": "mixamorig:RightFoot",
"RightToes": "mixamorig:RightToeBase"
},
"customBones": {
"Tail1": "mixamorig:Tail.001",
"Tail2": "mixamorig:Tail.002",
"EarL": "mixamorig:EarL",
"EarR": "mixamorig:EarR"
}
}

You only need to map bones that exist in your model. Missing optional bones are simply skipped.

DigitalHeaven defines 56 standard bones organized into groups. 17 are required for a valid humanoid rig; the rest are optional.

BoneRequired
Root
Hipsyes
Spineyes
Chestyes
UpperChest
Neckyes
Headyes
BoneRequired
LeftEye
RightEye
Jaw
BoneRequired
LeftShoulder
LeftUpperArmyes
LeftLowerArmyes
LeftHandyes
BoneRequired
RightShoulder
RightUpperArmyes
RightLowerArmyes
RightHandyes
BoneRequired
LeftThumbMetacarpal
LeftThumbProximal
LeftThumbDistal
LeftIndexProximal
LeftIndexIntermediate
LeftIndexDistal
LeftMiddleProximal
LeftMiddleIntermediate
LeftMiddleDistal
LeftRingProximal
LeftRingIntermediate
LeftRingDistal
LeftLittleProximal
LeftLittleIntermediate
LeftLittleDistal
BoneRequired
RightThumbMetacarpal
RightThumbProximal
RightThumbDistal
RightIndexProximal
RightIndexIntermediate
RightIndexDistal
RightMiddleProximal
RightMiddleIntermediate
RightMiddleDistal
RightRingProximal
RightRingIntermediate
RightRingDistal
RightLittleProximal
RightLittleIntermediate
RightLittleDistal
BoneRequired
LeftUpperLegyes
LeftLowerLegyes
LeftFootyes
LeftToes
BoneRequired
RightUpperLegyes
RightLowerLegyes
RightFootyes
RightToes

17 required bones: Hips, Spine, Chest, Neck, Head, LeftUpperArm, LeftLowerArm, LeftHand, RightUpperArm, RightLowerArm, RightHand, LeftUpperLeg, LeftLowerLeg, LeftFoot, RightUpperLeg, RightLowerLeg, RightFoot

39 optional bones: Root, UpperChest, face bones, shoulders, fingers, toes, and everything else.

The customBones property handles bones that aren’t part of the humanoid standard: tails, ears, wings, extra joints, whatever your model needs.

{
"customBones": {
"Tail1": "Armature_Tail.001",
"Tail2": "Armature_Tail.002",
"WingL": "Armature_Wing.L",
"WingR": "Armature_Wing.R"
}
}

Custom bone names are freeform; use whatever makes sense for your model. They’re used for armature linking and platform-specific features.

Set a bone to null to explicitly mark it as unmapped (useful when inheriting from a rig and you need to remove a mapping):

humanoid.dh-rig
{
"inherits": "base:rigs/humanoid.dh-rig",
"bones": {
"Jaw": null
}
}

If your model has LeftEye and/or RightEye bones mapped, you can define rotation limits that control how far the eyes can rotate when tracking a target. This is used by supported game mods to make your avatar’s eyes follow nearby players or points of interest.

Each direction specifies the eye bone’s local Euler rotation [X, Y, Z] when looking fully in that direction:

PropertyDescription
upLocal rotation when looking fully upward
downLocal rotation when looking fully downward
leftLocal rotation when looking fully left
rightLocal rotation when looking fully right
humanoid.dh-rig
{
"bones": {
"LeftEye": "mixamorig:LeftEye",
"RightEye": "mixamorig:RightEye"
},
"eyeRotationLimits": {
"up": [12, 0, 0],
"down": [-12, 0, 0],
"left": [0, -12, 0],
"right": [0, 12, 0]
}
}

For most models, eye bones rotate around the X axis for pitch (up/down) and the Y axis for yaw (left/right). A value of 12° in each direction is a good starting point. Adjust per-axis if your model’s bone orientation differs.

If eyeRotationLimits is omitted, no eye tracking is applied even if eye bones are mapped.

innerOuterSplit inside eyeRotationLimits gives separate yaw limits in degrees toward the nose (limitInner) and away from it (limitOuter), for eyes that turn less inward than outward. Absent, both eyes use left/right.

humanoid.dh-rig
{
"eyeRotationLimits": {
"up": [12, 0, 0],
"down": [-12, 0, 0],
"left": [0, -14, 0],
"right": [0, 14, 0],
"innerOuterSplit": { "limitInner": 8, "limitOuter": 14 }
}
}

Unity mods add the eye look with DigitalHeavenEyeLooks.Attach(avatarRoot, rig), or DigitalHeavenEyeLooks.AttachToClone(instance, prefabRig, prefabRoot) for an avatar cloned from a prefab under IL2CPP. Both read the avatar’s resolved dh.eyes, stored at import on the avatar root’s DigitalHeavenAvatar.EyeBehavior (DigitalHeavenEyeLooks.BehaviorOf(rig) finds it), and attach nothing when it turns both look and blink off. The avatar’s switches win over every global one, as in the engine, and each one it turns off is logged: [Face] '<avatar>': eye look disabled by the avatar, blinks disabled by the avatar. The component, DigitalHeavenEyeLook, hosts the same eye brain the engine runs: it rotates the eye bones within eyeRotationLimits and writes the eyelids blink, lookUp and lookDown shapes on the rig’s target mesh. A rig that names no mesh (or whose mesh is not on the copy) blinks on the skinned mesh under the avatar carrying the most of those shapes, as the engine does. What was resolved is one [Face] log line per avatar (Report holds it): the eye bones, or why the eyes stay still, and the blink shapes with their mesh. A shape the mesh lacks is named there with the mesh’s near names and skipped. Each shape’s weight is the larger of the brain’s and whatever the game or an animation already set, so a closed-eye emote stays closed.

A mod steers the gaze through these members. With none of them set, the eyes wander and blink on their own.

MemberKindDescription
LookTargetVector3 propertyA world position to look at this frame. Setting it sets HasLookTarget.
NormalizedLookVector2 propertyA look direction this frame relative to the head: X = yaw (-1 left, +1 right), Y = pitch (-1 down, +1 up), as fractions of the limits. Setting it sets HasNormalizedLook.
SetLookAtTransform(Transform) / ClearLookAtTransform()methodsFollow a transform across frames until cleared or destroyed. HasLookAtTransform reports it.
AddPointOfInterest(EyeLookPoi) / SetPointsOfInterest(list) / ClearPointsOfInterest()methodsOffer this frame’s nearby points (faces, hands, objects, mirrors). The list is reused, so offering points allocates nothing.
IsSpeaking, FocuspropertiesSpeaking raises the blink rate; focus (0 to 1) lowers it. These persist until changed.
OfferHeads(AvatarLookTargets, selfId, maxPois, mutualCosine)methodOffer this frame’s heads (Core’s AvatarLookTargets, the set the engine gathers its players’ heads into) as faces: the nearest few that are not this avatar, each marked when it looks back.
HostStepped / Step(dt)property, methodA host that knows when its game writes the pose sets HostStepped and calls Step right after; the component’s own LateUpdate then does nothing. An IL2CPP-injected component’s execution order is not honored, so this is the only way to run after the game’s pose there.
CopyTo(Transform)methodA FaceCopy that carries the eye rotations and eyelid weights onto a same-shaped copy of the avatar, such as a mirror’s reflection. Call its Apply after the copy’s own pose write.
BlinksStarted, SaccadesStarted, Steps, LastTargetpropertiesCounters and the current target, for a mod’s health log.

LookTarget, NormalizedLook and the points of interest last one frame: the component clears them after each step. A forced gaze takes the first of NormalizedLook, the followed transform, then LookTarget, and bypasses target selection while the saccades, lid follow and blinks still run. LookTarget suits a game with its own look-at position (Schedule I’s gaze system, PEAK’s), and NormalizedLook suits face tracking with raw pitch and yaw.

DigitalHeavenEyeLooks.Enabled turns the wandering and the blinks off for every avatar; a forced gaze is still followed. An avatar whose own dh.eyes turns look off ignores a forced gaze too. Most mods expose it as one config switch. Every tuning number lives on DigitalHeavenEyeLooks.Settings, read by every avatar each step; Core’s EyeLookSettingFields.All lists each field with its range and help, the table the engine’s client.anim.* preferences are built from, so a mod that exposes the full set (BONELAB) offers the engine’s names and defaults.

The viewPosition property defines the first-person camera offset in the avatar’s local space as an [x, y, z] float array. Platforms use this to position the player’s viewpoint (e.g. VRChat’s ViewPosition on the avatar descriptor).

humanoid.dh-rig
{
"viewPosition": [0, 1.32, 0.07]
}

If omitted, the platform uses its own default viewpoint.

In the engine the Y component is the avatar’s eye height, and it is the first choice of the three the compiler measures one by. Authoring it is how a rig says exactly how tall its wearer is; without it the build guesses along the head bone, and without a head bone it guesses from the model’s height and warns.

The Z component is carried too, as the compiled avatar’s eyeForward, and it places the first-person camera: the eye sits that far in front of the body’s vertical axis, which is the difference between looking out of a muzzle and looking out of the middle of a skull. Everything derived from the eye — the neck the camera swings on and the height the head is cut at — is measured from there. Authoring a viewPosition is therefore the one way a rig states its own first-person viewpoint exactly; without it the engine reads the eye bones of the bind pose instead.

Every blendshape name on this page — visemes, eyelids.blink, eyelids.lookUp and eyelids.lookDown — is checked at build time against the morph targets of the mesh it targets; a name that resolves to nothing is a build warning naming near matches, not an error. The same check covers a dh.renderer’s shapes: its part is looked up in the tree the resolved object builds, so a part of an added record, a nested record or a variant’s replacing model from another pallet finds the mesh the engine finds. A model the build cannot read is named in its own warning, never reported as “not a morph target”.

The visemes property maps the 15 standard Oculus viseme keys to blendshape names on the avatar’s target mesh. The Oculus set is DigitalHeaven’s reference set and every viseme is optional: whatever the avatar lacks is substituted as well as its face allows (see What an avatar’s mouth can show). A key that is not one of the 15 is ignored, and the build warns about it (keys are case-sensitive: AA is not aa).

KeyPhoneme
silSilence
PPP, B, M
FFF, V
THTh
DDD, T, N
kkK, G, NG
CHCh, J, Sh
SSS, Z
nnN (nasal)
RRR
aaA
EE
ihI
ohO
ouU

Each key maps to the name of a blendshape on the mesh named by the mesh field of the avatar’s dh.skeleton. Missing keys are skipped, and a face whose shapes follow a known naming needs no map at all (below).

humanoid.dh-rig
{
"visemes": {
"sil": "vrc.v_sil",
"PP": "vrc.v_PP",
"FF": "vrc.v_FF",
"TH": "vrc.v_TH",
"DD": "vrc.v_DD",
"kk": "vrc.v_kk",
"CH": "vrc.v_CH",
"SS": "vrc.v_SS",
"nn": "vrc.v_nn",
"RR": "vrc.v_RR",
"aa": "vrc.v_aa",
"E": "vrc.v_E",
"ih": "vrc.v_ih",
"oh": "vrc.v_oh",
"ou": "vrc.v_ou"
}
}

At runtime every host resolves one mouth plan per avatar from the rig and the face mesh’s blendshape names (Core’s MouthRig). For each of the 15 visemes it takes the first of:

  1. Direct: the viseme’s own shape. That is the shape visemes maps it to, or else a shape named vrc.v_<key>, v_<key> or viseme_<key> (any case), or else exactly <key> (aa, PP, … sil).
  2. Recipe: a fixed blend of two or three shapes the avatar does have, for example E from ih × 0.5 + aa × 0.5, or oh from ou × 0.6 + aa × 0.4. The five vowels aa, ih, ou, E and oh may also come from their MMD vowel shapes (あ, い, う, え, お, or a, i, u, e, o), so a vowels-only MMD face still gets every viseme a blend can make.
  3. Substitute: the nearest single shape present, for example PP falls back to sil and ou to oh.
  4. Missing: the viseme does not move the mouth.

The table is Core’s MouthRecipes; its weights follow the three-shape mixes Blender’s CATS plugin generates, and are a starting point to tune by eye.

Separately, the plan picks how the mouth opens for loudness alone: a mouth-open shape (a name reading Mouth Open or Jaw Open once case, spaces, underscores, dots and dashes are ignored, so Jaw_Open and jawOpen count), else the Jaw bone, else nothing. Hosts log the plan once per avatar in one line, for example direct sil PP … ou; open shape 'Mouth Open'.

An importer that keeps only referenced blendshapes (Unity’s BlendShapeImport.Referenced, the default in BONELAB and MegaBonk) also keeps every shape this resolution could use, since the plan is made after import.

One pipeline, shared by every host: a source (a microphone, a networked voice, a dh.speakSound clip) feeds an analyzer, the analyzer writes a mouth frame (15 viseme weights, loudness and laughter, each 0..1), and Core’s mouth brain drives the plan from it.

  • A frame with visemes shapes the mouth through the plan’s direct, recipe and substitute shapes.
  • While a voice is heard, its open visemes (everything but sil) take at least mouthMinOpen times the voice’s level, scaled up together so their proportions stay, or on aa when the analysis found none. A quiet or uncertain analysis still opens the mouth instead of sitting mostly on sil.
  • A loudness-only frame (the volume analyzer, or a host that only knows how loud a voice is) speaks aa on an avatar with visemes, and opens the mouth-open shape or the jaw on one without.
  • Silence returns the mouth to rest.
  • Combining with authored weights: a blink is combined with an authored weight by max, but a mouth is not. While the voice is heard, and for mouthHold seconds after, the driver owns every shape in the plan and the jaw, so a driven mouth can close a shape an author left open. Ownership ramps in and out with the attack and release, and the authored weight returns once the voice stops: authored + (driven − authored) × ownership.

Every tuning number is a preference (Core’s MouthSettingFields):

SettingDefaultMeaning
mouthEnabledonWhether avatars move their mouth to a voice at all
mouthVisemeSmoothing70OVRLipSync’s scale: 1 snaps to each analysis, 100 barely moves; the percent of the old weight kept per 10 ms
mouthVisemeStrength1Scale on every analyzed viseme weight
mouthAttack / mouthRelease0.03 s / 0.1 sHow fast a loudness-driven mouth opens and closes, and ownership ramps
mouthNoiseFloor0.01Loudness below this is silence
mouthGain8Multiplier from loudness past the floor to opening
mouthHold0.25 sHow long the driver keeps the mouth after the voice stops
mouthJawBoneonWhether a mouth with no shape to open may open the Jaw bone
mouthVolumeAttack / mouthVolumeRelease0.005 s / 0.05 sThe volume analyzer’s loudness envelope
mouthMinOpen0.5While a voice is heard, the least share of the mouth open visemes take, times its level (0 off)
mouthAutoLevelEnabledonWhether a live microphone runs through the auto level below
mouthAutoLevelTarget−20 dBFSThe level the automatic gain brings a voice’s loud parts to
mouthAutoLevelMaxGain24 dBThe most the gain raises a voice
mouthAutoLevelAttack / mouthAutoLevelRelease0.05 s / 1.5 sHow fast the gain falls for a louder voice and rises for a quieter one
mouthGateOpen / mouthGateClose10 dB / 6 dBHow far above the noise floor the input opens the gate and keeps it open
mouthGateMinimum−55 dBFSThe quietest input that can open the gate, whatever the floor
mouthGateHold / mouthGateFade0.2 s / 0.02 sHow long the gate stays open past the close level, and how fast it fades
mouthNoiseWindow2 sThe noise floor is the quietest input over this long

A microphone delivers a voice at whatever level its gain and the speaker’s distance give it; quiet speech into a headset can sit near −45 dBFS over room noise near −65 dBFS. The brain’s mouthNoiseFloor (0.01, −40 dBFS) and the MFCC’s mouthQuietRms (−50 dBFS) were set for a normal speaking level, so quiet speech fell under them and the mouth barely moved. Core’s MouthAutoLevel sits between a live microphone and its analyzer and fixes the level first:

  • Noise floor. The input is measured in 10 ms blocks; the floor is the quietest block over mouthNoiseWindow, the pauses between words, so it follows the room and never the voice.
  • Gate. It opens mouthGateOpen dB above the floor (never below mouthGateMinimum), stays open down to mouthGateClose dB above it plus mouthGateHold, and fades over mouthGateFade. A closed gate hands the analyzer silence, so steady room noise moves nothing.
  • Gain. While the gate is open, the gain heads for whatever brings the block to mouthAutoLevelTarget, between 0 and mouthAutoLevelMaxGain dB, falling fast and rising slowly, so it settles on the voice’s loud parts. A closed gate holds the gain for the next word.

On a synthetic voice at −45 dBFS over −65 dBFS noise, the old chain never opened the mouth; with the auto level the summed shape weight averages 0.7. Steady noise at −65, −50 and −40 dBFS keeps it shut. Turned off, the samples pass through untouched and are still measured, so a host’s trace reads the same numbers. Hosts run it for the local player’s microphone only: a networked voice is already gated by its sender.

A Unity host runs all of this through AvatarMouth (DigitalHeaven.Unity): it resolves the plan on an avatar clone, writes each step’s weights and jaw, and leaves the voice and the analyzer to the host. FaceCopy.Of carries the jaw and mouth weights onto a mirror reflection or a posed copy along with the eyes.

PlatformVisemes
VRChat✅ exported to the avatar descriptor (VisemeBlendShape mode, the 15 slots in order); VRChat analyzes and drives them itself
BONELAB🟡 driven from LabFusion’s voices, DH’s own microphone capture in single player (opt-in) and dh.speakSound clips; untested in game (see BONELAB)
Garry’s Mod🟡 the game reports voice volume; the Source export emits no mouth flexes yet
Engine❌ no voice chat, and dh.speakSound is not played yet
Resonite❔ Resonite’s own avatar creator may wire the shapes; DH writes no viseme mapping
MegaBonk, Minecraft, ULTRAKILL, Project Zomboid, Schedule I, Risk of Rain 2, Liar’s Bar❔ not checked

Core has two analyzers, and a host picks one per voice:

  • Volume (VolumeMouthAnalyzer) reports loudness only.
  • MFCC (MfccMouthAnalyzer) also reads the mouth’s shape. It is a plain C# port of uLipSync’s analysis (MIT), with no Burst, Jobs or Unity dependency, so every host can run it. Each analysis takes the newest 64 ms of the voice, computes 12 MFCCs exactly as uLipSync does, and scores them against a profile: the coefficients recorded while a voice held each sound. Its loudness is the volume analyzer’s, so both modes report the same level.

Each profile sound lands on one Oculus viseme. Weights sum to 1: each sound’s share is scaled by how loud the voice is (uLipSync’s log scale between mouthQuietRms and mouthFullRms), and the rest goes to sil.

Profile soundViseme
A / あaa
I / いih
U / うou
E / えE
O / おoh
N / んnn
SSS
- (breath) or anything unknownsil
any of the 15 Oculus keysitself

uLipSync’s sounds are vowels, so on its own the analyzer never produces PP, FF, TH, DD, kk, CH, RR or (without an S sound) SS. Two ways fill the gap:

  • Calibrate the consonant. A profile sound may be named by any Oculus key, so a voice calibrated with a PP or FF sound produces it.
  • Consonant guesses (mouthConsonants, off by default). These are heuristics, not recognition:
    • A high zero-crossing rate (unvoiced hiss) reads as SS above mouthSibilantSplit and CH below it. A hiss quieter than mouthWeakFricativeRms reads as FF.
    • A clearly voiced level collapsing within one analysis to mouthClosureDrop of itself reads as the lips closing. PP shows for mouthClosureHold. It is mixed in after the loudness scale, since a closed mouth is quiet.
    • TH, DD, kk and RR have no guess.
    • Both need a voice at a speaking level: a hiss counts only once the analysis ring is above mouthQuietRms and its share is scaled by the voice’s loudness, and a closure only after a level of at least mouthFullRms. A quiet microphone (−40 dBFS speech) produces neither; through the auto level both fire on a synthetic “asa” and “apa”.

Profiles. The default is uLipSync’s sample female profile (A I U E O and breath). Its sample male profile (with S) ships as well. Both are calibrated to someone else’s voice, so they read a voice only roughly.

A profile is uLipSync’s own export JSON, so a profile calibrated in uLipSync’s Unity editor loads unchanged (MfccProfile.FromJson). To calibrate in DH:

  1. Hold one sound.
  2. Pass the analyzer’s current Mfcc to MfccProfile.AddCalibration with the sound’s name, once per analysis. The profile keeps the latest 16 takes per sound, as uLipSync does.
  3. Save the profile with ToJson.

No host offers a calibration screen yet.

The MFCC analyzer’s tuning is a preference set of its own (Core’s MfccMouthSettingFields):

SettingDefaultMeaning
mouthAnalysisInterval0.01 sHow much of a voice accumulates before the next analysis
mouthQuietRms / mouthFullRms0.00316 / 0.0316The voice level where the shape starts and where it is full (uLipSync’s log10 −2.5 and −1.5)
mouthConsonantsoffGuess SS, CH, FF and PP
mouthHissStart / mouthHissFull2500 / 4000Zero crossings per second where a hiss starts and is certain
mouthSibilantSplit5000Zero crossings per second above which a hiss is SS, below CH
mouthWeakFricativeRms0.02A hiss quieter than this is FF
mouthClosureDrop0.2A voiced level falling to this fraction of itself in one analysis is PP
mouthClosureHold0.06 sHow long a detected closure shows

One analysis costs about 0.1 ms on a desktop CPU (net10, x64). It runs once per buffer a host hands it, and at most once per mouthAnalysisInterval.

PlatformMFCC analysis
BONELAB🟡 the MouthAnalysis preference: Shapes for an avatar with visemes, Volume otherwise by default; untested in game
Engine❌ no voice source yet
VRChat❌ not used: VRChat analyzes the voice itself (OVRLipSync)
Garry’s Mod, Resonite, MegaBonk, Minecraft, ULTRAKILL, Project Zomboid, Schedule I, Risk of Rain 2, Liar’s Bar❔ not checked

The jawRotationLimits property gives the Jaw bone’s open pose, written the same way as eyeRotationLimits: a local Euler rotation [X, Y, Z] in degrees, the bone’s rotation from rest with the mouth fully open. It needs Jaw mapped in bones.

humanoid.dh-rig
{
"bones": { "Jaw": "Jaw" },
"jawRotationLimits": { "open": [15, 0, 0] }
}

The jaw is the last fallback: the mouth opens by bone only when the face has no mouth-open shape and the avatar has no visemes, and only while mouthJawBone is on. Each integration decides whether to use it: a host where a driven jaw breaks the game turns mouthJawBone off by default. The build refuses an open that is not three numbers and warns when no Jaw bone is mapped.

Games that pose the avatar from their own humanoid. A mapped Jaw is part of the avatar’s Unity humanoid Avatar, so a game’s pose copied through Unity’s muscles could move it. A game rig with no jaw reports its jaw muscles as 0, which is Unity’s muscle-space neutral rather than a closed mouth, so the copy held the mouth open. The shared HumanoidPoseMirror therefore leaves the Jaw alone when the game’s rig maps none (Schedule I, MegaBonk, the ULTRAKILL mirror), and keeps copying it from a game rig that has one. Nothing needs to be unmapped in the rig.

The eyelids property maps eyelid states to blendshape names on the target mesh. Used by platforms that support eyelid tracking or blink animation.

PropertyTypeDescription
blinkstring or listBlendshape(s) for closed eyes: one name, or a list fired together
lookUpstring?Blendshape for eyelids when looking up
lookDownstring?Blendshape for eyelids when looking down

All three are optional — omit a key to skip that eyelid state.

blink takes one name or a list. Every entry of a list fires together. An entry is a name (closing both eyes) or an object naming the eye it closes, for an avatar with one shape per eye:

humanoid.dh-rig
{
"eyelids": {
"blink": [
{ "shape": "Blink_L", "eye": "left" },
{ "shape": "Blink_R", "eye": "right" }
]
}
}

eye is both (the default), left or right. The compiler warns when a list closes one eye and never the other.

VRChat’s descriptor has one blink slot and two personality sliders, so the VRChat export maps onto them:

  • The avatar’s resolved dh.eyes look.confidence becomes the Shy/Confident slider and look.activity the Calm/Excited slider. look.enabled: false turns the descriptor’s eye look off and drops the lid follow shapes.
  • The blink slot gets the first both-eyes entry of eyelids.blink. A list with per-eye shapes only has nothing to put there: the import warns, naming the avatar and its shapes, and exports no blink. blink.enabled: false exports no blink shape; with both off the descriptor gets no eyelids at all.
  • The blink timing and innerOuterSplit have no VRChat equivalent and are not exported.
humanoid.dh-rig
{
"eyelids": {
"lookUp": "Eye Look Up",
"lookDown": "Eye Look Down"
}
}

DigitalHeaven bone names are automatically mapped to Unity’s HumanBone names at runtime. You don’t need to think about Unity naming. Just use the standard names above and the runtime handles the translation.

The runtime component (DigitalHeavenRig) can:

  • Create a Unity Avatar from the bone mappings
  • Set up an Animator with the generated avatar
  • Build a HumanDescription for humanoid configuration

All 56 standard bones (except Root) have direct Unity equivalents.