Skip to content
Karn Kaul edited this page Oct 1, 2022 · 1 revision

Introduction

A ray is a line with a fixed starting point (but no endpoint). This can be modelled using two vectors: a position and a direction.

Code

Unit Vectors

A unit vector is simply one whose length is exactly 1.0. They are useful to indicate directions, since the magnitude of such a vector is mostly irrelevant, and also for further computation as the magnitude is always 1. We shall use a right-handed coordinate system: +x goes right, +y goes up, and +z goes away from the screen (which means -z goes "ahead" in the scene), with the origin at the centre of the space. Let's establish that by defining some constant vectors:

template <typename Type>
constexpr auto zero_v = Type{};
template <typename Type>
constexpr auto one_v = static_cast<Type>(1);

template <typename Type = float>
constexpr auto left_v = Vec<Type, 3>{-one_v<Type>, zero_v<Type>, zero_v<Type>};
template <typename Type = float>
constexpr auto right_v = Vec<Type, 3>{one_v<Type>, zero_v<Type>, zero_v<Type>};
template <typename Type = float>
constexpr auto up_v = Vec<Type, 3>{zero_v<Type>, one_v<Type>, zero_v<Type>};
template <typename Type = float>
constexpr auto down_v = Vec<Type, 3>{zero_v<Type>, -one_v<Type>, zero_v<Type>};
template <typename Type = float>
constexpr auto forward_v = Vec<Type, 3>{zero_v<Type>, zero_v<Type>, -one_v<Type>};
template <typename Type = float>
constexpr auto backward_v = Vec<Type, 3>{zero_v<Type>, zero_v<Type>, one_v<Type>};

Now a class can encapsulate the unit vector invariant:

nvec3.hpp

class nvec3 {
 public:
  nvec3() = default;
  nvec3(fvec3 const vec) : m_vec{normalize(vec)} {}

  fvec3 const& vec() const { return m_vec; }
  operator fvec3 const&() const { return vec(); }

 private:
  fvec3 m_vec{forward_v<>};
};

Defaults to pointing forwards (z = -1), and provides a constructor to normalize any arbitrary input. Ideally the input should not be zero: although we've handled that in normalize, the resulting nvec3 would not really be a unit vector. (It would have otherwise held inf / NaN or thrown a hardware floating point exception for division by zero; in my opinion this is a bit more acceptable.)

Ray

With all that in hand, the class for Ray is just a POD:

struct Ray {
  fvec3 origin{};
  nvec3 direction{};
};

It will be useful to obtain a point on a ray via its parametric form: p = ray.origin + t * ray.direction, where t is a positive float. (Negative t means that the point will be in the opposite direction of the ray.)

Let's add a member function to compute that:

fvec3 at(float const t) const { return origin + t * direction.vec(); }

Viewport

It is useful to decouple the scene view from the drawing dimensions: this way objects, cameras, etc in the scene need not change even as the target image / framebuffer size changes. Conventionally the view space is normalized (0 to 1 or -1 to +1), also known as Normalized Device Coordinates in graphics lingo. Since the target image may not be 1:1, we will crop the shorter side of the viewport to match their aspect ratios. Eg, since we are using a landscape oriented output image, the viewport width will be 1.0 and height smaller than 1.0.

   400px | 1.0
+----------------+
|                |
|                |  300px | (1.0 * 300.0 / 400.0)
|                |
+----------------+

Since the scene is in 3D space, the viewport needs to be a certain positive distance from the camera / eye, which we will encode as its depth. Counter-intuitively, we will emit rays from the camera to every pixel on the viewport (not towards the eye), and determine each one's colour based on objects in the ray's path.

                viewport
                   |
          ray      |
[camera] = = = >   |
                   |
<------depth------->

Encapsulating all this into a struct:

viewport.hpp

struct Viewport {
  fvec2 extent{1.0f};
  float depth{1.0f};

  static constexpr Viewport make(uvec2 extent, float depth, float scale = 2.0f) {
    auto const ar = static_cast<float>(extent.x()) / static_cast<float>(extent.y());
    return ar > 1.0f ? Viewport{fvec2{scale, scale / ar}, depth} : Viewport{fvec2{scale * ar, scale}, depth};
  }
};

With all that in hand, we are finally ready to trace rays!

main.cpp

Create the viewport and setup the camera origin:

static constexpr auto viewport = Viewport::make(extent, 2.0f, 2.0f);
static constexpr auto origin = fvec3{};

Compute the top-left from the camera; given our right-handed coordinate system, negative Z goes into the screen:

static constexpr auto horizontal = fvec3{viewport.extent.x(), 0.0f, 0.0f};
static constexpr auto vertical = fvec3{0.0f, viewport.extent.y(), 0.0f};
static constexpr auto top_left = origin + 0.5f * (-horizontal + vertical) + fvec3{0.0f, 0.0f, -viewport.depth};

Setup a gradient:

static constexpr fvec3 gradient[] = {Rgb::from_hex(0xffffff).to_f32(), Rgb::from_hex(0x002277).to_f32()};

Modify the loop to emit a ray for each pixel, scaled to the viewport, and use the y component of its direction to lerp between the gradient:

for (std::uint32_t row = 0; row < image.extent().y(); ++row) {
  auto const yt = static_cast<float>(row) / static_cast<float>(image.extent().y() - 1);
  for (std::uint32_t col = 0; col < image.extent().x(); ++col) {
    auto const xt = static_cast<float>(col) / static_cast<float>(image.extent().x() - 1);
    auto const dir = top_left + xt * horizontal - yt * vertical - origin;
    auto const ray = Ray{origin, dir};
    auto const t = 0.5f * (ray.direction.vec().y() + 1.0f);
    image[{row, col}] = Rgb::from_f32(lerp(gradient[0], gradient[1], t));
  }
}

ray

Housekeeping

There were a couple of small bugfixes / changes in the original commits, which are listed below. The wiki pages have since been updated to incorporate the fixes.

rgb.hpp

Fixed originally reversed / and * logic:

static constexpr float to_f32(std::uint8_t channel) { return static_cast<float>(channel) / 0xff; }
static constexpr std::uint8_t to_u8(float channel) { return static_cast<std::uint8_t>(channel * 0xff); }

vec.hpp

Added missing friend function for operator-:

friend constexpr Vec operator-(Vec const& v) {
  auto ret = v;
  ret = -ret;
  return ret;
}
Clone this wiki locally