From ffc73277970b061ebd67d9b493278eaba519a4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mercan=20Yazici?= Date: Fri, 24 Jul 2026 10:07:15 +0200 Subject: [PATCH 01/10] track average camera path length even on the path tracer --- SeeSharp/Integrators/PathTracer.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SeeSharp/Integrators/PathTracer.cs b/SeeSharp/Integrators/PathTracer.cs index 4689fe2..0a0b585 100644 --- a/SeeSharp/Integrators/PathTracer.cs +++ b/SeeSharp/Integrators/PathTracer.cs @@ -41,6 +41,13 @@ public class PathTracerBase : Integrator { TechPyramid techPyramidRaw; TechPyramid techPyramidWeighted; + ThreadLocal totalCamPathLen; + + /// + /// Average number of camera path vertices per pixel, of the last finished iteration. + /// + public float AverageCameraPathLength { get; private set; } + protected DenoiseBuffers denoiseBuffers; /// @@ -220,6 +227,7 @@ public override void Render(Scene scene) { scene.FrameBuffer.StartIteration(); timer.EndFrameBuffer(); + totalCamPathLen = new(true); OnPreIteration(sampleIndex); Parallel.For(0, scene.FrameBuffer.Height, row => { for (uint col = 0; col < scene.FrameBuffer.Width; ++col) { @@ -228,6 +236,9 @@ public override void Render(Scene scene) { RenderPixel((uint)row, col, ref rng, null); } }); + ulong totalVertices = 0; + foreach (ulong v in totalCamPathLen.Values) totalVertices += v; + AverageCameraPathLength = totalVertices / (float)(scene.FrameBuffer.Width * scene.FrameBuffer.Height); OnPostIteration(sampleIndex); timer.EndRender(); @@ -305,6 +316,7 @@ protected virtual RgbColor RenderPixel(uint row, uint col, ref RNG rng, PathGrap protected virtual RgbColor EstimateIncidentRadiance(Ray ray, ref PathState state, PathGraphNode graphVertex = null) { RgbColor radianceEstimate = RgbColor.Black; + ulong numVertices = 0; while (state.Depth <= MaxDepth) { var hit = scene.Raytracer.Trace(ray); @@ -320,6 +332,7 @@ protected virtual RgbColor EstimateIncidentRadiance(Ray ray, ref PathState state break; } + numVertices++; OnHit(ray, hit, ref state); SurfaceShader shader = new(hit, -ray.Direction, false); @@ -369,6 +382,8 @@ protected virtual RgbColor EstimateIncidentRadiance(Ray ray, ref PathState state state.PreviousSurvivalProbability = survivalProb; } + totalCamPathLen?.Value += numVertices; + return radianceEstimate; } From b881304b0b532994e475a8d53f03ed1c589b9cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mercan=20Yazici?= Date: Fri, 24 Jul 2026 10:08:07 +0200 Subject: [PATCH 02/10] capture potential memory leak in vcm when it aborts due to a render fail --- .../Integrators/Bidir/VertexConnectionAndMerging.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs b/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs index 4890f43..f3efadb 100644 --- a/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs +++ b/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs @@ -240,7 +240,17 @@ public override void Render(Scene scene) if (photonMap == null) photonMap = new(); - base.Render(scene); + try + { + base.Render(scene); + } + catch + { + // Always dispose the photon map or memory might leak + photonMap.Dispose(); + photonMap = null; + throw; + } // Store the technique pyramids if (RenderTechniquePyramid) From 8cf9d12f4173e581bcc40dba6cc4fe8c4f450f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mercan=20Yazici?= Date: Fri, 24 Jul 2026 10:31:55 +0200 Subject: [PATCH 03/10] fix bias in VCM when NumShadowRays != 0. This puts NumShadowRays into BidirBase. Fixes CameraStoringVCM, ClassicBidir, VertexCacheBidir and VCM. --- SeeSharp/Integrators/Bidir/BidirBase.Camera.cs | 17 ++++++++--------- SeeSharp/Integrators/Bidir/BidirBase.cs | 6 ++++++ SeeSharp/Integrators/Bidir/CameraStoringVCM.cs | 11 +++++------ SeeSharp/Integrators/Bidir/ClassicBidir.cs | 18 ------------------ SeeSharp/Integrators/Bidir/VertexCacheBidir.cs | 17 ----------------- 5 files changed, 19 insertions(+), 50 deletions(-) diff --git a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs index fabbfa3..14c184b 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs @@ -325,7 +325,7 @@ protected virtual RgbColor BidirConnections(in SurfaceShader shader, ref RNG rng protected virtual (Emitter, SurfaceSample) SampleNextEvent(SurfacePoint from, ref RNG rng) { var (light, lightProb) = SelectLight(from, ref rng); var lightSample = light.SampleUniformArea(rng.NextFloat2D()); - lightSample.Pdf *= lightProb; + lightSample.Pdf *= lightProb * NumShadowRays; return (light, lightSample); } @@ -339,10 +339,10 @@ protected virtual float NextEventPdf(SurfacePoint from, SurfacePoint to) { float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); if (to.Mesh == null) { // Background var direction = to.Position - from.Position; - return Scene.Background.DirectionPdf(direction) * backgroundProbability; + return Scene.Background.DirectionPdf(direction) * backgroundProbability * NumShadowRays; } else { // Emissive object var emitter = Scene.QueryEmitter(to); - return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability); + return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability) * NumShadowRays; } } @@ -383,8 +383,8 @@ protected virtual RgbColor PerformNextEventEstimation(in SurfaceShader shader, r return RgbColor.Black; // There is no background var sample = Scene.Background.SampleDirection(rng.NextFloat2D()); - sample.Pdf *= backgroundProbability; - sample.Weight /= backgroundProbability; + sample.Pdf *= backgroundProbability * NumShadowRays; + sample.Weight /= backgroundProbability * NumShadowRays; if (sample.Pdf == 0) // Prevent NaN return RgbColor.Black; @@ -536,10 +536,9 @@ protected virtual RgbColor OnBackgroundHit(Ray ray, ref CameraPath path) { // Compute the pdf of sampling the previous point by emission from the background float pdfEmit = ComputeBackgroundPdf(ray.Origin, -ray.Direction); - // Compute the pdf of sampling the same connection via next event estimation - float pdfNextEvent = Scene.Background.DirectionPdf(ray.Direction); - float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); - pdfNextEvent *= backgroundProbability; + // Compute the pdf of sampling the same connection via next event estimation. + float pdfNextEvent = NextEventPdf(new SurfacePoint { Position = ray.Origin }, + new SurfacePoint { Position = ray.Origin + ray.Direction }); int numPdfs = path.Vertices.Count; int lastCameraVertexIdx = numPdfs - 1; diff --git a/SeeSharp/Integrators/Bidir/BidirBase.cs b/SeeSharp/Integrators/Bidir/BidirBase.cs index 8a90b52..d5a237d 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.cs @@ -21,6 +21,12 @@ public abstract partial class BidirBase : Integrator /// public int NumLightPaths { get; set; } = -1; + /// + /// Number of shadow rays (next event estimation samples) per camera vertex. Zero disables the technique. + /// Must only be changed in-between rendering iterations. Otherwise: mayhem. + /// + public int NumShadowRays = 1; + /// /// The base seed to generate camera paths. /// diff --git a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs index 365710a..12b2dff 100644 --- a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs +++ b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs @@ -413,10 +413,9 @@ public virtual (float MISWeight, RgbColor UnweightedContrib) OnMissCameraPath(Ra // Compute the pdf of sampling the previous point by emission from the background float pdfEmit = ComputeBackgroundPdf(ray.Origin, -ray.Direction); - // Compute the pdf of sampling the same connection via next event estimation - float pdfNextEvent = Scene.Background.DirectionPdf(ray.Direction); - float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); - pdfNextEvent *= backgroundProbability; + // Compute the pdf of sampling the same connection via next event estimation. + float pdfNextEvent = NextEventPdf(new SurfacePoint { Position = ray.Origin }, + new SurfacePoint { Position = ray.Origin + ray.Direction }); var pathPdfs = new BidirPathPdfs(stackalloc float[state.Depth], stackalloc float[state.Depth]); pathPdfs.GatherCameraPdfs(state, state.Depth - 2); @@ -539,8 +538,8 @@ public virtual RgbColor PerformNextEventEstimation(in SurfaceShader shader, in C return RgbColor.Black; // There is no background var sample = Scene.Background.SampleDirection(state.Rng.NextFloat2D()); - sample.Pdf *= backgroundProbability; - sample.Weight /= backgroundProbability; + sample.Pdf *= backgroundProbability * NumShadowRays; + sample.Weight /= backgroundProbability * NumShadowRays; if (sample.Pdf == 0) // Prevent NaN return RgbColor.Black; diff --git a/SeeSharp/Integrators/Bidir/ClassicBidir.cs b/SeeSharp/Integrators/Bidir/ClassicBidir.cs index f91eec0..a8efe50 100644 --- a/SeeSharp/Integrators/Bidir/ClassicBidir.cs +++ b/SeeSharp/Integrators/Bidir/ClassicBidir.cs @@ -29,27 +29,9 @@ public class ClassicBidirBase : BidirBase /// public bool EnableLightTracer = true; - /// - /// Number of shadow rays to use for next event estimation along the camera path. If set to zero, - /// no next event estimation is performed. - /// - public int NumShadowRays = 1; - TechPyramid techPyramidRaw; TechPyramid techPyramidWeighted; - /// - protected override float NextEventPdf(SurfacePoint from, SurfacePoint to) { - return base.NextEventPdf(from, to) * NumShadowRays; - } - - /// - protected override (Emitter, SurfaceSample) SampleNextEvent(SurfacePoint from, ref RNG rng) { - var (light, sample) = base.SampleNextEvent(from, ref rng); - sample.Pdf *= NumShadowRays; - return (light, sample); - } - /// protected override void RegisterSample(RgbColor weight, float misWeight, Pixel pixel, int cameraPathLength, int lightPathLength, int fullLength) { diff --git a/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs b/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs index 3f13e11..6b0a6a0 100644 --- a/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs +++ b/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs @@ -18,11 +18,6 @@ public class VertexCacheBidirBase : BidirBase public int NumConnections = 1; - /// - /// Number of shadow rays to use for next event. Disabled if zero. - /// - public int NumShadowRays = 1; - /// /// Set to false to disable connections between light vertices and the camera /// @@ -47,18 +42,6 @@ public class VertexCacheBidirBase : BidirBase protected VertexSelector vertexSelector; - /// - protected override float NextEventPdf(SurfacePoint from, SurfacePoint to) { - return base.NextEventPdf(from, to) * NumShadowRays; - } - - /// - protected override (Emitter, SurfaceSample) SampleNextEvent(SurfacePoint from, ref RNG rng) { - var (light, sample) = base.SampleNextEvent(from, ref rng); - sample.Pdf *= NumShadowRays; - return (light, sample); - } - /// protected override (int, int, float) SelectBidirPath(SurfacePoint cameraPoint, Vector3 outDir, Pixel pixel, ref RNG rng) { From 9996fb6e867dd50931aa8bc9ce56592f782b637d Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Fri, 24 Jul 2026 11:33:12 +0200 Subject: [PATCH 04/10] always dispose PM accel in CamStoringVCM --- .../Integrators/Bidir/CameraStoringVCM.cs | 194 +++++++++--------- 1 file changed, 100 insertions(+), 94 deletions(-) diff --git a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs index 12b2dff..a36c9a3 100644 --- a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs +++ b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs @@ -163,115 +163,121 @@ public virtual void BuildImportonAccel() { public override void Render(Scene scene) => Render(scene, 0); - public void Render(Scene scene, int startAtIteration) { - Scene = scene; - IsolatedPixel = null; - - if (NumLightPaths < 0) - NumLightPaths = scene.FrameBuffer.Width * scene.FrameBuffer.Height; + public void Render(Scene scene, int startAtIteration) + { + try + { + Scene = scene; + IsolatedPixel = null; - if (EnableDenoiser) - DenoiseBuffers = new(scene.FrameBuffer); + if (NumLightPaths < 0) + NumLightPaths = scene.FrameBuffer.Width * scene.FrameBuffer.Height; - OnBeforeRender(); + if (EnableDenoiser) + DenoiseBuffers = new(scene.FrameBuffer); - if (RenderTechniquePyramid && MaxDepth > 10) { - Logger.Warning("MaxDepth is set above 10, but a technique pyramid was requested (RenderTechniquePyramid == true). To avoid excessive memory consumption, the RenderTechniquePyramid flag will be ignored."); - RenderTechniquePyramid = false; - } + OnBeforeRender(); - if (RenderTechniquePyramid) { - TechPyramidRaw = new TechPyramid(scene.FrameBuffer.Width, scene.FrameBuffer.Height, - minDepth: 1, maxDepth: MaxDepth, merges: EnableMerging); - TechPyramidWeighted = new TechPyramid(scene.FrameBuffer.Width, scene.FrameBuffer.Height, - minDepth: 1, maxDepth: MaxDepth, merges: EnableMerging); - } - - CameraPaths = new(scene.FrameBuffer.Width * scene.FrameBuffer.Height, Math.Min(MaxDepth + 1, 10)); - photonMap ??= new(); - - ProgressBar progressBar = new(prefix: "Rendering..."); - progressBar.Start(NumIterations); - RenderTimer timer = new(); - Stopwatch lightTracerTimer = new(); - Stopwatch pathTracerTimer = new(); - Stopwatch accelBuildTimer = new(); - ShadingStatCounter.Reset(); - scene.Raytracer.ResetStats(); - for (uint iter = (uint)startAtIteration; iter - startAtIteration < NumIterations; ++iter) { - long nextIterTime = timer.RenderTime + timer.PerIterationCost; - if (MaximumRenderTimeMs.HasValue && nextIterTime > MaximumRenderTimeMs.Value) { - Logger.Log("Maximum render time exhausted."); - // if (EnableDenoiser) DenoiseBuffers.Denoise(); - progressBar.Terminate(); - break; + if (RenderTechniquePyramid && MaxDepth > 10) { + Logger.Warning("MaxDepth is set above 10, but a technique pyramid was requested (RenderTechniquePyramid == true). To avoid excessive memory consumption, the RenderTechniquePyramid flag will be ignored."); + RenderTechniquePyramid = false; } - timer.StartIteration(); - - scene.FrameBuffer.StartIteration(); - timer.EndFrameBuffer(); - - OnStartIteration(iter); - try { - pathTracerTimer.Start(); - Parallel.For(0, Scene.FrameBuffer.Height, row => { - for (uint col = 0; col < Scene.FrameBuffer.Width; ++col) { - uint pixelIndex = (uint)(row * Scene.FrameBuffer.Width + col); - var rng = new RNG(BaseSeedCamera, pixelIndex, iter); - TraceCameraPath((uint)row, col, ref rng); - } - }); - pathTracerTimer.Stop(); - - accelBuildTimer.Start(); - if (EnableMerging) - BuildImportonAccel(); - accelBuildTimer.Stop(); - - lightTracerTimer.Start(); - TraceLightPaths(iter); - lightTracerTimer.Stop(); - } catch { - Logger.Log($"Exception in iteration {iter} out of {NumIterations}.", Verbosity.Error); - throw; + if (RenderTechniquePyramid) { + TechPyramidRaw = new TechPyramid(scene.FrameBuffer.Width, scene.FrameBuffer.Height, + minDepth: 1, maxDepth: MaxDepth, merges: EnableMerging); + TechPyramidWeighted = new TechPyramid(scene.FrameBuffer.Width, scene.FrameBuffer.Height, + minDepth: 1, maxDepth: MaxDepth, merges: EnableMerging); } - OnEndIteration(iter); - CameraPaths.Clear(); - timer.EndRender(); - // if (iter == NumIterations - 1 && EnableDenoiser) - // DenoiseBuffers.Denoise(); + CameraPaths = new(scene.FrameBuffer.Width * scene.FrameBuffer.Height, Math.Min(MaxDepth + 1, 10)); + photonMap ??= new(); + + ProgressBar progressBar = new(prefix: "Rendering..."); + progressBar.Start(NumIterations); + RenderTimer timer = new(); + Stopwatch lightTracerTimer = new(); + Stopwatch pathTracerTimer = new(); + Stopwatch accelBuildTimer = new(); + ShadingStatCounter.Reset(); + scene.Raytracer.ResetStats(); + for (uint iter = (uint)startAtIteration; iter - startAtIteration < NumIterations; ++iter) { + long nextIterTime = timer.RenderTime + timer.PerIterationCost; + if (MaximumRenderTimeMs.HasValue && nextIterTime > MaximumRenderTimeMs.Value) { + Logger.Log("Maximum render time exhausted."); + // if (EnableDenoiser) DenoiseBuffers.Denoise(); + progressBar.Terminate(); + break; + } - scene.FrameBuffer.EndIteration(); - timer.EndFrameBuffer(); + timer.StartIteration(); + + scene.FrameBuffer.StartIteration(); + timer.EndFrameBuffer(); + + OnStartIteration(iter); + try { + pathTracerTimer.Start(); + Parallel.For(0, Scene.FrameBuffer.Height, row => { + for (uint col = 0; col < Scene.FrameBuffer.Width; ++col) { + uint pixelIndex = (uint)(row * Scene.FrameBuffer.Width + col); + var rng = new RNG(BaseSeedCamera, pixelIndex, iter); + TraceCameraPath((uint)row, col, ref rng); + } + }); + pathTracerTimer.Stop(); + + accelBuildTimer.Start(); + if (EnableMerging) + BuildImportonAccel(); + accelBuildTimer.Stop(); + + lightTracerTimer.Start(); + TraceLightPaths(iter); + lightTracerTimer.Stop(); + } catch { + Logger.Log($"Exception in iteration {iter} out of {NumIterations}.", Verbosity.Error); + throw; + } + OnEndIteration(iter); + CameraPaths.Clear(); + timer.EndRender(); - progressBar.ReportDone(1); - timer.EndIteration(); - } + // if (iter == NumIterations - 1 && EnableDenoiser) + // DenoiseBuffers.Denoise(); - scene.FrameBuffer.MetaData["RenderTime"] = timer.RenderTime; - scene.FrameBuffer.MetaData["FrameBufferTime"] = timer.FrameBufferTime; - scene.FrameBuffer.MetaData["PathTracerTime"] = pathTracerTimer.ElapsedMilliseconds; - scene.FrameBuffer.MetaData["LightTracerTime"] = lightTracerTimer.ElapsedMilliseconds; - scene.FrameBuffer.MetaData["ShadingStats"] = ShadingStatCounter.Current; - scene.FrameBuffer.MetaData["RayTracerStats"] = scene.Raytracer.Stats; - scene.FrameBuffer.MetaData["BaseSeed"] = BaseSeed; + scene.FrameBuffer.EndIteration(); + timer.EndFrameBuffer(); - OnAfterRender(); + progressBar.ReportDone(1); + timer.EndIteration(); + } - if (RenderTechniquePyramid) { - TechPyramidRaw.Normalize(1.0f / Scene.FrameBuffer.CurIteration); - if (!string.IsNullOrEmpty(scene.FrameBuffer.Basename)) - TechPyramidRaw.WriteToFiles(Path.Join(scene.FrameBuffer.Basename, "techs-raw")); + scene.FrameBuffer.MetaData["RenderTime"] = timer.RenderTime; + scene.FrameBuffer.MetaData["FrameBufferTime"] = timer.FrameBufferTime; + scene.FrameBuffer.MetaData["PathTracerTime"] = pathTracerTimer.ElapsedMilliseconds; + scene.FrameBuffer.MetaData["LightTracerTime"] = lightTracerTimer.ElapsedMilliseconds; + scene.FrameBuffer.MetaData["ShadingStats"] = ShadingStatCounter.Current; + scene.FrameBuffer.MetaData["RayTracerStats"] = scene.Raytracer.Stats; + scene.FrameBuffer.MetaData["BaseSeed"] = BaseSeed; - TechPyramidWeighted.Normalize(1.0f / Scene.FrameBuffer.CurIteration); - if (!string.IsNullOrEmpty(scene.FrameBuffer.Basename)) - TechPyramidWeighted.WriteToFiles(Path.Join(scene.FrameBuffer.Basename, "techs-weighted")); - } + OnAfterRender(); - photonMap.Dispose(); - photonMap = null; + if (RenderTechniquePyramid) { + TechPyramidRaw.Normalize(1.0f / Scene.FrameBuffer.CurIteration); + if (!string.IsNullOrEmpty(scene.FrameBuffer.Basename)) + TechPyramidRaw.WriteToFiles(Path.Join(scene.FrameBuffer.Basename, "techs-raw")); + + TechPyramidWeighted.Normalize(1.0f / Scene.FrameBuffer.CurIteration); + if (!string.IsNullOrEmpty(scene.FrameBuffer.Basename)) + TechPyramidWeighted.WriteToFiles(Path.Join(scene.FrameBuffer.Basename, "techs-weighted")); + } + } + finally + { + photonMap?.Dispose(); + photonMap = null; + } } /// From 3e2d03fc59f19e3b7e14d2bee94eb587c4302b76 Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Fri, 24 Jul 2026 11:34:03 +0200 Subject: [PATCH 05/10] use try-finally --- .../Integrators/Bidir/VertexConnectionAndMerging.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs b/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs index f3efadb..d403f09 100644 --- a/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs +++ b/SeeSharp/Integrators/Bidir/VertexConnectionAndMerging.cs @@ -244,12 +244,10 @@ public override void Render(Scene scene) { base.Render(scene); } - catch + finally { - // Always dispose the photon map or memory might leak - photonMap.Dispose(); + photonMap?.Dispose(); photonMap = null; - throw; } // Store the technique pyramids @@ -267,9 +265,6 @@ public override void Render(Scene scene) Path.Join(scene.FrameBuffer.Basename, "techs-weighted") ); } - - photonMap.Dispose(); - photonMap = null; } Stopwatch mergeBuildTimer; @@ -936,4 +931,4 @@ in CorrelAwareRatios correlRatio sumReciprocals += nextReciprocal; // Hitting the emitter directly return sumReciprocals; } -} +} \ No newline at end of file From aa018d68db49efe2e5d2410558da70d4f6653709 Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Fri, 24 Jul 2026 11:44:32 +0200 Subject: [PATCH 06/10] pass meaningful parameters --- SeeSharp/Geometry/SurfacePoint.cs | 52 +++++++++++++++---- .../Integrators/Bidir/CameraStoringVCM.cs | 3 +- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/SeeSharp/Geometry/SurfacePoint.cs b/SeeSharp/Geometry/SurfacePoint.cs index 0937752..c2dccfb 100644 --- a/SeeSharp/Geometry/SurfacePoint.cs +++ b/SeeSharp/Geometry/SurfacePoint.cs @@ -4,41 +4,70 @@ /// Represents a point on the surface of a mesh in the scene. Wrapper around with /// additional SeeSharp specific material information. /// -public struct SurfacePoint { +public struct SurfacePoint +{ /// /// Position in world space /// - public Vector3 Position { get => hit.Position; set => hit.Position = value; } + public Vector3 Position + { + get => hit.Position; + set => hit.Position = value; + } /// /// Face normal at the point (i.e., actual geometric normal, not the shading normal) /// - public Vector3 Normal { get => hit.Normal; set => hit.Normal = value; } + public Vector3 Normal + { + get => hit.Normal; + set => hit.Normal = value; + } /// /// Barycentric coordinates within the primitive /// - public Vector2 BarycentricCoords { get => hit.BarycentricCoords; set => hit.BarycentricCoords = value; } + public Vector2 BarycentricCoords + { + get => hit.BarycentricCoords; + set => hit.BarycentricCoords = value; + } /// /// The mesh on which this point lies /// - public Mesh Mesh { get => hit.Mesh as Mesh; set => hit.Mesh = value; } + public Mesh Mesh + { + get => hit.Mesh as Mesh; + set => hit.Mesh = value; + } /// /// Index of the primitive within the mesh /// - public uint PrimId { get => hit.PrimId; set => hit.PrimId = value; } + public uint PrimId + { + get => hit.PrimId; + set => hit.PrimId = value; + } /// /// Offset that should be used to avoid self-intersection during ray tracing /// - public float ErrorOffset { get => hit.ErrorOffset; set => hit.ErrorOffset = value; } + public float ErrorOffset + { + get => hit.ErrorOffset; + set => hit.ErrorOffset = value; + } /// /// Distance from a previous point if this is a ray intersection /// - public float Distance { get => hit.Distance; set => hit.Distance = value; } + public float Distance + { + get => hit.Distance; + set => hit.Distance = value; + } /// /// Checks if the point is valid @@ -54,7 +83,8 @@ public struct SurfacePoint { /// Implicit cast from a TinyEmbree hit object for convenience /// /// - public static implicit operator SurfacePoint(Hit hit) { + public static implicit operator SurfacePoint(Hit hit) + { return new SurfacePoint { hit = hit }; } @@ -73,5 +103,7 @@ public static implicit operator SurfacePoint(Hit hit) { /// public Material Material => Mesh.Material; + public static SurfacePoint Invalid => new() { Mesh = null }; + Hit hit; -} +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs index a36c9a3..ba4ab36 100644 --- a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs +++ b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs @@ -420,8 +420,7 @@ public virtual (float MISWeight, RgbColor UnweightedContrib) OnMissCameraPath(Ra float pdfEmit = ComputeBackgroundPdf(ray.Origin, -ray.Direction); // Compute the pdf of sampling the same connection via next event estimation. - float pdfNextEvent = NextEventPdf(new SurfacePoint { Position = ray.Origin }, - new SurfacePoint { Position = ray.Origin + ray.Direction }); + float pdfNextEvent = NextEventPdf(state.Vertices[^1].Point, SurfacePoint.Invalid); var pathPdfs = new BidirPathPdfs(stackalloc float[state.Depth], stackalloc float[state.Depth]); pathPdfs.GatherCameraPdfs(state, state.Depth - 2); From 6203d508870eb2b6af07d42412bddfe0981b975b Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Fri, 24 Jul 2026 11:46:49 +0200 Subject: [PATCH 07/10] don't pass misleading / unused / nonsense data --- SeeSharp/Integrators/Bidir/BidirBase.Camera.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs index 14c184b..34eaa6d 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs @@ -537,8 +537,8 @@ protected virtual RgbColor OnBackgroundHit(Ray ray, ref CameraPath path) { float pdfEmit = ComputeBackgroundPdf(ray.Origin, -ray.Direction); // Compute the pdf of sampling the same connection via next event estimation. - float pdfNextEvent = NextEventPdf(new SurfacePoint { Position = ray.Origin }, - new SurfacePoint { Position = ray.Origin + ray.Direction }); + // TODO get the actual previous point (need the mesh, not just the position) + float pdfNextEvent = NextEventPdf(new SurfacePoint(), SurfacePoint.Invalid); int numPdfs = path.Vertices.Count; int lastCameraVertexIdx = numPdfs - 1; From 677c3f3f68c36d4ed23da040af39857202a27e1d Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Fri, 24 Jul 2026 12:06:25 +0200 Subject: [PATCH 08/10] separate bgn and area light pdf --- .../Integrators/Bidir/BidirBase.Camera.cs | 28 +++++++++++-------- SeeSharp/Integrators/Bidir/BidirBase.Light.cs | 6 ++-- .../Integrators/Bidir/CameraStoringVCM.cs | 24 ++++++++++------ 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs index 34eaa6d..6c27c4f 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs @@ -330,20 +330,26 @@ protected virtual (Emitter, SurfaceSample) SampleNextEvent(SurfacePoint from, re } /// - /// Computes the pdf used by + /// Computes the pdf used by for an area light source. Assumes that `to` is a valid point on an emitter. /// /// The shading point /// The point on the light source /// PDF of next event estimation - protected virtual float NextEventPdf(SurfacePoint from, SurfacePoint to) { + public virtual float NextEventPdf(SurfacePoint from, SurfacePoint to) { float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); - if (to.Mesh == null) { // Background - var direction = to.Position - from.Position; - return Scene.Background.DirectionPdf(direction) * backgroundProbability * NumShadowRays; - } else { // Emissive object - var emitter = Scene.QueryEmitter(to); - return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability) * NumShadowRays; - } + var emitter = Scene.QueryEmitter(to); + return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability) * NumShadowRays; + } + + /// + /// Computes the pdf used by for a background ray direction. + /// + /// The shading point + /// The direction from the shading point to the background + /// PDF of next event estimation + public virtual float NextEventPdf(SurfacePoint from, Vector3 direction) { + float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); + return Scene.Background.DirectionPdf(direction) * backgroundProbability * NumShadowRays; } /// @@ -503,7 +509,7 @@ protected virtual RgbColor OnEmitterHit(Emitter emitter, SurfacePoint hit, Vecto // Compute pdf values float pdfEmit = ComputeEmitterPdf(emitter, hit, outDir, reversePdfJacobian); - float pdfNextEvent = NextEventPdf(new SurfacePoint(), hit); // TODO get the actual previous point! + float pdfNextEvent = NextEventPdf(SurfacePoint.Invalid, hit); // TODO get the actual previous point! int numPdfs = path.Vertices.Count; int lastCameraVertexIdx = numPdfs - 1; @@ -538,7 +544,7 @@ protected virtual RgbColor OnBackgroundHit(Ray ray, ref CameraPath path) { // Compute the pdf of sampling the same connection via next event estimation. // TODO get the actual previous point (need the mesh, not just the position) - float pdfNextEvent = NextEventPdf(new SurfacePoint(), SurfacePoint.Invalid); + float pdfNextEvent = NextEventPdf(SurfacePoint.Invalid, ray.Direction); int numPdfs = path.Vertices.Count; int lastCameraVertexIdx = numPdfs - 1; diff --git a/SeeSharp/Integrators/Bidir/BidirBase.Light.cs b/SeeSharp/Integrators/Bidir/BidirBase.Light.cs index 6dfa427..f0197e6 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.Light.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.Light.cs @@ -74,7 +74,9 @@ void ConnectLightVertexToCamera(in PathVertex vertex, in PathVertex ancestor, Ve pathPdfs.PdfsCameraToLight[0] = response.PdfEmit; pathPdfs.PdfsCameraToLight[1] = pdfReverse; if (vertex.Depth == 1) - pathPdfs.PdfNextEvent = NextEventPdf(vertex.Point, ancestor.Point); + pathPdfs.PdfNextEvent = ancestor.Point + ? NextEventPdf(vertex.Point, ancestor.Point) + : NextEventPdf(vertex.Point, ancestor.Point.Position - vertex.Point.Position); float misWeight = LightTracerMis(vertex, pathPdfs, response.Pixel, distToCam); @@ -201,7 +203,7 @@ public virtual void TraceLightPaths(uint seed, uint iter) { else PathCache.Clear(); - LightPathWalk walkModifier = new(PathCache, (to, from, _) => NextEventPdf(from, to)); + LightPathWalk walkModifier = new(PathCache, (to, from, _) => to ? NextEventPdf(from, to) : NextEventPdf(from, to.Position - from.Position)); Parallel.For(0, NumLightPaths, idx => { var rng = new RNG(seed, (uint)idx, iter); diff --git a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs index ba4ab36..b1c0a47 100644 --- a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs +++ b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs @@ -420,7 +420,7 @@ public virtual (float MISWeight, RgbColor UnweightedContrib) OnMissCameraPath(Ra float pdfEmit = ComputeBackgroundPdf(ray.Origin, -ray.Direction); // Compute the pdf of sampling the same connection via next event estimation. - float pdfNextEvent = NextEventPdf(state.Vertices[^1].Point, SurfacePoint.Invalid); + float pdfNextEvent = NextEventPdf(state.Vertices[^1].Point, ray.Direction); var pathPdfs = new BidirPathPdfs(stackalloc float[state.Depth], stackalloc float[state.Depth]); pathPdfs.GatherCameraPdfs(state, state.Depth - 2); @@ -497,20 +497,26 @@ public virtual (Emitter, SurfaceSample) SampleNextEvent(SurfacePoint from, float } /// - /// Computes the pdf used by + /// Computes the pdf used by for an area light source. Assumes that `to` is a valid point on an emitter. /// /// The shading point /// The point on the light source /// PDF of next event estimation public virtual float NextEventPdf(SurfacePoint from, SurfacePoint to) { float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); - if (to.Mesh == null) { // Background - var direction = to.Position - from.Position; - return Scene.Background.DirectionPdf(direction) * backgroundProbability * NumShadowRays; - } else { // Emissive object - var emitter = Scene.QueryEmitter(to); - return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability) * NumShadowRays; - } + var emitter = Scene.QueryEmitter(to); + return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability) * NumShadowRays; + } + + /// + /// Computes the pdf used by for a background ray direction. + /// + /// The shading point + /// The direction from the shading point to the background + /// PDF of next event estimation + public virtual float NextEventPdf(SurfacePoint from, Vector3 direction) { + float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); + return Scene.Background.DirectionPdf(direction) * backgroundProbability * NumShadowRays; } /// From 9dd2c081cd18977722d133a48f7bb34e092b031c Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Fri, 24 Jul 2026 12:10:57 +0200 Subject: [PATCH 09/10] fix --- SeeSharp/Integrators/Bidir/CameraStoringVCM.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs index b1c0a47..4cd96b1 100644 --- a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs +++ b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs @@ -503,6 +503,9 @@ public virtual (Emitter, SurfaceSample) SampleNextEvent(SurfacePoint from, float /// The point on the light source /// PDF of next event estimation public virtual float NextEventPdf(SurfacePoint from, SurfacePoint to) { + // Switch to background case if "to" is a position in free space, not on a mesh + if (!to) return NextEventPdf(from, to.Position - from.Position); + float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); var emitter = Scene.QueryEmitter(to); return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability) * NumShadowRays; From 1e572dece5154b1bce3392713859a7e9b45257f7 Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Fri, 24 Jul 2026 12:11:27 +0200 Subject: [PATCH 10/10] better safe than sorry :D --- SeeSharp/Integrators/Bidir/BidirBase.Camera.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs index 6c27c4f..b90e0cd 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.Camera.cs @@ -336,6 +336,9 @@ protected virtual (Emitter, SurfaceSample) SampleNextEvent(SurfacePoint from, re /// The point on the light source /// PDF of next event estimation public virtual float NextEventPdf(SurfacePoint from, SurfacePoint to) { + // Switch to background case if "to" is a position in free space, not on a mesh + if (!to) return NextEventPdf(from, to.Position - from.Position); + float backgroundProbability = ComputeNextEventBackgroundProbability(/*hit*/); var emitter = Scene.QueryEmitter(to); return emitter.PdfUniformArea(to) * SelectLightPmf(from, emitter) * (1 - backgroundProbability) * NumShadowRays;