Graphics glossary 40 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 compute shader can evaluate thousands of sphere equations together, but parallelism does not alter the equation. Each invocation constructs one primary ray, substitutes it into the sphere's implicit surface and accepts the smallest root inside the requested interval. If the result differs from Lesson 4, “the GPU did it differently” is not an explanation. Either the data, arithmetic, interval or output has changed.
Begin with one sphere supplied through push constants. The scene buffer belongs to Lesson 21. Keeping only one primitive makes the first disagreement narrow: camera ray, sphere values, quadratic terms, roots or interval. The shader writes a normal diagnostic for a hit and a dark background for a miss, so Canvas-like shape drawing remains absent.
What you should be able to account for
- Declare a push-constant range and supply one centre-radius record before dispatch.
- Implement the half-b ray-sphere equation with an explicit minimum and maximum distance.
- Compare selected shader roots with the manual reference before accepting the silhouette.
Add one small command-supplied record
The diagnostic sphere needs four floats. Its centre occupies xyz and its radius occupies w. The pipeline layout now includes a compute-stage push-constant range as well as the descriptor set layout. Recreate the pipeline after changing its layout; an existing pipeline does not acquire a new interface because the host structure changed.
struct alignas(16) TracePush {
float centreX;
float centreY;
float centreZ;
float radius;
};
VkPushConstantRange traceRange{
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
.offset = 0,
.size = sizeof(TracePush)
};
VkPipelineLayoutCreateInfo layoutInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = 1,
.pSetLayouts = &setLayout,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &traceRange
};
Query VkPhysicalDeviceLimits::maxPushConstantsSize and establish that the 16-byte range fits. Immediately before the dispatch, record the value using the same pipeline layout and stage range:
TracePush tracePush{ 0.0f, 0.0f, -3.0f, 1.0f };
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_COMPUTE_BIT,
0,
sizeof(tracePush),
&tracePush);
vkCmdDispatch(commandBuffer, groupsX, groupsY, 1);
The command copies the supplied bytes into command-buffer state for the named range. The pointer need not remain alive after recording. The layout and shader interpretation must still agree.
Return a deliberate hit record
Use a small result structure rather than returning only a Boolean. The accepted distance is needed to reconstruct position and normal. The first version has no material or face orientation because those responsibilities are not required to prove one sphere intersection.
layout(push_constant) uniform TracePush {
vec4 centreRadius;
} tracePush;
struct Hit {
bool found;
float t;
vec3 position;
vec3 normal;
};
Hit noHit() {
Hit hit;
hit.found = false;
hit.t = 0.0;
hit.position = vec3(0.0);
hit.normal = vec3(0.0);
return hit;
}
Initialising every field avoids treating unspecified values from a miss as useful geometry. A shader compiler may optimise unused assignments, but the source contract remains readable.
Move the known equation, not an approximation of it
Hit intersectSphere(
Ray ray,
vec3 centre,
float radius,
float tMin,
float tMax)
{
vec3 offset = ray.origin - centre;
float a = dot(ray.direction, ray.direction);
float halfB = dot(offset, ray.direction);
float c = dot(offset, offset) - radius * radius;
float discriminant = halfB * halfB - a * c;
if (discriminant < 0.0) {
return noHit();
}
float rootScale = sqrt(discriminant);
float root = (-halfB - rootScale) / a;
if (root < tMin || root > tMax) {
root = (-halfB + rootScale) / a;
if (root < tMin || root > tMax) {
return noHit();
}
}
Hit hit;
hit.found = true;
hit.t = root;
hit.position = ray.origin + root * ray.direction;
hit.normal = normalize((hit.position - centre) / radius);
return hit;
}
Ray directions are normalised, so a should be close to one. Retaining it in the equation makes the function honest about its input and provides a diagnostic if a later direction loses unit length. The accepted interval begins at a small positive value and ends at a large finite maximum for the primary query. A root behind the camera or below the minimum is not made visible by being mathematically real.
Worked Vulkan account
How the Known Sphere Equation Runs in Compute
Moving the equation into a compute shader changes its execution environment. It must not change the roots, the accepted interval or the meaning of a hit.
What the code must establish
Each valid invocation constructs its camera ray and applies the same quadratic equation used in Part I. Parallel execution changes the number of rays evaluated together; it does not change the discriminant, root ordering or accepted ray interval. The shader must still reject roots behind the camera, below the minimum or beyond the current maximum.
What to inspect
For one diagnostic pixel, write the sphere offset, discriminant, two roots and accepted distance to a debug buffer or reserved pixels. Recalculate them on the CPU from the same ray and sphere. The silhouette is useful secondary evidence, but the numerical account identifies which part of the equation agreed.
What this establishes, and what it does not: A matching diagnostic ray establishes that the known equation survived one compute path. It does not establish the camera mapping for every pixel, the closest-hit rule for several objects or the correctness of later surface shading.
Write a diagnostic that exposes the hit
Ray ray = makePrimaryRay(gl_GlobalInvocationID.xy);
Hit hit = intersectSphere(
ray,
tracePush.centreRadius.xyz,
tracePush.centreRadius.w,
0.001,
1000000.0);
vec3 colour = hit.found
? 0.5 * (hit.normal + vec3(1.0))
: vec3(0.015, 0.025, 0.022);
imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy),
vec4(colour, 1.0));
The normal diagnostic distinguishes a geometric surface from a flat coloured disc. Its components should vary continuously across the visible sphere and its centre-facing normal should map near blue for the camera arrangement used here. Do not apply display gamma to this diagnostic comparison; the encoded normal is not a linear radiance value. Later lighting will separate linear calculation from display encoding.
Expect control-flow differences, not semantic differences
Invocations near the sphere execute the root and hit-record path; invocations outside return after the negative discriminant. The device may execute neighbouring invocations together, so this divergent control flow can affect performance. It does not permit a miss to borrow a hit from its neighbour. Optimisation begins after the per-invocation result agrees with the reference.
Replacing the branch with clever arithmetic may or may not improve a target implementation. It certainly increases the burden of showing that NaNs, tangent hits and rejected intervals remain correct. Keep the explicit version until measurement identifies it as a material cost.
Follow the centre ray
Let the camera origin be (0, 0, 0), direction (0, 0, -1), sphere centre (0, 0, -3) and radius 1. Then offset = (0, 0, 3), a = 1, halfB = -3, c = 8 and the discriminant is 1. The roots are 2 and 4. With interval [0.001, 1,000,000], the accepted root is 2, position is (0, 0, -2) and outward normal is (0, 0, 1). The shader and CPU trace should report those values within tolerance.
Read back a small witness
A complete debug readback path requires a destination buffer with transfer-destination use, compatible host-visible memory, an image-to-buffer copy or dedicated shader debug buffer, and barriers stating the relevant writes and reads. Do not map device-local image memory merely because it was allocated. For this lesson, a small storage debug buffer containing one selected invocation's a, halfB, c, discriminant and accepted root is clearer than reading the whole image.
Guard the debug write with an exact selected pixel and give it one record, so no atomic operation is required. After dispatch, make the shader write visible to a transfer or host read path as appropriate, wait for completion and compare the values. Remove the diagnostic from performance measurements.
Reject the wrong root
A ray-sphere equation produces roots -0.5 and 1.8 with interval [0.001, 10]. Which root is accepted?
The answer
The negative root lies behind the permitted start and is rejected. The second root 1.8 lies inside the interval and is accepted. Rejecting the first root does not imply a miss; the farther root must still be tested.
Official reference: the shader execution and interface rules remain defined by the Khronos shader chapter and shader-interface chapter.
Construct a discriminant witness set
Select three invocations whose rays produce a clear hit, a near-tangent result and a clear miss. Record the five quadratic values on both CPU and GPU. Move the sphere until the tangent case crosses zero and state the tolerance used when comparing the two implementations. Then deliberately reverse ray.origin - centre and show which terms and roots change. Restore the correct displacement only after the disagreement is explained numerically.
Carry the intersection function forward
Retain Ray, Hit and intersectSphere. Lesson 21 will replace the single push-constant sphere with aligned records in a shader storage buffer and reduce all valid candidates to the closest hit. The camera and output path do not change. The push-constant range may then be repurposed for small per-dispatch values rather than scene geometry.
We now have a sphere that exists because independent shader invocations solved its equation, not because a graphics pipeline rasterised a circle. The result is still deliberately limited: one object cannot expose traversal order, record layout or the closest-hit contract. Lesson 21 adds a scene buffer and ensures that buffer order does not become visibility order.