three-gpu-pathtracer 0.0.27 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/build/index.module.js +5 -3
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +4 -2
- package/build/index.umd.cjs.map +1 -1
- package/package.json +7 -7
- package/src/core/PathTracingSceneGenerator.js +1 -1
- package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
- package/src/webgpu/API.md +167 -19
- package/src/webgpu/AtlasTexture.js +2 -3
- package/src/webgpu/EquirectHdrInfoNode.js +89 -48
- package/src/webgpu/LightsInfoNode.js +1 -1
- package/src/webgpu/MegaKernelPathTracer.js +10 -3
- package/src/webgpu/PathTracerBackend.js +172 -21
- package/src/webgpu/WaveFrontPathTracer.js +156 -71
- package/src/webgpu/WebGPUPathTracer.js +628 -48
- package/src/webgpu/compute/ComputeKernel.js +8 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +187 -36
- package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
- package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
- package/src/webgpu/compute/ZeroOutKernel.js +3 -1
- package/src/webgpu/compute/targets.js +55 -0
- package/src/webgpu/compute/wavefront/LogicKernel.js +67 -20
- package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
- package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
- package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
- package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
- package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
- package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
- package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
- package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -25
- package/src/webgpu/compute/wavefront/structs.js +63 -37
- package/src/webgpu/constants.js +2 -2
- package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
- package/src/webgpu/index.d.ts +41 -6
- package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
- package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +35 -13
- package/src/webgpu/nodes/aux.wgsl.js +61 -0
- package/src/webgpu/nodes/eon.wgsl.js +1 -1
- package/src/webgpu/nodes/material.wgsl.js +2 -1
- package/src/webgpu/nodes/medium.wgsl.js +165 -0
- package/src/webgpu/nodes/random.wgsl.js +2 -0
- package/src/webgpu/nodes/sampling.wgsl.js +44 -3
- package/src/webgpu/nodes/structs.wgsl.js +7 -1
- package/src/webgpu/upscale/FSRUpscaler.js +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { StorageBufferAttribute } from 'three/webgpu';
|
|
2
|
+
import { ComputeKernel } from '../ComputeKernel.js';
|
|
3
|
+
import { storage, uniform, globalId } from 'three/tsl';
|
|
4
|
+
import { contextProxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
|
+
import { rngInit, rngNextBounce, rand2, RNG_INDEX_MEDIUM_DISTANCE, RNG_INDEX_MEDIUM_DIRECTION } from '../../nodes/random.wgsl.js';
|
|
6
|
+
import { LIGHT_FAR_DISTANCE } from '../../nodes/lights.wgsl.js';
|
|
7
|
+
import { sampleHenyeyGreensteinFn } from '../../nodes/sampling.wgsl.js';
|
|
8
|
+
import {
|
|
9
|
+
loadMediumFn,
|
|
10
|
+
mediumChannelPdfFn, sampleMediumDistanceFn, mediumDistancePdfFn,
|
|
11
|
+
MEDIUM_THROUGHPUT_EPSILON,
|
|
12
|
+
} from '../../nodes/medium.wgsl.js';
|
|
13
|
+
import { rayDataStruct, intersectionResultStruct, MEDIUM_QUEUE_LENGTH, MEDIUM_QUEUE_HEADER } from './structs.js';
|
|
14
|
+
|
|
15
|
+
// Random walks through scattering media, as in Cycles' subsurface kernel but capped per pass. A walk that
|
|
16
|
+
// reaches a surface hands its hit to LogicKernel like a traced ray, one that doesn't resumes next pass. The
|
|
17
|
+
// walk's weight goes into the staged scatter color.
|
|
18
|
+
export class MediumKernel extends ComputeKernel {
|
|
19
|
+
|
|
20
|
+
constructor( ) {
|
|
21
|
+
|
|
22
|
+
const params = {
|
|
23
|
+
maxSubsurfaceBounces: uniform( 256, 'uint' ),
|
|
24
|
+
stepsPerPass: uniform( 16, 'uint' ),
|
|
25
|
+
rayCount: uniform( 0, 'uint' ),
|
|
26
|
+
|
|
27
|
+
rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
|
|
28
|
+
mediumQueue: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
|
|
29
|
+
rayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
|
|
30
|
+
|
|
31
|
+
globalId: globalId,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
|
|
35
|
+
const raycastFirstHitFn = contextProxy( 'bvhData.fns.raycastFirstHit' );
|
|
36
|
+
|
|
37
|
+
const fn = wgslTagFn/* wgsl */`
|
|
38
|
+
|
|
39
|
+
fn compute( maxSubsurfaceBounces: u32, stepsPerPass: u32, rayCount: u32, globalId: vec3u ) -> void {
|
|
40
|
+
|
|
41
|
+
let rayDataStorage = &${ params.rayDataStorage };
|
|
42
|
+
let mediumQueue = &${ params.mediumQueue };
|
|
43
|
+
let rayIntersectionsStorage = &${ params.rayIntersectionsStorage };
|
|
44
|
+
|
|
45
|
+
if ( globalId.x >= mediumQueue[ ${ MEDIUM_QUEUE_LENGTH }u ] ) {
|
|
46
|
+
|
|
47
|
+
return;
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let index = mediumQueue[ ${ MEDIUM_QUEUE_HEADER }u + globalId.x ];
|
|
52
|
+
// whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
|
|
53
|
+
let input = ${ params.rayDataStorage }[ index ];
|
|
54
|
+
let medium = ${ loadMediumFn }( input.medium );
|
|
55
|
+
|
|
56
|
+
let indexUV = vec2u( input.pixelIndex >> 16, input.pixelIndex & 0xFFFF );
|
|
57
|
+
${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth + input.subsurfaceBounces );
|
|
58
|
+
|
|
59
|
+
var ray = Ray( input.origin, input.direction, 0.0 );
|
|
60
|
+
var weight = input.scatterColor;
|
|
61
|
+
var subsurfaceBounces = input.subsurfaceBounces;
|
|
62
|
+
var hasScattered = false;
|
|
63
|
+
var isFinished = false;
|
|
64
|
+
var isTerminated = false;
|
|
65
|
+
|
|
66
|
+
for ( var i = 0u; i < stepsPerPass; i ++ ) {
|
|
67
|
+
|
|
68
|
+
// only trace up to the sampled scatter event
|
|
69
|
+
let channelPdf = ${ mediumChannelPdfFn }( input.throughputColor * weight, medium.albedo );
|
|
70
|
+
let scatterDist = ${ sampleMediumDistanceFn }( medium, channelPdf, ${ rand2 }( ${ RNG_INDEX_MEDIUM_DISTANCE } ) );
|
|
71
|
+
ray.maxDist = scatterDist;
|
|
72
|
+
|
|
73
|
+
var hitResult: ${ raycastOutput };
|
|
74
|
+
let didHit = ${ raycastFirstHitFn }( ray, &hitResult );
|
|
75
|
+
let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
|
|
76
|
+
|
|
77
|
+
let scattered = scatterDist < surfaceDist;
|
|
78
|
+
let segmentDist = select( surfaceDist, scatterDist, scattered );
|
|
79
|
+
let transmittance = exp( - medium.extinction * segmentDist );
|
|
80
|
+
let distancePdf = dot( channelPdf, ${ mediumDistancePdfFn }( medium, segmentDist, scattered ) );
|
|
81
|
+
if ( distancePdf <= 0.0 ) {
|
|
82
|
+
|
|
83
|
+
isTerminated = true;
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ( ! scattered ) {
|
|
89
|
+
|
|
90
|
+
// reaching the surface, weighted by the chance of not scattering before it
|
|
91
|
+
weight *= transmittance / distancePdf;
|
|
92
|
+
|
|
93
|
+
// The hit fills the ray results from the back, by the walk's medium queue position, while the
|
|
94
|
+
// traced rays fill them from the front. A path slot walking a medium queued no ray this round,
|
|
95
|
+
// so the two ranges never exceed the slot count or overlap
|
|
96
|
+
let resultIndex = rayCount - 1u - globalId.x;
|
|
97
|
+
rayIntersectionsStorage[ resultIndex ].barycoord = hitResult.barycoord;
|
|
98
|
+
// a walk that leaves without hitting anything is a miss, as in the trace kernel
|
|
99
|
+
rayIntersectionsStorage[ resultIndex ].objectIndex = select( - 1, i32( hitResult.objectIndex ), didHit );
|
|
100
|
+
rayIntersectionsStorage[ resultIndex ].position = ray.origin + ray.direction * hitResult.dist;
|
|
101
|
+
rayIntersectionsStorage[ resultIndex ].dist = hitResult.dist;
|
|
102
|
+
rayIntersectionsStorage[ resultIndex ].normal = hitResult.normal.xyz;
|
|
103
|
+
rayIntersectionsStorage[ resultIndex ].side = hitResult.side;
|
|
104
|
+
rayIntersectionsStorage[ resultIndex ].indices = hitResult.indices.xyz;
|
|
105
|
+
rayDataStorage[ index ].rayIntersectionIndex = i32( resultIndex );
|
|
106
|
+
|
|
107
|
+
isFinished = true;
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
weight *= medium.albedo * medium.extinction * transmittance / distancePdf;
|
|
113
|
+
ray.origin += ray.direction * scatterDist;
|
|
114
|
+
ray.direction = ${ sampleHenyeyGreensteinFn }( ray.direction, medium.anisotropy, ${ rand2 }( ${ RNG_INDEX_MEDIUM_DIRECTION } ) );
|
|
115
|
+
hasScattered = true;
|
|
116
|
+
|
|
117
|
+
// capped separately from bounces, and ends once the throughput is negligible
|
|
118
|
+
subsurfaceBounces ++;
|
|
119
|
+
let throughput = input.throughputColor * weight / input.scatterPdf;
|
|
120
|
+
if ( subsurfaceBounces >= maxSubsurfaceBounces || max( max( throughput.r, throughput.g ), throughput.b ) < ${ MEDIUM_THROUGHPUT_EPSILON } ) {
|
|
121
|
+
|
|
122
|
+
isTerminated = true;
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
${ rngNextBounce }();
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// a zeroed pdf terminates the path in LogicKernel
|
|
132
|
+
rayDataStorage[ index ].scatterColor = weight;
|
|
133
|
+
rayDataStorage[ index ].scatterPdf = select( input.scatterPdf, 0.0, isTerminated );
|
|
134
|
+
rayDataStorage[ index ].origin = ray.origin;
|
|
135
|
+
rayDataStorage[ index ].direction = ray.direction;
|
|
136
|
+
rayDataStorage[ index ].subsurfaceBounces = subsurfaceBounces;
|
|
137
|
+
rayDataStorage[ index ].walkPending = select( 1u, 0u, isFinished || isTerminated );
|
|
138
|
+
if ( hasScattered ) {
|
|
139
|
+
|
|
140
|
+
rayDataStorage[ index ].mediumScattered = 1u;
|
|
141
|
+
rayDataStorage[ index ].isFullyTransmissive = 0u;
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
super( fn( params ) );
|
|
149
|
+
|
|
150
|
+
this.defineUniformAccessors( params );
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
@@ -3,7 +3,7 @@ import { StorageBufferAttribute } from 'three/webgpu';
|
|
|
3
3
|
import { uniform, storage, globalId } from 'three/tsl';
|
|
4
4
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
5
5
|
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
6
|
-
import { rayDataStruct,
|
|
6
|
+
import { rayDataStruct, PIXEL_QUEUE_CURRENT, PIXEL_QUEUE_ELEMENT_COUNT, PIXEL_QUEUE_HEADER } from './structs.js';
|
|
7
7
|
|
|
8
8
|
// Runs once per reset: assigns each of the first rayData-pool-count pixels to a path slot and parks
|
|
9
9
|
// the overflow pixel indices in the pixel queue. Slots are initialized so LogicKernel skips them and
|
|
@@ -15,7 +15,7 @@ export class PopulatePixelIndicesKernel extends ComputeKernel {
|
|
|
15
15
|
|
|
16
16
|
const params = {
|
|
17
17
|
rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
|
|
18
|
-
pixelQueue: storage( new StorageBufferAttribute( 1, 1 ),
|
|
18
|
+
pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
|
|
19
19
|
targetDimensions: uniform( new Vector2() ),
|
|
20
20
|
frameBudget: uniform( 0, 'uint' ),
|
|
21
21
|
globalId: globalId,
|
|
@@ -37,8 +37,8 @@ export class PopulatePixelIndicesKernel extends ComputeKernel {
|
|
|
37
37
|
let pixelCount = targetDimensions.x * targetDimensions.y;
|
|
38
38
|
if ( globalId.x == 0u && globalId.y == 0u ) {
|
|
39
39
|
|
|
40
|
-
pixelQueue
|
|
41
|
-
pixelQueue
|
|
40
|
+
pixelQueue[ ${ PIXEL_QUEUE_CURRENT }u ] = 0u;
|
|
41
|
+
pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ] = select( 0u, pixelCount - rayCount, pixelCount >= rayCount );
|
|
42
42
|
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -56,7 +56,7 @@ export class PopulatePixelIndicesKernel extends ComputeKernel {
|
|
|
56
56
|
|
|
57
57
|
} else {
|
|
58
58
|
|
|
59
|
-
pixelQueue
|
|
59
|
+
pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + pixelIndex - rayCount ] = pixelHash;
|
|
60
60
|
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -2,26 +2,26 @@ import { IndirectStorageBufferAttribute, StorageBufferAttribute } from 'three/we
|
|
|
2
2
|
import { storage } from 'three/tsl';
|
|
3
3
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
4
4
|
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
|
-
import {
|
|
5
|
+
import { RAY_QUEUE_LENGTH } from './structs.js';
|
|
6
6
|
|
|
7
7
|
// Converts a queue's atomic length counter into indirect dispatch arguments so the trace kernels can
|
|
8
8
|
// be dispatched with exactly as many threads as there are queued rays.
|
|
9
9
|
export class QueueLengthToDispatchKernel extends ComputeKernel {
|
|
10
10
|
|
|
11
|
-
constructor(
|
|
11
|
+
constructor( counterIndex = RAY_QUEUE_LENGTH, workgroupSize = 64 ) {
|
|
12
12
|
|
|
13
13
|
const params = {
|
|
14
|
-
|
|
14
|
+
queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ).toAtomic(),
|
|
15
15
|
outputDispatch: storage( new IndirectStorageBufferAttribute( 3, 1 ), 'u32' ).setName( 'outputDispatch' ),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const fn = wgslTagFn/* wgsl */`
|
|
19
19
|
fn compute() -> void {
|
|
20
20
|
|
|
21
|
-
let
|
|
21
|
+
let queueCounters = &${ params.queueCounters };
|
|
22
22
|
let outputDispatch = &${ params.outputDispatch };
|
|
23
23
|
|
|
24
|
-
let queueLength = atomicLoad( &
|
|
24
|
+
let queueLength = atomicLoad( &queueCounters[ ${ counterIndex }u ] );
|
|
25
25
|
outputDispatch[ 0 ] = ( queueLength + ${ workgroupSize - 1 }u ) / ${ workgroupSize }u;
|
|
26
26
|
outputDispatch[ 1 ] = 1u;
|
|
27
27
|
outputDispatch[ 2 ] = 1u;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { StorageBufferAttribute } from 'three/webgpu';
|
|
2
|
+
import { uniform, storage, globalId } from 'three/tsl';
|
|
3
|
+
import { ComputeKernel } from '../ComputeKernel.js';
|
|
4
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
|
+
import { rayDataStruct, MEDIUM_QUEUE_LENGTH, MEDIUM_QUEUE_HEADER } from './structs.js';
|
|
6
|
+
|
|
7
|
+
// Gathers the path slots walking a medium for MediumKernel's dispatch. Separate from MaterialKernel, which
|
|
8
|
+
// is at its storage buffer limit.
|
|
9
|
+
export class QueueMediumWalksKernel extends ComputeKernel {
|
|
10
|
+
|
|
11
|
+
constructor( ) {
|
|
12
|
+
|
|
13
|
+
const params = {
|
|
14
|
+
rayCount: uniform( 0, 'uint' ),
|
|
15
|
+
|
|
16
|
+
rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ).toReadOnly(),
|
|
17
|
+
mediumQueue: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ).toAtomic(),
|
|
18
|
+
|
|
19
|
+
globalId: globalId,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const fn = wgslTagFn/* wgsl */`
|
|
23
|
+
|
|
24
|
+
fn compute( rayCount: u32, globalId: vec3u ) -> void {
|
|
25
|
+
|
|
26
|
+
let rayDataStorage = &${ params.rayDataStorage };
|
|
27
|
+
let mediumQueue = &${ params.mediumQueue };
|
|
28
|
+
|
|
29
|
+
let index = globalId.x;
|
|
30
|
+
if ( index >= rayCount ) {
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if ( rayDataStorage[ index ].walkPending != 0u && rayDataStorage[ index ].objectIndex >= 0 ) {
|
|
37
|
+
|
|
38
|
+
let queueIndex = atomicAdd( &mediumQueue[ ${ MEDIUM_QUEUE_LENGTH }u ], 1u );
|
|
39
|
+
atomicStore( &mediumQueue[ ${ MEDIUM_QUEUE_HEADER }u + queueIndex ], index );
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
super( fn( params ) );
|
|
47
|
+
|
|
48
|
+
this.defineUniformAccessors( params );
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
@@ -2,7 +2,7 @@ import { StorageBufferAttribute } from 'three/webgpu';
|
|
|
2
2
|
import { uniform, storage, globalId } from 'three/tsl';
|
|
3
3
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
4
4
|
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
|
-
import { rayDataStruct,
|
|
5
|
+
import { rayDataStruct, PIXEL_QUEUE_ELEMENT_COUNT, PIXEL_QUEUE_HEADER } from './structs.js';
|
|
6
6
|
|
|
7
7
|
// Moves pixels between a range of path slots and the queue when the pool is resized. Slots added
|
|
8
8
|
// by a grow take a pixel from the queue tail and start idle, as MaterialKernel expects. Slots
|
|
@@ -14,7 +14,7 @@ export class ResetSlotsKernel extends ComputeKernel {
|
|
|
14
14
|
|
|
15
15
|
const params = {
|
|
16
16
|
rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
|
|
17
|
-
pixelQueue: storage( new StorageBufferAttribute( 1, 1 ),
|
|
17
|
+
pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ).toAtomic(),
|
|
18
18
|
start: uniform( 0, 'uint' ),
|
|
19
19
|
end: uniform( 0, 'uint' ),
|
|
20
20
|
addingSlots: uniform( 0, 'uint' ),
|
|
@@ -37,14 +37,14 @@ export class ResetSlotsKernel extends ComputeKernel {
|
|
|
37
37
|
if ( addingSlots == 0u ) {
|
|
38
38
|
|
|
39
39
|
// slots being removed hand their pixel back before the pool is discarded
|
|
40
|
-
let queueIndex = atomicAdd( &pixelQueue
|
|
41
|
-
atomicStore( &pixelQueue
|
|
40
|
+
let queueIndex = atomicAdd( &pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ], 1u );
|
|
41
|
+
atomicStore( &pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + queueIndex ], rayDataStorage[ index ].pixelIndex );
|
|
42
42
|
|
|
43
43
|
} else {
|
|
44
44
|
|
|
45
45
|
// the pool never exceeds the pixel count, so the queue holds enough for every new slot
|
|
46
|
-
let previousCount = atomicSub( &pixelQueue
|
|
47
|
-
rayDataStorage[ index ].pixelIndex = atomicLoad( &pixelQueue
|
|
46
|
+
let previousCount = atomicSub( &pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ], 1u );
|
|
47
|
+
rayDataStorage[ index ].pixelIndex = atomicLoad( &pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + previousCount - 1u ] );
|
|
48
48
|
rayDataStorage[ index ].resultColor = vec4f( 0.0 );
|
|
49
49
|
rayDataStorage[ index ].throughputColor = vec3f( 0.0 );
|
|
50
50
|
rayDataStorage[ index ].objectIndex = - 1;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { StorageBufferAttribute } from 'three/webgpu';
|
|
2
2
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
3
3
|
import { storage, globalId } from 'three/tsl';
|
|
4
|
-
import {
|
|
4
|
+
import { contextProxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
5
|
import { rngInit } from '../../nodes/random.wgsl.js';
|
|
6
|
-
import {
|
|
6
|
+
import { traceQueuedRayStruct, intersectionResultStruct, RAY_QUEUE_LENGTH } from './structs.js';
|
|
7
7
|
|
|
8
8
|
// Pure BVH traversal over the queued bounce rays: one thread per queued ray, writing a compact
|
|
9
9
|
// intersection result at the ray's queue index for LogicKernel to consume next frame.
|
|
@@ -12,34 +12,36 @@ export class TraceRayKernel extends ComputeKernel {
|
|
|
12
12
|
constructor( ) {
|
|
13
13
|
|
|
14
14
|
const params = {
|
|
15
|
-
bvhData: { value: null },
|
|
16
15
|
|
|
17
|
-
rayQueue: storage( new StorageBufferAttribute( 1, 1 ),
|
|
16
|
+
rayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
|
|
17
|
+
queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
|
|
18
18
|
rayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
|
|
19
19
|
|
|
20
20
|
globalId: globalId,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const raycastOutput =
|
|
24
|
-
const raycastFirstHitFn =
|
|
23
|
+
const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
|
|
24
|
+
const raycastFirstHitFn = contextProxy( 'bvhData.fns.raycastFirstHit' );
|
|
25
25
|
|
|
26
26
|
const fn = wgslTagFn /* wgsl */`
|
|
27
27
|
|
|
28
28
|
fn compute( globalId: vec3u ) -> void {
|
|
29
29
|
|
|
30
30
|
let rayQueue = &${ params.rayQueue };
|
|
31
|
+
let queueCounters = &${ params.queueCounters };
|
|
31
32
|
let rayIntersectionsStorage = &${ params.rayIntersectionsStorage };
|
|
32
33
|
|
|
33
34
|
let index = globalId.x;
|
|
34
|
-
if ( index >=
|
|
35
|
+
if ( index >= queueCounters[ ${ RAY_QUEUE_LENGTH }u ] ) {
|
|
35
36
|
|
|
36
37
|
return;
|
|
37
38
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
|
|
42
|
+
let queuedRay = ${ params.rayQueue }[ index ];
|
|
41
43
|
let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
|
|
42
|
-
${ rngInit }( indexUV, queuedRay.seed, queuedRay.
|
|
44
|
+
${ rngInit }( indexUV, queuedRay.seed, queuedRay.rngIndex );
|
|
43
45
|
|
|
44
46
|
let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
|
|
45
47
|
var hitResult: ${ raycastOutput };
|
|
@@ -1,61 +1,54 @@
|
|
|
1
1
|
import { StorageBufferAttribute } from 'three/webgpu';
|
|
2
2
|
import { ComputeKernel } from '../ComputeKernel.js';
|
|
3
3
|
import { storage, globalId } from 'three/tsl';
|
|
4
|
-
import {
|
|
4
|
+
import { contextProxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
5
|
import { rngInit } from '../../nodes/random.wgsl.js';
|
|
6
|
-
import {
|
|
6
|
+
import { traceQueuedRayStruct, shadowTraceResultStruct, SHADOW_RAY_QUEUE_LENGTH } from './structs.js';
|
|
7
7
|
|
|
8
|
-
// Pure BVH traversal over the queued shadow rays. Uses
|
|
9
|
-
//
|
|
10
|
-
// hit distance against the light distance.
|
|
8
|
+
// Pure BVH traversal over the queued shadow rays. Uses an any-hit traversal without face culling that
|
|
9
|
+
// passes partially transparent surfaces, accumulating the light they block.
|
|
11
10
|
export class TraceShadowRayKernel extends ComputeKernel {
|
|
12
11
|
|
|
13
12
|
constructor( ) {
|
|
14
13
|
|
|
15
14
|
const params = {
|
|
16
|
-
bvhData: { value: null },
|
|
17
15
|
|
|
18
|
-
shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ),
|
|
19
|
-
|
|
16
|
+
shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
|
|
17
|
+
queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
|
|
18
|
+
shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), shadowTraceResultStruct ),
|
|
20
19
|
|
|
21
20
|
globalId: globalId,
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
const raycastOutput =
|
|
25
|
-
const raycastShadowFn =
|
|
23
|
+
const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
|
|
24
|
+
const raycastShadowFn = contextProxy( 'bvhData.fns.raycastShadow' );
|
|
26
25
|
|
|
27
26
|
const fn = wgslTagFn /* wgsl */`
|
|
28
27
|
|
|
29
28
|
fn compute( globalId: vec3u ) -> void {
|
|
30
29
|
|
|
31
30
|
let shadowRayQueue = &${ params.shadowRayQueue };
|
|
31
|
+
let queueCounters = &${ params.queueCounters };
|
|
32
32
|
let shadowRayIntersectionsStorage = &${ params.shadowRayIntersectionsStorage };
|
|
33
33
|
|
|
34
34
|
let index = globalId.x;
|
|
35
|
-
if ( index >=
|
|
35
|
+
if ( index >= queueCounters[ ${ SHADOW_RAY_QUEUE_LENGTH }u ] ) {
|
|
36
36
|
|
|
37
37
|
return;
|
|
38
38
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
|
|
42
|
+
let queuedRay = ${ params.shadowRayQueue }[ index ];
|
|
42
43
|
let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
|
|
43
|
-
${ rngInit }( indexUV, queuedRay.seed, queuedRay.
|
|
44
|
+
${ rngInit }( indexUV, queuedRay.seed, queuedRay.rngIndex );
|
|
44
45
|
|
|
45
46
|
let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
|
|
46
47
|
var hitResult: ${ raycastOutput };
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} else {
|
|
53
|
-
|
|
54
|
-
shadowRayIntersectionsStorage[ index ].objectIndex = - 1;
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
shadowRayIntersectionsStorage[ index ].forwardBlocked = u32( hitResult.forwardBlocked );
|
|
48
|
+
let occluded = ${ raycastShadowFn }( ray, &hitResult );
|
|
49
|
+
shadowRayIntersectionsStorage[ index ].objectIndex = select( - 1, i32( hitResult.objectIndex ), occluded );
|
|
50
|
+
shadowRayIntersectionsStorage[ index ].skippedOcclusion = hitResult.skippedOcclusion;
|
|
51
|
+
shadowRayIntersectionsStorage[ index ].occlusion = hitResult.occlusion;
|
|
59
52
|
|
|
60
53
|
}
|
|
61
54
|
`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StructTypeNode } from 'three/webgpu';
|
|
2
|
+
import { mediumStateStruct } from '../../nodes/medium.wgsl.js';
|
|
2
3
|
|
|
3
4
|
// StructTypeNode that force-emits dependency structs. TSL can't discover a struct referenced only via
|
|
4
5
|
// a type string, so we build each dependency first, which registers its definition ahead of this struct.
|
|
@@ -28,7 +29,7 @@ class DependentStructTypeNode extends StructTypeNode {
|
|
|
28
29
|
|
|
29
30
|
// Persistent per-path-slot state. One slot per in-flight path, holding everything needed to resolve
|
|
30
31
|
// the previous frame's trace results and stage the next bounce.
|
|
31
|
-
|
|
32
|
+
const rayDataMembers = {
|
|
32
33
|
|
|
33
34
|
throughputColor: 'vec3f',
|
|
34
35
|
currentBounce: 'uint',
|
|
@@ -76,8 +77,10 @@ export const rayDataStruct = new StructTypeNode( {
|
|
|
76
77
|
// the traced segment length, for the transmission attenuation applied by MaterialKernel
|
|
77
78
|
dist: 'float',
|
|
78
79
|
|
|
79
|
-
// alpha test pass throughs, counted separately from the bounce count
|
|
80
|
+
// alpha test pass throughs, counted separately from the bounce count, and how far along the ray they
|
|
81
|
+
// moved the start of its traversal. The ray keeps the origin it scattered from
|
|
80
82
|
alphaDepth: 'uint',
|
|
83
|
+
rayStart: 'float',
|
|
81
84
|
|
|
82
85
|
// the camera segment's maximum trace distance, carried across alpha pass throughs
|
|
83
86
|
maxDist: 'float',
|
|
@@ -86,7 +89,30 @@ export const rayDataStruct = new StructTypeNode( {
|
|
|
86
89
|
// first dispersive interaction
|
|
87
90
|
dispersionWavelength: 'float',
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
// the first scatter's albedo and normal, 8 bits per channel, for the denoiser's auxiliary buffers
|
|
93
|
+
auxAlbedo: 'uint',
|
|
94
|
+
auxNormal: 'uint',
|
|
95
|
+
|
|
96
|
+
// scatter events taken inside media, counted separately from the bounce count
|
|
97
|
+
subsurfaceBounces: 'uint',
|
|
98
|
+
|
|
99
|
+
// whether MediumKernel walks the path's next segment, cleared once the walk reaches a surface or ends
|
|
100
|
+
walkPending: 'uint',
|
|
101
|
+
|
|
102
|
+
// whether the staged segment starts at a medium scatter event, where no light is sampled
|
|
103
|
+
mediumScattered: 'uint',
|
|
104
|
+
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// The path's medium, nested at the end to share the ray data buffer under the storage buffer limit. Sized
|
|
108
|
+
// separately since nested structs aren't.
|
|
109
|
+
export const rayDataStruct = new DependentStructTypeNode( {
|
|
110
|
+
...rayDataMembers,
|
|
111
|
+
medium: mediumStateStruct.name,
|
|
112
|
+
}, 'RayData', [ mediumStateStruct ] );
|
|
113
|
+
|
|
114
|
+
const rayDataLength = new StructTypeNode( rayDataMembers ).getLength();
|
|
115
|
+
rayDataStruct.getLength = () => rayDataLength + mediumStateStruct.getLength();
|
|
90
116
|
|
|
91
117
|
// A ray queued for BVH traversal by the trace kernels. A "maxDist" of zero traces unbounded.
|
|
92
118
|
export const traceQueuedRayStruct = new StructTypeNode( {
|
|
@@ -95,12 +121,14 @@ export const traceQueuedRayStruct = new StructTypeNode( {
|
|
|
95
121
|
pixelIndex: 'uint',
|
|
96
122
|
|
|
97
123
|
direction: 'vec3f',
|
|
98
|
-
|
|
124
|
+
|
|
125
|
+
// the traversal's random sequence index: bounces, alpha pass throughs and subsurface bounces, as in the megakernel
|
|
126
|
+
rngIndex: 'uint',
|
|
99
127
|
|
|
100
128
|
seed: 'uint',
|
|
101
|
-
alphaDepth: 'uint',
|
|
102
129
|
maxDist: 'float',
|
|
103
130
|
_alignment0: 'uint',
|
|
131
|
+
_alignment1: 'uint',
|
|
104
132
|
|
|
105
133
|
}, 'TraceQueuedRay' );
|
|
106
134
|
|
|
@@ -119,39 +147,37 @@ export const intersectionResultStruct = new StructTypeNode( {
|
|
|
119
147
|
|
|
120
148
|
indices: 'vec3u',
|
|
121
149
|
|
|
122
|
-
// shadow rays only: a face a bsdf ray would hit was passed
|
|
123
|
-
forwardBlocked: 'uint',
|
|
124
|
-
|
|
125
150
|
}, 'TraceResult' );
|
|
126
151
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}, '
|
|
140
|
-
|
|
152
|
+
// Shadow ray trace result, written by TraceShadowRayKernel at the ray's queue index and consumed by
|
|
153
|
+
// LogicKernel the following frame. objectIndex >= 0 encodes an occluded light.
|
|
154
|
+
export const shadowTraceResultStruct = new StructTypeNode( {
|
|
155
|
+
|
|
156
|
+
objectIndex: 'int',
|
|
157
|
+
|
|
158
|
+
// the share of bsdf rays the skipped faces would stop, whose light weight moves to the light sample
|
|
159
|
+
skippedOcclusion: 'float',
|
|
160
|
+
|
|
161
|
+
// the fraction of light blocked by the partially transparent surfaces passed
|
|
162
|
+
occlusion: 'float',
|
|
163
|
+
|
|
164
|
+
}, 'ShadowTraceResult' );
|
|
165
|
+
|
|
166
|
+
// The ray and shadow ray queues are plain arrays of queued rays, with their append-only lengths in a
|
|
167
|
+
// separate counters buffer: storage buffers are always bound as arrays, which can't hold a struct
|
|
168
|
+
// ending in a runtime-sized array.
|
|
169
|
+
export const RAY_QUEUE_LENGTH = 0;
|
|
170
|
+
export const SHADOW_RAY_QUEUE_LENGTH = 1;
|
|
171
|
+
export const QUEUE_COUNTER_COUNT = 2;
|
|
172
|
+
|
|
173
|
+
// The path slots walking through a medium this pass, for MediumKernel. Its length leads the same u32
|
|
174
|
+
// array, ahead of the elements, as MediumKernel is at its storage buffer limit.
|
|
175
|
+
export const MEDIUM_QUEUE_LENGTH = 0;
|
|
176
|
+
export const MEDIUM_QUEUE_HEADER = 1;
|
|
141
177
|
|
|
142
178
|
// Round-robin queue of pixel indices waiting for a free path slot when the output resolution exceeds
|
|
143
|
-
// the ray data pool
|
|
144
|
-
//
|
|
145
|
-
export const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
elements: 'array<atomic<u32>>',
|
|
149
|
-
}, 'PixelQueue' );
|
|
150
|
-
pixelQueueStruct.getLength = () => 2;
|
|
151
|
-
|
|
152
|
-
export const pixelQueueNonAtomicStruct = new DependentStructTypeNode( {
|
|
153
|
-
current: 'uint',
|
|
154
|
-
elementCount: 'uint',
|
|
155
|
-
elements: 'array<u32>',
|
|
156
|
-
}, 'PixelQueue' );
|
|
157
|
-
pixelQueueNonAtomicStruct.getLength = () => 2;
|
|
179
|
+
// the ray data pool, sized to the pixel count so retiring slots can always return their pixel. Its
|
|
180
|
+
// counters lead the same u32 array, ahead of the elements.
|
|
181
|
+
export const PIXEL_QUEUE_CURRENT = 0;
|
|
182
|
+
export const PIXEL_QUEUE_ELEMENT_COUNT = 1;
|
|
183
|
+
export const PIXEL_QUEUE_HEADER = 2;
|
package/src/webgpu/constants.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as bluedither from './nodes/rand/bluedither.wgsl.js';
|
|
|
13
13
|
export const RANDOM_PCG = pcg;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`.
|
|
16
|
+
* Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`. The default.
|
|
17
17
|
* @section Random Strategies
|
|
18
18
|
* @type {Object}
|
|
19
19
|
*/
|
|
@@ -21,7 +21,7 @@ export const RANDOM_SOBOL = sobol;
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* The same Sobol sequence for every pixel, offset by a blue noise dither so early frames read as
|
|
24
|
-
* smooth grain rather than clumped noise.
|
|
24
|
+
* smooth grain rather than clumped noise.
|
|
25
25
|
* @section Random Strategies
|
|
26
26
|
* @type {Object}
|
|
27
27
|
*/
|