three-gpu-pathtracer 0.0.26 → 0.0.27
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 -1
- package/build/index.module.js +18 -2
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +18 -2
- package/build/index.umd.cjs.map +1 -1
- package/package.json +2 -2
- package/src/core/utils/sceneUpdateUtils.js +2 -1
- package/src/uniforms/LightsInfoUniformStruct.js +16 -1
- package/src/webgpu/AtlasTexture.js +30 -9
- package/src/webgpu/EquirectHdrInfoNode.js +11 -1
- package/src/webgpu/LightsInfoNode.js +141 -3
- package/src/webgpu/MegaKernelPathTracer.js +1 -0
- package/src/webgpu/PathTracerBackend.js +2 -0
- package/src/webgpu/TurquinTexture.js +2 -0
- package/src/webgpu/WaveFrontPathTracer.js +13 -0
- package/src/webgpu/WebGPUPathTracer.js +9 -3
- package/src/webgpu/compute/ComputeKernel.js +10 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +75 -44
- package/src/webgpu/compute/wavefront/LogicKernel.js +77 -43
- package/src/webgpu/compute/wavefront/MaterialKernel.js +1 -1
- package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +6 -4
- package/src/webgpu/compute/wavefront/structs.js +3 -1
- package/src/webgpu/index.d.ts +38 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +79 -9
- package/src/webgpu/nodes/lights.wgsl.js +21 -1
- package/src/webgpu/nodes/structs.wgsl.js +1 -0
- package/src/webgpu/upscale/FSRUpscaler.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-gpu-pathtracer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "Path tracing renderer and utilities for three.js built on top of three-mesh-bvh.",
|
|
5
5
|
"module": "src/index.js",
|
|
6
6
|
"main": "build/index.umd.cjs",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@lookingglass/webxr": "^0.6.0",
|
|
45
45
|
"@monogrid/gainmap-js": "^3.0.5",
|
|
46
|
-
"@pmndrs/upscaler": "^0.
|
|
46
|
+
"@pmndrs/upscaler": "^0.3.0",
|
|
47
47
|
"@types/node": "^24.3.1",
|
|
48
48
|
"@types/three": "^0.185.1",
|
|
49
49
|
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
|
@@ -2,6 +2,7 @@ import { DataTexture, RGBAFormat, ClampToEdgeWrapping, FloatType, Vector3, Quate
|
|
|
2
2
|
import { bufferToHash } from '../utils/bufferToHash.js';
|
|
3
3
|
|
|
4
4
|
const LIGHT_PIXELS = 6;
|
|
5
|
+
const VISIBLE_TO_CAMERA_OFFSET = 11;
|
|
5
6
|
const RECT_AREA_LIGHT = 0;
|
|
6
7
|
const CIRC_AREA_LIGHT = 1;
|
|
7
8
|
const SPOT_LIGHT = 2;
|
|
@@ -82,7 +83,7 @@ export class LightsInfoUniformStruct {
|
|
|
82
83
|
|
|
83
84
|
type = SPOT_LIGHT;
|
|
84
85
|
|
|
85
|
-
} else if ( l.isDirectionalLight ) {
|
|
86
|
+
} else if ( l.isDirectionalLight || l.isSunLight ) {
|
|
86
87
|
|
|
87
88
|
type = DIR_LIGHT;
|
|
88
89
|
|
|
@@ -207,8 +208,22 @@ export class LightsInfoUniformStruct {
|
|
|
207
208
|
floatArray[ baseIndex + ( index ++ ) ] = target.y;
|
|
208
209
|
floatArray[ baseIndex + ( index ++ ) ] = target.z;
|
|
209
210
|
|
|
211
|
+
} else if ( l.isSunLight ) {
|
|
212
|
+
|
|
213
|
+
// the sun has no target and shines from its position toward the origin
|
|
214
|
+
target.setFromMatrixPosition( l.matrixWorld ).normalize();
|
|
215
|
+
|
|
216
|
+
// sample 3
|
|
217
|
+
// u vector
|
|
218
|
+
floatArray[ baseIndex + ( index ++ ) ] = target.x;
|
|
219
|
+
floatArray[ baseIndex + ( index ++ ) ] = target.y;
|
|
220
|
+
floatArray[ baseIndex + ( index ++ ) ] = target.z;
|
|
221
|
+
|
|
210
222
|
}
|
|
211
223
|
|
|
224
|
+
// whether camera rays see the light
|
|
225
|
+
floatArray[ baseIndex + VISIBLE_TO_CAMERA_OFFSET ] = l.visibleToCameraRays ? 1 : 0;
|
|
226
|
+
|
|
212
227
|
}
|
|
213
228
|
|
|
214
229
|
this.count = lights.length;
|
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
NoBlending,
|
|
7
7
|
Color,
|
|
8
8
|
Vector4,
|
|
9
|
+
Matrix3,
|
|
9
10
|
} from 'three/webgpu';
|
|
10
11
|
|
|
11
12
|
const MAX_TEXTURE_SIZE = 4096;
|
|
12
13
|
const _prevClearColor = new Color();
|
|
14
|
+
const _prevMatrix = new Matrix3();
|
|
13
15
|
|
|
14
16
|
function getTextureHash( texture ) {
|
|
15
17
|
|
|
@@ -438,21 +440,40 @@ export class AtlasTexture {
|
|
|
438
440
|
for ( let i = 0, l = textures.length; i < l; i ++ ) {
|
|
439
441
|
|
|
440
442
|
const { x, y, w, h, page } = placements[ i ];
|
|
441
|
-
|
|
442
|
-
// Clone the source so we get an independent texture handle
|
|
443
|
-
const texture = textures[ i ].clone();
|
|
444
|
-
texture.matrixAutoUpdate = false;
|
|
445
|
-
texture.matrix.identity();
|
|
446
|
-
|
|
447
|
-
quadMesh.material.map = texture;
|
|
443
|
+
const texture = textures[ i ];
|
|
448
444
|
|
|
449
445
|
// three.js uses the render target viewport / scissor
|
|
450
446
|
renderTarget.viewport.set( x, y, w, h );
|
|
451
447
|
renderTarget.scissor.set( x, y, w, h );
|
|
452
448
|
renderer.setRenderTarget( renderTarget, page );
|
|
453
|
-
quadMesh.render( renderer );
|
|
454
449
|
|
|
455
|
-
texture.
|
|
450
|
+
if ( texture.isRenderTargetTexture ) {
|
|
451
|
+
|
|
452
|
+
// a render target's content only lives on the GPU, so draw it directly
|
|
453
|
+
const prevAutoUpdate = texture.matrixAutoUpdate;
|
|
454
|
+
_prevMatrix.copy( texture.matrix );
|
|
455
|
+
texture.matrix.identity();
|
|
456
|
+
texture.matrixAutoUpdate = false;
|
|
457
|
+
|
|
458
|
+
quadMesh.material.map = texture;
|
|
459
|
+
quadMesh.render( renderer );
|
|
460
|
+
|
|
461
|
+
texture.matrixAutoUpdate = prevAutoUpdate;
|
|
462
|
+
texture.matrix.copy( _prevMatrix );
|
|
463
|
+
|
|
464
|
+
} else {
|
|
465
|
+
|
|
466
|
+
// Clone the source so we get an independent texture handle
|
|
467
|
+
const clone = texture.clone();
|
|
468
|
+
clone.matrixAutoUpdate = false;
|
|
469
|
+
clone.matrix.identity();
|
|
470
|
+
|
|
471
|
+
quadMesh.material.map = clone;
|
|
472
|
+
quadMesh.render( renderer );
|
|
473
|
+
|
|
474
|
+
clone.dispose();
|
|
475
|
+
|
|
476
|
+
}
|
|
456
477
|
|
|
457
478
|
}
|
|
458
479
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Matrix3 } from 'three/webgpu';
|
|
2
|
-
import { texture, sampler, uniform, wgslFn } from 'three/tsl';
|
|
2
|
+
import { texture, sampler, uniform, wgslFn, PI } from 'three/tsl';
|
|
3
3
|
import { EquirectHdrInfoUniform } from '../uniforms/EquirectHdrInfoUniform.js';
|
|
4
4
|
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
5
5
|
import { constants, environmentSampleStruct } from './nodes/structs.wgsl.js';
|
|
@@ -126,6 +126,16 @@ export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
|
|
|
126
126
|
}
|
|
127
127
|
`;
|
|
128
128
|
|
|
129
|
+
// the environment's average irradiance, for weighing it against the lights
|
|
130
|
+
this.getWeight = wgslTagFn/* wgsl */`
|
|
131
|
+
fn getEnvWeight() -> f32 {
|
|
132
|
+
|
|
133
|
+
let resolution = textureDimensions( ${ mapNode } );
|
|
134
|
+
return ${ intensityNode } * ${ PI } * ${ PI } * ${ totalSumNode } / ( 2.0 * f32( resolution.x * resolution.y ) );
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
|
|
129
139
|
this.getDirPdf = wgslTagFn/* wgsl */`
|
|
130
140
|
fn getEnvDirPdf( direction: vec3f ) -> f32 {
|
|
131
141
|
|
|
@@ -5,6 +5,7 @@ import { AtlasTexture } from './AtlasTexture.js';
|
|
|
5
5
|
import { LightsInfoUniformStruct } from '../uniforms/LightsInfoUniformStruct.js';
|
|
6
6
|
import { lightStruct, lightRecordStruct } from './nodes/structs.wgsl.js';
|
|
7
7
|
import { sampleTexelFunc } from './nodes/utils.wgsl.js';
|
|
8
|
+
import { luminanceFn } from './nodes/sampling.wgsl.js';
|
|
8
9
|
import {
|
|
9
10
|
RECT_AREA_LIGHT_TYPE,
|
|
10
11
|
CIRC_AREA_LIGHT_TYPE,
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
17
18
|
randomAreaLightSampleFn,
|
|
18
19
|
randomSpotLightSampleFn,
|
|
19
20
|
getSpotAttenuationFn,
|
|
21
|
+
getDistanceAttenuationFn,
|
|
22
|
+
getSpotLightNearestFn,
|
|
20
23
|
} from './nodes/lights.wgsl.js';
|
|
21
24
|
|
|
22
25
|
export class LightsInfoNode extends LightsInfoUniformStruct {
|
|
@@ -65,12 +68,13 @@ export class LightsInfoNode extends LightsInfoUniformStruct {
|
|
|
65
68
|
const src = this.tex.image.data;
|
|
66
69
|
this.buffer.array.set( src.subarray( 0, count * stride ) );
|
|
67
70
|
|
|
68
|
-
// rewrite the int fields ( lightType, iesProfile ) as i32 bits
|
|
71
|
+
// rewrite the int fields ( lightType, visibleToCameraRays, iesProfile ) as i32 bits
|
|
69
72
|
const intView = new Int32Array( this.buffer.array.buffer );
|
|
70
73
|
for ( let i = 0; i < count; i ++ ) {
|
|
71
74
|
|
|
72
75
|
const base = i * stride;
|
|
73
76
|
intView[ base + 3 ] = Math.round( src[ base + 3 ] );
|
|
77
|
+
intView[ base + 11 ] = Math.round( src[ base + 11 ] );
|
|
74
78
|
intView[ base + 21 ] = Math.round( src[ base + 21 ] );
|
|
75
79
|
|
|
76
80
|
}
|
|
@@ -84,12 +88,146 @@ export class LightsInfoNode extends LightsInfoUniformStruct {
|
|
|
84
88
|
|
|
85
89
|
_initFns() {
|
|
86
90
|
|
|
87
|
-
const { bufferNode, iesProfilesNode, iesInfoNode } = this;
|
|
91
|
+
const { bufferNode, countNode, iesProfilesNode, iesInfoNode } = this;
|
|
88
92
|
|
|
89
93
|
// profiles are sampled out of an atlas so filtering must resolve tile-relative wrapping
|
|
90
94
|
const sampleIesTexelFn = sampleTexelFunc( iesInfoNode, iesProfilesNode, 'sampleIesTexel' );
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
this.isLightVisibleToCamera = wgslTagFn/* wgsl */`
|
|
97
|
+
fn isLightVisibleToCamera( index: u32 ) -> bool {
|
|
98
|
+
|
|
99
|
+
return ${ bufferNode }[ index ].visibleToCameraRays != 0;
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
// A cheap estimate of how much light reaches a position from a light, ignoring occlusion and
|
|
105
|
+
// the surface orientation, used to choose which light to sample.
|
|
106
|
+
// TODO: weigh lights below the surface's horizon to zero using its normal, skipped for
|
|
107
|
+
// transmissive surfaces. Forward hit MIS would then need the previous vertex's normal too.
|
|
108
|
+
this.getLightWeight = wgslTagFn/* wgsl */`
|
|
109
|
+
fn getLightWeight( index: u32, position: vec3f ) -> f32 {
|
|
110
|
+
|
|
111
|
+
let light = ${ bufferNode }[ index ];
|
|
112
|
+
let power = ${ luminanceFn }( light.color * light.intensity );
|
|
113
|
+
if ( light.lightType == ${ SPOT_LIGHT_TYPE } ) {
|
|
114
|
+
|
|
115
|
+
// the most any point on the spot's disc lights the position. Ies profiles are not
|
|
116
|
+
// evaluated here
|
|
117
|
+
let nearest = ${ getSpotLightNearestFn }( light, position );
|
|
118
|
+
var attenuation = 1.0;
|
|
119
|
+
if ( light.iesProfile < 0 ) {
|
|
120
|
+
|
|
121
|
+
attenuation = ${ getSpotAttenuationFn }( light.coneCos, light.penumbraCos, nearest.x );
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return power * attenuation * ${ getDistanceAttenuationFn }( nearest.y, light.distance, light.decay );
|
|
126
|
+
|
|
127
|
+
} else if ( light.lightType == ${ POINT_LIGHT_TYPE } ) {
|
|
128
|
+
|
|
129
|
+
// the point light's world position is packed into the u slot
|
|
130
|
+
let dist = length( light.u - position );
|
|
131
|
+
return power * ${ getDistanceAttenuationFn }( dist, light.distance, light.decay );
|
|
132
|
+
|
|
133
|
+
} else if ( light.lightType == ${ DIR_LIGHT_TYPE } ) {
|
|
134
|
+
|
|
135
|
+
return power;
|
|
136
|
+
|
|
137
|
+
} else {
|
|
138
|
+
|
|
139
|
+
let toLight = light.position - position;
|
|
140
|
+
let distSq = dot( toLight, toLight );
|
|
141
|
+
if ( distSq == 0.0 ) {
|
|
142
|
+
|
|
143
|
+
return 0.0;
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// area lights only emit from their front face, and the distance is held above the size
|
|
148
|
+
// of the light so positions on or near it do not dominate
|
|
149
|
+
let normal = normalize( cross( light.u, light.v ) );
|
|
150
|
+
let cosTheta = dot( toLight, normal ) * inverseSqrt( distSq );
|
|
151
|
+
return power * light.area * max( cosTheta, 0.0 ) / max( distSq, light.area );
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
this.getLightsWeight = wgslTagFn/* wgsl */`
|
|
159
|
+
fn getLightsWeight( position: vec3f ) -> f32 {
|
|
160
|
+
|
|
161
|
+
var total = 0.0;
|
|
162
|
+
for ( var i = 0u; i < ${ countNode }; i ++ ) {
|
|
163
|
+
|
|
164
|
+
total += ${ this.getLightWeight }( i, position );
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return total;
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
// Chooses a light, or the environment when the returned index equals the light count, in
|
|
174
|
+
// proportion to the estimated weights. The pdf is zero when nothing can light the position.
|
|
175
|
+
this.selectLight = wgslTagFn/* wgsl */`
|
|
176
|
+
fn selectLight( position: vec3f, envWeight: f32, r: f32, selectionPdf: ptr<function, f32> ) -> u32 {
|
|
177
|
+
|
|
178
|
+
let count = ${ countNode };
|
|
179
|
+
let totalWeight = ${ this.getLightsWeight }( position ) + envWeight;
|
|
180
|
+
if ( totalWeight <= 0.0 ) {
|
|
181
|
+
|
|
182
|
+
*selectionPdf = 0.0;
|
|
183
|
+
return 0u;
|
|
184
|
+
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let targetWeight = r * totalWeight;
|
|
188
|
+
var accumulated = 0.0;
|
|
189
|
+
var lastWeighted = 0u;
|
|
190
|
+
var index = count;
|
|
191
|
+
for ( var i = 0u; i < count; i ++ ) {
|
|
192
|
+
|
|
193
|
+
let lightWeight = ${ this.getLightWeight }( i, position );
|
|
194
|
+
accumulated += lightWeight;
|
|
195
|
+
if ( lightWeight > 0.0 ) {
|
|
196
|
+
|
|
197
|
+
lastWeighted = i;
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if ( targetWeight < accumulated ) {
|
|
202
|
+
|
|
203
|
+
index = i;
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// rounding can carry the target past the last light when there is no environment
|
|
211
|
+
if ( index == count && envWeight <= 0.0 ) {
|
|
212
|
+
|
|
213
|
+
index = lastWeighted;
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var weight = envWeight;
|
|
218
|
+
if ( index < count ) {
|
|
219
|
+
|
|
220
|
+
weight = ${ this.getLightWeight }( index, position );
|
|
221
|
+
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
*selectionPdf = weight / totalWeight;
|
|
225
|
+
return index;
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
// sample the light at the given index
|
|
93
231
|
this.randomLightSample = wgslTagFn/* wgsl */`
|
|
94
232
|
fn randomLightSample( lightIndex: u32, rayOrigin: vec3f, ruv: vec2f ) -> ${ lightRecordStruct } {
|
|
95
233
|
|
|
@@ -208,6 +208,8 @@ export class PathTracerBackend {
|
|
|
208
208
|
this.outputTarget.dispose();
|
|
209
209
|
this.prevOutputTarget.dispose();
|
|
210
210
|
this.sampleCountTarget.dispose();
|
|
211
|
+
this.sampleCountClearKernel.dispose();
|
|
212
|
+
this.outputTargetClearKernel.dispose();
|
|
211
213
|
|
|
212
214
|
this._renderTask = null;
|
|
213
215
|
|
|
@@ -257,6 +257,19 @@ export class WaveFrontPathTracer extends PathTracerBackend {
|
|
|
257
257
|
this.rayDispatchConverter.outputDispatch.dispose();
|
|
258
258
|
this.shadowDispatchConverter.outputDispatch.dispose();
|
|
259
259
|
|
|
260
|
+
this.populatePixelIndicesKernel.dispose();
|
|
261
|
+
this.logicKernel.dispose();
|
|
262
|
+
this.materialKernel.dispose();
|
|
263
|
+
this.traceRayKernel.dispose();
|
|
264
|
+
this.traceShadowRayKernel.dispose();
|
|
265
|
+
this.rayDispatchConverter.dispose();
|
|
266
|
+
this.shadowDispatchConverter.dispose();
|
|
267
|
+
this.primeSampleCountersKernel.dispose();
|
|
268
|
+
this.tallySampleCountsKernel.dispose();
|
|
269
|
+
this.resetSlotsKernel.dispose();
|
|
270
|
+
this.copyRayDataKernel.dispose();
|
|
271
|
+
this.zeroDispatchKernel.dispose();
|
|
272
|
+
|
|
260
273
|
}
|
|
261
274
|
|
|
262
275
|
// The queue holds every pixel not owned by a slot. It is sized to the full pixel count so slots
|
|
@@ -151,7 +151,7 @@ export class WebGPUPathTracer {
|
|
|
151
151
|
set maxBounces( v ) {
|
|
152
152
|
|
|
153
153
|
this._pathTracer.maxBounces = v;
|
|
154
|
-
this.
|
|
154
|
+
this.reset();
|
|
155
155
|
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -188,7 +188,7 @@ export class WebGPUPathTracer {
|
|
|
188
188
|
set maxTransparentBounces( v ) {
|
|
189
189
|
|
|
190
190
|
this._pathTracer.maxTransparentBounces = v;
|
|
191
|
-
this.
|
|
191
|
+
this.reset();
|
|
192
192
|
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -1275,14 +1275,20 @@ export class WebGPUPathTracer {
|
|
|
1275
1275
|
this._bvhData.textureAtlas.dispose();
|
|
1276
1276
|
this._environmentCache.dispose();
|
|
1277
1277
|
this._backgroundCache.dispose();
|
|
1278
|
-
this._blitQuad.dispose();
|
|
1279
1278
|
this._lowResTarget.dispose();
|
|
1279
|
+
|
|
1280
|
+
// FullScreenQuad.dispose only disposes the shared geometry, not the material
|
|
1281
|
+
this._blitQuad.dispose();
|
|
1282
|
+
this._blitQuad.material.dispose();
|
|
1280
1283
|
this._atlasDebugQuad?.dispose();
|
|
1284
|
+
this._atlasDebugQuad?.material.dispose();
|
|
1281
1285
|
this._sampleDensityQuad?.dispose();
|
|
1286
|
+
this._sampleDensityQuad?.material.dispose();
|
|
1282
1287
|
|
|
1283
1288
|
if ( this._debugBoundsQuad !== undefined ) {
|
|
1284
1289
|
|
|
1285
1290
|
this._debugBoundsQuad.dispose();
|
|
1291
|
+
this._debugBoundsQuad.material.dispose();
|
|
1286
1292
|
|
|
1287
1293
|
}
|
|
1288
1294
|
|
|
@@ -73,11 +73,21 @@ export class ComputeKernel {
|
|
|
73
73
|
|
|
74
74
|
// forward the call parameters so they remain accessible via "computeNode.parameters"
|
|
75
75
|
node.parameters = this._fn.parameters;
|
|
76
|
+
|
|
77
|
+
// release the renderer's pipeline and bindings for the kernel being replaced
|
|
78
|
+
this.kernel?.dispose();
|
|
76
79
|
this.kernel = node.computeKernel( [ x, y, z ] );
|
|
77
80
|
return this;
|
|
78
81
|
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
dispose() {
|
|
85
|
+
|
|
86
|
+
this.kernel?.dispose();
|
|
87
|
+
this.kernel = null;
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
getDispatchSize( tx = 1, ty = 1, tz = 1, target = [] ) {
|
|
82
92
|
|
|
83
93
|
const [ wgx, wgy, wgz ] = this.workgroupSize;
|
|
@@ -52,6 +52,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
52
52
|
|
|
53
53
|
const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
|
|
54
54
|
const raycastFirstHitFn = proxyFn( 'bvhData.value.fns.raycastFirstHit', params );
|
|
55
|
+
const raycastShadowFn = proxyFn( 'bvhData.value.fns.raycastShadow', params );
|
|
55
56
|
const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params );
|
|
56
57
|
const getSurfaceRecordFn = proxyFn( 'bvhData.value.fns.getSurfaceRecord', params );
|
|
57
58
|
const getCameraRayFn = proxyFn( 'bvhData.value.fns.getCameraRay', params );
|
|
@@ -64,12 +65,17 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
64
65
|
const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
|
|
65
66
|
const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
|
|
66
67
|
const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
|
|
68
|
+
const getEnvWeight = proxy( 'envInfo.value.getWeight', params );
|
|
67
69
|
const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
|
|
68
70
|
|
|
69
71
|
// analytic scene lights pulled off the lightsInfo provider ( LightsInfoNode )
|
|
70
72
|
const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
|
|
71
73
|
const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
|
|
72
74
|
const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
|
|
75
|
+
const getLightWeightFn = proxyFn( 'lightsInfo.value.getLightWeight', params );
|
|
76
|
+
const getLightsWeightFn = proxyFn( 'lightsInfo.value.getLightsWeight', params );
|
|
77
|
+
const selectLightFn = proxyFn( 'lightsInfo.value.selectLight', params );
|
|
78
|
+
const isLightVisibleToCameraFn = proxyFn( 'lightsInfo.value.isLightVisibleToCamera', params );
|
|
73
79
|
|
|
74
80
|
const shader = wgslTagFn/* wgsl */`
|
|
75
81
|
|
|
@@ -146,45 +152,58 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
146
152
|
// A negative value marks a sampled wavelength whose RGB reconstruction weight has not been applied yet.
|
|
147
153
|
var dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
|
|
148
154
|
|
|
149
|
-
// one-sample NEE
|
|
150
|
-
// lightsDenom is the number of options
|
|
155
|
+
// one-sample NEE chooses between the analytic lights and the environment
|
|
151
156
|
let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
|
|
152
157
|
let lightsCount = ${ lightsCountNode };
|
|
153
|
-
var
|
|
158
|
+
var envWeight = 0.0;
|
|
154
159
|
if ( envActive ) {
|
|
155
160
|
|
|
156
|
-
|
|
161
|
+
envWeight = ${ getEnvWeight }();
|
|
157
162
|
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
// added last, since a camera ray miss overwrites the pixel with the background
|
|
166
|
+
var lightHits = vec3f( 0.0 );
|
|
167
|
+
var cameraHitLight = false;
|
|
168
|
+
|
|
160
169
|
for ( var bounce = 0u; bounce < maxBounces; bounce ++ ) {
|
|
161
170
|
|
|
162
171
|
var hitResult: ${ raycastOutput };
|
|
163
172
|
let didHit = ${ raycastFirstHitFn }( ray, &hitResult );
|
|
164
173
|
let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
|
|
165
174
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
// NEE's total weight at the vertex this ray left, for MIS. Camera rays have none
|
|
176
|
+
let isMISWeighted = misEnabled != 0u && bounce > 0u;
|
|
177
|
+
var lightTotalWeight = 0.0;
|
|
178
|
+
if ( isMISWeighted ) {
|
|
169
179
|
|
|
170
|
-
|
|
180
|
+
lightTotalWeight = ${ getLightsWeightFn }( ray.origin ) + envWeight;
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
|
|
182
|
+
}
|
|
174
183
|
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
// forward hits on area lights. Camera rays only see visibleToCameraRays lights, at full weight
|
|
185
|
+
for ( var li = 0u; li < lightsCount; li ++ ) {
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
misWeight = ${ misHeuristicFn }( bsdfPdf, lightPdf );
|
|
187
|
+
if ( bounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
|
|
180
188
|
|
|
181
|
-
|
|
189
|
+
continue;
|
|
190
|
+
|
|
191
|
+
}
|
|
182
192
|
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
var lightRec: ${ lightRecordStruct };
|
|
194
|
+
if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
|
|
195
|
+
|
|
196
|
+
var misWeight = 1.0;
|
|
197
|
+
if ( isMISWeighted ) {
|
|
198
|
+
|
|
199
|
+
let selectionPdf = ${ getLightWeightFn }( li, ray.origin ) / lightTotalWeight;
|
|
200
|
+
misWeight = ${ misHeuristicFn }( bsdfPdf, lightRec.pdf * selectionPdf );
|
|
185
201
|
|
|
186
202
|
}
|
|
187
203
|
|
|
204
|
+
lightHits += ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, bounce, clampDirect, clampIndirect );
|
|
205
|
+
cameraHitLight = cameraHitLight || bounce == 0u;
|
|
206
|
+
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
}
|
|
@@ -263,30 +282,31 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
263
282
|
let emission = ${ clampPathContributionFunc }( throughputColor * surface.emission, bounce, clampDirect, clampIndirect );
|
|
264
283
|
resultColor += vec4f( emission, 0.0 );
|
|
265
284
|
|
|
266
|
-
// next event estimation:
|
|
267
|
-
//
|
|
268
|
-
if ( misEnabled != 0u
|
|
285
|
+
// next event estimation: choose one light or the environment by its estimated
|
|
286
|
+
// contribution
|
|
287
|
+
if ( misEnabled != 0u ) {
|
|
269
288
|
|
|
270
|
-
// pick one light or the environment with a single sample
|
|
271
|
-
// TODO: importance-sample the selection by light intensity and solid angle
|
|
272
289
|
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 };
|
|
290
|
+
var selectionPdf = 0.0;
|
|
291
|
+
let lightIndex = ${ selectLightFn }( vertexData.position.xyz, envWeight, ruv.x, &selectionPdf );
|
|
292
|
+
if ( selectionPdf > 0.0 ) {
|
|
284
293
|
|
|
285
|
-
|
|
294
|
+
var lightRec: ${ lightRecordStruct };
|
|
295
|
+
if ( envActive && lightIndex == lightsCount ) {
|
|
286
296
|
|
|
287
|
-
|
|
297
|
+
// the environment, sampled from its CDF, as a light of kind ENVIRONMENT
|
|
298
|
+
let envSample = ${ sampleEnvDir }( ruv.yz );
|
|
299
|
+
lightRec.direction = envSample.direction;
|
|
300
|
+
lightRec.emission = envSample.color;
|
|
301
|
+
lightRec.pdf = envSample.pdf;
|
|
302
|
+
lightRec.dist = ${ LIGHT_FAR_DISTANCE };
|
|
303
|
+
lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
|
|
288
304
|
|
|
289
|
-
|
|
305
|
+
} else {
|
|
306
|
+
|
|
307
|
+
lightRec = ${ randomLightSampleFn }( lightIndex, vertexData.position.xyz, ruv.yz );
|
|
308
|
+
|
|
309
|
+
}
|
|
290
310
|
|
|
291
311
|
if ( lightRec.pdf > 0.0 ) {
|
|
292
312
|
|
|
@@ -296,20 +316,21 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
296
316
|
var shadowRay: ${ rayStruct };
|
|
297
317
|
shadowRay.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, lightRec.direction, hitResult.normal );
|
|
298
318
|
shadowRay.direction = lightRec.direction;
|
|
299
|
-
shadowRay.maxDist = lightRec.dist - ${ LIGHT_EPSILON };
|
|
319
|
+
shadowRay.maxDist = lightRec.dist * ( 1.0 - ${ LIGHT_EPSILON } );
|
|
300
320
|
|
|
301
321
|
// opaque occlusion up to the light distance. A shadow-specific any hit
|
|
302
322
|
// traversal could support tinted shadows from transmissive and partially
|
|
303
323
|
// opaque objects
|
|
304
324
|
var shadowHit: ${ raycastOutput };
|
|
305
|
-
let occluded = ${
|
|
325
|
+
let occluded = ${ raycastShadowFn }( shadowRay, &shadowHit );
|
|
306
326
|
if ( ! occluded ) {
|
|
307
327
|
|
|
308
|
-
|
|
309
|
-
lightPdf /= lightsDenom;
|
|
328
|
+
let lightPdf = lightRec.pdf * selectionPdf;
|
|
310
329
|
|
|
311
|
-
// env + area lights are also bsdf-sampled, so MIS-weight them - punctual lights take full weight
|
|
312
|
-
|
|
330
|
+
// env + area lights are also bsdf-sampled, so MIS-weight them - punctual lights take full weight,
|
|
331
|
+
// or when a bsdf ray could never reach the light
|
|
332
|
+
let isMISWeighted = ${ isMISWeightLightFn }( lightRec.lightType ) && ! shadowHit.forwardBlocked;
|
|
333
|
+
let misWeight = select( 1.0, ${ misHeuristicFn }( lightPdf, evalRec.pdf ), isMISWeighted );
|
|
313
334
|
let directLight = throughputColor * lightRec.emission * evalRec.color * misWeight / lightPdf;
|
|
314
335
|
let contribution = ${ clampPathContributionFunc }( directLight, bounce + 1u, clampDirect, clampIndirect );
|
|
315
336
|
resultColor += vec4f( contribution, 0.0 );
|
|
@@ -320,6 +341,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
320
341
|
|
|
321
342
|
}
|
|
322
343
|
|
|
344
|
+
}
|
|
345
|
+
|
|
323
346
|
}
|
|
324
347
|
|
|
325
348
|
let scatterRec = ${ bsdfSampleFn }( view, surface );
|
|
@@ -370,10 +393,10 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
370
393
|
if ( bounce > 0u && ! isFullyTransmissive ) {
|
|
371
394
|
|
|
372
395
|
var misWeight = 1.0;
|
|
373
|
-
if (
|
|
396
|
+
if ( isMISWeighted && envActive ) {
|
|
374
397
|
|
|
375
398
|
// match the env pdf scaling used by the NEE selection so the two estimators balance
|
|
376
|
-
let envPdf = ${ getEnvDirPdf }( ray.direction ) /
|
|
399
|
+
let envPdf = ${ getEnvDirPdf }( ray.direction ) * envWeight / lightTotalWeight;
|
|
377
400
|
misWeight = ${ misHeuristicFn }( bsdfPdf, envPdf );
|
|
378
401
|
|
|
379
402
|
}
|
|
@@ -450,6 +473,14 @@ export class PathTracerMegaKernel extends ComputeKernel {
|
|
|
450
473
|
|
|
451
474
|
}
|
|
452
475
|
|
|
476
|
+
// visible lights are opaque against a transparent background
|
|
477
|
+
resultColor += vec4f( lightHits, 0.0 );
|
|
478
|
+
if ( cameraHitLight ) {
|
|
479
|
+
|
|
480
|
+
resultColor.a = 1.0;
|
|
481
|
+
|
|
482
|
+
}
|
|
483
|
+
|
|
453
484
|
let nextSampleCount = sampleCount + 1;
|
|
454
485
|
// store the color rows top down to match a rasterized render target
|
|
455
486
|
let colorIndex = vec2u( indexUV.x, targetDimensions.y - 1u - indexUV.y );
|