Last time on my ray tracer journey I made a simple C++20 program that could trace rays from a camera and test if they intersect with a sphere.

This time I will make the scene more interesting using UV coordinates.

UV coordinates of a Sphere

When a ray hits a sphere we can use some simple trigonometry to find the intersection point’s Uv coordinates on the sphere. Again, I will just link to Scratch a Pixel instead of repeating the math.

Let’s add a uv member to the Intersection class:

struct Intersection
{
    ...
    Vec2 uv;
};

To be able to visualize the UV coordinates I calculate a checker pattern:

Vec4 checkerPattern(
    const Vec2 &uv, 
    const int checker_count, 
    const Vec4 dark, 
    const Vec4 bright
){
    const auto checker_x = int(uv.x * checker_count) % 2;
    const auto checker_y = int(uv.y * checker_count) % 2;

    const float checker = checker_x ^ checker_y;

    return bright * checker + dark * (1-checker);
}

Then I can update the colorization code to use the checker pattern:

const auto darkChecker = Vec4{0.5,0.5,0.5,0};
const auto lightChecker = Vec4{0.8,0.8,0.8,0};

if (intersection.has_value())
{
    const auto checker = checkerPattern(intersection->uv, 8, darkChecker, lightChecker);

    setPixel(x, y, checker);
}

Running the updated code results in this checker patter on the sphere:

Square and Object

Spheres are great but I decided it would be nice to have more variety in the scene, so next up is adding squares.

struct Square
{
    Vec3 p;
    Vec3 n;
    Vec3 right;
    float size;
};

A square is described by its center, normal vector, tangential vector (I called it right) and size.

Computing the intersection between a square and a ray should be easy, so I also omit the math for that. There are many sources detailing it online.

The interface for the intersection testing is then:

std::optional<Intersection> getIntersection(const Ray &ray, const Square &square);

Now I would like to be able to handle squares and circles in a generic way so I introduce an abstraction, objects:

using Object = std::variant<Circle, Square>;

Using std::variant over virtual inheritance has the advantage of not needing dynamic allocation and easier to use in GPU code later.

Later if I add more object types I can just extend this definition.

To dispatch an intersection call to the correct type contained in the variant I use std::visit:

const Object obj = ...;

const auto intersection = std::visit([&](auto&& o) {return getIntersection(ray, o);}, obj);

Rendering many objects

Let’s replace our single sphere with a list of objects:

std::vector<Object> objects;

objects.emplace_back(Square{
    .p = Vec3{0,0,2.5},
    .n = Vec3{0,0,-1},
    .right = Vec3{1,0,0},
    .size = 1,
});

...

Let’s factor out the ray casting from the rendering, while making sure to find the closest intersection:

std::optional<Intersection> cast(const Ray &ray)
{
    std::optional<Intersection> best = std::nullopt;

    for (const auto &obj : objects)
    {
        const auto intersection = std::visit([&](auto&& o) {return getIntersection(ray, o);}, obj);

        if (!intersection.has_value()) continue;
        
        if (!best.has_value() || best->t > intersection->t)
        {
            best = intersection;
        }
    }

    return best;
}

Putting it all together yields a render of a checkerboard room:

This time I introduced a new abstraction to be bale to handle different kinds of objects. I also extended the renderer with squares. Next up I will introduce basic materials to the renderer.