feat(webgpu-rt): emit intersection/any-hit dispatch + build AABB BVH

PipelineRTWebGPU emits a runIntersection mega-switch and the
RT_HAS_ANYHIT / RT_HAS_INTERSECTION consts (+ the @CRAFTER_RT_TRACE_USER
marker) that gate the library's new TRACE-stage user callbacks, so an
opaque triangle-only scene still const-folds them away. Mesh-WebGPU
builds a SAH BVH2 over AABB primitives and uploads them in primitive
order for the intersection shader to fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-02 22:09:20 +00:00
commit a91603c70b
2 changed files with 116 additions and 1 deletions

View file

@ -213,6 +213,25 @@ namespace {
nodes.emplace_back();
BuildRecursive(0, 0, triCount);
}
// AABB (procedural) geometry: one PrimRef per box, the box itself
// is the primitive bound. The same SAH BVH2 then partitions them.
void BuildFromAabbs(std::span<const RTAabb> aabbs) {
std::uint32_t count = static_cast<std::uint32_t>(aabbs.size());
prims.resize(count);
for (std::uint32_t i = 0; i < count; ++i) {
auto& pr = prims[i];
pr.box.Extend(aabbs[i].min);
pr.box.Extend(aabbs[i].max);
pr.centroid[0] = (pr.box.lo[0] + pr.box.hi[0]) * 0.5f;
pr.centroid[1] = (pr.box.lo[1] + pr.box.hi[1]) * 0.5f;
pr.centroid[2] = (pr.box.lo[2] + pr.box.hi[2]) * 0.5f;
pr.triIndex = i;
}
nodes.reserve(count * 2);
nodes.emplace_back();
BuildRecursive(0, 0, count);
}
};
}
@ -243,7 +262,10 @@ namespace {
indices.data(), static_cast<std::int32_t>(indices.size()),
builder.nodes.data(), static_cast<std::int32_t>(builder.nodes.size()),
primRemap.data(), static_cast<std::int32_t>(primRemap.size()),
attribsBytes.data(), static_cast<std::int32_t>(attribsBytes.size()));
attribsBytes.data(), static_cast<std::int32_t>(attribsBytes.size()),
/*geomType*/ 0,
/*opaqueFlag*/ mesh.opaque ? 1 : 0,
/*primCount*/ static_cast<std::int32_t>(mesh.triangleCount));
}
}
@ -273,3 +295,43 @@ void Mesh::Build(const CompressedMeshAsset& asset,
BuildBVHAndRegister(*this, vertices, indices, std::span(dataBytes));
}
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs,
bool opaque_,
WebGPUCommandEncoderRef /*cmd*/) {
const std::uint32_t count = static_cast<std::uint32_t>(aabbs.size());
opaque = opaque_;
triangleCount = 0; // not a triangle mesh
vertexCount = count * 2; // 2 "vertices" (min,max) per box
Builder builder;
builder.BuildFromAabbs(aabbs);
// The AABB stream is uploaded in *original* primitive order (2 vec3 per
// box). primRemap maps each BVH leaf slot back to its original index, so
// the intersection shader's _rtFetchAabb(meshRec, primId) reads the
// right box — exactly mirroring how the triangle path indexes vertices.
std::vector<Vector<float, 3, 3>> boxVerts(count * 2);
for (std::uint32_t i = 0; i < count; ++i) {
boxVerts[i*2 + 0] = Vector<float, 3, 3>{ aabbs[i].min[0], aabbs[i].min[1], aabbs[i].min[2] };
boxVerts[i*2 + 1] = Vector<float, 3, 3>{ aabbs[i].max[0], aabbs[i].max[1], aabbs[i].max[2] };
}
std::vector<std::uint32_t> primRemap(count);
for (std::uint32_t i = 0; i < count; ++i) {
primRemap[i] = builder.prims[i].triIndex;
}
const BVHNode& root = builder.nodes[0];
blasAddr = WebGPU::wgpuRegisterMeshBLAS(
root.aabbMin[0], root.aabbMin[1], root.aabbMin[2],
root.aabbMax[0], root.aabbMax[1], root.aabbMax[2],
boxVerts.data(), static_cast<std::int32_t>(boxVerts.size()),
nullptr, 0,
builder.nodes.data(), static_cast<std::int32_t>(builder.nodes.size()),
primRemap.data(), static_cast<std::int32_t>(primRemap.size()),
nullptr, 0,
/*geomType*/ 1,
/*opaqueFlag*/ opaque ? 1 : 0,
/*primCount*/ static_cast<std::int32_t>(count));
}