diff --git a/src/webgpu/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js index 2f4a3185..25f43f8b 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, queuedRayStruct.getLength() ); this.rayQueue.name = 'Ray Queue'; - this.rayQueueSize = new IndirectStorageBufferAttribute( 2, 1 ); - this.rayQueueSize.name = 'Ray Queue Size'; this.hitQueue = new IndirectStorageBufferAttribute( MAX_HIT_COUNT, queuedHitStruct.getLength() ); 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 ); @@ -160,8 +160,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { hitProcessDispatch, rayGenerationDispatch, - rayQueueSize, - hitQueueSize, + queueSizes, tileIndexBuffer, } = this; @@ -182,11 +181,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; @@ -211,11 +207,9 @@ export class WaveFrontPathTracer extends PathTracerBackend { sampleCountTarget, rayQueue, - rayQueueSize, - rayGenerationDispatch, - hitQueue, - hitQueueSize, + queueSizes, + rayGenerationDispatch, // hitProcessDispatch, tileIndexBuffer, @@ -224,7 +218,6 @@ export class WaveFrontPathTracer extends PathTracerBackend { rayIntersectionKernel, updateRayQueueParamsKernel, hitProcessKernel, - zeroDispatchKernel, lowResMode } = this; @@ -259,7 +252,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; @@ -270,7 +263,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 ++ ) { @@ -287,15 +280,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 @@ -304,15 +296,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/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index b658dd72..5fd8cc2c 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -133,6 +133,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/compute/wavefront/PrimeRayGenerationDispatchKernel.js b/src/webgpu/compute/wavefront/PrimeRayGenerationDispatchKernel.js index 9d7b661b..badb651c 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 bb89e968..7c7faf43 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; let resultColor = input.resultColor + vec4f( input.throughputColor * surface.emission, 0.0 ); rayQueue[ index ].origin = vertexData.position.xyz; rayQueue[ index ].direction = scatterRec.direction; diff --git a/src/webgpu/compute/wavefront/RayGenerationKernel.js b/src/webgpu/compute/wavefront/RayGenerationKernel.js index ea94dbac..4acb53f4 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 9a99063d..f4c6e88e 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 ( ${ raycastFirstHitFn }( ray, &hitResult ) ) { // 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 ec816ab8..84cf100b 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; } diff --git a/src/webgpu/lib/BVHComputeData.js b/src/webgpu/lib/BVHComputeData.js index f0935fca..68faea9a 100644 --- a/src/webgpu/lib/BVHComputeData.js +++ b/src/webgpu/lib/BVHComputeData.js @@ -116,7 +116,7 @@ const transformStruct = new StructTypeNode( { _alignment1: 'uint', }, 'TransformStruct' ); -const intersectionResultStruct = new StructTypeNode( { +export const intersectionResultStruct = new StructTypeNode( { indices: 'vec4u', normal: 'vec3f', didHit: 'bool', @@ -166,7 +166,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 } { diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index 46642200..2b60b574 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 { BVHComputeData, intersectionResultStruct, intersectsTriangle } from '../lib/BVHComputeData.js'; 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 { wgslTagCode, wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; +import { pcgRand } from './random.wgsl.js'; const _colorVec = new Vector4(); const transformStruct = new StructTypeNode( { @@ -35,11 +38,168 @@ 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 = wgslTagCode/* wgsl */` + var bvh_rayScalar = 1.0; + var bvh_material: ${ structs.material }; + var bvh_baseOpacity = 1.0; + `; + + 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 }, result: ptr ) -> u32 { + + // early-out if our object is completely transparent + if ( bvh_baseOpacity == 0.0 ) { + + return 0u; + + } + + 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 = 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 ); + + let dist = max( t0, 0.0 ); + if ( t1 < dist ) { + + return 0u; + + } else if ( result.didHit && dist * bvh_rayScalar >= result.dist ) { + + return 0u; + + } else { + + return 1u; + + } + + } + + `, + intersectRangeFn: wgslTagFn/* wgsl */` + ${ [ scratchRayScalar ] } + + fn intersectRange( ray: ${ rayStruct }, offset: u32, count: u32, result: ptr ) -> bool { + + var didHit = false; + 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 *= bvh_rayScalar; + if ( triResult.didHit && ( ! result.didHit || triResult.dist < result.dist ) ) { + + let material = bvh_material; + if ( material.transparent != 0 ) { + + let opacity = bvh_baseOpacity; + // TODO: sample albedo + alphaMap alpha + if ( opacity < ${ pcgRand }() ) { + + continue; + + } + + } + + result.didHit = true; + result.dist = triResult.dist; + result.normal = triResult.normal; + result.side = triResult.side; + result.barycoord = triResult.barycoord; + result.indices = vec4u( i0, i1, i2, ti ); + + didHit = true; + + } + + } + + return didHit; + + } + `, + transformShapeFn: wgslTagFn/* wgsl */` + ${ [ scratchRayScalar ] } + + fn transformRay( ray: ptr, objectIndex: u32 ) -> void { + + let toLocal = ${ storage.transforms }[ objectIndex ].inverseMatrixWorld; + ray.origin = ( toLocal * vec4f( ray.origin, 1.0 ) ).xyz; + ray.direction = ( toLocal * vec4f( ray.direction, 0.0 ) ).xyz; + + let len = length( ray.direction ); + ray.direction /= len; + bvh_rayScalar = 1.0 / len; + + let object = ${ storage.transforms }[ objectIndex ]; + bvh_material = ${ storage.materials }[ object.materialIndex ]; + if ( bvh_material.transparent == 1 ) { + + bvh_baseOpacity = bvh_material.opacity * object.color.a; + + } else { + + bvh_baseOpacity = 1.0; + + } + + } + `, + transformResultFn: wgslTagFn/* wgsl */` + fn transformResult( hit: ptr, objectIndex: u32 ) -> void { + + let toLocal = ${ storage.transforms }[ objectIndex ].inverseMatrixWorld; + hit.normal = normalize( ( transpose( toLocal ) * vec4f( hit.normal, 0.0 ) ).xyz ); + hit.objectIndex = objectIndex; + + } + `, + } ); + + } + update() { super.update();