diff --git a/SeeSharp/Integrators/Bidir/BidirBase.cs b/SeeSharp/Integrators/Bidir/BidirBase.cs index eb0fad2..3a9b1cf 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.cs @@ -395,6 +395,11 @@ void ConnectLightVertexToCamera(in PathVertex vertex, in PathVertex ancestor, Ve if (bsdfValue == RgbColor.Black) return; + // The shading cosine only cancels out with the Jacobian if the geometry aligns with the shading geometry + bsdfValue *= + float.Abs(Vector3.Dot(vertex.Point.ShadingNormal, dirToAncestor)) / + float.Abs(Vector3.Dot(vertex.Point.Normal, dirToAncestor)); + // Compute the surface area pdf of sampling the previous vertex instead var (pdfReverse, _) = shader.Pdf(dirToAncestor); if (ancestor.Point.Mesh != null) @@ -468,8 +473,11 @@ RgbColor Connect(in SurfaceShader shader, PathVertex vertex, PathVertex ancestor var dirFromCamToLight = Vector3.Normalize(vertex.Point.Position - shader.Point.Position); SurfaceShader lightShader = new(vertex.Point, dirToAncestor, true); + var bsdfWeightLight = lightShader.Evaluate(-dirFromCamToLight) * float.Abs(Vector3.Dot(vertex.Point.Normal, -dirFromCamToLight)); + bsdfWeightLight *= + float.Abs(Vector3.Dot(vertex.Point.ShadingNormal, dirToAncestor)) / + float.Abs(Vector3.Dot(vertex.Point.Normal, dirToAncestor)); - var bsdfWeightLight = lightShader.EvaluateWithCosine(-dirFromCamToLight); var bsdfWeightCam = shader.EvaluateWithCosine(dirFromCamToLight); if (bsdfWeightCam == RgbColor.Black || bsdfWeightLight == RgbColor.Black) diff --git a/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs b/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs index de020cf..2cbc34f 100644 --- a/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs +++ b/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs @@ -271,16 +271,23 @@ protected virtual RgbColor Merge(in CameraPath path, float cameraJacobian, in Su if (depth > MaxDepth || depth < MinDepth) return RgbColor.Black; + // Discard photons on (almost) perpendicular surfaces. This avoids outliers and somewhat reduces + // light leaks, but slightly amplifies darkening from kernel estimation bias. + if (float.Abs(Vector3.Dot(shader.Point.Normal, photon.Point.Normal)) < 0.4f) { + return RgbColor.Black; + } + // Compute the contribution of the photon var ancestor = LightPaths[idx.pathIdx, idx.vertexIdx - 1]; var dirToAncestor = Vector3.Normalize(ancestor.Point.Position - shader.Point.Position); var bsdfValue = shader.Evaluate(dirToAncestor); + bsdfValue *= + float.Abs(Vector3.Dot(shader.Point.ShadingNormal, dirToAncestor)) / + float.Abs(Vector3.Dot(photon.Point.Normal, dirToAncestor)); var photonContrib = photon.Weight * bsdfValue / NumLightPaths.Value; // Early exit + prevent NaN / Inf if (photonContrib == RgbColor.Black) return RgbColor.Black; - // Prevent outliers due to numerical issues with photons arriving almost parallel to the surface - if (Math.Abs(Vector3.Dot(dirToAncestor, shader.Point.Normal)) < 1e-4f) return RgbColor.Black; // Compute the missing pdf terms and the MIS weight var (pdfLightReverse, pdfCameraReverse) = shader.Pdf(dirToAncestor); diff --git a/SeeSharp/Integrators/Common/RandomWalk.cs b/SeeSharp/Integrators/Common/RandomWalk.cs index 85f3baa..ec0d118 100644 --- a/SeeSharp/Integrators/Common/RandomWalk.cs +++ b/SeeSharp/Integrators/Common/RandomWalk.cs @@ -191,6 +191,20 @@ RgbColor ContinueWalk(Ray ray, SurfacePoint previousPoint, float pdfDirection, R if (dirSample.PdfForward == 0 || dirSample.Weight == RgbColor.Black) break; + if (isOnLightSubpath) { + // The direction sample is multiplied by the shading cosine, but we need the geometric one + dirSample.Weight *= + float.Abs(Vector3.Dot(hit.Normal, dirSample.Direction)) / + float.Abs(Vector3.Dot(hit.ShadingNormal, dirSample.Direction)); + + // Rendering equation cosine cancels with the Jacobian, but only if geometry and shading geometry align + dirSample.Weight *= + float.Abs(Vector3.Dot(hit.ShadingNormal, -ray.Direction)) / + float.Abs(Vector3.Dot(hit.Normal, -ray.Direction)); + + SanityChecks.IsNormalized(ray.Direction); + } + // Continue the path with the next ray prefixWeight *= dirSample.Weight / survivalProb; depth++;