From 26d7ffd75948081c7992886eda3e5b746a0ba371 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 22 Mar 2026 11:38:24 +0900 Subject: [PATCH 1/6] Add opacity support --- src/webgpu/WebGPUPathTracer.js | 1 + src/webgpu/lib/BVHComputeData.js | 8 +- src/webgpu/nodes/PathtracerBVHComputeData.js | 143 ++++++++++++++++++- src/webgpu/nodes/random.wgsl.js | 7 + 4 files changed, 153 insertions(+), 6 deletions(-) diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index 8802a294f..fd46f1ef0 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -124,6 +124,7 @@ export class WebGPUPathTracer { // Build TLAS and compute functions const bvhData = new PathtracerBVHComputeData( scene ); bvhData.update(); + bvhData.useTransparencyRaycastFn(); this.textureArray.setTextures( this._renderer, bvhData.textures ); this._pathTracer.setTextures( this.textureArray.texture ); diff --git a/src/webgpu/lib/BVHComputeData.js b/src/webgpu/lib/BVHComputeData.js index 60ccd7fd1..bffa025f4 100644 --- a/src/webgpu/lib/BVHComputeData.js +++ b/src/webgpu/lib/BVHComputeData.js @@ -97,7 +97,7 @@ const transformStruct = new StructTypeNode( { _alignment1: 'uint', }, 'TransformStruct' ); -const intersectionResultStruct = new StructTypeNode( { +export const intersectionResultStruct = new StructTypeNode( { indices: 'vec4u', normal: 'vec3f', didHit: 'bool', @@ -147,7 +147,7 @@ function getTotalBVHByteLength( bvh ) { } -const intersectsTriangle = wgslTagFn/* wgsl */ ` +export const intersectsTriangle = wgslTagFn/* wgsl */ ` // fn fn intersectsTriangle( ray: ${ rayStruct }, a: vec3f, b: vec3f, c: vec3f ) -> ${ intersectionResultStruct } { @@ -394,7 +394,7 @@ export class BVHComputeData { } // Transform shape into object local space - let localShape = ${ transformShapeFn }( shape, transform.inverseMatrixWorld ); + let localShape = ${ transformShapeFn }( shape, transform.inverseMatrixWorld, t ); let blasHit = ${ blasFn( { shape: 'localShape', rootNodeIndex: 'transform.nodeOffset', bestDist: 'bestHit.dist' } ) }; if ( blasHit.didHit && blasHit.dist < bestHit.dist ) { @@ -836,7 +836,7 @@ export class BVHComputeData { transformShapeFn: wgslTagFn/* wgsl */` ${ [ scratchRayScalar ] } - fn transformRay( ray: ${ rayStruct }, toLocal: mat4x4f ) -> ${ rayStruct } { + fn transformRay( ray: ${ rayStruct }, toLocal: mat4x4f, objectId: u32 ) -> ${ rayStruct } { var localRay: Ray; localRay.origin = ( toLocal * vec4f( ray.origin, 1.0 ) ).xyz; diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index a1af4e8af..67ad603e6 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -1,9 +1,12 @@ import { BackSide, FrontSide, DoubleSide, BufferAttribute, BufferGeometry, StorageBufferAttribute, StructTypeNode, Vector4, SkinnedMesh } from 'three/webgpu'; -import { BVHComputeData } from '../lib/BVHComputeData.js'; -import { storage } from 'three/tsl'; +import { BVHComputeData, intersectionResultStruct, intersectsTriangle } from '../lib/BVHComputeData.js'; +import { storage, wgsl } from 'three/tsl'; import { SkinnedMeshBVH, MeshBVH, SAH } from 'three-mesh-bvh'; import { materialStruct } from './structs.wgsl.js'; import { getTextureHash } from '../../core/utils/sceneUpdateUtils.js'; +import { bvhNodeBoundsStruct, bvhNodeStruct, rayStruct } from '../lib/wgsl/structs.wgsl.js'; +import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; +import { pcgRand } from './random.wgsl.js'; const _colorVec = new Vector4(); const transformStruct = new StructTypeNode( { @@ -35,11 +38,147 @@ export class PathtracerBVHComputeData extends BVHComputeData { this.structs.transform = transformStruct; this.structs.material = materialStruct; + this.storage.materials = null; this.materials = []; this.bvhMap = new Map(); } + useTransparencyRaycastFn() { + + const { prefix, storage, structs, fns } = this; + + // raycast first hit + const scratchRayScalar = wgsl( /* wgsl */` + var ${ prefix }rayScalar = 1.0; + var ${ prefix }objectId = 0u; + ` ); + + fns.raycastFirstHit = this.getShapecastFn( { + name: prefix + 'RaycastFirstHit', + shapeStruct: rayStruct, + resultStruct: intersectionResultStruct, + + boundsOrderFn: wgslTagFn/* wgsl */` + fn getBoundsOrder( ray: ${ rayStruct }, splitAxis: u32, node: ${ bvhNodeStruct } ) -> bool { + + return ray.direction[ splitAxis ] >= 0.0; + + } + `, + intersectsBoundsFn: wgslTagFn/* wgsl */` + ${ [ scratchRayScalar ] } + + fn rayIntersectsBounds( ray: ${ rayStruct }, bounds: ${ bvhNodeBoundsStruct } ) -> f32 { + + let boundsMin = vec3( bounds.min[0], bounds.min[1], bounds.min[2] ); + let boundsMax = vec3( bounds.max[0], bounds.max[1], bounds.max[2] ); + + let invDir = 1.0 / ray.direction; + let tMinPlane = ( boundsMin - ray.origin ) * invDir; + let tMaxPlane = ( boundsMax - ray.origin ) * invDir; + + let tMinHit = vec3f( + min( tMinPlane.x, tMaxPlane.x ), + min( tMinPlane.y, tMaxPlane.y ), + min( tMinPlane.z, tMaxPlane.z ) + ); + + let tMaxHit = vec3f( + max( tMinPlane.x, tMaxPlane.x ), + max( tMinPlane.y, tMaxPlane.y ), + max( tMinPlane.z, tMaxPlane.z ) + ); + + let t0 = max( max( tMinHit.x, tMinHit.y ), tMinHit.z ); + let t1 = min( min( tMaxHit.x, tMaxHit.y ), tMaxHit.z ); + + let dist = max( t0, 0.0 ); + if ( t1 >= dist ) { + + return dist * ${ prefix }rayScalar; + + } else { + + return - 1.0; + + } + + } + + `, + intersectRangeFn: wgslTagFn/* wgsl */` + ${ [ scratchRayScalar ] } + + fn intersectRange( ray: ${ rayStruct }, offset: u32, count: u32, bestDist: f32 ) -> ${ intersectionResultStruct } { + + var bestHit: ${ intersectionResultStruct }; + bestHit.didHit = false; + bestHit.dist = bestDist; + + for ( var ti = offset; ti < offset + count; ti = ti + 1u ) { + + let i0 = ${ storage.index }[ ti * 3u ]; + let i1 = ${ storage.index }[ ti * 3u + 1u ]; + let i2 = ${ storage.index }[ ti * 3u + 2u ]; + + let a = ${ storage.attributes }[ i0 ].position.xyz; + let b = ${ storage.attributes }[ i1 ].position.xyz; + let c = ${ storage.attributes }[ i2 ].position.xyz; + + var triResult = ${ intersectsTriangle }( ray, a, b, c ); + triResult.dist *= ${ prefix }rayScalar; + if ( triResult.didHit && triResult.dist < bestHit.dist ) { + + let object = ${ storage.transforms }[ ${ prefix }objectId ]; + let material = ${ storage.materials }[ object.materialIndex ]; + let opacity = material.opacity * object.color.a; + if ( opacity < ${ pcgRand }() ) { + + continue; + + } + + bestHit = triResult; + bestHit.indices = vec4u( i0, i1, i2, ti ); + + } + + } + + return bestHit; + + } + `, + transformShapeFn: wgslTagFn/* wgsl */` + ${ [ scratchRayScalar ] } + + fn transformRay( ray: ${ rayStruct }, toLocal: mat4x4f, objectId: u32 ) -> ${ rayStruct } { + + var localRay: Ray; + localRay.origin = ( toLocal * vec4f( ray.origin, 1.0 ) ).xyz; + localRay.direction = ( toLocal * vec4f( ray.direction, 0.0 ) ).xyz; + + let len = length( localRay.direction ); + localRay.direction /= len; + ${ prefix }rayScalar = 1.0 / len; + ${ prefix }objectId = objectId; + + return localRay; + + } + `, + transformResultFn: wgslTagFn/* wgsl */` + fn transformResult( hit: ptr, toWorld: mat4x4f, toLocal: mat4x4f ) -> void { + + hit.normal = normalize( ( transpose( toLocal ) * vec4f( hit.normal, 0.0 ) ).xyz ); + + } + `, + } ); + + } + update() { super.update(); diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js index ecb2dee02..78ef5cd48 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -53,3 +53,10 @@ export const pcgRand2 = wgslFn( /*wgsl*/` return abs( vec2f(g_state.s0.xy) / f32(0xffffffffu) ); } `, [ pcg4d, pcgStateStruct ] ); + +export const pcgRand = wgslFn( /*wgsl*/` + fn pcgRand() -> f32 { + pcg4d(&g_state.s0); + return abs( f32(g_state.s0.x) / f32(0xffffffffu) ); + } +`, [ pcg4d, pcgStateStruct ] ); From ab5fa9211542c91d483dcbf327c4364082a54dd0 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 22 Mar 2026 12:14:11 +0900 Subject: [PATCH 2/6] merge hit and ray queues --- src/webgpu/WaveFrontPathTracer.js | 43 +++++++------------ .../PrimeRayGenerationDispatchKernel.js | 18 +++++--- .../compute/wavefront/ProcessHitsKernel.js | 14 +++--- .../compute/wavefront/RayGenerationKernel.js | 6 +-- .../wavefront/RayIntersectionKernel.js | 14 +++--- .../wavefront/UpdateRayQueueParamsKernel.js | 10 ++--- 6 files changed, 44 insertions(+), 61 deletions(-) diff --git a/src/webgpu/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js index baa7b0e66..9aee6b60f 100644 --- a/src/webgpu/WaveFrontPathTracer.js +++ b/src/webgpu/WaveFrontPathTracer.js @@ -29,13 +29,13 @@ export class WaveFrontPathTracer extends PathTracerBackend { // queues this.rayQueue = new IndirectStorageBufferAttribute( MAX_RAY_COUNT, 16 ); this.rayQueue.name = 'Ray Queue'; - this.rayQueueSize = new IndirectStorageBufferAttribute( 2, 1 ); - this.rayQueueSize.name = 'Ray Queue Size'; this.hitQueue = new IndirectStorageBufferAttribute( MAX_HIT_COUNT, 16 ); this.hitQueue.name = 'Hit Queue'; - this.hitQueueSize = new IndirectStorageBufferAttribute( 2, 1 ); - this.hitQueueSize.name = 'Hit Queue Size'; + + // [0] ray head, [1] ray count, [2] hit head, [3] hit count + this.queueSizes = new IndirectStorageBufferAttribute( 4, 1 ); + this.queueSizes.name = 'Queue Sizes'; // dispatches this.tileIndexBuffer = new IndirectStorageBufferAttribute( 2, 1 ); @@ -152,8 +152,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { hitProcessDispatch, rayGenerationDispatch, - rayQueueSize, - hitQueueSize, + queueSizes, tileIndexBuffer, } = this; @@ -174,11 +173,8 @@ export class WaveFrontPathTracer extends PathTracerBackend { // clear queues // TODO: why do we need to se the work group size here? - zeroDispatchKernel.target = rayQueueSize; - renderer.compute( zeroDispatchKernel.kernel, [ rayQueueSize.count ] ); - - zeroDispatchKernel.target = hitQueueSize; - renderer.compute( zeroDispatchKernel.kernel, [ hitQueueSize.count ] ); + zeroDispatchKernel.target = queueSizes; + renderer.compute( zeroDispatchKernel.kernel, [ queueSizes.count ] ); // clear dispatch sizes zeroDispatchKernel.target = hitProcessDispatch; @@ -203,11 +199,9 @@ export class WaveFrontPathTracer extends PathTracerBackend { sampleCountTarget, rayQueue, - rayQueueSize, - rayGenerationDispatch, - hitQueue, - hitQueueSize, + queueSizes, + rayGenerationDispatch, // hitProcessDispatch, tileIndexBuffer, @@ -216,7 +210,6 @@ export class WaveFrontPathTracer extends PathTracerBackend { rayIntersectionKernel, updateRayQueueParamsKernel, hitProcessKernel, - zeroDispatchKernel, lowResMode } = this; @@ -251,7 +244,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { primeRayGenerationDispatchKernel.tileCount.copy( tiles ); primeRayGenerationDispatchKernel.tileSize.copy( tileSize ); primeRayGenerationDispatchKernel.rayQueue = rayQueue; - primeRayGenerationDispatchKernel.rayQueueSize = rayQueueSize; + primeRayGenerationDispatchKernel.queueSizes = queueSizes; primeRayGenerationDispatchKernel.outputTileIndex = tileIndexBuffer; primeRayGenerationDispatchKernel.outputDispatch = rayGenerationDispatch; @@ -262,7 +255,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { enqueueRaysKernel.tileIndexBuffer = tileIndexBuffer; enqueueRaysKernel.tileSize.copy( tileSize ); enqueueRaysKernel.rayQueue = rayQueue; - enqueueRaysKernel.rayQueueSize = rayQueueSize; + enqueueRaysKernel.queueSizes = queueSizes; enqueueRaysKernel.sampleCountTarget = sampleCountTarget; for ( let i = 0; i < tiles.x * tiles.y; i ++ ) { @@ -279,15 +272,14 @@ export class WaveFrontPathTracer extends PathTracerBackend { const intersectDispatch = rayIntersectionKernel.getDispatchSize( RAYS_TO_PROCESS, 1, 1 ); rayIntersectionKernel.sampleCountTarget = sampleCountTarget; rayIntersectionKernel.rayQueue = rayQueue; - rayIntersectionKernel.rayQueueSize = rayQueueSize; + rayIntersectionKernel.queueSizes = queueSizes; rayIntersectionKernel.hitQueue = hitQueue; - rayIntersectionKernel.hitQueueSize = hitQueueSize; renderer.compute( rayIntersectionKernel.kernel, intersectDispatch ); // mark the rays as consumed const processed = intersectDispatch[ 0 ] * rayIntersectionKernel.workgroupSize[ 0 ]; updateRayQueueParamsKernel.processed = processed; - updateRayQueueParamsKernel.rayQueueSize = rayQueueSize; + updateRayQueueParamsKernel.queueSizes = queueSizes; renderer.compute( updateRayQueueParamsKernel.kernel, [ 1, 1, 1 ] ); // TODO: we should use an indirect dispatch here to only kick off the number of threads @@ -296,15 +288,10 @@ export class WaveFrontPathTracer extends PathTracerBackend { hitProcessKernel.sampleCountTarget = sampleCountTarget; hitProcessKernel.bounces = bounces; hitProcessKernel.rayQueue = rayQueue; - hitProcessKernel.rayQueueSize = rayQueueSize; + hitProcessKernel.queueSizes = queueSizes; hitProcessKernel.hitQueue = hitQueue; - hitProcessKernel.hitQueueSize = hitQueueSize; renderer.compute( hitProcessKernel.kernel, hitProcessKernel.getDispatchSize( processed, 1, 1 ) ); - - // TODO: for some reason we need to call "setWorkgroupSize" here? Is it because work group size - // is cached per parameters and resets? - zeroDispatchKernel.target = hitQueueSize; - renderer.compute( zeroDispatchKernel.kernel, [ hitQueueSize.count, 1, 1 ] ); + // Note: hit queue size ([2] and [3]) is reset at the top of the next iteration by PrimeRayGenerationDispatchKernel // Step 4: connect to lights // TODO diff --git a/src/webgpu/compute/wavefront/PrimeRayGenerationDispatchKernel.js b/src/webgpu/compute/wavefront/PrimeRayGenerationDispatchKernel.js index 9d7b661b3..badb651cd 100644 --- a/src/webgpu/compute/wavefront/PrimeRayGenerationDispatchKernel.js +++ b/src/webgpu/compute/wavefront/PrimeRayGenerationDispatchKernel.js @@ -17,7 +17,7 @@ export class PrimeRayGenerationDispatchKernel extends ComputeKernel { tileOffset: uniform( 1 ), rayQueue: storage( new IndirectStorageBufferAttribute( 1, queuedRayStruct.getLength() ), queuedRayStruct ).toReadOnly(), - rayQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ), + queueSizes: storage( new IndirectStorageBufferAttribute( 4, 1 ), 'u32' ), outputTileIndex: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ).setName( 'outputTileIndex' ), outputDispatch: storage( new IndirectStorageBufferAttribute( 3, 1 ), 'u32' ).setName( 'outputDispatch' ), @@ -33,24 +33,28 @@ export class PrimeRayGenerationDispatchKernel extends ComputeKernel { ) -> void { let rayQueue = &${ params.rayQueue }; - let rayQueueSize = &${ params.rayQueueSize }; + let queueSizes = &${ params.queueSizes }; let outputTileIndex = &${ params.outputTileIndex }; let outputDispatch = &${ params.outputDispatch }; + // reset hit queue size from previous iteration + queueSizes[ 2 ] = 0u; + queueSizes[ 3 ] = 0u; + // keep the queue index small let queueCapacity = arrayLength( rayQueue ); - if ( rayQueueSize[ 0 ] >= queueCapacity ) { + if ( queueSizes[ 0 ] >= queueCapacity ) { // uint division results in a floored value - let offset = rayQueueSize[ 0 ] / queueCapacity; - rayQueueSize[ 0 ] = rayQueueSize[ 0 ] - queueCapacity * offset; - rayQueueSize[ 1 ] = rayQueueSize[ 1 ] - queueCapacity * offset; + let offset = queueSizes[ 0 ] / queueCapacity; + queueSizes[ 0 ] = queueSizes[ 0 ] - queueCapacity * offset; + queueSizes[ 1 ] = queueSizes[ 1 ] - queueCapacity * offset; } // calculate the amount of elements in the queue - var queueSize = rayQueueSize[ 1 ] - rayQueueSize[ 0 ]; + var queueSize = queueSizes[ 1 ] - queueSizes[ 0 ]; // calculate the overhead of space in the queue and how much space we need to run a new tile let overhead = queueCapacity - queueSize; diff --git a/src/webgpu/compute/wavefront/ProcessHitsKernel.js b/src/webgpu/compute/wavefront/ProcessHitsKernel.js index 76975b954..aab2a058e 100644 --- a/src/webgpu/compute/wavefront/ProcessHitsKernel.js +++ b/src/webgpu/compute/wavefront/ProcessHitsKernel.js @@ -24,10 +24,8 @@ export class ProcessHitsKernel extends ComputeKernel { // rays rayQueue: storage( new IndirectStorageBufferAttribute( 1, queuedRayStruct.getLength() ), queuedRayStruct ), - rayQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ).toAtomic(), - hitQueue: storage( new IndirectStorageBufferAttribute( 1, queuedHitStruct.getLength() ), queuedHitStruct ), - hitQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ), + queueSizes: storage( new IndirectStorageBufferAttribute( 4, 1 ), 'u32' ).toAtomic(), textures: texture( new DataTexture() ), textureSampler: sampler( new DataTexture() ), @@ -51,18 +49,16 @@ export class ProcessHitsKernel extends ComputeKernel { ) -> void { let rayQueue = &${ params.rayQueue }; - let rayQueueSize = &${ params.rayQueueSize }; - let hitQueue = &${ params.hitQueue }; - let hitQueueSize = &${ params.hitQueueSize }; + let queueSizes = &${ params.queueSizes }; let materials = &${ proxy( 'bvhData.value.storage.materials', params ) }; let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) }; // skip any rays invocations beyond the ray count let hitQueueCapacity = arrayLength( hitQueue ); - let hitIndex = ( globalId.x + hitQueueSize[ 0 ] ); - if ( hitIndex >= hitQueueSize[ 1 ] ) { + let hitIndex = ( globalId.x + atomicLoad( &queueSizes[ 2 ] ) ); + if ( hitIndex >= atomicLoad( &queueSizes[ 3 ] ) ) { return; @@ -102,7 +98,7 @@ export class ProcessHitsKernel extends ComputeKernel { } else { let rayQueueCapacity = arrayLength( rayQueue ); - let index = atomicAdd( &rayQueueSize[ 1 ], 1 ) % rayQueueCapacity; + let index = atomicAdd( &queueSizes[ 1 ], 1 ) % rayQueueCapacity; rayQueue[ index ].origin = vertexData.position.xyz; rayQueue[ index ].direction = scatterRec.direction; rayQueue[ index ].pixel = indexUV; diff --git a/src/webgpu/compute/wavefront/RayGenerationKernel.js b/src/webgpu/compute/wavefront/RayGenerationKernel.js index 2ed578f62..e58526d63 100644 --- a/src/webgpu/compute/wavefront/RayGenerationKernel.js +++ b/src/webgpu/compute/wavefront/RayGenerationKernel.js @@ -21,7 +21,7 @@ export class RayGenerationKernel extends ComputeKernel { tileSize: uniform( new Vector2() ), rayQueue: storage( new IndirectStorageBufferAttribute( 1, queuedRayStruct.getLength() ), queuedRayStruct ), - rayQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ).toAtomic(), + queueSizes: storage( new IndirectStorageBufferAttribute( 4, 1 ), 'u32' ).toAtomic(), sampleCountTarget: textureStore( new StorageTexture() ).toReadWrite(), @@ -40,7 +40,7 @@ export class RayGenerationKernel extends ComputeKernel { ) -> void { let rayQueue = &${ params.rayQueue }; - let rayQueueSize = &${ params.rayQueueSize }; + let queueSizes = &${ params.queueSizes }; let tileIndexBuffer = &${ params.tileIndexBuffer }; // don't overstep the edge of the tile @@ -78,7 +78,7 @@ export class RayGenerationKernel extends ComputeKernel { // get the ray index let queueCapacity = arrayLength( rayQueue ); - let index = atomicAdd( &rayQueueSize[ 1 ], 1 ) % queueCapacity; + let index = atomicAdd( &queueSizes[ 1 ], 1 ) % queueCapacity; ${ pcgInit }( indexUV, seed ); diff --git a/src/webgpu/compute/wavefront/RayIntersectionKernel.js b/src/webgpu/compute/wavefront/RayIntersectionKernel.js index dba73871a..a821baedc 100644 --- a/src/webgpu/compute/wavefront/RayIntersectionKernel.js +++ b/src/webgpu/compute/wavefront/RayIntersectionKernel.js @@ -20,10 +20,8 @@ export class RayIntersectionKernel extends ComputeKernel { // rays rayQueue: storage( new IndirectStorageBufferAttribute( 1, queuedRayStruct.getLength() ), queuedRayStruct ).toReadOnly(), - rayQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ).toReadOnly(), - hitQueue: storage( new IndirectStorageBufferAttribute( 1, queuedHitStruct.getLength() ), queuedHitStruct ), - hitQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ).toAtomic(), + queueSizes: storage( new IndirectStorageBufferAttribute( 4, 1 ), 'u32' ).toAtomic(), // environment envMap: texture( new DataTexture() ), @@ -62,10 +60,8 @@ export class RayIntersectionKernel extends ComputeKernel { ) -> void { let rayQueue = &${ params.rayQueue }; - let rayQueueSize = &${ params.rayQueueSize }; - let hitQueue = &${ params.hitQueue }; - let hitQueueSize = &${ params.hitQueueSize }; + let queueSizes = &${ params.queueSizes }; let envInfo = EnvironmentInfo( envMapRotation, @@ -81,8 +77,8 @@ export class RayIntersectionKernel extends ComputeKernel { // skip any rays invocations beyond the ray count let queueCapacity = arrayLength( rayQueue ); - let rayIndex = ( globalId.x + rayQueueSize[ 0 ] ); - if ( rayIndex >= rayQueueSize[ 1 ] ) { + let rayIndex = ( globalId.x + atomicLoad( &queueSizes[ 0 ] ) ); + if ( rayIndex >= atomicLoad( &queueSizes[ 1 ] ) ) { return; @@ -102,7 +98,7 @@ export class RayIntersectionKernel extends ComputeKernel { if ( hitResult.didHit ) { // TODO: we process all of these materials immediately to push to the ray queue - let index = atomicAdd( &hitQueueSize[ 1 ], 1 ); + let index = atomicAdd( &queueSizes[ 3 ], 1 ); hitQueue[ index ].view = - input.direction; hitQueue[ index ].indices = hitResult.indices.xyz; hitQueue[ index ].barycoord = hitResult.barycoord; diff --git a/src/webgpu/compute/wavefront/UpdateRayQueueParamsKernel.js b/src/webgpu/compute/wavefront/UpdateRayQueueParamsKernel.js index ec816ab87..84cf100bc 100644 --- a/src/webgpu/compute/wavefront/UpdateRayQueueParamsKernel.js +++ b/src/webgpu/compute/wavefront/UpdateRayQueueParamsKernel.js @@ -9,21 +9,21 @@ export class UpdateRayQueueParamsKernel extends ComputeKernel { const params = { processed: uniform( 0 ), - rayQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ), + queueSizes: storage( new IndirectStorageBufferAttribute( 4, 1 ), 'u32' ), }; const fn = wgslTagFn/* wgsl */` fn compute( processed: u32 ) -> void { - let rayQueueSize = &${ params.rayQueueSize }; - var queueSize = rayQueueSize[ 1 ] - rayQueueSize[ 0 ]; + let queueSizes = &${ params.queueSizes }; + var queueSize = queueSizes[ 1 ] - queueSizes[ 0 ]; if ( processed > queueSize ) { - rayQueueSize[ 0 ] = rayQueueSize[ 1 ]; + queueSizes[ 0 ] = queueSizes[ 1 ]; } else { - rayQueueSize[ 0 ] = rayQueueSize[ 0 ] + processed; + queueSizes[ 0 ] = queueSizes[ 0 ] + processed; } From fafb27a22f458e707e7626c47abca071b780fe9b Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 22 Mar 2026 12:18:46 +0900 Subject: [PATCH 3/6] Check transparency flag --- src/webgpu/nodes/PathtracerBVHComputeData.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index 67ad603e6..000ff1bf6 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -46,7 +46,7 @@ export class PathtracerBVHComputeData extends BVHComputeData { useTransparencyRaycastFn() { - const { prefix, storage, structs, fns } = this; + const { prefix, storage, fns } = this; // raycast first hit const scratchRayScalar = wgsl( /* wgsl */` @@ -133,7 +133,7 @@ export class PathtracerBVHComputeData extends BVHComputeData { let object = ${ storage.transforms }[ ${ prefix }objectId ]; let material = ${ storage.materials }[ object.materialIndex ]; let opacity = material.opacity * object.color.a; - if ( opacity < ${ pcgRand }() ) { + if ( material.transparent != 0 && opacity < ${ pcgRand }() ) { continue; From 17d810f048eff1944871d7c7ee59b4f305d310b2 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 22 Mar 2026 12:31:22 +0900 Subject: [PATCH 4/6] Early out --- src/webgpu/nodes/PathtracerBVHComputeData.js | 40 ++++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index 000ff1bf6..de00e0081 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -46,12 +46,13 @@ export class PathtracerBVHComputeData extends BVHComputeData { useTransparencyRaycastFn() { - const { prefix, storage, fns } = this; + const { prefix, storage, structs, fns } = this; // raycast first hit const scratchRayScalar = wgsl( /* wgsl */` var ${ prefix }rayScalar = 1.0; - var ${ prefix }objectId = 0u; + var ${ prefix }material: ${ structs.material.name }; + var ${ prefix }baseOpacity = 1.0; ` ); fns.raycastFirstHit = this.getShapecastFn( { @@ -71,6 +72,13 @@ export class PathtracerBVHComputeData extends BVHComputeData { fn rayIntersectsBounds( ray: ${ rayStruct }, bounds: ${ bvhNodeBoundsStruct } ) -> f32 { + // early-out if our object is completely transparent + if ( ${ prefix }baseOpacity == 0.0 ) { + + return - 1.0; + + } + let boundsMin = vec3( bounds.min[0], bounds.min[1], bounds.min[2] ); let boundsMax = vec3( bounds.max[0], bounds.max[1], bounds.max[2] ); @@ -130,12 +138,17 @@ export class PathtracerBVHComputeData extends BVHComputeData { triResult.dist *= ${ prefix }rayScalar; if ( triResult.didHit && triResult.dist < bestHit.dist ) { - let object = ${ storage.transforms }[ ${ prefix }objectId ]; - let material = ${ storage.materials }[ object.materialIndex ]; - let opacity = material.opacity * object.color.a; - if ( material.transparent != 0 && opacity < ${ pcgRand }() ) { - continue; + let material = ${ prefix }material; + if ( material.transparent != 0 ) { + + let opacity = ${ prefix }baseOpacity; + // TODO: sample albedo + alphaMap alpha + if ( opacity < ${ pcgRand }() ) { + + continue; + + } } @@ -162,7 +175,18 @@ export class PathtracerBVHComputeData extends BVHComputeData { let len = length( localRay.direction ); localRay.direction /= len; ${ prefix }rayScalar = 1.0 / len; - ${ prefix }objectId = objectId; + + let object = ${ storage.transforms }[ objectId ]; + ${ prefix }material = ${ storage.materials }[ object.materialIndex ]; + if ( ${ prefix }material.transparent == 1 ) { + + ${ prefix }baseOpacity = ${ prefix }material.opacity * object.color.a; + + } else { + + ${ prefix }baseOpacity = 1.0; + + } return localRay; From 507b2021ba944a20945eb06921a25bdf48ffd69d Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 22 Mar 2026 14:08:54 +0900 Subject: [PATCH 5/6] Use wgslTagCode node --- src/webgpu/nodes/PathtracerBVHComputeData.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index de00e0081..893a71a44 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -1,11 +1,11 @@ import { BackSide, FrontSide, DoubleSide, BufferAttribute, BufferGeometry, StorageBufferAttribute, StructTypeNode, Vector4, SkinnedMesh } from 'three/webgpu'; import { BVHComputeData, intersectionResultStruct, intersectsTriangle } from '../lib/BVHComputeData.js'; -import { storage, wgsl } from 'three/tsl'; +import { storage } from 'three/tsl'; import { SkinnedMeshBVH, MeshBVH, SAH } from 'three-mesh-bvh'; import { materialStruct } from './structs.wgsl.js'; import { getTextureHash } from '../../core/utils/sceneUpdateUtils.js'; import { bvhNodeBoundsStruct, bvhNodeStruct, rayStruct } from '../lib/wgsl/structs.wgsl.js'; -import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; +import { wgslTagCode, wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; import { pcgRand } from './random.wgsl.js'; const _colorVec = new Vector4(); @@ -49,11 +49,11 @@ export class PathtracerBVHComputeData extends BVHComputeData { const { prefix, storage, structs, fns } = this; // raycast first hit - const scratchRayScalar = wgsl( /* wgsl */` + const scratchRayScalar = wgslTagCode/* wgsl */` var ${ prefix }rayScalar = 1.0; - var ${ prefix }material: ${ structs.material.name }; + var ${ prefix }material: ${ structs.material }; var ${ prefix }baseOpacity = 1.0; - ` ); + `; fns.raycastFirstHit = this.getShapecastFn( { name: prefix + 'RaycastFirstHit', From 66eb9a4ca3e554500eca46ff5c6f748d4f561127 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 21 Apr 2026 14:58:47 +0900 Subject: [PATCH 6/6] Simplify the min / max calls --- src/webgpu/nodes/PathtracerBVHComputeData.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index ab8c85ce7..2b60b5748 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -86,17 +86,8 @@ export class PathtracerBVHComputeData extends BVHComputeData { let tMinPlane = ( boundsMin - ray.origin ) * invDir; let tMaxPlane = ( boundsMax - ray.origin ) * invDir; - let tMinHit = vec3f( - min( tMinPlane.x, tMaxPlane.x ), - min( tMinPlane.y, tMaxPlane.y ), - min( tMinPlane.z, tMaxPlane.z ) - ); - - let tMaxHit = vec3f( - max( tMinPlane.x, tMaxPlane.x ), - max( tMinPlane.y, tMaxPlane.y ), - max( tMinPlane.z, tMaxPlane.z ) - ); + let tMinHit = min( tMinPlane, tMaxPlane ); + let tMaxHit = max( tMinPlane, tMaxPlane ); let t0 = max( max( tMinHit.x, tMinHit.y ), tMinHit.z ); let t1 = min( min( tMaxHit.x, tMaxHit.y ), tMaxHit.z );