RT Graphics Programming
Lesson 08 of 28

Part 1 · Ray tracing by hand

8. Shadows and the Problem of Zero

Send a second ray towards the light and handle the numerical boundary at the surface that created it.

Graphics glossary 28 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 shadow is not a dark shape placed beneath an object. It is the result of a failed visibility route from a surface point to a light sample. This is a useful correction because it turns a special visual effect into a familiar operation: construct a ray, give it an interval and ask whether any geometry occupies that interval.

The shadow ray begins on the surface that created it. In exact arithmetic, its origin belongs to that surface. In finite-precision arithmetic, the reconstructed hit position may be slightly inside, outside or exactly on the numerical boundary. If we ask for intersections beginning at zero, the surface can block its own route to the light. The resulting speckles or dark regions are commonly called shadow acne. The name is memorable; the cause is the part we need to retain.

What you should be able to account for

  • Construct a shadow ray with a maximum distance ending before the point light.
  • Stop the query at the first valid occluder rather than computing an unnecessary closest shaded hit.
  • Explain why a scale-aware origin or interval policy is required at a generating surface.

The light defines a bounded interval

From hit position P, compute the displacement to the light position, retain its length and normalise it. A blocking object matters only if it lies after the surface and before the light.

const toLight = subtract(light.position, hit.position);
const lightDistance = length(toLight);
const direction = scale(toLight, 1 / lightDistance);

const shadowRay = {
    origin: hit.position,
    direction
};

const blocked = anyHit(
    shadowRay,
    0.001,
    lightDistance - 0.001
);

The upper bound is essential. A sphere beyond the light lies on the same infinite line but cannot block light that ends before it. Asking for tMax = ∞ would answer whether anything exists in that direction, not whether the light route is occluded.

Any hit is sufficient

A primary ray needs the closest surface because that record will be shaded. A shadow ray needs a yes or no answer. Once one valid occluder is found, further object tests cannot change blocked into unblocked. A dedicated anyHit traversal can therefore return immediately.

function anyHit(ray, objects, tMin, tMax) {
    for (const object of objects) {
        if (object.intersect(ray, tMin, tMax) !== null) {
            return true;
        }
    }
    return false;
}

This early return is not merely an optimisation pasted onto the closest-hit function. It follows from a different required result. When the answer is existential, one witness is enough. When the answer needs a shaded surface, we must retain the closest record. Naming the two queries makes the purpose inspectable.

The origin is on a numerical boundary

Suppose a sphere hit is reconstructed with P = O + tD. Substituting P back into the sphere equation may produce a tiny residual rather than exact zero. A new outward ray beginning at P can then report a root close to zero. Treating that root as an occluder makes the surface shadow itself.

A common teaching policy moves the shadow origin a small distance along the oriented normal and also begins the accepted interval at a small positive value:

const epsilon = 0.001;
const shadowRay = {
    origin: add(hit.position, scale(hit.normal, epsilon)),
    direction
};

const blocked = anyHit(
    shadowRay,
    epsilon,
    lightDistance - epsilon
);

This is useful for our scene scale. It is not a universal constant for every renderer. Make the world one million times larger or smaller and the same value may cause a visible gap or fail to move the origin meaningfully. Robust systems relate offsets to floating-point error, position magnitude and primitive behaviour. Our immediate criterion is narrower: the policy must reject the generating-surface artefact without skipping a genuinely nearby occluder.

Shadow-boundary laboratory

Move a Shadow Ray off Its Surface

Compare an exact-surface origin with P plus epsilon N and inspect how that small change alters the accepted ray interval.

Change this, then watch this: Switch the shadow origin between P and P + epsilon N. Watch the origin, interval and self-hit decision change together.

Shadow diagnosticA deliberately exaggerated comparison between an exact-surface shadow origin and a biased origin.Ready
A deliberately exaggerated comparison between an exact-surface shadow origin and a biased origin.
Inspect the ray originA magnified surface boundary showing the exact point P and the offset point P plus epsilon N.Calculation
A magnified surface boundary showing the exact point P and the offset point P plus epsilon N.
Origin offset ε
0.001
Shadow origin
P + 0.001N
Accepted t interval
[0.001, distance)
Generating surface
Excluded

Oshadow = P + εN

What this establishes:

Expose the failure before correcting it

The laboratory offers an exact-surface diagnostic. It deliberately permits the near-zero boundary to count as a hit so the consequence is obvious. Restore the biased origin and the direct-light contribution returns where no other object occupies the bounded route.

This exaggerated mode is not evidence that every platform will produce the identical acne pattern from an exact origin. Floating-point evaluation details vary. It demonstrates the ambiguous query we would otherwise be relying upon: ask geometry whether a ray beginning on geometry hits geometry at zero. The corrected policy removes that ambiguity from the useful interval.

Keep an occluder, reject what lies beyond

The light is five units from the surface. One sphere intersects the shadow direction at t = 2.1, so the route is blocked. Another intersects at t = 7.4; it is beyond the light and irrelevant. A near-zero root at t = 0.00002 from the generating surface is rejected by a minimum of 0.001. The interval, not the existence of roots alone, states the shadow question.

Visibility gates direct light

The local terms from Lesson 7 are still calculated from the same directions. Direct diffuse and specular contributions are multiplied by zero when the shadow route is blocked and by one when it is clear. A small ambient teaching term remains so an occluded surface is not displayed as absolute black. That ambient value is not indirect illumination. It is a visible placeholder for transport we have not calculated.

const visibility = blocked ? 0 : 1;
const colour = add(
    scale(albedo, 0.055),
    scale(directLighting, visibility)
);

Calling the placeholder ambient light would be convenient but misleading if we forgot what it replaces. A complete path tracer would obtain indirect contribution by sampling additional transport paths. We shall keep the term modest and labelled rather than claim more physics than the calculation contains.

Choose the correct maximum

A point light is 3.5 units from the surface. Candidate intersections along its direction occur at t = 0.0001, t = 2.0 and t = 5.0. With a minimum of 0.001 and maximum just below 3.5, which result decides the shadow?

Reveal the interval

The near-zero root is below the minimum and rejected. The hit at 2.0 lies between surface and light, so it blocks the route and the query may return immediately. The hit at 5.0 is beyond the light and would not matter even if the earlier occluder were absent.

Find a useful bias range

Expose the bias as a logarithmic control from a very small value to a visibly excessive one. Record when self-intersection disappears and when contact shadows begin to detach. Repeat after scaling every scene coordinate by 100. Explain why one hard-coded world-space constant cannot express the same numerical policy at both scales.

Keep this model for Vulkan

Secondary-ray origin handling remains necessary in GPU ray tracing. Vulkan acceleration structures and traversal hardware do not decide the semantic interval for us. Shader code or ray-query setup must provide appropriate minimum and maximum values, and performance work can exploit the any-hit nature of an occlusion query only after the bounds are correct.

Our spheres can now cast shadows, but they still appear suspended in an empty background. Lesson 9 adds an infinite plane using a different intersection equation and demonstrates why the shared hit record was worth establishing.