three-gpu-pathtracer 0.0.26 → 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 +4 -2
- package/build/index.module.js +23 -5
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +22 -4
- package/build/index.umd.cjs.map +1 -1
- package/package.json +8 -8
- package/src/core/PathTracingSceneGenerator.js +1 -1
- package/src/core/utils/sceneUpdateUtils.js +2 -1
- package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
- package/src/uniforms/LightsInfoUniformStruct.js +16 -1
- package/src/webgpu/API.md +167 -19
- package/src/webgpu/AtlasTexture.js +32 -12
- package/src/webgpu/EquirectHdrInfoNode.js +97 -46
- package/src/webgpu/LightsInfoNode.js +142 -4
- package/src/webgpu/MegaKernelPathTracer.js +11 -3
- package/src/webgpu/PathTracerBackend.js +174 -21
- package/src/webgpu/TurquinTexture.js +2 -0
- package/src/webgpu/WaveFrontPathTracer.js +169 -71
- package/src/webgpu/WebGPUPathTracer.js +637 -51
- package/src/webgpu/compute/ComputeKernel.js +18 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +253 -71
- 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 +136 -55
- 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 -23
- package/src/webgpu/compute/wavefront/structs.js +63 -35
- package/src/webgpu/constants.js +2 -2
- package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
- package/src/webgpu/index.d.ts +79 -6
- package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
- package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +109 -17
- package/src/webgpu/nodes/aux.wgsl.js +61 -0
- package/src/webgpu/nodes/eon.wgsl.js +1 -1
- package/src/webgpu/nodes/lights.wgsl.js +21 -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 +8 -1
- package/src/webgpu/upscale/FSRUpscaler.js +3 -2
|
@@ -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, {
|
|
@@ -73,11 +81,21 @@ export class ComputeKernel {
|
|
|
73
81
|
|
|
74
82
|
// forward the call parameters so they remain accessible via "computeNode.parameters"
|
|
75
83
|
node.parameters = this._fn.parameters;
|
|
84
|
+
|
|
85
|
+
// release the renderer's pipeline and bindings for the kernel being replaced
|
|
86
|
+
this.kernel?.dispose();
|
|
76
87
|
this.kernel = node.computeKernel( [ x, y, z ] );
|
|
77
88
|
return this;
|
|
78
89
|
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
dispose() {
|
|
93
|
+
|
|
94
|
+
this.kernel?.dispose();
|
|
95
|
+
this.kernel = null;
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
81
99
|
getDispatchSize( tx = 1, ty = 1, tz = 1, target = [] ) {
|
|
82
100
|
|
|
83
101
|
const [ wgx, wgy, wgz ] = this.workgroupSize;
|
|
@@ -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,13 +57,16 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
50
57
|
globalId: globalId,
|
|
51
58
|
};
|
|
52
59
|
|
|
53
|
-
const raycastOutput =
|
|
54
|
-
const raycastFirstHitFn =
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
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' );
|
|
58
66
|
const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
|
|
59
67
|
const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
|
|
68
|
+
const materialsStorage = contextProxy( 'bvhData.storage.materials' );
|
|
69
|
+
const transformsStorage = contextProxy( 'bvhData.storage.transforms' );
|
|
60
70
|
|
|
61
71
|
// environment resources
|
|
62
72
|
const envTotalSumNode = proxy( 'envInfo.value.totalSumNode', params );
|
|
@@ -64,12 +74,17 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
64
74
|
const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
|
|
65
75
|
const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
|
|
66
76
|
const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
|
|
77
|
+
const getEnvWeight = proxy( 'envInfo.value.getWeight', params );
|
|
67
78
|
const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
|
|
68
79
|
|
|
69
80
|
// analytic scene lights pulled off the lightsInfo provider ( LightsInfoNode )
|
|
70
81
|
const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
|
|
71
82
|
const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
|
|
72
83
|
const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
|
|
84
|
+
const getLightWeightFn = proxyFn( 'lightsInfo.value.getLightWeight', params );
|
|
85
|
+
const getLightsWeightFn = proxyFn( 'lightsInfo.value.getLightsWeight', params );
|
|
86
|
+
const selectLightFn = proxyFn( 'lightsInfo.value.selectLight', params );
|
|
87
|
+
const isLightVisibleToCameraFn = proxyFn( 'lightsInfo.value.isLightVisibleToCamera', params );
|
|
73
88
|
|
|
74
89
|
const shader = wgslTagFn/* wgsl */`
|
|
75
90
|
|
|
@@ -86,6 +101,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
86
101
|
seed: u32,
|
|
87
102
|
maxBounces: u32,
|
|
88
103
|
maxTransparentBounces: u32,
|
|
104
|
+
maxSubsurfaceBounces: u32,
|
|
89
105
|
misEnabled: u32,
|
|
90
106
|
maxSamples: u32,
|
|
91
107
|
filterGlossy: f32,
|
|
@@ -93,12 +109,10 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
93
109
|
clampIndirect: f32,
|
|
94
110
|
|
|
95
111
|
transmissiveBackground: u32,
|
|
112
|
+
accumulateAux: u32,
|
|
96
113
|
|
|
97
114
|
) -> void {
|
|
98
115
|
|
|
99
|
-
let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
|
|
100
|
-
let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
|
|
101
|
-
|
|
102
116
|
// make sure we don't bleed over the edge of our tile
|
|
103
117
|
if ( globalId.x >= tileSize.x || globalId.y >= tileSize.y ) {
|
|
104
118
|
|
|
@@ -131,6 +145,13 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
131
145
|
var ray: ${ rayStruct };
|
|
132
146
|
if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
|
|
133
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
|
+
|
|
134
155
|
return;
|
|
135
156
|
|
|
136
157
|
}
|
|
@@ -140,59 +161,154 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
140
161
|
var resultColor = vec4f( 0, 0, 0, 1 );
|
|
141
162
|
var throughputColor = vec3f( 1.0 );
|
|
142
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;
|
|
143
169
|
var isFullyTransmissive = true;
|
|
144
170
|
var minPdf = 1.0;
|
|
145
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
|
+
|
|
146
180
|
// A negative value marks a sampled wavelength whose RGB reconstruction weight has not been applied yet.
|
|
147
181
|
var dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
|
|
148
182
|
|
|
149
|
-
// one-sample NEE
|
|
150
|
-
// lightsDenom is the number of options
|
|
183
|
+
// one-sample NEE chooses between the analytic lights and the environment
|
|
151
184
|
let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
|
|
152
185
|
let lightsCount = ${ lightsCountNode };
|
|
153
|
-
var
|
|
186
|
+
var envWeight = 0.0;
|
|
154
187
|
if ( envActive ) {
|
|
155
188
|
|
|
156
|
-
|
|
189
|
+
envWeight = ${ getEnvWeight }();
|
|
157
190
|
|
|
158
191
|
}
|
|
159
192
|
|
|
193
|
+
// added last, since a camera ray miss overwrites the pixel with the background
|
|
194
|
+
var lightHits = vec3f( 0.0 );
|
|
195
|
+
var cameraHitLight = false;
|
|
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
|
+
|
|
160
204
|
for ( var bounce = 0u; bounce < maxBounces; bounce ++ ) {
|
|
161
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
|
+
|
|
162
219
|
var hitResult: ${ raycastOutput };
|
|
163
|
-
let didHit = ${ raycastFirstHitFn }(
|
|
220
|
+
let didHit = ${ raycastFirstHitFn }( traceRay, &hitResult );
|
|
164
221
|
let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
|
|
165
222
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if ( bounce > 0u ) {
|
|
223
|
+
let segmentInMedium = medium.inside;
|
|
224
|
+
if ( medium.inside ) {
|
|
169
225
|
|
|
170
|
-
|
|
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 ) {
|
|
171
231
|
|
|
172
|
-
|
|
173
|
-
if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
|
|
232
|
+
break;
|
|
174
233
|
|
|
175
|
-
|
|
176
|
-
if ( misEnabled != 0u ) {
|
|
234
|
+
}
|
|
177
235
|
|
|
178
|
-
|
|
179
|
-
misWeight = ${ misHeuristicFn }( bsdfPdf, lightPdf );
|
|
236
|
+
if ( ! scattered ) {
|
|
180
237
|
|
|
181
|
-
|
|
238
|
+
// reaching the surface, weighted by the probability of no scatter event before it
|
|
239
|
+
throughputColor *= transmittance / distancePdf;
|
|
240
|
+
|
|
241
|
+
} else {
|
|
182
242
|
|
|
183
|
-
|
|
184
|
-
|
|
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;
|
|
185
258
|
|
|
186
259
|
}
|
|
187
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;
|
|
272
|
+
var lightTotalWeight = 0.0;
|
|
273
|
+
if ( isMISWeighted ) {
|
|
274
|
+
|
|
275
|
+
lightTotalWeight = ${ getLightsWeightFn }( ray.origin ) + envWeight;
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// forward hits on area lights along the traced segment. Camera rays only see visibleToCameraRays
|
|
280
|
+
// lights, at full weight
|
|
281
|
+
for ( var li = 0u; li < lightsCount; li ++ ) {
|
|
282
|
+
|
|
283
|
+
if ( bounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
|
|
284
|
+
|
|
285
|
+
continue;
|
|
286
|
+
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var lightRec: ${ lightRecordStruct };
|
|
290
|
+
if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist > rayStart && lightRec.dist < rayStart + surfaceDist ) {
|
|
291
|
+
|
|
292
|
+
var misWeight = 1.0;
|
|
293
|
+
if ( isMISWeighted ) {
|
|
294
|
+
|
|
295
|
+
let selectionPdf = ${ getLightWeightFn }( li, ray.origin ) / lightTotalWeight;
|
|
296
|
+
misWeight = ${ misHeuristicFn }( bsdfPdf, lightRec.pdf * selectionPdf );
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
lightHits += ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, bounce, clampDirect, clampIndirect );
|
|
301
|
+
cameraHitLight = cameraHitLight || bounce == 0u;
|
|
302
|
+
|
|
188
303
|
}
|
|
189
304
|
|
|
190
305
|
}
|
|
191
306
|
|
|
192
307
|
if ( didHit ) {
|
|
193
308
|
|
|
194
|
-
|
|
195
|
-
|
|
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 ];
|
|
196
312
|
|
|
197
313
|
// a matte surface hit by the camera ray renders as a fully transparent
|
|
198
314
|
if ( materialInfo.matte != 0 && bounce == 0u ) {
|
|
@@ -220,7 +336,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
220
336
|
var surface = ${ getSurfaceRecordFn }( materialInfo, vertexData, hitResult.side, hitResult.normal, view, blurRoughness );
|
|
221
337
|
|
|
222
338
|
// Stochastically pass through partially transparent surfaces by restarting
|
|
223
|
-
// the
|
|
339
|
+
// the traversal at the hit point, advancing the rng but not the bounce count.
|
|
224
340
|
if ( ${ rand1 }( ${ RNG_INDEX_ALPHA_TEST } ) > surface.opacity ) {
|
|
225
341
|
|
|
226
342
|
// stop once the transparent bounces run out
|
|
@@ -230,7 +346,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
230
346
|
|
|
231
347
|
}
|
|
232
348
|
|
|
233
|
-
|
|
349
|
+
traceOrigin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
|
|
350
|
+
rayStart += hitResult.dist;
|
|
234
351
|
${ rngNextBounce }();
|
|
235
352
|
transparentBounce ++;
|
|
236
353
|
bounce --;
|
|
@@ -252,8 +369,9 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
252
369
|
|
|
253
370
|
}
|
|
254
371
|
|
|
255
|
-
// attenuate the light transmitted through the volume when exiting a backface
|
|
256
|
-
|
|
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 ) {
|
|
257
375
|
|
|
258
376
|
throughputColor *= ${ transmissionAttenuationFunc }( hitResult.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
|
|
259
377
|
|
|
@@ -263,30 +381,31 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
263
381
|
let emission = ${ clampPathContributionFunc }( throughputColor * surface.emission, bounce, clampDirect, clampIndirect );
|
|
264
382
|
resultColor += vec4f( emission, 0.0 );
|
|
265
383
|
|
|
266
|
-
// next event estimation:
|
|
267
|
-
//
|
|
268
|
-
if ( misEnabled != 0u
|
|
384
|
+
// next event estimation: choose one light or the environment by its estimated
|
|
385
|
+
// contribution
|
|
386
|
+
if ( misEnabled != 0u ) {
|
|
269
387
|
|
|
270
|
-
// pick one light or the environment with a single sample
|
|
271
|
-
// TODO: importance-sample the selection by light intensity and solid angle
|
|
272
388
|
let ruv = ${ rand3 }( ${ RNG_INDEX_DIRECT_LIGHT_SAMPLE } );
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
// the environment, sampled from its CDF, as a light of kind ENVIRONMENT
|
|
278
|
-
let envSample = ${ sampleEnvDir }( ruv.yz );
|
|
279
|
-
lightRec.direction = envSample.direction;
|
|
280
|
-
lightRec.emission = envSample.color;
|
|
281
|
-
lightRec.pdf = envSample.pdf;
|
|
282
|
-
lightRec.dist = ${ LIGHT_FAR_DISTANCE };
|
|
283
|
-
lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
|
|
389
|
+
var selectionPdf = 0.0;
|
|
390
|
+
let lightIndex = ${ selectLightFn }( vertexData.position.xyz, envWeight, ruv.x, &selectionPdf );
|
|
391
|
+
if ( selectionPdf > 0.0 ) {
|
|
284
392
|
|
|
285
|
-
|
|
393
|
+
var lightRec: ${ lightRecordStruct };
|
|
394
|
+
if ( envActive && lightIndex == lightsCount ) {
|
|
286
395
|
|
|
287
|
-
|
|
396
|
+
// the environment, sampled from its CDF, as a light of kind ENVIRONMENT
|
|
397
|
+
let envSample = ${ sampleEnvDir }( ruv.yz );
|
|
398
|
+
lightRec.direction = envSample.direction;
|
|
399
|
+
lightRec.emission = envSample.color;
|
|
400
|
+
lightRec.pdf = envSample.pdf;
|
|
401
|
+
lightRec.dist = ${ LIGHT_FAR_DISTANCE };
|
|
402
|
+
lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
|
|
288
403
|
|
|
289
|
-
|
|
404
|
+
} else {
|
|
405
|
+
|
|
406
|
+
lightRec = ${ randomLightSampleFn }( lightIndex, vertexData.position.xyz, ruv.yz );
|
|
407
|
+
|
|
408
|
+
}
|
|
290
409
|
|
|
291
410
|
if ( lightRec.pdf > 0.0 ) {
|
|
292
411
|
|
|
@@ -296,21 +415,22 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
296
415
|
var shadowRay: ${ rayStruct };
|
|
297
416
|
shadowRay.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, lightRec.direction, hitResult.normal );
|
|
298
417
|
shadowRay.direction = lightRec.direction;
|
|
299
|
-
shadowRay.maxDist = lightRec.dist - ${ LIGHT_EPSILON };
|
|
418
|
+
shadowRay.maxDist = lightRec.dist * ( 1.0 - ${ LIGHT_EPSILON } );
|
|
300
419
|
|
|
301
|
-
// opaque occlusion up to the light distance
|
|
302
|
-
//
|
|
303
|
-
// opaque objects
|
|
420
|
+
// opaque occlusion up to the light distance, attenuated by the partially
|
|
421
|
+
// transparent surfaces passed
|
|
304
422
|
var shadowHit: ${ raycastOutput };
|
|
305
|
-
let occluded = ${
|
|
423
|
+
let occluded = ${ raycastShadowFn }( shadowRay, &shadowHit );
|
|
306
424
|
if ( ! occluded ) {
|
|
307
425
|
|
|
308
|
-
|
|
309
|
-
lightPdf /= lightsDenom;
|
|
426
|
+
let lightPdf = lightRec.pdf * selectionPdf;
|
|
310
427
|
|
|
311
|
-
// env + area lights are also bsdf
|
|
312
|
-
|
|
313
|
-
|
|
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;
|
|
314
434
|
let contribution = ${ clampPathContributionFunc }( directLight, bounce + 1u, clampDirect, clampIndirect );
|
|
315
435
|
resultColor += vec4f( contribution, 0.0 );
|
|
316
436
|
|
|
@@ -320,9 +440,20 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
320
440
|
|
|
321
441
|
}
|
|
322
442
|
|
|
443
|
+
}
|
|
444
|
+
|
|
323
445
|
}
|
|
324
446
|
|
|
325
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
|
+
|
|
326
457
|
if ( ${ isTerminatingScatterFunc }( scatterRec ) ) {
|
|
327
458
|
|
|
328
459
|
break;
|
|
@@ -353,6 +484,28 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
353
484
|
throughputColor *= scatterRec.color;
|
|
354
485
|
throughputColor /= scatterRec.pdf;
|
|
355
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
|
+
}
|
|
356
509
|
|
|
357
510
|
// exit if our throughput is 0.0
|
|
358
511
|
if ( all( throughputColor == vec3f( 0.0 ) ) ) {
|
|
@@ -364,16 +517,18 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
364
517
|
ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, hitResult.normal );
|
|
365
518
|
ray.direction = scatterRec.direction;
|
|
366
519
|
ray.maxDist = 0.0;
|
|
520
|
+
traceOrigin = ray.origin;
|
|
521
|
+
rayStart = 0.0;
|
|
367
522
|
|
|
368
523
|
} else {
|
|
369
524
|
|
|
370
525
|
if ( bounce > 0u && ! isFullyTransmissive ) {
|
|
371
526
|
|
|
372
527
|
var misWeight = 1.0;
|
|
373
|
-
if (
|
|
528
|
+
if ( isMISWeighted && envActive ) {
|
|
374
529
|
|
|
375
530
|
// match the env pdf scaling used by the NEE selection so the two estimators balance
|
|
376
|
-
let envPdf = ${ getEnvDirPdf }( ray.direction ) /
|
|
531
|
+
let envPdf = ${ getEnvDirPdf }( ray.direction ) * envWeight / lightTotalWeight;
|
|
377
532
|
misWeight = ${ misHeuristicFn }( bsdfPdf, envPdf );
|
|
378
533
|
|
|
379
534
|
}
|
|
@@ -450,6 +605,14 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
450
605
|
|
|
451
606
|
}
|
|
452
607
|
|
|
608
|
+
// visible lights are opaque against a transparent background
|
|
609
|
+
resultColor += vec4f( lightHits, 0.0 );
|
|
610
|
+
if ( cameraHitLight ) {
|
|
611
|
+
|
|
612
|
+
resultColor.a = 1.0;
|
|
613
|
+
|
|
614
|
+
}
|
|
615
|
+
|
|
453
616
|
let nextSampleCount = sampleCount + 1;
|
|
454
617
|
// store the color rows top down to match a rasterized render target
|
|
455
618
|
let colorIndex = vec2u( indexUV.x, targetDimensions.y - 1u - indexUV.y );
|
|
@@ -459,6 +622,25 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
459
622
|
textureStore( ${ params.sampleCountTarget }, indexUV, vec4( nextSampleCount ) );
|
|
460
623
|
textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
|
|
461
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
|
+
|
|
462
644
|
}`;
|
|
463
645
|
|
|
464
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
|
`;
|