rayzee 9.1.2 → 9.2.1

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 (49) hide show
  1. package/README.md +176 -16
  2. package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-CYgkQY8Z.js} +1 -1
  3. package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-PkgDLQcD.js} +72 -40
  4. package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-B6XIe3DU.js} +102 -92
  5. package/dist/rayzee.es.js +6209 -5571
  6. package/dist/rayzee.umd.js +137 -126
  7. package/package.json +3 -2
  8. package/src/AssetConfig.js +10 -5
  9. package/src/EngineEvents.js +41 -1
  10. package/src/EngineIssues.js +3 -0
  11. package/src/Headless.js +23 -7
  12. package/src/HeadlessCanvas.js +104 -0
  13. package/src/Passes/OIDNDenoiser.js +29 -4
  14. package/src/Passes/OIDNTemporalHistory.js +7 -0
  15. package/src/PathTracerApp.js +511 -136
  16. package/src/Platform.js +76 -0
  17. package/src/Processor/AssetLoader.js +66 -4
  18. package/src/Processor/BVHBuilder.js +2 -1
  19. package/src/Processor/EquirectHDRInfo.js +2 -1
  20. package/src/Processor/HostMemory.js +14 -0
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
  22. package/src/Processor/ParallelBVHBuilder.js +7 -6
  23. package/src/Processor/PlatformImageLoader.js +118 -0
  24. package/src/Processor/ResampleRGBA8.js +102 -0
  25. package/src/Processor/SceneProcessor.js +7 -6
  26. package/src/Processor/TextureCreator.js +162 -13
  27. package/src/Processor/TextureReadback.js +26 -10
  28. package/src/Processor/ToneMapGPU.js +45 -15
  29. package/src/Processor/Workers/PackWorker.js +28 -0
  30. package/src/Stages/Compositor.js +25 -6
  31. package/src/Stages/PathTracer.js +142 -25
  32. package/src/Stages/PathTracerStage.js +6 -96
  33. package/src/Storage/DownloadCache.js +30 -7
  34. package/src/TSL/DebugKernel.js +1 -1
  35. package/src/TSL/GenerateKernel.js +2 -5
  36. package/src/TSL/Random.js +3 -120
  37. package/src/TSL/ShadeKernel.js +3 -3
  38. package/src/index.js +2 -1
  39. package/src/managers/CameraManager.js +2 -2
  40. package/src/managers/DenoisingManager.js +76 -7
  41. package/src/managers/GoboManager.js +8 -1
  42. package/src/managers/IESManager.js +13 -2
  43. package/src/managers/InteractionManager.js +1 -1
  44. package/src/managers/LightManager.js +31 -0
  45. package/src/managers/OverlayManager.js +11 -1
  46. package/src/managers/VideoRenderManager.js +8 -0
  47. package/src/node/NodeWorker.js +144 -0
  48. package/src/node/index.js +9 -0
  49. package/src/node/nodePlatform.js +86 -0
@@ -2,13 +2,37 @@ import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColor
2
2
  import { alignBucketWidth, TEXTURE_CONSTANTS, MEMORY_CONSTANTS, MATERIAL_DATA_LAYOUT, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from '../EngineDefaults.js';
3
3
  import { packMaterial } from './MaterialPacking.js';
4
4
  import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
5
- import { ISSUE_CODES } from '../EngineIssues.js';
5
+ import PackWorker from './Workers/PackWorker.js?worker&inline';
6
+ import { resampleRGBA8 } from './ResampleRGBA8.js';
7
+ import { ISSUE_CODES, EngineIssueError } from '../EngineIssues.js';
6
8
  import { linearToSRGB } from './ToneMapCPU.js';
7
9
  import { getActiveColorManagement } from '../Color/ColorManagement.js';
8
10
  import { createLogger } from '../utils/Logger.js';
11
+ import { createWorker, hardwareThreads, hasWorkers } from '../Platform.js';
9
12
 
10
13
  const log = createLogger( 'textures' );
11
14
 
15
+ // Below this a pack is cheaper than starting a worker for it.
16
+ const WORKER_PACK_BYTES = 8 * 2 ** 20;
17
+
18
+ // Buckets pack concurrently; this keeps their workers within the cores, beside the BVH pool's.
19
+ const packSlots = { busy: 0, waiting: [] };
20
+
21
+ async function acquirePackSlot() {
22
+
23
+ if ( packSlots.busy < Math.max( 1, hardwareThreads() - 1 ) ) packSlots.busy ++;
24
+ else await new Promise( ( resolve ) => packSlots.waiting.push( resolve ) );
25
+
26
+ return () => {
27
+
28
+ const next = packSlots.waiting.shift();
29
+ if ( next ) next();
30
+ else packSlots.busy --;
31
+
32
+ };
33
+
34
+ }
35
+
12
36
  // A bucket array carries ONE colorSpace and the shader reads each slot from a fixed pool, so a
13
37
  // texture disagreeing with its pool cannot be re-routed — its pixels are re-encoded instead.
14
38
  const TRANSFER_LUT = { toSRGB: null, toLinear: null };
@@ -529,6 +553,7 @@ export class TextureCreator {
529
553
  /** A failed map array leaves those surfaces untextured — complete-looking and wrong. @private */
530
554
  _reportTextureFailure( map, error ) {
531
555
 
556
+ if ( error instanceof EngineIssueError ) throw error;
532
557
  this._issues?.record(
533
558
  ISSUE_CODES.TEXTURE_BUILD_FAILED,
534
559
  `${map} texture array failed to build — those surfaces render untextured`,
@@ -537,6 +562,17 @@ export class TextureCreator {
537
562
 
538
563
  }
539
564
 
565
+ // Layers are addressed by position, so a texture that cannot be read keeps its slot.
566
+ _reportTextureLayer( layer, cause ) {
567
+
568
+ this._issues?.record(
569
+ ISSUE_CODES.TEXTURE_BUILD_FAILED,
570
+ `texture ${layer} could not be read — its surfaces show a white placeholder`,
571
+ { layer, cause: String( cause?.message ?? cause ) }
572
+ );
573
+
574
+ }
575
+
540
576
  _clampTextureSize( size ) {
541
577
 
542
578
  const n = Math.floor( Number( size ) || TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE );
@@ -562,7 +598,7 @@ export class TextureCreator {
562
598
  offscreenCanvas: typeof OffscreenCanvas !== 'undefined',
563
599
  imageBitmap: typeof createImageBitmap !== 'undefined',
564
600
  workers: typeof Worker !== 'undefined',
565
- hardwareConcurrency: navigator.hardwareConcurrency || 4
601
+ hardwareConcurrency: hardwareThreads()
566
602
  };
567
603
 
568
604
  }
@@ -775,6 +811,16 @@ export class TextureCreator {
775
811
  const cached = this.textureCache.get( cacheKey );
776
812
  if ( cached ) return cached;
777
813
 
814
+ if ( typeof createImageBitmap === 'undefined' ) {
815
+
816
+ const packed = await this.processOnCPU( textures );
817
+ const reencoded = this._harmonizeTransfer( packed, textures, srgbPool );
818
+ this._applyColorManagement( packed, textures, srgbPool, reencoded );
819
+ this.textureCache.set( cacheKey, packed );
820
+ return packed;
821
+
822
+ }
823
+
778
824
  // Normalize non-drawable images (KTX2 CompressedTexture RGBA, DataTexture)
779
825
  const { normalized, bitmapsToClose } = await this._normalizeTexturesForProcessing( textures );
780
826
 
@@ -813,6 +859,7 @@ export class TextureCreator {
813
859
 
814
860
  } catch ( error ) {
815
861
 
862
+ if ( error instanceof EngineIssueError ) throw error;
816
863
  this._issues?.warn(
817
864
  ISSUE_CODES.TEXTURE_PROCESSING_FALLBACK,
818
865
  'worker texture processing failed — retrying on the main thread',
@@ -885,9 +932,9 @@ export class TextureCreator {
885
932
 
886
933
  this.activeWorkers ++;
887
934
 
888
- try {
935
+ const worker = new TexturesWorker();
889
936
 
890
- const worker = new TexturesWorker();
937
+ try {
891
938
 
892
939
  // Prepare textures for worker with direct transfer
893
940
  const texturesData = await this.prepareTexturesForWorkerDirect( textures );
@@ -934,11 +981,11 @@ export class TextureCreator {
934
981
 
935
982
  } );
936
983
 
937
- worker.terminate();
938
984
  return this.createDataArrayTextureFromResult( result );
939
985
 
940
986
  } finally {
941
987
 
988
+ worker.terminate();
942
989
  this.activeWorkers --;
943
990
 
944
991
  }
@@ -949,10 +996,18 @@ export class TextureCreator {
949
996
  async prepareTexturesForWorkerDirect( textures ) {
950
997
 
951
998
  const texturesData = [];
999
+ const placeholder = () => ( { data: new Uint8ClampedArray( [ 255, 255, 255, 255 ] ).buffer, width: 1, height: 1, isImageData: true } );
952
1000
 
953
- for ( const texture of textures ) {
1001
+ for ( let layer = 0; layer < textures.length; layer ++ ) {
954
1002
 
955
- if ( ! texture?.image ) continue;
1003
+ const texture = textures[ layer ];
1004
+ if ( ! texture?.image ) {
1005
+
1006
+ this._reportTextureLayer( layer, 'no image' );
1007
+ texturesData.push( placeholder() );
1008
+ continue;
1009
+
1010
+ }
956
1011
 
957
1012
  const flipY = texture.flipY !== false;
958
1013
 
@@ -964,7 +1019,7 @@ export class TextureCreator {
964
1019
  // getImageData off the main thread (Option 2 blocks it per texture).
965
1020
  const img = texture.image;
966
1021
  const canDirect = typeof createImageBitmap !== 'undefined' && (
967
- img instanceof HTMLImageElement
1022
+ ( typeof HTMLImageElement !== 'undefined' && img instanceof HTMLImageElement )
968
1023
  || ( typeof ImageBitmap !== 'undefined' && img instanceof ImageBitmap )
969
1024
  || ( typeof HTMLCanvasElement !== 'undefined' && img instanceof HTMLCanvasElement )
970
1025
  || ( typeof OffscreenCanvas !== 'undefined' && img instanceof OffscreenCanvas )
@@ -1008,7 +1063,8 @@ export class TextureCreator {
1008
1063
 
1009
1064
  } catch ( error ) {
1010
1065
 
1011
- console.warn( 'Failed to prepare texture for worker:', error );
1066
+ this._reportTextureLayer( layer, error );
1067
+ texturesData.push( placeholder() );
1012
1068
 
1013
1069
  }
1014
1070
 
@@ -1018,6 +1074,93 @@ export class TextureCreator {
1018
1074
 
1019
1075
  }
1020
1076
 
1077
+ /**
1078
+ * Packing without a browser (no createImageBitmap, no 2D canvas): each layer must already be
1079
+ * pixels — a DataTexture, or an image the platform's decodeImage decoded. A layer that matches its
1080
+ * bucket is copied exactly; one that does not is resampled bilinearly, where a browser's canvas
1081
+ * would use its own filter, so those texels can differ slightly from a browser render. A large
1082
+ * bucket packs in a worker: on the main thread it starved the BVH workers of their next task.
1083
+ */
1084
+ async processOnCPU( textures ) {
1085
+
1086
+ const layers = textures.map( ( texture, layer ) => {
1087
+
1088
+ const image = texture?.image;
1089
+ const pixels = texture?.isCompressedTexture ? texture.mipmaps?.[ 0 ] : image;
1090
+ const data = pixels?.data;
1091
+ if ( data && pixels.width > 0 && pixels.height > 0 && data.byteLength >= pixels.width * pixels.height * 4 ) {
1092
+
1093
+ return { data: new Uint8Array( data.buffer, data.byteOffset, pixels.width * pixels.height * 4 ), width: pixels.width, height: pixels.height, flipY: texture.flipY !== false };
1094
+
1095
+ }
1096
+
1097
+ this._reportTextureLayer( layer, image ? 'not decoded to pixels, and there is no browser to decode it' : 'no image' );
1098
+ return { data: new Uint8Array( [ 255, 255, 255, 255 ] ), width: 1, height: 1, flipY: false };
1099
+
1100
+ } );
1101
+
1102
+ const { maxWidth, maxHeight } = this.calculateOptimalDimensions( layers.map( ( image ) => ( { image } ) ) );
1103
+ const layerBytes = maxWidth * maxHeight * 4;
1104
+
1105
+ const packed = hasWorkers() && layerBytes * layers.length >= WORKER_PACK_BYTES
1106
+ ? await this._packInWorker( layers, maxWidth, maxHeight )
1107
+ : null;
1108
+ if ( packed ) return this.createDataArrayTextureFromBuffer( packed, maxWidth, maxHeight, layers.length );
1109
+
1110
+ const data = this.bufferPool.getBuffer( layerBytes * layers.length, Uint8Array );
1111
+ layers.forEach( ( layer, i ) => resampleRGBA8( layer, data, i * layerBytes, maxWidth, maxHeight ) );
1112
+
1113
+ return this.createDataArrayTextureFromBuffer( data, maxWidth, maxHeight, layers.length );
1114
+
1115
+ }
1116
+
1117
+ /** The packed pixels, or null to pack on the main thread instead. */
1118
+ async _packInWorker( layers, width, height ) {
1119
+
1120
+ const release = await acquirePackSlot();
1121
+ let worker = null;
1122
+
1123
+ try {
1124
+
1125
+ // A SharedArrayBuffer view crosses as itself; anything else as a copy of just its bytes,
1126
+ // made only once this bucket has its slot so waiting buckets hold no copies.
1127
+ const transfer = [];
1128
+ const sent = layers.map( ( layer ) => {
1129
+
1130
+ if ( typeof SharedArrayBuffer !== 'undefined' && layer.data.buffer instanceof SharedArrayBuffer ) return layer;
1131
+ const data = layer.data.slice();
1132
+ transfer.push( data.buffer );
1133
+ return { ...layer, data };
1134
+
1135
+ } );
1136
+
1137
+ worker = createWorker( PackWorker );
1138
+ return await new Promise( ( resolve, reject ) => {
1139
+
1140
+ worker.onmessage = ( e ) => ( e.data.error ? reject( new Error( e.data.error ) ) : resolve( e.data.data ) );
1141
+ worker.onerror = ( e ) => reject( e.error ?? new Error( e.message ?? 'texture pack worker failed' ) );
1142
+ worker.postMessage( { layers: sent, width, height }, transfer );
1143
+
1144
+ } );
1145
+
1146
+ } catch ( error ) {
1147
+
1148
+ this._issues?.warn(
1149
+ ISSUE_CODES.TEXTURE_PROCESSING_FALLBACK,
1150
+ 'worker texture packing failed — packing on the main thread',
1151
+ { cause: String( error?.message ?? error ) }
1152
+ );
1153
+ return null;
1154
+
1155
+ } finally {
1156
+
1157
+ worker?.terminate();
1158
+ release();
1159
+
1160
+ }
1161
+
1162
+ }
1163
+
1021
1164
  async processOnMainThreadBatch( textures, batchSize ) {
1022
1165
 
1023
1166
  const validTextures = textures.filter( tex => tex?.image );
@@ -1465,7 +1608,13 @@ export class TextureCreator {
1465
1608
 
1466
1609
  for ( const tex of textures ) {
1467
1610
 
1468
- if ( ! tex?.image ) continue;
1611
+ if ( ! tex?.image ) {
1612
+
1613
+ this._reportTextureLayer( normalized.length, 'no image' );
1614
+ normalized.push( null );
1615
+ continue;
1616
+
1617
+ }
1469
1618
 
1470
1619
  // RGBA CompressedTexture (KTX2 Basis transcode wraps output as CompressedTexture)
1471
1620
  if ( tex.isCompressedTexture && tex.format === RGBAFormat && tex.mipmaps?.[ 0 ]?.data ) {
@@ -1489,8 +1638,8 @@ export class TextureCreator {
1489
1638
  }
1490
1639
 
1491
1640
  // DataTexture with raw pixel array
1492
- if ( tex.image.data && ! ( tex.image instanceof HTMLImageElement ) &&
1493
- ! ( tex.image instanceof HTMLCanvasElement ) &&
1641
+ if ( tex.image.data && ! ( typeof HTMLImageElement !== 'undefined' && tex.image instanceof HTMLImageElement ) &&
1642
+ ! ( typeof HTMLCanvasElement !== 'undefined' && tex.image instanceof HTMLCanvasElement ) &&
1494
1643
  ! ( typeof ImageBitmap !== 'undefined' && tex.image instanceof ImageBitmap ) ) {
1495
1644
 
1496
1645
  const idx = normalized.length;
@@ -1522,7 +1671,7 @@ export class TextureCreator {
1522
1671
 
1523
1672
  } else {
1524
1673
 
1525
- console.warn( '[TextureCreator] Failed to create ImageBitmap:', result.reason );
1674
+ this._reportTextureLayer( index, result.reason );
1526
1675
 
1527
1676
  }
1528
1677
 
@@ -46,6 +46,25 @@ export class TextureReadback {
46
46
  */
47
47
  async read( source, width, height ) {
48
48
 
49
+ try {
50
+
51
+ return await this.renderer.readRenderTargetPixelsAsync( this.draw( source, width, height ), 0, 0, width, height );
52
+
53
+ } finally {
54
+
55
+ this.release();
56
+
57
+ }
58
+
59
+ }
60
+
61
+ /**
62
+ * Draws `source` into this pass's float target, for a caller that reads it on the GPU. Pair with
63
+ * `release()`.
64
+ * @returns {import('three').RenderTarget}
65
+ */
66
+ draw( source, width, height ) {
67
+
49
68
  this._ensureTarget( width, height );
50
69
 
51
70
  if ( this._source !== source ) {
@@ -63,19 +82,16 @@ export class TextureReadback {
63
82
  this.renderer.setRenderTarget( this.target );
64
83
  this.quad.render( this.renderer );
65
84
  this.renderer.setRenderTarget( previous );
85
+ return this.target;
66
86
 
67
- try {
68
-
69
- return await this.renderer.readRenderTargetPixelsAsync( this.target, 0, 0, width, height );
70
-
71
- } finally {
87
+ }
72
88
 
73
- // A float target the size of the frame — 132 MB at 4K — for something done once per
74
- // saved file. Not worth keeping between saves.
75
- this.target.dispose();
76
- this.target = null;
89
+ // A float target the size of the frame — 132 MB at 4K — for something done once per saved file.
90
+ // Not worth keeping between saves.
91
+ release() {
77
92
 
78
- }
93
+ this.target?.dispose();
94
+ this.target = null;
79
95
 
80
96
  }
81
97
 
@@ -39,7 +39,24 @@ function currentShader() {
39
39
  */
40
40
  export const TONE_MAP_WGSL = currentShader().wgsl;
41
41
 
42
- const packedWGSL = transformWGSL => /* wgsl */ `
42
+ // Where the linear colour comes from: a packed rgba16float buffer, or a float texture read by pixel.
43
+ const SOURCES = {
44
+ packed: /* wgsl */ `
45
+ @group(0) @binding(0) var<storage, read> src: array<u32>;
46
+
47
+ fn rayzee_source( x: u32, y: u32 ) -> vec4<f32> {
48
+ let a = ( y * params.width + x ) * 2u;
49
+ return vec4<f32>( unpack2x16float( src[ a ] ), unpack2x16float( src[ a + 1u ] ) );
50
+ }`,
51
+ texture: /* wgsl */ `
52
+ @group(0) @binding(0) var src: texture_2d<f32>;
53
+
54
+ fn rayzee_source( x: u32, y: u32 ) -> vec4<f32> {
55
+ return textureLoad( src, vec2<u32>( x, y ), 0 );
56
+ }`,
57
+ };
58
+
59
+ const toneMapPassWGSL = ( transformWGSL, input ) => /* wgsl */ `
43
60
  struct Params {
44
61
  width: u32,
45
62
  height: u32,
@@ -47,11 +64,12 @@ struct Params {
47
64
  flipY: u32,
48
65
  exposure: f32,
49
66
  saturation: f32,
67
+ alpha: u32,
50
68
  };
51
69
 
52
- @group(0) @binding(0) var<storage, read> src: array<u32>;
53
70
  @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
54
71
  @group(0) @binding(2) var<uniform> params: Params;
72
+ ${SOURCES[ input ]}
55
73
 
56
74
  ${transformWGSL}
57
75
 
@@ -62,30 +80,31 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
62
80
  var srcY = gid.y;
63
81
  if ( params.flipY == 1u ) { srcY = params.height - 1u - gid.y; }
64
82
 
65
- let a = ( srcY * params.width + gid.x ) * 2u;
66
- let rg = unpack2x16float( src[ a ] );
67
- let ba = unpack2x16float( src[ a + 1u ] );
68
-
69
- let mapped = rayzee_tone_map( vec3<f32>( rg.x, rg.y, ba.x ), params.mode, params.exposure, params.saturation );
83
+ let linear = rayzee_source( gid.x, srcY );
84
+ let mapped = rayzee_tone_map( linear.rgb, params.mode, params.exposure, params.saturation );
70
85
  let b8 = rayzee_to_u8( rayzee_encode( mapped, params.mode ) );
71
- dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( 255u << 24u );
86
+ var a8 = 255u;
87
+ if ( params.alpha == 1u ) { a8 = rayzee_to_u8( vec3<f32>( linear.a ) ).x; }
88
+ dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( a8 << 24u );
72
89
  }
73
90
  `;
74
91
 
75
- /** Uniform blocks round up to 16 bytes; the struct above is 24. */
92
+ /** Uniform blocks round up to 16 bytes; the struct above is 28. */
76
93
  const PARAMS_BYTES = 32;
77
94
 
78
95
  /**
79
- * Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel) into RGBA bytes.
96
+ * Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel), or with `input: 'texture'` a
97
+ * float texture read by pixel, into RGBA bytes.
80
98
  *
81
99
  * Bound to one device and one image size; `ensureSize()` reallocates when the size changes.
82
100
  */
83
101
  export class PackedToneMapper {
84
102
 
85
- constructor( device, label = 'rayzee:tonemap' ) {
103
+ constructor( device, label = 'rayzee:tonemap', { input = 'packed' } = {} ) {
86
104
 
87
105
  this.device = device;
88
106
  this.label = label;
107
+ this.input = input;
89
108
  this.width = 0;
90
109
  this.height = 0;
91
110
  this._pipeline = null;
@@ -120,7 +139,7 @@ export class PackedToneMapper {
120
139
  label: this.label,
121
140
  layout: 'auto',
122
141
  compute: {
123
- module: this.device.createShaderModule( { label: this.label, code: packedWGSL( wgsl ) } ),
142
+ module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input ) } ),
124
143
  entryPoint: 'main',
125
144
  },
126
145
  } );
@@ -206,15 +225,17 @@ export class PackedToneMapper {
206
225
  }
207
226
 
208
227
  /**
209
- * @param {GPUBuffer} src packed rgba16float, `width * height * 8` bytes, STORAGE-capable
228
+ * @param {GPUBuffer|GPUTexture} src packed rgba16float, `width * height * 8` bytes, STORAGE-capable;
229
+ * or with `input: 'texture'` a float texture at least `width × height`
210
230
  * @param {object} tone
211
231
  * @param {number} tone.exposure raw `renderer.toneMappingExposure`
212
232
  * @param {number} tone.toneMapping Three.js ToneMapping constant
213
233
  * @param {number} [tone.saturation=1]
214
234
  * @param {boolean} [tone.flipY=false]
235
+ * @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
215
236
  * @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
216
237
  */
217
- async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false } = {} ) {
238
+ async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false } = {} ) {
218
239
 
219
240
  if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
220
241
  if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
@@ -232,6 +253,7 @@ export class PackedToneMapper {
232
253
  this._paramU32[ 3 ] = flipY ? 1 : 0;
233
254
  this._paramF32[ 4 ] = exposure;
234
255
  this._paramF32[ 5 ] = saturation;
256
+ this._paramU32[ 6 ] = preserveAlpha ? 1 : 0;
235
257
  this.device.queue.writeBuffer( this._params, 0, this._paramData );
236
258
 
237
259
  const tableEntries = [ ...this._tables.entries() ]
@@ -241,7 +263,7 @@ export class PackedToneMapper {
241
263
  layout: this._pipeline.getBindGroupLayout( 0 ),
242
264
  entries: [
243
265
  ...tableEntries,
244
- { binding: 0, resource: { buffer: src, size: width * height * 8 } },
266
+ { binding: 0, resource: this.input === 'texture' ? src.createView() : { buffer: src, size: width * height * 8 } },
245
267
  { binding: 1, resource: { buffer: this._storage } },
246
268
  { binding: 2, resource: { buffer: this._params } },
247
269
  ],
@@ -263,6 +285,14 @@ export class PackedToneMapper {
263
285
 
264
286
  }
265
287
 
288
+ /** Frees the size-dependent buffers and keeps the pipeline, for a mapper used now and then. */
289
+ release() {
290
+
291
+ this._releaseBuffers();
292
+ this.width = this.height = 0;
293
+
294
+ }
295
+
266
296
  _releaseBuffers() {
267
297
 
268
298
  this._storage?.destroy();
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Packs RGBA8 layers into one texture array off the main thread, for hosts without a browser, where
3
+ * TextureCreator.processOnCPU would otherwise hold up the BVH workers it feeds.
4
+ *
5
+ * Input: { layers: [{ data: Uint8Array (SharedArrayBuffer-backed), width, height, flipY }], width, height }
6
+ * Output: { data: Uint8Array } — its buffer transferred
7
+ */
8
+
9
+ import { resampleRGBA8 } from '../ResampleRGBA8.js';
10
+
11
+ self.onmessage = function ( e ) {
12
+
13
+ const { layers, width, height } = e.data;
14
+
15
+ try {
16
+
17
+ const layerBytes = width * height * 4;
18
+ const data = new Uint8Array( layerBytes * layers.length );
19
+ layers.forEach( ( layer, i ) => resampleRGBA8( layer, data, i * layerBytes, width, height ) );
20
+ self.postMessage( { data }, [ data.buffer ] );
21
+
22
+ } catch ( error ) {
23
+
24
+ self.postMessage( { error: error.message } );
25
+
26
+ }
27
+
28
+ };
@@ -4,6 +4,16 @@ import { NoBlending, NoToneMapping } from 'three';
4
4
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
5
5
  import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
6
6
 
7
+ // In priority order: the first one published wins.
8
+ const LIGHT_SOURCES = [
9
+ 'oidn:output',
10
+ 'edgeFiltering:output',
11
+ 'bilateralFiltering:output',
12
+ 'asvgf:output',
13
+ 'nrd:output',
14
+ 'pathtracer:color',
15
+ ];
16
+
7
17
  /**
8
18
  * Compositor — Terminal pipeline stage.
9
19
  *
@@ -76,12 +86,21 @@ export class Compositor extends RenderStage {
76
86
  */
77
87
  resolveLightTexture( context ) {
78
88
 
79
- return context.getTexture( 'oidn:output' )
80
- || context.getTexture( 'edgeFiltering:output' )
81
- || context.getTexture( 'bilateralFiltering:output' )
82
- || context.getTexture( 'asvgf:output' )
83
- || context.getTexture( 'nrd:output' )
84
- || context.getTexture( 'pathtracer:color' );
89
+ return this.resolveLightSource( context )?.texture ?? null;
90
+
91
+ }
92
+
93
+ /** {@link resolveLightTexture} plus the context key it came from. */
94
+ resolveLightSource( context ) {
95
+
96
+ for ( const key of LIGHT_SOURCES ) {
97
+
98
+ const texture = context.getTexture( key );
99
+ if ( texture ) return { key, texture };
100
+
101
+ }
102
+
103
+ return null;
85
104
 
86
105
  }
87
106