Rendering

Ray Tracer

A recursive ray tracer with Phong illumination, texture mapping, shadows, reflections, refractions, and ray-differential-based anti-aliasing.

C++Ray TracingAnti-Aliasing
Mirror primitives with recursive reflections
Recursive mirror reflections with stratified supersampling, 64 samples per pixel.

Overview

Ray tracing renders an image by simulating light in reverse. For each pixel, a ray is shot from the camera into the scene, the closest intersection is found, and the surface color is computed from the lights it can see. Mirrors and glass recurse: reflection rays bounce off surfaces and refraction rays bend through them, and the results blend into the final color.

Cast

Primary rays

One ray per sub-pixel sample, from the camera through the image plane; intersect against spheres, cones, cubes, and meshes.

Shade

Phong illumination

Ambient, diffuse, and specular terms accumulated over point, directional, and spot lights.

Shadow

Shadow rays

Before adding each light's contribution, a ray is cast toward the light. If geometry blocks it, the light is skipped.

Recurse

Reflection & refraction

Mirror rays reflect about the normal; refraction follows Snell's law, falling back to total internal reflection when no exit angle exists.

Phong illumination model
$$I = k_a I_a + \sum_{\text{lights}} \left[k_d \max(\mathbf{N}\cdot\mathbf{L}, 0) + k_s \max(\mathbf{R}\cdot\mathbf{V}, 0)^s\right] I_l$$

Ambient \((k_a)\) approximates indirect light; diffuse \((k_d)\) scales with how directly the light hits the surface; specular \((k_s)\) creates highlights where the reflected light direction \(R\) aligns with the view direction \(V\), sharpened by the shininess exponent \(s\).

Texture mapping with shadows
Texture mapping with shadow rays and stratified supersampling.
Snell's law
$$\eta_1\sin\theta_1 = \eta_2\sin\theta_2$$

Texture aliasing & ray differentials

The sliders below show a classic failure mode: a checkerboard receding into the distance shimmers into noise. The cause: a single distant pixel covers a huge area of texture, but a naive lookup samples just one point of it, so neighboring pixels land on unrelated texels.

The fix has two parts:

Mipmaps

Pre-filtered pyramid

The texture is repeatedly downsampled into a pyramid, so a correctly averaged version already exists for any footprint size.

Ray differentials

Track the pixel footprint

Alongside each ray, track how its hit point moves when the ray is offset by one pixel. This gives the exact texture-space footprint each pixel covers, which selects the mip level.

Mip level selection
$$\lambda = \log_2\left( \max\left(\left\lVert\frac{\partial \mathbf{uv}}{\partial x}\right\rVert, \left\lVert\frac{\partial \mathbf{uv}}{\partial y}\right\rVert\right) \cdot \text{texture size}\right)$$

Sampling the two nearest mip levels bilinearly and blending between them (trilinear interpolation) produces the smooth, stable result on the right of each slider.

Only bilinear filtering Mipmaps with trilinear interpolation
Only bilinear filtering Mipmaps with trilinear interpolation
Only bilinear filtering Mipmaps with trilinear interpolation

Ray differentials & the chain of spaces

A ray differential is the answer to a simple question: if this ray had come from one pixel over, where would everything land instead? Formally, each ray carries four extra vectors: the derivatives of its origin and direction with respect to the pixel coordinates, \(\partial\mathbf{o}/\partial x\), \(\partial\mathbf{d}/\partial x\), \(\partial\mathbf{o}/\partial y\), \(\partial\mathbf{d}/\partial y\). To compute them, differentiate every transform a ray passes through on its way from a pixel index to a point on a surface.

Pixel space

Integer grid

A pixel index \((i, j)\) with the origin at the top-left, where rows grow downward, so y will need a flip.

NDC

Normalized device coords

Resolution-independent coordinates in \([-1, 1]^2\), centered on the image, y pointing up.

Camera space

Point on the film plane

NDC scaled by the field of view and aspect ratio onto a virtual film plane one unit in front of the eye.

World space

The actual ray

The camera-to-world matrix carries the film point into the scene; normalizing gives the ray direction.

Pixel → NDC (sample at the pixel center)
$$x_{n} = \frac{2(i + 0.5)}{W} - 1, \qquad y_{n} = 1 - \frac{2(j + 0.5)}{H}$$

Dividing by the resolution and remapping \([0,1] \to [-1,1]\) removes the image size from all later math; the subtraction order on \(y_n\) flips the downward pixel rows into an upward axis.

NDC → camera space (film plane at z = −1)
$$P_{cam} = \Big(\, x_{n}\, a \tan\tfrac{\theta_h}{2},\;\; y_{n} \tan\tfrac{\theta_h}{2},\;\; -1 \Big), \qquad a = \tfrac{W}{H}$$

\(\tan(\theta_h/2)\) is the half-height of the film plane one unit away. A wider field of view stretches the plane and the aspect ratio \(a\) widens x to keep pixels square. In camera space the eye sits at the origin looking down \(-z\), so the film point doubles as the ray direction.

Camera → world
$$\mathbf{o} = \text{eye}, \qquad \mathbf{v} = M_{cam\to world} \begin{bmatrix} P_{cam} \\ 0 \end{bmatrix}, \qquad \mathbf{d} = \frac{\mathbf{v}}{\lVert\mathbf{v}\rVert}$$

\(M_{cam\to world}\) is the inverse of the view matrix. The film point transforms with \(w = 0\): it's used as a direction, so it should rotate but not translate.

Differentiating the chain
$$\frac{\partial P_{cam}}{\partial x} = \Big(\tfrac{2\,a\tan(\theta_h/2)}{W},\, 0,\, 0\Big), \qquad \frac{\partial \mathbf{d}}{\partial x} = \frac{(I - \mathbf{d}\mathbf{d}^T)}{\lVert\mathbf{v}\rVert}\, M_{cam\to world}\begin{bmatrix} \partial P_{cam}/\partial x \\ 0 \end{bmatrix}$$

Everything up to normalization is linear, so its derivative is just "one pixel's worth of film plane" pushed through the same matrix. Normalization is the only nonlinear step: the projector \((I - \mathbf{d}\mathbf{d}^T)\) keeps the component of the change perpendicular to \(\mathbf{d}\). Sliding a unit vector can only rotate it, not lengthen it. For a pinhole camera \(\partial\mathbf{o}/\partial x = 0\): all rays leave the same point.

Transfer onto a surface (hit point P = o + t d, normal n)
$$\frac{\partial t}{\partial x} = -\frac{\left(\frac{\partial \mathbf{o}}{\partial x} + t\,\frac{\partial \mathbf{d}}{\partial x}\right)\cdot \mathbf{n}}{\mathbf{d}\cdot \mathbf{n}}, \qquad \frac{\partial P}{\partial x} = \frac{\partial \mathbf{o}}{\partial x} + t\,\frac{\partial \mathbf{d}}{\partial x} + \frac{\partial t}{\partial x}\,\mathbf{d}$$

Differentiating \(P = \mathbf{o} + t\mathbf{d}\) needs \(\partial t/\partial x\), how much sooner or later the neighboring ray hits the surface. Constraining the offset hit point to stay on the tangent plane gives the first equation; note the \(\mathbf{d}\cdot\mathbf{n}\) in the denominator, which is why footprints stretch at grazing angles, exactly where texture aliasing is worst.

World → texture space
$$\frac{\partial P}{\partial x} = \frac{\partial P}{\partial u}\frac{\partial u}{\partial x} + \frac{\partial P}{\partial v}\frac{\partial v}{\partial x} \quad\Longrightarrow\quad \frac{\partial \mathbf{uv}}{\partial x}$$

The last hop leaves world space entirely. The surface's own parameterization provides tangent vectors \(\partial P/\partial u\) and \(\partial P/\partial v\) (for a sphere or cone, differentiate its UV formula; for a mesh, they come from the triangle's vertex UVs). Expressing \(\partial P/\partial x\) in that basis is a tiny 2×2 linear solve, and its solution \(\partial\mathbf{uv}/\partial x\) is precisely the texture-space footprint that the mip level formula above consumes. One derivative, carried through five spaces: pixel, NDC, camera, world, and finally UV.


C++Phong IlluminationRay DifferentialsMipmapsSnell’s LawStratified Sampling