three-gpu-pathtracer 0.0.17 → 0.0.18

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 (34) hide show
  1. package/build/index.module.js +1000 -319
  2. package/build/index.module.js.map +1 -1
  3. package/build/index.umd.cjs +996 -316
  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 +33 -11
  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 +3 -3
  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/structs/lightsStruct.glsl.js +5 -7
  21. package/src/textures/BlueNoiseTexture.js +87 -0
  22. package/src/textures/ProceduralEquirectTexture.js +7 -8
  23. package/src/textures/blueNoise/BlueNoiseGenerator.js +115 -0
  24. package/src/textures/blueNoise/BlueNoiseSamples.js +214 -0
  25. package/src/textures/blueNoise/utils.js +24 -0
  26. package/src/uniforms/EquirectHdrInfoUniform.js +45 -8
  27. package/src/uniforms/LightsInfoUniformStruct.js +11 -7
  28. package/src/uniforms/RenderTarget2DArray.js +50 -3
  29. package/src/uniforms/StratifiedSamplesTexture.js +49 -0
  30. package/src/uniforms/stratified/StratifiedSampler.js +73 -0
  31. package/src/uniforms/stratified/StratifiedSamplerCombined.js +59 -0
  32. package/src/uniforms/utils.js +1 -1
  33. package/src/utils/GeometryPreparationUtils.js +8 -101
  34. 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
 
@@ -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;
2569
2525
 
2570
- newData = new Uint16Array( data.length );
2571
- for ( const i in data ) {
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 ) {
2572
2548
 
2573
- newData[ i ] = data[ i ];
2549
+ newData[ i ] = three.DataUtils.toHalfFloat( v );
2550
+
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; 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 );
@@ -6160,32 +6144,38 @@ bool bvhIntersectFogVolumeHit(
6160
6144
 
6161
6145
  }
6162
6146
 
6163
- float evaluateFresnelWeight( float cosTheta, float eta, float f0 ) {
6147
+ // TODO: disney fresnel was removed and replaced with this fresnel function to better align with
6148
+ // the glTF but is causing blown out pixels. Should be revisited
6149
+ // float evaluateFresnelWeight( float cosTheta, float eta, float f0 ) {
6164
6150
 
6165
- if ( totalInternalReflection( cosTheta, eta ) ) {
6151
+ // if ( totalInternalReflection( cosTheta, eta ) ) {
6166
6152
 
6167
- return 1.0;
6153
+ // return 1.0;
6168
6154
 
6169
- }
6155
+ // }
6170
6156
 
6171
- return schlickFresnel( cosTheta, f0 );
6157
+ // return schlickFresnel( cosTheta, f0 );
6172
6158
 
6173
- }
6159
+ // }
6174
6160
 
6175
- /*
6176
6161
  // https://schuttejoe.github.io/post/disneybsdf/
6177
6162
  float disneyFresnel( vec3 wo, vec3 wi, vec3 wh, float f0, float eta, float metalness ) {
6178
6163
 
6179
6164
  float dotHV = dot( wo, wh );
6180
- float dotHL = dot( wi, wh );
6165
+ if ( totalInternalReflection( dotHV, eta ) ) {
6166
+
6167
+ return 1.0;
6181
6168
 
6169
+ }
6170
+
6171
+ float dotHL = dot( wi, wh );
6182
6172
  float dielectricFresnel = dielectricFresnel( abs( dotHV ), eta );
6183
6173
  float metallicFresnel = schlickFresnel( dotHL, f0 );
6184
6174
 
6185
6175
  return mix( dielectricFresnel, metallicFresnel, metalness );
6186
6176
 
6187
6177
  }
6188
- */
6178
+
6189
6179
  `;
6190
6180
 
6191
6181
  const arraySamplerTexelFetchGLSL = /*glsl */`
@@ -6242,28 +6232,28 @@ bool bvhIntersectFogVolumeHit(
6242
6232
  }
6243
6233
 
6244
6234
  // returns [ 0, 1 ]
6245
- float rand() {
6235
+ float pcgRand() {
6246
6236
 
6247
6237
  pcg4d( WHITE_NOISE_SEED );
6248
6238
  return float( WHITE_NOISE_SEED.x ) / float( 0xffffffffu );
6249
6239
 
6250
6240
  }
6251
6241
 
6252
- vec2 rand2() {
6242
+ vec2 pcgRand2() {
6253
6243
 
6254
6244
  pcg4d( WHITE_NOISE_SEED );
6255
6245
  return vec2( WHITE_NOISE_SEED.xy ) / float(0xffffffffu);
6256
6246
 
6257
6247
  }
6258
6248
 
6259
- vec3 rand3() {
6249
+ vec3 pcgRand3() {
6260
6250
 
6261
6251
  pcg4d( WHITE_NOISE_SEED );
6262
6252
  return vec3( WHITE_NOISE_SEED.xyz ) / float( 0xffffffffu );
6263
6253
 
6264
6254
  }
6265
6255
 
6266
- vec4 rand4() {
6256
+ vec4 pcgRand4() {
6267
6257
 
6268
6258
  pcg4d( WHITE_NOISE_SEED );
6269
6259
  return vec4( WHITE_NOISE_SEED ) / float( 0xffffffffu );
@@ -6336,7 +6326,7 @@ bool bvhIntersectFogVolumeHit(
6336
6326
 
6337
6327
  // Jitter the camera ray by finding a uv coordinate at a random sample
6338
6328
  // around this pixel's UV coordinate for AA
6339
- vec2 ruv = sobol2( 0 );
6329
+ vec2 ruv = rand2( 0 );
6340
6330
  vec2 jitteredUv = vUv + vec2( tentFilter( ruv.x ) * ssd.x, tentFilter( ruv.y ) * ssd.y );
6341
6331
  Ray ray;
6342
6332
 
@@ -6381,7 +6371,7 @@ bool bvhIntersectFogVolumeHit(
6381
6371
 
6382
6372
  // get the aperture sample
6383
6373
  // if blades === 0 then we assume a circle
6384
- vec3 shapeUVW= sobol3( 1 );
6374
+ vec3 shapeUVW= rand3( 1 );
6385
6375
  int blades = physicalCamera.apertureBlades;
6386
6376
  float anamorphicRatio = physicalCamera.anamorphicRatio;
6387
6377
  vec2 apertureSample = blades == 0 ? sampleCircle( shapeUVW.xy ) : sampleRegularPolygon( blades, shapeUVW );
@@ -6417,6 +6407,9 @@ bool bvhIntersectFogVolumeHit(
6417
6407
  out vec3 color
6418
6408
  ) {
6419
6409
 
6410
+ // store the original bounce index so we can reset it after
6411
+ uint originalBounceIndex = sobolBounceIndex;
6412
+
6420
6413
  int traversals = state.traversals;
6421
6414
  int transmissiveTraversals = state.transmissiveTraversals;
6422
6415
  bool isShadowRay = state.isShadowRay;
@@ -6429,22 +6422,25 @@ bool bvhIntersectFogVolumeHit(
6429
6422
 
6430
6423
  color = vec3( 1.0 );
6431
6424
 
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.
6425
+ bool result = true;
6434
6426
  for ( int i = 0; i < traversals; i ++ ) {
6435
6427
 
6428
+ sobolBounceIndex ++;
6429
+
6436
6430
  int hitType = traceScene( ray, fogMaterial, surfaceHit );
6437
6431
 
6438
6432
  if ( hitType == FOG_HIT ) {
6439
6433
 
6440
- return true;
6434
+ result = true;
6435
+ break;
6441
6436
 
6442
6437
  } else if ( hitType == SURFACE_HIT ) {
6443
6438
 
6444
6439
  float totalDist = distance( startPoint, ray.origin + ray.direction * surfaceHit.dist );
6445
6440
  if ( totalDist > rayDist ) {
6446
6441
 
6447
- return false;
6442
+ result = false;
6443
+ break;
6448
6444
 
6449
6445
  }
6450
6446
 
@@ -6526,7 +6522,7 @@ bool bvhIntersectFogVolumeHit(
6526
6522
  bool useAlphaTest = alphaTest != 0.0;
6527
6523
  float transmissionFactor = ( 1.0 - metalness ) * transmission;
6528
6524
  if (
6529
- transmissionFactor < rand() && ! (
6525
+ transmissionFactor < rand( 9 ) && ! (
6530
6526
  // material sidedness
6531
6527
  material.side != 0.0 && surfaceHit.side == material.side
6532
6528
 
@@ -6534,11 +6530,12 @@ bool bvhIntersectFogVolumeHit(
6534
6530
  || useAlphaTest && albedo.a < alphaTest
6535
6531
 
6536
6532
  // opacity
6537
- || material.transparent && ! useAlphaTest && albedo.a < rand()
6533
+ || material.transparent && ! useAlphaTest && albedo.a < rand( 10 )
6538
6534
  )
6539
6535
  ) {
6540
6536
 
6541
- return true;
6537
+ result = true;
6538
+ break;
6542
6539
 
6543
6540
  }
6544
6541
 
@@ -6564,13 +6561,16 @@ bool bvhIntersectFogVolumeHit(
6564
6561
 
6565
6562
  } else {
6566
6563
 
6567
- return false;
6564
+ result = false;
6565
+ break;
6568
6566
 
6569
6567
  }
6570
6568
 
6571
6569
  }
6572
6570
 
6573
- return true;
6571
+ // reset the bounce index
6572
+ sobolBounceIndex = originalBounceIndex;
6573
+ return result;
6574
6574
 
6575
6575
  }
6576
6576
 
@@ -6601,7 +6601,7 @@ bool bvhIntersectFogVolumeHit(
6601
6601
 
6602
6602
  // offset the distance so we don't run into issues with particles on the same surface
6603
6603
  // as other objects
6604
- float particleDist = intersectFogVolume( fogMaterial, sobol( 1 ) );
6604
+ float particleDist = intersectFogVolume( fogMaterial, rand( 1 ) );
6605
6605
  if ( particleDist + RAY_OFFSET < surfaceHit.dist ) {
6606
6606
 
6607
6607
  surfaceHit.side = 1.0;
@@ -6664,6 +6664,7 @@ bool bvhIntersectFogVolumeHit(
6664
6664
 
6665
6665
  vec3 uvPrime = material.mapTransform * vec3( uv, 1 );
6666
6666
  albedo *= texture2D( textures, vec3( uvPrime.xy, material.map ) );
6667
+
6667
6668
  }
6668
6669
 
6669
6670
  if ( material.vertexColors ) {
@@ -6695,7 +6696,7 @@ bool bvhIntersectFogVolumeHit(
6695
6696
  || useAlphaTest && albedo.a < alphaTest
6696
6697
 
6697
6698
  // opacity
6698
- || material.transparent && ! useAlphaTest && albedo.a < sobol( 3 )
6699
+ || material.transparent && ! useAlphaTest && albedo.a < rand( 3 )
6699
6700
  ) {
6700
6701
 
6701
6702
  return SKIP_SURFACE;
@@ -6957,10 +6958,10 @@ bool bvhIntersectFogVolumeHit(
6957
6958
  vec3 result = vec3( 0.0 );
6958
6959
 
6959
6960
  // uniformly pick a light or environment map
6960
- if( lightsDenom != 0.0 && sobol( 5 ) < float( lights.count ) / lightsDenom ) {
6961
+ if( lightsDenom != 0.0 && rand( 5 ) < float( lights.count ) / lightsDenom ) {
6961
6962
 
6962
6963
  // sample a light or environment
6963
- LightRecord lightRec = randomLightSample( lights.tex, iesProfiles, lights.count, rayOrigin, sobol3( 6 ) );
6964
+ LightRecord lightRec = randomLightSample( lights.tex, iesProfiles, lights.count, rayOrigin, rand3( 6 ) );
6964
6965
 
6965
6966
  bool isSampleBelowSurface = ! surf.volumeParticle && dot( surf.faceNormal, lightRec.direction ) < 0.0;
6966
6967
  if ( isSampleBelowSurface ) {
@@ -6999,7 +7000,7 @@ bool bvhIntersectFogVolumeHit(
6999
7000
 
7000
7001
  // find a sample in the environment map to include in the contribution
7001
7002
  vec3 envColor, envDirection;
7002
- float envPdf = sampleEquirectProbability( sobol2( 7 ), envColor, envDirection );
7003
+ float envPdf = sampleEquirectProbability( rand2( 7 ), envColor, envDirection );
7003
7004
  envDirection = invEnvRotation3x3 * envDirection;
7004
7005
 
7005
7006
  // this env sampling is not set up for transmissive sampling and yields overly bright
@@ -7048,6 +7049,667 @@ bool bvhIntersectFogVolumeHit(
7048
7049
 
7049
7050
  `;
7050
7051
 
7052
+ const stratifiedTextureGLSL = /* glsl */`
7053
+
7054
+ uniform sampler2D stratifiedTexture;
7055
+ uniform sampler2D stratifiedOffsetTexture;
7056
+
7057
+ uint sobolPixelIndex = 0u;
7058
+ uint sobolPathIndex = 0u;
7059
+ uint sobolBounceIndex = 0u;
7060
+ vec4 pixelSeed = vec4( 0 );
7061
+
7062
+ vec4 rand4( int v ) {
7063
+
7064
+ ivec2 uv = ivec2( v, sobolBounceIndex );
7065
+ vec4 stratifiedSample = texelFetch( stratifiedTexture, uv, 0 );
7066
+ return fract( stratifiedSample + pixelSeed.r ); // blue noise + stratified samples
7067
+
7068
+ }
7069
+
7070
+ vec3 rand3( int v ) {
7071
+
7072
+ return rand4( v ).xyz;
7073
+
7074
+ }
7075
+
7076
+ vec2 rand2( int v ) {
7077
+
7078
+ return rand4( v ).xy;
7079
+
7080
+ }
7081
+
7082
+ float rand( int v ) {
7083
+
7084
+ return rand4( v ).x;
7085
+
7086
+ }
7087
+
7088
+ void rng_initialize( vec2 screenCoord, int frame ) {
7089
+
7090
+ // tile the small noise texture across the entire screen
7091
+ ivec2 noiseSize = ivec2( textureSize( stratifiedOffsetTexture, 0 ) );
7092
+ pixelSeed = texelFetch( stratifiedOffsetTexture, ivec2( screenCoord.xy ) % noiseSize, 0 );
7093
+
7094
+ }
7095
+
7096
+ `;
7097
+
7098
+ // Stratified Sampling based on implementation from hoverinc pathtracer
7099
+ // - https://github.com/hoverinc/ray-tracing-renderer
7100
+ // - http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html
7101
+
7102
+ function shuffle( arr ) {
7103
+
7104
+ for ( let i = arr.length - 1; i > 0; i -- ) {
7105
+
7106
+ const j = Math.floor( Math.random() * ( i + 1 ) );
7107
+ const x = arr[ i ];
7108
+ arr[ i ] = arr[ j ];
7109
+ arr[ j ] = x;
7110
+
7111
+ }
7112
+
7113
+ return arr;
7114
+
7115
+ }
7116
+
7117
+ // strataCount : The number of bins per dimension
7118
+ // dimensions : The number of dimensions to generate stratified values for
7119
+ class StratifiedSampler {
7120
+
7121
+ constructor( strataCount, dimensions ) {
7122
+
7123
+ const l = strataCount ** dimensions;
7124
+ const strata = new Uint16Array( l );
7125
+ let index = l;
7126
+
7127
+ // each integer represents a statum bin
7128
+ for ( let i = 0; i < l; i ++ ) {
7129
+
7130
+ strata[ i ] = i;
7131
+
7132
+ }
7133
+
7134
+ this.samples = new Float32Array( dimensions );
7135
+
7136
+ this.strataCount = strataCount;
7137
+
7138
+ this.restart = function () {
7139
+
7140
+ index = 0;
7141
+
7142
+ };
7143
+
7144
+ this.next = function () {
7145
+
7146
+ const { samples } = this;
7147
+
7148
+ if ( index >= strata.length ) {
7149
+
7150
+ shuffle( strata );
7151
+ this.restart();
7152
+
7153
+ }
7154
+
7155
+ let stratum = strata[ index ++ ];
7156
+
7157
+ for ( let i = 0; i < dimensions; i ++ ) {
7158
+
7159
+ samples[ i ] = ( stratum % strataCount + Math.random() ) / strataCount;
7160
+ stratum = Math.floor( stratum / strataCount );
7161
+
7162
+ }
7163
+
7164
+ return samples;
7165
+
7166
+ };
7167
+
7168
+ }
7169
+
7170
+ }
7171
+
7172
+ // Stratified Sampling based on implementation from hoverinc pathtracer
7173
+
7174
+ // Stratified set of data with each tuple stratified separately and combined
7175
+ class StratifiedSamplerCombined {
7176
+
7177
+ constructor( strataCount, listOfDimensions ) {
7178
+
7179
+ let totalDim = 0;
7180
+ for ( const dim of listOfDimensions ) {
7181
+
7182
+ totalDim += dim;
7183
+
7184
+ }
7185
+
7186
+ const combined = new Float32Array( totalDim );
7187
+ const strataObjs = [];
7188
+ let offset = 0;
7189
+ for ( const dim of listOfDimensions ) {
7190
+
7191
+ const sampler = new StratifiedSampler( strataCount, dim );
7192
+ sampler.samples = new Float32Array( combined.buffer, offset, sampler.samples.length );
7193
+ offset += sampler.samples.length * 4;
7194
+ strataObjs.push( sampler );
7195
+
7196
+ }
7197
+
7198
+ this.samples = combined;
7199
+
7200
+ this.strataCount = strataCount;
7201
+
7202
+ this.next = function () {
7203
+
7204
+ for ( const strata of strataObjs ) {
7205
+
7206
+ strata.next();
7207
+
7208
+ }
7209
+
7210
+ return combined;
7211
+
7212
+ };
7213
+
7214
+ this.restart = function () {
7215
+
7216
+ for ( const strata of strataObjs ) {
7217
+
7218
+ strata.restart();
7219
+
7220
+ }
7221
+
7222
+ };
7223
+
7224
+ }
7225
+
7226
+ }
7227
+
7228
+ class StratifiedSamplesTexture extends three.DataTexture {
7229
+
7230
+ constructor( count = 1, depth = 1, strata = 8 ) {
7231
+
7232
+ super( new Float32Array( 1 ), 1, 1, three.RGBAFormat, three.FloatType );
7233
+ this.minFilter = three.NearestFilter;
7234
+ this.magFilter = three.NearestFilter;
7235
+
7236
+ this.strata = strata;
7237
+ this.sampler = null;
7238
+
7239
+ this.init( count, depth, strata );
7240
+
7241
+ }
7242
+
7243
+ init( count, depth, strata = this.strata ) {
7244
+
7245
+ const { image } = this;
7246
+ if ( image.width === depth && image.height === count ) {
7247
+
7248
+ return;
7249
+
7250
+ }
7251
+
7252
+ const dimensions = new Array( count * depth ).fill( 4 );
7253
+ const sampler = new StratifiedSamplerCombined( strata, dimensions );
7254
+
7255
+ image.width = depth;
7256
+ image.height = count;
7257
+ image.data = sampler.samples;
7258
+
7259
+ this.sampler = sampler;
7260
+
7261
+ this.dispose();
7262
+ this.next();
7263
+
7264
+ }
7265
+
7266
+ next() {
7267
+
7268
+ this.sampler.next();
7269
+ this.needsUpdate = true;
7270
+
7271
+ }
7272
+
7273
+ }
7274
+
7275
+ function shuffleArray( array, random = Math.random ) {
7276
+
7277
+ for ( let i = array.length - 1; i > 0; i -- ) {
7278
+
7279
+ const replaceIndex = ~ ~ ( ( random() - 1e-6 ) * i );
7280
+ const tmp = array[ i ];
7281
+ array[ i ] = array[ replaceIndex ];
7282
+ array[ replaceIndex ] = tmp;
7283
+
7284
+ }
7285
+
7286
+ }
7287
+
7288
+ function fillWithOnes( array, count ) {
7289
+
7290
+ array.fill( 0 );
7291
+
7292
+ for ( let i = 0; i < count; i ++ ) {
7293
+
7294
+ array[ i ] = 1;
7295
+
7296
+ }
7297
+
7298
+ }
7299
+
7300
+ class BlueNoiseSamples {
7301
+
7302
+ constructor( size ) {
7303
+
7304
+ this.count = 0;
7305
+ this.size = - 1;
7306
+ this.sigma = - 1;
7307
+ this.radius = - 1;
7308
+ this.lookupTable = null;
7309
+ this.score = null;
7310
+ this.binaryPattern = null;
7311
+
7312
+ this.resize( size );
7313
+ this.setSigma( 1.5 );
7314
+
7315
+ }
7316
+
7317
+ findVoid() {
7318
+
7319
+ const { score, binaryPattern } = this;
7320
+
7321
+ let currValue = Infinity;
7322
+ let currIndex = - 1;
7323
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
7324
+
7325
+ if ( binaryPattern[ i ] !== 0 ) {
7326
+
7327
+ continue;
7328
+
7329
+ }
7330
+
7331
+ const pScore = score[ i ];
7332
+ if ( pScore < currValue ) {
7333
+
7334
+ currValue = pScore;
7335
+ currIndex = i;
7336
+
7337
+ }
7338
+
7339
+ }
7340
+
7341
+ return currIndex;
7342
+
7343
+ }
7344
+
7345
+ findCluster() {
7346
+
7347
+ const { score, binaryPattern } = this;
7348
+
7349
+ let currValue = - Infinity;
7350
+ let currIndex = - 1;
7351
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
7352
+
7353
+ if ( binaryPattern[ i ] !== 1 ) {
7354
+
7355
+ continue;
7356
+
7357
+ }
7358
+
7359
+ const pScore = score[ i ];
7360
+ if ( pScore > currValue ) {
7361
+
7362
+ currValue = pScore;
7363
+ currIndex = i;
7364
+
7365
+ }
7366
+
7367
+ }
7368
+
7369
+ return currIndex;
7370
+
7371
+ }
7372
+
7373
+ setSigma( sigma ) {
7374
+
7375
+ if ( sigma === this.sigma ) {
7376
+
7377
+ return;
7378
+
7379
+ }
7380
+
7381
+ // generate a radius in which the score will be updated under the
7382
+ // assumption that e^-10 is insignificant enough to be the border at
7383
+ // which we drop off.
7384
+ const radius = ~ ~ ( Math.sqrt( 10 * 2 * ( sigma ** 2 ) ) + 1 );
7385
+ const lookupWidth = 2 * radius + 1;
7386
+ const lookupTable = new Float32Array( lookupWidth * lookupWidth );
7387
+ const sigma2 = sigma * sigma;
7388
+ for ( let x = - radius; x <= radius; x ++ ) {
7389
+
7390
+ for ( let y = - radius; y <= radius; y ++ ) {
7391
+
7392
+ const index = ( radius + y ) * lookupWidth + x + radius;
7393
+ const dist2 = x * x + y * y;
7394
+ lookupTable[ index ] = Math.E ** ( - dist2 / ( 2 * sigma2 ) );
7395
+
7396
+ }
7397
+
7398
+ }
7399
+
7400
+ this.lookupTable = lookupTable;
7401
+ this.sigma = sigma;
7402
+ this.radius = radius;
7403
+
7404
+ }
7405
+
7406
+ resize( size ) {
7407
+
7408
+ if ( this.size !== size ) {
7409
+
7410
+ this.size = size;
7411
+ this.score = new Float32Array( size * size );
7412
+ this.binaryPattern = new Uint8Array( size * size );
7413
+
7414
+ }
7415
+
7416
+
7417
+ }
7418
+
7419
+ invert() {
7420
+
7421
+ const { binaryPattern, score, size } = this;
7422
+
7423
+ score.fill( 0 );
7424
+
7425
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
7426
+
7427
+ if ( binaryPattern[ i ] === 0 ) {
7428
+
7429
+ const y = ~ ~ ( i / size );
7430
+ const x = i - y * size;
7431
+ this.updateScore( x, y, 1 );
7432
+ binaryPattern[ i ] = 1;
7433
+
7434
+ } else {
7435
+
7436
+ binaryPattern[ i ] = 0;
7437
+
7438
+ }
7439
+
7440
+ }
7441
+
7442
+ }
7443
+
7444
+ updateScore( x, y, multiplier ) {
7445
+
7446
+ // TODO: Is there a way to keep track of the highest and lowest scores here to avoid have to search over
7447
+ // everything in the buffer?
7448
+ const { size, score, lookupTable } = this;
7449
+
7450
+ // const sigma2 = sigma * sigma;
7451
+ // const radius = Math.floor( size / 2 );
7452
+ const radius = this.radius;
7453
+ const lookupWidth = 2 * radius + 1;
7454
+ for ( let px = - radius; px <= radius; px ++ ) {
7455
+
7456
+ for ( let py = - radius; py <= radius; py ++ ) {
7457
+
7458
+ // const dist2 = px * px + py * py;
7459
+ // const value = Math.E ** ( - dist2 / ( 2 * sigma2 ) );
7460
+
7461
+ const lookupIndex = ( radius + py ) * lookupWidth + px + radius;
7462
+ const value = lookupTable[ lookupIndex ];
7463
+
7464
+ let sx = ( x + px );
7465
+ sx = sx < 0 ? size + sx : sx % size;
7466
+
7467
+ let sy = ( y + py );
7468
+ sy = sy < 0 ? size + sy : sy % size;
7469
+
7470
+ const sindex = sy * size + sx;
7471
+ score[ sindex ] += multiplier * value;
7472
+
7473
+ }
7474
+
7475
+ }
7476
+
7477
+ }
7478
+
7479
+ addPointIndex( index ) {
7480
+
7481
+ this.binaryPattern[ index ] = 1;
7482
+
7483
+ const size = this.size;
7484
+ const y = ~ ~ ( index / size );
7485
+ const x = index - y * size;
7486
+ this.updateScore( x, y, 1 );
7487
+ this.count ++;
7488
+
7489
+ }
7490
+
7491
+ removePointIndex( index ) {
7492
+
7493
+ this.binaryPattern[ index ] = 0;
7494
+
7495
+ const size = this.size;
7496
+ const y = ~ ~ ( index / size );
7497
+ const x = index - y * size;
7498
+ this.updateScore( x, y, - 1 );
7499
+ this.count --;
7500
+
7501
+ }
7502
+
7503
+ copy( source ) {
7504
+
7505
+ this.resize( source.size );
7506
+ this.score.set( source.score );
7507
+ this.binaryPattern.set( source.binaryPattern );
7508
+ this.setSigma( source.sigma );
7509
+ this.count = source.count;
7510
+
7511
+ }
7512
+
7513
+ }
7514
+
7515
+ class BlueNoiseGenerator {
7516
+
7517
+ constructor() {
7518
+
7519
+ this.random = Math.random;
7520
+ this.sigma = 1.5;
7521
+ this.size = 64;
7522
+ this.majorityPointsRatio = 0.1;
7523
+
7524
+ this.samples = new BlueNoiseSamples( 1 );
7525
+ this.savedSamples = new BlueNoiseSamples( 1 );
7526
+
7527
+ }
7528
+
7529
+ generate() {
7530
+
7531
+ // http://cv.ulichney.com/papers/1993-void-cluster.pdf
7532
+
7533
+ const {
7534
+ samples,
7535
+ savedSamples,
7536
+ sigma,
7537
+ majorityPointsRatio,
7538
+ size,
7539
+ } = this;
7540
+
7541
+ samples.resize( size );
7542
+ samples.setSigma( sigma );
7543
+
7544
+ // 1. Randomly place the minority points.
7545
+ const pointCount = Math.floor( size * size * majorityPointsRatio );
7546
+ const initialSamples = samples.binaryPattern;
7547
+
7548
+ fillWithOnes( initialSamples, pointCount );
7549
+ shuffleArray( initialSamples, this.random );
7550
+
7551
+ for ( let i = 0, l = initialSamples.length; i < l; i ++ ) {
7552
+
7553
+ if ( initialSamples[ i ] === 1 ) {
7554
+
7555
+ samples.addPointIndex( i );
7556
+
7557
+ }
7558
+
7559
+ }
7560
+
7561
+ // 2. Remove minority point that is in densest cluster and place it in the largest void.
7562
+ while ( true ) {
7563
+
7564
+ const clusterIndex = samples.findCluster();
7565
+ samples.removePointIndex( clusterIndex );
7566
+
7567
+ const voidIndex = samples.findVoid();
7568
+ if ( clusterIndex === voidIndex ) {
7569
+
7570
+ samples.addPointIndex( clusterIndex );
7571
+ break;
7572
+
7573
+ }
7574
+
7575
+ samples.addPointIndex( voidIndex );
7576
+
7577
+ }
7578
+
7579
+ // 3. PHASE I: Assign a rank to each progressively less dense cluster point and put it
7580
+ // in the dither array.
7581
+ const ditherArray = new Uint32Array( size * size );
7582
+ savedSamples.copy( samples );
7583
+
7584
+ let rank;
7585
+ rank = samples.count - 1;
7586
+ while ( rank >= 0 ) {
7587
+
7588
+ const clusterIndex = samples.findCluster();
7589
+ samples.removePointIndex( clusterIndex );
7590
+
7591
+ ditherArray[ clusterIndex ] = rank;
7592
+ rank --;
7593
+
7594
+ }
7595
+
7596
+ // 4. PHASE II: Do the same thing for the largest voids up to half of the total pixels using
7597
+ // the initial binary pattern.
7598
+ const totalSize = size * size;
7599
+ rank = savedSamples.count;
7600
+ while ( rank < totalSize / 2 ) {
7601
+
7602
+ const voidIndex = savedSamples.findVoid();
7603
+ savedSamples.addPointIndex( voidIndex );
7604
+ ditherArray[ voidIndex ] = rank;
7605
+ rank ++;
7606
+
7607
+ }
7608
+
7609
+ // 5. PHASE III: Invert the pattern and finish out by assigning a rank to the remaining
7610
+ // and iteratively removing them.
7611
+ savedSamples.invert();
7612
+
7613
+ while ( rank < totalSize ) {
7614
+
7615
+ const clusterIndex = savedSamples.findCluster();
7616
+ savedSamples.removePointIndex( clusterIndex );
7617
+ ditherArray[ clusterIndex ] = rank;
7618
+ rank ++;
7619
+
7620
+ }
7621
+
7622
+ return { data: ditherArray, maxValue: totalSize };
7623
+
7624
+ }
7625
+
7626
+ }
7627
+
7628
+ function getStride( channels ) {
7629
+
7630
+ if ( channels >= 3 ) {
7631
+
7632
+ return 4;
7633
+
7634
+ } else {
7635
+
7636
+ return channels;
7637
+
7638
+ }
7639
+
7640
+ }
7641
+
7642
+ function getFormat( channels ) {
7643
+
7644
+ switch ( channels ) {
7645
+
7646
+ case 1:
7647
+ return three.RedFormat;
7648
+ case 2:
7649
+ return three.RGFormat;
7650
+ default:
7651
+ return three.RGBAFormat;
7652
+
7653
+ }
7654
+
7655
+ }
7656
+
7657
+ class BlueNoiseTexture extends three.DataTexture {
7658
+
7659
+ constructor( size = 64, channels = 1 ) {
7660
+
7661
+ super( new Float32Array( 4 ), 1, 1, three.RGBAFormat, three.FloatType );
7662
+ this.minFilter = three.NearestFilter;
7663
+ this.magFilter = three.NearestFilter;
7664
+
7665
+ this.size = size;
7666
+ this.channels = channels;
7667
+ this.update();
7668
+
7669
+ }
7670
+
7671
+ update() {
7672
+
7673
+ const channels = this.channels;
7674
+ const size = this.size;
7675
+ const generator = new BlueNoiseGenerator();
7676
+ generator.channels = channels;
7677
+ generator.size = size;
7678
+
7679
+ const stride = getStride( channels );
7680
+ const format = getFormat( stride );
7681
+ if ( this.image.width !== size || format !== this.format ) {
7682
+
7683
+ this.image.width = size;
7684
+ this.image.height = size;
7685
+ this.image.data = new Float32Array( ( size ** 2 ) * stride );
7686
+ this.format = format;
7687
+ this.dispose();
7688
+
7689
+ }
7690
+
7691
+ const data = this.image.data;
7692
+ for ( let i = 0, l = channels; i < l; i ++ ) {
7693
+
7694
+ const result = generator.generate();
7695
+ const bin = result.data;
7696
+ const maxValue = result.maxValue;
7697
+
7698
+ for ( let j = 0, l2 = bin.length; j < l2; j ++ ) {
7699
+
7700
+ const value = bin[ j ] / maxValue;
7701
+ data[ j * stride + i ] = value;
7702
+
7703
+ }
7704
+
7705
+ }
7706
+
7707
+ this.needsUpdate = true;
7708
+
7709
+ }
7710
+
7711
+ }
7712
+
7051
7713
  class PhysicalPathTracingMaterial extends MaterialBase {
7052
7714
 
7053
7715
  onBeforeRender() {
@@ -7071,7 +7733,12 @@ bool bvhIntersectFogVolumeHit(
7071
7733
  FEATURE_DOF: 1,
7072
7734
  FEATURE_BACKGROUND_MAP: 0,
7073
7735
  FEATURE_FOG: 1,
7074
- FEATURE_SOBOL: 0,
7736
+
7737
+ // 0 = PCG
7738
+ // 1 = Sobol
7739
+ // 2 = Stratified List
7740
+ RANDOM_TYPE: 2,
7741
+
7075
7742
  // 0 = Perspective
7076
7743
  // 1 = Orthographic
7077
7744
  // 2 = Equirectangular
@@ -7113,6 +7780,8 @@ bool bvhIntersectFogVolumeHit(
7113
7780
 
7114
7781
  backgroundAlpha: { value: 1.0 },
7115
7782
  sobolTexture: { value: null },
7783
+ stratifiedTexture: { value: new StratifiedSamplesTexture() },
7784
+ stratifiedOffsetTexture: { value: new BlueNoiseTexture( 64, 1 ) },
7116
7785
  },
7117
7786
 
7118
7787
  vertexShader: /* glsl */`
@@ -7152,23 +7821,35 @@ bool bvhIntersectFogVolumeHit(
7152
7821
  ${ materialStructGLSL }
7153
7822
 
7154
7823
  // random
7155
- ${ pcgGLSL }
7156
- #if FEATURE_SOBOL
7824
+ #if RANDOM_TYPE == 2 // Stratified List
7157
7825
 
7826
+ ${ stratifiedTextureGLSL }
7827
+
7828
+ #elif RANDOM_TYPE == 1 // Sobol
7829
+
7830
+ ${ pcgGLSL }
7158
7831
  ${ sobolCommonGLSL }
7159
7832
  ${ sobolSamplingGLSL }
7160
7833
 
7161
- #else
7834
+ #define rand(v) sobol(v)
7835
+ #define rand2(v) sobol2(v)
7836
+ #define rand3(v) sobol3(v)
7837
+ #define rand4(v) sobol4(v)
7838
+
7839
+ #else // PCG
7840
+
7841
+ ${ pcgGLSL }
7162
7842
 
7163
7843
  // Using the sobol functions seems to break the the compiler on MacOS
7164
7844
  // - specifically the "sobolReverseBits" function.
7165
7845
  uint sobolPixelIndex = 0u;
7166
7846
  uint sobolPathIndex = 0u;
7167
7847
  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(); }
7848
+
7849
+ #define rand(v) pcgRand()
7850
+ #define rand2(v) pcgRand2()
7851
+ #define rand3(v) pcgRand3()
7852
+ #define rand4(v) pcgRand4()
7172
7853
 
7173
7854
  #endif
7174
7855
 
@@ -7355,7 +8036,7 @@ bool bvhIntersectFogVolumeHit(
7355
8036
 
7356
8037
  if ( state.firstRay || state.transmissiveRay ) {
7357
8038
 
7358
- gl_FragColor.rgb += sampleBackground( envRotation3x3 * ray.direction, sobol2( 2 ) ) * state.throughputColor;
8039
+ gl_FragColor.rgb += sampleBackground( envRotation3x3 * ray.direction, rand2( 2 ) ) * state.throughputColor;
7359
8040
  gl_FragColor.a = backgroundAlpha;
7360
8041
 
7361
8042
  } else {
@@ -7446,7 +8127,7 @@ bool bvhIntersectFogVolumeHit(
7446
8127
  }
7447
8128
 
7448
8129
  scatterRec = bsdfSample( - ray.direction, surf );
7449
- state.isShadowRay = scatterRec.specularPdf < sobol( 4 );
8130
+ state.isShadowRay = scatterRec.specularPdf < rand( 4 );
7450
8131
 
7451
8132
  bool isBelowSurface = ! surf.volumeParticle && dot( scatterRec.direction, surf.faceNormal ) < 0.0;
7452
8133
  vec3 hitPoint = stepRayOrigin( ray.origin, ray.direction, isBelowSurface ? - surf.faceNormal : surf.faceNormal, surfaceHit.dist );
@@ -7516,7 +8197,7 @@ bool bvhIntersectFogVolumeHit(
7516
8197
  rrProb = sqrt( rrProb );
7517
8198
  rrProb = max( rrProb, depthProb );
7518
8199
  rrProb = min( rrProb, 1.0 );
7519
- if ( sobol( 8 ) > rrProb ) {
8200
+ if ( rand( 8 ) > rrProb ) {
7520
8201
 
7521
8202
  break;
7522
8203
 
@@ -7947,10 +8628,9 @@ bool bvhIntersectFogVolumeHit(
7947
8628
  exports.QuiltPathTracingRenderer = QuiltPathTracingRenderer;
7948
8629
  exports.RenderTarget2DArray = RenderTarget2DArray;
7949
8630
  exports.ShapedAreaLight = ShapedAreaLight;
8631
+ exports.getDummyMesh = getDummyMesh;
7950
8632
  exports.getGroupMaterialIndicesAttribute = getGroupMaterialIndicesAttribute;
7951
- exports.mergeMeshes = mergeMeshes;
7952
8633
  exports.setCommonAttributes = setCommonAttributes;
7953
- exports.trimToAttributes = trimToAttributes;
7954
8634
 
7955
8635
  Object.defineProperty(exports, '__esModule', { value: true });
7956
8636