Resonite
Platform ID com.yellowdogman.resonite
Live link Push and update in place work against a live session, including on an avatar you have equipped and are wearing.
Resonite has no file format a tool can write. Its official external interface, ResoniteLink, edits a running session: slots, components and fields over a localhost WebSocket, plus imports for meshes and textures. So DigitalHeaven doesn’t export a Resonite file. DigitalHeaven.Platforms.Resonite pushes an avatar into the world you are hosting, as a new world object, and you equip it and save it to your inventory with Resonite’s own tools. Push the same avatar again later and the bridge finds it by its barcode and rewrites it in place.
The design and what a live probe settled are in the Resonite design note. The library references only Core and Content, and reads ResoniteLink’s JSON through a source-generated context, so it is NativeAOT-compatible like the Source bridge.
Before You Push
Section titled “Before You Push”- Host a session in Resonite (your own world or a local one).
- Turn ResoniteLink on: Dashboard → Session → Settings → ResoniteLink. It stays on until the world closes.
- Compile the avatar’s pallet into your workspace as usual.
Command Line
Section titled “Command Line”dh export resonite # list the workspace's avatars and each one's content hashdh export resonite <barcode> # push, finding the session on its owndh export resonite <barcode> --port 30431| Flag | Values | Does |
|---|---|---|
--port | a port number | The session’s ResoniteLink port. Without it, the bridge listens for the announcements every ResoniteLink session broadcasts on UDP 12512 for one announcement interval (about 10 seconds) and uses the one session it hears; with several, it names them and asks for a port |
--slot | a slot id | Which avatar to update when the world holds several pushes of the same barcode |
--new | on | Pushes a new world object even when the world already holds this avatar, so a test copy never touches the one you wear |
--lit | pbs, toon | Overrides litMaterial |
--spring-bones | on, off | Overrides springBones |
--hide-head | on, off | Overrides hideHead |
--rebuild | Rewrites the avatar even when its content hash already matches | |
--verbose, -v | Prints every step’s detail and every note |
What a Push Does
Section titled “What a Push Does”A push reports six steps:
- Find the session. Connect, and refuse a ResoniteLink version the bridge wasn’t built for (it was built for 0.13), naming both.
- Find the avatar in the world. Walk the world tree for a root slot tagged
dh:<barcode>. If it carries the same content hash, the push stops here with nothing to do. - Read the avatar from the workspace’s compiled pallets, as every DH host does.
- Convert it (see Conversion).
- Write into the world. Meshes and textures are imported first. Each provider slot is tagged with
its asset’s hash, so an unchanged mesh or texture is never uploaded twice. Then one data-model batch
creates or updates every slot and component. A new avatar then gets one call to Resonite’s own avatar
creator (
AvatarCreator.CreateBipedAvatar), which sets up IK, eyes, face tracking, protection and the proxies. DH never rebuilds those by hand. - Done. The content hash is written last, so a push that stops halfway never looks finished. The next push completes it, and runs the avatar creator if it never ran.
A new avatar lands 1.5 m in front of the first user in the world, facing them.
The content hash is a 16-character hex hash of the avatar, the applied settings, every pallet the avatar reads, and the bridge’s own build (a hash of its sources and the shared export code in Content).
Finding It Again
Section titled “Finding It Again”Resonite re-mints every id when a world or an item is saved, so the bridge never remembers ids. It marks what it owns instead:
- The avatar root’s
tagisdh:<barcode>, and the root carries aDigitalHeavendynamic variable space withDigitalHeaven/barcode,DigitalHeaven/palletVersion,DigitalHeaven/contentHashandDigitalHeaven/exporter. - Every slot DH creates below it is tagged
dh:<barcode>#<node>. The node is the bone’s model path (Armature/Hips/Spine). DH’s own slots use names that start with a colon::model(an unnamed model root),:headHide,:assets,:asset/<hash>,:anchorsand:anchor/<role>. - A slot without a
dh:tag is yours, and DH never touches it. A DH slot the avatar no longer has is removed on the next push, unless something of yours is below it; then it is left in place and reported.
A slot Resonite adds below a tagged slot takes the parent’s tag. The avatar creator’s CenteredRoot,
proxies, eye pivots and eye manager all do. So only the outermost slot with the root tag counts as the
avatar, and where two slots share a node’s tag, the one with the name DH gave it wins.
An update rewrites each DH slot’s transform and every DH component on it (renderers, dynamic bone chains
and the head hide), matched by type, under the component’s existing id. Whatever Resonite wired to those
components, such as visemes, eyes and VRIK, stays wired. Only the types DH writes on a slot are matched there,
so the avatar creator’s own RenderTransformOverride on the head is never taken for DH’s.
An update writes a slot’s transform only while the slot still hangs where DH put it. The avatar creator
re-parents some of DH’s slots and places them itself: each eye bone goes under an eye pivot of its own, at
the pivot’s origin, and the model goes under a CenteredRoot. A slot whose parent is a slot DH doesn’t own
is left exactly where Resonite put it. An earlier build wrote the eye’s head-relative rest position into the
pivot’s space and lifted both eyes 14 cm out of their sockets. A slot under another slot DH owns is one DH
moved itself, such as the head going in or out of the head hide, so DH moves it to the planned parent and
writes its transform.
Conversion
Section titled “Conversion”| Area | What happens |
|---|---|
| Axes | glTF is right-handed and Y up; Resonite is left-handed and Y up, with +X to the avatar’s right. The avatar is turned to face +Z from its own arms, and its left lands on −X. That change is a mirror, so every triangle’s winding is turned back and every tangent’s handedness flips. It is applied once, at the bridge’s edge, through Content’s shared BasisChange |
| UVs | V flips (glTF’s texture origin is the top left, Resonite’s the bottom left). Textures go up as the pallet stores them, top row first, which is what ResoniteLink takes |
| Meshes | importMeshRawData with the real bone names and bind poses, all four bone weights, every authored UV channel at its own dimension, and every morph target as a one-frame blendshape. Normal deltas are sent only when a shape has them |
| Skeleton | Every model node is a slot with its rest transform. A skinned renderer’s bone list is ordered as its mesh’s |
| Rig | dh.rig’s humanoid bones fill a BipedRig. A thumb named Unity’s way (proximal, intermediate, distal) moves down one joint, since Resonite’s thumb is metacarpal, proximal, distal |
| Head | The head is hidden from its wearer the way a hand-built Resonite avatar does it: a DH-owned Head Hide slot between the head bone and its parent, at rest, whose RenderTransformOverride scales everything below it to zero in the wearer’s own view (UserView) only. Every skinned renderer with a bone under the head is listed on it, so meshes skinned to an accessory’s copy of the head (whiskers, glasses) collapse too, and a rigid mesh below the head shrinks with the hierarchy. Mirrors, cameras and other users still see the head. The avatar creator’s own override on the head only moves the head with the headset; it hides nothing |
| Anchors | The viewpoint, hands, feet and hips are placed from the rig’s own bones and DH’s view position, with each hand’s tooltip, grabber and shelf placed as Resonite’s Unity SDK places them |
| Textures | importTexture2DRawData, uncompressed, at most 2048 px a side. Color textures are sRGB; normal maps and masks are linear |
| Blendshape weights | The avatar’s rest weights are copied onto the renderer, so the face matches DH |
Materials
Section titled “Materials”| DH material | Resonite material |
|---|---|
unlit | UnlitMaterial |
| lit, with a matcap | XiexeToonMaterial, with the matcap in its matcap slot |
lit, litMaterial: toon | XiexeToonMaterial |
| lit, both faces drawn | PBS_DualSidedMetallic |
| lit | PBS_Metallic |
| water, glass | PBS_Metallic blended, with a note |
The tint, base color, normal, occlusion and emission maps carry over. DH’s metallic-roughness texture is repacked into Resonite’s metallic map (metallic in red, smoothness in alpha), with the material’s metallic and roughness folded in. Detail maps, decals and parallax are not pushed, and a matcap mask or a matcap blend other than add has no Xiexe slot; each is reported as a note. A material that can’t be read is pushed flat gray and reported. It is never dropped.
Spring Bones
Section titled “Spring Bones”Each DH spring chain becomes a DynamicBoneChain on its anchor’s parent slot. A branching chain becomes one
component per branch. The bridge links each chain to its bones itself, since a chain written over
ResoniteLink isn’t linked by Resonite.
The motion is DH’s resolved rates as a continuous spring-damper, the same equivalence the Source export’s
jigglebones use: its stiffness becomes Elasticity and its damping Damping, the chain’s immobile
becomes Inertia, its radius BaseBoneRadius, and its gravity Gravity. This mapping has not been
calibrated against Resonite’s solver yet. DynamicBoneChain has no angle limits, so a limited chain (ears,
whiskers) swings freer than in DH. This is reported as one note per armature. The avatar’s springs overrides
can set Resonite’s own fields per chain.
Settings
Section titled “Settings”| Key | Values | Default | Does |
|---|---|---|---|
springBones | true, false | true | Whether spring chains become DynamicBoneChains |
litMaterial | pbs, toon | pbs | Which material a lit DH material becomes; toon is XiexeToonMaterial for all of them |
setupEyes | true, false | true | The avatar creator’s eye setup |
setupProtection | true, false | true | The avatar creator’s avatar protection |
setupVolumeMeter | true, false | false | The avatar creator’s voice volume meter |
setupFaceTracking | true, false | true | The avatar creator’s face tracking |
hideHead | true, false | true | Whether the head is hidden from its wearer’s own view |
maxTextureSize | pixels | 2048 | The largest texture side pushed |
materials | an object of material to override | Per material: component (a Resonite material, by short name or full type) and fields. A material is named by its path, its file name, or its file name without extension | |
springs | an object of chain to override | Per spring chain: fields of DynamicBoneChain. A chain is named by its target path or its last segment |
An override’s fields are set directly. A bare number is a float and a bare boolean a bool. Anything
else is written as ResoniteLink member JSON, as in the example below. A field Resonite can’t read is
skipped and reported.
Settings are layered. Each layer states only what it changes, and a higher layer wins:
- The built-in default.
- The workspace, in
dh-config.jsoncunderplatforms.resonite. - The avatar’s own pallet, at the avatar’s path under
platforms/resonite/(or the IDcom.yellowdogman.resonite), with.jsoncin place of.dh-avatar. Like every platform override, it is packed into the pallet as a plain file. - The user’s force:
dh export resoniteflags.
{ "litMaterial": "toon", "setupVolumeMeter": true, "materials": { "materials/main-fur.dh-mat": { "component": "XiexeToonMaterial", "fields": { "RimIntensity": 0.4, "Outline": { "$type": "enum", "value": "Lit", "enumType": "XiexeToonMaterial+OutlineStyle" } } } }, "springs": { "EarBase L": { "fields": { "Stiffness": 0.6, "Inertia": 0.3 } } }}An unknown key or value is skipped with a warning. It never fails a push.
Feature Support
Section titled “Feature Support”| Feature | Resonite |
|---|---|
| Overlay | No |
| Avatar replacement | Partial: pushed as a world object you equip and save yourself |
| Update in place | Yes |
| Spring bones | Partial: DynamicBoneChain, no limits, uncalibrated |
| Spring colliders | No |
| Eye look and blink | Partial: Resonite’s own eye setup from the avatar creator |
| Live mirror from the DH editor | No: stage 2 |
What Can’t Be Done
Section titled “What Can’t Be Done”- No round trip. ResoniteLink refuses to read assets back out.
- No Resonite file. The output is a live scene you save.
- No inventory save. You save the avatar yourself.
- Host only. You must host the session and turn ResoniteLink on.
Errors
Section titled “Errors”A failed push never throws. It returns a stable code: workspace-missing, avatar-not-found,
avatar-unreadable, not-humanoid, missing-bones (the avatar creator needs the head, both hands and
both upper arms), no-session, several-sessions, connection-failed, version-mismatch,
several-matches, refused (Resonite refused a request; the message carries its reason), canceled or
unexpected.
Development
Section titled “Development”- The library is
Platforms/DigitalHeaven.Platforms.Resonite:ResoniteExporteris the public face (ListAvatars,PushAsync,StateOfAsync,DiscoverSessionsAsync),Conversion/builds a plan with no session involved,Push/writes it,Marker/tags and finds, andLink/holds the client. - The client is DH’s own. It speaks the official
YellowDogMan.ResoniteLinkpackage’s message models, but not itsLinkInterfacetransport. That transport takes no lock on sends, never fails in-flight calls when the socket drops, returns null instead of the error for a refused request, and stops receiving on an unsolicited message. None of that can be fixed from outside it. - Ids are client-allocated, for slots, components and the slot transform fields a dynamic bone chain drives. So the whole avatar goes up in one batch with its references already resolved.
- Tests (
DigitalHeaven.Platforms.Resonite.Tests) need no Resonite. They cover a chiral avatar (a horn on one side only, since a symmetric body can’t show a mirror), the raw mesh layout byte by byte, tags and markers, settings layering, and find-by-tag over a world tree shaped like a live one. The suite runs with reflection-based JSON off. - Pictures of a pushed avatar can be taken without touching the Resonite window. Add an
InteractiveCamerawithPositioningMode: Manualon a slot under a posed parent, and call itsCapture()sync method. Resonite writes the photo (PNG withFormat: PNG) into its local asset cache,AppData/LocalLow/Yellow Dog Man Studios/Resonite/Assets/in your user folder.