post-photo

Where we left off

In the first episode, we made a really basic (but parallel) ray tracer. It can handle spheres and use diffuse mapping. We made three types of actors, the renderer, who starts the tracing, collects the answers and saves the picture; the scene, who manages the trace requests, gets the closest object color and provides answers to the renderer, and last but not least, we have an abstract shape which can calculate the intersection with a ray, and tell its color.

In this episode we will validate our Shape-Sphere split with a new mathematical object (Plane), make some shadows, and let our objects reflect and refract.

Plane

We have a Sphere. It has only two functions: an intersect, and a getNormal. If you are good at 3D coordinate geometry, you can write the plane yourself. We need a Point, a normal vector, and we are basically done!

object Plane {
  def props(point: Point, normalVector: Vec, color: Color, scene: ActorRef)
           (implicit epsilon: Double) =
    Props(new Plane(point, normalVector, color, scene))
}
 
class Plane(point: Point, normalVector: Vec, color: Color, scene: ActorRef)(implicit epsilon: Double) extends Shape(color, scene)(epsilon) {
  override def intersect(ray: Ray): Double = {
    val d = normalVector.dot(ray.direction)
    if(d >= epsilon) {
      -1.0
    } else {
      val t = ((point dot normalVector) - (ray.startingPoint dot normalVector)) / (ray.direction dot normalVector)
 
      if(t > epsilon) t
      else -1.0
    }
  }
 
  override def getNormal(intersectPoint: Point): Vec = normalVector
}

If we add it to our Main too, we will have a nice-looking surface. I think this proves that the initial “AddShape” decision was not a waste of time, and the Shape-Sphere split was a good move, too. We wrote just the necessary shape-specific code, created the actor and the new shape has appeared.

object Main {
  def main(args: Array[String]): Unit = {
        ...
    val sphere = system.actorOf(Sphere.props(Point(-100, 200, 600), 100, Color(1, 1, 0), scene))
    val plane = system.actorOf(Plane.props(Point(0, 300, 0), Vec(0, -1, 0), Color(1, 1, 1), scene))
        ...
  }
}

text

Shadows

The picture above is nice looking, but something is missing. Something is strange. (You can get shadowless pictures in real life, too… like this one, but this is not very common.) To make it more realistic, we will need to know if there is something between the intersection point and the light. Our communication will be modifed a bit. After that, we want to know a color of a given point of a given object, the object needs to request the scene. The scene needs to tell the object where the closest intersection is from the object’s intersection point to the light (if it has any). We also need to modify our Trace request to answer with the requested information (ColorMessageAns or IntersectMessageAns). Our new communication flow is:

text

(Renderer starts a ray. The scene broadcasts it to the objects. The objects compute the intersection points and answer to the scene. The scene chooses the closest one, and asks the object, what color it has on that very point. The object need to know if it is in shadow or not, so it casts a new ray. The scene broadcasts it, the objects compute intersections and answers as before. The scene tells the object the closest intersection. The object now knows if it is in a shadow or not, compute its color and answer to the scene. The scene answers to the original request from the renderer.)

The todolist is:

If we are lucky enough, we don’t need to modify other classes.

package object part2_2 {
    ...
    case class Trace(id: String, ray: Ray, needColor: Boolean = true)
    ...
}

Add one new parameter, break nearly 0 code :) (Only the scene will break, the renderer will produce a good Trace request because of the default value.)

object Scene {
  def props(light: Light)(implicit epsilon: Double) = Props(new Scene(light))
 
  case class TraceMapItem(bestShape: ActorRef, bestIntersect: Double, counter: Int, ray: Ray, needColor: Boolean)
}
  
class Scene(light: Light)(implicit epsilon: Double) extends Actor {
    ...
    case Trace(id, ray, needColor) =>
        trace(id, ray, needColor)
    ...
  private def getIntersectAns(id: String, intersect: Double) = {
    ...
    if(counter == 0) {
      if(data.needColor) {
        val reloadedData = tracemap(id)
        getColor(id, reloadedData.bestShape, reloadedData.ray, reloadedData.bestIntersect)
        tracemap -= id
      } else {
        ansMap(id) ! IntersectMessageAns(id, tracemap(id).bestIntersect)
        ansMap -= id
        tracemap -= id
      }
    }
  }
  
  private def trace(id: String, ray: Ray, needColor: Boolean) = {
    ...
    tracemap += (id -> TraceMapItem(null, Double.MaxValue, objects.size, ray, needColor))
  }

Most of the modification above is simple parameter extension. The only logical modification is when we answer to a NoNeedColor trace with an IntersectionMessageAns.

object Shape {
 
  case class ShadowMapItem(id: String, destination: ActorRef, bestIntersect: Double, diffuseShadedColor: Color, ambientLightedColor: Color)
 
}
  
abstract class Shape(val color: Color, scene: ActorRef)(implicit epsilon: Double) extends Actor {
 
  def uuid = java.util.UUID.randomUUID.toString
  
  val shadowMap = collection.mutable.Map.empty[String, ShadowMapItem]
  
  override def receive = {
    case IntersectMessageReq(id, ray) =>
      val ret = intersect(ray)
      sender ! IntersectMessageAns(id, ret)
    case ColorMessageReq(id, ray, intersectPoint, light) =>
      getRayColor(id, ray, intersectPoint, light)
    case IntersectMessageAns(id, ret) =>
      createBaseColorAns(id, ret)
  }
  
    ...
  
  def getRayColor(id: String, ray: Ray, intersectPoint: Point, light: Light): Unit = {
    var cosTheta: Double = computeCosTheta(intersectPoint, light)
    var ret = color mult light.color mult Color(cosTheta, cosTheta, cosTheta)
    val newId = uuid
    shadowMap += newId -> ShadowMapItem(id, sender, intersectPoint distance light.place, ret, color mult light.ambient) //1b
    val ray = Ray(intersectPoint, (light.place - intersectPoint).normalize)
    scene ! Trace(newId, ray, needColor = false) //1a
  }
  //2
  def createBaseColorAns(id: String, ret: Double) = {
    val data = shadowMap(id)
    if(ret >= data.distanceToLight || ret < epsilon) {
      data.destination ! ColorMessageAns(data.originId, data.diffuseShadedColor)
    } else {
      data.destination ! ColorMessageAns(data.originId, data.ambientLightedColor)
    }
    shadowMap -= id
  }
    ...
}

Not so much modification. The getRayColor no longer answers the question, it starts a new one (//1a), and saves the context to a map (//1b). When it gets the answer, a minimal logic relays the good answer to the original question (//2). IMPORTANT: the createBaseColorAns will get as many answers as the getRayColor questions. There is no broadcast in this layer. The broadcasting and aggregating is the scene’s responsibility.

This code contains my favorite UUID implementation. I use it to generate new ids.

If we run the modified code, we will get a shadow!

text

Nice one! Our next mission is full of shining and sparking!

Reflective surfaces

This topic was one of my first really interesting type theory problems on this project.

I want to implement the reflection once, and use it on any shape. The ReflectiveSphere needs to inhere from Sphere, and also needs to inhere from Reflective. But both Reflective and Sphere need to somehow inhere from Shape. It’s like a diamond inheritance problem. I never thought this problem can ever appear in “real life” :D. I tried multiple methods, and as always, the problem was not the language’s features, but how I wanted to use them. You can use traits (basically they are forsolving these types of problems). You can tell the trait what base class you want to extend, and you can override methods on them. The key question is how you separate your methods, because you can’t really fallback to super implementations anymore. And if you are not prepared for that, you will write nasty infinite recursive calls :D.

As the shadow before, if we want to tell the requester what is our color, we need to start a new trace, and get a reflected color from the scene, too.

(Maybe with a bit better code organization, we could make the “get reflected color” and “am I in shadow” questions paralell, but I don’t bother myself with that, maybe later when I will want better performance.)

One more thing! If I start rays again and again between two reflective objects, they will ping-pong till eternity, so I will need some iteration depth measurement, and If I hit that, I will fallback to some original shape color.

I will need to write the iteration where it is needed, split some functions in the Shape, and make my brand new Reflection trait. Create two new shapes (ReflectiveSphere and ReflectivePlane) with as minimal code as possible, and try out the whole thing.

package object part2_3 {
    ...
    case class Trace(id: String, ray: Ray, iterationsLeft: Int, needColor: Boolean = true)
    ...
    case class ColorMessageReq(id: String, ray: Ray, intersectPoint: Point, light: Light, iterationsLeft: Int)
    ...
}
object ImageRender {
  def props(width: Int, height: Int, iterations: Int, camera: Camera, filename: String, scene: ActorRef): Props =
    Props(new ImageRender(width, height, iterations, camera, filename, scene))
}
  
class ImageRender(width: Int, height: Int, iterations: Int, camera: Camera, filename: String, scene: ActorRef) extends Actor {
    ...
    scene ! Trace(s"$x,$y", ray, iterations)
    ...
}
object Scene {
  def props(light: Light)(implicit epsilon: Double) = Props(new Scene(light))
 
  case class TraceMapItem(bestShape: ActorRef, bestIntersect: Double, counter: Int, ray: Ray, iterationsLeft: Int, needColor: Boolean)
}
class Scene(light: Light)(implicit epsilon: Double) extends Actor {
  
    ...
  
  override def receive = {
    ...
    case Trace(id, ray, iterationsLeft, needColor) =>
      trace(id, ray, iterationsLeft, needColor)
    ...
  }
  private def getIntersectAns(id: String, intersect: Double) = {
    ...
    getColor(id, reloadedData.bestShape, reloadedData.ray, reloadedData.bestIntersect, reloadedData.iterationsLeft)
    ...
  }
  
  private def trace(id: String, ray: Ray, iterationsLeft: Int, needColor: Boolean) = {
    ...
    tracemap += (id -> TraceMapItem(null, Double.MaxValue, objects.size, ray, iterationsLeft, needColor))
  }
  
  private def getColor(id: String, shape: ActorRef, ray: Ray, intersect: Double, iterations: Int): Unit = {
    ...
    shape ! ColorMessageReq(id, ray, intersectPoint, light, iterations)
    ...
  }
}
abstract class Shape(val color: Color, scene: ActorRef)(implicit epsilon: Double) extends Actor {
 
  def uuid = java.util.UUID.randomUUID.toString
 
  val shadowMap = collection.mutable.Map.empty[String, ShadowMapItem]
 
  override def preStart(): Unit = {
    scene ! AddShape(self)
  }
 
  override def receive = {
    case IntersectMessageReq(id, ray) =>
      val ret = intersect(ray)
      sender ! IntersectMessageAns(id, ret)
    case ColorMessageReq(id, ray, intersectPoint, light, iterationsLeft) =>
      getRayColor(id, ray, intersectPoint, light, iterationsLeft)
    case IntersectMessageAns(id, ret) =>
      createBaseColorAns(id, ret)
    case ColorMessageAns(id, ret) => //this is new
      colorMessageReceived(id, ret)
  }
 
  def intersect(ray: Ray): Double
 
  def getNormal(intersectPoint: Point): Vec
 
  def colorMessageReceived(id: String, inColor: Color): Unit = ??? //not implemented, you can override it
 
  def getRayColor(id: String, ray: Ray, intersectPoint: Point, light: Light, iterationsLeft: Int): Unit = { //def implementation
    sendBaseColor(id: String, ray: Ray, intersectPoint: Point, light: Light, iterationsLeft: Int)
  }
 
  def sendBaseColor(id: String, ray: Ray, intersectPoint: Point, light: Light, iterationsLeft: Int): Unit = { //we can use this as fallback
    var cosTheta: Double = computeCosTheta(intersectPoint, light)
    var ret = color mult light.color mult Color(cosTheta, cosTheta, cosTheta)
    computeShadow(id,intersectPoint,light,ret)
  }
 
  def computeShadow(id: String, intersectPoint: Vec, light: Light, computedColor: Color): Unit = {
    computeShadow(id,intersectPoint,light,computedColor,color mult light.ambient) //def ambient color implementation
  }
 
  def computeShadow(id: String, intersectPoint: Vec, light: Light, computedColor: Color, ambientColor: Color): Unit = { //shadow computation separated from color computation
    val newId = uuid
    shadowMap += newId -> ShadowMapItem(id, sender, intersectPoint distance light.place, computedColor, ambientColor)
    val ray = Ray(intersectPoint, (light.place - intersectPoint).normalize)
    scene ! Trace(newId, ray, 1, needColor = false)
  }
 
  def createBaseColorAns(id: String, ret: Double) = {
    val data = shadowMap(id)
    if(ret >= data.distanceToLight || ret < epsilon) {
      data.destination ! ColorMessageAns(data.originId, data.diffuseShadedColor)
    } else {
      data.destination ! ColorMessageAns(data.originId, data.ambientLightedColor)
    }
    shadowMap -= id
  }
 
  def computeCosTheta(intersectPoint: Point, light: Light) = {
    val normal = getNormal(intersectPoint)
    val rayToLight = Ray(intersectPoint + (normal * epsilon), (light.place - intersectPoint).normalize)
 
    var cosTheta = normal dot rayToLight.direction
    if(cosTheta < 0) {
      cosTheta = 0
    }
    cosTheta
  }
}

At this point, if you fix the Scene init in the Main, you can run the code again and this will produce the same output as before. So we eventually did a minor refactor/reorganize cycle. Let’s start the magic!

object Reflective {
 
  case class ReflectiveMapItem(id: String, destination: ActorRef, intersectionPoint: Point, light: Light, ray: Ray)
 
}
 
trait Reflective {
  baseShape: Shape =>
  val fr: Color
  val kappa: Color
 
  val scene: ActorRef
  val epsilon: Double
 
  val ansMap = collection.mutable.Map.empty[String, ReflectiveMapItem]
 
  override def getRayColor(id: String, ray: Ray, intersectPoint: Point, light: Light, iteration: Int): Unit = {
    if(iteration == 0) { //3b
      sendBaseColor(id, ray, intersectPoint, light, 1)
    } else { //3a
      var normal = getNormal(intersectPoint).normalize
      if((normal dot ray.direction) > 0.0){
        normal = normal * -1.0
      }
      val cosTheta2 = -1.0 * (ray.direction dot normal)
      val reflectedRay = Ray(intersectPoint + normal * epsilon, (ray.direction + normal * 2 * cosTheta2).normalize)
      val newId = uuid
      ansMap += newId -> ReflectiveMapItem(id, sender(), intersectPoint, light, ray) //5
      scene ! Trace(newId, reflectedRay, iteration - 1) //4
    }
  }
  //6
  override def colorMessageReceived(id: String, plusColor: Color): Unit = {
    val data = ansMap(id)
    var cosTheta: Double = computeCosTheta(data.intersectionPoint, data.light, data.ray)
    val fresnelVal = fresnel(cosTheta)
    val computedColor = (color + plusColor) mult fresnelVal
    ansMap -= id
    computeShadow(data.id, data.intersectionPoint, data.light, computedColor)
  }
 
  def fresnel(costheta: Double): Color = {
    Color(computeFresnelToOneColor(costheta, Color.R), computeFresnelToOneColor(costheta, Color.G), computeFresnelToOneColor(costheta, Color.B))
  }
 
  private def computeFresnelToOneColor(costheta: Double, colorComponent: Int): Double = {
    val kappaSquared = kappa(colorComponent) * kappa(colorComponent)
    val frMinus1Squared = (fr(colorComponent) - 1.0) * (fr(colorComponent) - 1.0)
    val frPlus1Squared = (fr(colorComponent) + 1.0) * (fr(colorComponent) + 1.0)
 
    val numerator = frMinus1Squared + kappaSquared + Math.pow(1.0 - costheta, 5) * (4 * fr(colorComponent))
    val denominator = frPlus1Squared + kappaSquared
     
    numerator / denominator
  }
}

As I said before, this will be a trait, and this will require a Shape. If a reflective object needs to tell its color, we need to consider whether the iteration will let us get it from the scene (3a) or just fall back (3b). If we need to compute it, we start a new Trace (4). At this point, we need to save the actual state to a map for later reuse (5), because at some point, the scene will answer us with a ColorMessageAns and we will need to know where we left off the computation (we handle this in the shape with a non-implemented function, we will need to override that). When we get the color answer, we do some color adjustment and start the shadow computing (6). That’s all. (The two monster functions at the end of the class are just some mathematical approximations to the actual Fresnel equations (I copied it from some C++ code in the past and I have no source to it :( )) If you want to dive into the physics you can read this.

object Sphere {
  def props(origo: Point, radius: Double, color: Color, scene: ActorRef)(implicit epsilon: Double) = Props(new Sphere(origo, radius, color, scene))
 
  def propsWithReflective(origo: Point, radius: Double, color: Color, scene: ActorRef)(fr: Color, kappa: Color)(implicit epsilon: Double) = Props(new ReflectiveSphere(origo, radius, color, scene)(fr, kappa))
}
  
class ReflectiveSphere(origo: Point, radius: Double, color: Color, val scene: ActorRef)(val fr: Color, val kappa: Color)(implicit val epsilon: Double)
  extends Sphere(origo, radius, color, scene)(epsilon) with Reflective {
}

This is our ReflectiveSphere implementation. I think this is amazingly small :D

object Plane {
  def props(point: Vec, normalVector: Vec, color: Color, scene: ActorRef)(implicit epsilon: Double) = Props(new Plane(point, normalVector, color, scene))
 
  def propsWithReflective(point: Point, normalVector: Vec, color: Color, scene: ActorRef)(fr: Color, kappa: Color)(implicit epsilon: Double) = Props(new ReflectivePlane(point, normalVector, color, scene)(fr, kappa))
}
  
class ReflectivePlane(point: Point, normalVector: Vec, color: Color, val scene: ActorRef)(val fr: Color, val kappa: Color)(implicit val epsilon: Double)
  extends Plane(point, normalVector, color, scene)(epsilon) with Reflective {
}

Like the Sphere, the Plane can be reflective too! (The best would be if I could add parameters to a trait too and if I could write something like new Plane(params) with Reflective(refParams) but sadly we can’t do that…)

def main(args: Array[String]): Unit = {
    ...
  val sphere = system.actorOf(Sphere.propsWithReflective(Point(-100, -200, 600), 100, Color(0.6, 0, 0), scene)(Color(0.17, 1.5, 0.17), Color(1.8, 3.1, 0.9)))
  val sphere2 = system.actorOf(Sphere.props(Point(150, -200, 500), 100, Color(1, 1, 1), scene))
  val plane = system.actorOf(Plane.propsWithReflective(Point(0, -300, 0), Vec(0, 1, 0), Color(1, 1, 1), scene)(Color(0.17, 0.35, 1.5), Color(3.1, 2.7, 1.9)))
    ...
}

Modify our first sphere to be reflective, add a second (white) sphere, and add reflection to the plane, too. (The reflection’s values are mostly created with try/see/modify cycles. (I made up the sphere, the planes are from a book.)

text

Refractive surfaces

Like glass balls. If something is refractive, it is reflective too. So our new trait will extend reflective, and pretty much will work like that. The main difference is that we need to decide if the ray goes into the object, or it is reflected from it. But because we have written most of the code already in the reflection, our logic is nicely adoptable here too.

trait Refractive extends Reflective{
  baseShape : Shape =>
 
  var refractionMap = collection.mutable.Map.empty[String, ReflectiveMapItem]
 
  override def getRayColor(id: String, ray: Ray, intersectPoint: Point, light: Light, iteration: Int): Unit = {
    if(iteration == 0) {
      sendBaseColor(id, ray, intersectPoint, light, 1)
    } else {
      var normal = getNormal(intersectPoint).normalize
 
      var cosAlpha = normal dot ray.direction
      if(cosAlpha > 1) cosAlpha = 1
      if(cosAlpha < -1) cosAlpha = -1
      var refN = 1.0/fr.red
 
      if(cosAlpha < 0) {
        cosAlpha = cosAlpha * -1
      } else {
        refN = 1.0 / refN
        normal = normal * -1.0
      }
      val disc = 1.0 - ((1.0 - cosAlpha * cosAlpha) * refN * refN)
      if(disc > 0) {
        val reflectedRay = Ray(intersectPoint, (ray.direction * refN + normal * (cosAlpha * refN - Math.sqrt(disc))).normalize)
        val newId = uuid
        refractionMap += (newId -> ReflectiveMapItem(id, sender(), intersectPoint, light, ray))
        scene ! Trace(newId, reflectedRay, iteration - 1)
      } else {
        super.getRayColor(id, ray, intersectPoint, light, iteration)
      }
    }
  }
 
  override def colorMessageReceived(id: String, computedColor: Color): Unit = {
    if(refractionMap.get(id).isDefined) {
      val data = refractionMap(id)
      refractionMap -= id
      sender ! ColorMessageAns(data.id, computedColor)
    } else {
      super.colorMessageReceived(id,computedColor)
    }
  }
}

Most of the time we could safely fallback to the super functions. It would be nicer in the long run if we could separate the mathematical code and the communication+state-persistence code more, but at this point they rely on each other too much :( (NOTE: we don’t compute shadow if we refract.)

I made a small mistake in the first post… If you look carefully, our shape can only intersect with a ray that is started from outside of the sphere. At this point, I need to fix this silly mistake and calculate the intersection point and the normal’s coherent. Need to fix the Shape and Sphere too:

object Shape {
  ...
  case class ShadowMapItem(originId: String, destination: ActorRef, distanceToLight: Double, diffuseShadedColor: Color, ambientLightedColor: Color, intersectPoint: Point, ray: Ray)
}
 
abstract class Shape(val color: Color, scene: ActorRef)(implicit epsilon: Double) extends Actor {
  ...
  def sendBaseColor(id: String, ray: Ray, intersectPoint: Point, light: Light, iterationsLeft: Int): Unit = {
    var cosTheta: Double = computeCosTheta(intersectPoint, light, ray)
  ...
  def computeShadow(id: String, intersectPoint: Point, light: Light, computedColor: Color, ambientColor: Color): Unit = {
    val newId = uuid
    val ray = Ray(intersectPoint, (light.place - intersectPoint).normalize)
    shadowMap += newId -> ShadowMapItem(id, sender, intersectPoint distance light.place, computedColor, ambientColor, intersectPoint, ray)
    scene ! Trace(newId, ray, 1, needColor = false)
  }
  ...
  def computeCosTheta(intersectPoint: Point, light: Light, ray: Ray) = {
    var normal = getNormal(intersectPoint).normalize
    if((normal dot ray.direction) > 0.0) {
      normal = normal * -1
    }
    val rayToLight = Ray(intersectPoint + (normal * epsilon), (light.place - intersectPoint).normalize)
  ...
object Sphere {
    ...
  def propsWithRefractive(origo: Point, radius: Double, color: Color, scene: ActorRef)(fr: Color, kappa: Color)(implicit epsilon: Double) = Props(new RefractiveSphere(origo, radius, color, scene)(fr, kappa))
}
class RefractiveSphere(origo: Point, radius: Double, color: Color, val scene: ActorRef)(val fr: Color, val kappa: Color)(implicit val epsilon: Double)
  extends Sphere(origo, radius, color, scene)(epsilon) with Refractive {
}
class Sphere(origo: Point, radius: Double, color: Color, scene: ActorRef)(implicit epsilon: Double) extends Shape(color, scene)(epsilon) {
 
  override def intersect(ray: Ray): Double = {
    ...
    if(d < 0) {
      -1.0
    } else {
      val t1 = (-1.0 * b - Math.sqrt(d)) / (2.0 * a)
      val t2 = (-1.0 * b + Math.sqrt(d)) / (2.0 * a)
      if(t1 > epsilon) {
        t1
      } else if(t2 > epsilon) {
        t2
      } else {
        -1.0
      }
    }
  }

Add it to the main too.

object Main {
  def main(args: Array[String]): Unit = {
    ...
    val sphere3 = system.actorOf(Sphere.propsWithRefractive(Point(25, -200, 325), 100, Color(0, 0, 0), scene)(Color(1.13, 1.13, 1.13), Color(1.0, 1.0, 1.0)))
    ...
  }
}

We add the Refractive option to the sphere and add a new sphere in the Main (add after the spere2 or at least before the renderer ! “start” ).

![text](http://wanari-web-backend.wanari.net /uploads/72b5062abab74daa8eba5cb6b3f28e06.png)

Summary

We extended our previous code with a new object, and some new fancy features, such as shadows and reflection. The generated image looks really nice!

The next post will try to clusterize these methods using the Akka clusters. Stay tuned and follow us on Facebook to see more of what we do!

Any questions are welcome in the comment section. If you want to see the code altogether, check out the GitHub Akka Ray Tracer Repo.

member photo

His precision is only coupled by his attention to detail. (Really.) He is passionate about becoming more and more effective in software development and loves experimenting with new technologies.

Latest post by Gergő Törcsvári

Learning by Doing – BlockChain & Akka Tutorial