rayzee 5.2.0 → 5.3.2

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.2",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -128,6 +128,9 @@ export class PathTracerApp extends EventDispatcher {
128
128
 
129
129
  // Resolution state
130
130
  this._resizeDebounceTimer = null;
131
+ this._resizeObserver = null;
132
+ this._lastObservedWidth = 0;
133
+ this._lastObservedHeight = 0;
131
134
 
132
135
  }
133
136
 
@@ -383,7 +386,7 @@ export class PathTracerApp extends EventDispatcher {
383
386
  }
384
387
 
385
388
  clearTimeout( this._resizeDebounceTimer );
386
- window.removeEventListener( 'resize', this.resizeHandler );
389
+ this._resizeObserver?.disconnect();
387
390
 
388
391
  this.isInitialized = false;
389
392
 
@@ -639,6 +642,9 @@ export class PathTracerApp extends EventDispatcher {
639
642
  const width = this.canvas.clientWidth;
640
643
  const height = this.canvas.clientHeight;
641
644
  if ( width === 0 || height === 0 ) return;
645
+ if ( width === this._lastObservedWidth && height === this._lastObservedHeight ) return;
646
+ this._lastObservedWidth = width;
647
+ this._lastObservedHeight = height;
642
648
 
643
649
  this.renderer.setPixelRatio( 1.0 );
644
650
  this.renderer.setSize( width, height, false );
@@ -1014,6 +1020,7 @@ export class PathTracerApp extends EventDispatcher {
1014
1020
 
1015
1021
  this._sdf = new SceneProcessor();
1016
1022
  this.assetLoader = new AssetLoader( this.meshScene, this.cameraManager.camera, this.cameraManager.controls );
1023
+ this.assetLoader.setRenderer( this.renderer );
1017
1024
  this.assetLoader.createFloorPlane();
1018
1025
 
1019
1026
  this.cameraManager.controls.addEventListener( 'change', () => {
@@ -1154,10 +1161,10 @@ export class PathTracerApp extends EventDispatcher {
1154
1161
 
1155
1162
  // Resize handling
1156
1163
  this.onResize();
1157
- this.resizeHandler = () => this.onResize();
1158
1164
  if ( this._autoResize ) {
1159
1165
 
1160
- window.addEventListener( 'resize', this.resizeHandler );
1166
+ this._resizeObserver = new ResizeObserver( () => this.onResize() );
1167
+ this._resizeObserver.observe( this.canvas );
1161
1168
 
1162
1169
  }
1163
1170
 
@@ -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
+
@@ -111,6 +111,7 @@ export class Display extends RenderStage {
111
111
  dispose() {
112
112
 
113
113
  this.displayMaterial?.dispose();
114
+ this.displayQuad?.dispose();
114
115
 
115
116
  }
116
117
 
@@ -699,7 +699,7 @@ export class InteractionManager extends EventDispatcher {
699
699
 
700
700
  this.select( event.object );
701
701
  app.refreshFrame();
702
- app.dispatchEvent( { type: 'objectSelected', object: event.object, uuid: event.uuid } );
702
+ app.dispatchEvent( { type: EngineEvents.OBJECT_SELECTED, object: event.object, uuid: event.uuid } );
703
703
 
704
704
  } );
705
705
 
@@ -707,7 +707,7 @@ export class InteractionManager extends EventDispatcher {
707
707
 
708
708
  this.select( null );
709
709
  app.refreshFrame();
710
- app.dispatchEvent( { type: 'objectDeselected', object: event.object, uuid: event.uuid } );
710
+ app.dispatchEvent( { type: EngineEvents.OBJECT_DESELECTED, object: event.object, uuid: event.uuid } );
711
711
 
712
712
  } );
713
713