rayzee 5.2.0 → 5.3.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": "5.2.0",
3
+ "version": "5.3.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",
@@ -1014,6 +1014,7 @@ export class PathTracerApp extends EventDispatcher {
1014
1014
 
1015
1015
  this._sdf = new SceneProcessor();
1016
1016
  this.assetLoader = new AssetLoader( this.meshScene, this.cameraManager.camera, this.cameraManager.controls );
1017
+ this.assetLoader.setRenderer( this.renderer );
1017
1018
  this.assetLoader.createFloorPlane();
1018
1019
 
1019
1020
  this.cameraManager.controls.addEventListener( 'change', () => {
@@ -4,6 +4,7 @@ import { Box3, Vector3, RectAreaLight, Color, FloatType, LinearFilter, Equirecta
4
4
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
5
5
  import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
6
6
  import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
7
+ import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
7
8
  import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
8
9
  import { createMeshesFromMultiMaterialMesh } from 'three/addons/utils/SceneUtils.js';
9
10
  import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
@@ -41,6 +42,13 @@ export class AssetLoader extends EventDispatcher {
41
42
  this.loaderCache = {};
42
43
  this.uploadedFileInfo = null;
43
44
  this.animations = [];
45
+ this.renderer = null;
46
+
47
+ }
48
+
49
+ setRenderer( renderer ) {
50
+
51
+ this.renderer = renderer;
44
52
 
45
53
  }
46
54
 
@@ -698,8 +706,31 @@ export class AssetLoader extends EventDispatcher {
698
706
  dracoLoader.setDecoderConfig( { type: 'js' } );
699
707
  dracoLoader.setDecoderPath( 'https://www.gstatic.com/draco/v1/decoders/' );
700
708
 
709
+ const ktx2Loader = new KTX2Loader();
710
+ ktx2Loader.setTranscoderPath( 'https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/basis/' );
711
+
712
+ if ( this.renderer ) {
713
+
714
+ ktx2Loader.detectSupport( this.renderer );
715
+
716
+ // Force RGBA output for Basis Universal textures. GPU-compressed
717
+ // texture arrays (CompressedArrayTexture) are blocked by a Three.js
718
+ // TSL limitation: the node compiler maintains global state that
719
+ // survives dispose(), so swapping texture array formats between
720
+ // DataArrayTexture and CompressedArrayTexture at runtime causes
721
+ // WGSL compilation failures (unresolved uniform bindings).
722
+ ktx2Loader.workerConfig = {
723
+ astcSupported: false, etc1Supported: false, etc2Supported: false,
724
+ dxtSupported: false, bptcSupported: false, pvrtcSupported: false,
725
+ };
726
+
727
+ }
728
+
729
+ this.loaderCache.ktx2 = ktx2Loader;
730
+
701
731
  const loader = new GLTFLoader();
702
732
  loader.setDRACOLoader( dracoLoader );
733
+ loader.setKTX2Loader( ktx2Loader );
703
734
  loader.setMeshoptDecoder( MeshoptDecoder );
704
735
 
705
736
  this.loaderCache.gltf = loader;
@@ -542,55 +542,29 @@ export class GeometryExtractor {
542
542
  for ( let i = 0; i < triangleCount; i ++ ) {
543
543
 
544
544
  const i3 = i * 3;
545
+ const idxA = indices ? indices[ i3 + 0 ] : i3 + 0;
546
+ const idxB = indices ? indices[ i3 + 1 ] : i3 + 1;
547
+ const idxC = indices ? indices[ i3 + 2 ] : i3 + 2;
545
548
 
546
- // Get vertices
547
- if ( indices ) {
549
+ this.getVertex( positions, idxA, posA );
550
+ this.getVertex( positions, idxB, posB );
551
+ this.getVertex( positions, idxC, posC );
548
552
 
549
- this.getVertexFromIndices( positions, indices[ i3 + 0 ], posA );
550
- this.getVertexFromIndices( positions, indices[ i3 + 1 ], posB );
551
- this.getVertexFromIndices( positions, indices[ i3 + 2 ], posC );
553
+ this.getVertex( normals, idxA, normalA );
554
+ this.getVertex( normals, idxB, normalB );
555
+ this.getVertex( normals, idxC, normalC );
552
556
 
553
- this.getVertexFromIndices( normals, indices[ i3 + 0 ], normalA );
554
- this.getVertexFromIndices( normals, indices[ i3 + 1 ], normalB );
555
- this.getVertexFromIndices( normals, indices[ i3 + 2 ], normalC );
557
+ if ( uvs ) {
556
558
 
557
- if ( uvs ) {
558
-
559
- this.getVertexFromIndices( uvs, indices[ i3 + 0 ], uvA );
560
- this.getVertexFromIndices( uvs, indices[ i3 + 1 ], uvB );
561
- this.getVertexFromIndices( uvs, indices[ i3 + 2 ], uvC );
562
-
563
- } else {
564
-
565
- uvA.set( 0, 0 );
566
- uvB.set( 0, 0 );
567
- uvC.set( 0, 0 );
568
-
569
- }
559
+ this.getVertex( uvs, idxA, uvA );
560
+ this.getVertex( uvs, idxB, uvB );
561
+ this.getVertex( uvs, idxC, uvC );
570
562
 
571
563
  } else {
572
564
 
573
- this.getVertex( positions, i3 + 0, posA );
574
- this.getVertex( positions, i3 + 1, posB );
575
- this.getVertex( positions, i3 + 2, posC );
576
-
577
- this.getVertex( normals, i3 + 0, normalA );
578
- this.getVertex( normals, i3 + 1, normalB );
579
- this.getVertex( normals, i3 + 2, normalC );
580
-
581
- if ( uvs ) {
582
-
583
- this.getVertex( uvs, i3 + 0, uvA );
584
- this.getVertex( uvs, i3 + 1, uvB );
585
- this.getVertex( uvs, i3 + 2, uvC );
586
-
587
- } else {
588
-
589
- uvA.set( 0, 0 );
590
- uvB.set( 0, 0 );
591
- uvC.set( 0, 0 );
592
-
593
- }
565
+ uvA.set( 0, 0 );
566
+ uvB.set( 0, 0 );
567
+ uvC.set( 0, 0 );
594
568
 
595
569
  }
596
570
 
@@ -689,37 +663,18 @@ export class GeometryExtractor {
689
663
  }
690
664
 
691
665
  // Optimized attribute access methods
692
- getVertexFromIndices( attribute, index, target ) {
693
-
694
- if ( attribute.itemSize === 2 ) {
695
-
696
- target.x = attribute.array[ index * 2 ];
697
- target.y = attribute.array[ index * 2 + 1 ];
698
-
699
- } else if ( attribute.itemSize === 3 ) {
700
-
701
- target.x = attribute.array[ index * 3 ];
702
- target.y = attribute.array[ index * 3 + 1 ];
703
- target.z = attribute.array[ index * 3 + 2 ];
704
-
705
- }
706
-
707
- return target;
708
-
709
- }
710
-
711
666
  getVertex( attribute, index, target ) {
712
667
 
713
668
  if ( attribute.itemSize === 2 ) {
714
669
 
715
- target.x = attribute.array[ index * 2 ];
716
- target.y = attribute.array[ index * 2 + 1 ];
670
+ target.x = attribute.getX( index );
671
+ target.y = attribute.getY( index );
717
672
 
718
- } else if ( attribute.itemSize === 3 ) {
673
+ } else if ( attribute.itemSize >= 3 ) {
719
674
 
720
- target.x = attribute.array[ index * 3 ];
721
- target.y = attribute.array[ index * 3 + 1 ];
722
- target.z = attribute.array[ index * 3 + 2 ];
675
+ target.x = attribute.getX( index );
676
+ target.y = attribute.getY( index );
677
+ target.z = attribute.getZ( index );
723
678
 
724
679
  }
725
680
 
@@ -496,8 +496,11 @@ export class TextureCreator {
496
496
  const cached = this.textureCache.get( cacheKey );
497
497
  if ( cached ) return cached;
498
498
 
499
+ // Normalize non-drawable images (KTX2 CompressedTexture RGBA, DataTexture)
500
+ const { normalized, bitmapsToClose } = await this._normalizeTexturesForProcessing( textures );
501
+
499
502
  // Select optimal processing strategy
500
- const strategy = this.selectProcessingStrategy( textures );
503
+ const strategy = this.selectProcessingStrategy( normalized );
501
504
  let result;
502
505
 
503
506
  try {
@@ -505,19 +508,19 @@ export class TextureCreator {
505
508
  switch ( strategy.method ) {
506
509
 
507
510
  case 'worker-direct':
508
- result = await this.processWithWorkerDirect( textures );
511
+ result = await this.processWithWorkerDirect( normalized );
509
512
  break;
510
513
  case 'worker-chunked':
511
- result = await this.processWithWorkerChunked( textures, strategy.chunkSize );
514
+ result = await this.processWithWorkerChunked( normalized, strategy.chunkSize );
512
515
  break;
513
516
  case 'main-batch':
514
- result = await this.processOnMainThreadBatch( textures, strategy.batchSize );
517
+ result = await this.processOnMainThreadBatch( normalized, strategy.batchSize );
515
518
  break;
516
519
  case 'main-streaming':
517
- result = await this.processOnMainThreadStreaming( textures );
520
+ result = await this.processOnMainThreadStreaming( normalized );
518
521
  break;
519
522
  default:
520
- result = await this.processOnMainThreadSync( textures );
523
+ result = await this.processOnMainThreadSync( normalized );
521
524
 
522
525
  }
523
526
 
@@ -533,7 +536,11 @@ export class TextureCreator {
533
536
  } catch ( error ) {
534
537
 
535
538
  console.warn( 'Texture processing failed, trying fallback:', error );
536
- return await this.processOnMainThreadSync( textures );
539
+ return await this.processOnMainThreadSync( normalized );
540
+
541
+ } finally {
542
+
543
+ for ( const bmp of bitmapsToClose ) bmp.close();
537
544
 
538
545
  }
539
546
 
@@ -1258,6 +1265,109 @@ export class TextureCreator {
1258
1265
 
1259
1266
  }
1260
1267
 
1268
+ // ── KTX2 / DataTexture normalization ────────────────────────────────
1269
+
1270
+ /**
1271
+ * Normalize textures so every entry has a drawable `.image`.
1272
+ * - RGBA CompressedTexture (KTX2 Basis → RGBA): pixel data from mipmaps[0]
1273
+ * - GPU-compressed texture (BC7/ASTC/ETC2): warning (should be pre-decompressed)
1274
+ * - DataTexture (raw RGBA pixels): pixel data from image.data
1275
+ * - Regular texture: passed through as-is
1276
+ *
1277
+ * Bitmap creation is parallelized via Promise.all.
1278
+ */
1279
+ async _normalizeTexturesForProcessing( textures ) {
1280
+
1281
+ const normalized = [];
1282
+ const bitmapsToClose = [];
1283
+ const bitmapJobs = []; // { index, promise }
1284
+
1285
+ for ( const tex of textures ) {
1286
+
1287
+ if ( ! tex?.image ) continue;
1288
+
1289
+ // RGBA CompressedTexture (KTX2 Basis transcode wraps output as CompressedTexture)
1290
+ if ( tex.isCompressedTexture && tex.format === RGBAFormat && tex.mipmaps?.[ 0 ]?.data ) {
1291
+
1292
+ const mip = tex.mipmaps[ 0 ];
1293
+ const idx = normalized.length;
1294
+ normalized.push( null ); // placeholder — filled after Promise.all
1295
+ bitmapJobs.push( { index: idx, promise: _rawPixelsToBitmap( mip.data, mip.width, mip.height ) } );
1296
+ continue;
1297
+
1298
+ }
1299
+
1300
+ // True GPU-compressed texture in a mixed group — can't extract pixels on CPU.
1301
+ // All-compressed groups are handled by the CompressedArrayTexture path upstream.
1302
+ if ( tex.isCompressedTexture ) {
1303
+
1304
+ console.warn( '[TextureCreator] GPU-compressed texture in mixed group — using placeholder' );
1305
+ normalized.push( null );
1306
+ continue;
1307
+
1308
+ }
1309
+
1310
+ // DataTexture with raw pixel array
1311
+ if ( tex.image.data && ! ( tex.image instanceof HTMLImageElement ) &&
1312
+ ! ( tex.image instanceof HTMLCanvasElement ) &&
1313
+ ! ( typeof ImageBitmap !== 'undefined' && tex.image instanceof ImageBitmap ) ) {
1314
+
1315
+ const idx = normalized.length;
1316
+ normalized.push( null );
1317
+ bitmapJobs.push( { index: idx, promise: _rawPixelsToBitmap( tex.image.data, tex.image.width, tex.image.height ) } );
1318
+ continue;
1319
+
1320
+ }
1321
+
1322
+ normalized.push( tex );
1323
+
1324
+ }
1325
+
1326
+ // Resolve all bitmap conversions in parallel
1327
+ if ( bitmapJobs.length > 0 ) {
1328
+
1329
+ const results = await Promise.allSettled( bitmapJobs.map( j => j.promise ) );
1330
+
1331
+ for ( let i = 0; i < bitmapJobs.length; i ++ ) {
1332
+
1333
+ const { index } = bitmapJobs[ i ];
1334
+ const result = results[ i ];
1335
+
1336
+ if ( result.status === 'fulfilled' ) {
1337
+
1338
+ const bitmap = result.value;
1339
+ bitmapsToClose.push( bitmap );
1340
+ normalized[ index ] = { image: bitmap };
1341
+
1342
+ } else {
1343
+
1344
+ console.warn( '[TextureCreator] Failed to create ImageBitmap:', result.reason );
1345
+
1346
+ }
1347
+
1348
+ }
1349
+
1350
+ }
1351
+
1352
+ // Replace any remaining nulls (failed conversions) with a 1x1 white placeholder
1353
+ // to preserve array indexing alignment with material texture indices.
1354
+ for ( let i = 0; i < normalized.length; i ++ ) {
1355
+
1356
+ if ( normalized[ i ] === null ) {
1357
+
1358
+ const placeholder = new Uint8ClampedArray( [ 255, 255, 255, 255 ] );
1359
+ const bitmap = await createImageBitmap( new ImageData( placeholder, 1, 1 ) );
1360
+ bitmapsToClose.push( bitmap );
1361
+ normalized[ i ] = { image: bitmap };
1362
+
1363
+ }
1364
+
1365
+ }
1366
+
1367
+ return { normalized, bitmapsToClose };
1368
+
1369
+ }
1370
+
1261
1371
  createFallbackTexture() {
1262
1372
 
1263
1373
  const data = new Uint8Array( [ 255, 255, 255, 255 ] );
@@ -1291,3 +1401,14 @@ export class TextureCreator {
1291
1401
  }
1292
1402
 
1293
1403
  }
1404
+
1405
+ // ── Helpers ──────────────────────────────────────────────────────────
1406
+
1407
+ /** Convert raw RGBA pixel data to an ImageBitmap (zero-copy Uint8ClampedArray view). */
1408
+ function _rawPixelsToBitmap( data, width, height ) {
1409
+
1410
+ const clamped = new Uint8ClampedArray( data.buffer, data.byteOffset, data.byteLength );
1411
+ return createImageBitmap( new ImageData( clamped, width, height ) );
1412
+
1413
+ }
1414
+