VRChat
Platform ID com.vrchat.vrchat
DigitalHeaven.Mods.VRChat is a Unity Editor pipeline extension that automatically adds VRChat SDK components to avatar prefabs during import. When a pallet containing avatars is imported, each avatar prefab gets a fully configured VRCAvatarDescriptor and PipelineManager, and its spring chains and colliders become PhysBones — ready for upload via the VRChat SDK.
Ships as a pre-compiled DLL alongside DigitalHeaven.Unity.Editor. No source compilation required.
How It Works
Section titled “How It Works”The extension registers itself via [InitializeOnLoad] with EditorPipelineExtensionRegistry and hooks into the OnPrefabCreated event. For each avatar prefab:
- Checks if the definition is a dh.avatar (skips plain objects)
- Finds the
DigitalHeavenRigcomponent on the prefab - Reads rig data (view position, visemes, eye look, eyelids) and configures VRChat components
- Translates every spring chain and the spring colliders it lists into PhysBones
Features
Section titled “Features”| Feature | Status |
|---|---|
| Avatar descriptor (view, visemes, eye look, eyelids) | Yes |
| Spring bones, as PhysBones | Untested |
| Spring colliders, as PhysBone colliders | Untested |
The PhysBone translation builds and its math is tested, but no translated avatar has been uploaded yet.
Generated Components
Section titled “Generated Components”VRCAvatarDescriptor
Section titled “VRCAvatarDescriptor”| Feature | Source | Description |
|---|---|---|
| View Position | viewPosition | First-person camera offset |
| Lip Sync | visemes | Viseme blendshape mapping (15 Oculus visemes) |
| Eye Look | eyeRotationLimits | Eye bone rotation limits for gaze tracking |
| Eyelids | eyelids | Eyelid blendshapes for blink animation |
All features are optional — only the properties present in the rig definition are configured.
Lip sync is set to Viseme Blendshape mode. The VisemeSkinnedMesh is resolved from the mesh field of the avatar’s dh.skeleton.
Eye look rotations are converted from the rig’s Euler angle limits to VRChat’s quaternion format. Both eyes use the same rotation limits (linked = true).
Eyelid blendshape names are resolved to indices on the target mesh. Omitted eyelid states resolve to index -1 (none).
The avatar’s dh.eyes look axes fill the Shy/Confident and Calm/Excited sliders and look.enabled sets enableEyeLook; the blink slot takes the first both-eyes eyelids.blink shape, or none when blink.enabled is false (see What VRChat Receives).
PipelineManager
Section titled “PipelineManager”A PipelineManager component is added if not already present. This is required by the VRChat SDK for avatar uploads.
VRCPhysBone
Section titled “VRCPhysBone”Every dh.springBone chain becomes one VRCPhysBone on the chain’s root bone. VRChat runs PhysBones at a fixed 60 Hz, which is the rate DigitalHeaven’s spring numbers are authored at, so the translation writes a version 1.0, Advanced PhysBone: its step is DigitalHeaven’s own (keep a share of the velocity, pull a share of the way toward the animated pose, add gravity as a displacement).
| DigitalHeaven | PhysBone |
|---|---|
| chain root | rootTransform |
bones the chain did not take (ignore, children attached later) | ignoreTransforms |
pull, per step at 60 Hz, in either mode | pull |
velocity kept per step at 60 Hz (1 − damping, or spring) | spring; stiffness 0 |
gravity | gravity, matched on the chain’s mean segment length |
gravityFalloff | gravityCurve from 1 − gravityFalloff at the root to 1 at the tips; gravityFalloff 0 |
immobile with immobileFrame parent or node | immobile, immobileType All Motion |
immobile with immobileFrame sceneRoot | immobile, immobileType World |
limitType, maxAngleX, maxAngleZ | the same limit type and angles |
limitRotation | limitRotation, mirrored into Unity’s frame |
endpointPosition | endpointPosition, mirrored into Unity’s frame |
maxStretch, maxSquish | maxStretch, maxSquish |
radius | radius, in the root bone’s units |
colliders, in order | colliders, in the same order |
allowCollision none / both | allowCollision False / True |
allowCollision self / others | allowCollision Other, collisionFilter with allowSelf / allowOthers |
allowGrabbing, allowPosing | allowGrabbing + grabFilter, allowPosing + poseFilter, the same way |
grabMovement, snapToHand | grabMovement, snapToHand |
| a bone with several children aims at the first | multiChildType First |
| springs are an overlay on the animation | isAnimated on |
Hand interaction is opt-in, as in every DigitalHeaven host: a chain that authors no allowCollision, allowGrabbing or allowPosing is written with all three toggles False, so nobody’s hands boop it until the pallet says so. VRChat’s False still collides with the listed colliders, as DigitalHeaven’s none does. Every filter is written with contentTypes Everything, since SDK 3.9.1 reads an empty one as “admit nothing”. The DigitalHeaven spring component stays on the prefab with its simulation switched off; the SDK strips it with the other custom scripts on upload.
What carries over exactly: pull, velocity, the stretch band, the chain radius, the collider shapes and each chain’s collider list and order (collision runs before the limit in both), and the push-out response itself: VRChat’s hard-coded 0.25 collision friction belongs to its Simplified integration only, and an Advanced PhysBone, like DigitalHeaven, has none. What is approximate:
- Gravity. VRChat scales gravity by each segment’s length and DigitalHeaven does not, so a chain of even segments matches and an uneven one is close.
- Hinge and polar frames. An
anglelimit is a cone about the rest direction in both; which axis ahingeorpolarlimit reads as X is not yet checked in VRChat. AlimitRotationabout more than one axis may compose in a different order.
Each chain’s numbers and collider list are read from the avatar’s resolved object, its dh.springBone by the part it roots and its dh.springCollider components laid over the fitted colliders, so a base avatar in another pallet speaks for its chains too. A chain the resolved object cannot place by path, such as one in an added record whose bones an armature link renamed onto the wearer’s, translates from the rates on its spring component; the import log names it, and it gets no collider list or radius.
VRCPhysBoneCollider
Section titled “VRCPhysBoneCollider”Every collider id that a chain lists becomes one VRCPhysBoneCollider on a child of the avatar root named dhCollider_<id>, pointing at its bone. A fitted collider no chain lists is not emitted and costs nothing.
dh.springCollider | PhysBone collider |
|---|---|
bone (a humanoid role) or node | rootTransform |
sphere: offset, radius | Sphere: position in the bone’s local space, radius |
capsule: offset, tail, radius | Capsule: position at the midpoint, rotation turning +Y onto the axis, height = the axis length plus both caps |
| outside only | insideBounds off |
| chain bones collide as spheres | bonesAsSpheres on |
Offsets are object-space meters from the bone’s rest position, in the model’s glTF frame; the translation mirrors them into the avatar’s Unity frame and measures them against the bone at the pose the prefab is imported in. A radius is divided by the bone’s own scale, so a collider on an FBX armature scaled by 0.01 keeps its size. A bone with a non-uniform rest scale is refused with a warning, as in the engine. A collider a base avatar emitted that a variant no longer lists is switched off, since a variant cannot delete its base’s child.
Performance rank
Section titled “Performance rank”After translating, the import log states the avatar’s PhysBone numbers and the rank they earn on PC and on mobile, counted the way the SDK’s own performance scanner counts them: every PhysBone, every transform it simulates (its root included), every distinct listed collider, and one collision check per listed collider for each bone with one child, and for a tip only when an endpoint hangs past it. Thresholds are from VRChat’s performance ranking page:
| Excellent | Good | Medium | Poor | |
|---|---|---|---|---|
| PC components / transforms / colliders / checks | 4 / 16 / 4 / 32 | 8 / 64 / 8 / 128 | 16 / 128 / 16 / 256 | 32 / 256 / 32 / 512 |
| Mobile components / transforms / colliders / checks | 0 / 0 / 0 / 0 | 4 / 16 / 4 / 16 | 6 / 32 / 8 / 32 | 8 / 64 / 16 / 64 |
Past mobile Poor, Quest, Android and iOS remove every PhysBone and collider from the avatar even with Show Avatar on, and the import warns in words. With the starting collider lists of the spring collider design (tails on the hips, spine and legs, ears on the head, Mayu’s whiskers on the head, neck and chest):
| Avatar | Components | Transforms | Colliders | Checks | PC | Mobile |
|---|---|---|---|---|---|---|
| Mayu, torso and head | 9 | 31 | 5 | 50 | Medium | stripped (9 components) |
| Mayu, with legs | 9 | 31 | 9 | 82 | Medium | stripped (9 components, 82 checks) |
| Taidum, torso and head | 3 | 13 | 3 | 16 | Excellent | Good |
| Taidum, with legs | 3 | 13 | 7 | 40 | Good | Poor (40 checks) |
Rig Configuration
Section titled “Rig Configuration”All VRChat-specific data lives in the generic dh.rig definition — no VRChat-specific files are needed. The mesh field of the avatar’s dh.skeleton tells the system which SkinnedMeshRenderer holds the viseme and eyelid blendshapes.
{ "bones": { "Hips": "Hips", "Head": "Head", "LeftEye": "LeftEye", "..." : "..." }, "viewPosition": [0, 1.32, 0.07], "eyeRotationLimits": { "up": [12, 0, 0], "down": [-12, 0, 0], "left": [0, -12, 0], "right": [0, 12, 0] }, "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" }, "eyelids": { "lookUp": "Eye Look Up", "lookDown": "Eye Look Down" }}{ "model": "models/body.glb", "components": [ { "$type": "dh.skeleton", "rig": "rigs/humanoid.dh-rig", "mesh": "Body" } ]}Prefab Variants
Section titled “Prefab Variants”For avatars that inherit from a base avatar, the parent’s VRChat components are already present on the prefab variant. The extension uses a get-or-add pattern — it finds the existing VRCAvatarDescriptor and updates it with the child’s rig data rather than adding a duplicate. PhysBones work the same way: the base’s VRCPhysBone on a chain root and its dhCollider_<id> children are updated in place, and the variant’s colliders are resolved over its own fitted set and every dh.springCollider in its inherits chain within the pallet.
Platform Overrides
Section titled “Platform Overrides”The extension registers the vrchat platform, so its override folder, platforms/vrchat/ or platforms/com.vrchat.vrchat/, is read. You can place VRChat-specific material overrides in this directory to use different shaders or properties when importing for VRChat.
Requirements
Section titled “Requirements”- VRChat Avatars SDK (
com.vrchat.avatars) installed in the Unity project - DigitalHeaven.Unity.Editor set up and working
The DigitalHeaven.Mods.VRChat.dll automatically deploys to Assets/Plugins/DigitalHeaven/Editor/ alongside the editor DLL. The extension activates automatically when both the VRChat SDK and the DH editor plugin are present.
Development
Section titled “Development”The project is at Platforms/DigitalHeaven.Mods.VRChat/ in the repository.
| Target framework | netstandard2.1 |
| C# version | 13.0 (PolySharp) |
| Dependencies | DigitalHeaven.Core, DigitalHeaven.Unity, DigitalHeaven.Unity.Editor |
| VRChat references | VRCSDK3A.dll, VRCSDKBase.dll, VRCCore-Editor.dll, VRCCore-Standalone.dll, VRC.Dynamics.dll, VRC.SDK3.Dynamics.PhysBone.dll |
The VRChat SDK DLLs are referenced from External/Libs/VRChat/, tracked in git LFS and copied from a VRChat Avatars SDK 3.10.1 installation (com.vrchat.avatars and com.vrchat.base). They are compile-time references only and never ship. The PhysBone math (PhysBoneMapping, PhysBoneRank) lives in DigitalHeaven.Core and is tested there without the SDK.