rayzee 11.0.0 → 11.1.0

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/dist/{ArchiveImporter-BRQvLggb.js → ArchiveImporter-Hn35o6Z5.js} +1525 -680
  2. package/dist/BidirectionalIntegrator-CBw8hkeu.js +946 -0
  3. package/dist/{NeuralRetouch-BCXylThH.js → NeuralRetouch-BzNpiVHZ.js} +1 -1
  4. package/dist/{NeuralSuperRes-sZAeS142.js → NeuralSuperRes-Bk7bHg2B.js} +1 -1
  5. package/dist/{PathTracer-CQPW06Mm.js → PathTracer-72XufKAr.js} +79 -74
  6. package/dist/{PhysicalSky-BvqCw_dy.js → PhysicalSky-BgjdV88k.js} +1 -1
  7. package/dist/{RayzeeRenderer-COpwotpU.js → RayzeeRenderer-1QRO97eq.js} +3 -3
  8. package/dist/addons/archives.es.js +1 -1
  9. package/dist/addons/bidirectional.es.js +1 -1
  10. package/dist/addons/physical-sky.es.js +1 -1
  11. package/dist/{archiveFormats-BZw7TiDp.js → archiveFormats-tnAoUL1j.js} +28 -9
  12. package/dist/rayzee-core.es.js +4 -4
  13. package/dist/rayzee.es.js +11 -11
  14. package/dist/rayzee.umd.js +43 -43
  15. package/dist/{sceneCachePolicy-BSSsKY98.js → sceneCachePolicy-C67jbci-.js} +1 -1
  16. package/package.json +1 -1
  17. package/src/Processor/ArchiveCache.js +19 -6
  18. package/src/Processor/ArchiveImporter.js +9 -3
  19. package/src/Processor/ArchiveReader.js +22 -4
  20. package/src/Processor/PBRT/PBRTMaterials.js +132 -8
  21. package/src/Processor/PBRT/PBRTParser.js +20 -3
  22. package/src/Processor/PBRT/PBRTScattering.js +93 -0
  23. package/src/Processor/PBRT/PBRTSceneBuilder.js +513 -49
  24. package/src/Processor/PBRT/PBRTTextureBake.js +32 -7
  25. package/src/Processor/SceneMetadata.js +30 -5
  26. package/src/RayzeeRenderer.js +3 -2
  27. package/src/Stages/PathTracer.js +3 -5
  28. package/src/TSL/Bidirectional.js +6 -2
  29. package/src/TSL/ConnectKernel.js +31 -11
  30. package/src/TSL/LightSplatKernel.js +22 -7
  31. package/src/TSL/MergeKernel.js +13 -7
  32. package/src/TSL/ShadeKernel.js +51 -30
  33. package/src/integrators/BidirectionalIntegrator.js +6 -1
  34. package/dist/BidirectionalIntegrator-9lZQxFVH.js +0 -934
@@ -435,7 +435,7 @@ var Y = class {
435
435
  samples: this.samples.slice()
436
436
  };
437
437
  }
438
- }, X = "11.0.0", Z = 1e4, Q = 15e5;
438
+ }, X = "11.1.0", Z = 1e4, Q = 15e5;
439
439
  function $(e, t, n = Z) {
440
440
  return e >= n && t / Q < e / 3;
441
441
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "11.0.0",
3
+ "version": "11.1.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -16,30 +16,43 @@ export async function archiveKey( kind, file ) {
16
16
  /**
17
17
  * Decompresses a .tar.gz into storage once, indexing it on the way, and hands back the unpacked
18
18
  * .tar as a seekable File. A second call for the same file skips the work.
19
+ *
20
+ * With `filter` (and `part`, naming what it keeps) only the entries it keeps are written, end to end, and only they
21
+ * get offsets in the listing: a 31 GB scene archive need not fit to load the part of it chosen. An unpack of the whole
22
+ * archive serves any part.
19
23
  * @returns {Promise<?{file: File, index: {v:number, listing:Array}, release: function(): void, cached: boolean}>}
20
24
  * null when storage cannot take it — the caller reads the archive the old way.
21
25
  */
22
- export async function unpackTarGz( file, { storage, label = file.name, onProgress = null } ) {
26
+ export async function unpackTarGz( file, { storage, label = file.name, onProgress = null, filter = null, part = null } ) {
23
27
 
24
28
  if ( ! storage || typeof DecompressionStream === 'undefined' ) return null;
25
29
 
26
30
  const area = storage.area( ENGINE_AREAS.ARCHIVES );
27
- const key = await archiveKey( 'gunzip', file );
31
+ const whole = await archiveKey( 'gunzip', file );
32
+ let key = whole;
28
33
 
29
- let entry = await area.open( key );
34
+ let entry = await area.open( whole );
35
+ if ( ! entry && filter ) entry = await area.open( key = `${whole}|${part}` );
30
36
  const cached = !! entry;
31
37
 
32
38
  if ( ! entry ) {
33
39
 
34
- const writer = await area.create( key, { label, expectedBytes: file.size * GUESSED_EXPANSION } );
40
+ // A part's size is known only once its headers are read: a write past the quota falls back below.
41
+ const writer = await area.create( key, filter ? { label: `${label} (${part})` } : { label, expectedBytes: file.size * GUESSED_EXPANSION } );
35
42
  if ( ! writer ) return null;
36
43
 
37
- const indexer = createTarIndexer();
44
+ let out = null;
45
+ const indexer = createTarIndexer( filter ? { filter, sink: ( bytes ) => out.enqueue( bytes ) } : {} );
38
46
  const tap = new TransformStream( {
47
+ start( controller ) {
48
+
49
+ out = controller;
50
+
51
+ },
39
52
  transform( chunk, controller ) {
40
53
 
41
54
  indexer.push( chunk );
42
- controller.enqueue( chunk );
55
+ if ( ! filter ) controller.enqueue( chunk );
43
56
 
44
57
  },
45
58
  } );
@@ -129,13 +129,15 @@ export class ArchiveImporter {
129
129
  * A .tar.gz unpacked to storage, or null to read it the old way (no storage, or no room).
130
130
  * @private
131
131
  */
132
- async _unpackGzip( file, filename ) {
132
+ async _unpackGzip( file, filename, element = null ) {
133
133
 
134
+ const chosen = ( Array.isArray( element ) ? element : [ element ] ).filter( Boolean );
134
135
  try {
135
136
 
136
137
  return await unpackTarGz( file, {
137
138
  storage: this.loader.storage,
138
139
  label: filename,
140
+ ...( chosen.length ? { filter: elementFilter( chosen ), part: [ ...chosen ].sort().join( ',' ) } : {} ),
139
141
  onProgress: bytes => updateLoading( {
140
142
  isLoading: true, status: `Unpacking archive… ${( bytes / 1e9 ).toFixed( 1 )} GB`, progress: 4
141
143
  } ),
@@ -189,7 +191,7 @@ export class ArchiveImporter {
189
191
 
190
192
  if ( kind === 'gzip' ) {
191
193
 
192
- const unpacked = await this._unpackGzip( file, filename );
194
+ const unpacked = await this._unpackGzip( file, filename, element );
193
195
  if ( unpacked ) {
194
196
 
195
197
  try {
@@ -337,6 +339,7 @@ export class ArchiveImporter {
337
339
  meshCount: built.meshCount, entryPath: built.entryPath, candidates: built.candidates, frames: built.frames,
338
340
  warnings: built.warnings, triangleCount: built.triangleCount, placementCount: built.placementCount,
339
341
  mergedShapes: built.mergedShapes, skippedForBudget: built.skippedForBudget, droppedNoTemplate: built.droppedNoTemplate,
342
+ render: built.render ?? null,
340
343
  },
341
344
  } );
342
345
 
@@ -680,7 +683,10 @@ export class ArchiveImporter {
680
683
  // The light's own orientation and `scale` are already baked into the texture, so the
681
684
  // scene is only correct at rotation 0 / intensity 1 — pinned, whatever the host's defaults.
682
685
  // Without an infinite light pbrt has no environment at all.
683
- this.loader.sceneMetadata = { environment: environment?.texture ? { rotation: 0, intensity: 1 } : { enabled: false } };
686
+ this.loader.sceneMetadata = {
687
+ environment: environment?.texture ? { rotation: 0, intensity: 1 } : { enabled: false },
688
+ ...( built.render ? { render: { ...built.render } } : {} ),
689
+ };
684
690
 
685
691
  updateLoading( { isLoading: true, status: 'Processing PBRT geometry...', progress: 10 } );
686
692
  await this.loader.onModelLoad( this.loader.targetModel );
@@ -135,9 +135,12 @@ function isZeroBlock( h ) {
135
135
  */
136
136
  class TarStream {
137
137
 
138
- constructor( { filter = null, retain = null, byteBudget = DEFAULT_BYTE_BUDGET, onEntry = null } = {} ) {
138
+ constructor( { filter = null, retain = null, byteBudget = DEFAULT_BYTE_BUDGET, onEntry = null, sink = null } = {} ) {
139
139
 
140
140
  this.filter = filter;
141
+ // Passed each indexed entry's bytes in order: the listing then records offsets into what the sink wrote.
142
+ this.sink = sink;
143
+ this._sunk = 0;
141
144
  // Entries that pass `filter` but fail `retain` are indexed, not copied: the listing
142
145
  // records where their bytes live so a caller with a seekable source can read them later.
143
146
  // An uncompressed tar over a File is seekable, which is how a 7 GB archive loads without
@@ -190,6 +193,14 @@ class TarStream {
190
193
  } else {
191
194
 
192
195
  const n = Math.min( this._remaining, chunk.length - i );
196
+ if ( this.sink && this._indexed && this._sinkLeft > 0 ) {
197
+
198
+ const take = Math.min( n, this._sinkLeft );
199
+ this.sink( chunk.subarray( i, i + take ) );
200
+ this._sinkLeft -= take;
201
+
202
+ }
203
+
193
204
  if ( this._dst && this._copied < this._size ) {
194
205
 
195
206
  const take = Math.min( n, this._size - this._copied );
@@ -267,6 +278,13 @@ class TarStream {
267
278
 
268
279
  this.listing.push( { path, size } );
269
280
  this._indexed = this.filter ? this.filter( path, size ) : true;
281
+ if ( this.sink && this._indexed ) {
282
+
283
+ this._sinkStart = this._sunk;
284
+ this._sinkLeft = size;
285
+ this._sunk += size;
286
+
287
+ }
270
288
 
271
289
  if ( this._indexed && ! ( this.retain && ! this.retain( path, size ) ) ) {
272
290
 
@@ -313,7 +331,7 @@ class TarStream {
313
331
 
314
332
  if ( this._indexed && entry ) {
315
333
 
316
- entry.offset = this._bodyStart;
334
+ entry.offset = this.sink ? this._sinkStart : this._bodyStart;
317
335
  this._indexed = false;
318
336
 
319
337
  }
@@ -503,9 +521,9 @@ export async function indexTarHeaders( source, { onProgress = null } = {} ) {
503
521
  * Indexes a tar stream as it goes by — for writing a decompressing archive to disk and knowing
504
522
  * where every entry landed without a second pass.
505
523
  */
506
- export function createTarIndexer() {
524
+ export function createTarIndexer( { filter = null, sink = null } = {} ) {
507
525
 
508
- const tar = new TarStream( { retain: () => false } );
526
+ const tar = new TarStream( { retain: () => false, filter, sink } );
509
527
  return {
510
528
  push: chunk => tar.push( chunk ),
511
529
  finish: () => {
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { MeshPhysicalMaterial, Color, DoubleSide } from 'three';
12
+ import { namedScattering, dipoleReflectance, albedoForReflectance } from './PBRTScattering.js';
12
13
 
13
14
  // Normal-incidence reflectance approximations for pbrt's named conductor spectra.
14
15
  const METAL_ALBEDO = {
@@ -168,10 +169,17 @@ async function floatOrMean( params, name, ctx ) {
168
169
  * α = √roughness (`remaproughness`, the default) or α = roughness; the engine's GGX takes α = roughness². Passing
169
170
  * pbrt's number through made `remaproughness false` 0.1 an α of 0.01, a near mirror. `dflt` is the engine's.
170
171
  */
171
- async function resolveRoughness( params, dflt, ctx, prefix = '' ) {
172
+ const remapsRoughness = ( params ) => {
172
173
 
173
174
  const remap = params.remaproughness?.value?.[ 0 ];
174
- const alpha = ( r ) => ( remap === false || remap === 'false' ? r : Math.sqrt( Math.max( r, 0 ) ) );
175
+ return remap !== false && remap !== 'false';
176
+
177
+ };
178
+
179
+ async function resolveRoughness( params, dflt, ctx, prefix = '' ) {
180
+
181
+ const remap = remapsRoughness( params );
182
+ const alpha = ( r ) => ( remap ? Math.sqrt( Math.max( r, 0 ) ) : r );
175
183
  let a = null;
176
184
  const r = await floatOrMean( params, `${prefix}roughness`, ctx );
177
185
  if ( r !== null ) a = alpha( r );
@@ -187,6 +195,116 @@ async function resolveRoughness( params, dflt, ctx, prefix = '' ) {
187
195
 
188
196
  }
189
197
 
198
+ /**
199
+ * Sets `target` (`roughness` or `clearcoatRoughness`) as resolveRoughness does, except that a textured roughness becomes
200
+ * a roughness map with the remap baked in (`ctx.bakeFloatMap`), the value 1 multiplying it.
201
+ */
202
+ async function applyRoughness( mat, target, params, dflt, ctx, prefix = '' ) {
203
+
204
+ const single = params[ `${prefix}roughness` ];
205
+ const keys = single ? [ `${prefix}roughness` ] : [ `${prefix}uroughness`, `${prefix}vroughness` ].filter( ( key ) => params[ key ] );
206
+ if ( ctx.bakeFloatMap && keys.some( ( key ) => params[ key ].type === 'texture' ) ) {
207
+
208
+ const remap = remapsRoughness( params );
209
+ const alpha = ( r ) => ( remap ? Math.sqrt( Math.max( r, 0 ) ) : Math.max( r, 0 ) );
210
+ const map = await ctx.bakeFloatMap( params, keys, ( values ) => Math.sqrt( values.reduce( ( sum, r ) => sum + alpha( r ), 0 ) / values.length ) );
211
+ if ( map ) {
212
+
213
+ mat[ `${target}Map` ] = map;
214
+ mat[ target ] = 1;
215
+ return;
216
+
217
+ }
218
+
219
+ }
220
+
221
+ mat[ target ] = await resolveRoughness( params, dflt, ctx, prefix );
222
+
223
+ }
224
+
225
+ // A spectrum parameter as one RGB; a texture counts as its mean (`ctx.textureMeanRGB`).
226
+ async function spectrumMean( params, key, ctx, dflt ) {
227
+
228
+ const p = params[ key ];
229
+ if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB?.( p.value[ 0 ] ) ) ?? dflt;
230
+ return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
231
+
232
+ }
233
+
234
+ // pbrt's default subsurface medium (materials.cpp), mm⁻¹.
235
+ const SUBSURFACE_SIGMA_A = [ 0.0011, 0.0024, 0.014 ];
236
+ const SUBSURFACE_SIGMA_S = [ 2.55, 3.21, 3.77 ];
237
+
238
+ /**
239
+ * pbrt's `subsurface` as the engine's random walk under a dielectric boundary of `eta`: σt = σa + σs (times `scale`),
240
+ * single-scattering albedo σs / σt, mean free path 1 / σt. The coefficients come as pbrt takes them — a named medium
241
+ * (g 0), `sigma_a` and `sigma_s`, or a `reflectance` and `mfp` (scaled) inverted through the dipole — and default to
242
+ * pbrt's. The base colour is the reflectance the dipole gives, for what reads it as an albedo.
243
+ */
244
+ async function applySubsurface( mat, params, ctx ) {
245
+
246
+ const eta = pFloat( params, 'eta', 1.33 );
247
+ const scale = pFloat( params, 'scale', 1 );
248
+ const scaled = ( rgb ) => rgb.map( ( v ) => Math.max( 0, v * scale ) );
249
+ let g = pFloat( params, 'g', 0 );
250
+ let sigmaA, sigmaS;
251
+
252
+ const name = pString( params, 'name', '' );
253
+ const named = name ? namedScattering( name ) : null;
254
+ if ( name && ! named ) ctx.warn( `subsurface: no medium named "${name}" — using pbrt's default` );
255
+
256
+ if ( named ) {
257
+
258
+ sigmaA = scaled( named.sigmaA );
259
+ sigmaS = scaled( named.sigmaS );
260
+ g = 0;
261
+
262
+ } else if ( params.sigma_a && params.sigma_s ) {
263
+
264
+ sigmaA = scaled( await spectrumMean( params, 'sigma_a', ctx, SUBSURFACE_SIGMA_A ) );
265
+ sigmaS = scaled( await spectrumMean( params, 'sigma_s', ctx, SUBSURFACE_SIGMA_S ) );
266
+
267
+ } else if ( params.reflectance && ! name ) {
268
+
269
+ const reflectance = await spectrumMean( params, 'reflectance', ctx, [ 0.5, 0.5, 0.5 ] );
270
+ const mfp = scaled( params.mfp ? await spectrumMean( params, 'mfp', ctx, [ 1, 1, 1 ] ) : [ 1, 1, 1 ] );
271
+ sigmaA = [];
272
+ sigmaS = [];
273
+ for ( let c = 0; c < 3; c ++ ) {
274
+
275
+ const sigmaT = 1 / Math.max( mfp[ c ], 1e-9 );
276
+ // The reduced albedo σ′s / ( σa + σ′s ), σ′s = ( 1 − g ) σs, back to σs at this σt.
277
+ const reduced = albedoForReflectance( reflectance[ c ], eta );
278
+ sigmaS[ c ] = reduced * sigmaT / ( 1 - g + reduced * g );
279
+ sigmaA[ c ] = sigmaT - sigmaS[ c ];
280
+
281
+ }
282
+
283
+ } else {
284
+
285
+ sigmaA = scaled( SUBSURFACE_SIGMA_A );
286
+ sigmaS = scaled( SUBSURFACE_SIGMA_S );
287
+
288
+ }
289
+
290
+ const sigmaT = sigmaA.map( ( a, c ) => a + sigmaS[ c ] );
291
+ mat.subsurface = 1;
292
+ mat.subsurfaceColor = new Color( ...sigmaT.map( ( t, c ) => ( t > 0 ? sigmaS[ c ] / t : 0 ) ) );
293
+ mat.subsurfaceRadius = sigmaT.map( ( t ) => ( t > 0 ? 1 / t : 1e6 ) );
294
+ mat.subsurfaceRadiusScale = 1;
295
+ mat.subsurfaceAnisotropy = Math.max( - 0.99, Math.min( 0.99, g ) );
296
+ mat.ior = eta;
297
+ mat.metalness = 0;
298
+ mat.color.setRGB( ...sigmaT.map( ( t, c ) => {
299
+
300
+ const reducedS = ( 1 - g ) * sigmaS[ c ];
301
+ return t > 0 ? dipoleReflectance( reducedS / ( sigmaA[ c ] + reducedS ), eta ) : 0;
302
+
303
+ } ) );
304
+ await applyRoughness( mat, 'roughness', params, 0, ctx );
305
+
306
+ }
307
+
190
308
  // A conductor's colour: `reflectance`, else normal-incidence Fresnel of `eta`/`k` (under `prefix` for a coated
191
309
  // conductor). pbrt's default is copper.
192
310
  async function applyConductorColor( mat, params, ctx, prefix, applyAlbedo, setColor ) {
@@ -272,7 +390,7 @@ export async function buildMaterial( def, ctx ) {
272
390
  case 'metal': {
273
391
 
274
392
  await applyConductorColor( mat, params, ctx, '', applyAlbedo, setColor );
275
- mat.roughness = await resolveRoughness( params, 0.1, ctx );
393
+ await applyRoughness( mat, 'roughness', params, 0.1, ctx );
276
394
  break;
277
395
 
278
396
  }
@@ -281,9 +399,9 @@ export async function buildMaterial( def, ctx ) {
281
399
 
282
400
  // A conductor under a dielectric coat; pbrt's roughnesses default to 0 for both.
283
401
  await applyConductorColor( mat, params, ctx, 'conductor.', applyAlbedo, setColor );
284
- mat.roughness = await resolveRoughness( params, 0, ctx, 'conductor.' );
402
+ await applyRoughness( mat, 'roughness', params, 0, ctx, 'conductor.' );
285
403
  mat.clearcoat = 1;
286
- mat.clearcoatRoughness = await resolveRoughness( params, 0, ctx, 'interface.' );
404
+ await applyRoughness( mat, 'clearcoatRoughness', params, 0, ctx, 'interface.' );
287
405
  break;
288
406
 
289
407
  }
@@ -295,7 +413,7 @@ export async function buildMaterial( def, ctx ) {
295
413
  mat.metalness = 0;
296
414
  mat.color.setRGB( 1, 1, 1 );
297
415
  mat.ior = pFloat( params, 'eta', 1.5 );
298
- mat.roughness = await resolveRoughness( params, 0, ctx );
416
+ await applyRoughness( mat, 'roughness', params, 0, ctx );
299
417
  mat.thickness = type === 'thindielectric' ? 0 : pFloat( params, 'thickness', 0 );
300
418
  break;
301
419
 
@@ -307,7 +425,7 @@ export async function buildMaterial( def, ctx ) {
307
425
  mat.roughness = 0.6;
308
426
  mat.metalness = 0;
309
427
  mat.clearcoat = 1;
310
- mat.clearcoatRoughness = await resolveRoughness( params, 0, ctx );
428
+ await applyRoughness( mat, 'clearcoatRoughness', params, 0, ctx );
311
429
  break;
312
430
 
313
431
  }
@@ -407,7 +525,13 @@ export async function buildMaterial( def, ctx ) {
407
525
 
408
526
  }
409
527
 
410
- case 'subsurface':
528
+ case 'subsurface': {
529
+
530
+ await applySubsurface( mat, params, ctx );
531
+ break;
532
+
533
+ }
534
+
411
535
  case 'hair':
412
536
  case 'measured': {
413
537
 
@@ -603,15 +603,33 @@ export class PBRTParser {
603
603
  this.ir.film = {
604
604
  xresolution: this._num( params.xresolution, 1280 ),
605
605
  yresolution: this._num( params.yresolution, 720 ),
606
+ resolutionGiven: Boolean( params.xresolution || params.yresolution ),
606
607
  filename: this._str( params.filename, null )
607
608
  };
608
609
  break;
609
610
 
610
611
  }
611
612
 
613
+ // What the scene asks the renderer for; reported, not applied (PBRTSceneBuilder renderRequest).
614
+ case 'Integrator': {
615
+
616
+ this._expectString( 'Integrator type' );
617
+ const params = this._parseParams();
618
+ if ( params.maxdepth ) this.ir.integrator = { maxdepth: this._num( params.maxdepth, 5 ) };
619
+ break;
620
+
621
+ }
622
+
623
+ case 'Sampler': {
624
+
625
+ this._expectString( 'Sampler type' );
626
+ const params = this._parseParams();
627
+ if ( params.pixelsamples ) this.ir.sampler = { pixelsamples: this._num( params.pixelsamples, 16 ) };
628
+ break;
629
+
630
+ }
631
+
612
632
  // Consumed for completeness; not used by the engine.
613
- case 'Integrator':
614
- case 'Sampler':
615
633
  case 'PixelFilter':
616
634
  case 'Filter':
617
635
  case 'Accelerator':
@@ -759,7 +777,6 @@ export class PBRTParser {
759
777
  // pbrt: a shape of alpha 0 is never hit, and an area light on it emits nothing.
760
778
  const alpha = params.alpha;
761
779
  if ( alpha?.type === 'float' && alpha.value[ 0 ] <= 0 ) break;
762
- if ( alpha && ! ( alpha.type === 'float' && alpha.value[ 0 ] >= 1 ) ) this._warnOnce( 'alpha', 'shape "alpha" between 0 and 1, or as a texture, is not supported — drawn opaque' );
763
780
  const shape = {
764
781
  type,
765
782
  params,
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Scattering coefficients for pbrt's `subsurface` material: its named media and the inversion from a diffuse
3
+ * reflectance to the albedo that produces it.
4
+ */
5
+
6
+ // pbrt-v4's table (media.cpp): [ σ′s, σa ] in mm⁻¹, from Jensen et al. 2001 and Narasimhan et al. 2006.
7
+ const MEASURED = {
8
+ 'Apple': [[ 2.29, 2.39, 1.97 ], [ 0.0030, 0.0034, 0.046 ]],
9
+ 'Chicken1': [[ 0.15, 0.21, 0.38 ], [ 0.015, 0.077, 0.19 ]],
10
+ 'Chicken2': [[ 0.19, 0.25, 0.32 ], [ 0.018, 0.088, 0.20 ]],
11
+ 'Cream': [[ 7.38, 5.47, 3.15 ], [ 0.0002, 0.0028, 0.0163 ]],
12
+ 'Ketchup': [[ 0.18, 0.07, 0.03 ], [ 0.061, 0.97, 1.45 ]],
13
+ 'Marble': [[ 2.19, 2.62, 3.00 ], [ 0.0021, 0.0041, 0.0071 ]],
14
+ 'Potato': [[ 0.68, 0.70, 0.55 ], [ 0.0024, 0.0090, 0.12 ]],
15
+ 'Skimmilk': [[ 0.70, 1.22, 1.90 ], [ 0.0014, 0.0025, 0.0142 ]],
16
+ 'Skin1': [[ 0.74, 0.88, 1.01 ], [ 0.032, 0.17, 0.48 ]],
17
+ 'Skin2': [[ 1.09, 1.59, 1.79 ], [ 0.013, 0.070, 0.145 ]],
18
+ 'Spectralon': [[ 11.6, 20.4, 14.9 ], [ 0.00, 0.00, 0.00 ]],
19
+ 'Wholemilk': [[ 2.55, 3.21, 3.77 ], [ 0.0011, 0.0024, 0.014 ]],
20
+ 'Lowfat Milk': [[ 0.89187, 1.5136, 2.532 ], [ 0.002875, 0.00575, 0.0115 ]],
21
+ 'Reduced Milk': [[ 2.4858, 3.1669, 4.5214 ], [ 0.0025556, 0.0051111, 0.012778 ]],
22
+ 'Regular Milk': [[ 4.5513, 5.8294, 7.136 ], [ 0.0015333, 0.0046, 0.019933 ]],
23
+ 'Espresso': [[ 0.72378, 0.84557, 1.0247 ], [ 4.7984, 6.5751, 8.8493 ]],
24
+ 'Mint Mocha Coffee': [[ 0.31602, 0.38538, 0.48131 ], [ 3.772, 5.8228, 7.82 ]],
25
+ 'Lowfat Soy Milk': [[ 0.30576, 0.34233, 0.61664 ], [ 0.0014375, 0.0071875, 0.035937 ]],
26
+ 'Regular Soy Milk': [[ 0.59223, 0.73866, 1.4693 ], [ 0.0019167, 0.0095833, 0.065167 ]],
27
+ 'Lowfat Chocolate Milk': [[ 0.64925, 0.83916, 1.1057 ], [ 0.0115, 0.0368, 0.1564 ]],
28
+ 'Regular Chocolate Milk': [[ 1.4585, 2.1289, 2.9527 ], [ 0.010063, 0.043125, 0.14375 ]],
29
+ 'Coke': [[ 8.9053e-05, 8.372e-05, 0 ], [ 0.10014, 0.16503, 0.2468 ]],
30
+ 'Pepsi': [[ 6.1697e-05, 4.2564e-05, 0 ], [ 0.091641, 0.14158, 0.20729 ]],
31
+ 'Sprite': [[ 6.0306e-06, 6.4139e-06, 6.5504e-06 ], [ 0.001886, 0.0018308, 0.0020025 ]],
32
+ 'Gatorade': [[ 0.0024574, 0.003007, 0.0037325 ], [ 0.024794, 0.019289, 0.008878 ]],
33
+ 'Chardonnay': [[ 1.7982e-05, 1.3758e-05, 1.2023e-05 ], [ 0.010782, 0.011855, 0.023997 ]],
34
+ 'White Zinfandel': [[ 1.7501e-05, 1.9069e-05, 1.288e-05 ], [ 0.012072, 0.016184, 0.019843 ]],
35
+ 'Merlot': [[ 2.1129e-05, 0, 0 ], [ 0.11632, 0.25191, 0.29434 ]],
36
+ 'Budweiser Beer': [[ 2.4356e-05, 2.4079e-05, 1.0564e-05 ], [ 0.011492, 0.024911, 0.057786 ]],
37
+ 'Coors Light Beer': [[ 5.0922e-05, 4.301e-05, 0 ], [ 0.006164, 0.013984, 0.034983 ]],
38
+ 'Clorox': [[ 0.0024035, 0.0031373, 0.003991 ], [ 0.0033542, 0.014892, 0.026297 ]],
39
+ 'Apple Juice': [[ 0.00013612, 0.00015836, 0.000227 ], [ 0.012957, 0.023741, 0.052184 ]],
40
+ 'Cranberry Juice': [[ 0.00010402, 0.00011646, 7.8139e-05 ], [ 0.039437, 0.094223, 0.12426 ]],
41
+ 'Grape Juice': [[ 5.382e-05, 0, 0 ], [ 0.10404, 0.23958, 0.29325 ]],
42
+ 'Ruby Grapefruit Juice': [[ 0.011002, 0.010927, 0.011036 ], [ 0.085867, 0.18314, 0.25262 ]],
43
+ 'White Grapefruit Juice': [[ 0.22826, 0.23998, 0.32748 ], [ 0.0138, 0.018831, 0.056781 ]],
44
+ 'Shampoo': [[ 0.0007176, 0.0008303, 0.0009016 ], [ 0.014107, 0.045693, 0.061717 ]],
45
+ 'Strawberry Shampoo': [[ 0.00015671, 0.00015947, 1.518e-05 ], [ 0.01449, 0.05796, 0.075823 ]],
46
+ 'Head & Shoulders Shampoo': [[ 0.023805, 0.028804, 0.034306 ], [ 0.084621, 0.15688, 0.20365 ]],
47
+ 'Lemon Tea Powder': [[ 0.040224, 0.045264, 0.051081 ], [ 2.4288, 4.5757, 7.2127 ]],
48
+ 'Orange Powder': [[ 0.00015617, 0.00017482, 0.0001762 ], [ 0.001449, 0.003441, 0.007863 ]],
49
+ 'Pink Lemonade Powder': [[ 0.00012103, 0.00013073, 0.00012528 ], [ 0.001165, 0.002366, 0.003195 ]],
50
+ 'Cappuccino Powder': [[ 1.8436, 2.5851, 2.1662 ], [ 35.844, 49.547, 61.084 ]],
51
+ 'Salt Powder': [[ 0.027333, 0.032451, 0.031979 ], [ 0.28415, 0.3257, 0.34148 ]],
52
+ 'Sugar Powder': [[ 0.00022272, 0.00025513, 0.000271 ], [ 0.012638, 0.031051, 0.050124 ]],
53
+ 'Suisse Mocha Powder': [[ 2.7979, 3.5452, 4.3365 ], [ 17.502, 27.004, 35.433 ]],
54
+ 'Pacific Ocean Surface Water': [[ 0.0001764, 0.00032095, 0.00019617 ], [ 0.031845, 0.031324, 0.030147 ]],
55
+ };
56
+
57
+ /** A named medium's `{ sigmaS, sigmaA }` (σ′s with g = 0), or null. */
58
+ export function namedScattering( name ) {
59
+
60
+ const entry = MEASURED[ name ];
61
+ return entry ? { sigmaS: entry[ 0 ].slice(), sigmaA: entry[ 1 ].slice() } : null;
62
+
63
+ }
64
+
65
+ /**
66
+ * Total diffuse reflectance of a half space of reduced albedo α′ and relative index η under the classical dipole
67
+ * (Jensen et al. 2001): (α′ / 2) (1 + e^(−4/3 A √(3(1 − α′)))) e^(−√(3(1 − α′))).
68
+ */
69
+ export function dipoleReflectance( albedo, eta ) {
70
+
71
+ const fdr = - 1.440 / ( eta * eta ) + 0.710 / eta + 0.668 + 0.0636 * eta;
72
+ const A = ( 1 + fdr ) / ( 1 - fdr );
73
+ const s = Math.sqrt( 3 * ( 1 - albedo ) );
74
+ return albedo / 2 * ( 1 + Math.exp( - 4 / 3 * A * s ) ) * Math.exp( - s );
75
+
76
+ }
77
+
78
+ /** The reduced albedo whose dipole reflectance is `reflectance` (pbrt inverts its own table the same way). */
79
+ export function albedoForReflectance( reflectance, eta ) {
80
+
81
+ const target = Math.min( Math.max( reflectance, 0 ), dipoleReflectance( 1, eta ) );
82
+ let lo = 0, hi = 1;
83
+ for ( let i = 0; i < 48; i ++ ) {
84
+
85
+ const mid = ( lo + hi ) / 2;
86
+ if ( dipoleReflectance( mid, eta ) < target ) lo = mid;
87
+ else hi = mid;
88
+
89
+ }
90
+
91
+ return ( lo + hi ) / 2;
92
+
93
+ }