Porting a Ray Tracer to Rust, part 1

I’ve decided to port over my physically based ray tracer tray
to Rust to finally try out the language with a decent sized project.
In the series we’ll work through the implementation of a physically based ray tracer built
on the techniques discussed in Physically Based Rendering. I won’t go into a lot of
detail about rendering theory or the less exciting implementation details but will focus more on Rust
specific concerns and implementation decisions along with comparisons vs. my C++ version.
If you’re looking to learn more about ray tracing I highly recommend picking up Physically
Based Rendering and working through it. Hopefully throughout the series folks more experienced
with Rust can point out mistakes and improvements as well, since I have no experience with Rust
prior to this series.

With the intro out of the way, let’s get started! Since it’s the beginning of
the series this is my first time really working with Rust and our goal is pretty simple: render a white
sphere and save the image.

The Linear Algebra Module

I started by porting the linear algebra module used by tray over, since almost everything else
we do in the ray tracer will need basic linear algebra operations. This was pretty straightforward
but I did run into a few minor annoyances with things in the current state of Rust,
rustc 0.13.0-nightly (636663172 2014-12-28 16:21:58 +0000), namely: lack of function overloading
, default parameters and operator overloading for both left and right multiplication.

Function Overloading

This is a feature I used extensively throughout the C++ version of the ray tracer which helps
make things a bit more ergonomic. I first ran into this when implementing my vector type. In
tray I have overloads for the constructor so that it can be constructed with x, y and z all set
independently or by setting them all to the same value, as below.

This is convenient as there are many times when you simply want a vector that’s initialized
to all 0 or 1 for example. Since function overloading isn’t currently supported in Rust
these two methods require different names, I chose broadcast for my single value constructor.

I’d really prefer to just have an overload of new(x: f32) -> Vector which performed the same
construction that broadcast does currently, although this is a relatively minor annoyance.

Default Parameters

This relates closely to function overloading in that it also simplifies commonly written calls, eg. my
C++ vector constructors also default to set the values to 0, so constructing a vector of all 0
values is simply Vector{}. This doesn’t seem to be currently in Rust, but is another (somewhat minor)
feature that would be nice to have.

Overloading Left and Right Multiply

While it was very easy to overload the vector * scalar operator, writing the same overload
for scalar * vector doesn’t seem to be possible (please let me know if this has changed!). On the
topic of operator overloading, I do really like Rust’s decision to make them traits
because they can then be set as requirements for generic functions.
Below is my vector * scalar overload, it’s two extra lines than in C++ but is very clear to read.

Edit (12/31/14): ben0x539
mentioned that unsupported left-multiply overloading is a bug
which may be getting fixed in 19434.

Edit (5/9/15): Overloading left and right multiply is now supported in Rust. Additionally the method
for writing overloaded operators has changed significantly with the addition of associated types. The
current implementation of right and left multiply for Vector are now:

// Multiply a vector by a f32 on the right, eg. `vec * 2.0`
implMul<f32>forVector{typeOutput=Vector;/// Scale the vector by some value
fnmul(self,rhs:f32)->Vector{Vector{x:self.x*rhs,y:self.y*rhs,z:self.z*rhs}}}// Multiply a f32 by a vector on the right, eg. `2.0 * vec`
implMul<Vector>forf32{typeOutput=Vector;/// Scale the vector by some value
fnmul(self,rhs:Vector)->Vector{Vector{x:self*rhs.x,y:self*rhs.y,z:self*rhs.z}}}

Because operator overloading is done through traits we can set constraints on generic
functions to require that the types being worked on implement certain operations. This makes
the function clearer than in C++ (eg. what types it takes) and also gives much better compilation
errors when types not implementing the required operations are passed. For an extremely simple
comparison lets look at the C++ and Rust implementations of lerp.

If we misuse the lerp implementation in C++ we get a decent amount of errors. gcc-4.9
is quite clear about what went wrong, but these errors are shown as occuring inside the
function call (since that’s the point of error) and would be harder to parse for more complex functions.

As a user glancing through some template code in C++ it may not be clear what operations and functions
the type must provide (although for a simple lerp it is). Rust solves this by allowing you
to specify requirements on the traits implemented by the types. In lerp we need to be able
to multiply the types by floats, add the types to each other and make a copy of them to return.
Specifying these requirements up front makes the function interface much clearer:

If we misuse the lerp implementation in Rust we get some pretty clear and helpful errors,
telling us why the error occured and what we should do to resolve it. The error messages also
point us to the location of the bad call, which is much more useful than being pointed at a problem
inside some template function.
Note that in C++ we also didn’t get any errors about the lack of operator+, which we’d get next
if we fixed our missing operator*.

C++ also provides default implementations of the copy constructor for types while Rust does not,
thus in Rust we must require that Copy is implemented for the type. I think Rust’s decision to not
provide copy by default is a good one as it can cause trouble if you forget to delete the
copy constructor and copy-assign operators in C++ and your
type should not be copied. For most types that should be copyable that I’ve worked with so far
it’s simple enough to just use a compiler generated implementation via #[deriving(Copy)].

Rust’s Module System

In tray I chose to split up the various components of the ray tracer into their own libraries
which are then statically linked into the main executable.
This design is also possible in Rust by splitting functionality up into
independent modules. I had some difficulty initially understanding how to work with modules
and how inter-module dependencies worked but after reading through the
guide the end result is very nice. tray_rust is organized
with most functionality implemented in separate modules that are
built into a library and used by main.rs to render the image.

Under src I have the default executable and library files, main.rs and lib.rs. Also under src
are the various module subdirectories, eg. linalg for the linear algebra module, each containing
a mod.rs which defines some module level functions and re-exports the sub-modules implementing
other functionality, such as the vector module in linalg. The end result of this is that I can
have a very simple lib.rs which just publicly exports the various modules:

pubmodlinalg;pubmodfilm;pubmodgeometry;

In main.rs I can then tell Rust I need to build and link against the default library crate for
the executable, tray_rust which is defined by lib.rs, and use the modules:

Inter-module dependencies and circular dependencies are also handled very nicely. In the geometry module
if I need access to some of the types in linalg I can simply use linalg; and not need to worry about
dealing with fiddly link order requirements. Circular dependencies
aren’t a problem at all, which is nice coming from C++ where they can be a bit annoying, requiring
forward declarations and such.

Re-exporting From Modules

I initially struggled with the sheer amount of typing required for some of the nested modules. Since my
Vector struct is in the vector module within the linalg module to access it I would have to type
linalg::vector::Vector, which is a mouthful. While important to avoid naming conflicts I thought this
was pretty excessive and after some googling stumbled onto re-exporting from modules. This lets
us re-export the Vector type from the linalg module allowing us to use it as linalg::Vector,
which I think is more than enough to avoid name conflicts. This is done in the linalg module file,
linalg/mod.rs where we export the various sub-modules and re-export their types:

pubuseself::vector::Vector;pubmodvector;

Working With Traits

In my C++ ray tracer geometry is defined by an interface that provides methods such as intersect which
tests a ray for intersection with some piece of geometry, making it very easy to add new geometry types
to the ray tracer by implementing the interface. Additionally we separate the definition of some
geometry with its occurance in the
scene, so that a single model may appear multiple times in the scene with different materials and
transformations, a method known as instancing. To keep things simple when implementing our bounding
volume hierarchy our instance type also implements the geometry interface, simply transformating the
ray into object space and then calling the geometry’s intersect method.

This technique carries over directly to Rust in the form of traits. Instead of providing a base class
with pure virtual methods like we’d do in C++ we define a Geometry trait that provides these
same methods and write implementations for our geometry such as our Sphere type.
Below is the geometry trait implemented by the various scene geometry types in tray_rust.

intersect has an interesting return type, which lets me talk about another cool feature of Rust: no
null pointers! Instead Rust defines an Option<T> type which can be either Some(T) or None and
both cases must be considered. In C++ (and C, Java, etc) it’s easy to ignore the possibility that
a pointer may be null resulting in seg faults, null pointer exceptions and more. Rust avoids the billion
dollar mistake
by forcing you to not be sloppy.

In my C++ version I would take the DifferentialGeometry as a mutable reference and return a
bool if the ray intersected the object, filling out the differential geometry with the hit info if it
hit. In Rust this is expressed much cleaner, if there’s a hit we return Some(DifferentialGeometry)
otherwise we simply return None. The DifferentialGeometry also needs to send back information about
the instance and geometry that was hit, leading to the next topic: lifetimes in Rust.

Lifetimes in Rust

The concept and enforcement of ownership in Rust
was one of the features that initially got me interested in the language. For the most part lifetimes
are implicit in the language, however sometimes the compiler needs some assistance deducing lifetimes
for some variables. I first encountered this when writing my DifferentialGeometry struct which needs
to return back references to the hit Geometry and Instance so we can shade the hit point properly.
In order to verify the code is valid the compiler needs to check that the geometry and instance
referenced by the differential geometry will not be outlived by the differential geometry, as this
would result in dangling references. To specify this separate lifetime on a struct we’d write:

The message is actually quite clear, if I knew what an explicit lifetime bound was. However it
doesn’t seem to be mentioned in the guides which is what made resolving this error
much more challenging than previous ones. After a decent amount of googling I
found that while the way I wrote geom is correct for a regular struct a trait
requires an explicit lifetime bound since the type of data being referenced is unknown (we only know
it implements the trait). This is resolved by bounding the lifetime with
Geometry + 'a and the correct form is:

pubstructDifferentialGeometry<'a>{pubgeom:&'a(Geometry+'a),}

I’d like to find some more information on what exactly this means, such as
what a lifetime bound tells the compiler and how it’s used. Unfortunately I didn’t have much luck finding
information on this, if anyone has some links please do let me know.

Edit (12/31/14): @JakeGoulding pointed me to one of his answers
on stack overflow which I found to be helpful.
Another answer linked in the thread also has a really nice explanation. As a result of reading the
answers I’ve changed the geom member’s type to &'a (Geometry + 'static) to require the object
implementing the trait that we’re referring to be a struct, since we won’t be implementing Geometry
for reference types.

A Poor Design Choice

The DifferentialGeometry struct also contains a design decision that I’m not very happy with.
The differential geometry is initially created within the geometry that was hit, since it doesn’t know
about the instance that is using it that was hit it’s not able to set the instance member. This
results in the instance member of DifferentialGeometry having to be written as an optional reference,
when really it’s not optional. Any geometry we hit is associated with an instance, it’s just that
we don’t know which at the time of hitting the geometry itself, we need to go back up the call stack to
the instance that called the geometry’s intersect method.
This results in the DifferentialGeometry struct being written as:

One option is to give Instance its own intersect method and have the Geometry intersect function
just return back the hit point information and have the Instance fill out the geometry and instance
references. This isn’t such a good option though since it breaks the illusion that instances are just
geometry and will cause trouble when trying to write a bounding volume hierarchy that we can put
the instances in the scene into and can also use to construct BVHs on triangle meshes to accelerate
intersection testing.

As far as the end user of the intersect call is concerned there will always be a geometry and instance
reference on the DifferentialGeometry returned but I’m not sure how to express this. Another possibility
would be to have another Intersection type which didn’t have the option and could be constructed
from an Option<DifferentialGeometry> and would return None if the DifferentialGeometry or the
instance was None (although the latter wouldn’t happen), otherwise it would unwrap the instance
member’s option and return Some<Intersection>. I’ll probably look into implementing this as the
cleanest solution I can think of even though it’s really just a band-aid. If anyone has some ideas
or suggestions, please leave a comment or get in touch on Twitter
or IRC (I’m Twinklebear on freenode and moznet).

Testing

Rust also has built in support for specifying tests and benchmarks by placing #[test] or #[bench] attributes respectively. These are then run using
Cargo with cargo test or cargo bench. This built in support makes it very
easy to write and run unit and integration tests for your code and is really convenient coming from
C++ where testing is done through third party libraries.

Rustdoc

Rust comes with a built in documentation generation tool
that makes it really easy to have nice documentation for both the language and user libraries by
just running cargo doc on your crate. Since the generated doc site is all static pages it’s simple
to host them on Github pages or browse them locally.
For example here is the rustdoc for this project.
Good documentation is critical for any library or decent sized project and having it standardized
and built into the
language like this will hopefully improve the overall quality of documentation for user libraries.

Putting it all Together

Now that we’ve got modules to handle linear algebra, geometry and camera/image operations we have everything
we need to render a sphere! In main.rs we create our render
target, camera and sphere then attach the sphere to an instance and loop over the pixels in the image,
firing rays through each and checking for intersections:

The executable and library modules are built and the executable run with cargo run.
While the resulting image isn’t very impressive, we’re well on our way to writing a flexible
physically based ray tracer.

Final Thoughts

While I did encounter some minor annoyances with Rust I’m really happy with how the language
is shaping up as it nears 1.0 and look forward to the 1.0 release. One other difference compared
to C++ I didn’t mention is that variables in Rust are immutable by default and must be explicitly
declared mutable whereas C++ defaults to mutable variables.
I think Rust’s decision of immutable by default is great, most of the time I find that I really only
need a few mutable variables in my programs while everything else is constant or locally constant
and used to compute other constant results or operate on the few mutable variables.

Until Next Time

In the next post I’ll discuss the process of making tray_rust multithreaded and adding support
for some simple materials and lights which we’ll then render using Whitted recursive ray tracing.
While this ray tracing method doesn’t account for global illumination like path tracing or photon
mapping it’s simple to implement and very fast and will help us develop and test our abstractions
for integrators, materials and lights.

If you have comments, suggestions for improvements or just want to say “hi” feel free to comment below,
or contact me on Twitter or IRC (I’m Twinklebear on freenode and moznet).
The code for the Rust ray tracer is MIT licensed and available on Github.