diff --git a/.gitignore b/.gitignore index e657b0a2..0315f7dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ # User specific editor and IDE configurations .vs -launchSettings.json -!SeeSharp.Templates/content/SeeSharp.Blazor.Template/Properties/launchSettings.json .idea SeeSharp.sln.DotSettings.user diff --git a/MaterialTest/Pages/IntegratorTest.razor.cs b/MaterialTest/Pages/IntegratorTest.razor.cs index 521cf499..ebc203df 100644 --- a/MaterialTest/Pages/IntegratorTest.razor.cs +++ b/MaterialTest/Pages/IntegratorTest.razor.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Components; -using SeeSharp.Blazor; namespace MaterialTest.Pages; @@ -22,7 +21,7 @@ void RunExperiment() scene.Prepare(); VertexConnectionAndMerging vcm = new() { - NumIterations = NumSamples, + NumIterations = (uint)NumSamples, MaxDepth = MaxDepth, RenderTechniquePyramid = true }; diff --git a/README.md b/README.md index 376cafac..476ec0a8 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ using SeeSharp.Integrators.Bidir; // Configure an experiment that compares VCM and path tracing. class PathVsVcm : Experiment { public override List MakeMethods() => new() { - new("PathTracer", new PathTracer() { MaxDepth = 5, TotalSpp = 4 }), + new("PathTracer", new PathTracer() { MaxDepth = 5, NumIterations = 4 }), new("Vcm", new VertexConnectionAndMerging() { MaxDepth = 5, NumIterations = 2 }) }; } diff --git a/SeeSharp.Benchmark/GenericMaterial_Sampling.cs b/SeeSharp.Benchmark/GenericMaterial_Sampling.cs index 7d468e56..bb8ab86f 100644 --- a/SeeSharp.Benchmark/GenericMaterial_Sampling.cs +++ b/SeeSharp.Benchmark/GenericMaterial_Sampling.cs @@ -69,7 +69,7 @@ static float TestRender(GenericMaterial.Parameters parameters, string name) { scene.Prepare(); PathTracer integrator = new() { - TotalSpp = 1, + NumIterations = 1, NumShadowRays = 0, MaxDepth = 2, }; diff --git a/SeeSharp.Benchmark/Program.cs b/SeeSharp.Benchmark/Program.cs index ff46c0ad..9eb54f27 100644 --- a/SeeSharp.Benchmark/Program.cs +++ b/SeeSharp.Benchmark/Program.cs @@ -7,7 +7,7 @@ SceneRegistry.AddSourceRelativeToScript("../data/scenes"); BenchRender("PathTracer - 16spp", new PathTracer() { - TotalSpp = 16, + NumIterations = 16, }); BenchRender("BDPT - 8spp", new VertexCacheBidir() { @@ -51,4 +51,4 @@ void BenchRender(string name, Integrator integrator) { GenericMaterial_Sampling.Benchmark(); -VectorBench.BenchComputeBasisVectors(10000000); +VectorBench.BenchComputeBasisVectors(10000000); \ No newline at end of file diff --git a/SeeSharp.Blazor/LongSetting.razor b/SeeSharp.Blazor/LongSetting.razor new file mode 100644 index 00000000..fab53adb --- /dev/null +++ b/SeeSharp.Blazor/LongSetting.razor @@ -0,0 +1,25 @@ +@using Microsoft.AspNetCore.Mvc + +@namespace SeeSharp.Blazor + +@inherits SettingBase + +@{ + base.BuildRenderTree(__builder); +} + +@code { + protected override string Type => "number"; + + protected override long ParseValue(ChangeEventArgs e) + { + if (long.TryParse((string)e.Value, out long result)) + return result; + return Value; + } + + protected override Dictionary CustomAttributes => new() { + { "step", 1 } + }; +} + diff --git a/SeeSharp.Blazor/SceneSelector.razor b/SeeSharp.Blazor/SceneSelector.razor index 77a46387..6a7f8fcf 100644 --- a/SeeSharp.Blazor/SceneSelector.razor +++ b/SeeSharp.Blazor/SceneSelector.razor @@ -49,7 +49,7 @@ } else if (!string.IsNullOrEmpty(scene?.Name)) { -

Loaded "@(scene.Name)"

+

Loaded "@(scene.Name)" from "@scene.SourceDirectory"

} diff --git a/SeeSharp.Examples/MisCompensation.dib b/SeeSharp.Examples/MisCompensation.dib index cd01526b..f0a840a0 100644 --- a/SeeSharp.Examples/MisCompensation.dib +++ b/SeeSharp.Examples/MisCompensation.dib @@ -25,7 +25,7 @@ scene.FrameBuffer = new(512, 512, ""); scene.Prepare(); new PathTracer() { - TotalSpp = 10 + NumIterations = 10 }.Render(scene); var plain = scene.FrameBuffer.Image; @@ -36,7 +36,7 @@ scene.FrameBuffer = new(512, 512, ""); scene.Prepare(); new PathTracer() { - TotalSpp = 10 + NumIterations = 10 }.Render(scene); HTML(FlipBook.Make(("plain", plain), ("comp", scene.FrameBuffer.Image))) diff --git a/SeeSharp.Examples/PathVsVcm.cs b/SeeSharp.Examples/PathVsVcm.cs index d33ec990..d28faa13 100644 --- a/SeeSharp.Examples/PathVsVcm.cs +++ b/SeeSharp.Examples/PathVsVcm.cs @@ -1,8 +1,3 @@ -using SeeSharp.Experiments; -using SeeSharp.Integrators; -using SeeSharp.Integrators.Bidir; -using System.Collections.Generic; - namespace SeeSharp.Examples; /// @@ -10,7 +5,7 @@ namespace SeeSharp.Examples; /// class PathVsVcm : Experiment { public override List MakeMethods() => [ - new("PathTracer", new PathTracer() { TotalSpp = 4 }), + new("PathTracer", new PathTracer() { NumIterations = 4 }), new("Vcm", new VertexConnectionAndMerging() { NumIterations = 2 }) ]; } \ No newline at end of file diff --git a/SeeSharp.Examples/SphericalSampling.dib b/SeeSharp.Examples/SphericalSampling.dib index 0844d17e..e7aa1cff 100644 --- a/SeeSharp.Examples/SphericalSampling.dib +++ b/SeeSharp.Examples/SphericalSampling.dib @@ -37,18 +37,16 @@ scene.Prepare(); #!csharp abstract class DirectIllum : Integrator { - public int TotalSpp = 20; - protected Scene scene; public DirectIllum(int spp) { - TotalSpp = spp; + SampleCount = spp; } public override void Render(Scene scene) { this.scene = scene; - for (uint sampleIndex = 0; sampleIndex < TotalSpp; ++sampleIndex) { + for (uint sampleIndex = 0; sampleIndex < SampleCount; ++sampleIndex) { scene.FrameBuffer.StartIteration(); Parallel.For(0, scene.FrameBuffer.Height, row => { for (uint col = 0; col < scene.FrameBuffer.Width; ++col) { diff --git a/SeeSharp.IntegrationTests/LightProbeTest.cs b/SeeSharp.IntegrationTests/LightProbeTest.cs index a66136b5..7171b85c 100644 --- a/SeeSharp.IntegrationTests/LightProbeTest.cs +++ b/SeeSharp.IntegrationTests/LightProbeTest.cs @@ -88,4 +88,4 @@ public static void CornellProbe() { scene.FrameBuffer.WriteToFile(); } } -} +} \ No newline at end of file diff --git a/SeeSharp.IntegrationTests/OutlierCacheTest.cs b/SeeSharp.IntegrationTests/OutlierCacheTest.cs index 2198fe85..89e2efa4 100644 --- a/SeeSharp.IntegrationTests/OutlierCacheTest.cs +++ b/SeeSharp.IntegrationTests/OutlierCacheTest.cs @@ -15,7 +15,7 @@ public static void RenderPT() { scene.Prepare(); var integrator = new PathTracer() { - TotalSpp = 4, + NumIterations = 4, MaxDepth = 10, BaseSeed = 1234, }; diff --git a/SeeSharp.IntegrationTests/Program.cs b/SeeSharp.IntegrationTests/Program.cs index 038ddf71..41bfb769 100644 --- a/SeeSharp.IntegrationTests/Program.cs +++ b/SeeSharp.IntegrationTests/Program.cs @@ -12,7 +12,7 @@ public static void PathTracerTimeBudget() { scene.Prepare(); var integrator = new PathTracer() { - TotalSpp = 498989, + NumIterations = 498989, MaximumRenderTimeMs = 4500, MaxDepth = 5 }; @@ -43,4 +43,4 @@ static void Main(string[] args) { // OutlierCacheTest.RenderPT(); OutlierCacheTest.RenderVCM(); } -} +} \ No newline at end of file diff --git a/SeeSharp.PreviewRender/Program.cs b/SeeSharp.PreviewRender/Program.cs index d4e5209b..56e73839 100644 --- a/SeeSharp.PreviewRender/Program.cs +++ b/SeeSharp.PreviewRender/Program.cs @@ -44,12 +44,12 @@ static int Main( if (algo == "PT") { new PathTracer() { MaxDepth = maxdepth, - TotalSpp = samples, + NumIterations = (uint)samples, }.Render(sc); } else if (algo == "VCM") { new VertexConnectionAndMerging() { MaxDepth = maxdepth, - NumIterations = samples, + NumIterations = (uint)samples, }.Render(sc); } else { Logger.Error($"Unknown rendering algorithm: {algo}. Use PT or VCM"); @@ -67,4 +67,4 @@ static int Main( return 0; } -} +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/App.razor b/SeeSharp.ReferenceManager/App.razor new file mode 100644 index 00000000..6fd3ed1b --- /dev/null +++ b/SeeSharp.ReferenceManager/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/SeeSharp.ReferenceManager/Imports.cs b/SeeSharp.ReferenceManager/Imports.cs new file mode 100644 index 00000000..b6c33e5a --- /dev/null +++ b/SeeSharp.ReferenceManager/Imports.cs @@ -0,0 +1,32 @@ +global using System; +global using System.Collections.Concurrent; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.IO; +global using System.Linq; +global using System.Numerics; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Threading; +global using System.Threading.Tasks; + +global using TinyEmbree; +global using SimpleImageIO; + +global using SeeSharp; +global using SeeSharp.Cameras; +global using SeeSharp.Common; +global using SeeSharp.Experiments; +global using SeeSharp.Geometry; +global using SeeSharp.Images; +global using SeeSharp.Integrators; +global using SeeSharp.Integrators.Bidir; +global using SeeSharp.Integrators.Common; +global using SeeSharp.Integrators.Util; +global using SeeSharp.Sampling; +global using SeeSharp.Shading; +global using SeeSharp.Shading.Background; +global using SeeSharp.Shading.Emitters; +global using SeeSharp.Shading.Materials; + +global using SeeSharp.Blazor; diff --git a/SeeSharp.ReferenceManager/MainLayout.razor b/SeeSharp.ReferenceManager/MainLayout.razor new file mode 100644 index 00000000..a5af3489 --- /dev/null +++ b/SeeSharp.ReferenceManager/MainLayout.razor @@ -0,0 +1,3 @@ +@inherits LayoutComponentBase + +
@Body
diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor new file mode 100644 index 00000000..de298dcd --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor @@ -0,0 +1,52 @@ +@using SeeSharp.Experiments +@using SeeSharp +@using SeeSharp.Blazor +@using SeeSharp.Integrators +@using System.Reflection +@using System.Collections.Generic +@using System.ComponentModel + +
+
+ + +
+
+ +
+ @if (CurrentIntegrator != null) + { + var groups = GetParameterGroups(CurrentIntegrator); + + @foreach (var group in groups) + { +
+ + @group.Title + + +
+ @foreach (var prop in group.Properties) + { + + } + @foreach (var field in group.Fields) + { + + } +
+
+ } + } +
\ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.cs b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.cs new file mode 100644 index 00000000..956decf0 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Components; + +namespace SeeSharp.ReferenceManager.Pages; + +public partial class IntegratorSelector : ComponentBase +{ + [Parameter] public Scene scene { get; set; } = default!; + + public List addedIntegrators { get; private set; } = new(); + + public Integrator CurrentIntegrator => addedIntegrators.FirstOrDefault(); + + Type[] integratorTypes = Array.Empty(); + string selectedIntegrator; + private string lastIntegrator; + + protected override void OnInitialized() + { + var types = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && !type.IsAbstract && typeof(Integrator).IsAssignableFrom(type) && + !type.ContainsGenericParameters && !typeof(DebugVisualizer).IsAssignableFrom(type)); + integratorTypes = types.Where(t => !types.Any(other => other.IsSubclassOf(t))).ToArray(); + + if (integratorTypes.Length > 0) { + selectedIntegrator = integratorTypes[0].FullName; + lastIntegrator = selectedIntegrator; + ReplaceIntegrator(); + } + DocumentationReader.LoadXmlDocumentation(typeof(Integrator).Assembly); + } + + void OnSelectionChanged(ChangeEventArgs e) + { + selectedIntegrator = e.Value?.ToString(); + if (!string.IsNullOrEmpty(selectedIntegrator)) + { + lastIntegrator = selectedIntegrator; + ReplaceIntegrator(); + } + } + + void ReplaceIntegrator() + { + if (string.IsNullOrEmpty(selectedIntegrator)) return; + var type = integratorTypes.FirstOrDefault(t => t.FullName == selectedIntegrator); + if (type == null) return; + + addedIntegrators.Clear(); + + var integrator = (Integrator)Activator.CreateInstance(type)!; + addedIntegrators.Add(integrator); + + StateHasChanged(); + } + + protected List GetParameterGroups(Integrator integrator) + => IntegratorUtils.GetParameterGroups(integrator); + + protected string FormatClassName(Type t) => IntegratorUtils.FormatClassName(t); + + public void TriggerReset() + { + selectedIntegrator = lastIntegrator; + ReplaceIntegrator(); + } + + public bool TrySelectIntegrator(string simpleName) + { + var targetType = integratorTypes.FirstOrDefault(t => t.Name == simpleName || t.Name == simpleName + "`1"); + + if (targetType != null && targetType.FullName != selectedIntegrator) + { + selectedIntegrator = targetType.FullName; + ReplaceIntegrator(); + return true; + } + return targetType != null; + } + + public Type GetIntegratorType(string simpleName) + { + if (string.IsNullOrEmpty(simpleName)) return null; + return integratorTypes.FirstOrDefault(t => + t.Name.Equals(simpleName, StringComparison.OrdinalIgnoreCase) || + t.Name.Equals(simpleName + "`1", StringComparison.OrdinalIgnoreCase)); + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css new file mode 100644 index 00000000..f33a08c6 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css @@ -0,0 +1,46 @@ +.integrator-selector { + display: flex; + flex-direction: column; + gap: 0.4em; + margin-bottom: 10px; +} + +.integrator-container { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +summary { + cursor: pointer; + font-weight: 600; + font-size: 0.85em; + color: #000; + outline: none; + display: flex; + align-items: center; + padding: 4px 0; +} +summary::before { + content: '▶'; + font-size: 0.7em; + margin-right: 6px; + display: inline-block; + transition: transform 0.2s; + color: #000; +} +details[open] > summary::before { transform: rotate(90deg); } +summary::-webkit-details-marker { display: none; } + + +details > div { + margin-left: 6px; + padding-left: 12px; + border-left: 2px solid #e0e0e0; + margin-top: 2px; + margin-bottom: 2px; +} +details:hover > div { + border-left-color: #ccc; +} + \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorUtils.cs b/SeeSharp.ReferenceManager/Pages/IntegratorUtils.cs new file mode 100644 index 00000000..f40c2fb8 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorUtils.cs @@ -0,0 +1,80 @@ +using System.Reflection; + +namespace SeeSharp.ReferenceManager.Pages; + +public class ParameterGroup { + public string Title { get; set; } = ""; + public List Properties { get; set; } = new(); + public List Fields { get; set; } = new(); + public bool HasParameters => Properties.Any() || Fields.Any(); +} + +public static class IntegratorUtils { + public static List GetParameterGroups(Integrator integrator) + { + var groups = new List(); + var currentType = integrator.GetType(); + + var allProps = GetFilteredProps(currentType); + var allFields = GetFilteredFields(currentType); + + while (currentType != null && currentType != typeof(object)) { + bool IsCurrentDeclared(MemberInfo m) + { + var d = m.DeclaringType; + var cur = currentType; + if (d != null && d.IsGenericType && !d.IsGenericTypeDefinition) d = d.GetGenericTypeDefinition(); + if (cur != null && cur.IsGenericType && !cur.IsGenericTypeDefinition) cur = cur.GetGenericTypeDefinition(); + return d == cur; + } + + string title = FormatClassName(currentType); + + bool isGlobalSettings = (currentType == typeof(Integrator)); + if (isGlobalSettings) title = "Global Settings"; + + var group = new ParameterGroup { + Title = title, + Properties = allProps.Where(p => IsCurrentDeclared(p)) + .Where(p => !isGlobalSettings || (p.Name != "MaxDepth" && p.Name != "MinDepth")).ToList(), + Fields = allFields.Where(f => IsCurrentDeclared(f)).ToList() + }; + + if (group.HasParameters) + groups.Add(group); + + currentType = currentType.BaseType; + } + + return groups; + } + + public static string FormatClassName(Type t) + { + string name = t.Name; + if (name.Contains('`')) name = name.Substring(0, name.IndexOf('`')); + return System.Text.RegularExpressions.Regex.Replace(name, "(\\B[A-Z])", " $1"); + } + + public static bool IsVisible(MemberInfo m) + { + if (m is PropertyInfo p && (!p.CanRead || !p.CanWrite)) return false; + if (m is FieldInfo f && (f.IsLiteral || f.IsInitOnly)) return false; + + Type t = (m is PropertyInfo pi) ? pi.PropertyType : ((FieldInfo)m).FieldType; + Type underlyingType = Nullable.GetUnderlyingType(t) ?? t; + + return underlyingType.IsPrimitive; + } + + public static IEnumerable GetFilteredProps(Type type) => + type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); + + public static IEnumerable GetFilteredFields(Type type) => + type.GetFields(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); + + public static string GetDescription(MemberInfo member) + { + return DocumentationReader.GetSummary(member) ?? ""; + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor new file mode 100644 index 00000000..12bcbca4 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor @@ -0,0 +1,252 @@ +@page "/" +@using SeeSharp.Experiments +@using SeeSharp.Integrators +@using SeeSharp.Blazor +@using System.IO +@using SimpleImageIO +@using System.Reflection +@using System.Threading.Tasks +@using System.Text.Json +@using System.Text.Json.Nodes +@using System.Diagnostics +@using System.Linq +@using System.Text.RegularExpressions; + +
+ +
+ +
+ +
+
Parameters
+
+ +
+ +
+ + +
+
+ +
+
Available references
+
+ @if (referenceFiles.Count == 0) { +
No references found
+ } + @foreach (var file in referenceFiles) { +
+
+ @file.Resolution, max depth: @file.MaxDepth + @if (file.MinDepth > 1) + { + , min depth: @file.MinDepth + } + @if (file.FilePath.EndsWith("-partial.exr")) { + (Partial) + } +
+
+ } +
+ +
+
Render new
+ +
+ +
+ x +
+
+ +
+ + +
+ +
+ + +
+ + + +
+ SPP: + + +
+ +
+ +
Render more
+ +
+ + +
+ + + + +
+
+ +
+
+
Viewer
+
+ @if (flip != null) { + + } + else { + Select a reference or render + } +
+
+ +
+
Info
+
+ @if (selectedFile != null) { +
+ RenderTime: @selectedFile.RenderTimeDisplay + StartTime: @selectedFile.StartTimeDisplay + WriteTime: @selectedFile.WriteTimeDisplay + Iterations: @selectedFile.Spp + Version: @selectedFile.Version +
+ + @if (isVersionMismatch) + { +
+ Version Differs @ReferenceUtils.CurrentSeeSharpVersion +
+ } + else if (isVersionWarning) + { +
+ Version Differs @ReferenceUtils.CurrentSeeSharpVersion +
+ } + + @if (selectedFile.RenderSteps != null && selectedFile.RenderSteps.Count > 0) + { +
+
+ Render History +
+ @for (int i = 0; i < selectedFile.RenderSteps.Count; i++) + { + var step = selectedFile.RenderSteps[i]; +
+
+ @(i + 1) @step.Type +
+
+ RenderTime: @(step.DurationMs)ms
+ StartTime: @step.StartTime
+ WriteTime: @step.WriteTime +
+
+ } +
+
+
+ } + +
+ @if (isStructureMismatch) + { +
Parameters Mismatch
+ } +
+ Integrator Settings + +
+ +
+ +
+
+ Integrator: + @selectedFile.IntegratorName +
+ @{ + if (!string.IsNullOrEmpty(selectedFile.RawJsonConfig)) + { + var node = System.Text.Json.Nodes.JsonNode.Parse(selectedFile.RawJsonConfig); + if (node != null) + { + var ignoredKeys = new HashSet { + "RenderTime", "NumIterations", "RenderStartTime", "RenderWriteTime", + "SeeSharpVersion", "RenderSteps", "NaNWarnings" + }; + foreach (var kvp in node.AsObject()) + { + bool isExtra = extraKeys.Contains(kvp.Key); + if (!isExtra && ignoredKeys.Contains(kvp.Key)) continue; + + string valStr = kvp.Value?.ToString() ?? "null"; + string typeClass = "val"; + if (kvp.Value is System.Text.Json.Nodes.JsonValue jval) { + if (jval.TryGetValue(out _)) typeClass = "bool"; + else if (jval.TryGetValue(out _)) typeClass = "str"; + } + + string styleKey = isExtra ? "color: red; " : ""; + string titleAttr = isExtra ? "Parameter not found in current code" : ""; + +
+ @kvp.Key: + @valStr +
+ } + + if (missingKeys.Count > 0) + { +
+
Missing Parameters:
+ @foreach (var key in missingKeys) + { +
+ @key +
+ } +
+ } + } + } + } +
+
+
+ } + else { +
No image selected
+ } +
+
+ +
+
diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.cs b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.cs new file mode 100644 index 00000000..c10464a1 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.cs @@ -0,0 +1,484 @@ +using System.Text.Json.Nodes; + +namespace SeeSharp.ReferenceManager.Pages; + +public partial class ReferenceRendering +{ + private SceneSelector sceneSelector; + private IntegratorSelector integratorSelector; + private Scene scene; + private SceneFromFile currentSceneFile; + private string currentSceneDirectory = ""; + private FlipBook flip; + + private List referenceFiles = new(); + private ReferenceInfo selectedFile; + + private int renderWidth = 512; + private int renderHeight = 512; + private int renderMaxDepth = 5; + private int renderMinDepth = 1; + private int quickPreviewSpp = 1; + private bool isRendering = false; + private int additionalSpp = 32; + private bool isStructureMismatch = false; + private bool isVersionMismatch = false; + private bool isVersionWarning = false; + private HashSet extraKeys = new(); + private HashSet missingKeys = new(); + + private void OnSceneLoaded(SceneFromFile sceneFromFile) + { + currentSceneFile = sceneFromFile; + scene = sceneFromFile.MakeScene(); + + if (sceneFromFile != null) + { + currentSceneDirectory = sceneFromFile.SourceDirectory; + if (!string.IsNullOrEmpty(currentSceneDirectory)) + { + ReferenceUtils.ScanReferences(currentSceneDirectory, referenceFiles); + } + } + } + + private void ResetConfig() => integratorSelector?.TriggerReset(); + + private void SaveSceneConfig() + { + if (integratorSelector?.CurrentIntegrator == null) + return; + string folder = Path.Combine(currentSceneDirectory, "References"); + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + ReferenceUtils.SaveConfig(folder, integratorSelector.CurrentIntegrator); + StateHasChanged(); + } + + private void ApplyReferenceSettings() + { + if (selectedFile == null || string.IsNullOrEmpty(selectedFile.RawJsonConfig)) + return; + if (integratorSelector == null) + return; + + if (!string.IsNullOrEmpty(selectedFile.IntegratorName)) + { + bool success = integratorSelector.TrySelectIntegrator(selectedFile.IntegratorName); + } + + var currentIntegrator = integratorSelector.CurrentIntegrator; + if (currentIntegrator == null) + return; + + var node = JsonNode.Parse(selectedFile.RawJsonConfig); + if (node != null) + { + var options = new JsonSerializerOptions + { + IncludeFields = true, + PropertyNameCaseInsensitive = true, + }; + var loadedIntegrator = JsonSerializer.Deserialize( + node, + currentIntegrator.GetType(), + options + ); + + if (loadedIntegrator != null) + { + ReferenceUtils.CopyValues(currentIntegrator, loadedIntegrator); + if (selectedFile.MaxDepth > 0) + renderMaxDepth = selectedFile.MaxDepth; + if (selectedFile.MinDepth > 0) + renderMinDepth = selectedFile.MinDepth; + StateHasChanged(); + } + } + } + + private void SelectReference(ReferenceInfo file) + { + selectedFile = file; + if (File.Exists(file.FilePath)) + UpdateViewerFromFile(file.FilePath); + + CheckParamsMatch(); + } + + private void UpdateViewerFromFile(string path) + { + flip = null; + StateHasChanged(); + + RgbImage img = null; + if (File.Exists(path)) + { + // support legacy .exr files + var layers = Layers.LoadFromFile(path); + if (layers.TryGetValue("", out Image image)) + img = SceneFromFile.InpaintNaNs(image) as RgbImage; + else if (layers.TryGetValue("default", out var defaultImg)) + img = SceneFromFile.InpaintNaNs(defaultImg) as RgbImage; + UpdatePreviewFromMemory(img); + } + } + + private async void UpdatePreviewFromMemory(RgbImage image) + { + var imgClone = image.Copy(); + flip = null; + StateHasChanged(); + await Task.Delay(1); + flip = new FlipBook() + .Add("", imgClone) + .SetToolVisibility(false) + .SetCustomSizeCSS("width: 100%; height: 580px; resize: vertical; overflow: auto;"); + StateHasChanged(); + } + + async Task RenderQuickPreview() + { + if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) + return; + + isRendering = true; + selectedFile = null; + flip = null; + + await InvokeAsync(StateHasChanged); + + var curIntegrator = integratorSelector.addedIntegrators.First(); + + await Task.Run(async () => + { + var stopwatch = Stopwatch.StartNew(); + var renderScene = currentSceneFile.MakeScene(); + var renderIntegrator = ReferenceUtils.CloneIntegrator(curIntegrator); + + int targetSpp = quickPreviewSpp < 1 ? 1 : quickPreviewSpp; + renderIntegrator.NumIterations = (uint)targetSpp; + renderIntegrator.MinDepth = renderMinDepth; + renderIntegrator.MaxDepth = renderMaxDepth; + + renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); + renderScene.Prepare(); + renderIntegrator.Render(renderScene); + + stopwatch.Stop(); + + var resultImg = renderScene.FrameBuffer.Image.Copy() as RgbImage; + + await InvokeAsync(() => + { + UpdatePreviewFromMemory(resultImg); + }); + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + async Task RenderReference() + { + if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) + return; + + isRendering = true; + flip = null; + await InvokeAsync(StateHasChanged); + + var curIntegrator = integratorSelector.addedIntegrators.First(); + await Task.Run(async () => + { + var renderIntegrator = ReferenceUtils.CloneIntegrator(curIntegrator); + currentSceneFile.MaxDepth = renderMaxDepth; + currentSceneFile.MinDepth = renderMinDepth; + + string referencesRoot = Path.Join(currentSceneDirectory, "References"); + Directory.CreateDirectory(referencesRoot); + string minDepthString = renderMinDepth > 1 ? $"MinDepth{renderMinDepth}-" : ""; + string finalPath = Path.Combine( + referencesRoot, + $"{minDepthString}MaxDepth{renderMaxDepth}-Width{renderWidth}-Height{renderHeight}.exr" + ); + if (File.Exists(finalPath)) + File.Delete(finalPath); + + var resultImg = currentSceneFile.GetReferenceImage( + renderWidth, + renderHeight, + allowRender: true, + config: renderIntegrator + ); + + await InvokeAsync(() => + { + ReferenceUtils.ScanReferences(currentSceneDirectory, referenceFiles); + selectedFile = referenceFiles.FirstOrDefault(r => r.FilePath == finalPath); + if (selectedFile != null) + UpdateViewerFromFile(finalPath); + StateHasChanged(); + }); + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + async Task RenderMoreSamples(bool isResume) + { + if (currentSceneFile == null || selectedFile == null || !File.Exists(selectedFile.FilePath)) + return; + if (integratorSelector.addedIntegrators.Count == 0) + return; + + isRendering = true; + await InvokeAsync(StateHasChanged); + + await Task.Run(async () => + { + try + { + string filePath = selectedFile.FilePath; + var oldImg = new RgbImage(filePath); + int currentSpp = selectedFile.Spp; + + string folder = Path.GetDirectoryName(filePath); + string baseNameNoSuffix = Path.GetFileNameWithoutExtension(filePath) + .Replace("-partial", ""); + string jsonPath = Path.Combine( + folder, + baseNameNoSuffix + (filePath.Contains("-partial") ? "-partial.json" : ".json") + ); + + string thisStepStart = DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"); + JsonNode rootNode = null; + Integrator renderIntegrator = null; + long totalPreviousMs = 0; + int settingsTargetSpp = 0; + + if (File.Exists(jsonPath)) + { + rootNode = JsonNode.Parse(File.ReadAllText(jsonPath)); + totalPreviousMs = (long)(rootNode?["RenderTime"]?.GetValue() ?? 0); + var settingsNode = rootNode?["Settings"]; + if (settingsNode != null) + { + settingsTargetSpp = + settingsNode["TotalSpp"]?.GetValue() + ?? settingsNode["NumIterations"]?.GetValue() + ?? 0; + Type targetType = integratorSelector.GetIntegratorType( + rootNode["Name"]?.GetValue() + ); + if (targetType != null) + { + var options = new JsonSerializerOptions + { + IncludeFields = true, + PropertyNameCaseInsensitive = true, + }; + renderIntegrator = + JsonSerializer.Deserialize(settingsNode, targetType, options) + as Integrator; + } + } + } + + int batchSpp = isResume ? (settingsTargetSpp - currentSpp) : additionalSpp; + int finalTotalSpp = currentSpp + batchSpp; + if (batchSpp <= 0) + return; + + renderIntegrator.NumIterations = (uint)batchSpp; + if (!isResume) + { + uint originalBaseSeed = renderIntegrator.BaseSeed; + renderIntegrator.BaseSeed = originalBaseSeed + (uint)currentSpp; + } + + string currentPartialPath = Path.Combine(folder, baseNameNoSuffix + "-partial.exr"); + var fbFlags = + FrameBuffer.Flags.WriteContinously + | FrameBuffer.Flags.WriteExponentially + | FrameBuffer.Flags.IgnoreNanAndInf; + + scene.FrameBuffer = new FrameBuffer( + oldImg.Width, + oldImg.Height, + currentPartialPath, + fbFlags + ); + scene.Prepare(); + + var stopwatch = Stopwatch.StartNew(); + renderIntegrator.Render(scene); + stopwatch.Stop(); + + long thisStepMs = stopwatch.ElapsedMilliseconds; + string thisStepWrite = DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"); + + var newImg = scene.FrameBuffer.Image; + var finalImg = new RgbImage(oldImg.Width, oldImg.Height); + float wOld = (float)currentSpp / finalTotalSpp; + float wNew = (float)batchSpp / finalTotalSpp; + + Parallel.For( + 0, + finalImg.Height, + row => + { + for (int col = 0; col < finalImg.Width; ++col) + { + finalImg.SetPixel( + col, + row, + oldImg.GetPixel(col, row) * wOld + newImg.GetPixel(col, row) * wNew + ); + } + } + ); + + if (rootNode == null) + rootNode = new JsonObject(); + rootNode["RenderTime"] = totalPreviousMs + thisStepMs; + rootNode["RenderWriteTime"] = thisStepWrite; + rootNode["NumIterations"] = finalTotalSpp; + + var steps = rootNode["RenderSteps"]?.AsArray() ?? new JsonArray(); + steps.Add( + new JsonObject + { + ["Type"] = isResume ? "Resume" : "More", + ["DurationMs"] = thisStepMs, + ["StartTime"] = thisStepStart, + ["WriteTime"] = thisStepWrite, + } + ); + rootNode["RenderSteps"] = steps; + + if (!isResume && rootNode["Settings"] != null) + { + rootNode["Settings"]["TotalSpp"] = finalTotalSpp; + } + + string finalSavePath = isResume + ? Path.Combine(folder, baseNameNoSuffix + ".exr") + : filePath; + finalImg.WriteToFile(finalSavePath); + File.WriteAllText( + Path.ChangeExtension(finalSavePath, ".json"), + rootNode.ToJsonString(new JsonSerializerOptions { WriteIndented = true }) + ); + + if (File.Exists(currentPartialPath)) + File.Delete(currentPartialPath); + string partialJsonPath = Path.ChangeExtension(currentPartialPath, ".json"); + if (File.Exists(partialJsonPath)) + File.Delete(partialJsonPath); + + if (isResume && filePath != finalSavePath) + { + if (File.Exists(filePath)) + File.Delete(filePath); + string oldJ = Path.ChangeExtension(filePath, ".json"); + if (File.Exists(oldJ)) + File.Delete(oldJ); + } + + await InvokeAsync(() => + { + ReferenceUtils.ScanReferences(currentSceneDirectory, referenceFiles); + selectedFile = referenceFiles.FirstOrDefault(r => r.FilePath == finalSavePath); + if (selectedFile != null) + UpdateViewerFromFile(finalSavePath); + StateHasChanged(); + }); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + private void CheckParamsMatch() + { + isStructureMismatch = false; + isVersionMismatch = false; + isVersionWarning = false; + extraKeys.Clear(); + missingKeys.Clear(); + + if (selectedFile == null) + return; + if (string.IsNullOrEmpty(selectedFile.RawJsonConfig)) + return; + + (int Major, int Minor, int Patch) ParseSemVer(string ver) + { + if (string.IsNullOrEmpty(ver)) + return (0, 0, 0); + int idx = ver.IndexOfAny(new[] { '+', '-' }); + if (idx >= 0) + ver = ver.Substring(0, idx); + + var parts = ver.Split('.'); + int maj = parts.Length > 0 && int.TryParse(parts[0], out int m) ? m : 0; + int min = parts.Length > 1 && int.TryParse(parts[1], out int n) ? n : 0; + int pat = parts.Length > 2 && int.TryParse(parts[2], out int p) ? p : 0; + + return (maj, min, pat); + } + + string currentVerStr = ReferenceUtils.CurrentSeeSharpVersion; + string fileVerStr = selectedFile.Version; + + var cur = ParseSemVer(currentVerStr); + var file = ParseSemVer(fileVerStr); + + if (cur.Major != file.Major || cur.Minor != file.Minor) + isVersionMismatch = true; + else if (cur.Patch != file.Patch) + isVersionWarning = true; + + Type targetType = integratorSelector?.GetIntegratorType(selectedFile.IntegratorName); + if (targetType == null) + { + isStructureMismatch = true; + StateHasChanged(); + return; + } + + object dummyIntegrator = Activator.CreateInstance(targetType); + var options = new JsonSerializerOptions { IncludeFields = true }; + var codeJson = JsonSerializer.Serialize(dummyIntegrator, targetType, options); + var codeNode = JsonNode.Parse(codeJson)?.AsObject(); + var fileNode = JsonNode.Parse(selectedFile.RawJsonConfig)?.AsObject(); + + if (codeNode != null && fileNode != null) + { + var codeKeys = codeNode.Select(k => k.Key).ToHashSet(); + var fileKeys = fileNode.Select(k => k.Key).ToHashSet(); + + foreach (var key in fileKeys) + { + if (!codeKeys.Contains(key)) + extraKeys.Add(key); + } + foreach (var key in codeKeys) + { + if (!fileKeys.Contains(key)) + missingKeys.Add(key); + } + if (extraKeys.Count > 0 || missingKeys.Count > 0) + isStructureMismatch = true; + } + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css new file mode 100644 index 00000000..d573ae1b --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css @@ -0,0 +1,143 @@ +.experiment-layout { + display: grid; + grid-template-columns: 350px 350px 1fr; + grid-template-rows: auto 1fr; + gap: 15px; + height: calc(100vh - 80px); + padding: 10px; + box-sizing: border-box; +} + +.layout-header { + grid-column: 1 / -1; + background: transparent; +} + +.col-box { + display: flex; + flex-direction: column; + border: 1px solid #ccc; + background: white; + overflow: hidden; + height: 600px; +} + +.col-title { + padding: 8px 8px; + background: #eee; + border-bottom: 1px solid #ddd; + font-weight: bold; + font-size: 1em; +} + +.col-content { + flex-grow: 1; + overflow-y: auto; + padding: 10px; +} + +.ref-item { + padding: 1px; + border: 1px solid #eee; + margin-bottom: 2px; + cursor: pointer; + transition: background 0.2s; +} +.ref-item:hover { background-color: #f5f5f5; } +.ref-item.active { background-color: #e3f2fd; border-color: #2196F3; } + +.render-form { + border-top: 1px solid #ddd; + padding: 8px; +} + +.form-group { + margin-bottom: 5px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: smaller; +} +.form-group input { width: 70px; padding: 2px; } + +.right-col-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + height: auto; + overflow: visible; +} + +.viewer-box { + height: auto; + display: flex; + flex-direction: column; + border: 1px solid #ccc; + background: white; + overflow: hidden; +} + +.viewer-area { + flex: 1; + width: 100%; + background-color: #2e2e2e; + position: relative; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.meta-box { + height: auto; + min-height: 100px; + border: 1px solid #ccc; + background: white; + display: flex; + flex-direction: column; + margin-bottom: 10px; +} + +.meta-content { + padding: 5px 5px; + overflow-y: auto; + flex: 1; +} + +.meta-grid { + display: grid; + grid-template-columns: auto 1fr; + column-gap: 10px; + row-gap: 5px; + font-size: smaller; + align-items: center; +} +.meta-grid .label { + font-weight: bold; + text-align: right; +} + +.empty-tip { + color: #ccc; + text-align: center; + margin-top: 40px; + font-style: italic; +} + +.config-tree { + margin-left: 4px; + padding-left: 10px; + border-left: 2px solid #e0e0e0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.config-item { + font-size: smaller; + display: flex; +} + +.config-key { + margin-right: 6px; +} diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceUtils.cs b/SeeSharp.ReferenceManager/Pages/ReferenceUtils.cs new file mode 100644 index 00000000..c9e66631 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceUtils.cs @@ -0,0 +1,192 @@ +using System.Reflection; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace SeeSharp.ReferenceManager.Pages; + +public class ReferenceInfo { + public string FilePath { get; set; } = ""; + public string Resolution { get; set; } = ""; + public int MaxDepth { get; set; } + public int MinDepth { get; set; } + public string IntegratorName { get; set; } = ""; + public int Spp { get; set; } + public string RenderTimeDisplay { get; set; } = ""; + public string Version { get; set; } = ""; + public string StartTimeDisplay { get; set; } = ""; + public string WriteTimeDisplay { get; set; } = ""; + public string Timestamp { get; set; } = ""; + public string RawJsonConfig { get; set; } = ""; + public List RenderSteps { get; set; } = new(); +} + +public class RenderStep { + public string Type { get; set; } = ""; + public double DurationMs { get; set; } + public string StartTime { get; set; } = ""; + public string WriteTime { get; set; } = ""; +} + +public static class ReferenceUtils { + public static void ScanReferences(string sceneDir, List referenceFiles) { + referenceFiles.Clear(); + if (string.IsNullOrEmpty(sceneDir)) return; + + string refDir = Path.Combine(sceneDir, "References"); + if (!Directory.Exists(refDir)) return; + + var exrFiles = Directory.GetFiles(refDir, "*.exr") + .OrderByDescending(f => File.GetLastWriteTime(f)); + + foreach (var f in exrFiles) { + var info = new ReferenceInfo { + FilePath = f, + Timestamp = File.GetLastWriteTime(f).ToString("yyyy-MM-dd HH:mm:ss") + }; + ReadMetadataFromJson(info, f); + ParseExrName(info, f); + referenceFiles.Add(info); + } + } + + public static void ParseExrName(ReferenceInfo info, string filePath) { + string filename = Path.GetFileNameWithoutExtension(filePath); + var match = Regex.Match(filename, @"(?:MinDepth(?\d+)-)?MaxDepth(?\d+)-Width(?\d+)-Height(?\d+)", RegexOptions.IgnoreCase); + + if (match.Success) { + info.MinDepth = match.Groups["min"].Success ? int.Parse(match.Groups["min"].Value) : 1; + info.MaxDepth = int.Parse(match.Groups["max"].Value); + info.Resolution = $"{match.Groups["w"].Value}x{match.Groups["h"].Value}"; + } else { + if (string.IsNullOrEmpty(info.Resolution)) + Logger.Warning($"Can't find resolution."); + else if (info.MaxDepth <= 0) + Logger.Warning($"The maximum depth is wrong."); + } + } + + public static void ReadMetadataFromJson(ReferenceInfo info, string exrPath) { + string folder = Path.GetDirectoryName(exrPath); + string fileNameNoExt = Path.GetFileNameWithoutExtension(exrPath); + string jsonPath = Path.Combine(folder, $"{fileNameNoExt}.json"); + + if (!File.Exists(jsonPath)) return; + + string jsonContent = File.ReadAllText(jsonPath); + var root = JsonNode.Parse(jsonContent); + if (root == null) return; + + info.Version = root["SeeSharpVersion"]?.ToString() ?? ""; + info.StartTimeDisplay = root["RenderStartTime"]?.ToString() ?? ""; + info.WriteTimeDisplay = root["RenderWriteTime"]?.ToString() ?? ""; + + if (root["RenderTime"] != null) { + double ms = root["RenderTime"].GetValue(); + TimeSpan t = TimeSpan.FromMilliseconds(ms); + if (t.TotalMinutes >= 1) info.RenderTimeDisplay = $"{(int)t.TotalMinutes:D2} m {t.Seconds:D2} s"; + else if (t.TotalSeconds >= 1) info.RenderTimeDisplay = $"{t.TotalSeconds:F1} s"; + else info.RenderTimeDisplay = $"{ms:F0} ms"; + } + + if (root["NumIterations"] != null) info.Spp = root["NumIterations"].GetValue(); + + var stepsNode = root["RenderSteps"]; + if (stepsNode is JsonArray arr) { + foreach (var step in arr) { + if (step == null) continue; + info.RenderSteps.Add(new RenderStep { + Type = step["Type"]?.ToString() ?? "Unknown", + DurationMs = step["DurationMs"]?.GetValue() ?? 0, + StartTime = step["StartTime"]?.ToString() ?? "", + WriteTime = step["WriteTime"]?.ToString() ?? "" + }); + } + } + + var settingsNode = root["Settings"]; + if (settingsNode != null) { + var options = new JsonSerializerOptions { WriteIndented = true }; + info.RawJsonConfig = settingsNode.ToJsonString(options); + } + + string integratorName = root["Name"]?.GetValue(); + info.IntegratorName = integratorName?.Split('.')?.Last() ?? "Unknown"; + } + + public static void CopyValues(object target, object source) { + if (target == null || source == null || target.GetType() != source.GetType()) return; + var type = target.GetType(); + + bool IsConfigParam(Type t) { + return t == typeof(string) || t.IsValueType; + } + + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite)) { + if (IsConfigParam(prop.PropertyType)) + prop.SetValue(target, prop.GetValue(source)); + } + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { + if (IsConfigParam(field.FieldType)) + field.SetValue(target, field.GetValue(source)); + } + } + + public static Integrator CloneIntegrator(Integrator source) { + var type = source.GetType(); + var clone = (Integrator)Activator.CreateInstance(type); + CopyValues(clone, source); + return clone; + } + + static T GetFieldOrProperty(object instance, string name) + { + var type = instance.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance; + + var propSpp = type.GetProperty(name, flags); + if (propSpp != null) + return (T)propSpp.GetValue(instance); + + var fieldSpp = type.GetField(name, flags); + if (fieldSpp != null) + return (T)fieldSpp.GetValue(instance); + + throw new ArgumentException($"No field or property named '{name}' in '{instance.GetType()}'"); + } + + static void SetFieldOrProperty(object instance, string name, T value) + { + var type = instance.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance; + + var prop = type.GetProperty(name, flags); + if (prop != null && prop.CanWrite) + { + prop.SetValue(instance, value); + return; + } + + var field = type.GetField(name, flags); + if (field != null) + { + field.SetValue(instance, value); + return; + } + + throw new ArgumentException($"No field or property named '{name}' in '{instance.GetType()}'"); + } + + public static void SaveConfig(string folder, Integrator integrator) { + var options = new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }; + var rootNode = new JsonObject(); + rootNode.Add("Name", integrator.GetType().Name); + var settingsNode = JsonSerializer.SerializeToNode(integrator, integrator.GetType(), options); + rootNode.Add("Settings", settingsNode); + File.WriteAllText(Path.Combine(folder, "Config.json"), rootNode.ToJsonString(options)); + } + + public static string CurrentSeeSharpVersion { get; } = + typeof(Scene).Assembly + .GetCustomAttribute()? + .InformationalVersion ?? "Unknown"; +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/RenderSetting.razor b/SeeSharp.ReferenceManager/Pages/RenderSetting.razor new file mode 100644 index 00000000..50d1404a --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/RenderSetting.razor @@ -0,0 +1,41 @@ +@using System.Reflection +@using SeeSharp.Blazor +@using SeeSharp.ReferenceManager.Pages + +@{ + string name = Member.Name; + string desc = IntegratorUtils.GetDescription(Member); + + Type type = (Member is PropertyInfo p) ? p.PropertyType : ((FieldInfo)Member).FieldType; + Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; + TypeCode typeCode = Type.GetTypeCode(underlyingType); + + if (typeCode == TypeCode.Boolean) + { + bool val = Convert.ToBoolean(Getter() ?? false); + + } + else if (typeCode == TypeCode.Single || typeCode == TypeCode.Double) + { + float val = (float)Convert.ToDouble(Getter() ?? 0.0); + + } + else if (typeCode >= TypeCode.SByte && typeCode <= TypeCode.UInt64) + { + long val = Convert.ToInt64(Getter() ?? 0); + + } +} + +@code { + [Parameter] public MemberInfo Member { get; set; } = default!; + [Parameter] public Func Getter { get; set; } = default!; + [Parameter] public Action Setter { get; set; } = default!; +} diff --git a/SeeSharp.ReferenceManager/Pages/_Host.cshtml b/SeeSharp.ReferenceManager/Pages/_Host.cshtml new file mode 100644 index 00000000..356b5c71 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/_Host.cshtml @@ -0,0 +1,34 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@namespace SeeSharp.ReferenceManager.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + @Html.Raw(SeeSharp.Blazor.Scripts.AllScripts) + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/SeeSharp.ReferenceManager/Program.cs b/SeeSharp.ReferenceManager/Program.cs new file mode 100644 index 00000000..ffae204c --- /dev/null +++ b/SeeSharp.ReferenceManager/Program.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +SceneRegistry.AddSourceRelativeToScript("../Data/Scenes"); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +app.Run(); diff --git a/SeeSharp.ReferenceManager/Properties/launchSettings.json b/SeeSharp.ReferenceManager/Properties/launchSettings.json new file mode 100644 index 00000000..4b0b1aac --- /dev/null +++ b/SeeSharp.ReferenceManager/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "iisSettings": { + "iisExpress": { + "applicationUrl": "http://localhost:18831", + "sslPort": 44326 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7055;http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/SeeSharp.ReferenceManager/SeeSharp.ReferenceManager.csproj b/SeeSharp.ReferenceManager/SeeSharp.ReferenceManager.csproj new file mode 100644 index 00000000..ce65c30d --- /dev/null +++ b/SeeSharp.ReferenceManager/SeeSharp.ReferenceManager.csproj @@ -0,0 +1,17 @@ + + + + Exe + net9.0 + enable + + + + + + + + + + + diff --git a/SeeSharp.ReferenceManager/_Imports.razor b/SeeSharp.ReferenceManager/_Imports.razor new file mode 100644 index 00000000..178485ed --- /dev/null +++ b/SeeSharp.ReferenceManager/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using SeeSharp.ReferenceManager \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/appsettings.Development.json b/SeeSharp.ReferenceManager/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/SeeSharp.ReferenceManager/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/SeeSharp.ReferenceManager/appsettings.json b/SeeSharp.ReferenceManager/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/SeeSharp.ReferenceManager/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/SeeSharp.ReferenceManager/wwwroot/css/site.css b/SeeSharp.ReferenceManager/wwwroot/css/site.css new file mode 100644 index 00000000..ddc98cca --- /dev/null +++ b/SeeSharp.ReferenceManager/wwwroot/css/site.css @@ -0,0 +1,86 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 3.5rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +html { + font-family: system-ui; +} + +button { + background-color: #a4e1f2; + border-style: none; + /* border-width: 2px; + border-color: #245e6f; */ + color: black; + font-size: medium; + padding-left: 8px; + padding-right: 8px; + padding-bottom: 4px; + padding-top: 4px; +} + button:hover { + background-color: #c9eff4; + cursor: pointer; + } + button:disabled { + background-color: #e5f1f5; + color: #96b4bd; + border-color: #96b4bd; + } + +.experiment-settings { + display: flex; + gap: 0.25em; + flex-direction: column; + float: left; + margin-right: 1em; +} + +.experiment-results { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: flex-start; +} + +table { + border-collapse: collapse; +} +td, th { + border: none; + padding: 4px; +} +tr:hover { background-color: #e7f2f1; } +th { + padding-top: 6px; + padding-bottom: 6px; + text-align: left; + background-color: #4a96af; + color: white; + font-size: smaller; +} \ No newline at end of file diff --git a/SeeSharp.Templates/content/SeeSharp.Blazor.Template/Pages/Experiment.razor.cs b/SeeSharp.Templates/content/SeeSharp.Blazor.Template/Pages/Experiment.razor.cs index 109cc8db..1152a32e 100644 --- a/SeeSharp.Templates/content/SeeSharp.Blazor.Template/Pages/Experiment.razor.cs +++ b/SeeSharp.Templates/content/SeeSharp.Blazor.Template/Pages/Experiment.razor.cs @@ -25,7 +25,7 @@ void RunExperiment() PathTracer pathTracer = new() { - TotalSpp = NumSamples, + SampleCount = NumSamples, MaxDepth = MaxDepth, }; pathTracer.Render(scene); @@ -35,7 +35,7 @@ void RunExperiment() scene.FrameBuffer = new(Width, Height, ""); VertexConnectionAndMerging vcm = new() { - NumIterations = NumSamples, + SampleCount = NumSamples, MaxDepth = MaxDepth, }; vcm.Render(scene); diff --git a/SeeSharp.Templates/content/SeeSharp.Template/MyExperiment.cs b/SeeSharp.Templates/content/SeeSharp.Template/MyExperiment.cs index 46d968aa..7c3b2a93 100644 --- a/SeeSharp.Templates/content/SeeSharp.Template/MyExperiment.cs +++ b/SeeSharp.Templates/content/SeeSharp.Template/MyExperiment.cs @@ -5,7 +5,7 @@ namespace SeeSharp.Template; class MyExperiment : Experiment { public override List MakeMethods() => [ new("Path tracing", new PathTracer() { TotalSpp = 4 }), - new("VCM", new VertexConnectionAndMerging() { NumIterations = 2 }) + new("VCM", new VertexConnectionAndMerging() { SampleCount = 2 }) ]; public override void OnDone(string workingDirectory, IEnumerable sceneNames, IEnumerable sceneExposures) { diff --git a/SeeSharp.Validation/Validator.cs b/SeeSharp.Validation/Validator.cs index eb773aba..ebf93fcb 100644 --- a/SeeSharp.Validation/Validator.cs +++ b/SeeSharp.Validation/Validator.cs @@ -64,17 +64,17 @@ public static List Validate(ValidationSceneFactory sceneFactory, bool useT var algorithms = new Dictionary() { { "PathTracer", new PathTracer() { - TotalSpp = sceneFactory.SamplesPerPixel, + NumIterations = (uint)sceneFactory.SamplesPerPixel, MaxDepth = sceneFactory.MaxDepth, RenderTechniquePyramid = false, }}, { "ClassicBidir", new ClassicBidir() { - NumIterations = sceneFactory.SamplesPerPixel / 2, + NumIterations = (uint)sceneFactory.SamplesPerPixel / 2, MaxDepth = sceneFactory.MaxDepth, RenderTechniquePyramid = false, }}, { "Vcm", new VertexConnectionAndMerging() { - NumIterations = sceneFactory.SamplesPerPixel / 2, + NumIterations = (uint)sceneFactory.SamplesPerPixel / 2, MaxDepth = sceneFactory.MaxDepth, RenderTechniquePyramid = false, }} diff --git a/SeeSharp.sln b/SeeSharp.sln index c0161e52..aa5cbbc6 100644 --- a/SeeSharp.sln +++ b/SeeSharp.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Blazor", "SeeSharp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialTest", "MaterialTest\MaterialTest.csproj", "{5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.ReferenceManager", "SeeSharp.ReferenceManager\SeeSharp.ReferenceManager.csproj", "{EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,9 +36,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -170,5 +169,20 @@ Global {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x64.Build.0 = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.ActiveCfg = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.Build.0 = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x64.Build.0 = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x86.Build.0 = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|Any CPU.Build.0 = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x64.ActiveCfg = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x64.Build.0 = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x86.ActiveCfg = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/SeeSharp/Common/DocumentationReader.cs b/SeeSharp/Common/DocumentationReader.cs new file mode 100644 index 00000000..39cf6abd --- /dev/null +++ b/SeeSharp/Common/DocumentationReader.cs @@ -0,0 +1,70 @@ +using System.Reflection; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace SeeSharp.Blazor; + +/// +/// Load xml documentation files and retrieve summary to show description +/// +public static class DocumentationReader +{ + private static Dictionary loadedXmlDocumentation = new(); + + public static void LoadXmlDocumentation(Assembly assembly) + { + var assemblyPath = assembly.Location; + var xmlPath = Path.ChangeExtension(assemblyPath, ".xml"); + + if (File.Exists(xmlPath)) + { + try + { + var doc = XDocument.Load(xmlPath); + foreach (var member in doc.Descendants("member")) + { + var name = member.Attribute("name")?.Value; + var summary = member.Element("summary")?.Value.Trim(); + + if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(summary)) + { + string cleanSummary = Regex.Replace(summary, @"\s+", " "); + loadedXmlDocumentation[name] = cleanSummary; + } + } + } + catch (Exception ex) + { + Logger.Log($"Error loading XML: {ex.Message}", Verbosity.Error); + } + } + } + + public static string GetSummary(MemberInfo member) + { + if (member == null || member.DeclaringType == null) return ""; + + string prefix = member is PropertyInfo ? "P:" : "F:"; + + Type declaringType = member.DeclaringType; + string typeName = declaringType.FullName; + + if (declaringType.IsGenericType) + { + int bracketIndex = typeName.IndexOf('['); + if (bracketIndex > 0) + { + typeName = typeName.Substring(0, bracketIndex); + } + } + + string key = $"{prefix}{typeName}.{member.Name}"; + + if (loadedXmlDocumentation.TryGetValue(key, out var summary)) + { + return summary; + } + + return ""; + } +} \ No newline at end of file diff --git a/SeeSharp/Common/ProgressBar.cs b/SeeSharp/Common/ProgressBar.cs index e1fff70b..6cea8bd7 100644 --- a/SeeSharp/Common/ProgressBar.cs +++ b/SeeSharp/Common/ProgressBar.cs @@ -85,12 +85,18 @@ public ProgressBar(int numBlocks = 20, bool displayWork = true, bool displayTime watcher.WriteCharEvent += OnOtherOutput; } } + /// + /// Notifies that the process has started. + /// + /// Amount of steps that are performed in total (e.g., render iterations) + public void Start(uint totalWork) => Start((int) totalWork); /// /// Notifies that the process has started. /// /// Amount of steps that are performed in total (e.g., render iterations) - public void Start(int totalWork) { + public void Start(int totalWork) + { total = totalWork; done = 0; accumSeconds = 0.0; diff --git a/SeeSharp/Experiments/SceneConfig.cs b/SeeSharp/Experiments/SceneConfig.cs index 87fd8792..9afd5085 100644 --- a/SeeSharp/Experiments/SceneConfig.cs +++ b/SeeSharp/Experiments/SceneConfig.cs @@ -31,6 +31,7 @@ public abstract class SceneConfig { /// Width of the image /// Height of the image /// If false, missing references are not rendered and null is returned instead + /// Use integrator parameters if provided, otherwise read them from the JSON file /// The reference image - public abstract RgbImage GetReferenceImage(int width, int height, bool allowRender = true); + public abstract RgbImage GetReferenceImage(int width, int height, bool allowRender = true, Integrator config = null); } \ No newline at end of file diff --git a/SeeSharp/Experiments/SceneFromFile.cs b/SeeSharp/Experiments/SceneFromFile.cs index 21ad3af5..5defa754 100644 --- a/SeeSharp/Experiments/SceneFromFile.cs +++ b/SeeSharp/Experiments/SceneFromFile.cs @@ -46,7 +46,7 @@ public SceneFromFile WithName(string newName) { WriteIndented = true }; - static T InpaintNaNs(T image) where T : Image { + public static T InpaintNaNs(T image) where T : Image { bool hadAny = false; for (int row = 0; row < image.Height; ++row) { for (int col = 0; col < image.Width; ++col) { @@ -86,8 +86,9 @@ void TryAdd(int c, int r) { /// Width in pixels /// Height in pixels /// If false, missing references are not rendered and null is returned instead + /// Use integrator parameters if provided, otherwise read them from the JSON file /// The reference image - public override RgbImage GetReferenceImage(int width, int height, bool allowRender = true) { + public override RgbImage GetReferenceImage(int width, int height, bool allowRender = true, Integrator config = null) { string refDir = Path.Join(file.DirectoryName, "References"); Directory.CreateDirectory(refDir); @@ -110,6 +111,11 @@ public override RgbImage GetReferenceImage(int width, int height, bool allowRend string referenceSpecs = Path.Join(refDir, "Config.json"); Integrator refIntegrator = DefaultReferenceIntegrator; + if (config != null) { + refIntegrator = config; + goto IntegratorLoaded; + } + if (File.Exists(referenceSpecs)) { var json = JsonNode.Parse(File.ReadAllText(referenceSpecs)); @@ -150,7 +156,7 @@ public override RgbImage GetReferenceImage(int width, int height, bool allowRend // Overwrite the reference specs in case they got updated (e.g., migrated to new format) File.WriteAllText(referenceSpecs, $$""" { - "Name": "{{refIntegrator.GetType().FullName}}", + "Name": "{{refIntegrator.GetType().Name}}", "Settings": {{JsonSerializer.Serialize(refIntegrator, refIntegrator.GetType(), refSerializerOptions)}} } """); @@ -158,15 +164,31 @@ public override RgbImage GetReferenceImage(int width, int height, bool allowRend refIntegrator.MaxDepth = MaxDepth; refIntegrator.MinDepth = MinDepth; + string partialFilename = Path.Join(refDir, $"{minDepthString}MaxDepth{MaxDepth}-Width{width}-Height{height}-partial.exr"); + if (File.Exists(partialFilename)) File.Delete(partialFilename); + using Scene scn = MakeScene(); - scn.FrameBuffer = new(width, height, filename, + scn.FrameBuffer = new(width, height, partialFilename, FrameBuffer.Flags.IgnoreNanAndInf | FrameBuffer.Flags.WriteContinously | FrameBuffer.Flags.WriteExponentially); // output intermediate results exponentially to avoid loosing everything on a crash + + // Write settings to metadata for preview parameters in the info table + scn.FrameBuffer.MetaData["Name"] = refIntegrator.GetType().Name; + scn.FrameBuffer.MetaData["Settings"] = JsonSerializer.SerializeToNode(refIntegrator, refIntegrator.GetType(), refSerializerOptions); + scn.Prepare(); refIntegrator.Render(scn); scn.FrameBuffer.WriteToFile(); + if (File.Exists(filename)) File.Delete(filename); + File.Move(partialFilename, filename); + + string pJson = Path.ChangeExtension(partialFilename, ".json"); + string fJson = Path.ChangeExtension(filename, ".json"); + if (File.Exists(fJson)) File.Delete(fJson); + if (File.Exists(pJson)) File.Move(pJson, fJson); + return InpaintNaNs(scn.FrameBuffer.Image); } @@ -192,7 +214,7 @@ public SceneFromFile(string filename, int minDepth, int maxDepth, string name = public virtual Integrator DefaultReferenceIntegrator => new PathTracer() { BaseSeed = 571298512u, - TotalSpp = 512 + NumIterations = 512 }; /// diff --git a/SeeSharp/Integrators/Bidir/BidirBase.cs b/SeeSharp/Integrators/Bidir/BidirBase.cs index ea92190b..e758c76e 100644 --- a/SeeSharp/Integrators/Bidir/BidirBase.cs +++ b/SeeSharp/Integrators/Bidir/BidirBase.cs @@ -9,11 +9,6 @@ public abstract partial class BidirBase : Integrator { #region Parameters - /// - /// Number of iterations (batches of one sample per pixel) to render - /// - public int NumIterations { get; set; } = 2; - /// /// The maximum time in milliseconds that should be spent rendering. /// Excludes framebuffer overhead and other operations that are not part of the core rendering logic. @@ -104,7 +99,7 @@ public override void Render(Scene scene) { OnBeforeRender(); ProgressBar progressBar = new(prefix: "Rendering..."); - progressBar.Start(NumIterations); + progressBar.Start((int)NumIterations); RenderTimer timer = new(); Stopwatch lightTracerTimer = new(); Stopwatch pathTracerTimer = new(); diff --git a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs index 5e2b1a2f..365710a9 100644 --- a/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs +++ b/SeeSharp/Integrators/Bidir/CameraStoringVCM.cs @@ -5,8 +5,6 @@ namespace SeeSharp.Integrators.Bidir; public class CameraStoringVCM : Integrator where TLightPathData : new() { #region Parameters - public int NumIterations { get; set; } = 1; - /// /// The maximum time in milliseconds that should be spent rendering. /// Excludes framebuffer overhead and other operations that are not part of the core rendering logic. diff --git a/SeeSharp/Integrators/Bidir/PhotonMapper.cs b/SeeSharp/Integrators/Bidir/PhotonMapper.cs index b0995fc7..7345abca 100644 --- a/SeeSharp/Integrators/Bidir/PhotonMapper.cs +++ b/SeeSharp/Integrators/Bidir/PhotonMapper.cs @@ -5,11 +5,6 @@ /// computed from a fraction of the scene size. /// public class PhotonMapper : Integrator { - /// - /// Number of iterations to render. - /// - public int NumIterations = 2; - /// /// Maximum number of nearest neighbor photons to search for. /// diff --git a/SeeSharp/Integrators/Bidir/TechPyramid.cs b/SeeSharp/Integrators/Bidir/TechPyramid.cs index a329e14e..c3ac1c31 100644 --- a/SeeSharp/Integrators/Bidir/TechPyramid.cs +++ b/SeeSharp/Integrators/Bidir/TechPyramid.cs @@ -1,5 +1,4 @@ -using System.Linq; -using TechniqueNames = System.Collections.Generic.Dictionary +using TechniqueNames = System.Collections.Generic.Dictionary <(int cameraPathEdges, int lightPathEdges, int totalEdges), string>; namespace SeeSharp.Integrators.Bidir; @@ -120,4 +119,4 @@ public void WriteToFiles(string basename) { } return images; } -} +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs b/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs index fb85aeaa..3f13e11a 100644 --- a/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs +++ b/SeeSharp/Integrators/Bidir/VertexCacheBidir.cs @@ -235,4 +235,4 @@ protected virtual float LightPathReciprocals(int lastCameraVertexIdx, in BidirPa if (EnableHitting) sumReciprocals += nextReciprocal; // Hitting the emitter directly return sumReciprocals; } -} +} \ No newline at end of file diff --git a/SeeSharp/Integrators/DebugVisualizer.cs b/SeeSharp/Integrators/DebugVisualizer.cs index 562641b1..f19c6338 100644 --- a/SeeSharp/Integrators/DebugVisualizer.cs +++ b/SeeSharp/Integrators/DebugVisualizer.cs @@ -4,16 +4,11 @@ /// Renders a simple and fast grayscale visualization of a scene /// public class DebugVisualizer : Integrator { - /// - /// Number of anti-aliasing samples to take in each pixel - /// - public int TotalSpp = 1; - /// /// Renders the given scene. /// public override void Render(Scene scene) { - for (uint sampleIndex = 0; sampleIndex < TotalSpp; ++sampleIndex) { + for (uint sampleIndex = 0; sampleIndex < NumIterations; ++sampleIndex) { scene.FrameBuffer.StartIteration(); Parallel.For(0, scene.FrameBuffer.Height, row => { diff --git a/SeeSharp/Integrators/Integrator.cs b/SeeSharp/Integrators/Integrator.cs index 49e6f45b..b7abf342 100644 --- a/SeeSharp/Integrators/Integrator.cs +++ b/SeeSharp/Integrators/Integrator.cs @@ -15,8 +15,18 @@ public abstract class Integrator { /// public int MinDepth { get; set; } = 1; + /// + /// Seed used during rendering. Ideally, every call to with the + /// same seed should produce the exact same image. + /// Might not be true due to non-determinism in the algorithm. + /// public uint BaseSeed { get; set; } = 0x0C030114; + /// + /// Number of samples per pixel (or closest equivalent) to use when calling + /// + public uint NumIterations { get; set; } = 1; + /// /// Renders a scene to the frame buffer that is specified by the object. /// diff --git a/SeeSharp/Integrators/PathTracer.cs b/SeeSharp/Integrators/PathTracer.cs index 2346a5ca..4689fe23 100644 --- a/SeeSharp/Integrators/PathTracer.cs +++ b/SeeSharp/Integrators/PathTracer.cs @@ -10,11 +10,6 @@ public class PathTracer : PathTracerBase { } /// generic type provided. /// public class PathTracerBase : Integrator { - /// - /// Number of samples per pixel to render - /// - public int TotalSpp = 20; - /// /// The maximum time in milliseconds that should be spent rendering. /// Excludes framebuffer overhead and other operations that are not part of the core rendering logic. @@ -71,7 +66,7 @@ public virtual void RegisterSample(Pixel pixel, RgbColor weight, float misWeight bool isNextEvent) { if (!RenderTechniquePyramid) return; - weight /= TotalSpp; + weight /= NumIterations; int cameraEdges = (int)depth - (isNextEvent ? 1 : 0); techPyramidRaw.Add(cameraEdges, 0, (int)depth, pixel, weight); techPyramidWeighted.Add(cameraEdges, 0, (int)depth, pixel, weight * misWeight); @@ -208,11 +203,11 @@ public override void Render(Scene scene) { denoiseBuffers = new(scene.FrameBuffer); ProgressBar progressBar = new(prefix: "Rendering..."); - progressBar.Start(TotalSpp); + progressBar.Start((int)NumIterations); RenderTimer timer = new(); ShadingStatCounter.Reset(); scene.Raytracer.ResetStats(); - for (uint sampleIndex = 0; sampleIndex < TotalSpp; ++sampleIndex) { + for (uint sampleIndex = 0; sampleIndex < NumIterations; ++sampleIndex) { long nextIterTime = timer.RenderTime + timer.PerIterationCost; if (MaximumRenderTimeMs.HasValue && nextIterTime > MaximumRenderTimeMs.Value) { Logger.Log("Maximum render time exhausted."); @@ -236,7 +231,7 @@ public override void Render(Scene scene) { OnPostIteration(sampleIndex); timer.EndRender(); - if (sampleIndex == TotalSpp - 1 && EnableDenoiser) + if (sampleIndex == NumIterations - 1 && EnableDenoiser) denoiseBuffers.Denoise(); PostprocessIteration(sampleIndex); scene.FrameBuffer.EndIteration();