Skip to content

Commit

Permalink
Chapter 15 completed.
Browse files Browse the repository at this point in the history
  • Loading branch information
sraaphorst committed Jan 26, 2023
1 parent 4cc5447 commit e6ab79f
Show file tree
Hide file tree
Showing 26 changed files with 368,731 additions and 123 deletions.
20 changes: 20 additions & 0 deletions output/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The Ray Tracer Challenge Output Images

- These are the the images that are generated at the end of their respective chapters at the end of the Ray Tracer
Challenge and converted to PNG.

- **Be aware** that you look at the following images (depicted for artistic purposes only) at your own discretion.


- In particular, the file `ch15_world2.png` depicts ray tracing of male genitalia, and should not
be viewed by anyone who is not prepared to see such a thing. This is the rendering an open source male genitalia file
with:
- 49,156 points, each with a surface normal; amd
- 49,152 quadrilaterals (which are translated to 98,304 triangles).


- This is to demonstrate the functionality of the `SmoothTriangle` class amd the
ability of `KDTree`s to render large files quickly.

- This file is deemed to pass: [GitHub Sexually Obscene Content](https://docs.github.com/en/site-policy/acceptable-use-policies/github-sexually-obscene-content)
as is is not pornographic; consensual; and has no graphic depictions of sexual acts.
Binary file removed output/ch15_world1.png
Binary file not shown.
Binary file added output/ch15_world2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions src/main/kotlin/apps/ch15_world2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package apps

import input.OBJParser
import light.PointLight
import material.Material
import math.Color
import math.Matrix
import math.Tuple
import pattern.CheckerPattern
import scene.Camera
import scene.World
import shapes.Cube
import java.io.File
import kotlin.math.PI
import kotlin.system.measureTimeMillis

fun main() {
// 49.156 points.
// 49,152 quadrilaterals -> 98,304 triangles.
val modelStart = System.currentTimeMillis()
val model = OBJParser.fromURL({}.javaClass.getResource("/flaccid.obj"))
.groups.getValue(OBJParser.DefaultGroup)

val model1 = run {
val t = Matrix.translate(0, 0, -5) * Matrix.rotateY(4 * PI / 5) *
Matrix.translate(0, 3, 0) * Matrix.scale(0.05, 0.05,0.05)
val m = Material(Color.fromHex(0xffe5b2), specular = 0, shininess = 36, transparency = 0)
model.withTransformation(t).withMaterial(m)
}
println("Time elapsed (processing model): ${(System.currentTimeMillis() - modelStart) / 1000.0} s")

val room = run {
val m = Material(CheckerPattern(Color(0.25, 0.25, 0.25), Color(0.75, 0.75, 0.75),
Matrix.scale(0.25, 0.25, 0.25)), specular = 0)
val t = Matrix.scale(30, 30, 30)
Cube(t, m)
}

val world = run {
val light1 = PointLight(Tuple.point(-5, 10, -15), Color(0.6, 0.4, 0.4))
val light2 = PointLight(Tuple.point(8, 15, -10), Color(0.4, 0.6, 0.4))
World(listOf(room, model1), listOf(light1, light2))
}

val camera = run {
val from = Tuple.point(0, 5, -25)
val to = Tuple.point(0, 5, 0)
val up = Tuple.VY
val t = from.viewTransformationFrom(to, up)
Camera(1200, 1200, PI / 2, t)
}

val elapsed = measureTimeMillis {
val canvas = camera.render(world)
canvas.toPPMFile(File("output/ch15_world2.ppm"))
}
println("Time elapsed (rendering): ${elapsed / 1000.0} s")
}
36 changes: 30 additions & 6 deletions src/main/kotlin/input/OBJParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package input

import math.Tuple
import shapes.Group
import shapes.PlainTriangle
import shapes.SmoothTriangle
import shapes.Triangle
import java.io.*
import java.net.URI
Expand All @@ -20,8 +22,8 @@ class OBJParser(reader: Reader) {
val failedLines: List<String>

internal val points: Map<Int, Tuple>
private val textureCoordinates: List<TextureCoordinate>
private val normals: List<Tuple>
internal val textureCoordinates: Map<Int, TextureCoordinate>
internal val normals: Map<Int, Tuple>
private val faces: List<Face>

init {
Expand Down Expand Up @@ -148,9 +150,9 @@ class OBJParser(reader: Reader) {
}

// Advance the points by 1 since we begin at 1.
this.points = points.withIndex().associate { (idx, v) -> ((idx + 1) to v) }
this.textureCoordinates = textureCoordinates.toList()
this.normals = normals.toList()
this.points = incrementer(points)
this.textureCoordinates = incrementer(textureCoordinates)
this.normals = incrementer(normals)
this.faces = faces.toList()
this.failedLines = failedLines.toList()

Expand All @@ -163,11 +165,28 @@ class OBJParser(reader: Reader) {
private fun makeFace(face: Face): List<Triangle> {
// Retrieve the points themselves.
val points = face.pointIndices.map { points.getValue(it) }
val normals = face.normalIndices.map { normals.getValue(it) }

if (normals.isNotEmpty() && normals.size != points.size)
throw IllegalArgumentException("Illegal triangle face either needs normals at each point, or none at all.")

// Create the fan pattern of (v1, v2, v3), (v1, v3, v4), etc.
val p1 = points.first()
val remainingPoints = points.drop(1)
return remainingPoints.zipWithNext().map { (p2, p3) -> Triangle(p1, p2, p3) }

// If we have no normals, we create a PlainTriangle.
// Otherwise, we create a SmoothTriangle.
return if (normals.isEmpty())
remainingPoints.zipWithNext().map { (p2, p3) -> PlainTriangle(p1, p2, p3) }
else {
val n1 = normals.first()
val remainingNormals = normals.drop(1)
remainingPoints.zip(remainingNormals).zipWithNext().map { (pair2, pair3) ->
val (p2, n2) = pair2
val (p3, n3) = pair3
SmoothTriangle(p1, p2, p3, n1, n2, n3)
}
}
}

companion object {
Expand All @@ -190,5 +209,10 @@ class OBJParser(reader: Reader) {

fun fromURI(uri: URI) =
fromURL(uri.toURL())

// Since WaveFront OBJ files are 1-indexed, instead of having a list, for simplicity,
// we use a map where things are indexed properly. This converts a list to such a map.
private fun <T> incrementer(lst: List<T>): Map<Int, T> =
lst.withIndex().associate { (idx, elem) -> (idx + 1) to elem }
}
}
57 changes: 38 additions & 19 deletions src/main/kotlin/math/Intersection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,24 @@ package math
// By Sebastian Raaphorst, 2023.

import shapes.Shape
import shapes.Sphere

// Note that u and v are only used with SmoothTriangle.
// They represent a location on a triangle relative to its corners.
internal data class Intersection(
val t: Double,
val shape: Shape,
val uv: Pair<Double, Double>? = null) {
constructor(t: Number, shape: Shape, uv: Pair<Number, Number>? = null):
this(t.toDouble(), shape,
uv?.let { Pair(uv.first.toDouble(), uv.second.toDouble()) } )

init {
if (uv != null && (uv.first < 0 || uv.first > 1 || uv.second < 0 || uv.second > 1))
throw IllegalArgumentException("Illegal u/v value for smooth triangle: $shape has u=${uv}.")
}

internal data class Intersection(val t: Double, val shape: Shape) {
constructor(t: Number, shape: Shape):
this(t.toDouble(), shape)

// The hit is this, as calculated in World::colorAt, which is the only function that calls this.
fun computations(ray: Ray, xs: List<Intersection> = listOf(this)): Computations {
// The values returned correspond to n1 and n2.
tailrec fun calculateNs(xsRemain: List<Intersection> = xs,
Expand Down Expand Up @@ -50,28 +63,34 @@ internal data class Intersection(val t: Double, val shape: Shape) {

val point = ray.position(t)
val eyeV = -ray.direction
val normalV = shape.normalAt(point)
val normalV = shape.normalAt(point, this)
val inside = normalV.dot(eyeV) < 0
val adjNormalV = if (inside) -normalV else normalV
val reflectV = ray.direction.reflect(adjNormalV)
val (n1, n2) = calculateNs()
return Computations(t, shape, point, eyeV, adjNormalV, reflectV, inside, n1, n2)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Intersection) return false

if (!almostEquals(t, other.t)) return false
if (shape != other.shape) return false

return true
}

override fun hashCode(): Int {
var result = t.hashCode()
result = 31 * result + shape.hashCode()
return result
// override fun equals(other: Any?): Boolean {
// if (this === other) return true
// if (other !is Intersection) return false
//
// if (!almostEquals(t, other.t)) return false
// if (shape != other.shape) return false
//
// return true
// }
//
// override fun hashCode(): Int {
// var result = t.hashCode()
// result = 31 * result + shape.hashCode()
// return result
// }
companion object {
// This is to simplify calls to localNormalAt in test cases.
// They accept the DummyIntersection for the hit parameter since they do not rely on its value unless
// they are a SmoothTriangle.
internal val DummyIntersection = Intersection(0, Sphere())
}
}

Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/scene/World.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ data class World(val shapes: List<Shape>, val lights: List<Light>) {
// It connects the other functions together, which would be private if not for test cases.
internal fun colorAt(r: Ray, remaining: Int = DEFAULT_REMAINING): Color {
val xs = intersect(r)
val hit = xs.hit()
return xs.hit()?.computations(r, xs)?.let { shadeHit(it, remaining) } ?: Color.BLACK
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/shapes/Cone.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Cone(minimum: Number = Double.NEGATIVE_INFINITY,
return xs1 + xs2
}

override fun localNormalAt(localPoint: Tuple): Tuple {
override fun localNormalAt(localPoint: Tuple, hit: Intersection): Tuple {
val dist = localPoint.x * localPoint.x + localPoint.z * localPoint.z

// Check first if we are at the caps if they apply, and otherwise if we are on the body.
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/shapes/Cube.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Cube(transformation: Matrix = Matrix.I,
override fun localIntersect(rayLocal: Ray): List<Intersection> =
bounds.intersects(rayLocal).map { Intersection(it, this) }

override fun localNormalAt(localPoint: Tuple): Tuple {
override fun localNormalAt(localPoint: Tuple, hit: Intersection): Tuple {
val posX = localPoint.x.absoluteValue
val posY = localPoint.y.absoluteValue
val posZ = localPoint.z.absoluteValue
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/shapes/Cylinder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Cylinder(minimum: Number = Double.NEGATIVE_INFINITY,
return xs1 + xs2
}

override fun localNormalAt(localPoint: Tuple): Tuple {
override fun localNormalAt(localPoint: Tuple, hit: Intersection): Tuple {
val dist = localPoint.x * localPoint.x + localPoint.z * localPoint.z

// Check first if we are at the caps if they apply, and otherwise if we are on the body.
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/shapes/Group.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class Group(
fun localIntersect(rayLocal: Ray): List<Intersection> =
kdTree?.localIntersect(rayLocal) ?: localIntersectAll(rayLocal)

override fun localNormalAt(localPoint: Tuple): Tuple =
override fun localNormalAt(localPoint: Tuple, hit: Intersection): Tuple =
throw NotImplementedError("Groups do not have local normals.")

override val bounds: BoundingBox by lazy {
Expand Down
36 changes: 36 additions & 0 deletions src/main/kotlin/shapes/PlainTriangle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package shapes

// By Sebastian Raaphorst, 2023.

import material.Material
import math.Intersection
import math.Matrix
import math.Tuple

class PlainTriangle(
p1: Tuple,
p2: Tuple,
p3: Tuple,
transformation: Matrix = Matrix.I,
material: Material? = null,
castsShadow: Boolean = true,
parent: Shape? = null
): Triangle(p1, p2, p3, transformation, material, castsShadow, parent) {

// Calculate the normal.
internal val normal = e2.cross(e1).normalized

override fun withParent(parent: Shape?): Shape =
PlainTriangle(p1, p2, p3, transformation, material, castsShadow, parent)

override fun withMaterial(material: Material): Shape =
PlainTriangle(p1, p2, p3, transformation, material, castsShadow, parent)

// For plain triangles, we ignore u and v.
override fun createIntersection(t: Double, uv: Pair<Double, Double>?): Intersection =
Intersection(t, this)

// Note that this will still return a normal if the point is not on the triangle.
override fun localNormalAt(localPoint: Tuple, hit: Intersection): Tuple =
normal
}
2 changes: 1 addition & 1 deletion src/main/kotlin/shapes/Plane.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Plane(transformation: Matrix = Matrix.I,
return listOf(Intersection(t, this))
}

override fun localNormalAt(localPoint: Tuple): Tuple =
override fun localNormalAt(localPoint: Tuple, hit: Intersection): Tuple =
Tuple.VY

override val bounds: BoundingBox by lazy {
Expand Down
9 changes: 6 additions & 3 deletions src/main/kotlin/shapes/Shape.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,24 @@ abstract class Shape(val transformation: Matrix,
// normalAt transforms the point from world to object (local) space and passes it to localNormalAt
// which should comprise the concrete implementation of calculating the normal vector
// at the point for the Shape. Then normalAt transforms it back into world space.
internal fun normalAt(worldPoint: Tuple): Tuple {
// We pass the hit to normalAt because it is needed by SmoothTriangle to calculate the interpolated normal.
// To make test cases pass, give a default value of DummyIntersection.
internal fun normalAt(worldPoint: Tuple, hit: Intersection = Intersection.DummyIntersection): Tuple {
if (!worldPoint.isPoint())
throw IllegalArgumentException("Shape::normalAt requires a point: $worldPoint")

// Convert to object space.
val localPoint = worldToLocal(worldPoint)
val localNormal = localNormalAt(localPoint)
val localNormal = localNormalAt(localPoint, hit)

// Convert back to world space.
return normalToWorld(localNormal)
}

// Normal at a point in object (local) space.
// Normal should be returned in local space, and normalAt handles transforming it back to world space.
internal abstract fun localNormalAt(localPoint: Tuple): Tuple
// We pas the hit because it is used in SmoothTriangles to calculate the interpolated normal.
internal abstract fun localNormalAt(localPoint: Tuple, hit: Intersection = Intersection.DummyIntersection): Tuple

// Untransformed bounds for each Shape type.
internal abstract val bounds: BoundingBox
Expand Down
41 changes: 41 additions & 0 deletions src/main/kotlin/shapes/SmoothTriangle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package shapes

// By Sebastian Raaphorst, 2023.

import material.Material
import math.Intersection
import math.Matrix
import math.Tuple

class SmoothTriangle(
p1: Tuple,
p2: Tuple,
p3: Tuple,
internal val n1: Tuple,
internal val n2: Tuple,
internal val n3: Tuple,
transformation: Matrix = Matrix.I,
material: Material? = null,
castsShadow: Boolean = true,
parent: Shape? = null
): Triangle(p1, p2, p3, transformation, material, castsShadow, parent) {
init {
if (!n1.isVector() || !n2.isVector() || !n3.isVector())
throw IllegalArgumentException("Smooth triangle requires three vector normals: $n1, $n2, $n3.")
}

override fun withParent(parent: Shape?): Shape =
SmoothTriangle(p1, p2, p3, n1, n2, n3, transformation, material, castsShadow, parent)

override fun withMaterial(material: Material): Shape =
SmoothTriangle(p1, p2, p3, n1, n2, n3, transformation, material, castsShadow, parent)

override fun localNormalAt(localPoint: Tuple, hit: Intersection): Tuple {
val uv = hit.uv ?: throw IllegalArgumentException("SmoothTriangle received hit with u/v=null.")
val (u, v) = uv
return n2 * u + n3 * v + n1 * (1 - u - v)
}

override fun createIntersection(t: Double, uv: Pair<Double, Double>?): Intersection =
Intersection(t, this, uv)
}
Loading

0 comments on commit e6ab79f

Please sign in to comment.