diff --git a/neo/engine/models/Model.cpp b/neo/engine/models/Model.cpp index 019e3822..00c5873e 100644 --- a/neo/engine/models/Model.cpp +++ b/neo/engine/models/Model.cpp @@ -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 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) { diff --git a/neo/engine/opengl/gl_d3d12raylight.cpp b/neo/engine/opengl/gl_d3d12raylight.cpp index 7c2724df..9a6ce124 100644 --- a/neo/engine/opengl/gl_d3d12raylight.cpp +++ b/neo/engine/opengl/gl_d3d12raylight.cpp @@ -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); diff --git a/neo/engine/opengl/gl_d3d12shim.cpp b/neo/engine/opengl/gl_d3d12shim.cpp index d9346fa0..dcfcdb06 100644 --- a/neo/engine/opengl/gl_d3d12shim.cpp +++ b/neo/engine/opengl/gl_d3d12shim.cpp @@ -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];