feat(vulkan-rt): Mesh::BuildProcedural — AABB (procedural) BLAS build path (#33) #35
8 changed files with 357 additions and 18 deletions
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>
commit
40c4184d41
|
|
@ -1,26 +1,36 @@
|
|||
# RTVolume
|
||||
|
||||
WebGPU software ray tracing of **procedural (AABB) geometry** with an
|
||||
**any-hit** cut-out — the two features added for issue #13.
|
||||
Ray tracing of **procedural (AABB) geometry** with an **any-hit** cut-out
|
||||
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
|
||||
(`Mesh::BuildProcedural`, the WebGPU analog of `VK_GEOMETRY_TYPE_AABBS_KHR`).
|
||||
The hit group is a `RTShaderGroupType::ProceduralHitGroup` carrying:
|
||||
(`Mesh::BuildProcedural` — `VK_GEOMETRY_TYPE_AABBS_KHR` on Vulkan, the
|
||||
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 ray–sphere test that turns each box into a
|
||||
radius-1 sphere (runs in TRACE, once per box the ray enters);
|
||||
- `anyhit.wgsl` — returns `RT_ANYHIT_IGNORE` for half the cells of a
|
||||
spherical checkerboard, so the ray passes through and the background /
|
||||
spheres behind show through (the visible proof any-hit runs);
|
||||
- `closesthit.wgsl` — normal-based Lambert shading, tinted per instance.
|
||||
- `intersection.wgsl` / `intersection.glsl` — analytic ray–sphere test that
|
||||
turns each box into a radius-1 sphere (runs once per box the ray enters);
|
||||
- `anyhit.wgsl` / `anyhit.glsl` — ignores the intersection for half the
|
||||
cells of a spherical checkerboard, so the ray passes through and the
|
||||
background / spheres behind show through (the visible proof any-hit runs);
|
||||
- `closesthit.wgsl` / `closesthit.glsl` — normal-based Lambert shading,
|
||||
tinted per instance.
|
||||
|
||||
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
|
||||
instance flag to `kRTGeometryInstanceForceOpaque` (or build the mesh with
|
||||
`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
|
||||
```
|
||||
|
||||
Native Vulkan (fixed camera in raygen.glsl):
|
||||
|
||||
```
|
||||
crafter-build -r
|
||||
```
|
||||
|
|
|
|||
30
examples/RTVolume/anyhit.glsl
Normal file
30
examples/RTVolume/anyhit.glsl
Normal 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
|
||||
}
|
||||
}
|
||||
38
examples/RTVolume/closesthit.glsl
Normal file
38
examples/RTVolume/closesthit.glsl
Normal 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);
|
||||
}
|
||||
32
examples/RTVolume/intersection.glsl
Normal file
32
examples/RTVolume/intersection.glsl
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
12
examples/RTVolume/miss.glsl
Normal file
12
examples/RTVolume/miss.glsl
Normal 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);
|
||||
}
|
||||
|
|
@ -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("resolve.wgsl"));
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
50
examples/RTVolume/raygen.glsl
Normal file
50
examples/RTVolume/raygen.glsl
Normal 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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue