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
- Run
TestRadiancePrefilteringGgx on any backend.
- Open
tests/compare-gold.cmd and look at the three images.
- 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:
- 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.
- 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
Release Type: GitHub
Version:
master(ed6f739f2), and every gold image back to 2018Platform(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.RenderCubeMapdraws the-Zface withSpriteEffects.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/-Zseam, mean absolute luminance difference between the two edge rows:-Zrow mirroredMip 0 is the strongest evidence. With
DoNotFilterHighestLevelset, 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
f1across three gold buckets, which rules out a backend cause:The residual
After the mirror is accounted for, the other seams still drift as roughness rises:
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/+Zseam 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:
RadiancePrefilteringGGXNoComputeShadersamples the source withRadianceMap.SampleLevel(sampler, L, mipLevel), which is seamless across faces by construction. It picksmipLevelfrom 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
TestRadiancePrefilteringGgxon any backend.tests/compare-gold.cmdand look at the three images.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:
TestRadiancePrefilteringGgx.RenderCubeMapshould rotate the-Zface 180 degrees rather than flip it vertically. This moves gold images for that test on every platform.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