Merge pull request 'feat(webgpu-rt): any-hit + AABB (procedural) geometry support' (#14) from claude/issue-13 into master
This commit is contained in:
commit
b9f65f5273
17 changed files with 785 additions and 55 deletions
|
|
@ -22,7 +22,11 @@ The two backends share the same C++ surface for the high-level pieces
|
||||||
(`*Vulkan` vs `*WebGPU`) live behind `#ifdef CRAFTER_GRAPHICS_WINDOW_DOM`.
|
(`*Vulkan` vs `*WebGPU`) live behind `#ifdef CRAFTER_GRAPHICS_WINDOW_DOM`.
|
||||||
Vulkan ray tracing is hardware (`VK_KHR_ray_tracing_pipeline`); WebGPU
|
Vulkan ray tracing is hardware (`VK_KHR_ray_tracing_pipeline`); WebGPU
|
||||||
ray tracing is a library-built software path (BVH + traceRay in a
|
ray tracing is a library-built software path (BVH + traceRay in a
|
||||||
compute pipeline composed from user-supplied WGSL stages).
|
compute pipeline composed from user-supplied WGSL stages). The WebGPU
|
||||||
|
path supports triangle and AABB (procedural, `VK_GEOMETRY_TYPE_AABBS_KHR`)
|
||||||
|
geometry, closest-hit / miss / any-hit / intersection shaders — see
|
||||||
|
[examples/RTVolume](examples/RTVolume/README.md) for procedural spheres
|
||||||
|
shaded through an intersection shader with an any-hit cut-out.
|
||||||
|
|
||||||
> **Native RT status:** reading an acceleration structure through
|
> **Native RT status:** reading an acceleration structure through
|
||||||
> `VK_EXT_descriptor_heap` currently aborts with `VK_ERROR_DEVICE_LOST` on
|
> `VK_EXT_descriptor_heap` currently aborts with `VK_ERROR_DEVICE_LOST` on
|
||||||
|
|
|
||||||
|
|
@ -1357,6 +1357,15 @@ struct BVHNode {
|
||||||
// per-vertex stride lives in the user's WGSL — the library doesn't store
|
// per-vertex stride lives in the user's WGSL — the library doesn't store
|
||||||
// it because the layout is example-defined (Sponza uses 8 u32 / vertex
|
// it because the layout is example-defined (Sponza uses 8 u32 / vertex
|
||||||
// for VertexNormalTangentUVPacked).
|
// for VertexNormalTangentUVPacked).
|
||||||
|
//
|
||||||
|
// geomType selects the BLAS primitive kind: 0 = triangles (the default;
|
||||||
|
// the vertices/indices streams are positions + a triangle index buffer),
|
||||||
|
// 1 = AABBs (VK_GEOMETRY_TYPE_AABBS_KHR — the vertices stream holds 2 vec3
|
||||||
|
// per primitive [min, max] and there are no indices; a registered
|
||||||
|
// intersection shader determines the hit). opaque is the geometry's opaque
|
||||||
|
// bit (1 = opaque, no any-hit; 0 = any-hit may run, subject to the ray /
|
||||||
|
// instance force flags). triangleCount doubles as the AABB primitive count
|
||||||
|
// for geomType == 1.
|
||||||
struct MeshRecord {
|
struct MeshRecord {
|
||||||
rootAabbMin: vec3<f32>,
|
rootAabbMin: vec3<f32>,
|
||||||
vertexOffset: u32,
|
vertexOffset: u32,
|
||||||
|
|
@ -1366,6 +1375,21 @@ struct MeshRecord {
|
||||||
primRemapOffset: u32,
|
primRemapOffset: u32,
|
||||||
triangleCount: u32,
|
triangleCount: u32,
|
||||||
attribsOffset: u32,
|
attribsOffset: u32,
|
||||||
|
geomType: u32,
|
||||||
|
opaque: u32,
|
||||||
|
_padMr0: u32,
|
||||||
|
_padMr1: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Result of an intersection shader (procedural / AABB geometry) and the
|
||||||
|
// TRACE-stage candidate inspection. hit gates the rest; t is the
|
||||||
|
// object-space ray parameter; attribs are forwarded to closest-hit /
|
||||||
|
// any-hit (HitInfo.attribs); hitKind is user-defined (e.g. front/back).
|
||||||
|
struct IntersectionResult {
|
||||||
|
hit: bool,
|
||||||
|
t: f32,
|
||||||
|
attribs: vec2<f32>,
|
||||||
|
hitKind: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Per-instance TLAS record built by the TLAS-build compute pass.
|
// Per-instance TLAS record built by the TLAS-build compute pass.
|
||||||
|
|
@ -1469,6 +1493,28 @@ fn _rtFetchTri(meshRec: MeshRecord, triIndex: u32) -> array<vec3<f32>, 3> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch one procedural AABB (geomType == 1). The vertices heap stores
|
||||||
|
// 2 vec3 per primitive [min, max]; vertexOffset is the per-mesh base in
|
||||||
|
// vec3 units (matching the triangle path's vertexOffset units).
|
||||||
|
fn _rtFetchAabb(meshRec: MeshRecord, primIndex: u32) -> array<vec3<f32>, 2> {
|
||||||
|
let base = (meshRec.vertexOffset + primIndex * 2u) * 3u;
|
||||||
|
return array<vec3<f32>, 2>(
|
||||||
|
vec3<f32>(vertices[base + 0u], vertices[base + 1u], vertices[base + 2u]),
|
||||||
|
vec3<f32>(vertices[base + 3u], vertices[base + 4u], vertices[base + 5u]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DXR/VK opacity resolution. Ray FORCE flags win, then instance FORCE
|
||||||
|
// flags, then the geometry's own opaque bit. Non-opaque candidates run
|
||||||
|
// the any-hit shader during traversal.
|
||||||
|
fn _rtResolveOpaque(rayFlags: u32, instFlags: u32, geomOpaque: bool) -> bool {
|
||||||
|
if ((rayFlags & RT_FLAG_OPAQUE) != 0u) { return true; }
|
||||||
|
if ((rayFlags & RT_FLAG_NO_OPAQUE) != 0u) { return false; }
|
||||||
|
if ((instFlags & RT_INSTANCE_FORCE_OPAQUE) != 0u) { return true; }
|
||||||
|
if ((instFlags & RT_INSTANCE_FORCE_NO_OPAQUE) != 0u) { return false; }
|
||||||
|
return geomOpaque;
|
||||||
|
}
|
||||||
|
|
||||||
fn _rtAabb(ro: vec3<f32>, invRd: vec3<f32>, mn: vec3<f32>, mx: vec3<f32>, tMax: f32) -> bool {
|
fn _rtAabb(ro: vec3<f32>, invRd: vec3<f32>, mn: vec3<f32>, mx: vec3<f32>, tMax: f32) -> bool {
|
||||||
// Reject degenerate (mn > mx) boxes outright. The min(t0,t1)/
|
// Reject degenerate (mn > mx) boxes outright. The min(t0,t1)/
|
||||||
// max(t0,t1) trick below silently re-orients an inverted box
|
// max(t0,t1) trick below silently re-orients an inverted box
|
||||||
|
|
@ -1681,9 +1727,41 @@ fn rtEmitRay(origin: vec3<f32>, tMin: f32, dir: vec3<f32>, tMax: f32,
|
||||||
wfPayload[r.payloadSlot] = payload;
|
wfPayload[r.payloadSlot] = payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opaque-only BLAS descent (no anyhit — TRACE runs zero user code).
|
// Inspect one candidate hit: run the any-hit shader for non-opaque
|
||||||
fn _rtwTraverseBlas(rayObj: RayDesc, flags: u32, meshRec: MeshRecord,
|
// geometry, commit on accept. Returns 0 = ignored (keep searching, do not
|
||||||
instanceId: u32, hitGroupBase: u32,
|
// shrink bestT), 1 = committed (continue traversal with the tighter bestT),
|
||||||
|
// 2 = end search (committed + terminate). rayWorld is what any-hit sees
|
||||||
|
// as the ray (world space, matching closest-hit); cand carries the
|
||||||
|
// object-space hit. When RT_HAS_ANYHIT is false the any-hit call is
|
||||||
|
// const-folded away and this reduces to a plain commit — so opaque,
|
||||||
|
// triangle-only scenes keep TRACE's zero-user-code footprint.
|
||||||
|
fn _rtwCommitCandidate(rayWorld: RayDesc, flags: u32, instFlags: u32,
|
||||||
|
geomOpaque: bool, hitGroupBase: u32, cand: HitInfo,
|
||||||
|
payload: ptr<function, Payload>,
|
||||||
|
bestHit: ptr<function, HitInfo>,
|
||||||
|
bestT: ptr<function, f32>) -> u32 {
|
||||||
|
let opaque = _rtResolveOpaque(flags, instFlags, geomOpaque);
|
||||||
|
var verdict = RT_ANYHIT_ACCEPT;
|
||||||
|
if (RT_HAS_ANYHIT && !opaque) {
|
||||||
|
verdict = runAnyHit(hitGroupBase, rayWorld, cand, payload);
|
||||||
|
}
|
||||||
|
if (verdict == RT_ANYHIT_IGNORE) { return 0u; }
|
||||||
|
*bestHit = cand;
|
||||||
|
*bestT = cand.t;
|
||||||
|
if (verdict == RT_ANYHIT_END_SEARCH) { return 2u; }
|
||||||
|
if ((flags & RT_FLAG_TERMINATE_ON_FIRST_HIT) != 0u) { return 2u; }
|
||||||
|
return 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BLAS descent. Handles both triangle and AABB (procedural) geometry and
|
||||||
|
// runs the any-hit shader for non-opaque candidates. payload is threaded
|
||||||
|
// in so any-hit / intersection can read & mutate it (_wfTrace writes it
|
||||||
|
// back). For opaque triangle-only scenes the user-callback branches
|
||||||
|
// const-fold out (RT_HAS_ANYHIT / RT_HAS_INTERSECTION both false), leaving
|
||||||
|
// the original pure-traversal loop.
|
||||||
|
fn _rtwTraverseBlas(rayObj: RayDesc, rayWorld: RayDesc, flags: u32, instFlags: u32,
|
||||||
|
meshRec: MeshRecord, instanceId: u32, hitGroupBase: u32,
|
||||||
|
payload: ptr<function, Payload>,
|
||||||
bestHit: ptr<function, HitInfo>,
|
bestHit: ptr<function, HitInfo>,
|
||||||
bestT: ptr<function, f32>) -> bool {
|
bestT: ptr<function, f32>) -> bool {
|
||||||
let invD = vec3<f32>(1.0) / rayObj.direction;
|
let invD = vec3<f32>(1.0) / rayObj.direction;
|
||||||
|
|
@ -1698,6 +1776,34 @@ fn _rtwTraverseBlas(rayObj: RayDesc, flags: u32, meshRec: MeshRecord,
|
||||||
sp = sp - 1u; nodeRel = stack[sp]; continue;
|
sp = sp - 1u; nodeRel = stack[sp]; continue;
|
||||||
}
|
}
|
||||||
if (node.primCount > 0u) {
|
if (node.primCount > 0u) {
|
||||||
|
if (RT_HAS_INTERSECTION && meshRec.geomType == 1u) {
|
||||||
|
// ── AABB / procedural geometry (VK_GEOMETRY_TYPE_AABBS) ──
|
||||||
|
if ((flags & RT_FLAG_SKIP_AABBS) == 0u) {
|
||||||
|
for (var i: u32 = 0u; i < node.primCount; i = i + 1u) {
|
||||||
|
let primId = primRemap[meshRec.primRemapOffset + node.firstChildOrPrim + i];
|
||||||
|
let box = _rtFetchAabb(meshRec, primId);
|
||||||
|
if (!_rtAabb(rayObj.origin, invD, box[0], box[1], *bestT)) { continue; }
|
||||||
|
var iray: RayDesc;
|
||||||
|
iray.origin = rayObj.origin; iray.tMin = rayObj.tMin;
|
||||||
|
iray.direction = rayObj.direction; iray.tMax = *bestT;
|
||||||
|
let ir = runIntersection(hitGroupBase, iray, box[0], box[1], primId);
|
||||||
|
if (!ir.hit) { continue; }
|
||||||
|
if (ir.t < rayObj.tMin || ir.t > *bestT) { continue; }
|
||||||
|
var cand: HitInfo;
|
||||||
|
cand.t = ir.t;
|
||||||
|
cand.instanceId = instanceId;
|
||||||
|
cand.primitiveId = primId;
|
||||||
|
cand.hitGroupIndex = hitGroupBase;
|
||||||
|
cand.attribs = ir.attribs;
|
||||||
|
cand.objectRayOrigin = rayObj.origin;
|
||||||
|
cand.objectRayDirection = rayObj.direction;
|
||||||
|
let r = _rtwCommitCandidate(rayWorld, flags, instFlags,
|
||||||
|
meshRec.opaque != 0u, hitGroupBase,
|
||||||
|
cand, payload, bestHit, bestT);
|
||||||
|
if (r == 2u) { return true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((flags & RT_FLAG_SKIP_TRIANGLES) == 0u) {
|
||||||
for (var i: u32 = 0u; i < node.primCount; i = i + 1u) {
|
for (var i: u32 = 0u; i < node.primCount; i = i + 1u) {
|
||||||
let triIndex = primRemap[meshRec.primRemapOffset + node.firstChildOrPrim + i];
|
let triIndex = primRemap[meshRec.primRemapOffset + node.firstChildOrPrim + i];
|
||||||
let verts = _rtFetchTri(meshRec, triIndex);
|
let verts = _rtFetchTri(meshRec, triIndex);
|
||||||
|
|
@ -1716,9 +1822,11 @@ fn _rtwTraverseBlas(rayObj: RayDesc, flags: u32, meshRec: MeshRecord,
|
||||||
candidate.attribs = vec2<f32>(tr.u, tr.v);
|
candidate.attribs = vec2<f32>(tr.u, tr.v);
|
||||||
candidate.objectRayOrigin = rayObj.origin;
|
candidate.objectRayOrigin = rayObj.origin;
|
||||||
candidate.objectRayDirection = rayObj.direction;
|
candidate.objectRayDirection = rayObj.direction;
|
||||||
*bestHit = candidate;
|
let r = _rtwCommitCandidate(rayWorld, flags, instFlags,
|
||||||
*bestT = tr.t;
|
meshRec.opaque != 0u, hitGroupBase,
|
||||||
if ((flags & RT_FLAG_TERMINATE_ON_FIRST_HIT) != 0u) { return true; }
|
candidate, payload, bestHit, bestT);
|
||||||
|
if (r == 2u) { return true; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (sp == 0u) { break; }
|
if (sp == 0u) { break; }
|
||||||
sp = sp - 1u; nodeRel = stack[sp]; continue;
|
sp = sp - 1u; nodeRel = stack[sp]; continue;
|
||||||
|
|
@ -1749,6 +1857,7 @@ fn _rtwTraverseBlas(rayObj: RayDesc, flags: u32, meshRec: MeshRecord,
|
||||||
|
|
||||||
fn _rtwTraverseTlas(rayWorld: RayDesc, flags: u32, cullMask: u32,
|
fn _rtwTraverseTlas(rayWorld: RayDesc, flags: u32, cullMask: u32,
|
||||||
sbtRecordOffset: u32,
|
sbtRecordOffset: u32,
|
||||||
|
payload: ptr<function, Payload>,
|
||||||
bestHit: ptr<function, HitInfo>,
|
bestHit: ptr<function, HitInfo>,
|
||||||
bestT: ptr<function, f32>) -> bool {
|
bestT: ptr<function, f32>) -> bool {
|
||||||
let invD = vec3<f32>(1.0) / rayWorld.direction;
|
let invD = vec3<f32>(1.0) / rayWorld.direction;
|
||||||
|
|
@ -1793,7 +1902,7 @@ fn _rtwTraverseTlas(rayWorld: RayDesc, flags: u32, cullMask: u32,
|
||||||
let hitGroupBase = sbtRecordOffset + hitGroupOffset;
|
let hitGroupBase = sbtRecordOffset + hitGroupOffset;
|
||||||
let meshRec = meshRecords[inst.blasMeshIdx];
|
let meshRec = meshRecords[inst.blasMeshIdx];
|
||||||
let pre = *bestT;
|
let pre = *bestT;
|
||||||
let endSearch = _rtwTraverseBlas(rayObj, effective, meshRec, i, hitGroupBase, bestHit, bestT);
|
let endSearch = _rtwTraverseBlas(rayObj, rayWorld, effective, iflags, meshRec, i, hitGroupBase, payload, bestHit, bestT);
|
||||||
if ((*bestT) < pre || endSearch) {
|
if ((*bestT) < pre || endSearch) {
|
||||||
(*bestHit).objectToWorldR0 = inst.objectToWorldR0;
|
(*bestHit).objectToWorldR0 = inst.objectToWorldR0;
|
||||||
(*bestHit).objectToWorldR1 = inst.objectToWorldR1;
|
(*bestHit).objectToWorldR1 = inst.objectToWorldR1;
|
||||||
|
|
@ -1861,7 +1970,19 @@ fn _wfTrace(i: u32) {
|
||||||
var bestHit: HitInfo;
|
var bestHit: HitInfo;
|
||||||
bestHit.t = ray.tMax;
|
bestHit.t = ray.tMax;
|
||||||
var bestT = ray.tMax;
|
var bestT = ray.tMax;
|
||||||
_rtwTraverseTlas(rd, ray.flags, ray.cullMask & 0xFFu, ray.sbtRecordOffset, &bestHit, &bestT);
|
// Any-hit / intersection shaders run inside traversal and may read &
|
||||||
|
// mutate the payload, so load it here and write it back below. For an
|
||||||
|
// opaque triangle-only scene both consts are false and the payload
|
||||||
|
// touch const-folds away — TRACE keeps reading zero user state.
|
||||||
|
var payload: Payload;
|
||||||
|
if (RT_HAS_ANYHIT || RT_HAS_INTERSECTION) {
|
||||||
|
_wfPixel = ray.pixel;
|
||||||
|
payload = wfPayload[ray.payloadSlot];
|
||||||
|
}
|
||||||
|
_rtwTraverseTlas(rd, ray.flags, ray.cullMask & 0xFFu, ray.sbtRecordOffset, &payload, &bestHit, &bestT);
|
||||||
|
if (RT_HAS_ANYHIT || RT_HAS_INTERSECTION) {
|
||||||
|
wfPayload[ray.payloadSlot] = payload;
|
||||||
|
}
|
||||||
var hr: HitResult;
|
var hr: HitResult;
|
||||||
if (bestT < ray.tMax) {
|
if (bestT < ray.tMax) {
|
||||||
hr.t = bestHit.t;
|
hr.t = bestHit.t;
|
||||||
|
|
@ -1966,7 +2087,10 @@ fn _rqTraverseBlas(rayObj: RayDesc, flags: u32, meshRec: MeshRecord,
|
||||||
sp = sp - 1u; nodeRel = stack[sp]; continue;
|
sp = sp - 1u; nodeRel = stack[sp]; continue;
|
||||||
}
|
}
|
||||||
if (node.primCount > 0u) {
|
if (node.primCount > 0u) {
|
||||||
for (var i: u32 = 0u; i < node.primCount; i = i + 1u) {
|
// rayQuery is triangle-only; AABB (procedural) BLAS need an
|
||||||
|
// intersection shader the rayQuery path doesn't run, so skip
|
||||||
|
// their leaves rather than misread the AABB stream as triangles.
|
||||||
|
for (var i: u32 = 0u; i < node.primCount * select(0u, 1u, meshRec.geomType == 0u); i = i + 1u) {
|
||||||
let triIndex = primRemap[meshRec.primRemapOffset + node.firstChildOrPrim + i];
|
let triIndex = primRemap[meshRec.primRemapOffset + node.firstChildOrPrim + i];
|
||||||
let verts = _rtFetchTri(meshRec, triIndex);
|
let verts = _rtFetchTri(meshRec, triIndex);
|
||||||
let tr = _rtTri(rayObj.origin, rayObj.direction,
|
let tr = _rtTri(rayObj.origin, rayObj.direction,
|
||||||
|
|
@ -2572,7 +2696,7 @@ function rtInit() {
|
||||||
rtState.attribsHeap = makeRtHeap();
|
rtState.attribsHeap = makeRtHeap();
|
||||||
rtState.meshRecordsCapacity = 16;
|
rtState.meshRecordsCapacity = 16;
|
||||||
rtState.meshRecordsBuffer = device.createBuffer({
|
rtState.meshRecordsBuffer = device.createBuffer({
|
||||||
size: rtState.meshRecordsCapacity * 48,
|
size: rtState.meshRecordsCapacity * 64,
|
||||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
||||||
});
|
});
|
||||||
rtState.rtHeader = device.createBuffer({
|
rtState.rtHeader = device.createBuffer({
|
||||||
|
|
@ -2635,12 +2759,12 @@ function rtMeshRecordsEnsure(meshCount) {
|
||||||
let cap = rtState.meshRecordsCapacity;
|
let cap = rtState.meshRecordsCapacity;
|
||||||
while (cap < meshCount) cap *= 2;
|
while (cap < meshCount) cap *= 2;
|
||||||
const ng = device.createBuffer({
|
const ng = device.createBuffer({
|
||||||
size: cap * 48,
|
size: cap * 64,
|
||||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
||||||
});
|
});
|
||||||
const enc = device.createCommandEncoder();
|
const enc = device.createCommandEncoder();
|
||||||
enc.copyBufferToBuffer(rtState.meshRecordsBuffer, 0, ng, 0,
|
enc.copyBufferToBuffer(rtState.meshRecordsBuffer, 0, ng, 0,
|
||||||
rtState.meshRecordsCapacity * 48);
|
rtState.meshRecordsCapacity * 64);
|
||||||
queue.submit([enc.finish()]);
|
queue.submit([enc.finish()]);
|
||||||
rtState.meshRecordsBuffer.destroy();
|
rtState.meshRecordsBuffer.destroy();
|
||||||
rtState.meshRecordsBuffer = ng;
|
rtState.meshRecordsBuffer = ng;
|
||||||
|
|
@ -2653,9 +2777,11 @@ env.wgpuRegisterMeshBLAS = (minX, minY, minZ, maxX, maxY, maxZ,
|
||||||
indicesPtr, indexCount,
|
indicesPtr, indexCount,
|
||||||
bvhNodesPtr, bvhNodeCount,
|
bvhNodesPtr, bvhNodeCount,
|
||||||
primRemapPtr, primRemapCount,
|
primRemapPtr, primRemapCount,
|
||||||
attribsPtr, attribsByteCount) => {
|
attribsPtr, attribsByteCount,
|
||||||
|
geomType, opaqueFlag, primCount) => {
|
||||||
if (!rtState.vertHeap) rtInit();
|
if (!rtState.vertHeap) rtInit();
|
||||||
console.log(`[crafter-wgpu] mesh BLAS: bbox=(${minX.toFixed(1)}..${maxX.toFixed(1)}, ${minY.toFixed(1)}..${maxY.toFixed(1)}, ${minZ.toFixed(1)}..${maxZ.toFixed(1)}), ${vertexCount} verts, ${indexCount/3} tris, attribs=${attribsByteCount}B`);
|
const kind = (geomType === 1) ? `${primCount} aabbs` : `${indexCount/3} tris`;
|
||||||
|
console.log(`[crafter-wgpu] mesh BLAS: bbox=(${minX.toFixed(1)}..${maxX.toFixed(1)}, ${minY.toFixed(1)}..${maxY.toFixed(1)}, ${minZ.toFixed(1)}..${maxZ.toFixed(1)}), ${vertexCount} verts, ${kind}, opaque=${opaqueFlag}, attribs=${attribsByteCount}B`);
|
||||||
|
|
||||||
const vBytes = vertexCount * 12;
|
const vBytes = vertexCount * 12;
|
||||||
const iBytes = indexCount * 4;
|
const iBytes = indexCount * 4;
|
||||||
|
|
@ -2701,8 +2827,8 @@ env.wgpuRegisterMeshBLAS = (minX, minY, minZ, maxX, maxY, maxZ,
|
||||||
const handle = rtState.nextMeshHandle++;
|
const handle = rtState.nextMeshHandle++;
|
||||||
rtMeshRecordsEnsure(handle + 1);
|
rtMeshRecordsEnsure(handle + 1);
|
||||||
|
|
||||||
// Build the MeshRecord (48 bytes) and write it.
|
// Build the MeshRecord (64 bytes) and write it.
|
||||||
const rec = new ArrayBuffer(48);
|
const rec = new ArrayBuffer(64);
|
||||||
const f32 = new Float32Array(rec);
|
const f32 = new Float32Array(rec);
|
||||||
const u32 = new Uint32Array(rec);
|
const u32 = new Uint32Array(rec);
|
||||||
f32[0] = minX; f32[1] = minY; f32[2] = minZ;
|
f32[0] = minX; f32[1] = minY; f32[2] = minZ;
|
||||||
|
|
@ -2711,9 +2837,16 @@ env.wgpuRegisterMeshBLAS = (minX, minY, minZ, maxX, maxY, maxZ,
|
||||||
u32[7] = iOff;
|
u32[7] = iOff;
|
||||||
u32[8] = nOff;
|
u32[8] = nOff;
|
||||||
u32[9] = rOff;
|
u32[9] = rOff;
|
||||||
u32[10] = (vertexCount > 0) ? (indexCount / 3) : 0;
|
// triangleCount field doubles as the primitive count (= AABB count for
|
||||||
|
// geomType 1). Triangle meshes derive it from the index buffer.
|
||||||
|
u32[10] = (geomType === 1) ? (primCount >>> 0)
|
||||||
|
: ((vertexCount > 0) ? (indexCount / 3) : 0);
|
||||||
u32[11] = aOff;
|
u32[11] = aOff;
|
||||||
queue.writeBuffer(rtState.meshRecordsBuffer, handle * 48, rec);
|
u32[12] = (geomType === 1) ? 1 : 0; // geomType
|
||||||
|
u32[13] = opaqueFlag ? 1 : 0; // opaque bit
|
||||||
|
u32[14] = 0; // _padMr0
|
||||||
|
u32[15] = 0; // _padMr1
|
||||||
|
queue.writeBuffer(rtState.meshRecordsBuffer, handle * 64, rec);
|
||||||
|
|
||||||
return handle;
|
return handle;
|
||||||
};
|
};
|
||||||
|
|
@ -2924,6 +3057,12 @@ env.wgpuLoadRTPipeline = (wgslPtr, wgslLen, bindingsPtr, bindingsCount) => {
|
||||||
+ beforeHelpers + "\n" + rtWgslPureHelpers + "\n"
|
+ beforeHelpers + "\n" + rtWgslPureHelpers + "\n"
|
||||||
+ rtWgslWavefrontHelpers + "\n" + afterHelpers;
|
+ rtWgslWavefrontHelpers + "\n" + afterHelpers;
|
||||||
|
|
||||||
|
// When the pipeline registers any-hit / intersection shaders, those run
|
||||||
|
// inside TRACE and may sample the user's @group(3+) resources — so TRACE
|
||||||
|
// needs the full user pipeline layout (and its bind groups set at
|
||||||
|
// dispatch). PipelineRTWebGPU emits this exact marker when so.
|
||||||
|
const traceHasUser = fullWgsl.includes("@CRAFTER_RT_TRACE_USER = true");
|
||||||
|
|
||||||
// Parse user bindings (same wire format as wgpuLoadCustomShader). For
|
// Parse user bindings (same wire format as wgpuLoadCustomShader). For
|
||||||
// the wavefront RT pipeline, group 0 = WfParams, group 1 = data heaps,
|
// the wavefront RT pipeline, group 0 = WfParams, group 1 = data heaps,
|
||||||
// group 2 = indirect args — so user bindings must start at group 3.
|
// group 2 = indirect args — so user bindings must start at group 3.
|
||||||
|
|
@ -3003,11 +3142,14 @@ env.wgpuLoadRTPipeline = (wgslPtr, wgslLen, bindingsPtr, bindingsCount) => {
|
||||||
const entry = {
|
const entry = {
|
||||||
genPipe: mk(userLayout, "wfGenerate"),
|
genPipe: mk(userLayout, "wfGenerate"),
|
||||||
prepPipe: mk(prepLayout, "wfPrep"),
|
prepPipe: mk(prepLayout, "wfPrep"),
|
||||||
tracePipe: mk(traceLayout, "wfTrace"),
|
// TRACE gets the user layout only when any-hit / intersection
|
||||||
|
// shaders run there; otherwise it keeps the minimal params+data
|
||||||
|
// layout and zero user code (the common opaque-triangle path).
|
||||||
|
tracePipe: mk(traceHasUser ? userLayout : traceLayout, "wfTrace"),
|
||||||
shadePipe: mk(userLayout, "wfShade"),
|
shadePipe: mk(userLayout, "wfShade"),
|
||||||
resolvePipe: mk(userLayout, "wfResolve"),
|
resolvePipe: mk(userLayout, "wfResolve"),
|
||||||
paramsBgl, dataBgl, indirectBgl, emptyBgl, userBgls,
|
paramsBgl, dataBgl, indirectBgl, emptyBgl, userBgls,
|
||||||
byGroup, sortedGroups,
|
byGroup, sortedGroups, traceHasUser,
|
||||||
};
|
};
|
||||||
const handle = newHandle();
|
const handle = newHandle();
|
||||||
rtPipelines.set(handle, entry);
|
rtPipelines.set(handle, entry);
|
||||||
|
|
@ -3187,6 +3329,9 @@ env.wgpuDispatchRT = (pipelineHandle, pushPtr, pushBytes,
|
||||||
p.setPipeline(pipe.tracePipe);
|
p.setPipeline(pipe.tracePipe);
|
||||||
p.setBindGroup(0, paramsBg, [slotOff(traceSlot)]);
|
p.setBindGroup(0, paramsBg, [slotOff(traceSlot)]);
|
||||||
p.setBindGroup(1, dataBg);
|
p.setBindGroup(1, dataBg);
|
||||||
|
// Any-hit / intersection shaders run in TRACE and may read the
|
||||||
|
// user @group(3+) resources — bind them when present.
|
||||||
|
if (pipe.traceHasUser) setUser(p);
|
||||||
p.dispatchWorkgroupsIndirect(wf.indirect, 0);
|
p.dispatchWorkgroupsIndirect(wf.indirect, 0);
|
||||||
p.end();
|
p.end();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
examples/RTVolume/README.md
Normal file
26
examples/RTVolume/README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# RTVolume
|
||||||
|
|
||||||
|
WebGPU software ray tracing of **procedural (AABB) geometry** with an
|
||||||
|
**any-hit** cut-out — the two features added for issue #13.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
crafter-build --target=wasm32-wasip1 -r
|
||||||
|
```
|
||||||
24
examples/RTVolume/anyhit.wgsl
Normal file
24
examples/RTVolume/anyhit.wgsl
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// RTVolume any-hit shader (runs in TRACE on every candidate sphere hit,
|
||||||
|
// because the geometry is registered non-opaque). Punches a spherical
|
||||||
|
// checkerboard of holes: for half the cells it returns RT_ANYHIT_IGNORE,
|
||||||
|
// so the ray passes straight through and the background / spheres behind
|
||||||
|
// show through. Returning RT_ANYHIT_ACCEPT keeps the hit. This is the
|
||||||
|
// visible proof the any-hit path runs — with it the spheres are perforated,
|
||||||
|
// without it they would be solid.
|
||||||
|
fn anyhit_main(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) -> u32 {
|
||||||
|
// Object-space hit point on the unit sphere → its normal/direction.
|
||||||
|
let posObj = hit.objectRayOrigin + hit.objectRayDirection * hit.t;
|
||||||
|
let n = normalize(posObj);
|
||||||
|
|
||||||
|
let PI = 3.14159265;
|
||||||
|
let longitude = atan2(n.z, n.x); // [-PI, PI]
|
||||||
|
let latitude = asin(clamp(n.y, -1.0, 1.0)); // [-PI/2, PI/2]
|
||||||
|
|
||||||
|
let cu = i32(floor((longitude + PI) / PI * 6.0));
|
||||||
|
let cv = i32(floor((latitude + PI * 0.5) / PI * 6.0));
|
||||||
|
|
||||||
|
if (((cu + cv) & 1) == 0) {
|
||||||
|
return RT_ANYHIT_IGNORE; // cut-out cell — see through
|
||||||
|
}
|
||||||
|
return RT_ANYHIT_ACCEPT;
|
||||||
|
}
|
||||||
37
examples/RTVolume/closesthit.wgsl
Normal file
37
examples/RTVolume/closesthit.wgsl
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// RTVolume closest-hit (runs in SHADE). Shades the procedural sphere by
|
||||||
|
// its surface normal with a fixed sun + ambient, tinted per instance.
|
||||||
|
//
|
||||||
|
// Payload declared here so the assembler sees it before wfPayload / SHADE.
|
||||||
|
struct Payload {
|
||||||
|
color: vec3<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SUN_DIR_TO_LIGHT: vec3<f32> = vec3<f32>(0.40, 0.85, 0.35);
|
||||||
|
const SUN_COLOR: vec3<f32> = vec3<f32>(1.20, 1.10, 0.95);
|
||||||
|
const AMBIENT_COLOR: vec3<f32> = vec3<f32>(0.16, 0.18, 0.24);
|
||||||
|
|
||||||
|
fn instanceAlbedo(i: u32) -> vec3<f32> {
|
||||||
|
let h = i * 2654435761u;
|
||||||
|
return vec3<f32>(
|
||||||
|
0.35 + 0.6 * f32((h >> 0u) & 255u) / 255.0,
|
||||||
|
0.35 + 0.6 * f32((h >> 8u) & 255u) / 255.0,
|
||||||
|
0.35 + 0.6 * f32((h >> 16u) & 255u) / 255.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn closesthit_main(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) {
|
||||||
|
// Object-space hit point on the unit sphere is its object-space normal.
|
||||||
|
let posObj = hit.objectRayOrigin + hit.objectRayDirection * hit.t;
|
||||||
|
let nObj = normalize(posObj);
|
||||||
|
let nWorld = normalize(vec3<f32>(
|
||||||
|
dot(hit.objectToWorldR0.xyz, nObj),
|
||||||
|
dot(hit.objectToWorldR1.xyz, nObj),
|
||||||
|
dot(hit.objectToWorldR2.xyz, nObj)));
|
||||||
|
|
||||||
|
let albedo = instanceAlbedo(hit.customIndex);
|
||||||
|
let viewDir = -ray.direction;
|
||||||
|
let nFacing = select(-nWorld, nWorld, dot(nWorld, viewDir) > 0.0);
|
||||||
|
let sunDir = normalize(SUN_DIR_TO_LIGHT);
|
||||||
|
let nDotL = max(0.0, dot(nFacing, sunDir));
|
||||||
|
|
||||||
|
rtAccumulate(albedo * (AMBIENT_COLOR + SUN_COLOR * nDotL));
|
||||||
|
}
|
||||||
33
examples/RTVolume/intersection.wgsl
Normal file
33
examples/RTVolume/intersection.wgsl
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// RTVolume intersection shader (runs in TRACE, per AABB the ray enters).
|
||||||
|
// Analytic ray-sphere test: the unit box [-1,1]^3 is treated as the
|
||||||
|
// bounding volume of a sphere of radius 1 centred at the box centre. The
|
||||||
|
// ray is in object space and is NOT normalised (it is worldToObject *
|
||||||
|
// worldRay), so the returned t is directly comparable to the world-space
|
||||||
|
// ray parameter the tracer commits — solve the quadratic with the general
|
||||||
|
// a = dot(d,d) form rather than assuming |d| == 1.
|
||||||
|
fn intersection_main(ray: RayDesc, aabbMin: vec3<f32>, aabbMax: vec3<f32>,
|
||||||
|
primitiveId: u32) -> IntersectionResult {
|
||||||
|
var r: IntersectionResult;
|
||||||
|
r.hit = false;
|
||||||
|
|
||||||
|
let center = (aabbMin + aabbMax) * 0.5;
|
||||||
|
let radius = (aabbMax.x - aabbMin.x) * 0.5;
|
||||||
|
|
||||||
|
let oc = ray.origin - center;
|
||||||
|
let a = dot(ray.direction, ray.direction);
|
||||||
|
let b = 2.0 * dot(oc, ray.direction);
|
||||||
|
let c = dot(oc, oc) - radius * radius;
|
||||||
|
let disc = b * b - 4.0 * a * c;
|
||||||
|
if (disc < 0.0) { return r; }
|
||||||
|
|
||||||
|
let sq = sqrt(disc);
|
||||||
|
var t = (-b - sq) / (2.0 * a); // near root
|
||||||
|
if (t < ray.tMin) { t = (-b + sq) / (2.0 * a); } // fall back to far root
|
||||||
|
if (t < ray.tMin || t > ray.tMax) { return r; }
|
||||||
|
|
||||||
|
r.hit = true;
|
||||||
|
r.t = t;
|
||||||
|
r.attribs = vec2<f32>(0.0);
|
||||||
|
r.hitKind = 0u;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
199
examples/RTVolume/main.cpp
Normal file
199
examples/RTVolume/main.cpp
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
// RTVolume — procedural (AABB) ray tracing on the WebGPU wavefront tracer.
|
||||||
|
// Demonstrates the two features this example was written to exercise:
|
||||||
|
//
|
||||||
|
// * VK_GEOMETRY_TYPE_AABBS_KHR equivalent — 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
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
int main() { return 0; } // native path is hardware RT; out of scope here
|
||||||
|
#else
|
||||||
|
|
||||||
|
import Crafter.Graphics;
|
||||||
|
import Crafter.Math;
|
||||||
|
import Crafter.Event;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using namespace Crafter;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int kGrid = 3;
|
||||||
|
constexpr float kSpacing = 3.0f;
|
||||||
|
|
||||||
|
struct CameraGPU {
|
||||||
|
float origin[3]; float pad0;
|
||||||
|
float right[3]; float tanHalf;
|
||||||
|
float up[3]; float aspect;
|
||||||
|
float forward[3]; float pad1;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CameraGPU) == 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const int instanceCount = kGrid * kGrid * kGrid;
|
||||||
|
std::println("[RTVolume] grid {}^3 = {} procedural spheres", kGrid, instanceCount);
|
||||||
|
|
||||||
|
Device::Initialize();
|
||||||
|
static Window window(1280, 720, "RTVolume");
|
||||||
|
auto cmd = window.StartInit();
|
||||||
|
|
||||||
|
DescriptorHeapWebGPU heap;
|
||||||
|
heap.Initialize(/*images*/ 1, /*buffers*/ 2, /*samplers*/ 1);
|
||||||
|
|
||||||
|
// SBT order fixes the shader indices used by the groups below.
|
||||||
|
std::array<WebGPUShader, 6> shaders {{
|
||||||
|
WebGPUShader(fs::path("raygen.wgsl"), "raygen_main", WebGPURTStage::Raygen),
|
||||||
|
WebGPUShader(fs::path("miss.wgsl"), "miss_main", WebGPURTStage::Miss),
|
||||||
|
WebGPUShader(fs::path("closesthit.wgsl"), "closesthit_main", WebGPURTStage::ClosestHit),
|
||||||
|
WebGPUShader(fs::path("anyhit.wgsl"), "anyhit_main", WebGPURTStage::AnyHit),
|
||||||
|
WebGPUShader(fs::path("intersection.wgsl"), "intersection_main", WebGPURTStage::Intersection),
|
||||||
|
WebGPUShader(fs::path("resolve.wgsl"), "resolve_main", WebGPURTStage::Resolve),
|
||||||
|
}};
|
||||||
|
ShaderBindingTableWebGPU sbt;
|
||||||
|
sbt.Init(shaders);
|
||||||
|
|
||||||
|
std::array<RTShaderGroup, 1> raygenGroups {{ { .type = RTShaderGroupType::General, .generalShader = 0 } }};
|
||||||
|
std::array<RTShaderGroup, 1> missGroups {{ { .type = RTShaderGroupType::General, .generalShader = 1 } }};
|
||||||
|
// One procedural hit group: closest-hit + any-hit + intersection.
|
||||||
|
std::array<RTShaderGroup, 1> hitGroups {{ {
|
||||||
|
.type = RTShaderGroupType::ProceduralHitGroup,
|
||||||
|
.closestHitShader = 2,
|
||||||
|
.anyHitShader = 3,
|
||||||
|
.intersectionShader = 4,
|
||||||
|
} }};
|
||||||
|
|
||||||
|
std::array<UICustomBinding, 1> bindings {{
|
||||||
|
{ .group = 3, .binding = 0, .kind = UICustomBindingKind::Buffer, ._pad = 0, .pushOffset = 0 },
|
||||||
|
}};
|
||||||
|
|
||||||
|
PipelineRTWebGPU pipeline;
|
||||||
|
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, sbt, bindings);
|
||||||
|
|
||||||
|
// ── 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. ─────────
|
||||||
|
static std::array<RTAabb, 1> boxes {{
|
||||||
|
{ .min = {-1.0f, -1.0f, -1.0f}, .max = {1.0f, 1.0f, 1.0f} },
|
||||||
|
}};
|
||||||
|
static Mesh sphere;
|
||||||
|
sphere.BuildProcedural(boxes, /*opaque*/ false, cmd);
|
||||||
|
|
||||||
|
// ── Camera buffer + handle array. ─────────────────────────────────
|
||||||
|
WebGPUBuffer<CameraGPU, true> cameraBuf;
|
||||||
|
cameraBuf.Create(1);
|
||||||
|
static std::array<std::uint32_t, 1> userHandles { cameraBuf.handle };
|
||||||
|
|
||||||
|
// ── 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);
|
||||||
|
}
|
||||||
|
RenderingElement3D::BuildTLAS(cmd, 0);
|
||||||
|
|
||||||
|
window.descriptorHeap = &heap;
|
||||||
|
window.FinishInit();
|
||||||
|
|
||||||
|
RTPass rtPass(&pipeline);
|
||||||
|
rtPass.handlesPtr = userHandles.data();
|
||||||
|
rtPass.handlesCount = static_cast<std::uint32_t>(userHandles.size());
|
||||||
|
rtPass.maxDepth = 1; // primary only
|
||||||
|
window.passes.push_back(&rtPass);
|
||||||
|
|
||||||
|
// ── Free camera framing the grid. ─────────────────────────────────
|
||||||
|
const float ext = float(kGrid - 1) * kSpacing;
|
||||||
|
struct CamState {
|
||||||
|
Vector<float, 3, 4> position;
|
||||||
|
float yaw;
|
||||||
|
float pitch;
|
||||||
|
} cam {
|
||||||
|
Vector<float, 3, 4>{ ext * 1.1f, ext * 0.8f, ext * 1.6f },
|
||||||
|
0.0f, 0.0f,
|
||||||
|
};
|
||||||
|
{
|
||||||
|
Vector<float, 3, 4> d { -cam.position.x, -cam.position.y, -cam.position.z };
|
||||||
|
const float len = std::sqrt(d.x*d.x + d.y*d.y + d.z*d.z);
|
||||||
|
cam.yaw = std::atan2(d.z, d.x);
|
||||||
|
cam.pitch = std::asin(d.y / len);
|
||||||
|
}
|
||||||
|
|
||||||
|
Input::Map inputMap;
|
||||||
|
Input::Action& moveAct = inputMap.AddAction("Move", Input::ActionType::Vector2);
|
||||||
|
Input::Action& lookAct = inputMap.AddAction("Look", Input::ActionType::Vector2);
|
||||||
|
moveAct.bindings = { Input::WASDBind{
|
||||||
|
Key(CrafterKeys::W), Key(CrafterKeys::S), Key(CrafterKeys::A), Key(CrafterKeys::D) } };
|
||||||
|
lookAct.bindings = { Input::MouseDeltaBind{ 1.0f } };
|
||||||
|
inputMap.Attach(window);
|
||||||
|
|
||||||
|
const float kMoveSpeed = ext * 0.8f + 1.0f;
|
||||||
|
const float kLookSens = 0.05f;
|
||||||
|
const float kDt = 1.0f / 60.0f;
|
||||||
|
|
||||||
|
EventListener<void> camTick(&window.onBeforeUpdate, [&]() {
|
||||||
|
inputMap.Tick();
|
||||||
|
cam.yaw += lookAct.vector2.x * kLookSens;
|
||||||
|
cam.pitch -= lookAct.vector2.y * kLookSens;
|
||||||
|
cam.pitch = std::clamp(cam.pitch, -1.55f, 1.55f);
|
||||||
|
|
||||||
|
const float cp = std::cos(cam.pitch), sp = std::sin(cam.pitch);
|
||||||
|
const float cy = std::cos(cam.yaw), sy = std::sin(cam.yaw);
|
||||||
|
Vector<float, 3, 4> forward { cp * cy, sp, cp * sy };
|
||||||
|
Vector<float, 3, 4> worldUp { 0.0f, 1.0f, 0.0f };
|
||||||
|
Vector<float, 3, 4> right { forward.y*worldUp.z - forward.z*worldUp.y,
|
||||||
|
forward.z*worldUp.x - forward.x*worldUp.z,
|
||||||
|
forward.x*worldUp.y - forward.y*worldUp.x };
|
||||||
|
const float rLen = std::sqrt(right.x*right.x + right.y*right.y + right.z*right.z);
|
||||||
|
right.x /= rLen; right.y /= rLen; right.z /= rLen;
|
||||||
|
Vector<float, 3, 4> up { right.y*forward.z - right.z*forward.y,
|
||||||
|
right.z*forward.x - right.x*forward.z,
|
||||||
|
right.x*forward.y - right.y*forward.x };
|
||||||
|
|
||||||
|
const float dx = moveAct.vector2.x * kMoveSpeed * kDt;
|
||||||
|
const float dy = moveAct.vector2.y * kMoveSpeed * kDt;
|
||||||
|
cam.position.x += right.x*dx + forward.x*dy;
|
||||||
|
cam.position.y += right.y*dx + forward.y*dy;
|
||||||
|
cam.position.z += right.z*dx + forward.z*dy;
|
||||||
|
|
||||||
|
CameraGPU& g = cameraBuf.value[0];
|
||||||
|
g.origin[0]=cam.position.x; g.origin[1]=cam.position.y; g.origin[2]=cam.position.z; g.pad0=0;
|
||||||
|
g.right[0]=right.x; g.right[1]=right.y; g.right[2]=right.z;
|
||||||
|
g.up[0]=up.x; g.up[1]=up.y; g.up[2]=up.z;
|
||||||
|
g.forward[0]=forward.x; g.forward[1]=forward.y; g.forward[2]=forward.z;
|
||||||
|
g.aspect = float(window.width) / float(window.height);
|
||||||
|
g.tanHalf = std::tan(70.0f * 3.14159265f / 360.0f);
|
||||||
|
g.pad1 = 0;
|
||||||
|
cameraBuf.FlushDevice();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.Render();
|
||||||
|
window.StartUpdate();
|
||||||
|
window.StartSync();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
7
examples/RTVolume/miss.wgsl
Normal file
7
examples/RTVolume/miss.wgsl
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// RTVolume miss (runs in SHADE). Vertical sky gradient — also what shows
|
||||||
|
// through the any-hit cut-out cells.
|
||||||
|
fn miss_main(ray: RayDesc, payload: ptr<function, Payload>) {
|
||||||
|
let t = clamp(ray.direction.y * 0.5 + 0.5, 0.0, 1.0);
|
||||||
|
rtAccumulate(mix(vec3<f32>(0.05, 0.07, 0.12),
|
||||||
|
vec3<f32>(0.45, 0.60, 0.85), t));
|
||||||
|
}
|
||||||
48
examples/RTVolume/project.cpp
Normal file
48
examples/RTVolume/project.cpp
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import std;
|
||||||
|
import Crafter.Build;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
|
bool isWasm = false;
|
||||||
|
for (std::string_view a : args) {
|
||||||
|
if (a.starts_with("--target=") && a.find("wasm") != std::string_view::npos) {
|
||||||
|
isWasm = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> graphicsArgs(args.begin(), args.end());
|
||||||
|
Configuration* graphics = LocalProject({
|
||||||
|
.projectFile = "../../project.cpp",
|
||||||
|
.args = graphicsArgs,
|
||||||
|
});
|
||||||
|
|
||||||
|
Configuration cfg;
|
||||||
|
cfg.path = "./";
|
||||||
|
cfg.name = "RTVolume";
|
||||||
|
cfg.outputName = "RTVolume";
|
||||||
|
cfg.type = ConfigurationType::Executable;
|
||||||
|
if (isWasm) {
|
||||||
|
cfg.target = "wasm32-wasip1";
|
||||||
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_DOM", ""});
|
||||||
|
cfg.compileFlags.push_back("-msimd128");
|
||||||
|
}
|
||||||
|
ApplyStandardArgs(cfg, args);
|
||||||
|
cfg.dependencies = { graphics };
|
||||||
|
|
||||||
|
std::array<fs::path, 0> ifaces = {};
|
||||||
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
||||||
|
if (isWasm) {
|
||||||
|
cfg.files.emplace_back(fs::path("raygen.wgsl"));
|
||||||
|
cfg.files.emplace_back(fs::path("intersection.wgsl"));
|
||||||
|
cfg.files.emplace_back(fs::path("anyhit.wgsl"));
|
||||||
|
cfg.files.emplace_back(fs::path("closesthit.wgsl"));
|
||||||
|
cfg.files.emplace_back(fs::path("miss.wgsl"));
|
||||||
|
cfg.files.emplace_back(fs::path("resolve.wgsl"));
|
||||||
|
EnableWasiBrowserRuntime(cfg);
|
||||||
|
}
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
34
examples/RTVolume/raygen.wgsl
Normal file
34
examples/RTVolume/raygen.wgsl
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// RTVolume raygen (runs in GENERATE). Host-driven pinhole camera at
|
||||||
|
// @group(3) (groups 0..2 are reserved by the wavefront pipeline:
|
||||||
|
// 0 = WfParams, 1 = data heaps, 2 = indirect args).
|
||||||
|
struct Camera {
|
||||||
|
origin: vec3<f32>,
|
||||||
|
pad0: f32,
|
||||||
|
right: vec3<f32>,
|
||||||
|
tanHalf: f32,
|
||||||
|
up: vec3<f32>,
|
||||||
|
aspect: f32,
|
||||||
|
forward: vec3<f32>,
|
||||||
|
pad1: f32,
|
||||||
|
};
|
||||||
|
@group(3) @binding(0) var<storage, read> camera : Camera;
|
||||||
|
|
||||||
|
fn raygen_main(gid: vec3<u32>) {
|
||||||
|
if (gid.x >= wfParams.surfaceW || gid.y >= wfParams.surfaceH) { return; }
|
||||||
|
|
||||||
|
let pixelf = vec2<f32>(f32(gid.x), f32(gid.y));
|
||||||
|
let res = vec2<f32>(f32(wfParams.surfaceW), f32(wfParams.surfaceH));
|
||||||
|
let uv = (pixelf + vec2<f32>(0.5)) / res;
|
||||||
|
let ndc = uv * 2.0 - vec2<f32>(1.0);
|
||||||
|
|
||||||
|
let direction = normalize(
|
||||||
|
camera.right * (ndc.x * camera.aspect * camera.tanHalf) +
|
||||||
|
camera.up * (-ndc.y * camera.tanHalf) +
|
||||||
|
camera.forward);
|
||||||
|
|
||||||
|
var p: Payload;
|
||||||
|
p.color = vec3<f32>(0.0);
|
||||||
|
|
||||||
|
rtEmitPrimaryRay(camera.origin, 0.01, direction, 100000.0,
|
||||||
|
0u, 0xFFu, 0u, 0u, p);
|
||||||
|
}
|
||||||
7
examples/RTVolume/resolve.wgsl
Normal file
7
examples/RTVolume/resolve.wgsl
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// RTVolume RESOLVE-stage tonemap: Reinhard + gamma 2.2 over the linear
|
||||||
|
// accumulator.
|
||||||
|
fn resolve_main(coord: vec2<u32>, hdr: vec4<f32>) -> vec4<f32> {
|
||||||
|
let mapped = hdr.rgb / (hdr.rgb + vec3<f32>(1.0));
|
||||||
|
let g = pow(mapped, vec3<f32>(1.0 / 2.2));
|
||||||
|
return vec4<f32>(g, 1.0);
|
||||||
|
}
|
||||||
|
|
@ -213,6 +213,25 @@ namespace {
|
||||||
nodes.emplace_back();
|
nodes.emplace_back();
|
||||||
BuildRecursive(0, 0, triCount);
|
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()),
|
indices.data(), static_cast<std::int32_t>(indices.size()),
|
||||||
builder.nodes.data(), static_cast<std::int32_t>(builder.nodes.size()),
|
builder.nodes.data(), static_cast<std::int32_t>(builder.nodes.size()),
|
||||||
primRemap.data(), static_cast<std::int32_t>(primRemap.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));
|
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));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ namespace {
|
||||||
"fn _crafter_default_anyhit(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) -> u32 { return RT_ANYHIT_ACCEPT; }";
|
"fn _crafter_default_anyhit(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) -> u32 { return RT_ANYHIT_ACCEPT; }";
|
||||||
constexpr std::string_view kPlaceholderMiss =
|
constexpr std::string_view kPlaceholderMiss =
|
||||||
"fn _crafter_default_miss(ray: RayDesc, payload: ptr<function, Payload>) {}";
|
"fn _crafter_default_miss(ray: RayDesc, payload: ptr<function, Payload>) {}";
|
||||||
|
constexpr std::string_view kPlaceholderIntersection =
|
||||||
|
"fn _crafter_default_intersection(ray: RayDesc, aabbMin: vec3<f32>, aabbMax: vec3<f32>, primitiveId: u32) -> IntersectionResult { var r: IntersectionResult; r.hit = false; return r; }";
|
||||||
|
|
||||||
void AppendCase(std::string& out,
|
void AppendCase(std::string& out,
|
||||||
std::uint32_t hitGroupIndex,
|
std::uint32_t hitGroupIndex,
|
||||||
|
|
@ -60,6 +62,17 @@ namespace {
|
||||||
out += entryFn;
|
out += entryFn;
|
||||||
out += "(ray, hit, payload); }\n";
|
out += "(ray, hit, payload); }\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// intersection has a return type — forwards the AABB args + the result.
|
||||||
|
void AppendIntersectionCase(std::string& out,
|
||||||
|
std::uint32_t hitGroupIndex,
|
||||||
|
std::string_view entryFn) {
|
||||||
|
out += " case ";
|
||||||
|
out += std::to_string(hitGroupIndex);
|
||||||
|
out += "u: { return ";
|
||||||
|
out += entryFn;
|
||||||
|
out += "(ray, aabbMin, aabbMax, primitiveId); }\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PipelineRTWebGPU::Init(WebGPUCommandEncoderRef /*cmd*/,
|
void PipelineRTWebGPU::Init(WebGPUCommandEncoderRef /*cmd*/,
|
||||||
|
|
@ -150,6 +163,46 @@ void PipelineRTWebGPU::Init(WebGPUCommandEncoderRef /*cmd*/,
|
||||||
wgsl += " }\n";
|
wgsl += " }\n";
|
||||||
wgsl += "}\n";
|
wgsl += "}\n";
|
||||||
|
|
||||||
|
// runIntersection — per-AABB procedural intersection dispatch. For a
|
||||||
|
// ProceduralHitGroup the intersection shader determines the hit; for
|
||||||
|
// triangle groups (or groups with no intersection shader) the default
|
||||||
|
// reports no hit, so the BLAS leaf falls back to the triangle path.
|
||||||
|
wgsl += "\nfn runIntersection(hg: u32, ray: RayDesc, aabbMin: vec3<f32>, aabbMax: vec3<f32>, primitiveId: u32) -> IntersectionResult {\n";
|
||||||
|
wgsl += " switch hg {\n";
|
||||||
|
bool anyIntersection = false;
|
||||||
|
for (std::uint32_t i = 0; i < hitGroups.size(); ++i) {
|
||||||
|
const auto& g = hitGroups[i];
|
||||||
|
if (g.intersectionShader == kRTShaderUnused) continue;
|
||||||
|
if (g.intersectionShader >= sbt.shaders.size()) continue;
|
||||||
|
const auto& fn = sbt.shaders[g.intersectionShader].entryFn;
|
||||||
|
AppendIntersectionCase(wgsl, i, fn);
|
||||||
|
anyIntersection = true;
|
||||||
|
}
|
||||||
|
if (!anyIntersection) wgsl += " // (no intersection shaders registered)\n";
|
||||||
|
wgsl += " default: { }\n";
|
||||||
|
wgsl += " }\n";
|
||||||
|
wgsl += " var none: IntersectionResult;\n";
|
||||||
|
wgsl += " none.hit = false;\n";
|
||||||
|
wgsl += " return none;\n";
|
||||||
|
wgsl += "}\n";
|
||||||
|
|
||||||
|
// Trace-time capability flags. The library traversal (injected at the
|
||||||
|
// marker below) gates its any-hit / intersection callbacks on these
|
||||||
|
// consts, so a triangle-only opaque scene dead-strips all user code out
|
||||||
|
// of TRACE and keeps its zero-user-code register footprint. When either
|
||||||
|
// is set the JS side also gives the TRACE pipeline the user bind-group
|
||||||
|
// layout (so any-hit / intersection shaders can sample @group(3+)
|
||||||
|
// resources) — it scans for the exact `@CRAFTER_RT_TRACE_USER` marker.
|
||||||
|
wgsl += "\nconst RT_HAS_ANYHIT: bool = ";
|
||||||
|
wgsl += (anyAnyhit ? "true" : "false");
|
||||||
|
wgsl += ";\n";
|
||||||
|
wgsl += "const RT_HAS_INTERSECTION: bool = ";
|
||||||
|
wgsl += (anyIntersection ? "true" : "false");
|
||||||
|
wgsl += ";\n";
|
||||||
|
if (anyAnyhit || anyIntersection) {
|
||||||
|
wgsl += "// @CRAFTER_RT_TRACE_USER = true\n";
|
||||||
|
}
|
||||||
|
|
||||||
// runResolve — RESOLVE-stage tonemap hook. The first registered
|
// runResolve — RESOLVE-stage tonemap hook. The first registered
|
||||||
// Resolve shader wins; with none, identity passthrough (alpha forced
|
// Resolve shader wins; with none, identity passthrough (alpha forced
|
||||||
// to 1) so the wavefront output matches a megakernel that wrote raw
|
// to 1) so the wavefront output matches a megakernel that wrote raw
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,17 @@ export namespace Crafter {
|
||||||
};
|
};
|
||||||
static_assert(sizeof(BVHNode) == 32);
|
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 {
|
class Mesh {
|
||||||
public:
|
public:
|
||||||
// BLAS "handle": opaque identity that goes into
|
// 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.
|
// as `vertexAttribs : array<u32>` with a per-mesh u32-word offset.
|
||||||
void Build(const ::Crafter::CompressedMeshAsset& asset,
|
void Build(const ::Crafter::CompressedMeshAsset& asset,
|
||||||
WebGPUCommandEncoderRef cmd = 0);
|
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
|
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -60,18 +60,26 @@ export namespace Crafter {
|
||||||
inline constexpr std::uint8_t kRTGeometryInstanceForceOpaque = 0x4;
|
inline constexpr std::uint8_t kRTGeometryInstanceForceOpaque = 0x4;
|
||||||
inline constexpr std::uint8_t kRTGeometryInstanceForceNoOpaque = 0x8;
|
inline constexpr std::uint8_t kRTGeometryInstanceForceNoOpaque = 0x8;
|
||||||
|
|
||||||
// Hit-group identification. Matches VkRayTracingShaderGroupTypeKHR for
|
// Hit-group identification. Matches VkRayTracingShaderGroupTypeKHR.
|
||||||
// the two types we actually support (general + triangles-hit).
|
// 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 {
|
enum class RTShaderGroupType : std::uint8_t {
|
||||||
General = 0, // raygen / miss / callable
|
General = 0, // raygen / miss / callable
|
||||||
TrianglesHitGroup = 1,
|
TrianglesHitGroup = 1,
|
||||||
|
ProceduralHitGroup = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cross-backend description of one entry in the shader-group array
|
// Cross-backend description of one entry in the shader-group array
|
||||||
// passed to PipelineRT::Init. Mirrors the meaningful subset of
|
// passed to PipelineRT::Init. Mirrors the meaningful subset of
|
||||||
// VkRayTracingShaderGroupCreateInfoKHR: per group, the type and the
|
// VkRayTracingShaderGroupCreateInfoKHR: per group, the type and the
|
||||||
// indices (into the SBT's shader array) for general / closestHit /
|
// 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;
|
inline constexpr std::uint32_t kRTShaderUnused = 0xFFFFFFFFu;
|
||||||
|
|
||||||
struct RTShaderGroup {
|
struct RTShaderGroup {
|
||||||
|
|
@ -79,5 +87,6 @@ export namespace Crafter {
|
||||||
std::uint32_t generalShader = kRTShaderUnused;
|
std::uint32_t generalShader = kRTShaderUnused;
|
||||||
std::uint32_t closestHitShader = kRTShaderUnused;
|
std::uint32_t closestHitShader = kRTShaderUnused;
|
||||||
std::uint32_t anyHitShader = kRTShaderUnused;
|
std::uint32_t anyHitShader = kRTShaderUnused;
|
||||||
|
std::uint32_t intersectionShader = kRTShaderUnused;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,14 @@ export namespace Crafter {
|
||||||
// buffer through unchanged. Signature:
|
// buffer through unchanged. Signature:
|
||||||
// fn <entryFn>(coord: vec2<u32>, hdr: vec4<f32>) -> vec4<f32>
|
// 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
|
// 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>)
|
// ClosestHit: fn <entryFn>(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>)
|
||||||
// AnyHit: fn <entryFn>(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) -> u32
|
// AnyHit: fn <entryFn>(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) -> u32
|
||||||
// returns RT_ANYHIT_ACCEPT / RT_ANYHIT_IGNORE / RT_ANYHIT_END_SEARCH.
|
// 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` /
|
// `RayDesc`, `HitInfo`, the `RT_*` flag/return constants, the `tlas` /
|
||||||
// BLAS / mesh-record bindings, and the `traceRay` function are all
|
// BLAS / mesh-record bindings, and the `traceRay` function are all
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,12 @@ namespace Crafter::WebGPU {
|
||||||
// that gets appended to a global attribs heap and exposed to RT
|
// that gets appended to a global attribs heap and exposed to RT
|
||||||
// closest-hit shaders as `vertexAttribs : array<u32>` at
|
// closest-hit shaders as `vertexAttribs : array<u32>` at
|
||||||
// @group(1) @binding(7). Pass (nullptr, 0) for positions-only meshes.
|
// @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")))
|
__attribute__((import_module("env"), import_name("wgpuRegisterMeshBLAS")))
|
||||||
extern "C" std::uint32_t wgpuRegisterMeshBLAS(
|
extern "C" std::uint32_t wgpuRegisterMeshBLAS(
|
||||||
float minX, float minY, float minZ,
|
float minX, float minY, float minZ,
|
||||||
|
|
@ -175,7 +181,8 @@ namespace Crafter::WebGPU {
|
||||||
const void* indicesPtr, std::int32_t indexCount,
|
const void* indicesPtr, std::int32_t indexCount,
|
||||||
const void* bvhNodesPtr, std::int32_t bvhNodeCount,
|
const void* bvhNodesPtr, std::int32_t bvhNodeCount,
|
||||||
const void* primRemapPtr, std::int32_t primRemapCount,
|
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
|
// RT pipeline build. The library composes WGSL by concatenating the
|
||||||
// traversal library, generated hit-group switches, and the user-
|
// traversal library, generated hit-group switches, and the user-
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue