This ADR defines the standard for embedding spring bone physics parameters in Decentraland
wearable .gltf model files using a glTF vendor extension. Spring bones are extra
bones — not part of the base avatar armature — whose transforms are driven by a physics
simulation rather than animation clips, enabling hair, earrings, capes, belts, and similar
wearable elements to move dynamically in response to avatar locomotion and gravity. A spring
root bone is identified by a node name containing springbone (case-insensitive)
combined with the presence of the DCL_spring_bone_joint extension in its glTF
node extensions field. All descendant bones of a root automatically form its
spring chain. The parameter set mirrors the VRM VRMC_springBone convention.
Colliders are out of scope for this version.
Currently all wearable elements that extend beyond the base avatar armature — hair, earrings, ponytails, capes, belts, and similar accessories — are completely static. They do not react to the avatar's movement, animations, or environmental forces such as gravity. This is a significant visual quality gap compared to industry-standard avatar formats such as VRM and MMD, which have supported spring/physics bones for years.
Decentraland avatars use a custom armature in gltf format (see
ADR-57). Each wearable carries only the bones it needs and is fully
autonomous. This architecture is compatible with adding optional spring bone definitions
without affecting wearables that do not use them.
The implementation will use the UniVRM VRMSpringBone component in the Unity-based
Explorer. The parameter set defined here is chosen to be directly compatible with that
component's expectations, avoiding any translation layer between stored data and the
simulation runtime.
This standard affects every system that renders avatars: the Explorer (local and remote players), the Builder wearable editor, the Marketplace avatar preview, and the login/backpack screen. A shared spec is required for all of these to interoperate correctly.
Vocabulary:
springbone (case-insensitive, anywhere in the name) and the presence
of the DCL_spring_bone_joint extension.
center: An optional reference node name. When specified, inertia is
calculated relative to that node's space rather than world space, preventing excessive sway
during locomotion.
Multiple approaches were considered for where to store spring bone parameters.
extras
Data lives in the root glTF object's extras field under a
DCL_springBones key, with an explicit springs array listing chains
and joints by node index. This mirrors how VRM's extension is structured.
Not chosen because it does not allow creators to set parameters directly in Blender using bone Custom Properties. This approach would require parameters to be entered entirely through the Builder UI or a future Blender plugin, which is an unnecessary constraint. Node-level configuration is natively exported by Blender without any plugin.
Spring bone parameters live in the wearable item's metadata, stored externally as JSON and
fetched independently from the gltf model at runtime. The model contains only the
bone nodes; the physics configuration is not embedded in the file.
{
"springBones": [
{
"name": "SpringBone_hair_l",
"stiffness": 2.0,
"gravityPower": 0.3,
"gravityDir": [0, -1, 0],
"drag": 0.5
}
]
}
Not chosen because it introduces two sources of truth that must be kept in sync
indefinitely. If a creator renames or restructures bones in their model and re-uploads the
.glb, the metadata references break silently. This desynchronization risk is
structural, it cannot be fully engineered away. Additionally, the Explorer would need to
reconcile two separate resources at avatar load time, adding error surface. Backend schema
changes and migrations would be required. Creators coming from Blender or any DCC tool would
lose any authoring work done outside the Builder, as there is no mechanism to carry node-level
properties into external metadata.
extras
Physics parameters live directly in each spring root bone's glTF node
extras field. Chains are inferred at load time from the node hierarchy.
This approach was native to standard glTF and allowed node-level configuration. However, using
extras for structured data is less formal and makes the intent of the data less
clear to tooling and other readers.
DCL_spring_bone_joint
Physics parameters live in a formal glTF vendor extension (DCL_spring_bone_joint)
within each spring root bone's extensions field. Chains are inferred at load time
from the node hierarchy. The extension is declared in the root
extensionsUsed array.
This approach provides:
extensions object signals to all tooling that this is
structured, spec-driven data
version field allows for schema evolution without
breaking parsing
The trade-off is that chain topology must be reconstructed from the hierarchy at load time
rather than read from an explicit list. This is addressed by the
springbone naming convention, which makes spring bone nodes unambiguous to both
artists and tooling.
All spring bone nodes MUST have a name containing the substring
springbone (case-insensitive). The substring can appear at any position in the
name.
Examples:
| Node name | Valid? | Meaning |
|---|---|---|
SpringBone_hair_left |
✓ | Left hair chain root |
hair_springbone_l |
✓ | Left hair chain root (variant) |
springbone_earring_r |
✓ | Right earring chain root |
ponytail_SPRINGBONE |
✓ | Ponytail chain root (case-insensitive) |
skirt_1 |
✗ | Not a spring bone (missing "springbone") |
SpringBoneCollider |
✓ | Valid name, but not a root unless it also has the extension |
The springbone substring is the convention by which artists and tooling identify
spring bone nodes in the hierarchy. The Explorer identifies spring root bones by the
combination of a node name containing springbone (case-insensitive) and the
presence of the DCL_spring_bone_joint extension. Both signals are required.
A node is a spring root bone if and only if:
springbone (case-insensitive), ANDextensions object contains the DCL_spring_bone_joint key
The spring chain owned by a root consists of the root bone itself and all its glTF node descendants, traversed depth-first.
Each node in a spring chain SHOULD have at most one child that is also part of the chain, forming a linear sequence from root to tip. Branching topologies — where a node has more than one spring bone child — are not enforced against but MAY produce unexpected simulation behavior.
{
"extensionsUsed": ["DCL_spring_bone_joint"],
"nodes": [
{
"name": "SpringBone_hair_left",
"extensions": {
"DCL_spring_bone_joint": {
"version": 1,
"stiffness": 1.0,
"gravityPower": 1.0,
"gravityDir": [0, -1, 0],
"drag": 0.5,
"isRoot": true,
"center": "Avatar_Hips"
}
}
}
]
}
The DCL_spring_bone_joint extension object resides within a node's
extensions field. All parameters are OPTIONAL. When absent, the defaults defined
below apply. The root glTF object MUST declare DCL_spring_bone_joint in its
extensionsUsed array if any node uses this extension.
| Parameter | Type | Range | Default | Description |
|---|---|---|---|---|
version |
integer | ≥ 1 | required (must be 1) |
Schema version for forward compatibility. Current version is 1. Required to
identify the extension schema variant.
|
stiffness |
float | ≥ 0 | 1.0 |
Rigidity, how strongly the bone returns to its rest pose. 0 = fully loose,
follows gravity only. Higher values = firmer, follows the body more closely.
|
gravityPower |
float | ≥ 0 | 1.0 |
Magnitude of the gravity force applied to the bone every frame. 0 =
unaffected by gravity.
|
gravityDir |
vec3 normalized | — | [0, -1, 0] |
Direction of the gravity force in world space. Default simulates natural downward gravity. Can be used to simulate wind or a floating effect. |
drag |
float | 0–1 | 0.5 |
Deceleration / damping. 0 = bone swings freely for a long time.
1 = bone settles almost instantly.
|
isRoot |
boolean | — | true |
Declares whether this node is the root of a spring chain (true) or a chain
member that overrides inherited parameters (false).
|
center |
string (node name) | valid node name | none | OPTIONAL. Name of a reference bone node. When set, inertia is evaluated relative to that node's space, preventing excessive sway during locomotion. The referenced node MUST exist and MUST NOT be part of any spring chain. |
gravityDir SHOULD be a unit vector. If the provided vector is not normalized, the
Explorer MUST normalize it before use.
gravityDir reference values:
| Value | Effect |
|---|---|
[0, -1, 0] |
Downward (natural gravity, default) |
[0, 1, 0] |
Upward (floating / supernatural effect) |
[1, 0, 0] |
Leftward |
[-1, 0, 0] |
Rightward |
[0, 0, 1] |
Forward |
[0, 0, -1] |
Backward |
Axes are in world space. Diagonal values are valid.
The Explorer MUST reconstruct spring chains at load time using the following procedure:
extensionsUsed array
includes DCL_spring_bone_joint. If not, no spring bones are present in the
file.
springbone (case-insensitive) and whose extensions field contains
the key DCL_spring_bone_joint. The Explorer MAY also use the
isRoot field to distinguish root bones (isRoot: true or absent)
from chain members that override inherited parameters (isRoot: false).
version field in the
extension. If version is not 1, log a warning and skip that node
(or handle according to implementation strategy for unknown versions).
center is specified, resolve the node name. If the
name does not correspond to any node in the file, or if it points to a spring bone node, the
Explorer SHOULD log a warning and MUST fall back to world space.
The Explorer MUST apply spring bone simulation to all spring root bones found in any loaded wearable. This applies to the local player's avatar and all other players' avatars in the scene.
For performance reasons, the Explorer SHOULD disable spring bone simulation for avatars beyond a distance threshold from the local player. The threshold value is an implementation detail.
Spring bone simulation MUST also be active in all avatar renderers outside the main world, including but not limited to the login screen and the backpack/wearables preview.
The Builder MUST detect all spring root bones in an uploaded .glb by scanning for
nodes whose name contains springbone (case-insensitive) and whose
extensions field contains the key DCL_spring_bone_joint. For each
detected root, the Builder MUST expose its parameters as editable controls and SHOULD prefill
them with the values found in the extension.
When a creator saves changes, the Builder MUST write the updated parameter values back
directly into the DCL_spring_bone_joint extension object of each affected node in
the .glb. The .glb is always the authoritative source of spring bone
configuration.
The following are explicitly excluded and MAY be addressed in a future revision:
gravityDir can approximate per-wearable wind, but
there is no scene-level wind force.
The schema is designed to allow future addition of colliders logic and other parameters without breaking existing files.
{
"asset": { "version": "2.0" },
"extensionsUsed": ["DCL_spring_bone_joint"],
"nodes": [
{ "name": "Avatar_Head", "children": [1, 2] },
{
"name": "SpringBone_earring_r",
"children": [3],
"extensions": {
"DCL_spring_bone_joint": {
"version": 1,
"stiffness": 0.5,
"gravityPower": 1.0,
"gravityDir": [0, -1, 0],
"drag": 0.6,
"isRoot": true,
"center": "Avatar_Hips"
}
}
},
{
"name": "SpringBone_hair_left",
"children": [4],
"extensions": {
"DCL_spring_bone_joint": {
"version": 1,
"stiffness": 2.0,
"gravityPower": 0.8,
"gravityDir": [0, -1, 0],
"drag": 0.4,
"isRoot": true,
"center": "Avatar_Hips"
}
}
},
{ "name": "springbone_earring_r_tip" },
{ "name": "SpringBone_hair_left_tip" }
]
}
In this example:
extensionsUsed declares DCL_spring_bone_joint availability
springbone_earring_r and SpringBone_hair_left are
spring roots (both contain the extension and have springbone in their names)
springbone_earring_r -> springbone_earring_r_tip hangs from
Ear_R
SpringBone_hair_left -> SpringBone_hair_left_tip hangs from
Neck
Avatar_Hips by name as their center_tip nodes are chain tips and their extension parameters (if any) are
ignored by the simulation
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.