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
|
@@ -46,6 +46,14 @@ export class ComputeKernel {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const node = parameters[ key ];
|
|
49
|
+
|
|
50
|
+
// name storage buffers so equivalent kernels generate identical WGSL and hit the pipeline cache
|
|
51
|
+
if ( node.isStorageBufferNode && ! node.name ) {
|
|
52
|
+
|
|
53
|
+
node.setName( `kernel_${ key }` );
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
if ( 'value' in node ) {
|
|
50
58
|
|
|
51
59
|
Object.defineProperty( this, key, {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { DataTexture, Vector2
|
|
1
|
+
import { DataTexture, Vector2 } from 'three/webgpu';
|
|
2
2
|
import { ComputeKernel } from './ComputeKernel.js';
|
|
3
|
+
import { createAuxTarget, createOutputTarget, createSampleCountTarget } from './targets.js';
|
|
3
4
|
import { texture, sampler, uniform, globalId, textureStore } from 'three/tsl';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { blendAuxFn, isAuxSurfaceFn, AUX_LAYERS } from '../nodes/aux.wgsl.js';
|
|
6
|
+
import { rngInit, rngNextBounce, rand1, rand2, rand3, RNG_INDEX_RAY_JITTER, RNG_INDEX_BACKGROUND_SAMPLE, RNG_INDEX_DIRECT_LIGHT_SAMPLE, RNG_INDEX_RUSSIAN_ROULETTE, RNG_INDEX_DISPERSION_WAVELENGTH, RNG_INDEX_ALPHA_TEST, RNG_INDEX_MEDIUM_DISTANCE, RNG_INDEX_MEDIUM_DIRECTION } from '../nodes/random.wgsl.js';
|
|
7
|
+
import { misHeuristicFn, weightedAlphaBlendFn, sampleHenyeyGreensteinFn } from '../nodes/sampling.wgsl.js';
|
|
8
|
+
import { mediumStruct, enterMediumFn, mediumChannelPdfFn, sampleMediumDistanceFn, mediumDistancePdfFn, MEDIUM_THROUGHPUT_EPSILON } from '../nodes/medium.wgsl.js';
|
|
9
|
+
import { proxy, proxyFn, contextProxy, contextProxyFn, wgslTagFn, rayStruct } from 'three-mesh-bvh/webgpu';
|
|
7
10
|
import { clampPathContributionFunc, isTerminatingScatterFunc, offsetRayOriginFunc } from '../nodes/utils.wgsl.js';
|
|
8
11
|
import { lightRecordStruct } from '../nodes/structs.wgsl.js';
|
|
9
12
|
import { ENVIRONMENT_LIGHT_TYPE, LIGHT_FAR_DISTANCE, LIGHT_EPSILON, isMISWeightLightFn } from '../nodes/lights.wgsl.js';
|
|
@@ -15,15 +18,14 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
15
18
|
constructor( ) {
|
|
16
19
|
|
|
17
20
|
const params = {
|
|
18
|
-
bvhData: { value: null },
|
|
19
21
|
material: { value: null },
|
|
20
22
|
envInfo: { value: null },
|
|
21
23
|
lightsInfo: { value: null },
|
|
22
24
|
|
|
23
25
|
// targets
|
|
24
|
-
prevOutputTarget: textureStore(
|
|
25
|
-
outputTarget: textureStore(
|
|
26
|
-
sampleCountTarget: textureStore(
|
|
26
|
+
prevOutputTarget: textureStore( createOutputTarget() ).toReadOnly(),
|
|
27
|
+
outputTarget: textureStore( createOutputTarget() ).toWriteOnly(),
|
|
28
|
+
sampleCountTarget: textureStore( createSampleCountTarget() ).toReadWrite(),
|
|
27
29
|
|
|
28
30
|
// tiles
|
|
29
31
|
offset: uniform( new Vector2() ),
|
|
@@ -33,6 +35,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
33
35
|
seed: uniform( 0 ),
|
|
34
36
|
maxBounces: uniform( 5 ),
|
|
35
37
|
maxTransparentBounces: uniform( 5, 'uint' ),
|
|
38
|
+
maxSubsurfaceBounces: uniform( 256, 'uint' ),
|
|
36
39
|
misEnabled: uniform( 1, 'uint' ),
|
|
37
40
|
maxSamples: uniform( 0, 'uint' ),
|
|
38
41
|
filterGlossy: uniform( 1 ),
|
|
@@ -42,6 +45,10 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
42
45
|
backgroundInfo: { value: null },
|
|
43
46
|
|
|
44
47
|
transmissiveBackground: uniform( TRANSMISSIVE_BACKGROUND_OVERLAY ),
|
|
48
|
+
accumulateAux: uniform( 0, 'uint' ),
|
|
49
|
+
|
|
50
|
+
// the per pixel first hit albedo and normal, as running means like the color
|
|
51
|
+
auxTarget: textureStore( createAuxTarget() ).toReadWrite(),
|
|
45
52
|
|
|
46
53
|
textures: texture( new DataTexture() ),
|
|
47
54
|
textureSampler: sampler( new DataTexture() ),
|
|
@@ -50,14 +57,16 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
50
57
|
globalId: globalId,
|
|
51
58
|
};
|
|
52
59
|
|
|
53
|
-
const raycastOutput =
|
|
54
|
-
const raycastFirstHitFn =
|
|
55
|
-
const raycastShadowFn =
|
|
56
|
-
const sampleTrianglePointFn =
|
|
57
|
-
const getSurfaceRecordFn =
|
|
58
|
-
const getCameraRayFn =
|
|
60
|
+
const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
|
|
61
|
+
const raycastFirstHitFn = contextProxyFn( 'bvhData.fns.raycastFirstHit' );
|
|
62
|
+
const raycastShadowFn = contextProxyFn( 'bvhData.fns.raycastShadow' );
|
|
63
|
+
const sampleTrianglePointFn = contextProxyFn( 'bvhData.fns.sampleTrianglePoint' );
|
|
64
|
+
const getSurfaceRecordFn = contextProxyFn( 'bvhData.fns.getSurfaceRecord' );
|
|
65
|
+
const getCameraRayFn = contextProxyFn( 'bvhData.fns.getCameraRay' );
|
|
59
66
|
const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
|
|
60
67
|
const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
|
|
68
|
+
const materialsStorage = contextProxy( 'bvhData.storage.materials' );
|
|
69
|
+
const transformsStorage = contextProxy( 'bvhData.storage.transforms' );
|
|
61
70
|
|
|
62
71
|
// environment resources
|
|
63
72
|
const envTotalSumNode = proxy( 'envInfo.value.totalSumNode', params );
|
|
@@ -92,6 +101,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
92
101
|
seed: u32,
|
|
93
102
|
maxBounces: u32,
|
|
94
103
|
maxTransparentBounces: u32,
|
|
104
|
+
maxSubsurfaceBounces: u32,
|
|
95
105
|
misEnabled: u32,
|
|
96
106
|
maxSamples: u32,
|
|
97
107
|
filterGlossy: f32,
|
|
@@ -99,12 +109,10 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
99
109
|
clampIndirect: f32,
|
|
100
110
|
|
|
101
111
|
transmissiveBackground: u32,
|
|
112
|
+
accumulateAux: u32,
|
|
102
113
|
|
|
103
114
|
) -> void {
|
|
104
115
|
|
|
105
|
-
let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
|
|
106
|
-
let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
|
|
107
|
-
|
|
108
116
|
// make sure we don't bleed over the edge of our tile
|
|
109
117
|
if ( globalId.x >= tileSize.x || globalId.y >= tileSize.y ) {
|
|
110
118
|
|
|
@@ -137,6 +145,13 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
137
145
|
var ray: ${ rayStruct };
|
|
138
146
|
if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
|
|
139
147
|
|
|
148
|
+
// a declined pixel never takes a sample, so with a cap it counts as finished
|
|
149
|
+
if ( maxSamples != 0u ) {
|
|
150
|
+
|
|
151
|
+
textureStore( ${ params.sampleCountTarget }, indexUV, vec4( maxSamples ) );
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
return;
|
|
141
156
|
|
|
142
157
|
}
|
|
@@ -146,9 +161,22 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
146
161
|
var resultColor = vec4f( 0, 0, 0, 1 );
|
|
147
162
|
var throughputColor = vec3f( 1.0 );
|
|
148
163
|
var bsdfPdf = 0.0;
|
|
164
|
+
|
|
165
|
+
// Alpha pass throughs only move where the BVH traversal starts, "rayStart" along the ray, so the
|
|
166
|
+
// ray keeps the origin it scattered from, which its MIS weights are measured from
|
|
167
|
+
var rayStart = 0.0;
|
|
168
|
+
var traceOrigin = ray.origin;
|
|
149
169
|
var isFullyTransmissive = true;
|
|
150
170
|
var minPdf = 1.0;
|
|
151
171
|
var transparentBounce = 0u;
|
|
172
|
+
|
|
173
|
+
// the scattering volume the path is in. Media don't nest
|
|
174
|
+
var medium: ${ mediumStruct };
|
|
175
|
+
var subsurfaceBounces = 0u;
|
|
176
|
+
|
|
177
|
+
// no light is sampled at a scatter event, so whatever the next segment reaches takes full weight
|
|
178
|
+
var mediumScattered = false;
|
|
179
|
+
|
|
152
180
|
// A negative value marks a sampled wavelength whose RGB reconstruction weight has not been applied yet.
|
|
153
181
|
var dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
|
|
154
182
|
|
|
@@ -166,14 +194,81 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
166
194
|
var lightHits = vec3f( 0.0 );
|
|
167
195
|
var cameraHitLight = false;
|
|
168
196
|
|
|
197
|
+
// The first surface the path scatters off fills the denoiser's auxiliary images. Until one is found
|
|
198
|
+
// the last mirror or transmission hit stands in, for paths that reach the environment. The albedo
|
|
199
|
+
// is tinted by the throughput of the surfaces looked past to reach it
|
|
200
|
+
var auxFound = false;
|
|
201
|
+
var auxAlbedo = vec3f( 0.0 );
|
|
202
|
+
var auxNormal = vec3f( 0.0 );
|
|
203
|
+
|
|
169
204
|
for ( var bounce = 0u; bounce < maxBounces; bounce ++ ) {
|
|
170
205
|
|
|
206
|
+
// Inside a medium, sample a scatter event before the next surface and only trace up to it
|
|
207
|
+
var channelPdf = vec3f( 0.0 );
|
|
208
|
+
var scatterDist = 0.0;
|
|
209
|
+
var traceRay = ray;
|
|
210
|
+
traceRay.origin = traceOrigin;
|
|
211
|
+
if ( medium.inside ) {
|
|
212
|
+
|
|
213
|
+
channelPdf = ${ mediumChannelPdfFn }( throughputColor, medium.albedo );
|
|
214
|
+
scatterDist = ${ sampleMediumDistanceFn }( medium, channelPdf, ${ rand2 }( ${ RNG_INDEX_MEDIUM_DISTANCE } ) );
|
|
215
|
+
traceRay.maxDist = select( scatterDist, min( ray.maxDist, scatterDist ), ray.maxDist > 0.0 );
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
171
219
|
var hitResult: ${ raycastOutput };
|
|
172
|
-
let didHit = ${ raycastFirstHitFn }(
|
|
220
|
+
let didHit = ${ raycastFirstHitFn }( traceRay, &hitResult );
|
|
173
221
|
let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
|
|
174
222
|
|
|
175
|
-
|
|
176
|
-
|
|
223
|
+
let segmentInMedium = medium.inside;
|
|
224
|
+
if ( medium.inside ) {
|
|
225
|
+
|
|
226
|
+
let scattered = scatterDist < surfaceDist;
|
|
227
|
+
let segmentDist = select( surfaceDist, scatterDist, scattered );
|
|
228
|
+
let transmittance = exp( - medium.extinction * segmentDist );
|
|
229
|
+
let distancePdf = dot( channelPdf, ${ mediumDistancePdfFn }( medium, segmentDist, scattered ) );
|
|
230
|
+
if ( distancePdf <= 0.0 ) {
|
|
231
|
+
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if ( ! scattered ) {
|
|
237
|
+
|
|
238
|
+
// reaching the surface, weighted by the probability of no scatter event before it
|
|
239
|
+
throughputColor *= transmittance / distancePdf;
|
|
240
|
+
|
|
241
|
+
} else {
|
|
242
|
+
|
|
243
|
+
throughputColor *= medium.albedo * medium.extinction * transmittance / distancePdf;
|
|
244
|
+
ray.origin = traceOrigin + ray.direction * scatterDist;
|
|
245
|
+
traceOrigin = ray.origin;
|
|
246
|
+
rayStart = 0.0;
|
|
247
|
+
ray.direction = ${ sampleHenyeyGreensteinFn }( ray.direction, medium.anisotropy, ${ rand2 }( ${ RNG_INDEX_MEDIUM_DIRECTION } ) );
|
|
248
|
+
ray.maxDist = 0.0;
|
|
249
|
+
mediumScattered = true;
|
|
250
|
+
isFullyTransmissive = false;
|
|
251
|
+
|
|
252
|
+
// subsurface bounces are capped separately from bounces
|
|
253
|
+
subsurfaceBounces ++;
|
|
254
|
+
let maxThroughput = max( max( throughputColor.r, throughputColor.g ), throughputColor.b );
|
|
255
|
+
if ( subsurfaceBounces >= maxSubsurfaceBounces || maxThroughput < ${ MEDIUM_THROUGHPUT_EPSILON } ) {
|
|
256
|
+
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
${ rngNextBounce }();
|
|
262
|
+
bounce --;
|
|
263
|
+
continue;
|
|
264
|
+
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// NEE's total weight at the vertex this ray left, for MIS. Camera rays have none, and
|
|
270
|
+
// neither do scatter events in a medium
|
|
271
|
+
let isMISWeighted = misEnabled != 0u && bounce > 0u && ! mediumScattered;
|
|
177
272
|
var lightTotalWeight = 0.0;
|
|
178
273
|
if ( isMISWeighted ) {
|
|
179
274
|
|
|
@@ -181,7 +276,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
181
276
|
|
|
182
277
|
}
|
|
183
278
|
|
|
184
|
-
// forward hits on area lights. Camera rays only see visibleToCameraRays
|
|
279
|
+
// forward hits on area lights along the traced segment. Camera rays only see visibleToCameraRays
|
|
280
|
+
// lights, at full weight
|
|
185
281
|
for ( var li = 0u; li < lightsCount; li ++ ) {
|
|
186
282
|
|
|
187
283
|
if ( bounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
|
|
@@ -191,7 +287,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
191
287
|
}
|
|
192
288
|
|
|
193
289
|
var lightRec: ${ lightRecordStruct };
|
|
194
|
-
if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
|
|
290
|
+
if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist > rayStart && lightRec.dist < rayStart + surfaceDist ) {
|
|
195
291
|
|
|
196
292
|
var misWeight = 1.0;
|
|
197
293
|
if ( isMISWeighted ) {
|
|
@@ -210,8 +306,9 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
210
306
|
|
|
211
307
|
if ( didHit ) {
|
|
212
308
|
|
|
213
|
-
|
|
214
|
-
|
|
309
|
+
// whole structs are read from the buffer, not a pointer alias, which Safari fails to compile
|
|
310
|
+
let objectInfo = ${ transformsStorage }[ hitResult.objectIndex ];
|
|
311
|
+
var materialInfo = ${ materialsStorage }[ objectInfo.materialIndex ];
|
|
215
312
|
|
|
216
313
|
// a matte surface hit by the camera ray renders as a fully transparent
|
|
217
314
|
if ( materialInfo.matte != 0 && bounce == 0u ) {
|
|
@@ -239,7 +336,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
239
336
|
var surface = ${ getSurfaceRecordFn }( materialInfo, vertexData, hitResult.side, hitResult.normal, view, blurRoughness );
|
|
240
337
|
|
|
241
338
|
// Stochastically pass through partially transparent surfaces by restarting
|
|
242
|
-
// the
|
|
339
|
+
// the traversal at the hit point, advancing the rng but not the bounce count.
|
|
243
340
|
if ( ${ rand1 }( ${ RNG_INDEX_ALPHA_TEST } ) > surface.opacity ) {
|
|
244
341
|
|
|
245
342
|
// stop once the transparent bounces run out
|
|
@@ -249,7 +346,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
249
346
|
|
|
250
347
|
}
|
|
251
348
|
|
|
252
|
-
|
|
349
|
+
traceOrigin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
|
|
350
|
+
rayStart += hitResult.dist;
|
|
253
351
|
${ rngNextBounce }();
|
|
254
352
|
transparentBounce ++;
|
|
255
353
|
bounce --;
|
|
@@ -271,8 +369,9 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
271
369
|
|
|
272
370
|
}
|
|
273
371
|
|
|
274
|
-
// attenuate the light transmitted through the volume when exiting a backface
|
|
275
|
-
|
|
372
|
+
// attenuate the light transmitted through the volume when exiting a backface. A scattering
|
|
373
|
+
// medium has already applied its transmittance above
|
|
374
|
+
if ( hitResult.side < 0.0 && materialInfo.transmission > 0.0 && ! segmentInMedium ) {
|
|
276
375
|
|
|
277
376
|
throughputColor *= ${ transmissionAttenuationFunc }( hitResult.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
|
|
278
377
|
|
|
@@ -318,20 +417,20 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
318
417
|
shadowRay.direction = lightRec.direction;
|
|
319
418
|
shadowRay.maxDist = lightRec.dist * ( 1.0 - ${ LIGHT_EPSILON } );
|
|
320
419
|
|
|
321
|
-
// opaque occlusion up to the light distance
|
|
322
|
-
//
|
|
323
|
-
// opaque objects
|
|
420
|
+
// opaque occlusion up to the light distance, attenuated by the partially
|
|
421
|
+
// transparent surfaces passed
|
|
324
422
|
var shadowHit: ${ raycastOutput };
|
|
325
423
|
let occluded = ${ raycastShadowFn }( shadowRay, &shadowHit );
|
|
326
424
|
if ( ! occluded ) {
|
|
327
425
|
|
|
328
426
|
let lightPdf = lightRec.pdf * selectionPdf;
|
|
329
427
|
|
|
330
|
-
// env + area lights are also bsdf
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
let
|
|
334
|
-
let
|
|
428
|
+
// env + area lights are also found by bsdf rays, so MIS-weight them. Punctual lights, and lights
|
|
429
|
+
// past the bounce limit, take full weight. Bsdf rays stopped by faces the shadow ray skipped can't
|
|
430
|
+
// find the light, so their share of the weight moves to the light sample
|
|
431
|
+
let isMISWeighted = ${ isMISWeightLightFn }( lightRec.lightType ) && bounce + 1u < maxBounces;
|
|
432
|
+
let misWeight = select( 1.0, mix( ${ misHeuristicFn }( lightPdf, evalRec.pdf ), 1.0, shadowHit.skippedOcclusion ), isMISWeighted );
|
|
433
|
+
let directLight = throughputColor * lightRec.emission * evalRec.color * misWeight * ( 1.0 - shadowHit.occlusion ) / lightPdf;
|
|
335
434
|
let contribution = ${ clampPathContributionFunc }( directLight, bounce + 1u, clampDirect, clampIndirect );
|
|
336
435
|
resultColor += vec4f( contribution, 0.0 );
|
|
337
436
|
|
|
@@ -346,6 +445,15 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
346
445
|
}
|
|
347
446
|
|
|
348
447
|
let scatterRec = ${ bsdfSampleFn }( view, surface );
|
|
448
|
+
|
|
449
|
+
if ( ! auxFound ) {
|
|
450
|
+
|
|
451
|
+
auxFound = ${ isAuxSurfaceFn }( surface, scatterRec );
|
|
452
|
+
auxAlbedo = saturate( throughputColor * surface.color );
|
|
453
|
+
auxNormal = surface.normal;
|
|
454
|
+
|
|
455
|
+
}
|
|
456
|
+
|
|
349
457
|
if ( ${ isTerminatingScatterFunc }( scatterRec ) ) {
|
|
350
458
|
|
|
351
459
|
break;
|
|
@@ -376,6 +484,28 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
376
484
|
throughputColor *= scatterRec.color;
|
|
377
485
|
throughputColor /= scatterRec.pdf;
|
|
378
486
|
bsdfPdf = scatterRec.pdf;
|
|
487
|
+
mediumScattered = false;
|
|
488
|
+
|
|
489
|
+
// As in "KHR_materials_scatter", the medium's surface bsdf is its boundary: the walk ends when
|
|
490
|
+
// light refracts out through it, and continues inside when it reflects back
|
|
491
|
+
if ( segmentInMedium && hitResult.objectIndex == medium.object && hitResult.side < 0.0 && scatterRec.isTransmissive ) {
|
|
492
|
+
|
|
493
|
+
medium.inside = false;
|
|
494
|
+
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// refracting into a front face of a scattering material starts a random walk inside it
|
|
498
|
+
if ( scatterRec.isTransmissive && ! surface.thinWall && hitResult.side > 0.0 ) {
|
|
499
|
+
|
|
500
|
+
let entered = ${ enterMediumFn }( materialInfo, hitResult.objectIndex );
|
|
501
|
+
|
|
502
|
+
if ( entered.inside ) {
|
|
503
|
+
|
|
504
|
+
medium = entered;
|
|
505
|
+
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
}
|
|
379
509
|
|
|
380
510
|
// exit if our throughput is 0.0
|
|
381
511
|
if ( all( throughputColor == vec3f( 0.0 ) ) ) {
|
|
@@ -387,6 +517,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
387
517
|
ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, hitResult.normal );
|
|
388
518
|
ray.direction = scatterRec.direction;
|
|
389
519
|
ray.maxDist = 0.0;
|
|
520
|
+
traceOrigin = ray.origin;
|
|
521
|
+
rayStart = 0.0;
|
|
390
522
|
|
|
391
523
|
} else {
|
|
392
524
|
|
|
@@ -490,6 +622,25 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
490
622
|
textureStore( ${ params.sampleCountTarget }, indexUV, vec4( nextSampleCount ) );
|
|
491
623
|
textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
|
|
492
624
|
|
|
625
|
+
// the auxiliary images are blended the same way, in the same row order
|
|
626
|
+
if ( accumulateAux != 0u ) {
|
|
627
|
+
|
|
628
|
+
var auxPacked: vec3u;
|
|
629
|
+
for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
|
|
630
|
+
|
|
631
|
+
auxPacked[ layer ] = textureLoad( ${ params.auxTarget }, colorIndex, layer ).r;
|
|
632
|
+
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
auxPacked = ${ blendAuxFn }( auxPacked, auxAlbedo, auxNormal, 1.0 / f32( nextSampleCount ) );
|
|
636
|
+
for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
|
|
637
|
+
|
|
638
|
+
textureStore( ${ params.auxTarget }, colorIndex, layer, vec4u( auxPacked[ layer ] ) );
|
|
639
|
+
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
}
|
|
643
|
+
|
|
493
644
|
}`;
|
|
494
645
|
|
|
495
646
|
super( shader( params ) );
|
|
@@ -37,11 +37,11 @@ export class PrimeSampleCountersKernel extends ComputeKernel {
|
|
|
37
37
|
const fn = wgslTagFn/* wgsl */`
|
|
38
38
|
fn compute() -> void {
|
|
39
39
|
|
|
40
|
-
atomicStore( &${ params.counters }.minSamples, 0xFFFFFFFFu );
|
|
41
|
-
atomicStore( &${ params.counters }.maxSamples, 0u );
|
|
42
|
-
atomicStore( &${ params.counters }.totalLo, 0u );
|
|
43
|
-
atomicStore( &${ params.counters }.totalHi, 0u );
|
|
44
|
-
atomicStore( &${ params.counters }.pixelCount, 0u );
|
|
40
|
+
atomicStore( &${ params.counters }[ 0 ].minSamples, 0xFFFFFFFFu );
|
|
41
|
+
atomicStore( &${ params.counters }[ 0 ].maxSamples, 0u );
|
|
42
|
+
atomicStore( &${ params.counters }[ 0 ].totalLo, 0u );
|
|
43
|
+
atomicStore( &${ params.counters }[ 0 ].totalHi, 0u );
|
|
44
|
+
atomicStore( &${ params.counters }[ 0 ].pixelCount, 0u );
|
|
45
45
|
|
|
46
46
|
}
|
|
47
47
|
`;
|
|
@@ -86,16 +86,16 @@ export class TallySampleCountsKernel extends ComputeKernel {
|
|
|
86
86
|
|
|
87
87
|
let samples = combinedField & ${ SAMPLE_COUNT_MASK }u;
|
|
88
88
|
|
|
89
|
-
atomicMin( &${ params.counters }.minSamples, samples );
|
|
90
|
-
atomicMax( &${ params.counters }.maxSamples, samples );
|
|
91
|
-
atomicAdd( &${ params.counters }.pixelCount, 1u );
|
|
89
|
+
atomicMin( &${ params.counters }[ 0 ].minSamples, samples );
|
|
90
|
+
atomicMax( &${ params.counters }[ 0 ].maxSamples, samples );
|
|
91
|
+
atomicAdd( &${ params.counters }[ 0 ].pixelCount, 1u );
|
|
92
92
|
|
|
93
93
|
// the sum overflowed if it came out smaller than the value already there, so carry
|
|
94
94
|
// the wrap into "totalHi"
|
|
95
|
-
let previousTotal = atomicAdd( &${ params.counters }.totalLo, samples );
|
|
95
|
+
let previousTotal = atomicAdd( &${ params.counters }[ 0 ].totalLo, samples );
|
|
96
96
|
if ( previousTotal + samples < previousTotal ) {
|
|
97
97
|
|
|
98
|
-
atomicAdd( &${ params.counters }.totalHi, 1u );
|
|
98
|
+
atomicAdd( &${ params.counters }[ 0 ].totalHi, 1u );
|
|
99
99
|
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ComputeKernel } from './ComputeKernel.js';
|
|
2
|
+
import { textureStore, globalId } from 'three/tsl';
|
|
3
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
4
|
+
import { createAuxImageTarget, createAuxTarget } from './targets.js';
|
|
5
|
+
import { unpackAuxAlbedoFn, unpackAuxNormalFn, AUX_LAYERS } from '../nodes/aux.wgsl.js';
|
|
6
|
+
|
|
7
|
+
// Unpacks the auxiliary target into separate albedo and normal images
|
|
8
|
+
export class UnpackAuxKernel extends ComputeKernel {
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
|
|
12
|
+
const params = {
|
|
13
|
+
auxTarget: textureStore( createAuxTarget() ).toReadOnly(),
|
|
14
|
+
albedoTarget: textureStore( createAuxImageTarget() ).toWriteOnly(),
|
|
15
|
+
normalTarget: textureStore( createAuxImageTarget() ).toWriteOnly(),
|
|
16
|
+
globalId: globalId,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const fn = wgslTagFn/* wgsl */`
|
|
20
|
+
fn compute( globalId: vec3u ) -> void {
|
|
21
|
+
|
|
22
|
+
let dims = textureDimensions( ${ params.auxTarget } );
|
|
23
|
+
if ( globalId.x >= dims.x || globalId.y >= dims.y ) {
|
|
24
|
+
|
|
25
|
+
return;
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var packed: vec3u;
|
|
30
|
+
for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
|
|
31
|
+
|
|
32
|
+
packed[ layer ] = textureLoad( ${ params.auxTarget }, globalId.xy, layer ).r;
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
textureStore( ${ params.albedoTarget }, globalId.xy, vec4f( ${ unpackAuxAlbedoFn }( packed ), 1.0 ) );
|
|
37
|
+
textureStore( ${ params.normalTarget }, globalId.xy, vec4f( ${ unpackAuxNormalFn }( packed ), 1.0 ) );
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
super( fn( params ) );
|
|
43
|
+
|
|
44
|
+
this.defineUniformAccessors( params );
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
@@ -12,10 +12,12 @@ export class ZeroOutKernel extends ComputeKernel {
|
|
|
12
12
|
outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(),
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
// resolved when the shader is built, so an array texture target is cleared a layer per z
|
|
16
|
+
const coord = () => params.outputTarget.value.isArrayTexture ? 'globalId.xy, globalId.z' : 'globalId.xy';
|
|
15
17
|
const fn = wgslTagFn/* wgsl */`
|
|
16
18
|
fn compute( globalId: vec3u ) -> void {
|
|
17
19
|
|
|
18
|
-
textureStore( ${ params.outputTarget },
|
|
20
|
+
textureStore( ${ params.outputTarget }, ${ coord }, vec4( 0, 0, 0, 1 ) );
|
|
19
21
|
|
|
20
22
|
}
|
|
21
23
|
`;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ColorManagement, FloatType, HalfFloatType, RGBAFormat } from 'three';
|
|
2
|
+
import { RedIntegerFormat, StorageArrayTexture, StorageTexture, UnsignedIntType } from 'three/webgpu';
|
|
3
|
+
import { AUX_LAYERS } from '../nodes/aux.wgsl.js';
|
|
4
|
+
|
|
5
|
+
// The storage textures the backends accumulate into. The kernels are created with these too, so
|
|
6
|
+
// their pipelines are compiled against the formats they are dispatched with.
|
|
7
|
+
|
|
8
|
+
// the color accumulation, in the working color space
|
|
9
|
+
export function createOutputTarget( name = '' ) {
|
|
10
|
+
|
|
11
|
+
const target = new StorageTexture( 1, 1 );
|
|
12
|
+
target.format = RGBAFormat;
|
|
13
|
+
target.type = FloatType;
|
|
14
|
+
target.colorSpace = ColorManagement.workingColorSpace;
|
|
15
|
+
target.name = name;
|
|
16
|
+
target.generateMipmaps = false;
|
|
17
|
+
return target;
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// the per pixel sample count and its flags
|
|
22
|
+
export function createSampleCountTarget( name = '' ) {
|
|
23
|
+
|
|
24
|
+
const target = new StorageTexture( 1, 1 );
|
|
25
|
+
target.format = RedIntegerFormat;
|
|
26
|
+
target.type = UnsignedIntType;
|
|
27
|
+
target.name = name;
|
|
28
|
+
target.generateMipmaps = false;
|
|
29
|
+
return target;
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// the denoiser's first hit albedo and normal, packed as halves into the layers. See "aux.wgsl.js"
|
|
34
|
+
export function createAuxTarget( name = '' ) {
|
|
35
|
+
|
|
36
|
+
const target = new StorageArrayTexture( 1, 1, AUX_LAYERS );
|
|
37
|
+
target.format = RedIntegerFormat;
|
|
38
|
+
target.type = UnsignedIntType;
|
|
39
|
+
target.name = name;
|
|
40
|
+
target.generateMipmaps = false;
|
|
41
|
+
return target;
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// an albedo or normal image unpacked from the auxiliary target, for the denoiser
|
|
46
|
+
export function createAuxImageTarget( name = '' ) {
|
|
47
|
+
|
|
48
|
+
const target = new StorageTexture( 1, 1 );
|
|
49
|
+
target.format = RGBAFormat;
|
|
50
|
+
target.type = HalfFloatType;
|
|
51
|
+
target.name = name;
|
|
52
|
+
target.generateMipmaps = false;
|
|
53
|
+
return target;
|
|
54
|
+
|
|
55
|
+
}
|