Skip to content

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.

The extension registers itself via [InitializeOnLoad] with EditorPipelineExtensionRegistry and hooks into the OnPrefabCreated event. For each avatar prefab:

  1. Checks if the definition is a dh.avatar (skips plain objects)
  2. Finds the DigitalHeavenRig component on the prefab
  3. Reads rig data (view position, visemes, eye look, eyelids) and configures VRChat components
  4. Translates every spring chain and the spring colliders it lists into PhysBones
FeatureStatus
Avatar descriptor (view, visemes, eye look, eyelids)Yes
Spring bones, as PhysBonesUntested
Spring colliders, as PhysBone collidersUntested

The PhysBone translation builds and its math is tested, but no translated avatar has been uploaded yet.

FeatureSourceDescription
View PositionviewPositionFirst-person camera offset
Lip SyncvisemesViseme blendshape mapping (15 Oculus visemes)
Eye LookeyeRotationLimitsEye bone rotation limits for gaze tracking
EyelidseyelidsEyelid 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).

A PipelineManager component is added if not already present. This is required by the VRChat SDK for avatar uploads.

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).

DigitalHeavenPhysBone
chain rootrootTransform
bones the chain did not take (ignore, children attached later)ignoreTransforms
pull, per step at 60 Hz, in either modepull
velocity kept per step at 60 Hz (1 − damping, or spring)spring; stiffness 0
gravitygravity, matched on the chain’s mean segment length
gravityFalloffgravityCurve from 1 − gravityFalloff at the root to 1 at the tips; gravityFalloff 0
immobile with immobileFrame parent or nodeimmobile, immobileType All Motion
immobile with immobileFrame sceneRootimmobile, immobileType World
limitType, maxAngleX, maxAngleZthe same limit type and angles
limitRotationlimitRotation, mirrored into Unity’s frame
endpointPositionendpointPosition, mirrored into Unity’s frame
maxStretch, maxSquishmaxStretch, maxSquish
radiusradius, in the root bone’s units
colliders, in ordercolliders, in the same order
allowCollision none / bothallowCollision False / True
allowCollision self / othersallowCollision Other, collisionFilter with allowSelf / allowOthers
allowGrabbing, allowPosingallowGrabbing + grabFilter, allowPosing + poseFilter, the same way
grabMovement, snapToHandgrabMovement, snapToHand
a bone with several children aims at the firstmultiChildType First
springs are an overlay on the animationisAnimated 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 angle limit is a cone about the rest direction in both; which axis a hinge or polar limit reads as X is not yet checked in VRChat. A limitRotation about 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.

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.springColliderPhysBone collider
bone (a humanoid role) or noderootTransform
sphere: offset, radiusSphere: position in the bone’s local space, radius
capsule: offset, tail, radiusCapsule: position at the midpoint, rotation turning +Y onto the axis, height = the axis length plus both caps
outside onlyinsideBounds off
chain bones collide as spheresbonesAsSpheres 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.

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:

ExcellentGoodMediumPoor
PC components / transforms / colliders / checks4 / 16 / 4 / 328 / 64 / 8 / 12816 / 128 / 16 / 25632 / 256 / 32 / 512
Mobile components / transforms / colliders / checks0 / 0 / 0 / 04 / 16 / 4 / 166 / 32 / 8 / 328 / 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):

AvatarComponentsTransformsCollidersChecksPCMobile
Mayu, torso and head931550Mediumstripped (9 components)
Mayu, with legs931982Mediumstripped (9 components, 82 checks)
Taidum, torso and head313316ExcellentGood
Taidum, with legs313740GoodPoor (40 checks)

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.

rigs/humanoid.dh-rig
{
"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"
}
}
avatar.dh-avatar
{
"model": "models/body.glb",
"components": [
{ "$type": "dh.skeleton", "rig": "rigs/humanoid.dh-rig", "mesh": "Body" }
]
}

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.

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.

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.

The project is at Platforms/DigitalHeaven.Mods.VRChat/ in the repository.

Target frameworknetstandard2.1
C# version13.0 (PolySharp)
DependenciesDigitalHeaven.Core, DigitalHeaven.Unity, DigitalHeaven.Unity.Editor
VRChat referencesVRCSDK3A.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.