Link Search Menu Expand Document

6: Planes and Boxes


Download code
06-more-shapes.zip
Live example
examples/06-more-shapes/index.html

We’re going to add support for two new shapes to our ray-tracer.

Plane

A plane is an infinite flat shape. A plane has no thickness; it’s a boundary that divides the world in half, with half of the world being “inside” the plane and the other half being “outside”. To create a plane, we specify the normal direction (i.e. if we were standing on the plane, which way would be “up”), and the distance from the origin measured along that normal.

For example, to create an infinite “floor” in our scene, we create a new plane whose normal vector is <0,1,0> (i.e. straight up), at a distance of 0 from the origin.

Here’s the code for the plane:

// modules/shapes/plane.js

import { Shape } from '../shape.js';

export class Plane extends Shape {

    constructor(normal, distance, appearance) {
        super(appearance);
        this.normal = normal;
        this.distance = distance;
    }

    intersect = ray => {
        let angle = ray.direction.dot(this.normal);
        // if the dot-product is zero, the ray is perpendicular to the plane's normal,
        // therefore the ray is parallel to the plane and will never intersect.
        if (angle == 0) return [];

        let b = this.normal.dot(ray.start.subtract(this.normal.scale(this.distance)));
        return [-b / angle];
    };

    getNormalAt = _ => this.normal;
}

Box

We’re also going to add a box - a rectangular box defined by two opposite corners.

For now, boxes will always be aligned with the x/y/z axes; we’ll look at how to rotate shapes later in the workshop.

The box is actually pretty simple, but most operations have to be repeated three times – once for each axis.

There is an excellent interactive visualisation of the intersection algorithm used for boxes at graphicscompendium.com:

https://graphicscompendium.com/raytracing/16-boxes

Here’s the code for the box:

// modules/shapes/box.js

import { Shape } from '../shape.js';
import { THRESHOLD } from '../settings.js';
import { Vector } from '../vector.js';

const axes = ['x', 'y', 'z'];

export class Box extends Shape {

    constructor(corner1, corner2, appearance) {
        super(appearance);
        this.lowerCorner = new Vector(Math.min(corner1.x, corner2.x), Math.min(corner1.y, corner2.y), Math.min(corner1.z, corner2.z));
        this.upperCorner = new Vector(Math.max(corner1.x, corner2.x), Math.max(corner1.y, corner2.y), Math.max(corner1.z, corner2.z));
        this.vertices = [this.lowerCorner, this.upperCorner];
    }

    contains = (point, axis) => this.lowerCorner[axis] < point[axis] && point[axis] < this.upperCorner[axis];

    intersectOnAxis = (axis, ray) => {
        let [o1, o2] = axes.filter(a => a != axis);
        let intersections = new Array();
        if (ray.direction[axis] == 0) return [];
        this.vertices.forEach(vertex => {
            let intersect = (vertex[axis] - ray.start[axis]) / ray.direction[axis];
            let point = ray.start.add(ray.direction.scale(intersect));
            if (this.contains(point, o1) && this.contains(point, o2)) intersections.push(intersect);
        });
        return intersections;
    }

    intersect = (ray) => {
        return this.intersectOnAxis('x', ray)
            .concat(this.intersectOnAxis('y', ray))
            .concat(this.intersectOnAxis('z', ray));
    }

    getNormalAt = (pos) => {
        if (Math.abs(this.lowerCorner.x - pos.x) < THRESHOLD) return Vector.X.invert();
        if (Math.abs(this.upperCorner.x - pos.x) < THRESHOLD) return Vector.X;
        if (Math.abs(this.lowerCorner.y - pos.y) < THRESHOLD) return Vector.Y.invert();
        if (Math.abs(this.upperCorner.y - pos.y) < THRESHOLD) return Vector.Y;
        if (Math.abs(this.lowerCorner.z - pos.z) < THRESHOLD) return Vector.Z.invert();
        if (Math.abs(this.upperCorner.z - pos.z) < THRESHOLD) return Vector.Z;
        throw (new Error(`The point ${pos.toString()} is not on the surface of ${this.toString()}`));
    }
    toString = () => `box(${this.lowerCorner.toString()}, ${this.upperCorner.toString()})`;
}

A scene with more shapes

Now modify main.js to add a plane and a box to our scene:

// main.js

import { Renderer } from './modules/renderer.js';
import * as ExampleScenes from './scenes/examples.js';

let canvas = document.getElementById('my-canvas');
let ctx = canvas.getContext('2d');
let renderer = new Renderer(canvas.width, canvas.height);

function paintPixel(x, y, color) {  
  ctx.fillStyle = color.html;
  ctx.fillRect(x, y, 1, 1);
}

let scene = ExampleScenes.AssortedShapes();
renderer.render(scene, paintPixel);

Reload the browser, and you should see something like this:

image-20220320011118565

Nice. Except… it’s kinda hard to tell whether the shapes are floating in space or resting on the plane, right? That’s because none of our shapes is casting any shadows. We’ll add shadows in the next section.


Download code
06-more-shapes.zip
Live example
examples/06-more-shapes/index.html