VibeXR is a single-file WebXR framework. Grabbing, pointing, pressing buttons and the hand menu work out of the box, so a project only writes its own world: small enough for one sentence to make.
Sponsored by VITURE→Everything built on VibeXR is a world, and every world opens from one place. Pick one and you walk straight in; from inside, a portal or the Worlds window on your wrist takes you to the next. Quest carries you across without leaving immersion.
Credit. Dream Cage, Robot Zoo, Microduck, Archiscape, Physics Cabinet, Roadster Night, Immersive Vibe and the Interaction Playground are built and maintained by rongxuanmu and published at rongxuanmu.com, where each has a full write-up and demo video.
One idea holds VibeXR together: a project is a world, and a world is the only thing anyone writes. Not an app, not an engine setup — an object that builds its content and says which things can be touched. Hands, rays, poke, menus, passthrough and the session belong to the framework, and every world inherits them unchanged.
Tens to a few hundred lines. That is what lets an agent write one in a single pass, and what makes a sentence-to-world box plausible at all.
A world never touches input or rendering rules, so a boring world is possible but a world where nothing can be grabbed is not.
init / enter / update / exit. Everything a world creates under its root is disposed when it unloads, so worlds can be swapped all day.
Because every world is the same kind of thing, they link by default: one address, portals between them, a Worlds window on your wrist, and the people in a world see each other's head and hands.
const Hello = {
id: "hello",
title: "Hello VibeXR",
init(ctx) {
const { THREE: T, root } = ctx;
root.add(new T.HemisphereLight(0xffffff, 0x222222, 1.6));
const cube = new T.Mesh(new T.BoxGeometry(0.12, 0.12, 0.12), new T.MeshStandardMaterial({ color: 0x7fb2ff }));
const head = ctx.app.input.head; // in front of you, a little below eye height
cube.position.set(head.x, head.y - 0.25, head.z - 0.8);
root.add(cube);
const it = ctx.add(cube, { draggable: true, twoHand: true }); // grab near or far, scale with two hands
it.on("tap", () => cube.material.color.setHex(Math.random() * 0xffffff));
const reset = ctx.button("Reset", { onClick: () => ctx.app.worlds.reload() });
reset.root.position.copy(cube.position).add(new T.Vector3(0, -0.16, 0));
root.add(reset.root);
}
};
VXR.app.worlds.register(Hello);
await VXR.app.worlds.load("hello");
Grab it however you like; tap to recolour; scale with two hands. Paste it as a second module script after the framework — the framework file itself is never edited. More examples →
Rendering a cube takes a dozen lines. Making it grabbable from across the room — keeping its pick distance, scalable with two hands, pokeable, with the ray stopping exactly at the surface — takes thousands, and every one of them is a tuning decision. Models are good at the first. They are not good at the second.
Pictures, not interactionScene, materials and animation look right. You put the headset on and nothing can be picked up.
No sense of what feels rightRays punch through surfaces. Grabbed objects fly into your face. Panels spawn outside the field of view.
Every prototype restartsTwo days of grab tuning, thrown away. The next prototype re-derives it, and re-breaks it.
Context runs outDeep engine hierarchies and boilerplate fill the window, and then the syntax hallucinations start.
All four point at one thing: what the model lacks isn't rendering, it's interaction sense. It has never stood inside a headset. It doesn't know that a ray punching through a desk is wrong, or that a far-grabbed object landing on your nose is wrong. That knowledge isn't in the public corpus, and no prompt recovers it.
So the thing to supply isn't a better prompt. It's a layer that is already correct — and that the model never touches.
For glasses, this matters more, not less. The questions a prototype exists to answer are the feel questions: can you grab it, is it comfortable to press, does the image hold when you turn your head. Code that merely runs cannot answer them — and if it can't, the prototype has no decision value.
Three jobs. Each part serves a human and a model at the same time, and that duality is the whole design.
Everything you can walk into right now — paintings, robots, toys, physics, a whole town — each a single world, all reachable from one lobby.
The interaction core: four verbs across four input types, a UI kit, a palm menu, and the world contract.
Where worlds get published: upload, automatic review, a link to share. Next, one box: say a sentence and walk into the world it makes, in thirty seconds.
To an object, every input is the same thing. They share four verbs — point, pinch, poke, grab — near and far. You write an object once; it works with all of them, and the model never needs to know which headset is on someone's head.
No target: a ray. Target within reach: the ray retracts, direct touch takes over. Target far away: the ray stays. Cursor and ray blend, never snap.
A far grab keeps its pick distance, with gain only on fast motion. Wheel or thumbstick pushes and pulls. Without this, everything you grab lands on your nose.
Fingertips and controller tips press surfaces and volumes and report press depth, so a button can have real travel.
precise, constrain, translateOnly, multi, twoHand, longPress, draggable — writing an object means saying how it may be treated.
Constant-width white hover outline that stays clean on multi-part CAD meshes, an outlined hand that occludes what's behind it, a soft click on press and release.
One Enter button: AR with video see-through on where it exists, VR through the same button where it doesn't. Table height calibrates from head position on entry.
Feeding a model the source dump is not retrieval. The framework splits into three layers because a model is missing something different at each stage.
Every interactable option, the world lifecycle, the UI kit inventory — one page. Fixes calls that are simply wrong: invented parameters, misspelled options.
Every default carries why it is that value and how it was wrong before. Fixes code that runs but feels wrong. This is the layer models are missing, and the only one that can't be copied from anywhere else.
Full source of every gallery world, tagged by genre. Fixes "I don't know what a finished world looks like." Editing beats authoring.
Face-the-user is held-only and keeps the angle you left it at. Rotation pivots on the window centre, not the bottom handle. The far ray ends at the hit point with no temporal smoothing. Windows scale with distance (K=0.8) so angular size stays constant. No physical inertia.
Layer 2, verbatim. Each line is one "built it, it was wrong, changed it" — exactly what a model cannot learn from public corpora.The corpus gets it written correctly; the harness gets it verified before delivery. Three pieces: an agent skill stating the world contract and the zero-edits-to-framework line; a desktop self-test loop that drives synthetic pointer events, steps frames by hand, and asserts numerically (“head turns 45°, object heading changes 0.000°”) rather than eyeballing a screenshot; and single-file delivery — one HTML, three.js via import map, no build chain, so generate → verify → share collapses to minutes.
Everything a coding agent needs to write a VibeXR world is published here as plain Markdown: a skill that sets the boundaries, and the three corpus layers it retrieves from. Your agent writes the world; the framework already did the hands.
For Claude Code, drop it into your skills folder. Any other agent: point it at llms.txt, which indexes everything below.
mkdir -p ~/.claude/skills/vibexr && curl -fsSL https://vibexr.io/skill/vibexr/SKILL.md -o ~/.claude/skills/vibexr/SKILL.md
Describe the world, not the interaction. Grab, ray, poke, menus and passthrough come with the framework.
Use the vibexr skill: a desk-sized solar system I can pick planets out of, with a button that pauses the orbits.
The result is one HTML file. The agent checks it on the desktop first — synthetic pointer events and numeric asserts, not screenshots — then you open the same file in a headset browser and press Enter.
Upload the HTML file at studio.vibexr.io and sign in with GitHub. An automatic review runs first: a safety scan, then a test render in a headless browser with a simulated headset. A person approves it, and it gets its own link.
skill/vibexr/SKILL.mdThe agent skill: the world contract, the never-edit-the-framework line, the workflow, the rules that matter most.
docs/api.mdLayer 1 — API surface. Every ctx helper, interactable option, event and UI piece, on one page.
docs/rules.mdLayer 2 — defaults and lessons. Each rule with its value, why, and how it was wrong before.
docs/tutorial.mdStart here: a step-by-step tutorial, no XR knowledge needed (中文版: /docs/tutorial.zh).
docs/examples.mdLayer 3 — world examples and recipes, using only the real API.
framework/vibexr.htmlThe framework itself: one HTML file, three.js via import map, no build. Start every world from this.
studio.vibexr.ioUpload, automatic review and your list of worlds. Sign in with GitHub.
llms.txtThe index an agent reads first.
Where a world goes once it works: upload the HTML file, let Studio check it, get a link to send around. The same place will grow a box that turns a sentence into a world.
One HTML file, the same one your agent made. Your worlds and their versions are listed in one place.
A safety scan of the code, then a test render in a headless browser with a simulated headset, at desktop and phone sizes. You get the report.
Anything the checks are unsure about waits for a human before it goes out.
Approved worlds get a link that opens in any headset browser. Listing in the lobby comes next.
This half does not exist yet — it is what the framework and the worlds are for. One box in Studio: say a sentence, or type a line, and walk into the world it makes. Not a demo reel, not a file to download — the thing you just said, close enough to reach out and grab.
We are designing against these numbers rather than measuring them yet. Latency is not a performance metric here — it is whether the experience exists at all. Past a minute, people switch away, and the feeling of the thing I said became a world is gone by the time they come back. So it is a hard constraint to design against, not something to optimise afterwards.
Every reason is in the two sections above. Strip any one of them and 30 seconds stops being reachable.
Tens to a few hundred lines, not a whole application. Without the world contract there is no 30-second budget to begin with.
So the result is grabbable, pointable and pokeable on the first try — no "it works but feels wrong, go again" round trips. Those were the expensive ones.
Nothing to compile or bundle. The world hot-mounts onto the framework already running; the page never reloads.
One fewer round trip is several fewer seconds.
Compile and attach what has already formed instead of waiting for the last token. The moment things start appearing matters far more than the moment they finish.
Keep skeletons for common genres ready so the model only fills in the difference.
When a generated world throws, fall back to the last good one and say why. One failure costs more than ten slow seconds.
Time to first token, time to complete, time to compile and mount — measured separately, or you cannot tell which part to cut.
The framework runs today and Studio publishes worlds; turning a sentence into a world is the next thing to build. Honest about the order:
Box to generated world to hot-mount, end to end — correctness first, speed second.
Stream the mount, pre-warm the skeletons, and measure each segment rather than guessing at it.
When a generated world throws, fall back to the last good one and say why.
The defaults and the reasoning behind them are published in docs/rules.md; every headset session adds to them.
Approved Studio worlds show up in the vibexr.io lobby next to the built-in ones, and e-mail sign-in joins GitHub.
Most paths are currently exercised on the desktop. The headset passes come next.