Hybrid Rasterization and Ray Tracing

This advanced example shows hybrid rendering that leverages both rasterization (for primary visibility and alpha blending) and ray tracing (for shadows).
- The scene contains opaque objects, alpha-blended spheres, and a ground plane.
- Rasterization fills a G-buffer with world positions, normals, and colors.
- An OIT (order-independent transparency) fragment linked list handles alpha blending.
- Ray tracing then shoots shadow rays from G-buffer positions to determine lighting.
This architecture represents modern game rendering where rasterization handles base rendering and ray tracing adds realistic lighting and shadows.
Overview
What this demonstrates:
Combining rasterization and ray tracing, G-buffer generation, OIT for alpha blending, ray traced shadows, acceleration structure rebuilds, compute particle updates, multi-pass compositing.
Use cases:
Modern game engines, hybrid pipelines, ray traced shadows/reflections on rasterized geometry, performance-quality balance.
Requirements
- Vulkan Version: 1.2+
- Extensions: VK_KHR_ray_tracing_pipeline, VK_KHR_acceleration_structure
- Features: rayTracingPipeline, accelerationStructure, bufferDeviceAddress
Why Hybrid?
Rasterization: Fast primary visibility Ray Tracing: Accurate shadows/reflections Combine: Performance + Quality
The scene is composed of:
- An Opaque Ground Plane
- Multiple Alpha Blended Spheres
- Multiple Opaque Spheres
Algorithm Overview
- Compute Animation: The sphere's positions and velocities are updated using a compute shader.
- GBuffer Z-Fill: A depth-only pre-pass is performed for opaque meshes.
- GBuffer OIT Fill: For alpha-blended meshes, we use a fragment linked list to store color and depth information.
- GBuffer Opaque Fill: Record WorldPosition, WorldNormals, and Color for the opaque meshes.
- Acceleration Structure Rebuild: AccelerationStructures are updated or rebuilt based on the new sphere positions.
- Ray Tracing Shadows: For each world position recorded in the GBuffer, we compute a ray to the light source. Any intersection means the point is in shadow.
- Compositing:
- Final color for opaque areas is retrieved from the GBuffer.
- Alpha fragments are sorted by depth and blended against the opaque background.
- Shadow information from the Ray Tracing pass is applied to modify the final color.
BLAS Generation
Each distinct mesh (opaque spheres, alpha spheres, ground plane) gets its own Bottom-Level Acceleration Structure (BLAS).
Sphere BLAS (opaque and alpha):
The same sphere mesh is shared across all sphere instances.
One BLAS entry per sphere is created:
- each referencing the shared vertex buffer but with a per-sphere transform read from
blasTransformBuffer (written each frame by the compute shader).
PreferFastBuild is chosen over PreferFastTrace because the spheres move every frame.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 | m_as.opaqueSpheresBlas = m_device.createAccelerationStructure(AccelerationStructureOptions{
.label = "OpaqueSphereBLAS",
.type = AccelerationStructureType::BottomLevel,
.flags = AccelerationStructureFlagBits::PreferFastBuild,
.geometryTypesAndCount = std::vector<AccelerationStructureOptions::GeometryTypeAndCount>(
OpaqueSpheresCount,
{
.geometry = AccelerationStructureGeometryTrianglesData{
.vertexFormat = Format::R32G32B32_SFLOAT,
.vertexStride = sizeof(Vertex),
.maxVertex = static_cast<uint32_t>(m_sphereMesh.vertexCount - 1),
},
.maxPrimitiveCount = static_cast<uint32_t>(m_sphereMesh.vertexCount / 3),
}),
});
m_as.alphaSpheresBlas = m_device.createAccelerationStructure(AccelerationStructureOptions{
.label = "AlphaSphereBLAS",
.type = AccelerationStructureType::BottomLevel,
.flags = AccelerationStructureFlagBits::PreferFastBuild,
.geometryTypesAndCount = std::vector<AccelerationStructureOptions::GeometryTypeAndCount>(
AlphaSpheresCount,
{
.geometry = AccelerationStructureGeometryTrianglesData{
.vertexFormat = Format::R32G32B32_SFLOAT,
.vertexStride = sizeof(Vertex),
.maxVertex = static_cast<uint32_t>(m_sphereMesh.vertexCount - 1),
},
.maxPrimitiveCount = static_cast<uint32_t>(m_sphereMesh.vertexCount / 3),
}),
});
auto buildSphereTriangleGeometries = [this](Handle<AccelerationStructure_t> dstStructure,
size_t count, size_t transformOffset = 0) {
std::vector<AccelerationStructureGeometry> geometries;
geometries.reserve(count);
for (size_t i = 0; i < count; ++i) {
geometries.emplace_back(
AccelerationStructureGeometryTrianglesData{
.vertexFormat = Format::R32G32B32_SFLOAT,
.vertexData = m_sphereMesh.vertexBuffer,
.vertexStride = sizeof(Vertex),
.maxVertex = static_cast<uint32_t>(m_sphereMesh.vertexCount - 1),
.transformData = m_particles.blasTransformBuffer,
.transformDataOffset = transformOffset + i * sizeof(VkTransformMatrixKHR),
});
}
return BuildAccelerationStructureOptions::BuildOptions{
.geometries = std::move(geometries),
.destinationStructure = dstStructure,
.buildRangeInfos = std::vector<BuildAccelerationStructureOptions::BuildRangeInfo>(
count,
{
.primitiveCount = static_cast<uint32_t>(m_sphereMesh.vertexCount / 3),
}),
};
};
m_as.opaqueSpheresASBuildOptions = {
.buildGeometryInfos = { buildSphereTriangleGeometries(m_as.opaqueSpheresBlas, OpaqueSpheresCount, 0) },
};
m_as.alphaSpheresASBuildOptions = {
.buildGeometryInfos = { buildSphereTriangleGeometries(m_as.alphaSpheresBlas, AlphaSpheresCount, OpaqueSpheresCount * sizeof(VkTransformMatrixKHR)) },
};
|
Filename: hybrid_raster_rt/hybrid_raster_rt.cpp
Ground plane BLAS:
The plane is static, so its BLAS is built only once (on the first frame).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | m_as.opaquePlaneBlas = m_device.createAccelerationStructure(AccelerationStructureOptions{
.label = "PlaneBLAS",
.type = AccelerationStructureType::BottomLevel,
.flags = AccelerationStructureFlagBits::PreferFastBuild,
.geometryTypesAndCount = {
{
.geometry = AccelerationStructureGeometryTrianglesData{
.vertexFormat = Format::R32G32B32_SFLOAT,
.vertexStride = sizeof(Vertex),
.maxVertex = static_cast<uint32_t>(m_planeMesh.vertexCount - 1),
},
.maxPrimitiveCount = static_cast<uint32_t>(m_planeMesh.vertexCount / 3),
},
},
});
m_as.opaquePlaneASBuildOptions = {
.buildGeometryInfos = {
{
.geometries = {
AccelerationStructureGeometryTrianglesData{
.vertexFormat = Format::R32G32B32_SFLOAT,
.vertexData = m_planeMesh.vertexBuffer,
.vertexStride = sizeof(Vertex),
.maxVertex = static_cast<uint32_t>(m_planeMesh.vertexCount - 1),
},
},
.destinationStructure = m_as.opaquePlaneBlas,
.buildRangeInfos = {
{ .primitiveCount = static_cast<uint32_t>(m_planeMesh.vertexCount / 3) },
},
},
},
};
|
Filename: hybrid_raster_rt/hybrid_raster_rt.cpp
TLAS:
- The Top-Level Acceleration Structure references all three BLAS as instances.
- The two sphere BLAS use
TriangleFacingCullDisable so both sides of every sphere triangle are visible.
- The opaque spheres carry
ForceOpaque (skips any-hit) while the alpha spheres carry ForceNoOpaque (enables any-hit for transparency):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 | m_as.tBlas = m_device.createAccelerationStructure(AccelerationStructureOptions{
.label = "TBLAS",
.type = AccelerationStructureType::TopLevel,
.flags = AccelerationStructureFlagBits::PreferFastBuild,
.geometryTypesAndCount = {
{
.geometry = AccelerationStructureGeometryInstancesData{},
.maxPrimitiveCount = 3, // 3 BLAS
},
},
});
m_as.tlASBuildOptions = {
.buildGeometryInfos = {
{
.geometries = {
AccelerationStructureGeometryInstancesData{
.data = {
AccelerationStructureGeometryInstance{
.flags = GeometryInstanceFlagBits::TriangleFacingCullDisable | GeometryInstanceFlagBits::ForceOpaque,
.accelerationStructure = m_as.opaqueSpheresBlas,
},
AccelerationStructureGeometryInstance{
.flags = GeometryInstanceFlagBits::TriangleFacingCullDisable | GeometryInstanceFlagBits::ForceNoOpaque,
.accelerationStructure = m_as.alphaSpheresBlas,
},
AccelerationStructureGeometryInstance{
.flags = GeometryInstanceFlagBits::TriangleFacingCullDisable | GeometryInstanceFlagBits::ForceOpaque,
.accelerationStructure = m_as.opaquePlaneBlas,
},
},
},
},
.destinationStructure = m_as.tBlas,
.buildRangeInfos = {
{ .primitiveCount = 3 }, // 3 BLAS
},
},
},
};
|
Filename: hybrid_raster_rt/hybrid_raster_rt.cpp
Shadow Ray Tracing Pipeline
The shadow pass uses four shaders and a two-hit-group SBT.
Two separate hit shaders handle the two BLAS types differently:
- opaque geometry uses the closest-hit shader
- alpha geometry uses the any-hit shader to accumulate a hit counter rather than short-circuit on the first intersection:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | // Create raytracing shaders
auto rayTracingGenShaderPath = KDGpuExample::assetDir().file("shaders/examples/hybrid_raster_rt/shadow.rgen.spv");
auto rayTracingMissShaderPath = KDGpuExample::assetDir().file("shaders/examples/hybrid_raster_rt/shadow.rmiss.spv");
auto rayTracingAnyHitShaderPath = KDGpuExample::assetDir().file("shaders/examples/hybrid_raster_rt/shadow.rahit.spv");
auto rayTracingClosestHitShaderPath = KDGpuExample::assetDir().file("shaders/examples/hybrid_raster_rt/shadow.rchit.spv");
auto rayTracingGenShader = m_device.createShaderModule(readShaderFile(rayTracingGenShaderPath));
auto rayTracingMissShader = m_device.createShaderModule(readShaderFile(rayTracingMissShaderPath));
auto rayTracingAnyHitShader = m_device.createShaderModule(readShaderFile(rayTracingAnyHitShaderPath));
auto rayTracingClosestHitShader = m_device.createShaderModule(readShaderFile(rayTracingClosestHitShaderPath));
// Create a pipeline layout (array of bind group layouts)
m_shadowPass.pipelineLayout = m_device.createPipelineLayout({
.label = "RTShadows",
.bindGroupLayouts = { m_gbuffer.opaqueNormalDepthBindGroupLayout, m_gbuffer.shadowBindGroupLayout, m_as.tsASBindGroupLayout },
.pushConstantRanges = { m_global.lightPosPushConstant },
});
// Create a raytracing pipeline
m_shadowPass.pipeline = m_device.createRayTracingPipeline({
.shaderStages = {
ShaderStage{
.shaderModule = rayTracingGenShader.handle(),
.stage = ShaderStageFlagBits::RaygenBit,
},
ShaderStage{
.shaderModule = rayTracingMissShader.handle(),
.stage = ShaderStageFlagBits::MissBit,
},
ShaderStage{
.shaderModule = rayTracingAnyHitShader.handle(),
.stage = ShaderStageFlagBits::AnyHitBit, // For Alpha BLAS
},
ShaderStage{
.shaderModule = rayTracingClosestHitShader.handle(),
.stage = ShaderStageFlagBits::ClosestHitBit, // For Opaque BLAS
},
},
.shaderGroups = {
// Gen
RayTracingShaderGroupOptions{
.type = RayTracingShaderGroupType::General,
.generalShaderIndex = 0,
},
// Miss
RayTracingShaderGroupOptions{
.type = RayTracingShaderGroupType::General,
.generalShaderIndex = 1,
},
// Hit
RayTracingShaderGroupOptions{
.type = RayTracingShaderGroupType::TrianglesHit,
.closestHitShaderIndex = 3,
.anyHitShaderIndex = 2,
},
},
.layout = m_shadowPass.pipelineLayout,
});
// Shader Binding Table
m_shadowPass.sbt = RayTracingShaderBindingTable(&m_device, RayTracingShaderBindingTableOptions{
.nbrMissShaders = 1,
.nbrHitShaders = 1,
});
m_shadowPass.sbt.addRayGenShaderGroup(m_shadowPass.pipeline, 0);
m_shadowPass.sbt.addMissShaderGroup(m_shadowPass.pipeline, 1);
m_shadowPass.sbt.addHitShaderGroup(m_shadowPass.pipeline, 2);
|
Filename: hybrid_raster_rt/hybrid_raster_rt.cpp
Per-Frame Render Passes
Compute Animation
The sphere's positions and velocities are updated using a compute shader.
1
2
3
4
5
6
7
8
9
10
11
12
13 | commandRecorder.beginDebugLabel(DebugLabelOptions{
.label = "Compute - Particles Update",
.color = { 0.0f, 1.0f, 0.0f, 1.0f },
});
auto computePass = commandRecorder.beginComputePass();
computePass.setPipeline(m_particles.computePipeline);
computePass.setBindGroup(0, m_particles.particleBindGroup);
constexpr size_t LocalWorkGroupXSize = 256;
computePass.dispatchCompute(ComputeCommand{ .workGroupX = ParticlesCount / LocalWorkGroupXSize + 1 });
computePass.end();
commandRecorder.endDebugLabel();
|
Filename: hybrid_raster_rt/hybrid_raster_rt.cpp
Acceleration Structure Rebuild
After the compute pass writes new per-sphere transforms into blasTransformBuffer, a buffer barrier ensures the AS build sees the updated data.
Dynamic BLAS are rebuilt every frame; the static plane BLAS is only built once:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | commandRecorder.beginDebugLabel(DebugLabelOptions{
.label = "Acceleration Structures Rebuild",
.color = { 1.0f, 0.0f, 0.0f, 1.0f },
});
commandRecorder.bufferMemoryBarrier(KDGpu::BufferMemoryBarrierOptions{
.srcStages = KDGpu::PipelineStageFlagBit::ComputeShaderBit,
.srcMask = KDGpu::AccessFlagBit::ShaderWriteBit,
.dstStages = KDGpu::PipelineStageFlagBit::AccelerationStructureBuildBit,
.dstMask = KDGpu::AccessFlagBit::AccelerationStructureReadBit,
.buffer = m_particles.blasTransformBuffer,
});
commandRecorder.buildAccelerationStructures(m_as.opaqueSpheresASBuildOptions);
commandRecorder.buildAccelerationStructures(m_as.alphaSpheresASBuildOptions);
if (!m_as.hasBuiltStaticBlas) {
// Only needs to be done once
commandRecorder.buildAccelerationStructures(m_as.opaquePlaneASBuildOptions);
m_as.hasBuiltStaticBlas = true;
}
// Wait for the BLAS to have been built prior to building the TLAS
commandRecorder.memoryBarrier(MemoryBarrierOptions{
.srcStages = PipelineStageFlags(PipelineStageFlagBit::AccelerationStructureBuildBit),
.dstStages = PipelineStageFlags(PipelineStageFlagBit::AccelerationStructureBuildBit),
.memoryBarriers = {
{
.srcMask = AccessFlags(AccessFlagBit::AccelerationStructureWriteBit),
.dstMask = AccessFlags(AccessFlagBit::AccelerationStructureReadBit),
},
},
});
commandRecorder.buildAccelerationStructures(m_as.tlASBuildOptions);
commandRecorder.endDebugLabel();
|
Filename: hybrid_raster_rt/hybrid_raster_rt.cpp
Shadow Ray Tracing Pass
For each pixel, the ray generation shader reads the G-Buffer world position and fires a shadow ray towards the light.
The pipeline waits for both the TLAS rebuild and the G-Buffer opaque fill before issuing the trace:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74 | commandRecorder.beginDebugLabel(DebugLabelOptions{
.label = "Shadow RT",
.color = { 0.5f, 1.0f, 0.5f, 1.0f },
});
// Await TLAS rebuild
commandRecorder.memoryBarrier(MemoryBarrierOptions{
.srcStages = PipelineStageFlags(PipelineStageFlagBit::AccelerationStructureBuildBit),
.dstStages = PipelineStageFlags(PipelineStageFlagBit::RayTracingShaderBit),
.memoryBarriers = {
{
.srcMask = AccessFlagBit::AccelerationStructureWriteBit,
.dstMask = AccessFlagBit::AccelerationStructureReadBit,
},
},
});
// Await GBuffer World Pos filling
commandRecorder.textureMemoryBarrier(KDGpu::TextureMemoryBarrierOptions{
.srcStages = KDGpu::PipelineStageFlags(KDGpu::PipelineStageFlagBit::ColorAttachmentOutputBit),
.srcMask = KDGpu::AccessFlagBit::ColorAttachmentWriteBit,
.dstStages = KDGpu::PipelineStageFlags(KDGpu::PipelineStageFlagBit::RayTracingShaderBit),
.dstMask = KDGpu::AccessFlagBit::ShaderReadBit,
.oldLayout = KDGpu::TextureLayout::ColorAttachmentOptimal,
.newLayout = KDGpu::TextureLayout::ShaderReadOnlyOptimal,
.texture = m_gbuffer.posTexture,
.range = {
.aspectMask = KDGpu::TextureAspectFlagBits::ColorBit,
.levelCount = 1,
},
});
// Transition Shadow Image to General Layout
commandRecorder.textureMemoryBarrier(TextureMemoryBarrierOptions{
.srcStages = KDGpu::PipelineStageFlags(KDGpu::PipelineStageFlagBit::TopOfPipeBit),
.srcMask = KDGpu::AccessFlagBit::None,
.dstStages = KDGpu::PipelineStageFlags(KDGpu::PipelineStageFlagBit::RayTracingShaderBit),
.dstMask = KDGpu::AccessFlagBit::ShaderStorageReadBit | KDGpu::AccessFlagBit::ShaderStorageWriteBit,
.oldLayout = m_gbuffer.shadowTextureLayout,
.newLayout = KDGpu::TextureLayout::General,
.texture = m_gbuffer.shadowTexture,
.range = {
.aspectMask = KDGpu::TextureAspectFlagBits::ColorBit,
.levelCount = 1,
},
});
m_gbuffer.shadowTextureLayout = KDGpu::TextureLayout::General;
auto rtPass = commandRecorder.beginRayTracingPass();
rtPass.setPipeline(m_shadowPass.pipeline);
rtPass.pushConstant(m_global.lightPosPushConstant, &m_global.lightPos);
rtPass.setBindGroup(0, m_gbuffer.opaqueNormalDepthBindGroup);
rtPass.setBindGroup(1, m_gbuffer.shadowBindGroup);
rtPass.setBindGroup(2, m_as.tsASBindGroup);
// Issue RT Trace call using the SBT table we previously filled
// Do note one thing:
// Opaque BLAS will use closest hit shader (the any hit shader is disabled for BLAS marked opaque)
// ALPHA BLAS will use the any hit shader
rtPass.traceRays(RayTracingCommand{
.raygenShaderBindingTable = m_shadowPass.sbt.rayGenShaderRegion(),
.missShaderBindingTable = m_shadowPass.sbt.missShaderRegion(),
.hitShaderBindingTable = m_shadowPass.sbt.hitShaderRegion(),
.extent = {
.width = m_swapchainExtent.width,
.height = m_swapchainExtent.height,
.depth = 1,
},
});
rtPass.end();
commandRecorder.endDebugLabel();
|
Filename: hybrid_raster_rt/hybrid_raster_rt.cpp
Shadow Shaders
All shadow shaders are under assets/shaders/examples/hybrid_raster_rt/.
Ray Generation — shadow.rgen
For each launched pixel the shader samples the G-Buffer world position. If the position is valid (non-zero), it fires a shadow ray toward the light source. The tMin is set to the maximum possible sphere diameter (4.0) to avoid self-intersection against the sphere the world position lies on. The result is the accumulated hitCounter value written to the shadow image:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 | #version 460 core
#extension GL_EXT_ray_tracing : enable
layout(location = 0) rayPayloadEXT uint hitCounter;
layout(set = 0, binding = 0) uniform sampler2D gBufferWorldPositions;
layout(set = 0, binding = 1) uniform sampler2D gBufferWorldNormals;
layout(set = 0, binding = 2) uniform sampler2D gBufferColors;
layout(set = 0, binding = 3) uniform sampler2D gBufferDepth;
layout(set = 1, binding = 0, r32f) uniform writeonly image2D shadows;
layout(set = 2, binding = 0) uniform accelerationStructureEXT topLevelAS;
layout(push_constant) uniform PushConstants
{
vec3 lightWorldPos;
}
pushConstants;
void main()
{
const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
const vec2 uv = pixelCenter / vec2(gl_LaunchSizeEXT.xy);
// We want to issue a ray that start at each world pos we have recorded for the opaque geometries
// and goes back to the light source. Any intersection along the ray means we have shadows
// Ray
vec3 origin = texture(gBufferWorldPositions, uv).xyz;
vec3 direction = normalize(pushConstants.lightWorldPos - origin);
uint rayFlags = gl_RayFlagsNoneEXT;
// Note: most of our meshes are spheres.
// Given a world position on a sphere could be anywhere even underneath,
// we want to avoid finding intersections against that same sphere. To do so, we set tMin at a value that is the largest possible sphere diameter
float tMin = 4.0;
float tMax = 1000.0;
hitCounter = 0;
if (origin != vec3(0.0, 0.0, 0.0)) { // Only bother tracing if we have a valid world pos
traceRayEXT(topLevelAS, // acceleration structure
rayFlags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
origin, // ray origin
tMin, // ray min range
direction, // ray direction
tMax, // ray max range
0 // hitCounter (location = 0)
);
}
imageStore(shadows, ivec2(gl_LaunchIDEXT.xy), vec4(float(hitCounter)));
}
|
Filename: hybrid_raster_rt/shadow.rgen
Closest Hit — shadow.rchit
For opaque geometry the closest-hit shader is invoked on the first solid intersection. It sets hitCounter to 3 (a non-zero sentinel meaning "fully in shadow") to signal that the ray was blocked:
| #version 460 core
#extension GL_EXT_ray_tracing : enable
layout(location = 0) rayPayloadInEXT uint hitCounter;
void main()
{
hitCounter = 3;
}
|
Filename: hybrid_raster_rt/shadow.rchit
Any Hit — shadow.rahit
For alpha geometry the any-hit shader fires on every intersected triangle rather than only the nearest one. It increments hitCounter each time, allowing the compositing pass to use the count as a shadow intensity:
| #version 460 core
#extension GL_EXT_ray_tracing : enable
layout(location = 0) rayPayloadInEXT uint hitCounter;
void main()
{
hitCounter = hitCounter + 1;
}
|
Filename: hybrid_raster_rt/shadow.rahit
Miss — shadow.rmiss
If the ray reaches tMax without hitting anything, the miss shader fires and resets hitCounter to 0 (fully lit):
| #version 460 core
#extension GL_EXT_ray_tracing : enable
layout(location = 0) rayPayloadInEXT uint hitCounter;
void main()
{
hitCounter = 0;
}
|
Filename: hybrid_raster_rt/shadow.rmiss
Rasterization phase: Linear in triangle count, excellent cache locality, highly optimized on all GPUs.
Ray tracing phase: Relatively predictable cost - one ray per pixel for shadows. More complex for reflections (many rays per pixel).
Hybrid benefits: Rasterization provides fast primary shading; ray tracing adds quality without per-pixel overhead of full ray tracing.
Vulkan Specifications
Key extensions and features used:
Further Reading
See Also
Updated on 2026-03-31 at 00:02:07 +0000