From a35dc5cad4ee95a8b0037e3e043c2f923c5bd51d Mon Sep 17 00:00:00 2001 From: Alexander Gluschenko Date: Tue, 26 May 2026 00:01:12 +0400 Subject: [PATCH 1/4] Upgrade project to .NET 10 and modernize CI Add global.json to pin .NET SDK 10.0 and roll forward to latestFeature. Bump project TargetFrameworks to net10.0 / net10.0-windows and update BenchmarkDotNet and Microsoft.Extensions package versions to 0.15.6 and 10.0.8 respectively. Update solution VisualStudioVersion to 18.0.0.0. Modernize GitHub Actions usage (actions/checkout, setup-dotnet, upload/download-artifact) to v4 and switch workflow dotnet-version to 10.0.x. Small formatting/newline fix in RenderBox.csproj. These changes move the codebase and CI to .NET 10 and newer action versions. --- .github/workflows/build.yml | 16 ++++++++-------- global.json | 6 ++++++ .../RenderBox.Benchmarks.csproj | 4 ++-- src/RenderBox.Shared/RenderBox.Shared.csproj | 10 +++++----- src/RenderBox.sln | 4 ++-- src/RenderBox/RenderBox.csproj | 4 ++-- 6 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 global.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9dba274..f907cb3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,11 +14,11 @@ jobs: ROOT: ./src steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: 📂 Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: 📂 Files working-directory: ${{env.ROOT}} @@ -28,7 +28,7 @@ jobs: working-directory: ${{env.ROOT}}/RenderBox run: dotnet publish -p:PublishProfile=FolderProfile -c Release -o out - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: github.ref == 'refs/heads/master' with: name: builds @@ -45,17 +45,17 @@ jobs: NUGET_AUTH_TOKEN: ${{secrets.token}} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: builds path: ${{env.ROOT}} - name: 📂 Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: 📂 Pack RenderBox working-directory: ${{env.ROOT}}/RenderBox/out diff --git a/global.json b/global.json new file mode 100644 index 0000000..34cc7a1 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.300", + "rollForward": "latestFeature" + } +} diff --git a/src/RenderBox.Benchmarks/RenderBox.Benchmarks.csproj b/src/RenderBox.Benchmarks/RenderBox.Benchmarks.csproj index e14e43d..4c16c89 100644 --- a/src/RenderBox.Benchmarks/RenderBox.Benchmarks.csproj +++ b/src/RenderBox.Benchmarks/RenderBox.Benchmarks.csproj @@ -2,13 +2,13 @@ Exe - net8.0 + net10.0 enable enable - + diff --git a/src/RenderBox.Shared/RenderBox.Shared.csproj b/src/RenderBox.Shared/RenderBox.Shared.csproj index 2d6680a..54c936c 100644 --- a/src/RenderBox.Shared/RenderBox.Shared.csproj +++ b/src/RenderBox.Shared/RenderBox.Shared.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 true enable enable @@ -18,10 +18,10 @@ - - - - + + + + diff --git a/src/RenderBox.sln b/src/RenderBox.sln index 9e1dcc2..44627d7 100644 --- a/src/RenderBox.sln +++ b/src/RenderBox.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29411.108 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.0.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderBox", "RenderBox\RenderBox.csproj", "{90B35404-D527-48B2-A12C-0A9A9097672D}" EndProject diff --git a/src/RenderBox/RenderBox.csproj b/src/RenderBox/RenderBox.csproj index ccc4e98..1e1e04b 100644 --- a/src/RenderBox/RenderBox.csproj +++ b/src/RenderBox/RenderBox.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows + net10.0-windows true Resources\RenderBoxLogo.ico true @@ -33,4 +33,4 @@ - \ No newline at end of file + From 9a64115a3f481ab147dc51df09ceb3d0b4789355 Mon Sep 17 00:00:00 2001 From: Alexander Gluschenko Date: Tue, 26 May 2026 00:44:47 +0400 Subject: [PATCH 2/4] Improve null-safety and nullable annotations Introduce nullable annotations and null-safety checks across the codebase. Key changes: - Updated Equals signatures to accept nullable object? and simplified equality checks for Vector/Quaternion types. - Initialized several Camera matrices (ViewMatrix, PosMatrix, ViewPosMatrix, RayMatrix) to avoid uninitialized state. - Made shape/light references nullable where appropriate: Shape.Light, Ray.HitObject, MandelbrotRenderer.Filter, Paint.Bitmap, various renderer/thread fields and events, and Scene.Lights now defaults to an empty collection. - Hardened PathTraceRenderer: changed TracePath/FindClosestHit/IsShadow signatures to accept nullable currentShape, added null guards and casts (throwing on unexpected null Hit.HitObject) and adjusted lighting/shadow logic to use the non-null hit object. - Streamlined Scene.UpdateLights to filter null lights safely. - ThreadManager: initialize pool to Array.Empty, make onDone nullable, simplify Kill and worker invocation logic. - Renderer/Pages: make GetOptionPageType and other APIs nullable-aware, add defensive checks in CanvasPage.SetupRender and Render, initialize page renderer sources with null-forgiving pattern where necessary, and ensure option page instances are validated before invoking. - Minor refactors and safety improvements (type checks in MandelbrotPage filter handling, removed unused imports). These changes reduce potential null-reference crashes and make intent clearer for nullable reference types. --- src/RenderBox.Shared/Core/Color.cs | 2 +- src/RenderBox.Shared/Core/Vector.cs | 23 +++-------- .../Modules/PathTracer/Camera.cs | 4 ++ .../Modules/PathTracer/Light.cs | 2 +- .../Modules/PathTracer/Ray.cs | 2 +- .../Modules/PathTracer/Scene.cs | 4 +- .../Modules/PathTracer/Shape.cs | 2 +- .../Services/Renderers/MandelbrotRenderer.cs | 2 +- .../Services/Renderers/PathTraceRenderer.cs | 20 +++++----- src/RenderBox/Services/Rendering/Paint.cs | 3 +- src/RenderBox/Services/Rendering/Renderer.cs | 10 ++--- .../Services/Rendering/ThreadManager.cs | 14 ++----- src/RenderBox/Views/MainWindow.xaml.cs | 2 +- src/RenderBox/Views/Pages/CanvasPage.xaml.cs | 38 +++++++++++++++---- src/RenderBox/Views/Pages/DrawingPage.xaml.cs | 2 +- .../Views/Pages/MandelbrotPage.xaml.cs | 8 +++- .../Views/Pages/PathTracePage.xaml.cs | 2 +- src/RenderBox/Views/Pages/TerrainPage.xaml.cs | 3 -- 18 files changed, 77 insertions(+), 66 deletions(-) diff --git a/src/RenderBox.Shared/Core/Color.cs b/src/RenderBox.Shared/Core/Color.cs index b6341f2..ea98286 100644 --- a/src/RenderBox.Shared/Core/Color.cs +++ b/src/RenderBox.Shared/Core/Color.cs @@ -76,7 +76,7 @@ private static byte GetChannel(float n) public override int GetHashCode() => GetRaw(); [MethodImpl(Runtime.IMPL_OPTIONS)] - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is Color color) { diff --git a/src/RenderBox.Shared/Core/Vector.cs b/src/RenderBox.Shared/Core/Vector.cs index a75b6be..6e216d6 100644 --- a/src/RenderBox.Shared/Core/Vector.cs +++ b/src/RenderBox.Shared/Core/Vector.cs @@ -85,12 +85,8 @@ public Vector2(float x) : this(x, x) { } public float Length => MathHelpres.FastSqrt(x * x + y * y); [MethodImpl(Runtime.IMPL_OPTIONS)] - public override bool Equals(object obj) - => EqualsInternal(obj as Vector2?); - - [MethodImpl(Runtime.IMPL_OPTIONS)] - private bool EqualsInternal(Vector2? vector) - => vector.HasValue && vector.Value == this; + public override bool Equals(object? obj) + => obj is Vector2 vector && vector == this; public override int GetHashCode() => HashCode.Combine(x, y); @@ -177,12 +173,8 @@ public Vector3(float x) : this(x, x, x) { } public float Length => MathHelpres.FastSqrt(x * x + y * y + z * z); [MethodImpl(Runtime.IMPL_OPTIONS)] - public override bool Equals(object obj) - => EqualsInternal(obj as Vector3?); - - [MethodImpl(Runtime.IMPL_OPTIONS)] - private bool EqualsInternal(Vector3? vector) - => vector.HasValue && vector.Value == this; + public override bool Equals(object? obj) + => obj is Vector3 vector && vector == this; public override int GetHashCode() => HashCode.Combine(x, y, z); } @@ -289,11 +281,8 @@ public float this[int i] public static bool operator !=(Quaternion a, Quaternion b) => !(a == b); - public override bool Equals(object obj) - => EqualsInternal(obj as Quaternion?); - - private bool EqualsInternal(Quaternion? vector) - => vector.HasValue && vector.Value == this; + public override bool Equals(object? obj) + => obj is Quaternion vector && vector == this; public override int GetHashCode() => HashCode.Combine(M); } diff --git a/src/RenderBox.Shared/Modules/PathTracer/Camera.cs b/src/RenderBox.Shared/Modules/PathTracer/Camera.cs index d71d6cc..999cfb1 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Camera.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Camera.cs @@ -22,7 +22,11 @@ public Camera(Vector3 position) Target = new Vector3(0.0, 0.0, 0.0); Position = position; + ViewMatrix = new Quaternion(true); + PosMatrix = new Quaternion(true); BiasMatrix = GetBiasMatrixInverse(); + ViewPosMatrix = new Quaternion(true); + RayMatrix = new Quaternion(true); } public void LookAt(Vector3 position, Vector3 target, bool rotateAround) diff --git a/src/RenderBox.Shared/Modules/PathTracer/Light.cs b/src/RenderBox.Shared/Modules/PathTracer/Light.cs index 9819a13..670b8d7 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Light.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Light.cs @@ -11,7 +11,7 @@ public class Light public float LinearAttenuation { get; set; } public float QuadraticAttenuation { get; set; } - public IShape Shape { get; set; } + public IShape Shape { get; set; } = null!; public Light(Color color, double intensity) { diff --git a/src/RenderBox.Shared/Modules/PathTracer/Ray.cs b/src/RenderBox.Shared/Modules/PathTracer/Ray.cs index 8c06fb2..4e88e51 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Ray.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Ray.cs @@ -23,7 +23,7 @@ public struct Hit { public Vector3 Position { get; set; } public Vector3 Normal { get; set; } - public Shape HitObject { get; set; } + public Shape? HitObject { get; set; } public bool IsHitting => HitObject != null; } diff --git a/src/RenderBox.Shared/Modules/PathTracer/Scene.cs b/src/RenderBox.Shared/Modules/PathTracer/Scene.cs index a50ef93..c33a690 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Scene.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Scene.cs @@ -7,7 +7,7 @@ public class Scene public Color BackgroundColor { get; set; } = new Color(.2f, .2f, .2f); public Color AmbientColor { get; set; } = new Color(.1f, .1f, .1f); public List Shapes { get; set; } = new List(); - public IEnumerable Lights { get; private set; } + public IEnumerable Lights { get; private set; } = Array.Empty(); public bool LightingEnabled { get; set; } = true; public bool ShadowsEnabled { get; set; } = true; @@ -22,7 +22,7 @@ public Scene() public void UpdateLights() { - Lights = Shapes.Where(x => x.Light != null).Select(x => x.Light).ToArray(); + Lights = Shapes.Select(x => x.Light).Where(x => x is not null).Cast().ToArray(); } } } diff --git a/src/RenderBox.Shared/Modules/PathTracer/Shape.cs b/src/RenderBox.Shared/Modules/PathTracer/Shape.cs index b37c8f9..b93136f 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Shape.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Shape.cs @@ -15,7 +15,7 @@ public abstract class Shape : IShape { public Vector3 Position { get; set; } public Material Material { get; set; } - public Light Light { get; private set; } + public Light? Light { get; private set; } public Shape(Vector3 pos, Color diffuse) { diff --git a/src/RenderBox/Services/Renderers/MandelbrotRenderer.cs b/src/RenderBox/Services/Renderers/MandelbrotRenderer.cs index b985ec4..0c1c8d3 100644 --- a/src/RenderBox/Services/Renderers/MandelbrotRenderer.cs +++ b/src/RenderBox/Services/Renderers/MandelbrotRenderer.cs @@ -18,7 +18,7 @@ public class MandelbrotRenderer : Renderer public int Iterations { get; set; } = 100; public double Extent { get; set; } = 2; - public IPaletteFilter Filter { get; set; } + public IPaletteFilter? Filter { get; set; } public MandelbrotRenderer(Paint paint) : base(paint) { diff --git a/src/RenderBox/Services/Renderers/PathTraceRenderer.cs b/src/RenderBox/Services/Renderers/PathTraceRenderer.cs index 6b4a672..6ba6763 100644 --- a/src/RenderBox/Services/Renderers/PathTraceRenderer.cs +++ b/src/RenderBox/Services/Renderers/PathTraceRenderer.cs @@ -127,7 +127,7 @@ int GetRenderPriority(int x, int y) } } - private Color TracePath(RenderContext context, Camera camera, Ray ray, Color back, int depth = 0, Shape currentShape = null) + private Color TracePath(RenderContext context, Camera camera, Ray ray, Color back, int depth = 0, Shape? currentShape = null) { if (Mode == RenderMode.Time) { @@ -148,7 +148,8 @@ private Color TracePath(RenderContext context, Camera camera, Ray ray, Color bac return back; // Nothing was hit } - var material = hit.HitObject.Material; + var hitObject = hit.HitObject ?? throw new InvalidOperationException("A hit must include the shape it intersected."); + var material = hitObject.Material; var emittance = material.Color; //material.emittance; var position = hit.Position; @@ -177,7 +178,7 @@ private Color TracePath(RenderContext context, Camera camera, Ray ray, Color bac return new Color(x, y, z); } - if (hit.HitObject.Light != null) + if (hitObject.Light != null) { return emittance; } @@ -192,7 +193,7 @@ private Color TracePath(RenderContext context, Camera camera, Ray ray, Color bac var newRayDirection = Refract(ray.Direction, normal, material.RefractionEta); var newRay = new Ray(position, newRayDirection); - var incoming = TracePath(context, camera, newRay, back, depth + 1, hit.HitObject); + var incoming = TracePath(context, camera, newRay, back, depth + 1, hitObject); var refractedColor = incoming; //emittance * incoming; @@ -285,6 +286,7 @@ private Color Enlight(Color emittance, Hit hit) private Color LightIntensity(Hit hit, Light light, Vector3 lightPosition, Color ambientColor, float ambientFactor) { + var hitObject = hit.HitObject ?? throw new InvalidOperationException("A hit must include the shape it intersected."); var lightColor = light.Color; var lightDirection = lightPosition - hit.Position; @@ -302,25 +304,25 @@ private Color LightIntensity(Hit hit, Light light, Vector3 lightPosition, Color if (ndotLD > 0) { - if (!IsShadow(hit.HitObject, lightPosition, lightDirection, (float)lightDistance)) + if (!IsShadow(hitObject, lightPosition, lightDirection, (float)lightDistance)) { var linghtnessMul = (ambientColor * ambientFactor + lightColor * ndotLD) / attenuation; return lightColor * linghtnessMul; } } - var refraction = hit.HitObject.Material.Refraction; + var refraction = hitObject.Material.Refraction; if (refraction > 0) { - lightColor *= hit.HitObject.Material.Color; + lightColor *= hitObject.Material.Color; } var ambientMul = (ambientColor * ambientFactor + lightColor * ndotLD * refraction) / attenuation; return lightColor * ambientMul; } - private bool IsShadow(Shape currentShape, Vector3 lightPosition, Vector3 lightDirection, float lightDistance) + private bool IsShadow(Shape? currentShape, Vector3 lightPosition, Vector3 lightDirection, float lightDistance) { if (!Scene.ShadowsEnabled) { @@ -374,7 +376,7 @@ private float CalcAmbientOcclusion(Hit hit) return 1f - (factor / Scene.GISamples) * 4f; } - private Hit FindClosestHit(Ray ray, float maxDistance, Shape currentShape = null) + private Hit FindClosestHit(Ray ray, float maxDistance, Shape? currentShape = null) { var closestHit = new Hit(); diff --git a/src/RenderBox/Services/Rendering/Paint.cs b/src/RenderBox/Services/Rendering/Paint.cs index 3aea96c..dfb24f6 100644 --- a/src/RenderBox/Services/Rendering/Paint.cs +++ b/src/RenderBox/Services/Rendering/Paint.cs @@ -11,7 +11,7 @@ public class Paint : IDisposable public const int DPI = 96; public Image Image { get; private set; } - public WriteableBitmap Bitmap { get; private set; } + public WriteableBitmap? Bitmap { get; private set; } public int Width => Bitmap?.PixelWidth ?? 0; public int Height => Bitmap?.PixelHeight ?? 0; public double Scale { get; private set; } @@ -45,7 +45,6 @@ private void CreateBitmap(Image img, int width, int height) var bitmap = new WriteableBitmap(width, height, DPI, DPI, PixelFormats.Bgr32, null); img.Source = bitmap; - Bitmap = null; Bitmap = bitmap; } diff --git a/src/RenderBox/Services/Rendering/Renderer.cs b/src/RenderBox/Services/Rendering/Renderer.cs index ee5a984..568442b 100644 --- a/src/RenderBox/Services/Rendering/Renderer.cs +++ b/src/RenderBox/Services/Rendering/Renderer.cs @@ -13,18 +13,18 @@ public abstract class Renderer : IDisposable public int BatchSize { get; set; } = 32; public Paint Paint { get; private set; } - public event RenderStartHandler OnRenderStarted; - public event RenderCompleteHandler OnRenderComplete; + public event RenderStartHandler? OnRenderStarted; + public event RenderCompleteHandler? OnRenderComplete; - private Thread _renderThread; + private Thread? _renderThread; public Renderer(Paint paint) { Paint = paint; } - public Type GetOptionPageType() + public Type? GetOptionPageType() { var type = GetType(); var optionsType = typeof(IOptionsPage<>); @@ -114,7 +114,7 @@ public virtual RenderContext BuildContext(Dispatcher dispatcher) protected virtual void BatchScreen(RenderContext context, RenderScreenBatch renderScreenBatch, - GetRenderPriority getRenderPriority = null) + GetRenderPriority? getRenderPriority = null) { using var threadManager = new ThreadManager(); diff --git a/src/RenderBox/Services/Rendering/ThreadManager.cs b/src/RenderBox/Services/Rendering/ThreadManager.cs index 4470f1e..926408a 100644 --- a/src/RenderBox/Services/Rendering/ThreadManager.cs +++ b/src/RenderBox/Services/Rendering/ThreadManager.cs @@ -7,8 +7,8 @@ public class ThreadManager : IDisposable public ThreadManagerState State { get; private set; } = ThreadManagerState.Created; private ConcurrentQueue _queue = new ConcurrentQueue(); - private Thread[] _pool; - private Action _onDone; + private Thread[] _pool = Array.Empty(); + private Action? _onDone; private int _endedThreads = 0; private struct Routine @@ -70,18 +70,12 @@ private void SortQueue() private void Kill() { - if (_pool is null) return; - _queue.Clear(); _onDone = null; for (var i = 0; i < _pool.Length; i++) { - if (_pool[i] != null) - { - _pool[i].Interrupt(); - _pool[i] = null; - } + _pool[i].Interrupt(); } } @@ -95,7 +89,7 @@ private void ThreadProcess() { if (_queue.TryDequeue(out var routine)) { - routine.Action?.Invoke(); + routine.Action.Invoke(); } if (_queue.IsEmpty) diff --git a/src/RenderBox/Views/MainWindow.xaml.cs b/src/RenderBox/Views/MainWindow.xaml.cs index 4f33577..3104e5b 100644 --- a/src/RenderBox/Views/MainWindow.xaml.cs +++ b/src/RenderBox/Views/MainWindow.xaml.cs @@ -67,7 +67,7 @@ private void OnTabsSelectionChanged(object sender, SelectionChangedEventArgs e) } } - private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e) + private void OnClosing(object? sender, System.ComponentModel.CancelEventArgs e) { var renderers = _pages.Select(x => x.Value).Select(x => x.Renderer).ToArray(); diff --git a/src/RenderBox/Views/Pages/CanvasPage.xaml.cs b/src/RenderBox/Views/Pages/CanvasPage.xaml.cs index 6edec06..5fe367e 100644 --- a/src/RenderBox/Views/Pages/CanvasPage.xaml.cs +++ b/src/RenderBox/Views/Pages/CanvasPage.xaml.cs @@ -6,7 +6,6 @@ using System.Windows.Input; using RenderBox.Services.Options; using RenderBox.Services.Rendering; -using RenderBox.Shared.Modules.PathTracer; namespace RenderBox.Views.Pages { @@ -15,9 +14,7 @@ public partial class CanvasPage : Page, IDisposable public bool IsActive { get; set; } public bool IsStarted { get; private set; } - public Renderer Renderer { get; set; } - public Camera MainCamera { get; set; } - public Scene Scene { get; set; } + public Renderer? Renderer { get; private set; } private readonly ObservableCollection _log; private readonly Stopwatch _timer = new(); @@ -64,6 +61,11 @@ public void Dispose() private void OnLoaded(object sender, RoutedEventArgs e) { var window = Window.GetWindow(this); + if (window is null) + { + return; + } + window.KeyUp += OnKeyPress; // SidePanel.Visibility = Visibility.Hidden; @@ -71,7 +73,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) SizeChanged += OnSizeChanged; } - private void SetupRender(Type type) + private void SetupRender(Type? type) { var scale = Resolution.Value; var w = (int)(ActualWidth * scale); @@ -79,7 +81,17 @@ private void SetupRender(Type type) if (Renderer == null) { - Renderer = (Renderer)Activator.CreateInstance(type, new Paint(Image, w, h, scale)); + if (type is null) + { + throw new InvalidOperationException("A renderer type is required before the first render."); + } + + if (Activator.CreateInstance(type, new Paint(Image, w, h, scale)) is not Renderer renderer) + { + throw new InvalidOperationException($"Could not create renderer '{type.FullName}'."); + } + + Renderer = renderer; Renderer.OnRenderStarted += () => _timer.Restart(); Renderer.OnRenderComplete += () => _log.Add($"Render frame: {_timer.ElapsedMilliseconds} ms"); @@ -89,7 +101,12 @@ private void SetupRender(Type type) { var page = Activator.CreateInstance(pageType); var useSource = pageType.GetMethod(nameof(IOptionsPage.UseSource)); - _ = useSource.Invoke(page, new[] { Renderer }); + if (page is null || useSource is null) + { + throw new InvalidOperationException($"Could not create options page '{pageType.FullName}'."); + } + + _ = useSource.Invoke(page, new object[] { Renderer }); _ = OptionsFrame.Navigate(page); } @@ -102,6 +119,11 @@ private void SetupRender(Type type) private void Render() { + if (Renderer is null) + { + return; + } + Renderer.Render(Dispatcher); } @@ -166,7 +188,7 @@ private void Resolution_ValueChanged(object sender, RoutedPropertyChangedEventAr } } - private void OnKeyPress(object sender, KeyEventArgs e) + private void OnKeyPress(object? sender, KeyEventArgs e) { if (IsActive) { diff --git a/src/RenderBox/Views/Pages/DrawingPage.xaml.cs b/src/RenderBox/Views/Pages/DrawingPage.xaml.cs index 9cd80f3..d1c735a 100644 --- a/src/RenderBox/Views/Pages/DrawingPage.xaml.cs +++ b/src/RenderBox/Views/Pages/DrawingPage.xaml.cs @@ -9,7 +9,7 @@ namespace RenderBox.Views.Pages { public partial class DrawingPage : Page, IOptionsPage { - private DrawingRenderer _source; + private DrawingRenderer _source = null!; public DrawingPage() { diff --git a/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs b/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs index e7d2704..b3a5b1c 100644 --- a/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs +++ b/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs @@ -7,7 +7,7 @@ namespace RenderBox.Views.Pages { public partial class MandelbrotPage : Page, IOptionsPage { - private MandelbrotRenderer _source; + private MandelbrotRenderer _source = null!; public MandelbrotPage() { @@ -36,6 +36,10 @@ public MandelbrotPage() foreach (var filter in filters) { var instance = Activator.CreateInstance(filter); + if (instance is not IPaletteFilter paletteFilter) + { + continue; + } var button = new Button { @@ -45,7 +49,7 @@ public MandelbrotPage() button.Click += (s, e) => { - _source.Filter = (IPaletteFilter)instance; + _source.Filter = paletteFilter; _source.Render(Dispatcher); }; diff --git a/src/RenderBox/Views/Pages/PathTracePage.xaml.cs b/src/RenderBox/Views/Pages/PathTracePage.xaml.cs index df1068d..843469e 100644 --- a/src/RenderBox/Views/Pages/PathTracePage.xaml.cs +++ b/src/RenderBox/Views/Pages/PathTracePage.xaml.cs @@ -6,7 +6,7 @@ namespace RenderBox.Views.Pages { public partial class PathTracePage : Page, IOptionsPage { - private PathTraceRenderer _source; + private PathTraceRenderer _source = null!; public PathTracePage() { diff --git a/src/RenderBox/Views/Pages/TerrainPage.xaml.cs b/src/RenderBox/Views/Pages/TerrainPage.xaml.cs index 54dce4c..a9d0b04 100644 --- a/src/RenderBox/Views/Pages/TerrainPage.xaml.cs +++ b/src/RenderBox/Views/Pages/TerrainPage.xaml.cs @@ -6,8 +6,6 @@ namespace RenderBox.Views.Pages { public partial class TerrainPage : Page, IOptionsPage { - private TerrainRenderer _source; - public TerrainPage() { InitializeComponent(); @@ -15,7 +13,6 @@ public TerrainPage() public void UseSource(TerrainRenderer source) { - _source = source; } } } From 528c62af4cf077447dbd238440aab7cb0c459395 Mon Sep 17 00:00:00 2001 From: Alexander Gluschenko Date: Tue, 26 May 2026 00:55:19 +0400 Subject: [PATCH 3/4] Add Matrix4x4 and rotation support for shapes/camera Introduce Matrix4x4 with rotation, multiplication and transform helpers and add Rotation properties to Camera and Shape. Implement local/world point and direction transforms and inverse via transpose to support rotated shapes. Update Sphere, Mesh and Box to transform rays, positions, normals and light emission correctly (Mesh now tests triangles in local space and converts hits back to world space; SetData keeps local vertices). Add Camera.TransformDirection and rotation setters (degrees/radians) and wire camera rotation controls in the renderer. Update CornellBox to demonstrate rotated boxes. --- src/RenderBox.Shared/Core/Vector.cs | 126 ++++++++++++++++++ .../Modules/PathTracer/Camera.cs | 23 ++++ .../Modules/PathTracer/Scenes/CornellBox.cs | 8 +- .../Modules/PathTracer/Shape.cs | 41 ++++++ .../Modules/PathTracer/Shapes/Box.cs | 2 +- .../Modules/PathTracer/Shapes/Mesh.cs | 21 ++- .../Modules/PathTracer/Shapes/Sphere.cs | 16 ++- .../Services/Renderers/PathTraceRenderer.cs | 14 +- 8 files changed, 231 insertions(+), 20 deletions(-) diff --git a/src/RenderBox.Shared/Core/Vector.cs b/src/RenderBox.Shared/Core/Vector.cs index 6e216d6..800430a 100644 --- a/src/RenderBox.Shared/Core/Vector.cs +++ b/src/RenderBox.Shared/Core/Vector.cs @@ -287,6 +287,132 @@ public override bool Equals(object? obj) public override int GetHashCode() => HashCode.Combine(M); } + public struct Matrix4x4 + { + private readonly float[] M; + + public static Matrix4x4 Identity => new(true); + + public Matrix4x4(bool identity) : this() + { + M = new float[16]; + + if (!identity) return; + + M[0] = 1.0f; + M[5] = 1.0f; + M[10] = 1.0f; + M[15] = 1.0f; + } + + public float this[int i] + { + get => M[i]; + set => M[i] = value; + } + + public static Matrix4x4 CreateRotation(Vector3 rotation) + { + return CreateRotationZ(rotation.z) * CreateRotationY(rotation.y) * CreateRotationX(rotation.x); + } + + public static Matrix4x4 CreateRotationX(float angle) + { + var result = Identity; + var sin = MathF.Sin(angle); + var cos = MathF.Cos(angle); + + result[5] = cos; + result[6] = sin; + result[9] = -sin; + result[10] = cos; + + return result; + } + + public static Matrix4x4 CreateRotationY(float angle) + { + var result = Identity; + var sin = MathF.Sin(angle); + var cos = MathF.Cos(angle); + + result[0] = cos; + result[2] = -sin; + result[8] = sin; + result[10] = cos; + + return result; + } + + public static Matrix4x4 CreateRotationZ(float angle) + { + var result = Identity; + var sin = MathF.Sin(angle); + var cos = MathF.Cos(angle); + + result[0] = cos; + result[1] = sin; + result[4] = -sin; + result[5] = cos; + + return result; + } + + public static Matrix4x4 operator *(Matrix4x4 a, Matrix4x4 b) + { + var result = new Matrix4x4(false); + + for (var row = 0; row < 4; row++) + { + for (var column = 0; column < 4; column++) + { + var value = 0f; + for (var i = 0; i < 4; i++) + { + value += a[row + i * 4] * b[i + column * 4]; + } + + result[row + column * 4] = value; + } + } + + return result; + } + + public Matrix4x4 Transpose() + { + var result = new Matrix4x4(false); + + for (var row = 0; row < 4; row++) + { + for (var column = 0; column < 4; column++) + { + result[row + column * 4] = M[column + row * 4]; + } + } + + return result; + } + + public Vector3 TransformPoint(Vector3 point) + { + return Transform(point, 1); + } + + public Vector3 TransformDirection(Vector3 direction) + { + return Transform(direction, 0); + } + + private Vector3 Transform(Vector3 vector, float w) + { + return new Vector3( + M[0] * vector.x + M[4] * vector.y + M[8] * vector.z + M[12] * w, + M[1] * vector.x + M[5] * vector.y + M[9] * vector.z + M[13] * w, + M[2] * vector.x + M[6] * vector.y + M[10] * vector.z + M[14] * w); + } + } + public static class VectorMath { #region Vector2 diff --git a/src/RenderBox.Shared/Modules/PathTracer/Camera.cs b/src/RenderBox.Shared/Modules/PathTracer/Camera.cs index 999cfb1..7cd8874 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Camera.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Camera.cs @@ -5,6 +5,7 @@ namespace RenderBox.Shared.Modules.PathTracer public class Camera { public Vector3 Position { get; set; } + public Vector3 Rotation { get; set; } public Vector3 Target { get; set; } public int MaxBounceDepth { get; set; } = 3; public float FOV { get; set; } = 90; @@ -21,6 +22,7 @@ public Camera(Vector3 position) Target = new Vector3(0.0, 0.0, 0.0); Position = position; + Rotation = Vector3.Zero; ViewMatrix = new Quaternion(true); PosMatrix = new Quaternion(true); @@ -56,6 +58,27 @@ public void CalculateRayMatrix() RayMatrix = ViewMatrix * PosMatrix * BiasMatrix * ViewPosMatrix; } + public Vector3 TransformDirection(Vector3 direction) + { + return Matrix4x4.CreateRotation(Rotation).TransformDirection(direction); + } + + public Camera SetRotation(Vector3 rotation) + { + Rotation = rotation; + return this; + } + + public Camera SetRotationDegrees(Vector3 rotation) + { + Rotation = new Vector3( + MathHelpres.DegToRad(rotation.x), + MathHelpres.DegToRad(rotation.y), + MathHelpres.DegToRad(rotation.z)); + + return this; + } + private Quaternion GetBiasMatrix() { var B = new Quaternion(false); diff --git a/src/RenderBox.Shared/Modules/PathTracer/Scenes/CornellBox.cs b/src/RenderBox.Shared/Modules/PathTracer/Scenes/CornellBox.cs index 50befae..75b247e 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Scenes/CornellBox.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Scenes/CornellBox.cs @@ -50,11 +50,11 @@ public CornellBox() new Sphere(new Vector3(1.8, 0, 0), .2f, Color.White), - new Box(new Vector3(0, -1.5, -1), Color.Yellow), - new Box(new Vector3(1, -1.5, -1), Color.Red), - new Box(new Vector3(-1, -1.5, -1), Color.Blue), + new Box(new Vector3(0, -1.5, -1), Color.Yellow).SetRotationDegrees(new Vector3(0, 35, 0)), + new Box(new Vector3(1, -1.5, -1), Color.Red).SetRotationDegrees(new Vector3(0, -25, 0)), + new Box(new Vector3(-1, -1.5, -1), Color.Blue).SetRotationDegrees(new Vector3(0, 0, 25)), - new Box(new Vector3(-1.6, -1.9, 1.4), Color.White, new Vector3(0.24f, 0.24f, 0.24f)) { Material = metal }, + new Box(new Vector3(-1.6, -1.9, 1.4), Color.White, new Vector3(0.24f, 0.24f, 0.24f)) { Material = metal }.SetRotationDegrees(new Vector3(0, 45, 0)), }); UpdateLights(); diff --git a/src/RenderBox.Shared/Modules/PathTracer/Shape.cs b/src/RenderBox.Shared/Modules/PathTracer/Shape.cs index b93136f..c8aee6e 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Shape.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Shape.cs @@ -14,12 +14,14 @@ public interface IShape public abstract class Shape : IShape { public Vector3 Position { get; set; } + public Vector3 Rotation { get; set; } public Material Material { get; set; } public Light? Light { get; private set; } public Shape(Vector3 pos, Color diffuse) { Position = pos; + Rotation = Vector3.Zero; // Material = new Material { @@ -35,6 +37,45 @@ public Shape SetLight(Light light) return this; } + public Shape SetRotation(Vector3 rotation) + { + Rotation = rotation; + return this; + } + + public Shape SetRotationDegrees(Vector3 rotation) + { + Rotation = new Vector3( + MathHelpres.DegToRad(rotation.x), + MathHelpres.DegToRad(rotation.y), + MathHelpres.DegToRad(rotation.z)); + + return this; + } + + protected Matrix4x4 RotationMatrix => Matrix4x4.CreateRotation(Rotation); + protected Matrix4x4 InverseRotationMatrix => RotationMatrix.Transpose(); + + protected Vector3 LocalToWorldPoint(Vector3 point) + { + return Position + RotationMatrix.TransformPoint(point); + } + + protected Vector3 WorldToLocalPoint(Vector3 point) + { + return InverseRotationMatrix.TransformPoint(point - Position); + } + + protected Vector3 LocalToWorldDirection(Vector3 direction) + { + return RotationMatrix.TransformDirection(direction); + } + + protected Vector3 WorldToLocalDirection(Vector3 direction) + { + return InverseRotationMatrix.TransformDirection(direction); + } + public abstract Vector3 CalcNormal(Vector3 pos); public abstract bool GetIntersection(Ray ray, double maxDistance, out Hit hit, out double distance); diff --git a/src/RenderBox.Shared/Modules/PathTracer/Shapes/Box.cs b/src/RenderBox.Shared/Modules/PathTracer/Shapes/Box.cs index 508793a..5a11c89 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Shapes/Box.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Shapes/Box.cs @@ -57,7 +57,7 @@ public Box(Vector3 position, Color diffuse, Vector3? scale = null) : base(positi public override Vector3 GetLightEmission(Vector3 random) { - return Scale * random; + return LocalToWorldDirection(Scale * random); } } } diff --git a/src/RenderBox.Shared/Modules/PathTracer/Shapes/Mesh.cs b/src/RenderBox.Shared/Modules/PathTracer/Shapes/Mesh.cs index 8461c45..205e42a 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Shapes/Mesh.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Shapes/Mesh.cs @@ -18,6 +18,9 @@ public Mesh(Vector3 position, Color diffuse) : base(position, diffuse) public override bool GetIntersection(Ray ray, double maxDistance, out Hit hit, out double distance) { hit = new Hit(); + distance = double.PositiveInfinity; + + var localRay = new Ray(WorldToLocalPoint(ray.Origin), WorldToLocalDirection(ray.Direction)); for (int k = 0; k < TrianglesCount; ++k) { @@ -25,33 +28,39 @@ public override bool GetIntersection(Ray ray, double maxDistance, out Hit hit, o var v1 = Vertices[Indices[k * 3 + 1]]; var v2 = Vertices[Indices[k * 3 + 2]]; - if (RayTriangleIntersect(v0, v1, v2, ray, out var localHit)) + if (RayTriangleIntersect(v0, v1, v2, localRay, out var localHit)) { if (localHit.IsHitting) { - var dist = (localHit.Position - ray.Origin).Length; + var worldPosition = LocalToWorldPoint(localHit.Position); + var dist = (worldPosition - ray.Origin).Length; if (dist > maxDistance) { continue; } - hit = localHit; + if (dist < distance) + { + localHit.Position = worldPosition; + localHit.Normal = Normalize(LocalToWorldDirection(localHit.Normal)); + hit = localHit; + distance = dist; + } } } } - distance = (hit.Position - ray.Origin).Length; return hit.IsHitting; } public override Vector3 CalcNormal(Vector3 pos) { - return Normalize(pos - Position); + return Normalize(LocalToWorldDirection(Normalize(WorldToLocalPoint(pos)))); } public void SetData(IEnumerable verts, List indices) { - Vertices = verts.Select(x => Position + x).ToList(); + Vertices = verts.ToList(); Indices = indices; TrianglesCount = indices.Count / 3; } diff --git a/src/RenderBox.Shared/Modules/PathTracer/Shapes/Sphere.cs b/src/RenderBox.Shared/Modules/PathTracer/Shapes/Sphere.cs index 5940535..853ce5a 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Shapes/Sphere.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Shapes/Sphere.cs @@ -16,10 +16,11 @@ public override bool GetIntersection(Ray ray, double maxDistance, out Hit hit, o { hit = new Hit(); - var delta = ray.Origin - Position; + var localRay = new Ray(WorldToLocalPoint(ray.Origin), WorldToLocalDirection(ray.Direction)); + var delta = localRay.Origin; - var a = Dot(ray.Direction, ray.Direction); - var b = 2 * Dot(ray.Direction, delta); + var a = Dot(localRay.Direction, localRay.Direction); + var b = 2 * Dot(localRay.Direction, delta); var c = Dot(delta, delta) - Radius * Radius; double dt = b * b - 4 * a * c; @@ -38,7 +39,8 @@ public override bool GetIntersection(Ray ray, double maxDistance, out Hit hit, o return false; } - var position = ray.Origin + ray.Direction * (float)D; + var localPosition = localRay.Origin + localRay.Direction * (float)D; + var position = LocalToWorldPoint(localPosition); var dist = Distance(position, ray.Origin); if (dist > maxDistance) @@ -48,7 +50,7 @@ public override bool GetIntersection(Ray ray, double maxDistance, out Hit hit, o } hit.Position = position; - hit.Normal = CalcNormal(hit.Position); + hit.Normal = CalcNormal(position); hit.HitObject = this; distance = dist; @@ -58,12 +60,12 @@ public override bool GetIntersection(Ray ray, double maxDistance, out Hit hit, o public override Vector3 CalcNormal(Vector3 pos) { - return Normalize(pos - Position); + return Normalize(LocalToWorldDirection(Normalize(WorldToLocalPoint(pos)))); } public override Vector3 GetLightEmission(Vector3 random) { - return Normalize(random) * Radius; + return LocalToWorldDirection(Normalize(random) * Radius); } } } diff --git a/src/RenderBox/Services/Renderers/PathTraceRenderer.cs b/src/RenderBox/Services/Renderers/PathTraceRenderer.cs index 6ba6763..d72d61b 100644 --- a/src/RenderBox/Services/Renderers/PathTraceRenderer.cs +++ b/src/RenderBox/Services/Renderers/PathTraceRenderer.cs @@ -87,7 +87,7 @@ int GetRenderPriority(int x, int y) var posX = (2 * (x + 0.5f) / width - 1) * aspectRatio * fovScale; var posY = (1 - 2 * (y + 0.5f) / height) * fovScale; // - var dir = Normalize(new Vector3(posX, posY, -1)); + var dir = Normalize(camera.TransformDirection(new Vector3(posX, posY, -1))); var ray = new Ray(orig, dir); // var color = TracePath(context, camera, ray, Scene.BackgroundColor); @@ -417,7 +417,17 @@ public override void OnKeyPress(Key key, Action onRender) if (key == Key.W) MainCamera.Position += Vector3.Back * 0.5f; if (key == Key.S) MainCamera.Position += Vector3.Forward * 0.5f; - if (origPos != MainCamera.Position) + var origRotation = MainCamera.Rotation; + var rotationStep = (float)MathHelpres.DegToRad(5); + + if (key == Key.Left) MainCamera.Rotation += Vector3.Up * rotationStep; + if (key == Key.Right) MainCamera.Rotation += Vector3.Down * rotationStep; + if (key == Key.Up) MainCamera.Rotation += Vector3.Left * rotationStep; + if (key == Key.Down) MainCamera.Rotation += Vector3.Right * rotationStep; + if (key == Key.Z) MainCamera.Rotation += Vector3.Forward * rotationStep; + if (key == Key.X) MainCamera.Rotation += Vector3.Back * rotationStep; + + if (origPos != MainCamera.Position || origRotation != MainCamera.Rotation) { onRender(); } From 4e6c8a1cadfe444a85d5caffb0c6be88ec5a9d03 Mon Sep 17 00:00:00 2001 From: Alexander Gluschenko Date: Tue, 26 May 2026 01:35:52 +0400 Subject: [PATCH 4/4] Use nullable sources with guarded Source property Improve null-safety across the path tracer and UI pages. - Light.cs: change Shape from IShape with null-forgiving to nullable Shape?. - Scene.cs: tighten UpdateLights filter to require x?.Shape is not null before casting to Light. - DrawingPage, MandelbrotPage, PathTracePage: make backing renderer fields nullable, add a guarded Source property that throws if UseSource wasn't called, and update all usages to go through Source to avoid null-forgiving fields and NREs. These changes make initialization expectations explicit and reduce unsafe null assertions. --- .../Modules/PathTracer/Light.cs | 2 +- .../Modules/PathTracer/Scene.cs | 2 +- src/RenderBox/Views/Pages/DrawingPage.xaml.cs | 9 +++-- .../Views/Pages/MandelbrotPage.xaml.cs | 25 ++++++------ .../Views/Pages/PathTracePage.xaml.cs | 39 ++++++++++--------- 5 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/RenderBox.Shared/Modules/PathTracer/Light.cs b/src/RenderBox.Shared/Modules/PathTracer/Light.cs index 670b8d7..70cb018 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Light.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Light.cs @@ -11,7 +11,7 @@ public class Light public float LinearAttenuation { get; set; } public float QuadraticAttenuation { get; set; } - public IShape Shape { get; set; } = null!; + public Shape? Shape { get; set; } public Light(Color color, double intensity) { diff --git a/src/RenderBox.Shared/Modules/PathTracer/Scene.cs b/src/RenderBox.Shared/Modules/PathTracer/Scene.cs index c33a690..ee5474e 100644 --- a/src/RenderBox.Shared/Modules/PathTracer/Scene.cs +++ b/src/RenderBox.Shared/Modules/PathTracer/Scene.cs @@ -22,7 +22,7 @@ public Scene() public void UpdateLights() { - Lights = Shapes.Select(x => x.Light).Where(x => x is not null).Cast().ToArray(); + Lights = Shapes.Select(x => x.Light).Where(x => x?.Shape is not null).Cast().ToArray(); } } } diff --git a/src/RenderBox/Views/Pages/DrawingPage.xaml.cs b/src/RenderBox/Views/Pages/DrawingPage.xaml.cs index d1c735a..54a8697 100644 --- a/src/RenderBox/Views/Pages/DrawingPage.xaml.cs +++ b/src/RenderBox/Views/Pages/DrawingPage.xaml.cs @@ -9,7 +9,8 @@ namespace RenderBox.Views.Pages { public partial class DrawingPage : Page, IOptionsPage { - private DrawingRenderer _source = null!; + private DrawingRenderer? _source; + private DrawingRenderer Source => _source ?? throw new InvalidOperationException($"{nameof(UseSource)} must be called before using this page."); public DrawingPage() { @@ -23,11 +24,11 @@ public void UseSource(DrawingRenderer source) private void BlitButton_Click(object sender, RoutedEventArgs e) { - _source.SetRender((context) => + Source.SetRender((context) => { - _source.Paint.FillRect(0, 0, context.Width, context.Height, Color.Green); + Source.Paint.FillRect(0, 0, context.Width, context.Height, Color.Green); }); - _source.Render(Dispatcher); + Source.Render(Dispatcher); } } } diff --git a/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs b/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs index b3a5b1c..d9aaca7 100644 --- a/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs +++ b/src/RenderBox/Views/Pages/MandelbrotPage.xaml.cs @@ -7,7 +7,8 @@ namespace RenderBox.Views.Pages { public partial class MandelbrotPage : Page, IOptionsPage { - private MandelbrotRenderer _source = null!; + private MandelbrotRenderer? _source; + private MandelbrotRenderer Source => _source ?? throw new InvalidOperationException($"{nameof(UseSource)} must be called before using this page."); public MandelbrotPage() { @@ -27,8 +28,8 @@ public MandelbrotPage() noFilterButton.Click += (s, e) => { - _source.Filter = null; - _source.Render(Dispatcher); + Source.Filter = null; + Source.Render(Dispatcher); }; EffectsPanel.Children.Add(noFilterButton); @@ -49,8 +50,8 @@ public MandelbrotPage() button.Click += (s, e) => { - _source.Filter = paletteFilter; - _source.Render(Dispatcher); + Source.Filter = paletteFilter; + Source.Render(Dispatcher); }; EffectsPanel.Children.Add(button); @@ -61,18 +62,18 @@ public void UseSource(MandelbrotRenderer source) { _source = source; - Iterations.Text = _source.Iterations.ToString(); - Extent.Text = _source.Extent.ToString(); - BatchSize.Text = _source.BatchSize.ToString(); + Iterations.Text = Source.Iterations.ToString(); + Extent.Text = Source.Extent.ToString(); + BatchSize.Text = Source.BatchSize.ToString(); } private void ApplyButton_Click(object sender, System.Windows.RoutedEventArgs e) { - _source.Iterations = int.TryParse(Iterations.Text, out var a) ? a : 0; - _source.Extent = double.TryParse(Extent.Text, out var b) ? b : 0; - _source.BatchSize = int.TryParse(BatchSize.Text, out var c) ? c : 0; + Source.Iterations = int.TryParse(Iterations.Text, out var a) ? a : 0; + Source.Extent = double.TryParse(Extent.Text, out var b) ? b : 0; + Source.BatchSize = int.TryParse(BatchSize.Text, out var c) ? c : 0; - _source.Render(Dispatcher); + Source.Render(Dispatcher); } } diff --git a/src/RenderBox/Views/Pages/PathTracePage.xaml.cs b/src/RenderBox/Views/Pages/PathTracePage.xaml.cs index 843469e..07dccdf 100644 --- a/src/RenderBox/Views/Pages/PathTracePage.xaml.cs +++ b/src/RenderBox/Views/Pages/PathTracePage.xaml.cs @@ -6,7 +6,8 @@ namespace RenderBox.Views.Pages { public partial class PathTracePage : Page, IOptionsPage { - private PathTraceRenderer _source = null!; + private PathTraceRenderer? _source; + private PathTraceRenderer Source => _source ?? throw new InvalidOperationException($"{nameof(UseSource)} must be called before using this page."); public PathTracePage() { @@ -17,31 +18,31 @@ public void UseSource(PathTraceRenderer source) { _source = source; - Lighting.IsChecked = _source.Scene.LightingEnabled; - Shadows.IsChecked = _source.Scene.ShadowsEnabled; - SoftShadows.IsChecked = _source.Scene.SoftShadows; - AmbientOcclusion.IsChecked = _source.Scene.AmbientOcclusion; - GISamples.Text = _source.Scene.GISamples.ToString(); - FOV.Text = _source.MainCamera.FOV.ToString(); - CameraDistance.Text = _source.MainCamera.MaxDistance.ToString(); + Lighting.IsChecked = Source.Scene.LightingEnabled; + Shadows.IsChecked = Source.Scene.ShadowsEnabled; + SoftShadows.IsChecked = Source.Scene.SoftShadows; + AmbientOcclusion.IsChecked = Source.Scene.AmbientOcclusion; + GISamples.Text = Source.Scene.GISamples.ToString(); + FOV.Text = Source.MainCamera.FOV.ToString(); + CameraDistance.Text = Source.MainCamera.MaxDistance.ToString(); - BatchSize.Text = _source.BatchSize.ToString(); + BatchSize.Text = Source.BatchSize.ToString(); } private void ApplyButton_Click(object sender, System.Windows.RoutedEventArgs e) { - _source.Mode = GetRenderMode(); - _source.BatchSize = int.TryParse(BatchSize.Text, out int num) ? num : 0; + Source.Mode = GetRenderMode(); + Source.BatchSize = int.TryParse(BatchSize.Text, out int num) ? num : 0; - _source.Scene.LightingEnabled = Lighting.IsChecked ?? false; - _source.Scene.ShadowsEnabled = Shadows.IsChecked ?? false; - _source.Scene.SoftShadows = SoftShadows.IsChecked ?? false; - _source.Scene.AmbientOcclusion = AmbientOcclusion.IsChecked ?? false; - _source.Scene.GISamples = int.TryParse(GISamples.Text, out num) ? num : 0; - _source.MainCamera.FOV = float.TryParse(FOV.Text, out var f) ? f : 0; - _source.MainCamera.MaxDistance = float.TryParse(CameraDistance.Text, out f) ? f : 0; + Source.Scene.LightingEnabled = Lighting.IsChecked ?? false; + Source.Scene.ShadowsEnabled = Shadows.IsChecked ?? false; + Source.Scene.SoftShadows = SoftShadows.IsChecked ?? false; + Source.Scene.AmbientOcclusion = AmbientOcclusion.IsChecked ?? false; + Source.Scene.GISamples = int.TryParse(GISamples.Text, out num) ? num : 0; + Source.MainCamera.FOV = float.TryParse(FOV.Text, out var f) ? f : 0; + Source.MainCamera.MaxDistance = float.TryParse(CameraDistance.Text, out f) ? f : 0; - _source.Render(Dispatcher); + Source.Render(Dispatcher); } private RenderMode GetRenderMode()