Portals Wearable Standard
Version 1. This page is the contract between a wearable file and every place a Portals avatar appears:
/avatar;- the Shop;
- the Play lobby;
- hosted games;
- games built with the Portals engine.
A wearable that follows it behaves the same way on all of them, with no code in any game. For the rig itself — bone names, the template, and skinned versus rigid attachment — see Wearable standards.
| Part | Status |
|---|---|
| File rules, attachment forms, budgets and upload validation | Supported now |
| Separate static and dynamic files | In development. Until then, the validated upload is the one file every runtime loads |
| Extra bones on skinned wearables (capes, tails) | In development. Uploads are accepted; until the static file ships, runtimes before the release that enables them fold these bones onto the hips |
| Named animation clips with triggers, material animation | In development |
| Secondary motion (spring bones: capes, hair, tails) | In development |
| Particle emitters and trails | In development |
Everything marked in development is specified here so files can be authored against it. Each part is enabled by the Portals avatar SDK release named in the changelog.
Principles
- A wearable is data, never code. The GLB describes what should happen; the Portals avatar SDK does it. Wearables may not contain:
- scripts;
- custom shaders;
- external file references;
- lights or cameras.
- The file describes itself. Everything a wearable does is declared inside its GLB. Any runtime handed the file gets the full behaviour, with no side-channel flags.
- Standards first. Motion uses core glTF animation. Secondary motion uses
VRMC_springBone. Material animation uses a subset ofKHR_animation_pointer. One Portals extension,PORTALS_wearable, covers only what no standard does: clip triggers, particle emitters and trails. - It must look right standing still. With every dynamic feature off, a wearable must look correct in its rest pose. That is how it appears:
- in thumbnails;
- to players who ask for reduced motion;
- to distant or over-budget avatars;
- in games pinned to an older SDK.
- The platform owns the budget. Limits are checked at upload and enforced by the SDK at runtime. A wearable renders on other players' devices, so no file is trusted to stay within budget.
File rules
Format. One self-contained binary glTF 2.0 file (
.glb). Every buffer and image is embedded, and there are no external URIs. The file must pass the Khronos glTF Validator without errors.Textures. PNG or JPEG.
Rig. The full Portals rig, attached as a skinned mesh or as rigid parts under rig bones (Wearable standards).
Extensions.
extensionsRequiredmay list onlyKHR_mesh_quantization, a geometry encoding every Portals runtime decodes. A file that requires any other extension is rejected, because an older runtime could not show it at all.Extensions a wearable may use:
Extension Use KHR_texture_transformUV offset, scale and rotation KHR_mesh_quantizationCompact vertex data KHR_materials_emissive_strengthGlow brighter than 1.0 KHR_materials_unlitFlat, unlit materials KHR_materials_specular,KHR_materials_ior,KHR_materials_clearcoat,KHR_materials_transmissionRicher surfaces KHR_animation_pointerMaterial animation (allowlisted properties, below) VRMC_springBoneSecondary motion PORTALS_wearableClip triggers, emitters, trails Any other extension is removed at upload and reported to the creator. Lights (
KHR_lights_punctual) and cameras are removed too.Animation moves only the wearable. A channel that animates a rig bone, or a node above one, would pose the avatar, so it is removed at upload and reported. Such channels are usually character motion exported with the garment. A clip left with nothing to move is rejected.
What Portals makes from an upload
Every upload is validated on the server. Portals then derives the files avatars actually load:
| File | Contents | Used by |
|---|---|---|
| Static | The wearable in rest pose on the canonical rig, with no clips, springs or effects | thumbnails, reduced motion, distant avatars, games on older SDKs |
| Dynamic | The validated file with its declared behaviour | every current Portals avatar surface and game |
A wearable with nothing dynamic has only a static file. Both files are immutable, and each is named by its content. The creator's original upload is kept privately so it can be processed again when the standard adds features.
PORTALS_wearable
A root-level extension object:
{
"extensionsUsed": ["PORTALS_wearable"],
"extensions": {
"PORTALS_wearable": {
"standard": 1,
"clips": [
{ "animation": 0, "trigger": "always" },
{ "animation": 1, "trigger": "jump", "loop": "once", "fade": 0.1, "priority": 1 }
],
"emitters": [],
"trails": [],
"covers": ["leftHand", "rightHand"]
}
}
}
standard(required) is the version of this document the file was written for. A runtime that knows an older version plays the parts it understands and ignores the rest.- A file without
PORTALS_wearableis a static wearable: its animations never play. Items from before this standard that Portals marks as animated keep looping their first animation.
Clips
Each entry binds a glTF animation to a trigger:
| Field | Type | Default | Meaning |
|---|---|---|---|
animation |
integer | required | Index into the file's animations |
trigger |
trigger | required | When it plays (see Triggers) |
loop |
"repeat" | "once" |
"repeat" |
Loop, or play once and hold the last frame |
fade |
number, 0–2 s | 0.2 |
Cross-fade in and out |
priority |
integer, 0–10 | 0 |
When several clips match, the highest priority plays |
One clip plays at a time:
- the highest
priorityamong the clips whose trigger matches plays; - at equal priority the more specific trigger wins:
equip, thenemote, then a locomotion state, thenalways; - the clip playing when the item goes on starts at full weight, and after that each change cross-fades by the incoming clip's
fade; - a clip that starts again starts from its first frame;
- an
equipclip plays once, whatever itsloopsays.
A clip may animate only nodes inside the wearable, including its extra bones, plus the allowlisted material properties below. It can never pose the avatar.
Triggers
A closed list. Portals derives the avatar's state, so the same wearable behaves the same in every game. The Portals avatar SDK reads it from the body clip it plays; a game that animates the body itself reports it with avatar.setMotionState():
always: while worn;equip: once, when the item goes on;- the locomotion states:
idle,walk,run,sprint,jump,fall,land,crouch,swim,climbandsit; emote: while any emote plays.
Games cannot define their own triggers. That keeps a wearable's behaviour independent of any one game's code.
Emitters
Particles attached to a node of the wearable:
| Field | Type | Meaning |
|---|---|---|
node |
integer | Node the emitter follows |
shape |
object | { "type": "point" }, { "type": "sphere", "radius": r }, { "type": "box", "size": [x, y, z] } or { "type": "cone", "radius": r, "angle": degrees } |
rate |
number | Particles per second |
bursts |
array | Optional { "time": seconds, "count": n } bursts on each trigger start |
lifetime |
[min, max] seconds |
Particle life |
speed |
[min, max] m/s |
Initial speed along the shape's emission direction |
size |
{ "start", "end" } metres |
Size over life |
color |
{ "start": [r,g,b,a], "end": [r,g,b,a] } |
Linear colour and alpha over life |
gravity |
number | Multiplier on world gravity (0 floats, negative rises) |
texture |
integer | Optional index into the file's textures |
flipbook |
{ "columns", "rows", "fps" } |
Optional sprite-sheet animation of texture |
blending |
"alpha" | "additive" |
How particles combine |
space |
"local" | "world" |
Particles move with the wearable, or stay where they were emitted |
trigger |
trigger | When it emits; default always |
maxParticles |
integer | Cap on live particles for this emitter |
Shapes emit along a direction of their own: a point or sphere in every direction outward, a cone within angle of the node's +Y axis, and a box along the node's +Y. An emitter bound to equip runs for the first second the item is worn.
Trails
Ribbons that follow a node, such as a sword swing or a wingtip:
| Field | Type | Meaning |
|---|---|---|
node |
integer | Node the trail follows |
width |
number, metres | Ribbon width |
lifetime |
number, seconds | How long a point of the trail lasts |
color |
{ "start", "end" } |
Colour along the trail's age |
blending |
"alpha" | "additive" |
How it combines |
trigger |
trigger | When it draws; default always |
A trail's ribbon spans its width along its node's local Y axis, so orient the node to set which way the ribbon faces.
Covers
covers lists the regions of the wearer's body the wearable hides (SDK 0.53.0). A covered region is left out of the wearer's body, face, hair and base outfit, down to the triangle. Nothing of it can show through the wearable, however the body moves inside it. A full-body costume covers every region, and a pair of gloves covers the hands.
| Region | Starts at | Region | Starts at |
|---|---|---|---|
head |
Head (with the face bones) |
neck |
Neck |
torso |
Spine, Spine1, Spine2, both Shoulders |
hips |
Hips |
leftUpperArm |
Arm.L |
rightUpperArm |
Arm.R |
leftLowerArm |
ForeArm.L |
rightLowerArm |
ForeArm.R |
leftHand |
Hand.L (with the fingers) |
rightHand |
Hand.R |
leftUpperLeg |
UpLeg.L |
rightUpperLeg |
UpLeg.R |
leftLowerLeg |
Leg.L |
rightLowerLeg |
Leg.R |
leftFoot |
Foot.L (with the toes) |
rightFoot |
Foot.R |
Left is the wearer's left.
- What counts as a region: a bone belongs to the region of the nearest of these bones at or above it. A triangle of the body belongs to the region its three vertices are most weighted to, so a region's edge falls where the body's weights change, near the joint. Cover a little past the joint so the edge is under the wearable.
- Several items: covers from every worn item add up. Taking an item off shows again only what no other item still covers.
- What hides: covers hide the wearer's body and the clothing worn under the item, the skinned items that cover nothing themselves (SDK 0.54.0). Rigid accessories such as hats, glasses and held items sit outside and stay. So do other items that cover, so gloves never cut holes in a costume. Nothing of another player is touched.
- Slots still apply: they decide what else can be worn with the item, and hide the base outfit layers the item replaces.
- Each region once: a region may be listed only once, and the SDK reports covers in the order above.
Cloth
cloth lists pieces of cloth the runtime simulates against the wearer's body (SDK 0.54.0): a sheet, a robe or a dress that hangs, swings and is pushed by the body it covers, instead of following bones. Each piece is a coarse proxy mesh the runtime simulates, and the meshes it drives, which follow the proxy while it runs:
"cloth": [
{ "mesh": 12, "drives": [11], "pinned": [0, 1, 2, 3], "bending": 0.3, "inertia": 0.12 }
]
| Field | Type | Default | Meaning |
|---|---|---|---|
mesh |
node index | required | The proxy: a skinned mesh of one indexed triangle primitive, never drawn |
drives |
node indices, 1–8 | required | Skinned meshes that follow the proxy while it simulates |
pinned |
proxy vertex indices | required | Vertices held to the body by their skin; the rest of the cloth hangs from them |
thickness |
number, 0–0.1 m | 0.02 |
How far the cloth keeps from the body |
bending |
number, 0–1 | 0.1 |
How much it resists bending further than it rests: 0 is silk, 1 is felt |
damping |
number, 0–20 per second | 2 |
How fast its motion relative to the wearer dies away: moving along with the wearer costs it nothing, and only the air (drag) holds it back |
friction |
number, 0–1 | 0.5 |
How much it grips the body it rests on |
gravity |
number, 0–2 | 1 |
How hard gravity pulls it, as a multiple of Earth's |
inertia |
number, 0–1 | 0.3 |
How much of the wearer's travel it feels: 0 moves with the body, 1 is left behind by it. The travel is the hips' place and the wearer's heading, not the pelvis's sway |
drag |
number, 0–1 | 0.3 |
How much the air holds it back face-on, so it billows and trails as it moves |
colliders |
body collider groups | all of them | The groups from portals:head to portals:lowerLegs it rests on and is pushed by |
maxDistance |
number, 0.01–2 m | none | How far the cloth may drift from where the body carries its rest shape, at its farthest from its pins; nearer them, in proportion |
- The proxy is the cloth's shape at rest, as it hangs on the Portals avatar. Every edge becomes a thread that resists stretching and gives under compression, so the cloth folds. Each vertex resists bending further than it rests, between the neighbours either side of it, so the cloth keeps the folds it was draped in and folds broadly rather than crumpling. Each vertex is tethered to the nearest pin by the length of cloth between them, so a long cloth never stretches under its own weight. Keep it coarse: a few hundred vertices around the body is plenty.
- The drives keep their own look. Each of their vertices is bound, when the item goes on, to the point of the proxy it lies over along its normal, and follows that point as the cloth moves. A vertex that faces across the proxy nearest it, such as one round a rolled edge, is bound to that nearest point. Make the drives the proxy's own surface, refined: detail that sits off the proxy, such as folds the proxy doesn't have, can't move with the cloth, and tears where the cloth bends. Shade the cloth with its material and let the light show its folds: shading painted from its rest folds stays put when they move.
- Thick cloth: a drive may be a closed shell with a face either side of the proxy, along its normals, joined round its hems and holes. Both faces hang from the same points of the cloth and draw one-sided, so the cloth shows its thickness wherever its edges turn. Keep the faces closer to the proxy than folds of it come to each other, under half the 2.2 cm the runtime keeps folds apart.
- Pins follow the skin, and hold the cloth to the body: a hood's rim, a waistband, the shoulders of a cape.
- The body is the wearer's canonical colliders, fitted to their body type, and the floor under the wearer's feet. Limbs are collided one-sided: cloth a hand or leg swings into is pushed back out, away from the body, past it, so a fast limb never punches through and leaves the cloth closed behind it. List in
collidersonly what should visibly move the cloth. A limb the cloth hides and covers, like the arms under a sheet, pushes it about where no one can see why, so leave it out, and drape the proxy as though it weren't there. - Max distance: with
maxDistance, each part of the cloth stays within a distance of where the body would carry its rest shape. That is none at the pins, growing with the length of cloth between, tomaxDistanceat the cloth's farthest from them, so the cloth bends away from its pins smoothly. The body can always push the cloth off. Where the body carries the rest shape into itself, as in a lean or a tuck, it is held outside instead; where it carries it below the floor, as a squat lowers it, it pools out along the floor the way a long skirt does, so a landing piles the cloth round the feet without folding it through itself. However fast the wearer sprints or jumps, the cloth swings and trails without folding far enough to tangle or to fly off the body. Near the pins the rest shape is carried by the bone the pins hang from. Toward the hem it is carried by the hips' place and heading alone, so a spine that leans or a pelvis that tilts never tilts the hanging cloth with it. - The floor stays where the wearer last stood while they are in the air, so a jump never lifts it into the cloth.
- Folds keep apart: parts of the cloth that aren't neighbours are kept from passing through each other, so a fold lies on another instead of poking through it and showing the cloth's inside.
- When it doesn't run: where the cloth can't run, the drives show their own skinning, springs and all, just as a runtime that predates cloth shows them. That happens past the crowd's cloth budget, below full dynamics quality, or with reduced motion. Skin the drives so that fallback looks right on its own.
- Starting over: cloth starts from its rest shape, carried to where the body is and eased into its pose, when the item goes on, when it starts running again, and after a teleport.
- Proxies can't be shared: a proxy or a driven mesh belongs to one piece of cloth.
Secondary motion
Capes, skirts, hair, tails, ears and chains swing using VRMC_springBone 1.0, authored with the VRM add-on for Blender or any tool that writes it.
- Spring joints are the wearable's own extra bones (below). They can't be rig bones. The last joint of a spring marks where the chain ends and is not rotated.
- Body colliders belong to Portals. Collider groups named
portals:head,portals:neck,portals:torso,portals:hips,portals:upperArms,portals:lowerArms,portals:hands,portals:upperLegsandportals:lowerLegsmust be placed on rig bones. At runtime the SDK replaces their shapes with the canonical collider set for the wearer's body type, fitted to the Portals avatar body and its default outfit, so a cape collides with every body the same way. The rig template ships with these groups. - Wearable colliders (for example a shield the cape should drape over) may be placed on the wearable's own nodes.
- Centre space. A spring's
centernames the node it simulates in, and moving that node doesn't make the chain lag. Leave it unset for a cape or a tail that should stream behind a runner. Set it tomixamorig:Hipsfor a sheet, dress or skirt that should move with the body and swing only from how the body and limbs move it. It may be a rig bone or one of the wearable's own nodes (SDK 0.52.0). - A collider around a joint's own origin is skipped for that joint, since no tail could leave it.
- Springs do not collide with the game world. Games can set a wind that every spring feels, in the units of a spring's
gravityPower: a wind of 1 pushes as hard as a gravity power of 1 pulls. - An avatar placed somewhere new carries its springs with it; nothing stretches across the map.
Material animation
KHR_animation_pointer channels may target only these properties of the wearable's own materials:
/materials/{i}/pbrMetallicRoughness/baseColorFactor/materials/{i}/emissiveFactor/materials/{i}/extensions/KHR_materials_emissive_strength/emissiveStrength/materials/{i}/{texture}/extensions/KHR_texture_transform/offset, where{texture}is one of the following:pbrMetallicRoughness/baseColorTexture;emissiveTexture;normalTexture.
Any other pointer is removed at upload and reported.
Extra bones
A skinned wearable may carry bones beyond the rig: cape chains, tail segments, ear bones.
- Every extra bone descends from a rig bone. The one exception is
neutral_bone, which Blender's exporter adds for vertices with no bone weights. Runtimes bind it to the rig root. - An extra bone may not reuse a rig bone's name. A node with a rig bone's name is bound to that bone of the avatar, so it must sit under the same parent as in the rig. A file may carry a second full copy of the rig.
- Extra bones belong to the one wearable instance, so two wearables can each have a bone named
Cape_01.
In the static file, the weights on extra bones are folded into their nearest rig ancestor, so the static file still looks right in rest pose.
Budgets
Every wearable must fit the limits below, and the SDK enforces the same limits on every avatar at runtime. Limits for the in-development parts are published with the SDK release that enables them.
| Limit | Cosmetic | Full avatar |
|---|---|---|
| File size | 50 MB | 50 MB |
| Triangles | 120,000 | 150,000 |
| Largest texture side | 4096 px | 4096 px |
| Decoded texture memory | 128 MB | 128 MB |
| Materials | 16 | 48 |
| Mesh primitives (draw calls) | 64 | 96 |
| Animation clips | 8 | 8 |
| Emitters / live particles per emitter | 4 / 256 | 4 / 256 |
| Trails | 2 | 2 |
| Spring joints | 64 | 64 |
| Spring colliders of the wearable's own | 32 | 32 |
| Cloth pieces / particles (proxy vertices) | 2 / 2,048 | 2 / 2,048 |
How games run it
A game needs nothing beyond the avatars.update(dt) call it already makes. The SDK:
- plays clips, springs and effects from that call, using the game's
dt, so pausing the game pauses them; - runs the springs and effects after anything that poses bones (
onAfterUpdate), so they follow ragdolls and aim poses; - runs the local player at full quality, and lowers quality for distant and crowded avatars, one feature at a time;
- runs cloth for the avatars the game controls and those nearest them, within a particle budget, and shows the rest of the cloth on its springs;
- shows the static file to a player who asks for reduced motion;
- resets springs after a teleport, so a cape never stretches across the map;
- never lets one wearable exceed its budget.
Edit styles
A style is another look for a wearable you already sell. Everyone who owns the item, now and later, can wear any of its published styles for free, and the look they bought, the Original, never changes.
- Adding one: on Sell in the Shop, open a published wearable's styles, upload a model for each body the item is made for, and let its image capture. Submit it for review; owners can pick it once it is approved.
- Same fit: a style changes each body's model, images and viewer settings. The item's slots, bodies and what it hides stay the item's own.
- Same rules: each style's files follow this standard and its budgets, and are validated and processed like any upload. A style moves only as its own file describes; the legacy
animatedflag never applies to it. - Where owners choose it: on
/avatar, the brush button on the item's tile. The Shop shows an item's styles under its preview so buyers can try them on. - Taking one down: withdraw a live style and everyone wearing it goes back to the Original. Drafts can be deleted. An item offers at most 12 styles; withdrawn ones don't count.
- In games: nothing to do. A player's saved look carries the files of the style they wear, so every game shows it.
Validation errors
The upload rejects a file for any of these reasons:
- it is not a self-contained GLB, or the glTF Validator reports an error;
- it requires an extension;
- a texture is not PNG or JPEG;
- it exceeds a budget;
- a
PORTALS_wearablefield is out of range or points at something that does not exist, orcoversnames a region that is not in the list or names one twice; - a piece of
clothhas a proxy that is not one skinned triangle primitive, drives a mesh that isn't skinned, pins no vertex or one the proxy doesn't have, shares a mesh with another piece, or takes the wearable past its cloth budget; - a clip plays an animation with nothing a wearable may move;
- a bone breaks the rules above.
Each error names the field or limit it failed, and the upload lists every error at once. The rest-pose check joins this list when Portals starts deriving separate static files.
