Graphics glossary 39 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.
Spheres are compact because four values define the whole surface. Most rendered assets arrive as triangles, and triangles expose a different Vulkan problem before their equation even runs: several buffers must agree about what one record means. A vertex buffer stores positions, an index buffer selects three of them, and the shader must apply the same stride and index interpretation used by the host.
Add indexed triangles to the compute tracer without invoking the rasterisation pipeline. Each invocation still follows its own ray. It reads three vertex positions, solves the ray-triangle intersection from Lesson 10 and offers the resulting hit to the same closest-distance reduction.
What you should be able to account for
- Define storage-buffer records for vertices and triangle indices with inspectable strides.
- Implement a bounded Moller-Trumbore intersection and retain barycentric coordinates.
- Diagnose an index or stride fault from buffer contents rather than from a distorted final image alone.
Choose records the shader cannot misunderstand
Use one vec4-sized position per vertex and one uvec4-sized index record per triangle. The spare w components cost space, but they make both storage-buffer strides 16 bytes and keep this first contract obvious.
struct alignas(16) VertexGPU {
float x;
float y;
float z;
float spare;
};
struct alignas(16) TriangleGPU {
std::uint32_t i0;
std::uint32_t i1;
std::uint32_t i2;
std::uint32_t materialIndex;
};
static_assert(sizeof(VertexGPU) == 16);
static_assert(sizeof(TriangleGPU) == 16);
The triangle record uses its fourth component for a material index, so no separate metadata lookup is required. Validate every index against the vertex count on the CPU. A valid storage-buffer descriptor does not make an out-of-range index meaningful.
layout(std430, set = 0, binding = 5) readonly buffer VertexBuffer {
vec4 positions[];
} vertices;
layout(std430, set = 0, binding = 6) readonly buffer TriangleBuffer {
uvec4 indicesAndMaterial[];
} triangles;
Add both bindings to the set layout and descriptor pool, create device-local buffers with storage and transfer-destination use, and upload them through staging. Supply vertex and triangle counts in the trace control. Record a transfer-to-compute dependency before the first dispatch that reads them.
Fetch three positions explicitly
uvec4 record = triangles.indicesAndMaterial[triangleIndex];
vec3 a = vertices.positions[record.x].xyz;
vec3 b = vertices.positions[record.y].xyz;
vec3 c = vertices.positions[record.z].xyz;
During bring-up, reserve a debug mode that writes a, b and c for one chosen triangle to a readback buffer. If the CPU believes the indices are (0, 1, 2), establish that the shader sees those exact unsigned values and positions before investigating the intersection arithmetic.
Worked Vulkan account
How a Byte Layout Becomes Triangle Data
The triangle equation can be correct while its inputs are nonsense. Host and shader must first agree on the exact addresses from which the indices and positions are read.
What the code must establish
The host and shader agree on the member types, offsets, alignment and stride of vertex and index records. The shader calculates the record address from the base, index and declared stride, reads the three vertex indices and then loads the corresponding positions before applying the familiar triangle equation.
What to inspect
Use host offset checks and shader-layout information to record every member offset and the final stride. In a diagnostic route, read one chosen triangle's indices and positions back from the shader. Establish this byte account before altering the intersection code, because a correct equation cannot repair an incorrect address.
What this establishes, and what it does not: One correctly reconstructed triangle establishes the selected buffer address and values. It does not cover every index range, winding, degenerate triangle or shared-edge intersection case.
Move the triangle equation intact
Extend the private hit record with vec3 barycentric and initialise it to zero in noHit. Sphere hits may leave that field at zero because they do not use triangle coordinates; a retained triangle overwrites it with three valid weights.
Hit intersectTriangle(
Ray ray,
vec3 a,
vec3 b,
vec3 c,
float tMin,
float tMax)
{
const float determinantEpsilon = 0.0000001;
vec3 edge1 = b - a;
vec3 edge2 = c - a;
vec3 p = cross(ray.direction, edge2);
float determinant = dot(edge1, p);
if (abs(determinant) < determinantEpsilon) {
return noHit();
}
float inverseDeterminant = 1.0 / determinant;
vec3 fromA = ray.origin - a;
float u = dot(fromA, p) * inverseDeterminant;
if (u < 0.0 || u > 1.0) {
return noHit();
}
vec3 q = cross(fromA, edge1);
float v = dot(ray.direction, q) * inverseDeterminant;
if (v < 0.0 || u + v > 1.0) {
return noHit();
}
float t = dot(edge2, q) * inverseDeterminant;
if (t < tMin || t > tMax) {
return noHit();
}
Hit hit = noHit();
hit.found = true;
hit.t = t;
hit.position = ray.origin + t * ray.direction;
vec3 outward = normalize(cross(edge1, edge2));
hit.normal = dot(ray.direction, outward) < 0.0
? outward : -outward;
hit.barycentric = vec3(1.0 - u - v, u, v);
return hit;
}
This version accepts both triangle orientations because it tests the absolute determinant and later faces the normal against the arriving ray. If the course renderer chooses back-face culling, implement it as an explicit policy and test the sign appropriate to the chosen winding convention.
Offer triangles to the same reduction
After testing spheres, loop over triangle records with the current closestDistance as their maximum. When a triangle candidate is retained, assign its material index and primitive identity. The reverse order, triangles followed by spheres, must produce the same physical closest hit.
for (uint index = 0; index < control.triangleCount; ++index) {
uvec4 record = triangles.indicesAndMaterial[index];
Hit candidate = intersectTriangle(
ray,
vertices.positions[record.x].xyz,
vertices.positions[record.y].xyz,
vertices.positions[record.z].xyz,
tMin,
closestDistance);
if (candidate.found) {
candidate.materialIndex = record.w;
closest = candidate;
closestDistance = candidate.t;
}
}
The closest-hit contract is shared even though the primitive data and equation differ. The shadow query can use a corresponding Boolean triangle test and return as soon as either kind of geometry blocks the bounded interval.
Read barycentric evidence
A retained triangle hit reports u = 0.2 and v = 0.3. The three weights are therefore (0.5, 0.2, 0.3). They are non-negative and sum to one, so the point is inside the triangle. A value such as u = 0.8, v = 0.5 fails because their sum exceeds one even though each value considered alone is between zero and one.
Use barycentrics before adding vertex attributes
Display the three barycentric components directly as RGB. The vertices should approach pure red, green and blue according to the chosen component order, with smooth linear variation across the triangle. A discontinuity or impossible negative region points to the intersection or vertex ordering rather than to the material system.
Once positions are correct, a parallel normal buffer could be indexed by the same record and interpolated with the barycentric weights. Do not normalise the weights; they already sum to one within arithmetic tolerance. Interpolate the vertex normals and normalise the resulting direction. A geometric normal remains useful for orientation and offsets even after a smooth shading normal is introduced.
Diagnose stride before mathematics
The worked account above contrasts the declared 16-byte stride with an incorrect shorter stride. On the real shader, a stride disagreement often makes the first record look correct and later records progressively wrong. Capture at least two records. Compare raw buffer offsets, shader indices and fetched positions.
Do not repair this by changing triangle coordinates until the corrupted shape happens to look acceptable. The host and shader are interpreting different byte sequences. Establish a single layout contract with compile-time size checks, validation-layer feedback where applicable, and a shader witness.
Interpret a triangle record
A record contains indices (4, 1, 7) and material index 3. Which buffer supplies the position of the third vertex, and which value selects appearance?
The answer
Vertex position 7 supplies the third position. The separate fourth value 3 selects the material after this triangle becomes the closest hit. Treating 3 as another vertex index or 7 as a material would be a record-contract error.
Official reference: Vulkan buffer objects and their memory binding are defined by the Khronos resource chapter; shader storage layout is described in the mapping data guide.
Make layout faults observable
Upload two triangles whose indices and positions are deliberately easy to recognise. Record the CPU byte offset of each triangle, the four unsigned values read by the shader and the three fetched positions. Render a barycentric diagnostic and test one ray near each edge. Then introduce a controlled host-stride error in a separate debug copy, capture the resulting witness and restore the correct layout. Explain why the fault first appears at a particular record.
Carry mixed geometry into secondary work
Retain the vertex and triangle buffers, barycentric hit data and shared closest/any-hit contracts. Lesson 25 will allow a retained material to launch another ray. The new ray will query the same mixed scene, so neither the geometry buffers nor their descriptors need to know whether a ray is primary or secondary.
The compute renderer can now trace the primitive from which most practical scenes are assembled while keeping the manual equation visible. This is deliberately exhaustive traversal: every ray still asks every sphere and every triangle. Before replacing that cost with hardware traversal, we need to complete the reference path with bounded secondary rays and sampling.