rayzee 11.7.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 (79) hide show
  1. package/README.md +31 -6
  2. package/dist/{ArchiveImporter-DYcRXeKE.js → ArchiveImporter-BJxszU_k.js} +2353 -1356
  3. package/dist/{BidirectionalIntegrator-BvjRpbTP.js → BidirectionalIntegrator-DWg66vAd.js} +302 -301
  4. package/dist/{BufferLayout-xPRH9jYD.js → BufferLayout-Bzgccg1o.js} +1 -0
  5. package/dist/{ColorManagement-oeEk7oSu.js → ColorManagement-B_HjAiy2.js} +1 -1
  6. package/dist/{EngineIssues-CIZnXHsF.js → EngineIssues-CfJiBaNj.js} +19 -8
  7. package/dist/{NeuralRetouch-ClaMiKYg.js → NeuralRetouch-BD9E-Yht.js} +2 -2
  8. package/dist/{NeuralSuperRes-WNEXun9s.js → NeuralSuperRes-C8Mf3tJX.js} +2 -2
  9. package/dist/{PathTracer-B9wZPPVL.js → PathTracer-DeE9UJ1n.js} +3530 -3402
  10. package/dist/{PhysicalSky-DcjItqKi.js → PhysicalSky-cKy75-oR.js} +45 -45
  11. package/dist/{RayzeeRenderer-p-NeE6E2.js → RayzeeRenderer-CzLyBA3G.js} +378 -374
  12. package/dist/SceneMetadata-C17UfOEp.js +183 -0
  13. package/dist/{SolarLimb-bZE6y63S.js → SolarLimb-DRYy8tln.js} +184 -172
  14. package/dist/{ToneMapGPU-sAs8wdgI.js → ToneMapGPU-4GPwoG_c.js} +13 -6
  15. package/dist/addons/archives.es.js +1 -1
  16. package/dist/addons/bidirectional.es.js +1 -1
  17. package/dist/addons/color.es.js +1 -1
  18. package/dist/addons/physical-sky.es.js +1 -1
  19. package/dist/addons/storage.es.js +1 -1
  20. package/dist/{openStorage-Fe9BoS1_.js → openStorage-CdyVjmps.js} +1 -1
  21. package/dist/rayzee-core.es.js +7 -7
  22. package/dist/rayzee.es.js +821 -794
  23. package/dist/rayzee.umd.js +112 -103
  24. package/dist/{sceneCachePolicy-B9w3BnaP.js → sceneCachePolicy-ywbGKHD3.js} +9 -6
  25. package/package.json +1 -1
  26. package/src/EngineDefaults.js +1 -0
  27. package/src/EngineIssues.js +21 -6
  28. package/src/Passes/AIUpscaler.js +4 -0
  29. package/src/Passes/OIDNDenoiser.js +8 -6
  30. package/src/Passes/OIDNTemporalHistory.js +1 -1
  31. package/src/PathTracerApp.js +10 -1
  32. package/src/Processor/ArchiveImporter.js +106 -27
  33. package/src/Processor/AssetLoader.js +22 -0
  34. package/src/Processor/BufferLayout.js +3 -3
  35. package/src/Processor/GeometryExtractor.js +12 -4
  36. package/src/Processor/LightSerializer.js +6 -2
  37. package/src/Processor/MaterialPacking.js +2 -2
  38. package/src/Processor/PBRT/PBRTMaterials.js +49 -13
  39. package/src/Processor/PBRT/PBRTParser.js +144 -22
  40. package/src/Processor/PBRT/PBRTSceneBuilder.js +287 -67
  41. package/src/Processor/PBRT/PBRTTextureBake.js +191 -9
  42. package/src/Processor/PBRT/index.js +19 -4
  43. package/src/Processor/PlatformImageLoader.js +9 -0
  44. package/src/Processor/SceneProcessor.js +42 -26
  45. package/src/Processor/TextureCreator.js +44 -4
  46. package/src/Processor/ToneMapCPU.js +34 -5
  47. package/src/Processor/ToneMapGPU.js +15 -9
  48. package/src/Processor/USD/USDScene.js +981 -17
  49. package/src/RayzeeRenderer.js +85 -28
  50. package/src/Stages/AutoExposure.js +21 -8
  51. package/src/Stages/Compositor.js +39 -1
  52. package/src/Stages/DenoiserSettings.js +0 -4
  53. package/src/Stages/LocalExposure.js +44 -18
  54. package/src/Stages/NRD.js +53 -24
  55. package/src/Stages/NormalDepth.js +12 -6
  56. package/src/Stages/PathTracer.js +27 -1
  57. package/src/Stages/PathTracerStage.js +14 -2
  58. package/src/Storage/SceneGraphCodec.js +6 -3
  59. package/src/TSL/BVHTraversal.js +33 -20
  60. package/src/TSL/CameraRay.js +12 -1
  61. package/src/TSL/Common.js +25 -8
  62. package/src/TSL/DebugKernel.js +3 -2
  63. package/src/TSL/Displacement.js +66 -113
  64. package/src/TSL/FinalWriteKernel.js +4 -7
  65. package/src/TSL/GenerateKernel.js +6 -2
  66. package/src/TSL/LightSplatKernel.js +7 -4
  67. package/src/TSL/LightsCore.js +6 -2
  68. package/src/TSL/LightsDirect.js +33 -11
  69. package/src/TSL/LightsSampling.js +45 -12
  70. package/src/TSL/SceneResources.js +19 -0
  71. package/src/TSL/ShadeKernel.js +3 -4
  72. package/src/TSL/Struct.js +1 -0
  73. package/src/integrators/BidirectionalIntegrator.js +1 -0
  74. package/src/managers/CameraManager.js +9 -0
  75. package/src/managers/DenoisingManager.js +1 -0
  76. package/src/managers/InteractionManager.js +6 -0
  77. package/src/managers/MaterialDataManager.js +11 -0
  78. package/src/managers/UniformManager.js +3 -1
  79. package/dist/SceneMetadata-D2Sh-0e-.js +0 -110
@@ -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-CIZnXHsF.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.7.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.7.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,
@@ -6,10 +6,14 @@
6
6
  */
7
7
  export const ISSUE_CODES = Object.freeze( {
8
8
  ADAPTER_SOFTWARE: 'adapter.software',
9
+ ADAPTER_UNAVAILABLE: 'adapter.unavailable',
10
+ DEVICE_UNAVAILABLE: 'device.unavailable',
11
+ WEBGPU_UNAVAILABLE: 'webgpu.unavailable',
9
12
  ASSET_UNREACHABLE: 'asset.unreachable',
10
13
  ASSET_AMBIGUOUS_ENTRY: 'asset.ambiguous_entry',
11
14
  ASSET_ARCHIVE_TOO_LARGE: 'asset.archive_too_large',
12
15
  ASSET_ENTRY_TOO_LARGE: 'asset.entry_too_large',
16
+ ASSET_PARAMETERS_IGNORED: 'asset.parameters_ignored',
13
17
  TEXTURE_BUILD_FAILED: 'texture.build_failed',
14
18
  TEXTURE_PROCESSING_FALLBACK: 'texture.processing_fallback',
15
19
  TEXTURE_LIMIT_EXCEEDED: 'texture.limit_exceeded',
@@ -42,9 +46,9 @@ export const ISSUE_SEVERITY = Object.freeze( { ERROR: 'error', WARNING: 'warning
42
46
 
43
47
  export class EngineIssueError extends Error {
44
48
 
45
- constructor( issue ) {
49
+ constructor( issue, options ) {
46
50
 
47
- super( `[${issue.code}] ${issue.message}` );
51
+ super( `[${issue.code}] ${issue.message}`, options );
48
52
  this.name = 'EngineIssueError';
49
53
  this.code = issue.code;
50
54
  this.detail = issue.detail;
@@ -67,13 +71,24 @@ export class IssueLog {
67
71
  /** @throws {EngineIssueError} when strict and severity is 'error' */
68
72
  record( code, message, detail = {}, severity = ISSUE_SEVERITY.ERROR ) {
69
73
 
70
- const issue = Object.freeze( { code, message, detail, severity, at: Date.now() } );
71
- this._entries.push( issue );
74
+ const issue = this._add( code, message, detail, severity );
75
+ if ( this.strict && severity === ISSUE_SEVERITY.ERROR ) throw new EngineIssueError( issue );
76
+ return issue;
72
77
 
73
- this._onIssue?.( issue );
78
+ }
74
79
 
75
- if ( this.strict && severity === ISSUE_SEVERITY.ERROR ) throw new EngineIssueError( issue );
80
+ /** For what nothing can continue past: records an error and throws it, strict or not. */
81
+ fail( code, message, detail = {}, cause ) {
76
82
 
83
+ throw new EngineIssueError( this._add( code, message, detail, ISSUE_SEVERITY.ERROR ), cause === undefined ? undefined : { cause } );
84
+
85
+ }
86
+
87
+ _add( code, message, detail, severity ) {
88
+
89
+ const issue = Object.freeze( { code, message, detail, severity, at: Date.now() } );
90
+ this._entries.push( issue );
91
+ this._onIssue?.( issue );
77
92
  return issue;
78
93
 
79
94
  }
@@ -78,6 +78,7 @@ export class AIUpscaler extends EventDispatcher {
78
78
  * @param {Function} [options.getGPUTextures] - Returns { color: GPUTexture } for HDR path
79
79
  * @param {Function} [options.getExposure] - Returns current exposure multiplier
80
80
  * @param {Function} [options.getToneMapping] - Returns Three.js ToneMapping constant
81
+ * @param {Function} [options.getTransparentBackground] - Whether the HDR path keeps the render's coverage as alpha
81
82
  * @param {number} [options.tileSize] - Override default tile size
82
83
  */
83
84
  constructor( output, renderer, options = {} ) {
@@ -101,6 +102,7 @@ export class AIUpscaler extends EventDispatcher {
101
102
  this.getToneMapping = options.getToneMapping || ( () => ACESFilmicToneMapping );
102
103
  this.getSaturation = options.getSaturation || ( () => 1.0 );
103
104
  this.getDisplayGain = options.getDisplayGain || ( () => null );
105
+ this.getTransparentBackground = options.getTransparentBackground || ( () => false );
104
106
 
105
107
  // Configuration
106
108
  this.enabled = false;
@@ -506,6 +508,8 @@ export class AIUpscaler extends EventDispatcher {
506
508
  gain: this.getDisplayGain(),
507
509
  } );
508
510
  this._toneMapper.release();
511
+ // The render's alpha is coverage; the canvas shows an opaque backdrop opaque, and so does the upscale.
512
+ if ( ! this.getTransparentBackground() ) planes.fill( 1, 3 * width * height );
509
513
 
510
514
  return { planes, width, height, isHDR: true };
511
515
 
@@ -443,10 +443,10 @@ export class OIDNDenoiser extends EventDispatcher {
443
443
 
444
444
  } while ( this._reloadPending );
445
445
 
446
- // Fire a start() that arrived mid-load. Guard on unet+enabled so a failed load
447
- // or a disable during loading doesn't kick off a denoise. Outside the loop, so a
448
- // no-op reload still releases it — it used to be stranded by the early return below.
449
- if ( this._pendingStart && this.unet && this.enabled ) {
446
+ // Fire a start() that arrived mid-load, unless it was disabled meanwhile. After a failed
447
+ // load too: it denoises nothing but still sends 'end', which its caller waits for. Outside
448
+ // the loop, so a no-op reload still releases it — it used to be stranded by the early return below.
449
+ if ( this._pendingStart && this.enabled ) {
450
450
 
451
451
  const continuous = this._pendingStartContinuous;
452
452
  this._pendingStart = false;
@@ -617,8 +617,6 @@ export class OIDNDenoiser extends EventDispatcher {
617
617
 
618
618
  async execute( continuous = false ) {
619
619
 
620
- if ( ! this.enabled || ! this.unet ) return false;
621
-
622
620
  // Create abort controller for this execution
623
621
  const runId = ++ this._runId;
624
622
  this.state.abortController = new AbortController();
@@ -626,6 +624,10 @@ export class OIDNDenoiser extends EventDispatcher {
626
624
 
627
625
  try {
628
626
 
627
+ // Inside the try, so the finally ends what start() announced: with no weights (a failed
628
+ // load) the Denoising badge, the closing upscaler and a video export waited forever.
629
+ if ( ! this.enabled || ! this.unet ) return false;
630
+
629
631
  // The GPU path bails out early — no textures yet, no device — by returning false, and
630
632
  // a caller that read that as success published a picture nothing had written to.
631
633
  return await this._executeUNetGPU( continuous ) !== false;
@@ -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 );
@@ -5,6 +5,7 @@ import { MotionVector } from './Stages/MotionVector.js';
5
5
  import { NRD } from './Stages/NRD.js';
6
6
  import { AutoExposure, AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
7
7
  import { LocalExposure, LOCAL_EXPOSURE_DEFAULTS } from './Stages/LocalExposure.js';
8
+ import { backdropGain } from './Processor/ToneMapCPU.js';
8
9
  import { PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings } from './EngineDefaults.js';
9
10
  import { createLogger } from './utils/Logger.js';
10
11
  import { EngineEvents } from './EngineEvents.js';
@@ -323,6 +324,7 @@ export class PathTracerApp extends RayzeeRenderer {
323
324
 
324
325
  if ( this.stages.autoExposure ) this.stages.autoExposure.setCompensation( value );
325
326
  else core.applyExposure( value );
327
+ this.stages.compositor?.setBackdropExposure( value );
326
328
 
327
329
  },
328
330
  onCameraProjection: ( value ) => {
@@ -423,7 +425,12 @@ export class PathTracerApp extends RayzeeRenderer {
423
425
  const le = this.stages.localExposure;
424
426
  // A readback outside the loop (headless) can come before the loop would have built it from this image.
425
427
  if ( le?.wantsBuild ) le.build( this.pipeline.context );
426
- return le?.toneGain() ?? null;
428
+ const local = le?.toneGain() ?? null;
429
+ if ( this.pipeline?.context.getState( 'pathtracer:backdrop' ) !== 'color' ) return local;
430
+
431
+ // As the compositor shows it: a solid backdrop at the manual exposure, untouched by auto and local exposure.
432
+ const { toneMappingExposure, toneMapping } = this.renderer;
433
+ return { ...local, backdrop: backdropGain( this.settings.get( 'exposure' ) ?? 1, toneMappingExposure, toneMapping ) };
427
434
 
428
435
  }
429
436
 
@@ -1130,6 +1137,8 @@ export class PathTracerApp extends RayzeeRenderer {
1130
1137
  if ( ! ae ) return;
1131
1138
 
1132
1139
  ae.setCompensation( this.settings.get( 'exposure' ) ?? 1 );
1140
+ // A solid backdrop stays at the manual exposure while auto exposure moves the picture.
1141
+ this.stages.compositor.setBackdropExposure( this.settings.get( 'exposure' ) ?? 1 );
1133
1142
  if ( this.stages.localExposure ) this.stages.localExposure.onChange = () => this._refreshFinished();
1134
1143
  ae.onChange = () => this._refreshFinished();
1135
1144
 
@@ -26,9 +26,10 @@ import { loadPBRTScene, pickEntryPath, VirtualFS, PBRT_BUILD_REVISION } from './
26
26
  import { loadUSDScene, listUSDRootLayers, listUSDParts, USDFiles, USDStage, USD_LAYER } from './USD/index.js';
27
27
  import { pfmTexture } from './PBRT/PFM.js';
28
28
  import { extractSceneMetadata } from './SceneMetadata.js';
29
- import { ISSUE_CODES, ISSUE_SEVERITY } from '../EngineIssues.js';
29
+ import { ISSUE_CODES, ISSUE_SEVERITY, EngineIssueError } from '../EngineIssues.js';
30
30
  import { ARCHIVE_FORMATS } from './archiveFormats.js';
31
- import { withHostWorker } from '../Platform.js';
31
+ import { withHostWorker, getPlatform } from '../Platform.js';
32
+ import { platformImageFromBytes } from './PlatformImageLoader.js';
32
33
 
33
34
  const MTL_TEXTURE_TIMEOUT_MS = 30000;
34
35
  const MAIN_MODEL_FILES = [ 'scene.gltf', 'scene.glb', 'model.gltf', 'model.glb', 'main.gltf', 'main.glb', 'asset.gltf', 'asset.glb' ];
@@ -81,16 +82,17 @@ export class ArchiveImporter {
81
82
  constructor( loader ) {
82
83
 
83
84
  this.loader = loader;
84
- this._pendingGraph = null;
85
+ this._store = null;
85
86
  this.lastPBRTStats = null;
86
87
  this.lastUSDStats = null;
87
88
 
88
89
  }
89
90
 
90
- /** 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. */
91
92
  release() {
92
93
 
93
- this._pendingGraph = null;
94
+ this._store?.decide( false );
95
+ this._store = null;
94
96
 
95
97
  }
96
98
 
@@ -382,7 +384,8 @@ export class ArchiveImporter {
382
384
 
383
385
  /**
384
386
  * Stores a freshly parsed scene for the next open. Encoded here, before onModelLoad or the
385
- * 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}).
386
389
  * @private
387
390
  */
388
391
  _storeGraph( key, built, label ) {
@@ -400,7 +403,7 @@ export class ArchiveImporter {
400
403
  meshCount: built.meshCount, entryPath: built.entryPath, candidates: built.candidates, frames: built.frames,
401
404
  warnings: built.warnings, triangleCount: built.triangleCount, placementCount: built.placementCount,
402
405
  mergedShapes: built.mergedShapes, skippedForBudget: built.skippedForBudget, droppedNoTemplate: built.droppedNoTemplate,
403
- render: built.render ?? null,
406
+ render: built.render ?? null, parameters: built.parameters ?? null,
404
407
  },
405
408
  } );
406
409
 
@@ -412,13 +415,29 @@ export class ArchiveImporter {
412
415
 
413
416
  }
414
417
 
415
- 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;
416
436
 
417
437
  // A parse slow enough on its own is written now, during the build. Held until the build
418
438
  // ends, it kept every array the build replaces alive with it — the float normals and the
419
439
  // instance matrices, ~1 GB on the whole Moana subset, at the build's peak.
420
- if ( worthStoring( pending.parseMs, encoded.byteLength ) ) this._writeGraph( area, pending );
421
- else this._pendingGraph = pending;
440
+ if ( worthStoring( store.parseMs, encoded.byteLength ) ) this._writeGraph( area, store );
422
441
 
423
442
  }
424
443
 
@@ -429,34 +448,57 @@ export class ArchiveImporter {
429
448
  */
430
449
  flushPendingGraph( buildMs ) {
431
450
 
432
- const pending = this._pendingGraph;
433
- this._pendingGraph = null;
451
+ const store = this._store;
452
+ if ( ! store || store.writing ) return;
434
453
  const area = this.loader.storage?.area( ENGINE_AREAS.SCENES );
435
- if ( ! pending || ! area || ! worthStoring( pending.parseMs + buildMs, pending.encoded.byteLength ) ) return;
436
- 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 );
437
471
 
438
472
  }
439
473
 
440
474
  /** Writes an encoded graph in the background, letting each array go once it is on disk. @private */
441
- _writeGraph( area, pending ) {
475
+ _writeGraph( area, store ) {
442
476
 
477
+ store.writing = true;
443
478
  ( async () => {
444
479
 
445
- const writer = await area.create( pending.key, { label: `${pending.label} (scene)`, expectedBytes: pending.encoded.byteLength } );
446
- if ( ! writer ) return;
480
+ let writer = null;
447
481
  try {
448
482
 
449
- await writeSceneGraph( writer, pending.encoded, { release: true } );
450
- 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();
451
489
 
452
490
  } catch ( error ) {
453
491
 
454
- await writer.abort();
455
- 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;
456
498
 
457
499
  }
458
500
 
459
- } )().catch( error => console.warn( 'Storing the scene failed:', error ) );
501
+ } )();
460
502
 
461
503
  }
462
504
 
@@ -646,10 +688,11 @@ export class ArchiveImporter {
646
688
 
647
689
  const stored = graphKey ? await this._loadStoredGraph( graphKey, source, { imageFromBytes, envFromBytes } ) : null;
648
690
  const built = stored ?? await loadPBRTScene( { vfs: zip, source, entryPath, plyParser, imageFromBytes, envFromBytes, ...shape } );
691
+ this.loader._throwDeferred();
649
692
 
650
693
  const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
651
694
  frames, parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
652
- droppedNoTemplate } = built;
695
+ droppedNoTemplate, parameters } = built;
653
696
  if ( stored ) console.info( `PBRT scene "${loadedEntry}" opened from storage in ${( buildMs / 1000 ).toFixed( 1 )} s, skipping the parse` );
654
697
 
655
698
  // Phase breakdown for scaling work; the engine's own build timings live in
@@ -677,6 +720,8 @@ export class ArchiveImporter {
677
720
 
678
721
  }
679
722
 
723
+ if ( parameters ) this._reportParameters( parameters, loadedEntry );
724
+
680
725
  // Diagnostics — surface what each mesh resolved to (helps debug black/wrong materials).
681
726
  if ( report && report.length && typeof console.table === 'function' ) {
682
727
 
@@ -726,10 +771,42 @@ export class ArchiveImporter {
726
771
 
727
772
  }
728
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
+
729
793
  /** A texture map from an archive, tagged with its path so a stored scene can read it back. @private */
730
794
  async _archiveImage( bytes, fname ) {
731
795
 
732
- const texture = await this._pbrtTextureFromBytes( bytes, fname );
796
+ let texture;
797
+ try {
798
+
799
+ texture = await this._pbrtTextureFromBytes( bytes, fname );
800
+
801
+ } catch ( error ) {
802
+
803
+ // As a glTF's image: its surfaces render untextured, and a strict host hears of it at the end of the load.
804
+ if ( error instanceof EngineIssueError ) throw error;
805
+ this.loader._reportImageFailure( fname, error );
806
+ return null;
807
+
808
+ }
809
+
733
810
  if ( texture ) texture.userData[ ARCHIVE_PATH ] = fname;
734
811
  return texture;
735
812
 
@@ -803,6 +880,7 @@ export class ArchiveImporter {
803
880
  maxTriangles: options.maxTriangles, maxPlacements: options.maxPlacements, mergeShapesAbove: options.mergeShapesAbove,
804
881
  curveSteps: options.curveSteps, curveSides: options.curveSides, curveTolerance: options.curveTolerance,
805
882
  } );
883
+ this.loader._throwDeferred();
806
884
 
807
885
  const { group, environment, warnings, triangleCount, placementCount, skippedForBudget, meshCount, parseMs, buildMs, fitNote } = built;
808
886
  this.lastUSDStats = { parseMs, buildMs, loaderMs: performance.now() - start, triangleCount, placementCount, skippedForBudget, meshCount, fitNote };
@@ -915,9 +993,10 @@ export class ArchiveImporter {
915
993
 
916
994
  } else {
917
995
 
918
- // png / jpg / webp / gif / bmp
919
- const bitmap = await createImageBitmap( new Blob( [ bytes ] ) );
920
- tex = new Texture( bitmap );
996
+ // png / jpg / webp / gif / bmp: the host's decoder where it has one (Node), else the browser's.
997
+ if ( getPlatform().decodeImage ) tex = await platformImageFromBytes( bytes, fname );
998
+ else if ( typeof createImageBitmap === 'function' ) tex = new Texture( await createImageBitmap( new Blob( [ bytes ] ) ) );
999
+ else throw new Error( 'no image decoder in this runtime: configurePlatform( nodePlatform( { decodeImage } ) ) — see "Running in Node"' );
921
1000
  tex.colorSpace = SRGBColorSpace;
922
1001
 
923
1002
  }
@@ -52,6 +52,20 @@ function standInForSplit( source ) {
52
52
 
53
53
  }
54
54
 
55
+ // glTF's znear is a camera's near clipping plane (Blender writes its clip start there): the path tracer honours it.
56
+ export const cameraClipPlugin = {
57
+ name: 'RAYZEE_camera_clip',
58
+ afterRoot( result ) {
59
+
60
+ for ( const scene of result.scenes ) scene.traverse( ( object ) => {
61
+
62
+ if ( object.isCamera && object.near > 0 ) object.userData.clipStart = object.near;
63
+
64
+ } );
65
+
66
+ },
67
+ };
68
+
55
69
  // Luminous efficacy glTF, three.js and Blender's exporter all assume (PBR_WATTS_TO_LUMENS).
56
70
  const LUMENS_PER_WATT = 683;
57
71
 
@@ -406,6 +420,13 @@ export class AssetLoader extends EventDispatcher {
406
420
 
407
421
  }
408
422
 
423
+ /** @see ArchiveImporter#commitPendingGraph */
424
+ commitPendingGraph( model ) {
425
+
426
+ this.archives?.commitPendingGraph( model );
427
+
428
+ }
429
+
409
430
  // Blob's own readers, not FileReader: Node has no FileReader.
410
431
  readFileAsArrayBuffer( file ) {
411
432
 
@@ -768,6 +789,7 @@ export class AssetLoader extends EventDispatcher {
768
789
  if ( getPlatform().decodeImage ) loader.register( ( parser ) => platformImagesPlugin( parser, onImageFailure ) );
769
790
  else if ( ! hasImageDecoder() ) loader.register( ( parser ) => missingImageDecoderPlugin( parser, onImageFailure ) );
770
791
  loader.register( ( parser ) => diffuseTransmissionPlugin( parser ) );
792
+ loader.register( () => cameraClipPlugin );
771
793
 
772
794
  return loader;
773
795
 
@@ -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
 
@@ -32,6 +32,9 @@ function effectiveEmission( light ) {
32
32
 
33
33
  }
34
34
 
35
+ // A point or spot light's radius (Blender's Radius, USD's SphereLight radius): `userData.radius`, in scene units.
36
+ const lightRadius = ( light ) => Math.max( 0, Number( light.userData?.radius ) || 0 );
37
+
35
38
  export class LightSerializer {
36
39
 
37
40
  constructor() {
@@ -216,7 +219,8 @@ export class LightSerializer {
216
219
  em.r, em.g, em.b, // color (3) — incl. temperature tint
217
220
  em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
218
221
  light.distance || 0.0, // cutoff distance (0 = infinite) (1)
219
- light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
222
+ light.decay !== undefined ? light.decay : 2.0, // decay exponent (1)
223
+ lightRadius( light ), // radius: a disc this size facing the lit point, 0 = a point (1)
220
224
  ],
221
225
  importance: importance,
222
226
  light: light
@@ -266,7 +270,7 @@ export class LightSerializer {
266
270
  goboIntensity, // signed gobo strength (1)
267
271
  iesIndex, // IES profile index, -1 = none (1)
268
272
  iesIntensity, // IES blend [0,1] (1)
269
- 0.0, // reserved (1)
273
+ lightRadius( light ), // radius, as a point light's (1)
270
274
  0.0, // reserved (1) — keeps stride at 20 (vec4 aligned)
271
275
  ],
272
276
  importance: importance,
@@ -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