Summary
With Instanced Stereo and MegaLights enabled, local lights in the right eye are missing or wrong per screen tile.
Example: a point light inside a carved pumpkin lights the pumpkin’s face and the ground in front of it in the left eye.
In the right eye the face stays dark and the light pool on the ground is missing. Shadows and fog lighting also differ
between the eyes. The result depends on the view, because each right-eye tile receives the light list of the same
tile in the left eye’s image.
What Type of Bug are you experiencing?
Rendering (Graphics / Niagara)
Steps to Reproduce
- Place a point light (MegaLights on) inside a closed mesh with an opening, e.g. a hollow sphere or box with a
window cut into it. The light should only escape through the opening. - Instanced Stereo ON. Package and run with
-emulatestereo(or play in a headset). - Compare the two halves of the window. In the right half the inside of the mesh and the light pool through the
opening are dark or missing. Moving the view changes which right-eye areas are affected.
Expected Result
Both eyes should match
Observed Result
Both eyes do not match
Affects Versions
5.8
Platform(s)
OpenXR
Upload an image
Additional Notes
Root cause (5.8 source)
1. Light grid cells (main cause)
LightGridInjection.cpp: under single-pass stereo the secondary view writes its light-grid cells into the
PRIMARY view’sNumCulledLightsGrid/CulledLightDataGridbuffers. Its cells are stored after the primary’s, at
ForwardLightStruct.CulledBufferOffsetISR(=MaxNumCells).LightGridCommon.ushComputeLightGridCellIndex(..., EyeIndex)only addsCulledBufferOffsetISRwhen
EyeIndex != 0.- These passes run separately per view, also for the secondary view, but hard-code
EyeIndex = 0. So in the right
eye they read the LEFT eye’s per-tile light lists:MegaLights/MegaLightsSampling.usf(~line 316):const uint EyeIndex = 0; // TODOMegaLights/MegaLightsVolumeSampling.usf(~line 270):const uint EyeIndex = 0;StochasticLighting/StochasticLightingTileClassification.usf(~line 625):const uint EyeIndex = 0;VolumetricFog.usf(~line 1055) andVolumetricCloud.usf(~line 1258):ComputeLightGridCellIndex(..., 0)
- The Virtual Shadow Map shaders do this correctly: they use
uint EyeIndex = View.StereoPassIndex;
(VirtualShadowMapLightGrid.ush,VirtualShadowMapPageMarking.usf). - Note: with Instanced Stereo enabled,
ShaderCompiler.cppdefinesINSTANCED_STEREO=1for every non-ray-tracing
shader, global compute shaders included. So these shaders do compile theEyeIndexbranch; they just always pass 0.
2. Light positions (secondary cause, small error)
LightGridInjection.cpp(~line 1682): the secondary view also reuses the primary’sForwardLightBuffer. Its light
positions are relative to the primary’s PreViewTranslation, with the difference stored in
PreViewTranslationOffsetISR.GetForwardLightData_Internal/GetLightViewDatainLightGridCommon.ushonly add that offset for
EyeIndex != 0. MegaLights (GetForwardLightData(i, 0)everywhere),VolumetricFog.usfand
TranslucentLightInjectionShaders.usfpass 0, so right-eye lights are shifted by the eye separation.
Suggested fix
Pass View.StereoPassIndex as EyeIndex in the per-view passes listed above, as the VSM shaders already do.
Alternatively, give the secondary view its own light grid and light buffer.
Workaround we verified
A shader header included right after LightGridCommon.ush in the affected shaders. It wraps
ComputeLightGridCellIndex, GetForwardLightData, GetLocalLightData and the *FromGrid variants. When a call
passes EyeIndex == 0 but View.StereoPassIndex != 0, it adds CulledBufferOffsetISR / PreViewTranslationOffsetISR.
This fixes the right eye in a packaged build (-emulatestereo) and in the headset, with no visible performance cost.
