test(vulkan-rt): port RTVolume example to native Vulkan (#33)
Regression test for the procedural BLAS path: the same 3x3x3 grid of unit-box AABBs runs through a PROCEDURAL_HIT_GROUP_KHR group whose GLSL intersection shader (reportIntersectionEXT) turns each box into a radius-1 sphere, the any-hit shader punches the spherical-checkerboard cut-out (visible proof non-opaque geometry runs any-hit), and the closest-hit shades per-instance tints — the WebGPU example behavior reproduced natively. Fixed camera in raygen.glsl; the WebGPU/DOM path is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a9e42f9c90
commit
40c4184d41
8 changed files with 357 additions and 18 deletions
|
|
@ -1,22 +1,183 @@
|
|||
// RTVolume — procedural (AABB) ray tracing on the WebGPU wavefront tracer.
|
||||
// Demonstrates the two features this example was written to exercise:
|
||||
// RTVolume — procedural (AABB) ray tracing on both backends. Demonstrates
|
||||
// the two features this example was written to exercise:
|
||||
//
|
||||
// * VK_GEOMETRY_TYPE_AABBS_KHR equivalent — a BLAS built from AABBs
|
||||
// * VK_GEOMETRY_TYPE_AABBS_KHR — a BLAS built from AABBs
|
||||
// (Mesh::BuildProcedural) whose surface is supplied by an intersection
|
||||
// shader (here an analytic ray–sphere test). The boxes are unit cubes
|
||||
// [-1,1]^3; the intersection shader turns each into a sphere.
|
||||
//
|
||||
// * any-hit — the spheres are registered non-opaque, and an any-hit
|
||||
// shader punches a spherical checkerboard of holes by returning
|
||||
// RT_ANYHIT_IGNORE for half the cells. Without any-hit the spheres are
|
||||
// shader punches a spherical checkerboard of holes by ignoring the
|
||||
// intersection for half the cells. Without any-hit the spheres are
|
||||
// solid; with it you can see the background (and other spheres)
|
||||
// through the cut-out cells.
|
||||
//
|
||||
// A 3×3×3 grid of these procedural spheres is shaded by surface normal +
|
||||
// a fixed sun. WebGPU/DOM only — this is the software RT path.
|
||||
// a fixed sun. The Vulkan path runs the same scene through hardware RT
|
||||
// (PROCEDURAL_HIT_GROUP_KHR with GLSL intersection / any-hit shaders, a
|
||||
// fixed camera in raygen.glsl); the WebGPU path is the software wavefront
|
||||
// tracer with a host-driven free camera.
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
int main() { return 0; } // native path is hardware RT; out of scope here
|
||||
#include "vulkan/vulkan.h"
|
||||
|
||||
import Crafter.Graphics;
|
||||
import Crafter.Math;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
constexpr int kGrid = 3;
|
||||
constexpr float kSpacing = 3.0f;
|
||||
}
|
||||
|
||||
int main() {
|
||||
const int instanceCount = kGrid * kGrid * kGrid;
|
||||
std::println("[RTVolume] grid {}^3 = {} procedural spheres", kGrid, instanceCount);
|
||||
|
||||
Device::Initialize();
|
||||
Window window(1280, 720, "RTVolume");
|
||||
VkCommandBuffer cmd = window.StartInit();
|
||||
|
||||
DescriptorHeapVulkan descriptorHeap;
|
||||
descriptorHeap.Initialize(/*images*/ 1, /*buffers*/ 1, /*samplers*/ 0);
|
||||
|
||||
// Specialization constant: the TLAS slot offset (same pattern as the
|
||||
// Sponza example). Camera is fixed in raygen.glsl — no user buffers.
|
||||
VkSpecializationMapEntry raygenEntry = { .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) };
|
||||
VkSpecializationInfo raygenSpec = {
|
||||
.mapEntryCount = 1, .pMapEntries = &raygenEntry,
|
||||
.dataSize = sizeof(std::uint16_t), .pData = &descriptorHeap.bufferStartElement,
|
||||
};
|
||||
|
||||
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
||||
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
||||
|
||||
// SBT order fixes the shader indices used by the groups below.
|
||||
std::array<VulkanShader, 5> shaders {{
|
||||
{ "raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &raygenSpec },
|
||||
{ "miss.spv", "main", VK_SHADER_STAGE_MISS_BIT_KHR, nullptr },
|
||||
{ "closesthit.spv", "main", VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR, nullptr },
|
||||
{ "anyhit.spv", "main", VK_SHADER_STAGE_ANY_HIT_BIT_KHR, nullptr },
|
||||
{ "intersection.spv", "main", VK_SHADER_STAGE_INTERSECTION_BIT_KHR, nullptr },
|
||||
}};
|
||||
ShaderBindingTableVulkan shaderTable;
|
||||
shaderTable.Init(shaders);
|
||||
|
||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> raygenGroups {{ {
|
||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
|
||||
.generalShader = 0, .closestHitShader = VK_SHADER_UNUSED_KHR,
|
||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
||||
} }};
|
||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> missGroups {{ {
|
||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
|
||||
.generalShader = 1, .closestHitShader = VK_SHADER_UNUSED_KHR,
|
||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
||||
} }};
|
||||
// One procedural hit group: closest-hit + any-hit + intersection.
|
||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> hitGroups {{ {
|
||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR,
|
||||
.generalShader = VK_SHADER_UNUSED_KHR, .closestHitShader = 2,
|
||||
.anyHitShader = 3, .intersectionShader = 4,
|
||||
} }};
|
||||
|
||||
PipelineRTVulkan pipeline;
|
||||
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable);
|
||||
|
||||
// ── One procedural unit-box BLAS. The intersection shader treats the
|
||||
// box as the bounding volume of a radius-1 sphere centred at the
|
||||
// object origin. opaque=false so the any-hit cut-out runs. ─────────
|
||||
std::array<RTAabb, 1> boxes {{
|
||||
{ .min = {-1.0f, -1.0f, -1.0f}, .max = {1.0f, 1.0f, 1.0f} },
|
||||
}};
|
||||
Mesh sphere;
|
||||
sphere.BuildProcedural(boxes, /*opaque*/ false, cmd);
|
||||
|
||||
// ── Instance grid. ─────────────────────────────────────────────────
|
||||
static std::vector<RenderingElement3D> renderers;
|
||||
renderers.reserve(static_cast<std::size_t>(instanceCount));
|
||||
const float origin0 = -0.5f * static_cast<float>(kGrid - 1) * kSpacing;
|
||||
for (int x = 0; x < kGrid; ++x)
|
||||
for (int y = 0; y < kGrid; ++y)
|
||||
for (int z = 0; z < kGrid; ++z) {
|
||||
renderers.emplace_back();
|
||||
RenderingElement3D& r = renderers.back();
|
||||
auto& tx = r.instance.transform.matrix;
|
||||
tx[0][0] = 1; tx[0][1] = 0; tx[0][2] = 0; tx[0][3] = origin0 + float(x) * kSpacing;
|
||||
tx[1][0] = 0; tx[1][1] = 1; tx[1][2] = 0; tx[1][3] = origin0 + float(y) * kSpacing;
|
||||
tx[2][0] = 0; tx[2][1] = 0; tx[2][2] = 1; tx[2][3] = origin0 + float(z) * kSpacing;
|
||||
r.instance.instanceCustomIndex = static_cast<std::uint32_t>(renderers.size() - 1);
|
||||
r.instance.mask = 0xFF;
|
||||
r.instance.instanceShaderBindingTableRecordOffset = 0;
|
||||
// flags = 0: do NOT force opaque, so the any-hit shader runs.
|
||||
r.instance.flags = 0;
|
||||
r.instance.accelerationStructureReference = sphere.blasAddr;
|
||||
RenderingElement3D::Add(&r);
|
||||
}
|
||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||
RenderingElement3D::BuildTLAS(cmd, f);
|
||||
}
|
||||
|
||||
window.FinishInit();
|
||||
|
||||
// Write descriptors: TLAS at bufSlots[0], output image at imgSlots[0].
|
||||
// Per-frame replicated — same pattern as the Sponza example.
|
||||
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
||||
VkImageDescriptorInfoEXT outImgInfos[Window::numFrames];
|
||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||
outImgInfos[f] = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
||||
.pView = &window.imageViews[f],
|
||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
||||
std::vector<VkHostAddressRangeEXT> destinations;
|
||||
resources.reserve(Window::numFrames * 2);
|
||||
destinations.reserve(Window::numFrames * 2);
|
||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||
resources.push_back({
|
||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
||||
.data = { .pAddressRange = &tlasRanges[f] },
|
||||
});
|
||||
destinations.push_back({
|
||||
.address = descriptorHeap.resourceHeap[f].value
|
||||
+ descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
||||
});
|
||||
resources.push_back({
|
||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||
.data = { .pImage = &outImgInfos[f] },
|
||||
});
|
||||
destinations.push_back({
|
||||
.address = descriptorHeap.resourceHeap[f].value
|
||||
+ descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
||||
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
||||
});
|
||||
}
|
||||
Device::vkWriteResourceDescriptorsEXT(Device::device,
|
||||
static_cast<std::uint32_t>(resources.size()),
|
||||
resources.data(), destinations.data());
|
||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||
descriptorHeap.resourceHeap[f].FlushDevice();
|
||||
}
|
||||
|
||||
window.descriptorHeap = &descriptorHeap;
|
||||
RTPass rtPass(&pipeline);
|
||||
window.passes.push_back(&rtPass);
|
||||
|
||||
window.Render();
|
||||
window.StartSync();
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
|
||||
import Crafter.Graphics;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue