rayzee 7.28.2 → 7.29.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.28.2",
3
+ "version": "7.29.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -836,7 +836,7 @@ export class SceneProcessor {
836
836
  // (the prior model-load path omitted this, leaving albedo un-decoded / too bright).
837
837
  const buildBucket = ( list, srgb ) => list.length === 0
838
838
  ? Promise.resolve( null )
839
- : this.textureCreator.createTexturesToDataTexture( list ).then( tex => {
839
+ : this.textureCreator.createTexturesToDataTexture( list, { srgbPool: srgb } ).then( tex => {
840
840
 
841
841
  if ( tex && srgb ) tex.colorSpace = SRGBColorSpace;
842
842
  return tex;
@@ -1,7 +1,38 @@
1
- import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, RepeatWrapping } from "three";
1
+ import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping } from "three";
2
2
  import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance } from '../EngineDefaults.js';
3
3
  import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
4
4
  import { ISSUE_CODES } from '../EngineIssues.js';
5
+ import { linearToSRGB } from './ToneMapCPU.js';
6
+ import { createLogger } from '../utils/Logger.js';
7
+
8
+ const log = createLogger( 'textures' );
9
+
10
+ // A bucket array carries ONE colorSpace and the shader reads each slot from a fixed pool, so a
11
+ // texture disagreeing with its pool cannot be re-routed — its pixels are re-encoded instead.
12
+ const TRANSFER_LUT = { toSRGB: null, toLinear: null };
13
+
14
+ function transferLUT( direction ) {
15
+
16
+ if ( ! TRANSFER_LUT[ direction ] ) {
17
+
18
+ const lut = new Uint8Array( 256 );
19
+ for ( let i = 0; i < 256; i ++ ) {
20
+
21
+ const c = i / 255;
22
+ const v = direction === 'toSRGB'
23
+ ? linearToSRGB( c )
24
+ : ( c <= 0.04045 ? c / 12.92 : Math.pow( ( c + 0.055 ) / 1.055, 2.4 ) );
25
+ lut[ i ] = Math.max( 0, Math.min( 255, Math.round( v * 255 ) ) );
26
+
27
+ }
28
+
29
+ TRANSFER_LUT[ direction ] = lut;
30
+
31
+ }
32
+
33
+ return TRANSFER_LUT[ direction ];
34
+
35
+ }
5
36
 
6
37
  // Canvas pooling for efficient reuse of canvas elements
7
38
  class CanvasPool {
@@ -541,13 +572,76 @@ export class TextureCreator {
541
572
 
542
573
  }
543
574
 
575
+ /**
576
+ * Re-encodes any layer whose declared colour space disagrees with its pool, so the GPU's
577
+ * decode — or absence of one — lands on the values the author stored. An untagged texture
578
+ * keeps its pool's convention, which is what every loader produces for glTF.
579
+ *
580
+ * @param {import('three').DataArrayTexture} arrayTex
581
+ * @param {Array<import('three').Texture>} textures - pre-normalization list, in layer order
582
+ * @param {boolean} srgbPool - true when the array is tagged SRGBColorSpace
583
+ */
584
+ _harmonizeTransfer( arrayTex, textures, srgbPool ) {
585
+
586
+ const data = arrayTex?.image?.data;
587
+ if ( ! data ) return;
588
+
589
+ // Mirrors _normalizeTexturesForProcessing's skip rule, which is what sets layer order.
590
+ const kept = textures.filter( t => t?.image );
591
+ const want = srgbPool ? SRGBColorSpace : LinearSRGBColorSpace;
592
+ const layerSize = arrayTex.image.width * arrayTex.image.height * 4;
593
+ const converted = [];
594
+
595
+ for ( let layer = 0; layer < kept.length; layer ++ ) {
596
+
597
+ const declared = kept[ layer ].colorSpace;
598
+ // Only an explicit disagreement is acted on; NoColorSpace defers to the pool.
599
+ if ( declared !== SRGBColorSpace && declared !== LinearSRGBColorSpace ) continue;
600
+ if ( declared === want ) continue;
601
+
602
+ const lut = transferLUT( srgbPool ? 'toSRGB' : 'toLinear' );
603
+ const start = layer * layerSize;
604
+ const end = Math.min( start + layerSize, data.length );
605
+ // RGB only — alpha is never transfer-encoded.
606
+ for ( let i = start; i < end; i += 4 ) {
607
+
608
+ data[ i ] = lut[ data[ i ] ];
609
+ data[ i + 1 ] = lut[ data[ i + 1 ] ];
610
+ data[ i + 2 ] = lut[ data[ i + 2 ] ];
611
+
612
+ }
613
+
614
+ converted.push( kept[ layer ].name || kept[ layer ].source?.uuid?.slice( 0, 8 ) || `layer ${layer}` );
615
+
616
+ }
617
+
618
+ if ( converted.length === 0 ) return;
619
+
620
+ arrayTex.needsUpdate = true;
621
+ const names = converted.slice( 0, 4 ).join( ', ' ) + ( converted.length > 4 ? `, +${converted.length - 4} more` : '' );
622
+
623
+ if ( srgbPool ) {
624
+
625
+ log.info( `re-encoded ${converted.length}/${kept.length} linear-declared colour map(s) to sRGB so the GPU decode round-trips: ${names}` );
626
+
627
+ } else {
628
+
629
+ // Storing a decoded value in an 8-bit linear array is the lossy direction, and a
630
+ // transfer-encoded data map is almost always an authoring slip rather than intent.
631
+ log.warn( `decoded ${converted.length}/${kept.length} sRGB-declared data map(s) to linear — precision is lost in the darks, check the asset: ${names}` );
632
+
633
+ }
634
+
635
+ }
636
+
544
637
  // Unified texture processing with strategy selection
545
- async createTexturesToDataTexture( textures ) {
638
+ async createTexturesToDataTexture( textures, { srgbPool = false } = {} ) {
546
639
 
547
640
  if ( ! textures || textures.length === 0 ) return null;
548
641
 
549
- // Check cache first
550
- const cacheKey = this.textureCache.generateHash( textures );
642
+ // Pool is part of the identity: the same list packed for the colour pool and the data pool
643
+ // gets different pixels (see _harmonizeTransfer), so one key must not serve both.
644
+ const cacheKey = `${srgbPool ? 's' : 'l'}:${this.textureCache.generateHash( textures )}`;
551
645
  const cached = this.textureCache.get( cacheKey );
552
646
  if ( cached ) return cached;
553
647
 
@@ -579,6 +673,7 @@ export class TextureCreator {
579
673
  // Cache successful result
580
674
  if ( result ) {
581
675
 
676
+ this._harmonizeTransfer( result, textures, srgbPool );
582
677
  this.textureCache.set( cacheKey, result );
583
678
 
584
679
  }
@@ -592,7 +687,9 @@ export class TextureCreator {
592
687
  'worker texture processing failed — retrying on the main thread',
593
688
  { cause: String( error?.message ?? error ) }
594
689
  );
595
- return await this.processOnMainThreadSync( normalized );
690
+ const fallback = await this.processOnMainThreadSync( normalized );
691
+ this._harmonizeTransfer( fallback, textures, srgbPool );
692
+ return fallback;
596
693
 
597
694
  } finally {
598
695