# World-anchored tunneling *for third-person VR*

How Duskcone combines an authored game camera with tracked stereo viewing, an adaptive spatial aperture, and stable surroundings.

## Overview

Duskcone is a Meta Quest/OpenXR port of Dusklight, the reimplementation of *The Legend of Zelda: Twilight Princess*. Its comfort system presents the game in stereo through a spatial opening—the gameplay portal—during camera movement. The opening stays anchored in the viewer’s surroundings, with a stable environment around it. When the camera settles, the opening can expand and the surroundings fade away, returning to full surrounding VR.

This extends Google Daydream’s tunneling approach in two directions: the aperture responds to the **source of camera motion**, and its boundary is **geometry in the viewer’s space**. A larger aperture accompanies ordinary gameplay movement; a smaller one accompanies camera corrections and forced motion. A separate proximity fade, Face Guard, addresses geometry that comes too close to the viewer.

The implementation uses established rendering techniques to combine these views. The following sections trace the system from camera inputs through motion selection and aperture transitions to rendering. Duskcone supplies the concrete implementation and tuning values; another game can use the same arrangement with its own camera and movement hooks. Personal observations behind the comfort policy are collected in the design appendix.

> [!WARNING]
> **Draft — work in progress.** This paper is still being revised. It documents Duskcone’s current camera and comfort system, but explanations and illustrations may contain gaps or inaccuracies.

## 1. Tunneling around an authored camera

Google’s 2017 Daydream locomotion article describes restricting the moving scene while providing a stable peripheral reference, analogous to watching a moving image on a television in a stationary room. It also recommends fading the effect to make its transitions less distracting.[^1]

The Daydream Elements demo names the aperture the **iris** and the surrounding static environment the **cage**. Its documentation reports that high-contrast gridlines were an effective grounding cue in Google’s testing. The important distinction is between the moving primary scene and a surrounding reference that remains stationary as the viewer looks around.[^2]

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 235" role="img" aria-labelledby="lineage-title lineage-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="lineage-title">A moving primary scene inside a stable peripheral environment</title>
<desc id="lineage-desc">A grid representing the stable home surrounds a smaller rectangle containing the moving gameplay scene. Four inward arrows identify the aperture.</desc>
<rect x="30" y="24" width="460" height="174" rx="4"/>
<g class="paper-faint"><path d="M30 68h460M30 112h460M30 156h460M98 24v174M166 24v174M234 24v174M302 24v174M370 24v174M438 24v174"/></g>
<rect x="145" y="57" width="230" height="108" fill="var(--panel, #fff)" class="paper-signal"/>
<path d="m155 142 47-34 37 21 56-46 69 59" class="paper-signal"/>
<path d="M117 43l20 13m-9-1 9 1-3-8M404 43l-20 13m3-8-3 8 9-1M117 180l20-13m-9 1 9-1-3 8M404 180l-20-13m3 8-3-8 9 1" class="paper-signal"/>
<path d="M375 105h145M490 180h30"/>
<text x="535" y="101" font-size="16" class="paper-signal">Moving gameplay</text><text x="535" y="124" font-size="13" class="paper-signal">iris / portal</text>
<text x="535" y="176" font-size="16">Stable surroundings</text><text x="535" y="199" font-size="13">cage / home</text>
</svg>
<figcaption>Figure 1. Moving gameplay and stable surroundings have separate motion responsibilities.</figcaption>
</figure>

Duskcone expands on that approach by accommodating an existing third-person camera, distinguishing ordinary motion from automatic motion, and anchoring the aperture in the world. The surrounding reference can be a room from the game or the player’s physical room through passthrough.

The game continues to determine camera placement, direction, lock-on behavior, and cutscene framing. Those decisions belong to a design in which combat, animation, controls, and camera movement work together. The headset adds stereo depth and freely tracked looking without replacing that camera process with the player’s head pose.

This constraint also determines the comfort strategy. Rewriting locomotion to eliminate acceleration or camera rotation would change the game. Instead, the presentation changes how much of that motion occupies the viewer’s field of view, and provides a stationary reference around it.

## 2. Camera and reference frames

The game camera and the tracked viewer are independent inputs. Combining them gives the original third-person view stereo depth and freely tracked looking: the player can turn to see beyond the original camera frame or lean to see objects shift relative to one another. These viewing changes leave the game’s camera direction and controls intact.

The game supplies an authored camera position and orientation; the XR runtime supplies a tracked position, orientation, and field of view for each eye. Each rendered eye camera combines those inputs:

$$
\mathbf e_i=\mathbf e_{\mathrm{game}}+U B_{\mathrm{game}}\mathbf p_i,
\qquad R_{\mathrm{eye},i}=B_{\mathrm{game}}R_{\mathrm{tracked},i}.
$$

Here p is the tracked eye position in meters, U converts meters to game units, and B is the orthonormal basis formed from the authored camera’s right, up, and backward directions. OpenXR’s local negative Z direction therefore maps onto the game camera’s forward direction. The tracked rotation is applied in this basis. Each eye’s projection is built from its runtime-provided field-of-view (FOV) angles and the game’s near/far clipping distances. The two distinct eye positions and projections provide binocular depth throughout the view, including inside the portal.

### Keep head tracking responsive

The authored camera may be interpolated between game updates. Apply that interpolation first, then compose it with the runtime’s eye poses predicted for display time. This lets game-camera movement remain smooth without applying its interpolation delay to the player’s head tracking.

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 240" role="img" aria-labelledby="frames-title frames-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="frames-title">Compose the game camera with tracked eye poses</title>
<desc id="frames-desc">The authored game camera and independently tracked eye poses combine to produce two eye cameras. Those cameras render the game with binocular depth and tracked looking.</desc>
<rect x="18" y="24" width="216" height="62" rx="5"/><text x="126" y="50" text-anchor="middle" font-size="15">Authored game camera</text><text x="126" y="71" text-anchor="middle" font-size="12">Position and orientation</text>
<rect x="18" y="149" width="216" height="62" rx="5" class="paper-tracked"/><text x="126" y="175" text-anchor="middle" font-size="15" class="paper-tracked">Tracked eye poses</text><text x="126" y="197" text-anchor="middle" font-size="12">Predicted display-time view</text>
<path d="M234 55h40v47h31m-7-5 7 5-7 5"/>
<path d="M234 180h40v-47h31m-7-5 7 5-7 5" class="paper-tracked"/>
<rect x="305" y="86" width="199" height="62" rx="5"/><text x="404.5" y="112" text-anchor="middle" font-size="15">Compose eye cameras</text><text x="404.5" y="134" text-anchor="middle" font-size="12">One view per eye</text>
<path d="M504 117h39m-7-5 7 5-7 5" class="paper-signal"/>
<rect x="543" y="86" width="198" height="62" rx="5" class="paper-signal"/><text x="642" y="112" text-anchor="middle" font-size="15" class="paper-signal">Stereo game view</text><text x="642" y="134" text-anchor="middle" font-size="12">Depth and tracked looking</text>
</svg>
<figcaption>Figure 2. The game supplies the base camera; tracked eye poses add stereo depth and independent looking.</figcaption>
</figure>

The peripheral room and portal plane belong to the XR presentation space. Each eye projects the same spatial geometry from its current pose. Turning or leaning changes the portal’s position and perspective in the eye image; it does not pull the portal along with the gaze. The room responds to head tracking while remaining independent of the game camera’s travel.

System recentering changes the relationship to the runtime’s reference space. The portal and room use that reference relationship together, preserving normal recentering behavior.

### Render the view the eyes can see

Changing the projection is only part of adding tracked stereo viewing. The game’s original visibility tests may discard an object outside its flat camera frame even though a turned or leaning viewer can see it. Duskcone retains the authored visibility test and rescues rejected draws with a conservative stereo frustum. The rendering view can widen while the game continues to use its authored camera for gameplay decisions.

In another engine, the equivalent is to make draw culling cover both composed eye views. This matters inside the portal as well as in full VR: the opening limits which part of the stereo image is shown after visibility has already been decided.

### Scale is a presentation decision

The game’s baseline scale is **100 game units per meter**. Gameplay is presented at **50 units per meter**, making the world twice as large relative to the viewer’s physical head and eye separation.

At baseline scale, viewing the scene stereoscopically from a third-person camera made it resemble a miniature. That impression worked against the tone and apparent scale of the flat game. Doubling the presented world brings it closer to that subjective scale. This changes the conversion used for rendering, not the game’s simulation or camera logic.

The surrounding room uses the baseline 100 units per meter. It is a separate environment around the viewer, so its scale can be chosen independently of the third-person gameplay view.

## 3. Three aperture targets

The portal moves continuously between three target presentations:

<figure class="paper-figure">
<svg xmlns="http://www.w3.org/2000/svg" class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" viewBox="0 0 760 190" role="img" aria-labelledby="states-title states-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<title id="states-title">The three aperture targets</title>
<desc id="states-desc">A surrounding 360 degree view, a large rectangular portal, and a smaller rectangular portal.</desc>
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<g transform="translate(51 17) scale(1.5)" class="paper-signal"><path d="M12 34c0-22 72-22 72 0s-72 22-72 0" opacity=".6"/><path d="m7 39 5-5 5 5m62-10 5 5 5-5"/><circle cx="48" cy="24" r="6"/><path d="M37 39c0-13 22-13 22 0"/><text x="48" y="62" text-anchor="middle" font-size="11">360°</text></g>
<g transform="translate(309 20) scale(1.5)"><g class="paper-faint"><rect x="4" y="6" width="88" height="52" rx="2"/><path d="m4 6 12 7m76-7-12 7M4 58l12-7m76 7-12-7"/></g><g class="paper-signal"><rect x="16" y="13" width="64" height="38" rx="1" fill="currentColor" fill-opacity=".08"/><path d="m17 40 15-10 13 7 16-12 18 15" opacity=".6"/></g></g>
<g transform="translate(565 20) scale(1.5)"><g class="paper-faint"><rect x="4" y="6" width="88" height="52" rx="2"/><path d="m4 6 22 13m66-13L70 19M4 58l22-13m66 13L70 45"/></g><g class="paper-signal"><rect x="26" y="19" width="44" height="26" rx="1" fill="currentColor" fill-opacity=".08"/><path d="m27 38 10-7 9 5 11-9 12 11" opacity=".6"/></g></g>
<g text-anchor="middle"><text x="123" y="143" font-size="16">Still</text><text x="123" y="167" font-size="13">Full surrounding VR</text><text x="381" y="143" font-size="16">Ordinary motion</text><text x="381" y="167" font-size="13">Gameplay aperture</text><text x="637" y="143" font-size="16">Automatic motion</text><text x="637" y="167" font-size="13">Safety aperture</text></g>
</svg>
<figcaption>Figure 3. Full surrounding VR, the larger gameplay aperture, and the smaller safety aperture.</figcaption>
</figure>

| Target | Selection | Presentation |
| --- | --- | --- |
| Full VR | The game camera has settled. | The peripheral room disappears and gameplay occupies the entire surrounding view. |
| Gameplay aperture | Ordinary camera movement, such as following the player’s movement or manual camera steering. | A larger aperture retains more of the moving scene. |
| Safety aperture | Automatic movement, such as a collision correction, knockback followed by the camera, or a moving cutscene shot. | A smaller aperture exposes more of the stable surroundings. |

The distinction comes from the experience of playing: predictable movement initiated through the controls has been easier to accommodate than imposed camera movement. The implementation approximates that distinction by measuring which parts of the game caused the camera to move. Ordinary motion is the default; a smaller aperture requires positive evidence of automatic motion.

Options can retain the room when still or hold a smaller aperture throughout cutscenes. These alter the policy while using the same geometry and rendering paths.

## 4. Measure continuous camera motion

Motion measurement uses the game camera before tracked eye poses are applied. Each sample records its position (the authored “eye”), look-at point (the “center” or “focus”), forward and up directions, bank, and vertical FOV. Translation, rotation, roll, and FOV change are normalized separately:

$$
\begin{aligned}
q_{\mathrm{translation}}&=\frac{\|\Delta\mathbf e\|}{U\,\Delta t\,v_0},\\
q_{\mathrm{rotation}}&=\frac{\angle(\mathbf f_0,\mathbf f_1)}{\Delta t\,\omega_0},\\
q_{\mathrm{roll}}&=\frac{\max\bigl(\angle(\mathbf u_0,\mathbf u_1),|\Delta b|\bigr)}{\Delta t\,\omega_0},\\
q_{\mathrm{FOV}}&=\frac{|\Delta\phi|}{\Delta t\,\omega_0},\\
q_{\mathrm{motion}}&=\max_j q_j.
\end{aligned}
$$

The vectors e, f, and u are the authored eye position, forward direction, and up direction; b is bank and φ is vertical FOV. Δt is the interval between camera samples. In Duskcone, U is 50 game units per meter, v₀ is 0.15 m/s, and ω₀ is 6°/s. Bank differences wrap to the shortest signed angle. Taking the maximum lets a significant change in any component request an aperture without depending on unrelated components.

### Camera cuts are not continuous travel

A change of shot is a new starting pose, rather than continuous travel between two positions. Explicit camera cuts and large pose discontinuities reset presentation and motion history. This prevents interpolation through the jump and starts subsequent motion measurements from the new shot.

## 5. Attribute motion that reached the final camera

The observed score says how much the camera moved. Attribution asks which part of that movement came from an automatic source. A character can be in hit stun without moving the camera; a terrain probe can request a tilt that is later canceled. The classifier therefore uses contributions recorded where camera corrections and actor displacement are actually applied, then matches them against the final authored camera.

The integration needs four kinds of evidence:

| Evidence | Where to record it | How it is used |
| --- | --- | --- |
| Camera correction offset | After collision or shake modifies the intended camera pose. | Changes in the offset identify both the correction and its release. |
| Applied camera movement | After a terrain adjustment passes through smoothing and limits. | Only the surviving contribution is eligible for attribution. |
| Resolved character displacement | Where platforms, knockback, pulls, falls, and ordinary locomotion change the character’s position. | A short per-source history matches delayed camera following. |
| Camera ownership | Where the game selects an event or fixed-angle camera. | An automatic camera can attribute its observed motion directly. |

### Camera corrections

Collision and shake publish offsets from the intended camera pose. Their change between consecutive samples supplies the contribution for that update. A constant collision offset produces no motion; entering the obstruction and easing back out both do. Shake can contribute eye and focus offsets, roll, and FOV change.

A zero offset and a missing sample have different meanings. Publishing zero measures the release of a correction. A missing sample breaks that source’s history, so the next value establishes a new baseline instead of replaying an old correction.

Terrain adjustment needs a different measurement because the camera smooths and limits its requested pitch. Duskcone tracks the scalar pitch with and without the terrain contribution through the same smoothing step, then publishes the applied difference as eye-around-focus movement. A later camera override may still cancel it, so the final matching step checks that it survived. This tracks one contribution through the camera solver without rerunning the whole camera.

### Actor displacement and delayed following

The actor is the character being followed. Its movement may reach the camera a few updates later. Each source keeps a short budget of outstanding displacement, covering ordinary locomotion and automatic sources such as platforms, knockback, pulls, and falls. The history window accommodates camera-follow lag without leaving old movement eligible indefinitely. A camera cut clears these budgets along with the camera history.

Before matching actor following, subtract camera-center movement already explained by camera corrections. Otherwise a shake that moves the focus could consume a pending knockback impulse as well. The remaining center movement consumes matching displacement from the actor budgets, oldest first. Each matched portion is spent once and each unspent impulse expires independently. New displacement cannot prolong an older impulse’s lifetime.

Using center movement separates following from orbiting. Turning the camera around a stationary focus cannot spend actor displacement. Conversely, a moving focus can change the viewing direction even when the camera’s eye position stays fixed, so attribution retains both eye and center contributions.

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 220" role="img" aria-labelledby="evidence-title evidence-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="evidence-title">A bounded displacement-evidence window</title><desc id="evidence-desc">A displacement impulse enters the history. Delayed camera following consumes portions of the evidence. Any unspent evidence expires when its window elapses.</desc>
<path d="M40 105h670m-8-5 8 5-8 5"/>
<g class="paper-faint"><path d="M60 92v26M160 92v26M260 92v26M360 92v26M460 92v26M560 92v26M660 92v26"/></g>
<text x="60" y="140" text-anchor="middle" font-size="13">1</text><text x="160" y="140" text-anchor="middle" font-size="13">2</text><text x="260" y="140" text-anchor="middle" font-size="13">3</text><text x="360" y="140" text-anchor="middle" font-size="13">4</text><text x="460" y="140" text-anchor="middle" font-size="13">5</text><text x="560" y="140" text-anchor="middle" font-size="13">6</text><text x="660" y="140" text-anchor="middle" font-size="13">7</text>
<path d="M60 80V42m-5 7 5-7 5 7" class="paper-signal"/><text x="44" y="28" font-size="14" class="paper-signal">Displacement</text>
<path d="M260 82v-27M360 82v-27" class="paper-tracked"/><text x="230" y="39" font-size="14" class="paper-tracked">Camera follow spends evidence</text>
<path d="M660 66v28m-5-7 5 7 5-7"/><text x="575" y="50" font-size="14">Old impulse expires</text>
<path d="M60 169v10h600v-10" class="paper-faint"/><text x="360" y="203" text-anchor="middle" font-size="13">Each impulse has its own expiry. Updates shown schematically.</text>
</svg>
<figcaption>Figure 4. Displacement evidence survives camera-follow lag, but is consumed once and expires independently.</figcaption>
</figure>

When ordinary movement and automatic displacement point in the same direction, they share the observed following proportionally. For example, equal outstanding platform and player displacements each receive half of a smaller matching camera movement. The player’s share is consumed along with the platform’s, but only the platform share contributes to safety attribution. This keeps held input from canceling genuine automatic movement or giving it unlimited credit.

The history is bounded evidence matching, rather than a second camera simulation. Following beyond the history window receives no credit from the expired displacement. Disabling an actor source clears its pending safety evidence and records subsequent displacement with ordinary locomotion.

### Bound evidence by observation

Vector matching projects observed motion onto an evidence direction, then bounds its individual axis components by what survived in the observed motion. Projection alone could manufacture vertical evidence from forward movement when an intended vertical correction was canceled elsewhere in the camera process.

The safety score is bounded by the corresponding observed components:

$$
q_{\mathrm{safety}}=\max_j\min\bigl(a_j,q_j\bigr).
$$

Here a is the attributed score and q is observed motion. Translation, roll, and FOV scores use the matched deltas. Attributed rotation is measured by removing the matched eye-around-focus contribution from the final viewing direction. Bounding each score by its corresponding observation prevents a small automatic rotation from borrowing a large ordinary translation to cross the safety threshold.

An event-owned camera attributes its observed score directly; enabled fixed-angle camera ownership uses the same rule. Merely running an event does not establish that ownership.

<details>
<summary>The evidence-matching primitive</summary>

This helper from `src/dusk/openxr_portal_motion.hpp` projects movement onto the evidence direction, limits it to the available amount, then bounds each component by the observed movement. It is used for both camera contributions and actor-follow budgets.

```cpp
inline float match(float observed, float evidence) {
    return evidence > 0.0f ? std::clamp(observed, 0.0f, evidence)
                          : std::clamp(observed, evidence, 0.0f);
}

inline Vector match(Vector observed, Vector evidence) {
    const float length = evidence.length();
    if (length < 0.0001f) {
        return {};
    }
    const Vector direction = evidence * (1.0f / length);
    const Vector projected = direction * std::clamp(observed.dot(direction), 0.0f, length);
    return {match(observed.x, projected.x), match(observed.y, projected.y),
            match(observed.z, projected.z)};
}
```

</details>

## 6. Fast narrowing and gradual reopening

The aperture controller confirms sustained motion before selecting a target. It keeps an independent state for the observed and safety scores. Each enters at a normalized score of at least 1.0 for two consecutive samples. Once active, it remains active while its score exceeds 0.5. This difference between entry and exit thresholds is hysteresis; it avoids repeated activation around the entry boundary.

Confirmed ordinary motion selects the gameplay aperture. Confirmed safety motion takes priority and selects the smaller aperture. With neither active, the controller can reopen.

Requiring two samples rejects an isolated change of shot that immediately settles. It adds one camera-update interval after the first qualifying sample. Keeping the two confirmation states separate also prevents an ordinary-motion candidate from becoming a safety request on the strength of a single later attributed sample.

Aperture selection follows updates to the game camera; the visible transition updates at display cadence. The controller approaches its target exponentially:

$$
o_{n+1}=o_n+(o_{\mathrm{target}}-o_n)\bigl(1-e^{-k\Delta t}\bigr).
$$

Openness o ranges from 0 to 1, and Δt is the bounded elapsed presentation time. Narrowing uses a response rate of 10 per second; ordinary reopening uses 4 per second. Established cutscene context uses a slower reopening rate of 0.75 per second. These are exponential response rates, not fixed animation durations.

A safety request also starts a release hold. The ordinary hold is 0.5 seconds; sustained event context can extend it to 2 seconds. This keeps the portal from reopening during brief pauses within a moving sequence. Continued safety motion restarts the hold, and the hold takes precedence over the larger gameplay target as well as full expansion.

Presentation policies can retain an aperture independently of motion: targeting holds the gameplay size, and the fixed-cutscene option holds the safety size. Safety motion and its release hold take priority over targeting. These policies select a target on the same transition path described below.

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 210" role="img" aria-labelledby="response-title response-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="response-title">Rapid narrowing, a safety hold, and gradual reopening</title><desc id="response-desc">A schematic aperture-size curve closes rapidly after confirmed motion, stays small during motion and release hold, and expands gradually after the hold expires.</desc>
<path d="M50 25v140h655"/><text x="14" y="96" font-size="12" transform="rotate(-90 14 96)">aperture size</text>
<path d="M56 40h74c15 0 22 82 51 91h236c76 0 76-86 245-91" class="paper-signal" stroke-width="2.5"/>
<path d="M139 20v145M311 20v145M417 20v145" class="paper-faint" stroke-dasharray="4 5"/>
<text x="140" y="189" text-anchor="middle" font-size="12">motion confirmed</text><text x="310" y="189" text-anchor="middle" font-size="12">motion ends</text><text x="420" y="189" text-anchor="middle" font-size="12">hold ends</text>
<text x="202" y="109" font-size="13">safety target</text><text x="333" y="109" font-size="13">release hold</text><text x="485" y="74" font-size="13" class="paper-signal">gradual reopening</text>
</svg>
<figcaption>Figure 5. Rapid narrowing, a release hold, and gradual reopening. The curve illustrates the policy, rather than a fixed-duration animation.</figcaption>
</figure>

The asymmetry is deliberate: reduce exposure promptly when continuous motion begins, then restore the surrounding game view gradually. Confirmation rejects isolated changes, hysteresis stabilizes the moving/still decision, and the release hold bridges short pauses. The response rate controls how the visible aperture reaches its selected target.

## 7. From a spatial window to full VR

The aperture is a rectangle with a center, right/up basis, and dimensions in XR presentation space. Its projected boundary divides each eye’s image into gameplay inside and surroundings outside; this boundary defines the portal’s rendering mask. A nominal vertical angle calibrates the rectangle’s size at a chosen distance:

$$
H_0=2d\tan(24^\circ),\qquad W_0=\rho H_0,
\qquad s(\theta)=\frac{\tan(\theta/2)}{\tan(24^\circ)}.
$$

The base calibration is 48° vertical. Distance d defaults to 2 m; the aspect ratio ρ follows the UI’s widescreen or original framing. Safety and gameplay angles select scale factors relative to that base, with gameplay no smaller than safety. The default sizes are 42° for safety and 72° for gameplay.

At 2 m, a centered 42° aperture has a nominal height of about 1.54 m; a 72° aperture has a nominal height of about 2.91 m. These are authoring dimensions. A lean, an oblique view, or movement of the plane changes the apparent angle, as it would for a physical opening.

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 265" role="img" aria-labelledby="geometry-title geometry-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="geometry-title">A shared portal plane projected from two eye positions</title><desc id="geometry-desc">Two eyes see one fixed portal plane. A second head position projects the same plane obliquely. Nominal angle is a neutral-pose calibration rather than a screen-fixed boundary.</desc>
<path d="M42 214h425" class="paper-faint"/><circle cx="102" cy="123" r="10" class="paper-tracked"/><circle cx="121" cy="123" r="10" class="paper-tracked"/>
<path d="M330 36v171" class="paper-signal" stroke-width="4"/><path d="M112 123 330 36M112 123 330 207" class="paper-tracked"/>
<path d="M152 107a50 50 0 0 1 0 32" class="paper-tracked"/><text x="173" y="128" font-size="15" class="paper-tracked">θ</text>
<path d="M112 225h218m-7-5 7 5-7 5M119 220l-7 5 7 5"/><text x="221" y="252" text-anchor="middle" font-size="14">calibration distance d</text>
<text x="358" y="126" font-size="15" class="paper-signal">height H</text><text x="70" y="87" font-size="14" class="paper-tracked">two eyes</text>
<circle cx="154" cy="48" r="8"/><path d="M154 48 330 36M154 48 330 207" stroke-dasharray="5 5"/><text x="478" y="64" font-size="15">Same plane, different head pose</text><text x="478" y="91" font-size="13">Each eye projects its own boundary.</text><text x="478" y="116" font-size="13">The plane does not follow the gaze.</text>
</svg>
<figcaption>Figure 6. Both eyes project one shared plane. A different head position changes its apparent angle and perspective.</figcaption>
</figure>

### Opening the window into a surrounding view

A finite window in front of the viewer cannot become full VR merely by getting wider: it would still exclude the view behind them. The transition therefore has three coordinated parts. The opening grows, the plane approaches the neutral viewing origin—the authored reference position, rather than the live head position—and the stable room fades away. Once the room is gone, the spatial mask is disabled and gameplay is drawn in every direction.

<figure class="paper-figure">
<svg xmlns="http://www.w3.org/2000/svg" class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" viewBox="0 0 760 245" role="img" aria-labelledby="expansion-title expansion-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<title id="expansion-title">Opening a finite portal into a surrounding view</title>
<desc id="expansion-desc">Three stages show a window in a stable room, a growing window with fading surroundings, and a full surrounding view with no mask.</desc>
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<rect x="20" y="25" width="204" height="130" rx="3"/><g class="paper-faint"><path d="M20 58h204M20 91h204M20 122h204M54 25v130M88 25v130M122 25v130M156 25v130M190 25v130"/></g>
<rect x="67" y="57" width="110" height="66" fill="var(--panel, #fff)" class="paper-signal"/><path d="m77 106 23-19 26 13 25-27 17 33" class="paper-signal"/>
<path d="M240 90h33m-7-5 7 5-7 5"/>
<g class="paper-faint" opacity=".4"><rect x="290" y="25" width="204" height="130" rx="3"/><path d="M290 58h204M290 91h204M290 122h204M324 25v130M358 25v130M392 25v130M426 25v130M460 25v130"/></g>
<rect x="303" y="35" width="178" height="110" fill="var(--panel, #fff)" class="paper-signal"/><path d="m318 123 42-37 37 22 37-46 32 61" class="paper-signal"/>
<path d="M510 90h33m-7-5 7 5-7 5"/>
<g transform="translate(570 16) scale(1.55)" class="paper-signal"><path d="M12 34c0-22 72-22 72 0s-72 22-72 0" opacity=".6"/><path d="m7 39 5-5 5 5m62-10 5 5 5-5"/><circle cx="48" cy="24" r="6"/><path d="M37 39c0-13 22-13 22 0"/><text x="48" y="62" text-anchor="middle" font-size="11">360°</text></g>
<g text-anchor="middle"><text x="122" y="188" font-size="15">Spatial window</text><text x="122" y="213" font-size="13">Opaque surroundings</text><text x="392" y="188" font-size="15">Grow · approach · fade</text><text x="392" y="213" font-size="13">Gameplay extends beneath the room</text><text x="644" y="188" font-size="15">Unrestricted view</text><text x="644" y="213" font-size="13">Room and mask disabled</text></g>
</svg>
<figcaption>Figure 7. The plane grows and approaches while the room fades. Full VR uses an unrestricted mask, so the final view has no finite window boundary.</figcaption>
</figure>

Openness maps to geometric scale using a bounded hyperbola:

$$
s(o)=\min\left(\frac{s_{\min}}{\max(1-o,s_{\min}/s_{\max})},s_{\max}\right),
\qquad o_{\mathrm{gameplay}}=1-\frac{s_{\min}}{s_{\mathrm{gameplay}}}.
$$

The minimum scale is the safety aperture’s scale; the gameplay target selects its larger configured scale. Both motion targets and full expansion use one continuous size-and-distance path. The hyperbola makes the final part of that path grow rapidly beyond the ordinary aperture sizes. Scale is capped at 32, and room opacity follows a smooth fade between scales 8 and 24:

$$
\alpha_{\mathrm{room}}=1-S\left(\frac{s-8}{24-8}\right),
\qquad S(x)=3c^2-2c^3,\quad c=\operatorname{clamp}(x,0,1).
$$

The plane’s forward distance uses a smooth path toward the neutral origin and then behind it, ending at −2 m. It crosses the origin at 95% of the scale path, after the room has faded away. Disabling the mask before that crossing avoids making a plane intersecting or passing behind the viewer define the surrounding view. The path is authored relative to the presentation origin; it is not continually repositioned around the live head pose.

The maximum-size option stops expansion at the beginning of the room fade. It retains an opaque peripheral environment and a bounded opening even when the camera is still. Cutscene trim can additionally crop the plane’s vertical extent, reproducing the authored framing as spatial geometry.

## 8. Stencil, feather, and reprojection motion

The plane is projected separately for each eye into a stencil buffer, which marks pixels inside and outside the opening. That projected shape is the authoritative mask. An inset band—the feather ring—softens the boundary by blending gameplay and surroundings across it. The passes are arranged as follows:

| Pass | Responsibility |
| --- | --- |
| Aperture stencil | Clear stencil to exterior 0; write interior 1 with the projected plane. |
| Gameplay | With an opaque room or passthrough exterior, accept fragments inside stencil 1. |
| Room exterior | Draw the stable environment outside the aperture. |
| Feather ring | Blend the boundary in portal-local coordinates. |
| Room fade | Draw complete gameplay, then composite the disappearing room over the exterior and feather. |

During the room fade, gameplay must exist beneath the translucent surroundings. The stencil is rebuilt for the room overlay rather than used to remove the underlying game image. Passthrough instead uses projection-layer alpha to reveal the runtime’s physical-room layer below it. Once the exterior has disappeared, the unrestricted full-VR state skips the visible room and aperture passes.

### Conservative hardware scissoring

The union of both eyes’ projected portal corners supplies a conservative hardware scissor: a screen-aligned rectangle that limits rasterization. It intersects any narrower scissor already requested by the game. The per-eye stencil still determines the true boundary. Invalid or near-plane projections fall back to the full viewport rather than incorrectly clipping the view.

Scissoring is disabled during the translucent room fade. While the room is opaque, stencil rejection and scissoring can reduce gameplay fragment work, offsetting some of the added backdrop and feather work. A large union rectangle can cover much more screen area than the visible per-eye opening; its coverage is not an estimate of the actual aperture’s fragment coverage.

### A feather attached to the plane

Feather width is authored in degrees at the neutral calibration, then converted to an inset f proportional to the current portal height H:

$$
\frac{f}{H}=\frac{\tan(\theta_{\mathrm{feather}})}{2\tan(24^\circ)}.
$$

The resulting boundary is spatial: its apparent width changes with distance and viewing angle along with the plane itself. For current portal width W and local coordinates (x, y), gameplay coverage C is:

$$
\begin{aligned}
u&=\operatorname{clamp}\left(\frac{W/2-|x|}{f},0,1\right),\\
v&=\operatorname{clamp}\left(\frac{H/2-|y|}{f},0,1\right),\\
C&=S(uv).
\end{aligned}
$$

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 210" role="img" aria-labelledby="feather-title feather-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="feather-title">Portal-local feather and separate interior and exterior tests</title><desc id="feather-desc">An outer stencil rectangle contains an inset feather ring around the gameplay interior. The exterior is home stencil zero, the aperture stencil one. A corner uses the product of two edge coverages.</desc>
<rect x="24" y="24" width="332" height="162" rx="3" class="paper-faint"/>
<rect x="58" y="46" width="264" height="118" class="paper-signal"/><rect x="78" y="66" width="224" height="78" class="paper-signal" opacity=".45"/>
<path d="M58 46 78 66M322 46 302 66M58 164 78 144M322 164 302 144" class="paper-signal"/>
<text x="190" y="111" text-anchor="middle" font-size="16">gameplay · stencil 1</text>
<path d="M356 31h25"/><text x="393" y="36" font-size="14">home · stencil 0</text>
<path d="M322 59h59" class="paper-signal"/><text x="393" y="64" font-size="14" class="paper-signal">projected plane boundary</text>
<path d="M312 149h69" class="paper-signal"/><text x="393" y="155" font-size="14" class="paper-signal">inset ring · portal-local distance</text>
<text x="393" y="190" font-size="13">At a corner: smoothstep(u × v)</text>
</svg>
<figcaption>Figure 8. A spatial ring blends between gameplay and surroundings. Multiplying the two edge coverages makes the corners continuous.</figcaption>
</figure>

The rectangular ring consists of eight non-overlapping triangles. This avoids double blending at corners; the product of edge coverages avoids a visible switch between the nearest horizontal and vertical edges. A shallow inward bevel places the ring’s inner edge behind the stencil plane while preserving the outer boundary. That depth preserves some feather when viewing the portal nearly edge-on, where a completely flat ring would collapse into a thin line. The stencil boundary itself remains flat.

The room’s feather opacity is proportional to one minus coverage. Passthrough uses the complementary projection alpha. A small 0.25° feather remains at the minimum setting to soften aliasing at the edge.

### Motion at the boundary

Application SpaceWarp generates intermediate display frames by extrapolating rendered color using depth and motion vectors. At the portal edge, moving gameplay meets a stationary room, so the feather needs a corresponding transition between their motion fields. Duskcone uses a wider 10° feather calibration with SpaceWarp to give that transition room to blend.

A motion-feather pass blends away gameplay motion in proportion to visible room coverage while retaining the projected boundary’s tracking-space motion. Current and previous eye projections supply that boundary motion.

This is an approximate transition between the two motion fields, but in practice it suppresses the most noticeable boundary artifacts in SpaceWarp’s extrapolated frames.

The camera-cut reset described earlier also invalidates the previous-frame data used for extrapolation. The game’s interface is submitted in its own composition layer, separate from the stereo scene whose motion is being extrapolated. Color, depth, and motion need to describe the same portal composition, including its transition to full VR.

## 9. A stable room within a standalone rendering budget

The exterior needs a stable reference that fits both the game’s atmosphere and the headset’s frame budget. Link’s House supplies a room from the game’s own assets. In our experience it grounds the viewer less strongly than Daydream’s grid: the moving gameplay image can make the stationary house seem to move with it. Passthrough has provided the strongest grounding, at the cost of in-game atmosphere and additional runtime overhead.

The virtual room can be drawn live or reused from captured images. A **cubemap** stores six views covering the surrounding directions. An **RGB-D cubemap** stores color and depth, allowing the captured room to be approximately reprojected from a new eye position. These environment paths share the same aperture geometry, with different rendering costs:

| Environment path | Head-tracking response | Rendering tradeoff |
| --- | --- | --- |
| Live room | Render the room from the current stereo eyes. | Direct geometric response, with a second scene to draw each frame. |
| RGB-D room cache | Reproject captured color and depth from the current eyes. | A backdrop sampling pass plus periodic capture; translated views are approximate. |
| Flat cubemap | Select captured color by current ray direction. | Rotation responds directly; translated-view parallax is absent. |
| Passthrough | The runtime reconstructs the physical room. | Strong physical reference with runtime camera/reconstruction cost. |

Drawing a second complete stereo scene is a significant cost on a standalone headset. Duskcone’s normal virtual-room path therefore uses **cached per-eye RGB-D cubemaps**. Their capture anchors—the eye positions from which the room was captured—and current eye poses belong to the stable environment’s reference frame, independently of the game camera. The captures use visibility culling for their own six directions. This keeps the room responsive to head tracking while spreading its scene rendering over multiple frames.

<details>
<summary>RGB-D cache integration and sampling</summary>

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 210" role="img" aria-labelledby="cube-title cube-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="cube-title">Capture, cache, and reprojection of a static room</title><desc id="cube-desc">A static room feeds a six-face per-eye color and depth snapshot. The snapshot and current tracked eyes feed backdrop sampling, which is drawn outside the portal.</desc>
<rect x="14" y="54" width="146" height="85" rx="5"/><path d="m39 82 22-18 22 18v31H39Z"/><text x="100" y="111" text-anchor="middle" font-size="14">Room</text>
<path d="M160 97h40m-7-5 7 5-7 5"/>
<g class="paper-signal"><rect x="211" y="64" width="32" height="32"/><rect x="243" y="64" width="32" height="32"/><rect x="275" y="64" width="32" height="32"/><rect x="307" y="64" width="32" height="32"/><rect x="243" y="32" width="32" height="32"/><rect x="243" y="96" width="32" height="32"/></g>
<text x="274" y="157" text-anchor="middle" font-size="13">color + depth · per eye</text>
<path d="M339 80h46m-7-5 7 5-7 5"/>
<rect x="390" y="54" width="158" height="85" rx="5"/><text x="469" y="87" text-anchor="middle" font-size="14">Backdrop sampling</text><text x="469" y="113" text-anchor="middle" font-size="12">current ray + captured depth</text>
<path d="M469 183v-44m-5 7 5-7 5 7" class="paper-tracked"/><text x="469" y="205" text-anchor="middle" font-size="13" class="paper-tracked">tracked eye pose</text>
<path d="M548 97h34m-7-5 7 5-7 5"/>
<rect x="590" y="54" width="151" height="85" rx="5"/><rect x="635" y="78" width="62" height="38" class="paper-signal"/><text x="666" y="159" text-anchor="middle" font-size="13">portal exterior</text>
</svg>
<figcaption>Figure 9. Cache integration: per-eye room captures and current eye poses supply the portal’s stable exterior.</figcaption>
</figure>

Each eye has six captured color/depth faces. For a current eye ray, the shader samples source depth, reconstructs a point relative to the capture anchor, and estimates where that surface lies along the current ray. The corrected source direction supplies the color lookup. Additional depth lookups can refine the estimate; the default uses one.

For a unit ray r, capture anchor a, current eye e, and captured radial depth D, the first correction is:

$$
\begin{aligned}
\mathbf p_0&=\mathbf a+D(\mathbf r)\mathbf r,\\
t_0&=\max\bigl((\mathbf p_0-\mathbf e)\cdot\mathbf r,\varepsilon\bigr),\\
\mathbf d_{\mathrm{source}}&=\mathbf e+t_0\mathbf r-\mathbf a.
\end{aligned}
$$

The cubemap is addressed by the direction of d; the implementation uses ε = 0.001 m to keep the estimate in front of the eye. This corrects the lookup toward the surface implied by captured depth without rendering that surface again.

Rotation can be handled by changing cubemap direction alone. Translation needs depth to approximate parallax. This inverse sampling method reuses visible surfaces from the capture; it cannot invent surfaces that were hidden behind them.

</details>

### Position-triggered re-capture

A new capture is requested when the head moves more than **15 cm from the capture position**. Rotation alone can reuse the cubemap. Capture is incremental, refreshing a cubemap face for both eyes per step. The eye poses are frozen for the pending batch, so all its faces share coherent capture origins.

The completed snapshots remain in use while a second buffer pair receives the replacement. Publishing only the complete set avoids mixing faces captured from different positions. A smaller refresh distance reduces the translation the cache must approximate but requests captures more often; a larger distance saves capture work while allowing more reprojection error. The threshold and room geometry need to be chosen together.

### Choosing the room

A static interior with broad surfaces, comfortable viewing distances, and few thin occluders is a good fit for the cache. Leaning can reveal surfaces that were hidden from the capture, producing disocclusion artifacts. Such artifacts would make the room a distracting peripheral reference, so the environment’s geometry is part of the comfort design.

Link’s House fits these criteria well and retains the game’s atmosphere.[^house-ladder] The cache still adds per-pixel backdrop sampling and feathering, but avoids recurring full-scene rendering. Live rendering remains available when direct geometric response is worth that cost.

Passthrough places the runtime’s reconstruction layer beneath an alpha-enabled projection layer. Transparent pixels outside the aperture reveal the physical room; the feather blends that transparency into gameplay. The feature runs while those pixels are exposed and can pause when full VR completely occludes it. An unavailable passthrough path falls back to the virtual room.

## 10. Face Guard: nearby geometry

The portal limits exposure to camera motion. **Face Guard addresses geometry that crowds the viewer’s face.** A third-person camera and freely tracked head can place a wall, character, or prop very close to an eye. The flat camera’s framing and collision rules do not necessarily leave suitable clearance for every stereo viewpoint.

Face Guard changes the visibility of nearby geometry without moving the camera or changing collision. Distance determines coverage—the fraction of pixels to retain. A repeating 4 × 4 Bayer pattern supplies a threshold for each pixel, so lower coverage progressively discards more fragments:

$$
C(r)=\max\left(C_{\min},S\left(\frac{r^2-r_{\mathrm{start}}^2}{r_{\mathrm{end}}^2-r_{\mathrm{start}}^2}\right)\right),
\qquad \text{discard if }C(r)\leq B(\mathrm{pixel}).
$$

Distance r is measured from each eye; r_start and r_end delimit the fade band. The minimum coverage C_min is zero for the normal fade. Squared distance avoids a square root and makes the transition nonlinear in distance. Ordered discard keeps the existing opaque rendering model, avoiding the sorting requirements of general translucent geometry.

<figure class="paper-figure">
<svg class="paper-diagram" style="color:var(--paper, #202020);background:var(--panel, #fff)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 245" role="img" aria-labelledby="guard-title guard-desc" fill="none" stroke="currentColor" stroke-width="1.5">
<style>.paper-diagram text{font-family:"Fira Sans",system-ui,sans-serif;fill:currentColor;stroke:none}.paper-signal{color:var(--gold, #936724)}.paper-tracked{color:var(--blue, #32667d)}.paper-faint{opacity:.4}</style>
<title id="guard-title">Face Guard distance bands for nearby geometry</title><desc id="guard-desc">A side-view head sits inside a fully hidden inner radius, followed by a dithered transition band and visible geometry outside the outer radius. Standard radii are one half meter and one meter.</desc>
<path d="M55 183h657" class="paper-faint"/>
<circle cx="95" cy="124" r="27" class="paper-tracked"/><path d="M71 107h48v29H71" class="paper-tracked"/>
<path d="M247 42v139M399 42v139" stroke-dasharray="5 6"/>
<path d="M127 124h570"/>
<text x="174" y="71" text-anchor="middle" font-size="14">hidden</text><text x="323" y="71" text-anchor="middle" font-size="14" class="paper-signal">dither transition</text><text x="548" y="71" text-anchor="middle" font-size="14">visible geometry</text>
<g class="paper-signal"><path d="M266 90v71M285 103v46M305 90v71M324 96v58M344 90v71M364 95v59M383 90v71" stroke-dasharray="3 7"/></g>
<path d="M520 90v71M580 90v71M640 90v71"/>
<text x="247" y="205" text-anchor="middle" font-size="13">r_start = 0.5 m</text><text x="399" y="205" text-anchor="middle" font-size="13">r_end = 1 m</text>
<text x="407" y="234" text-anchor="middle" font-size="12">Standard calibration; schematic distance axis. Eligibility varies by draw.</text>
</svg>
<figcaption>Figure 10. Face Guard’s hidden inner region, dithered transition, and unchanged geometry beyond the outer radius.</figcaption>
</figure>

The setting controls the outer radius; the normal inner radius is half that value. Standard uses a 0.5–1 m band, Comfortable uses 1–2 m, and Off disables the gameplay fade.

Materials can opt out or use a softer fade. The soft mode uses a smaller band—from 10% to 50% of the configured radius—and retains a minimum coverage of 1/16. This lets selected surfaces remain faintly present very close to the viewer.

Removing foreground fragments can reveal a wall’s reverse side, an object’s interior, or geometry originally hidden by that surface. It can also weaken occlusion and increase fragment work. Strength is therefore adjustable: the feature trades some near-field visual integrity for a less crowded stereo view.

## 11. Integration and tuning

### Integration sequence

1. **Establish the reference frames.** Compose the authored camera with the tracked eye poses for gameplay rendering, after any camera interpolation. Update draw culling to cover the composed stereo view. Place the portal and peripheral environment in the shared XR reference space, applying system recentering consistently to both.
2. **Implement a fixed aperture first.** Project one shared plane into both eyes and use the stencil/pass sequence above. Start with a simple live environment or passthrough. Verify the interior, exterior, and feather from both eyes, including oblique views and near-plane crossings. Keep the hardware scissor conservative and restore its state after gameplay.
3. **Measure ordinary camera motion.** Sample the authored camera independently of tracked viewing, normalize its deltas using the actual sample interval, and confirm sustained movement. Clear history at camera cuts; a settled cut should not act like continuous travel. The thresholds above are starting values for tuning, not requirements for another game.
4. **Add attribution and safety selection.** Publish the correction-offset, applied-movement, resolved-displacement, and ownership evidence described in section 5. Match it against the final camera, removing correction-center motion before consuming delayed-follow budgets. Keep ordinary locomotion in those budgets so mixed movement is shared rather than vetoed, and give the resulting safety score its own confirmation state.
5. **Connect the aperture controller.** Update openness at display cadence, then derive plane scale, distance, feather inset, and room opacity. Preserve the complete gameplay image underneath the fading room. Switch to the unrestricted mask once the exterior disappears, and keep the maximum-size option on the bounded part of the transition.
6. **Add proximity and temporal rendering support.** Apply Face Guard in eye-relative distances, converting units consistently and choosing eligible materials. If the renderer uses motion extrapolation, integrate the feather’s motion/depth treatment and reset temporal history at cuts. Check the complete composition in extrapolated frames as well as native frames.
7. **Fit the environment to the frame budget.** Introduce the cubemap cache if live rendering is too costly. Freeze capture poses for each batch, retain the completed set until its replacement is ready, and tune the refresh distance for the chosen room.

### Tuning parameters

Aperture sizes, distance, feather width, environment, Face Guard, and motion-source policies are adjustable. Maximum-size and cutscene options can retain a stable peripheral reference more consistently. Presets provide starting points; individual settings remain available.

| Preset | Safety / gameplay angle | Face Guard outer radius |
| --- | --- | --- |
| Extra Comfort | 30° / 54° | 2 m |
| Standard | 42° / 72° | 1 m |
| Uncomfortable | 60° / 108° | Disabled |

Extra Comfort selects passthrough on Android when available. The wider preset disables optional safety-source triggers; event-camera ownership remains part of attribution. Presets therefore change the environment and source policy as well as the two aperture sizes. Maximum-size and fixed-cutscene behavior remain separate settings.

Thresholds, confirmation, release holds, transition rates, and the plane’s expansion path should be tuned together. A nominal angle is not a constant screen-space field of view once the plane moves or the viewer leans. A response rate is not a fixed completion time, especially after the hyperbolic scale mapping.

### Check the boundaries between subsystems

The cases below exercise the places where camera evidence, aperture policy, and rendering meet. They are useful checks while integrating the system, without depending on Duskcone’s particular camera modes.

| Case | Expected behavior |
| --- | --- |
| Hold the game camera still, then turn, lean, and recenter. | The stereo view responds to tracking, objects beyond the flat camera frame remain visible, and the portal and room share the reference-space change. |
| Cut between still shots. | No interpolated travel is created between shots, and attribution from the previous shot is cleared. |
| Move with a constant collision offset; then enter and leave an obstruction. | The constant offset adds no safety motion; its approach and release supply measured contributions. |
| Request terrain tilt that is clamped or canceled later in the camera solver. | Only the adjustment that reaches the final camera can contribute. |
| Hold movement input during knockback or platform travel. | Ordinary and automatic displacement share following; held input does not erase the automatic share. |
| Orbit around a stationary focus after an actor impulse. | Orbit does not consume the pending follow budget. Later ordinary movement cannot reuse evidence that was spent or expired. |
| Expand the portal while looking obliquely or away from it. | The spatial feather remains visible at oblique angles; full VR eventually has no finite aperture boundary. Unsafe scissor projections fall back conservatively. |
| Lean far enough to request a new room capture. | All pending faces use the frozen capture poses; the completed set stays visible until its replacement is ready. |
| Approach nearby geometry with Face Guard enabled. | Normal, soft, and exempt materials follow their selected policies, including their depth and motion rendering. |
| Inspect the portal edge in extrapolated as well as native frames. | Color, depth, and motion describe the same boundary; a smooth color blend alone does not establish correct temporal behavior. |

The attribution and camera-cut tests in the source map cover the corresponding numerical cases. Spatial composition and the room’s grounding effect also need to be inspected in the headset with the chosen environment and settings.

## Appendix A. Design observations

These are personal observations and ideas behind the system, rather than results of a formal study.

### Tolerance and wanting to return

Being able to tolerate a headset is not necessarily the same as finding it inviting to use. A player may stop feeling overtly sick while still finding the experience subtly demanding. That burden could show up as shorter sessions or a headset left unused, without a clear sensation of discomfort during play.

This is one reason to treat comfort as part of the presentation for everyone, rather than only as an accommodation for new VR users. The intended audience includes someone with an old Quest on a shelf who wants to sit down and play. Settings should let the experience suit that person without asking them to acquire a particular tolerance first.

### Attention and motion source

During traversal, combat, or a directed cutscene, attention is often concentrated on the action. A stable peripheral view may cost relatively little of that moment’s experience. When the game is still, the surrounding world becomes more available to explore. Opening into full VR at those moments follows that distinction.

Predictability matters too. Development experience suggested that self-initiated movement could support a larger aperture than unexpected corrections or forced movement. That is why the motion policy gives ordinary gameplay a wider view and looks for evidence before selecting the smaller aperture.

These observations motivate adjustable presentation. My own tolerance increased while working on the port, making it harder to judge the experience of a less frequent VR user.

## Implementation source

The camera and motion policy live in Dusklight’s game integration; the projection and rendering passes live in the Aurora backend. These entry points connect the descriptions above to the implementation. The tuning values belong to this implementation, while the evidence publishers are the main integration work for another game.

| Area | Source and entry points |
| --- | --- |
| Stereo composition, culling, and scale | `src/dusk/openxr_camera.cpp`: `compose_eye_camera`, `compose_stereo_clipping_camera`, `apply_active_stereo`, `ActiveStereoCameraScope`, `last_stereo_frustum_contains`, and `last_stereo_frustum_intersects`. Draw-culling integration in `src/m_Do/m_Do_lib.cpp`. Gameplay scale in `src/dusk/openxr.hpp`; room scale in `src/dusk/openxr_portal_backdrop.cpp`. |
| Policy and geometry | `src/dusk/openxr.cpp`: `PortalTuning`, `sample_portal_camera_motion`, `sample_portal_camera`, `target_openness`, `WorldPortalScissorScope`. Anchored-plane calibration in `extern/aurora/lib/xr/openxr_ui.cpp`. |
| Attribution and evidence publication | `src/dusk/openxr_portal_motion.hpp`: `match`, `Correction`, `FollowDisplacements::consume`, `TerrainPitch`, `attribute`, `ComponentScores`, and `Confirmation`. Collision, terrain, and shake publishers in `src/d/d_camera.cpp`; resolved actor-displacement publishers in `src/d/actor/d_a_alink.cpp`. |
| Discontinuities | `src/dusk/openxr_camera_cut.hpp`, `src/dusk/interp/camera.cpp`, and `src/dusk/interp/frame_interpolation.cpp`. Regression coverage in `tests/openxr_camera_cut_test.cpp` and `tests/openxr_portal_motion_test.cpp`. |
| Runtime eye poses | `extern/aurora/lib/xr/openxr_frame.cpp`: views located at predicted display time. |
| Update cadence and history | `src/dusk/game_clock.h`: 30 Hz camera samples before presentation interpolation. `FollowDisplacements` keeps six updates (200 ms). `sample_portal_camera` bounds presentation Δt to 1/240–0.1 seconds. |
| Stencil and pass composition | `extern/aurora/lib/xr/openxr_backdrop.cpp`: portal preparation, projected scissor, room overlay, and passthrough feather. |
| Portal and reprojection shaders | `extern/aurora/lib/gfx/rgbd_reprojection.cpp`: `project_portal`, `ring_vertex`, `feather_coverage`, snapshot sampling, and motion feather. |
| Room capture and refresh | `src/dusk/openxr_portal_backdrop.cpp`; `extern/aurora/lib/xr/openxr_cubemap_snapshot.cpp`; refresh radius in `extern/aurora/lib/xr/openxr_state.hpp`. |
| Passthrough | `extern/aurora/lib/xr/openxr_passthrough.cpp`, with layer composition in `openxr_frame.cpp`. |
| Face Guard | `src/dusk/openxr.cpp`: `gameplay_near_geometry_fade`; eye-view parameters in `src/dusk/openxr_camera.cpp`; discard coverage and soft mode in `extern/aurora/lib/gx/shader.cpp` and `shader_info.cpp`. |
| Settings and presets | `src/dusk/settings.cpp`, `src/dusk/ui/settings.cpp`, `src/dusk/ui/preset.cpp`, and `preset.hpp`. |

Further project notes: `docs/openxr-camera-comfort.md` and `docs/openxr-architecture.md`.

[^1]: Rob Jagnow, Google Daydream Labs. [*Locomotion in VR*](https://blog.google/products-and-platforms/products/google-ar-vr/daydream-labs-locomotion-vr/), 6 June 2017.
[^2]: Google VR Elements. [*Tunneling demo*](https://developers.google.com/vr/elements/tunneling). Iris/cage terminology, implementation controls, and stable-background guidance.
[^house-ladder]: A nearby ladder is the main exception. It has no distinct object identifier, so it is removed from the capture with a distance-based fade, reusing the near-geometry fade machinery described under [Face Guard](#10-face-guard-nearby-geometry).
