From 6c20bbf9c35279946b268231250dbbdb8db0fb64 Mon Sep 17 00:00:00 2001 From: Sebastian Raaphorst Date: Thu, 19 Jan 2023 01:39:57 -0500 Subject: [PATCH] Chapter 14: World rendered but not working. --- src/main/kotlin/apps/ch14_world.kt | 91 ++++++++++++ src/main/kotlin/apps/ch14_world.yaml | 205 +++++++++++++++++++++++++++ src/main/kotlin/shapes/Cone.kt | 3 + src/main/kotlin/shapes/Cube.kt | 3 + src/main/kotlin/shapes/Cylinder.kt | 3 + src/main/kotlin/shapes/Group.kt | 10 ++ src/main/kotlin/shapes/Plane.kt | 3 + src/main/kotlin/shapes/Shape.kt | 3 + src/main/kotlin/shapes/Sphere.kt | 3 + src/test/kotlin/shapes/ShapeTest.kt | 6 + 10 files changed, 330 insertions(+) create mode 100644 src/main/kotlin/apps/ch14_world.kt create mode 100644 src/main/kotlin/apps/ch14_world.yaml diff --git a/src/main/kotlin/apps/ch14_world.kt b/src/main/kotlin/apps/ch14_world.kt new file mode 100644 index 0000000..5a02094 --- /dev/null +++ b/src/main/kotlin/apps/ch14_world.kt @@ -0,0 +1,91 @@ +package apps + +// By Sebastian Raaphorst, 2023. +// From https://forum.raytracerchallenge.com/thread/13/groups-scene-description + +import light.PointLight +import material.Material +import math.Color +import math.Matrix +import math.Tuple +import scene.Camera +import scene.World +import shapes.* +import java.io.File +import kotlin.math.PI +import kotlin.system.measureTimeMillis + +fun main() { + val transforms = listOf(0, PI / 3, 2 * PI / 3, PI, 4 * PI / 3, 5 * PI / 3) + .map(Matrix::rotateY) + + val leg = run { + val sphere = Sphere(Matrix.translate(0, 0, -1) * Matrix.scale(0.25, 0.25, 0.25)) + val cylinder = Cylinder(0, 1, false, + Matrix.translate(0, 0, -1) * Matrix.rotateY(-PI / 6) * + Matrix.rotateZ(-PI / 2) * Matrix.scale(0.25, 1, 0.25)) + Group(children = listOf(sphere, cylinder)) + } + + val cap = run { + val trans = Matrix.rotateX(-PI / 4 ) * Matrix.scale(0.24606, 1.37002, 0.24606) + val cones = transforms.map { + Cone(-1, 0, false, it * trans) + } + Group(children = cones) + } + + val wacky = run { + val legs = transforms.map(leg::withTransformation) + val cap1 = cap.withTransformation(Matrix.translate(0, 1, 0)) + val cap2 = cap.withTransformation(Matrix.rotateX(PI) * Matrix.translate(0, 1, 0)) + Group(children = legs + listOf(cap1, cap2)) + } + + val backdrop = run { + val t = Matrix.translate(0, 0, 100) * Matrix.rotateX(PI / 2) + val m = Material(Color.WHITE, ambient = 1, diffuse = 0, specular = 0) + Plane(t, m) + } + + val wacky1 = run { + val t = Matrix.translate(-2.8, 0, 0) * Matrix.rotateX(0.4363) * Matrix.rotateY(PI / 18) + val m = Material(Color(0.9, 0.2, 0.4), ambient = 0.2, diffuse = 0.8, specular = 0.7, shininess = 20) + wacky.withTransformation(t).withMaterial(m) + } + + val wacky2 = run { + val t = Matrix.rotateY(PI / 18) + val m = Material(Color(0.2, 0.9, 0.6), ambient = 0.2, diffuse = 0.8, specular = 0.7, shininess = 20) + wacky.withTransformation(t).withMaterial(m) + } + + val wacky3 = run { + val t = Matrix.translate(2.8, 0, 0) * Matrix.rotateX(-0.4363) * Matrix.rotateY(-PI / 18) + val m = Material(Color(0.2, 0.3, 1), ambient = 0.2, diffuse = 0.8, specular = 0.7, shininess = 20) + wacky.withTransformation(t).withMaterial(m) + } + + val world = run { + val color = Color(0.25, 0.25, 0.25) + val light1 = PointLight(Tuple.point(10_000, 10_000, -10_000), color) + val light2 = PointLight(Tuple.point(-10_000, 10_000, -10_000), color) + val light3 = PointLight(Tuple.point(10_000, -10_000, -10_000), color) + val light4 = PointLight(Tuple.point(-10_000, -10_000, -10_000), color) + World(listOf(backdrop, wacky1, wacky2, wacky3), listOf(light1, light2, light3, light4)) + } + + val camera = run { + val from = Tuple.point(0, 0, -9) + val to = Tuple.PZERO + val t = from.viewTransformationFrom(to, Tuple.VY) + Camera(2400, 1200, 0.9, t) + } + + val elapsed = measureTimeMillis { + val canvas = camera.render(world) + canvas.toPPMFile(File("output/ch14_world.ppm")) + } + println("Time elapsed: ${elapsed / 1000.0} s") +} + diff --git a/src/main/kotlin/apps/ch14_world.yaml b/src/main/kotlin/apps/ch14_world.yaml new file mode 100644 index 0000000..ad8053e --- /dev/null +++ b/src/main/kotlin/apps/ch14_world.yaml @@ -0,0 +1,205 @@ +# ====================================================== +# group.yml +# +# This file describes the scene illustrated at the start +# of chapter 14, "Groups", in "The Ray Tracer +# Challenge" +# +# This scene description assumes: +# +# 1. Your ray tracer supports multiple light sources. +# If it does not, you can omit the extra light +# sources and bump up the existing light's intensity +# to [1, 1, 1]. +# 2. Child objects in a group inherit their default +# material from the parent group. If you haven't +# implemented this optional feature, you'll need +# arrange other means of texturing the child +# elements (or accept that all elements of the +# scene will be white). +# +# by Jamis Buck +# ====================================================== + +# ====================================================== +# the camera +# ====================================================== + +- add: camera + width: 300 + height: 100 + field-of-view: 0.9 + from: [0, 0, -9] + to: [0, 0, 0] + up: [0, 1, 0] + +# ====================================================== +# light sources +# ====================================================== + +- add: light + at: [ 10000, 10000, -10000 ] + intensity: [ 0.25, 0.25, 0.25 ] +- add: light + at: [ -10000, 10000, -10000 ] + intensity: [ 0.25, 0.25, 0.25 ] +- add: light + at: [ 10000, -10000, -10000 ] + intensity: [ 0.25, 0.25, 0.25 ] +- add: light + at: [ -10000, -10000, -10000 ] + intensity: [ 0.25, 0.25, 0.25 ] + +# ====================================================== +# These describe groups that will be reused within +# the scene. (You can think of these as functions that +# return a new instance of the given shape each time they +# are referenced.) +# ====================================================== + +- define: leg + value: + add: group + children: + - add: sphere + transform: + - [ scale, 0.25, 0.25, 0.25 ] + - [ translate, 0, 0, -1 ] + - add: cylinder + min: 0 + max: 1 + closed: false + transform: + - [ scale, 0.25, 1, 0.25 ] + - [ rotate-z, -1.5708 ] + - [ rotate-y, -0.5236 ] + - [ translate, 0, 0, -1 ] + +- define: cap + value: + add: group + children: + - add: cone + min: -1 + max: 0 + closed: false + transform: + - [ scale, 0.24606, 1.37002, 0.24606 ] + - [ rotate-x, -0.7854 ] + - add: cone + min: -1 + max: 0 + closed: false + transform: + - [ scale, 0.24606, 1.37002, 0.24606 ] + - [ rotate-x, -0.7854 ] + - [ rotate-y, 1.0472 ] + - add: cone + min: -1 + max: 0 + closed: false + transform: + - [ scale, 0.24606, 1.37002, 0.24606 ] + - [ rotate-x, -0.7854 ] + - [ rotate-y, 2.0944 ] + - add: cone + min: -1 + max: 0 + closed: false + transform: + - [ scale, 0.24606, 1.37002, 0.24606 ] + - [ rotate-x, -0.7854 ] + - [ rotate-y, 3.1416 ] + - add: cone + min: -1 + max: 0 + closed: false + transform: + - [ scale, 0.24606, 1.37002, 0.24606 ] + - [ rotate-x, -0.7854 ] + - [ rotate-y, 4.1888 ] + - add: cone + min: -1 + max: 0 + closed: false + transform: + - [ scale, 0.24606, 1.37002, 0.24606 ] + - [ rotate-x, -0.7854 ] + - [ rotate-y, 5.236 ] + +- define: wacky + value: + add: group + children: + - add: leg + - add: leg + transform: + - [ rotate-y, 1.0472 ] + - add: leg + transform: + - [ rotate-y, 2.0944 ] + - add: leg + transform: + - [ rotate-y, 3.1416 ] + - add: leg + transform: + - [ rotate-y, 4.1888 ] + - add: leg + transform: + - [ rotate-y, 5.236 ] + - add: cap + transform: + - [ translate, 0, 1, 0 ] + - add: cap + transform: + - [ translate, 0, 1, 0 ] + - [ rotate-x, 3.1416 ] + +# ====================================================== +# Construct the scene itself +# ====================================================== + +# a white backdrop +- add: plane + transform: + - [ rotate-x, 1.5708 ] + - [ translate, 0, 0, 100 ] + material: + color: [ 1, 1, 1 ] + ambient: 1 + diffuse: 0 + specular: 0 + +- add: wacky + transform: + - [ rotate-y, 0.1745 ] + - [ rotate-x, 0.4363 ] + - [ translate, -2.8, 0, 0 ] + material: + color: [ 0.9, 0.2, 0.4 ] + ambient: 0.2 + diffuse: 0.8 + specular: 0.7 + shininess: 20 + +- add: wacky + transform: + - [ rotate-y, 0.1745 ] + material: + color: [ 0.2, 0.9, 0.6 ] + ambient: 0.2 + diffuse: 0.8 + specular: 0.7 + shininess: 20 + +- add: wacky + transform: + - [ rotate-y, -0.1745 ] + - [ rotate-x, -0.4363 ] + - [ translate, 2.8, 0, 0 ] + material: + color: [ 0.2, 0.3, 1.0 ] + ambient: 0.2 + diffuse: 0.8 + specular: 0.7 + shininess: 20 diff --git a/src/main/kotlin/shapes/Cone.kt b/src/main/kotlin/shapes/Cone.kt index 6f2d9cb..c00a7e8 100644 --- a/src/main/kotlin/shapes/Cone.kt +++ b/src/main/kotlin/shapes/Cone.kt @@ -24,6 +24,9 @@ class Cone(minimum: Number = Double.NEGATIVE_INFINITY, override fun withParent(parent: Shape?): Shape = Cone(minimum, maximum, closed, transformation, material, castsShadow, parent) + override fun withMaterial(material: Material): Shape = + Cone(minimum, maximum, closed, transformation, material, castsShadow, parent) + override fun localIntersect(rayLocal: Ray): List { val a = rayLocal.direction.x * rayLocal.direction.x - rayLocal.direction.y * rayLocal.direction.y + diff --git a/src/main/kotlin/shapes/Cube.kt b/src/main/kotlin/shapes/Cube.kt index 0a764e1..e863d3b 100644 --- a/src/main/kotlin/shapes/Cube.kt +++ b/src/main/kotlin/shapes/Cube.kt @@ -15,6 +15,9 @@ class Cube(transformation: Matrix = Matrix.I, override fun withParent(parent: Shape?): Shape = Cube(transformation, material, castsShadow, parent) + override fun withMaterial(material: Material): Shape = + Cube(transformation, material, castsShadow, parent) + override fun localIntersect(rayLocal: Ray): List = bounds.intersects(rayLocal).map { Intersection(it, this) } diff --git a/src/main/kotlin/shapes/Cylinder.kt b/src/main/kotlin/shapes/Cylinder.kt index 16d9308..4bc9701 100644 --- a/src/main/kotlin/shapes/Cylinder.kt +++ b/src/main/kotlin/shapes/Cylinder.kt @@ -22,6 +22,9 @@ class Cylinder(minimum: Number = Double.NEGATIVE_INFINITY, override fun withParent(parent: Shape?): Shape = Cylinder(minimum, maximum, closed, transformation, material, castsShadow, parent) + override fun withMaterial(material: Material): Shape = + Cylinder(minimum, maximum, closed, transformation, material, castsShadow, parent) + override fun localIntersect(rayLocal: Ray): List { val a = rayLocal.direction.x * rayLocal.direction.x + rayLocal.direction.z * rayLocal.direction.z diff --git a/src/main/kotlin/shapes/Group.kt b/src/main/kotlin/shapes/Group.kt index 4666a94..c1097a7 100644 --- a/src/main/kotlin/shapes/Group.kt +++ b/src/main/kotlin/shapes/Group.kt @@ -29,6 +29,16 @@ class Group(transformation: Matrix = Matrix.I, override fun withParent(parent: Shape?): Shape = Group(transformation, children, castsShadow, parent) + fun withTransformation(transformation: Matrix): Shape { + if (!transformation.isTransformation()) + throw IllegalArgumentException("Shapes must have 4x4 transformation matrices:\n" + + "\tShape: ${javaClass.name}\nTransformation:\n${transformation.show()}") + return Group(transformation, children, castsShadow, parent) + } + + override fun withMaterial(material: Material): Shape = + Group(transformation, children.map { it.withMaterial(material) }, castsShadow, parent) + // Only process children if the ray intersects the bounding box for this group. override fun localIntersect(rayLocal: Ray): List = if (bounds.intersects(rayLocal).isNotEmpty()) diff --git a/src/main/kotlin/shapes/Plane.kt b/src/main/kotlin/shapes/Plane.kt index 215c09b..7ea0764 100644 --- a/src/main/kotlin/shapes/Plane.kt +++ b/src/main/kotlin/shapes/Plane.kt @@ -14,6 +14,9 @@ class Plane(transformation: Matrix = Matrix.I, override fun withParent(parent: Shape?): Shape = Plane(transformation, material, castsShadow, parent) + override fun withMaterial(material: Material): Shape = + Plane(transformation, material, castsShadow, parent) + override fun localIntersect(rayLocal: Ray): List { if (rayLocal.direction.y.absoluteValue < DEFAULT_PRECISION) return emptyList() diff --git a/src/main/kotlin/shapes/Shape.kt b/src/main/kotlin/shapes/Shape.kt index deff8e8..84b22eb 100644 --- a/src/main/kotlin/shapes/Shape.kt +++ b/src/main/kotlin/shapes/Shape.kt @@ -22,6 +22,9 @@ abstract class Shape(val transformation: Matrix, // This method should only be invoked by Groups containing the object. internal abstract fun withParent(parent: Shape? = null): Shape + // This method creates a copy of the object with the specified material. + abstract fun withMaterial(material: Material): Shape + // Convert a point from world space to object space. // Later on, we use parent here. internal fun worldToLocal(tuple: Tuple): Tuple = diff --git a/src/main/kotlin/shapes/Sphere.kt b/src/main/kotlin/shapes/Sphere.kt index d68b062..51d3778 100644 --- a/src/main/kotlin/shapes/Sphere.kt +++ b/src/main/kotlin/shapes/Sphere.kt @@ -15,6 +15,9 @@ class Sphere(transformation: Matrix = Matrix.I, override fun withParent(parent: Shape?): Shape = Sphere(transformation, material, castsShadow, parent) + override fun withMaterial(material: Material): Shape = + Sphere(transformation, material, castsShadow, parent) + override fun localIntersect(rayLocal: Ray): List { val sphereToRay = rayLocal.origin - Tuple.PZERO diff --git a/src/test/kotlin/shapes/ShapeTest.kt b/src/test/kotlin/shapes/ShapeTest.kt index b7fdf59..81bd4bf 100644 --- a/src/test/kotlin/shapes/ShapeTest.kt +++ b/src/test/kotlin/shapes/ShapeTest.kt @@ -22,6 +22,12 @@ class ShapeTest { return s } + override fun withMaterial(material: Material): Shape { + val s = TestShape(transformation, material, parent) + s.savedRay = savedRay + return s + } + override fun localIntersect(rayLocal: Ray): List { savedRay = rayLocal return emptyList()