Ray tracing shaders trace rays against geometry — but they never see the geometry. They see the acceleration structure: a spatial index (BVH) the driver builds once so a ray can skip 99 % of triangles in O(log n) steps. Vulkan splits this into two levels:
as.txt
BLAS (bottom) mesh geometry: vertices + indices, per-object
▲
TLAS (top) scene: instances — transform + BLAS pointer + mask- BLAS = “what is the object” — one per unique mesh. Expensive to build, cheap to update in place.
- TLAS = “where is the object” — instances referencing BLASes
with a
VkTransformMatrixKHReach. Rebuilt every frame for moving objects.
The minimal recipe
blas.cpp
// 1. Geometry description (vertex buffer + triangle layout)
vk::AccelerationStructureGeometryKHR geo{};
geo.geometryType = vk::GeometryTypeKHR::eTriangles;
geo.geometry.triangles.vertexFormat = vk::Format::eR32G32B32Sfloat;
geo.geometry.triangles.vertexData = {vtxAddr};
geo.geometry.triangles.vertexStride = sizeof(Vertex);
geo.geometry.triangles.maxVertex = vtxCount;
geo.geometry.triangles.indexType = vk::IndexType::eUint32;
geo.geometry.triangles.indexData = {idxAddr};
geo.flags = vk::GeometryFlagBitsKHR::eOpaque; // no anyhit needed
// 2. Size query — you do NOT guess; ask the device
vk::AccelerationStructureBuildGeometryInfoKHR build{};
build.type = vk::AccelerationStructureTypeKHR::eBottomLevel;
build.geometryCount = 1;
build.pGeometries = &geo;
auto sizes = dev.getAccelerationStructureBuildSizesKHR(
build, 1, &primCount);
// sizes.accelerationStructureSize + buildScratchSize
// 3. Allocate the AS buffer + scratch
auto asBuf = allocBuf(sizes.accelerationStructureSize,
eStorageBuffer | eAccelerationStructureStorage);
auto scratch = allocBuf(sizes.buildScratchSize);
// 4. Create + build on GPU
vk::AccelerationStructureCreateInfoKHR ci{};
ci.type = vk::AccelerationStructureTypeKHR::eBottomLevel;
ci.buffer = asBuf;
ci.size = sizes.accelerationStructureSize;
auto blas = dev.createAccelerationStructureKHRUnique(ci);
build.scratchData = scratchAddr;
cmd.buildAccelerationStructuresKHR(build, &range);The TLAS is identical in shape but eTopLevel with instance buffer
instead of triangles.
Building is a GPU job
build.txt
build = first-time construction, into scratch + dest
update = modify in place (eUpdate flag), for moved instances
copy = vkCmdCopyAccelerationStructureKHR — clone or compactPer frame you typically rebuild the TLAS (few instances) and update or rebuild only the changed BLASes (e.g., skinned meshes stay in-place-update, new objects get a new BLAS).
What the ray shader sees
ray.rgen
#extension GL_EXT_ray_tracing : require
layout(set = 0, binding = 0) uniform accelerationStructureEXT tlas;
rayQueryEXT q;
rayQueryInitializeEXT(q, tlas, gl_RayFlagsOpaqueEXT,
0xFF, origin, tmin, dir, tmax);
while (rayQueryProceedEXT(q)) {}
if (rayQueryGetIntersectionTypeEXT(q, true) !=
gl_RayQueryCommittedIntersectionNoneEXT) { /* hit */ }The shader never knows about triangles — only about the TLAS handle
you bound as a descriptor (VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR).
Practical rules
- TLAS rebuild every frame is normal — it’s a few thousand bytes; BLAS rebuilds are the expensive part.
eOpaqueon geometry is the single biggest perf win — skips anyhit evaluation for the entire object.- Scratch buffers can be shared per-build if you serialize; for parallel builds keep one scratch per in-flight frame.
- Raytracing needs
VK_KHR_acceleration_structure+VK_KHR_ray_tracing_pipeline(orVK_KHR_ray_query) — checkaccelerationStructurefeature bit first.
If this post helped you, please consider buying me a coffee or donating via PayPal to support research & publishing of new posts on TechOverflow