Stream avatar poses over WebRTC, not a mocap suit's worth of bandwidth.
Posy is an open, compact protocol for streaming humanoid body, finger and facial pose between many users in real time — ~116 bytes per frame, roughly 70× smaller than VMC, purpose-built for WebRTC data channels.
Channel label avatar-pose · protocol string posy/1
Three questions, sixty seconds
What is this, does it work, and how do I use it. Everything below exists to answer one of those — nothing here is here to look impressive.
Good fit
- You build multi-user VTuber / avatar apps in the browser or over WebRTC
- You need many participants over normal, non-datacenter internet connections
- You want lightweight full-body + finger + face sync, not lossless studio capture
- You just want to hang out with a few friends — one of you can host the room from home
Not a fit (yet)
- You need lossless studio mocap recording — use VMC or BVH instead
- You need a fully serverless peer-to-peer mesh — Posy routes through one peer, though that peer can simply be you
- You want v2 features today — delta encoding, field stripping, props & scenes are deferred
But why not just use VMC?
VMC Protocol is great, but it's fundamentally a local-machine protocol — it assumes one sender and one receiver on the same LAN, at a size that gets expensive fast once you multiply it by dozens of participants. Posy exists for the case VMC never targeted: many people, real internet connections, one avatar's worth of pose data squeezed into a single UDP-sized datagram. The closed-source alternatives that already solve this just don't publish a spec — so here we are.
One peer routes — and that peer can be you
Posy needs exactly one participant to act as the router: it owns the session clock, room membership, playspace and per-subscriber rate control. That's a role, not a product you have to rent. Voice rides on ordinary WebRTC Opus tracks; pose rides on an unreliable, unordered data channel — because a late pose frame is a useless one.
The routing peer MUST NOT modify a forwarded frame's byte content in v1 — that's what makes rate-tiering a pure frame-drop operation, and what makes a lightweight self-hosted router realistic.
Just want to hang out with friends? Host it yourself.
You don't need infrastructure to use Posy. One person in the group runs the routing role on their own machine and everyone else connects to them — the same shape as hosting a game server for your friends. Because the router only forwards bytes it never rewrites, the job is light: a handful of peers is very comfortable on an ordinary home connection.
Whoever has the best upload runs the routing role alongside their own client. Typical home fibre handles a small group at NORMAL without noticing.
Pose routing is bytes-in-bytes-out with no transcoding, so the smallest tier of box goes a long way. Bandwidth is the cost, not CPU.
Any SFU supporting per-producer data-channel forwarding works — mediasoup does natively. Posy never depends on a specific one.
If your upload is the tight spot, you have real levers before giving up: drop everyone to NORMAL or MINIMAL, let non-performers join as receive-only viewers, or hand the routing role to whoever in the group has the best upstream. Play with the numbers in the bandwidth calculator — small rooms are cheaper than most people expect.
1. Sender captures pose
A client resolves its tracker/avatar into VRM 1.0 normalized humanoid bones — identity rotation in T-pose, each quaternion a local parent-relative delta.
2. A routing peer forwards, never rewrites
Frames travel sender → router → receivers. That router can be a hosted SFU, a VPS, or just one of your own machines — it only drops and forwards frames, it never touches their bytes.
3. Receiver applies directly
A receiver applies incoming quaternions straight to its own normalized rig. It never needs to know the sender's skeleton, avatar type or tracker.
Coordinate system (§3, normative)
- Right-handed, Y-up, model faces +Z, metres.
- Every humanoid bone has identity rotation in the T-pose.
- Each quaternion is the bone's local, parent-relative delta from T-pose.
- The hips bone additionally carries the root position block.
Conversion is always the sender's job. VRM 1.0 needs none; VRM 0.x reads the already-resolved normalized rig; MMD precomputes inverse(q_rest) · q_current once at load.
Explicitly out of scope for v1
- Delta / keyframe encoding
- Server-side field stripping
- Physics or props
- Scene state
Listed on purpose so v1 doesn't accidentally block them later — see §11 of the spec.
Build a frame, byte by byte
Everything here runs the real formulas from Posy-1.0.md §5 — toggle flags and bones and watch the frame size, bit layout and bandwidth respond live.
Loading the packet builder… (needs JavaScript)
§5.3 smallest-three, live
one quaternion → 4 bytesRotate a bone below. The demo runs the exact encode/decode algorithm from the spec's Appendix B in your browser — drop the largest component, sign-fix, quantise the other three to 10 bits each, pack into one u32.
One humanoid bone, rotating in its parent's frame — exactly what a single 4-byte quaternion on the wire represents.
–Red component (?) is the largest — it's dropped and reconstructed on decode, never sent over the wire.
––
Per-person cost is trivial. The router's uplink is the budget.
Nobody's individual connection ever struggles with Posy — a pose stream is smaller than the voice call it rides alongside. What scales is the total the routing peer has to push back out, which is exactly why viewers are so cheap to add. Numbers use the §6 methodology and reproduce the spec's worked examples.
–Ingress stays tiny no matter the room size — only senders upload, and each uploads one small stream.
– even at FULL. That's the number that matters if you're the one hosting from home.The normative core, made browsable
The full text lives in spec/Posy-1.0.md — this is a navigable view of the tables you'll actually reference while implementing.
64-bit mask, bit n set ⇒ quaternion for bone n present, appearing in the payload in ascending bit order. v1 constraint: bits 25–54 (fingers) MUST be 0.
Both blocks end with two extra i8 gaze bytes (yaw, pitch), mapped −45°…+45°.
24 bytes total — left hand first (12 B), then right hand (12 B). Per-hand layout:
| Offset | Type | Field |
|---|
Curl (0 = extended … 255 = fully curled) drives all three joints with a fixed distribution — proximal 90°, intermediate 110°, distal 70° (thumb: proximal 60°, distal 80° only). Linear in the curl byte.
Splay (−127…+127 → −15°…+15°) applies to the proximal joint only, positive = abduction toward the thumb side.
Thumb opposition (0…255 → 0°…60°) rotates the thumb metacarpal across the palm.
Senders MUST NOT also set finger bone bits in bone_mask — one byte per axis is enough because hand tracking rarely delivers more precision than 256 levels anyway.
Error handling is server-enforced, not sender-negotiated. Receivers never validate beyond a length check — they only ever see frames the server has already validated.
| Code | Meaning | Server action |
|---|
What each piece is for
This is the intended shape of the repository end-to-end. Green is already there; amber is planned and ordered by the task list below — nothing here is clutter, each node answers one specific question.
The critical path is Phases 1–2
A spec plus passing test vectors is already a usable open standard. Everything past that is comfort — ordered so each phase unblocks the next.
Rationale, kept out of the spec
Keeps the normative document lean while still answering the 'why' questions somewhere.
Try it in two minutes
git clone https://github.com/Ron81/posy.git cd posy/examples/loopback-demo npm install && npm start
1. Read the normative core
Start with §3 (coordinates), §4 (bones) and §5 (packets) of Posy-1.0.md — that's the part every conformant implementation must agree on.
2. Try the loopback demo (planned)
Zero infrastructure: encode → decode → render locally, no server or signup required. Once published it becomes the README's demo GIF.
3. Validate against test vectors (planned)
Decode each .bin fixture, diff against its .json, reject the ones marked expect: reject. That's conformance.
4. Grab the reference encoder/decoder (planned)
reference/js will ship a small, dependency-free TS implementation — read it, fork it, or just use it as a spec cross-check.