Progressive 3D over plain HTTP. The first 50 KB is already a complete, skinned avatar.
Install · Quick start · CLI · API · How it works · Live demo
A GLB is atomic: nothing renders until the last byte lands. On a slow connection that is several seconds of blank frame on every embed, and it is the first thing a visitor sees.
@three-ws/avatar-stream repacks a GLB into an A3S stream: nested layers
ordered so that any byte-prefix of the file is a complete, skinned, spec-valid
avatar. One ranged request puts a posed character on screen; everything after it
refines the mesh already rendering, in place.
No custom server. No WebSocket. No new codec. Just a file, ordered intelligently,
served with the Range header every CDN has supported since 1997.
michelle.glb 829.8 KB -> first frame at 16,597 ms on slow 3G
michelle.a3s 54.7 KB -> first frame at 1,094 ms on slow 3G 15.2x faster
npm install @three-ws/avatar-streamThe browser entry (. and ./three) has zero dependencies. Packing pulls in
@gltf-transform/*, meshoptimizer, draco3dgltf, and sharp, all declared as
optional peers so a front-end bundle never sees them.
npx @three-ws/avatar-stream pack avatar.glb -o avatar.a3spacked avatar.glb -> avatar.a3s in 281 ms
source 829.8 KB
container 1629.2 KB
base layer 50.8 KB (6.1% of source, 842 of 28106 triangles)
level kind triangles bytes cumulative
0 base 842 50.8 KB 54.7 KB
1 patch 2810 557.3 KB 612.0 KB
2 patch 8430 220.2 KB 832.2 KB
3 patch 28106 797.0 KB 1629.2 KB
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { A3SPlayer } from '@three-ws/avatar-stream/three';
const player = await A3SPlayer.load('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/avatars/michelle.a3s', {
THREE,
GLTFLoader,
onLayer: (l) => console.log(`level ${l.level}: ${l.triangleCount} triangles`),
});
scene.add(player.scene); // already skinned, posed, and framed
await player.refine(); // detail arrives without disturbing camera or poseload() resolves as soon as the coarse avatar is renderable, after exactly one
HTTP request. refine() walks the remaining layers, mutating the geometry that
is already on screen.
Any static host works, because an A3S file is a static file:
GET /avatars/michelle.a3s
Range: bytes=0-56035
three.ws also packs on demand at
/api/avatar-stream?src=/avatars/michelle.glb,
which serves 206 Partial Content and will hand you the layer table as JSON with
&format=json.
a3s pack <input.glb> [-o out.a3s] [--levels 0.03,0.1,0.3,1] [--base-texture 64] [--json]
a3s inspect <file.a3s|url> [--json]
a3s verify <file.a3s|url> [--deep] [--json]
a3s extract <file.a3s|url> [-o preview.glb]
a3s bench <input.glb> [--connection slow-3g|fast-3g|4g] [--json]verify --deep replays every layer headlessly and proves the refined surface
matches the source triangle for triangle, with no renderer involved.
extract writes layer 0 out as a standalone GLB. It opens in Blender, in the
Khronos validator, in any glTF viewer, because it is not a custom blob:
a3s extract avatar.a3s -o preview.glb # 50.8 KB, 842 triangles, fully rigged| Export | Purpose |
|---|---|
A3SStream.open(target, options) |
Open a URL, bytes, or custom source. One request to a renderable base layer. |
stream.base |
Layer 0 as a Uint8Array. A complete GLB. |
stream.layer(level, { verify }) |
Fetch one refinement layer. |
stream.layers({ verify }) |
Async iterator over the refinement layers. |
httpSource(url, { fetch, headers }) |
Range-request byte source. Handles hosts that ignore Range. |
decodePreamble / decodeHeader / encodeContainer |
The container format itself. |
| Export | Purpose |
|---|---|
A3SPlayer.load(target, { THREE, GLTFLoader, onLayer }) |
Open a stream and parse layer 0 into a scene. |
player.refine(options) |
Apply every remaining layer, coarse to fine. |
applyPrimitivePatch / applyTexturePatch |
The primitives, if you drive refinement yourself. |
three and GLTFLoader are injected, never imported, so this package pins no
renderer version and never ships a second copy of three into your bundle.
| Export | Purpose |
|---|---|
pack(glbBytes, options) |
GLB to A3S. Returns { container, header, stats }. |
reconstruct(target, options) |
Replay a stream into plain typed arrays, no GPU. |
triangleFingerprint(positions, indices) |
Order-independent surface fingerprint, for tests. |
Edge-collapse simplification has a useful property: simplify a mesh successively, finest to coarsest, and the vertices surviving at each level are a strict subset of those at the level below. The levels nest.
So the packer ranks every vertex by the coarsest level it reaches, sorts the vertex buffer by that rank, and level k's vertices become exactly the first V(k) entries. A refinement layer only ever appends vertices the client lacks. It never rewrites a byte already sent.
Skinning survives untouched, because a simplifier emits a new index buffer over
the vertex array it was handed, carrying JOINTS_0 and WEIGHTS_0 along with it.
That is why the 842-triangle base layer is already bound to all 65 joints.
Animation clips are deliberately excluded from layer 0. On a rigged character the keyframe data often outweighs the mesh several times over, and it is the one part invisible in the first frame. Clips arrive in the first patch as a geometry-free companion GLB whose tracks bind by node name.
The wire format is specified in
specs/AVATAR_STREAM.md
and is CC0. Independent implementations are welcome.
Base layer as a share of source, across 17 production avatars:
| Avatar | Source | Base layer | Share | Triangles |
|---|---|---|---|---|
pumpfun-pill-cupsey-static |
1641 KB | 67 KB | 4.1% | 1336 of 44544 |
parametric-base |
4244 KB | 234 KB | 5.5% | 902 of 27676 |
michelle |
830 KB | 51 KB | 6.1% | 842 of 28106 |
xbot |
781 KB | 48 KB | 6.1% | 1472 of 49112 |
studio |
1796 KB | 154 KB | 8.6% | 1349 of 15817 |
brainstem |
3120 KB | 404 KB | 12.9% | 6813 of 61666 |
realistic-male |
1206 KB | 202 KB | 16.8% | 587 of 10677 |
Every base layer in that corpus passes Khronos glTF validation with zero errors, and every stream replays to a surface identical to its source.
Apache-2.0
If avatar-stream saves you time, star it on GitHub. Stars are how other developers and AI agents find the repositories worth trusting, and they cost you one click.
Know someone who would use it? Post on X · Share on Bluesky · Share on LinkedIn · Submit to Hacker News · Share on Reddit
Coding agents and LLM tooling can read this repo directly: AGENTS.md, llms.txt, llms-full.txt. Point an agent at https://github.com/nirholas/avatar-stream and it has the context it needs.
- All repositories by nirholas: the full catalog, grouped by topic
- three.ws: the platform for 3D AI agents with Solana wallets, a skill marketplace and x402 payments
- Questions or ideas: open an issue or start a discussion
three.ws is a platform for 3D AI agents with Solana wallets: avatars, a skill marketplace, x402 payments and more than seventy MCP servers. @three-ws/avatar-stream is one package from it.
- Documentation site: https://nirholas.github.io/avatar-stream/
- npm: https://www.npmjs.com/package/@three-ws/avatar-stream
- Canonical source: https://github.com/nirholas/three.ws (this repository is a generated mirror of
packages/avatar-stream) - Agent-readable summary: llms.txt and AGENTS.md
- Issues and ideas: https://github.com/nirholas/avatar-stream/issues