Building a Walkable VR Mini-Museum in Unity

I built a small VR museum in Unity that you can walk around in, and got it running standalone on a Meta Quest 3 with no PC attached.

I love learning about history and art, and VR lets you stand next to a statue instead of looking at a photo of it. I wanted to find out what it takes to get from an empty Unity scene to that experience, and where the real problems are along the way.

This post covers the setup, why performance matters so much in VR, the problems I hit (there were several), and what I want to measure next.

Setup

ItemWhat I used
EngineUnity 6 (6000.0.77f1), URP
XROpenXR + XR Interaction Toolkit 3.0.11
HeadsetMeta Quest 3
Dev PCNVIDIA GeForce RTX 2060 (6 GB)
EnvironmentMuseum VR Asset Pack Vol. IV (Unity Asset Store)

The player rig is a standard XR Origin:

  • Main Camera with a Tracked Pose Driver (Input System) for head tracking
  • Left / Right hand objects with Tracked Pose Drivers, plus hand models animated by trigger and grip input
  • Locomotion: left thumbstick to move, right thumbstick to turn, plus gravity
  • An Input Action Manager that enables the XRI Default Input Actions

I tested in two ways: in the Unity Editor through Quest Link (PC VR), and as an Android build installed on the headset (standalone).

Why performance matters so much in VR

In VR, a dropped frame is not just a visual glitch. It can make people feel sick.

The headset refreshes at a fixed rate, and every frame has to be ready in time:

Refresh rateTime per frame
72 Hz13.9 ms
90 Hz11.1 ms

Three things make this budget tight:

  • Two eyes. Every frame is rendered twice, once per eye, from slightly different positions.
  • A mobile chip. In standalone mode, the Quest 3 renders everything on its own phone-class GPU, not a desktop graphics card.
  • My own additions. The asset itself is built for VR, with optimized scans averaging about 6,000 triangles. But I added stronger real-time lights to make it look better on video, on top of real-time shadows and post-processing. Those are expensive on a mobile GPU.

One setting that helps is Single Pass Instanced rendering (called Multiview on Android). Instead of drawing the scene once for each eye, Unity draws both eyes in a single pass using instancing. This roughly halves the draw call submissions compared to Multi-pass. My standalone build uses Single Pass Instanced / Multiview.

Experiment: Multi-pass vs Single Pass Instanced

Switching to Single Pass Instanced cut batches and SetPass calls roughly in half, but the frame rate stayed the same.

How I measured: Unity Editor in Play mode through Quest Link (USB cable), Quest 3 at 72 Hz. I stood in the same spot, looking at the same statue, and switched only the Render Mode (XR Plug-in Management → OpenXR → Windows tab). For each mode I took three readings from the Stats panel and one frame from the Profiler.

MetricMulti-passSingle Pass InstancedChange
Batches12061−49%
SetPass calls2613−50%
Shadow casters11164−42%
RenderPipelineManager, main thread (ms)1.501.01−33%
Render thread (ms)10.910.3about the same
Frame rate (FPS)72.270.4about the same

Stats values are the average of three readings. RenderPipelineManager is from a single Profiler frame.

Multi-pass

Single Pass Instanced

What the numbers say:

  • The draw work halved. In Multi-pass, Unity draws the scene once for each eye. Single Pass Instanced draws both eyes with one draw call, so batches and SetPass calls drop by half. The shadow pass also seems to run once instead of once per eye.
  • The frame rate did not move, and that is expected. It is locked to the headset’s 72 Hz refresh rate. In both modes, the main thread spends about 9 to 10 ms of each frame waiting (Gfx.WaitForRenderThread). On my PC, the CPU is not the bottleneck; the headset sets the pace.
  • So FPS is the wrong metric here. When the frame rate is capped, draw calls and thread times show the real difference.

Limits of this test: this ran in the Editor on a desktop RTX 2060, and the Profiler could not show GPU time. The Single Pass Instanced FPS readings varied more (68.5 to 73.0), which I treat as noise with only three samples. On a standalone Quest 3, where the CPU and GPU are mobile chips, halving the draw calls should matter much more. That is the next thing to measure.

Are the statues the problem?

No. The statue scans are already light, so polygons are not where the cost is.

I opened three of the statues in the mesh Inspector:

StatueVerticesTriangles
Perseus with the Head of Medusa14,14714,211
Ugolino8,2368,174
Alpheus and Arethusa7,4818,621

Even the heaviest, Perseus, is about 14k triangles. The asset’s documentation says its 13 models average about 6,000 triangles each, so all of them together come to roughly 80k.

Looking at the wireframes was the most interesting part for me. The meshes are fairly coarse; you can see the triangles on the faces and the folds of cloth. Yet in the headset they look finely carved. That detail comes from the textures, mainly the 2048×2048 normal maps, not from the geometry. For scanned historical artifacts, this is the key trade-off: keep the mesh light for the headset, and let the textures carry the craftsmanship.

The ~480k triangles in Stats therefore come mostly from rendering two eyes, the shadow pass, and the room itself. That changed my priorities for the next round: instead of starting with LODs, I plan to look at lighting, shadows and post-processing first, and I’ll share the results in an upcoming post.

Two small things I found while inspecting the statue:

  • An Animator with no controller, set to Always Animate. It does nothing on a static statue but still runs. It can be removed, or the model imported with Animation Type: None.
  • A Mesh Collider using the full statue mesh. For a walking-only experience, a simple box or capsule collider is enough.

Problems I hit, and how I fixed them

Most of my time went into these. Each one taught me something about how the VR pipeline fits together.

SymptomCauseFix
Meta Horizon Link warned that my GPU driver can freeze during playKnown issue in newer NVIDIA driversClean install of driver 591.86, with auto-updates off
The scene looked like a flat window, lagging 5 to 10 seconds behind my headLink was running over Wi-Fi (Air Link), not the cableDisabled the network connection and used the USB cable
Hands stuck at floor level, not trackingThe controllers showed up with a generic input layout. The pose lived at devicepose/position, but the XRI actions looked for devicePositionFound it with the Input Debugger, then added matching bindings to the actions
Android build failed in CMake, then in GradleMy project folder name contained [, which build tools read as a special characterRenamed the folder and deleted the old build cache
The app opened as a flat 2D panel on the headsetXR was not enabled for the Android buildEnabled OpenXR and Meta Quest Support on the Android tab

The biggest lesson: when something looks wrong in VR, first find out where it breaks. Unity’s Stats panel showed over 350 FPS in the Editor while the headset still stuttered. That pointed away from my scene and toward the connection between the PC and the headset.

Result

The museum now runs as a standalone app on the Quest 3. I can unplug the cable, walk between the statues, and turn with the thumbsticks.

It was beautiful. Standing next to the statues, I felt I could almost reach out and touch them, the way I would in a real museum. I added stronger lights than the asset’s sample scene, which brought out the detail in the marble. It felt like having a luxurious museum all to myself.

Those extra lights have a cost, though. Real-time lights and shadows are among the most expensive things for a mobile GPU. I haven’t measured the standalone frame rate yet, so that is the first thing to check.

Seeing the same scene on PC VR and on the headset alone made the performance question feel real. On PC, the RTX 2060 had plenty of headroom. On the Quest 3, the same scene has to fit inside a much smaller budget.

What I want to measure next

Getting it running was step one. Next, I want numbers from the headset itself:

  1. Measure standalone FPS with Meta’s OVR Metrics Tool, which shows frame rate inside the headset.
  2. Repeat the Multi-pass vs Single Pass Instanced comparison on the headset itself, where the hardware limits are much tighter.
  3. Cut lighting cost first. The asset includes a baked lighting setup. Bake my stronger lights too, and limit real-time shadows.
  4. Review post-processing and textures. Check the Global Volume effects, and use ASTC compression (and 1024 maps where possible) for the 2048×2048 textures.
  5. Clean up components. Remove the empty Animators and replace full-mesh colliders with simple ones.
  6. LODs last, only if the frame time still says geometry is the problem.

Museums, historical sites and cultural heritage are a great fit for VR, but they are also full of detailed scanned models. Keeping that detail while holding a steady 72 or 90 FPS on a standalone headset is the challenge I want to keep working on.

Leave a Comment