rayzee 5.1.1 → 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.1.1",
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",
@@ -80,6 +80,7 @@ export const ENGINE_DEFAULTS = {
80
80
  renderLimitMode: 'frames',
81
81
  renderTimeLimit: 30,
82
82
  renderMode: 0,
83
+ enableAlphaShadows: false,
83
84
  tiles: 3,
84
85
  tilesHelper: false,
85
86
  showLightHelper: false,
@@ -337,6 +338,80 @@ export const TRIANGLE_DATA_LAYOUT = {
337
338
  UV_C_MAT_OFFSET: 28
338
339
  };
339
340
 
341
+ // Material data layout constants — single source of truth for material buffer offsets.
342
+ // Shared between CPU writers (TextureCreator, MaterialDataManager) and GPU readers (Common.js getMaterial).
343
+ export const MATERIAL_DATA_LAYOUT = {
344
+
345
+ SLOTS_PER_MATERIAL: 27, // vec4 slots per material
346
+ FLOATS_PER_MATERIAL: 108, // total floats per material (27 × 4)
347
+
348
+ // ── Flat float offsets (CPU side) ────────────────────────────────
349
+ // Used as: data[ materialIndex * FLOATS_PER_MATERIAL + offset ]
350
+ // Ordered for cache-line coherence: shadow/culling → BxDF core → maps → extended → transforms
351
+
352
+ // Slot 0: ior + transmission + thickness + emissiveIntensity [shadow]
353
+ IOR: 0, TRANSMISSION: 1, THICKNESS: 2, EMISSIVE_INTENSITY: 3,
354
+ // Slot 1: attenuationColor.rgb + attenuationDistance [shadow]
355
+ ATTENUATION_COLOR: 4, ATTENUATION_DISTANCE: 7,
356
+ // Slot 2: opacity + side + transparent + alphaTest [shadow + culling]
357
+ OPACITY: 8, SIDE: 9, TRANSPARENT: 10, ALPHA_TEST: 11,
358
+ // Slot 3: alphaMode + depthWrite + normalScale [shadow]
359
+ ALPHA_MODE: 12, DEPTH_WRITE: 13, NORMAL_SCALE: 14,
360
+ // Slot 4: color.rgb + metalness [BxDF core]
361
+ COLOR: 16, METALNESS: 19,
362
+ // Slot 5: emissive.rgb + roughness [BxDF core]
363
+ EMISSIVE: 20, ROUGHNESS: 23,
364
+ // Slot 6: map indices (albedo, normal, roughness, metalness) [maps]
365
+ ALBEDO_MAP_INDEX: 24, NORMAL_MAP_INDEX: 25, ROUGHNESS_MAP_INDEX: 26, METALNESS_MAP_INDEX: 27,
366
+ // Slot 7: map indices (emissive, bump) + clearcoat [maps]
367
+ EMISSIVE_MAP_INDEX: 28, BUMP_MAP_INDEX: 29, CLEARCOAT: 30, CLEARCOAT_ROUGHNESS: 31,
368
+ // Slot 8: dispersion + visible + sheen + sheenRoughness [extended BxDF]
369
+ DISPERSION: 32, VISIBLE: 33, SHEEN: 34, SHEEN_ROUGHNESS: 35,
370
+ // Slot 9: sheenColor.rgb + (reserved) [extended BxDF]
371
+ SHEEN_COLOR: 36,
372
+ // Slot 10: specularIntensity + specularColor.rgb [extended BxDF]
373
+ SPECULAR_INTENSITY: 40, SPECULAR_COLOR: 41,
374
+ // Slot 11: iridescence + iridescenceIOR + iridescenceThicknessRange [extended BxDF]
375
+ IRIDESCENCE: 44, IRIDESCENCE_IOR: 45, IRIDESCENCE_THICKNESS_RANGE: 46,
376
+ // Slot 12: bumpScale + displacementScale + displacementMapIndex + (padding)
377
+ BUMP_SCALE: 48, DISPLACEMENT_SCALE: 49, DISPLACEMENT_MAP_INDEX: 50,
378
+
379
+ // ── Transform float offsets (8 floats each: 7 matrix values + 1 padding) ──
380
+ ALBEDO_TRANSFORM: 52,
381
+ NORMAL_TRANSFORM: 60,
382
+ ROUGHNESS_TRANSFORM: 68,
383
+ METALNESS_TRANSFORM: 76,
384
+ EMISSIVE_TRANSFORM: 84,
385
+ BUMP_TRANSFORM: 92,
386
+ DISPLACEMENT_TRANSFORM: 100,
387
+
388
+ // ── Vec4 slot indices (GPU/TSL side) ─────────────────────────────
389
+ // Used with getDatafromStorageBuffer( buf, matIdx, int(slot), int(SLOTS_PER_MATERIAL) )
390
+ SLOT: {
391
+ IOR_TRANSMISSION: 0, // [shadow] ior, transmission, thickness, emissiveIntensity
392
+ ATTENUATION: 1, // [shadow] attenuationColor, attenuationDistance
393
+ OPACITY_ALPHA: 2, // [shadow+culling] opacity, side, transparent, alphaTest
394
+ ALPHA_MODE: 3, // [shadow] alphaMode, depthWrite, normalScale
395
+ COLOR_METALNESS: 4, // [BxDF] color.rgb, metalness
396
+ EMISSIVE_ROUGHNESS: 5, // [BxDF] emissive.rgb, roughness
397
+ MAP_INDICES_A: 6, // [maps] albedo, normal, roughness, metalness
398
+ MAP_INDICES_B: 7, // [maps] emissive, bump, clearcoat, clearcoatRoughness
399
+ DISPERSION_SHEEN: 8, // [extended] dispersion, visible, sheen, sheenRoughness
400
+ SHEEN_COLOR: 9, // [extended] sheenColor, reserved
401
+ SPECULAR: 10, // [extended] specularIntensity, specularColor
402
+ IRIDESCENCE: 11, // [extended] iridescence, iridescenceIOR, iridescenceThicknessRange
403
+ BUMP_DISPLACEMENT: 12, // bumpScale, displacementScale, displacementMapIndex
404
+ ALBEDO_TRANSFORM_A: 13, ALBEDO_TRANSFORM_B: 14,
405
+ NORMAL_TRANSFORM_A: 15, NORMAL_TRANSFORM_B: 16,
406
+ ROUGHNESS_TRANSFORM_A: 17, ROUGHNESS_TRANSFORM_B: 18,
407
+ METALNESS_TRANSFORM_A: 19, METALNESS_TRANSFORM_B: 20,
408
+ EMISSIVE_TRANSFORM_A: 21, EMISSIVE_TRANSFORM_B: 22,
409
+ BUMP_TRANSFORM_A: 23, BUMP_TRANSFORM_B: 24,
410
+ DISPLACEMENT_TRANSFORM_A: 25, DISPLACEMENT_TRANSFORM_B: 26,
411
+ },
412
+
413
+ };
414
+
340
415
  // BVH node leaf markers
341
416
  export const BVH_LEAF_MARKERS = {
342
417
  TRIANGLE_LEAF: - 1, // Leaf containing triangle references
@@ -364,14 +439,14 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
364
439
  // Render mode configurations
365
440
  export const FINAL_RENDER_CONFIG = {
366
441
  maxSamples: 30, bounces: 20, transmissiveBounces: 8, samplesPerPixel: 1,
367
- renderMode: 1, tiles: 3, tilesHelper: false,
442
+ renderMode: 1, enableAlphaShadows: true, tiles: 3, tilesHelper: false,
368
443
  enableOIDN: true, oidnQuality: 'balance',
369
444
  interactionModeEnabled: false,
370
445
  };
371
446
 
372
447
  export const PREVIEW_RENDER_CONFIG = {
373
448
  maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
374
- samplesPerPixel: ENGINE_DEFAULTS.samplesPerPixel, renderMode: ENGINE_DEFAULTS.renderMode,
449
+ samplesPerPixel: ENGINE_DEFAULTS.samplesPerPixel, renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
375
450
  transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
376
451
  tiles: ENGINE_DEFAULTS.tiles, tilesHelper: ENGINE_DEFAULTS.tilesHelper,
377
452
  enableOIDN: false, oidnQuality: 'fast',
@@ -730,6 +730,7 @@ export class PathTracerApp extends EventDispatcher {
730
730
  }, { silent: true } );
731
731
 
732
732
  this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
733
+ this.stages.pathTracer?.setUniform( 'enableAlphaShadows', config.enableAlphaShadows ?? false );
733
734
  this.stages.pathTracer?.tileManager?.setTileCount( config.tiles );
734
735
 
735
736
  const tileHelper = this.overlayManager?.getHelper( 'tiles' );
@@ -1013,6 +1014,7 @@ export class PathTracerApp extends EventDispatcher {
1013
1014
 
1014
1015
  this._sdf = new SceneProcessor();
1015
1016
  this.assetLoader = new AssetLoader( this.meshScene, this.cameraManager.camera, this.cameraManager.controls );
1017
+ this.assetLoader.setRenderer( this.renderer );
1016
1018
  this.assetLoader.createFloorPlane();
1017
1019
 
1018
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
 
@@ -17,6 +17,7 @@ import { TextureNode } from 'three/webgpu';
17
17
  import { LinearFilter, DataArrayTexture } from 'three';
18
18
  import { pathTracerMain } from '../TSL/PathTracer.js';
19
19
  import { setMeshVisibilityBuffer } from '../TSL/BVHTraversal.js';
20
+ import { setShadowAlbedoMaps, setAlphaShadowsUniform } from '../TSL/LightsDirect.js';
20
21
  import { BuildTimer } from './BuildTimer.js';
21
22
 
22
23
  const WG_SIZE = 8;
@@ -192,6 +193,9 @@ export class ShaderBuilder {
192
193
  // Set per-mesh visibility buffer (module-level in BVHTraversal.js, read during graph construction)
193
194
  setMeshVisibilityBuffer( stage.meshVisibilityStorageNode );
194
195
 
196
+ // Set alpha-shadow uniform (module-level in LightsDirect.js, read at runtime)
197
+ setAlphaShadowsUniform( stage.uniforms.get( 'enableAlphaShadows' ) );
198
+
195
199
  const envTex = texture( stage.environment.environmentTexture );
196
200
 
197
201
  // Adaptive sampling texture
@@ -228,6 +232,11 @@ export class ShaderBuilder {
228
232
  const emissiveMapsTex = mat.emissiveMaps ? texture( mat.emissiveMaps ) : createArrayPlaceholder();
229
233
  const displacementMapsTex = mat.displacementMaps ? texture( mat.displacementMaps ) : createArrayPlaceholder();
230
234
 
235
+ // Set albedo texture array for alpha-aware shadow rays (module-level in LightsDirect.js).
236
+ // Always pass the texture node (real or placeholder) so alpha-cutout code is emitted
237
+ // into the shader at graph construction time. Runtime albedoMapIndex >= 0 guards sampling.
238
+ setShadowAlbedoMaps( albedoMapsTex );
239
+
231
240
  const result = {
232
241
  triStorage, bvhStorage, matStorage, emissiveTriStorage, lightBVHStorage,
233
242
  envTex, adaptiveSamplingTex, marginalCDFStorage, conditionalCDFStorage,
@@ -1,5 +1,5 @@
1
1
  import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace } from "three";
2
- import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX } from '../EngineDefaults.js';
2
+ import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
3
3
 
4
4
  // Canvas pooling for efficient reuse of canvas elements
5
5
  class CanvasPool {
@@ -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
 
@@ -858,9 +865,9 @@ export class TextureCreator {
858
865
  */
859
866
  createMaterialRawData( materials ) {
860
867
 
861
- const pixelsRequired = TEXTURE_CONSTANTS.PIXELS_PER_MATERIAL;
862
- const dataInEachPixel = TEXTURE_CONSTANTS.RGBA_COMPONENTS;
863
- const dataLengthPerMaterial = pixelsRequired * dataInEachPixel;
868
+ // Layout is defined by MATERIAL_DATA_LAYOUT in EngineDefaults.js.
869
+ // The inline array below must match that layout exactly (positional order = canonical layout).
870
+ const dataLengthPerMaterial = MATERIAL_DATA_LAYOUT.FLOATS_PER_MATERIAL;
864
871
  const totalMaterials = materials.length;
865
872
 
866
873
  const size = totalMaterials * dataLengthPerMaterial;
@@ -879,19 +886,34 @@ export class TextureCreator {
879
886
  const bumpMapMatrices = mat.bumpMapMatrices ?? DEFAULT_TEXTURE_MATRIX;
880
887
  const displacementMapMatrices = mat.displacementMapMatrices ?? DEFAULT_TEXTURE_MATRIX;
881
888
 
889
+ // Slot order: shadow/culling → BxDF core → maps → extended → displacement → transforms
890
+ // Must match MATERIAL_DATA_LAYOUT in EngineDefaults.js exactly.
882
891
  const materialData = [
883
- mat.color.r, mat.color.g, mat.color.b, mat.metalness,
884
- mat.emissive.r, mat.emissive.g, mat.emissive.b, mat.roughness,
892
+ // Slot 0: shadow core (ior, transmission, thickness, emissiveIntensity)
885
893
  mat.ior, mat.transmission, mat.thickness, mat.emissiveIntensity,
894
+ // Slot 1: shadow (attenuationColor, attenuationDistance)
886
895
  mat.attenuationColor.r, mat.attenuationColor.g, mat.attenuationColor.b, mat.attenuationDistance,
896
+ // Slot 2: shadow + culling (opacity, side, transparent, alphaTest)
897
+ mat.opacity, mat.side, mat.transparent, mat.alphaTest,
898
+ // Slot 3: shadow (alphaMode, depthWrite, normalScale)
899
+ mat.alphaMode, mat.depthWrite, mat.normalScale?.x ?? 1, mat.normalScale?.y ?? 1,
900
+ // Slot 4: BxDF core (color, metalness)
901
+ mat.color.r, mat.color.g, mat.color.b, mat.metalness,
902
+ // Slot 5: BxDF core (emissive, roughness)
903
+ mat.emissive.r, mat.emissive.g, mat.emissive.b, mat.roughness,
904
+ // Slot 6: map indices A (albedo, normal, roughness, metalness)
905
+ mat.map, mat.normalMap, mat.roughnessMap, mat.metalnessMap,
906
+ // Slot 7: map indices B (emissive, bump, clearcoat, clearcoatRoughness)
907
+ mat.emissiveMap, mat.bumpMap, mat.clearcoat, mat.clearcoatRoughness,
908
+ // Slot 8: extended BxDF (dispersion, visible, sheen, sheenRoughness)
887
909
  mat.dispersion, mat.visible, mat.sheen, mat.sheenRoughness,
910
+ // Slot 9: extended BxDF (sheenColor, reserved)
888
911
  mat.sheenColor.r, mat.sheenColor.g, mat.sheenColor.b, 1,
912
+ // Slot 10: extended BxDF (specularIntensity, specularColor)
889
913
  mat.specularIntensity, mat.specularColor.r, mat.specularColor.g, mat.specularColor.b,
914
+ // Slot 11: extended BxDF (iridescence)
890
915
  mat.iridescence, mat.iridescenceIOR, mat.iridescenceThicknessRange[ 0 ], mat.iridescenceThicknessRange[ 1 ],
891
- mat.map, mat.normalMap, mat.roughnessMap, mat.metalnessMap,
892
- mat.emissiveMap, mat.bumpMap, mat.clearcoat, mat.clearcoatRoughness,
893
- mat.opacity, mat.side, mat.transparent, mat.alphaTest,
894
- mat.alphaMode, mat.depthWrite, mat.normalScale?.x ?? 1, mat.normalScale?.y ?? 1,
916
+ // Slot 12: displacement
895
917
  mat.bumpScale, mat.displacementScale, mat.displacementMap, 0,
896
918
  mapMatrix[ 0 ], mapMatrix[ 1 ], mapMatrix[ 2 ], mapMatrix[ 3 ],
897
919
  mapMatrix[ 4 ], mapMatrix[ 5 ], mapMatrix[ 6 ], 1,
@@ -1243,6 +1265,109 @@ export class TextureCreator {
1243
1265
 
1244
1266
  }
1245
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
+
1246
1371
  createFallbackTexture() {
1247
1372
 
1248
1373
  const data = new Uint8Array( [ 255, 255, 255, 255 ] );
@@ -1276,3 +1401,14 @@ export class TextureCreator {
1276
1401
  }
1277
1402
 
1278
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
+
@@ -31,6 +31,7 @@ const SETTING_ROUTES = {
31
31
  anamorphicRatio: { uniform: 'anamorphicRatio', reset: true },
32
32
  samplingTechnique: { uniform: 'samplingTechnique', reset: true },
33
33
  fireflyThreshold: { uniform: 'fireflyThreshold', reset: true },
34
+ enableAlphaShadows: { uniform: 'enableAlphaShadows', reset: true },
34
35
  enableEmissiveTriangleSampling: { uniform: 'enableEmissiveTriangleSampling', reset: true },
35
36
  emissiveBoost: { uniform: 'emissiveBoost', reset: true },
36
37
  visMode: { uniform: 'visMode', reset: true },
@@ -26,7 +26,7 @@ import {
26
26
  } from 'three/tsl';
27
27
 
28
28
  import { Ray, HitInfo } from './Struct.js';
29
- import { getDatafromStorageBuffer, MATERIAL_SLOTS } from './Common.js';
29
+ import { getDatafromStorageBuffer, MATERIAL_SLOTS, MATERIAL_SLOT } from './Common.js';
30
30
  import { RandomPointInCircle } from './Random.js';
31
31
 
32
32
  // ================================================================================
@@ -136,7 +136,7 @@ const fastRayAABBDst = wgslFn( `
136
136
  // Per-mesh visibility handled at BLAS-pointer level; material visibility always 1.
137
137
  export const passesSideCulling = Fn( ( [ materialIndex, rayDirection, normal, materialBuffer ] ) => {
138
138
 
139
- const sideData = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 10 ), int( MATERIAL_SLOTS ) );
139
+ const sideData = getDatafromStorageBuffer( materialBuffer, materialIndex, int( MATERIAL_SLOT.OPACITY_ALPHA ), int( MATERIAL_SLOTS ) );
140
140
  const side = int( sideData.g );
141
141
  const rayDotNormal = rayDirection.dot( normal );
142
142
  const doubleSide = side.equal( int( 2 ) );
@@ -268,7 +268,7 @@ export const traverseBVH = Fn( ( [
268
268
  } );
269
269
 
270
270
  // If we found a very close hit, we can terminate early
271
- If( closestHit.didHit.and( closestHit.dst.lessThan( 1e-6 ) ), () => {
271
+ If( closestHit.didHit.and( closestHit.dst.lessThan( 0.001 ) ), () => {
272
272
 
273
273
  Break();
274
274
 
@@ -451,6 +451,12 @@ export const traverseBVHShadow = Fn( ( [
451
451
  closestHit.hitPoint.assign( ray.origin.add( ray.direction.mul( triResult.x ) ) );
452
452
  closestHit.normal.assign( normalize( cross( pB.sub( pA ), pC.sub( pA ) ) ) );
453
453
 
454
+ // Store barycentrics + triangle index for deferred UV computation.
455
+ // Actual UV interpolation happens in traceShadowRay only when
456
+ // the material needs alpha testing — zero overhead for opaque hits.
457
+ closestHit.uv.assign( vec2( triResult.y, triResult.z ) );
458
+ closestHit.triangleIndex.assign( triIndex );
459
+
454
460
  // Shadow ray only needs any hit — skip remaining triangles in leaf
455
461
  Break();
456
462