three-gpu-pathtracer 0.0.17 → 0.0.19

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.
Files changed (36) hide show
  1. package/build/index.module.js +1013 -322
  2. package/build/index.module.js.map +1 -1
  3. package/build/index.umd.cjs +1010 -320
  4. package/build/index.umd.cjs.map +1 -1
  5. package/package.json +2 -2
  6. package/src/core/DynamicPathTracingSceneGenerator.js +80 -40
  7. package/src/core/PathTracingRenderer.js +28 -34
  8. package/src/core/PathTracingSceneGenerator.js +11 -60
  9. package/src/materials/pathtracing/LambertPathTracingMaterial.js +1 -1
  10. package/src/materials/pathtracing/PhysicalPathTracingMaterial.js +37 -12
  11. package/src/materials/pathtracing/glsl/attenuateHit.glsl.js +19 -9
  12. package/src/materials/pathtracing/glsl/cameraUtils.glsl.js +2 -2
  13. package/src/materials/pathtracing/glsl/directLightContribution.glsl.js +4 -4
  14. package/src/materials/pathtracing/glsl/getSurfaceRecord.glsl.js +2 -1
  15. package/src/materials/pathtracing/glsl/traceScene.glsl.js +1 -1
  16. package/src/shader/bsdf/bsdfSampling.glsl.js +14 -10
  17. package/src/shader/common/fresnel.glsl.js +15 -9
  18. package/src/shader/rand/pcg.glsl.js +4 -4
  19. package/src/shader/rand/stratifiedTexture.glsl.js +45 -0
  20. package/src/shader/sampling/equirectSampling.glsl.js +8 -1
  21. package/src/shader/structs/lightsStruct.glsl.js +5 -7
  22. package/src/textures/BlueNoiseTexture.js +87 -0
  23. package/src/textures/ProceduralEquirectTexture.js +7 -8
  24. package/src/textures/blueNoise/BlueNoiseGenerator.js +115 -0
  25. package/src/textures/blueNoise/BlueNoiseSamples.js +214 -0
  26. package/src/textures/blueNoise/utils.js +24 -0
  27. package/src/uniforms/EquirectHdrInfoUniform.js +45 -8
  28. package/src/uniforms/LightsInfoUniformStruct.js +11 -7
  29. package/src/uniforms/MaterialsTexture.js +1 -1
  30. package/src/uniforms/RenderTarget2DArray.js +50 -3
  31. package/src/uniforms/StratifiedSamplesTexture.js +49 -0
  32. package/src/uniforms/stratified/StratifiedSampler.js +73 -0
  33. package/src/uniforms/stratified/StratifiedSamplerCombined.js +59 -0
  34. package/src/uniforms/utils.js +1 -1
  35. package/src/utils/GeometryPreparationUtils.js +8 -101
  36. package/src/workers/PathTracingSceneWorker.js +18 -8
@@ -457,9 +457,6 @@
457
457
 
458
458
  }
459
459
 
460
- const _scissor = new three.Vector4();
461
- const _viewport = new three.Vector4();
462
-
463
460
  function* renderTask() {
464
461
 
465
462
  const {
@@ -501,12 +498,21 @@
501
498
  const h = _primaryTarget.height;
502
499
  material.resolution.set( w * subW, h * subH );
503
500
  material.sobolTexture = _sobolTarget.texture;
501
+ material.stratifiedTexture.init( 20, material.bounces + material.transmissiveBounces + 5 );
502
+ material.stratifiedTexture.next();
504
503
  material.seed ++;
505
504
 
506
505
  const tilesX = this.tiles.x || 1;
507
506
  const tilesY = this.tiles.y || 1;
508
507
  const totalTiles = tilesX * tilesY;
509
- const dprInv = ( 1 / _renderer.getPixelRatio() );
508
+
509
+ const pxSubW = Math.ceil( w * subW );
510
+ const pxSubH = Math.ceil( h * subH );
511
+ const pxSubX = Math.floor( subX * w );
512
+ const pxSubY = Math.floor( subY * h );
513
+
514
+ const pxTileW = Math.ceil( pxSubW / tilesX );
515
+ const pxTileH = Math.ceil( pxSubH / tilesY );
510
516
 
511
517
  for ( let y = 0; y < tilesY; y ++ ) {
512
518
 
@@ -555,40 +561,28 @@
555
561
 
556
562
  }
557
563
 
564
+ // set the scissor and the viewport on the render target
565
+ // note that when using the webgl renderer set viewport the device pixel ratio
566
+ // is multiplied into the field causing some pixels to not be rendered
567
+ const reverseTy = tilesY - ty - 1;
568
+ _primaryTarget.scissor.set(
569
+ pxSubX + tx * pxTileW,
570
+ pxSubY + reverseTy * pxTileH,
571
+ Math.min( pxTileW, pxSubW - tx * pxTileW ),
572
+ Math.min( pxTileH, pxSubH - reverseTy * pxTileH ),
573
+ );
574
+
575
+ _primaryTarget.viewport.set(
576
+ pxSubX,
577
+ pxSubY,
578
+ pxSubW,
579
+ pxSubH,
580
+ );
581
+
558
582
  // three.js renderer takes values relative to the current pixel ratio
559
583
  _renderer.setRenderTarget( _primaryTarget );
560
584
  _renderer.setScissorTest( true );
561
585
 
562
- // set the scissor window for a subtile
563
- _scissor.x = tx * w / tilesX;
564
- _scissor.y = ( tilesY - ty - 1 ) * h / tilesY;
565
- _scissor.z = w / tilesX;
566
- _scissor.w = h / tilesY;
567
-
568
- // adjust for the subframe
569
- _scissor.x = subX * w + subW * _scissor.x;
570
- _scissor.y = subY * h + subH * _scissor.y;
571
- _scissor.z = subW * _scissor.z;
572
- _scissor.w = subH * _scissor.w;
573
-
574
- // round for floating point cases
575
- _scissor.x = _scissor.x;
576
- _scissor.y = _scissor.y;
577
- _scissor.z = _scissor.z;
578
- _scissor.w = _scissor.w;
579
-
580
- // multiply inverse of DPR in because threes multiplies it in
581
- _scissor.multiplyScalar( dprInv ).ceil();
582
-
583
- _viewport.x = subX * w;
584
- _viewport.y = subY * h;
585
- _viewport.z = subW * w;
586
- _viewport.w = subH * h;
587
- _viewport.multiplyScalar( dprInv ).ceil();
588
-
589
- _renderer.setScissor( _scissor );
590
- _renderer.setViewport( _viewport );
591
-
592
586
  _renderer.autoClear = false;
593
587
  _fsQuad.render( _renderer );
594
588
 
@@ -1078,25 +1072,6 @@
1078
1072
 
1079
1073
  }
1080
1074
 
1081
- function trimToAttributes( geometry, attributes ) {
1082
-
1083
- // trim any unneeded attributes
1084
- if ( attributes ) {
1085
-
1086
- for ( const key in geometry.attributes ) {
1087
-
1088
- if ( ! attributes.includes( key ) ) {
1089
-
1090
- geometry.deleteAttribute( key );
1091
-
1092
- }
1093
-
1094
- }
1095
-
1096
- }
1097
-
1098
- }
1099
-
1100
1075
  function setCommonAttributes( geometry, options ) {
1101
1076
 
1102
1077
  const { attributes = [], normalMapRequired = false } = options;
@@ -1114,6 +1089,13 @@
1114
1089
 
1115
1090
  }
1116
1091
 
1092
+ if ( ! geometry.attributes.uv2 && ( attributes && attributes.includes( 'uv2' ) ) ) {
1093
+
1094
+ const vertCount = geometry.attributes.position.count;
1095
+ geometry.setAttribute( 'uv2', new three.BufferAttribute( new Float32Array( vertCount * 2 ), 2, false ) );
1096
+
1097
+ }
1098
+
1117
1099
  if ( ! geometry.attributes.tangent && ( attributes && attributes.includes( 'tangent' ) ) ) {
1118
1100
 
1119
1101
  if ( normalMapRequired ) {
@@ -1162,169 +1144,46 @@
1162
1144
 
1163
1145
  }
1164
1146
 
1165
- function mergeMeshes( meshes, options = {} ) {
1166
-
1167
- options = { attributes: null, cloneGeometry: true, ...options };
1147
+ const dummyMaterial = new three.MeshBasicMaterial();
1148
+ function getDummyMesh() {
1168
1149
 
1169
- const transformedGeometry = [];
1170
- const materialSet = new Set();
1171
- for ( let i = 0, l = meshes.length; i < l; i ++ ) {
1150
+ const emptyGeometry = new three.BufferGeometry();
1151
+ emptyGeometry.setAttribute( 'position', new three.BufferAttribute( new Float32Array( 9 ), 3 ) );
1152
+ return new three.Mesh( emptyGeometry, dummyMaterial );
1172
1153
 
1173
- // save any materials
1174
- const mesh = meshes[ i ];
1175
- if ( mesh.visible === false ) continue;
1176
-
1177
- if ( Array.isArray( mesh.material ) ) {
1178
-
1179
- mesh.material.forEach( m => materialSet.add( m ) );
1180
-
1181
- } else {
1182
-
1183
- materialSet.add( mesh.material );
1184
-
1185
- }
1186
-
1187
- }
1188
-
1189
- const materials = Array.from( materialSet );
1190
- for ( let i = 0, l = meshes.length; i < l; i ++ ) {
1191
-
1192
- // ensure the matrix world is up to date
1193
- const mesh = meshes[ i ];
1194
- if ( mesh.visible === false ) continue;
1195
-
1196
- mesh.updateMatrixWorld();
1197
-
1198
- // apply the matrix world to the geometry
1199
- const originalGeometry = meshes[ i ].geometry;
1200
- const geometry = options.cloneGeometry ? originalGeometry.clone() : originalGeometry;
1201
- geometry.applyMatrix4( mesh.matrixWorld );
1202
-
1203
- if ( mesh.matrixWorld.determinant() < 0 ) {
1204
-
1205
- geometry.index.array.reverse();
1206
-
1207
- }
1154
+ }
1208
1155
 
1209
- // ensure our geometry has common attributes
1210
- setCommonAttributes( geometry, {
1211
- attributes: options.attributes,
1212
- normalMapRequired: ! ! mesh.material.normalMap,
1213
- } );
1214
- trimToAttributes( geometry, options.attributes );
1156
+ class DynamicPathTracingSceneGenerator {
1215
1157
 
1216
- // create the material index attribute
1217
- const materialIndexAttribute = getGroupMaterialIndicesAttribute( geometry, mesh.material, materials );
1218
- geometry.setAttribute( 'materialIndex', materialIndexAttribute );
1158
+ get initialized() {
1219
1159
 
1220
- transformedGeometry.push( geometry );
1160
+ return Boolean( this.bvh );
1221
1161
 
1222
1162
  }
1223
1163
 
1224
- const textureSet = new Set();
1225
- materials.forEach( material => {
1226
-
1227
- for ( const key in material ) {
1164
+ constructor( objects ) {
1228
1165
 
1229
- const value = material[ key ];
1230
- if ( value && value.isTexture ) {
1166
+ // ensure the objects is an array
1167
+ if ( ! Array.isArray( objects ) ) {
1231
1168
 
1232
- textureSet.add( value );
1233
-
1234
- }
1169
+ objects = [ objects ];
1235
1170
 
1236
1171
  }
1237
1172
 
1238
- } );
1239
-
1240
- const geometry = BufferGeometryUtils_js.mergeGeometries( transformedGeometry, false );
1241
- const textures = Array.from( textureSet );
1242
- return { geometry, materials, textures };
1243
-
1244
- }
1245
-
1246
- class PathTracingSceneGenerator {
1247
-
1248
- prepScene( scene ) {
1173
+ // use a dummy object for a fallback
1174
+ const finalObjects = [ ...objects ];
1175
+ if ( finalObjects.length === 0 ) {
1249
1176
 
1250
- scene = Array.isArray( scene ) ? scene : [ scene ];
1251
-
1252
- const meshes = [];
1253
- const lights = [];
1254
-
1255
- for ( let i = 0, l = scene.length; i < l; i ++ ) {
1256
-
1257
- scene[ i ].traverseVisible( c => {
1258
-
1259
- if ( c.isSkinnedMesh || c.isMesh && c.morphTargetInfluences ) {
1260
-
1261
- const generator = new threeMeshBvh.StaticGeometryGenerator( c );
1262
- generator.attributes = [ 'position', 'color', 'normal', 'tangent', 'uv', 'uv2' ];
1263
- generator.applyWorldTransforms = false;
1264
- const mesh = new three.Mesh(
1265
- generator.generate(),
1266
- c.material,
1267
- );
1268
- mesh.matrixWorld.copy( c.matrixWorld );
1269
- mesh.matrix.copy( c.matrixWorld );
1270
- mesh.matrix.decompose( mesh.position, mesh.quaternion, mesh.scale );
1271
- meshes.push( mesh );
1272
-
1273
- } else if ( c.isMesh ) {
1274
-
1275
- meshes.push( c );
1276
-
1277
- } else if (
1278
- c.isRectAreaLight ||
1279
- c.isSpotLight ||
1280
- c.isPointLight ||
1281
- c.isDirectionalLight
1282
- ) {
1283
-
1284
- lights.push( c );
1285
-
1286
- }
1287
-
1288
- } );
1177
+ finalObjects.push( getDummyMesh() );
1289
1178
 
1290
1179
  }
1291
1180
 
1292
- return {
1293
- ...mergeMeshes( meshes, {
1294
- attributes: [ 'position', 'normal', 'tangent', 'uv', 'color' ],
1295
- } ),
1296
- lights,
1297
- };
1298
-
1299
- }
1300
-
1301
- generate( scene, options = {} ) {
1302
-
1303
- const { materials, textures, geometry, lights } = this.prepScene( scene );
1304
- const bvhOptions = { strategy: threeMeshBvh.SAH, ...options, maxLeafTris: 1 };
1305
- return {
1306
- scene,
1307
- materials,
1308
- textures,
1309
- lights,
1310
- bvh: new threeMeshBvh.MeshBVH( geometry, bvhOptions ),
1311
- };
1312
-
1313
- }
1314
-
1315
- }
1316
-
1317
- class DynamicPathTracingSceneGenerator {
1318
-
1319
- get initialized() {
1320
-
1321
- return Boolean( this.bvh );
1322
-
1323
- }
1324
-
1325
- constructor( scene ) {
1181
+ // options
1182
+ this.bvhOptions = {};
1183
+ this.attributes = [ 'position', 'normal', 'tangent', 'color', 'uv', 'uv2' ];
1326
1184
 
1327
- this.objects = Array.isArray( scene ) ? scene : [ scene ];
1185
+ // state
1186
+ this.objects = finalObjects;
1328
1187
  this.bvh = null;
1329
1188
  this.geometry = new three.BufferGeometry();
1330
1189
  this.materials = null;
@@ -1348,64 +1207,75 @@
1348
1207
 
1349
1208
  dispose() {}
1350
1209
 
1351
- generate() {
1210
+ prepScene() {
1352
1211
 
1353
- const { objects, staticGeometryGenerator, geometry, lights } = this;
1354
- if ( this.bvh === null ) {
1212
+ if ( this.bvh !== null ) {
1355
1213
 
1356
- const attributes = [ 'position', 'normal', 'tangent', 'uv', 'color' ];
1214
+ return;
1357
1215
 
1358
- for ( let i = 0, l = objects.length; i < l; i ++ ) {
1216
+ }
1359
1217
 
1360
- objects[ i ].traverse( c => {
1218
+ const { objects, staticGeometryGenerator, geometry, lights, attributes } = this;
1219
+ for ( let i = 0, l = objects.length; i < l; i ++ ) {
1361
1220
 
1362
- if ( c.isMesh ) {
1221
+ objects[ i ].traverse( c => {
1363
1222
 
1364
- const normalMapRequired = ! ! c.material.normalMap;
1365
- setCommonAttributes( c.geometry, { attributes, normalMapRequired } );
1223
+ if ( c.isMesh ) {
1366
1224
 
1367
- } else if (
1368
- c.isRectAreaLight ||
1369
- c.isSpotLight ||
1370
- c.isPointLight ||
1371
- c.isDirectionalLight
1372
- ) {
1225
+ const normalMapRequired = ! ! c.material.normalMap;
1226
+ setCommonAttributes( c.geometry, { attributes, normalMapRequired } );
1373
1227
 
1374
- lights.push( c );
1228
+ } else if (
1229
+ c.isRectAreaLight ||
1230
+ c.isSpotLight ||
1231
+ c.isPointLight ||
1232
+ c.isDirectionalLight
1233
+ ) {
1375
1234
 
1376
- }
1235
+ lights.push( c );
1377
1236
 
1378
- } );
1237
+ }
1379
1238
 
1380
- }
1239
+ } );
1381
1240
 
1382
- const textureSet = new Set();
1383
- const materials = staticGeometryGenerator.getMaterials();
1384
- materials.forEach( material => {
1241
+ }
1385
1242
 
1386
- for ( const key in material ) {
1243
+ const textureSet = new Set();
1244
+ const materials = staticGeometryGenerator.getMaterials();
1245
+ materials.forEach( material => {
1387
1246
 
1388
- const value = material[ key ];
1389
- if ( value && value.isTexture ) {
1247
+ for ( const key in material ) {
1390
1248
 
1391
- textureSet.add( value );
1249
+ const value = material[ key ];
1250
+ if ( value && value.isTexture ) {
1392
1251
 
1393
- }
1252
+ textureSet.add( value );
1394
1253
 
1395
1254
  }
1396
1255
 
1397
- } );
1256
+ }
1398
1257
 
1399
- staticGeometryGenerator.attributes = attributes;
1400
- staticGeometryGenerator.generate( geometry );
1258
+ } );
1259
+
1260
+ staticGeometryGenerator.attributes = attributes;
1261
+ staticGeometryGenerator.generate( geometry );
1262
+
1263
+ const materialIndexAttribute = getGroupMaterialIndicesAttribute( geometry, materials, materials );
1264
+ geometry.setAttribute( 'materialIndex', materialIndexAttribute );
1265
+ geometry.clearGroups();
1266
+
1267
+ this.materials = materials;
1268
+ this.textures = Array.from( textureSet );
1269
+
1270
+ }
1401
1271
 
1402
- const materialIndexAttribute = getGroupMaterialIndicesAttribute( geometry, materials, materials );
1403
- geometry.setAttribute( 'materialIndex', materialIndexAttribute );
1404
- geometry.clearGroups();
1272
+ generate() {
1273
+
1274
+ const { objects, staticGeometryGenerator, geometry, bvhOptions } = this;
1275
+ if ( this.bvh === null ) {
1405
1276
 
1406
- this.bvh = new threeMeshBvh.MeshBVH( geometry );
1407
- this.materials = materials;
1408
- this.textures = Array.from( textureSet );
1277
+ this.prepScene();
1278
+ this.bvh = new threeMeshBvh.MeshBVH( geometry, { strategy: threeMeshBvh.SAH, maxLeafTris: 1, ...bvhOptions } );
1409
1279
 
1410
1280
  return {
1411
1281
  lights: this.lights,
@@ -1435,6 +1305,30 @@
1435
1305
 
1436
1306
  }
1437
1307
 
1308
+ class PathTracingSceneGenerator {
1309
+
1310
+ generate( scene, options = {} ) {
1311
+
1312
+ // ensure scene transforms are up to date
1313
+ // TODO: remove this?
1314
+ if ( Array.isArray( scene ) ) {
1315
+
1316
+ scene.forEach( s => s.updateMatrixWorld( true ) );
1317
+
1318
+ } else {
1319
+
1320
+ scene.updateMatrixWorld( true );
1321
+
1322
+ }
1323
+
1324
+ const generator = new DynamicPathTracingSceneGenerator( scene );
1325
+ generator.bvhOptions = options;
1326
+ return generator.generate();
1327
+
1328
+ }
1329
+
1330
+ }
1331
+
1438
1332
  // https://github.com/gkjohnson/webxr-sandbox/blob/main/skinned-mesh-batching/src/MaterialReducer.js
1439
1333
 
1440
1334
  function isTypedArray( arr ) {
@@ -1799,8 +1693,8 @@
1799
1693
  constructor( width = 512, height = 512 ) {
1800
1694
 
1801
1695
  super(
1802
- new Uint16Array( width * height * 4 ),
1803
- width, height, three.RGBAFormat, three.HalfFloatType, three.EquirectangularReflectionMapping,
1696
+ new Float32Array( width * height * 4 ),
1697
+ width, height, three.RGBAFormat, three.FloatType, three.EquirectangularReflectionMapping,
1804
1698
  three.RepeatWrapping, three.ClampToEdgeWrapping, three.LinearFilter, three.LinearFilter,
1805
1699
  );
1806
1700
 
@@ -1832,10 +1726,10 @@
1832
1726
 
1833
1727
  const i = y * width + x;
1834
1728
  const i4 = 4 * i;
1835
- data[ i4 + 0 ] = three.DataUtils.toHalfFloat( _color.r );
1836
- data[ i4 + 1 ] = three.DataUtils.toHalfFloat( _color.g );
1837
- data[ i4 + 2 ] = three.DataUtils.toHalfFloat( _color.b );
1838
- data[ i4 + 3 ] = three.DataUtils.toHalfFloat( 1.0 );
1729
+ data[ i4 + 0 ] = ( _color.r );
1730
+ data[ i4 + 1 ] = ( _color.g );
1731
+ data[ i4 + 2 ] = ( _color.b );
1732
+ data[ i4 + 3 ] = ( 1.0 );
1839
1733
 
1840
1734
  }
1841
1735
 
@@ -1890,7 +1784,7 @@
1890
1784
  // when rendering each texture to the texture array they must have a consistent color space.
1891
1785
  function getTextureHash( t ) {
1892
1786
 
1893
- return `${ t.source.uuid }:${ t.encoding }`;
1787
+ return `${ t.source.uuid }:${ t.colorSpace }`;
1894
1788
 
1895
1789
  }
1896
1790
 
@@ -2102,7 +1996,7 @@
2102
1996
 
2103
1997
  let index = 0;
2104
1998
  const pixelCount = materials.length * MATERIAL_PIXELS;
2105
- const dimension = Math.ceil( Math.sqrt( pixelCount ) );
1999
+ const dimension = Math.ceil( Math.sqrt( pixelCount ) ) || 1;
2106
2000
  const { threeCompatibilityTransforms, image, features } = this;
2107
2001
 
2108
2002
  // get the list of textures with unique sources
@@ -2440,7 +2334,7 @@
2440
2334
 
2441
2335
  };
2442
2336
 
2443
- const fsQuad = new Pass_js.FullScreenQuad( new three.MeshBasicMaterial() );
2337
+ const fsQuad = new Pass_js.FullScreenQuad( new CopyMaterial() );
2444
2338
  this.fsQuad = fsQuad;
2445
2339
 
2446
2340
  }
@@ -2475,7 +2369,6 @@
2475
2369
  texture.matrix.identity();
2476
2370
 
2477
2371
  fsQuad.material.map = texture;
2478
- fsQuad.material.transparent = true;
2479
2372
 
2480
2373
  renderer.setRenderTarget( this, i );
2481
2374
  fsQuad.render( renderer );
@@ -2505,6 +2398,54 @@
2505
2398
 
2506
2399
  }
2507
2400
 
2401
+ class CopyMaterial extends three.ShaderMaterial {
2402
+
2403
+ get map() {
2404
+
2405
+ return this.uniforms.map.value;
2406
+
2407
+ }
2408
+ set map( v ) {
2409
+
2410
+ this.uniforms.map.value = v;
2411
+
2412
+ }
2413
+
2414
+ constructor() {
2415
+
2416
+ super( {
2417
+ uniforms: {
2418
+
2419
+ map: { value: null },
2420
+
2421
+ },
2422
+
2423
+ vertexShader: /* glsl */`
2424
+ varying vec2 vUv;
2425
+ void main() {
2426
+
2427
+ vUv = uv;
2428
+ gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
2429
+
2430
+ }
2431
+ `,
2432
+
2433
+ fragmentShader: /* glsl */`
2434
+ uniform sampler2D map;
2435
+ varying vec2 vUv;
2436
+ void main() {
2437
+
2438
+ gl_FragColor = texture2D( map, vUv );
2439
+
2440
+ }
2441
+ `
2442
+ } );
2443
+
2444
+ }
2445
+
2446
+
2447
+ }
2448
+
2508
2449
  function toHalfFloatArray( f32Array ) {
2509
2450
 
2510
2451
  const f16Array = new Uint16Array( f32Array.length );
@@ -2556,7 +2497,7 @@
2556
2497
  }
2557
2498
 
2558
2499
  // ensures the data is all floating point values and flipY is false
2559
- function preprocessEnvMap( envMap ) {
2500
+ function preprocessEnvMap( envMap, targetType = three.HalfFloatType ) {
2560
2501
 
2561
2502
  const map = envMap.clone();
2562
2503
  map.source = new three.Source( { ...map.image } );
@@ -2565,17 +2506,54 @@
2565
2506
  // TODO: is there a simple way to avoid cloning and adjusting the env map data here?
2566
2507
  // convert the data from half float uint 16 arrays to float arrays for cdf computation
2567
2508
  let newData = data;
2568
- if ( map.type === three.HalfFloatType ) {
2509
+ if ( map.type !== targetType ) {
2510
+
2511
+ if ( targetType === three.HalfFloatType ) {
2512
+
2513
+ newData = new Uint16Array( data.length );
2514
+
2515
+ } else {
2516
+
2517
+ newData = new Float32Array( data.length );
2518
+
2519
+ }
2520
+
2521
+ let maxIntValue;
2522
+ if ( data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array ) {
2523
+
2524
+ maxIntValue = 2 ** ( 8 * data.BYTES_PER_ELEMENT - 1 ) - 1;
2525
+
2526
+ } else {
2527
+
2528
+ maxIntValue = 2 ** ( 8 * data.BYTES_PER_ELEMENT ) - 1;
2529
+
2530
+ }
2531
+
2532
+ for ( let i = 0, l = data.length; i < l; i ++ ) {
2533
+
2534
+ let v = data[ i ];
2535
+ if ( map.type === three.HalfFloatType ) {
2536
+
2537
+ v = three.DataUtils.fromHalfFloat( data[ i ] );
2538
+
2539
+ }
2540
+
2541
+ if ( map.type !== three.FloatType && map.type !== three.HalfFloatType ) {
2542
+
2543
+ v /= maxIntValue;
2544
+
2545
+ }
2546
+
2547
+ if ( targetType === three.HalfFloatType ) {
2569
2548
 
2570
- newData = new Uint16Array( data.length );
2571
- for ( const i in data ) {
2549
+ newData[ i ] = three.DataUtils.toHalfFloat( v );
2572
2550
 
2573
- newData[ i ] = data[ i ];
2551
+ }
2574
2552
 
2575
2553
  }
2576
2554
 
2577
2555
  map.image.data = newData;
2578
- map.type = three.HalfFloatType;
2556
+ map.type = targetType;
2579
2557
 
2580
2558
  }
2581
2559
 
@@ -2667,7 +2645,7 @@
2667
2645
  // https://pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Sampling_Light_Sources#InfiniteAreaLights
2668
2646
  const map = preprocessEnvMap( hdr );
2669
2647
  map.wrapS = three.RepeatWrapping;
2670
- map.wrapT = three.RepeatWrapping;
2648
+ map.wrapT = three.ClampToEdgeWrapping;
2671
2649
 
2672
2650
  const { width, height, data } = map.image;
2673
2651
 
@@ -2879,6 +2857,13 @@
2879
2857
  const baseIndex = i * LIGHT_PIXELS * 4;
2880
2858
  let index = 0;
2881
2859
 
2860
+ // initialize to 0
2861
+ for ( let p = 0; p < LIGHT_PIXELS * 4; p ++ ) {
2862
+
2863
+ floatArray[ baseIndex + p ] = 0;
2864
+
2865
+ }
2866
+
2882
2867
  // sample 1
2883
2868
  // position
2884
2869
  l.getWorldPosition( v );
@@ -2943,7 +2928,7 @@
2943
2928
 
2944
2929
  } else if ( l.isSpotLight ) {
2945
2930
 
2946
- const radius = l.radius;
2931
+ const radius = l.radius || 0;
2947
2932
  eye.setFromMatrixPosition( l.matrixWorld );
2948
2933
  target.setFromMatrixPosition( l.target.matrixWorld );
2949
2934
  m.lookAt( eye, target, up );
@@ -2973,24 +2958,21 @@
2973
2958
  // radius
2974
2959
  floatArray[ baseIndex + ( index ++ ) ] = radius;
2975
2960
 
2976
- // near
2977
- floatArray[ baseIndex + ( index ++ ) ] = l.shadow.camera.near;
2978
-
2979
2961
  // decay
2980
2962
  floatArray[ baseIndex + ( index ++ ) ] = l.decay;
2981
2963
 
2982
2964
  // distance
2983
2965
  floatArray[ baseIndex + ( index ++ ) ] = l.distance;
2984
2966
 
2985
- // sample 6
2986
2967
  // coneCos
2987
2968
  floatArray[ baseIndex + ( index ++ ) ] = Math.cos( l.angle );
2988
2969
 
2970
+ // sample 6
2989
2971
  // penumbraCos
2990
2972
  floatArray[ baseIndex + ( index ++ ) ] = Math.cos( l.angle * ( 1 - l.penumbra ) );
2991
2973
 
2992
2974
  // iesProfile
2993
- floatArray[ baseIndex + ( index ++ ) ] = iesTextures.indexOf( l.iesTexture );
2975
+ floatArray[ baseIndex + ( index ++ ) ] = l.iesTexture ? iesTextures.indexOf( l.iesTexture ) : - 1;
2994
2976
 
2995
2977
  } else if ( l.isPointLight ) {
2996
2978
 
@@ -3007,7 +2989,7 @@
3007
2989
  index += 4;
3008
2990
 
3009
2991
  // sample 5
3010
- index += 2;
2992
+ index += 1;
3011
2993
 
3012
2994
  floatArray[ baseIndex + ( index ++ ) ] = l.decay;
3013
2995
  floatArray[ baseIndex + ( index ++ ) ] = l.distance;
@@ -4421,18 +4403,16 @@
4421
4403
  vec4 s4 = texelFetch1D( tex, i + 4u );
4422
4404
  vec4 s5 = texelFetch1D( tex, i + 5u );
4423
4405
  l.radius = s4.r;
4424
- l.near = s4.g;
4425
- l.decay = s4.b;
4426
- l.distance = s4.a;
4406
+ l.decay = s4.g;
4407
+ l.distance = s4.b;
4408
+ l.coneCos = s4.a;
4427
4409
 
4428
- l.coneCos = s5.r;
4429
- l.penumbraCos = s5.g;
4430
- l.iesProfile = int( round( s5.b ) );
4410
+ l.penumbraCos = s5.r;
4411
+ l.iesProfile = int( round( s5.g ) );
4431
4412
 
4432
4413
  } else {
4433
4414
 
4434
4415
  l.radius = 0.0;
4435
- l.near = 0.0;
4436
4416
  l.decay = 0.0;
4437
4417
  l.distance = 0.0;
4438
4418
 
@@ -5150,15 +5130,17 @@ bool bvhIntersectFogVolumeHit(
5150
5130
 
5151
5131
  // TODO: subsurface approx?
5152
5132
 
5153
- float F = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
5133
+ // float F = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
5134
+ float F = disneyFresnel( wo, wi, wh, surf.f0, surf.eta, surf.metalness );
5154
5135
  color = ( 1.0 - F ) * transFactor * metalFactor * wi.z * surf.color * ( retro + lambert ) / PI;
5136
+
5155
5137
  return wi.z / PI;
5156
5138
 
5157
5139
  }
5158
5140
 
5159
5141
  vec3 diffuseDirection( vec3 wo, SurfaceRecord surf ) {
5160
5142
 
5161
- vec3 lightDirection = sampleSphere( sobol2( 11 ) );
5143
+ vec3 lightDirection = sampleSphere( rand2( 11 ) );
5162
5144
  lightDirection.z += 1.0;
5163
5145
  lightDirection = normalize( lightDirection );
5164
5146
 
@@ -5203,7 +5185,7 @@ bool bvhIntersectFogVolumeHit(
5203
5185
  vec3 halfVector = ggxDirection(
5204
5186
  wo,
5205
5187
  vec2( roughness ),
5206
- sobol2( 12 )
5188
+ rand2( 12 )
5207
5189
  );
5208
5190
 
5209
5191
  // apply to new ray by reflecting off the new normal
@@ -5240,7 +5222,7 @@ bool bvhIntersectFogVolumeHit(
5240
5222
  vec3 halfVector = ggxDirection(
5241
5223
  wo,
5242
5224
  vec2( filteredRoughness ),
5243
- sobol2( 13 )
5225
+ rand2( 13 )
5244
5226
  );
5245
5227
 
5246
5228
  vec3 lightDirection = refract( normalize( - wo ), halfVector, eta );
@@ -5262,7 +5244,8 @@ bool bvhIntersectFogVolumeHit(
5262
5244
  color = surf.transmission * surf.color;
5263
5245
 
5264
5246
  // PDF
5265
- float F = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
5247
+ // float F = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
5248
+ float F = disneyFresnel( wo, wi, wh, surf.f0, surf.eta, surf.metalness );
5266
5249
  if ( F >= 1.0 ) {
5267
5250
 
5268
5251
  return 0.0;
@@ -5277,7 +5260,7 @@ bool bvhIntersectFogVolumeHit(
5277
5260
 
5278
5261
  float roughness = surf.filteredRoughness;
5279
5262
  float eta = surf.eta;
5280
- vec3 halfVector = normalize( vec3( 0.0, 0.0, 1.0 ) + sampleSphere( sobol2( 13 ) ) * roughness );
5263
+ vec3 halfVector = normalize( vec3( 0.0, 0.0, 1.0 ) + sampleSphere( rand2( 13 ) ) * roughness );
5281
5264
  vec3 lightDirection = refract( normalize( - wo ), halfVector, eta );
5282
5265
 
5283
5266
  if ( surf.thinFilm ) {
@@ -5318,7 +5301,7 @@ bool bvhIntersectFogVolumeHit(
5318
5301
  vec3 halfVector = ggxDirection(
5319
5302
  wo,
5320
5303
  vec2( roughness ),
5321
- sobol2( 14 )
5304
+ rand2( 14 )
5322
5305
  );
5323
5306
 
5324
5307
  // apply to new ray by reflecting off the new normal
@@ -5353,7 +5336,8 @@ bool bvhIntersectFogVolumeHit(
5353
5336
 
5354
5337
  float metalness = surf.metalness;
5355
5338
  float transmission = surf.transmission;
5356
- float fEstimate = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
5339
+ // float fEstimate = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
5340
+ float fEstimate = disneyFresnel( wo, wi, wh, surf.f0, surf.eta, surf.metalness );
5357
5341
 
5358
5342
  float transSpecularProb = mix( max( 0.25, fEstimate ), 1.0, metalness );
5359
5343
  float diffSpecularProb = 0.5 + 0.5 * metalness;
@@ -5469,7 +5453,7 @@ bool bvhIntersectFogVolumeHit(
5469
5453
  ScatterRecord sampleRec;
5470
5454
  sampleRec.specularPdf = 0.0;
5471
5455
  sampleRec.pdf = 1.0 / ( 4.0 * PI );
5472
- sampleRec.direction = sampleSphere( sobol2( 16 ) );
5456
+ sampleRec.direction = sampleSphere( rand2( 16 ) );
5473
5457
  sampleRec.color = surf.color / ( 4.0 * PI );
5474
5458
  return sampleRec;
5475
5459
 
@@ -5521,7 +5505,7 @@ bool bvhIntersectFogVolumeHit(
5521
5505
  vec3 wi;
5522
5506
  vec3 clearcoatWi;
5523
5507
 
5524
- float r = sobol( 15 );
5508
+ float r = rand( 15 );
5525
5509
  if ( r <= cdf[0] ) { // diffuse
5526
5510
 
5527
5511
  wi = diffuseDirection( wo, surf );
@@ -5605,10 +5589,17 @@ bool bvhIntersectFogVolumeHit(
5605
5589
  // samples the color given env map with CDF and returns the pdf of the direction
5606
5590
  float sampleEquirect( vec3 direction, inout vec3 color ) {
5607
5591
 
5592
+ float totalSum = envMapInfo.totalSum;
5593
+ if ( totalSum == 0.0 ) {
5594
+
5595
+ color = vec3( 0.0 );
5596
+ return 1.0;
5597
+
5598
+ }
5599
+
5608
5600
  vec2 uv = equirectDirectionToUv( direction );
5609
5601
  color = texture2D( envMapInfo.map, uv ).rgb;
5610
5602
 
5611
- float totalSum = envMapInfo.totalSum;
5612
5603
  float lum = luminance( color );
5613
5604
  ivec2 resolution = textureSize( envMapInfo.map, 0 );
5614
5605
  float pdf = lum / totalSum;
@@ -6160,32 +6151,38 @@ bool bvhIntersectFogVolumeHit(
6160
6151
 
6161
6152
  }
6162
6153
 
6163
- float evaluateFresnelWeight( float cosTheta, float eta, float f0 ) {
6154
+ // TODO: disney fresnel was removed and replaced with this fresnel function to better align with
6155
+ // the glTF but is causing blown out pixels. Should be revisited
6156
+ // float evaluateFresnelWeight( float cosTheta, float eta, float f0 ) {
6164
6157
 
6165
- if ( totalInternalReflection( cosTheta, eta ) ) {
6158
+ // if ( totalInternalReflection( cosTheta, eta ) ) {
6166
6159
 
6167
- return 1.0;
6160
+ // return 1.0;
6168
6161
 
6169
- }
6162
+ // }
6170
6163
 
6171
- return schlickFresnel( cosTheta, f0 );
6164
+ // return schlickFresnel( cosTheta, f0 );
6172
6165
 
6173
- }
6166
+ // }
6174
6167
 
6175
- /*
6176
6168
  // https://schuttejoe.github.io/post/disneybsdf/
6177
6169
  float disneyFresnel( vec3 wo, vec3 wi, vec3 wh, float f0, float eta, float metalness ) {
6178
6170
 
6179
6171
  float dotHV = dot( wo, wh );
6180
- float dotHL = dot( wi, wh );
6172
+ if ( totalInternalReflection( dotHV, eta ) ) {
6173
+
6174
+ return 1.0;
6181
6175
 
6176
+ }
6177
+
6178
+ float dotHL = dot( wi, wh );
6182
6179
  float dielectricFresnel = dielectricFresnel( abs( dotHV ), eta );
6183
6180
  float metallicFresnel = schlickFresnel( dotHL, f0 );
6184
6181
 
6185
6182
  return mix( dielectricFresnel, metallicFresnel, metalness );
6186
6183
 
6187
6184
  }
6188
- */
6185
+
6189
6186
  `;
6190
6187
 
6191
6188
  const arraySamplerTexelFetchGLSL = /*glsl */`
@@ -6242,28 +6239,28 @@ bool bvhIntersectFogVolumeHit(
6242
6239
  }
6243
6240
 
6244
6241
  // returns [ 0, 1 ]
6245
- float rand() {
6242
+ float pcgRand() {
6246
6243
 
6247
6244
  pcg4d( WHITE_NOISE_SEED );
6248
6245
  return float( WHITE_NOISE_SEED.x ) / float( 0xffffffffu );
6249
6246
 
6250
6247
  }
6251
6248
 
6252
- vec2 rand2() {
6249
+ vec2 pcgRand2() {
6253
6250
 
6254
6251
  pcg4d( WHITE_NOISE_SEED );
6255
6252
  return vec2( WHITE_NOISE_SEED.xy ) / float(0xffffffffu);
6256
6253
 
6257
6254
  }
6258
6255
 
6259
- vec3 rand3() {
6256
+ vec3 pcgRand3() {
6260
6257
 
6261
6258
  pcg4d( WHITE_NOISE_SEED );
6262
6259
  return vec3( WHITE_NOISE_SEED.xyz ) / float( 0xffffffffu );
6263
6260
 
6264
6261
  }
6265
6262
 
6266
- vec4 rand4() {
6263
+ vec4 pcgRand4() {
6267
6264
 
6268
6265
  pcg4d( WHITE_NOISE_SEED );
6269
6266
  return vec4( WHITE_NOISE_SEED ) / float( 0xffffffffu );
@@ -6336,7 +6333,7 @@ bool bvhIntersectFogVolumeHit(
6336
6333
 
6337
6334
  // Jitter the camera ray by finding a uv coordinate at a random sample
6338
6335
  // around this pixel's UV coordinate for AA
6339
- vec2 ruv = sobol2( 0 );
6336
+ vec2 ruv = rand2( 0 );
6340
6337
  vec2 jitteredUv = vUv + vec2( tentFilter( ruv.x ) * ssd.x, tentFilter( ruv.y ) * ssd.y );
6341
6338
  Ray ray;
6342
6339
 
@@ -6381,7 +6378,7 @@ bool bvhIntersectFogVolumeHit(
6381
6378
 
6382
6379
  // get the aperture sample
6383
6380
  // if blades === 0 then we assume a circle
6384
- vec3 shapeUVW= sobol3( 1 );
6381
+ vec3 shapeUVW= rand3( 1 );
6385
6382
  int blades = physicalCamera.apertureBlades;
6386
6383
  float anamorphicRatio = physicalCamera.anamorphicRatio;
6387
6384
  vec2 apertureSample = blades == 0 ? sampleCircle( shapeUVW.xy ) : sampleRegularPolygon( blades, shapeUVW );
@@ -6417,6 +6414,9 @@ bool bvhIntersectFogVolumeHit(
6417
6414
  out vec3 color
6418
6415
  ) {
6419
6416
 
6417
+ // store the original bounce index so we can reset it after
6418
+ uint originalBounceIndex = sobolBounceIndex;
6419
+
6420
6420
  int traversals = state.traversals;
6421
6421
  int transmissiveTraversals = state.transmissiveTraversals;
6422
6422
  bool isShadowRay = state.isShadowRay;
@@ -6429,22 +6429,25 @@ bool bvhIntersectFogVolumeHit(
6429
6429
 
6430
6430
  color = vec3( 1.0 );
6431
6431
 
6432
- // TODO: we should be using sobol sampling here instead of rand but the sobol bounce and path indices need to be incremented
6433
- // and then reset.
6432
+ bool result = true;
6434
6433
  for ( int i = 0; i < traversals; i ++ ) {
6435
6434
 
6435
+ sobolBounceIndex ++;
6436
+
6436
6437
  int hitType = traceScene( ray, fogMaterial, surfaceHit );
6437
6438
 
6438
6439
  if ( hitType == FOG_HIT ) {
6439
6440
 
6440
- return true;
6441
+ result = true;
6442
+ break;
6441
6443
 
6442
6444
  } else if ( hitType == SURFACE_HIT ) {
6443
6445
 
6444
6446
  float totalDist = distance( startPoint, ray.origin + ray.direction * surfaceHit.dist );
6445
6447
  if ( totalDist > rayDist ) {
6446
6448
 
6447
- return false;
6449
+ result = false;
6450
+ break;
6448
6451
 
6449
6452
  }
6450
6453
 
@@ -6526,7 +6529,7 @@ bool bvhIntersectFogVolumeHit(
6526
6529
  bool useAlphaTest = alphaTest != 0.0;
6527
6530
  float transmissionFactor = ( 1.0 - metalness ) * transmission;
6528
6531
  if (
6529
- transmissionFactor < rand() && ! (
6532
+ transmissionFactor < rand( 9 ) && ! (
6530
6533
  // material sidedness
6531
6534
  material.side != 0.0 && surfaceHit.side == material.side
6532
6535
 
@@ -6534,11 +6537,12 @@ bool bvhIntersectFogVolumeHit(
6534
6537
  || useAlphaTest && albedo.a < alphaTest
6535
6538
 
6536
6539
  // opacity
6537
- || material.transparent && ! useAlphaTest && albedo.a < rand()
6540
+ || material.transparent && ! useAlphaTest && albedo.a < rand( 10 )
6538
6541
  )
6539
6542
  ) {
6540
6543
 
6541
- return true;
6544
+ result = true;
6545
+ break;
6542
6546
 
6543
6547
  }
6544
6548
 
@@ -6564,13 +6568,16 @@ bool bvhIntersectFogVolumeHit(
6564
6568
 
6565
6569
  } else {
6566
6570
 
6567
- return false;
6571
+ result = false;
6572
+ break;
6568
6573
 
6569
6574
  }
6570
6575
 
6571
6576
  }
6572
6577
 
6573
- return true;
6578
+ // reset the bounce index
6579
+ sobolBounceIndex = originalBounceIndex;
6580
+ return result;
6574
6581
 
6575
6582
  }
6576
6583
 
@@ -6601,7 +6608,7 @@ bool bvhIntersectFogVolumeHit(
6601
6608
 
6602
6609
  // offset the distance so we don't run into issues with particles on the same surface
6603
6610
  // as other objects
6604
- float particleDist = intersectFogVolume( fogMaterial, sobol( 1 ) );
6611
+ float particleDist = intersectFogVolume( fogMaterial, rand( 1 ) );
6605
6612
  if ( particleDist + RAY_OFFSET < surfaceHit.dist ) {
6606
6613
 
6607
6614
  surfaceHit.side = 1.0;
@@ -6664,6 +6671,7 @@ bool bvhIntersectFogVolumeHit(
6664
6671
 
6665
6672
  vec3 uvPrime = material.mapTransform * vec3( uv, 1 );
6666
6673
  albedo *= texture2D( textures, vec3( uvPrime.xy, material.map ) );
6674
+
6667
6675
  }
6668
6676
 
6669
6677
  if ( material.vertexColors ) {
@@ -6695,7 +6703,7 @@ bool bvhIntersectFogVolumeHit(
6695
6703
  || useAlphaTest && albedo.a < alphaTest
6696
6704
 
6697
6705
  // opacity
6698
- || material.transparent && ! useAlphaTest && albedo.a < sobol( 3 )
6706
+ || material.transparent && ! useAlphaTest && albedo.a < rand( 3 )
6699
6707
  ) {
6700
6708
 
6701
6709
  return SKIP_SURFACE;
@@ -6957,10 +6965,10 @@ bool bvhIntersectFogVolumeHit(
6957
6965
  vec3 result = vec3( 0.0 );
6958
6966
 
6959
6967
  // uniformly pick a light or environment map
6960
- if( lightsDenom != 0.0 && sobol( 5 ) < float( lights.count ) / lightsDenom ) {
6968
+ if( lightsDenom != 0.0 && rand( 5 ) < float( lights.count ) / lightsDenom ) {
6961
6969
 
6962
6970
  // sample a light or environment
6963
- LightRecord lightRec = randomLightSample( lights.tex, iesProfiles, lights.count, rayOrigin, sobol3( 6 ) );
6971
+ LightRecord lightRec = randomLightSample( lights.tex, iesProfiles, lights.count, rayOrigin, rand3( 6 ) );
6964
6972
 
6965
6973
  bool isSampleBelowSurface = ! surf.volumeParticle && dot( surf.faceNormal, lightRec.direction ) < 0.0;
6966
6974
  if ( isSampleBelowSurface ) {
@@ -6995,11 +7003,11 @@ bool bvhIntersectFogVolumeHit(
6995
7003
 
6996
7004
  }
6997
7005
 
6998
- } else {
7006
+ } else if ( envMapInfo.totalSum != 0.0 && environmentIntensity != 0.0 ) {
6999
7007
 
7000
7008
  // find a sample in the environment map to include in the contribution
7001
7009
  vec3 envColor, envDirection;
7002
- float envPdf = sampleEquirectProbability( sobol2( 7 ), envColor, envDirection );
7010
+ float envPdf = sampleEquirectProbability( rand2( 7 ), envColor, envDirection );
7003
7011
  envDirection = invEnvRotation3x3 * envDirection;
7004
7012
 
7005
7013
  // this env sampling is not set up for transmissive sampling and yields overly bright
@@ -7048,6 +7056,667 @@ bool bvhIntersectFogVolumeHit(
7048
7056
 
7049
7057
  `;
7050
7058
 
7059
+ const stratifiedTextureGLSL = /* glsl */`
7060
+
7061
+ uniform sampler2D stratifiedTexture;
7062
+ uniform sampler2D stratifiedOffsetTexture;
7063
+
7064
+ uint sobolPixelIndex = 0u;
7065
+ uint sobolPathIndex = 0u;
7066
+ uint sobolBounceIndex = 0u;
7067
+ vec4 pixelSeed = vec4( 0 );
7068
+
7069
+ vec4 rand4( int v ) {
7070
+
7071
+ ivec2 uv = ivec2( v, sobolBounceIndex );
7072
+ vec4 stratifiedSample = texelFetch( stratifiedTexture, uv, 0 );
7073
+ return fract( stratifiedSample + pixelSeed.r ); // blue noise + stratified samples
7074
+
7075
+ }
7076
+
7077
+ vec3 rand3( int v ) {
7078
+
7079
+ return rand4( v ).xyz;
7080
+
7081
+ }
7082
+
7083
+ vec2 rand2( int v ) {
7084
+
7085
+ return rand4( v ).xy;
7086
+
7087
+ }
7088
+
7089
+ float rand( int v ) {
7090
+
7091
+ return rand4( v ).x;
7092
+
7093
+ }
7094
+
7095
+ void rng_initialize( vec2 screenCoord, int frame ) {
7096
+
7097
+ // tile the small noise texture across the entire screen
7098
+ ivec2 noiseSize = ivec2( textureSize( stratifiedOffsetTexture, 0 ) );
7099
+ pixelSeed = texelFetch( stratifiedOffsetTexture, ivec2( screenCoord.xy ) % noiseSize, 0 );
7100
+
7101
+ }
7102
+
7103
+ `;
7104
+
7105
+ // Stratified Sampling based on implementation from hoverinc pathtracer
7106
+ // - https://github.com/hoverinc/ray-tracing-renderer
7107
+ // - http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html
7108
+
7109
+ function shuffle( arr ) {
7110
+
7111
+ for ( let i = arr.length - 1; i > 0; i -- ) {
7112
+
7113
+ const j = Math.floor( Math.random() * ( i + 1 ) );
7114
+ const x = arr[ i ];
7115
+ arr[ i ] = arr[ j ];
7116
+ arr[ j ] = x;
7117
+
7118
+ }
7119
+
7120
+ return arr;
7121
+
7122
+ }
7123
+
7124
+ // strataCount : The number of bins per dimension
7125
+ // dimensions : The number of dimensions to generate stratified values for
7126
+ class StratifiedSampler {
7127
+
7128
+ constructor( strataCount, dimensions ) {
7129
+
7130
+ const l = strataCount ** dimensions;
7131
+ const strata = new Uint16Array( l );
7132
+ let index = l;
7133
+
7134
+ // each integer represents a statum bin
7135
+ for ( let i = 0; i < l; i ++ ) {
7136
+
7137
+ strata[ i ] = i;
7138
+
7139
+ }
7140
+
7141
+ this.samples = new Float32Array( dimensions );
7142
+
7143
+ this.strataCount = strataCount;
7144
+
7145
+ this.restart = function () {
7146
+
7147
+ index = 0;
7148
+
7149
+ };
7150
+
7151
+ this.next = function () {
7152
+
7153
+ const { samples } = this;
7154
+
7155
+ if ( index >= strata.length ) {
7156
+
7157
+ shuffle( strata );
7158
+ this.restart();
7159
+
7160
+ }
7161
+
7162
+ let stratum = strata[ index ++ ];
7163
+
7164
+ for ( let i = 0; i < dimensions; i ++ ) {
7165
+
7166
+ samples[ i ] = ( stratum % strataCount + Math.random() ) / strataCount;
7167
+ stratum = Math.floor( stratum / strataCount );
7168
+
7169
+ }
7170
+
7171
+ return samples;
7172
+
7173
+ };
7174
+
7175
+ }
7176
+
7177
+ }
7178
+
7179
+ // Stratified Sampling based on implementation from hoverinc pathtracer
7180
+
7181
+ // Stratified set of data with each tuple stratified separately and combined
7182
+ class StratifiedSamplerCombined {
7183
+
7184
+ constructor( strataCount, listOfDimensions ) {
7185
+
7186
+ let totalDim = 0;
7187
+ for ( const dim of listOfDimensions ) {
7188
+
7189
+ totalDim += dim;
7190
+
7191
+ }
7192
+
7193
+ const combined = new Float32Array( totalDim );
7194
+ const strataObjs = [];
7195
+ let offset = 0;
7196
+ for ( const dim of listOfDimensions ) {
7197
+
7198
+ const sampler = new StratifiedSampler( strataCount, dim );
7199
+ sampler.samples = new Float32Array( combined.buffer, offset, sampler.samples.length );
7200
+ offset += sampler.samples.length * 4;
7201
+ strataObjs.push( sampler );
7202
+
7203
+ }
7204
+
7205
+ this.samples = combined;
7206
+
7207
+ this.strataCount = strataCount;
7208
+
7209
+ this.next = function () {
7210
+
7211
+ for ( const strata of strataObjs ) {
7212
+
7213
+ strata.next();
7214
+
7215
+ }
7216
+
7217
+ return combined;
7218
+
7219
+ };
7220
+
7221
+ this.restart = function () {
7222
+
7223
+ for ( const strata of strataObjs ) {
7224
+
7225
+ strata.restart();
7226
+
7227
+ }
7228
+
7229
+ };
7230
+
7231
+ }
7232
+
7233
+ }
7234
+
7235
+ class StratifiedSamplesTexture extends three.DataTexture {
7236
+
7237
+ constructor( count = 1, depth = 1, strata = 8 ) {
7238
+
7239
+ super( new Float32Array( 1 ), 1, 1, three.RGBAFormat, three.FloatType );
7240
+ this.minFilter = three.NearestFilter;
7241
+ this.magFilter = three.NearestFilter;
7242
+
7243
+ this.strata = strata;
7244
+ this.sampler = null;
7245
+
7246
+ this.init( count, depth, strata );
7247
+
7248
+ }
7249
+
7250
+ init( count, depth, strata = this.strata ) {
7251
+
7252
+ const { image } = this;
7253
+ if ( image.width === depth && image.height === count ) {
7254
+
7255
+ return;
7256
+
7257
+ }
7258
+
7259
+ const dimensions = new Array( count * depth ).fill( 4 );
7260
+ const sampler = new StratifiedSamplerCombined( strata, dimensions );
7261
+
7262
+ image.width = depth;
7263
+ image.height = count;
7264
+ image.data = sampler.samples;
7265
+
7266
+ this.sampler = sampler;
7267
+
7268
+ this.dispose();
7269
+ this.next();
7270
+
7271
+ }
7272
+
7273
+ next() {
7274
+
7275
+ this.sampler.next();
7276
+ this.needsUpdate = true;
7277
+
7278
+ }
7279
+
7280
+ }
7281
+
7282
+ function shuffleArray( array, random = Math.random ) {
7283
+
7284
+ for ( let i = array.length - 1; i > 0; i -- ) {
7285
+
7286
+ const replaceIndex = ~ ~ ( ( random() - 1e-6 ) * i );
7287
+ const tmp = array[ i ];
7288
+ array[ i ] = array[ replaceIndex ];
7289
+ array[ replaceIndex ] = tmp;
7290
+
7291
+ }
7292
+
7293
+ }
7294
+
7295
+ function fillWithOnes( array, count ) {
7296
+
7297
+ array.fill( 0 );
7298
+
7299
+ for ( let i = 0; i < count; i ++ ) {
7300
+
7301
+ array[ i ] = 1;
7302
+
7303
+ }
7304
+
7305
+ }
7306
+
7307
+ class BlueNoiseSamples {
7308
+
7309
+ constructor( size ) {
7310
+
7311
+ this.count = 0;
7312
+ this.size = - 1;
7313
+ this.sigma = - 1;
7314
+ this.radius = - 1;
7315
+ this.lookupTable = null;
7316
+ this.score = null;
7317
+ this.binaryPattern = null;
7318
+
7319
+ this.resize( size );
7320
+ this.setSigma( 1.5 );
7321
+
7322
+ }
7323
+
7324
+ findVoid() {
7325
+
7326
+ const { score, binaryPattern } = this;
7327
+
7328
+ let currValue = Infinity;
7329
+ let currIndex = - 1;
7330
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
7331
+
7332
+ if ( binaryPattern[ i ] !== 0 ) {
7333
+
7334
+ continue;
7335
+
7336
+ }
7337
+
7338
+ const pScore = score[ i ];
7339
+ if ( pScore < currValue ) {
7340
+
7341
+ currValue = pScore;
7342
+ currIndex = i;
7343
+
7344
+ }
7345
+
7346
+ }
7347
+
7348
+ return currIndex;
7349
+
7350
+ }
7351
+
7352
+ findCluster() {
7353
+
7354
+ const { score, binaryPattern } = this;
7355
+
7356
+ let currValue = - Infinity;
7357
+ let currIndex = - 1;
7358
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
7359
+
7360
+ if ( binaryPattern[ i ] !== 1 ) {
7361
+
7362
+ continue;
7363
+
7364
+ }
7365
+
7366
+ const pScore = score[ i ];
7367
+ if ( pScore > currValue ) {
7368
+
7369
+ currValue = pScore;
7370
+ currIndex = i;
7371
+
7372
+ }
7373
+
7374
+ }
7375
+
7376
+ return currIndex;
7377
+
7378
+ }
7379
+
7380
+ setSigma( sigma ) {
7381
+
7382
+ if ( sigma === this.sigma ) {
7383
+
7384
+ return;
7385
+
7386
+ }
7387
+
7388
+ // generate a radius in which the score will be updated under the
7389
+ // assumption that e^-10 is insignificant enough to be the border at
7390
+ // which we drop off.
7391
+ const radius = ~ ~ ( Math.sqrt( 10 * 2 * ( sigma ** 2 ) ) + 1 );
7392
+ const lookupWidth = 2 * radius + 1;
7393
+ const lookupTable = new Float32Array( lookupWidth * lookupWidth );
7394
+ const sigma2 = sigma * sigma;
7395
+ for ( let x = - radius; x <= radius; x ++ ) {
7396
+
7397
+ for ( let y = - radius; y <= radius; y ++ ) {
7398
+
7399
+ const index = ( radius + y ) * lookupWidth + x + radius;
7400
+ const dist2 = x * x + y * y;
7401
+ lookupTable[ index ] = Math.E ** ( - dist2 / ( 2 * sigma2 ) );
7402
+
7403
+ }
7404
+
7405
+ }
7406
+
7407
+ this.lookupTable = lookupTable;
7408
+ this.sigma = sigma;
7409
+ this.radius = radius;
7410
+
7411
+ }
7412
+
7413
+ resize( size ) {
7414
+
7415
+ if ( this.size !== size ) {
7416
+
7417
+ this.size = size;
7418
+ this.score = new Float32Array( size * size );
7419
+ this.binaryPattern = new Uint8Array( size * size );
7420
+
7421
+ }
7422
+
7423
+
7424
+ }
7425
+
7426
+ invert() {
7427
+
7428
+ const { binaryPattern, score, size } = this;
7429
+
7430
+ score.fill( 0 );
7431
+
7432
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
7433
+
7434
+ if ( binaryPattern[ i ] === 0 ) {
7435
+
7436
+ const y = ~ ~ ( i / size );
7437
+ const x = i - y * size;
7438
+ this.updateScore( x, y, 1 );
7439
+ binaryPattern[ i ] = 1;
7440
+
7441
+ } else {
7442
+
7443
+ binaryPattern[ i ] = 0;
7444
+
7445
+ }
7446
+
7447
+ }
7448
+
7449
+ }
7450
+
7451
+ updateScore( x, y, multiplier ) {
7452
+
7453
+ // TODO: Is there a way to keep track of the highest and lowest scores here to avoid have to search over
7454
+ // everything in the buffer?
7455
+ const { size, score, lookupTable } = this;
7456
+
7457
+ // const sigma2 = sigma * sigma;
7458
+ // const radius = Math.floor( size / 2 );
7459
+ const radius = this.radius;
7460
+ const lookupWidth = 2 * radius + 1;
7461
+ for ( let px = - radius; px <= radius; px ++ ) {
7462
+
7463
+ for ( let py = - radius; py <= radius; py ++ ) {
7464
+
7465
+ // const dist2 = px * px + py * py;
7466
+ // const value = Math.E ** ( - dist2 / ( 2 * sigma2 ) );
7467
+
7468
+ const lookupIndex = ( radius + py ) * lookupWidth + px + radius;
7469
+ const value = lookupTable[ lookupIndex ];
7470
+
7471
+ let sx = ( x + px );
7472
+ sx = sx < 0 ? size + sx : sx % size;
7473
+
7474
+ let sy = ( y + py );
7475
+ sy = sy < 0 ? size + sy : sy % size;
7476
+
7477
+ const sindex = sy * size + sx;
7478
+ score[ sindex ] += multiplier * value;
7479
+
7480
+ }
7481
+
7482
+ }
7483
+
7484
+ }
7485
+
7486
+ addPointIndex( index ) {
7487
+
7488
+ this.binaryPattern[ index ] = 1;
7489
+
7490
+ const size = this.size;
7491
+ const y = ~ ~ ( index / size );
7492
+ const x = index - y * size;
7493
+ this.updateScore( x, y, 1 );
7494
+ this.count ++;
7495
+
7496
+ }
7497
+
7498
+ removePointIndex( index ) {
7499
+
7500
+ this.binaryPattern[ index ] = 0;
7501
+
7502
+ const size = this.size;
7503
+ const y = ~ ~ ( index / size );
7504
+ const x = index - y * size;
7505
+ this.updateScore( x, y, - 1 );
7506
+ this.count --;
7507
+
7508
+ }
7509
+
7510
+ copy( source ) {
7511
+
7512
+ this.resize( source.size );
7513
+ this.score.set( source.score );
7514
+ this.binaryPattern.set( source.binaryPattern );
7515
+ this.setSigma( source.sigma );
7516
+ this.count = source.count;
7517
+
7518
+ }
7519
+
7520
+ }
7521
+
7522
+ class BlueNoiseGenerator {
7523
+
7524
+ constructor() {
7525
+
7526
+ this.random = Math.random;
7527
+ this.sigma = 1.5;
7528
+ this.size = 64;
7529
+ this.majorityPointsRatio = 0.1;
7530
+
7531
+ this.samples = new BlueNoiseSamples( 1 );
7532
+ this.savedSamples = new BlueNoiseSamples( 1 );
7533
+
7534
+ }
7535
+
7536
+ generate() {
7537
+
7538
+ // http://cv.ulichney.com/papers/1993-void-cluster.pdf
7539
+
7540
+ const {
7541
+ samples,
7542
+ savedSamples,
7543
+ sigma,
7544
+ majorityPointsRatio,
7545
+ size,
7546
+ } = this;
7547
+
7548
+ samples.resize( size );
7549
+ samples.setSigma( sigma );
7550
+
7551
+ // 1. Randomly place the minority points.
7552
+ const pointCount = Math.floor( size * size * majorityPointsRatio );
7553
+ const initialSamples = samples.binaryPattern;
7554
+
7555
+ fillWithOnes( initialSamples, pointCount );
7556
+ shuffleArray( initialSamples, this.random );
7557
+
7558
+ for ( let i = 0, l = initialSamples.length; i < l; i ++ ) {
7559
+
7560
+ if ( initialSamples[ i ] === 1 ) {
7561
+
7562
+ samples.addPointIndex( i );
7563
+
7564
+ }
7565
+
7566
+ }
7567
+
7568
+ // 2. Remove minority point that is in densest cluster and place it in the largest void.
7569
+ while ( true ) {
7570
+
7571
+ const clusterIndex = samples.findCluster();
7572
+ samples.removePointIndex( clusterIndex );
7573
+
7574
+ const voidIndex = samples.findVoid();
7575
+ if ( clusterIndex === voidIndex ) {
7576
+
7577
+ samples.addPointIndex( clusterIndex );
7578
+ break;
7579
+
7580
+ }
7581
+
7582
+ samples.addPointIndex( voidIndex );
7583
+
7584
+ }
7585
+
7586
+ // 3. PHASE I: Assign a rank to each progressively less dense cluster point and put it
7587
+ // in the dither array.
7588
+ const ditherArray = new Uint32Array( size * size );
7589
+ savedSamples.copy( samples );
7590
+
7591
+ let rank;
7592
+ rank = samples.count - 1;
7593
+ while ( rank >= 0 ) {
7594
+
7595
+ const clusterIndex = samples.findCluster();
7596
+ samples.removePointIndex( clusterIndex );
7597
+
7598
+ ditherArray[ clusterIndex ] = rank;
7599
+ rank --;
7600
+
7601
+ }
7602
+
7603
+ // 4. PHASE II: Do the same thing for the largest voids up to half of the total pixels using
7604
+ // the initial binary pattern.
7605
+ const totalSize = size * size;
7606
+ rank = savedSamples.count;
7607
+ while ( rank < totalSize / 2 ) {
7608
+
7609
+ const voidIndex = savedSamples.findVoid();
7610
+ savedSamples.addPointIndex( voidIndex );
7611
+ ditherArray[ voidIndex ] = rank;
7612
+ rank ++;
7613
+
7614
+ }
7615
+
7616
+ // 5. PHASE III: Invert the pattern and finish out by assigning a rank to the remaining
7617
+ // and iteratively removing them.
7618
+ savedSamples.invert();
7619
+
7620
+ while ( rank < totalSize ) {
7621
+
7622
+ const clusterIndex = savedSamples.findCluster();
7623
+ savedSamples.removePointIndex( clusterIndex );
7624
+ ditherArray[ clusterIndex ] = rank;
7625
+ rank ++;
7626
+
7627
+ }
7628
+
7629
+ return { data: ditherArray, maxValue: totalSize };
7630
+
7631
+ }
7632
+
7633
+ }
7634
+
7635
+ function getStride( channels ) {
7636
+
7637
+ if ( channels >= 3 ) {
7638
+
7639
+ return 4;
7640
+
7641
+ } else {
7642
+
7643
+ return channels;
7644
+
7645
+ }
7646
+
7647
+ }
7648
+
7649
+ function getFormat( channels ) {
7650
+
7651
+ switch ( channels ) {
7652
+
7653
+ case 1:
7654
+ return three.RedFormat;
7655
+ case 2:
7656
+ return three.RGFormat;
7657
+ default:
7658
+ return three.RGBAFormat;
7659
+
7660
+ }
7661
+
7662
+ }
7663
+
7664
+ class BlueNoiseTexture extends three.DataTexture {
7665
+
7666
+ constructor( size = 64, channels = 1 ) {
7667
+
7668
+ super( new Float32Array( 4 ), 1, 1, three.RGBAFormat, three.FloatType );
7669
+ this.minFilter = three.NearestFilter;
7670
+ this.magFilter = three.NearestFilter;
7671
+
7672
+ this.size = size;
7673
+ this.channels = channels;
7674
+ this.update();
7675
+
7676
+ }
7677
+
7678
+ update() {
7679
+
7680
+ const channels = this.channels;
7681
+ const size = this.size;
7682
+ const generator = new BlueNoiseGenerator();
7683
+ generator.channels = channels;
7684
+ generator.size = size;
7685
+
7686
+ const stride = getStride( channels );
7687
+ const format = getFormat( stride );
7688
+ if ( this.image.width !== size || format !== this.format ) {
7689
+
7690
+ this.image.width = size;
7691
+ this.image.height = size;
7692
+ this.image.data = new Float32Array( ( size ** 2 ) * stride );
7693
+ this.format = format;
7694
+ this.dispose();
7695
+
7696
+ }
7697
+
7698
+ const data = this.image.data;
7699
+ for ( let i = 0, l = channels; i < l; i ++ ) {
7700
+
7701
+ const result = generator.generate();
7702
+ const bin = result.data;
7703
+ const maxValue = result.maxValue;
7704
+
7705
+ for ( let j = 0, l2 = bin.length; j < l2; j ++ ) {
7706
+
7707
+ const value = bin[ j ] / maxValue;
7708
+ data[ j * stride + i ] = value;
7709
+
7710
+ }
7711
+
7712
+ }
7713
+
7714
+ this.needsUpdate = true;
7715
+
7716
+ }
7717
+
7718
+ }
7719
+
7051
7720
  class PhysicalPathTracingMaterial extends MaterialBase {
7052
7721
 
7053
7722
  onBeforeRender() {
@@ -7071,7 +7740,12 @@ bool bvhIntersectFogVolumeHit(
7071
7740
  FEATURE_DOF: 1,
7072
7741
  FEATURE_BACKGROUND_MAP: 0,
7073
7742
  FEATURE_FOG: 1,
7074
- FEATURE_SOBOL: 0,
7743
+
7744
+ // 0 = PCG
7745
+ // 1 = Sobol
7746
+ // 2 = Stratified List
7747
+ RANDOM_TYPE: 2,
7748
+
7075
7749
  // 0 = Perspective
7076
7750
  // 1 = Orthographic
7077
7751
  // 2 = Equirectangular
@@ -7113,6 +7787,8 @@ bool bvhIntersectFogVolumeHit(
7113
7787
 
7114
7788
  backgroundAlpha: { value: 1.0 },
7115
7789
  sobolTexture: { value: null },
7790
+ stratifiedTexture: { value: new StratifiedSamplesTexture() },
7791
+ stratifiedOffsetTexture: { value: new BlueNoiseTexture( 64, 1 ) },
7116
7792
  },
7117
7793
 
7118
7794
  vertexShader: /* glsl */`
@@ -7152,23 +7828,35 @@ bool bvhIntersectFogVolumeHit(
7152
7828
  ${ materialStructGLSL }
7153
7829
 
7154
7830
  // random
7155
- ${ pcgGLSL }
7156
- #if FEATURE_SOBOL
7831
+ #if RANDOM_TYPE == 2 // Stratified List
7157
7832
 
7833
+ ${ stratifiedTextureGLSL }
7834
+
7835
+ #elif RANDOM_TYPE == 1 // Sobol
7836
+
7837
+ ${ pcgGLSL }
7158
7838
  ${ sobolCommonGLSL }
7159
7839
  ${ sobolSamplingGLSL }
7160
7840
 
7161
- #else
7841
+ #define rand(v) sobol(v)
7842
+ #define rand2(v) sobol2(v)
7843
+ #define rand3(v) sobol3(v)
7844
+ #define rand4(v) sobol4(v)
7845
+
7846
+ #else // PCG
7847
+
7848
+ ${ pcgGLSL }
7162
7849
 
7163
7850
  // Using the sobol functions seems to break the the compiler on MacOS
7164
7851
  // - specifically the "sobolReverseBits" function.
7165
7852
  uint sobolPixelIndex = 0u;
7166
7853
  uint sobolPathIndex = 0u;
7167
7854
  uint sobolBounceIndex = 0u;
7168
- float sobol( int v ) { return rand(); }
7169
- vec2 sobol2( int v ) { return rand2(); }
7170
- vec3 sobol3( int v ) { return rand3(); }
7171
- vec4 sobol4( int v ) { return rand4(); }
7855
+
7856
+ #define rand(v) pcgRand()
7857
+ #define rand2(v) pcgRand2()
7858
+ #define rand3(v) pcgRand3()
7859
+ #define rand4(v) pcgRand4()
7172
7860
 
7173
7861
  #endif
7174
7862
 
@@ -7286,7 +7974,10 @@ bool bvhIntersectFogVolumeHit(
7286
7974
  // inverse environment rotation
7287
7975
  envRotation3x3 = mat3( environmentRotation );
7288
7976
  invEnvRotation3x3 = inverse( envRotation3x3 );
7289
- lightsDenom = environmentIntensity == 0.0 && lights.count != 0u ? float( lights.count ) : float( lights.count + 1u );
7977
+ lightsDenom =
7978
+ ( environmentIntensity == 0.0 || envMapInfo.totalSum == 0.0 ) && lights.count != 0u ?
7979
+ float( lights.count ) :
7980
+ float( lights.count + 1u );
7290
7981
 
7291
7982
  // final color
7292
7983
  gl_FragColor = vec4( 0, 0, 0, 1 );
@@ -7355,7 +8046,7 @@ bool bvhIntersectFogVolumeHit(
7355
8046
 
7356
8047
  if ( state.firstRay || state.transmissiveRay ) {
7357
8048
 
7358
- gl_FragColor.rgb += sampleBackground( envRotation3x3 * ray.direction, sobol2( 2 ) ) * state.throughputColor;
8049
+ gl_FragColor.rgb += sampleBackground( envRotation3x3 * ray.direction, rand2( 2 ) ) * state.throughputColor;
7359
8050
  gl_FragColor.a = backgroundAlpha;
7360
8051
 
7361
8052
  } else {
@@ -7446,7 +8137,7 @@ bool bvhIntersectFogVolumeHit(
7446
8137
  }
7447
8138
 
7448
8139
  scatterRec = bsdfSample( - ray.direction, surf );
7449
- state.isShadowRay = scatterRec.specularPdf < sobol( 4 );
8140
+ state.isShadowRay = scatterRec.specularPdf < rand( 4 );
7450
8141
 
7451
8142
  bool isBelowSurface = ! surf.volumeParticle && dot( scatterRec.direction, surf.faceNormal ) < 0.0;
7452
8143
  vec3 hitPoint = stepRayOrigin( ray.origin, ray.direction, isBelowSurface ? - surf.faceNormal : surf.faceNormal, surfaceHit.dist );
@@ -7516,7 +8207,7 @@ bool bvhIntersectFogVolumeHit(
7516
8207
  rrProb = sqrt( rrProb );
7517
8208
  rrProb = max( rrProb, depthProb );
7518
8209
  rrProb = min( rrProb, 1.0 );
7519
- if ( sobol( 8 ) > rrProb ) {
8210
+ if ( rand( 8 ) > rrProb ) {
7520
8211
 
7521
8212
  break;
7522
8213
 
@@ -7947,10 +8638,9 @@ bool bvhIntersectFogVolumeHit(
7947
8638
  exports.QuiltPathTracingRenderer = QuiltPathTracingRenderer;
7948
8639
  exports.RenderTarget2DArray = RenderTarget2DArray;
7949
8640
  exports.ShapedAreaLight = ShapedAreaLight;
8641
+ exports.getDummyMesh = getDummyMesh;
7950
8642
  exports.getGroupMaterialIndicesAttribute = getGroupMaterialIndicesAttribute;
7951
- exports.mergeMeshes = mergeMeshes;
7952
8643
  exports.setCommonAttributes = setCommonAttributes;
7953
- exports.trimToAttributes = trimToAttributes;
7954
8644
 
7955
8645
  Object.defineProperty(exports, '__esModule', { value: true });
7956
8646