post-photo

About this series

Most of the time, when you learn a new concept or a new programming language from a(n) (e-)book, you get the basics. But these are really far from the production-ready code with the new toolstack. The only good way to learn programming (like most of the other things in life) is to practice. So, because I want to be more and more confident in actor architectures, the Scala language, and Akka capabilities, I often start a half-day or a weekend toy-project just to try something out. This post (and the other ones in the series) will try to summarize my problems, and show how I solved them. If you are the type of developer who wants to see the show for it first, check out the GitHub repo, then come back to read the post.

About the topic choice

When I was in school, there was a Computer Graphics class where the students had to make four out of five (at that time really hard) assignments. The third one was always about ray-tracing, and most of the students used the skipcard for it. I think this is the most interesting part of computer graphics. (Java/Scala backend programmers are rarely heard to like computer graphics or to know how the graphic-card actually renders things. I always liked this topic, but I really hated the pain what came with shader debugging, so that’s why I ended up on backend instead of making new game engines.)

What is ray tracing?

Ray tracing (as its name says) is about tracing the path of rays based on the theory in physics of light modeled with really tiny balls. The picture creation is purely mathematical: we have light rays, that intersect objects (mostly given with mathematical formulas), we compute angles, and modify a color based on that. Most of the time we start rays from the camera and when they intersect objects we try to figure out if they are visible from the light or hidden behind other objects (a.k.a. shadow computing). When we have reflective and refractive objects in our scene we start more rays to get the intersection point’s color (because of this step this method is sometimes called “recursive ray tracing” too).

The problem with ray tracing is its computational cost. When you cast a ray, you need to compute all of the intersection points with the objects. If there are some, you need the closest one, you need minimum one different vector operation (to calculate its color). Alternatively, if you calculate shadows you need another tracing phase. Furthermore, if the intersected object is reflective/refractive you have to cast new ray(s). (With modern GPUs the picture creation is really far from this method, they are cheating at every step for speed. :D)

Wikipedia summarizes this correctly (btw the whole page is worth reading if you have never met this technique before).

It has lots of inner mathematics; vector and intersection point calculations between lines and other mathematical objects (like sphere, plane, cylinder, infinite cones and other general quadratic shapes). Also it has some not-so-easy physics too, like;

The goal of this post is not about teaching you how to do ray tracing in general, but rather about learning Akka and Scala, so most of the time I won’t go deep into the mathematical background. I will try to include some helpful links or posts if you really want to understand why that “costheta” is computed that way, but I want to focus on the architectural design.

Why is it possible with Akka?

The basic architecture in most of the (recursive) ray tracers is (1) generate all the starting rays, and (2) compute the ray color with one function call. This function is called recursively if multiple rays are cast due to reflection or shadow computations. The basic architecture’s code is paralleled along the initial rays, and serial at the intersection computations for example. My initial idea was; what if an object in our virtual space is an actor. So when I start a ray, and want to know “which object is the closest?”, instead of iterating through all of the objects and calling blocking functions, I broadcast a message to them and wait for their responses asynchronously . When I need to know a color on an intersection point, the object can start a ray instead of recursively calling a function. So with this mindset, I can separate some logic, and make async boundaries between the calculations. In theory, I will have better options for splitting the problem to clusters, and If I use pools instead of single actors, I will have better thread usage (I can compute a single ray in parallel, not just a group of them). Of course this will have a huge memory cost compared to the standard implementation, but this is still a manageable cost (not gigabytes).

Here is all the above in two neat GIFs for you:

text text

Starting point

Before anything fancy, we will need to write some essential classes. We will start with the Vec and the Color (these are going to have some logic).

case class Vec(x: Double, y: Double, z: Double) {
 
  def +(other: Vec): Vec = {
    Vec(x + other.x, y + other.y, z + other.z)
  }
 
  def -(other: Vec): Vec = {
    this + (other * -1)
  }
 
  def *(scalar: Double): Vec = {
    Vec(x * scalar, y * scalar, z * scalar)
  }
 
  def /(scalar: Double): Vec = {
    this * (1 / scalar)
  }
 
  def normalize: Vec = {
    this * (1 / length)
  }
 
  def length: Double = {
    Math.sqrt(lengthSquared)
  }
 
  def lengthSquared: Double = {
    x * x + y * y + z * z
  }
 
  def sum: Double = {
    x + y + z
  }
 
  def mult(other: Vec): Vec = {
    Vec(x * other.x, y * other.y, z * other.z)
  }
 
  def dot(other: Vec): Double = {
    (this mult other).sum
  }
 
  def distance(other: Vec): Double = {
    Math.sqrt(
      (x - other.x) * (x - other.x) +
        (y - other.y) * (y - other.y) +
        (z - other.z) * (z - other.z)
    )
  }
}

Some explanation:

case class Color(red: Double, green: Double, blue: Double) {
 
  def +(other: Color): Color = {
    Color(red + other.red, green + other.green, blue + other.blue)
  }
 
  def *(scalar: Double) {
    Color(red * scalar, green * scalar, blue * scalar)
  }
 
  def mult(other: Color): Color = {
    Color(red * other.red, green * other.green, blue * other.blue)
  }
 
  def getImageColor: Int = {
    val forced = forceToRange
    val jcolor = new java.awt.Color(forced.red.toFloat, forced.green.toFloat, forced.blue.toFloat)
    jcolor.getRGB
  }
 
  def forceToRange: Color = {
    Color(forceToRange(red), forceToRange(green), forceToRange(blue))
  }
 
  private def forceToRange(value: Double): Double = {
    if(value > 1) 1
    else if(value < 0) 0
    else value
  }
}

Our Color object will be similar to Vec:

package object part1_1 {
 
  type Point = Vec
  val Point = Vec
 
  case class Ray(startingPoint: Point, direction: Vec)
 
  case class Light(color: Color, place: Point, ambient: Color)
 
  case class Camera(place: Point)
}

Finally here is our package object with some of the case classes “wrapped up”.

I made a a case of syntactic sugar; redefining the Vec type as a Point, too. Note that if you do only the type declaration, every function parameter can be a Point instead of a Vec, but the apply method will not work. With the val+type declaration, you can change Vec to Point anywhere you want. I use it for clarifying parameter lists, but I made this refactor in a later dev cycle so there may be some Point-Vec inconsistency (but the code will compile and function anyway, this is only for the code readers ;) ).

I will use Akka’s props pattern in the companion objects.

First picture

We will begin with baby steps. Create a scene, add a Sphere to it, start and absorb rays, collect ray-colors, write out the picture to a file.

As a starting point, we write some communication protocols between our actors:

package object part1_1 {
  ...
  
  case class AddShape(obj: ActorRef)
 
  case class Trace(id: String, ray: Ray)
  
  case class IntersectMessageReq(id: String, ray: Ray)
 
  case class IntersectMessageAns(id: String, inters: Double)
  
  case class ColorMessageReq(id: String, ray: Ray, intersectPoint: Point, light: Light)
 
  case class ColorMessageAns(id: String, color: Color)
}

text

Start it with a bottom-up class creation! We will create a Sphere first (an object in the animation above), then a Scene and finally the Render. After these steps, we will be ready to create our first picture!

object Sphere {
  def props(origo: Point, radius: Double, color: Color, scene: ActorRef)(implicit epsilon: Double) = Props(new Sphere(origo, radius, color, scene))
}
 
class Sphere(origo: Point, radius: Double, color: Color, scene: ActorRef)
            (implicit epsilon: Double) extends Actor {
 
  //1
  override def preStart(): Unit = {
    scene ! AddShape(self)
  }
 
  override def receive = {
    //2a
    case IntersectMessageReq(id, ray) =>
      val ret = intersect(ray)
      sender ! IntersectMessageAns(id, ret)
    //3
    case ColorMessageReq(id, ray, intersectPoint, light) =>
      sender ! ColorMessageAns(id, color)
  }
 
  //2b
  def intersect(ray: Ray): Double = {
    val a = ray.direction.lengthSquared
    val b = 2.0 * (ray.direction mult (ray.startingPoint - origo)).sum
    val c = origo.lengthSquared + ray.startingPoint.lengthSquared - (2.0 * (origo mult ray.startingPoint).sum) - radius * radius
    val d = b * b - 4.0 * a * c
    if(d < 0) {
      return -1.0
    }
    val t = (-1.0 * b - Math.sqrt(d)) / (2.0 * a)
    if(t > epsilon) {
      t
    }
    else {
      0.0
    }
  }
}
object Scene {
  def props(light: Light)(implicit epsilon: Double) = Props(new Scene(light))
 
  case class TraceMapItem(bestShape: ActorRef, bestIntersect: Double, counter: Int, ray: Ray)
}
 
class Scene(light: Light)(implicit epsilon: Double) extends Actor {
  var objects = Seq.empty[ActorRef]
 
  val tracemap = collection.mutable.Map.empty[String, TraceMapItem]
  val ansMap = collection.mutable.Map.empty[String, ActorRef]
 
  override def receive = {
    //4
    case AddShape(obj) =>
      objects :+= obj
    //5a
    case Trace(id, ray) =>
      trace(id, ray)
    //6a
    case IntersectMessageAns(id, intersect) =>
      getIntersectAns(id, intersect)
    //8
    case ColorMessageAns(id, color) =>
      ansMap(id) ! ColorMessageAns(id, color)
      ansMap -= id
  }
 
  private def getIntersectAns(id: String, intersect: Double) = {
    val data = tracemap(id)
    val counter = data.counter - 1
    //6b
    if(intersect > epsilon && intersect < data.bestIntersect) {
      tracemap += (id -> data.copy(bestShape = sender, bestIntersect = intersect, counter = counter))
    } else {
      tracemap += (id -> data.copy(counter = counter))
    }
    //6c
    if(counter == 0) {
      val reloadedData = tracemap(id)
      getColor(id, reloadedData.bestShape, reloadedData.ray, reloadedData.bestIntersect)
      tracemap -= id
    }
  }
 
  private def trace(id: String, ray: Ray) = {
    ansMap += (id -> sender)
    objects.foreach(x => {
      //5c
      x ! IntersectMessageReq(id, ray)
    })
    //5b
    tracemap += (id -> TraceMapItem(null, Double.MaxValue, objects.size, ray))
  }
 
  private def getColor(id: String, shape: ActorRef, ray: Ray, intersect: Double): Unit = {
    if(shape != null) {
      //7a
      val intersectPoint = ray.startingPoint + (ray.direction * intersect)
      shape ! ColorMessageReq(id, ray, intersectPoint, light)
    } else {
      //7b
      ansMap(id) ! ColorMessageAns(id, Color(0, 0.5, 0.5))
      ansMap -= id
    }
  }
}
object ImageRender {
  def props(width: Int, height: Int, camera: Camera, iteration: Int, filename: String, scene: ActorRef): Props =
    Props(new ImageRender(width, height, camera, iteration, filename, scene))
}
 
class ImageRender(width: Int, height: Int, camera: Camera, iteration: Int, filename: String, scene: ActorRef) extends Actor {
 
  val image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
  var left = width * height
 
  override def receive: Receive = {
    //9a
    case "start" => startTrace
    //10
    case msg: ColorMessageAns =>
      writeToImage(msg)
      left -= 1
      //11a
      if(left == 0) {
        saveToFile(image, filename)
        //11c
        println("saved")
      }
  }
 
  def startTrace = {
    val w2 = width / 2.0
    val h2 = height / 2.0
    //9b
    for(x <- 0 until width) {
      for(y <- 0 until height) {
        val startingPoint = Point(x - w2, y - h2, 0)
        val ray = Ray(startingPoint, (startingPoint - camera.place).normalize)
        //9c
        scene ! Trace(s"$x,$y", ray)
      }
    }
  }
  //10b
  def writeToImage(msg: ColorMessageAns) = {
    val coords = msg.id.split(',').map(_.toInt)
    image.setRGB(coords(0), coords(1), msg.color.getImageColor)
  }
 
  def saveToFile(img: BufferedImage, filename: String): Unit = {
    try {
      //11b
      val outputfile = new File(filename)
      ImageIO.write(img, "png", outputfile)
    } catch {
      case e: IOException => e.printStackTrace
    }
  }
}
object Main {
  def main(args: Array[String]): Unit = {
    implicit val epsilon = 0.0001
    val system = ActorSystem("MyActorSystem")
 
    val camera = Camera(Point(0, 0, -500))
    val light = Light(Color(1, 1, 1), Point(200, -200, 0), Color(0.2, 0.2, 0.2))
    val scene = system.actorOf(Scene.props(light))
 
    val sphere = system.actorOf(Sphere.props(Point(-100, 200, 600), 100, Color(1, 1, 0), scene))
     
    val renderer = system.actorOf(ImageRender.props(600, 600, camera, 5, "test.png", scene))
 
    renderer ! "start"
  }
}

text

Pretty lame, but it’s our sphere from our ray tracer… Lets polish the code a bit and make this “ball” better looking!

Some improvements

First of all, we have some really good generic code on our sphere, so split it into a general abstract “Shape” and a shrunk “Sphere”. And add some color adjustments since we are here already!

abstract class Shape(val color: Color, scene: ActorRef)(implicit epsilon: Double) extends Actor {
  //12a
  override def preStart(): Unit = {
    scene ! AddShape(self)
  }
  //12b
  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, iteration)
  }
  //13
  def intersect(ray: Ray): Double
  //15
  def getNormal(intersectPoint: Point): Vec
  //14
  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)
    sender ! ColorMessageAns(id, ret)
  }
 
  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
  }
}
class Sphere(origo: Point, radius: Double, color: Color, scene: ActorRef)
            (implicit epsilon: Double)
  extends Shape(color, scene)(epsilon) {
  //13
  override def intersect(ray: Ray): Double = {
    val a = ray.direction.lengthQuad
    val b = 2.0 * (ray.direction mult (ray.startingPoint - origo)).sum
    val c = origo.lengthQuad + ray.startingPoint.lengthQuad - (2.0 * (origo mult ray.startingPoint).sum) - radius * radius
    val d = b * b - 4.0 * a * c
    if(d < 0) {
      return -1.0
    }
    val t = (-1.0 * b - Math.sqrt(d)) / (2.0 * a)
    if(t > epsilon) {
      t
    }
    else {
      0.0
    }
  }
  //15
  override def getNormal(intersectPoint: Point): Vec = {
    (intersectPoint - origo).normalize
  }
}

This split will be good, because if we want to add some generic code to every single one of our shapes (just like shadows), then we can add the code to the Shape class. This way, the shape-specific computations (like intersection point computations) will belong to the concrete shape objects. In the next part of this series, we will add more code to the base Shape, and try to leave the concrete shapes as pure as possible. (So no shadow computing, or refraction handling in the Sphere, it is not its responsibility!)

RUN IT AGAIN!

text

Much better! It looks like a ball!

Summary

In this post we started to write a really basic ray tracer, defined some messaging protocol between actors, and it can draw pretty spheres! In the next post we will extend this tracer with a plane, shadows, reflective and refractive objects.

Come back on June 20th for this episode of Ray tracing with Akka! [the Ed.]

This is a “learning tutorial” for both the readers and the author so if you have any suggestions or questions, feel free to comment below!

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