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/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..800430a 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,15 +281,138 @@ 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); } + 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 d71d6cc..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,8 +22,13 @@ 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); BiasMatrix = GetBiasMatrixInverse(); + ViewPosMatrix = new Quaternion(true); + RayMatrix = new Quaternion(true); } public void LookAt(Vector3 position, Vector3 target, bool rotateAround) @@ -52,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/Light.cs b/src/RenderBox.Shared/Modules/PathTracer/Light.cs index 9819a13..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; } + public Shape? Shape { get; set; } 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..ee5474e 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?.Shape is not null).Cast().ToArray(); } } } 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 b37c8f9..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 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.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 + 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..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); @@ -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(); @@ -415,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(); } 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..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; + 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 e7d2704..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; + 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); @@ -36,6 +37,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,8 +50,8 @@ public MandelbrotPage() button.Click += (s, e) => { - _source.Filter = (IPaletteFilter)instance; - _source.Render(Dispatcher); + Source.Filter = paletteFilter; + Source.Render(Dispatcher); }; EffectsPanel.Children.Add(button); @@ -57,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 df1068d..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; + 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() 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; } } }