RT Graphics Programming
Lesson 09 of 28

Part 1 · Ray tracing by hand

9. Planes and a Grounded Scene

Introduce a second geometric primitive without changing the contract used by the rest of the renderer.

Graphics glossary 26 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

A second primitive is a test of the renderer's structure. If adding a plane requires a second shading route, a second shadow system and a new form of closest-hit loop, the first design captured the sphere rather than the general problem. A plane should differ where its geometry differs and agree everywhere the scene asks the same questions.

We shall use an infinite plane as a ground surface. Its intersection is simpler than the sphere quadratic, but it has a new boundary: a ray can be parallel to the plane. The plane will return the same hit-record fields, and the existing lighting and shadow calculations will consume them without knowing which equation produced the record.

What you should be able to account for

Define every point on the plane

A plane can be described by one point Q on it and a unit normal N. Any other point P belongs to the plane when its displacement from Q is perpendicular to the normal:

(P − Q) · N = 0

Substitute the ray point P(t) = O + tD:

(O + tD − Q) · N = 0

Collect the term containing t and solve:

t = ((Q − O) · N) / (D · N)

The denominator measures how much the ray direction approaches or leaves the plane along its normal. If it is zero, the ray is parallel. It either never meets the plane or lies within it, in which case there is no unique first intersection. Our visibility query reports no hit for both cases.

function hitPlane(ray, plane, tMin, tMax) {
    const denominator = dot(ray.direction, plane.normal);
    if (Math.abs(denominator) < 1e-7) {
        return null;
    }

    const t = dot(
        subtract(plane.point, ray.origin),
        plane.normal
    ) / denominator;

    if (t < tMin || t > tMax) {
        return null;
    }

    const position = pointOnRay(ray, t);
    return makeHitRecord(ray, t, position, plane.normal, plane.material);
}

The small denominator threshold protects the division from a ray that is nearly parallel. As with the shadow bias, the value is a policy for this numerical scale, not a magic constant. A near-parallel ray may meet the infinite plane at a very large distance. The scene's maximum interval should also decide whether that distant result is useful.

Use the shared record

The plane supplies distance, position, oriented normal, face flag and material. The closest-hit loop compares its t with sphere results. The shadow traversal accepts it as an occluder. The lighting code reads the normal and material. None of those callers requires a plane-specific branch.

This agreement is more valuable than making the primitive names look alike. It means each layer owns a stable criterion. Geometry decides whether and where. Scene traversal decides which valid result is nearest. Shading decides how the retained surface contributes. A shared hit record is the point at which those responsibilities meet.

Plane and material laboratory

Keep the Plane Fixed, Change the Checker

Hold one plane hit constant while checker frequency changes only the material lookup made from the retained world position.

Change this, then watch this: Change the checker frequency. The checker lookup and ground pattern change; the plane hit distance and normal remain fixed.

Grounded sceneA shared scene whose infinite plane uses a position-dependent checker material.Ready
A shared scene whose infinite plane uses a position-dependent checker material.
One plane hitA diagnostic ray, plane normal, calculated hit distance and checker-cell lookup.Calculation
A diagnostic ray, plane normal, calculated hit distance and checker-cell lookup.
Denominator D · N
-0.707
Plane hit t
2.828
Hit position P
(0.350, -1.000, -1.000)
Checker lookup
Calculating

t = ((Q − O) · N) ÷ (D · N) = 2.828

What this establishes:

Make the material depend on position

An infinite plane with one constant colour provides little visual evidence of scale or orientation. We can calculate a checkerboard from the hit position. Multiply the world X and Z coordinates by a frequency, take the integer cell for each, add them and select one of two colours from the parity.

function checkerAlbedo(position, frequency) {
    const cellX = Math.floor(position[0] * frequency);
    const cellZ = Math.floor(position[2] * frequency);
    const even = Math.abs((cellX + cellZ) % 2) === 0;
    return even ? [0.76, 0.75, 0.67] : [0.09, 0.13, 0.12];
}

The plane equation does not change when the checker frequency changes. Only the material interpretation of the hit position changes. This makes the laboratory a direct test of the boundary we wanted: geometry remains fixed while a procedural material produces a different pattern across the same records.

A downward ray meets the ground

Let the ground contain Q = (0, -1, 0) with N = (0, 1, 0). A ray begins at O = (0, 1, 0) with direction D = (0, -1, 0). The numerator is (Q - O) · N = (0, -2, 0) · N = -2. The denominator is D · N = -1. Their quotient is t = 2, giving position (0, -1, 0).

Do not let an infinite surface become infinite work

The plane extends without bound, but the image and ray interval are finite. We do not iterate over plane points. One substitution produces the possible distance for a ray. This is a useful distinction between geometric extent and computational work. An infinite mathematical primitive can be cheaper to intersect than a finite mesh containing thousands of triangles.

Its visual extent can still create difficulties. At the horizon, checker cells become smaller than pixels and a single centre sample produces severe aliasing. The intersection is correct for the chosen sample; the pixel estimate is inadequate for the rapidly changing material signal. Lesson 13 will make that necessary distinction explicit.

Classify the denominator

The plane normal is (0, 1, 0). What happens for directions (1, 0, 0), (0, -1, 0) and the normalised form of (1, -0.000000001, 0)?

Reveal the decisions

The first direction has denominator zero and is parallel. The second has denominator -1 and approaches the plane directly. The third has an extremely small negative denominator; mathematically it meets the infinite plane very far away, but the near-parallel threshold and finite ray maximum may reject it as numerically or operationally irrelevant.

Separate geometry from appearance

Add a second procedural material mode based on concentric rings in world XZ distance. Switch between rings and checks without changing hitPlane. Then deliberately place the pattern calculation inside the intersection function and list the new dependencies it creates. Restore the separation after the comparison.

Keep this model for Vulkan

Procedural materials translate naturally to shader code because each invocation can derive appearance from its hit position. The Vulkan implementation will need explicit coordinate-space and precision choices. The present division remains useful: an intersection supplies position and normal; material code interprets them. A procedural pattern should not alter whether geometry was hit.

The scene is now grounded and the renderer has survived a second primitive. Lesson 10 introduces the triangle, the finite primitive from which general polygonal meshes are normally assembled.