August 4th 2009

Interactive RayTracer 1: A barebone raytracer

1 Star2 Stars3 Stars4 Stars5 Stars (1 votes, average: 1.00 out of 5)
Loading...Loading...

At last, I’m starting with my first post on my attempt on Interactive RayTracing. This first one will only be on the generic global implementation.

A matrix library must be used, the same basis class will be used for each element, point or color, but with a different size (if needed). I will use a typedef for defining each of them. This will help explaining what is going on. I will not explain the code of the library, althoug the optimization of the raytracer will surely have to be done in this part of the code as well.

So a vector will be named (for the moment) Vector3df, a point Point3df, a normal Normal3df and a color Color. All elements will live in the IRT namespace.

Basic classes

These basis classes, with the ray class that will be shown later, are what makes the raytracer work. The primitives describe what objects are in the scene, which color they have, if they are transprent, if they reflect light, … The scene itself is a smart container for the primitives and the lights which can allow for fast ray traversal (if implemeted). The raytracer traces rays through the scene and saves the result in an image.

First the Ray class is only an aggregation of two data, a vector and a starting point.

  class Ray
  {
  private:
    Point3df origin_;
    Vector3df direction_;

  public:
    _export_tools Ray(const Point3df& origin, const Vector3df& direction);

    _export_tools ~Ray();

    const Point3df& origin() const
    {
      return origin_;
    }
    Point3df& origin()
    {
      return origin_;
    }
    const Vector3df& direction() const
    {
      return direction_;
    }
    Vector3df& direction()
    {
      return direction_;
    }
  };

The _export_tools macro is used for Windows shared libraries, where you have to explicitely tell which function is to be exported (or imported). This will change in the future when the library will be completely template.

  Ray::Ray(const Point3df& origin, const Vector3df& direction)
    :origin_(origin), direction_(direction)
  {
  }

  Ray::~Ray()
  {
  }

Primitives

A primitive is an object in the scene. For the moment, it will be a pure virtual class (an interface) with some methods, like the intersection as well as the color computation.

  class Primitive
  {
  public:
    virtual ~Primitive()
    {}

    virtual bool intersect(const Ray& ray, float& dist) = 0;

    virtual void computeColorNormal(const Ray& ray, float dist, Color& color, Vector3df& normal) = 0;
  };

My first primitive will be a Sphere with a uniform color. The most complex computation here is the intersection between a ray and the sphere:

  Sphere::Sphere(const Point3df& center, float radius)
    :center(center), radius(radius), color(1.f)
  {}

  void Sphere::setColor(const Color& color)
  {
    this->color = color;
  }

  bool Sphere::intersect(const Ray& ray, float& dist)
  {
    float A = 1.f;
    float B = (ray.direction() * (ray.origin() - center));
    float C = norm2(ray.origin() - center) - radius * radius;

    float delta = (B * B - A * C);

    if(delta < 0.f)
      return false;
    float disc = sqrt(delta);
    if(dist = - (B + disc) < 0.)
      dist = - (B - disc);
    return true;
  }

The idea is to consider a straight line parametered with dist. With this equation, we add to the system the equation of the sphere. This way, a second degree polynomial must be solved, and the smallest positive root is the solution (if it exists). To simplify computation, the direction vector is considered unitary.

Now, the color can be computed, as well as the normal (very easy to do with a sphere). In fact the normal is not used here, but will be used in the future.

  void Sphere::computeColorNormal(const Ray& ray, float dist, Color& color, Normal3df& normal)
  {
    Vector3df collide(ray.origin() + dist*ray.direction());
    normal = collide - center;
    normal *= 1./(sqrt(norm2(normal)));

    color = this->color;
  }

The scene

The scene stores the primitives. Primitives may be added, deleted or accessed. BEsides, the scene is in charge of finding objects hit by a ray with the method getFirstCollision().
simple_scene.h

  class SimpleScene
  {
  private:
    std::vector primitives;

  public:
    _export_tools SimpleScene();

    _export_tools ~SimpleScene();

    _export_tools Primitive* getPrimitive(unsigned long index);

    _export_tools Primitive* removePrimitive(unsigned long index);

    _export_tools long getFirstCollision(const Ray& ray, float& dist);

    _export_tools bool addPrimitive(Primitive* primitive);
  };

The scene is also in charge of deleting the primitives it stores. When a new primitive is added to the scene, it is charge of its life time.
Here is the actual code:

  SimpleScene::SimpleScene()
    :primitives()
  {
  }

  SimpleScene::~SimpleScene()
  {
    for(std::vector::const_iterator it = primitives.begin(); it != primitives.end(); ++it)
      delete *it;
  }

  Primitive* SimpleScene::getPrimitive(unsigned long index)
  {
    return primitives[index];
  }

Wen a primitive is removed from the scene, it is returned to the user:

  Primitive* SimpleScene::removePrimitive(unsigned long index)
  {
    std::vector::iterator it = primitives.begin();
    std::advance(it, index);
    Primitive* primitive = *it;
    primitives.erase(it);
    return primitive;
  }

Now, this is the main kernel. In this case, each primitive is tested and if the ray hits the primitive, the distance from the ray to the primitive is updated in dist and the primitive index is returned.

  long SimpleScene::getFirstCollision(const Ray& ray, float& dist)
  {
    float min_dist = std::numeric_limits::max();
    long min_primitive = -1;

    for(std::vector::const_iterator it = primitives.begin(); it != primitives.end(); ++it)
    {
      float dist;
      bool test = (*it)->intersect(ray, dist);

      if(test)
      {
        min_primitive = it - primitives.begin();
        min_dist = dist;
      }
    }

    if(min_primitive == -1)
      return -1;
    else
    {
      dist = min_dist;
      return min_primitive;
    }
  }

  bool SimpleScene::addPrimitive(Primitive* primitive)
  {
    if(std::find(primitives.begin(), primitives.end(), primitive) != primitives.end())
      return false;

    primitives.push_back(primitive);
    return true;
  }

A primitive cannot be added twice to the the scene. If the insertion fails, false is returned by addPrimitive().

The raytracer

A raytracer can use any scene from the moment it is possible to get the primitive hit by a ray.

  class Raytracer
  {
  private:
    void generateRay(unsigned long x, unsigned long y, Ray& ray) const;

    void computeColor(const Ray& ray, Color& color) const;

    void updateParameters();
  public:
    _export_tools Raytracer(unsigned long pixelWidth, unsigned long pixelHeight, float width, float height, float depth);

    _export_tools ~Raytracer();

    _export_tools void draw(float* screen) const;

    _export_tools void setViewer(float width, float height, const Vector3df& origin, const Vector3df& direction);

    _export_tools void setResolution(unsigned long pixelWidth, unsigned long pixelHeight);

    _export_tools void setScene(SimpleScene* scene);

  private:
    Point3df origin;
    Vector3df direction;
    unsigned long pixelWidth;
    unsigned long pixelHeight;
    float width;
    float height;
    float depth;

    float precompWidth;
    float precompHeight;

    SimpleScene* scene;
  };

Besides the key raytracer methods, this class provides an interface to set the camera (even if the code does not do this at the moment).

First, a screen has a height and a width with a number of pixels for those. The screen is at some distance from the camera (the depth).

  Raytracer::Raytracer(unsigned long pixelWidth, unsigned long pixelHeight, float width, float height, float depth)
    :origin(0.), direction(0.), pixelWidth(pixelWidth), pixelHeight(pixelHeight), width(width), height(height), depth(depth)
  {
    direction(2) = 1;
    updateParameters();
  }

  Raytracer::~Raytracer()
  {
  }

With these elements, some additional parameters are precomputed.

  void Raytracer::generateRay(unsigned long x, unsigned long y, Ray& ray) const
  {
    ray.direction()(0) = precompWidth * (x - pixelWidth / 2.f);
    ray.direction()(1) = precompHeight * (y - pixelHeight / 2.f);
    ray.direction()(2) = depth;

    ray.direction() *= 1./(sqrt(norm2(ray.direction())));
  }

This method builds an unitary ray in place. Based on the position of the camera, a ray to each pixel is generated.

  void Raytracer::draw(float* screen) const
  {
    Ray ray(origin, direction);
    for(unsigned long j = 0; j < pixelHeight; ++j)
    {
      for(unsigned long i = 0; i < pixelWidth; ++i)
      {
        generateRay(i, j, ray);
        Color color(0.);

        computeColor(ray, color);

        for(unsigned int k = 0; k < nbColors; ++k)
          screen[nbColors * (j* pixelWidth + i) + k] = color(k);
      }
    }
  }

This was the main method to draw an image from the scene. It works by computing the color for each ray.

To get this color, the following method searches the scene for the nearest hit object. If one is found, the color is generated by computeColorNormal(). This method is also the one that will be in charge of the secondary rays and shadows, but this is for another post.

  void Raytracer::computeColor(const Ray& ray, Color& color) const
  {
    float dist;
    long index = scene->getFirstCollision(ray, dist);
    if(index < 0)
      return;

    Normal3df normal;
    Primitive* primitive = scene->getPrimitive(index);
    primitive->computeColorNormal(ray, dist, color, normal);
  }

The next methods help updating the raytracer.

  void Raytracer::setScene(SimpleScene* scene)
  {
    this->scene = scene;
  }

  void Raytracer::setResolution(unsigned long pixelWidth, unsigned long pixelHeight)
  {
    this->pixelWidth = pixelWidth;
    this->pixelHeight = pixelHeight;

    updateParameters();
  }

  void Raytracer::setViewer(float width, float height, const Vector3df& origin, const Vector3df& direction)
  {
    this->width = width;
    this->height = height;
    this->origin = origin;
    this->direction = direction;

    updateParameters();
  }

  void Raytracer::updateParameters()
  {
    precompWidth = width / pixelWidth;
    precompHeight = height / pixelHeight;
  }

Coming next

It is now possible to use the library for a basic rendering in C++, but I've decided to add a small Python wrapper to allow for more easy testing. This will be the main topic of my next post on raytracing.

Link to Launchpad.

Buy Me a Coffee!



Other Amount:



Your Email Address :



Tags: ,

10 Comments »

10 Responses to “Interactive RayTracer 1: A barebone raytracer”

  1. rpg.314 on 09 Aug 2009 at 11:44 AM #

    For the purpose of managing your vectors (not std::vectors), points and other fixed sized arrays, I strongly recommend that you have a look at Eigen Library.

    http://eigen.tuxfamily.org/index.php?title=Main_Page

    With this, you get lots of goodies for free and the API could not have been possibly designed better.

  2. rpg.314 on 09 Aug 2009 at 11:48 AM #

    Personally, I am of the opinion that doubles are not required. Are floats not enough or you chose doubles just to be on the safe side?

  3. Matt on 09 Aug 2009 at 1:55 PM #

    I know of Eigen (although I didn’t know about it at the time). As I don’t need more than what I have at the moment, I don’t see the need of using a more complicated library, which meand another big dependency. Safe from the fact that there are far more maths function available, there is no added value for me, as I use fixed-size arrays, which means that the compiler can optimize as it sees fits.

    To sum up what I’m saying, if I have at one point the need for a better library, I will test several implementations with a simple test. It’s rather simple, in fact: fixed-size arrays must be implemented statically (no new). If the library can support that, it can replace my own math library.

  4. Matt on 09 Aug 2009 at 1:56 PM #

    I use floats for the computation.

  5. rpg.314 on 09 Aug 2009 at 7:09 PM #

    You may already know this, but, if the size of the array is known at compile time then space is allocated statically in Eigen. And yes, optimization in eigen is way better than what compilers (atleast gcc) can do.

    Though as you said, you may not need it for now. But I am sure that if you need to use it, you’s like it very much.

  6. Matt on 09 Aug 2009 at 9:14 PM #

    Optimization is done by the compiler based on information given by the library. So if gcc cannot optimize, Eigen will not help much! Eigen performance is mainly based on the restrict keyword which I already blogged about. If the compiler proposes it, it can achieve great performance. For the moment, I do not require Eigen because I already can use every optimization process Eigen exposes (restrict is not useful for local stack variables, and besides, some compilers don’t recognie restrict when it is not a attribute of a function parameter, like what Blitz used to do).

  7. rpg.314 on 10 Aug 2009 at 6:52 AM #

    Optimization is done by the compiler based on information given by the library. So if gcc cannot optimize, Eigen will not help much!

    It seems you are unaware of eigen’s use of expression templates. Look here for the mechanism and to see the kind if code that can be generated.

    http://eigen.tuxfamily.org/dox/InsideEigenExample.html

  8. Matt on 10 Aug 2009 at 7:51 PM #

    I know what expression templates are (I’ve implemented them in one of the first versions of my math library back when I did my PhD, there are still some headers left in IRT). Expression templates are based on what a compiler can do. Here, the information is that instead of doing u = v+ w in a brute-force approach, you create a temporary object that will contain the fact that v and w are to be added, and then you use the assignment operator to resize u and iterate inside the temporary object to create the actual sum. If the compiler know that there is no pointer alias (restrict/fixed-size data for instance), it can generate vectorized code (this is what icpc would do for instance).
    Expression templates are only a way to express something to the compiler, I think you and I can agree on this matter. The only difference is that I don’t consider this optimization is to be better than what a compiler can do. It’s a another way of programming, a known C++ way of expressing the fact that big temporary can be avoided, and then the compiler uses this knowledge to output vectorized code. If you don’t express this, the compiler may output vectorized code, but not as optimized, and without the vectorization, the speed gain of expression templates is not that impressive.

  9. vic on 06 Dec 2009 at 12:38 PM #

    What about other primitives? (like Box/Cone/Triangle?)

  10. Matt on 06 Dec 2009 at 12:51 PM #

    At the time of the tutorial, I didn’t need other primitives. They can be easilly added though, the interface is really easy to implement.

Trackback URI | Comments RSS

Leave a Reply

« | »

  • Blog Vitals

    Blog Stats
    6,592,282
    192
    213
    44
  • Advertisement