Graphics glossary 37 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 material painted behind an object. It is the result of asking whether the interval from a surface towards a light is clear. The compute shader already has every component of that question: a ray structure, primitive equations and a scene buffer. What changes is the interval and the required answer.
The primary query wants the closest surface over a long forward interval. The shadow query wants any blocker after a small starting offset but before the light. Once one valid blocker is found, searching for a closer one is unnecessary.
What you should be able to account for
- Construct a bounded shadow ray from the accepted surface towards a point light.
- Explain separately the small origin offset and the maximum distance before the light.
- Measure shadow-query work without turning an instrumentation counter into a rendering dependency.
Ask the cheaper question
Write a Boolean scene query that exits on its first accepted candidate. It can call the same sphere equation and discard the returned position, normal and material. A specialised primitive predicate could later avoid that unused work, but using one trusted equation first prevents two subtly different intersection policies.
bool anySceneHit(Ray ray, float tMin, float tMax) {
for (uint index = 0; index < control.sphereCount; ++index) {
vec4 sphere = scene.spheres[index].centreRadius;
Hit candidate = intersectSphere(
ray, sphere.xyz, sphere.w, tMin, tMax);
if (candidate.found) {
return true;
}
}
return false;
}
This result is independent of which blocker is encountered first because every valid blocker produces the same Boolean answer. Record order may affect the amount of work due to early exit, but not whether the light is blocked.
Build the interval from the light
vec3 toLight = uniforms.lightPosition.xyz - hit.position;
float lightDistance = length(toLight);
vec3 lightDirection = toLight / lightDistance;
const float shadowEpsilon = 0.001;
Ray shadowRay;
shadowRay.origin = hit.position + hit.normal * shadowEpsilon;
shadowRay.direction = lightDirection;
bool blocked = anySceneHit(
shadowRay,
shadowEpsilon,
lightDistance - shadowEpsilon);
The maximum is not infinity. A sphere two units beyond the point light cannot prevent photons travelling from that light to the surface. The lower bound and shifted origin address numerical self-intersection close to the source surface. They are related safeguards, not permission to choose a huge arbitrary offset.
For a point light, subtracting the epsilon from the upper bound also avoids treating geometry exactly at or just beyond the light position as a blocker. If the resulting maximum is not greater than the minimum, the interval is empty and the query should be skipped.
Worked Vulkan account
How a Shadow Query Stops at the Light
A shadow ray asks a bounded visibility question from the surface to the light. Intersections outside that interval do not answer it.
What the code must establish
The shadow query begins near the accepted surface and ends just before the point light. It asks whether any geometry intersects that finite interval. The shader may stop at the first accepted blocker because the identity of the closest blocker is irrelevant to this particular visibility question.
What to inspect
Record the shifted origin, minimum distance, light distance, maximum distance and candidate intersection used by one shadow ray. Test the same candidate before the light, beyond the light and below the minimum. The visibility result must follow interval membership rather than mere existence on the infinite ray.
What this establishes, and what it does not: A correct occluded-or-visible flag establishes one bounded direct-light query. It does not establish that the origin bias is suitable at every scene scale or that reflections and other secondary rays use the right intervals.
Apply visibility to direct light only
vec3 shadeLocal(Hit hit, Ray ray) {
MaterialGPU material = materials[hit.materialIndex];
vec3 ambient = 0.025 * material.albedoRoughness.xyz;
vec3 toLight = uniforms.lightPosition.xyz - hit.position;
float lightDistance = length(toLight);
vec3 L = toLight / lightDistance;
Ray shadowRay = Ray(
hit.position + hit.normal * control.shadowEpsilon,
L);
bool blocked = anySceneHit(
shadowRay,
control.shadowEpsilon,
lightDistance - control.shadowEpsilon);
if (blocked) {
return ambient;
}
return ambient + directLight(material, hit, ray, L);
}
The ambient teaching approximation remains because it does not claim to arrive directly from this point light. Diffuse and specular terms are suppressed together when the point light is invisible. A later environment or indirect-light contribution would have its own visibility and sampling rules.
Do not hide the epsilon
Put the shadow epsilon in a named per-dispatch control rather than scattering a literal across functions. Test a deliberate range. If it is too small, round-off may cause the originating surface to shadow itself. If it is too large, a ray can leap over a nearby legitimate blocker or detach contact shadows.
A fixed world-space epsilon also changes meaning if the entire scene is rescaled. A renderer that must operate across different scene scales can account for hit distance, coordinate magnitude and floating-point error. The fixed value remains useful here because its failure can be observed directly and compared with the manual calculation from Lesson 8. State the scene scale and the chosen value whenever results are recorded.
Bound one light query
A hit lies at (0, 0, -2), and the point light lies at (0, 3, -2), so the light distance is 3. With epsilon 0.001, the shadow interval ends at 2.999. A blocker intersected at distance 1.2 makes the point shadowed. An intersection at 3.4 is behind the light and is rejected. Changing the maximum to a large scene distance would incorrectly let the latter object cast a shadow from this light.
Add counters as observers
Lesson 14 counted manual work. The compute tracer can do the same with a small storage buffer whose unsigned counters are updated atomically. Possible counters include primary primitive tests, shadow rays and shadow primitive tests. Atomics are necessary because many invocations update the same locations.
layout(std430, set = 0, binding = 4) buffer TraceCounters {
uint primaryTests;
uint shadowRays;
uint shadowTests;
} counters;
// Before launching a shadow query:
atomicAdd(counters.shadowRays, 1u);
// Inside the shadow-loop body:
atomicAdd(counters.shadowTests, 1u);
Reset the counter buffer before the dispatch, make that reset visible to compute shader reads and writes, then make shader writes visible to the transfer or host readback used after completion. A convenient command sequence is fill buffer, barrier, trace dispatch, barrier, copy to a host-visible readback buffer. Wait for the submission's fence before reading the mapped result.
Counters perturb performance. Every pixel may contend on the same cache lines, so do not use an instrumented timing as the renderer's normal cost. They answer structural questions: how many tests were requested and how often early exit occurred.
Expect early exit to vary across invocations
Some shadow rays return on their first sphere; others inspect the whole array. Neighbouring shader invocations may therefore take different control paths. This divergence is a performance concern, not a reason to replace the bounded logic. The reference shader should first report the right visibility for every selected pixel.
Record reordering can reduce or increase the counter total because likely blockers appear earlier or later. It must not change the shadow mask. This distinction between answer and work becomes important when acceleration structures choose traversal order internally.
Place a blocker beyond the light
The light is 4.0 units from the hit. Another sphere intersects the shadow ray at 5.5. Does it block this point light?
The answer
No. The shadow maximum is slightly less than 4.0, so 5.5 lies outside the requested interval. It could matter for another ray or another light, but it cannot obstruct the segment ending at this light.
Official reference: the Khronos synchronization chapter defines availability and visibility, and the synchronization examples show common transfer and compute dependencies.
Map correctness and work separately
Produce a binary shadow-mask output and capture it for the scene in two different sphere-buffer orders. The masks must match exactly. For the same two runs, read the shadow-test counter and explain any difference using early exit. Next sweep the epsilon across at least five values, including one that produces acne and one that visibly detaches a contact shadow. Record the smallest value that remains stable for the present scene scale rather than claiming that it is universal.
Carry the bounded query to new geometry
Retain anySceneHit, its explicit interval and the optional counters. Lesson 24 will add indexed triangles to the same scene-query contract. A shadow ray will not care whether its first blocker is a sphere or triangle; it will care only that the primitive returns a valid distance inside the requested bounds.
The scene now contains absence as a calculated result: direct light is present only when a bounded secondary query finds no obstruction. We have not invented a special shadow-drawing operation. We have reused the ray question with a different interval and a cheaper stopping condition.