From 3c34ae4eefec19325c08b7315801910308474223 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Dec 2023 14:52:46 -0800 Subject: [PATCH 01/14] Whoops... forgot to update examples again. --- Examples/BallWithAreaLight/BallWithAreaLight.swift | 1 - Examples/BarthSextic/BarthSextic.swift | 1 - Examples/Blob/Blob.swift | 1 - Examples/Breather/Breather.swift | 1 - Examples/Cavatappi/Cavatappi.swift | 1 - Examples/DecoCube/DecoCube.swift | 1 - Examples/Die/Die.swift | 1 - Examples/DimlyLitScene/DimlyLitScene.swift | 1 - Examples/FishEye/FishEye.swift | 1 - Examples/HappyHalloween/HappyHalloween.swift | 1 - Examples/HollowedSphere/HollowedSphere.swift | 1 - Examples/Hourglass/Hourglass.swift | 1 - Examples/QuickStart/QuickStart.swift | 1 - Examples/RainbowBall/RainbowBall.swift | 1 - Examples/Rings/Rings.swift | 1 - Examples/StarPrism/StarPrism.swift | 1 - Examples/Superellipsoids/Superellipsoids.swift | 1 - Examples/TDOR/TDOR.swift | 1 - Examples/Vase/Vase.swift | 1 - Examples/Wine/Wine.swift | 1 - 20 files changed, 20 deletions(-) diff --git a/Examples/BallWithAreaLight/BallWithAreaLight.swift b/Examples/BallWithAreaLight/BallWithAreaLight.swift index 081e168..736dc2b 100644 --- a/Examples/BallWithAreaLight/BallWithAreaLight.swift +++ b/Examples/BallWithAreaLight/BallWithAreaLight.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct BallWithAreaLight: ScintillaApp { var world: World = World { diff --git a/Examples/BarthSextic/BarthSextic.swift b/Examples/BarthSextic/BarthSextic.swift index 57fceef..c584768 100644 --- a/Examples/BarthSextic/BarthSextic.swift +++ b/Examples/BarthSextic/BarthSextic.swift @@ -10,7 +10,6 @@ import ScintillaLib let φ: Double = 1.61833987 -@available(macOS 12.0, *) @main struct BarthSextic: ScintillaApp { var world: World = World { diff --git a/Examples/Blob/Blob.swift b/Examples/Blob/Blob.swift index 03bc6cb..b227ae0 100644 --- a/Examples/Blob/Blob.swift +++ b/Examples/Blob/Blob.swift @@ -8,7 +8,6 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct Blob: ScintillaApp { var world = World { diff --git a/Examples/Breather/Breather.swift b/Examples/Breather/Breather.swift index 403d4fb..1e34419 100644 --- a/Examples/Breather/Breather.swift +++ b/Examples/Breather/Breather.swift @@ -22,7 +22,6 @@ func z(u: Double, v: Double) -> Double { } // ACHTUNG: This takes a while to render! -@available(macOS 12.0, *) @main struct Breather: ScintillaApp { var world = World { diff --git a/Examples/Cavatappi/Cavatappi.swift b/Examples/Cavatappi/Cavatappi.swift index bc544bb..7658a91 100644 --- a/Examples/Cavatappi/Cavatappi.swift +++ b/Examples/Cavatappi/Cavatappi.swift @@ -8,7 +8,6 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct Cavatappi: ScintillaApp { var world = World { diff --git a/Examples/DecoCube/DecoCube.swift b/Examples/DecoCube/DecoCube.swift index 82915b0..0f988ce 100644 --- a/Examples/DecoCube/DecoCube.swift +++ b/Examples/DecoCube/DecoCube.swift @@ -19,7 +19,6 @@ func decoCubeColor(_ x: Double, _ y: Double, _ z: Double) -> (Double, Double, Do return (pow(x*x + y*y + z*z, 0.5)/3.0, 1.0, 0.5) } -@available(macOS 12.0, *) @main struct DecoCube: ScintillaApp { var world = World { diff --git a/Examples/Die/Die.swift b/Examples/Die/Die.swift index 63ea9b6..c9b2a1d 100644 --- a/Examples/Die/Die.swift +++ b/Examples/Die/Die.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct Die: ScintillaApp { var world: World { diff --git a/Examples/DimlyLitScene/DimlyLitScene.swift b/Examples/DimlyLitScene/DimlyLitScene.swift index 85abd78..6922fca 100644 --- a/Examples/DimlyLitScene/DimlyLitScene.swift +++ b/Examples/DimlyLitScene/DimlyLitScene.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct DimlyLitScene: ScintillaApp { var world = World { diff --git a/Examples/FishEye/FishEye.swift b/Examples/FishEye/FishEye.swift index 72ea0fc..1e50fde 100644 --- a/Examples/FishEye/FishEye.swift +++ b/Examples/FishEye/FishEye.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct FishEye: ScintillaApp { var world = World { diff --git a/Examples/HappyHalloween/HappyHalloween.swift b/Examples/HappyHalloween/HappyHalloween.swift index 9090b71..4fa620d 100644 --- a/Examples/HappyHalloween/HappyHalloween.swift +++ b/Examples/HappyHalloween/HappyHalloween.swift @@ -20,7 +20,6 @@ func stem(x: Double, y: Double, z: Double) -> Double { 0.2*sin(5.0*atan2(x, z)) - 1 // Adds periodic ribbing on the surface } -@available(macOS 12.0, *) @main struct HappyHalloween: ScintillaApp { var world: World = World { diff --git a/Examples/HollowedSphere/HollowedSphere.swift b/Examples/HollowedSphere/HollowedSphere.swift index 0d0bc76..67c8a2f 100644 --- a/Examples/HollowedSphere/HollowedSphere.swift +++ b/Examples/HollowedSphere/HollowedSphere.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct HollowedSphere: ScintillaApp { var world = World { diff --git a/Examples/Hourglass/Hourglass.swift b/Examples/Hourglass/Hourglass.swift index 21c7d5b..e749b5e 100644 --- a/Examples/Hourglass/Hourglass.swift +++ b/Examples/Hourglass/Hourglass.swift @@ -8,7 +8,6 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct Hourglass: ScintillaApp { var world = World { diff --git a/Examples/QuickStart/QuickStart.swift b/Examples/QuickStart/QuickStart.swift index 6896f52..cca6db0 100644 --- a/Examples/QuickStart/QuickStart.swift +++ b/Examples/QuickStart/QuickStart.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct QuickStart: ScintillaApp { var world = World { diff --git a/Examples/RainbowBall/RainbowBall.swift b/Examples/RainbowBall/RainbowBall.swift index 32bf451..78692a4 100644 --- a/Examples/RainbowBall/RainbowBall.swift +++ b/Examples/RainbowBall/RainbowBall.swift @@ -8,7 +8,6 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct RainbowBall: ScintillaApp { var world = World { diff --git a/Examples/Rings/Rings.swift b/Examples/Rings/Rings.swift index 8d8c4a3..d31abce 100644 --- a/Examples/Rings/Rings.swift +++ b/Examples/Rings/Rings.swift @@ -8,7 +8,6 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct Rings: ScintillaApp { var world = World { diff --git a/Examples/StarPrism/StarPrism.swift b/Examples/StarPrism/StarPrism.swift index 9be02a9..0cd8d15 100644 --- a/Examples/StarPrism/StarPrism.swift +++ b/Examples/StarPrism/StarPrism.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct StarPrism: ScintillaApp { var world = World { diff --git a/Examples/Superellipsoids/Superellipsoids.swift b/Examples/Superellipsoids/Superellipsoids.swift index 865ffc4..1913c09 100644 --- a/Examples/Superellipsoids/Superellipsoids.swift +++ b/Examples/Superellipsoids/Superellipsoids.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct Superellipsoids: ScintillaApp { var world: World = World { diff --git a/Examples/TDOR/TDOR.swift b/Examples/TDOR/TDOR.swift index 853b08d..adce97f 100644 --- a/Examples/TDOR/TDOR.swift +++ b/Examples/TDOR/TDOR.swift @@ -8,7 +8,6 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct TDOR: ScintillaApp { var world = World { diff --git a/Examples/Vase/Vase.swift b/Examples/Vase/Vase.swift index 830ced8..8d95fba 100644 --- a/Examples/Vase/Vase.swift +++ b/Examples/Vase/Vase.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct Vase: ScintillaApp { var world = World { diff --git a/Examples/Wine/Wine.swift b/Examples/Wine/Wine.swift index 336c9d4..cdfc3c9 100644 --- a/Examples/Wine/Wine.swift +++ b/Examples/Wine/Wine.swift @@ -7,7 +7,6 @@ import ScintillaLib -@available(macOS 12.0, *) @main struct Wine: ScintillaApp { var world = World { From c0935d2ca2394fe6e59b0300db6814b3b403d372 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Dec 2023 14:55:17 -0800 Subject: [PATCH 02/14] Whoops... forgot to update README again. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 825ea37..549b4b5 100644 --- a/README.md +++ b/README.md @@ -603,7 +603,6 @@ There are times when you do not necessarily want to combine shapes to make new s ```swift import ScintillaLib -@available(macOS 12.0, *) @main struct QuickStart: ScintillaApp { var world = World { @@ -633,7 +632,6 @@ Notice that we have to apply the same rotation twice. We can do better than this ```swift import ScintillaLib -@available(macOS 12.0, *) @main struct QuickStart: ScintillaApp { var world = World { @@ -662,7 +660,6 @@ It's not a huge gain in this example but if you are constructing scenes with man ```swift import ScintillaLib -@available(macOS 12.0, *) @main struct QuickStart: ScintillaApp { var world = World { @@ -772,7 +769,6 @@ You can also have multiple lights, which you can use to create scenes with multi import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct Cavatappi: ScintillaApp { var world = World { @@ -810,7 +806,6 @@ Lights can also be configured to fade over the distance travelled to objects in ```swift import ScintillaLib -@available(macOS 12.0, *) @main struct DimlyLitScene: ScintillaApp { var world = World { @@ -932,7 +927,6 @@ You can also optionally render a scene with antialiasing. In the image above, yo import Darwin import ScintillaLib -@available(macOS 12.0, *) @main struct Cavatappi: ScintillaApp { var world = World { From e4ff6ce187395bef685c0c02d3fedd193f1babd7 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Dec 2023 15:42:17 -0800 Subject: [PATCH 03/14] Moved camera out of world and into a separate property of ScintillaApp. Also removed all async calls in World. --- Examples/Die/Die.swift | 13 +-- Examples/QuickStart/QuickStart.swift | 13 +-- Sources/ScintillaLib/Camera.swift | 80 ++++++++++++++ Sources/ScintillaLib/Intersection.swift | 4 +- Sources/ScintillaLib/ScintillaApp.swift | 5 +- Sources/ScintillaLib/ScintillaView.swift | 6 +- Sources/ScintillaLib/Shape.swift | 22 ++-- Sources/ScintillaLib/World.swift | 128 +++-------------------- Sources/ScintillaLib/WorldBuilder.swift | 10 +- 9 files changed, 130 insertions(+), 151 deletions(-) diff --git a/Examples/Die/Die.swift b/Examples/Die/Die.swift index c9b2a1d..be97b63 100644 --- a/Examples/Die/Die.swift +++ b/Examples/Die/Die.swift @@ -9,17 +9,18 @@ import ScintillaLib @main struct Die: ScintillaApp { + var camera = Camera(width: 800, + height: 600, + viewAngle: PI/3, + from: Point(0, 5, -10), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world: World { let orange: Material = .solidColor(1, 0.5, 0) .reflective(0.2) return World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 5, -10), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Cube() .material(orange) diff --git a/Examples/QuickStart/QuickStart.swift b/Examples/QuickStart/QuickStart.swift index cca6db0..d27328b 100644 --- a/Examples/QuickStart/QuickStart.swift +++ b/Examples/QuickStart/QuickStart.swift @@ -9,13 +9,14 @@ import ScintillaLib @main struct QuickStart: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 2, -2), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 2, -2), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(.solidColor(1, 0, 0)) diff --git a/Sources/ScintillaLib/Camera.swift b/Sources/ScintillaLib/Camera.swift index 6b94b5d..6baf96d 100644 --- a/Sources/ScintillaLib/Camera.swift +++ b/Sources/ScintillaLib/Camera.swift @@ -17,6 +17,7 @@ public struct Camera { var halfHeight: Double @_spi(Testing) public var pixelSize: Double var antialiasing: Bool + var totalPixels: Int public init(width horizontalSize: Int, height verticalSize: Int, @@ -61,5 +62,84 @@ public struct Camera { self.halfHeight = halfHeight self.pixelSize = pixelSize self.antialiasing = antialiasing + + self.totalPixels = horizontalSize * verticalSize + } + + @_spi(Testing) public func rayForPixel(_ pixelX: Int, _ pixelY: Int, _ dx: Double = 0.5, _ dy: Double = 0.5) -> Ray { + // The offset from the edge of the canvas to the pixel's center + let offsetX = (Double(pixelX) + dx) * self.pixelSize + let offsetY = (Double(pixelY) + dy) * self.pixelSize + + // The untransformed coordinates of the pixel in world space. + // (Remember that the camera looks toward -z, so +x is to the *left*.) + let worldX = self.halfWidth - offsetX + let worldY = self.halfHeight - offsetY + + // Using the camera matrix, transform the canvas point and the origin, + // and then compute the ray's direction vector. + // (Remember that the canvas is at z=-1) + let pixel = self.inverseViewTransform.multiply(Point(worldX, worldY, -1)) + let origin = self.inverseViewTransform.multiply(Point(0, 0, 0)) + let direction = pixel.subtract(origin).normalize() + + return Ray(origin, direction) + } + + private func sendProgress(newPercentRendered: Double, + newElapsedTime: Range, + to updateClosure: @MainActor @escaping (Double, Range) -> Void) { + Task { + await updateClosure(newPercentRendered, newElapsedTime) + } + } + + public func render(world: World, + updateClosure: @MainActor @escaping (Double, Range) -> Void) async -> Canvas { + var renderedPixels = 0 + var percentRendered = 0.0 + let startingTime = Date() + sendProgress(newPercentRendered: percentRendered, + newElapsedTime: startingTime.. Computations { + @_spi(Testing) public func prepareComputations(_ world: World, _ ray: Ray, _ allIntersections: [Intersection]) -> Computations { let point = ray.position(self.t) let eye = ray.direction.negate() - var normal = await self.shape.normal(world, point, self.uv) + var normal = self.shape.normal(world, point, self.uv) let isInside: Bool if normal.dot(eye) < 0 { isInside = true diff --git a/Sources/ScintillaLib/ScintillaApp.swift b/Sources/ScintillaLib/ScintillaApp.swift index ba00cfd..86821f9 100644 --- a/Sources/ScintillaLib/ScintillaApp.swift +++ b/Sources/ScintillaLib/ScintillaApp.swift @@ -9,12 +9,15 @@ import SwiftUI @MainActor public protocol ScintillaApp: App { @WorldBuilder var world: World { get } + var camera: Camera { get set } } public extension ScintillaApp { var body: some Scene { WindowGroup { - ScintillaView(world: world, fileName: String(describing: Self.self) + ".png") + ScintillaView(camera: camera, + world: world, + fileName: String(describing: Self.self) + ".png") .onDisappear { exit(0) } diff --git a/Sources/ScintillaLib/ScintillaView.swift b/Sources/ScintillaLib/ScintillaView.swift index 151ec25..4f09d82 100644 --- a/Sources/ScintillaLib/ScintillaView.swift +++ b/Sources/ScintillaLib/ScintillaView.swift @@ -13,9 +13,11 @@ import SwiftUI @State var elapsedTime: Range = Date().. Vector { - let localPoint = await self.worldToObject(world, worldPoint) + @_spi(Testing) public func normal(_ world: World, _ worldPoint: Point, _ uv: UV = .none) -> Vector { + let localPoint = self.worldToObject(world, worldPoint) let localNormal = self.localNormal(localPoint, uv) - return await self.objectToWorld(world, localNormal) + return self.objectToWorld(world, localNormal) } } // CSG and group extensions extension Shape { - @_spi(Testing) public func worldToObject(_ world: World, _ worldPoint: Point) async -> Point { + @_spi(Testing) public func worldToObject(_ world: World, _ worldPoint: Point) -> Point { var objectPoint = worldPoint if let parentId = self.parentId { - guard let parentShape = await world.findShape(parentId) else { + guard let parentShape = world.findShape(parentId) else { fatalError("Whoops... unable to find parent shape!") } switch parentShape { case let group as Group: - objectPoint = await group.worldToObject(world, worldPoint) + objectPoint = group.worldToObject(world, worldPoint) case let csg as CSG: - objectPoint = await csg.worldToObject(world, worldPoint) + objectPoint = csg.worldToObject(world, worldPoint) default: fatalError("Whoops... parent object is somehow neither a Group nor CSG") } @@ -167,21 +167,21 @@ extension Shape { return self.inverseTransform.multiply(objectPoint) } - @_spi(Testing) public func objectToWorld(_ world: World, _ objectNormal: Vector) async -> Vector { + @_spi(Testing) public func objectToWorld(_ world: World, _ objectNormal: Vector) -> Vector { var worldNormal = self.inverseTransposeTransform.multiply(objectNormal) worldNormal[3] = 0 worldNormal = worldNormal.normalize() if let parentId = self.parentId { - guard let parentShape = await world.findShape(parentId) else { + guard let parentShape = world.findShape(parentId) else { fatalError("Whoops... unable ot find parent shape!") } switch parentShape { case let group as Group: - worldNormal = await group.objectToWorld(world, worldNormal) + worldNormal = group.objectToWorld(world, worldNormal) case let csg as CSG: - worldNormal = await csg.objectToWorld(world, worldNormal) + worldNormal = csg.objectToWorld(world, worldNormal) default: fatalError("Whoops... parent object is somehow neither a Group nor CSG") } diff --git a/Sources/ScintillaLib/World.swift b/Sources/ScintillaLib/World.swift index 93b83c4..a203260 100644 --- a/Sources/ScintillaLib/World.swift +++ b/Sources/ScintillaLib/World.swift @@ -9,35 +9,13 @@ import Foundation @_spi(Testing) public let MAX_RECURSIVE_CALLS = 5 -public actor World { - @_spi(Testing) public var camera: Camera +public struct World { @_spi(Testing) public var lights: [Light] @_spi(Testing) public var shapes: [Shape] - var totalPixels: Int - - public init(@WorldBuilder builder: () -> (Camera, [WorldObject])) { - let (camera, objects) = builder() - - var lights: [Light] = [] - var shapes: [Shape] = [] - for object in objects { - switch object { - case .light(let light): - lights.append(light) - case .shape(let shape): - shapes.append(shape) - } - } - - self.camera = camera - self.lights = lights - self.shapes = shapes - self.totalPixels = camera.horizontalSize * camera.verticalSize - } - - public init(_ camera: Camera, @WorldObjectBuilder builder: () -> [WorldObject]) { + public init(@WorldBuilder builder: () -> [WorldObject]) { let objects = builder() + var lights: [Light] = [] var shapes: [Shape] = [] for object in objects { @@ -51,15 +29,11 @@ public actor World { self.lights = lights self.shapes = shapes - self.camera = camera - self.totalPixels = camera.horizontalSize * camera.verticalSize } - public init(_ camera: Camera, _ lights: [Light], _ shapes: [Shape]) { - self.camera = camera + public init(_ lights: [Light], _ shapes: [Shape]) { self.lights = lights self.shapes = shapes - self.totalPixels = camera.horizontalSize * camera.verticalSize } public func findShape(_ shapeId: UUID) -> Shape? { @@ -121,7 +95,7 @@ public actor World { } } - @_spi(Testing) public func shadeHit(_ computations: Computations, _ remainingCalls: Int) async -> Color { + @_spi(Testing) public func shadeHit(_ computations: Computations, _ remainingCalls: Int) -> Color { let material = computations.object.material var surfaceColor = Color(0, 0, 0) @@ -144,8 +118,8 @@ public actor World { surfaceColor = surfaceColor.blend(tempColor) } - let reflectedColor = await self.reflectedColorAt(computations, remainingCalls) - let refractedColor = await self.refractedColorAt(computations, remainingCalls) + let reflectedColor = self.reflectedColorAt(computations, remainingCalls) + let refractedColor = self.refractedColorAt(computations, remainingCalls) if material.properties.reflective > 0 && material.properties.transparency > 0 { let reflectance = self.schlickReflectance(computations) @@ -157,18 +131,18 @@ public actor World { } } - @_spi(Testing) public func reflectedColorAt(_ computations: Computations, _ remainingCalls: Int) async -> Color { + @_spi(Testing) public func reflectedColorAt(_ computations: Computations, _ remainingCalls: Int) -> Color { if remainingCalls == 0 { return .black } else if computations.object.material.properties.reflective == 0 { return .black } else { let reflected = Ray(computations.overPoint, computations.reflected) - return await self.colorAt(reflected, remainingCalls-1).multiplyScalar(computations.object.material.properties.reflective) + return self.colorAt(reflected, remainingCalls-1).multiplyScalar(computations.object.material.properties.reflective) } } - @_spi(Testing) public func refractedColorAt(_ computations: Computations, _ remainingCalls: Int) async -> Color { + @_spi(Testing) public func refractedColorAt(_ computations: Computations, _ remainingCalls: Int) -> Color { if remainingCalls == 0 { return .black } else if computations.object.material.properties.transparency == 0 { @@ -200,21 +174,21 @@ public actor World { // Find the color of the refracted ray, making sure to multiply // by the transparency value to account for any opacity - return await self.colorAt(refracted, remainingCalls - 1) + return self.colorAt(refracted, remainingCalls - 1) .multiplyScalar(computations.object.material.properties.transparency) } } } - @_spi(Testing) public func colorAt(_ ray: Ray, _ remainingCalls: Int) async -> Color { + @_spi(Testing) public func colorAt(_ ray: Ray, _ remainingCalls: Int) -> Color { let allIntersections = self.intersect(ray) let hit = hit(allIntersections) switch hit { case .none: return .black case .some(let intersection): - let computations = await intersection.prepareComputations(self, ray, allIntersections) - return await self.shadeHit(computations, remainingCalls) + let computations = intersection.prepareComputations(self, ray, allIntersections) + return self.shadeHit(computations, remainingCalls) } } @@ -251,78 +225,4 @@ public actor World { fatalError("Whoops! Encountered unsupported light implementation!") } } - - @_spi(Testing) public func rayForPixel(_ pixelX: Int, _ pixelY: Int, _ dx: Double = 0.5, _ dy: Double = 0.5) -> Ray { - // The offset from the edge of the canvas to the pixel's center - let offsetX = (Double(pixelX) + dx) * self.camera.pixelSize - let offsetY = (Double(pixelY) + dy) * self.camera.pixelSize - - // The untransformed coordinates of the pixel in world space. - // (Remember that the camera looks toward -z, so +x is to the *left*.) - let worldX = self.camera.halfWidth - offsetX - let worldY = self.camera.halfHeight - offsetY - - // Using the camera matrix, transform the canvas point and the origin, - // and then compute the ray's direction vector. - // (Remember that the canvas is at z=-1) - let pixel = self.camera.inverseViewTransform.multiply(Point(worldX, worldY, -1)) - let origin = self.camera.inverseViewTransform.multiply(Point(0, 0, 0)) - let direction = pixel.subtract(origin).normalize() - - return Ray(origin, direction) - } - - private func sendProgress(newPercentRendered: Double, - newElapsedTime: Range, - to updateClosure: @MainActor @escaping (Double, Range) -> Void) { - Task { await updateClosure(newPercentRendered, newElapsedTime) } - } - - public func render(updateClosure: @MainActor @escaping (Double, Range) -> Void) async -> Canvas { - var renderedPixels = 0 - var percentRendered = 0.0 - let startingTime = Date() - sendProgress(newPercentRendered: percentRendered, - newElapsedTime: startingTime.. (Camera, [WorldObject]) { + public static func buildFinalResult(_ world: [WorldObject]) -> [WorldObject] { return world } - public static func buildBlock(_ camera: Camera, _ objects: [WorldObject]...) -> (Camera, [WorldObject]) { - return (camera, Array(objects.joined())) - } - - public static func buildExpression(_ camera: Camera) -> Camera { - return camera - } - public static func buildExpression(_ light: Light) -> [WorldObject] { return [.light(light)] } From 72b6d63a7215939bcb0a8496a9c952640b4776ae Mon Sep 17 00:00:00 2001 From: quephird Date: Mon, 4 Dec 2023 22:21:04 -0800 Subject: [PATCH 04/14] Temporarily disabled running of tests; constructors in World are now chained. --- Examples/HollowedSphere/HollowedSphere.swift | 13 +++++++------ Package.swift | 6 +++--- Sources/ScintillaLib/World.swift | 3 +-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Examples/HollowedSphere/HollowedSphere.swift b/Examples/HollowedSphere/HollowedSphere.swift index 67c8a2f..8c88340 100644 --- a/Examples/HollowedSphere/HollowedSphere.swift +++ b/Examples/HollowedSphere/HollowedSphere.swift @@ -9,13 +9,14 @@ import ScintillaLib @main struct HollowedSphere: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 1.5, -2), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 1.5, -2), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(.solidColor(0, 0, 1)) diff --git a/Package.swift b/Package.swift index ee414d9..ce59650 100644 --- a/Package.swift +++ b/Package.swift @@ -23,9 +23,9 @@ let package = Package( dependencies: []), // Tests - .testTarget( - name: "ScintillaLibTests", - dependencies: ["ScintillaLib"]), +// .testTarget( +// name: "ScintillaLibTests", +// dependencies: ["ScintillaLib"]), // Examples .executableTarget( diff --git a/Sources/ScintillaLib/World.swift b/Sources/ScintillaLib/World.swift index a203260..284d9db 100644 --- a/Sources/ScintillaLib/World.swift +++ b/Sources/ScintillaLib/World.swift @@ -27,8 +27,7 @@ public struct World { } } - self.lights = lights - self.shapes = shapes + self.init(lights, shapes) } public init(_ lights: [Light], _ shapes: [Shape]) { From 02d6a3f5caaea2e04fc367f6c25003b3a1db60a7 Mon Sep 17 00:00:00 2001 From: quephird Date: Tue, 5 Dec 2023 21:08:38 -0800 Subject: [PATCH 05/14] OMFG I think I solved the performance issue. --- Examples/Wine/Wine.swift | 137 ++++++++++++++++--------------- Sources/ScintillaLib/CSG.swift | 20 ++--- Sources/ScintillaLib/Group.swift | 20 ++--- Sources/ScintillaLib/World.swift | 27 +++--- 4 files changed, 105 insertions(+), 99 deletions(-) diff --git a/Examples/Wine/Wine.swift b/Examples/Wine/Wine.swift index cdfc3c9..c6a0ab0 100644 --- a/Examples/Wine/Wine.swift +++ b/Examples/Wine/Wine.swift @@ -9,7 +9,14 @@ import ScintillaLib @main struct Wine: ScintillaApp { - var world = World { + var camera = Camera(width: 600, + height: 600, + viewAngle: PI/3, + from: Point(0, 3, -10), + to: Point(0, 3, 0), + up: Vector(0, 1, 0)) + + var world: World { let bottleGreen: Material = .solidColor(0.0, 0.2, 0.0) .transparency(1.0) .shininess(1.0) @@ -25,76 +32,72 @@ struct Wine: ScintillaApp { .reflective(1.0) .refractive(1.5) - Camera(width: 600, - height: 600, - viewAngle: PI/3, - from: Point(0, 3, -10), - to: Point(0, 3, 0), - up: Vector(0, 1, 0)) - PointLight(position: Point(-10, 10, -10)) - Plane() - .material(.solidColor(1.0, 1.0, 1.0) - .shininess(1.0)) - Group { + return World { + PointLight(position: Point(-10, 10, -10)) + Plane() + .material(.solidColor(1.0, 1.0, 1.0) + .shininess(1.0)) Group { - Cylinder(bottomY: 0.0, // Bottle body - topY: 5.0, - isCapped: true) - .material(bottleGreen) - Sphere() // Transition to neck - .material(bottleGreen) - .scale(1.0, 0.6, 1.0) - .translate(0.0, 5.0, 0.0) - Cylinder(bottomY: 5.6, // Neck - topY: 7.0, + Group { + Cylinder(bottomY: 0.0, // Bottle body + topY: 5.0, + isCapped: true) + .material(bottleGreen) + Sphere() // Transition to neck + .material(bottleGreen) + .scale(1.0, 0.6, 1.0) + .translate(0.0, 5.0, 0.0) + Cylinder(bottomY: 5.6, // Neck + topY: 7.0, + isCapped: true) + .material(bottleGreen) + .scale(0.4, 1.0, 0.4) + } + .difference { // Hollow out bottle + Cylinder(bottomY: 0.05, + topY: 4.95, + isCapped: true) + .material(bottleGreen) + Sphere() + .material(bottleGreen) + .scale(0.95, 0.55, 0.95) + .translate(0.0, 5.0, 0.0) + Cylinder(bottomY: 5.6, + topY: 7.0, + isCapped: true) + .material(bottleGreen) + .scale(0.35, 1.0, 0.35) + } + Cylinder(bottomY: 0.04, // Wine in bottle + topY: 2.0, isCapped: true) - .material(bottleGreen) - .scale(0.4, 1.0, 0.4) + .material(wineRed) + .scale(0.94, 0.94, 0.94) } - .difference { // Hollow out bottle - Cylinder(bottomY: 0.05, - topY: 4.95, - isCapped: true) - .material(bottleGreen) - Sphere() - .material(bottleGreen) - .scale(0.95, 0.55, 0.95) - .translate(0.0, 5.0, 0.0) - Cylinder(bottomY: 5.6, - topY: 7.0, - isCapped: true) - .material(bottleGreen) - .scale(0.35, 1.0, 0.35) + .translate(-2, 0, 0) + Group { + Sphere() // Wine glass base + .material(wineGlass) + .scale(1.0, 0.2, 1.0) + Cylinder(bottomY: 0.05, topY: 1.5, isCapped: true) // Stem + .material(wineGlass) + .scale(0.15, 1.0, 0.15) + SurfaceOfRevolution(yzPoints: [(1.5, 0.2), // Body + (2.0, 1.0), + (3.5, 0.9)]) + .material(wineGlass) + SurfaceOfRevolution(yzPoints: [(1.55, 0.15), // Wine in glass + (2.0, 0.95), + (2.5, 0.85)]) + .material(wineRed) } - Cylinder(bottomY: 0.04, // Wine in bottle - topY: 2.0, - isCapped: true) - .material(wineRed) - .scale(0.94, 0.94, 0.94) - } - .translate(-2, 0, 0) - Group { - Sphere() // Wine glass base - .material(wineGlass) - .scale(1.0, 0.2, 1.0) - Cylinder(bottomY: 0.05, topY: 1.5, isCapped: true) // Stem - .material(wineGlass) - .scale(0.15, 1.0, 0.15) - SurfaceOfRevolution(yzPoints: [(1.5, 0.2), // Body - (2.0, 1.0), - (3.5, 0.9)]) - .material(wineGlass) - SurfaceOfRevolution(yzPoints: [(1.55, 0.15), // Wine in glass - (2.0, 0.95), - (2.5, 0.85)]) - .material(wineRed) + .translate(1.0, 0.0, 0.0) + Cylinder(bottomY: 0.0, topY: 0.8, isCapped: true) // Cork + .material(.solidColor(0.8, 0.7, 0.6)) + .scale(0.25, 1.0, 0.25) + .rotateX(PI/2) + .rotateY(PI/3) + .translate(-0.5, 0.25, -2.5) } - .translate(1.0, 0.0, 0.0) - Cylinder(bottomY: 0.0, topY: 0.8, isCapped: true) // Cork - .material(.solidColor(0.8, 0.7, 0.6)) - .scale(0.25, 1.0, 0.25) - .rotateX(PI/2) - .rotateY(PI/3) - .translate(-0.5, 0.25, -2.5) } } diff --git a/Sources/ScintillaLib/CSG.swift b/Sources/ScintillaLib/CSG.swift index 7077b1f..ceb8c00 100644 --- a/Sources/ScintillaLib/CSG.swift +++ b/Sources/ScintillaLib/CSG.swift @@ -23,27 +23,27 @@ public struct CSG: Shape { self.right.parentId = self.id } - public func findShape(_ shapeId: UUID) -> Shape? { + public func getAllChildren() -> [Shape] { + var allChildren: [Shape] = [] + for shape in [self.left, self.right] { - if shape.id == shapeId { - return shape - } + allChildren.append(shape) switch shape { case let csg as CSG: - if let shape = csg.findShape(shapeId) { - return shape + for childShape in csg.getAllChildren() { + allChildren.append(childShape) } case let group as Group: - if let shape = group.findShape(shapeId) { - return shape + for childShape in group.getAllChildren() { + allChildren.append(childShape) } default: - continue + break } } - return nil + return allChildren } static func makeCSG(_ operation: Operation, _ baseShape: Shape, @ShapeBuilder _ otherShapesBuilder: () -> [Shape]) -> Shape { diff --git a/Sources/ScintillaLib/Group.swift b/Sources/ScintillaLib/Group.swift index c3a1ec7..37edb47 100644 --- a/Sources/ScintillaLib/Group.swift +++ b/Sources/ScintillaLib/Group.swift @@ -19,27 +19,27 @@ public struct Group: Shape { } } - public func findShape(_ shapeId: UUID) -> Shape? { + public func getAllChildren() -> [Shape] { + var allChildren: [Shape] = [] + for shape in self.children { - if shape.id == shapeId { - return shape - } + allChildren.append(shape) switch shape { case let csg as CSG: - if let shape = csg.findShape(shapeId) { - return shape + for childShape in csg.getAllChildren() { + allChildren.append(childShape) } case let group as Group: - if let shape = group.findShape(shapeId) { - return shape + for childShape in group.getAllChildren() { + allChildren.append(childShape) } default: - continue + break } } - return nil + return allChildren } @_spi(Testing) public func localIntersect(_ localRay: Ray) -> [Intersection] { diff --git a/Sources/ScintillaLib/World.swift b/Sources/ScintillaLib/World.swift index 284d9db..2f59159 100644 --- a/Sources/ScintillaLib/World.swift +++ b/Sources/ScintillaLib/World.swift @@ -13,6 +13,8 @@ public struct World { @_spi(Testing) public var lights: [Light] @_spi(Testing) public var shapes: [Shape] + private var shapeCache: [UUID: Shape] + public init(@WorldBuilder builder: () -> [WorldObject]) { let objects = builder() @@ -33,29 +35,30 @@ public struct World { public init(_ lights: [Light], _ shapes: [Shape]) { self.lights = lights self.shapes = shapes - } - public func findShape(_ shapeId: UUID) -> Shape? { - for shape in self.shapes { - if shape.id == shapeId { - return shape - } + var newCache: [UUID: Shape] = [:] + for shape in shapes { + newCache[shape.id] = shape switch shape { case let csg as CSG: - if let shape = csg.findShape(shapeId) { - return shape + for childShape in csg.getAllChildren() { + newCache[childShape.id] = childShape } case let group as Group: - if let shape = group.findShape(shapeId) { - return shape + for childShape in group.getAllChildren() { + newCache[childShape.id] = childShape } default: - continue + break } } - return nil + self.shapeCache = newCache + } + + public func findShape(_ shapeId: UUID) -> Shape? { + return self.shapeCache[shapeId] } @_spi(Testing) public func intersect(_ ray: Ray) -> [Intersection] { From 1c224858d370c82a2f6a85069b87b64cc5321069 Mon Sep 17 00:00:00 2001 From: quephird Date: Tue, 5 Dec 2023 22:21:13 -0800 Subject: [PATCH 06/14] Updated TDOR sketch --- Examples/TDOR/TDOR.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Examples/TDOR/TDOR.swift b/Examples/TDOR/TDOR.swift index adce97f..1cdeda6 100644 --- a/Examples/TDOR/TDOR.swift +++ b/Examples/TDOR/TDOR.swift @@ -10,13 +10,14 @@ import ScintillaLib @main struct TDOR: ScintillaApp { + var camera = Camera(width: 600, + height: 600, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 600, - height: 600, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ImplicitSurface(bottomFrontLeft: (-0.5, 0.0, -0.5), topBackRight: (0.5, 1.0, 0.5), { x, y, z in From 554c4d4195d207cbb497b7e25d1f073b5eb94116 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 10 Dec 2023 14:38:07 -0800 Subject: [PATCH 07/14] Moved `includes()` into Shape protocol and made Group and CSG have specilalizatons of that method to avoid using a switch and casting. Also now passing IDs instead of entire shapes, all to avoid copying and adding performance overhead. --- Sources/ScintillaLib/CSG.swift | 4 ++++ Sources/ScintillaLib/Group.swift | 4 ++++ Sources/ScintillaLib/Shape.swift | 17 ++++++++--------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Sources/ScintillaLib/CSG.swift b/Sources/ScintillaLib/CSG.swift index ceb8c00..36bed90 100644 --- a/Sources/ScintillaLib/CSG.swift +++ b/Sources/ScintillaLib/CSG.swift @@ -107,6 +107,10 @@ public struct CSG: Shape { return self.filterIntersections(allIntersections) } + public func includes(_ otherID: UUID) -> Bool { + return id == otherID || left.includes(otherID) || right.includes(otherID) + } + // The concept of a normal vector to a CSG object is meaningless and should never be called @_spi(Testing) public func localNormal(_ localPoint: Point, _ uv: UV = .none) -> Vector { fatalError("Whoops... this should never be called for a Group shape") diff --git a/Sources/ScintillaLib/Group.swift b/Sources/ScintillaLib/Group.swift index 37edb47..3a852c9 100644 --- a/Sources/ScintillaLib/Group.swift +++ b/Sources/ScintillaLib/Group.swift @@ -56,6 +56,10 @@ public struct Group: Shape { return allIntersections } + public func includes(_ otherID: UUID) -> Bool { + return id == otherID || children.contains(where: { shape in shape.includes(otherID) }) + } + // The concept of a normal vector to a Group is meaningless and should never be called @_spi(Testing) public func localNormal(_ localPoint: Point, _ uv: UV = .none) -> Vector { fatalError("Whoops... this should never be called for a Group shape") diff --git a/Sources/ScintillaLib/Shape.swift b/Sources/ScintillaLib/Shape.swift index c676b67..78243ea 100644 --- a/Sources/ScintillaLib/Shape.swift +++ b/Sources/ScintillaLib/Shape.swift @@ -12,6 +12,8 @@ public protocol Shape { func localIntersect(_ localRay: Ray) -> [Intersection] func localNormal(_ localPoint: Point, _ uv: UV) -> Vector + + func includes(_ otherID: UUID) -> Bool } extension Shape { @@ -190,14 +192,11 @@ extension Shape { return worldNormal } - func includes(_ other: Shape) -> Bool { - switch self { - case let group as Group: - return group.children.contains(where: {shape in shape.includes(other)}) - case let csg as CSG: - return csg.left.includes(other) || csg.right.includes(other) - default: - return self.id == other.id - } + public func includes(_ otherID: UUID) -> Bool { + return self.id == otherID + } + + internal func includes(_ other: some Shape) -> Bool { + return includes(other.id) } } From 277f52fe96083d5cb6e15ea4688853f5db193cb6 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 10 Dec 2023 15:37:21 -0800 Subject: [PATCH 08/14] Made code in CSG.filterInterections() a wee bit clearer. --- Sources/ScintillaLib/CSG.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ScintillaLib/CSG.swift b/Sources/ScintillaLib/CSG.swift index 36bed90..47d7437 100644 --- a/Sources/ScintillaLib/CSG.swift +++ b/Sources/ScintillaLib/CSG.swift @@ -67,7 +67,6 @@ public struct CSG: Shape { @_spi(Testing) public func filterIntersections(_ allIntersections: [Intersection]) -> [Intersection] { // Begin outside of both children - var leftHit = false var insideLeft = false var insideRight = false @@ -77,7 +76,7 @@ public struct CSG: Shape { for intersection in allIntersections { // If the intersection's object is part of the "left" child, // then leftHit is true - leftHit = self.left.includes(intersection.shape) + let leftHit = self.left.includes(intersection.shape) if self.isIntersectionAllowed(leftHit, insideLeft, insideRight) { filteredIntersections.append(intersection) From 582883b2374227a78f34d1ff805c2c3711c5dc8f Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 10 Dec 2023 19:24:32 -0800 Subject: [PATCH 09/14] Cache in World is now only for maintaining the relationship between child shape IDs to their parents. Consequently, parentId is no longer a nullable property of Shape. --- Sources/ScintillaLib/CSG.swift | 27 +++--------- Sources/ScintillaLib/Group.swift | 31 +++---------- Sources/ScintillaLib/Shape.swift | 44 +++++-------------- .../ScintillaLib/SharedShapeProperties.swift | 8 ++-- Sources/ScintillaLib/World.swift | 24 +++------- 5 files changed, 33 insertions(+), 101 deletions(-) diff --git a/Sources/ScintillaLib/CSG.swift b/Sources/ScintillaLib/CSG.swift index 47d7437..5198b29 100644 --- a/Sources/ScintillaLib/CSG.swift +++ b/Sources/ScintillaLib/CSG.swift @@ -18,32 +18,15 @@ public struct CSG: Shape { self.operation = operation self.left = left self.right = right - - self.left.parentId = self.id - self.right.parentId = self.id } - public func getAllChildren() -> [Shape] { - var allChildren: [Shape] = [] - - for shape in [self.left, self.right] { - allChildren.append(shape) - - switch shape { - case let csg as CSG: - for childShape in csg.getAllChildren() { - allChildren.append(childShape) - } - case let group as Group: - for childShape in group.getAllChildren() { - allChildren.append(childShape) - } - default: - break - } + public func populateParentCache(_ cache: inout [UUID : Shape], parent: Shape?) { + if let parent { + cache[self.id] = parent } - return allChildren + left.populateParentCache(&cache, parent: self) + right.populateParentCache(&cache, parent: self) } static func makeCSG(_ operation: Operation, _ baseShape: Shape, @ShapeBuilder _ otherShapesBuilder: () -> [Shape]) -> Shape { diff --git a/Sources/ScintillaLib/Group.swift b/Sources/ScintillaLib/Group.swift index 3a852c9..6be4b65 100644 --- a/Sources/ScintillaLib/Group.swift +++ b/Sources/ScintillaLib/Group.swift @@ -12,34 +12,17 @@ public struct Group: Shape { var children: [Shape] = [] public init(@ShapeBuilder builder: () -> [Shape]) { - let children = builder() - for var child in children { - child.parentId = self.id - self.children.append(child) - } + self.children = builder() } - public func getAllChildren() -> [Shape] { - var allChildren: [Shape] = [] - - for shape in self.children { - allChildren.append(shape) - - switch shape { - case let csg as CSG: - for childShape in csg.getAllChildren() { - allChildren.append(childShape) - } - case let group as Group: - for childShape in group.getAllChildren() { - allChildren.append(childShape) - } - default: - break - } + public func populateParentCache(_ cache: inout [UUID : Shape], parent: Shape?) { + if let parent { + cache[self.id] = parent } - return allChildren + for child in children { + child.populateParentCache(&cache, parent: self) + } } @_spi(Testing) public func localIntersect(_ localRay: Ray) -> [Intersection] { diff --git a/Sources/ScintillaLib/Shape.swift b/Sources/ScintillaLib/Shape.swift index 78243ea..87555ba 100644 --- a/Sources/ScintillaLib/Shape.swift +++ b/Sources/ScintillaLib/Shape.swift @@ -14,6 +14,8 @@ public protocol Shape { func localNormal(_ localPoint: Point, _ uv: UV) -> Vector func includes(_ otherID: UUID) -> Bool + + func populateParentCache(_ cache: inout [UUID: Shape], parent: Shape?) } extension Shape { @@ -45,12 +47,6 @@ extension Shape { get { sharedProperties.inverseTransposeTransform } } - @inlinable - public var parentId: UUID? { - get { sharedProperties.parentID } - set { sharedProperties.parentID = newValue } - } - @inlinable public var castsShadow: Bool { get { sharedProperties.castsShadow } @@ -148,22 +144,17 @@ extension Shape { // CSG and group extensions extension Shape { + public func populateParentCache(_ cache: inout [UUID : Shape], parent: Shape?) { + if let parent { + cache[self.id] = parent + } + } + @_spi(Testing) public func worldToObject(_ world: World, _ worldPoint: Point) -> Point { var objectPoint = worldPoint - if let parentId = self.parentId { - guard let parentShape = world.findShape(parentId) else { - fatalError("Whoops... unable to find parent shape!") - } - - switch parentShape { - case let group as Group: - objectPoint = group.worldToObject(world, worldPoint) - case let csg as CSG: - objectPoint = csg.worldToObject(world, worldPoint) - default: - fatalError("Whoops... parent object is somehow neither a Group nor CSG") - } + if let parentShape = world.parent(of: self.id) { + objectPoint = parentShape.worldToObject(world, worldPoint) } return self.inverseTransform.multiply(objectPoint) @@ -174,19 +165,8 @@ extension Shape { worldNormal[3] = 0 worldNormal = worldNormal.normalize() - if let parentId = self.parentId { - guard let parentShape = world.findShape(parentId) else { - fatalError("Whoops... unable ot find parent shape!") - } - - switch parentShape { - case let group as Group: - worldNormal = group.objectToWorld(world, worldNormal) - case let csg as CSG: - worldNormal = csg.objectToWorld(world, worldNormal) - default: - fatalError("Whoops... parent object is somehow neither a Group nor CSG") - } + if let parentShape = world.parent(of: self.id) { + worldNormal = parentShape.objectToWorld(world, worldNormal) } return worldNormal diff --git a/Sources/ScintillaLib/SharedShapeProperties.swift b/Sources/ScintillaLib/SharedShapeProperties.swift index 150578a..edd4b10 100644 --- a/Sources/ScintillaLib/SharedShapeProperties.swift +++ b/Sources/ScintillaLib/SharedShapeProperties.swift @@ -13,9 +13,6 @@ public struct SharedShapeProperties { self.inverseTransposeTransform = transform.inverse().transpose() } - @_spi(Testing) public var id: UUID = UUID() - public var material: Material = .basicMaterial() - public var transform: Matrix4 = .identity { didSet { self.inverseTransform = transform.inverse() @@ -25,6 +22,9 @@ public struct SharedShapeProperties { public private(set) var inverseTransform: Matrix4 public private(set) var inverseTransposeTransform: Matrix4 - public var parentID: UUID? + + @_spi(Testing) public var id: UUID = UUID() + public var material: Material = .basicMaterial() + public var castsShadow: Bool = true } diff --git a/Sources/ScintillaLib/World.swift b/Sources/ScintillaLib/World.swift index 2f59159..37fba6c 100644 --- a/Sources/ScintillaLib/World.swift +++ b/Sources/ScintillaLib/World.swift @@ -13,7 +13,7 @@ public struct World { @_spi(Testing) public var lights: [Light] @_spi(Testing) public var shapes: [Shape] - private var shapeCache: [UUID: Shape] + private var parentCache: [UUID: Shape] public init(@WorldBuilder builder: () -> [WorldObject]) { let objects = builder() @@ -38,27 +38,13 @@ public struct World { var newCache: [UUID: Shape] = [:] for shape in shapes { - newCache[shape.id] = shape - - switch shape { - case let csg as CSG: - for childShape in csg.getAllChildren() { - newCache[childShape.id] = childShape - } - case let group as Group: - for childShape in group.getAllChildren() { - newCache[childShape.id] = childShape - } - default: - break - } + shape.populateParentCache(&newCache, parent: nil) } - - self.shapeCache = newCache + self.parentCache = newCache } - public func findShape(_ shapeId: UUID) -> Shape? { - return self.shapeCache[shapeId] + public func parent(of shapeId: UUID) -> Shape? { + return self.parentCache[shapeId] } @_spi(Testing) public func intersect(_ ray: Ray) -> [Intersection] { From 475e859c7af9d3a743ec3b4c555ed70b3e9611c7 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 10 Dec 2023 21:30:59 -0800 Subject: [PATCH 10/14] Two things in this commit: we are now caching child IDs in CSG shapes to speed up lookups, and we promoted the top level intersect function to be part of the Shape protocol to improve performance further. --- Sources/ScintillaLib/CSG.swift | 21 ++++++++++++-------- Sources/ScintillaLib/Group.swift | 14 ++++++++----- Sources/ScintillaLib/ImplicitSurface.swift | 2 +- Sources/ScintillaLib/ParametricSurface.swift | 2 +- Sources/ScintillaLib/Prism.swift | 6 +++--- Sources/ScintillaLib/Shape.swift | 20 ++++++++++--------- Sources/ScintillaLib/World.swift | 2 +- 7 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Sources/ScintillaLib/CSG.swift b/Sources/ScintillaLib/CSG.swift index 5198b29..ed799c7 100644 --- a/Sources/ScintillaLib/CSG.swift +++ b/Sources/ScintillaLib/CSG.swift @@ -13,11 +13,20 @@ public struct CSG: Shape { var operation: Operation var left: Shape var right: Shape + var rightChildIDs: Set public init(_ operation: Operation, _ left: Shape, _ right: Shape) { self.operation = operation self.left = left self.right = right + self.rightChildIDs = Set(right.getAllChildIDs()) + } + + public func getAllChildIDs() -> [UUID] { + var childIDs = [self.id] + childIDs.append(contentsOf: self.left.getAllChildIDs()) + childIDs.append(contentsOf: self.rightChildIDs) + return childIDs } public func populateParentCache(_ cache: inout [UUID : Shape], parent: Shape?) { @@ -57,9 +66,9 @@ public struct CSG: Shape { var filteredIntersections: [Intersection] = [] for intersection in allIntersections { - // If the intersection's object is part of the "left" child, + // If the intersection's object is _not_ part of the right child, // then leftHit is true - let leftHit = self.left.includes(intersection.shape) + let leftHit = !self.rightChildIDs.contains(intersection.shape.id) if self.isIntersectionAllowed(leftHit, insideLeft, insideRight) { filteredIntersections.append(intersection) @@ -77,8 +86,8 @@ public struct CSG: Shape { } @_spi(Testing) public func localIntersect(_ localRay: Ray) -> [Intersection] { - let leftIntersections = self.left.intersect(localRay) - let rightIntersections = self.right.intersect(localRay) + let leftIntersections = self.left._intersect(localRay) + let rightIntersections = self.right._intersect(localRay) var allIntersections = leftIntersections allIntersections.append(contentsOf: rightIntersections) @@ -89,10 +98,6 @@ public struct CSG: Shape { return self.filterIntersections(allIntersections) } - public func includes(_ otherID: UUID) -> Bool { - return id == otherID || left.includes(otherID) || right.includes(otherID) - } - // The concept of a normal vector to a CSG object is meaningless and should never be called @_spi(Testing) public func localNormal(_ localPoint: Point, _ uv: UV = .none) -> Vector { fatalError("Whoops... this should never be called for a Group shape") diff --git a/Sources/ScintillaLib/Group.swift b/Sources/ScintillaLib/Group.swift index 6be4b65..3af587e 100644 --- a/Sources/ScintillaLib/Group.swift +++ b/Sources/ScintillaLib/Group.swift @@ -25,11 +25,19 @@ public struct Group: Shape { } } + public func getAllChildIDs() -> [UUID] { + var childIDs = [self.id] + for child in children { + childIDs += child.getAllChildIDs() + } + return childIDs + } + @_spi(Testing) public func localIntersect(_ localRay: Ray) -> [Intersection] { var allIntersections: [Intersection] = [] for child in children { - let intersections = child.intersect(localRay) + let intersections = child._intersect(localRay) allIntersections.append(contentsOf: intersections) } @@ -39,10 +47,6 @@ public struct Group: Shape { return allIntersections } - public func includes(_ otherID: UUID) -> Bool { - return id == otherID || children.contains(where: { shape in shape.includes(otherID) }) - } - // The concept of a normal vector to a Group is meaningless and should never be called @_spi(Testing) public func localNormal(_ localPoint: Point, _ uv: UV = .none) -> Vector { fatalError("Whoops... this should never be called for a Group shape") diff --git a/Sources/ScintillaLib/ImplicitSurface.swift b/Sources/ScintillaLib/ImplicitSurface.swift index eb6d4d0..763e235 100644 --- a/Sources/ScintillaLib/ImplicitSurface.swift +++ b/Sources/ScintillaLib/ImplicitSurface.swift @@ -54,7 +54,7 @@ public struct ImplicitSurface: Shape { // First we check to see if the ray intersects the bounding shape; // note that we need a pair of hits in order to construct a range // of values for t below... - let boundingBoxIntersections = self.boundingShape.intersect(localRay) + let boundingBoxIntersections = self.boundingShape._intersect(localRay) guard boundingBoxIntersections.count == 2 else { return [] } diff --git a/Sources/ScintillaLib/ParametricSurface.swift b/Sources/ScintillaLib/ParametricSurface.swift index e97bfcc..1b80001 100644 --- a/Sources/ScintillaLib/ParametricSurface.swift +++ b/Sources/ScintillaLib/ParametricSurface.swift @@ -117,7 +117,7 @@ public struct ParametricSurface: Shape { // First we check to see if the ray intersects the bounding shape; // note that we need a pair of hits in order to construct a range // of values for t below... - let boundingBoxIntersections = self.boundingShape.intersect(localRay) + let boundingBoxIntersections = self.boundingShape._intersect(localRay) guard boundingBoxIntersections.count == 2 else { return [] } diff --git a/Sources/ScintillaLib/Prism.swift b/Sources/ScintillaLib/Prism.swift index 9bac769..58ce160 100644 --- a/Sources/ScintillaLib/Prism.swift +++ b/Sources/ScintillaLib/Prism.swift @@ -32,7 +32,7 @@ public struct Prism: Shape { @_spi(Testing) public func localIntersect(_ localRay: Ray) -> [Intersection] { // Check bounding box and bail if the ray misses - let boundingBoxIntersections = self.boundingBox.intersect(localRay) + let boundingBoxIntersections = self.boundingBox._intersect(localRay) guard boundingBoxIntersections.count == 2 else { return [] } @@ -52,7 +52,7 @@ public struct Prism: Shape { // Check if ray hits base of prism let basePlane = Plane().translate(0, yBase, 0) - if let planeIntersection = basePlane.intersect(localRay).first { + if let planeIntersection = basePlane._intersect(localRay).first { let maybeHitPoint = localRay.position(planeIntersection.t) if isInsidePolygon(maybeHitPoint, self.xzPoints, yBase) { intersections.append(Intersection(planeIntersection.t, self)) @@ -61,7 +61,7 @@ public struct Prism: Shape { // Check if ray hits top of prism let topPlane = Plane().translate(0, yTop, 0) - if let planeIntersection = topPlane.intersect(localRay).first { + if let planeIntersection = topPlane._intersect(localRay).first { let maybeHitPoint = localRay.position(planeIntersection.t) if isInsidePolygon(maybeHitPoint, self.xzPoints, yTop) { intersections.append(Intersection(planeIntersection.t, self)) diff --git a/Sources/ScintillaLib/Shape.swift b/Sources/ScintillaLib/Shape.swift index 87555ba..27cc941 100644 --- a/Sources/ScintillaLib/Shape.swift +++ b/Sources/ScintillaLib/Shape.swift @@ -10,12 +10,18 @@ import Foundation public protocol Shape { var sharedProperties: SharedShapeProperties { get set } + // The method below is not intended to be overridden but is here to allow Swift + // to give each Shape a copy of the default implementation, instead of having + // all Shape types indirectly accessing the shared implementation and incurring + // more copying in the process, and thus ultimately improving performance. + func _intersect(_ worldRay: Ray) -> [Intersection] + func localIntersect(_ localRay: Ray) -> [Intersection] func localNormal(_ localPoint: Point, _ uv: UV) -> Vector - func includes(_ otherID: UUID) -> Bool - func populateParentCache(_ cache: inout [UUID: Shape], parent: Shape?) + + func getAllChildIDs() -> [UUID] } extension Shape { @@ -130,7 +136,7 @@ extension Shape { // Shared implementations extension Shape { - @_spi(Testing) public func intersect(_ worldRay: Ray) -> [Intersection] { + @_spi(Testing) public func _intersect(_ worldRay: Ray) -> [Intersection] { let localRay = worldRay.transform(self.inverseTransform) return self.localIntersect(localRay) } @@ -172,11 +178,7 @@ extension Shape { return worldNormal } - public func includes(_ otherID: UUID) -> Bool { - return self.id == otherID - } - - internal func includes(_ other: some Shape) -> Bool { - return includes(other.id) + public func getAllChildIDs() -> [UUID] { + return [self.id] } } diff --git a/Sources/ScintillaLib/World.swift b/Sources/ScintillaLib/World.swift index 37fba6c..310d804 100644 --- a/Sources/ScintillaLib/World.swift +++ b/Sources/ScintillaLib/World.swift @@ -48,7 +48,7 @@ public struct World { } @_spi(Testing) public func intersect(_ ray: Ray) -> [Intersection] { - var intersections = self.shapes.flatMap({shape in shape.intersect(ray)}) + var intersections = self.shapes.flatMap({shape in shape._intersect(ray)}) intersections .sort(by: { i1, i2 in i1.t < i2.t From 541c7e306598e215dfb4d15bb81b354f9b07749a Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 10 Dec 2023 21:55:12 -0800 Subject: [PATCH 11/14] Updated all examples. --- Examples/BallWithAreaLight/BallWithAreaLight.swift | 13 +++++++------ Examples/BarthSextic/BarthSextic.swift | 13 +++++++------ Examples/Blob/Blob.swift | 13 +++++++------ Examples/Breather/Breather.swift | 13 +++++++------ Examples/Cavatappi/Cavatappi.swift | 13 +++++++------ Examples/DecoCube/DecoCube.swift | 13 +++++++------ Examples/DimlyLitScene/DimlyLitScene.swift | 14 ++++++++------ Examples/FishEye/FishEye.swift | 13 +++++++------ Examples/HappyHalloween/HappyHalloween.swift | 13 +++++++------ Examples/Hourglass/Hourglass.swift | 13 +++++++------ Examples/RainbowBall/RainbowBall.swift | 13 +++++++------ Examples/Rings/Rings.swift | 13 +++++++------ Examples/StarPrism/StarPrism.swift | 13 +++++++------ Examples/Superellipsoids/Superellipsoids.swift | 13 +++++++------ Examples/Vase/Vase.swift | 13 +++++++------ 15 files changed, 106 insertions(+), 90 deletions(-) diff --git a/Examples/BallWithAreaLight/BallWithAreaLight.swift b/Examples/BallWithAreaLight/BallWithAreaLight.swift index 736dc2b..eb3abad 100644 --- a/Examples/BallWithAreaLight/BallWithAreaLight.swift +++ b/Examples/BallWithAreaLight/BallWithAreaLight.swift @@ -9,13 +9,14 @@ import ScintillaLib @main struct BallWithAreaLight: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 2, -5), + to: Point(0, 1, 0), + up: Vector(0, 1, 0)) + var world: World = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 2, -5), - to: Point(0, 1, 0), - up: Vector(0, 1, 0)) AreaLight(corner: Point(-5, 5, -5), uVec: Vector(2, 0, 0), uSteps: 10, diff --git a/Examples/BarthSextic/BarthSextic.swift b/Examples/BarthSextic/BarthSextic.swift index c584768..42619e7 100644 --- a/Examples/BarthSextic/BarthSextic.swift +++ b/Examples/BarthSextic/BarthSextic.swift @@ -12,13 +12,14 @@ let φ: Double = 1.61833987 @main struct BarthSextic: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world: World = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-5, 5, -5)) ImplicitSurface(center: (0.0, 0.0, 0.0), radius: 2.0) { x, y, z in 4.0*(φ*φ*x*x-y*y)*(φ*φ*y*y-z*z)*(φ*φ*z*z-x*x) - (1.0+2.0*φ)*(x*x+y*y+z*z-1.0)*(x*x+y*y+z*z-1.0) diff --git a/Examples/Blob/Blob.swift b/Examples/Blob/Blob.swift index b227ae0..bca4641 100644 --- a/Examples/Blob/Blob.swift +++ b/Examples/Blob/Blob.swift @@ -10,13 +10,14 @@ import ScintillaLib @main struct Blob: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ImplicitSurface(bottomFrontLeft: (-2, -2, -2), topBackRight: (2, 2, 2), { x, y, z in diff --git a/Examples/Breather/Breather.swift b/Examples/Breather/Breather.swift index 1e34419..6c3aab7 100644 --- a/Examples/Breather/Breather.swift +++ b/Examples/Breather/Breather.swift @@ -24,13 +24,14 @@ func z(u: Double, v: Double) -> Double { // ACHTUNG: This takes a while to render! @main struct Breather: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -15), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -15), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ParametricSurface(bottomFrontLeft: (-8, -5, -5), topBackRight: (8, 5, 5), diff --git a/Examples/Cavatappi/Cavatappi.swift b/Examples/Cavatappi/Cavatappi.swift index 7658a91..eac3819 100644 --- a/Examples/Cavatappi/Cavatappi.swift +++ b/Examples/Cavatappi/Cavatappi.swift @@ -10,13 +10,14 @@ import ScintillaLib @main struct Cavatappi: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 7, -15), + to: Point(0, 7, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 7, -15), - to: Point(0, 7, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) PointLight(position: Point(10, 10, -10)) ParametricSurface(bottomFrontLeft: (-3.5, 0, -3.5), diff --git a/Examples/DecoCube/DecoCube.swift b/Examples/DecoCube/DecoCube.swift index 0f988ce..9110883 100644 --- a/Examples/DecoCube/DecoCube.swift +++ b/Examples/DecoCube/DecoCube.swift @@ -21,13 +21,14 @@ func decoCubeColor(_ x: Double, _ y: Double, _ z: Double) -> (Double, Double, Do @main struct DecoCube: ScintillaApp { + var camera = Camera(width: 600, + height: 600, + viewAngle: PI/3, + from: Point(2, 1, -6), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 600, - height: 600, - viewAngle: PI/3, - from: Point(2, 1, -6), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ImplicitSurface(bottomFrontLeft: (-2.5, -2.5, -2.5), topBackRight: (2.5, 2.5, 2.5), { x, y, z in diff --git a/Examples/DimlyLitScene/DimlyLitScene.swift b/Examples/DimlyLitScene/DimlyLitScene.swift index 6922fca..64727af 100644 --- a/Examples/DimlyLitScene/DimlyLitScene.swift +++ b/Examples/DimlyLitScene/DimlyLitScene.swift @@ -9,13 +9,15 @@ import ScintillaLib @main struct DimlyLitScene: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) + PointLight(position: Point(-10, 10, 0), fadeDistance: 10) Sphere() diff --git a/Examples/FishEye/FishEye.swift b/Examples/FishEye/FishEye.swift index 1e50fde..96d6209 100644 --- a/Examples/FishEye/FishEye.swift +++ b/Examples/FishEye/FishEye.swift @@ -9,13 +9,14 @@ import ScintillaLib @main struct FishEye: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material( diff --git a/Examples/HappyHalloween/HappyHalloween.swift b/Examples/HappyHalloween/HappyHalloween.swift index 4fa620d..2c019d4 100644 --- a/Examples/HappyHalloween/HappyHalloween.swift +++ b/Examples/HappyHalloween/HappyHalloween.swift @@ -22,13 +22,14 @@ func stem(x: Double, y: Double, z: Double) -> Double { @main struct HappyHalloween: ScintillaApp { + var camera = Camera(width: 600, + height: 600, + viewAngle: PI/3, + from: Point(0, 2, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world: World = World { - Camera(width: 600, - height: 600, - viewAngle: PI/3, - from: Point(0, 2, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-2, 5, -5)) ImplicitSurface(center: (0.0, 0.0, 0.0), radius: 2.0) { x, y, z in pumpkin(x: x, y: y, z: z) diff --git a/Examples/Hourglass/Hourglass.swift b/Examples/Hourglass/Hourglass.swift index e749b5e..75e6800 100644 --- a/Examples/Hourglass/Hourglass.swift +++ b/Examples/Hourglass/Hourglass.swift @@ -10,13 +10,14 @@ import ScintillaLib @main struct Hourglass: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 1, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 1, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ParametricSurface(bottomFrontLeft: (-1.0, -1.0, -1.0), topBackRight: (1.0, 1.0, 1.0), diff --git a/Examples/RainbowBall/RainbowBall.swift b/Examples/RainbowBall/RainbowBall.swift index 78692a4..0cfef2c 100644 --- a/Examples/RainbowBall/RainbowBall.swift +++ b/Examples/RainbowBall/RainbowBall.swift @@ -10,13 +10,14 @@ import ScintillaLib @main struct RainbowBall: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 2, -2), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 2, -2), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(.colorFunction(.hsl) { x, y, z in diff --git a/Examples/Rings/Rings.swift b/Examples/Rings/Rings.swift index d31abce..279a267 100644 --- a/Examples/Rings/Rings.swift +++ b/Examples/Rings/Rings.swift @@ -10,13 +10,14 @@ import ScintillaLib @main struct Rings: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(-10, 7, -10), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(-10, 7, -10), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ParametricSurface(bottomFrontLeft: (-6, -3, -6), topBackRight: (6, 3, 6), diff --git a/Examples/StarPrism/StarPrism.swift b/Examples/StarPrism/StarPrism.swift index 0cd8d15..e54f341 100644 --- a/Examples/StarPrism/StarPrism.swift +++ b/Examples/StarPrism/StarPrism.swift @@ -9,13 +9,14 @@ import ScintillaLib @main struct StarPrism: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 5, -5), + to: Point(0, 1, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 5, -5), - to: Point(0, 1, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-5, 5, -5)) Prism(bottomY: 0.0, topY: 2.0, diff --git a/Examples/Superellipsoids/Superellipsoids.swift b/Examples/Superellipsoids/Superellipsoids.swift index 1913c09..ef3505f 100644 --- a/Examples/Superellipsoids/Superellipsoids.swift +++ b/Examples/Superellipsoids/Superellipsoids.swift @@ -9,13 +9,14 @@ import ScintillaLib @main struct Superellipsoids: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -12), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world: World = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -12), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(0, 5, -5)) for (i, e) in [0.25, 0.5, 1.0, 2.0, 2.5].enumerated() { for (j, n) in [0.25, 0.5, 1.0, 2.0, 2.5].enumerated() { diff --git a/Examples/Vase/Vase.swift b/Examples/Vase/Vase.swift index 8d95fba..7985b15 100644 --- a/Examples/Vase/Vase.swift +++ b/Examples/Vase/Vase.swift @@ -9,13 +9,14 @@ import ScintillaLib @main struct Vase: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 7, -10), + to: Point(0, 2, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 7, -10), - to: Point(0, 2, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-5, 5, -5)) SurfaceOfRevolution(yzPoints: [(0.0, 2.0), (1.0, 2.0), (2.0, 1.0), (3.0, 0.5), (6.0, 0.5)]) .material(.solidColor(0.5, 0.6, 0.8)) From d3dfd1b3cb0cb6cf795ae580a813835c7ab9d97d Mon Sep 17 00:00:00 2001 From: quephird Date: Mon, 11 Dec 2023 12:56:52 -0800 Subject: [PATCH 12/14] Updated README. --- README.md | 451 ++++++++++++++++++++++-------------------------------- 1 file changed, 179 insertions(+), 272 deletions(-) diff --git a/README.md b/README.md index 549b4b5..443eacd 100644 --- a/README.md +++ b/README.md @@ -11,21 +11,21 @@ This is a library that is intended to be used to generate ray traced scenes. I h * Click the Add Package button in the main dialog box * Click the Add Package button in the new confirmation dialog box * Observe that ScintillaLib is now in the list under Package Dependencies in the Project Navigator -* Delete main.swift -* Create a new Swift file, say QuickStart.swift and add the following code: +* Find main.swift and rename it to `QuickStart.swift` and add the following code: ```swift import ScintillaLib @main struct QuickStart: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 2, -2), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 2, -2), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(.solidColor(1, 0, 0)) @@ -45,6 +45,80 @@ struct QuickStart: ScintillaApp { Scintilla allows you to describe and render scenes using a light source, a camera, and a collection of shapes, each shape having an associated material. Shapes can then be combined with each other using constructive solid geometry. Below is a discussion on each of these features. +## Constructing a scene + +To construct a scene, you need to create a `Camera` instance and a `World` instance. A `Camera` takes the following four arguments: + +* the width of the resultant image in pixels +* the height of the resultant image in pixels +* the solid angle in radians specifying the field of view +* the point designating its origin +* the point designating where it is pointing at +* the vector representing which way is up. + +A `World` instance is created with the following objects: + +* one or more `Light`s +* one or more `Shape`s + +Lights and shapes are discussed in detail below. + +`World` also supports enumerating shapes using result builders, so you can write the following: + +```swift +World { + PointLight(position: Point(-10, 10, -10)) + Sphere() + .material(.solidColor(1, 0, 0)) + .translate(-2, 0, 0) + Sphere() + .material(.solidColor(0, 1, 0)) + Sphere() + .material(.solidColor(0, 0, 1)) + .translate(2, 0, 0) +``` + +Note the lack of commas separating the parameters to the `World` constructor as well as not needing brackets around the `PointLight` and `Sphere` objects. + +## Rendering a scene + +Scintilla comes with a component that allows you to easily create an application and render a scene. In order to do this, first create a new Xcode project, using the Command Line Tool template. + +![](./images/CLI_template.png) + +Make sure you have added Scintilla as a package dependency. If you have not already, go to File -> Add Packages, and in that dialog box, enter the URL of this Git repository and click Add Package. Xcode should successfully download the library and add it to the project. + +Now that you're ready to use Scintilla, all you need to do is create a new Swift file, say `QuickStart.swift`. Add the following code as an example scene: + +```swift +import ScintillaLib + +@main +struct QuickStart: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 2, -2), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + + var world = World { + PointLight(position: Point(-10, 10, -10)) + Sphere() + .material(.solidColor(1, 0, 0)) + } +``` + +Please note the following about the example above: + +* You must `import ScintillaLib` +* You need to annotate the struct with `@main` +* Your struct must conform to the `ScintallaApp` protocol +* The struct must have the `camera` property, which is of type `Camera` +* The struct must have the `world` property, which is of type `World` + +If you've done all that, you now have a bona fide application and should be able to run it through Xcode. And if all goes well, you should see a window open with the rendered image, and the file `QuickStart.png`, which corresponds with the name of your struct, on your desktop. + ## Primitive shapes The following primitive shapes are available: @@ -98,13 +172,14 @@ import ScintillaLib @main struct SuperellipsoidScene: ScintillaApp { - var world: World = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -12), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -12), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + + var world = World { PointLight(position: Point(0, 5, -5)) for (i, e) in [0.25, 0.5, 1.0, 2.0, 2.5].enumerated() { for (j, n) in [0.25, 0.5, 1.0, 2.0, 2.5].enumerated() { @@ -141,13 +216,14 @@ import ScintillaLib @main struct MyWorld: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ImplicitSurface(bottomFrontLeft: (-2, -2, -2), topBackRight: (2, 2, 2), { x, y, z in @@ -177,13 +253,14 @@ let φ: Double = 1.61833987 @main struct MyImplicitSurface: ScintillaApp { - var world: World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + + var world = World { PointLight(position: Point(-5, 5, -5)) ImplicitSurface(center: (0.0, 0.0, 0.0), radius: 2.0) { x, y, z in @@ -218,13 +295,14 @@ import ScintillaLib @main struct Hourglass: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 1, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 1, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ParametricSurface(bottomFrontLeft: (-1.0, -1.0, -1.0), topBackRight: (1.0, 1.0, 1.0), @@ -253,13 +331,14 @@ import ScintillaLib @main struct Hourglass: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 1, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 1, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ParametricSurface(bottomFrontLeft: (-1.0, -1.0, -1.0), topBackRight: (1.0, 1.0, 1.0), @@ -295,13 +374,14 @@ import ScintillaLib @main struct Hourglass: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 1, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 1, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) ParametricSurface(bottomFrontLeft: (-1.0, -1.0, -1.0), topBackRight: (1.0, 1.0, 1.0), @@ -341,13 +421,14 @@ import ScintillaLib @main struct PrismScene: ScintillaApp { - var world: World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 5, -5), - to: Point(0, 1, 0), - up: Vector(0, 1, 0)) + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 5, -5), + to: Point(0, 1, 0), + up: Vector(0, 1, 0)) + + var world = World { PointLight(position: Point(-5, 5, -5)) Prism(bottomY: 0.0, topY: 2.0, @@ -386,13 +467,14 @@ import ScintillaLib @main struct SorScene: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 7, -10), + to: Point(0, 2, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 7, -10), - to: Point(0, 2, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-5, 5, -5)) SurfaceOfRevolution(yzPoints: [(0.0, 2.0), (1.0, 2.0), @@ -596,101 +678,6 @@ Sphere() ![](./images/CSG.png) -## Groups - -There are times when you do not necessarily want to combine shapes to make new shapes like the above; sometimes you just want to be able to group them together so that they can be moved or otherwise transformed together. For example, if you wanted to take two spheres and rotate them both about each other around the z-axis, you could do this: - -```swift -import ScintillaLib - -@main -struct QuickStart: ScintillaApp { - var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - PointLight(position: Point(-10, 10, -10)) - Sphere() - .material(.solidColor(1, 0, 0)) - .translate(-1, 0, 0) - .rotateZ(PI/2) - Sphere() - .material(.solidColor(0, 1, 0)) - .translate(1, 0, 0) - .rotateZ(PI/2) - } -} -``` - -![](./images/TwoSpheresNotGrouped.png) - -Notice that we have to apply the same rotation twice. We can do better than this by putting the two spheres in a group and rotate that: - -```swift -import ScintillaLib - -@main -struct QuickStart: ScintillaApp { - var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - PointLight(position: Point(-10, 10, -10)) - Group { - Sphere() - .material(.solidColor(1, 0, 0)) - .translate(-1, 0, 0) - Sphere() - .material(.solidColor(0, 1, 0)) - .translate(1, 0, 0) - } - .rotateZ(PI/2) - } -} -``` - -It's not a huge gain in this example but if you are constructing scenes with many more objects, you can save a _lot_ of code duplication. Even in the example below, you can see the savings because you don't have to create and transform four spheres individually; you can just group together two of them, and create translate two copies of the group: - -```swift -import ScintillaLib - -@main -struct QuickStart: ScintillaApp { - var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - PointLight(position: Point(-10, 10, -10)) - for x in [-1.5, 1.5] { - Group { - Sphere() - .material(.solidColor(1, 0, 0)) - .translate(-1, 0, 0) - Sphere() - .material(.solidColor(0, 1, 0)) - .translate(1, 0, 0) - } - .rotateZ(PI/2) - .translate(x, 0, 0) - } - } -} -``` - -Note that groups can also take advantage of result builders, as you can see above, in that you can list multiple objects of a group all at once instead of nesting groups of pairs of objects. - -![](./images/TwoGroupsOfTwoSpheres.png) - - ## Lights Scintilla currently supports two kinds of `Light`s: `PointLight` and `AreaLight`. `PointLight` minimally requires a position to be constructed and defaults to a white color if no other one is specified. Light rays emanate from a single point, the `PointLight`'s position, and are cast on the world. @@ -737,14 +724,15 @@ A scene rendered with an area light at the same position as the point light abov import ScintillaLib @main -struct MyWorld: ScintillaApp { - var world: World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 2, -5), - to: Point(0, 1, 0), - up: Vector(0, 1, 0)) +struct BallWithAreaLight: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 2, -5), + to: Point(0, 1, 0), + up: Vector(0, 1, 0)) + + var world = World { AreaLight(corner: Point(-5, 5, -5), uVec: Vector(2, 0, 0), uSteps: 10, @@ -769,15 +757,17 @@ You can also have multiple lights, which you can use to create scenes with multi import Darwin import ScintillaLib +@available(macOS 12.0, *) @main struct Cavatappi: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 7, -15), + to: Point(0, 7, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 7, -15), - to: Point(0, 7, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) PointLight(position: Point(10, 10, -10)) ParametricSurface(bottomFrontLeft: (-3.5, 0, -3.5), @@ -806,15 +796,17 @@ Lights can also be configured to fade over the distance travelled to objects in ```swift import ScintillaLib +@available(macOS 12.0, *) @main struct DimlyLitScene: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 0, -5), + to: Point(0, 0, 0), + up: Vector(0, 1, 0)) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, 0), fadeDistance: 10) Sphere() @@ -828,115 +820,30 @@ struct DimlyLitScene: ScintillaApp { As with the accuracy and max gradient parameters for parametric surfaces, you might have to experiment with various values in order to get the effect you want. -## Constructing a scene - -To construct a scene, you need to create a `World` instance with the following objects - -* one `Camera` -* one or more `Light`s -* one or more `Shape`s - -Lights and shapes are discussed above. A `Camera` takes the following four arguments: - -* the width of the resultant image in pixels -* the height of the resultant image in pixels -* the solid angle in radians specifying the field of view -* the point designating its origin -* the point designating where it is pointing at -* the vector representing which way is up. - -`World` also supports enumerating shapes using result builders, so you can do the following: - -```swift -World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 3, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - PointLight(position: Point(-10, 10, -10)) - Sphere() - .material(.solidColor(1, 0, 0)) - .translate(-2, 0, 0) - Sphere() - .material(.solidColor(0, 1, 0)) - Sphere() - .material(.solidColor(0, 0, 1)) - .translate(2, 0, 0) -``` - -Note the lack of commas separating the parameters to the `World` constructor as well as not needing brackets around the `Sphere` objects. - -## Rendering a scene - -Scintilla comes with a component that allows you to easily create an application and render a scene. In order to do this, first create a new Xcode project, using the Command Line Tool template. - -![](./images/CLI_template.png) - -Next add Scintilla as a package dependency via File -> Add Packages; in that dialog box, enter the URL of this Git repository and click Add Package. Xcode should successfully download the library and add it to the project. +## Antialiasing -Now that you're ready to use Scintilla, all you need to do is create a new Swift file, say `MyWorld.swift`. Add the following code as an example scene: +You can also optionally render a scene with antialiasing. In the image below, you can see that the various edges of the object are pretty jagged and take away from its verisimilitude. -```swift -import ScintillaLib - -@main -struct MyWorld: ScintillaApp { - var world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -2), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - PointLight(position: Point(-10, 10, -10)) - Sphere() - .material(.solidColor(0, 0, 1)) - .intersection { - Cube() - .material(.solidColor(1, 0, 0)) - .scale(0.8, 0.8, 0.8) - } - .difference { - for (thetaX, thetaZ) in [(0, 0), (0, PI/2), (PI/2, 0)] { - Cylinder() - .material(.solidColor(0, 1, 0)) - .scale(0.5, 0.5, 0.5) - .rotateX(thetaX) - .rotateZ(thetaZ) - } - } - .rotateY(PI/6) - } -} -``` - -Please note the following about the example above: - -* You must `import ScintillaLib` -* You need to annotate the struct with `@main` -* Your struct must conform to the `ScintallaApp` protocol -* The struct must have the `world` property, which is of type `World` - -If you've done all that, you now have a bona fide application and should be able to run it through Xcode. And if all goes well, you should see a window open with the rendered image, and the file `MyWorld.png` on your desktop. +![](./images/Cavatappi.png) -You can also optionally render a scene with antialiasing. In the image above, you can see that the various edges of the object are pretty jagged and take away from the verisimilitude of the image. By adding a property modifier to the `World` object, `.antialiasing(true)`, we can improve its quality: +By adding setting the `antialiasing` parameter of the `Camera` object to true, we can significantly improve the quality of the image: ```swift import Darwin import ScintillaLib +@available(macOS 12.0, *) @main struct Cavatappi: ScintillaApp { + var camera = Camera(width: 400, + height: 400, + viewAngle: PI/3, + from: Point(0, 7, -15), + to: Point(0, 7, 0), + up: Vector(0, 1, 0), + antialiasing: true) + var world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 7, -15), - to: Point(0, 7, 0), - up: Vector(0, 1, 0), - antialiasing: true) PointLight(position: Point(-10, 10, -10)) PointLight(position: Point(10, 10, -10)) ParametricSurface(bottomFrontLeft: (-3.5, 0, -3.5), From 6d1734877d21aaa9d9520b5bc82da3fe50152d67 Mon Sep 17 00:00:00 2001 From: quephird Date: Mon, 11 Dec 2023 18:55:20 -0800 Subject: [PATCH 13/14] Updated tests and got them all to pass. --- Package.swift | 6 +- Tests/ScintillaLibTests/CameraTests.swift | 23 ++ Tests/ScintillaLibTests/GroupTests.swift | 2 +- .../ScintillaLibTests/IntersectionTests.swift | 25 +- Tests/ScintillaLibTests/ShapeTests.swift | 16 +- Tests/ScintillaLibTests/SphereTests.swift | 49 +-- Tests/ScintillaLibTests/TorusTests.swift | 10 +- Tests/ScintillaLibTests/WorldTests.swift | 342 +++++------------- 8 files changed, 158 insertions(+), 315 deletions(-) diff --git a/Package.swift b/Package.swift index ce59650..ee414d9 100644 --- a/Package.swift +++ b/Package.swift @@ -23,9 +23,9 @@ let package = Package( dependencies: []), // Tests -// .testTarget( -// name: "ScintillaLibTests", -// dependencies: ["ScintillaLib"]), + .testTarget( + name: "ScintillaLibTests", + dependencies: ["ScintillaLib"]), // Examples .executableTarget( diff --git a/Tests/ScintillaLibTests/CameraTests.swift b/Tests/ScintillaLibTests/CameraTests.swift index f5c887c..b681b0b 100644 --- a/Tests/ScintillaLibTests/CameraTests.swift +++ b/Tests/ScintillaLibTests/CameraTests.swift @@ -28,4 +28,27 @@ class CameraTests: XCTestCase { let expectedValue = 0.01 XCTAssert(actualValue.isAlmostEqual(expectedValue)) } + + func testRayForPixelForCenterOfCanvas() throws { + let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) + let ray = camera.rayForPixel(100, 50) + XCTAssert(ray.origin.isAlmostEqual(Point(0, 0, 0))) + XCTAssert(ray.direction.isAlmostEqual(Vector(0, 0, -1))) + } + + func testRayForPixelForCornerOfCanvas() throws { + let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) + let ray = camera.rayForPixel(0, 0) + XCTAssert(ray.origin.isAlmostEqual(Point(0, 0, 0))) + XCTAssert(ray.direction.isAlmostEqual(Vector(0.66519, 0.33259, -0.66851))) + } + + func testRayForPixelForTransformedCamera() throws { + let transform = Matrix4.rotationY(PI/4) + .multiply(.translation(0, -2, 5)) + let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: transform) + let ray = camera.rayForPixel(100, 50) + XCTAssert(ray.origin.isAlmostEqual(Point(0, 2, -5))) + XCTAssert(ray.direction.isAlmostEqual(Vector(sqrt(2)/2, 0, -sqrt(2)/2))) + } } diff --git a/Tests/ScintillaLibTests/GroupTests.swift b/Tests/ScintillaLibTests/GroupTests.swift index 5b52cc0..d8ef829 100644 --- a/Tests/ScintillaLibTests/GroupTests.swift +++ b/Tests/ScintillaLibTests/GroupTests.swift @@ -45,7 +45,7 @@ class GroupTests: XCTestCase { .scale(2, 2, 2) let ray = Ray(Point(10, 0, -10), Vector(0, 0, 1)) - let allIntersections = group.intersect(ray) + let allIntersections = group._intersect(ray) XCTAssertEqual(allIntersections.count, 2) } } diff --git a/Tests/ScintillaLibTests/IntersectionTests.swift b/Tests/ScintillaLibTests/IntersectionTests.swift index 9c098f5..f54b4d9 100644 --- a/Tests/ScintillaLibTests/IntersectionTests.swift +++ b/Tests/ScintillaLibTests/IntersectionTests.swift @@ -61,22 +61,14 @@ class IntersectionTests: XCTestCase { XCTAssertEqual(h.shape.id, s2.id) } - let testCamera = Camera(width: 800, - height: 600, - viewAngle:PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - func testPrepareComputationsOutside() async throws { let ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) let shape = Sphere() let world = World { - testCamera shape } let intersection = Intersection(4, shape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) + let computations = intersection.prepareComputations(world, ray, [intersection]) XCTAssertEqual(computations.t, intersection.t) XCTAssertEqual(computations.object.id, shape.id) XCTAssert(computations.point.isAlmostEqual(Point(0, 0, -1))) @@ -89,11 +81,10 @@ class IntersectionTests: XCTestCase { let ray = Ray(Point(0, 0, 0), Vector(0, 0, 1)) let shape = Sphere() let world = World { - testCamera shape } let intersection = Intersection(1, shape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) + let computations = intersection.prepareComputations(world, ray, [intersection]) XCTAssertEqual(computations.t, intersection.t) XCTAssertEqual(computations.object.id, shape.id) XCTAssert(computations.point.isAlmostEqual(Point(0, 0, 1))) @@ -107,11 +98,10 @@ class IntersectionTests: XCTestCase { let shape = Sphere() .translate(0, 0, 1) let world = World { - testCamera shape } let intersection = Intersection(5, shape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) + let computations = intersection.prepareComputations(world, ray, [intersection]) XCTAssertTrue(computations.overPoint[2] < -EPSILON/2) XCTAssertTrue(computations.point[2] > computations.overPoint[2]) } @@ -121,11 +111,10 @@ class IntersectionTests: XCTestCase { let shape = Sphere() .translate(0, 0, 1) let world = World { - testCamera shape } let intersection = Intersection(5, shape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) + let computations = intersection.prepareComputations(world, ray, [intersection]) XCTAssertTrue(computations.underPoint[2] > EPSILON/2) XCTAssertTrue(computations.point[2] < computations.underPoint[2]) } @@ -133,12 +122,11 @@ class IntersectionTests: XCTestCase { func testPrepareComputationsReflected() async throws { let shape = Plane() let world = World { - testCamera shape } let ray = Ray(Point(0, 1, -1), Vector(0, -sqrt(2)/2, sqrt(2)/2)) let intersection = Intersection(sqrt(2), shape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) + let computations = intersection.prepareComputations(world, ray, [intersection]) XCTAssertTrue(computations.reflected.isAlmostEqual(Vector(0, sqrt(2)/2, sqrt(2)/2))) } @@ -153,7 +141,6 @@ class IntersectionTests: XCTestCase { .material(.basicMaterial().refractive(2.5)) .translate(0, 0, 0.25) let world = World { - testCamera glassSphereA glassSphereB glassSphereC @@ -179,7 +166,7 @@ class IntersectionTests: XCTestCase { for index in 0...5 { let intersection = allIntersections[index] - let computations = await intersection.prepareComputations(world, ray, allIntersections) + let computations = intersection.prepareComputations(world, ray, allIntersections) let actualValue = (computations.n1, computations.n2) let expectedValue = expectedValues[index] XCTAssertTrue(actualValue == expectedValue) diff --git a/Tests/ScintillaLibTests/ShapeTests.swift b/Tests/ScintillaLibTests/ShapeTests.swift index f8fc398..53154f1 100644 --- a/Tests/ScintillaLibTests/ShapeTests.swift +++ b/Tests/ScintillaLibTests/ShapeTests.swift @@ -9,19 +9,11 @@ import XCTest @_spi(Testing) import ScintillaLib class ShapeTests: XCTestCase { - let testCamera = Camera(width: 800, - height: 600, - viewAngle:PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - func testWorldToObjectForNestedObject() async throws { let s = Sphere() .translate(5, 0, 0) let world = World { - testCamera Group { Group { s @@ -31,7 +23,7 @@ class ShapeTests: XCTestCase { .rotateY(PI/2) } - let actualValue = await s.worldToObject(world, Point(-2, 0, -10)) + let actualValue = s.worldToObject(world, Point(-2, 0, -10)) let expectedValue = Point(0, 0, -1) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } @@ -41,7 +33,6 @@ class ShapeTests: XCTestCase { .translate(5, 0, 0) let world = World { - testCamera Group { Group { s @@ -51,7 +42,7 @@ class ShapeTests: XCTestCase { .rotateY(PI/2) } - let actualValue = await s.objectToWorld(world, Vector(sqrt(3)/3, sqrt(3)/3, sqrt(3)/3)) + let actualValue = s.objectToWorld(world, Vector(sqrt(3)/3, sqrt(3)/3, sqrt(3)/3)) let expectedValue = Vector(0.28571, 0.42857, -0.85714) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } @@ -61,7 +52,6 @@ class ShapeTests: XCTestCase { .translate(5, 0, 0) let world = World { - testCamera Group { Group { s @@ -71,7 +61,7 @@ class ShapeTests: XCTestCase { .rotateY(PI/2) } - let actualValue = await s.normal(world, Point(1.7321, 1.1547, -5.5774)) + let actualValue = s.normal(world, Point(1.7321, 1.1547, -5.5774)) let expectedValue = Vector(0.28570, 0.42854, -0.85716) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } diff --git a/Tests/ScintillaLibTests/SphereTests.swift b/Tests/ScintillaLibTests/SphereTests.swift index d55b05a..b3b3013 100644 --- a/Tests/ScintillaLibTests/SphereTests.swift +++ b/Tests/ScintillaLibTests/SphereTests.swift @@ -12,7 +12,7 @@ class SphereTests: XCTestCase { func testIntersectTangent() throws { let r = Ray(Point(0, 1, -5), Vector(0, 0, 1)) let s = Sphere() - let intersections = s.intersect(r) + let intersections = s.localIntersect(r) XCTAssertEqual(intersections.count, 1) XCTAssert(intersections[0].t.isAlmostEqual(5.0)) } @@ -20,14 +20,14 @@ class SphereTests: XCTestCase { func testIntersectMiss() throws { let r = Ray(Point(0, 2, -5), Vector(0, 0, 1)) let s = Sphere() - let intersections = s.intersect(r) + let intersections = s.localIntersect(r) XCTAssertEqual(intersections.count, 0) } func testIntersectInside() throws { let r = Ray(Point(0, 0, 0), Vector(0, 0, 1)) let s = Sphere() - let intersections = s.intersect(r) + let intersections = s.localIntersect(r) XCTAssertEqual(intersections.count, 2) XCTAssert(intersections[0].t.isAlmostEqual(-1.0)) XCTAssert(intersections[1].t.isAlmostEqual(1.0)) @@ -36,7 +36,7 @@ class SphereTests: XCTestCase { func testIntersectSphereBehind() throws { let r = Ray(Point(0, 0, 5), Vector(0, 0, 1)) let s = Sphere() - let intersections = s.intersect(r) + let intersections = s.localIntersect(r) XCTAssertEqual(intersections.count, 2) XCTAssert(intersections[0].t.isAlmostEqual(-6.0)) XCTAssert(intersections[1].t.isAlmostEqual(-4.0)) @@ -46,7 +46,7 @@ class SphereTests: XCTestCase { let worldRay = Ray(Point(0, 0, -5), Vector(0, 0, 1)) let s = Sphere() .scale(2, 2, 2) - let intersections = s.intersect(worldRay) + let intersections = s._intersect(worldRay) XCTAssertEqual(intersections.count, 2) XCTAssert(intersections[0].t.isAlmostEqual(3)) XCTAssert(intersections[1].t.isAlmostEqual(7)) @@ -56,86 +56,73 @@ class SphereTests: XCTestCase { let worldRay = Ray(Point(0, 0, -5), Vector(0, 0, 1)) let s = Sphere() .translate(5, 0, 0) - let intersections = s.intersect(worldRay) + let intersections = s._intersect(worldRay) XCTAssertEqual(intersections.count, 0) } - let testCamera = Camera(width: 800, - height: 600, - viewAngle:PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - - func testNormalPointOnXAxis() async throws { + func testNormalPointOnXAxis() throws { let p = Point(1, 0, 0) let s = Sphere() let world = World { - testCamera s } - let actualValue = await s.normal(world, p) + let actualValue = s.normal(world, p) let expectedValue = Vector(1, 0, 0) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testNormalPointOnYAxis() async throws { + func testNormalPointOnYAxis() throws { let p = Point(0, 1, 0) let s = Sphere() let world = World { - testCamera s } - let actualValue = await s.normal(world, p) + let actualValue = s.normal(world, p) let expectedValue = Vector(0, 1, 0) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testNormalPointOnZAxis() async throws { + func testNormalPointOnZAxis() throws { let p = Point(0, 0, 1) let s = Sphere() let world = World { - testCamera s } - let actualValue = await s.normal(world, p) + let actualValue = s.normal(world, p) let expectedValue = Vector(0, 0, 1) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testNormalNonaxialPoint() async throws { + func testNormalNonaxialPoint() throws { let p = Point(sqrt(3)/3, sqrt(3)/3, sqrt(3)/3) let s = Sphere() let world = World { - testCamera s } - let actualValue = await s.normal(world, p) + let actualValue = s.normal(world, p) let expectedValue = Vector(sqrt(3)/3, sqrt(3)/3, sqrt(3)/3) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testNormalTranslatedSphere() async throws { + func testNormalTranslatedSphere() throws { let s = Sphere() .translate(0, 1, 0) let world = World { - testCamera s } - let actualValue = await s.normal(world, Point(0, 1.70711, -0.70711)) + let actualValue = s.normal(world, Point(0, 1.70711, -0.70711)) let expectedValue = Vector(0, 0.70711, -0.70711) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testNormalTransformedSphere() async throws { + func testNormalTransformedSphere() throws { let s = Sphere() .scale(1, 0.5, 1) .rotateY(PI/5) let world = World { - testCamera s } - let actualValue = await s.normal(world, Point(0, sqrt(2)/2, -sqrt(2)/2)) + let actualValue = s.normal(world, Point(0, sqrt(2)/2, -sqrt(2)/2)) let expectedValue = Vector(0, 0.97014, -0.24254) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } diff --git a/Tests/ScintillaLibTests/TorusTests.swift b/Tests/ScintillaLibTests/TorusTests.swift index 641d5e0..e57121a 100644 --- a/Tests/ScintillaLibTests/TorusTests.swift +++ b/Tests/ScintillaLibTests/TorusTests.swift @@ -29,7 +29,7 @@ class TorusTests: XCTestCase { func testIntersectFourHits() throws { let r = Ray(Point(-5, 0, 0), Vector(1, 0, 0)) let torus = Torus(majorRadius: 3, minorRadius: 1) - let intersections = torus.intersect(r) + let intersections = torus.localIntersect(r) let expectedHits = [1.0, 3.0, 7.0, 9.0] let actualHits = intersections.map { intersection in return intersection.t @@ -40,7 +40,7 @@ class TorusTests: XCTestCase { func testIntersectThreeHitsWithOneHitTangent() throws { let r = Ray(Point(-5, 0, -2), Vector(1, 0, 0)) let torus = Torus(majorRadius: 3, minorRadius: 1) - let intersections = torus.intersect(r) + let intersections = torus.localIntersect(r) let expectedHits = [1.53590, 5.0, 8.46410] let actualHits = intersections.map { intersection in return intersection.t @@ -51,7 +51,7 @@ class TorusTests: XCTestCase { func testIntersectTwoHits() throws { let r = Ray(Point(-5, 0, -3), Vector(1, 0, 0)) let torus = Torus(majorRadius: 3, minorRadius: 1) - let intersections = torus.intersect(r) + let intersections = torus.localIntersect(r) let expectedHits = [2.35425, 7.64575] let actualHits = intersections.map { intersection in return intersection.t @@ -62,7 +62,7 @@ class TorusTests: XCTestCase { func testIntersectOneHitTangentRay() throws { let r = Ray(Point(-5, 0, -4), Vector(1, 0, 0)) let torus = Torus(majorRadius: 3, minorRadius: 1) - let intersections = torus.intersect(r) + let intersections = torus.localIntersect(r) let expectedHits = [5.0] let actualHits = intersections.map { intersection in return intersection.t @@ -73,7 +73,7 @@ class TorusTests: XCTestCase { func testIntersectMiss() throws { let r = Ray(Point(-5, 0, -5), Vector(1, 0, 0)) let torus = Torus(majorRadius: 3, minorRadius: 1) - let intersections = torus.intersect(r) + let intersections = torus.localIntersect(r) XCTAssert(intersections.isEmpty) } } diff --git a/Tests/ScintillaLibTests/WorldTests.swift b/Tests/ScintillaLibTests/WorldTests.swift index 0b9ec4b..141e0eb 100644 --- a/Tests/ScintillaLibTests/WorldTests.swift +++ b/Tests/ScintillaLibTests/WorldTests.swift @@ -8,21 +8,8 @@ import XCTest @_spi(Testing) import ScintillaLib -let testCamera = Camera(width: 800, - height: 600, - viewAngle:PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) - func testWorld() -> World { World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -36,10 +23,10 @@ func testWorld() -> World { } class WorldTests: XCTestCase { - func testIntersect() async throws { + func testIntersect() throws { let world = testWorld() let ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) - let intersections = await world.intersect(ray) + let intersections = world.intersect(ray) XCTAssertEqual(intersections.count, 4) XCTAssert(intersections[0].t.isAlmostEqual(4)) XCTAssert(intersections[1].t.isAlmostEqual(4.5)) @@ -47,25 +34,19 @@ class WorldTests: XCTestCase { XCTAssert(intersections[3].t.isAlmostEqual(6)) } - func testShadeHit() async throws { + func testShadeHit() throws { let world = testWorld() let ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) - let shape = await world.shapes[0] + let shape = world.shapes[0] let intersection = Intersection(4, shape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) - let actualValue = await world.shadeHit(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, [intersection]) + let actualValue = world.shadeHit(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0.38066, 0.47583, 0.28549) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testShadeHitInside() async throws { + func testShadeHitInside() throws { let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(0, 0.25, 0), color: Color(1, 1, 1)) Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -78,25 +59,19 @@ class WorldTests: XCTestCase { } let ray = Ray(Point(0, 0, 0), Vector(0, 0, 1)) - let shape = await world.shapes[1] + let shape = world.shapes[1] let intersection = Intersection(0.5, shape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) - let actualValue = await world.shadeHit(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, [intersection]) + let actualValue = world.shadeHit(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0.90498, 0.90498, 0.90498) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testShadeHitIntersectionInShadow() async throws { + func testShadeHitIntersectionInShadow() throws { let s1 = Sphere() let s2 = Sphere() .translate(0, 0, 10) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(0, 0, -10), color: Color(1, 1, 1)) s1 s2 @@ -104,76 +79,76 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, 5), Vector(0, 0, 1)) let intersection = Intersection(4, s2) - let computations = await intersection.prepareComputations(world, ray, [intersection]) - let actualValue = await world.shadeHit(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, [intersection]) + let actualValue = world.shadeHit(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0.1, 0.1, 0.1) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testColorAtMiss() async throws { + func testColorAtMiss() throws { let world = testWorld() let ray = Ray(Point(0, 0, -5), Vector(0, 1, 0)) - let actualValue = await world.colorAt(ray, MAX_RECURSIVE_CALLS) + let actualValue = world.colorAt(ray, MAX_RECURSIVE_CALLS) let expectedValue = Color(0, 0, 0) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testColorAtHit() async throws { + func testColorAtHit() throws { let world = testWorld() let ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) - let actualValue = await world.colorAt(ray, MAX_RECURSIVE_CALLS) + let actualValue = world.colorAt(ray, MAX_RECURSIVE_CALLS) let expectedValue = Color(0.38066, 0.47583, 0.2855) XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - // Ignore this test for now - // - // func testColorAtIntersectionBehindRay() throws { - // let world = testWorld() - // let outerSphere = world.objects[0] - // outerSphere.material.ambient = 1.0 - // let innerSphere = world.objects[1] - // innerSphere.material.ambient = 1.0 - // - // let ray = Ray(point(0, 0, 0.75), vector(0, 0, -1)) - // let actualValue = world.colorAt(ray, MAX_RECURSIVE_CALLS) - // let expectedValue = Color(0.8, 1.0, 0.6) - // XCTAssert(actualValue.isAlmostEqual(expectedValue)) - // } - - func testIsShadowedPointAndLightNotCollinear() async throws { +// Ignore this test for now +// +// func testColorAtIntersectionBehindRay() throws { +// let world = testWorld() +// let outerSphere = world.objects[0] +// outerSphere.material.ambient = 1.0 +// let innerSphere = world.objects[1] +// innerSphere.material.ambient = 1.0 +// +// let ray = Ray(point(0, 0, 0.75), vector(0, 0, -1)) +// let actualValue = world.colorAt(ray, MAX_RECURSIVE_CALLS) +// let expectedValue = Color(0.8, 1.0, 0.6) +// XCTAssert(actualValue.isAlmostEqual(expectedValue)) +// } + + func testIsShadowedPointAndLightNotCollinear() throws { let world = testWorld() let worldPoint = Point(0, 10, 0) - let light = await world.lights[0] - let result = await world.isShadowed(light.position, worldPoint) + let light = world.lights[0] + let result = world.isShadowed(light.position, worldPoint) XCTAssertFalse(result) } - func testIsShadowedObjectBetweenPointAndLight() async throws { + func testIsShadowedObjectBetweenPointAndLight() throws { let world = testWorld() let worldPoint = Point(10, -10, 10) - let light = await world.lights[0] - let result = await world.isShadowed(light.position, worldPoint) + let light = world.lights[0] + let result = world.isShadowed(light.position, worldPoint) XCTAssertTrue(result) } - func testIsShadowedObjectBehindLight() async throws { + func testIsShadowedObjectBehindLight() throws { let world = testWorld() let worldPoint = Point(-20, 20, -20) - let light = await world.lights[0] - let result = await world.isShadowed(light.position, worldPoint) + let light = world.lights[0] + let result = world.isShadowed(light.position, worldPoint) XCTAssertFalse(result) } - func testIsShadowedObjectBehindPoint() async throws { + func testIsShadowedObjectBehindPoint() throws { let world = testWorld() let worldPoint = Point(-2, 2, -2) - let light = await world.lights[0] - let result = await world.isShadowed(light.position, worldPoint) + let light = world.lights[0] + let result = world.isShadowed(light.position, worldPoint) XCTAssertFalse(result) } - func testIntensityOfPointLight() async throws { + func testIntensityOfPointLight() throws { let testCases = [ (Point(0, 1.0001, 0), 1.0), (Point(-1.0001, 0, 0), 1.0), @@ -185,15 +160,15 @@ class WorldTests: XCTestCase { ] let world = testWorld() - let light = await world.lights[0] + let light = world.lights[0] for (worldPoint, expectedIntensity) in testCases { - let actualIntesity = await world.intensity(light, worldPoint) + let actualIntesity = world.intensity(light, worldPoint) XCTAssertEqual(actualIntesity, expectedIntensity) } } - func testIntensityOfAreaLightWithNoJitter() async throws { + func testIntensityOfAreaLightWithNoJitter() throws { let areaLight = AreaLight(corner: Point(-0.5, -0.5, -5), color: Color(1, 1, 1), uVec: Vector(1, 0, 0), @@ -202,12 +177,6 @@ class WorldTests: XCTestCase { vSteps: 2, jitter: NoJitter()) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) areaLight Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -227,12 +196,12 @@ class WorldTests: XCTestCase { (Point(0, 0, -2), 1.0), ] for (worldPoint, expectedIntensity) in testCases { - let actualIntensity = await world.intensity(areaLight, worldPoint) + let actualIntensity = world.intensity(areaLight, worldPoint) XCTAssertEqual(actualIntensity, expectedIntensity) } } - func testIntensityOfAreaLightWithPseduorandomJitter() async throws { + func testIntensityOfAreaLightWithPseduorandomJitter() throws { let areaLight = AreaLight(corner: Point(-0.5, -0.5, -5), color: Color(1, 1, 1), uVec: Vector(1, 0, 0), @@ -241,12 +210,6 @@ class WorldTests: XCTestCase { vSteps: 2, jitter: PseudorandomJitter([0.7, 0.3, 0.9, 0.1, 0.5])) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) areaLight Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -266,24 +229,18 @@ class WorldTests: XCTestCase { (Point(0, 0, -2), 1.0), ] for (worldPoint, expectedIntensity) in testCases { - let actualIntensity = await world.intensity(areaLight, worldPoint) + let actualIntensity = world.intensity(areaLight, worldPoint) XCTAssertEqual(actualIntensity, expectedIntensity) print(actualIntensity) } } - func testReflectedColorForNonreflectiveMaterial() async { + func testReflectedColorForNonreflectiveMaterial() throws { let secondShape = Sphere() .material(SolidColor(1.0, 1.0, 1.0) .ambient(1.0)) .scale(0.5, 0.5, 0.5) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -296,24 +253,18 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, 0), Vector(0, 0, 1)) let intersection = Intersection(1, secondShape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) - let actualValue = await world.reflectedColorAt(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, [intersection]) + let actualValue = world.reflectedColorAt(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0, 0, 0) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testShadeHitWithReflectiveMaterial() async throws { + func testShadeHitWithReflectiveMaterial() throws { let anotherShape = Plane() .material(.basicMaterial() .reflective(0.5)) .translate(0, -1, 0) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -328,20 +279,14 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, -3), Vector(0, -sqrt(2)/2, sqrt(2)/2)) let intersection = Intersection(sqrt(2), anotherShape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) - let actualValue = await world.shadeHit(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, [intersection]) + let actualValue = world.shadeHit(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0.87676, 0.92434, 0.82917) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testColorAtTerminatesForWorldWithMutuallyReflectiveSurfaces() async throws { + func testColorAtTerminatesForWorldWithMutuallyReflectiveSurfaces() throws { let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(0, 0, 0)) Plane() .material(.basicMaterial().reflective(1.0)) @@ -353,20 +298,14 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, 0), Vector(0, 1, 0)) // The following call should terminate; no need to test return value - let _ = await world.colorAt(ray, MAX_RECURSIVE_CALLS) + let _ = world.colorAt(ray, MAX_RECURSIVE_CALLS) } - func testColorAtMaxRecursiveDepth() async throws { + func testColorAtMaxRecursiveDepth() throws { let additionalShape = Plane() .material(.basicMaterial().reflective(0.5)) .translate(0, -1, 0) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -381,38 +320,32 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, -3), Vector(0, -sqrt(2)/2, sqrt(2)/2)) let intersection = Intersection(sqrt(2), additionalShape) - let computations = await intersection.prepareComputations(world, ray, [intersection]) - let actualValue = await world.reflectedColorAt(computations, 0) + let computations = intersection.prepareComputations(world, ray, [intersection]) + let actualValue = world.reflectedColorAt(computations, 0) let expectedValue = Color.black XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testRefractedColorWithOpaqueSurface() async throws { + func testRefractedColorWithOpaqueSurface() throws { let world = testWorld() - let firstShape = await world.shapes[0] + let firstShape = world.shapes[0] let ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) let allIntersections = [ Intersection(4, firstShape), Intersection(6, firstShape), ] - let computations = await allIntersections[0].prepareComputations(world, ray, allIntersections) - let actualValue = await world.refractedColorAt(computations, MAX_RECURSIVE_CALLS) + let computations = allIntersections[0].prepareComputations(world, ray, allIntersections) + let actualValue = world.refractedColorAt(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0, 0, 0) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testRefractedColorAtMaximumRecursiveDepth() async throws { + func testRefractedColorAtMaximumRecursiveDepth() throws { let firstShape = Sphere() .material(.basicMaterial() .transparency(1.0) .refractive(1.5)) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) firstShape Sphere() @@ -424,24 +357,18 @@ class WorldTests: XCTestCase { Intersection(4, firstShape), Intersection(6, firstShape), ] - let computations = await allIntersections[0].prepareComputations(world, ray, allIntersections) - let actualValue = await world.refractedColorAt(computations, 0) + let computations = allIntersections[0].prepareComputations(world, ray, allIntersections) + let actualValue = world.refractedColorAt(computations, 0) let expectedValue = Color.black XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testRefractedColorUnderTotalInternalReflection() async throws { + func testRefractedColorUnderTotalInternalReflection() throws { let firstShape = Sphere() .material(.basicMaterial() .transparency(1.0) .refractive(1.5)) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) firstShape Sphere() @@ -453,13 +380,13 @@ class WorldTests: XCTestCase { Intersection(-sqrt(2)/2, firstShape), Intersection(sqrt(2)/2, firstShape), ] - let computations = await allIntersections[1].prepareComputations(world, ray, allIntersections) - let actualValue = await world.refractedColorAt(computations, MAX_RECURSIVE_CALLS) + let computations = allIntersections[1].prepareComputations(world, ray, allIntersections) + let actualValue = world.refractedColorAt(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color.black XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testRefractedColorWithRefractedRay() async throws { + func testRefractedColorWithRefractedRay() throws { class TestPattern: ScintillaLib.Pattern { override init(_ transform: Matrix4, _ properties: MaterialProperties = MaterialProperties()) { super.init(transform, properties) @@ -483,12 +410,6 @@ class WorldTests: XCTestCase { .scale(0.5, 0.5, 0.5) let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) shapeA shapeB @@ -501,13 +422,13 @@ class WorldTests: XCTestCase { Intersection(0.4899, shapeB), Intersection(0.9899, shapeA), ] - let computations = await allIntersections[2].prepareComputations(world, ray, allIntersections) - let actualValue = await world.refractedColorAt(computations, MAX_RECURSIVE_CALLS) + let computations = allIntersections[2].prepareComputations(world, ray, allIntersections) + let actualValue = world.refractedColorAt(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0, 0.99888, 0.04722) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testShadeHitWithTransparentMaterial() async throws { + func testShadeHitWithTransparentMaterial() throws { let floor = Plane() .material(.basicMaterial() .transparency(0.5) @@ -517,13 +438,8 @@ class WorldTests: XCTestCase { .material(SolidColor(1, 0, 0) .ambient(0.5)) .translate(0, -3.5, -0.5) + let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -540,24 +456,19 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, -3), Vector(0, -sqrt(2)/2, sqrt(2)/2)) let intersection = Intersection(sqrt(2), floor) let allIntersections = [intersection] - let computations = await intersection.prepareComputations(world, ray, allIntersections) - let actualValue = await world.shadeHit(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, allIntersections) + let actualValue = world.shadeHit(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0.93642, 0.68642, 0.68642) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testSchlickReflectanceForTotalInternalReflection() async throws { + func testSchlickReflectanceForTotalInternalReflection() throws { let glass = SolidColor(1.0, 1.0, 1.0) .transparency(1.0) .refractive(1.5) let glassySphere = Sphere().material(glass) + let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) glassySphere } @@ -567,24 +478,19 @@ class WorldTests: XCTestCase { Intersection(-sqrt(2)/2, glassySphere), Intersection(sqrt(2)/2, glassySphere), ] - let computations = await allIntersections[1].prepareComputations(world, ray, allIntersections) - let actualValue = await world.schlickReflectance(computations) + let computations = allIntersections[1].prepareComputations(world, ray, allIntersections) + let actualValue = world.schlickReflectance(computations) let expectedValue = 1.0 XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testSchlickReflectanceForPerpendicularRay() async throws { + func testSchlickReflectanceForPerpendicularRay() throws { let glass = SolidColor(1.0, 1.0, 1.0) .transparency(1.0) .refractive(1.5) let glassySphere = Sphere().material(glass) + let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) glassySphere } @@ -594,24 +500,19 @@ class WorldTests: XCTestCase { Intersection(-1, glassySphere), Intersection(1, glassySphere), ] - let computations = await allIntersections[1].prepareComputations(world, ray, allIntersections) - let actualValue = await world.schlickReflectance(computations) + let computations = allIntersections[1].prepareComputations(world, ray, allIntersections) + let actualValue = world.schlickReflectance(computations) let expectedValue = 0.04 XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testSchlickReflectanceForSmallAngleAndN2GreaterThanN1() async throws { + func testSchlickReflectanceForSmallAngleAndN2GreaterThanN1() throws { let glass = SolidColor(1.0, 1.0, 1.0) .transparency(1.0) .refractive(1.5) let glassySphere = Sphere().material(glass) + let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) glassySphere } @@ -619,13 +520,13 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0.99, -2), Vector(0, 0, 1)) let intersection = Intersection(1.8589, glassySphere) let allIntersections = [intersection] - let computations = await intersection.prepareComputations(world, ray, allIntersections) - let actualValue = await world.schlickReflectance(computations) + let computations = intersection.prepareComputations(world, ray, allIntersections) + let actualValue = world.schlickReflectance(computations) let expectedValue = 0.48873 XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testShadeHitWithReflectiveAndTransparentMaterial() async throws { + func testShadeHitWithReflectiveAndTransparentMaterial() throws { let floor = Plane() .material(.basicMaterial() .transparency(0.5) @@ -636,13 +537,8 @@ class WorldTests: XCTestCase { .material(SolidColor(1, 0, 0) .ambient(0.5)) .translate(0, -3.5, -0.5) + let world = World { - Camera(width: 800, - height: 600, - viewAngle: PI/3, - from: Point(0, 1, -1), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) PointLight(position: Point(-10, 10, -10)) Sphere() .material(SolidColor(0.8, 1.0, 0.6) @@ -659,21 +555,16 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, -3), Vector(0, -sqrt(2)/2, sqrt(2)/2)) let intersection = Intersection(sqrt(2), floor) let allIntersections = [intersection] - let computations = await intersection.prepareComputations(world, ray, allIntersections) - let actualValue = await world.shadeHit(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, allIntersections) + let actualValue = world.shadeHit(computations, MAX_RECURSIVE_CALLS) let expectedValue = Color(0.93391, 0.69643, 0.69243) XCTAssertTrue(actualValue.isAlmostEqual(expectedValue)) } - func testShadeHitWithTwoLightsAndVerifyThereAreTwoShadows() async throws { + func testShadeHitWithTwoLightsAndVerifyThereAreTwoShadows() throws { let floor = Plane().translate(0, -1, 0) + let world = World { - Camera(width: 400, - height: 400, - viewAngle: PI/3, - from: Point(0, 0, -5), - to: Point(0, 0, 0), - up: Vector(0, 1, 0)) // Light above and to the left of the sphere PointLight(position: Point(-10, 10, 0)) // Light above and to the right of the sphere @@ -694,44 +585,9 @@ class WorldTests: XCTestCase { let ray = Ray(Point(0, 0, -5), direction) let intersection = Intersection(t, floor) let allIntersections = [intersection] - let computations = await intersection.prepareComputations(world, ray, allIntersections) - let actualColor = await world.shadeHit(computations, MAX_RECURSIVE_CALLS) + let computations = intersection.prepareComputations(world, ray, allIntersections) + let actualColor = world.shadeHit(computations, MAX_RECURSIVE_CALLS) XCTAssertTrue(actualColor.isAlmostEqual(expectedColor)) } } - - func testRayForPixelForCenterOfCanvas() async throws { - let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) - let lights = [PointLight(position: Point(-10, 10, -10))] - let shapes: [Shape] = [] - let world = World(camera, lights, shapes) - - let ray = await world.rayForPixel(100, 50) - XCTAssert(ray.origin.isAlmostEqual(Point(0, 0, 0))) - XCTAssert(ray.direction.isAlmostEqual(Vector(0, 0, -1))) - } - - func testRayForPixelForCornerOfCanvas() async throws { - let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) - let lights = [PointLight(position: Point(-10, 10, -10))] - let objects: [Shape] = [] - let world = World(camera, lights, objects) - - let ray = await world.rayForPixel(0, 0) - XCTAssert(ray.origin.isAlmostEqual(Point(0, 0, 0))) - XCTAssert(ray.direction.isAlmostEqual(Vector(0.66519, 0.33259, -0.66851))) - } - - func testRayForPixelForTransformedCamera() async throws { - let transform = Matrix4.rotationY(PI/4) - .multiply(.translation(0, -2, 5)) - let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: transform) - let lights = [PointLight(position: Point(-10, 10, -10))] - let objects: [Shape] = [] - let world = World(camera, lights, objects) - - let ray = await world.rayForPixel(100, 50) - XCTAssert(ray.origin.isAlmostEqual(Point(0, 2, -5))) - XCTAssert(ray.direction.isAlmostEqual(Vector(sqrt(2)/2, 0, -sqrt(2)/2))) - } } From 7bea988fbc75c7671b1a0b5557e86999e3c54ea8 Mon Sep 17 00:00:00 2001 From: quephird Date: Tue, 12 Dec 2023 12:16:48 -0800 Subject: [PATCH 14/14] Finally made Camera an actor and updated tests. --- Sources/ScintillaLib/Camera.swift | 2 +- Tests/ScintillaLibTests/CameraTests.swift | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/ScintillaLib/Camera.swift b/Sources/ScintillaLib/Camera.swift index 6baf96d..4365d55 100644 --- a/Sources/ScintillaLib/Camera.swift +++ b/Sources/ScintillaLib/Camera.swift @@ -7,7 +7,7 @@ import Foundation -public struct Camera { +public actor Camera { var horizontalSize: Int var verticalSize: Int var fieldOfView: Double diff --git a/Tests/ScintillaLibTests/CameraTests.swift b/Tests/ScintillaLibTests/CameraTests.swift index b681b0b..a396183 100644 --- a/Tests/ScintillaLibTests/CameraTests.swift +++ b/Tests/ScintillaLibTests/CameraTests.swift @@ -9,45 +9,45 @@ import XCTest @_spi(Testing) import ScintillaLib class CameraTests: XCTestCase { - func testPixelSizeForHorizontalCanvas() throws { + func testPixelSizeForHorizontalCanvas() async throws { let camera = Camera(width: 200, height: 125, viewAngle: PI/2, viewTransform: .identity) - let actualValue = camera.pixelSize + let actualValue = await camera.pixelSize let expectedValue = 0.01 XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testPixelSizeForVerticalCanvas() throws { + func testPixelSizeForVerticalCanvas() async throws { let camera = Camera(width: 125, height: 200, viewAngle: PI/2, viewTransform: .identity) - let actualValue = camera.pixelSize + let actualValue = await camera.pixelSize let expectedValue = 0.01 XCTAssert(actualValue.isAlmostEqual(expectedValue)) } - func testRayForPixelForCenterOfCanvas() throws { + func testRayForPixelForCenterOfCanvas() async throws { let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) - let ray = camera.rayForPixel(100, 50) + let ray = await camera.rayForPixel(100, 50) XCTAssert(ray.origin.isAlmostEqual(Point(0, 0, 0))) XCTAssert(ray.direction.isAlmostEqual(Vector(0, 0, -1))) } - func testRayForPixelForCornerOfCanvas() throws { + func testRayForPixelForCornerOfCanvas() async throws { let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) - let ray = camera.rayForPixel(0, 0) + let ray = await camera.rayForPixel(0, 0) XCTAssert(ray.origin.isAlmostEqual(Point(0, 0, 0))) XCTAssert(ray.direction.isAlmostEqual(Vector(0.66519, 0.33259, -0.66851))) } - func testRayForPixelForTransformedCamera() throws { + func testRayForPixelForTransformedCamera() async throws { let transform = Matrix4.rotationY(PI/4) .multiply(.translation(0, -2, 5)) let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: transform) - let ray = camera.rayForPixel(100, 50) + let ray = await camera.rayForPixel(100, 50) XCTAssert(ray.origin.isAlmostEqual(Point(0, 2, -5))) XCTAssert(ray.direction.isAlmostEqual(Vector(sqrt(2)/2, 0, -sqrt(2)/2))) }