Skip to content

Commit

Permalink
Work in progress.
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldoo committed Feb 18, 2015
1 parent 0e66099 commit ad14d25
Show file tree
Hide file tree
Showing 16 changed files with 87 additions and 40 deletions.
10 changes: 7 additions & 3 deletions timetrace/README.md
Expand Up @@ -7,13 +7,17 @@ Test: sbt test
Style: sbt scalastyle-generate-config / sbt scalastyle


PLAN:

Will start by doing all lighting (even direct) from photon map
Raytrace out from camera will find first hit, then query PM
Later extend this to recurse only down the direct (straight) path, in case of participating media


TODO:
* Rendering a sequence of frames
* Shadowing
* Participating media
* Global illumination
* Specular surfaces
* Participating media
* Movable surfaces
* Movable lights
* Movable camera
Expand Up @@ -3,9 +3,10 @@ package timetrace
import timetrace.math.Vector4
import timetrace.material.Material
import timetrace.shape.ShapeHit
import timetrace.math.RayLike

sealed case class RayHit(
val ray: Ray,
sealed class Hit[R <: RayLike](
val ray: R,
val shapeHit: ShapeHit,
val material: Material) {
}
10 changes: 4 additions & 6 deletions timetrace/src/main/scala/timetrace/Ray.scala
Expand Up @@ -2,12 +2,10 @@ package timetrace

import timetrace.math.Vector3
import timetrace.math.Vector4
import timetrace.math.RayLike

sealed case class Ray(val start: Vector4, val direction: Vector4.SpatiallyNormalized) {
class Ray(val location: Vector4, val direction: Vector4.SpatiallyNormalized) extends RayLike {
assume(direction.t == -1.0) // Photons only travel backward in time

override def toString: String = s"Ray($start -> $direction)"

def march(v: Double): Vector4 = {
start + direction * v
}
override def toString: String = s"Ray($location -> $direction)"
}
26 changes: 22 additions & 4 deletions timetrace/src/main/scala/timetrace/Raytrace.scala
Expand Up @@ -4,26 +4,28 @@ import timetrace.light.Light
import timetrace.math.Vector3
import timetrace.math.MathUtils._
import timetrace.math.Vector4
import timetrace.math.RayLike
import timetrace.photon.Photon

class Raytrace(val scene: Scene) {

def raytrace(ray: Ray): Color = {
assert(ray.direction.t == -1.0)

val hit: Option[RayHit] = firstHit(ray)
val hit: Option[Hit[Ray]] = firstHit(ray)
hit.map(calculateDirectLighting _).getOrElse(Color.BLACK)

}

def firstHit(ray: Ray): Option[RayHit] = {
def pickClosest(a: RayHit, b: RayHit) = {
def firstHit[R <: RayLike](ray: R): Option[Hit[R]] = {
def pickClosest(a: Hit[R], b: Hit[R]): Hit[R] = {
if (a.shapeHit.t < b.shapeHit.t) a else b
}

scene.things.flatMap(_.intersect(ray)).reduceOption(pickClosest _)
}

def calculateDirectLighting(hit: RayHit): Color = {
def calculateDirectLighting(hit: Hit[Ray]): Color = {

def contributionFromLight(light: Light): Color = {
val hitLocation: Vector4 = hit.ray.march(hit.shapeHit.t)
Expand All @@ -41,4 +43,20 @@ class Raytrace(val scene: Scene) {
scene.lights.map(contributionFromLight _).reduce(_ + _)
}

def generatePhotons(): List[Photon] = {
assume(scene.lights.size == 1)

val light = scene.lights(0)

val photon: Photon = light.emitPhoton

val hit: Option[Hit[Photon]] = firstHit(photon)

hit.map(ph => {
val hitLocation = ph.ray.march(ph.shapeHit.t)

new Photon(hitLocation, ph.ray.direction, ph.ray.color)
}).toList
}

}
16 changes: 10 additions & 6 deletions timetrace/src/main/scala/timetrace/Renderer.scala
Expand Up @@ -22,11 +22,13 @@ import timetrace.material.WhiteDiffuseMaterial

object Renderer {

private val PHOTON_SCATTERING_PARTITIONS = 1000

def main(args: Array[String]): Unit = {
val camera: Camera = DefaultStillCamera
val scene = new Scene( //
List(Thing(new Plane(Vector3(0.0, 1.0, 0.0).normalize(), -1.0), WhiteDiffuseMaterial)), //
List(new SinglePulsePointLight(Vector3(0.0, 1.0, 0.0), Color.WHITE, 1.0)))
List(new SinglePulsePointLight(Vector3(0.0, 1.0, 0.0), Color.WHITE, 0.0, 1.0)))

val downscale = 4

Expand All @@ -40,7 +42,9 @@ object Renderer {
val sparkContext = new SparkContext(sparkConf)

try {
val photons: RDD[Photon] = sparkContext.parallelize(1 to 1000, 1000).flatMap(generatePhotonBatch(job))
val photons: RDD[Photon] = sparkContext //
.parallelize(1 to PHOTON_SCATTERING_PARTITIONS, PHOTON_SCATTERING_PARTITIONS) //
.flatMap(generatePhotonBatch(job))

val photonMap: Broadcast[PhotonMap] = sparkContext.broadcast(buildPhotonMap(photons.collect))

Expand Down Expand Up @@ -95,11 +99,11 @@ object Renderer {
}

def generatePhotonBatch(job: RenderJob)(n: Int): Seq[Photon] = {
Iterator.continually(generatePhotonsFromSingleEmission(job)).flatten.take(job.photonCount / 1000).toSeq
}
val raytracer: Raytrace = new Raytrace(job.scene)

val photonsToGenerate = job.photonCount / PHOTON_SCATTERING_PARTITIONS

def generatePhotonsFromSingleEmission(job: RenderJob): Seq[Photon] = {
List(Photon(null, null, null))
Iterator.continually(raytracer.generatePhotons()).flatten.take(photonsToGenerate).toSeq
}

def convertToImageFile(job: RenderJob)(frame: Frame): (Int, Array[Byte]) = {
Expand Down
7 changes: 4 additions & 3 deletions timetrace/src/main/scala/timetrace/Thing.scala
Expand Up @@ -2,10 +2,11 @@ package timetrace

import timetrace.shape.Shape
import timetrace.material.Material
import timetrace.math.RayLike

sealed case class Thing(val shape: Shape, val material: Material) {

def intersect(ray: Ray): Option[RayHit] = {
shape.intersect(ray).map(sh => RayHit(ray, sh, material))
def intersect[R <: RayLike](ray: R): Option[Hit[R]] = {
shape.intersect(ray).map(sh => new Hit[R](ray, sh, material))
}
}
}
3 changes: 3 additions & 0 deletions timetrace/src/main/scala/timetrace/light/Light.scala
Expand Up @@ -2,8 +2,11 @@ package timetrace.light

import timetrace.math.Vector3
import timetrace.Color
import timetrace.photon.Photon

trait Light extends Serializable {
val location: Vector3
def colorAtTime(t: Double): Color

def emitPhoton: Photon
}
Expand Up @@ -2,13 +2,17 @@ package timetrace.light

import timetrace.Color
import timetrace.math.Vector3
import scala.collection.immutable.NumericRange

class SinglePulsePointLight(val location: Vector3, val color: Color, val pulseDuration: Double) extends Light {
class SinglePulsePointLight(val location: Vector3, val color: Color, val minT: Double, val maxT: Double) extends Light {
def colorAtTime(t: Double): Color = {
if (0.0 <= t && t < pulseDuration) {

if (minT <= t && t < maxT) {
color
} else {
Color.BLACK
}
}

def emitPhoton = ???
}
Expand Up @@ -5,4 +5,6 @@ import timetrace.math.Vector3

class StaticPointLight(val location: Vector3, val color: Color) extends Light {
def colorAtTime(t: Double) = color

def emitPhoton = ???
}
11 changes: 11 additions & 0 deletions timetrace/src/main/scala/timetrace/math/RayLike.scala
@@ -0,0 +1,11 @@
package timetrace.math

trait RayLike {
def location(): Vector4

def direction(): Vector4.SpatiallyNormalized

def march(v: Double): Vector4 = {
location + direction * v
}
}
8 changes: 5 additions & 3 deletions timetrace/src/main/scala/timetrace/photon/Photon.scala
Expand Up @@ -3,7 +3,9 @@ package timetrace.photon
import timetrace.Color
import timetrace.math.Vector3
import timetrace.math.Vector4
import timetrace.math.RayLike

case class Photon(val location: Vector4, val direction: Vector3.Normalized, val color: Color) {

}
/// Represents light from a light source as it is inbound on an interacting thing
case class Photon(val location: Vector4, val direction: Vector4.SpatiallyNormalized, val color: Color) extends RayLike {
assume(direction.t == 1.0) // Photons only travel forward in time
}
6 changes: 3 additions & 3 deletions timetrace/src/main/scala/timetrace/shape/Plane.scala
Expand Up @@ -2,8 +2,8 @@ package timetrace.shape

import timetrace.math.Vector4
import timetrace.Ray
import timetrace.RayHit
import timetrace.math.Vector3
import timetrace.math.RayLike

/**
* Set of points x, satisfying x dot normal == offset
Expand All @@ -12,8 +12,8 @@ class Plane(val normal: Vector3.Normalized, val offset: Double) extends Shape {

override def toString = s"Plane($normal, $offset)"

def intersect(ray: Ray): Option[ShapeHit] = {
val t = (offset - (ray.start.truncateTo3 dot normal)) / (ray.direction.truncateTo3() dot normal)
def intersect(ray: RayLike): Option[ShapeHit] = {
val t = (offset - (ray.location.truncateTo3 dot normal)) / (ray.direction.truncateTo3() dot normal)

if (t > 0.0 && t < Double.PositiveInfinity)
Some(new ShapeHit(t, normal))
Expand Down
4 changes: 2 additions & 2 deletions timetrace/src/main/scala/timetrace/shape/Shape.scala
@@ -1,10 +1,10 @@
package timetrace.shape

import timetrace.Ray
import timetrace.RayHit
import timetrace.math.Vector4
import timetrace.math.RayLike

trait Shape extends Serializable {

def intersect(ray: Ray): Option[ShapeHit]
def intersect(ray: RayLike): Option[ShapeHit]
}
4 changes: 2 additions & 2 deletions timetrace/src/test/scala/timetrace/RayTest.scala
Expand Up @@ -22,14 +22,14 @@ class RayTest extends UnitSpec {
{
val v = new Ray(start, direction)

v.start should equal(start)
v.location should equal(start)
v.direction should equal(direction)
}
}
}

it should "have a nice toString" in {
val ray = Ray(
val ray = new Ray(
Vector4(1.0, 2.0, 3.0, 4.0),
Vector4(0.0, 1.0, 0.0, -1.0).spatiallyNormalize())
ray.toString() should equal("Ray([1.0, 2.0, 3.0, 4.0] -> [0.0, 1.0, 0.0, -1.0])")
Expand Down
2 changes: 1 addition & 1 deletion timetrace/src/test/scala/timetrace/RaytraceTest.scala
Expand Up @@ -24,7 +24,7 @@ class RaytraceTest extends UnitSpec with ColorMatchers {
List(Thing(new Plane(Vector3(0.0, 0.0, 1.0).normalize, 0.0), WhiteDiffuseMaterial)),
List.empty)
val ray = new Ray(new Vector4(0.0, 0.0, 1.0, 0.0), new Vector4(0.0, 0.0, -1.0, -1.0).spatiallyNormalize())
val hit: Option[RayHit] = new Raytrace(scene).firstHit(ray)
val hit: Option[Hit[Ray]] = new Raytrace(scene).firstHit(ray)

hit should be('defined)
}
Expand Down
5 changes: 2 additions & 3 deletions timetrace/src/test/scala/timetrace/shape/PlaneTest.scala
Expand Up @@ -8,7 +8,6 @@ import org.scalacheck.Gen
import timetrace.math.Vector4Test
import timetrace.RayTest
import timetrace.Ray
import timetrace.RayHit
import timetrace.Generators
import timetrace.RayTest
import timetrace.math.Vector3
Expand All @@ -28,7 +27,7 @@ class PlaneTest extends UnitSpec {
forAll(PlaneTest.planes, RayTest.rays) {
(plane: Plane, ray: Ray) =>
{
val currentSide = Math.signum((ray.start.truncateTo3 dot plane.normal) - plane.offset)
val currentSide = Math.signum((ray.location.truncateTo3 dot plane.normal) - plane.offset)
val eventualSide = Math.signum(ray.direction.truncateTo3() dot plane.normal)

val rayHit: Option[ShapeHit] = plane.intersect(ray)
Expand All @@ -45,7 +44,7 @@ class PlaneTest extends UnitSpec {

it should "fail to intersect when rays are parallel" in {
val plane = new Plane(Vector3(1.0, 0.0, 0.0).normalize, 0.0)
val ray = Ray(Vector4(1.0, 0.0, 0.0, 0.0), Vector4(0.0, 1.0, 0.0, 1.0).spatiallyNormalize())
val ray = new Ray(Vector4(1.0, 0.0, 0.0, 0.0), Vector4(0.0, 1.0, 0.0, 1.0).spatiallyNormalize())

val rayHit = plane.intersect(ray)
}
Expand Down

0 comments on commit ad14d25

Please sign in to comment.