Repository navigation
[Bugfix] Shade each XR eye from its own position - #1285
Merged
untoldengine merged 2 commits intoOct 10, 2026
Merged
Conversation
added 2 commits
October 9, 2026 18:05
In XR the lighting, model, batch, transparency, sky and render-extension passes took their view vector from CameraComponent.localPosition, which UntoldEngineXR sets to the head centre once a frame for streaming (setXRCameraWorldPosition). Both eyes were shaded as if seen from between them, so specular highlights and Fresnel had no stereo disparity and read as painted on the surface up close. renderXR now stores the position of the eye it draws, taken from that eye's view matrix, in renderInfo.xrEyeCameraPosition, and the shading passes read shadingCameraPosition(_:), which returns it (through the scene root, like before) or the camera's position outside XR; the non-XR draw clears it. Streaming, LOD, octree and culling keep the head centre. Gaussian splats keep it too: their colour is evaluated once per frame for both eyes in the shared preprocess.
SwiftFormat 0.60.1 (the version CI pins) wants a doc comment on a lone property declaration; the lint job failed on it.
miogds
force-pushed
the
bugfix/xr_per_eye_shading
branch
from
October 9, 2026 16:14
90ed292 to
534d3e2
Compare
miogds
marked this pull request as ready for review
October 9, 2026 16:14
untoldengine
approved these changes
Oct 9, 2026
miogds
added a commit
to miolabs/UntoldEngine
that referenced
this pull request
Oct 10, 2026
Sync fork develop with upstream: per-eye shading, LOD hidden parts, IBL diffuse (untoldengine#1285, untoldengine#1324, untoldengine#1329)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In XR every shading pass used the head-centre camera position for its view vector, so both eyes were lit as if seen from between them. Specular highlights and Fresnel then carry no stereo disparity: up close they read as painted onto the surface instead of sitting at the depth of a reflection. Each eye is now shaded from its own position.
Checked on an Apple Vision Pro through the editor's Mac-rendered preview (see Verification).
Cause
UntoldEngineXR.syncStreamingCameraPositionsetsCameraComponent.localPositionto the headset centre once a frame (setXRCameraWorldPosition). That is right for streaming, where 3 cm do not matter. But the light pass and the other shading passes read the same value throughSceneRootTransform.effectiveCameraPosition(cameraComponent.localPosition), whilerenderXRonly updated each eye'sviewSpace.Changes
renderXRstores the position of the eye it draws, taken from that eye's view matrix (eyePosition(fromViewMatrix:)), inrenderInfo.xrEyeCameraPosition. The non-XRdraw(in:)clears it.shadingCameraPosition(_:)returns that eye position, or the camera's own position outside XR, folded through the scene root as before. It replaces the 17 shading reads: the light pass, the model and batch uniforms of the geometry, shadow-free, transparency and debug passes, the sky, and render-extension surfaces.Verification
New
ShadingCameraPositionTests(4): eye position from a rotated, translated view matrix; camera position outside XR; the eye, not the head centre, in XR; the scene root applied to the eye position.swift test --filter UntoldEngineTestson the rebased branch: 1,673 passed, 2 skipped (the external render extension package test run from a copy namedUntoldEngine, where it passes). Strict-concurrency build: 0 warnings.xcodebuild -scheme UntoldEngine -destination 'generic/platform=visionOS Simulator': build succeeded.xcodebuild -scheme UntoldEngine -destination 'generic/platform=visionOS Simulator': build succeeded.No new SwiftFormat findings in the changed files.
Checked on an Apple Vision Pro (the editor's preview, the Mac rendering both eyes), on the glass and paint of parked cars at arm's length with an HDR environment and a directional light, closing each eye in turn and switching the eye position off and on in the same build. With the head-centre position the reflection looks painted onto the glass; with each eye's own position it sits at its depth, and at some angles the two eyes see it slightly differently, as on real glass. Nothing else differed between the two runs.