---
name: quest-passthrough-camera-capture
description: Use when reading Meta Quest Passthrough Camera Access (PCA) frames into a Unity texture — freezing a room snapshot, feeding a shader, or piping camera pixels to generation/inference — especially when the feed's lifetime, permission gating, color space, or a blocking readback is in play.
license: MIT
tags: [unity, quest, passthrough, pca, mixed-reality, meta-xr, rendertexture, shader]
agents: [claude-code, codex]
category: xr
metadata:
version: "1.0.0"
capabilities:
network: false
filesystem: readwrite
tools: [Read, Edit, Write]
requires-secrets: []
resources:
- path: story.md
type: file
---Reading passthrough camera frames on Quest
PassthroughCameraAccess (Meta XR Core / MRUK) hands you the headset's real
camera feed. It is a native-written texture owned by a component with its own
lifecycle — treat it as a borrowed handle, never as an asset.
Never bind the feed's texture to a renderer
GetTexture() returns the texture the feed writes into from the render thread
and stops writing the moment the feed is disabled. Anything that disables the
feed — a controller's OnDisable, a withdrawn permission, a torn-down scene —
leaves a bound quad showing a live room that freezes, goes black, or references
a destroyed native handle, depending on timing.
Blit into a RenderTexture you allocate and release:
_target = new RenderTexture(source.width, source.height, 0, source.graphicsFormat);
Graphics.Blit(source, _target);
Sizing and format both matter:
- Size from the source texture each time, not once at startup.
CurrentResolutionis what the device granted, not what you requested, and it can differ from the request or change mid-session. - Match
source.graphicsFormat. The feed isR8G8B8A8_SRGB; blitting into a linear target darkens the whole image subtly enough to read as "the capture is murky" rather than as a color-space bug.
Guard every access on `enabled && IsPlaying`
GetTexture() internally validates state and logs an error when the
component is disabled or unpermitted. An unguarded per-frame call produces a
per-frame error line and returns null. Check feed.enabled && feed.IsPlaying
first.
Never call `GetColors()` in a frame path
It is AsyncGPUReadback.RequestIntoNativeArray(...).WaitForCompletion() — a
blocking stall, and its own docs say so. If pixels must reach the CPU, issue a
non-blocking AsyncGPUReadback and consume it a frame or more later. For
display or shader input, the pixels never need to leave the GPU at all.
Capture continuously, freeze by stopping
When you want a frame at a specific moment (an entry, a trigger, a gesture),
do not capture at that moment — allocation plus copy on the one frame the
user is watching is the worst possible placement. Instead blit on a slow
cadence beforehand into a texture nothing samples, and on the trigger simply
stop blitting. The last frame taken becomes the frozen one.
The freeze then has no separate flag to fall out of sync with: not capturing
is frozen.
Do not create a second feed
If anything else in the project already runs PassthroughCameraAccess
components (a stereo lens, a CV pipeline), find and read the existing one
rather than adding your own — they contend for the same camera. Search the
scene with a bounded retry; the feed may not exist on the frame your component
lands, because composition is often permission-gated.
Design for the feed being absent
PCA is device-only and permission-gated. In the Editor, in tests, on an
unsupported headset, or after a denied camera permission there is simply no
feed. That path must be a designed outcome with a bundled fallback asset — not
an error state — because it is the path every non-device run takes.
Keep "has a target" and "has a frame" as separate questions. An
allocated-but-never-blitted RenderTexture is a perfectly valid Texture full
of nothing, and preferring it over a fallback turns every permission-denied
session into a black rectangle.
What testing can and cannot reach
Extract the copy into a source-agnostic method (LatchFrom(Texture)) so an
ordinary Texture2D can stand in for the feed. That gives real coverage of
sizing, creation, release, and whether the blit moves pixels — assert a known
color reads back from the target, because a latch that sizes perfectly and
copies nothing looks identical to a working one from every angle except the
user's.
What no desktop test can answer: whether the live frame arrives upright.
The feed is native-written and Graphics.Blit follows the platform's
texture-origin convention, which a Texture2D stand-in gets right for reasons
a native handle may not. Put "is it flipped or mirrored, and on which axis?" on
the device checklist explicitly.
Related
OVRPassthroughLayer.textureOpacity dims what the wearer sees through the
compositor. It does not affect the raw PCA feed at all — a frame captured while
the passthrough layer is fully dimmed still shows the lit room.