Avatar poses in 115 bytes a frame
Posy is an open protocol for streaming humanoid body, finger and face poses between many participants in real time, over a WebRTC data channel or a WebSocket.
- header + bone_mask16
- 13 bone rotations52
- position + hips height8
- fingers, both hands24
- 13 expressions + gaze15
Read the spec
Open the demo
Build a frame
Apache-2.0 · channel label avatar-pose · protocol posy/1
Is it for you?
A fit
- You build multi-user avatar applications, in the browser or native
- Many participants on ordinary internet connections
- Body, fingers and face in a small frame, for VRM 1.0, VRM 0.x and PMX avatars
- A small group hosted from one member's home connection
Not a fit
- Lossless motion-capture recording: use VMC or BVH
- A peer-to-peer mesh without a routing peer
- Delta encoding, field stripping, props and scene state: deferred to v2 (spec §11)
How it works
Every pose frame goes from the sender to the routing peer and from there to the receivers. The routing peer owns the session clock, room membership, authorization and rate control. It is a role: a participant's machine, a VPS or a hosted SFU can take it.
- The sender declares, then sends. It declares the data types its tracker delivers and its avatar can show, and sends only those. A lost frame is never resent: every frame stands alone.
- The routing peer forwards. It checks a frame's length and that it stays within what the sender was granted, without decoding the rotations. It drops whole frames to keep a rate tier and never changes a frame's bytes.
- The receiver drives the sender's avatar. Each receiver applies the frame to its copy of the sender's avatar, interpolates between frames and conceals short gaps. Nothing is retargeted onto another rig.
- Posy avatar space (§3)
- Right-handed, Y-up, the model faces +Z, metres. Every humanoid bone has identity rotation in the T-pose, and each quaternion is a bone's rotation relative to its parent. It coincides with the normalized rig of VRM 1.0 runtimes; VRM 0.x and PMX rigs are adapted by the avatar driver on each end (§3.2). The wire is the same for all three.
- Beyond the humanoid body (§2.6)
- Tails, animal ears, further limbs and an avatar's own expressions are declared extras. The sender names them once as its avatar file does. A frame then carries 4 bytes per declared bone and 1 byte per declared value, with no names on the wire.
Build a frame
The formulas of spec §5. Toggle bones and blocks and read the frame's size, mask and declaration.
Smallest-three: one quaternion in 4 bytes (§5.3)
Drop the largest component, make it non-negative, quantise the other three to 10 bits each, pack them into one u32.
- normalised quaternion (x, y, z, w)
–The marked component (?) is the largest. It is dropped and rebuilt on decode.
- packed u32
–- bit layout: index (2) a (10) b (10) c (10)
–- round-trip error
–Worst case ≈ 0.25°. The identity gives ≈ 0.14°: 0 / 0 / 0 lies on a rounding tie.
Bandwidth
A sender uploads one stream, and its size does not grow with the room. What grows is what the routing peer sends back out, so viewers are cheap and a large room needs a good uplink. The figures follow spec §6 for the typical upper-body frame.
–
| Room | – |
| Viewer downstream | – |
| Sender downstreameveryone else's pose, without their own | – |
| Routing peer ingress | – |
| One stream at FULL / NORMAL / MINIMAL | – / – / – kbit/s |
Tables of the spec
What you look up while implementing. The full and normative text is spec/Posy.md.
A 64-bit mask. Bit n set means the quaternion of bone n is present; quaternions follow in ascending bit order. The colours are the declared types a bit belongs to. A relay refuses a frame that sets a bit of a type the sender was not granted, or one of bits 55–63.
One byte per slot, 0–255 for 0.0–1.0. Both blocks end with two i8 gaze bytes, yaw and pitch, ±127 for ±45°: the line of sight, measured from the head. The Perfect-Sync order is the 52 ARKit names sorted by code unit. A tongue, and any expression beyond these, is a declared extra value.
24 bytes: left hand (12 B), then right hand (12 B). Per hand:
| offset | type | field |
|---|
Curl (0 = straight … 255 = fully curled) bends all three joints of a finger: proximal 90°, intermediate 110°, distal 70°, about Z, into the palm.
Splay (−127…+127 for −30°…+30°) turns the proximal joint about Y; positive is toward the thumb side.
The thumb is driven by its three bytes together: splay ±90° in the palm's plane, opposition 0–60° toward the palm with an equal roll, curl 60° and 80° on the two outer joints. A receiver measures once how its avatar's thumb rests and turns the result accordingly. The definition is in spec §5.5; the pose vectors f01–f09 pin it.
A frame carries this block or per-joint finger bones (bits 25–54), never both. With neither, a receiver shows the rest hand.
Errors are enforced by the server. A receiver checks a frame's length and nothing else: it only sees frames the server has let through.
| code | meaning | server action |
|---|
Implementing Posy
- Read the core. §3 (avatar space), §4 (bones) and §5 (packets) of
spec/Posy.mdare what every implementation has to agree on. §2 is the session layer. - Use or read the reference codec.
posy-protocolon npm (reference/js): TypeScript, no runtime dependencies. - Pass the test vectors.
testvectors/frameschecks bytes: decode each.bin, compare with its.json, refuse the ones markedreject.testvectors/poseschecks meaning: axes and signs of legs and fingers. - See it move. The loopback demo sends an avatar through a lossy channel, in the browser and without a server.
npm install posy-protocol