post-photo

Where did we stop last time?

Read the first part of this series for an intorduction to ray tracing and what I wanted to achieve. Here we created a sphere and a shadow. Read part two for a discussion on creating a plane and letting our objects reflect and refract. Part three has been the most advanced, in here we built a cluster. That last bit has been shared by Jonas Boner and Konrad Malawski amongst others, so I feel like I have to be onto something! :D Enjoy Part 4! As always, I am happy to hear your feedback and suggestions!

Construct new shapes from others

In this post I will show how easily we can construct shapes from our previous shapes (let’s name them constructed shapes), and how can we speed up the computation with bounded objects.

Let’s start the first category, the constructed shapes. The idea is that if I want to create a cylinder I will need a part from an infinite cylinder, and a tiny part from two planes. I have the plane as a shape, I can create the infinite cylinder easily too. What if I don’t make a new shape and implement the plane and infinite cylinder again? I just want to drop and keep points based on easy rules from the three already-programmed shapes to make a new one for me. With a fast brainstorming, we can abstract our constructed shape to communication and shape specific logic again. (Sadly we can’t reuse the Shape :( )

object ConstructedShape {
  case class IntersectMapItem(id: String, ray: Ray, counter: Int, bestShape: ActorRef, bestIntersect: Double)
}
 
abstract class ConstructedShape(scene: ActorRef)(implicit epsilon: Double) extends Actor {
 
  def uuid = java.util.UUID.randomUUID.toString
 
  val intersectMap = collection.mutable.Map.empty[String, IntersectMapItem]
 
  override def receive = {
    case IntersectMessageReq(id, ray) =>
      broadcastToObjects(id, ray) //1a
    case IntersectMessageAns(id, ret) =>
      val mapItem = intersectMap(id)
      val intersectionPoint = mapItem.ray.startingPoint + (mapItem.ray.direction * ret)
      val modifiedRet = modifyIntersection(sender, intersectionPoint, ret) //2a
      val newMapItem = createNewMapItem(sender, modifiedRet, mapItem)
      if(newMapItem.counter == 0) { //3a
        sendAnswerToIntersect(newMapItem)
      }
      writeBackTheNewIntersectItem(id, newMapItem)
    case msg: ColorMessageReq =>
      val obj = getTheObjectByPoint(msg.intersectPoint) //4a
      obj ! msg
    case msg: Trace =>
      scene forward msg //5a
    case msg: ColorMessageAns =>
      scene ! msg //5b
  }
 
  def objects: Seq[ActorRef] //1c
 
  def getTheObjectByPoint(intersectPoint: Point): ActorRef //4b
 
  def modifyIntersection(sender: ActorRef, intersectionPoint: Point, ret: Double): Double //2b
  //3b
  private def createNewMapItem(sender: ActorRef, ret: Double, mapItem: IntersectMapItem): IntersectMapItem = {
    if(ret > epsilon) {
      if(ret < mapItem.bestIntersect) {
        mapItem.copy(counter = mapItem.counter - 1, bestIntersect = ret, bestShape = sender)
      } else {
        mapItem.copy(counter = mapItem.counter - 1)
      }
    } else {
      mapItem.copy(counter = mapItem.counter - 1)
    }
  }
  //1b
  private def broadcastToObjects(id: String, ray: Ray) = {
    val newId = uuid
    intersectMap += (newId -> IntersectMapItem(id, ray, objects.size, null, Double.MaxValue))
    objects.foreach(obj => obj ! IntersectMessageReq(newId, ray))
  }
 
  private def sendAnswerToIntersect(newMapItem: IntersectMapItem): Unit = {
    if(newMapItem.bestShape != null) {
      scene ! IntersectMessageAns(newMapItem.id, newMapItem.bestIntersect)
    }
    else {
      scene ! IntersectMessageAns(newMapItem.id, -1.0)
    }
  }
 
  private def writeBackTheNewIntersectItem(id: String, newMapItem: IntersectMapItem) = {
    if(newMapItem.counter == 0) {
      intersectMap -= id
    } else {
      intersectMap += (id -> newMapItem)
    }
  }
 
}

Here, I show you the promised infinite cylinder too. (Its code looks like the sphere’s :) )

object InfiniteCylinder {
  case class InfiniteCylinderData(pointOnAxis: Point, normalOfAxes: Vec, radius: Double, color: Color) extends ShapeData {
    def props(scene: ActorRef)(implicit epsilon: Double): Props = InfiniteCylinder.props(pointOnAxis, normalOfAxes, radius, color, scene)
  }
 
  def props(pointOnAxis: Point, normalOfAxes: Vec, radius: Double, color: Color, scene: ActorRef)(implicit epsilon: Double): Props =
    Props(new InfiniteCylinder(pointOnAxis, normalOfAxes, radius, color, scene))
}
 
class InfiniteCylinder(pointOnAxis: Point, normalOfAxes: Vec, radius: Double, color: Color, scene: ActorRef)(implicit epsilon: Double)
  extends Shape(color, scene)(epsilon) {
 
  override def intersect(ray: Ray): Double = {
    val pointsDelta = ray.startingPoint - pointOnAxis
    val helper1 = ray.direction - normalOfAxes * (ray.direction dot normalOfAxes)
    val helper2 = pointsDelta - normalOfAxes * (pointsDelta dot normalOfAxes)
    val a = helper1 dot helper1
    val b = 2.0 * (helper1 dot helper2)
    val c = (helper2 dot helper2) - (radius * radius)
    val d = b * b - 4.0 * a * c
    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
      }
    }
  }
 
  override def getNormal(intersectPoint: Point): Vec = {
    val t = ((intersectPoint dot normalOfAxes) - (pointOnAxis dot normalOfAxes)) / (normalOfAxes dot normalOfAxes)
    val origo = pointOnAxis + (normalOfAxes * t)
    (intersectPoint - origo).normalize
  }
}

And finally we can implement the cylinder!

object Cylinder {
  case class CylinderData(pointOnAxis: Point, normalOfAxes: Vec, radius: Double, height: Double, color: Color) extends ShapeData {
    def props(scene: ActorRef)(implicit epsilon: Double): Props = Cylinder.props(pointOnAxis, normalOfAxes, radius, height, color, scene)
  }
 
  def props(pointOnAxis: Point, normalOfAxes: Vec, radius: Double, height: Double, color: Color, scene: ActorRef)(implicit epsilon: Double): Props =
    Props(new Cylinder(pointOnAxis, normalOfAxes, radius, height, color, scene))
}
 
class Cylinder(pointOnAxis: Point, normalOfAxes: Vec, radius: Double, height: Double, color: Color, scene: ActorRef)(implicit epsilon: Double)
  extends ConstructedShape(scene)(epsilon) {
 
  //6a
  val topPlanePoint: Point = pointOnAxis + (normalOfAxes * (height / 2))
  val bottomPlanePoint: Point = pointOnAxis - (normalOfAxes * (height / 2))
   
  val topPlane: ActorRef = context.actorOf(Plane.props(topPlanePoint, normalOfAxes, color, self))
  val cylinder: ActorRef = context.actorOf(InfiniteCylinder.props(pointOnAxis, normalOfAxes, radius, color, self))
  val bottomPlane: ActorRef = context.actorOf(Plane.props(bottomPlanePoint, normalOfAxes * -1, color, self))
  //6b
  val objectList = Seq(topPlane, cylinder, bottomPlane)
 
  override def objects = objectList
 
  override def modifyIntersection(sender: ActorRef, intersectionPoint: Point, ret: Double) = {
    if(sender == topPlane) {
      if(isItOnPlane(topPlanePoint, intersectionPoint)) {
        ret
      } else {
        -1.0
      }
    } else if(sender == bottomPlane) {
      if(isItOnPlane(bottomPlanePoint, intersectionPoint)) {
        ret
      } else {
        -1.0
      }
    } else {
      //8
      val distanceSquared = pointOnAxis.distance(intersectionPoint) * pointOnAxis.distance(intersectionPoint)
      val maxDistance = (height / 2) * (height / 2) + radius * radius
      if(distanceSquared < maxDistance) {
        ret
      } else {
        -1.0
      }
    }
  }
 
  override def getTheObjectByPoint(intersectionPoint: Point) = {
    if(isItOnPlane(topPlanePoint, intersectionPoint)) {
      topPlane
    }
    else if(isItOnPlane(bottomPlanePoint, intersectionPoint)) {
      bottomPlane
    }
    else {
      cylinder
    }
  }
  //7
  private def isItOnPlane(planePoint: Point, intersectionPoint: Point): Boolean = {
    planePoint.distance(intersectionPoint) < radius
  }
}

As you can see it’s really easy. I initialized the three inner shapes (6a) and added them to a seq (6b). I can decide if a point is on the top or on the bottom plane (7). With a bit more logic I could cut the infinite cylinder’s cape to finite (8). And all of the aforementioned logic was enough to construct this new shape from two previously created ones \o/

object Main {
  def main(args: Array[String]): Unit = {
    implicit val epsilon = 0.00001
 
    val system = ActorSystem("ClusterSystem")
 
    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 cylinder = CylinderData(Point(350, -200, 500), Vec(0, 1, -0.5).normalize, 100, 200, Color(1, 1, 1))
    val cylinder2 = InfiniteCylinderData(Point(-100, -200, 600), Vec(0, 1, 0.2).normalize, 100, Color(1, 1, 1))
  
    val scene = system.actorOf(Scene.props(light, Seq(cylinder, cylinder2)))
    val renderer = system.actorOf(ImageRender.props(600, 600, 5, camera, "test.png", scene))
 
    renderer ! "start"
  }
}

The main function still works without clustering – for faster testing – as we develop new types or shapes.

text

We have a working infinite and finite cylinder!

Bounded shapes

Let’s talk about another topic. Sometimes you can’t construct a “fast” intersect function. You can’t just solve a 2nd degree equation because you don’t have that kind of explicit information about the shape. For example, you have metaballs (I will show them later), and the only thing you can calculate is whether the given point is inside or outside of the shape. We can’t compute this information for all of the ray’s points, so we need to make the scope smaller. Most of the time we can define one (or more) object(s) (with a cheap intersection computation) and wrap the computation-heavy object. This will be a standalone actor, which will need a list of bounder objects and the bounded object, and do the optimization with those information.

object BoundedShape {
  def props(bounderObjectsData: Seq[ShapeData], boundedObjectData: ShapeData, scene: ActorRef)(implicit epsilon: Double) =
    Props(new BoundedShape(bounderObjectsData, boundedObjectData, scene))
 
  case class IntersectMapItem(id: String, ray: Ray, counter: Int, hadHit: Boolean)
}
 
class BoundedShape(bounderObjectsData: Seq[ShapeData], boundedObjectData: ShapeData, scene: ActorRef)(implicit epsilon: Double) extends Actor {
 
  def uuid = java.util.UUID.randomUUID.toString
 
  var bounderObjects = Seq.empty[ActorRef]
  var boundedObject: ActorRef = _
  val intersectMap = collection.mutable.Map.empty[String, IntersectMapItem]
  //9a
  override def preStart(): Unit = {
    createBounderObjects()
    createBoundedObject()
  }
 
  override def receive = {
    case IntersectMessageReq(id, ray) =>
      broadcastToBounders(id, ray)
    case IntersectMessageAns(id, ret) if sender != boundedObject =>
      handleIntersectAnsFromBounders(id, ret)
    //12
    case msg: IntersectMessageAns if sender == boundedObject =>
      scene ! msg
    case msg: ColorMessageReq =>
      boundedObject ! msg
    case msg: Trace =>
      scene forward msg
    case msg: ColorMessageAns =>
      scene ! msg
  }
 
  private def handleIntersectAnsFromBounders(id: String, ret: Double) = {
    val mapItem = intersectMap(id)
    val newMapItem = createNewMapItem(ret, mapItem)
    if(mapItem.hadHit != newMapItem.hadHit) {
      boundedObject ! IntersectMessageReq(mapItem.id, mapItem.ray) //11a
    }
    if(newMapItem.counter == 0 && !newMapItem.hadHit) {
      scene ! IntersectMessageAns(newMapItem.id, -1.0) //11b
    }
    writeBackTheNewIntersectItem(id, newMapItem)
  }
 
  private def createNewMapItem(ret: Double, mapItem: IntersectMapItem) = {
    if(ret > epsilon) {
      mapItem.copy(counter = mapItem.counter - 1, hadHit = true)
    } else {
      mapItem.copy(counter = mapItem.counter - 1)
    }
  }
 
  private def writeBackTheNewIntersectItem(id: String, newMapItem: IntersectMapItem) = {
    if(newMapItem.counter == 0) {
      intersectMap -= id
    } else {
      intersectMap += (id -> newMapItem)
    }
  }
  //10
  private def broadcastToBounders(id: String, ray: Ray) = {
    val newId = uuid
    intersectMap += (newId -> IntersectMapItem(id, ray, bounderObjects.size, hadHit = false))
    bounderObjects.foreach(obj => obj ! IntersectMessageReq(newId, ray))
  }
  //9c
  private def createBoundedObject() = {
    boundedObject = context.actorOf(boundedObjectData.props(self))
  }
  //9b
  private def createBounderObject(obj: ShapeData) = {
    bounderObjects :+= context.actorOf(obj.props(self))
  }
  //9b
  private def createBounderObjects() = {
    bounderObjectsData.foreach(obj => createBounderObject(obj))
  }
}

And we can use this in the future like:

case class MetaBallData(balls: Seq[SphereData], depth: Int, resolution: Int) extends ShapeData {
  def props(scene: ActorRef)(implicit epsilon: Double) = {
    BoundedShape.props(balls.map(b => b.copy(radius = b.radius + 2)), NonBoundedMetaballData(balls, depth, resolution), scene)
  }
}

MetaBall

As I said it is a tricky “shape”. You can picture it like the molecules in Chemistry class. There are spheres with different radii, and the shape comes from the distances between those spheres. There is a general function which can tell you whether a given point in the space is inside or outsode of this shape. So the intersection is like going in a line step by step and computing the “are we in the shape?” function. We can get better results if, when we get a positive result we step back, and start again with smaller steps (with recursion for example :)). This method has a huge computational cost so most of the time we use bounder objects to compute intersections only when there is a high probability. I will show you an implementation of this shape, but it won’t be able refract correctly :( (I don’t mind if the ray is started from the inside of the object.)

object MetaBall {
  //18b
  case class MetaBallData(balls: Seq[SphereData], depth: Int, resolution: Int) extends ShapeData {
    def props(scene: ActorRef)(implicit epsilon: Double) = {
      BoundedShape.props(balls.map(b => b.copy(radius = b.radius + 2)), NonBoundedMetaballData(balls, depth, resolution), scene)
    }
  }
  //18a
  case class NonBoundedMetaballData(balls: Seq[SphereData], depth: Int, resolution: Int) extends ShapeData {
    def props(scene: ActorRef)(implicit epsilon: Double) = {
      MetaBall.props(balls, depth, resolution, scene)
    }
  }
  def props(balls: Seq[SphereData], depth: Int, resolution: Int, scene: ActorRef)(implicit epsilon: Double) = Props(new MetaBall(balls, depth, resolution, scene))
}
 
class MetaBall(balls: Seq[SphereData], depth: Int, resolution: Int, scene: ActorRef)(implicit epsilon: Double)
  extends Shape(Color(1, 1, 1), scene)(epsilon) {
 
  val boundingBall = computeBoundingBall()
 
  override def intersect(ray: Ray): Double = {
    val a = ray.direction.lengthSquared
    val b = 2.0 * (ray.direction mult (ray.startingPoint - boundingBall.origo)).sum
    val c = boundingBall.origo.lengthSquared + ray.startingPoint.lengthSquared - (2.0 * (boundingBall.origo mult ray.startingPoint).sum) - boundingBall.radius * boundingBall.radius
    val d = b * b - 4.0 * a * c
    if(d < 0) {
      -1.0
    } else {
      val stepInPoint = (-1.0 * b - Math.sqrt(d)) / (2.0 * a) //13b
      val stepOutPoint = (-1.0 * b + Math.sqrt(d)) / (2.0 * a) //13b
      findIntersection(ray, stepInPoint, stepOutPoint, depth, resolution) //14a
    }
  }
 
  @tailrec
  private def findIntersection(ray: Ray, lowerBound: Double, upperBound: Double, depth: Int, resolution: Int): Double = {
    val step = (upperBound - lowerBound) / resolution
    //14b
    (1 to resolution).find(i => pointIsInObject(ray, lowerBound, step, i)) match {
      case Some(i) if depth == 0 =>
        i * step + lowerBound //15a
      case Some(i) =>
        findIntersection(ray, (i - 1) * step + lowerBound, i * step + lowerBound, depth - 1, resolution) //15b
      case None =>
        -1.0
    }
  }
  //14c
  private def pointIsInObject(ray: Ray, lowerBound: Double, step: Double, i: Int) = {
    val actualPoint = ray.startingPoint + ray.direction * (i * step + lowerBound)
    val sumWeight = -0.7 + balls.map(b => (actualPoint distance b.origo) / b.radius).filter(_ < 1).map(k => weightFunction(k)).sum
    sumWeight > epsilon
  }
 
  def weightFunction(distance: Double): Double = {
    1.0 - (4 * Math.pow(distance, 6) - 17 * Math.pow(distance, 4) + 22 * Math.pow(distance, 2)) / 9.0
  }
  //17
  override def getNormal(intersectPoint: Point): Vec = {
    Vec(1, 1, 1)
  }
  //16a
  override def sendBaseColor(id: String, ray: Ray, intersectPoint: Point, light: Light, iterationsLeft: Int): Unit = {
    val (normal, color) = computeNormalAndColor(intersectPoint)
    var cosTheta: Double = computeCosTheta(intersectPoint, light, normal)
    var ret = color mult light.color mult Color(cosTheta, cosTheta, cosTheta)
    computeShadow(id, intersectPoint, light, ret)
  }
  //16b
  def computeNormalAndColor(intersection: Point): (Point, Color) = {
    val (normal, color, weightSum) = balls.map(b => (computeNormalForBall(b, intersection), b.color))
      .filter(tup => tup._1 != Vec(0, 0, 0))
      .foldLeft[Tuple3[Vec, Color, Double]]((Vec(0, 0, 0), Color(0, 0, 0), 0.0))((a, b) => (a._1 + b._1, a._2 + b._2 * b._1.length, a._3 + b._1.length))
    (normal.normalize, color * (1.0 / weightSum))
  }
  //16c
  def computeNormalForBall(ball: SphereData, intersection: Point): Vec = {
    val rate = (intersection - ball.origo).length / ball.radius
    if(rate < 1) {
      (intersection - ball.origo) * (-1.0 * (-12.0 / 9.0 * Math.pow(rate, 4) + 34.0 / 9.0 * Math.pow(rate, 2) - 22.0 / 9.0) * 2.0 / Math.pow(ball.radius, 2))
    } else {
      Vec(0, 0, 0)
    }
  }
 
  def computeCosTheta(intersectPoint: Point, light: Light, normal: Vec): Double = {
    val rayToLight = Ray(intersectPoint + (normal * epsilon), (light.place - intersectPoint).normalize)
 
    var cosTheta = normal dot rayToLight.direction
    if(cosTheta < 0) {
      cosTheta = 0
    }
    cosTheta
  }
  //13a
  private def computeBoundingBall(): SphereData = {
    val origo = balls.map(b => b.origo).fold(Point(0, 0, 0))((p1, p2) => p1 + p2) / balls.size
    val radius = balls.map(b => (origo distance b.origo) + b.radius).max
    SphereData(origo, radius, Color(1, 1, 1))
  }
}

Works like a charm!

My original raytracer homework was a metaball one (this implementation is mostly my old code rewritten in scala). We needed to show an assigned molecule (every student got one from a pool). My molecule was the C2H3S and if you want another, you can browse the linked site, parse out some information from the 3D model datasheets and render them. (As you will see below I use the given x,y,z coordinates from the json/xml, but I have no idea where I found those radiuses anno :) )

My metaball looks like this with code:

def placeCorrection(v: Point) = (v * 55 * 1.8 mult Vec(1, -1, 1)) + Vec(0, 0, 500)
 
def radiusCorrection(r: Double) = r * 1.8
 
val s = SphereData(placeCorrection(Point(-1.4692, -0.1747, 0)), radiusCorrection(102.0), Color(1, 1, 0))
val c1 = SphereData(placeCorrection(Point(0.1511, 0.4172, 0)), radiusCorrection(77.0), Color(0.5, 0.5, 0))
val c2 = SphereData(placeCorrection(Point(1.3181, -0.2425, 0)), radiusCorrection(77.0), Color(0.5, 0.5, 0))
val h1 = SphereData(placeCorrection(Point(0.237, 1.502, 0)), radiusCorrection(38.0), Color(1, 1, 1))
val h2 = SphereData(placeCorrection(Point(2.2568, 0.3019, 0)), radiusCorrection(38.0), Color(1, 1, 1))
val h3 = SphereData(placeCorrection(Point(1.3643, -1.3267, 0)), radiusCorrection(38.0), Color(1, 1, 1))
 
val metaball = MetaBallData(Seq(s, c1, c2, h1, h2, h3), 2, 100)

And it looks like this after rendered:

text

(Funny thing that my reference code made this more gray because I sometimes wrote 34.0/9.0 and sometimes 34/9. I found a ~4 year old mistake :D )

The power of mathematics!

So I got challenged after the first post; “Can you render the scala logo?” My answer was a “why not” but overall I spent a solid two days with it :D (Compared with the objects in this post above, they got a maximum of four hours from my life altogether, mostly because they had documented mathematical models, or I just needed to port/code an idea.) I wanted to solve the problem really generally with stripes between curves. After a half day of writing up and solving equations (with my really rusted coordinate geometry knowledge) I ended up with “I don’t want to solve this with this generality” so I started to work with a “cut it out of a cylinder” method (I had the cylinder already at this point). I made a “general” helix-stripe-like shape (with a fixed y axes because I hate coordinate system transforms ;) ). The code is not interesting, the mathematics in it is. But if you want to understand it, I highly recommend to try to draw the variables from the code (like the pointL does not mean much, but you can draw it from the equation) :D

object ScalaLogo {
  case class ScalaLogoData(pointOnAxis: Point, normalOfAxes: Vec, textureOrigo: Point, radius: Double, height: Double, steepness: Double, thickness: Double, color: Color) extends ShapeData {
    def props(scene: ActorRef)(implicit epsilon: Double): Props = ScalaLogo.props(pointOnAxis, normalOfAxes, textureOrigo, radius, height, steepness, thickness, color, scene)
  }
 
  def props(pointOnAxis: Point, normalOfAxes: Vec, textureOrigo: Point, radius: Double, height: Double, steepness: Double, thickness: Double, color: Color, scene: ActorRef)(implicit epsilon: Double): Props =
    Props(new ScalaLogo(pointOnAxis, radius, height, steepness, thickness, color, scene))
}
 
class ScalaLogo(pointOnAxis: Point, radius: Double, height: Double, steepness: Double, thickness: Double, color: Color, scene: ActorRef)(implicit epsilon: Double)
  extends Shape(color, scene)(epsilon) {
  require(steepness > 0)
 
  val lineDistance = (steepness * 2.0 * radius * Math.PI) * Math.sin(Math.PI / 2.0 - Math.atan(steepness))
  val spaces = (1.0 - (thickness / lineDistance)) * lineDistance
  val normalOfAxes: Vec = Vec(0, 1, 0)
  val textureOrigo = pointOnAxis - (Vec(0, 1, 0) * height / 2) - (Vec(0, 0, 1) * radius)
  val textureOrigoWoY = textureOrigo.copy(y = 0)
  val pointOnAxisWoY = pointOnAxis.copy(y = 0)
  val dif = 65.0
 
  override def intersect(ray: Ray): Double = {
    val pointsDelta = ray.startingPoint - pointOnAxis
    val helper1 = ray.direction - normalOfAxes * (ray.direction dot normalOfAxes)
    val helper2 = pointsDelta - normalOfAxes * (pointsDelta dot normalOfAxes)
    val a = helper1 dot helper1
    val b = 2.0 * (helper1 dot helper2)
    val c = (helper2 dot helper2) - (radius * radius)
    val d = b * b - 4.0 * a * c
    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(partOfTheShape(ray, t1)) {
        t1
      } else if(partOfTheShape(ray, t2)) {
        t2
      } else {
        -1.0
      }
    }
  }
 
  private def partOfTheShape(ray: Ray, distance: Double): Boolean = {
    if(distance > epsilon) {
      val intersectionPoint = ray.startingPoint + ray.direction * distance
      val distanceSquared = pointOnAxis.distance(intersectionPoint) * pointOnAxis.distance(intersectionPoint)
      val maxDistance = (height / 2) * (height / 2) + radius * radius
      if(distanceSquared < maxDistance) {
        partOfTheModifiedSurface(intersectionPoint)
      } else {
        false
      }
    } else {
      false
    }
  }
 
  private def partOfTheModifiedSurface(intersectionPoint: Point): Boolean = {
    val v = intersectionPoint.y - textureOrigo.y
    val pointWoY = intersectionPoint.copy(y = 0)
    val u = radius * Math.acos((pointOnAxisWoY - pointWoY).normalize dot (pointOnAxisWoY - textureOrigoWoY).normalize) *
      (if(textureOrigo.x > intersectionPoint.x) -1 else 1)
    partOfTheTexture(u, v)
  }
 
  private def partOfTheTexture(u: Double, v: Double): Boolean = {
    val vec = Vec(1, steepness, 0).normalize
    val origo = Vec(0, 0, 0)
    val point = Vec(u, v, 0)
 
    val pointL = origo + vec * ((point - origo) dot vec)
    val distance = pointL.distance(point)
    val k = Math.ceil(distance / (thickness + spaces))
    val pointF = pointL + (point - pointL).normalize * k * (thickness + spaces)
 
    if(pointF.y + dif > height || pointF.y - dif < 0.0) {
      false
    } else {
      distance > k * (thickness + spaces) - thickness
    }
  }
 
  override def getNormal(intersectPoint: Point): Vec = {
    val t = ((intersectPoint dot normalOfAxes) - (pointOnAxis dot normalOfAxes)) / (normalOfAxes dot normalOfAxes)
    val origo = pointOnAxis + (normalOfAxes * t)
    (intersectPoint - origo).normalize
  }
}

The logo itself can be created with this line of code:

val scalalogo = ScalaLogoData(Point(0, -100, 500), Vec(0, 1, 0).normalize, Point(0, -100, 400), 100, 400, 1.0/6.0, 80, Color(1, 0, 0))

And it looks like the scala logo :D

text

Ending thoughts

As we have reached the end of this series; I think this was an interesting tour in the Akka world, and I will use the actor concept more confidently. I know some of my code is not clean and tidy (like returning a Vec(0,0,0) and then checking if the returned val is eq to Vec(0,0,0) or not; instead of using Option), but writing functions without while, or for with break/return was challenging enough at some points ;) (or finding where those messages were leaking or were remaining unanswered). The whole code could be more beautiful if somebody reviewed it from time to time, this is the bad side of programming alone… (The good side is that mostly, you do what you want in projects like this :) ) That would be nice if I would implement some kind of “loosing messages” algo and some supervision but I had no time for that. :(

This small tour ended up with a fairly good concept (this post proves this mostly). If you have a primitive shape, you can easily code it down. If you have a more complex shape, you can write it mostly from the primitives. I didn’t show that, but you can do texturing with it (like the reflection/refraction you can define a trait, do some shape-specific coordinate transformation and you are basically done). It has a working reflection and refraction implementation, and if you want to make a shape reflective/refractive you can do it at maximum 10 lines of mostly boilerplate code (maybe it is possible to write a macro for that too, I will challenge somebody for a possible future post with it ;) ). We can manipulate surface points too so if you have a sphere, you can make a golf ball from it (displacement mapping). The casutic would be an interesting one, but I think that can be solved too with a little more brainstorming. We have a cluster/grid model to outsource our higher computation pictures to other nodes.

We don’t have multiple lights. I think if I want it ever, I will need to refactor a lot. I think this would be a lot of work compared to the one mentioned in the section above.

Last but not least, I need to speak about the performance. It’s terrible… I thought it will be under the one threaded C code. It’s sadly not. As I said in the previous post, if I use cluster with one node it’s slower (due to the serialization and network). For reference I measured my old C code with the metaballs, and my newly written Scala code with the same object (same perspective, same computation depth). There the numbers are minimally rounded (it’s time to upgrade my home PC):

text

It would be nice to measure and tune the performance of the application, but our workload is rising so I left it for a “try it yourself and make suggestions” comment section if others are interested too ;) (also you can fork the project on github and start experimenting with it).

Thanks for reading, and I hope you enjoyed this mini learning series. As always, if you have comments/ideas, feel free to contact me or write in the comment section.

To see our upcoming Akka series, follow us on LinkedIn or like us on Facebook – we can also answer your questions there too.

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