diff --git a/timetrace/README.md b/timetrace/README.md index 04cc7943..87bc1d19 100644 --- a/timetrace/README.md +++ b/timetrace/README.md @@ -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 diff --git a/timetrace/src/main/scala/timetrace/RayHit.scala b/timetrace/src/main/scala/timetrace/Hit.scala similarity index 69% rename from timetrace/src/main/scala/timetrace/RayHit.scala rename to timetrace/src/main/scala/timetrace/Hit.scala index 979f31db..78f25a1e 100644 --- a/timetrace/src/main/scala/timetrace/RayHit.scala +++ b/timetrace/src/main/scala/timetrace/Hit.scala @@ -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) { } diff --git a/timetrace/src/main/scala/timetrace/Ray.scala b/timetrace/src/main/scala/timetrace/Ray.scala index 1c901a5c..c9360d4d 100644 --- a/timetrace/src/main/scala/timetrace/Ray.scala +++ b/timetrace/src/main/scala/timetrace/Ray.scala @@ -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)" } diff --git a/timetrace/src/main/scala/timetrace/Raytrace.scala b/timetrace/src/main/scala/timetrace/Raytrace.scala index bdd38a33..865b2ea7 100644 --- a/timetrace/src/main/scala/timetrace/Raytrace.scala +++ b/timetrace/src/main/scala/timetrace/Raytrace.scala @@ -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) @@ -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 + } + } diff --git a/timetrace/src/main/scala/timetrace/Renderer.scala b/timetrace/src/main/scala/timetrace/Renderer.scala index 18e0168b..01a5380c 100644 --- a/timetrace/src/main/scala/timetrace/Renderer.scala +++ b/timetrace/src/main/scala/timetrace/Renderer.scala @@ -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 @@ -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)) @@ -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]) = { diff --git a/timetrace/src/main/scala/timetrace/Thing.scala b/timetrace/src/main/scala/timetrace/Thing.scala index 57f48f27..6f061429 100644 --- a/timetrace/src/main/scala/timetrace/Thing.scala +++ b/timetrace/src/main/scala/timetrace/Thing.scala @@ -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)) } -} \ No newline at end of file +} diff --git a/timetrace/src/main/scala/timetrace/light/Light.scala b/timetrace/src/main/scala/timetrace/light/Light.scala index c3948933..a5910ec5 100644 --- a/timetrace/src/main/scala/timetrace/light/Light.scala +++ b/timetrace/src/main/scala/timetrace/light/Light.scala @@ -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 } diff --git a/timetrace/src/main/scala/timetrace/light/SinglePulsePointLight.scala b/timetrace/src/main/scala/timetrace/light/SinglePulsePointLight.scala index b97b6864..f9d26c1d 100644 --- a/timetrace/src/main/scala/timetrace/light/SinglePulsePointLight.scala +++ b/timetrace/src/main/scala/timetrace/light/SinglePulsePointLight.scala @@ -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 = ??? } diff --git a/timetrace/src/main/scala/timetrace/light/StaticPointLight.scala b/timetrace/src/main/scala/timetrace/light/StaticPointLight.scala index 0d7fead3..db7e7c98 100644 --- a/timetrace/src/main/scala/timetrace/light/StaticPointLight.scala +++ b/timetrace/src/main/scala/timetrace/light/StaticPointLight.scala @@ -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 = ??? } diff --git a/timetrace/src/main/scala/timetrace/math/RayLike.scala b/timetrace/src/main/scala/timetrace/math/RayLike.scala new file mode 100644 index 00000000..bff68433 --- /dev/null +++ b/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 + } +} diff --git a/timetrace/src/main/scala/timetrace/photon/Photon.scala b/timetrace/src/main/scala/timetrace/photon/Photon.scala index 82c7b815..ed5b77af 100644 --- a/timetrace/src/main/scala/timetrace/photon/Photon.scala +++ b/timetrace/src/main/scala/timetrace/photon/Photon.scala @@ -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) { - -} \ No newline at end of file +/// 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 +} diff --git a/timetrace/src/main/scala/timetrace/shape/Plane.scala b/timetrace/src/main/scala/timetrace/shape/Plane.scala index 70620b25..18c4baaa 100644 --- a/timetrace/src/main/scala/timetrace/shape/Plane.scala +++ b/timetrace/src/main/scala/timetrace/shape/Plane.scala @@ -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 @@ -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)) diff --git a/timetrace/src/main/scala/timetrace/shape/Shape.scala b/timetrace/src/main/scala/timetrace/shape/Shape.scala index 1c1e8825..507d3b0a 100644 --- a/timetrace/src/main/scala/timetrace/shape/Shape.scala +++ b/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] } diff --git a/timetrace/src/test/scala/timetrace/RayTest.scala b/timetrace/src/test/scala/timetrace/RayTest.scala index 62a90e81..5629d433 100644 --- a/timetrace/src/test/scala/timetrace/RayTest.scala +++ b/timetrace/src/test/scala/timetrace/RayTest.scala @@ -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])") diff --git a/timetrace/src/test/scala/timetrace/RaytraceTest.scala b/timetrace/src/test/scala/timetrace/RaytraceTest.scala index 5499d658..6559220b 100644 --- a/timetrace/src/test/scala/timetrace/RaytraceTest.scala +++ b/timetrace/src/test/scala/timetrace/RaytraceTest.scala @@ -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) } diff --git a/timetrace/src/test/scala/timetrace/shape/PlaneTest.scala b/timetrace/src/test/scala/timetrace/shape/PlaneTest.scala index 2e5afd29..97538f27 100644 --- a/timetrace/src/test/scala/timetrace/shape/PlaneTest.scala +++ b/timetrace/src/test/scala/timetrace/shape/PlaneTest.scala @@ -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 @@ -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) @@ -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) }