diff --git a/neo/engine/opengl/gl_d3d12shim.cpp b/neo/engine/opengl/gl_d3d12shim.cpp index 4192c9d4..e638688e 100644 --- a/neo/engine/opengl/gl_d3d12shim.cpp +++ b/neo/engine/opengl/gl_d3d12shim.cpp @@ -660,6 +660,7 @@ enum PipelineMode static void QD3D12_FlushQueuedBatches(); static PipelineMode PickPipeline(bool useTex0, bool useTex1); static Mat4 CurrentMVP(); +static Mat4 QD3D12_ProjectionWithoutAutomaticJitter(const Mat4& projection); struct RetiredResource { @@ -2189,7 +2190,9 @@ static bool QD3D12_UpdateCameraInfoFromCurrentMatrices() if (g_gl.projStack.empty() || g_gl.modelStack.empty()) return false; - const float* glProjection = g_gl.projStack.back().m; + const Mat4 unjitteredProjection = QD3D12_ProjectionWithoutAutomaticJitter(g_gl.projStack.back()); + const float* glProjection = unjitteredProjection.m; + const float* rasterProjection = g_gl.projStack.back().m; const float* modelView = g_gl.modelStack.back().m; const float* modelToWorld = g_gl.modelMatrix.m; const UINT renderWidth = g_currentWindow ? g_currentWindow->renderWidth : 0; @@ -2201,7 +2204,7 @@ static bool QD3D12_UpdateCameraInfoFromCurrentMatrices() g_qd3d12AutoCamera.cachedRenderWidth == renderWidth && g_qd3d12AutoCamera.cachedRenderHeight == renderHeight && g_gl.cameraState.valid && - memcmp(g_qd3d12AutoCamera.cachedProjection, glProjection, sizeof(g_qd3d12AutoCamera.cachedProjection)) == 0 && + memcmp(g_qd3d12AutoCamera.cachedProjection, rasterProjection, sizeof(g_qd3d12AutoCamera.cachedProjection)) == 0 && memcmp(g_qd3d12AutoCamera.cachedModelView, modelView, sizeof(g_qd3d12AutoCamera.cachedModelView)) == 0 && memcmp(g_qd3d12AutoCamera.cachedModelToWorld, modelToWorld, sizeof(g_qd3d12AutoCamera.cachedModelToWorld)) == 0) { @@ -2331,7 +2334,7 @@ static bool QD3D12_UpdateCameraInfoFromCurrentMatrices() g_qd3d12AutoCamera.cachedMotionHistoryReset = g_gl.motionHistoryReset; g_qd3d12AutoCamera.cachedRenderWidth = renderWidth; g_qd3d12AutoCamera.cachedRenderHeight = renderHeight; - QD3D12_MatrixCopy(g_qd3d12AutoCamera.cachedProjection, glProjection); + QD3D12_MatrixCopy(g_qd3d12AutoCamera.cachedProjection, rasterProjection); QD3D12_MatrixCopy(g_qd3d12AutoCamera.cachedModelView, modelView); QD3D12_MatrixCopy(g_qd3d12AutoCamera.cachedModelToWorld, modelToWorld); return true; @@ -8586,7 +8589,7 @@ static sl::Result QD3D12_SetStreamlineCommonConstants(sl::FrameToken& frameToken memcpy(&consts.clipToCameraView, g_gl.cameraState.clipToView.m, sizeof(float) * 16); memcpy(&consts.clipToPrevClip, g_gl.cameraState.clipToPrevClip.m, sizeof(float) * 16); memcpy(&consts.prevClipToClip, g_gl.cameraState.prevClipToClip.m, sizeof(float) * 16); - consts.jitterOffset = { g_gl.jitterX, g_gl.jitterY }; + consts.jitterOffset = { -g_gl.jitterX, -g_gl.jitterY }; // The G-buffer stores motion as previous UV minus current UV after removing // projection jitter, so it is already in normalized screen space. consts.mvecScale = { 1.0f, 1.0f }; @@ -8959,6 +8962,74 @@ static void QD3D12_ConfigureDLSSFrameGeneration(QD3D12Window&, void*) {} static void QD3D12_LogDLSSFrameGenerationPresentResult() {} #endif +static float QD3D12_Halton(uint32_t index, uint32_t base) +{ + float result = 0.0f; + float fraction = 1.0f / (float)base; + while (index > 0u) + { + result += (float)(index % base) * fraction; + index /= base; + fraction /= (float)base; + } + return result; +} + +static bool QD3D12_ShouldUseAutomaticProjectionJitter(const QD3D12Window& w) +{ + return !w.isPbuffer && + QD3D12_AllowsGamePresentationFeatures(w) && + g_gl.upscalerBackend == QD3D12_UPSCALER_DLSS && + QD3D12_WantsDLSSRayReconstruction(); +} + +static void QD3D12_UpdateAutomaticProjectionJitter(QD3D12Window& w) +{ + if (!QD3D12_ShouldUseAutomaticProjectionJitter(w)) + { + g_gl.jitterX = 0.0f; + g_gl.jitterY = 0.0f; + return; + } + + const uint32_t jitterIndex = (uint32_t)(g_gl.frameSerial % 8u) + 1u; + constexpr float kRRMicroJitterPixels = 0.18f; + g_gl.jitterX = (QD3D12_Halton(jitterIndex, 2u) - 0.5f) * kRRMicroJitterPixels; + g_gl.jitterY = (QD3D12_Halton(jitterIndex, 3u) - 0.5f) * kRRMicroJitterPixels; +} + +static void QD3D12_ApplyAutomaticProjectionJitter(Mat4& projection) +{ + if (!g_currentWindow || g_gl.matrixMode != GL_PROJECTION) + return; + if (fabsf(g_gl.jitterX) <= 1.0e-6f && fabsf(g_gl.jitterY) <= 1.0e-6f) + return; + if (!QD3D12_IsPerspectiveProjectionCM(projection.m)) + return; + + const float renderWidth = (float)std::max(1u, g_currentWindow->renderWidth); + const float renderHeight = (float)std::max(1u, g_currentWindow->renderHeight); + projection.m[8] += (2.0f * g_gl.jitterX) / renderWidth; + projection.m[9] += (2.0f * g_gl.jitterY) / renderHeight; +} + +static Mat4 QD3D12_ProjectionWithoutAutomaticJitter(const Mat4& projection) +{ + Mat4 unjittered = projection; + if (!g_currentWindow) + return unjittered; + if (fabsf(g_gl.jitterX) <= 1.0e-6f && fabsf(g_gl.jitterY) <= 1.0e-6f) + return unjittered; + if (!QD3D12_IsPerspectiveProjectionCM(unjittered.m)) + return unjittered; + + const float renderWidth = (float)std::max(1u, g_currentWindow->renderWidth); + const float renderHeight = (float)std::max(1u, g_currentWindow->renderHeight); + unjittered.m[8] -= (2.0f * g_gl.jitterX) / renderWidth; + unjittered.m[9] -= (2.0f * g_gl.jitterY) / renderHeight; + return unjittered; +} + #if defined(QD3D12_ENABLE_FFX) struct QD3D12FfxState { @@ -12348,6 +12419,7 @@ void QD3D12_BeginFrame() g_gl.deferScreenSpaceParticlesToEndFrame = false; g_gl.deferredScreenSpaceParticleTlas.Reset(); QD3D12_UpdateViewportState(); + QD3D12_UpdateAutomaticProjectionJitter(w); FrameResources& fr = w.frames[w.frameIndex]; QD3D12_CHECK(fr.cmdAlloc->Reset()); @@ -15656,6 +15728,7 @@ void APIENTRY glLoadMatrixf(const GLfloat* m) auto& top = QD3D12_CurrentMatrixStack().back(); memcpy(top.m, m, sizeof(top.m)); + QD3D12_ApplyAutomaticProjectionJitter(top); } void APIENTRY glGetIntegerv(GLenum pname, GLint* params) @@ -15970,6 +16043,7 @@ void APIENTRY glFrustum(GLdouble left, GLdouble right, auto& topMat = QD3D12_CurrentMatrixStack().back(); topMat = Mat4::Multiply(topMat, proj); + QD3D12_ApplyAutomaticProjectionJitter(topMat); } void APIENTRY glDepthFunc(GLenum func) diff --git a/neo/engine/opengl/gl_raytracing.cpp b/neo/engine/opengl/gl_raytracing.cpp index b933d228..88705101 100644 --- a/neo/engine/opengl/gl_raytracing.cpp +++ b/neo/engine/opengl/gl_raytracing.cpp @@ -3689,6 +3689,12 @@ float2 Rand2(inout uint rng) return float2(Rand(rng), Rand(rng)); } +float2 RayReconstructionTemporalDisk(uint2 pixel, uint sampleIndex, float radius) +{ + uint rng = InitRng(pixel, gFrameIndex, sampleIndex); + return ConcentricSampleDisk(Rand2(rng)) * radius; +} + float3 SampleCosineWorld(float3 N, inout uint rng) { float3 tangent, bitangent; @@ -3953,6 +3959,8 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 // Use a deterministic low-discrepancy pattern instead. With a single sample, // use the light center so default point lights are hard-shadowed and stable. float areaRadius = (sampleCount > 1u && Lgt.samples > 1u) ? max(GetPointLightMaxRadius(Lgt) * 0.03, 0.12) : 0.0; + if (gEnableDenoiser == 0u && Lgt.samples != 0u) + areaRadius = max(areaRadius, min(max(GetPointLightMaxRadius(Lgt) * 0.006, 0.04), 1.25)); float rand = Hash12((float2)pixel + worldPos.xy + float2(worldPos.z, centerDist)); float3 diffuseAccum = 0.0; @@ -3962,7 +3970,9 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3 for (uint s = 0u; s < sampleCount; ++s) { float2 disk = float2(0.0, 0.0); - if (areaRadius > 0.0) + if (areaRadius > 0.0 && gEnableDenoiser == 0u) + disk = RayReconstructionTemporalDisk(pixel, 0xA11E00u + s, areaRadius); + else if (areaRadius > 0.0) disk = ConcentricSampleDisk(Hammersley2D(s, sampleCount, rand)) * areaRadius; float3 sampleLightPos = Lgt.position + tangent * disk.x + bitangent * disk.y; @@ -4039,6 +4049,8 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V for (uint s = 0u; s < sampleCount; ++s) { float2 uv = StableRectLightSampleUV(s, sampleCount); + if (gEnableDenoiser == 0u) + uv = clamp(uv + RayReconstructionTemporalDisk(pixel, 0xEC7000u + s, 0.18), -0.85, 0.85); float3 sampleLightPos = Lgt.position + @@ -4178,10 +4190,12 @@ float3 EstimateSingleLightVolumetricScattering(uint2 pixel, float3 cameraPos, fl stepCount = min(max(stepCount, 8u), 32u); float stepLen = viewDist / (float)stepCount; - // Do not frame-jitter the march. This renderer has no temporal GI history, - // so varying the volume sample positions every frame creates visible sparkle. - // A centered deterministic slice is stable and the spatial denoiser can smooth it. float jitter = 0.5; + if (gEnableDenoiser == 0u) + { + uint jitterRng = InitRng(pixel, gFrameIndex, 0x701u); + jitter = lerp(0.42, 0.58, Rand(jitterRng)); + } float density = EstimateVolumeDensityFromLight(Lgt); float anisotropy = (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT) ? 0.55 : 0.35;