rayzee 5.1.0 → 5.2.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.0",
3
+ "version": "5.2.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' );
@@ -142,7 +142,6 @@ export class RenderStage {
142
142
 
143
143
  const renderMode = context.getState( 'renderMode' ) || 0;
144
144
  const tileRenderingComplete = context.getState( 'tileRenderingComplete' );
145
- const frame = context.getState( 'frame' ) || 0;
146
145
 
147
146
  switch ( this.executionMode ) {
148
147
 
@@ -1,4 +1,4 @@
1
- import { Box3, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping, LinearMipmapLinearFilter,
1
+ import { Box3, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping,
2
2
  TextureLoader, Mesh, MeshStandardMaterial, MeshPhysicalMaterial, CircleGeometry, Points, PointsMaterial, LoadingManager, EventDispatcher
3
3
  } from 'three';
4
4
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
@@ -171,8 +171,6 @@ export class AssetLoader extends EventDispatcher {
171
171
  }
172
172
 
173
173
  texture.generateMipmaps = true;
174
- // texture.minFilter = LinearMipmapLinearFilter;
175
- // texture.magFilter = LinearFilter;
176
174
 
177
175
  this.applyEnvironmentToScene( texture );
178
176
  this.dispatchEvent( { type: 'load', texture } );
@@ -353,7 +351,7 @@ export class AssetLoader extends EventDispatcher {
353
351
 
354
352
  }
355
353
 
356
- async findAndLoadModelFromZip( zip, filename ) {
354
+ async findAndLoadModelFromZip( zip ) {
357
355
 
358
356
  const mainModelFiles = [
359
357
  'scene.gltf', 'scene.glb', 'model.gltf', 'model.glb',
@@ -459,7 +457,6 @@ export class AssetLoader extends EventDispatcher {
459
457
  if ( extension === 'gltf' ) {
460
458
 
461
459
  const gltfContent = strFromU8( fileContent );
462
- const gltfJson = JSON.parse( gltfContent );
463
460
  const manager = new LoadingManager();
464
461
  const gltfDir = filePath.split( '/' ).slice( 0, - 1 ).join( '/' );
465
462
 
@@ -1,4 +1,4 @@
1
- import { DataUtils, HalfFloatType, FloatType } from 'three';
1
+ import { DataUtils, HalfFloatType, FloatType, SRGBColorSpace } from 'three';
2
2
 
3
3
  /**
4
4
  * Binary search to find the closest index
@@ -38,18 +38,35 @@ function colorToLuminance( r, g, b ) {
38
38
 
39
39
  }
40
40
 
41
+ /**
42
+ * sRGB to linear conversion (IEC 61966-2-1 transfer function)
43
+ */
44
+ function sRGBToLinear( c ) {
45
+
46
+ return c <= 0.04045 ? c / 12.92 : ( ( c + 0.055 ) / 1.055 ) ** 2.4;
47
+
48
+ }
49
+
41
50
  /**
42
51
  * Extract Float32 RGBA pixel data from an environment map.
43
- * Handles HalfFloat/integer type conversion and Y-flip.
52
+ * Handles HalfFloat/integer type conversion, canvas extraction for
53
+ * non-DataTexture images (JPG/PNG), sRGB-to-linear conversion, and Y-flip.
44
54
  * @returns {{ floatData: Float32Array, width: number, height: number }}
45
55
  */
46
56
  export function extractFloatData( envMap ) {
47
57
 
48
- const { width, height, data } = envMap.image;
58
+ const { width, height } = envMap.image;
59
+ let data = envMap.image.data;
60
+ let needsSRGBToLinear = false;
49
61
 
62
+ // No CPU-accessible data — extract from HTMLImageElement / ImageBitmap via canvas
50
63
  if ( ! data ) {
51
64
 
52
- throw new Error( 'EquirectHDRInfo: Environment map must have CPU-accessible image data. Render target textures are not supported.' );
65
+ const canvas = new OffscreenCanvas( width, height );
66
+ const ctx = canvas.getContext( '2d' );
67
+ ctx.drawImage( envMap.image, 0, 0, width, height );
68
+ data = ctx.getImageData( 0, 0, width, height ).data;
69
+ needsSRGBToLinear = true;
53
70
 
54
71
  }
55
72
 
@@ -72,7 +89,7 @@ export function extractFloatData( envMap ) {
72
89
 
73
90
  } else {
74
91
 
75
- // Integer types (Uint8, Int16, etc.)
92
+ // Integer types (Uint8, Uint8Clamped, Int16, etc.)
76
93
  let maxIntValue;
77
94
  if ( data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array ) {
78
95
 
@@ -93,6 +110,26 @@ export function extractFloatData( envMap ) {
93
110
 
94
111
  }
95
112
 
113
+ // Also flag sRGB conversion for DataTextures explicitly marked as sRGB
114
+ if ( ! needsSRGBToLinear && envMap.colorSpace === SRGBColorSpace ) {
115
+
116
+ needsSRGBToLinear = true;
117
+
118
+ }
119
+
120
+ // Convert sRGB to linear so CDF luminance matches GPU-sampled linear values
121
+ if ( needsSRGBToLinear ) {
122
+
123
+ for ( let i = 0, l = floatData.length; i < l; i += 4 ) {
124
+
125
+ floatData[ i ] = sRGBToLinear( floatData[ i ] );
126
+ floatData[ i + 1 ] = sRGBToLinear( floatData[ i + 1 ] );
127
+ floatData[ i + 2 ] = sRGBToLinear( floatData[ i + 2 ] );
128
+
129
+ }
130
+
131
+ }
132
+
96
133
  // Remove Y-flip for CDF computation
97
134
  if ( envMap.flipY ) {
98
135
 
@@ -820,8 +820,8 @@ export class SceneProcessor {
820
820
 
821
821
  // Factory method for creating any additional scene elements
822
822
  // Currently returns an empty array by default
823
- const white = new Color( 0xffffff );
824
- const black = new Color( 0x000000 );
823
+ // const white = new Color( 0xffffff );
824
+ // const black = new Color( 0x000000 );
825
825
  return [
826
826
  // { position: new Vector3( - 4, 2, 0 ), radius: 0.8, material: { color: white, emissive: black, emissiveIntensity: 0, roughness: 1.0 } },
827
827
  // { position: new Vector3( - 1.5, 2, 0 ), radius: 0.8, material: { color: white, emissive: black, emissiveIntensity: 0, roughness: 1.0 } },
@@ -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 {
@@ -858,9 +858,9 @@ export class TextureCreator {
858
858
  */
859
859
  createMaterialRawData( materials ) {
860
860
 
861
- const pixelsRequired = TEXTURE_CONSTANTS.PIXELS_PER_MATERIAL;
862
- const dataInEachPixel = TEXTURE_CONSTANTS.RGBA_COMPONENTS;
863
- const dataLengthPerMaterial = pixelsRequired * dataInEachPixel;
861
+ // Layout is defined by MATERIAL_DATA_LAYOUT in EngineDefaults.js.
862
+ // The inline array below must match that layout exactly (positional order = canonical layout).
863
+ const dataLengthPerMaterial = MATERIAL_DATA_LAYOUT.FLOATS_PER_MATERIAL;
864
864
  const totalMaterials = materials.length;
865
865
 
866
866
  const size = totalMaterials * dataLengthPerMaterial;
@@ -879,19 +879,34 @@ export class TextureCreator {
879
879
  const bumpMapMatrices = mat.bumpMapMatrices ?? DEFAULT_TEXTURE_MATRIX;
880
880
  const displacementMapMatrices = mat.displacementMapMatrices ?? DEFAULT_TEXTURE_MATRIX;
881
881
 
882
+ // Slot order: shadow/culling → BxDF core → maps → extended → displacement → transforms
883
+ // Must match MATERIAL_DATA_LAYOUT in EngineDefaults.js exactly.
882
884
  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,
885
+ // Slot 0: shadow core (ior, transmission, thickness, emissiveIntensity)
885
886
  mat.ior, mat.transmission, mat.thickness, mat.emissiveIntensity,
887
+ // Slot 1: shadow (attenuationColor, attenuationDistance)
886
888
  mat.attenuationColor.r, mat.attenuationColor.g, mat.attenuationColor.b, mat.attenuationDistance,
889
+ // Slot 2: shadow + culling (opacity, side, transparent, alphaTest)
890
+ mat.opacity, mat.side, mat.transparent, mat.alphaTest,
891
+ // Slot 3: shadow (alphaMode, depthWrite, normalScale)
892
+ mat.alphaMode, mat.depthWrite, mat.normalScale?.x ?? 1, mat.normalScale?.y ?? 1,
893
+ // Slot 4: BxDF core (color, metalness)
894
+ mat.color.r, mat.color.g, mat.color.b, mat.metalness,
895
+ // Slot 5: BxDF core (emissive, roughness)
896
+ mat.emissive.r, mat.emissive.g, mat.emissive.b, mat.roughness,
897
+ // Slot 6: map indices A (albedo, normal, roughness, metalness)
898
+ mat.map, mat.normalMap, mat.roughnessMap, mat.metalnessMap,
899
+ // Slot 7: map indices B (emissive, bump, clearcoat, clearcoatRoughness)
900
+ mat.emissiveMap, mat.bumpMap, mat.clearcoat, mat.clearcoatRoughness,
901
+ // Slot 8: extended BxDF (dispersion, visible, sheen, sheenRoughness)
887
902
  mat.dispersion, mat.visible, mat.sheen, mat.sheenRoughness,
903
+ // Slot 9: extended BxDF (sheenColor, reserved)
888
904
  mat.sheenColor.r, mat.sheenColor.g, mat.sheenColor.b, 1,
905
+ // Slot 10: extended BxDF (specularIntensity, specularColor)
889
906
  mat.specularIntensity, mat.specularColor.r, mat.specularColor.g, mat.specularColor.b,
907
+ // Slot 11: extended BxDF (iridescence)
890
908
  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,
909
+ // Slot 12: displacement
895
910
  mat.bumpScale, mat.displacementScale, mat.displacementMap, 0,
896
911
  mapMatrix[ 0 ], mapMatrix[ 1 ], mapMatrix[ 2 ], mapMatrix[ 3 ],
897
912
  mapMatrix[ 4 ], mapMatrix[ 5 ], mapMatrix[ 6 ], 1,
@@ -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 ) );
@@ -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
 
package/src/TSL/Common.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  MaterialClassification,
6
6
  MISStrategy,
7
7
  RayTracingMaterial,
8
+ ShadowMaterial,
8
9
  } from './Struct.js';
9
10
 
10
11
  export const PI = 3.14159;
@@ -16,7 +17,11 @@ export const MIN_CLEARCOAT_ROUGHNESS = 0.089;
16
17
  export const MAX_ROUGHNESS = 1.0;
17
18
  export const MIN_PDF = 0.001;
18
19
  export const REC709_LUMINANCE_COEFFICIENTS = vec3( 0.2126, 0.7152, 0.0722 );
19
- export const MATERIAL_SLOTS = 27;
20
+ import { MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
21
+
22
+ export const MATERIAL_SLOTS = MATERIAL_DATA_LAYOUT.SLOTS_PER_MATERIAL;
23
+ export const MATERIAL_SLOT = MATERIAL_DATA_LAYOUT.SLOT;
24
+ const S = MATERIAL_SLOT;
20
25
 
21
26
  // XYZ to sRGB color space conversion matrix
22
27
  export const XYZ_TO_REC709 = mat3(
@@ -313,33 +318,33 @@ export const arrayToMat3 = wgslFn( `
313
318
 
314
319
  export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
315
320
 
316
- const data0 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 0 ), int( MATERIAL_SLOTS ) ).toVar();
317
- const data1 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 1 ), int( MATERIAL_SLOTS ) ).toVar();
318
- const data2 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 2 ), int( MATERIAL_SLOTS ) ).toVar();
319
- const data3 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 3 ), int( MATERIAL_SLOTS ) ).toVar();
320
- const data4 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 4 ), int( MATERIAL_SLOTS ) ).toVar();
321
- const data5 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 5 ), int( MATERIAL_SLOTS ) ).toVar();
322
- const data6 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 6 ), int( MATERIAL_SLOTS ) ).toVar();
323
- const data7 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 7 ), int( MATERIAL_SLOTS ) ).toVar();
324
- const data8 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 8 ), int( MATERIAL_SLOTS ) ).toVar();
325
- const data9 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 9 ), int( MATERIAL_SLOTS ) ).toVar();
326
- const data10 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 10 ), int( MATERIAL_SLOTS ) ).toVar();
327
- const data11 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 11 ), int( MATERIAL_SLOTS ) ).toVar();
328
- const data12 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 12 ), int( MATERIAL_SLOTS ) ).toVar();
329
- const data13 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 13 ), int( MATERIAL_SLOTS ) ).toVar();
330
- const data14 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 14 ), int( MATERIAL_SLOTS ) ).toVar();
331
- const data15 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 15 ), int( MATERIAL_SLOTS ) ).toVar();
332
- const data16 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 16 ), int( MATERIAL_SLOTS ) ).toVar();
333
- const data17 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 17 ), int( MATERIAL_SLOTS ) ).toVar();
334
- const data18 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 18 ), int( MATERIAL_SLOTS ) ).toVar();
335
- const data19 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 19 ), int( MATERIAL_SLOTS ) ).toVar();
336
- const data20 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 20 ), int( MATERIAL_SLOTS ) ).toVar();
337
- const data21 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 21 ), int( MATERIAL_SLOTS ) ).toVar();
338
- const data22 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 22 ), int( MATERIAL_SLOTS ) ).toVar();
339
- const data23 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 23 ), int( MATERIAL_SLOTS ) ).toVar();
340
- const data24 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 24 ), int( MATERIAL_SLOTS ) ).toVar();
341
- const data25 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 25 ), int( MATERIAL_SLOTS ) ).toVar();
342
- const data26 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 26 ), int( MATERIAL_SLOTS ) ).toVar();
321
+ const data0 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.COLOR_METALNESS ), int( MATERIAL_SLOTS ) ).toVar();
322
+ const data1 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EMISSIVE_ROUGHNESS ), int( MATERIAL_SLOTS ) ).toVar();
323
+ const data2 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.IOR_TRANSMISSION ), int( MATERIAL_SLOTS ) ).toVar();
324
+ const data3 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ATTENUATION ), int( MATERIAL_SLOTS ) ).toVar();
325
+ const data4 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.DISPERSION_SHEEN ), int( MATERIAL_SLOTS ) ).toVar();
326
+ const data5 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SHEEN_COLOR ), int( MATERIAL_SLOTS ) ).toVar();
327
+ const data6 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SPECULAR ), int( MATERIAL_SLOTS ) ).toVar();
328
+ const data7 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.IRIDESCENCE ), int( MATERIAL_SLOTS ) ).toVar();
329
+ const data8 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.MAP_INDICES_A ), int( MATERIAL_SLOTS ) ).toVar();
330
+ const data9 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.MAP_INDICES_B ), int( MATERIAL_SLOTS ) ).toVar();
331
+ const data10 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.OPACITY_ALPHA ), int( MATERIAL_SLOTS ) ).toVar();
332
+ const data11 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ALPHA_MODE ), int( MATERIAL_SLOTS ) ).toVar();
333
+ const data12 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.BUMP_DISPLACEMENT ), int( MATERIAL_SLOTS ) ).toVar();
334
+ const data13 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ALBEDO_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
335
+ const data14 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ALBEDO_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
336
+ const data15 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.NORMAL_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
337
+ const data16 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.NORMAL_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
338
+ const data17 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ROUGHNESS_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
339
+ const data18 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ROUGHNESS_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
340
+ const data19 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.METALNESS_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
341
+ const data20 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.METALNESS_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
342
+ const data21 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EMISSIVE_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
343
+ const data22 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EMISSIVE_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
344
+ const data23 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.BUMP_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
345
+ const data24 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.BUMP_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
346
+ const data25 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.DISPLACEMENT_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
347
+ const data26 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.DISPLACEMENT_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
343
348
 
344
349
  return RayTracingMaterial( {
345
350
  color: vec4( data0.rgb, 1.0 ),
@@ -390,6 +395,35 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
390
395
 
391
396
  } );
392
397
 
398
+ // ── Shadow material thin reader (7 slot reads instead of 27) ─────────────
399
+ // Only fetches fields needed by traceShadowRay: alpha, transmission, attenuation, albedo transform.
400
+
401
+ export const getShadowMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
402
+
403
+ const data2 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.IOR_TRANSMISSION ), int( MATERIAL_SLOTS ) ).toVar();
404
+ const data3 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ATTENUATION ), int( MATERIAL_SLOTS ) ).toVar();
405
+ const data8 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.MAP_INDICES_A ), int( MATERIAL_SLOTS ) ).toVar();
406
+ const data10 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.OPACITY_ALPHA ), int( MATERIAL_SLOTS ) ).toVar();
407
+ const data11 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ALPHA_MODE ), int( MATERIAL_SLOTS ) ).toVar();
408
+ const data13 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ALBEDO_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
409
+ const data14 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.ALBEDO_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
410
+
411
+ return ShadowMaterial( {
412
+ color: vec4( 1.0 ), // Shadow path never samples full textures; color.a is always 1.0
413
+ ior: data2.r,
414
+ transmission: data2.g,
415
+ attenuationColor: data3.rgb,
416
+ attenuationDistance: data3.a,
417
+ albedoMapIndex: int( data8.r ),
418
+ opacity: data10.r,
419
+ transparent: data10.b,
420
+ alphaTest: data10.a,
421
+ alphaMode: int( data11.r ),
422
+ albedoTransform: arrayToMat3( { data1: data13, data2: data14 } ),
423
+ } );
424
+
425
+ } );
426
+
393
427
  // ── Edge-stopping weight (normal + depth) ──────────────────────────────────
394
428
  // Used by ASVGF and SSRC for temporal/spatial reprojection edge-stopping.
395
429
 
@@ -28,7 +28,7 @@ import {
28
28
  } from 'three/tsl';
29
29
 
30
30
  import { struct } from './structProxy.js';
31
- import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic } from './Common.js';
31
+ import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT } from './Common.js';
32
32
  import { RandomValue } from './Random.js';
33
33
  import { calculateMaterialPDF } from './LightsSampling.js';
34
34
 
@@ -326,9 +326,8 @@ export const calculateEmissiveLightPdf = Fn( ( [
326
326
  const area = triangleArea( triData.v0, triData.v1, triData.v2 );
327
327
 
328
328
  // Targeted material read: only fetch emissive data (2 vec4s instead of full 27)
329
- const MATERIAL_SLOTS = int( 27 );
330
- const matData1 = getDatafromStorageBuffer( materialBuffer, triData.materialIndex, int( 1 ), MATERIAL_SLOTS );
331
- const matData2 = getDatafromStorageBuffer( materialBuffer, triData.materialIndex, int( 2 ), MATERIAL_SLOTS );
329
+ const matData1 = getDatafromStorageBuffer( materialBuffer, triData.materialIndex, int( MATERIAL_SLOT.EMISSIVE_ROUGHNESS ), MATERIAL_SLOTS );
330
+ const matData2 = getDatafromStorageBuffer( materialBuffer, triData.materialIndex, int( MATERIAL_SLOT.IOR_TRANSMISSION ), MATERIAL_SLOTS );
332
331
  const avgEmissive = matData1.x.add( matData1.y ).add( matData1.z ).div( 3.0 );
333
332
  const power = max( avgEmissive.mul( matData2.a ).mul( area ), float( 1e-10 ) );
334
333
  const selectionPdf = power.div( max( emissiveTotalPower, float( 1e-10 ) ) );