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
@@ -0,0 +1,214 @@
1
+ export class BlueNoiseSamples {
2
+
3
+ constructor( size ) {
4
+
5
+ this.count = 0;
6
+ this.size = - 1;
7
+ this.sigma = - 1;
8
+ this.radius = - 1;
9
+ this.lookupTable = null;
10
+ this.score = null;
11
+ this.binaryPattern = null;
12
+
13
+ this.resize( size );
14
+ this.setSigma( 1.5 );
15
+
16
+ }
17
+
18
+ findVoid() {
19
+
20
+ const { score, binaryPattern } = this;
21
+
22
+ let currValue = Infinity;
23
+ let currIndex = - 1;
24
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
25
+
26
+ if ( binaryPattern[ i ] !== 0 ) {
27
+
28
+ continue;
29
+
30
+ }
31
+
32
+ const pScore = score[ i ];
33
+ if ( pScore < currValue ) {
34
+
35
+ currValue = pScore;
36
+ currIndex = i;
37
+
38
+ }
39
+
40
+ }
41
+
42
+ return currIndex;
43
+
44
+ }
45
+
46
+ findCluster() {
47
+
48
+ const { score, binaryPattern } = this;
49
+
50
+ let currValue = - Infinity;
51
+ let currIndex = - 1;
52
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
53
+
54
+ if ( binaryPattern[ i ] !== 1 ) {
55
+
56
+ continue;
57
+
58
+ }
59
+
60
+ const pScore = score[ i ];
61
+ if ( pScore > currValue ) {
62
+
63
+ currValue = pScore;
64
+ currIndex = i;
65
+
66
+ }
67
+
68
+ }
69
+
70
+ return currIndex;
71
+
72
+ }
73
+
74
+ setSigma( sigma ) {
75
+
76
+ if ( sigma === this.sigma ) {
77
+
78
+ return;
79
+
80
+ }
81
+
82
+ // generate a radius in which the score will be updated under the
83
+ // assumption that e^-10 is insignificant enough to be the border at
84
+ // which we drop off.
85
+ const radius = ~ ~ ( Math.sqrt( 10 * 2 * ( sigma ** 2 ) ) + 1 );
86
+ const lookupWidth = 2 * radius + 1;
87
+ const lookupTable = new Float32Array( lookupWidth * lookupWidth );
88
+ const sigma2 = sigma * sigma;
89
+ for ( let x = - radius; x <= radius; x ++ ) {
90
+
91
+ for ( let y = - radius; y <= radius; y ++ ) {
92
+
93
+ const index = ( radius + y ) * lookupWidth + x + radius;
94
+ const dist2 = x * x + y * y;
95
+ lookupTable[ index ] = Math.E ** ( - dist2 / ( 2 * sigma2 ) );
96
+
97
+ }
98
+
99
+ }
100
+
101
+ this.lookupTable = lookupTable;
102
+ this.sigma = sigma;
103
+ this.radius = radius;
104
+
105
+ }
106
+
107
+ resize( size ) {
108
+
109
+ if ( this.size !== size ) {
110
+
111
+ this.size = size;
112
+ this.score = new Float32Array( size * size );
113
+ this.binaryPattern = new Uint8Array( size * size );
114
+
115
+ }
116
+
117
+
118
+ }
119
+
120
+ invert() {
121
+
122
+ const { binaryPattern, score, size } = this;
123
+
124
+ score.fill( 0 );
125
+
126
+ for ( let i = 0, l = binaryPattern.length; i < l; i ++ ) {
127
+
128
+ if ( binaryPattern[ i ] === 0 ) {
129
+
130
+ const y = ~ ~ ( i / size );
131
+ const x = i - y * size;
132
+ this.updateScore( x, y, 1 );
133
+ binaryPattern[ i ] = 1;
134
+
135
+ } else {
136
+
137
+ binaryPattern[ i ] = 0;
138
+
139
+ }
140
+
141
+ }
142
+
143
+ }
144
+
145
+ updateScore( x, y, multiplier ) {
146
+
147
+ // TODO: Is there a way to keep track of the highest and lowest scores here to avoid have to search over
148
+ // everything in the buffer?
149
+ const { size, score, lookupTable } = this;
150
+
151
+ // const sigma2 = sigma * sigma;
152
+ // const radius = Math.floor( size / 2 );
153
+ const radius = this.radius;
154
+ const lookupWidth = 2 * radius + 1;
155
+ for ( let px = - radius; px <= radius; px ++ ) {
156
+
157
+ for ( let py = - radius; py <= radius; py ++ ) {
158
+
159
+ // const dist2 = px * px + py * py;
160
+ // const value = Math.E ** ( - dist2 / ( 2 * sigma2 ) );
161
+
162
+ const lookupIndex = ( radius + py ) * lookupWidth + px + radius;
163
+ const value = lookupTable[ lookupIndex ];
164
+
165
+ let sx = ( x + px );
166
+ sx = sx < 0 ? size + sx : sx % size;
167
+
168
+ let sy = ( y + py );
169
+ sy = sy < 0 ? size + sy : sy % size;
170
+
171
+ const sindex = sy * size + sx;
172
+ score[ sindex ] += multiplier * value;
173
+
174
+ }
175
+
176
+ }
177
+
178
+ }
179
+
180
+ addPointIndex( index ) {
181
+
182
+ this.binaryPattern[ index ] = 1;
183
+
184
+ const size = this.size;
185
+ const y = ~ ~ ( index / size );
186
+ const x = index - y * size;
187
+ this.updateScore( x, y, 1 );
188
+ this.count ++;
189
+
190
+ }
191
+
192
+ removePointIndex( index ) {
193
+
194
+ this.binaryPattern[ index ] = 0;
195
+
196
+ const size = this.size;
197
+ const y = ~ ~ ( index / size );
198
+ const x = index - y * size;
199
+ this.updateScore( x, y, - 1 );
200
+ this.count --;
201
+
202
+ }
203
+
204
+ copy( source ) {
205
+
206
+ this.resize( source.size );
207
+ this.score.set( source.score );
208
+ this.binaryPattern.set( source.binaryPattern );
209
+ this.setSigma( source.sigma );
210
+ this.count = source.count;
211
+
212
+ }
213
+
214
+ }
@@ -0,0 +1,24 @@
1
+ export function shuffleArray( array, random = Math.random ) {
2
+
3
+ for ( let i = array.length - 1; i > 0; i -- ) {
4
+
5
+ const replaceIndex = ~ ~ ( ( random() - 1e-6 ) * i );
6
+ const tmp = array[ i ];
7
+ array[ i ] = array[ replaceIndex ];
8
+ array[ replaceIndex ] = tmp;
9
+
10
+ }
11
+
12
+ }
13
+
14
+ export function fillWithOnes( array, count ) {
15
+
16
+ array.fill( 0 );
17
+
18
+ for ( let i = 0; i < count; i ++ ) {
19
+
20
+ array[ i ] = 1;
21
+
22
+ }
23
+
24
+ }
@@ -1,4 +1,4 @@
1
- import { DataTexture, RedFormat, LinearFilter, DataUtils, HalfFloatType, Source, RepeatWrapping, RGBAFormat } from 'three';
1
+ import { DataTexture, RedFormat, LinearFilter, DataUtils, HalfFloatType, Source, RepeatWrapping, RGBAFormat, FloatType, ClampToEdgeWrapping } from 'three';
2
2
  import { toHalfFloatArray } from '../utils/TextureUtils.js';
3
3
 
4
4
  function binarySearchFindClosestIndexOf( array, targetValue, offset = 0, count = array.length ) {
@@ -39,7 +39,7 @@ function colorToLuminance( r, g, b ) {
39
39
  }
40
40
 
41
41
  // ensures the data is all floating point values and flipY is false
42
- function preprocessEnvMap( envMap ) {
42
+ function preprocessEnvMap( envMap, targetType = HalfFloatType ) {
43
43
 
44
44
  const map = envMap.clone();
45
45
  map.source = new Source( { ...map.image } );
@@ -48,17 +48,54 @@ function preprocessEnvMap( envMap ) {
48
48
  // TODO: is there a simple way to avoid cloning and adjusting the env map data here?
49
49
  // convert the data from half float uint 16 arrays to float arrays for cdf computation
50
50
  let newData = data;
51
- if ( map.type === HalfFloatType ) {
51
+ if ( map.type !== targetType ) {
52
52
 
53
- newData = new Uint16Array( data.length );
54
- for ( const i in data ) {
53
+ if ( targetType === HalfFloatType ) {
55
54
 
56
- newData[ i ] = data[ i ];
55
+ newData = new Uint16Array( data.length );
56
+
57
+ } else {
58
+
59
+ newData = new Float32Array( data.length );
60
+
61
+ }
62
+
63
+ let maxIntValue;
64
+ if ( data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array ) {
65
+
66
+ maxIntValue = 2 ** ( 8 * data.BYTES_PER_ELEMENT - 1 ) - 1;
67
+
68
+ } else {
69
+
70
+ maxIntValue = 2 ** ( 8 * data.BYTES_PER_ELEMENT ) - 1;
71
+
72
+ }
73
+
74
+ for ( let i = 0, l = data.length; i < l; i ++ ) {
75
+
76
+ let v = data[ i ];
77
+ if ( map.type === HalfFloatType ) {
78
+
79
+ v = DataUtils.fromHalfFloat( data[ i ] );
80
+
81
+ }
82
+
83
+ if ( map.type !== FloatType && map.type !== HalfFloatType ) {
84
+
85
+ v /= maxIntValue;
86
+
87
+ }
88
+
89
+ if ( targetType === HalfFloatType ) {
90
+
91
+ newData[ i ] = DataUtils.toHalfFloat( v );
92
+
93
+ }
57
94
 
58
95
  }
59
96
 
60
97
  map.image.data = newData;
61
- map.type = HalfFloatType;
98
+ map.type = targetType;
62
99
 
63
100
  }
64
101
 
@@ -150,7 +187,7 @@ export class EquirectHdrInfoUniform {
150
187
  // https://pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Sampling_Light_Sources#InfiniteAreaLights
151
188
  const map = preprocessEnvMap( hdr );
152
189
  map.wrapS = RepeatWrapping;
153
- map.wrapT = RepeatWrapping;
190
+ map.wrapT = ClampToEdgeWrapping;
154
191
 
155
192
  const { width, height, data } = map.image;
156
193
 
@@ -57,6 +57,13 @@ export class LightsInfoUniformStruct {
57
57
  const baseIndex = i * LIGHT_PIXELS * 4;
58
58
  let index = 0;
59
59
 
60
+ // initialize to 0
61
+ for ( let p = 0; p < LIGHT_PIXELS * 4; p ++ ) {
62
+
63
+ floatArray[ baseIndex + p ] = 0;
64
+
65
+ }
66
+
60
67
  // sample 1
61
68
  // position
62
69
  l.getWorldPosition( v );
@@ -121,7 +128,7 @@ export class LightsInfoUniformStruct {
121
128
 
122
129
  } else if ( l.isSpotLight ) {
123
130
 
124
- const radius = l.radius;
131
+ const radius = l.radius || 0;
125
132
  eye.setFromMatrixPosition( l.matrixWorld );
126
133
  target.setFromMatrixPosition( l.target.matrixWorld );
127
134
  m.lookAt( eye, target, up );
@@ -151,24 +158,21 @@ export class LightsInfoUniformStruct {
151
158
  // radius
152
159
  floatArray[ baseIndex + ( index ++ ) ] = radius;
153
160
 
154
- // near
155
- floatArray[ baseIndex + ( index ++ ) ] = l.shadow.camera.near;
156
-
157
161
  // decay
158
162
  floatArray[ baseIndex + ( index ++ ) ] = l.decay;
159
163
 
160
164
  // distance
161
165
  floatArray[ baseIndex + ( index ++ ) ] = l.distance;
162
166
 
163
- // sample 6
164
167
  // coneCos
165
168
  floatArray[ baseIndex + ( index ++ ) ] = Math.cos( l.angle );
166
169
 
170
+ // sample 6
167
171
  // penumbraCos
168
172
  floatArray[ baseIndex + ( index ++ ) ] = Math.cos( l.angle * ( 1 - l.penumbra ) );
169
173
 
170
174
  // iesProfile
171
- floatArray[ baseIndex + ( index ++ ) ] = iesTextures.indexOf( l.iesTexture );
175
+ floatArray[ baseIndex + ( index ++ ) ] = l.iesTexture ? iesTextures.indexOf( l.iesTexture ) : - 1;
172
176
 
173
177
  } else if ( l.isPointLight ) {
174
178
 
@@ -185,7 +189,7 @@ export class LightsInfoUniformStruct {
185
189
  index += 4;
186
190
 
187
191
  // sample 5
188
- index += 2;
192
+ index += 1;
189
193
 
190
194
  floatArray[ baseIndex + ( index ++ ) ] = l.decay;
191
195
  floatArray[ baseIndex + ( index ++ ) ] = l.distance;
@@ -186,7 +186,7 @@ export class MaterialsTexture extends DataTexture {
186
186
 
187
187
  let index = 0;
188
188
  const pixelCount = materials.length * MATERIAL_PIXELS;
189
- const dimension = Math.ceil( Math.sqrt( pixelCount ) );
189
+ const dimension = Math.ceil( Math.sqrt( pixelCount ) ) || 1;
190
190
  const { threeCompatibilityTransforms, image, features } = this;
191
191
 
192
192
  // get the list of textures with unique sources
@@ -2,11 +2,11 @@ import {
2
2
  WebGLArrayRenderTarget,
3
3
  RGBAFormat,
4
4
  UnsignedByteType,
5
- MeshBasicMaterial,
6
5
  Color,
7
6
  RepeatWrapping,
8
7
  LinearFilter,
9
8
  NoToneMapping,
9
+ ShaderMaterial,
10
10
  } from 'three';
11
11
  import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
12
12
  import { reduceTexturesToUniqueSources } from './utils.js';
@@ -31,7 +31,7 @@ export class RenderTarget2DArray extends WebGLArrayRenderTarget {
31
31
 
32
32
  };
33
33
 
34
- const fsQuad = new FullScreenQuad( new MeshBasicMaterial() );
34
+ const fsQuad = new FullScreenQuad( new CopyMaterial() );
35
35
  this.fsQuad = fsQuad;
36
36
 
37
37
  }
@@ -66,7 +66,6 @@ export class RenderTarget2DArray extends WebGLArrayRenderTarget {
66
66
  texture.matrix.identity();
67
67
 
68
68
  fsQuad.material.map = texture;
69
- fsQuad.material.transparent = true;
70
69
 
71
70
  renderer.setRenderTarget( this, i );
72
71
  fsQuad.render( renderer );
@@ -95,3 +94,51 @@ export class RenderTarget2DArray extends WebGLArrayRenderTarget {
95
94
  }
96
95
 
97
96
  }
97
+
98
+ class CopyMaterial extends ShaderMaterial {
99
+
100
+ get map() {
101
+
102
+ return this.uniforms.map.value;
103
+
104
+ }
105
+ set map( v ) {
106
+
107
+ this.uniforms.map.value = v;
108
+
109
+ }
110
+
111
+ constructor() {
112
+
113
+ super( {
114
+ uniforms: {
115
+
116
+ map: { value: null },
117
+
118
+ },
119
+
120
+ vertexShader: /* glsl */`
121
+ varying vec2 vUv;
122
+ void main() {
123
+
124
+ vUv = uv;
125
+ gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
126
+
127
+ }
128
+ `,
129
+
130
+ fragmentShader: /* glsl */`
131
+ uniform sampler2D map;
132
+ varying vec2 vUv;
133
+ void main() {
134
+
135
+ gl_FragColor = texture2D( map, vUv );
136
+
137
+ }
138
+ `
139
+ } );
140
+
141
+ }
142
+
143
+
144
+ }
@@ -0,0 +1,49 @@
1
+ import { DataTexture, FloatType, NearestFilter, RGBAFormat } from 'three';
2
+ import { StratifiedSamplerCombined } from './stratified/StratifiedSamplerCombined.js';
3
+
4
+ export class StratifiedSamplesTexture extends DataTexture {
5
+
6
+ constructor( count = 1, depth = 1, strata = 8 ) {
7
+
8
+ super( new Float32Array( 1 ), 1, 1, RGBAFormat, FloatType );
9
+ this.minFilter = NearestFilter;
10
+ this.magFilter = NearestFilter;
11
+
12
+ this.strata = strata;
13
+ this.sampler = null;
14
+
15
+ this.init( count, depth, strata );
16
+
17
+ }
18
+
19
+ init( count, depth, strata = this.strata ) {
20
+
21
+ const { image } = this;
22
+ if ( image.width === depth && image.height === count ) {
23
+
24
+ return;
25
+
26
+ }
27
+
28
+ const dimensions = new Array( count * depth ).fill( 4 );
29
+ const sampler = new StratifiedSamplerCombined( strata, dimensions );
30
+
31
+ image.width = depth;
32
+ image.height = count;
33
+ image.data = sampler.samples;
34
+
35
+ this.sampler = sampler;
36
+
37
+ this.dispose();
38
+ this.next();
39
+
40
+ }
41
+
42
+ next() {
43
+
44
+ this.sampler.next();
45
+ this.needsUpdate = true;
46
+
47
+ }
48
+
49
+ }
@@ -0,0 +1,73 @@
1
+ // Stratified Sampling based on implementation from hoverinc pathtracer
2
+ // - https://github.com/hoverinc/ray-tracing-renderer
3
+ // - http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html
4
+
5
+ export function shuffle( arr ) {
6
+
7
+ for ( let i = arr.length - 1; i > 0; i -- ) {
8
+
9
+ const j = Math.floor( Math.random() * ( i + 1 ) );
10
+ const x = arr[ i ];
11
+ arr[ i ] = arr[ j ];
12
+ arr[ j ] = x;
13
+
14
+ }
15
+
16
+ return arr;
17
+
18
+ }
19
+
20
+ // strataCount : The number of bins per dimension
21
+ // dimensions : The number of dimensions to generate stratified values for
22
+ export class StratifiedSampler {
23
+
24
+ constructor( strataCount, dimensions ) {
25
+
26
+ const l = strataCount ** dimensions;
27
+ const strata = new Uint16Array( l );
28
+ let index = l;
29
+
30
+ // each integer represents a statum bin
31
+ for ( let i = 0; i < l; i ++ ) {
32
+
33
+ strata[ i ] = i;
34
+
35
+ }
36
+
37
+ this.samples = new Float32Array( dimensions );
38
+
39
+ this.strataCount = strataCount;
40
+
41
+ this.restart = function () {
42
+
43
+ index = 0;
44
+
45
+ };
46
+
47
+ this.next = function () {
48
+
49
+ const { samples } = this;
50
+
51
+ if ( index >= strata.length ) {
52
+
53
+ shuffle( strata );
54
+ this.restart();
55
+
56
+ }
57
+
58
+ let stratum = strata[ index ++ ];
59
+
60
+ for ( let i = 0; i < dimensions; i ++ ) {
61
+
62
+ samples[ i ] = ( stratum % strataCount + Math.random() ) / strataCount;
63
+ stratum = Math.floor( stratum / strataCount );
64
+
65
+ }
66
+
67
+ return samples;
68
+
69
+ };
70
+
71
+ }
72
+
73
+ }
@@ -0,0 +1,59 @@
1
+ // Stratified Sampling based on implementation from hoverinc pathtracer
2
+ // - https://github.com/hoverinc/ray-tracing-renderer
3
+ // - http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html
4
+
5
+ import { StratifiedSampler } from './StratifiedSampler.js';
6
+
7
+ // Stratified set of data with each tuple stratified separately and combined
8
+ export class StratifiedSamplerCombined {
9
+
10
+ constructor( strataCount, listOfDimensions ) {
11
+
12
+ let totalDim = 0;
13
+ for ( const dim of listOfDimensions ) {
14
+
15
+ totalDim += dim;
16
+
17
+ }
18
+
19
+ const combined = new Float32Array( totalDim );
20
+ const strataObjs = [];
21
+ let offset = 0;
22
+ for ( const dim of listOfDimensions ) {
23
+
24
+ const sampler = new StratifiedSampler( strataCount, dim );
25
+ sampler.samples = new Float32Array( combined.buffer, offset, sampler.samples.length );
26
+ offset += sampler.samples.length * 4;
27
+ strataObjs.push( sampler );
28
+
29
+ }
30
+
31
+ this.samples = combined;
32
+
33
+ this.strataCount = strataCount;
34
+
35
+ this.next = function () {
36
+
37
+ for ( const strata of strataObjs ) {
38
+
39
+ strata.next();
40
+
41
+ }
42
+
43
+ return combined;
44
+
45
+ };
46
+
47
+ this.restart = function () {
48
+
49
+ for ( const strata of strataObjs ) {
50
+
51
+ strata.restart();
52
+
53
+ }
54
+
55
+ };
56
+
57
+ }
58
+
59
+ }
@@ -2,7 +2,7 @@
2
2
  // when rendering each texture to the texture array they must have a consistent color space.
3
3
  export function getTextureHash( t ) {
4
4
 
5
- return `${ t.source.uuid }:${ t.encoding }`;
5
+ return `${ t.source.uuid }:${ t.colorSpace }`;
6
6
 
7
7
  }
8
8