rayzee 9.5.0 → 11.0.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 (78) hide show
  1. package/README.md +22 -20
  2. package/dist/{ArchiveImporter-xuUJiklP.js → ArchiveImporter-BRQvLggb.js} +12 -13
  3. package/dist/{BidirectionalIntegrator-C5PKjJoT.js → BidirectionalIntegrator-9lZQxFVH.js} +87 -87
  4. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  5. package/dist/{ColorManagement-CheBCPaW.js → ColorManagement-oeEk7oSu.js} +171 -171
  6. package/dist/{NeuralRetouch-CW11yLZ_.js → NeuralRetouch-BCXylThH.js} +3 -3
  7. package/dist/{NeuralSuperRes-BekaecYg.js → NeuralSuperRes-sZAeS142.js} +3 -3
  8. package/dist/{PathTracer-Ccl9KOuF.js → PathTracer-CQPW06Mm.js} +2911 -2669
  9. package/dist/{PhysicalSky-Dv2PMFGW.js → PhysicalSky-BvqCw_dy.js} +2 -2
  10. package/dist/{RayzeeRenderer-BQxnzCTD.js → RayzeeRenderer-COpwotpU.js} +338 -329
  11. package/dist/{SolarLimb-ken-JbDK.js → SolarLimb-CPaeZVwz.js} +10 -10
  12. package/dist/{ToneMapGPU-BHi5JzIq.js → ToneMapGPU-CjU3osDd.js} +1 -1
  13. package/dist/{ViewTransforms-D0rTLBsX.js → ViewTransforms-CYN7bq2w.js} +2 -2
  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/rayzee-core.es.js +11 -11
  19. package/dist/rayzee.es.js +734 -599
  20. package/dist/rayzee.umd.js +102 -102
  21. package/dist/sceneCachePolicy-BSSsKY98.js +443 -0
  22. package/package.json +1 -1
  23. package/src/Color/ViewTransforms.js +5 -1
  24. package/src/EngineDefaults.js +59 -1032
  25. package/src/Headless.js +6 -5
  26. package/src/Passes/OIDNTemporalHistory.js +1 -1
  27. package/src/PathTracerApp.js +5 -3
  28. package/src/Processor/AssetLoader.js +6 -5
  29. package/src/Processor/BVHBuilder.js +10 -31
  30. package/src/Processor/BVHLeafFold.js +4 -6
  31. package/src/Processor/BVHRefitter.js +3 -46
  32. package/src/Processor/BufferLayout.js +291 -0
  33. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  34. package/src/Processor/GeometryExtractor.js +3 -3
  35. package/src/Processor/HostMemory.js +7 -1
  36. package/src/Processor/InstanceTable.js +1 -1
  37. package/src/Processor/MaterialPacking.js +2 -1
  38. package/src/Processor/PBRT/PBRTSceneBuilder.js +1 -1
  39. package/src/Processor/QueueManager.js +28 -3
  40. package/src/Processor/SceneProcessor.js +4 -2
  41. package/src/Processor/SplitBLAS.js +1 -1
  42. package/src/Processor/StorageTexturePool.js +22 -1
  43. package/src/Processor/TLASBuilder.js +1 -3
  44. package/src/Processor/TextureBuckets.js +237 -0
  45. package/src/Processor/TextureCreator.js +10 -1
  46. package/src/Processor/Workers/TexturesWorker.js +5 -6
  47. package/src/RayzeeRenderer.js +29 -32
  48. package/src/RenderSettings.js +26 -28
  49. package/src/Stages/ASVGF.js +8 -7
  50. package/src/Stages/AutoExposure.js +14 -5
  51. package/src/Stages/BilateralFilter.js +2 -1
  52. package/src/Stages/DenoiserSettings.js +182 -0
  53. package/src/Stages/EdgeFilter.js +2 -1
  54. package/src/Stages/MotionVector.js +1 -1
  55. package/src/Stages/NRD.js +2 -3
  56. package/src/Stages/NormalDepth.js +3 -2
  57. package/src/Stages/PathTracer.js +72 -4
  58. package/src/Stages/PathTracerStage.js +2 -1
  59. package/src/Stages/Variance.js +1 -1
  60. package/src/TSL/BVHTraversal.js +4 -1
  61. package/src/TSL/Common.js +1 -1
  62. package/src/TSL/EmissiveSampling.js +1 -1
  63. package/src/TSL/LightVisibility.js +111 -0
  64. package/src/TSL/LightsSampling.js +151 -15
  65. package/src/TSL/Random.js +1 -1
  66. package/src/TSL/ShadeKernel.js +88 -3
  67. package/src/TSL/TextureSampling.js +1 -1
  68. package/src/core.js +5 -9
  69. package/src/index.js +10 -21
  70. package/src/integrators/BidirectionalIntegrator.js +2 -1
  71. package/src/managers/CameraManager.js +17 -7
  72. package/src/managers/DenoisingManager.js +14 -10
  73. package/src/managers/EnvironmentManager.js +32 -13
  74. package/src/managers/MaterialDataManager.js +3 -3
  75. package/src/managers/UniformManager.js +2 -2
  76. package/dist/EngineDefaults-DH3bI5t-.js +0 -844
  77. package/dist/sceneCachePolicy-BmwYOYDu.js +0 -183
  78. /package/dist/{Logger-BnHghprG.js → Logger-dDDGj9Av.js} +0 -0
package/src/Headless.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The supported way to render without a person watching. Defaults are the batch renderer's —
3
- * strict, physical profile, deterministic — all reversible, none reversible by accident.
3
+ * strict, deterministic — all reversible, none reversible by accident.
4
4
  *
5
5
  * @example
6
6
  * const shot = await renderHeadless( { model: url, width: 1920, height: 1080, samples: 256 } );
@@ -20,11 +20,10 @@ import { PathTracerApp } from './PathTracerApp.js';
20
20
  * @param {number} [options.samples=64] - samples to accumulate
21
21
  * @param {'linear'|'srgb'} [options.colorSpace='srgb']
22
22
  * @param {boolean} [options.strict=true]
23
- * @param {string} [options.profile='physical'] - see RENDER_PROFILES
24
23
  * @param {boolean} [options.deterministic=true]
25
24
  * @param {boolean} [options.allowEarlyRetire=false] - needs `deterministic: false` to be reachable
26
25
  * @param {boolean} [options.denoise=false] - one final OIDN pass, read back instead of the raw accumulation
27
- * @param {Object} [options.settings] - applied after the model loads
26
+ * @param {Object} [options.settings] - applied before the model loads and again after it
28
27
  * @param {function(number): void} [options.onProgress] - running sample count
29
28
  * @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number,
30
29
  * colorSpace: string, source: string, samples: number, retiredBy: string, issues: Object[], adapter: Object,
@@ -95,7 +94,6 @@ export async function openHeadless( {
95
94
  width = 1920,
96
95
  height = 1080,
97
96
  strict = true,
98
- profile = 'physical',
99
97
  deterministic = true,
100
98
  settings = null,
101
99
  storage = false,
@@ -103,7 +101,7 @@ export async function openHeadless( {
103
101
  } = {} ) {
104
102
 
105
103
  // Off unless asked for: cached state from an earlier run must not change what a batch renders.
106
- const app = new PathTracerApp( canvas ?? null, { autoResize: false, strict, profile, storage, hostMemoryGB } );
104
+ const app = new PathTracerApp( canvas ?? null, { autoResize: false, strict, storage, hostMemoryGB } );
107
105
 
108
106
  try {
109
107
 
@@ -114,7 +112,10 @@ export async function openHeadless( {
114
112
  await app.init();
115
113
  app.setCanvasSize( width, height );
116
114
 
115
+ // Before the load as well: some are read while a model loads (areaLightIntensityScale).
116
+ if ( settings ) app.settings.setMany( settings, { silent: true } );
117
117
  if ( model ) await app.loadModel( model );
118
+ // Again after it, over anything the model's metadata set.
118
119
  if ( settings ) app.settings.setMany( settings, { silent: true } );
119
120
 
120
121
  // After the model: scene metadata can turn adaptive sampling back on.
@@ -1,4 +1,4 @@
1
- import { GBUFFER_MISS_THRESHOLD } from '../EngineDefaults.js';
1
+ import { GBUFFER_MISS_THRESHOLD } from '../Stages/DenoiserSettings.js';
2
2
 
3
3
  /**
4
4
  * History of the frames the path tracer restarted while the view moved, reprojected into the
@@ -7,8 +7,8 @@ import { NRD } from './Stages/NRD.js';
7
7
  import { Variance } from './Stages/Variance.js';
8
8
  import { BilateralFilter } from './Stages/BilateralFilter.js';
9
9
  import { EdgeFilter } from './Stages/EdgeFilter.js';
10
- import { AutoExposure } from './Stages/AutoExposure.js';
11
- import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings } from './EngineDefaults.js';
10
+ import { AutoExposure, AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
11
+ import { PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings } from './EngineDefaults.js';
12
12
  import { createLogger } from './utils/Logger.js';
13
13
  import { EngineEvents } from './EngineEvents.js';
14
14
  import { ISSUE_CODES } from './EngineIssues.js';
@@ -71,7 +71,9 @@ export class PathTracerApp extends RayzeeRenderer {
71
71
  super( canvas, options );
72
72
  this.setColorManagement( ColorManagement );
73
73
  this.setStorageOpener( acquireSharedStorage );
74
+ // Per-axis render scale while the camera moves (0.5 = a quarter of the pixels); 1 turns it off.
74
75
  this.settings.define( 'interactionRenderScale', {
76
+ default: 0.5,
75
77
  apply: () => {
76
78
 
77
79
  if ( this.stages?.pathTracer?.interactionMode ) this._requestRenderScale( this._interactionRenderScale() );
@@ -153,7 +155,7 @@ export class PathTracerApp extends RayzeeRenderer {
153
155
  stages.variance = new Variance( renderer, { enabled: false } );
154
156
  stages.bilateralFilter = new BilateralFilter( renderer, { enabled: false } );
155
157
  stages.edgeFilter = new EdgeFilter( renderer, { enabled: false } );
156
- stages.autoExposure = new AutoExposure( renderer, { enabled: DEFAULT_STATE.autoExposure ?? false } );
158
+ stages.autoExposure = new AutoExposure( renderer, { enabled: AUTO_EXPOSURE_DEFAULTS.autoExposure } );
157
159
 
158
160
  return [
159
161
  stages.normalDepth, stages.motionVector, stages.nrd, stages.asvgf,
@@ -17,7 +17,7 @@ import { diffuseTransmissionPlugin } from './GLTFDiffuseTransmission.js';
17
17
  import { decodersOnDemand } from './GLTFDecoders.js';
18
18
  import { extractSceneMetadata } from './SceneMetadata.js';
19
19
  import { ISSUE_CODES } from '../EngineIssues.js';
20
- import { getRenderProfile } from '../EngineDefaults.js';
20
+ import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
21
21
 
22
22
  // Define supported file formats
23
23
  // What the core reads itself; every other format is registered (registerFormat) or comes with an add-on.
@@ -70,7 +70,7 @@ function areaLightPowerFactor( node, width, height, userData ) {
70
70
  */
71
71
  export class AssetLoader extends EventDispatcher {
72
72
 
73
- constructor( scene, camera, controls, { issues = null, profile = null } = {} ) {
73
+ constructor( scene, camera, controls, { issues = null, areaLightIntensityScale = null } = {} ) {
74
74
 
75
75
  super();
76
76
  this.scene = scene;
@@ -109,7 +109,7 @@ export class AssetLoader extends EventDispatcher {
109
109
  this._formats = new Map();
110
110
 
111
111
  this._issues = issues;
112
- this._profile = profile ?? getRenderProfile();
112
+ this._areaLightIntensityScale = areaLightIntensityScale ?? ( () => ENGINE_DEFAULTS.areaLightIntensityScale );
113
113
 
114
114
  // A glTF whose external texture 404s still loads. Only place the engine sees the URL.
115
115
  // ZIP paths build their own managers and are not covered.
@@ -187,7 +187,8 @@ export class AssetLoader extends EventDispatcher {
187
187
 
188
188
  _keyed( kind, id ) {
189
189
 
190
- return id ? `${kind}:${this._profile?.name ?? ''}:${id}` : null;
190
+ // The empty field held a profile name that was never set; kept so stored caches and sessions still match.
191
+ return id ? `${kind}::${id}` : null;
191
192
 
192
193
  }
193
194
 
@@ -1175,7 +1176,7 @@ export class AssetLoader extends EventDispatcher {
1175
1176
  const power = userData.intensity * areaLightPowerFactor( object, userData.width, userData.height, userData );
1176
1177
  const light = new RectAreaLight(
1177
1178
  new Color( ...userData.color ),
1178
- power * this._profile.areaLightIntensityScale,
1179
+ power * this._areaLightIntensityScale(),
1179
1180
  userData.width,
1180
1181
  userData.height
1181
1182
  );
@@ -1,25 +1,14 @@
1
1
  import { ReinsertionOptimizer } from './ReinsertionOptimizer.js';
2
- // Logger is worker-safe (globalThis only, storage access guarded), unlike Constants.js below.
2
+ // Logger is worker-safe (globalThis only, storage access guarded).
3
3
  import { createLogger, fmt } from '../utils/Logger.js';
4
4
  import { foldLeaves } from './BVHLeafFold.js';
5
5
  import { hasWorkers } from '../Platform.js';
6
+ import { BVH_LEAF_MARKERS, bvhIndexView, assertBVHIndexFits, TRIANGLE_DATA_LAYOUT } from './BufferLayout.js';
6
7
 
7
8
  const log = createLogger( 'bvh' );
8
9
 
9
- // Inline copies of EngineDefaults.BVH_LEAF_MARKERS / bvhIndexView — this module also runs inside
10
- // a worker, where importing Constants is not safe. Keep in step with EngineDefaults.js.
11
- // Index fields are u32 BIT PATTERNS, never float values: as floats they round past 2^24.
12
- const TRIANGLE_LEAF = 0x40000000;
13
- const FRONTIER_LEAF = 0x40000002;
14
- const indexView = f32 => new Uint32Array( f32.buffer, f32.byteOffset, f32.length );
15
- const BVH_MAX_INDEX = 0x40000000;
16
-
17
- function assertFits( count, what ) {
18
-
19
- if ( count >= BVH_MAX_INDEX ) throw new RangeError( `${what} is ${count}, at or past the BVH index limit of ${BVH_MAX_INDEX}` );
20
- return count;
21
-
22
- }
10
+ const TRIANGLE_LEAF = BVH_LEAF_MARKERS.TRIANGLE_LEAF;
11
+ const FRONTIER_LEAF = BVH_LEAF_MARKERS.FRONTIER;
23
12
 
24
13
  // Injected, not imported: this module also runs inside the worker, and `?worker&inline`
25
14
  // would embed a second copy of BVHWorker's source there.
@@ -31,16 +20,6 @@ export function setBVHWorkerFactory( factory ) {
31
20
 
32
21
  }
33
22
 
34
- // Inline copy of TRIANGLE_DATA_LAYOUT (mirrors Constants.js).
35
- // Cannot import Constants.js because BVHBuilder runs inside BVHWorker
36
- // where `window` (used elsewhere in Constants.js) is not defined.
37
- const TRIANGLE_DATA_LAYOUT = {
38
- FLOATS_PER_TRIANGLE: 20,
39
- POSITION_A_OFFSET: 0,
40
- POSITION_B_OFFSET: 4,
41
- POSITION_C_OFFSET: 8
42
- };
43
-
44
23
  const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
45
24
 
46
25
  // The record buffer is uint; positions are f32 in it. Views share the memory, no copy.
@@ -1507,9 +1486,9 @@ export class BVHBuilder {
1507
1486
  // Inner: [leftMin.xyz, leftChild] [leftMax.xyz, rightChild] [rightMin.xyz, 0] [rightMax.xyz, 0]
1508
1487
  // Leaf: [triOffset, triCount, 0, -1] [0,0,0,0] [0,0,0,0] [0,0,0,0]
1509
1488
  const FLOATS_PER_NODE = 16;
1510
- assertFits( nodes.length, 'BLAS node count' );
1489
+ assertBVHIndexFits( nodes.length, 'BLAS node count' );
1511
1490
  const data = new Float32Array( nodes.length * FLOATS_PER_NODE );
1512
- const idx = indexView( data );
1491
+ const idx = bvhIndexView( data );
1513
1492
 
1514
1493
  for ( let i = 0; i < nodes.length; i ++ ) {
1515
1494
 
@@ -1581,10 +1560,10 @@ export class BVHBuilder {
1581
1560
  }
1582
1561
 
1583
1562
  // Second pass: write flat data
1584
- assertFits( nodes.length, 'BLAS node count' );
1563
+ assertBVHIndexFits( nodes.length, 'BLAS node count' );
1585
1564
  const data = new Float32Array( nodes.length * FLOATS_PER_NODE );
1586
1565
  const frontierMap = [];
1587
- const idx = indexView( data );
1566
+ const idx = bvhIndexView( data );
1588
1567
 
1589
1568
  for ( let i = 0; i < nodes.length; i ++ ) {
1590
1569
 
@@ -1663,9 +1642,9 @@ export class BVHBuilder {
1663
1642
  }
1664
1643
 
1665
1644
  // Allocate final array
1666
- assertFits( totalNodes, 'assembled BLAS node count' );
1645
+ assertBVHIndexFits( totalNodes, 'assembled BLAS node count' );
1667
1646
  const finalData = new Float32Array( totalNodes * FLOATS_PER_NODE );
1668
- const finalIdx = indexView( finalData );
1647
+ const finalIdx = bvhIndexView( finalData );
1669
1648
 
1670
1649
  // Copy top-level data
1671
1650
  finalData.set( topFlatData );
@@ -1,12 +1,10 @@
1
- // Inline copies of EngineDefaults' BVH_LEAF_MARKERS.TRIANGLE_LEAF, BVH_MAX_INDEX and
2
- // BVH_FOLDED_LEAF_MAX: this runs in the BVH workers, which cannot import it. Keep in step.
3
- const TRIANGLE_LEAF = 0x40000000;
4
- const BVH_MAX_INDEX = 0x40000000;
5
- const BVH_FOLDED_LEAF_MAX = 15;
1
+ import { BVH_LEAF_MARKERS, BVH_MAX_INDEX, BVH_FOLDED_LEAF_MAX } from './BufferLayout.js';
2
+
3
+ const TRIANGLE_LEAF = BVH_LEAF_MARKERS.TRIANGLE_LEAF;
6
4
  const FLOATS_PER_NODE = 16;
7
5
  const FOLDED = 0x80000000;
8
6
 
9
- /** A folded leaf as its parent stores it (EngineDefaults: BVH_FOLDED_LEAF_MAX). */
7
+ /** A folded leaf as its parent stores it (see BVH_FOLDED_LEAF_MAX). */
10
8
  export const foldedRef = ( first, count ) => ( ~ ( ( first << 4 ) | count ) ) >>> 0;
11
9
  export const isFoldedRef = ref => ref >= FOLDED;
12
10
  export const foldedFirst = ref => ( ( ~ ref ) >>> 0 ) >>> 4;
@@ -10,18 +10,7 @@
10
10
  */
11
11
 
12
12
  import { isFoldedRef, foldedFirst, foldedCount } from './BVHLeafFold.js';
13
-
14
- // Inline copy of layout constants (source of truth: EngineDefaults.js).
15
- // Cannot import because this runs inside Web Workers where window is not defined.
16
- const TRIANGLE_DATA_LAYOUT = {
17
- FLOATS_PER_TRIANGLE: 20,
18
- POSITION_A_OFFSET: 0,
19
- POSITION_B_OFFSET: 4,
20
- POSITION_C_OFFSET: 8,
21
- NORMAL_A_PACKED_OFFSET: 3,
22
- NORMAL_B_PACKED_OFFSET: 7,
23
- NORMAL_C_PACKED_OFFSET: 11,
24
- };
13
+ import { TRIANGLE_DATA_LAYOUT, packNormalOct, BVH_LEAF_MARKERS } from './BufferLayout.js';
25
14
 
26
15
  const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
27
16
 
@@ -62,41 +51,9 @@ const triAccess = ( triangleData ) => {
62
51
 
63
52
  };
64
53
 
65
- // Octahedral snorm16 pair, matching packNormalOct in EngineDefaults (not importable here).
66
- function packNormalOct( x, y, z ) {
67
-
68
- const len = Math.sqrt( x * x + y * y + z * z );
69
- if ( len > 0 ) {
70
-
71
- x /= len; y /= len; z /= len;
72
-
73
- } else {
74
-
75
- x = 0; y = 0; z = 1;
76
-
77
- }
78
-
79
- const sum = Math.abs( x ) + Math.abs( y ) + Math.abs( z );
80
- let u = x / sum, v = y / sum;
81
- if ( z < 0 ) {
82
-
83
- const au = u, av = v;
84
- u = ( 1 - Math.abs( av ) ) * ( au >= 0 ? 1 : - 1 );
85
- v = ( 1 - Math.abs( au ) ) * ( av >= 0 ? 1 : - 1 );
86
-
87
- }
88
-
89
- const qu = Math.round( Math.min( 1, Math.max( - 1, u ) ) * 32767 ) & 0xffff;
90
- const qv = Math.round( Math.min( 1, Math.max( - 1, v ) ) * 32767 ) & 0xffff;
91
- return ( ( qv << 16 ) | qu ) >>> 0;
92
-
93
- }
94
-
95
54
  const FLOATS_PER_NODE = 16; // 4 vec4s per BVH node
96
- // Inline copies of EngineDefaults.BVH_LEAF_MARKERS — this module also runs inside a worker.
97
- // Index fields are u32 BIT PATTERNS (exact past 2^24), read through `indexView`.
98
- const LEAF_MARKER = 0x40000000;
99
- const BLAS_POINTER_MARKER = 0x40000001;
55
+ const LEAF_MARKER = BVH_LEAF_MARKERS.TRIANGLE_LEAF;
56
+ const BLAS_POINTER_MARKER = BVH_LEAF_MARKERS.BLAS_POINTER_LEAF;
100
57
 
101
58
  // Relative to the matrix's own magnitude, never an absolute floor — see transformBoundsToWorld.
102
59
  const SINGULAR_REL_EPS = 1e-12;
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Triangle record: 5 uvec4 lanes (80 bytes). The buffer is declared `uvec4` so packed lanes
3
+ * keep their exact bit pattern — a packSnorm2x16 result can land in the f32 NaN range, and an
4
+ * f32 lane may canonicalise it (the same reason the G-buffer is uvec4). Positions and UVs are
5
+ * written as f32 through a Float32Array view of the same memory and read back with
6
+ * `uintBitsToFloat`, so they carry full precision; only normals are compressed.
7
+ *
8
+ * Was 32 floats (128 B) with 4 dead padding lanes. At 128 B the 2 GB V8 ArrayBuffer cap put a
9
+ * hard ceiling of 16.7M triangles on the scene — 80 B lifts that to 26.8M and cuts geometry
10
+ * VRAM by the same 37.5%. Each normal now rides in its position's spare .w lane, so the three
11
+ * vec4 loads the intersection test already does carry the normals with them.
12
+ */
13
+ export const TRIANGLE_DATA_LAYOUT = {
14
+ FLOATS_PER_TRIANGLE: 20,
15
+
16
+ POSITION_A_OFFSET: 0, // f32 xyz + packed normal A
17
+ POSITION_B_OFFSET: 4,
18
+ POSITION_C_OFFSET: 8,
19
+
20
+ NORMAL_A_PACKED_OFFSET: 3, // oct16 in each position's .w lane
21
+ NORMAL_B_PACKED_OFFSET: 7,
22
+ NORMAL_C_PACKED_OFFSET: 11,
23
+
24
+ UV_AB_OFFSET: 12, // f32 uvA.xy, uvB.xy
25
+ UV_C_OFFSET: 16, // f32 uvC.xy
26
+ MATERIAL_FLAGS_OFFSET: 18, // materialIndex | side << 24 | shadowBlockerBits << 26 (two bits)
27
+ MESH_INDEX_OFFSET: 19
28
+ };
29
+
30
+ export const TRI_MATERIAL_MASK = 0xffffff;
31
+ export const TRI_SIDE_SHIFT = 24; // 0 front, 1 back, 2 double
32
+ export const TRI_BLOCKER_SHIFT = 26; // 1 = blocks shadow rays whatever the settings
33
+ export const TRI_BLOCKER_ALPHA_SHIFT = 27; // 1 = blocks them unless alpha-cutout shadows are on
34
+
35
+ /**
36
+ * How a shadow ray settles on this material without fetching it, mirroring traceShadowRay:
37
+ * bit 0 set = always a blocker; bit 1 set = a blocker while alpha-cutout shadows are off
38
+ * (MASK/BLEND with nothing else letting light through); 0 = light may pass, so the shadow
39
+ * traversal has to find the nearest such surface.
40
+ */
41
+ export function shadowBlockerBits( material ) {
42
+
43
+ if ( ! material ) return 0;
44
+ const solid = ( material.transmission || 0 ) === 0
45
+ && ( ( material.transparent | 0 ) === 0 || ( material.opacity ?? 1 ) >= 1 );
46
+ if ( ! solid ) return 0;
47
+ return ( material.alphaMode | 0 ) === 0 ? 1 : 2;
48
+
49
+ }
50
+
51
+ /**
52
+ * Material index plus the per-triangle flags the shader reads without touching the
53
+ * material buffer: `side` for inline culling and the two shadow-blocker bits.
54
+ */
55
+ export function packTriangleFlags( materialIndex, material ) {
56
+
57
+ return ( ( materialIndex & TRI_MATERIAL_MASK )
58
+ | ( ( material?.side ?? 0 ) << TRI_SIDE_SHIFT )
59
+ | ( shadowBlockerBits( material ) << TRI_BLOCKER_SHIFT ) ) >>> 0;
60
+
61
+ }
62
+
63
+ /**
64
+ * Octahedral-encode a unit normal into one u32 (two snorm16). Worst-case error is ~0.03°,
65
+ * well under what normal maps and barycentric interpolation already contribute.
66
+ * A degenerate (zero-length) normal encodes as +Z rather than NaN.
67
+ */
68
+ export function packNormalOct( x, y, z ) {
69
+
70
+ const len = Math.sqrt( x * x + y * y + z * z );
71
+ if ( len > 0 ) {
72
+
73
+ x /= len; y /= len; z /= len;
74
+
75
+ } else {
76
+
77
+ x = 0; y = 0; z = 1;
78
+
79
+ }
80
+
81
+ const sum = Math.abs( x ) + Math.abs( y ) + Math.abs( z );
82
+ let u = x / sum, v = y / sum;
83
+ if ( z < 0 ) {
84
+
85
+ const au = u, av = v;
86
+ u = ( 1 - Math.abs( av ) ) * ( au >= 0 ? 1 : - 1 );
87
+ v = ( 1 - Math.abs( au ) ) * ( av >= 0 ? 1 : - 1 );
88
+
89
+ }
90
+
91
+ const qu = Math.round( Math.min( 1, Math.max( - 1, u ) ) * 32767 ) & 0xffff;
92
+ const qv = Math.round( Math.min( 1, Math.max( - 1, v ) ) * 32767 ) & 0xffff;
93
+ return ( ( qv << 16 ) | qu ) >>> 0;
94
+
95
+ }
96
+
97
+ /** Inverse of packNormalOct; writes into `out` (length >= 3) and returns it. */
98
+ export function unpackNormalOct( packed, out ) {
99
+
100
+ const u = ( ( packed << 16 ) >> 16 ) / 32767;
101
+ const v = ( packed >> 16 ) / 32767;
102
+ let x = u, y = v, z = 1 - Math.abs( u ) - Math.abs( v );
103
+ if ( z < 0 ) {
104
+
105
+ const ax = x, ay = y;
106
+ x = ( 1 - Math.abs( ay ) ) * ( ax >= 0 ? 1 : - 1 );
107
+ y = ( 1 - Math.abs( ax ) ) * ( ay >= 0 ? 1 : - 1 );
108
+
109
+ }
110
+
111
+ const len = Math.sqrt( x * x + y * y + z * z ) || 1;
112
+ out[ 0 ] = x / len; out[ 1 ] = y / len; out[ 2 ] = z / len;
113
+ return out;
114
+
115
+ }
116
+
117
+ // Material data layout constants — single source of truth for material buffer offsets.
118
+ // Shared between CPU writers (TextureCreator, MaterialDataManager) and GPU readers (Common.js getMaterial).
119
+ export const MATERIAL_DATA_LAYOUT = {
120
+
121
+ SLOTS_PER_MATERIAL: 34, // vec4 slots per material
122
+ FLOATS_PER_MATERIAL: 136, // total floats per material (34 × 4)
123
+
124
+ // ── Flat float offsets (CPU side) ────────────────────────────────
125
+ // Used as: data[ materialIndex * FLOATS_PER_MATERIAL + offset ]
126
+ // Ordered for cache-line coherence: shadow/culling → BxDF core → maps → extended → transforms
127
+
128
+ // Slot 0: ior + transmission + thickness + emissiveIntensity [shadow]
129
+ IOR: 0, TRANSMISSION: 1, THICKNESS: 2, EMISSIVE_INTENSITY: 3,
130
+ // Slot 1: attenuationColor.rgb + attenuationDistance [shadow]
131
+ ATTENUATION_COLOR: 4, ATTENUATION_DISTANCE: 7,
132
+ // Slot 2: opacity + side + transparent + alphaTest [shadow + culling]
133
+ OPACITY: 8, SIDE: 9, TRANSPARENT: 10, ALPHA_TEST: 11,
134
+ // Slot 3: alphaMode + depthWrite + normalScale [shadow]
135
+ ALPHA_MODE: 12, DEPTH_WRITE: 13, NORMAL_SCALE: 14,
136
+ // Slot 4: color.rgb + metalness [BxDF core]
137
+ COLOR: 16, METALNESS: 19,
138
+ // Slot 5: emissive.rgb + roughness [BxDF core]
139
+ EMISSIVE: 20, ROUGHNESS: 23,
140
+ // Slot 6: map indices (albedo, normal, roughness, metalness) [maps]
141
+ ALBEDO_MAP_INDEX: 24, NORMAL_MAP_INDEX: 25, ROUGHNESS_MAP_INDEX: 26, METALNESS_MAP_INDEX: 27,
142
+ // Slot 7: map indices (emissive, bump) + clearcoat [maps]
143
+ EMISSIVE_MAP_INDEX: 28, BUMP_MAP_INDEX: 29, CLEARCOAT: 30, CLEARCOAT_ROUGHNESS: 31,
144
+ // Slot 8: dispersion + visible + sheen + sheenRoughness [extended BxDF]
145
+ DISPERSION: 32, VISIBLE: 33, SHEEN: 34, SHEEN_ROUGHNESS: 35,
146
+ // Slot 9: sheenColor.rgb + diffuseTransmission [extended BxDF]
147
+ SHEEN_COLOR: 36, DIFFUSE_TRANSMISSION: 39,
148
+ // Slot 10: specularIntensity + specularColor.rgb [extended BxDF]
149
+ SPECULAR_INTENSITY: 40, SPECULAR_COLOR: 41,
150
+ // Slot 11: iridescence + iridescenceIOR + iridescenceThicknessRange [extended BxDF]
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,
154
+
155
+ // ── Transform float offsets (8 floats each: 7 matrix values + 1 padding) ──
156
+ ALBEDO_TRANSFORM: 52,
157
+ NORMAL_TRANSFORM: 60,
158
+ ROUGHNESS_TRANSFORM: 68,
159
+ METALNESS_TRANSFORM: 76,
160
+ EMISSIVE_TRANSFORM: 84,
161
+ BUMP_TRANSFORM: 92,
162
+ DISPLACEMENT_TRANSFORM: 100,
163
+
164
+ // ── Subsurface scattering (3 slots appended after transforms) ────
165
+ // Slot 27: subsurfaceColor.rgb (scatter albedo) + subsurface weight
166
+ SUBSURFACE_COLOR: 108, SUBSURFACE: 111,
167
+ // Slot 28: subsurfaceRadius.rgb (mean free path) + radius scale
168
+ SUBSURFACE_RADIUS: 112, SUBSURFACE_RADIUS_SCALE: 115,
169
+ // Slot 29: subsurfaceAnisotropy g (116) + surface anisotropy (strength 117, rotation 118, map index 119)
170
+ SUBSURFACE_ANISOTROPY: 116, ANISOTROPY: 117, ANISOTROPY_ROTATION: 118, ANISOTROPY_MAP_INDEX: 119,
171
+ // Slot 30: extension-texture map indices A (transmission, clearcoat, clearcoatRoughness, sheenColor)
172
+ TRANSMISSION_MAP_INDEX: 120, CLEARCOAT_MAP_INDEX: 121, CLEARCOAT_ROUGHNESS_MAP_INDEX: 122, SHEEN_COLOR_MAP_INDEX: 123,
173
+ // Slot 31: extension-texture map indices B (sheenRoughness, iridescence, iridescenceThickness, specularIntensity)
174
+ SHEEN_ROUGHNESS_MAP_INDEX: 124, IRIDESCENCE_MAP_INDEX: 125, IRIDESCENCE_THICKNESS_MAP_INDEX: 126, SPECULAR_INTENSITY_MAP_INDEX: 127,
175
+ // Slot 32: extension-texture map index C (specularColor) + diffuseTransmissionColor.rgb
176
+ SPECULAR_COLOR_MAP_INDEX: 128, DIFFUSE_TRANSMISSION_COLOR: 129,
177
+ // Slot 33: extension-texture map indices D (diffuseTransmission, diffuseTransmissionColor) + 2 reserved;
178
+ // read only for a material with diffuse transmission
179
+ DIFFUSE_TRANSMISSION_MAP_INDEX: 132, DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX: 133,
180
+
181
+ // ── Vec4 slot indices (GPU/TSL side) ─────────────────────────────
182
+ // Used with getDatafromStorageBuffer( buf, matIdx, int(slot), int(SLOTS_PER_MATERIAL) )
183
+ SLOT: {
184
+ IOR_TRANSMISSION: 0, // [shadow] ior, transmission, thickness, emissiveIntensity
185
+ ATTENUATION: 1, // [shadow] attenuationColor, attenuationDistance
186
+ OPACITY_ALPHA: 2, // [shadow+culling] opacity, side, transparent, alphaTest
187
+ ALPHA_MODE: 3, // [shadow] alphaMode, depthWrite, normalScale
188
+ COLOR_METALNESS: 4, // [BxDF] color.rgb, metalness
189
+ EMISSIVE_ROUGHNESS: 5, // [BxDF] emissive.rgb, roughness
190
+ MAP_INDICES_A: 6, // [maps] albedo, normal, roughness, metalness
191
+ MAP_INDICES_B: 7, // [maps] emissive, bump, clearcoat, clearcoatRoughness
192
+ DISPERSION_SHEEN: 8, // [extended] dispersion, visible, sheen, sheenRoughness
193
+ SHEEN_COLOR: 9, // [extended] sheenColor, diffuseTransmission
194
+ SPECULAR: 10, // [extended] specularIntensity, specularColor
195
+ IRIDESCENCE: 11, // [extended] iridescence, iridescenceIOR, iridescenceThicknessRange
196
+ BUMP_DISPLACEMENT: 12, // bumpScale, displacementScale, displacementMapIndex
197
+ ALBEDO_TRANSFORM_A: 13, ALBEDO_TRANSFORM_B: 14,
198
+ NORMAL_TRANSFORM_A: 15, NORMAL_TRANSFORM_B: 16,
199
+ ROUGHNESS_TRANSFORM_A: 17, ROUGHNESS_TRANSFORM_B: 18,
200
+ METALNESS_TRANSFORM_A: 19, METALNESS_TRANSFORM_B: 20,
201
+ EMISSIVE_TRANSFORM_A: 21, EMISSIVE_TRANSFORM_B: 22,
202
+ BUMP_TRANSFORM_A: 23, BUMP_TRANSFORM_B: 24,
203
+ DISPLACEMENT_TRANSFORM_A: 25, DISPLACEMENT_TRANSFORM_B: 26,
204
+ SUBSURFACE_A: 27, // subsurfaceColor.rgb, subsurface weight
205
+ SUBSURFACE_B: 28, // subsurfaceRadius.rgb, subsurfaceRadiusScale
206
+ SUBSURFACE_C: 29, // subsurfaceAnisotropy g, anisotropy, anisotropyRotation, anisotropyMapIndex
207
+ EXT_MAP_INDICES_A: 30, // transmission, clearcoat, clearcoatRoughness, sheenColor map indices
208
+ EXT_MAP_INDICES_B: 31, // sheenRoughness, iridescence, iridescenceThickness, specularIntensity map indices
209
+ EXT_MAP_INDICES_C: 32, // specularColor map index, diffuseTransmissionColor
210
+ EXT_MAP_INDICES_D: 33, // diffuseTransmission, diffuseTransmissionColor map indices + 2 reserved
211
+ },
212
+
213
+ };
214
+
215
+ // glTF/three.js spell "no volume absorption" as attenuationDistance = Infinity, while the GPU
216
+ // contract is `> 0 = on` (calculateBeerLawAbsorption). Every writer into ATTENUATION_DISTANCE
217
+ // collapses both spellings to 0 so no shader divides by Inf.
218
+ export const normalizeAttenuationDistance = d => ( Number.isFinite( d ) && d > 0 ? d : 0 );
219
+
220
+ /**
221
+ * Node tags, written into slot [3] of a BVH node as a raw u32 bit pattern.
222
+ *
223
+ * Node indices, triangle offsets and counts are integers living inside a Float32Array. Written as
224
+ * float *values* they round silently past 2^24 (16,777,216): in a 24M-node scene half of every
225
+ * BLAS pointer landed on a neighbouring node and that geometry vanished from the render with no
226
+ * error at all. Every one of those fields is written as a u32 bit pattern instead, exact to 2^30.
227
+ *
228
+ * Tags sit above {@link BVH_MAX_INDEX} so a single unsigned compare separates a leaf from an inner
229
+ * node's left-child index. All three are ordinary finite floats — nothing lands in the NaN range,
230
+ * which an f32 storage buffer is free to canonicalise.
231
+ */
232
+ export const BVH_MAX_INDEX = 0x40000000; // 2^30
233
+
234
+ /**
235
+ * Slot [1] of a BLAS-pointer leaf holds its placement index, which {@link BVH_MAX_INDEX} keeps
236
+ * below 2^30. Bit 30 is therefore free to say the leaf's matrix is identity: geometry no other
237
+ * placement shares is baked to world space at extraction, and a ray reaching it needs no
238
+ * transform at all. Mask the bit off before using the slot as an index.
239
+ */
240
+ export const TLAS_LEAF_IDENTITY = 0x40000000;
241
+ export const TLAS_PLACEMENT_MASK = 0x3fffffff;
242
+
243
+ export const BVH_LEAF_MARKERS = {
244
+ TRIANGLE_LEAF: 0x40000000, // leaf containing triangle references
245
+ BLAS_POINTER_LEAF: 0x40000001, // TLAS leaf pointing to a BLAS root node
246
+ FRONTIER: 0x40000002, // parallel-build placeholder, overwritten during assembly
247
+ };
248
+
249
+ /**
250
+ * A triangle leaf of at most this many triangles is folded into its parent: the child's slot ([3]
251
+ * or [7]) holds `~( first << 4 | count )`, the very value traversal pushes, so an inner node is
252
+ * read exactly as before and only popping a negative entry differs. Node indices stay below
253
+ * {@link BVH_MAX_INDEX}, leaf tags between it and 2^31, folded leaves from 2^31 up — so the count
254
+ * has four bits and the first triangle must stay below {@link BVH_FOLDED_FIRST_LIMIT}.
255
+ */
256
+ export const BVH_FOLDED_LEAF_MAX = 15;
257
+ export const BVH_FOLDED_FIRST_LIMIT = 1 << 27;
258
+
259
+ /** A u32 view over a float buffer, for writing index fields as exact bit patterns. */
260
+ export function bvhIndexView( f32 ) {
261
+
262
+ return new Uint32Array( f32.buffer, f32.byteOffset, f32.length );
263
+
264
+ }
265
+
266
+ /**
267
+ * Refuse to build a BVH whose indices would collide with the leaf tags.
268
+ *
269
+ * Throws rather than degrades: the failure this replaces was a scene that rendered with half its
270
+ * geometry silently missing, which is far worse than a scene that refuses to load. Guarding the
271
+ * totals covers every individual write, since no index can exceed the count it indexes into.
272
+ *
273
+ * @param {number} count - node or triangle total about to be indexed
274
+ * @param {string} what - what the count is, for the message
275
+ * @throws {RangeError}
276
+ */
277
+ export function assertBVHIndexFits( count, what ) {
278
+
279
+ if ( count >= BVH_MAX_INDEX ) {
280
+
281
+ throw new RangeError(
282
+ `${what} is ${count.toLocaleString()}, at or past the BVH index limit of ` +
283
+ `${BVH_MAX_INDEX.toLocaleString()}. Node indices are stored as u32 bit patterns and the ` +
284
+ 'leaf tags occupy everything above that.'
285
+ );
286
+
287
+ }
288
+
289
+ return count;
290
+
291
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { DataTexture, RGBAFormat, FloatType, NearestFilter } from 'three';
10
- import { TRIANGLE_DATA_LAYOUT, TRI_MATERIAL_MASK } from '../EngineDefaults.js';
10
+ import { TRIANGLE_DATA_LAYOUT, TRI_MATERIAL_MASK } from './BufferLayout.js';
11
11
  import { LightBVHBuilder } from './LightBVHBuilder.js';
12
12
  import { createLogger, fmt } from '../utils/Logger.js';
13
13
  import { convertLinearTriple, getWorkingMatrix } from '../Color/WorkingMatrix.js';
@@ -1,7 +1,7 @@
1
1
  import { BufferAttribute, Vector3, Vector2, Color, Matrix3, Matrix4, FrontSide, BackSide, DoubleSide, RGBAFormat } from "three";
2
- import {
3
- TEXTURE_CONSTANTS, TRIANGLE_DATA_LAYOUT, MATERIAL_DEFAULTS, packNormalOct, packTriangleFlags
4
- } from '../EngineDefaults.js';
2
+ import { MATERIAL_DEFAULTS } from '../EngineDefaults.js';
3
+ import { TRIANGLE_DATA_LAYOUT, packNormalOct, packTriangleFlags } from './BufferLayout.js';
4
+ import { TEXTURE_CONSTANTS } from './TextureBuckets.js';
5
5
  import { ISSUE_CODES } from '../EngineIssues.js';
6
6
  import { ChunkedRecords, SHARED_MEMORY_AVAILABLE } from './ChunkedRecords.js';
7
7
  import { createLogger, fmt, warnOnce } from '../utils/Logger.js';
@@ -14,7 +14,7 @@
14
14
  * that actually runs out (BVH assembly), where it asks only for that step's bytes.
15
15
  */
16
16
 
17
- import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
17
+ import { TRIANGLE_DATA_LAYOUT } from './BufferLayout.js';
18
18
  import { DEFAULT_CHUNK_BYTES } from './ChunkedRecords.js';
19
19
 
20
20
  /** Bytes one stored triangle occupies in the triangle records. */
@@ -59,6 +59,12 @@ export const SAFE_SCENE_BYTES = 7040 * 1024 * 1024;
59
59
  */
60
60
  export const MAX_SCENE_BYTES = 9216 * 1024 * 1024;
61
61
 
62
+ // Scene-archive budgets with the memory spill on: the triangles and BVH leave the heap as the build goes, so the host
63
+ // passes these instead of the importer's own. The whole 15-part Moana subset (55.7M / 7.0M) loads cold under them, at
64
+ // an 8.1 GB preflight estimate; the preflight, not the budget, refuses anything larger.
65
+ export const SPILL_TRIANGLE_BUDGET = 60_000_000;
66
+ export const SPILL_PLACEMENT_BUDGET = 8_000_000;
67
+
62
68
  /**
63
69
  * The host's memory in GB: the host's own figure when it gives one, else Chrome's
64
70
  * `navigator.deviceMemory`, else an assumed 4 (every other runtime lacks it).
@@ -9,7 +9,7 @@
9
9
  * denormalised one. World bounds and the inverse transform are derived on demand.
10
10
  */
11
11
 
12
- import { BVH_LEAF_MARKERS, TRIANGLE_DATA_LAYOUT, assertBVHIndexFits, bvhIndexView } from '../EngineDefaults.js';
12
+ import { TRIANGLE_DATA_LAYOUT, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from './BufferLayout.js';
13
13
 
14
14
  const IDENTITY = Float64Array.from( [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
15
15
 
@@ -1,4 +1,5 @@
1
- import { MATERIAL_DATA_LAYOUT as M, MATERIAL_DEFAULTS as D, normalizeAttenuationDistance } from '../EngineDefaults.js';
1
+ import { MATERIAL_DEFAULTS as D } from '../EngineDefaults.js';
2
+ import { MATERIAL_DATA_LAYOUT as M, normalizeAttenuationDistance } from './BufferLayout.js';
2
3
 
3
4
  // The first 8 Matrix3 elements; the GPU rebuilds the 9th (always 1).
4
5
  const IDENTITY_UV = [ 1, 0, 0, 0, 1, 0, 0, 0 ];
@@ -91,7 +91,7 @@ const DEFAULT_TRIANGLE_BUDGET = 45_000_000;
91
91
  // Placements are their own budget: geometry is shared, but each costs a TLAS leaf and an
92
92
  // instance record. isCoastline's 5.09M loaded and rendered at 23 fps.
93
93
  const DEFAULT_PLACEMENT_BUDGET = 6_000_000;
94
- export { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from '../../EngineDefaults.js';
94
+ export { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from '../HostMemory.js';
95
95
  // Non-instanced shapes merge into a few large meshes: isPalmRig declares 173,338 of them,
96
96
  // and one BufferGeometry + Mesh each measured 3.3 GB — the loader OOM'd before tracing.
97
97
  const DEFAULT_MERGE_MIN_SHAPES = 256;