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.).
Properties
Section titled “Properties”| Property | Type | Required | Description |
|---|---|---|---|
$type | "dh.rig" | no | Type identifier |
name | string | no | Display name |
description | string | no | Description |
inherits | string | no | Barcode of the rig to inherit from |
bones | object | yes | Maps standard bone names → model bone names |
customBones | object | no | Maps custom (non-standard) bone names → model bone names |
eyeRotationLimits | object | no | Eye rotation limits as local Euler angles (see Eye Look-At) |
jawRotationLimits | object | no | The Jaw bone’s open pose as a local Euler angle (see Jaw) |
viewPosition | [x, y, z] | no | First-person camera offset in local space (see View Position) |
visemes | object | no | Maps standard viseme keys to blendshape names (see Visemes) |
eyelids | object | no | Eyelid 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
$typeis not needed in source files. The compiler adds it automatically during builds.
Example
Section titled “Example”Mapping a Mixamo-rigged model:
{ "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.
Standard Bones
Section titled “Standard Bones”DigitalHeaven defines 56 standard bones organized into groups. 17 are required for a valid humanoid rig; the rest are optional.
| Bone | Required |
|---|---|
| Root | |
| Hips | yes |
| Spine | yes |
| Chest | yes |
| UpperChest | |
| Neck | yes |
| Head | yes |
| Bone | Required |
|---|---|
| LeftEye | |
| RightEye | |
| Jaw |
Left Arm
Section titled “Left Arm”| Bone | Required |
|---|---|
| LeftShoulder | |
| LeftUpperArm | yes |
| LeftLowerArm | yes |
| LeftHand | yes |
Right Arm
Section titled “Right Arm”| Bone | Required |
|---|---|
| RightShoulder | |
| RightUpperArm | yes |
| RightLowerArm | yes |
| RightHand | yes |
Left Hand
Section titled “Left Hand”| Bone | Required |
|---|---|
| LeftThumbMetacarpal | |
| LeftThumbProximal | |
| LeftThumbDistal | |
| LeftIndexProximal | |
| LeftIndexIntermediate | |
| LeftIndexDistal | |
| LeftMiddleProximal | |
| LeftMiddleIntermediate | |
| LeftMiddleDistal | |
| LeftRingProximal | |
| LeftRingIntermediate | |
| LeftRingDistal | |
| LeftLittleProximal | |
| LeftLittleIntermediate | |
| LeftLittleDistal |
Right Hand
Section titled “Right Hand”| Bone | Required |
|---|---|
| RightThumbMetacarpal | |
| RightThumbProximal | |
| RightThumbDistal | |
| RightIndexProximal | |
| RightIndexIntermediate | |
| RightIndexDistal | |
| RightMiddleProximal | |
| RightMiddleIntermediate | |
| RightMiddleDistal | |
| RightRingProximal | |
| RightRingIntermediate | |
| RightRingDistal | |
| RightLittleProximal | |
| RightLittleIntermediate | |
| RightLittleDistal |
Left Leg
Section titled “Left Leg”| Bone | Required |
|---|---|
| LeftUpperLeg | yes |
| LeftLowerLeg | yes |
| LeftFoot | yes |
| LeftToes |
Right Leg
Section titled “Right Leg”| Bone | Required |
|---|---|
| RightUpperLeg | yes |
| RightLowerLeg | yes |
| RightFoot | yes |
| RightToes |
Summary
Section titled “Summary”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.
Custom Bones
Section titled “Custom Bones”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.
Setting a Null Mapping
Section titled “Setting a Null Mapping”Set a bone to null to explicitly mark it as unmapped (useful when inheriting from a rig and you need to remove a mapping):
{ "inherits": "base:rigs/humanoid.dh-rig", "bones": { "Jaw": null }}Eye Look-At
Section titled “Eye Look-At”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:
| Property | Description |
|---|---|
up | Local rotation when looking fully upward |
down | Local rotation when looking fully downward |
left | Local rotation when looking fully left |
right | Local rotation when looking fully right |
{ "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.
Inner and outer yaw
Section titled “Inner and outer yaw”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.
{ "eyeRotationLimits": { "up": [12, 0, 0], "down": [-12, 0, 0], "left": [0, -14, 0], "right": [0, 14, 0], "innerOuterSplit": { "limitInner": 8, "limitOuter": 14 } }}Runtime API (Eye Look-At)
Section titled “Runtime API (Eye Look-At)”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.
| Member | Kind | Description |
|---|---|---|
LookTarget | Vector3 property | A world position to look at this frame. Setting it sets HasLookTarget. |
NormalizedLook | Vector2 property | A 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() | methods | Follow a transform across frames until cleared or destroyed. HasLookAtTransform reports it. |
AddPointOfInterest(EyeLookPoi) / SetPointsOfInterest(list) / ClearPointsOfInterest() | methods | Offer this frame’s nearby points (faces, hands, objects, mirrors). The list is reused, so offering points allocates nothing. |
IsSpeaking, Focus | properties | Speaking raises the blink rate; focus (0 to 1) lowers it. These persist until changed. |
OfferHeads(AvatarLookTargets, selfId, maxPois, mutualCosine) | method | Offer 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, method | A 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) | method | A 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, LastTarget | properties | Counters 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.
View Position
Section titled “View Position”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).
{ "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.
Visemes
Section titled “Visemes”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).
| Key | Phoneme |
|---|---|
sil | Silence |
PP | P, B, M |
FF | F, V |
TH | Th |
DD | D, T, N |
kk | K, G, NG |
CH | Ch, J, Sh |
SS | S, Z |
nn | N (nasal) |
RR | R |
aa | A |
E | E |
ih | I |
oh | O |
ou | U |
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).
{ "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" }}What an avatar’s mouth can show
Section titled “What an avatar’s mouth can show”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:
- Direct: the viseme’s own shape. That is the shape
visemesmaps it to, or else a shape namedvrc.v_<key>,v_<key>orviseme_<key>(any case), or else exactly<key>(aa,PP, …sil). - Recipe: a fixed blend of two or three shapes the avatar does have, for example
Efromih× 0.5 +aa× 0.5, orohfromou× 0.6 +aa× 0.4. The five vowelsaa,ih,ou,Eandohmay also come from their MMD vowel shapes (あ,い,う,え,お, ora,i,u,e,o), so a vowels-only MMD face still gets every viseme a blend can make. - Substitute: the nearest single shape present, for example
PPfalls back tosilandoutooh. - 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.
How the mouth is driven
Section titled “How the mouth is driven”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 leastmouthMinOpentimes the voice’s level, scaled up together so their proportions stay, or onaawhen the analysis found none. A quiet or uncertain analysis still opens the mouth instead of sitting mostly onsil. - A loudness-only frame (the volume analyzer, or a host that only knows how loud a voice is) speaks
aaon 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
mouthHoldseconds 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):
| Setting | Default | Meaning |
|---|---|---|
mouthEnabled | on | Whether avatars move their mouth to a voice at all |
mouthVisemeSmoothing | 70 | OVRLipSync’s scale: 1 snaps to each analysis, 100 barely moves; the percent of the old weight kept per 10 ms |
mouthVisemeStrength | 1 | Scale on every analyzed viseme weight |
mouthAttack / mouthRelease | 0.03 s / 0.1 s | How fast a loudness-driven mouth opens and closes, and ownership ramps |
mouthNoiseFloor | 0.01 | Loudness below this is silence |
mouthGain | 8 | Multiplier from loudness past the floor to opening |
mouthHold | 0.25 s | How long the driver keeps the mouth after the voice stops |
mouthJawBone | on | Whether a mouth with no shape to open may open the Jaw bone |
mouthVolumeAttack / mouthVolumeRelease | 0.005 s / 0.05 s | The volume analyzer’s loudness envelope |
mouthMinOpen | 0.5 | While a voice is heard, the least share of the mouth open visemes take, times its level (0 off) |
mouthAutoLevelEnabled | on | Whether a live microphone runs through the auto level below |
mouthAutoLevelTarget | −20 dBFS | The level the automatic gain brings a voice’s loud parts to |
mouthAutoLevelMaxGain | 24 dB | The most the gain raises a voice |
mouthAutoLevelAttack / mouthAutoLevelRelease | 0.05 s / 1.5 s | How fast the gain falls for a louder voice and rises for a quieter one |
mouthGateOpen / mouthGateClose | 10 dB / 6 dB | How far above the noise floor the input opens the gate and keeps it open |
mouthGateMinimum | −55 dBFS | The quietest input that can open the gate, whatever the floor |
mouthGateHold / mouthGateFade | 0.2 s / 0.02 s | How long the gate stays open past the close level, and how fast it fades |
mouthNoiseWindow | 2 s | The noise floor is the quietest input over this long |
Microphone auto level
Section titled “Microphone auto level”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
mouthGateOpendB above the floor (never belowmouthGateMinimum), stays open down tomouthGateClosedB above it plusmouthGateHold, and fades overmouthGateFade. 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 andmouthAutoLevelMaxGaindB, 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.
| Platform | Visemes |
|---|---|
| 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 |
Reading the mouth’s shape
Section titled “Reading the mouth’s shape”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 sound | Viseme |
|---|---|
A / あ | aa |
I / い | ih |
U / う | ou |
E / え | E |
O / お | oh |
N / ん | nn |
S | SS |
- (breath) or anything unknown | sil |
| any of the 15 Oculus keys | itself |
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
PPorFFsound produces it. - Consonant guesses (
mouthConsonants, off by default). These are heuristics, not recognition:- A high zero-crossing rate (unvoiced hiss) reads as
SSabovemouthSibilantSplitandCHbelow it. A hiss quieter thanmouthWeakFricativeRmsreads asFF. - A clearly voiced level collapsing within one analysis to
mouthClosureDropof itself reads as the lips closing.PPshows formouthClosureHold. It is mixed in after the loudness scale, since a closed mouth is quiet. TH,DD,kkandRRhave no guess.- Both need a voice at a speaking level: a hiss counts only once the analysis ring is above
mouthQuietRmsand its share is scaled by the voice’s loudness, and a closure only after a level of at leastmouthFullRms. A quiet microphone (−40 dBFS speech) produces neither; through the auto level both fire on a synthetic “asa” and “apa”.
- A high zero-crossing rate (unvoiced hiss) reads as
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:
- Hold one sound.
- Pass the analyzer’s current
MfcctoMfccProfile.AddCalibrationwith the sound’s name, once per analysis. The profile keeps the latest 16 takes per sound, as uLipSync does. - 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):
| Setting | Default | Meaning |
|---|---|---|
mouthAnalysisInterval | 0.01 s | How much of a voice accumulates before the next analysis |
mouthQuietRms / mouthFullRms | 0.00316 / 0.0316 | The voice level where the shape starts and where it is full (uLipSync’s log10 −2.5 and −1.5) |
mouthConsonants | off | Guess SS, CH, FF and PP |
mouthHissStart / mouthHissFull | 2500 / 4000 | Zero crossings per second where a hiss starts and is certain |
mouthSibilantSplit | 5000 | Zero crossings per second above which a hiss is SS, below CH |
mouthWeakFricativeRms | 0.02 | A hiss quieter than this is FF |
mouthClosureDrop | 0.2 | A voiced level falling to this fraction of itself in one analysis is PP |
mouthClosureHold | 0.06 s | How 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.
| Platform | MFCC 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.
{ "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.
Eyelids
Section titled “Eyelids”The eyelids property maps eyelid states to blendshape names on the target mesh. Used by platforms that support eyelid tracking or blink animation.
| Property | Type | Description |
|---|---|---|
blink | string or list | Blendshape(s) for closed eyes: one name, or a list fired together |
lookUp | string? | Blendshape for eyelids when looking up |
lookDown | string? | 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:
{ "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.
What VRChat Receives
Section titled “What VRChat Receives”VRChat’s descriptor has one blink slot and two personality sliders, so the VRChat export maps onto them:
- The avatar’s resolved
dh.eyeslook.confidencebecomes the Shy/Confident slider andlook.activitythe Calm/Excited slider.look.enabled: falseturns 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: falseexports no blink shape; with both off the descriptor gets no eyelids at all. - The blink timing and
innerOuterSplithave no VRChat equivalent and are not exported.
{ "eyelids": { "lookUp": "Eye Look Up", "lookDown": "Eye Look Down" }}Unity Integration
Section titled “Unity Integration”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
Avatarfrom the bone mappings - Set up an
Animatorwith the generated avatar - Build a
HumanDescriptionfor humanoid configuration
All 56 standard bones (except Root) have direct Unity equivalents.