Skip to content

dh.avatar

Extension: .dh-avatar
Type ID: dh.avatar

An avatar is the top-level definition for a wearable character. It extends dh.object, meaning it places a model and carries components, children and objects exactly as an object does, and adds avatar-specific properties.

PropertyTypeRequiredDescription
$type"dh.avatar"noType identifier
namestringnoDisplay name
descriptionstringnoDescription
modelstringnoThe model the avatar is made of (.glb or .fbx); a variant writing it replaces the inherited model
inheritsstringnoBarcode of the avatar to inherit from
propertiesobjectnoKey-value pairs of float properties (e.g. mass, height)
shadowRadiusnumbernoRadius in meters of the grounding shadow under the avatar, at its own size. Overrides the measured one; must be positive
shadowBodyRadiusnumbernoHorizontal radius in meters of the torso and hips the ambient shadow takes its occlusion from. Overrides the measured one; must be positive
componentsarraynoThe root’s components
childrenarraynoOverrides of the model’s parts
objectsarraynoRecords the avatar adds: outfits and accessories

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

avatar.dh-avatar
{
"name": "Custom Mayu",
"inherits": "tech.azuki.avatars.mayu:avatar.dh-avatar",
"properties": {
"height": 1.42
},
"model": "models/body.glb",
"components": [
{ "$type": "dh.skeleton", "rig": "rigs/humanoid.dh-rig" }
],
"children": [
{
"path": "Body",
"components": [ { "$type": "dh.renderer", "materials": { "Skin": "materials/skin.dh-mat" } } ]
}
],
"objects": [
{
"$type": "instance",
"id": "glasses",
"source": "accessories/glasses/glasses.dh-obj",
"components": [ { "$type": "dh.armatureLink", "target": "", "bone": "Head" } ]
}
]
}

An avatar takes every component an object takes, on the same owners. These are the ones that make a character:

ComponentWhereWhat it does
dh.skeletonroot or armature partThe rig the armature is described by; humanoid when the rig maps humanoid bones
dh.renderera mesh partMaterials by slot name and blendshape weights
dh.armatureLinkan added record or a partLinks an outfit’s armature bone to bone, or hangs an accessory on one bone
dh.springBonea chain’s root boneSpring physics on a bone chain (hair, tails, ears)
dh.springColliderrootAdds or overrides a sphere or capsule the chains listing it collide with
dh.eyesrootHow the eyes behave: look and blink on/off, personality, blink timing
dh.reactionrootWhat the avatar does on a game event (a jump, a hit, its death): dh.playSound / dh.speakSound actions

Every one of them inherits by the one merge rule: a variant states only what departs, and "removed": true drops an inherited component, part or record. How an accessory’s own extra bone chains are grafted onto the body, and how keepWorldTransform links one without moving it, is on the dh.armatureLink entry.

The rig describes the eyes as a model: the eye bones, how far they turn, the eyelid shapes. How they behave is the avatar’s, stated with a root dh.eyes component: whether they look around, whether they blink, how shy or excited they are, how fast they blink. It inherits down the inherits chain field by field, so an avatar whose eyes are closed for good (its lid shapes held at 1) can turn both off over a base that keeps them:

pitr-mayu.dh-avatar
{
"inherits": "tech.azuki.avatars.mayu:mayu-tora.dh-avatar",
"components": [
{ "$type": "dh.eyes", "look": { "enabled": false }, "blink": { "enabled": false } }
],
"children": [
{ "path": "Armature/Body", "components": [ { "$type": "dh.renderer", "shapes": { "Howawa L": 1, "Howawa R": 1 } } ] }
]
}

What the avatar does when a game reports something about it (it jumped, it took a hit, it died) is a root dh.reaction component per event and tier, holding dh.speakSound (from the mouth) and dh.playSound (from the body) actions. A reaction is keyed by its on and tier, so a variant stating one replaces the inherited reaction’s actions whole, and "removed": true drops it:

mltn-taidum.dh-avatar
{
"inherits": "io.mltn.avatars.taidum-base:taidum.dh-avatar",
"components": [
{ "$type": "dh.reaction", "on": "dying", "actions": [
{ "$type": "dh.speakSound", "clips": ["dev.pitr:sounds/hiss.ogg"] }
] }
]
}

A platform’s settings file (the one that holds npcs) can carry components of its own, dh.reactions laid over the whole chain by the same rule, for a reaction that is right on one platform only. The file’s old patches list is ignored with a warning naming the rename. The events, their tiers, the default face each leaves and every platform’s support are on the avatar game events page.

In the engine, a player wears an avatar by its barcode: avatar dev.example.avatars.nova:nova.dh-avatar from the console, or the persisted client.avatar preference. The server replicates it per player and every client draws it on that player’s pawn in bind pose; avatar none takes it off and draws the placeholder box instead. The engine’s Options → Player page and a mod’s config window both offer the same picker over the same list, and both apply through that same path. By default the model is fitted: drawn at whatever multiplier puts its eyes at the standard 1.64 m, so swapping avatars never changes how tall you are. See sizing.

A compiled avatar carries eyeHeight — how far its eyes sit above its feet, in meters — eyeForward, how far in front of its vertical axis they sit, and eyeHeightSource, the rule that produced both. None of them is authored on the avatar: all are measured by the compiler and written into the compiled asset, because every client that draws a player has to fit that player to the same number, and a number measured at build time is the same on all of them by construction.

The precedence, first answer wins:

eyeHeightSourceHow it was measured
authoredThe rig’s viewPosition Y, and its Z as eyeForward. Exact, and the only one a person controls.
guessedEye height only, eyeForward zero: 0.65 of the way from the head bone’s own origin to its tip — the top of the neck to the crown, as the rig itself draws it. The fraction is AvatarEyeMeasure.EyeGuessAlongHead.
boundsEye height only, eyeForward zero: 1.64 / 1.83 of the model’s height, and a build warning. Nothing in a bounds box knows a hat from a head.

An avatar the build could not measure at all carries no eye height and is worn at its native size, with a warning.

Both constants are compile-time constants rather than preferences, deliberately: a client-side knob here would let two machines fit the same pawn to two different heights. Changing one means rebuilding the pallet.

The authored row is the one you control, and the editor can now move it live: a staged avatar’s view position is written by prefab.viewPosition into the viewPosition of its root dh.skeleton, in the document’s layer — see setting the view position. The delta is held for the open document; where it is written to disk is chosen by the destination chooser, which is not built yet, so a moved eye does not reach a compiled avatar until then.

Adding the fields is additive — no PalletFormat.CurrentVersion bump, so no dh build --clean. An avatar compiled before this carries no eye height and wears native until it is rebuilt; one compiled before eyeForward existed reads as zero, and the engine measures the rig’s eye bones instead — see the first-person body.

A compiled avatar carries shadowRadius and shadowBodyRadius: how much ground its feet and body cover, in meters at the avatar’s own size. The engine’s grounding shadow is drawn at that size times the size the avatar is drawn at (its fit times the player’s scale), so a chibi gets a small disc and a wide-hipped avatar a wider one. There is no preference for the size.

The compiler measures both from the rest pose, from the skeleton alone, by AvatarFootprintMeasure:

  • The hips, the upper and lower legs, the feet, the toes (extended half a foot length past the toes bone, for the toe tip) and the shoulder joints, projected onto the ground, around the point the avatar stands on.
  • shadowBodyRadius is the reach of the hips, upper legs and shoulder joints. shadowRadius is the larger of that and the reach of the lower legs, feet and toe tips. Both are padded by 0.15 of the hips’ height for the thickness of the limbs.
  • No arm bone past the shoulder joint is read, so a T-pose and an A-pose of one rig give one answer. A tail, hair, ears or a skirt never widen it either, since only the humanoid roles count. A mesh scan would see the skirt and the fur, but a tail dragging on the ground would widen the disc with them.
  • An avatar whose rig maps no hips and no leg joint carries neither field, and is drawn with the reference hull’s radius.

Writing either field on the avatar overrides that one: the compiler keeps what was written and measures only the other. A value that is not a positive number is a build error. The constants are compile-time, like the eye height’s; changing one means rebuilding the pallet.

PlatformReads the footprint
Engine, Windows desktop✅
Engine, iOS and Steam Frame✅ the shared client draws it
VRChat, Unity-based game mods❌ no grounding shadow drawn
Garry’s Mod, Minecraft, Project Zomboid, POSTAL 2, Resonite❌ the host game draws its own shadows

An avatar compiled before this carries neither field. The engine then measures the same rule over the loaded skeleton, so it gets the same answer until it is rebuilt. Adding the fields is additive: no PalletFormat.CurrentVersion bump.

A compiled avatar carries fittedSpringColliders: a sphere or capsule per body part, measured by the compiler from the avatar’s own mesh, so every base has colliders before anyone authors one. They are the lowest layer of the avatar’s dh.springCollider components, and the compiled avatar’s resolved object carries them merged with every authored one. A spring chain collides with the ones it lists in its colliders; an authored dh.springCollider with the same id overrides a fitted one field by field.

Each is fitted for these humanoid roles, where the rig maps them, and named by the role in camelCase:

IdAxis the fit measures along
hips, spine, chest, upperChest, neckFrom the bone to the next mapped role up the spine
headFrom the head bone to its crown, the same crown the eye height measures to
leftUpperLeg, rightUpperLegToward the lower leg, through the vertex centroids
leftLowerLeg, rightLowerLegToward the foot, through the vertex centroids
leftFoot, rightFootToward the toes (the avatar’s forward without them), through the vertex centroids

How the fit works:

  • Only the body counts: the skinned mesh with the most vertices, named in the build log. A separate mesh on the calf (a dewclaw, a sock) does not shrink the leg.
  • Vertices belong to the role their heaviest skin weight is on, or to a helper bone under it that is neither a mapped role nor a spring chain. A tail’s own vertices never inflate the hips.
  • A leg vertex needs at least half its weight on the leg and must sit on the leg’s side of the body, which keeps the crotch out of the thigh.
  • A leg’s capsule runs through the centroids of ten bands cut along it, not along the bone. A digitigrade shin on a plantigrade bone leans back to a hock, and the capsule follows it. A leg’s capsule never reaches above its own joint.
  • The ends sit at the 10th and 90th percentile along the axis, and the radius is the median distance from it, inset on purpose so a tail rests against the body rather than hovering over fur. The ends are then pulled in by the radius; a part that is wider than it is long comes out a sphere.
  • Blendshapes are not evaluated. A variant that fattens the body through one overrides radius by hand.

A mapped role with fewer than 16 weighted vertices gets no collider and a build warning. The feet are fitted but, like every fitted collider, collide with nothing until a chain lists them.

Every number here is a compile-time constant in SpringColliderFit, for the reason the eye height’s are: a client-side knob would let two clients push one tail out of two different bodies. dh validate fits and logs every avatar too, reading an FBX only from the conversion cache a build left, so it never runs Blender.

Adding the field is additive: no PalletFormat.CurrentVersion bump. An avatar compiled before it carries no fitted colliders until it is rebuilt.

The same list carries the avatar’s hands: a palm and five finger capsules per hand, the volumes a host reads as hands when a chain lets them touch or grab it. They are not fitted to the mesh but generated from the hand’s bones, the way VRChat’s SDK generates its own, by Core’s SpringHandColliders:

IdHangs onRuns
leftHand, rightHandThe handFrom 30% of the way from the wrist to the mean knuckle to the knuckles, radius 30% of the knuckles’ spread (index to little)
leftThumb … rightLittleThe finger’s first bone (the thumb’s proximal)From that joint to past the last, by 90% of the last segment; radius 12% of that length

A finger’s capsule ends on its last bone: a host reads its start off the first bone and its end off the last, so a curled finger’s capsule curls with it. A finger with fewer than two mapped bones, and a hand with no mapped knuckle, gets none. Like every fitted collider, each one is overridden by its id, so an avatar maker tunes a thumb with { "$type": "dh.springCollider", "id": "leftThumb", "radius": 0.011 }. A host that meets an avatar compiled before hands were generated builds the same capsules from the bones itself.

Drop a thumbnail.png beside the definition — mayu.dh-avatar takes mayu.thumbnail.png — and dh build packs it into the pallet, so every picker that lists this avatar shows the picture instead of a badge. Nothing is rendered for you: unlike a map, whose picture a build-time camera can take, an avatar’s thumbnail is whatever you authored.

The packed entry keeps the map-flavored name it has always had (<asset>.dh-mapthumb, written by MapThumbnailFormat); the format was already asset-agnostic, so it carries an avatar’s picture unchanged.

Every avatar is also an NPC on the platforms that spawn them (Garry’s Mod, and BONELAB as an experiment). How an NPC treats the player is its disposition, one token shared by every platform:

DispositionMeaning
hostileAttacks the player
friendlyDoes not attack the player

Each platform keeps its own base NPC and spawn entry ids, and shows the disposition beside the avatar’s name, as in Mayu (Hostile), or, on BONELAB, as a row of the spawn menu’s one entry per avatar. More dispositions (neutral, fearful) can join later.

A creator turns an avatar’s NPCs off for one platform in that platform’s per-avatar settings file: the avatar’s path under platforms/<platform>/ (the platform’s alias or ID), with .jsonc in place of .dh-avatar, packed like any platform override.

platforms/bonelab/avatars/mayu.jsonc (for avatars/mayu.dh-avatar)
{ "npcs": false }

npcs takes true or false, or the strings "on", "off", "true", "false", "1" or "0"; anything else is ignored with a warning. A platform that lets the player choose may also read the creator’s defaults from the same file: npcDisposition, a disposition token, and npcBase, the platform’s own id of the NPC the body is built on (on BONELAB, a crate barcode). The player’s own choice overrides them, and an unreadable value is ignored with a warning. components holds the platform’s own reactions. Other keys in the file belong to the platform.

dh.avatar extends dh.object. The only difference is the properties dictionary for avatar-level metadata. Everything about components, part overrides, added records and inheritance works identically.

Think of it this way: an object is a reusable building block (an accessory, outfit piece). An avatar is the root-level thing that represents a complete character.