A Minimal Ray-Tracer: Rendering Simple Shapes (Sphere, Cube, Disk, Plane, etc.)

News (August, 31): We are working on Scratchapixel 3.0 at the moment (current version of 2). The idea is to make the project open source by storing the content of the website on GitHub as Markdown files. In practice, that means you and the rest of the community will be able to edit the content of the pages if you want to contribute (typos and bug fixes, rewording sentences). You will also be able to contribute by translating pages to different languages if you want to. Then when we publish the site we will translate the Markdown files to HTML. That means new design as well.

That's what we are busy with right now and why there won't be a lot of updates in the weeks to come. More news about SaP 3.0 soon.

We are looking for native Engxish (yes we know there's a typo here) speakers that will be willing to readproof a few lessons. If you are interested please get in touch on Discord, in the #scratchapixel3-0 channel. Also looking for at least one experienced full dev stack dev that would be willing to give us a hand with the next design.

Feel free to send us your requests, suggestions, etc. (on Discord) to help us improve the website.

And you can also donate). Donations go directly back into the development of the project. The more donation we get the more content you will get and the quicker we will be able to deliver it to you.

Ray-Plane Intersection

Figure 1: ray-plane intersection.

In this chapter we will learn how to compute the intersection of a ray with a plane and a disk. We know from the lesson on Geometry that the dot (or scalar) product of two vectors which are perpendicular to each other is always equal to 0:

$$A \cdot B = 0$$

Again this is only true if A and B are perpendicular. A plane can be defined as a point representing how far the plane is from the world origin and a normal (defining the orientation of the plane). Let's call this point $p_0$ and the plane normal $n$. A vector can be computed from any point on the plane by subtracting $p_0$ from this point which we will call $p$. Since the vector resulting from this subtraction lies in the plane, it should be perpendicular to the plane's normal, thus using the property that the dot product of two perpendicular vectors is equal to 0, we can write (equation 1):

$$(p - p_0) \cdot n = 0$$

Similarly, a ray can be defined using the following parametric form (equation 2):

$$l_0 + l * t = p$$

where $l_0$ is the origin of the ray and $l$ is the ray direction. This only means that we can compute the position of any point along the ray from the ray's origin, its direction and the term $t$ which is a positive real number (which as usual, is the parametric distance from the origin of the ray to the point of interest along the ray). If the ray and the plane intersect, then they share a point, the point where the line intersects the plane. If this point is $p$, we can insert equation 2 in equation 1, and we get:

$$(l_0 + l * t - p_0) \cdot n = 0$$

What we are interested in here, is to compute a value for $t$ from which we can compute the position of this intersection point using the ray parametric equation. Solving for $t$ we get:

$$\begin{array}{l}l * t \cdot n + (l_0 - p_o) \cdot n = 0\\t = -{\dfrac{(l_0-p_0)\cdot n}{l \cdot n}} = {\dfrac{(p_0 - l_0) \cdot n}{l \cdot n}}\end{array}$$

Note that the plane and the ray are parallel when the denominator (the term $l \cdot n$ gets close to 0. Either the plane and the ray perfectly coincide in which case there is an infinity of solutions or the ray is away from the plane in which case there is no intersection. Generally in a C++ implementation, when the denominator is lower than a very small value, we simply return false (no intersection was found).

bool intersectPlane(const Vec3f &n, const Vec3f &p0, const Vec3f &l0, const Vec3f &l, float &t) { // assuming vectors are all normalized float denom = dotProduct(n, l); if (denom > 1e-6) { Vec3f p0l0 = p0 - l0; t = dotProduct(p0l0, n) / denom; return (t >= 0); } return false; }

Ray-Disk Intersection

Figure 2: ray-disk intersection.

The ray-disk intersection routine is very simple. A disk is generally defined by a position (the disk center's position), a normal and a radius. First we can test if the ray intersects the plane in which lies the disk. For the ray-plane intersection step, we can simply use the code we have developed for the ray-plane intersection test. If the ray intersects this plane, all we have to do is to compute the intersection point, then compute the distance from this point to this disk's center. If this distance is lower or equal to the disk radius, then the ray intersects the disk. Note that as an optimisation, you can test the square of the distance against the square of the disk's radius. The square distance can be computed from the dot product of this vector (v in the code) with itself. Technically, computing the distance would require to take the square root of this dot product. However we can also test the result of this dot product directly against the square of the radius (which is generally precomputed) to avoid using a square root operation which is expensive.

bool intersectDisk(const Vec3f &n, const Vec3f &p0, const float &radius, const Vec3f &l0, const Vec3 &l) { float t = 0; if (intersectPlane(n, p0, l0, l, t)) { Vec3f p = l0 + l * t; Vec3f v = p - p0; float d2 = dot(v, v); return (sqrtf(d2) <= radius); // or you can use the following optimisation (and precompute radius^2) // return d2 <= radius2; // where radius2 = radius * radius } return false; }