Ray Tracing with Akka – Part 4

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! (big grin) 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 (sad) )

  • When the actor receives an IntersectMessageReq, it needs to broadcast it to all of the inner objects (so we will need to know the list of inner objects (1c)). (1a, 1b)
  • When the actor receives an IntersectMessageAns, it needs to decide whether or not it is the part of the constructed object. (2a, 2b)
    • If all of the answers have been received (which belong to one broadcast) the actor needs to reply the closest intersection point to the requester. (3a, 3b)
  • When the actor gets a ColorMessageReq it needs to know which object the given intersection point belongs to and send this point to that object. (4a, 4b)
  • All of the other messages can be relayed or forwarded. (5a, 5b)

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

And finally we can implement the cylinder!

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/

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

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.

  • When we start, we need to create the objects from ShapeData. (9a, 9b, 9c)
  • When we get an IntersectMessageReq we need to broadcast to all bounders (10)
  • When we get answer from bounder object
    • If no intersect, then decrease the counter or if it was the last, tell the scene we didn’t intersect (11a)
    • If has intersect, then relay it to the bounded object (11b)
  • We relay or forward All other messages (12)

And we can use this in the future like:

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 (smile)). 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 (sad) (I don’t mind if the ray is started from the inside of the object.)

  • We create a big bounding ball (13a) for a good start and end point on our steps (the steps are configurable, we let the user decide how many steps he wants) (13b)
  • We start stepping in the ray from the given start point towards the given end point (14a). Everytime we compute whether we are inside or outside of the shape (14b, 14c)
    • If we are inside, then we return the distance from the ray’s starting point (15a) or step back and do the stepping with smaller steps again (15b) (the depth is configurable too)
  • We compute the color differently from how the “shape” does, so we override the base color creation with normal vector and color weighting (16a, 16b, 16c)
  • We need to override the getNormal function, but we will never use it (sad) (17)
  • We make a NonBoundedShapeData (18a) and a classic ShapeData (18b) (we need the nonbounded one for the BoundedShapeCreation)

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 (sad)  )

My metaball looks like this with code:

And it looks like this after rendered:

(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 (big grin) )

 

 

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 (big grin) (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 (wink) ). 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) (big grin)

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

And it looks like the scala logo (big grin)

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 (wink) (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 (smile)) That would be nice if I would implement some kind of “loosing messages” algo and some supervision but I had no time for that (sad)

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 (wink)). 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):

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 (wink) (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.

Gergő Törcsvári

Gergő Törcsvári

Software Developer at Wanari
I would love to change the world, but they won’t give me the source code (yet).