Darklabs · VR Lab
Make VR
in a browser tab.
No engine to install, no store to get through: a VR app can be a web page. This is a short, honest course in how, with a tiny example you can step into right now. It is what I have learned building the little VR bits in Darklabs, so it is not a complete manual, and I say where I have not tested something.
01 · Try it
Hello, VR
This is a real WebXR scene, about forty lines. On a headset browser (Meta Quest Browser, for example) the button puts you inside it. On a normal computer you see it as a 3D page; move the pointer to look around. Trigger on a controller changes a cube's colour.
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.xr.enabled = true; // 1. tell three.js to talk to the headset
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, 1.6, 0.1, 50);
camera.position.set(0, 1.6, 2.6); // eye height, in metres
// a ring of cubes, 1.5 m from the middle
for (let i = 0; i < 8; i++) { /* add a cube at angle i/8 * 2π */ }
renderer.setAnimationLoop((t) => { // 2. NOT requestAnimationFrame:
ring.rotation.y = t / 4000; // the headset sets the pace
renderer.render(scene, camera);
});
button.onclick = async () => { // 3. a user gesture starts the session
const s = await navigator.xr.requestSession("immersive-vr",
{ optionalFeatures: ["local-floor"] });
renderer.xr.setSession(s);
};
const c = renderer.xr.getController(0); // 4. a controller's pointing ray
c.addEventListener("selectstart", () => { /* raycast from c, recolour the cube */ });
scene.add(c);
02 · The course
Eight small lessons
Each one is a few lines to read and one thing to try. They go from easiest to hardest. The code is three.js with no build step; the same copy of three.js that runs the rest of Darklabs is used here.
1 A cube, a loop and a button
Turn on renderer.xr.enabled, drive everything from renderer.setAnimationLoop (the headset sets the frame timing, so requestAnimationFrame is the wrong loop), and start the session from a button, because browsers only allow it after a user gesture. WebXR also needs a secure page: HTTPS, or localhost while developing. Check navigator.xr.isSessionSupported("immersive-vr") before showing the button.
Try: change the cube ring above to a different shape and make it spin the other way.
2 Spaces, metres and scale
Everything is in metres. Ask for local-floor as an optional feature and y = 0 is the floor. To make something huge feel like a tabletop model, scale the world, not the camera: the IDEA game scales a whole store down to 0.026 and sits it on a virtual table.
Try: put one cube exactly 1 m in front of you at eye height and check it feels like a metre.
3 Controllers and pointing
renderer.xr.getController(i) gives a pointing ray. It fires selectstart and selectend for the trigger and squeezestart for the grip. To pick things, raycast from the controller's world matrix. One bug worth knowing: add controller objects and listeners once. If you add them each time a session starts, every re-entry adds another ray and another handler (a review caught exactly this in the IDEA game).
Try: make the cube you point at glow, and clear the glow on selectend.
4 Comfort comes first
Do not move the player's view unless they are driving it. Keep the horizon steady, prefer snap turns to smooth ones, keep text and buttons about one to two metres away, and protect the frame rate: a stutter is what makes people feel unwell. If a thing can be a stationary tabletop, let it be one.
Try: add a snap turn of 30 degrees on the thumbstick and see how it feels compared with a smooth turn.
5 Performance: the budget
A headset renders twice, at a high refresh rate, on a phone-class chip. So: few draw calls (merge or InstancedMesh repeated things, as the IDEA game does for its route arrows), few lights, no new objects inside the frame loop, and test early on the real device. renderer.xr.setFoveation() and setFramebufferScaleFactor() trade sharpness for speed.
Try: draw 2,000 cubes as separate meshes, then as one instanced mesh, and compare the frame rate.
6 Sound in space
Web Audio has 3D sound built in: a PannerNode with the HRTF model places a sound in space, and you move the audio listener to match the player's head every frame. Start the AudioContext from the same click that enters VR, so the browser lets it play.
Try: attach a quiet tone to one cube so it hums from where it sits.
7 Test without a headset, then with one
Browser extensions that emulate a headset and controllers (Meta's Immersive Web Emulator is one) let you test the logic at a desk. They will not tell you about comfort or real performance, so always finish on a real device. On a Quest, open the page in its browser, and use USB debugging if you need the console.
Try: run this page in an emulator, then on a headset if you have one, and note what differs.
8 Ship it kindly
Feature-detect and keep a flat-screen fallback, as IDEA does. Say what is untested. Put a short comfort note on the page. If your page makes sound, never start it before a click.
Try: publish a one-file example on GitHub Pages and open it on your phone.
Good places to keep learning
Official pages, not mine: MDN: the WebXR Device API · Immersive Web samples · three.js WebXR examples · Meta's WebXR documentation.
See a larger working example: IDEA: Drunk Bots, a quick parody game with a tabletop VR mode. It is a prototype and has not been tested on every headset.
03 · Support
Where your support should go
“If there's one project you're going to support, make it this one.”
Hero Drop
Hero Drop is the VR game I am building. It is the thing all of this is practice for.
I am not looking for direct support for Darklabs right now. If you want to give money, time or attention to anything, please give it to Hero Drop's development, and to the small community around Hyper Dash, whose game is being switched off on 1 November 2026.
04 · Hyper Dash
Last chance to play it online
Hyper Dash is a free-to-play team shooter for VR, made by Triangle Factory, on Meta Quest and Steam. Its developers have announced that the online servers shut down on 1 November 2026. A final update lets the game be played offline afterwards, but the online matches, the part that makes it what it is, end then.
I think of playing Hyper Dash as a kind of prerequisite for Hero Drop: it is where I learned what I want that kind of game to feel like. A lot is going on there right now, the servers are busy and the community is very active, and that includes the competitive scene: Dash League, an independent league that has played Hyper Dash matches for years and streams them. If you have ever been curious, this is the last stretch to play it with other people.
I do not know the developers personally. This page is not made, endorsed or approved by Triangle Factory or by Dash League, and the names belong to their owners. Links go to their own official pages; there are no images or text copied from them. Sources: the developers' announcement and UploadVR's report.
Counting to 1 November 2026 (check the official post for the exact hour).
The video loads from YouTube only when you press play (in its privacy-friendly mode), so nothing is sent to YouTube before that.
05 · Alerts
Get an alert, no account needed
Three ways, pick any. None of them asks for your email or name.
1. Notifications from me
Announcements (a Hero Drop update, a reminder before the servers go) are sent to a free notification channel. Subscribe in the ntfy app or on the web, and your phone or browser will buzz.
Channel name:
2. Alert me while this page is open
Allow notifications here and a message arrives on this screen the moment one is sent. It only works while this tab is open.
Off. Turning it on connects this tab to ntfy.sh, which sees your IP address, as any website would.
3. Put it in your calendar
Two reminders, with nothing sent anywhere: a week before the Hyper Dash servers close, and the day itself.
Anyone who knows the channel name can post to it, so treat a message as a note, not a verdict. Links are never made clickable here.