feat(vulkan-rt): Mesh::BuildProcedural — AABB (procedural) BLAS build path (#33) #35

Merged
catbot merged 2 commits from claude/issue-33 into master 2026-06-13 02:18:31 +02:00
10 changed files with 445 additions and 44 deletions

View file

@ -1,26 +1,36 @@
# RTVolume # RTVolume
WebGPU software ray tracing of **procedural (AABB) geometry** with an Ray tracing of **procedural (AABB) geometry** with an **any-hit** cut-out
**any-hit** cut-out — the two features added for issue #13. on both backends — software WebGPU (issue #13) and native Vulkan hardware
RT (issue #33).
A 3×3×3 grid of unit boxes is registered as an AABB BLAS A 3×3×3 grid of unit boxes is registered as an AABB BLAS
(`Mesh::BuildProcedural`, the WebGPU analog of `VK_GEOMETRY_TYPE_AABBS_KHR`). (`Mesh::BuildProcedural``VK_GEOMETRY_TYPE_AABBS_KHR` on Vulkan, the
The hit group is a `RTShaderGroupType::ProceduralHitGroup` carrying: software AABB-leaf path on WebGPU). The hit group is procedural
(`RTShaderGroupType::ProceduralHitGroup` /
`VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR`) carrying:
- `intersection.wgsl` — analytic raysphere test that turns each box into a - `intersection.wgsl` / `intersection.glsl` — analytic raysphere test that
radius-1 sphere (runs in TRACE, once per box the ray enters); turns each box into a radius-1 sphere (runs once per box the ray enters);
- `anyhit.wgsl` — returns `RT_ANYHIT_IGNORE` for half the cells of a - `anyhit.wgsl` / `anyhit.glsl` — ignores the intersection for half the
spherical checkerboard, so the ray passes through and the background / cells of a spherical checkerboard, so the ray passes through and the
spheres behind show through (the visible proof any-hit runs); background / spheres behind show through (the visible proof any-hit runs);
- `closesthit.wgsl` — normal-based Lambert shading, tinted per instance. - `closesthit.wgsl` / `closesthit.glsl` — normal-based Lambert shading,
tinted per instance.
The geometry is registered **non-opaque** and the instances clear their The geometry is registered **non-opaque** and the instances clear their
force-opaque flag, which is what lets the any-hit shader run. Flip the force-opaque flag, which is what lets the any-hit shader run. Flip the
instance flag to `kRTGeometryInstanceForceOpaque` (or build the mesh with instance flag to `kRTGeometryInstanceForceOpaque` (or build the mesh with
`opaque = true`) to skip any-hit and see solid spheres. `opaque = true`) to skip any-hit and see solid spheres.
WebGPU/DOM only: WebGPU/DOM (free camera, WASD + mouse):
``` ```
crafter-build --target=wasm32-wasip1 -r crafter-build --target=wasm32-wasip1 -r
``` ```
Native Vulkan (fixed camera in raygen.glsl):
```
crafter-build -r
```

View file

@ -0,0 +1,30 @@
#version 460
#extension GL_EXT_ray_tracing : enable
// RTVolume any-hit shader — runs on every candidate sphere hit because
// the geometry is built non-opaque (BuildProcedural opaque=false) and
// the instances don't force-opaque. Punches a spherical checkerboard of
// holes: for half the cells it calls ignoreIntersectionEXT, so the ray
// passes straight through and the background / spheres behind show
// through — the visible proof the any-hit path runs. Mirrors
// anyhit.wgsl on the WebGPU path.
hitAttributeEXT vec2 attribs;
layout(location = 0) rayPayloadInEXT vec3 hitValue;
void main() {
// Object-space hit point on the unit sphere → its normal/direction.
vec3 posObj = gl_ObjectRayOriginEXT + gl_ObjectRayDirectionEXT * gl_HitTEXT;
vec3 n = normalize(posObj);
const float PI = 3.14159265;
float longitude = atan(n.z, n.x); // [-PI, PI]
float latitude = asin(clamp(n.y, -1.0, 1.0)); // [-PI/2, PI/2]
int cu = int(floor((longitude + PI) / PI * 6.0));
int cv = int(floor((latitude + PI * 0.5) / PI * 6.0));
if (((cu + cv) & 1) == 0) {
ignoreIntersectionEXT; // cut-out cell — see through
}
}

View file

@ -0,0 +1,38 @@
#version 460
#extension GL_EXT_ray_tracing : enable
// RTVolume closest-hit — shades the committed procedural sphere hit by
// its surface normal with a fixed sun + ambient, tinted per instance
// (gl_InstanceCustomIndexEXT). Mirrors closesthit.wgsl on the WebGPU
// path.
hitAttributeEXT vec2 attribs;
layout(location = 0) rayPayloadInEXT vec3 hitValue;
const vec3 SUN_DIR_TO_LIGHT = vec3(0.40, 0.85, 0.35);
const vec3 SUN_COLOR = vec3(1.20, 1.10, 0.95);
const vec3 AMBIENT_COLOR = vec3(0.16, 0.18, 0.24);
vec3 instanceAlbedo(uint i) {
uint h = i * 2654435761u;
return vec3(
0.35 + 0.6 * float((h >> 0) & 255u) / 255.0,
0.35 + 0.6 * float((h >> 8) & 255u) / 255.0,
0.35 + 0.6 * float((h >> 16) & 255u) / 255.0);
}
void main() {
// Object-space hit point on the unit sphere is its object-space
// normal; gl_ObjectToWorldEXT's rotation part takes it to world.
vec3 posObj = gl_ObjectRayOriginEXT + gl_ObjectRayDirectionEXT * gl_HitTEXT;
vec3 nObj = normalize(posObj);
vec3 nWorld = normalize(gl_ObjectToWorldEXT * vec4(nObj, 0.0));
vec3 albedo = instanceAlbedo(uint(gl_InstanceCustomIndexEXT));
vec3 viewDir = -gl_WorldRayDirectionEXT;
vec3 nFacing = dot(nWorld, viewDir) > 0.0 ? nWorld : -nWorld;
vec3 sunDir = normalize(SUN_DIR_TO_LIGHT);
float nDotL = max(0.0, dot(nFacing, sunDir));
hitValue = albedo * (AMBIENT_COLOR + SUN_COLOR * nDotL);
}

View file

@ -0,0 +1,32 @@
#version 460
#extension GL_EXT_ray_tracing : enable
// RTVolume intersection shader — runs once per AABB the ray enters
// (VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR). Analytic
// ray-sphere test: the unit box [-1,1]^3 built by Mesh::BuildProcedural
// is treated as the bounding volume of a radius-1 sphere centred at the
// object origin. The object-space ray is NOT normalised (it is
// worldToObject * worldRay), so the reported t is directly comparable to
// the world-space ray parameter — solve the quadratic with the general
// a = dot(d,d) form rather than assuming |d| == 1. Mirrors
// intersection.wgsl on the WebGPU path.
hitAttributeEXT vec2 attribs;
void main() {
vec3 oc = gl_ObjectRayOriginEXT; // sphere centre is the origin
vec3 d = gl_ObjectRayDirectionEXT;
float a = dot(d, d);
float b = 2.0 * dot(oc, d);
float c = dot(oc, oc) - 1.0; // radius 1
float disc = b * b - 4.0 * a * c;
if (disc < 0.0) return;
float sq = sqrt(disc);
float t = (-b - sq) / (2.0 * a); // near root
if (t < gl_RayTminEXT) t = (-b + sq) / (2.0 * a); // fall back to far root
if (t < gl_RayTminEXT || t > gl_RayTmaxEXT) return;
attribs = vec2(0.0);
reportIntersectionEXT(t, 0u);
}

View file

@ -1,22 +1,183 @@
// RTVolume — procedural (AABB) ray tracing on the WebGPU wavefront tracer. // RTVolume — procedural (AABB) ray tracing on both backends. Demonstrates
// Demonstrates the two features this example was written to exercise: // 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 // (Mesh::BuildProcedural) whose surface is supplied by an intersection
// shader (here an analytic raysphere test). The boxes are unit cubes // shader (here an analytic raysphere test). The boxes are unit cubes
// [-1,1]^3; the intersection shader turns each into a sphere. // [-1,1]^3; the intersection shader turns each into a sphere.
// //
// * any-hit — the spheres are registered non-opaque, and an any-hit // * any-hit — the spheres are registered non-opaque, and an any-hit
// shader punches a spherical checkerboard of holes by returning // shader punches a spherical checkerboard of holes by ignoring the
// RT_ANYHIT_IGNORE for half the cells. Without any-hit the spheres are // intersection for half the cells. Without any-hit the spheres are
// solid; with it you can see the background (and other spheres) // solid; with it you can see the background (and other spheres)
// through the cut-out cells. // through the cut-out cells.
// //
// A 3×3×3 grid of these procedural spheres is shaded by surface normal + // 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 #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 #else
import Crafter.Graphics; import Crafter.Graphics;

View file

@ -0,0 +1,12 @@
#version 460
#extension GL_EXT_ray_tracing : enable
// RTVolume miss — vertical sky gradient, also what shows through the
// any-hit cut-out cells. Mirrors miss.wgsl on the WebGPU path.
layout(location = 0) rayPayloadInEXT vec3 hitValue;
void main() {
float t = clamp(gl_WorldRayDirectionEXT.y * 0.5 + 0.5, 0.0, 1.0);
hitValue = mix(vec3(0.05, 0.07, 0.12), vec3(0.45, 0.60, 0.85), t);
}

View file

@ -43,6 +43,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.files.emplace_back(fs::path("miss.wgsl")); cfg.files.emplace_back(fs::path("miss.wgsl"));
cfg.files.emplace_back(fs::path("resolve.wgsl")); cfg.files.emplace_back(fs::path("resolve.wgsl"));
EnableWasiBrowserRuntime(cfg); EnableWasiBrowserRuntime(cfg);
} else {
cfg.shaders.emplace_back(fs::path("raygen.glsl"), std::string("main"), ShaderType::RayGen);
cfg.shaders.emplace_back(fs::path("miss.glsl"), std::string("main"), ShaderType::Miss);
cfg.shaders.emplace_back(fs::path("closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
cfg.shaders.emplace_back(fs::path("anyhit.glsl"), std::string("main"), ShaderType::AnyHit);
cfg.shaders.emplace_back(fs::path("intersection.glsl"), std::string("main"), ShaderType::Intersect);
} }
return cfg; return cfg;
} }

View file

@ -0,0 +1,50 @@
#version 460
#extension GL_EXT_ray_tracing : enable
#extension GL_EXT_shader_image_load_formatted : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable
#extension GL_EXT_descriptor_heap : enable
#extension GL_EXT_nonuniform_qualifier : enable
// Specialization constant set from descriptorHeap.bufferStartElement —
// same pattern as the Sponza example. The TLAS lives at descriptor_heap
// slot `bufferStart`, the per-frame output image at heap slot 0.
layout(constant_id = 0) const uint16_t bufferStart = 0us;
layout(descriptor_heap) uniform accelerationStructureEXT topLevelAS[];
layout(descriptor_heap) uniform writeonly image2D image[];
layout(location = 0) rayPayloadEXT vec3 hitValue;
void main() {
uvec2 pixel = gl_LaunchIDEXT.xy;
uvec2 resolution = gl_LaunchSizeEXT.xy;
vec2 uv = (vec2(pixel) + 0.5) / vec2(resolution);
vec2 ndc = uv * 2.0 - 1.0;
// Fixed camera framing the 3x3x3 grid (extent = (kGrid-1)*kSpacing
// = 6): position ext*(1.1, 0.8, 1.6) looking at the grid centre —
// the WebGPU example's initial free-camera pose.
vec3 origin = vec3(6.6, 4.8, 9.6);
vec3 forward = normalize(-origin);
vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0)));
vec3 up = cross(right, forward);
float aspect = float(resolution.x) / float(resolution.y);
float tanHalf = tan(radians(70.0) * 0.5);
vec3 direction = normalize(
right * (ndc.x * aspect * tanHalf) +
up * (-ndc.y * tanHalf) +
forward);
traceRayEXT(
topLevelAS[bufferStart],
gl_RayFlagsNoneEXT,
0xff,
0, 0, 0,
origin,
0.01,
direction,
100000.0,
0);
imageStore(image[0], ivec2(pixel), vec4(hitValue, 1.0));
}

View file

@ -39,31 +39,10 @@ namespace {
constexpr VkBufferUsageFlags2 kIndexUsageBase = constexpr VkBufferUsageFlags2 kIndexUsageBase =
kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkCommandBuffer cmd) { // Shared BLAS sizing / creation / build-record tail — geometry-type
VkDeviceOrHostAddressConstKHR vertexAddr; // agnostic; both the triangle and the AABB (procedural) paths feed
vertexAddr.deviceAddress = self.vertexBuffer.address; // their single geometry through here.
void RecordBLASBuildFromGeometry(Mesh& self, const VkAccelerationStructureGeometryKHR& blasGeometry, std::uint32_t primitiveCount, VkCommandBuffer cmd) {
VkDeviceOrHostAddressConstKHR indexAddr;
indexAddr.deviceAddress = self.indexBuffer.address;
auto trianglesData = VkAccelerationStructureGeometryTrianglesDataKHR {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR,
.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT,
.vertexData = vertexAddr,
.vertexStride = sizeof(Vector<float, 3, 3>),
.maxVertex = vertexCount - 1,
.indexType = VK_INDEX_TYPE_UINT32,
.indexData = indexAddr,
.transformData = {.deviceAddress = 0}
};
VkAccelerationStructureGeometryDataKHR geometryData(trianglesData);
VkAccelerationStructureGeometryKHR blasGeometry {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR,
.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR,
.geometry = geometryData,
.flags = VK_GEOMETRY_OPAQUE_BIT_KHR
};
VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{ VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR, .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR,
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR, .type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
@ -72,7 +51,6 @@ namespace {
.pGeometries = &blasGeometry, .pGeometries = &blasGeometry,
}; };
auto primitiveCount = indexCount / 3;
VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = { VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
}; };
@ -122,6 +100,34 @@ namespace {
}; };
self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo); self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
} }
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkCommandBuffer cmd) {
VkDeviceOrHostAddressConstKHR vertexAddr;
vertexAddr.deviceAddress = self.vertexBuffer.address;
VkDeviceOrHostAddressConstKHR indexAddr;
indexAddr.deviceAddress = self.indexBuffer.address;
auto trianglesData = VkAccelerationStructureGeometryTrianglesDataKHR {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR,
.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT,
.vertexData = vertexAddr,
.vertexStride = sizeof(Vector<float, 3, 3>),
.maxVertex = vertexCount - 1,
.indexType = VK_INDEX_TYPE_UINT32,
.indexData = indexAddr,
.transformData = {.deviceAddress = 0}
};
VkAccelerationStructureGeometryDataKHR geometryData(trianglesData);
VkAccelerationStructureGeometryKHR blasGeometry {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR,
.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR,
.geometry = geometryData,
.flags = VK_GEOMETRY_OPAQUE_BIT_KHR
};
RecordBLASBuildFromGeometry(self, blasGeometry, indexCount / 3, cmd);
}
} }
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) { void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
@ -203,3 +209,32 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) {
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, cmd); RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, cmd);
} }
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd) {
this->opaque = opaque;
// 24-byte-stride VkAabbPositionsKHR-compatible build input
// (static_assert'd in the interface). Same usage set as the triangle
// inputs: AS-build read-only + device address.
aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast<std::uint32_t>(aabbs.size()));
std::memcpy(aabbBuffer.value, aabbs.data(), aabbs.size() * sizeof(RTAabb));
aabbBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
VkAccelerationStructureGeometryAabbsDataKHR aabbsData {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR,
.data = { .deviceAddress = aabbBuffer.address },
.stride = sizeof(RTAabb)
};
VkAccelerationStructureGeometryDataKHR geometryData;
geometryData.aabbs = aabbsData;
VkAccelerationStructureGeometryKHR blasGeometry {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR,
.geometryType = VK_GEOMETRY_TYPE_AABBS_KHR,
.geometry = geometryData,
// Non-opaque by default (mirrors the WebGPU path) so any-hit
// shaders run for procedural geometry unless the caller opts out.
.flags = opaque ? static_cast<VkGeometryFlagsKHR>(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{}
};
RecordBLASBuildFromGeometry(*this, blasGeometry, static_cast<std::uint32_t>(aabbs.size()), cmd);
}

View file

@ -31,12 +31,27 @@ import Crafter.Asset;
import :VulkanBuffer; import :VulkanBuffer;
export namespace Crafter { export namespace Crafter {
// One procedural primitive's axis-aligned bounding box, in object
// space. Layout-compatible with VkAabbPositionsKHR (24-byte stride),
// so a span of these uploads directly as a
// VK_GEOMETRY_TYPE_AABBS_KHR build input. Mirrors the DOM-side
// definition below so portable user code compiles unchanged.
struct RTAabb {
float min[3];
float max[3];
};
static_assert(sizeof(RTAabb) == sizeof(VkAabbPositionsKHR));
class Mesh { class Mesh {
public: public:
VulkanBuffer<char, false> scratchBuffer; VulkanBuffer<char, false> scratchBuffer;
VulkanBuffer<char, false> blasBuffer; VulkanBuffer<char, false> blasBuffer;
VulkanBuffer<Vector<float, 3, 3>, true> vertexBuffer; VulkanBuffer<Vector<float, 3, 3>, true> vertexBuffer;
VulkanBuffer<std::uint32_t, true> indexBuffer; VulkanBuffer<std::uint32_t, true> indexBuffer;
// AABB build input for the procedural path (BuildProcedural).
// Lifetime contract matches vertexBuffer/indexBuffer: must stay
// alive until the build submitted on `cmd` completes.
VulkanBuffer<RTAabb, true> aabbBuffer;
// Lives until the cmd buffer issued by the compressed Build path // Lives until the cmd buffer issued by the compressed Build path
// completes execution. Kept as a member so the recorded // completes execution. Kept as a member so the recorded
// vkCmdDecompressMemoryEXT references valid memory until the queue // vkCmdDecompressMemoryEXT references valid memory until the queue
@ -57,6 +72,18 @@ export namespace Crafter {
// Region 2 (data) is not consumed here — the caller decompresses it // Region 2 (data) is not consumed here — the caller decompresses it
// into their own buffer if needed (or uses Compression::DecompressCPU). // into their own buffer if needed (or uses Compression::DecompressCPU).
void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd); void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd);
// Build an AABB (procedural) BLAS from a list of object-space
// boxes (VK_GEOMETRY_TYPE_AABBS_KHR). The hit group bound to
// instances of this mesh must be a
// VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR group
// carrying an intersection shader; that shader is invoked for
// each box the ray enters and reports the surface hit via
// reportIntersectionEXT. `opaque` maps to
// VK_GEOMETRY_OPAQUE_BIT_KHR: pass false (the WebGPU-path
// default) to let any-hit shaders run. Same scratch/lifetime
// handling as the triangle Build; instances reference blasAddr
// exactly like a triangle BLAS.
void BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd);
}; };
} }
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM #endif // !CRAFTER_GRAPHICS_WINDOW_DOM