Skip to content

Prefiltered cubemap shows a seam between the bottom and back faces in TestRadiancePrefilteringGgx #3328

Description

@sasvdw

Release Type: GitHub

Version: master (ed6f739f2), and every gold image back to 2018

Platform(s): Windows. Not backend specific — the same numbers appear on Direct3D11 WARP, Direct3D12 WARP and NVIDIA hardware.

Describe the bug

@Ethereal77 noticed a clear seam across the bottom and back faces of the prefiltered cubemap in TestRadiancePrefilteringGgx, and found the same seam in gold images going back to 2018 (discussion).

Measurement says most of that seam comes from the test display code, not from the prefilter. TestRadiancePrefilteringGgx.RenderCubeMap draws the -Z face with SpriteEffects.FlipVertically (TestRadiancePrefilteringGgx.cs:136). A vertical cross layout needs a 180 degree rotation there, which is a vertical flip and a horizontal flip. The test applies half of that, so the bottom face of the cross appears mirrored left to right against its neighbor.

A smaller discontinuity does remain after that is accounted for. It grows with roughness, so it is worth a look, but it is not what the eye is picking up.

Evidence

Measured on the committed gold images at tests/Stride.Graphics.Tests.11_0/Windows.Direct3D12/WARP/. The cross layout is 768x1024 with 256x256 tiles. Adjacent faces in the cross share an edge on the sphere, so a continuous function must give near equal values along that edge.

Step across the -Y / -Z seam, mean absolute luminance difference between the two edge rows:

Image as drawn with the -Z row mirrored
mip 0 8.42 0.46
mip 1 4.42 0.72
mip 2 3.61 1.49

Mip 0 is the strongest evidence. With DoNotFilterHighestLevel set, mip 0 is a copy of the source probe and the filter never touches it. The source is a captured light probe and is continuous. Mirroring the row drops the step by a factor of 18, so the correct adjacency is the mirrored one and the display transform is wrong.

The same measurement on f1 across three gold buckets, which rules out a backend cause:

Bucket as drawn mirrored
Direct3D11 / WARP 4.42 0.72
Direct3D12 / WARP 4.42 0.72
Direct3D11 / NVIDIA GeForce RTX 5080 4.35 0.72

The residual

After the mirror is accounted for, the other seams still drift as roughness rises:

Image +Y / +Z -X / +Z +Z / +X within-face variation
mip 0 0.73 0.28 0.32 5.24
mip 1 0.52 0.37 0.24 1.06
mip 2 2.69 0.54 0.47 1.02

At mip 0 and mip 1 every seam sits below the normal variation inside a face, which is what a continuous cubemap looks like. At mip 2 the +Y / +Z seam reaches 2.6 times the local variation.

Filtering should reduce a discontinuity, not raise it, so this direction is wrong and the cause is worth finding. The likely candidate: RadiancePrefilteringGGXNoComputeShader samples the source with RadianceMap.SampleLevel(sampler, L, mipLevel), which is seamless across faces by construction. It picks mipLevel from the sample solid angle, so a higher roughness reads a higher source mip. If the source mip chain is generated per face, the edge texels of the higher mips are wrong before the filter ever runs, and no amount of seamless sampling recovers them.

To Reproduce

  1. Run TestRadiancePrefilteringGgx on any backend.
  2. Open tests/compare-gold.cmd and look at the three images.
  3. The seam sits between the bottom face and the back face, at the boundary between the third and fourth row of the cross.

The gold images already committed show it without running anything.

Expected behavior

A prefiltered cubemap approximates the illumination function over a sphere. The faces only discretize that sphere, so the result must stay continuous across every face boundary. A discontinuity is a high frequency difference that the filter failed to reduce, and it shows up as a visible artifact on rough surfaces.

Suggested fix

Two separate changes:

  1. The display transform. TestRadiancePrefilteringGgx.RenderCubeMap should rotate the -Z face 180 degrees rather than flip it vertically. This moves gold images for that test on every platform.
  2. The residual discontinuity. Confirm whether the source cubemap mip chain is generated per face. If it is, that is the defect, and it affects every cubemap in the engine rather than this one test.

Item 1 is small and self contained. Item 2 needs its own investigation, and it may deserve a separate issue once somebody confirms the cause.

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions