feat(webgpu-rt): add intersection stage, procedural hit group, AABB BLAS API

Extends the cross-backend RT type surface for procedural geometry +
any-hit on the WebGPU path:

- RTShaderGroupType::ProceduralHitGroup + RTShaderGroup::intersectionShader
  (mirror VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR).
- WebGPURTStage::Intersection for AABB intersection shaders.
- Mesh::BuildProcedural(span<RTAabb>, opaque) — the WebGPU analog of a
  VK_GEOMETRY_TYPE_AABBS_KHR geometry.
- wgpuRegisterMeshBLAS gains geomType / opaqueFlag / primCount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-02 22:09:14 +00:00
commit 321fe596a7
4 changed files with 66 additions and 15 deletions

View file

@ -87,6 +87,17 @@ export namespace Crafter {
};
static_assert(sizeof(BVHNode) == 32);
// One procedural primitive's axis-aligned bounding box, in object
// space. The analog of VkAabbPositionsKHR — the BLAS stores these
// instead of triangles and an intersection shader (registered in the
// hit group as a ProceduralHitGroup) reports the actual surface hit
// for each AABB the ray enters.
struct RTAabb {
float min[3];
float max[3];
};
static_assert(sizeof(RTAabb) == 24);
class Mesh {
public:
// BLAS "handle": opaque identity that goes into
@ -119,6 +130,19 @@ export namespace Crafter {
// as `vertexAttribs : array<u32>` with a per-mesh u32-word offset.
void Build(const ::Crafter::CompressedMeshAsset& asset,
WebGPUCommandEncoderRef cmd = 0);
// Build an AABB (procedural) BLAS from a list of object-space boxes
// — the WebGPU analog of a VK_GEOMETRY_TYPE_AABBS_KHR geometry. The
// hit group bound to instances of this mesh must be a
// ProceduralHitGroup carrying an intersection shader; that shader is
// invoked for each box the ray enters and reports the surface hit.
// `opaque` is the geometry's opaque bit: pass false to let any-hit
// shaders run (the default for procedural geometry, which is usually
// transparent / volumetric). The `cmd` parameter is unused on
// WebGPU — kept for API symmetry with the triangle path.
void BuildProcedural(std::span<const RTAabb> aabbs,
bool opaque = false,
WebGPUCommandEncoderRef cmd = 0);
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -60,24 +60,33 @@ export namespace Crafter {
inline constexpr std::uint8_t kRTGeometryInstanceForceOpaque = 0x4;
inline constexpr std::uint8_t kRTGeometryInstanceForceNoOpaque = 0x8;
// Hit-group identification. Matches VkRayTracingShaderGroupTypeKHR for
// the two types we actually support (general + triangles-hit).
// Hit-group identification. Matches VkRayTracingShaderGroupTypeKHR.
// General — raygen / miss / callable
// TrianglesHitGroup — closest-hit/any-hit over triangle geometry
// ProceduralHitGroup — closest-hit/any-hit + an intersection shader
// over AABB (VK_GEOMETRY_TYPE_AABBS_KHR) geometry
enum class RTShaderGroupType : std::uint8_t {
General = 0, // raygen / miss / callable
TrianglesHitGroup = 1,
General = 0, // raygen / miss / callable
TrianglesHitGroup = 1,
ProceduralHitGroup = 2,
};
// Cross-backend description of one entry in the shader-group array
// passed to PipelineRT::Init. Mirrors the meaningful subset of
// VkRayTracingShaderGroupCreateInfoKHR: per group, the type and the
// indices (into the SBT's shader array) for general / closestHit /
// anyHit, with kRTShaderUnused == VK_SHADER_UNUSED_KHR for "none".
// anyHit / intersection, with kRTShaderUnused == VK_SHADER_UNUSED_KHR
// for "none". `intersectionShader` is only consulted for
// ProceduralHitGroup; it names the shader run for each AABB the ray
// enters (the analog of VkRayTracingShaderGroupCreateInfoKHR::
// intersectionShader).
inline constexpr std::uint32_t kRTShaderUnused = 0xFFFFFFFFu;
struct RTShaderGroup {
RTShaderGroupType type = RTShaderGroupType::General;
std::uint32_t generalShader = kRTShaderUnused;
std::uint32_t closestHitShader = kRTShaderUnused;
std::uint32_t anyHitShader = kRTShaderUnused;
RTShaderGroupType type = RTShaderGroupType::General;
std::uint32_t generalShader = kRTShaderUnused;
std::uint32_t closestHitShader = kRTShaderUnused;
std::uint32_t anyHitShader = kRTShaderUnused;
std::uint32_t intersectionShader = kRTShaderUnused;
};
}

View file

@ -14,15 +14,23 @@ import std;
export namespace Crafter {
enum class WebGPURTStage : std::uint8_t {
Raygen = 0,
Miss = 1,
ClosestHit = 2,
AnyHit = 3,
Raygen = 0,
Miss = 1,
ClosestHit = 2,
AnyHit = 3,
// Wavefront RESOLVE-stage tonemap/output hook. Optional: if no
// Resolve shader is registered, RESOLVE writes the linear accum
// buffer through unchanged. Signature:
// fn <entryFn>(coord: vec2<u32>, hdr: vec4<f32>) -> vec4<f32>
Resolve = 4,
Resolve = 4,
// Intersection shader for AABB (procedural) geometry. Run for each
// AABB primitive the ray enters during TRACE; reports whether the
// procedural surface is hit and at what distance. Signature:
// fn <entryFn>(ray: RayDesc, aabbMin: vec3<f32>, aabbMax: vec3<f32>,
// primitiveId: u32) -> IntersectionResult
// `ray` is in object space. IntersectionResult{ hit, t, attribs,
// hitKind } is declared by the library prelude.
Intersection = 5,
};
// One WGSL shader source + the function name PipelineRTWebGPU should
@ -35,6 +43,9 @@ export namespace Crafter {
// ClosestHit: fn <entryFn>(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>)
// AnyHit: fn <entryFn>(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) -> u32
// returns RT_ANYHIT_ACCEPT / RT_ANYHIT_IGNORE / RT_ANYHIT_END_SEARCH.
// Intersection: fn <entryFn>(ray: RayDesc, aabbMin: vec3<f32>, aabbMax: vec3<f32>,
// primitiveId: u32) -> IntersectionResult
// IntersectionResult{ hit: bool, t: f32, attribs: vec2<f32>, hitKind: u32 }.
//
// `RayDesc`, `HitInfo`, the `RT_*` flag/return constants, the `tlas` /
// BLAS / mesh-record bindings, and the `traceRay` function are all

View file

@ -167,6 +167,12 @@ namespace Crafter::WebGPU {
// that gets appended to a global attribs heap and exposed to RT
// closest-hit shaders as `vertexAttribs : array<u32>` at
// @group(1) @binding(7). Pass (nullptr, 0) for positions-only meshes.
// `geomType` selects the primitive kind: 0 = triangles (the
// verticesPtr/indicesPtr streams), 1 = AABBs (VK_GEOMETRY_TYPE_AABBS) —
// then verticesPtr holds 2 vec3 per primitive [min, max], indexCount is
// 0, and an intersection shader supplies the hit. `opaqueFlag` is the
// geometry's opaque bit (0 lets any-hit run). `primCount` is the
// triangle / AABB primitive count.
__attribute__((import_module("env"), import_name("wgpuRegisterMeshBLAS")))
extern "C" std::uint32_t wgpuRegisterMeshBLAS(
float minX, float minY, float minZ,
@ -175,7 +181,8 @@ namespace Crafter::WebGPU {
const void* indicesPtr, std::int32_t indexCount,
const void* bvhNodesPtr, std::int32_t bvhNodeCount,
const void* primRemapPtr, std::int32_t primRemapCount,
const void* attribsPtr, std::int32_t attribsByteCount);
const void* attribsPtr, std::int32_t attribsByteCount,
std::int32_t geomType, std::int32_t opaqueFlag, std::int32_t primCount);
// RT pipeline build. The library composes WGSL by concatenating the
// traversal library, generated hit-group switches, and the user-