Skip to content

Lab2_20150923

Mauricio A Rovira Galvez edited this page Sep 22, 2015 · 1 revision

Lab 2: Ray Tracing a Circle

September 23, 2015

In this lab, you will learn how to implement a very simple ray tracer that does the following things:

  • Set up a pinhole camera
  • Generate primary rays for this camera
  • Ray-trace a circle

Getting set up

If you haven't done so, please clone icg into some folder. For this example, it will be cloned into the Desktop (~/Deskop). We can do this by opening a terminal and entering the following (alternatively, you can download the source code zip archive to the Desktop and extract it):

~: git clone https://github.com/luckysama/icg.git ~/Desktop/icg

Once it is cloned (or unzipped) do one of the following:

  • In the same terminal, type:
~: cd Desktop/icg
~/Desktop/icg: open -a "Qt\ Creator.app" CMakeLists.txt
  • or, open Qt Creator, and go to "File > Open File or Project..." and select Desktop/icg/CMakeLists.txt.

If everything is set up correctly, we can now move on to create the necessary files. While you can write all of your code in the provided main.cpp file, it is recommended (both for this and in general) to split up your code into several files. With this in mind, create the following files in the same folder that contains main (here ~/Desktop/icg/raytrace):

  1. Camera.h (Optional: see Appendix 1 about declaring things in headers)
  2. Circle.h
  3. ImagePlane.h

Once the files are ready, go back to Qt Creator, and go to "Build > Run CMake". Once that finishes, you should see that under the raytrace folder, the new files will appear. With this, you can proceed to the next section.

The Camera

Here we will implement a simplified version of a Pinhole camera. To accomplish this, we only need the position of the camera and a way to create rays. With all these in mind, let's write some code! First, lets look at Camera.h (if you want to know more, you can look at Appendix 1 regarding the usage of #pragma once and Appendix 2 regarding inline):

#pragma once
#include "icg_common.h"
#include <Eigen/Geometry>

class Camera{
public:
    // We only need the position of the camera.
    Camera(vec3 const& eye) : mEye(eye) { }

    // Given coordinates of a pixel in the view plane, we generate its primary ray
    inline Eigen::ParametrizedLine<float, 3> generateRay(vec3 const& pt){
        typedef Eigen::ParametrizedLine<float, 3> ray3;

        vec3 origin = mEye
        vec3 direction = mEye - pt;
        return ray3(origin, direction);
    }

private:
    vec3 mEye; // The position of the camera.
};

The Circle

Intersecting a circle is very similar to intersecting with a plane. The only difference is that once we compute the hit point with the plane that contains the circle, we need to make sure that the distance between that point and our centre is less than our radius.

In order to ray trace a circle, we only need:

  1. A centre,
  2. a radius,
  3. a function that tells us if we hit the circle or not.

So we write the following in Circle.h

#pragma once
#include "icg_common.h"
#include <Eigen/Geometry>

class Circle{
public:
    Circle(vec3 const& c, float r) :
        mCentre(c),
        mNormal(c), // We use the centre as our normal.
        mRadius(r)
    { }

    inline bool intersectRay(Eigen::ParametrizedLine<Float, 3> const& ray){
        // Intersect against the plane first to compute the t value.
        // Note that if the direction or the normal are 0, we divide by 0!
        float d = (-mCentre).dot(mNormal);
        float t = -(ray.origin().dot(mNormal) + d) / (ray.direction().dot(mNormal));

        vec3 hitPoint = ray.pointAt(t);
        vec3 dist = hitPoint - mCentre;
        return (dist.norm() < mRadius) ? true : false; 
    }

private:
    float mRadius;
    vec3 mCentre, mNormal;
};

The last thing is the image plane.

Image Plane

This will hold the details pertaining the plane where the pixels are ultimately projected in 3D space. For this, we only need 4 things:

  1. The lower and upper bounds of our image plane and
  2. the width and height of our image.

Finally, we want a way to take the position of the pixel and map it to 3D space. Let's look at how we accomplish this:

#pragma once
#include "icg_common.h"

class ImagePlane{
public:
    ImagePlane(vec2 const& llCorner, // The lower-left corner (l, b, 0)
               vec2 const& urCorner, // The upper-right corner (r, t, 0)
               int const& xRes, int const& yRes) : // The dimensions of the image
        l(llCorner.x()),
        b(llCorner.y()),
        r(urCorner.x()),
        t(urCorner.y()),
        mRows(xRes),
        mCols(yRes)
    { }

    inline vec3 generatePixelPos(int i, int j){
        float u = l + (r - l) * (i + 0.5f) / mRows;
        float v = b + (t - b) * (j + 0.5f) / mCols;
        return vec2(u, v, 0);
    }

private:
    int mRows, mCols;
    float l, r, b, t;
};

With all of this done, lets see how everything fits together.

Putting it all together.

At this point we are finally ready to assemble our ray tracer. For this example, suppose that we have the following:

  1. Camera set at (0, 0, -1),
  2. unit circle centred at (0, 0, 1), and
  3. image of size 200x200 spanning from (-1, -1, 0) to (1, 1, 0).

First things first, let us include our new headers. At the top of the file, enter the following:

#include "icg_common.h"
#include <Eigen/Geometry>
#include "Camera.h"
#include "Circle.h"
#include "ImagePlane.h"

Now we set up our image. Go to the declaration of MyImage and change the dimensions.

struct MyImage{
    int cols = 200 // was 640.
    int rows = 200 // was 480.
    ...
};

Next go down to main and after the firs TODO set up the camera and circle.

int main(int, char**){
    ...
    /// TODO: define camera position and sphere position here
    Circle circle(vec3(0, 0, 1), 1.0f);
    Camera camera(vec3(0, 0, -1));

Now we set our image plane. Immediately after the declaration of the circle add:

    ImagePlane plane(vec3(-1, -1, 0), vec3(1, 1, 0), image.rows, image.cols);

Finally, inside the nested for-loops, replace their current content with the algorithm to generate the ray and the test for intersection against the circle:

    for (int row = 0; row < image.rows; ++row) {
        for (int col = 0; col < image.cols; ++col) {
            // Construct u and v.
            vec3 pt = plane.generatePixelPos(row, col);
            ray3 ray = camera.generateRay(pt);
            
            image(row, col) = (circle.intersectRay(ray)) ? white() : black();
        }
    }
    ...

Compile and run your code. If everything works you should see a white circle on the screen. Congratulations! You can now ray trace a circle!

Appendices (optional!)

These parts are optional, you can read about them once you have completed the lab.

Appendix 1: Using #pragma once

Up until this point, you are probably familiar with the usage of header guards to prevent multiple inclusions of the same header. These are typically presented as

#ifndef _SOME_NAME_HERE
#define _SOME_NAME_HERE
...
#endif

The issues that crop up with this approach are the following:

  1. The macros used as header guards must be unique and
  2. they can get quite long which may lead to errors from programmers.

The easiest way of solving the first problem is by agreeing on a naming convention for the header guards. Consider this example from Boost:

// File located at boost/include/foo/bar.hpp
#ifndef BOOST_INCLUDE_FOO_BAR_HPP
#define BOOST_INCLUDE_FOO_BAR_HPP
...
#endif

It is easy to see why the names would all be unique (we can't have two files in the same directory with the same name), but it doesn't solve the length.

In order to solve this, compilers now support a new type of header guard that uses the #pragma directive. This is usually used for sending messages (see the error message for OpenCV in main.cpp) and for disabling compiler warnings. The usage as a header guard is as follows:

#pragma once
...

This form is considerably shorter and less error prone, also (at least in the beginning) they are faster than the standard header guards. At this point in time you are probably wondering "well, if it is easier, why doesn't everyone use it?" Here's why:

  1. While it is widely supported, it is not yet standard. Looking at the latest available copy of the C++ Standard, shows that pragmas "cause the implementation to behave in an implementation-defined manner. The behavior might cause translation to fail or cause the translator or the resulting program to behave in a non-conforming manner. Any pragma that is not recognized by the implementation is ignored". What this translates to is that pragmas have no standardized behaviour and are completely dependent on the currently used compiler.

  2. Not all compilers currently support it. While it is true that most widely used compilers have support for it, projects that need to support all compilers cannot use pragma once only. Generally what is done is something like this:

#ifndef _SOME_MACRO
#define _SOME_MACRO

#pragma once
...
#endif

This takes advantage of what the standard says about ignoring unknown pragmas. This way, if the compiler has no support for it, it can quite happily ignore the pragma and the code compiles properly.

The final reason pertains to the speed of using #pragma once. While they may have been originally faster, current compilers (such as MSVC and GCC) are all optimized to handle both header guards and pragmas. See here for a comparison (a bit old, since now MSVC behaves almost identically to GCC in this regard). With this in mind, there is no real difference in using pragmas or header guards.

So, after all is said and done, what is the bottom line? It is quite simple: if you are working in a framework with a given standard (such as this one) then use whatever they use. Otherwise, simply exercise your judgement. It all boils down to this:

  • If you want your code to be compiler-independent, then either use only header guards or use the combination given above.
  • If you know for sure that you will only be compiling with a compiler that supports pragmas, then you can use #pragma once.

Appendix 2: Inlining functions

Recall that when programming in C, we generally declare functions in a header file and write the implementations in a c file. In other words, we have this:

// Header.h
#ifndef HEADER_H
#define HEADER_H

void foo();

#endif

// Header.c
#include "Header.h"

void foo() { ... }

So the question now is: what happens if we declare and implement in the header?

This is a practice known as inlining functions. The reason for the name is that functions that are implemented in header files carry an (implicit) directive called inline. Consider the following example:

// We use pragma once here to save space
#pragma once

int factorial(int n) { ... }

Though I haven't explicitly written it, there is a hidden inline before the declaration of factorial. In fact, the code above is equivalent to this:

#pragma once

inline int factorial(int n) { ... }

So what is this inline business?

The inline directive is a hint to the compiler that tells it to replace all calls to this function with the contents of the function itself. In other words, instead of incurring in a function call every time we call factorial, we simply paste the body of the function where we called it. Sounds great! We save one function call and our code can run faster... or does it?

Notice that hint is in bold. The first caveat of inlining is this: it is just a hint to the compiler. That means that it is quite happy to ignore your petition to inline a function (there are ways around this, but we won't cover them here) if it doesn't think it is worth it. That means that even you inlined every function/class in your code (don't do this!!!) the compiler does not guarantee that said code will actually be inlined.

The second caveat has two parts. The first one is related to what inlining (assuming it succeeds) does. It essentially replaces the function call with the contents of said function. Now, imagine that you inline a function that is 100 lines and suppose that this function is commonly called throughout your code. Every function call would be replaced by 100 lines! Your code would grow tremendously!

This is obviously a problem, since the larger code size means that you may incur in page faults, which will ultimately slow down your program instead of optimizing it.

The second part is header dependency. Suppose we define our factorial function as follows:

inline int factorial(int n)
{
    if (n == 1) return 1;
    else return n * factorial(n - 1);
}

Notice how we don't handle the case n = 0. Further, suppose that this function is declared in a header that is included by 80% of your code. Now, say we wanted to take care of the case n = 0. We change the function to the following:

inline int factorial(int n)
{
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

We only changed the body of the function. Not the parameters or return type, just the body. Then we re-compile our code and stare in horror as 80% of our code has to be re-compiled! Why is this happening?! Simple: since we replace every call to factorial with the contents of the function, any change to the function will result in a re-compile of every file that includes that header. You can imagine this gets quite expensive in very large code bases.

So what is the bottom line here? Why/When should we inline functions? First and foremost, follow whatever standard the project you're working on uses. If you have freedom to choose, then here are some guidelines:

  1. Never inline constructors.
  2. Inline functions that are trivial (less than 3 lines of code).
  3. Never inline early on. Specifically, it is always preferable to split the declaration from the implementation and then profile your application. If your profiler shows that inlining can help, then do so. Otherwise, don't.

If you want more references on inlining, you can take a look here or take a look at "Effective C++" from Scott Meyers.