A Vite + React + Three.js (WebGPU) template built around three systems:
https://github.com/wonglok/effectnode-b3-template-code/releases/tag/r001
src/b3/b3-blender) that runs a WebSocket server (default localhost:8765) and streams the scene — geometry, materials, lights, camera, HDRIs and textures — to the browserscene.zip statically, with navmesh + character rig layered on topsrc/runtime-intelligence) — a local REST + WebSocket service that answers questions about the live scene graph and applies mutations, so a coding agent (or you, over curl) can diagnose and tweak the running app without a rebuild. See Three.js runtime diagnosissrc/b3/b3-runtime/src/components/AvatarSDK) — a manifest-driven body/head/motion assembler with a live picker UI. See Avatar wardrobe system# install
bun install
# run the dev server
bun run dev
# type-check + production build
bun run build
# lint
bun run lint
# preview the production build
bun run preview
websockets into Blender's bundled Python on first usehttp://localhost:5173/dev — the Dev page connects to Blender and renders the scene in real timeThe Dev page sidebar can:
scene.zip — pick a folder via the File System Access API (persisted in IndexedDB), and every snapshot is written there automatically. Tip: point it at public/deploy/ so the deployment page picks it up; Vite is configured to serve it without hot-reloading/deployment) fetches /deploy/scene.zip and plays it with the walkable navmesh + character rigsrc/runtime-intelligence is a local agent bridge in two halves: a backend (Express + socket.io, default localhost:4343) that holds no scene state and merely forwards requests, and an editor (IntelligenceScan) mounted inside each page's R3F canvas that answers them about the live scene. bun run dev starts it next to Vite, and Vite proxies /api + /socket.io to it, so everything works same-origin from http://localhost:5173.
At least one tab must be open, or every query answers 503. Every request fans out to all connected tabs and comes back with one labelled answer per device, so a single call compares an iPhone, an Android and a laptop against the same scene.
# who is connected? These labels are what `?editor=` takes.
curl -s localhost:4343/api/editors | jq '.editors[] | {label, viewport, devicePixelRatio}'
# what is a frame costing, on every device at once?
curl -s localhost:4343/api/query/performance \
| jq '.result.responses[] | {who: .editor.label, fps: .result.runtime.framerate.fps}'
# why is GPU memory growing? (aimed at one device)
curl -s 'localhost:4343/api/query/memory?editor=mac' \
| jq '.result.responses[0].result | {gpu, leakCandidates}'
| Route | Answers |
|---|---|
GET /api/editors | who is connected — label, platform, viewport, DPR |
GET /api/query/scene | scene graph, world-space bounds, cull flags |
GET /api/query/performance | geometry cost + live fps / frame budget / effect timing / browser environment |
GET /api/query/memory | GPU registry, leak candidates, instancing candidates |
GET /api/query/drawcalls | per-object draw calls and their bound textures |
GET /api/query/shader | captured WGSL source, live uniforms, wired feature slots |
POST /api/mutation/patch | RFC 6902 JSON Patch against any object |
POST /api/mutation/eval | run a snippet with $0 / scene / camera / gl in scope |
POST /api/mutation/dispose | detach a subtree and release its GPU resources |
Add ?editor=<id or label substring> to any route, GET or POST, to aim it at one tab instead of all of them — ?editor=safari, ?editor=iphone. An ambiguous selector is refused rather than guessed at.
Mutations are local-only — they refuse non-loopback callers and cross-origin browser requests, and the editor disables them outside a dev build. eval is arbitrary JavaScript execution in the page, by design. Note that a mutation without ?editor= lands on every connected tab.
The full protocol — selector resolution, failure modes, how to read each facet, and the WebGPU optimisation heuristics — lives in src/runtime-intelligence/skill/query-runtime.md.
Characters are assembled at runtime from three swappable parts, described by the manifest at public/char/avatar.manifest.json:
{
"sdk": "mixamo-adapter/avatar", "version": 2,
"gender": "male",
"assets": {
"body": "/char/male/body/water-guy.glb",
"face": "/char/male/face/low-poly-west-head.glb"
},
"body": { "position": [0, -0.045, 0], "rotation": [-90, 0, 0], "scale": [1, 1, 1] },
"head": { "position": [0, 0, 0], "rotation": [0, 0, 0], "scale": [1, 1, 1] }
}
/char/{male,female}/body/char/{male,female}/face/char/motion-2/fbx (locomotion, gesture, gun, longbow, breakdance, pro-magic, shooter, stay)Every body↔head pairing seats differently, so the manifest carries an offsets table keyed by the body/head URL pair; classifyHeadCompose selects the plan and seats the head on the shared mixamorig skeleton at load. Parts are GLBs, so the texture budget is enforced on load — embedded images are capped to MAX_TEXTURE_SIZE and shared across clones.
In the UI, AvatarPicker is the bottom-right button on the Dev / Preview / Production canvases. Gender, body and head chips live-apply to the Zustand useAvatarStore that NavMeshRig subscribes to, so the walking character restyles in place; the "stay motion" chips are preview-only and loop inside the popup's own preview canvas.
| Route | Page | What it does |
|---|---|---|
/ | Home | Landing page + add-on download |
/dev | Dev | Live WebGPU sync canvas, snapshot → OPFS → optimizer, zip export, navmesh mode |
/preview | Preview | Plays the deployment zip out of OPFS — whatever the Dev page last exported |
/production | Production | Plays the static /deploy/scene.zip, with navmesh + character rig + avatar wardrobe |
/deployment | Deployed | Alias of /production — routes to the same ProductionPage |
src/
├── main.tsx # entry
├── AppRouter.tsx # routes
├── pages/ # Home / Dev / Preview / Production (+ /deployment alias)
├── components/ # app components (navmesh rig, joystick, emotion buttons, avatar store)
├── runtime-intelligence/ # agent bridge — Express + socket.io backend, in-canvas editor, skill doc
└── b3/ # the B3 packages
├── b3-blender/ # Blender add-on — WebSocket server + scene streaming (Python)
└── b3-runtime/ # @effectnode/b3-runtime — React runtime library (published to npm)
└── components/AvatarSDK/ # avatar wardrobe: manifest, head compose, rig, motion library
three/webgpu + TSL materials)343 commits
TypeScript
90.4%
Python
9.0%
A Vite + React + Three.js (WebGPU) template built around three systems:
https://github.com/wonglok/effectnode-b3-template-code/releases/tag/r001
src/b3/b3-blender) that runs a WebSocket server (default localhost:8765) and streams the scene — geometry, materials, lights, camera, HDRIs and textures — to the browserscene.zip statically, with navmesh + character rig layered on topsrc/runtime-intelligence) — a local REST + WebSocket service that answers questions about the live scene graph and applies mutations, so a coding agent (or you, over curl) can diagnose and tweak the running app without a rebuild. See Three.js runtime diagnosissrc/b3/b3-runtime/src/components/AvatarSDK) — a manifest-driven body/head/motion assembler with a live picker UI. See Avatar wardrobe system# install
bun install
# run the dev server
bun run dev
# type-check + production build
bun run build
# lint
bun run lint
# preview the production build
bun run preview
websockets into Blender's bundled Python on first usehttp://localhost:5173/dev — the Dev page connects to Blender and renders the scene in real timeThe Dev page sidebar can:
scene.zip — pick a folder via the File System Access API (persisted in IndexedDB), and every snapshot is written there automatically. Tip: point it at public/deploy/ so the deployment page picks it up; Vite is configured to serve it without hot-reloading/deployment) fetches /deploy/scene.zip and plays it with the walkable navmesh + character rigsrc/runtime-intelligence is a local agent bridge in two halves: a backend (Express + socket.io, default localhost:4343) that holds no scene state and merely forwards requests, and an editor (IntelligenceScan) mounted inside each page's R3F canvas that answers them about the live scene. bun run dev starts it next to Vite, and Vite proxies /api + /socket.io to it, so everything works same-origin from http://localhost:5173.
At least one tab must be open, or every query answers 503. Every request fans out to all connected tabs and comes back with one labelled answer per device, so a single call compares an iPhone, an Android and a laptop against the same scene.
# who is connected? These labels are what `?editor=` takes.
curl -s localhost:4343/api/editors | jq '.editors[] | {label, viewport, devicePixelRatio}'
# what is a frame costing, on every device at once?
curl -s localhost:4343/api/query/performance \
| jq '.result.responses[] | {who: .editor.label, fps: .result.runtime.framerate.fps}'
# why is GPU memory growing? (aimed at one device)
curl -s 'localhost:4343/api/query/memory?editor=mac' \
| jq '.result.responses[0].result | {gpu, leakCandidates}'
| Route | Answers |
|---|---|
GET /api/editors | who is connected — label, platform, viewport, DPR |
GET /api/query/scene | scene graph, world-space bounds, cull flags |
GET /api/query/performance | geometry cost + live fps / frame budget / effect timing / browser environment |
GET /api/query/memory | GPU registry, leak candidates, instancing candidates |
GET /api/query/drawcalls | per-object draw calls and their bound textures |
GET /api/query/shader | captured WGSL source, live uniforms, wired feature slots |
POST /api/mutation/patch | RFC 6902 JSON Patch against any object |
POST /api/mutation/eval | run a snippet with $0 / scene / camera / gl in scope |
POST /api/mutation/dispose | detach a subtree and release its GPU resources |
Add ?editor=<id or label substring> to any route, GET or POST, to aim it at one tab instead of all of them — ?editor=safari, ?editor=iphone. An ambiguous selector is refused rather than guessed at.
Mutations are local-only — they refuse non-loopback callers and cross-origin browser requests, and the editor disables them outside a dev build. eval is arbitrary JavaScript execution in the page, by design. Note that a mutation without ?editor= lands on every connected tab.
The full protocol — selector resolution, failure modes, how to read each facet, and the WebGPU optimisation heuristics — lives in src/runtime-intelligence/skill/query-runtime.md.
Characters are assembled at runtime from three swappable parts, described by the manifest at public/char/avatar.manifest.json:
{
"sdk": "mixamo-adapter/avatar", "version": 2,
"gender": "male",
"assets": {
"body": "/char/male/body/water-guy.glb",
"face": "/char/male/face/low-poly-west-head.glb"
},
"body": { "position": [0, -0.045, 0], "rotation": [-90, 0, 0], "scale": [1, 1, 1] },
"head": { "position": [0, 0, 0], "rotation": [0, 0, 0], "scale": [1, 1, 1] }
}
/char/{male,female}/body/char/{male,female}/face/char/motion-2/fbx (locomotion, gesture, gun, longbow, breakdance, pro-magic, shooter, stay)Every body↔head pairing seats differently, so the manifest carries an offsets table keyed by the body/head URL pair; classifyHeadCompose selects the plan and seats the head on the shared mixamorig skeleton at load. Parts are GLBs, so the texture budget is enforced on load — embedded images are capped to MAX_TEXTURE_SIZE and shared across clones.
In the UI, AvatarPicker is the bottom-right button on the Dev / Preview / Production canvases. Gender, body and head chips live-apply to the Zustand useAvatarStore that NavMeshRig subscribes to, so the walking character restyles in place; the "stay motion" chips are preview-only and loop inside the popup's own preview canvas.
| Route | Page | What it does |
|---|---|---|
/ | Home | Landing page + add-on download |
/dev | Dev | Live WebGPU sync canvas, snapshot → OPFS → optimizer, zip export, navmesh mode |
/preview | Preview | Plays the deployment zip out of OPFS — whatever the Dev page last exported |
/production | Production | Plays the static /deploy/scene.zip, with navmesh + character rig + avatar wardrobe |
/deployment | Deployed | Alias of /production — routes to the same ProductionPage |
src/
├── main.tsx # entry
├── AppRouter.tsx # routes
├── pages/ # Home / Dev / Preview / Production (+ /deployment alias)
├── components/ # app components (navmesh rig, joystick, emotion buttons, avatar store)
├── runtime-intelligence/ # agent bridge — Express + socket.io backend, in-canvas editor, skill doc
└── b3/ # the B3 packages
├── b3-blender/ # Blender add-on — WebSocket server + scene streaming (Python)
└── b3-runtime/ # @effectnode/b3-runtime — React runtime library (published to npm)
└── components/AvatarSDK/ # avatar wardrobe: manifest, head compose, rig, motion library
three/webgpu + TSL materials)343 commits
TypeScript
90.4%
Python
9.0%