Ray Tracing S01E03


The vec3 Class

In episode 1 I made a simple image by assigning rgb values to individual variables in a loop across the x- and y-coordinates.

This example produces the same image, but it introduces the vec3 class to perform calculations with 3-dimensional vectors. To keep it simple, this class can be used for x, y, z-coordinates as well as r, g, b-color values. There are getter methods for both x, y, and z as well as r, g, b.

The complete example is also available in my git repo at github.com/celeph/ray-tracing.

// vec3.h
#ifndef VEC3H
#define VEC3H

#include <math.h>
#include <stdlib.h>
#include <iostream>

class vec3 {
public:
  float e[3];

  vec3() {}
  vec3(float e0, float e1, float e2) {
    e[0] = e0;
    e[1] = e1;
    e[2] = e2;
  }

  inline float x() const { return e[0]; }
  inline float y() const { return e[1]; }
  inline float z() const { return e[2]; }

  inline float r() const { return e[0]; }
  inline float g() const { return e[1]; }
  inline float b() const { return e[2]; }

  inline const vec3& operator+() const { return *this; }
  inline vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); }
  inline float operator[](int i) const { return e[i]; }
  inline float& operator[](int i) { return e[i]; }

  inline vec3& operator+=(const vec3 &v2);
  inline vec3& operator-=(const vec3 &v2);
  inline vec3& operator*=(const vec3 &v2);
  inline vec3& operator/=(const vec3 &v2);
  inline vec3& operator*=(const float t);
  inline vec3& operator/=(const float t);

  inline float length() const {
    return sqrt(e[0]*e[0] + e[1]*e[1] + e[2]*e[2]);
  }

  inline float squared_length() const {
    return e[0]*e[0] + e[1]*e[1] + e[2]*e[2];
  }

  inline void make_unit_vector();
};

inline std::istream& operator>>(std::istream &is, vec3 &t) {
  is >> t.e[0] >> t.e[1] >> t.e[2];
  return is;
}

inline std::ostream& operator<<(std::ostream &os, const vec3 &t) {
  os << t.e[0] << " " << t.e[1] << " " << t.e[2];
  return os;
}

inline void vec3::make_unit_vector() {
  float k = 1.0 / sqrt(e[0]*e[0] + e[1]*e[1] + e[2]*e[2]);
  e[0] *= k;
  e[1] *= k;
  e[2] *= k;
}

inline vec3 operator+(const vec3 &v1, const vec3 &v2) {
  return vec3(v1.e[0] + v2.e[0], v1.e[1] + v2.e[1], v1.e[2] + v2.e[2]);
}

inline vec3 operator-(const vec3 &v1, const vec3 &v2) {
  return vec3(v1.e[0] - v2.e[0], v1.e[1] - v2.e[1], v1.e[2] - v2.e[2]);
}

inline vec3 operator*(const vec3 &v1, const vec3 &v2) {
  return vec3(v1.e[0] * v2.e[0], v1.e[1] * v2.e[1], v1.e[2] * v2.e[2]);
}

inline vec3 operator/(const vec3 &v1, const vec3 &v2) {
  return vec3(v1.e[0] / v2.e[0], v1.e[1] / v2.e[1], v1.e[2] / v2.e[2]);
}

inline vec3 operator*(float t, const vec3 &v) {
  return vec3(t * v.e[0], t * v.e[1], t * v.e[2]);
}

inline vec3 operator/(const vec3 &v, float t) {
  return vec3(v.e[0] / t, v.e[1] / t, v.e[2] / t);
}

inline vec3 operator*(const vec3 &v, float t) {
  return vec3(t * v.e[0], t * v.e[1], t * v.e[2]);
}

inline float dot(const vec3 &v1, const vec3 &v2) {
  return v1.e[0] * v2.e[0] + v1.e[1] * v2.e[1] + v1.e[2] * v2.e[2];
}

inline vec3 cross(const vec3 &v1, const vec3 &v2) {
  return vec3(
    (v1.e[1] * v2.e[2] - v1.e[2] * v2.e[1]),
    (-(v1.e[0] * v2.e[2] - v1.e[2] * v2.e[0])),
    (v1.e[0] * v2.e[1] - v1.e[1] * v2.e[0])
  );
}

inline vec3& vec3::operator+=(const vec3 &v) {
  e[0] += v.e[0];
  e[1] += v.e[1];
  e[2] += v.e[2];
  return *this;
}

inline vec3& vec3::operator*=(const vec3 &v) {
  e[0] *= v.e[0];
  e[1] *= v.e[1];
  e[2] *= v.e[2];
  return *this;
}

inline vec3& vec3::operator/=(const vec3 &v) {
  e[0] /= v.e[0];
  e[1] /= v.e[1];
  e[2] /= v.e[2];
  return *this;
}

inline vec3& vec3::operator-=(const vec3 &v) {
  e[0] -= v.e[0];
  e[1] -= v.e[1];
  e[2] -= v.e[2];
  return *this;
}

inline vec3& vec3::operator*=(const float t) {
  e[0] *= t;
  e[1] *= t;
  e[2] *= t;
  return *this;
}

inline vec3& vec3::operator/=(const float t) {
  float k = 1.0/t;
  e[0] *= k;
  e[1] *= k;
  e[2] *= k;
  return *this;
}

inline vec3 unit_vector(vec3 v) {
  return v / v.length();
}
#endif


// vec3-example.cpp
#include <iostream>
#include "vec3.h"
int main() {
  int nx = 200;
  int ny = 100;
  std::cout << "P3\n" << nx << " " << ny << "\n255\n";
  for (int j = ny-1; j &gt;= 0; j--) {
    for (int i = 0; i &lt; nx; i++) {
      vec3 col( float(i) / float(nx), float(j) / float(ny), 0.2 );
      int ir = int(255.99 * col[0]);
      int ig = int(255.99 * col[1]);
      int ib = int(255.99 * col[2]);
      std::cout << ir << " " << ig << " " << ib << "\n";
    }
  }
}

vec3 with Javascript and Canvas

The beauty of C++ is that it supports operator overloading and allows you to add two 3D vectors as easily as vsum = v1 + v2 or multiply them as easily as vproduct = v1 * v2. Similarly, you can perform operations like vsum += v etc. This makes it a perfect language choice for 3D or other type computation while keeping the code short and legible.

Unfortunately, Javascript doesn’t support the same type of overloading. For each operation I’ll have to define a function add(), mul(), div() instead.

This is not a huge problem, but it makes the code a bit less easy to read when there are multiple add, multiply, divide operations chained in one longer expression.

Maybe there are more elegant solutions? Let me know if you know of any better ways to do it. I’d love to update my old-school Javascript skill set. I know kids these days do it differently. :)

Here’s my example in Javascript:

Coming next: S01E04 - The Ray Class

Or go back to: S01E02 - Other Image Formats with stb_image.h

Or start over at the beginning: S01E01 - Introduction

Credits and Further Reading