The headset needed a smaller world, not just a smaller file
The Spatial Commerce showroom's desktop scene has about 2.18 million vertices to draw. A standalone headset draws it twice per frame and collapses to single-digit frame rates, so I gave VR a decimated model of its own.
- Project
- Spatial Commerce
- Role
- VR mode and headset model, in a team of three
- Stack
- three.js, React Three Fiber, WebXR
- Status
- Live at spatialcommerce.se
The headset gets its own model
Our showroom's desktop scene is too much for a standalone headset to draw, so I gave the headset a model with far fewer triangles, loaded only in VR.
Three of us built the Spatial Commerce showroom in June 2026. It's a space you move through rather than a page you browse, with a guided tour from stop to stop and shopping inside the scene. The four products are samples, and the order step sends nothing over the network.
My public fork of the team's repository holds 31 commits from three authors, made between 5 and 12 June, and 13 of them are mine. They include the VR mode, the headset model it loads and the installable PWA. The VR mode adds a headset-icon button above the theme toggle, labelled "Enter VR" only when the browser reports support for immersive VR. Anywhere else it is dimmed and labelled "VR not supported on this device".
The problem was vertices, not megabytes
Count before you cut, because the count tells you which cut you need. The desktop scene is a glTF exported from Blender, with 110 meshes, 99 materials, 71 textures and exactly one glass (transmission) material. On the live site, its geometry buffer is 32.3 MB and the textures add 7.8 MB, so a desktop visitor downloads about 40 MB of scene behind a progress loader.
In a headset, a different count matters, and I noted it in my commit for the headset model: about 2.18 million render vertices across those 110 meshes. The target was a Quest 2, and a standalone headset like that draws the scene twice per frame, once for each eye, 72 to 90 times a second. On the full scene the headset collapses to single-digit frame rates, and the world swims with your head.
So the headset needed its own model. Not because the download was too big, but because there was too much to draw: 2.18 million vertices, twice per frame.
Decimation, not compression
Why not just compress the scene? Because compression shrinks the download, not the drawing. Draco and WebP (compression for the geometry and for the images) make the files smaller, but the headset unpacks them before it draws, so every vertex is still there.
Decimation removes triangles from the meshes themselves, so it was the fix. In the commit, I wrote it down as four steps, with compression last:
- Decimate at a "~10% triangle ratio", keeping the bounding boxes intact.
- Cap the textures at 512 px.
- Join draw calls.
- Then compress, with Draco and WebP.
The model in that commit kept about a quarter of the desktop's triangles, with 418K vertices and 75 draw calls. It loads only in a headset session, so a desktop visitor never downloads it. The commit gives counts, not a frame rate measured on the headset afterwards, so I won't quote one. The commit's file is about 3 MB; spatialcommerce.se still serves an earlier, heavier export of 4.4 MB.
The headset gives up more than triangles
Decimation was the main cut. My VR mode, in commit cffcb78, makes the smaller ones once a session starts:
- Post-processing is off, bloom included, because EffectComposer, which runs it, isn't WebXR-safe.
- Shadows are off, and the one glass material is neutralised.
- Heavy foveation, which draws the edges of each eye's view at a lower resolution, and a framebuffer scaled to 0.8 keep the fill rate down: fewer pixels for the GPU to fill each frame.
- Movement is teleport-only (aim at the floor, pull the trigger), so there is no thumbstick drift.
None of it reaches the desktop, which keeps full fidelity.
Two models to keep in step
The desktop scene and the headset model are separate files, so a change to the room is a change to both.
The headset also sees a plainer room: about a quarter of the triangles, textures at 512 px, no bloom, no shadows and no glass. The desktop keeps the full scene, and pays for it with about 40 MB to download behind a progress loader.
I'd make the same trade again, because a room that swims is worse than a plain one.
My skills page lists "Spatial and WebXR experiences for headsets and the browser". In this showroom, the "and" meant a second, smaller room. It's live at spatialcommerce.se. Thank you to the two people I built the showroom with.