Secondary bounce lighting now uses a stable per-hit RNG for sky/direct-light correction instead of frame-varying RNG, so bounce lighting should stop crawling/sparkling as much.

This commit is contained in:
Justin Marshall
2026-05-24 15:42:55 -07:00
parent 1bd467154e
commit 4ccdb56258
3 changed files with 181 additions and 36 deletions
+25
View File
@@ -561,6 +561,8 @@ void idRenderModelStatic::UpdateDXR(uint32_t& dxrBottomAcel, int onlySurface)
{
int numDXRVerts = 0;
int numDXRIndexes = 0;
float averageColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
float averageColorWeight = 0.0f;
for (int i = 0; i < surfaces.Num(); i++)
{
@@ -580,6 +582,21 @@ void idRenderModelStatic::UpdateDXR(uint32_t& dxrBottomAcel, int onlySurface)
numDXRVerts += surf->geometry->numVerts;
numDXRIndexes += surf->geometry->numIndexes;
if (surf->shader)
{
idImage* diffuseImage = surf->shader->GetDiffuseImage(NULL);
if (diffuseImage)
{
const float* c = diffuseImage->GetAverageColor();
const float weight = (surf->geometry->numIndexes > 0) ? (float)surf->geometry->numIndexes : 1.0f;
averageColor[0] += c[0] * weight;
averageColor[1] += c[1] * weight;
averageColor[2] += c[2] * weight;
averageColor[3] += c[3] * weight;
averageColorWeight += weight;
}
}
}
std::vector<glRaytracingVertex_t> vertices(numDXRVerts);
@@ -644,6 +661,14 @@ void idRenderModelStatic::UpdateDXR(uint32_t& dxrBottomAcel, int onlySurface)
desc.averageColor[1] = 1.0f;
desc.averageColor[2] = 1.0f;
desc.averageColor[3] = 1.0f;
if (averageColorWeight > 0.0f)
{
const float invWeight = 1.0f / averageColorWeight;
desc.averageColor[0] = idMath::ClampFloat(0.0f, 1.0f, averageColor[0] * invWeight);
desc.averageColor[1] = idMath::ClampFloat(0.0f, 1.0f, averageColor[1] * invWeight);
desc.averageColor[2] = idMath::ClampFloat(0.0f, 1.0f, averageColor[2] * invWeight);
desc.averageColor[3] = idMath::ClampFloat(0.0f, 1.0f, averageColor[3] * invWeight);
}
if (!dxrBottomAcel)
{
+140 -33
View File
@@ -2863,6 +2863,52 @@ float3 EnhanceBumpNormal(uint2 pixel, float3 worldPos, float3 baseAlbedo, bool i
return outN;
}
float3 StabilizeSkeletalNormal(uint2 pixel, float3 worldPos, float3 N)
{
int2 p = int2(pixel);
int2 maxP = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
float3 accum = N * 3.0;
float weightSum = 3.0;
[unroll]
for (int y = -1; y <= 1; ++y)
{
[unroll]
for (int x = -1; x <= 1; ++x)
{
if (x == 0 && y == 0)
continue;
int2 sp = clamp(p + int2(x, y), int2(0, 0), maxP);
float4 samplePos = gPositionTex.Load(int3(sp, 0));
uint sampleGeoFlag = DecodeGeometryFlag(samplePos.w);
if ((sampleGeoFlag & GEOMETRY_FLAG_SKELETAL) == 0u)
continue;
float3 sampleDelta = samplePos.xyz - worldPos;
float distSq = dot(sampleDelta, sampleDelta);
if (distSq > 16.0)
continue;
float3 sampleN = SafeNormalizeLocal(gNormalTex.Load(int3(sp, 0)).xyz, N);
if (dot(sampleN, N) < 0.0)
sampleN = -sampleN;
float nd = saturate(dot(sampleN, N));
if (nd < 0.20)
continue;
float w = nd * rcp(1.0 + distSq * 0.35);
accum += sampleN * w;
weightSum += w;
}
}
float3 smoothed = SafeNormalizeLocal(accum / max(weightSum, 1.0e-4), N);
return SafeNormalizeLocal(lerp(N, smoothed, 0.70), N);
}
[shader("miss")]
void ShadowMiss(inout ShadowPayload payload)
{
@@ -3112,7 +3158,8 @@ float GetPointLightMaxRadius(Light Lgt)
float3 r = GetPointLightRadius(Lgt);
return max(max(r.x, r.y), r.z);
}
)"
R"(
float3 Doom3SafeNormalizeOr(float3 v, float3 fallbackDir)
{
float lenSq = dot(v, v);
@@ -3819,6 +3866,17 @@ uint InitRng(uint2 pixel, uint frameIndex, uint sampleIndex)
return PcgHash(seed) | 1u;
}
uint InitSpatialRng(uint2 pixel, float3 worldPos, uint salt)
{
uint seed = pixel.x * 1973u;
seed ^= pixel.y * 9277u;
seed ^= asuint(worldPos.x * 0.03125) * 1597334677u;
seed ^= asuint(worldPos.y * 0.03125) * 3812015801u;
seed ^= asuint(worldPos.z * 0.03125) * 2798796415u;
seed ^= salt * 374761393u;
return PcgHash(seed) | 1u;
}
float Rand(inout uint rng)
{
rng = PcgHash(rng);
@@ -4542,6 +4600,24 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
return 0.0;
}
float3 EstimateUnresolvedBounceRadiance(float3 hitPos, float3 hitN, float3 albedo)
{
hitN = SafeNormalizeOr(hitN, float3(0.0, 0.0, 1.0));
albedo = max(saturate(albedo), float3(0.03, 0.03, 0.03));
float upness = saturate(hitN.z * 0.5 + 0.5);
float3 lighting = gAmbientColor.rgb * (gAmbientColor.a * 0.025);
lighting += GetSkyRadiance(hitN) * (0.035 + 0.035 * upness);
[loop]
for (uint i = 0; i < gLightCount; ++i)
{
lighting += EstimateFastBounceLight(hitPos, hitN, gLights[i]) * 0.35;
}
return clamp(albedo * max(lighting, 0.0), 0.0, 6.0);
}
float3 EstimateShadowedBounceLight(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, Light Lgt, inout uint rng)
{
float3 spec = 0.0;
@@ -4596,8 +4672,11 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
{
bool hitIsSkeletal = (hitGeoFlag & GEOMETRY_FLAG_SKELETAL) != 0u;
bool hitIsUnlit = (hitGeoFlag & GEOMETRY_FLAG_UNLIT) != 0u;
uint lightingRng = InitSpatialRng(hitPixel, hitPos, 0xB04157u);
hitN = SafeNormalizeOr(hitN, float3(0.0, 0.0, 1.0));
if (hitIsSkeletal)
hitN = StabilizeSkeletalNormal(hitPixel, hitPos, hitN);
hitV = SafeNormalizeOr(hitV, -hitN);
hitAlbedo = saturate(hitAlbedo);
@@ -4611,7 +4690,7 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
// Real occluded sky contribution at the secondary hit. The previous GI path
// used a fixed sky term, so corners/cavities received too much indirect light.
lighting += EstimateBounceSkyLighting(hitPos, hitN, rng) * (0.14 + 0.10 * upness);
lighting += EstimateBounceSkyLighting(hitPos, hitN, lightingRng) * (0.14 + 0.10 * upness);
// Glow-map emissive no longer participates in secondary GI. It remains a
// direct visible/bloom-only effect until real material-space emissive lighting
@@ -4628,10 +4707,9 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
}
lighting += fastAllLights;
// Visibility correction: replace a small rotating subset of the unshadowed
// baseline with real shadowed direct lighting. Temporal accumulation in the
// compute pass below makes this converge without tracing every light at every
// bounce hit.
// Visibility correction: replace a small stable subset of the unshadowed
// baseline with real shadowed direct lighting. Keying this to the hit
// position avoids frame-to-frame sparkle while keeping the all-lights bounce.
uint correctionBudget = 0u;
if (gLightCount > 0u)
{
@@ -4642,10 +4720,10 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
if (correctionBudget > 0u)
{
uint start = PcgHash(rng ^ 0x9E3779B9u) % gLightCount;
rng = PcgHash(rng + 0xBB67AE85u);
uint stride = (gLightCount > 1u) ? (1u + (PcgHash(rng ^ 0x3C6EF372u) % (gLightCount - 1u))) : 1u;
rng = PcgHash(rng + 0xA54FF53Au);
uint start = PcgHash(lightingRng ^ 0x9E3779B9u) % gLightCount;
lightingRng = PcgHash(lightingRng + 0xBB67AE85u);
uint stride = (gLightCount > 1u) ? (1u + (PcgHash(lightingRng ^ 0x3C6EF372u) % (gLightCount - 1u))) : 1u;
lightingRng = PcgHash(lightingRng + 0xA54FF53Au);
[loop]
for (uint c = 0u; c < correctionBudget; ++c)
@@ -4653,7 +4731,7 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
uint lightIndex = (start + c * stride) % gLightCount;
Light Lgt = gLights[lightIndex];
float3 fast = EstimateFastBounceLight(hitPos, hitN, Lgt);
float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, Lgt, rng);
float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, Lgt, lightingRng);
lighting += shadowed - fast;
}
}
@@ -4866,9 +4944,9 @@ float3 TraceOneIndirectBouncePath(uint2 pixel, float3 worldPos, float3 N, float3
uint2 hitPixel = pathPixel;
float3 hitPos = rayHitPos;
float3 hitNormal = SafeNormalizeOr(-bounceDir, pathNormal);
float3 hitAlbedo = float3(0.55, 0.55, 0.55);
uint hitGeoFlag = GEOMETRY_FLAG_NONE;
float3 meshAverageColor = GetMeshAverageColor(meshIndex);
float3 hitAlbedo = meshAverageColor;
uint hitGeoFlag = GEOMETRY_FLAG_NONE;
bool hasGBufferMaterial = TryFetchGBufferAtRayHit(
rayHitPos,
@@ -4885,25 +4963,30 @@ float3 TraceOneIndirectBouncePath(uint2 pixel, float3 worldPos, float3 N, float3
hitNormal = -hitNormal;
float3 hitV = SafeNormalizeOr(-bounceDir, pathView);
float3 bouncedRadiance = EstimateDirectLightingForBounceHit(
hitPixel,
hitPos,
hitNormal,
hitV,
hitAlbedo,
hitGeoFlag,
rng);
if (!hasGBufferMaterial)
float3 bouncedRadiance = 0.0;
if (hasGBufferMaterial)
{
// The ray hit real TLAS geometry, but material lookup via camera
// G-buffer failed because the surface is hidden/off-screen. Keep the
// bounce alive with a neutral material and a little environment tint.
float3 skyTint = GetSkyRadiance(SafeNormalizeOr(reflect(bounceDir, hitNormal), float3(0.0, 0.0, 1.0)));
bouncedRadiance += skyTint * 0.045;
bouncedRadiance = EstimateDirectLightingForBounceHit(
hitPixel,
hitPos,
hitNormal,
hitV,
hitAlbedo,
hitGeoFlag,
rng);
bouncedRadiance *= meshAverageColor * 1.12;
}
else
{
// The ray hit real TLAS geometry, but material lookup through the
// camera G-buffer failed or landed on an unrelated visible pixel.
// Use mesh-derived color instead of neutral grey/screen material.
hitPos = rayHitPos;
hitAlbedo = meshAverageColor;
bouncedRadiance = EstimateUnresolvedBounceRadiance(hitPos, hitNormal, hitAlbedo);
}
bouncedRadiance *= meshAverageColor * 1.12;
bouncedRadiance = CompressEmissiveRadiance(bouncedRadiance, depth == 0u ? 8.0 : 5.0);
accum += throughput * bouncedRadiance;
@@ -5360,8 +5443,9 @@ void RayGen()
uint geoFlag = DecodeGeometryFlag(positionSample.w);
bool isSkeletal = (geoFlag & GEOMETRY_FLAG_SKELETAL) != 0u;
bool isUnlit = (geoFlag & GEOMETRY_FLAG_UNLIT) != 0u;
float4 normalSample = LoadSceneNormal(pixel);
float3 N = EnhanceBumpNormal(pixel, worldPos, baseAlbedo, isSkeletal);
if (isSkeletal)
N = StabilizeSkeletalNormal(pixel, worldPos, N);
float3 V = normalize(gCameraPos.xyz - worldPos);
if (isUnlit)
@@ -5538,6 +5622,8 @@ float Luminance(float3 c)
return dot(c, float3(0.2126, 0.7152, 0.0722));
}
static const uint GEOMETRY_FLAG_SKELETAL = 1u;
static const uint GEOMETRY_FLAG_UNLIT = 2u;
static const uint GEOMETRY_FLAG_GLASS = 4u;
uint DecodeGeometryFlag(float geoFlag)
@@ -5599,9 +5685,10 @@ float GeometryAwareWeight(
return 0.0;
// Do not smear lighting across material-class boundaries. This is
// particularly important for glass, because the primary G-buffer sample can
// be glass while the ray visibility must continue through it.
if (((centerGeoFlag ^ sampleGeoFlag) & GEOMETRY_FLAG_GLASS) != 0u)
// particularly important for skeletal pixels and glass, because both can sit
// in front of unrelated world lighting in the same screen neighborhood.
const uint classMask = GEOMETRY_FLAG_SKELETAL | GEOMETRY_FLAG_UNLIT | GEOMETRY_FLAG_GLASS;
if (((centerGeoFlag ^ sampleGeoFlag) & classMask) != 0u)
return 0.0;
float3 centerLighting = DemodulateLighting(centerRadiance, centerAlbedo);
@@ -5736,6 +5823,16 @@ void DenoiseCS(uint3 dispatchThreadId : SV_DispatchThreadID)
int2 sp = int2(pixel) + int2(x, y);
if (sp.x < 0 || sp.y < 0 || sp.x >= (int)gScreenSize.x || sp.y >= (int)gScreenSize.y)
continue;
float sampleDepth = gDepthTex.Load(int3(sp, 0));
if (sampleDepth <= 0.0 || sampleDepth >= 1.0)
continue;
float4 samplePos4 = gPositionTex.Load(int3(sp, 0));
uint sampleGeoFlag = DecodeGeometryFlag(samplePos4.w);
const uint classMask = GEOMETRY_FLAG_SKELETAL | GEOMETRY_FLAG_UNLIT | GEOMETRY_FLAG_GLASS;
if (((centerGeoFlag ^ sampleGeoFlag) & classMask) != 0u)
continue;
float3 raw = gPathTraceTex.Load(int3(sp, 0)).rgb;
minRaw = min(minRaw, raw);
maxRaw = max(maxRaw, raw);
@@ -5801,6 +5898,11 @@ float3 SafeNormalTemporal(float3 n)
return n * rsqrt(lenSq);
}
uint DecodeGeometryFlagTemporal(float geoFlag)
{
return (uint)floor(max(geoFlag, 0.0) + 0.5);
}
float3 ClampHistoryToCurrent(float3 history, float3 current)
{
// Clamp enough to kill rare GI/volume fireflies before they enter history,
@@ -5819,6 +5921,9 @@ void TemporalAccumCS(uint3 dispatchThreadId : SV_DispatchThreadID)
float4 raw = gPathTraceTex.Load(int3(pixel, 0));
float depth = gDepthTex.Load(int3(pixel, 0));
float4 positionSample = gPositionTex.Load(int3(pixel, 0));
uint geoFlag = DecodeGeometryFlagTemporal(positionSample.w);
bool isSkeletal = (geoFlag & 1u) != 0u;
if (depth <= 0.0 || depth >= 1.0 || gMaxBounces <= 1u)
{
@@ -5847,6 +5952,8 @@ void TemporalAccumCS(uint3 dispatchThreadId : SV_DispatchThreadID)
// current-frame weight so the accumulator does not leave obvious trails.
float currentWeight = max(1.0 / (historyCount + 1.0), 0.035);
currentWeight = max(currentWeight, saturate(relChange * 0.24));
if (isSkeletal)
currentWeight = max(currentWeight, 0.25);
currentWeight = saturate(currentWeight);
float3 resolved = lerp(historyColor, max(raw.rgb, 0.0), currentWeight);
+16 -3
View File
@@ -361,6 +361,8 @@ void QD3D12_SetCameraInfo(
const float* clipToView,
const float* clipToPrevClip,
const float* prevClipToClip,
const float* worldToView,
const float* viewToWorld,
const float* cameraPos,
const float* cameraRight,
const float* cameraUp,
@@ -534,6 +536,8 @@ struct QD3D12CameraState
Mat4 clipToView = Mat4::Identity();
Mat4 clipToPrevClip = Mat4::Identity();
Mat4 prevClipToClip = Mat4::Identity();
Mat4 worldToView = Mat4::Identity();
Mat4 viewToWorld = Mat4::Identity();
float cameraPos[3] = { 0.0f, 0.0f, 0.0f };
float cameraRight[3] = { 1.0f, 0.0f, 0.0f };
float cameraUp[3] = { 0.0f, 1.0f, 0.0f };
@@ -2173,6 +2177,8 @@ static bool QD3D12_UpdateCameraInfoFromCurrentMatrices()
clipToView,
clipToPrevClip,
prevClipToClip,
worldToView,
viewToWorld,
cameraPos,
cameraRight,
cameraUp,
@@ -9094,9 +9100,8 @@ static void QD3D12_RunUpscalerOrBlit(QD3D12Window& w)
rrOptions.normalRoughnessMode = sl::DLSSDNormalRoughnessMode::ePacked;
rrOptions.alphaUpscalingEnabled = sl::Boolean::eFalse;
Mat4 identity = Mat4::Identity();
memcpy(&rrOptions.worldToCameraView, identity.m, sizeof(float) * 16);
memcpy(&rrOptions.cameraViewToWorld, identity.m, sizeof(float) * 16);
memcpy(&rrOptions.worldToCameraView, g_gl.cameraState.worldToView.m, sizeof(float) * 16);
memcpy(&rrOptions.cameraViewToWorld, g_gl.cameraState.viewToWorld.m, sizeof(float) * 16);
const sl::Result rrOptionsResult = slDLSSDSetOptions(g_qd3d12Sl.viewport, rrOptions);
if (rrOptionsResult != sl::Result::eOk)
@@ -18521,6 +18526,8 @@ void QD3D12_SetCameraInfo(
const float* clipToView,
const float* clipToPrevClip,
const float* prevClipToClip,
const float* worldToView,
const float* viewToWorld,
const float* cameraPos,
const float* cameraRight,
const float* cameraUp,
@@ -18542,6 +18549,12 @@ void QD3D12_SetCameraInfo(
if (prevClipToClip)
memcpy(g_gl.cameraState.prevClipToClip.m, prevClipToClip, sizeof(float) * 16);
if (worldToView)
memcpy(g_gl.cameraState.worldToView.m, worldToView, sizeof(float) * 16);
if (viewToWorld)
memcpy(g_gl.cameraState.viewToWorld.m, viewToWorld, sizeof(float) * 16);
if (cameraPos)
{
g_gl.cameraState.cameraPos[0] = cameraPos[0];