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
@@ -5,10 +5,15 @@
5
5
  * A layer is an image as pbrt evaluates it: linear values, top row first, `channels` 1 (a float texture reads
6
6
  * an image's alpha where it has one, else the mean of its colour) or 3, already scaled and inverted, and the uv
7
7
  * mapping pbrt applies before the lookup (st = scale · uv + delta, image row from the top = 1 − t).
8
+ *
9
+ * pbrt's position mappings (`planar`, `spherical`, `cylindrical`) read no uv: their (s, t) becomes a uv set generated per
10
+ * shape (`generateUV`), and a layer in one carries it as `uv` (`{ type, key, … }`; null for the mesh's own uv).
8
11
  */
9
12
 
10
13
  const MAX_BAKE_SIZE = 4096;
11
14
 
15
+ export const uvKey = ( uv ) => uv?.key ?? '';
16
+
12
17
  export const srgbToLinear = ( v ) => ( v <= 0.04045 ? v / 12.92 : ( ( v + 0.055 ) / 1.055 ) ** 2.4 );
13
18
  export const linearToSRGB = ( v ) => ( v <= 0.0031308 ? v * 12.92 : 1.055 * v ** ( 1 / 2.4 ) - 0.055 );
14
19
 
@@ -20,7 +25,7 @@ export const linearToSRGB = ( v ) => ( v <= 0.0031308 ? v * 12.92 : 1.055 * v **
20
25
  * @param {(v: number) => number} [options.decode] - 8-bit value / 255 → linear; omitted for float data
21
26
  * @param {number} [options.scale=1]
22
27
  * @param {boolean} [options.invert=false]
23
- * @param {{ su: number, sv: number, du: number, dv: number }} [options.mapping]
28
+ * @param {{ su: number, sv: number, du: number, dv: number, uv?: object }} [options.mapping]
24
29
  */
25
30
  export function makeLayer( pixels, { channels, decode = null, scale = 1, invert = false, mapping = null } ) {
26
31
 
@@ -29,7 +34,9 @@ export function makeLayer( pixels, { channels, decode = null, scale = 1, invert
29
34
  const data = new Float32Array( width * height * channels );
30
35
  const topDown = pixels.topDown !== false;
31
36
  // pbrt's encoding applies to every channel of an 8-bit image, alpha included.
32
- const read = ( i ) => ( decode ? decode( pixels.data[ i ] / 255 ) : pixels.data[ i ] );
37
+ const bytes = decode && ( pixels.data instanceof Uint8Array || pixels.data instanceof Uint8ClampedArray );
38
+ const lut = bytes ? Float64Array.from( { length: 256 }, ( _, v ) => decode( v / 255 ) ) : null;
39
+ const read = lut ? ( i ) => lut[ pixels.data[ i ] ] : ( i ) => ( decode ? decode( pixels.data[ i ] / 255 ) : pixels.data[ i ] );
33
40
  const fromAlpha = channels === 1 && stride === 4 && hasAlpha( pixels );
34
41
  const mean = channels === 1 && stride >= 3 && ! fromAlpha;
35
42
 
@@ -53,7 +60,7 @@ export function makeLayer( pixels, { channels, decode = null, scale = 1, invert
53
60
 
54
61
  }
55
62
 
56
- return { width, height, channels, data, su: 1, sv: 1, du: 0, dv: 0, ...mapping };
63
+ return { width, height, channels, data, su: 1, sv: 1, du: 0, dv: 0, uv: null, ...mapping };
57
64
 
58
65
  }
59
66
 
@@ -76,6 +83,15 @@ export function layerMean( layer ) {
76
83
 
77
84
  }
78
85
 
86
+ function layerMeanRGB( { data, channels } ) {
87
+
88
+ const sum = [ 0, 0, 0 ];
89
+ for ( let i = 0; i < data.length; i ++ ) sum[ i % channels ] += data[ i ];
90
+ const texels = data.length / channels || 1;
91
+ return [ 0, 1, 2 ].map( ( c ) => sum[ Math.min( c, channels - 1 ) ] / texels );
92
+
93
+ }
94
+
79
95
  // Bilinear, repeating, at pbrt's (s, t).
80
96
  function sample( layer, s, t, out ) {
81
97
 
@@ -102,20 +118,36 @@ function sample( layer, s, t, out ) {
102
118
  /**
103
119
  * Each term is `{ rgb }` (a constant) or `{ layer, tint? }` (an image, times `tint`); `clamp` clamps it to [0, 1]
104
120
  * first, as pbrt clamps a colour texture read as an albedo, before anything mixes it. Evaluates
105
- * `combine( values )` over the texel grid of the largest image among the terms, in that image's uv mapping;
106
- * a term with another mapping is sampled through it. The result is sRGB bytes laid out for a DataTexture
121
+ * `combine( values )` over the texel grid of the largest image among the terms in the first image's uv set, in that
122
+ * image's uv mapping; a term with another mapping is sampled through it, and one in another uv set counts as its mean
123
+ * (`averaged` says how many). The result is sRGB bytes laid out for a DataTexture
107
124
  * (bottom row first), clamped to [0, 1] as pbrt clamps an albedo. Null when every term is a constant.
108
125
  * @param {Array<{ rgb?: number[], layer?: object, tint?: number[] }>} terms
109
126
  * @param {(values: number[][]) => number[]} combine - linear RGB from each term's linear RGB
110
127
  * @param {object} [options]
111
128
  * @param {(values: number[][]) => number} [options.alphaOf] - the alpha channel, linear; opaque without it
112
129
  * @param {boolean} [options.linear=false] - linear bytes, for a data map
113
- * @returns {{ data: Uint8Array, width: number, height: number, mapping: { su: number, sv: number, du: number, dv: number } } | null}
130
+ * @returns {{ data: Uint8Array, width: number, height: number, mapping: { su: number, sv: number, du: number, dv: number, uv: object|null }, averaged: number } | null}
114
131
  */
115
132
  export function bake( terms, combine, { alphaOf = null, linear = false } = {} ) {
116
133
 
117
- let driver = null;
118
- for ( const term of terms ) if ( term.layer && ( ! driver || term.layer.width * term.layer.height > driver.width * driver.height ) ) driver = term.layer;
134
+ const set = uvKey( terms.find( ( term ) => term.layer )?.layer.uv );
135
+ let driver = null, averaged = 0;
136
+ terms = terms.map( ( term ) => {
137
+
138
+ if ( ! term.layer ) return term;
139
+ if ( uvKey( term.layer.uv ) !== set ) {
140
+
141
+ averaged ++;
142
+ const mean = layerMeanRGB( term.layer );
143
+ return { rgb: mean.map( ( v, c ) => v * ( term.tint?.[ c ] ?? 1 ) ), clamp: term.clamp };
144
+
145
+ }
146
+
147
+ if ( ! driver || term.layer.width * term.layer.height > driver.width * driver.height ) driver = term.layer;
148
+ return term;
149
+
150
+ } );
119
151
  if ( ! driver ) return null;
120
152
 
121
153
  const width = Math.min( driver.width, MAX_BAKE_SIZE ), height = Math.min( driver.height, MAX_BAKE_SIZE );
@@ -166,7 +198,7 @@ export function bake( terms, combine, { alphaOf = null, linear = false } = {} )
166
198
 
167
199
  }
168
200
 
169
- return { data: out, width, height, mapping: { su: driver.su, sv: driver.sv, du: driver.du, dv: driver.dv } };
201
+ return { data: out, width, height, mapping: { su: driver.su, sv: driver.sv, du: driver.du, dv: driver.dv, uv: driver.uv ?? null }, averaged };
170
202
 
171
203
  }
172
204
 
@@ -177,3 +209,153 @@ export const mixOf = ( [ a, b, amount ] ) => {
177
209
  return [ a[ 0 ] + ( b[ 0 ] - a[ 0 ] ) * t, a[ 1 ] + ( b[ 1 ] - a[ 1 ] ) * t, a[ 2 ] + ( b[ 2 ] - a[ 2 ] ) * t ];
178
210
 
179
211
  };
212
+
213
+ // The coordinate that goes once around the axis: spherical φ is t, cylindrical φ is s.
214
+ const WRAPPED = { spherical: 1, cylindrical: 0 };
215
+
216
+ /** pbrt-v4's (s, t) of a point in texture space (textures.h: PlanarMapping, SphericalMapping, CylindricalMapping). */
217
+ export function mapPoint( mapping, x, y, z, out ) {
218
+
219
+ if ( mapping.type === 'planar' ) {
220
+
221
+ const { vs, vt } = mapping;
222
+ out[ 0 ] = mapping.ds + x * vs[ 0 ] + y * vs[ 1 ] + z * vs[ 2 ];
223
+ out[ 1 ] = mapping.dt + x * vt[ 0 ] + y * vt[ 1 ] + z * vt[ 2 ];
224
+
225
+ } else if ( mapping.type === 'spherical' ) {
226
+
227
+ const length = Math.hypot( x, y, z ) || 1;
228
+ const phi = Math.atan2( y, x );
229
+ out[ 0 ] = Math.acos( Math.min( 1, Math.max( - 1, z / length ) ) ) / Math.PI;
230
+ out[ 1 ] = ( phi < 0 ? phi + 2 * Math.PI : phi ) / ( 2 * Math.PI );
231
+
232
+ } else {
233
+
234
+ out[ 0 ] = ( Math.PI + Math.atan2( y, x ) ) / ( 2 * Math.PI );
235
+ out[ 1 ] = z;
236
+
237
+ }
238
+
239
+ return out;
240
+
241
+ }
242
+
243
+ /**
244
+ * pbrt's (s, t) at each vertex as a uv set, `toTexture` (column-major) taking `positions` to texture space. A triangle
245
+ * across the seam gets copies of its vertices a turn on (`extra` names their sources, `index` the rewritten triangles,
246
+ * null when none); a vertex on the axis takes its triangle's mean angle.
247
+ * @param {Float32Array} positions
248
+ * @param {ArrayLike<number>|null} index
249
+ * @param {number[]} toTexture
250
+ * @param {{ type: string }} mapping
251
+ * @returns {{ uv: Float32Array, index: Uint32Array|null, extra: Uint32Array }}
252
+ */
253
+ export function generateUV( positions, index, toTexture, mapping ) {
254
+
255
+ const count = positions.length / 3;
256
+ const m = toTexture;
257
+ const k = WRAPPED[ mapping.type ];
258
+ const st = new Float64Array( count * 2 );
259
+ const radius = k === undefined ? null : new Float64Array( count );
260
+ const out = [ 0, 0 ];
261
+ let maxRadius = 0;
262
+
263
+ for ( let i = 0; i < count; i ++ ) {
264
+
265
+ const px = positions[ i * 3 ], py = positions[ i * 3 + 1 ], pz = positions[ i * 3 + 2 ];
266
+ const x = m[ 0 ] * px + m[ 4 ] * py + m[ 8 ] * pz + m[ 12 ];
267
+ const y = m[ 1 ] * px + m[ 5 ] * py + m[ 9 ] * pz + m[ 13 ];
268
+ const z = m[ 2 ] * px + m[ 6 ] * py + m[ 10 ] * pz + m[ 14 ];
269
+ mapPoint( mapping, x, y, z, out );
270
+ st[ i * 2 ] = out[ 0 ];
271
+ st[ i * 2 + 1 ] = out[ 1 ];
272
+ if ( ! radius ) continue;
273
+ radius[ i ] = Math.hypot( x, y );
274
+ if ( radius[ i ] > maxRadius ) maxRadius = radius[ i ];
275
+
276
+ }
277
+
278
+ const extra = [];
279
+ let triangles = index;
280
+
281
+ if ( radius ) {
282
+
283
+ const onAxis = maxRadius * 1e-6;
284
+ const copies = new Map();
285
+ const corner = [ 0, 0, 0 ], value = [ 0, 0, 0 ];
286
+ const triangleCount = ( index ? index.length : count ) / 3;
287
+
288
+ for ( let t = 0; t < triangleCount; t ++ ) {
289
+
290
+ let lo = Infinity, hi = - Infinity, sum = 0, around = 0;
291
+ for ( let c = 0; c < 3; c ++ ) {
292
+
293
+ const v = corner[ c ] = index ? index[ t * 3 + c ] : t * 3 + c;
294
+ if ( radius[ v ] <= onAxis ) continue;
295
+ lo = Math.min( lo, st[ v * 2 + k ] );
296
+ hi = Math.max( hi, st[ v * 2 + k ] );
297
+
298
+ }
299
+
300
+ const across = hi - lo > 0.5;
301
+ for ( let c = 0; c < 3; c ++ ) {
302
+
303
+ const w = st[ corner[ c ] * 2 + k ];
304
+ if ( radius[ corner[ c ] ] <= onAxis ) value[ c ] = NaN;
305
+ else {
306
+
307
+ value[ c ] = across && w < 0.5 ? w + 1 : w;
308
+ sum += value[ c ];
309
+ around ++;
310
+
311
+ }
312
+
313
+ }
314
+
315
+ for ( let c = 0; c < 3; c ++ ) {
316
+
317
+ const v = corner[ c ];
318
+ const w = Number.isNaN( value[ c ] ) ? ( around > 0 ? sum / around : st[ v * 2 + k ] ) : value[ c ];
319
+ if ( w === st[ v * 2 + k ] ) continue;
320
+
321
+ if ( ! index ) {
322
+
323
+ st[ v * 2 + k ] = w;
324
+ continue;
325
+
326
+ }
327
+
328
+ const key = `${v}|${w}`;
329
+ let copy = copies.get( key );
330
+ if ( copy === undefined ) {
331
+
332
+ copies.set( key, copy = count + extra.length / 2 );
333
+ extra.push( v, w );
334
+
335
+ }
336
+
337
+ if ( triangles === index ) triangles = Uint32Array.from( index );
338
+ triangles[ t * 3 + c ] = copy;
339
+
340
+ }
341
+
342
+ }
343
+
344
+ }
345
+
346
+ const added = extra.length / 2;
347
+ const uv = new Float32Array( ( count + added ) * 2 );
348
+ for ( let i = 0; i < count * 2; i ++ ) uv[ i ] = st[ i ];
349
+ const sources = new Uint32Array( added );
350
+ for ( let i = 0; i < added; i ++ ) {
351
+
352
+ const v = sources[ i ] = extra[ i * 2 ];
353
+ uv[ ( count + i ) * 2 ] = st[ v * 2 ];
354
+ uv[ ( count + i ) * 2 + 1 ] = st[ v * 2 + 1 ];
355
+ uv[ ( count + i ) * 2 + k ] = extra[ i * 2 + 1 ];
356
+
357
+ }
358
+
359
+ return { uv, index: triangles === index ? null : triangles, extra: sources };
360
+
361
+ }
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { freeNow } from './buffers.js';
18
- import { PBRTParser } from './PBRTParser.js';
18
+ import { PBRTParser, ParamUsage } from './PBRTParser.js';
19
19
  import { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
20
20
  import { findFrameSequence, FrameSequenceMerger, motionFromShutter, SEQUENCE_FPS } from './PBRTAnimation.js';
21
21
 
@@ -380,7 +380,8 @@ export async function listEntryPathsFrom( vfs ) {
380
380
  * @param {boolean} [args.convertHandedness=true]
381
381
  * @param {boolean} [args.animation=true] - load a frame sequence (`frame25.pbrt`, `frame35.pbrt`, …)
382
382
  * as one animated scene instead of its first frame alone. Ignored when `entryPath` names a file.
383
- * @returns {Promise<{group, camera, environment, animations, warnings, entryPath, frames}>}
383
+ * @returns {Promise<{group, camera, environment, animations, warnings, entryPath, frames, parameters}>} - `parameters`: what
384
+ * ParamUsage.finish() reports, the parameters this load ignored or approximated
384
385
  */
385
386
  export async function loadPBRTScene( args ) {
386
387
 
@@ -401,6 +402,7 @@ export async function loadPBRTScene( args ) {
401
402
  }
402
403
 
403
404
  const entryPath = requested || candidates[ 0 ];
405
+ const usage = new ParamUsage();
404
406
 
405
407
  // Parse (with Include resolution). Bytes go straight to the lexer — a scene file can
406
408
  // be larger than the longest string JavaScript will build.
@@ -415,7 +417,8 @@ export async function loadPBRTScene( args ) {
415
417
  // scene file at once — the difference between 5.7 GB resident and a few MB.
416
418
  releaseInclude: ( include, currentDir, bytes ) => vfs.releasePath( include, currentDir, bytes ),
417
419
  maxPlacements: args.maxPlacements,
418
- instanceIncludes: args.instanceIncludes
420
+ instanceIncludes: args.instanceIncludes,
421
+ usage
419
422
  } );
420
423
 
421
424
  const ir = await parser.parse( bytes, path.includes( '/' ) ? path.slice( 0, path.lastIndexOf( '/' ) ) : '' );
@@ -490,6 +493,7 @@ export async function loadPBRTScene( args ) {
490
493
  curveSteps: args.curveSteps,
491
494
  curveSides: args.curveSides,
492
495
  curveTolerance: args.curveTolerance,
496
+ usage,
493
497
  resolvePLY: async ( filename ) => {
494
498
 
495
499
  const rec = vfs.findRecord( filename );
@@ -515,9 +519,20 @@ export async function loadPBRTScene( args ) {
515
519
  } );
516
520
 
517
521
  const buildStart = performance.now();
518
- const result = await builder.build( ir );
522
+ let result, parameters;
523
+ try {
524
+
525
+ result = await builder.build( ir );
526
+
527
+ } finally {
528
+
529
+ parameters = usage.finish();
530
+
531
+ }
532
+
519
533
  return {
520
534
  ...result,
535
+ parameters,
521
536
  entryPath: sequence ? sequence[ 0 ].path : entryPath,
522
537
  frames: sequence ? sequence.map( f => f.path ) : null,
523
538
  candidates,
@@ -97,6 +97,15 @@ export async function loadPlatformImage( url, mimeType = mimeTypeOf( { uri: url
97
97
 
98
98
  }
99
99
 
100
+ /** An image file's bytes as a texture, decoded by the platform: {@link loadPlatformImage} without the fetch. */
101
+ export async function platformImageFromBytes( bytes, filename ) {
102
+
103
+ const texture = pixelTexture( await decodeBytes( bytes, mimeTypeOf( { uri: filename } ) ), { shared: true } );
104
+ texture.flipY = true;
105
+ return texture;
106
+
107
+ }
108
+
100
109
  /**
101
110
  * A GLTFLoader plugin for a runtime with no way to decode an image (Node without `decodeImage`): each image fails with
102
111
  * what to configure, where three.js's own path threw `self is not defined` and took the whole load down.
@@ -63,6 +63,14 @@ const GEOMETRY_QUEUE_BYTES = 256 * 1024 * 1024;
63
63
 
64
64
  const log = createLogger( 'scene' );
65
65
 
66
+ // The per-type texture lists an extraction hands over (GeometryExtractor.getExtractedData).
67
+ const TEXTURE_LISTS = Object.freeze( [
68
+ 'maps', 'normalMaps', 'bumpMaps', 'roughnessMaps', 'metalnessMaps', 'emissiveMaps', 'displacementMaps',
69
+ 'anisotropyMaps', 'transmissionMaps', 'clearcoatMaps', 'clearcoatRoughnessMaps', 'sheenColorMaps',
70
+ 'sheenRoughnessMaps', 'iridescenceMaps', 'iridescenceThicknessMaps', 'specularIntensityMaps', 'specularColorMaps',
71
+ 'diffuseTransmissionMaps', 'diffuseTransmissionColorMaps',
72
+ ] );
73
+
66
74
  // A BLAS's nodes: one array, or a split mesh's parts in order — never one array that large.
67
75
  const nodeParts = nodes => ( Array.isArray( nodes ) ? nodes : [ nodes ] );
68
76
  const nodeBytes = nodes => nodeParts( nodes ).reduce( ( sum, part ) => sum + part.byteLength, 0 );
@@ -913,25 +921,7 @@ export class SceneProcessor {
913
921
  this.materialTriangleCounts = extractedData.materialTriangleCounts; // Per-material tri count for sort-bin remap
914
922
  this.meshes = extractedData.meshes;
915
923
  this.meshTriangleRanges = extractedData.meshTriangleRanges; // Per-mesh { start, count } for TLAS/BLAS
916
- this.maps = extractedData.maps;
917
- this.normalMaps = extractedData.normalMaps;
918
- this.bumpMaps = extractedData.bumpMaps;
919
- this.roughnessMaps = extractedData.roughnessMaps;
920
- this.metalnessMaps = extractedData.metalnessMaps;
921
- this.emissiveMaps = extractedData.emissiveMaps;
922
- this.displacementMaps = extractedData.displacementMaps;
923
- this.anisotropyMaps = extractedData.anisotropyMaps;
924
- this.transmissionMaps = extractedData.transmissionMaps;
925
- this.clearcoatMaps = extractedData.clearcoatMaps;
926
- this.clearcoatRoughnessMaps = extractedData.clearcoatRoughnessMaps;
927
- this.sheenColorMaps = extractedData.sheenColorMaps;
928
- this.sheenRoughnessMaps = extractedData.sheenRoughnessMaps;
929
- this.iridescenceMaps = extractedData.iridescenceMaps;
930
- this.iridescenceThicknessMaps = extractedData.iridescenceThicknessMaps;
931
- this.specularIntensityMaps = extractedData.specularIntensityMaps;
932
- this.specularColorMaps = extractedData.specularColorMaps;
933
- this.diffuseTransmissionMaps = extractedData.diffuseTransmissionMaps ?? [];
934
- this.diffuseTransmissionColorMaps = extractedData.diffuseTransmissionColorMaps ?? [];
924
+ this._adoptTextureLists( extractedData );
935
925
  this.directionalLights = extractedData.directionalLights;
936
926
  this.cameras = extractedData.cameras;
937
927
 
@@ -2453,13 +2443,7 @@ export class SceneProcessor {
2453
2443
  // Update material arrays (but keep existing triangle data)
2454
2444
  this.materials = extractedData.materials;
2455
2445
  this.meshes = extractedData.meshes; // Update mesh data
2456
- this.maps = extractedData.maps;
2457
- this.normalMaps = extractedData.normalMaps;
2458
- this.bumpMaps = extractedData.bumpMaps;
2459
- this.roughnessMaps = extractedData.roughnessMaps;
2460
- this.metalnessMaps = extractedData.metalnessMaps;
2461
- this.emissiveMaps = extractedData.emissiveMaps;
2462
- this.displacementMaps = extractedData.displacementMaps;
2446
+ this._adoptTextureLists( extractedData );
2463
2447
 
2464
2448
  // Bucket textures, remap material indices, regenerate raw material data, and
2465
2449
  // build the consolidated bucket arrays — same path as the initial build.
@@ -2487,6 +2471,13 @@ export class SceneProcessor {
2487
2471
 
2488
2472
  }
2489
2473
 
2474
+ /** @private The extraction's material indices point into these lists, so every one is taken from it. */
2475
+ _adoptTextureLists( extractedData ) {
2476
+
2477
+ for ( const name of TEXTURE_LISTS ) this[ name ] = extractedData[ name ] ?? [];
2478
+
2479
+ }
2480
+
2490
2481
  /**
2491
2482
  * Dispose only material-related textures
2492
2483
  * @private
@@ -2726,6 +2717,7 @@ export class SceneProcessor {
2726
2717
  const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
2727
2718
  const triRanges = [];
2728
2719
  const blasRanges = [];
2720
+ const refit = [];
2729
2721
  let owners = 0;
2730
2722
  for ( let i = 0; i < table.count; i ++ ) {
2731
2723
 
@@ -2736,6 +2728,7 @@ export class SceneProcessor {
2736
2728
  if ( ! p ) continue;
2737
2729
  this._updateMeshTrianglePositions( i, p );
2738
2730
  blasRanges.push( table.blasOffsetOf( i ), table.blasNodeCountOf( i ) );
2731
+ refit.push( i );
2739
2732
 
2740
2733
  // Smooth normals overwrite the face normals just computed, so they follow per mesh.
2741
2734
  if ( normalsFor ) {
@@ -2764,6 +2757,12 @@ export class SceneProcessor {
2764
2757
  const msg = e.data;
2765
2758
  if ( msg.type === 'refitComplete' ) {
2766
2759
 
2760
+ // Later moves and main-thread refits box placements by these.
2761
+ for ( const i of refit ) table.recomputeAABB( i, this.bvh, this.triangles );
2762
+ table.computeGroupAABBs();
2763
+ // The worker boxes a group copy by its shown members only.
2764
+ const leaves = this._hiddenGroupLeaves();
2765
+ if ( leaves.length ) this._refitTLAS( leaves );
2767
2766
  resolve( { refitTimeMs: msg.refitTimeMs, triRanges, bvhRanges } );
2768
2767
 
2769
2768
  } else if ( msg.type === 'error' ) {
@@ -2979,6 +2978,22 @@ export class SceneProcessor {
2979
2978
 
2980
2979
  }
2981
2980
 
2981
+ /** The TLAS leaves of every copy of a group with a hidden member. @private */
2982
+ _hiddenGroupLeaves() {
2983
+
2984
+ const table = this.instanceTable;
2985
+ const leaves = [];
2986
+ for ( const g of table.groups ) {
2987
+
2988
+ if ( g.members.every( ( _, j ) => table.visible[ g.starts[ j ] ] ) ) continue;
2989
+ for ( let c = 0; c < g.count; c ++ ) leaves.push( table.leafOfPlacement( g.starts[ 0 ] + c ) );
2990
+
2991
+ }
2992
+
2993
+ return leaves;
2994
+
2995
+ }
2996
+
2982
2997
  /**
2983
2998
  * A grouped mesh's parts are traced as one object, so a part moved alone takes its siblings with it: each sibling
2984
2999
  * is given the first moved member's world transform, and all of them are moved.
@@ -4276,6 +4291,7 @@ export class SceneProcessor {
4276
4291
  // scene's 4), producing a tree of a different size that the swap below rejects —
4277
4292
  // so every rebuild was thrown away.
4278
4293
  maxLeafSize: this.bvhBuilder.maxLeafSize,
4294
+ foldLeaves: this._foldLeaves === true,
4279
4295
  numBins: this.bvhBuilder.numBins,
4280
4296
  maxBins: this.bvhBuilder.maxBins,
4281
4297
  minBins: this.bvhBuilder.minBins,
@@ -1,4 +1,4 @@
1
- import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping, FloatType, HalfFloatType } from "three";
1
+ import { DataArrayTexture, RGBAFormat, RGFormat, RedFormat, LinearFilter, UnsignedByteType, UnsignedShortType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping, FloatType, HalfFloatType, DataUtils } from "three";
2
2
  import { MATERIAL_DATA_LAYOUT, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from './BufferLayout.js';
3
3
  import { alignBucketWidth, TEXTURE_CONSTANTS } from './TextureBuckets.js';
4
4
  import { packMaterial } from './MaterialPacking.js';
@@ -1260,7 +1260,7 @@ export class TextureCreator {
1260
1260
 
1261
1261
  const image = texture?.image;
1262
1262
  const pixels = texture?.isCompressedTexture ? texture.mipmaps?.[ 0 ] : image;
1263
- const data = pixels?.data;
1263
+ const data = texture?.isCompressedTexture ? pixels?.data : rgba8Pixels( texture );
1264
1264
  if ( data && pixels.width > 0 && pixels.height > 0 && data.byteLength >= pixels.width * pixels.height * 4 ) {
1265
1265
 
1266
1266
  return { data: new Uint8Array( data.buffer, data.byteOffset, pixels.width * pixels.height * 4 ), width: pixels.width, height: pixels.height, flipY: texture.flipY !== false };
@@ -1817,7 +1817,8 @@ export class TextureCreator {
1817
1817
 
1818
1818
  const idx = normalized.length;
1819
1819
  normalized.push( null );
1820
- bitmapJobs.push( { index: idx, flipY: tex.flipY, promise: _rawPixelsToBitmap( tex.image.data, tex.image.width, tex.image.height ) } );
1820
+ const pixels = rgba8Pixels( tex );
1821
+ bitmapJobs.push( { index: idx, flipY: tex.flipY, promise: pixels ? _rawPixelsToBitmap( pixels, tex.image.width, tex.image.height ) : Promise.reject( new Error( 'pixels in a layout the texture arrays cannot take' ) ) } );
1821
1822
  continue;
1822
1823
 
1823
1824
  }
@@ -1900,10 +1901,49 @@ export class TextureCreator {
1900
1901
  // ── Helpers ──────────────────────────────────────────────────────────
1901
1902
 
1902
1903
  /** Convert raw RGBA pixel data to an ImageBitmap (zero-copy Uint8ClampedArray view). */
1903
- function _rawPixelsToBitmap( data, width, height ) {
1904
+ // Async, so a bad layer rejects into its placeholder instead of throwing through the whole bucket.
1905
+ async function _rawPixelsToBitmap( data, width, height ) {
1904
1906
 
1905
1907
  const clamped = new Uint8ClampedArray( data.buffer, data.byteOffset, data.byteLength );
1906
1908
  return createImageBitmap( new ImageData( clamped, width, height ) );
1907
1909
 
1908
1910
  }
1909
1911
 
1912
+ /**
1913
+ * A data texture's pixels as RGBA8 (the arrays are 8-bit), or null. Float and half-float maps — an archive's EXR, HDR or
1914
+ * PFM — clamp to [0, 1]; one channel fills grey. Read as bytes, a float buffer was noise in Node and failed the bucket in
1915
+ * the browser.
1916
+ */
1917
+ export function rgba8Pixels( texture ) {
1918
+
1919
+ const image = texture?.image;
1920
+ const data = image?.data;
1921
+ if ( ! data || ! ( image.width > 0 ) || ! ( image.height > 0 ) ) return null;
1922
+ const count = image.width * image.height;
1923
+ if ( data instanceof Uint8Array || data instanceof Uint8ClampedArray ) {
1924
+
1925
+ return data.byteLength >= count * 4 ? new Uint8Array( data.buffer, data.byteOffset, count * 4 ) : null;
1926
+
1927
+ }
1928
+
1929
+ const channels = { [ RGBAFormat ]: 4, [ RGFormat ]: 2, [ RedFormat ]: 1 }[ texture.format ] ?? Math.round( data.length / count );
1930
+ if ( ! ( channels >= 1 && channels <= 4 ) || data.length < count * channels ) return null;
1931
+ const half = texture.type === HalfFloatType;
1932
+ const scale = texture.type === UnsignedShortType ? 1 / 65535 : 1;
1933
+ const out = new Uint8Array( count * 4 );
1934
+ for ( let i = 0; i < count; i ++ ) {
1935
+
1936
+ for ( let c = 0; c < 4; c ++ ) {
1937
+
1938
+ const raw = c < channels ? data[ i * channels + c ] : null;
1939
+ const v = raw === null ? ( c === 3 ? 1 : out[ i * 4 ] / 255 ) : ( half ? DataUtils.fromHalfFloat( raw ) : raw * scale );
1940
+ out[ i * 4 + c ] = Math.max( 0, Math.min( 255, Math.round( v * 255 ) ) );
1941
+
1942
+ }
1943
+
1944
+ }
1945
+
1946
+ return out;
1947
+
1948
+ }
1949
+
@@ -98,6 +98,26 @@ export function effectiveExposure( exposure, toneMapping ) {
98
98
 
99
99
  }
100
100
 
101
+ /**
102
+ * The gain that shows a pixel of a solid backdrop at `heldAt` while the picture is shown at `exposure`: auto
103
+ * exposure does not move the backdrop. 1 under a view that applies no exposure.
104
+ */
105
+ export function backdropGain( heldAt, exposure, toneMapping ) {
106
+
107
+ return effectiveExposure( heldAt, toneMapping ) / effectiveExposure( exposure, toneMapping );
108
+
109
+ }
110
+
111
+ /**
112
+ * A pixel's gain once the backdrop is held: `backdrop` where the camera saw only the backdrop (coverage 0), the
113
+ * scene's own gain where it saw the scene, blended by coverage between. The GPU pass computes the same.
114
+ */
115
+ export function heldGain( gain, backdrop, coverage ) {
116
+
117
+ return gain + ( backdrop - gain ) * ( 1 - Math.min( Math.max( coverage, 0 ), 1 ) );
118
+
119
+ }
120
+
101
121
  /** Rec.709 luminance coefficients (same as Display / Common.js). */
102
122
  const LUM_R = 0.2126, LUM_G = 0.7152, LUM_B = 0.0722;
103
123
 
@@ -128,8 +148,9 @@ export function applySaturation( out, saturation ) {
128
148
  * @param {number} options.toneMapping - a registered view transform id
129
149
  * @param {number} [options.saturation=1]
130
150
  * @param {boolean} [options.preserveAlpha=false]
131
- * @param {{fn: function(number, number, number, number): number, width: number, height: number}} [options.pixelGain] -
132
- * the twin of the GPU pass's `gain`: `fn( u, v, luminance, log2 exposure )` per pixel, before the curve
151
+ * @param {{fn?: function(number, number, number, number): number, width: number, height: number, backdrop?: number}} [options.pixelGain] -
152
+ * the twin of the GPU pass's `gain`: `fn( u, v, luminance, log2 exposure )` per pixel, before the curve, and
153
+ * `backdrop` the gain a solid backdrop is held at (`heldGain`)
133
154
  * @returns {Uint8ClampedArray} RGBA bytes
134
155
  */
135
156
  export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1, preserveAlpha = false, pixelGain = null } ) {
@@ -146,9 +167,17 @@ export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1,
146
167
  let g = gain;
147
168
  if ( pixelGain ) {
148
169
 
149
- const p = i >> 2, x = p % pixelGain.width, y = ( p - x ) / pixelGain.width;
150
- const lum = linear[ i ] * 0.2126 + linear[ i + 1 ] * 0.7152 + linear[ i + 2 ] * 0.0722;
151
- g *= pixelGain.fn( ( x + 0.5 ) / pixelGain.width, ( y + 0.5 ) / pixelGain.height, lum, logExposure );
170
+ let pg = 1;
171
+ if ( pixelGain.fn ) {
172
+
173
+ const p = i >> 2, x = p % pixelGain.width, y = ( p - x ) / pixelGain.width;
174
+ const lum = linear[ i ] * 0.2126 + linear[ i + 1 ] * 0.7152 + linear[ i + 2 ] * 0.0722;
175
+ pg = pixelGain.fn( ( x + 0.5 ) / pixelGain.width, ( y + 0.5 ) / pixelGain.height, lum, logExposure );
176
+
177
+ }
178
+
179
+ if ( pixelGain.backdrop > 0 ) pg = heldGain( pg, pixelGain.backdrop, linear[ i + 3 ] );
180
+ g *= pg;
152
181
 
153
182
  }
154
183