Resonite
Stage 1 built Stage 1 is built: see Resonite for how to use it. What building it settled records where the code proved this design wrong or filled in what it left open.
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, textures and audio. So DigitalHeaven does not export a Resonite file. It pushes an avatar into the world the user is hosting, and the user saves it to their inventory with Resonite’s own tools.
Stage 1 is the push and an update-in-place of an avatar DH pushed before. Stage 2 is a live mirror from the DH editor.
What a live probe settled
Section titled “What a live probe settled”A throwaway probe was run against Resonite 2026.9.18.82 with ResoniteLink 0.13.1 on 2026-10-01.
| Question | Answer |
|---|---|
| Can a ResoniteLink client write to the avatar the host is wearing? | Yes. A slot added under the worn AvatarRoot read back with the right parent. |
Do Slot.tag and dynamic variables survive saving to inventory and respawning? | Yes, on both a world object and a saved and re-equipped avatar. |
| Do entity IDs survive? | No. Every ID was re-minted (DHProbe_… came back as Reso_9481, the avatar root went from Reso_79C to Reso_85A7). |
Does uniqueSessionId identify the world session? | No. It counts client connections (0, 1, 2…). It namespaces client IDs and cannot detect a world reload. |
| Where does a working avatar keep its assets? | In the world’s Root/Assets/Holder - Assets, outside the avatar subtree. |
Does DynamicBoneChain have angle limits? | No. Its fields are inertia, damping, elasticity, stiffness, stretch, gravity, collision and grabbing. |
The type name for a generic component is the component attacher’s syntax: [FrooxEngine]FrooxEngine.DynamicValueVariable<string>.
⇒ An avatar is found again by what it carries, never by an ID DH remembered.
The marker
Section titled “The marker”Every pushed avatar carries a marker on its root slot. The identity is the avatar’s barcode (pallet.id:path/to/asset, see Pallets), the same universal content identifier every other DH host uses.
On the avatar root slot:
tagisdh:<barcode>, for exampledh:dev.pitr:avatars/taidum/avatar.dh-avatar. One string search over the world tree finds every DH avatar without fetching component data.- A
DynamicVariableSpacenamedDigitalHeaven, holdingDynamicValueVariable<string>components:
| Variable | Value | Used for |
|---|---|---|
DigitalHeaven/barcode | the avatar’s barcode | Identity. The tag says the same thing; this copy is what ProtoFlux and other tools read. |
DigitalHeaven/palletVersion | the pallet manifest’s version | Showing which build is worn. |
DigitalHeaven/contentHash | hash of the avatar, its settings and every pallet it reads | Skipping a push that would change nothing (the same naming idea as the Source export). |
DigitalHeaven/exporter | the bridge’s build identity | Telling a stale push from a current one. |
On every slot DH creates below the root, tag is dh:<barcode>#<node>, where <node> is the node’s path in the avatar’s model. That is how an update re-matches bones and meshes after Resonite has re-minted every ID.
A slot without a dh: tag belongs to the user, and DH never touches it.
Where the code lives
Section titled “Where the code lives”Platforms/DigitalHeaven.Platforms.Resonite, following DigitalHeaven.Platforms.Source: net10.0, referencing Core, Content and the official YellowDogMan.ResoniteLink package (MIT, netstandard2.0, depending only on System.Text.Json).
ResoniteExporteris the public face, mirroringSourceExporter:ListAvatars,PushandStateOf.dh export resonite <avatar> [--port N]is a newIExportTargetin the existing export verb, not a separate command. With no--port, it listens for the session announcements ResoniteLink broadcasts on UDP 12512 and picks the session (or asks when there are several).- Settings layer as Source’s do: built-in defaults, then the workspace’s
platforms.resoniteblock, then the avatar’splatforms/resonite/<avatar>.jsonc, then the user’s force. The placeholderplatforms/resonite/avatar-metadata.jsoncin the pallet layout becomes that file. - A small DH-owned client for the reference client’s flaws. That client takes no lock on sends (two concurrent binary imports can interleave frames), never fails in-flight calls when the socket drops, returns
nullinstead of the error on a failedaddSlot, and tears down its receive loop on an unsolicited message. A wrapper can’t fix any of this, since the cast that loseserrorInfois private. So the DH client speaks the package’s own message models over a transport of its own: sends serialized, a timeout on every request,errorInfokept, and every waiting call failed when the connection drops.
Studio and the engine editor call ResoniteExporter too. Nothing in this project references the Compiler or Imaging.
- Connect and read
requestSessionDatafor the Resonite and ResoniteLink versions. Refuse a ResoniteLink major version the bridge was not built against, with the versions named. - Find an existing avatar: walk
Rootwith component data off, and collect slots taggeddh:<barcode>. That includes one the host is wearing. Several matches are listed, and the user picks. - No match: create. Build the slot tree, import the assets, wire the renderers, place the anchors, stamp the marker, then make one
callStaticSyncMethodtoFrooxEngine.AvatarCreator.CreateBipedAvatarwith the viewpoint, hand, foot and hip anchors. Resonite builds IK, eyes, face tracking, protection and the volume meter itself; DH never recreates those components by hand. - Match: update in place. Compare
contentHash; equal means nothing to do. Otherwise rewrite only DH-tagged slots, at slot granularity (a DH slot’s DH components are replaced as a set). Avatar creation is not re-run. - Assets go under one DH-owned slot inside the avatar,
DigitalHeaven Assets, never loose in the world’s Assets. Each provider slot is tagged with its asset’s content hash, so an unchanged mesh or texture is not uploaded again.
Saving to inventory stays the user’s step: ResoniteLink has no inventory call.
An asset import cannot share a batch with the component that references its URL (imports are not data-model operations), so every push is at least two round trips: create the slots and components, then import the assets and patch their URLs in.
Conversion
Section titled “Conversion”| Area | Plan |
|---|---|
| Basis | glTF is right-handed and Y-up, Resonite is left-handed and Y-up. Applied once, at the bridge’s outbound edge: positions, normals, rotations, bind poses, tangent sign and triangle winding. BasisChange and ValveWorldSpace in Content (the latter began as the Source bridge’s SourceSpace) already carry a general basis change (Point, normals and rotation conjugation); both bridges call it. |
| UVs | glTF’s origin is top-left and Resonite’s matches Unity’s bottom-left, so V flips. That is a separate conversion from the basis change. |
| Meshes | importMeshRawData with real bone names, the full bone-weight count and every UV channel’s own dimension. The Unity SDK loses all three; DH has no reason to. Blendshape weights are normalized to 0–1, and normal and tangent deltas are sent only when a shape has them. |
| Skeleton | Bones are slots; each SkinnedMeshRenderer.Bones list is ordered to match its mesh’s bone list. |
| Anchors | Viewpoint, hands, feet and hips are placed from dh.rig’s semantic bones. This is a direct lookup, where the Unity SDK had to infer from Unity’s humanoid rig. |
| Textures | importTexture2DRawData with uncompressed pixels. ResoniteLink has no message for block-compressed data. A compiled pallet holds a texture as PNG, JPEG or a UASTC KTX 2, and Content decodes all three to RGBA8, so Core needed no new block decoder. |
| Materials | DH has no toon shader, so a lit material with a matcap (or every lit material, with litMaterial: toon) maps to XiexeToonMaterial, a two-sided lit material to PBS_DualSidedMetallic, any other lit one to PBS_Metallic, and unlit to UnlitMaterial. The override lives in the avatar’s platforms/resonite/<avatar>.jsonc (a materials block) rather than in a dh.material, since its fields are ResoniteLink member JSON a dh.material has no schema for. A material with no target is reported, never silently dropped. |
| Spring bones | Each DH spring chain maps to a DynamicBoneChain. With no angle limits, motion that DH’s limits shape (ears, whiskers) will differ; that is reported, and the per-avatar override can set Resonite’s own fields. |
| Blendshape weights at rest | Copied onto the renderer, so the face matches the DH preview. |
Stage 2: live mirror
Section titled “Stage 2: live mirror”The DH editor drives the same bridge continuously. Because every slot DH owns is addressed by dh:<barcode>#<node>, an edit maps to an updateSlot or updateComponent without a session-side map that a reload could invalidate. Resonite never sends events (field monitoring and outbound messages are not built), so the mirror is one-way. Which change source in the editor drives it (the console-verb stream, the replication stream or a periodic diff) is decided when stage 2 starts.
What cannot be done
Section titled “What cannot be done”- No round trip. ResoniteLink refuses to read assets back out, on content-protection grounds.
- No Resonite file. The output is a live scene the user saves.
- No inventory save. The user saves the avatar themselves.
- Host only. The user must host the session and turn ResoniteLink on (Dashboard → Session → Settings). It stays on until the world closes.
What building it settled
Section titled “What building it settled”Stage 1 was built against the same Resonite 2026.9.18.82 and ResoniteLink 0.13.1, pushing the stick figure and mltn Taidum as new world objects.
- Resonite copies a parent’s tag onto the slots it adds below it. The avatar creator’s
CenteredRootand proxies carry the root’sdh:<barcode>, and its eye pivot and eye manager carry an eye’s and the head’s#<node>tag. So only the outermost slot with the root tag is the avatar, and where two slots share a node’s tag, the one named as DH named it is DH’s. - The avatar creator moves the model under a
CenteredRootof its own and leaves DH’s reference slots in place. Finding by tag doesn’t care, and a slot new to the model on an update goes beside its siblings underCenteredRoot. - Ids for members can be client-allocated too, not only for slots and components: a slot added with its
positionfield’s id set keeps that id. So a whole create, references included, goes up in one data-model batch. - A
DynamicBoneChainwritten over ResoniteLink isn’t linked to its bones. Its bones list is stored, but each bone’s position and rotation drives stay empty, and nothing moves. The bridge sets those drives itself, to the bone slots’ own transform fields. - A failed operation doesn’t stop its batch. The rest of the batch still applies. So the content hash is written last and alone, and a push that finds a DH avatar with no
AvatarRoot(the creator never ran) completes the create rather than updating. - An update keeps component ids. Each DH component on a DH slot is updated under its existing id, matched by type, instead of replaced. In the test, a toon-material push kept all 391 model and asset slots and every renderer, and only the three changed materials got new provider slots.
- The spring mapping is the Source export’s spring-damper equivalence (now shared in Content): stiffness becomes
Elasticityand dampingDamping. For Taidum’s ears that gives an elasticity near 664 and a damping near 39, against the hand-tuned 62 and 5 of the reference avatar. Either Resonite’s units differ from a spring constant in 1/s², or DH’s ears are much snappier. This is uncalibrated, and stays an open question. - An update must not write what the avatar creator placed. The creator puts each eye bone under an eye pivot of its own, at the pivot’s origin, and the model under a
CenteredRoot. An update that rewrote every DH slot’s rest transform lifted the eyes of a worn avatar 14 cm out of their sockets. Now a slot whose parent is a slot DH doesn’t own is left where Resonite put it. - The avatar creator doesn’t hide the head. Its
AvatarUserViewHeadOverrideandRenderTransformOverrideon the head only move the head with the headset. A working hand-built avatar (Mayu) hides it with its ownHead Hideslot between the neck and the head, scaled to zero inUserViewand listing the head’s skinned renderers. DH builds the same slot. The whiskers and glasses skinned to an accessory’s copy of the head needed no rebinding, since those copies already hang below the main head. - An unnamed model root (a glTF scene root with an empty path) takes the node
:model, and two bones with one path (an armature link’s graft) are told apart by a~2suffix. - Pictures for review come from an in-world camera:
InteractiveCamera.Capture()over ResoniteLink writes the photo into Resonite’s local asset cache, with no screen or window capture involved.
Open questions
Section titled “Open questions”- Does an update keep VRIK and the eye drivers intact on an avatar someone is wearing? DH components keep their ids on an update, and slots are never replaced, so nothing they point at goes away. The world-object test bears that out, but the worn case is untested.
- How should DH’s spring parameters map onto
DynamicBoneChain’s damping, elasticity, stiffness and inertia? See the numbers above. mltn’s working Taidum avatar (damping 5, elasticity 61.96, stiffness 0.76, inertia 0.2 on the ears) is a reference to measure against, not a value to copy. - Should the
#<node>part of a slot tag be the model’s node path, or a stable node ID if the model gains one?