diff --git a/Examples/BallWithAreaLight/BallWithAreaLight.swift b/Examples/BallWithAreaLight/BallWithAreaLight.swift index 081e168..eb3abad 100644 --- a/Examples/BallWithAreaLight/BallWithAreaLight.swift +++ b/Examples/BallWithAreaLight/BallWithAreaLight.swift @@ -7,16 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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 57fceef..42619e7 100644 --- a/Examples/BarthSextic/BarthSextic.swift +++ b/Examples/BarthSextic/BarthSextic.swift @@ -10,16 +10,16 @@ import ScintillaLib let φ: Double = 1.61833987 -@available(macOS 12.0, *) @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 03bc6cb..bca4641 100644 --- a/Examples/Blob/Blob.swift +++ b/Examples/Blob/Blob.swift @@ -8,16 +8,16 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @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 403d4fb..6c3aab7 100644 --- a/Examples/Breather/Breather.swift +++ b/Examples/Breather/Breather.swift @@ -22,16 +22,16 @@ func z(u: Double, v: Double) -> Double { } // ACHTUNG: This takes a while to render! -@available(macOS 12.0, *) @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 bc544bb..eac3819 100644 --- a/Examples/Cavatappi/Cavatappi.swift +++ b/Examples/Cavatappi/Cavatappi.swift @@ -8,16 +8,16 @@ 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), diff --git a/Examples/DecoCube/DecoCube.swift b/Examples/DecoCube/DecoCube.swift index 82915b0..9110883 100644 --- a/Examples/DecoCube/DecoCube.swift +++ b/Examples/DecoCube/DecoCube.swift @@ -19,16 +19,16 @@ 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 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/Die/Die.swift b/Examples/Die/Die.swift index 63ea9b6..be97b63 100644 --- a/Examples/Die/Die.swift +++ b/Examples/Die/Die.swift @@ -7,20 +7,20 @@ import ScintillaLib -@available(macOS 12.0, *) @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/DimlyLitScene/DimlyLitScene.swift b/Examples/DimlyLitScene/DimlyLitScene.swift index 85abd78..64727af 100644 --- a/Examples/DimlyLitScene/DimlyLitScene.swift +++ b/Examples/DimlyLitScene/DimlyLitScene.swift @@ -7,16 +7,17 @@ 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() diff --git a/Examples/FishEye/FishEye.swift b/Examples/FishEye/FishEye.swift index 72ea0fc..96d6209 100644 --- a/Examples/FishEye/FishEye.swift +++ b/Examples/FishEye/FishEye.swift @@ -7,16 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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 9090b71..2c019d4 100644 --- a/Examples/HappyHalloween/HappyHalloween.swift +++ b/Examples/HappyHalloween/HappyHalloween.swift @@ -20,16 +20,16 @@ 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 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/HollowedSphere/HollowedSphere.swift b/Examples/HollowedSphere/HollowedSphere.swift index 0d0bc76..8c88340 100644 --- a/Examples/HollowedSphere/HollowedSphere.swift +++ b/Examples/HollowedSphere/HollowedSphere.swift @@ -7,16 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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/Examples/Hourglass/Hourglass.swift b/Examples/Hourglass/Hourglass.swift index 21c7d5b..75e6800 100644 --- a/Examples/Hourglass/Hourglass.swift +++ b/Examples/Hourglass/Hourglass.swift @@ -8,16 +8,16 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @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/QuickStart/QuickStart.swift b/Examples/QuickStart/QuickStart.swift index 6896f52..d27328b 100644 --- a/Examples/QuickStart/QuickStart.swift +++ b/Examples/QuickStart/QuickStart.swift @@ -7,16 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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/Examples/RainbowBall/RainbowBall.swift b/Examples/RainbowBall/RainbowBall.swift index 32bf451..0cfef2c 100644 --- a/Examples/RainbowBall/RainbowBall.swift +++ b/Examples/RainbowBall/RainbowBall.swift @@ -8,16 +8,16 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @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 8d8c4a3..279a267 100644 --- a/Examples/Rings/Rings.swift +++ b/Examples/Rings/Rings.swift @@ -8,16 +8,16 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @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 9be02a9..e54f341 100644 --- a/Examples/StarPrism/StarPrism.swift +++ b/Examples/StarPrism/StarPrism.swift @@ -7,16 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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 865ffc4..ef3505f 100644 --- a/Examples/Superellipsoids/Superellipsoids.swift +++ b/Examples/Superellipsoids/Superellipsoids.swift @@ -7,16 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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/TDOR/TDOR.swift b/Examples/TDOR/TDOR.swift index 853b08d..1cdeda6 100644 --- a/Examples/TDOR/TDOR.swift +++ b/Examples/TDOR/TDOR.swift @@ -8,16 +8,16 @@ import Darwin import ScintillaLib -@available(macOS 12.0, *) @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 diff --git a/Examples/Vase/Vase.swift b/Examples/Vase/Vase.swift index 830ced8..7985b15 100644 --- a/Examples/Vase/Vase.swift +++ b/Examples/Vase/Vase.swift @@ -7,16 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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)) diff --git a/Examples/Wine/Wine.swift b/Examples/Wine/Wine.swift index 336c9d4..c6a0ab0 100644 --- a/Examples/Wine/Wine.swift +++ b/Examples/Wine/Wine.swift @@ -7,10 +7,16 @@ import ScintillaLib -@available(macOS 12.0, *) @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) @@ -26,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/README.md b/README.md index 825ea37..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,104 +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 - -@available(macOS 12.0, *) -@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 - -@available(macOS 12.0, *) -@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 - -@available(macOS 12.0, *) -@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. @@ -740,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, @@ -775,13 +760,14 @@ 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), @@ -813,13 +799,14 @@ 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() @@ -833,100 +820,13 @@ 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 +## Antialiasing -Lights and shapes are discussed above. A `Camera` takes the following four arguments: +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. -* 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. - -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: - -```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 @@ -935,14 +835,15 @@ 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), diff --git a/Sources/ScintillaLib/CSG.swift b/Sources/ScintillaLib/CSG.swift index 7077b1f..ed799c7 100644 --- a/Sources/ScintillaLib/CSG.swift +++ b/Sources/ScintillaLib/CSG.swift @@ -13,37 +13,29 @@ 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.left.parentId = self.id - self.right.parentId = self.id + self.rightChildIDs = Set(right.getAllChildIDs()) } - public func findShape(_ shapeId: UUID) -> Shape? { - for shape in [self.left, self.right] { - if shape.id == shapeId { - return shape - } + public func getAllChildIDs() -> [UUID] { + var childIDs = [self.id] + childIDs.append(contentsOf: self.left.getAllChildIDs()) + childIDs.append(contentsOf: self.rightChildIDs) + return childIDs + } - switch shape { - case let csg as CSG: - if let shape = csg.findShape(shapeId) { - return shape - } - case let group as Group: - if let shape = group.findShape(shapeId) { - return shape - } - default: - continue - } + public func populateParentCache(_ cache: inout [UUID : Shape], parent: Shape?) { + if let parent { + cache[self.id] = parent } - return nil + left.populateParentCache(&cache, parent: self) + right.populateParentCache(&cache, parent: self) } static func makeCSG(_ operation: Operation, _ baseShape: Shape, @ShapeBuilder _ otherShapesBuilder: () -> [Shape]) -> Shape { @@ -67,7 +59,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 @@ -75,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 - leftHit = self.left.includes(intersection.shape) + let leftHit = !self.rightChildIDs.contains(intersection.shape.id) if self.isIntersectionAllowed(leftHit, insideLeft, insideRight) { filteredIntersections.append(intersection) @@ -95,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) diff --git a/Sources/ScintillaLib/Camera.swift b/Sources/ScintillaLib/Camera.swift index 6b94b5d..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 @@ -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.. [Shape]) { - let children = builder() - for var child in children { - child.parentId = self.id - self.children.append(child) - } + self.children = builder() } - public func findShape(_ shapeId: UUID) -> Shape? { - for shape in self.children { - if shape.id == shapeId { - return shape - } - - switch shape { - case let csg as CSG: - if let shape = csg.findShape(shapeId) { - return shape - } - case let group as Group: - if let shape = group.findShape(shapeId) { - return shape - } - default: - continue - } + public func populateParentCache(_ cache: inout [UUID : Shape], parent: Shape?) { + if let parent { + cache[self.id] = parent } - return nil + for child in children { + child.populateParentCache(&cache, parent: self) + } + } + + 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) } 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/Intersection.swift b/Sources/ScintillaLib/Intersection.swift index 4ddc928..4ceda58 100644 --- a/Sources/ScintillaLib/Intersection.swift +++ b/Sources/ScintillaLib/Intersection.swift @@ -55,10 +55,10 @@ public struct Intersection { return (n1, n2) } - @_spi(Testing) public func prepareComputations(_ world: World, _ ray: Ray, _ allIntersections: [Intersection]) async -> 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/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/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().. [Intersection] + func localIntersect(_ localRay: Ray) -> [Intersection] func localNormal(_ localPoint: Point, _ uv: UV) -> Vector + + func populateParentCache(_ cache: inout [UUID: Shape], parent: Shape?) + + func getAllChildIDs() -> [UUID] } extension Shape { @@ -43,12 +53,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 } @@ -132,72 +136,49 @@ 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) } - @_spi(Testing) public func normal(_ world: World, _ worldPoint: Point, _ uv: UV = .none) async -> 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 { + 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 = await world.findShape(parentId) else { - fatalError("Whoops... unable to find parent shape!") - } - - switch parentShape { - case let group as Group: - objectPoint = await group.worldToObject(world, worldPoint) - case let csg as CSG: - objectPoint = await 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) } - @_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 { - fatalError("Whoops... unable ot find parent shape!") - } - - switch parentShape { - case let group as Group: - worldNormal = await group.objectToWorld(world, worldNormal) - case let csg as CSG: - worldNormal = await 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 } - 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 getAllChildIDs() -> [UUID] { + return [self.id] } } 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 93b83c4..310d804 100644 --- a/Sources/ScintillaLib/World.swift +++ b/Sources/ScintillaLib/World.swift @@ -9,35 +9,15 @@ 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 + private var parentCache: [UUID: Shape] - 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 { @@ -49,44 +29,26 @@ public actor World { } } - self.lights = lights - self.shapes = shapes - self.camera = camera - self.totalPixels = camera.horizontalSize * camera.verticalSize + self.init(lights, shapes) } - 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? { - for shape in self.shapes { - if shape.id == shapeId { - return shape - } - switch shape { - case let csg as CSG: - if let shape = csg.findShape(shapeId) { - return shape - } - case let group as Group: - if let shape = group.findShape(shapeId) { - return shape - } - default: - continue - } + var newCache: [UUID: Shape] = [:] + for shape in shapes { + shape.populateParentCache(&newCache, parent: nil) } + self.parentCache = newCache + } - return nil + public func parent(of shapeId: UUID) -> Shape? { + return self.parentCache[shapeId] } @_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 @@ -121,7 +83,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 +106,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 +119,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 +162,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 +213,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)] } diff --git a/Tests/ScintillaLibTests/CameraTests.swift b/Tests/ScintillaLibTests/CameraTests.swift index f5c887c..a396183 100644 --- a/Tests/ScintillaLibTests/CameraTests.swift +++ b/Tests/ScintillaLibTests/CameraTests.swift @@ -9,23 +9,46 @@ 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() async throws { + let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) + 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() async throws { + let camera = Camera(width: 201, height: 101, viewAngle: PI/2, viewTransform: .identity) + 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() 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 = 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))) + } } 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))) - } }