rayzee 11.8.0 → 11.9.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 (53) hide show
  1. package/README.md +18 -6
  2. package/dist/{ArchiveImporter-_sJm5uaK.js → ArchiveImporter-BJxszU_k.js} +1549 -1232
  3. package/dist/{BidirectionalIntegrator-Dzt4Z97x.js → BidirectionalIntegrator-DWg66vAd.js} +240 -240
  4. package/dist/{BufferLayout-xPRH9jYD.js → BufferLayout-Bzgccg1o.js} +1 -0
  5. package/dist/{ColorManagement-30MfpHwQ.js → ColorManagement-B_HjAiy2.js} +1 -1
  6. package/dist/{EngineIssues-xljoXM0E.js → EngineIssues-CfJiBaNj.js} +1 -0
  7. package/dist/{NeuralRetouch-BKQCsmEs.js → NeuralRetouch-BD9E-Yht.js} +1 -1
  8. package/dist/{NeuralSuperRes-C4ymq68e.js → NeuralSuperRes-C8Mf3tJX.js} +1 -1
  9. package/dist/{PathTracer-C_to_i40.js → PathTracer-DeE9UJ1n.js} +3433 -3363
  10. package/dist/{PhysicalSky-bMHbm_7H.js → PhysicalSky-cKy75-oR.js} +45 -45
  11. package/dist/{RayzeeRenderer-B_aB6VTJ.js → RayzeeRenderer-CzLyBA3G.js} +13 -8
  12. package/dist/{SceneMetadata-CSmlJW4c.js → SceneMetadata-C17UfOEp.js} +1 -1
  13. package/dist/{SolarLimb-bZE6y63S.js → SolarLimb-DRYy8tln.js} +184 -172
  14. package/dist/addons/archives.es.js +1 -1
  15. package/dist/addons/bidirectional.es.js +1 -1
  16. package/dist/addons/color.es.js +1 -1
  17. package/dist/addons/physical-sky.es.js +1 -1
  18. package/dist/addons/storage.es.js +1 -1
  19. package/dist/{openStorage-B085D9FR.js → openStorage-CdyVjmps.js} +1 -1
  20. package/dist/rayzee-core.es.js +7 -7
  21. package/dist/rayzee.es.js +567 -559
  22. package/dist/rayzee.umd.js +106 -101
  23. package/dist/{sceneCachePolicy-D_uBSbQF.js → sceneCachePolicy-ywbGKHD3.js} +9 -6
  24. package/package.json +1 -1
  25. package/src/EngineDefaults.js +1 -0
  26. package/src/EngineIssues.js +1 -0
  27. package/src/Passes/OIDNTemporalHistory.js +1 -1
  28. package/src/Processor/ArchiveImporter.js +83 -21
  29. package/src/Processor/AssetLoader.js +7 -0
  30. package/src/Processor/BufferLayout.js +3 -3
  31. package/src/Processor/GeometryExtractor.js +12 -4
  32. package/src/Processor/MaterialPacking.js +2 -2
  33. package/src/Processor/PBRT/PBRTMaterials.js +47 -12
  34. package/src/Processor/PBRT/PBRTParser.js +144 -22
  35. package/src/Processor/PBRT/PBRTSceneBuilder.js +287 -67
  36. package/src/Processor/PBRT/PBRTTextureBake.js +191 -9
  37. package/src/Processor/PBRT/index.js +19 -4
  38. package/src/Processor/SceneProcessor.js +42 -26
  39. package/src/RayzeeRenderer.js +4 -2
  40. package/src/Stages/DenoiserSettings.js +0 -4
  41. package/src/Stages/NRD.js +53 -24
  42. package/src/Stages/NormalDepth.js +5 -3
  43. package/src/Stages/PathTracer.js +2 -0
  44. package/src/Stages/PathTracerStage.js +5 -1
  45. package/src/Storage/SceneGraphCodec.js +6 -3
  46. package/src/TSL/BVHTraversal.js +33 -20
  47. package/src/TSL/Common.js +25 -8
  48. package/src/TSL/Displacement.js +66 -113
  49. package/src/TSL/LightsDirect.js +33 -11
  50. package/src/TSL/SceneResources.js +8 -0
  51. package/src/TSL/ShadeKernel.js +2 -2
  52. package/src/TSL/Struct.js +1 -0
  53. package/src/managers/MaterialDataManager.js +11 -0
@@ -1,7 +1,7 @@
1
1
  import { t as e } from "./EngineEvents-CLckhYbL.js";
2
2
  import { a as t, i as n, r } from "./Logger-dDDGj9Av.js";
3
- import { f as i, v as a, y as o } from "./BufferLayout-xPRH9jYD.js";
4
- import { n as s } from "./EngineIssues-xljoXM0E.js";
3
+ import { f as i, v as a, y as o } from "./BufferLayout-Bzgccg1o.js";
4
+ import { n as s } from "./EngineIssues-CfJiBaNj.js";
5
5
  import { n as c, r as l } from "./shared-BGBcSxBQ.js";
6
6
  import { n as u } from "./AssetConfig-DSEntE65.js";
7
7
  import { BackSide as d, BufferAttribute as f, Color as p, DoubleSide as m, FrontSide as h, Matrix3 as g, Matrix4 as _, RGBAFormat as v, Vector2 as y, Vector3 as b } from "three";
@@ -230,6 +230,7 @@ var E = de({
230
230
  normalScale: [1, 1],
231
231
  bumpScale: 1,
232
232
  displacementScale: 1,
233
+ displacementBias: 0,
233
234
  transparent: 0,
234
235
  alphaTest: 0,
235
236
  alphaMode: 0,
@@ -1069,7 +1070,7 @@ var Be = class {
1069
1070
  this.expandedTriangleCount += u.count, this.meshTriangleRanges.push(u), u.count > 0 && !l && a && this._geometryRanges.set(r, {
1070
1071
  ...u,
1071
1072
  meshIndex: n
1072
- }), this._recordPlacements(e, n, l, c, u.count > 0 && a), u.count > 0 && this._onRange?.(n, u);
1073
+ }), u.count > 0 && (this._recordPlacements(e, n, l, c, a), this._onRange?.(n, u));
1073
1074
  }
1074
1075
  _recordPlacements(e, t, n = !1, r = !1, i = !1) {
1075
1076
  let a = e.matrixWorld.elements, o = this.instanceSource;
@@ -1232,6 +1233,7 @@ var Be = class {
1232
1233
  normalScale: o("normalScale"),
1233
1234
  bumpScale: o("bumpScale"),
1234
1235
  displacementScale: o("displacementScale"),
1236
+ displacementBias: o("displacementBias"),
1235
1237
  transparent: +!!e.transparent,
1236
1238
  alphaTest: o("alphaTest"),
1237
1239
  alphaMode: Pe(e),
@@ -1298,9 +1300,10 @@ var Be = class {
1298
1300
  }
1299
1301
  extractGeometry(e, t, n, r = !1, i = !1) {
1300
1302
  e.updateMatrix(), e.updateMatrixWorld();
1301
- let a = e.geometry;
1303
+ let a = e.geometry, o = a.attributes.position;
1304
+ if (!o) return;
1302
1305
  a.attributes.normal || a.computeVertexNormals();
1303
- let o = a.attributes.position, s = a.attributes.normal, c = a.attributes.uv, l = a.index ? a.index.array : null, u = l ? l.length / 3 : o.count / 3;
1306
+ let s = a.attributes.normal, c = a.attributes.uv, l = a.index ? a.index.array : null, u = Math.floor((l ? l.length : o.count) / 3);
1304
1307
  if (i) {
1305
1308
  let r = e.count ?? e.instanceMatrix.array.length / 16, i = this._matrixPool.mat4, a = new _();
1306
1309
  for (let d = 0; d < r; d++) {
@@ -1594,7 +1597,7 @@ var vt = class {
1594
1597
  samples: this.samples.slice()
1595
1598
  };
1596
1599
  }
1597
- }, yt = "11.8.0", $ = 1e4, bt = 15e5;
1600
+ }, yt = "11.9.0", $ = 1e4, bt = 15e5;
1598
1601
  function xt(e, t, n = $) {
1599
1602
  return e >= n && t / bt < e / 3;
1600
1603
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "11.8.0",
3
+ "version": "11.9.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",
@@ -117,6 +117,7 @@ export const MATERIAL_DEFAULTS = deepFreeze( {
117
117
  normalScale: [ 1, 1 ],
118
118
  bumpScale: 1,
119
119
  displacementScale: 1,
120
+ displacementBias: 0,
120
121
  transparent: 0,
121
122
  alphaTest: 0,
122
123
  alphaMode: 0,
@@ -13,6 +13,7 @@ export const ISSUE_CODES = Object.freeze( {
13
13
  ASSET_AMBIGUOUS_ENTRY: 'asset.ambiguous_entry',
14
14
  ASSET_ARCHIVE_TOO_LARGE: 'asset.archive_too_large',
15
15
  ASSET_ENTRY_TOO_LARGE: 'asset.entry_too_large',
16
+ ASSET_PARAMETERS_IGNORED: 'asset.parameters_ignored',
16
17
  TEXTURE_BUILD_FAILED: 'texture.build_failed',
17
18
  TEXTURE_PROCESSING_FALLBACK: 'texture.processing_fallback',
18
19
  TEXTURE_LIMIT_EXCEEDED: 'texture.limit_exceeded',
@@ -221,7 +221,7 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
221
221
 
222
222
  }
223
223
 
224
- let rough = textureLoad( shading, p, 0 ).w;
224
+ let rough = abs( textureLoad( shading, p, 0 ).w );
225
225
  var cap = mix( P.smoothLength, P.maxLength, smoothstep( P.roughStart, P.roughEnd, rough ) );
226
226
  if ( movedPx ) { cap = min( cap, P.movedLength ); }
227
227
  let newLen = min( len + 1.0, cap );
@@ -82,16 +82,17 @@ export class ArchiveImporter {
82
82
  constructor( loader ) {
83
83
 
84
84
  this.loader = loader;
85
- this._pendingGraph = null;
85
+ this._store = null;
86
86
  this.lastPBRTStats = null;
87
87
  this.lastUSDStats = null;
88
88
 
89
89
  }
90
90
 
91
- /** Drops a parsed scene still waiting to be stored: the model it was for is gone. */
91
+ /** Abandons a parsed scene the engine has not accepted: the model it was for is gone, or its load failed. */
92
92
  release() {
93
93
 
94
- this._pendingGraph = null;
94
+ this._store?.decide( false );
95
+ this._store = null;
95
96
 
96
97
  }
97
98
 
@@ -383,7 +384,8 @@ export class ArchiveImporter {
383
384
 
384
385
  /**
385
386
  * Stores a freshly parsed scene for the next open. Encoded here, before onModelLoad or the
386
- * build touch it; written in the background.
387
+ * build touch it; written in the background, and visible to an open only once the engine
388
+ * accepts the scene ({@link commitPendingGraph}).
387
389
  * @private
388
390
  */
389
391
  _storeGraph( key, built, label ) {
@@ -401,7 +403,7 @@ export class ArchiveImporter {
401
403
  meshCount: built.meshCount, entryPath: built.entryPath, candidates: built.candidates, frames: built.frames,
402
404
  warnings: built.warnings, triangleCount: built.triangleCount, placementCount: built.placementCount,
403
405
  mergedShapes: built.mergedShapes, skippedForBudget: built.skippedForBudget, droppedNoTemplate: built.droppedNoTemplate,
404
- render: built.render ?? null,
406
+ render: built.render ?? null, parameters: built.parameters ?? null,
405
407
  },
406
408
  } );
407
409
 
@@ -413,13 +415,29 @@ export class ArchiveImporter {
413
415
 
414
416
  }
415
417
 
416
- const pending = { key, encoded, label, parseMs: ( built.parseMs ?? 0 ) + ( built.buildMs ?? 0 ), triangles: built.triangleCount ?? null };
418
+ let settle;
419
+ const stop = new AbortController();
420
+ const store = {
421
+ key, encoded, label, model: built.group, writing: false, signal: stop.signal,
422
+ parseMs: ( built.parseMs ?? 0 ) + ( built.buildMs ?? 0 ), triangles: built.triangleCount ?? null,
423
+ accepted: new Promise( resolve => {
424
+
425
+ settle = resolve;
426
+
427
+ } ),
428
+ decide: accepted => {
429
+
430
+ settle( accepted );
431
+ if ( ! accepted ) stop.abort();
432
+
433
+ },
434
+ };
435
+ this._store = store;
417
436
 
418
437
  // A parse slow enough on its own is written now, during the build. Held until the build
419
438
  // ends, it kept every array the build replaces alive with it — the float normals and the
420
439
  // instance matrices, ~1 GB on the whole Moana subset, at the build's peak.
421
- if ( worthStoring( pending.parseMs, encoded.byteLength ) ) this._writeGraph( area, pending );
422
- else this._pendingGraph = pending;
440
+ if ( worthStoring( store.parseMs, encoded.byteLength ) ) this._writeGraph( area, store );
423
441
 
424
442
  }
425
443
 
@@ -430,34 +448,57 @@ export class ArchiveImporter {
430
448
  */
431
449
  flushPendingGraph( buildMs ) {
432
450
 
433
- const pending = this._pendingGraph;
434
- this._pendingGraph = null;
451
+ const store = this._store;
452
+ if ( ! store || store.writing ) return;
435
453
  const area = this.loader.storage?.area( ENGINE_AREAS.SCENES );
436
- if ( ! pending || ! area || ! worthStoring( pending.parseMs + buildMs, pending.encoded.byteLength ) ) return;
437
- this._writeGraph( area, pending );
454
+ if ( area && worthStoring( store.parseMs + buildMs, store.encoded.byteLength ) ) this._writeGraph( area, store );
455
+ else this._store = null;
456
+
457
+ }
458
+
459
+ /**
460
+ * The engine accepted `model`: the scene stored for it is committed once written. Until then the
461
+ * entry has no metadata, so no open can restore it.
462
+ * @param {import('three').Object3D} model - the model whose load the engine finished
463
+ */
464
+ commitPendingGraph( model ) {
465
+
466
+ const store = this._store;
467
+ if ( ! store || store.model !== model ) return;
468
+ this._store = null;
469
+ store.model = null;
470
+ store.decide( store.writing );
438
471
 
439
472
  }
440
473
 
441
474
  /** Writes an encoded graph in the background, letting each array go once it is on disk. @private */
442
- _writeGraph( area, pending ) {
475
+ _writeGraph( area, store ) {
443
476
 
477
+ store.writing = true;
444
478
  ( async () => {
445
479
 
446
- const writer = await area.create( pending.key, { label: `${pending.label} (scene)`, expectedBytes: pending.encoded.byteLength } );
447
- if ( ! writer ) return;
480
+ let writer = null;
448
481
  try {
449
482
 
450
- await writeSceneGraph( writer, pending.encoded, { release: true } );
451
- await writer.commit( { triangles: pending.triangles } );
483
+ writer = await area.create( store.key, { label: `${store.label} (scene)`, expectedBytes: store.encoded.byteLength } );
484
+ if ( ! writer ) return;
485
+ await writeSceneGraph( writer, store.encoded, { release: true, signal: store.signal } );
486
+ store.encoded = null;
487
+ if ( await store.accepted ) await writer.commit( { triangles: store.triangles } );
488
+ else await writer.abort();
452
489
 
453
490
  } catch ( error ) {
454
491
 
455
- await writer.abort();
456
- console.warn( 'Storing the scene failed:', error );
492
+ await writer?.abort().catch( () => {} );
493
+ if ( ! store.signal.aborted ) console.warn( 'Storing the scene failed:', error );
494
+
495
+ } finally {
496
+
497
+ store.encoded = null;
457
498
 
458
499
  }
459
500
 
460
- } )().catch( error => console.warn( 'Storing the scene failed:', error ) );
501
+ } )();
461
502
 
462
503
  }
463
504
 
@@ -651,7 +692,7 @@ export class ArchiveImporter {
651
692
 
652
693
  const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
653
694
  frames, parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
654
- droppedNoTemplate } = built;
695
+ droppedNoTemplate, parameters } = built;
655
696
  if ( stored ) console.info( `PBRT scene "${loadedEntry}" opened from storage in ${( buildMs / 1000 ).toFixed( 1 )} s, skipping the parse` );
656
697
 
657
698
  // Phase breakdown for scaling work; the engine's own build timings live in
@@ -679,6 +720,8 @@ export class ArchiveImporter {
679
720
 
680
721
  }
681
722
 
723
+ if ( parameters ) this._reportParameters( parameters, loadedEntry );
724
+
682
725
  // Diagnostics — surface what each mesh resolved to (helps debug black/wrong materials).
683
726
  if ( report && report.length && typeof console.table === 'function' ) {
684
727
 
@@ -728,6 +771,25 @@ export class ArchiveImporter {
728
771
 
729
772
  }
730
773
 
774
+ /**
775
+ * Every parameter the scene sets that the loader ignored or approximated, once, each with the number of directives
776
+ * that set it: one warning and one console list, never cut short.
777
+ * @private
778
+ */
779
+ _reportParameters( { unused, approximated }, entry ) {
780
+
781
+ const ignored = Object.keys( unused ).length, approx = Object.keys( approximated ).length;
782
+ if ( ignored + approx === 0 ) return;
783
+ const message = `${ignored} pbrt parameter(s) ignored and ${approx} approximated in "${entry}"`;
784
+ const lines = [
785
+ ...Object.entries( unused ).map( ( [ key, count ] ) => ` ignored ${key} ×${count}` ),
786
+ ...Object.entries( approximated ).map( ( [ key, count ] ) => ` approximated ${key} ×${count}` )
787
+ ];
788
+ console.warn( `PBRT loader: ${message} (× directives that set it)\n${lines.join( '\n' )}` );
789
+ this.loader._issues?.warn( ISSUE_CODES.ASSET_PARAMETERS_IGNORED, message, { entry, unused, approximated } );
790
+
791
+ }
792
+
731
793
  /** A texture map from an archive, tagged with its path so a stored scene can read it back. @private */
732
794
  async _archiveImage( bytes, fname ) {
733
795
 
@@ -420,6 +420,13 @@ export class AssetLoader extends EventDispatcher {
420
420
 
421
421
  }
422
422
 
423
+ /** @see ArchiveImporter#commitPendingGraph */
424
+ commitPendingGraph( model ) {
425
+
426
+ this.archives?.commitPendingGraph( model );
427
+
428
+ }
429
+
423
430
  // Blob's own readers, not FileReader: Node has no FileReader.
424
431
  readFileAsArrayBuffer( file ) {
425
432
 
@@ -149,8 +149,8 @@ export const MATERIAL_DATA_LAYOUT = {
149
149
  SPECULAR_INTENSITY: 40, SPECULAR_COLOR: 41,
150
150
  // Slot 11: iridescence + iridescenceIOR + iridescenceThicknessRange [extended BxDF]
151
151
  IRIDESCENCE: 44, IRIDESCENCE_IOR: 45, IRIDESCENCE_THICKNESS_RANGE: 46,
152
- // Slot 12: bumpScale + displacementScale + displacementMapIndex + (padding)
153
- BUMP_SCALE: 48, DISPLACEMENT_SCALE: 49, DISPLACEMENT_MAP_INDEX: 50,
152
+ // Slot 12: bumpScale + displacementScale + displacementMapIndex + displacementBias
153
+ BUMP_SCALE: 48, DISPLACEMENT_SCALE: 49, DISPLACEMENT_MAP_INDEX: 50, DISPLACEMENT_BIAS: 51,
154
154
 
155
155
  // ── Transform float offsets (8 floats each: 7 matrix values + 1 padding) ──
156
156
  ALBEDO_TRANSFORM: 52,
@@ -193,7 +193,7 @@ export const MATERIAL_DATA_LAYOUT = {
193
193
  SHEEN_COLOR: 9, // [extended] sheenColor, diffuseTransmission
194
194
  SPECULAR: 10, // [extended] specularIntensity, specularColor
195
195
  IRIDESCENCE: 11, // [extended] iridescence, iridescenceIOR, iridescenceThicknessRange
196
- BUMP_DISPLACEMENT: 12, // bumpScale, displacementScale, displacementMapIndex
196
+ BUMP_DISPLACEMENT: 12, // bumpScale, displacementScale, displacementMapIndex, displacementBias
197
197
  ALBEDO_TRANSFORM_A: 13, ALBEDO_TRANSFORM_B: 14,
198
198
  NORMAL_TRANSFORM_A: 15, NORMAL_TRANSFORM_B: 16,
199
199
  ROUGHNESS_TRANSFORM_A: 17, ROUGHNESS_TRANSFORM_B: 18,
@@ -862,8 +862,13 @@ export class GeometryExtractor {
862
862
  this.expandedTriangleCount += range.count;
863
863
  this.meshTriangleRanges.push( range );
864
864
  if ( range.count > 0 && ! bake && shareable ) this._geometryRanges.set( key, { ...range, meshIndex } );
865
- this._recordPlacements( mesh, meshIndex, bake, expand, range.count > 0 && shareable );
866
- if ( range.count > 0 ) this._onRange?.( meshIndex, range );
865
+ // A placement with nothing to trace would get a TLAS leaf with no BLAS to point at.
866
+ if ( range.count > 0 ) {
867
+
868
+ this._recordPlacements( mesh, meshIndex, bake, expand, shareable );
869
+ this._onRange?.( meshIndex, range );
870
+
871
+ }
867
872
 
868
873
  }
869
874
 
@@ -1227,6 +1232,7 @@ export class GeometryExtractor {
1227
1232
  normalScale: pick( 'normalScale' ),
1228
1233
  bumpScale: pick( 'bumpScale' ),
1229
1234
  displacementScale: pick( 'displacementScale' ),
1235
+ displacementBias: pick( 'displacementBias' ),
1230
1236
 
1231
1237
  transparent: material.transparent ? 1 : 0,
1232
1238
  alphaTest: pick( 'alphaTest' ),
@@ -1345,13 +1351,15 @@ export class GeometryExtractor {
1345
1351
  mesh.updateMatrixWorld();
1346
1352
 
1347
1353
  const geometry = mesh.geometry;
1348
- if ( ! geometry.attributes.normal ) geometry.computeVertexNormals();
1349
1354
  const positions = geometry.attributes.position;
1355
+ if ( ! positions ) return;
1356
+ if ( ! geometry.attributes.normal ) geometry.computeVertexNormals();
1350
1357
  const normals = geometry.attributes.normal;
1351
1358
  const uvs = geometry.attributes.uv;
1352
1359
  const indices = geometry.index ? geometry.index.array : null;
1353
1360
 
1354
- const triangleCount = indices ? indices.length / 3 : positions.count / 3;
1361
+ // Whole triangles only, as three.js draws them: a trailing vertex or two read past the end as NaN.
1362
+ const triangleCount = Math.floor( ( indices ? indices.length : positions.count ) / 3 );
1355
1363
 
1356
1364
  if ( expandInstances ) {
1357
1365
 
@@ -16,7 +16,7 @@ const TRANSFORMS = [
16
16
 
17
17
  /** Floats no field writes; the packer gives them fixed values. */
18
18
  export const MATERIAL_RESERVED_FLOATS = Object.freeze( [
19
- M.VISIBLE, M.DISPLACEMENT_MAP_INDEX + 1, M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 1, M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 2,
19
+ M.VISIBLE, M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 1, M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 2,
20
20
  ] );
21
21
 
22
22
  /**
@@ -129,6 +129,7 @@ export function packMaterial( data, base, mat ) {
129
129
  s( M.BUMP_SCALE, mat.bumpScale, D.bumpScale );
130
130
  s( M.DISPLACEMENT_SCALE, mat.displacementScale, D.displacementScale );
131
131
  data[ base + M.DISPLACEMENT_MAP_INDEX ] = mapIndex( mat.displacementMap );
132
+ s( M.DISPLACEMENT_BIAS, mat.displacementBias, D.displacementBias );
132
133
 
133
134
  for ( const [ key, offset ] of TRANSFORMS ) {
134
135
 
@@ -163,7 +164,6 @@ export function packMaterial( data, base, mat ) {
163
164
  data[ base + M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX ] = mapIndex( mat.diffuseTransmissionColorMap );
164
165
 
165
166
  data[ base + M.VISIBLE ] = 1;
166
- data[ base + M.DISPLACEMENT_MAP_INDEX + 1 ] = 0;
167
167
  data[ base + M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 1 ] = 0;
168
168
  data[ base + M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 2 ] = 0;
169
169
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { MeshPhysicalMaterial, Color, DoubleSide } from 'three';
12
12
  import { namedScattering, dipoleReflectance, albedoForReflectance } from './PBRTScattering.js';
13
+ import { approximated, param } from './PBRTParser.js';
13
14
 
14
15
  // Normal-incidence reflectance approximations for pbrt's named conductor spectra.
15
16
  const METAL_ALBEDO = {
@@ -54,21 +55,37 @@ function blackbodyToRGB( kelvin ) {
54
55
 
55
56
  export function pFloat( params, name, dflt ) {
56
57
 
57
- const p = params[ name ];
58
+ const p = param( params, name );
59
+ if ( p && p.type !== 'float' && p.type !== 'integer' ) approximated( params, name );
58
60
  return p && typeof p.value[ 0 ] === 'number' ? p.value[ 0 ] : dflt;
59
61
 
60
62
  }
61
63
 
64
+ /** An index of refraction: a sampled spectrum ( λ value pairs ) is read at 550 nm, linearly between its samples. */
65
+ export function pEta( params, name, dflt ) {
66
+
67
+ const p = param( params, name );
68
+ if ( p?.type !== 'spectrum' || typeof p.value[ 0 ] !== 'number' ) return pFloat( params, name, dflt );
69
+ approximated( params, name );
70
+ const v = p.value;
71
+ if ( v.length < 2 ) return dflt;
72
+ let i = 2;
73
+ while ( i < v.length - 1 && v[ i ] < 550 ) i += 2;
74
+ if ( i >= v.length - 1 || v[ i - 2 ] >= 550 ) return v[ i >= v.length - 1 ? i - 1 : 1 ];
75
+ return v[ i - 1 ] + ( v[ i + 1 ] - v[ i - 1 ] ) * ( 550 - v[ i - 2 ] ) / ( v[ i ] - v[ i - 2 ] );
76
+
77
+ }
78
+
62
79
  export function pString( params, name, dflt ) {
63
80
 
64
- const p = params[ name ];
81
+ const p = param( params, name );
65
82
  return p && typeof p.value[ 0 ] === 'string' ? p.value[ 0 ] : dflt;
66
83
 
67
84
  }
68
85
 
69
86
  export function pBool( params, name, dflt ) {
70
87
 
71
- const v = params[ name ]?.value?.[ 0 ];
88
+ const v = param( params, name )?.value?.[ 0 ];
72
89
  return v === undefined ? dflt : v === true || v === 'true';
73
90
 
74
91
  }
@@ -80,7 +97,7 @@ export function pBool( params, name, dflt ) {
80
97
  */
81
98
  export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
82
99
 
83
- const p = params[ name ];
100
+ const p = param( params, name );
84
101
  if ( ! p ) return { rgb: dfltRGB, texture: null };
85
102
 
86
103
  // Reference to a named texture.
@@ -91,6 +108,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
91
108
  if ( tex && tex.texture ) return { rgb: tex.constant ?? null, texture: tex.texture };
92
109
  if ( tex && tex.constant ) return { rgb: tex.constant, texture: null };
93
110
  ctx.warn( `texture "${texName}" could not be resolved` );
111
+ approximated( params, name );
94
112
  return { rgb: dfltRGB, texture: null };
95
113
 
96
114
  }
@@ -110,6 +128,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
110
128
 
111
129
  if ( p.type === 'blackbody' ) {
112
130
 
131
+ approximated( params, name );
113
132
  return { rgb: blackbodyToRGB( p.value[ 0 ] ), texture: null };
114
133
 
115
134
  }
@@ -117,6 +136,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
117
136
  if ( p.type === 'spectrum' ) {
118
137
 
119
138
  // Named spectrum (e.g. "metal-Au-eta") or sampled [lambda val ...].
139
+ approximated( params, name );
120
140
  if ( typeof p.value[ 0 ] === 'string' ) {
121
141
 
122
142
  const key = namedMetalKey( p.value[ 0 ] );
@@ -139,6 +159,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
139
159
 
140
160
  }
141
161
 
162
+ approximated( params, name );
142
163
  return { rgb: dfltRGB, texture: null };
143
164
 
144
165
  }
@@ -155,9 +176,10 @@ function namedMetalKey( s ) {
155
176
  // A float parameter's value, or a float texture's mean (`ctx.floatTextureMean`); null when absent or unreadable.
156
177
  async function floatOrMean( params, name, ctx ) {
157
178
 
158
- const p = params[ name ];
179
+ const p = param( params, name );
159
180
  if ( ! p ) return null;
160
181
  if ( typeof p.value[ 0 ] === 'number' ) return p.value[ 0 ];
182
+ approximated( params, name );
161
183
  if ( p.type === 'texture' ) return ( await ctx.floatTextureMean?.( p.value[ 0 ] ) ) ?? null;
162
184
  return null;
163
185
 
@@ -171,7 +193,7 @@ async function floatOrMean( params, name, ctx ) {
171
193
  */
172
194
  const remapsRoughness = ( params ) => {
173
195
 
174
- const remap = params.remaproughness?.value?.[ 0 ];
196
+ const remap = param( params, 'remaproughness' )?.value?.[ 0 ];
175
197
  return remap !== false && remap !== 'false';
176
198
 
177
199
  };
@@ -225,8 +247,14 @@ async function applyRoughness( mat, target, params, dflt, ctx, prefix = '' ) {
225
247
  // A spectrum parameter as one RGB; a texture counts as its mean (`ctx.textureMeanRGB`).
226
248
  async function spectrumMean( params, key, ctx, dflt ) {
227
249
 
228
- const p = params[ key ];
229
- if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB?.( p.value[ 0 ] ) ) ?? dflt;
250
+ const p = param( params, key );
251
+ if ( p?.type === 'texture' ) {
252
+
253
+ approximated( params, key );
254
+ return ( await ctx.textureMeanRGB?.( p.value[ 0 ] ) ) ?? dflt;
255
+
256
+ }
257
+
230
258
  return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
231
259
 
232
260
  }
@@ -243,7 +271,7 @@ const SUBSURFACE_SIGMA_S = [ 2.55, 3.21, 3.77 ];
243
271
  */
244
272
  async function applySubsurface( mat, params, ctx ) {
245
273
 
246
- const eta = pFloat( params, 'eta', 1.33 );
274
+ const eta = pEta( params, 'eta', 1.33 );
247
275
  const scale = pFloat( params, 'scale', 1 );
248
276
  const scaled = ( rgb ) => rgb.map( ( v ) => Math.max( 0, v * scale ) );
249
277
  let g = pFloat( params, 'g', 0 );
@@ -353,6 +381,7 @@ export async function buildMaterial( def, ctx ) {
353
381
 
354
382
  const type = def?.type || 'diffuse';
355
383
  const params = def?.params || {};
384
+ ctx.trackParams?.( params, `Material ${type}`, 'type' );
356
385
  const mat = new MeshPhysicalMaterial( { side: DoubleSide, roughness: 1, metalness: 0 } );
357
386
 
358
387
  const setColor = ( rgb ) => {
@@ -416,7 +445,7 @@ export async function buildMaterial( def, ctx ) {
416
445
  mat.transmission = 1;
417
446
  mat.metalness = 0;
418
447
  mat.color.setRGB( 1, 1, 1 );
419
- mat.ior = pFloat( params, 'eta', 1.5 );
448
+ mat.ior = pEta( params, 'eta', 1.5 );
420
449
  await applyRoughness( mat, 'roughness', params, 0, ctx );
421
450
  mat.thickness = type === 'thindielectric' ? 0 : pFloat( params, 'thickness', 0 );
422
451
  break;
@@ -447,7 +476,13 @@ export async function buildMaterial( def, ctx ) {
447
476
  const refl = await resolveSpectrum( params, 'reflectance', ctx, [ 0.25, 0.25, 0.25 ] );
448
477
  const base = ( v ) => Math.min( 1, unit( v ) / ( 1 - dt ) );
449
478
  applyAlbedo( { texture: refl.texture, rgb: refl.rgb ? refl.rgb.map( base ) : ( refl.texture ? [ 1, 1, 1 ].map( base ) : null ) } );
450
- if ( trans.texture ) ctx.warn( 'diffusetransmission: a textured "transmittance" is not supported — using 0.5' );
479
+ if ( trans.texture ) {
480
+
481
+ ctx.warn( 'diffusetransmission: a textured "transmittance" is not supported — using 0.5' );
482
+ approximated( params, 'transmittance' );
483
+
484
+ }
485
+
451
486
  mat.diffuseTransmission = dt;
452
487
  mat.diffuseTransmissionColor = new Color().setRGB( ...T.map( ( v ) => ( dt > 0 ? Math.min( 1, v / dt ) : 1 ) ) );
453
488
  mat.roughness = 1;
@@ -477,7 +512,7 @@ export async function buildMaterial( def, ctx ) {
477
512
  // — physically loose but visually faithful, and crucially silent on success
478
513
  // instead of warning per shape. Recursive: mix-in-mix terminates as the
479
514
  // chain bottoms out at a non-mix.
480
- const matNames = params.materials?.value || [];
515
+ const matNames = param( params, 'materials' )?.value || [];
481
516
  let t = Math.max( 0, Math.min( 1, pFloat( params, 'amount', 0.5 ) ) );
482
517
  const defA = matNames[ 0 ] ? ctx.namedMaterials?.get( matNames[ 0 ] ) : null;
483
518
  const defB = matNames[ 1 ] ? ctx.namedMaterials?.get( matNames[ 1 ] ) : null;