When r.SSR.Stencil is enabled, there is wrong logic observed.

(This is a translation of a [Japanese [Content removed] by Kamikawa.)

Expected Behavior:

When r.SSR.Stencil is enabled, it is expected to mark the areas that should not be calculated in the stencil buffer. After that, the main SSR process begins, and the heavy pixel shader processing is supposed to be correctly culled by Early Stencil.

Explanation of the logic that is supposedly wrong in the code:

It’s about the behavior of StencilSetup in Engine\Shaders\Private\SSRT\SSRTReflections.usf.

Inside void ScreenSpaceReflectionsStencilPS, there is the following code.

#if !defined(TILE_COMPUTE_SSR)
if (RoughnessFade > 0.0 && bNoMaterial)
{
// we are going to compute SSR for this pixel, so we discard this
// pixel shader invocation to not overwrite the stencil buffer and
// therefore execute ScreenSpaceReflectionsPS() for this pixel.
discard;
}
#endif

// we are not going to compute SSR for this pixel, so we clear the color
// since ScreenSpaceReflectionsPS() won't be executed in this pixel.
OutColor = 0;
#if SSR_OUTPUT_FOR_DENOISER
OutClosestHitDistance = DENOISER_INVALID_HIT_DISTANCE;
#endif


According to the comments, the output part corresponds to the area where SSR is NOT computed. However, the actually output stencil looks like the attached image below. So the result appears to be reversed. Also, areas with high roughness is supposed to be discarded, so it is strange that everything except the sky becomes perfectly clean in this state.

[Image Removed]

The image below shows the stencil test during the Actual SSR Pass. In reality, the discarded areas correspond to the sky area that was marked as output.

[Image Removed]

Now let me show the source for the RenderState setup when calling the Main SSR Pass.

Source:

Engine\Source\Runtime\Renderer\Private\ScreenSpaceRayTracing.cpp

if (SSRStencilPrePass)
{
  // Clobbers the stencil to pixel that should not compute SSR
  GraphicsPSOInit.DepthStencilState =
    TStaticDepthStencilState<false, CF_Always, true, CF_Equal,
    SO_Keep, SO_Keep, SO_Keep>::GetRHI();
}

In this code, CF_Equal is set for the stencil test. However, since the areas that should NOT be computed are marked in the stencil, CF_NotEqual should be there. However, if simply reversing this, the sky would become the computation target. So, you can guess the problem already exists at the StencilSetup stage.

In the ScreenSpaceReflectionsStencilPS shader, the side that is NOT marked in the stencil and gets discarded mid-way is actually the area that is computed in the ScreenSpaceReflections shader. Let me show the conditions again from earlier in the code:

const float Roughness = GetRoughness(GBuffer);
const bool bNoMaterial = GBuffer.ShadingModelID == 0;

#endif // SUBTRATE_GBUFFER_FORMAT==1

const float RoughnessFade = GetRoughnessFade(Roughness);

#if !defined(TILE_COMPUTE_SSR)
if (RoughnessFade > 0.0 && bNoMaterial)
{
  discard;
}
#endif

Since bNoMaterial represents Unlit, the area should not be computed.

if (RoughnessFade > 0.0 && bNoMaterial)

If fixing the stencil test as intended, the current condition would let Unlit materials with high RoughnessFade become the computation target, which is incorrect.

(To be continued)

[Attachment Removed]

再現手順
Extract the attached SSR_StencilTest.zip into a UE 5.6 project.

Launch the project.

Enter the following console command:

r.SSR.Stencil 1

You can then observe the issue.

[Attachment Removed]

(Continued)

Also, on the ScreenSpaceReflections shader, the discard logic is written as follows:

float RoughnessFade = GetRoughnessFade(Roughness);
// Early out. Useless if using the stencil prepass.
BRANCH if (RoughnessFade <= 0.0 || bNoMaterial)
{
    return;
}

So, the condition in ScreenSpaceReflectionsStencilPS needs to be completely inverted.

As such, the correct condition should be:

if (RoughnessFade > 0.0 && bNoMaterial == false)
{
    discard;
}

This may be confusing, but since the discarded side is the area that will be computed by SSR, Unlit materials should pass this condition, and areas with RoughnessFade <= 0 should pass, as well.

Since RoughnessFade applies only to non-Unlit materials, this condition becomes logically good.

After applying the corrected code, the resulting stencil looks like the attached image below. You can now clearly see that the sky and high-roughness areas are correctly marked. These painted areas are the ones that should be discarded.

[Image Removed]The image below is the stencil test on the ScreenSpaceReflections side (after fix).

[Image Removed]Compared to the original code, you can confirm that the regions failing the stencil test have changed.

I think this is the expected stencil test result.

Additional Information:

I have confirmed that the same code exists in version 5.7, as well.

I’d appreciate it if you could look into this issue.

[Attachment Removed]

Thank you for reporting this issue and providing a detailed explanation and fix. I’ve created the following issue for tracking Unreal Engine Issues and Bug Tracker (UE\-358749\). Epic will be on holiday break starting next week (Dec 22, 2025) and ending Jan 5, 2026 and there will be no responses from Epic on public or private tickets during that time, though you may receive replies from the community on public tickets.

We wish you happy holidays!

[Attachment Removed]