rayzee 5.1.1 → 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.1",
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' );
@@ -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 ) );
@@ -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
 
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
 
@@ -356,8 +356,7 @@ export const TraceDebugMode = Fn( ( [
356
356
  const bounceDir = cosineWeightedSample( { N: normalA, xi } ).toVar();
357
357
 
358
358
  // Trace secondary ray from the hit point (offset along normal to avoid self-intersection)
359
- const debugEps = max( float( 1e-4 ), length( hitInfo.hitPoint ).mul( 1e-6 ) );
360
- const bounceOrigin = hitInfo.hitPoint.add( normalA.mul( debugEps ) ).toVar();
359
+ const bounceOrigin = hitInfo.hitPoint.add( normalA.mul( 0.001 ) ).toVar();
361
360
  const bounceRay = Ray( { origin: bounceOrigin, direction: bounceDir } );
362
361
 
363
362
  const bounceHit = HitInfo.wrap( traverseBVH(
@@ -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 ) ) );
@@ -257,7 +257,7 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
257
257
  const t = dot( light.position.sub( rayOrigin ), normal ).mul( invDenom ).toVar();
258
258
 
259
259
  // Skip intersections behind the ray
260
- If( t.greaterThan( 1e-5 ), () => {
260
+ If( t.greaterThan( 0.001 ), () => {
261
261
 
262
262
  // Optimized rectangle test using vector rejection
263
263
  const hitPoint = rayOrigin.add( rayDirection.mul( t ) );
@@ -25,10 +25,11 @@ import {
25
25
  clamp,
26
26
  smoothstep,
27
27
  select,
28
+ texture,
28
29
  } from 'three/tsl';
29
30
 
30
- import { Ray, RayTracingMaterial, HitInfo, DirectionSample, MaterialCache } from './Struct.js';
31
- import { PI, TWO_PI, EPSILON, REC709_LUMINANCE_COEFFICIENTS, powerHeuristic, getMaterial } from './Common.js';
31
+ import { Ray, ShadowMaterial, HitInfo, DirectionSample, MaterialCache } from './Struct.js';
32
+ import { PI, TWO_PI, EPSILON, REC709_LUMINANCE_COEFFICIENTS, powerHeuristic, getShadowMaterial, getDatafromStorageBuffer } from './Common.js';
32
33
  import { fresnelSchlickFloat } from './Fresnel.js';
33
34
  import { iorToFresnel0 } from './Fresnel.js';
34
35
  import {
@@ -37,6 +38,33 @@ import {
37
38
  } from './LightsCore.js';
38
39
  import { calculateBeerLawAbsorption, calculateShadowTransmittance } from './MaterialTransmission.js';
39
40
  import { RandomValue } from './Random.js';
41
+ import { getTransformedUV } from './TextureSampling.js';
42
+
43
+ // Module-level state for alpha-cutout shadow testing.
44
+ // Set by ShaderBuilder before graph construction (same pattern as _meshVisibilityBuffer in BVHTraversal.js).
45
+ let _shadowAlbedoMaps = null;
46
+ let _enableAlphaShadows = null;
47
+
48
+ /**
49
+ * Set the albedo texture array node for alpha-aware shadow rays.
50
+ * Must be called before the shader graph is constructed.
51
+ * @param {TextureNode} maps - TSL texture node for the albedo array
52
+ */
53
+ export function setShadowAlbedoMaps( maps ) {
54
+
55
+ _shadowAlbedoMaps = maps;
56
+
57
+ }
58
+
59
+ /**
60
+ * Set the runtime uniform node that toggles alpha-cutout shadows.
61
+ * @param {UniformNode} node - TSL int uniform (0 = disabled, 1 = enabled)
62
+ */
63
+ export function setAlphaShadowsUniform( node ) {
64
+
65
+ _enableAlphaShadows = node;
66
+
67
+ }
40
68
 
41
69
  // ================================================================================
42
70
  // SHADOW RAY MATERIAL TRANSPARENCY
@@ -102,11 +130,82 @@ export const traceShadowRay = Fn( ( [
102
130
 
103
131
  } );
104
132
 
105
- // Fetch material for the hit surface
106
- const shadowMaterial = RayTracingMaterial.wrap( getMaterial( shadowHit.materialIndex, materialBuffer ) );
133
+ // Fetch material for the hit surface (thin reader: 7 slots instead of 27)
134
+ const shadowMaterial = ShadowMaterial.wrap( getShadowMaterial( shadowHit.materialIndex, materialBuffer ) );
135
+
136
+ // ---------------------------------------------------------------
137
+ // Alpha-cutout handling (MASK / BLEND with albedo texture alpha)
138
+ // Gated by runtime uniform + alphaMode check — zero overhead for opaque materials.
139
+ // UV computation deferred here from BVH traversal: barycentrics stored in shadowHit.uv,
140
+ // triangle index in shadowHit.triangleIndex. Actual UV interpolation only when needed.
141
+ // ---------------------------------------------------------------
142
+ const alphaCutout = tslBool( false ).toVar();
143
+
144
+ if ( _enableAlphaShadows ) If( _enableAlphaShadows.equal( int( 1 ) ), () => {
145
+
146
+ // Sample texture alpha once (shared by MASK and BLEND paths).
147
+ // Deferred UV: barycentrics in shadowHit.uv, triangle index in shadowHit.triangleIndex.
148
+ const texAlpha = float( 1.0 ).toVar();
149
+
150
+ if ( _shadowAlbedoMaps ) {
151
+
152
+ If( shadowMaterial.albedoMapIndex.greaterThanEqual( int( 0 ) ), () => {
153
+
154
+ const baryU = shadowHit.uv.x;
155
+ const baryV = shadowHit.uv.y;
156
+ const baryW = float( 1.0 ).sub( baryU ).sub( baryV );
157
+ const TRI_STRIDE = int( 8 );
158
+ const uvData1 = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 6 ), TRI_STRIDE );
159
+ const uvData2 = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 7 ), TRI_STRIDE );
160
+ const hitUV = uvData1.xy.mul( baryW ).add( uvData1.zw.mul( baryU ) ).add( uvData2.xy.mul( baryV ) );
161
+ const albedoUV = getTransformedUV( { uv: hitUV, transform: shadowMaterial.albedoTransform } );
162
+ texAlpha.assign( texture( _shadowAlbedoMaps, albedoUV ).depth( int( shadowMaterial.albedoMapIndex ) ).a );
163
+
164
+ } );
165
+
166
+ }
167
+
168
+ If( shadowMaterial.alphaMode.equal( int( 1 ) ), () => {
169
+
170
+ // MASK mode: binary alpha cutout
171
+ const effectiveAlpha = shadowMaterial.color.a.mul( texAlpha );
172
+ const cutoff = select( shadowMaterial.alphaTest.greaterThan( 0.0 ), shadowMaterial.alphaTest, float( 0.5 ) );
173
+ If( effectiveAlpha.lessThan( cutoff ), () => {
174
+
175
+ alphaCutout.assign( true );
176
+
177
+ } );
178
+
179
+ } ).ElseIf( shadowMaterial.alphaMode.equal( int( 2 ) ), () => {
180
+
181
+ // BLEND mode: modulate transmittance by alpha
182
+ const blendAlpha = clamp( shadowMaterial.color.a.mul( shadowMaterial.opacity ).mul( texAlpha ), 0.0, 1.0 );
183
+ transmittance.mulAssign( float( 1.0 ).sub( blendAlpha ) );
184
+
185
+ If( transmittance.lessThan( 0.005 ), () => {
186
+
187
+ transmittance.assign( 0.0 );
188
+ Break();
189
+
190
+ } );
191
+
192
+ alphaCutout.assign( true );
193
+
194
+ } );
195
+
196
+ } );
197
+
198
+ // ---------------------------------------------------------------
199
+ // Surface interaction: alpha-skip, transmission, transparent, or opaque
200
+ // ---------------------------------------------------------------
201
+ If( alphaCutout, () => {
202
+
203
+ // Alpha-transparent surface — advance ray past it
204
+ const alphaEps = max( float( 1e-5 ), length( shadowHit.hitPoint ).mul( 1e-6 ) );
205
+ rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( alphaEps ) ) );
206
+ remainingDist.subAssign( shadowHit.dst.add( alphaEps ) );
107
207
 
108
- // Handle transmissive materials
109
- If( shadowMaterial.transmission.greaterThan( 0.0 ), () => {
208
+ } ).ElseIf( shadowMaterial.transmission.greaterThan( 0.0 ), () => {
110
209
 
111
210
  const entering = dot( dir, shadowHit.normal ).lessThan( 0.0 );
112
211
  const N = select( entering, shadowHit.normal, shadowHit.normal.negate() );
@@ -142,9 +241,8 @@ export const traceShadowRay = Fn( ( [
142
241
  } );
143
242
 
144
243
  // Continue ray past transmissive surface
145
- const transEps = max( float( 1e-5 ), length( shadowHit.hitPoint ).mul( 1e-6 ) );
146
- rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( transEps ) ) );
147
- remainingDist.subAssign( shadowHit.dst.add( transEps ) );
244
+ rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( 0.001 ) ) );
245
+ remainingDist.subAssign( shadowHit.dst.add( 0.001 ) );
148
246
 
149
247
  } ).ElseIf( shadowMaterial.transparent, () => {
150
248
 
@@ -159,9 +257,8 @@ export const traceShadowRay = Fn( ( [
159
257
  } );
160
258
 
161
259
  // Continue ray past transparent surface
162
- const alphaEps = max( float( 1e-5 ), length( shadowHit.hitPoint ).mul( 1e-6 ) );
163
- rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( alphaEps ) ) );
164
- remainingDist.subAssign( shadowHit.dst.add( alphaEps ) );
260
+ rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( 0.001 ) ) );
261
+ remainingDist.subAssign( shadowHit.dst.add( 0.001 ) );
165
262
 
166
263
  } ).Else( () => {
167
264
 
@@ -259,7 +356,7 @@ export const estimateLightImportance = Fn( ( [ light, hitPoint, normal, material
259
356
 
260
357
  If( lightFacing.greaterThan( 0.0 ), () => {
261
358
 
262
- const solidAngle = light.area.div( max( distSq, 1e-4 ) );
359
+ const solidAngle = light.area.div( max( distSq, 0.1 ) );
263
360
  const power = light.intensity.mul( dot( light.color, REC709_LUMINANCE_COEFFICIENTS ) ).mul( light.area );
264
361
 
265
362
  // Material-aware weighting
@@ -661,7 +758,7 @@ export const calculatePointLightContribution = Fn( ( [
661
758
  const rayOffset = calculateRayOffset( hitPoint, normal, material );
662
759
  const rayOrigin = hitPoint.add( rayOffset );
663
760
 
664
- const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.mul( 0.999 ), rngState );
761
+ const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.sub( 0.001 ), rngState );
665
762
 
666
763
  If( visibility.greaterThan( 0.0 ), () => {
667
764
 
@@ -713,7 +810,7 @@ export const calculateSpotLightContribution = Fn( ( [
713
810
  const rayOffset = calculateRayOffset( hitPoint, normal, material );
714
811
  const rayOrigin = hitPoint.add( rayOffset );
715
812
 
716
- const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.mul( 0.999 ), rngState );
813
+ const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.sub( 0.001 ), rngState );
717
814
 
718
815
  If( visibility.greaterThan( 0.0 ), () => {
719
816
 
@@ -71,7 +71,6 @@ import {
71
71
  calculatePointLightImportance,
72
72
  calculateSpotLightImportance,
73
73
  traceShadowRay,
74
- calculateRayOffset,
75
74
  } from './LightsDirect.js';
76
75
 
77
76
  import { traverseBVHShadow } from './BVHTraversal.js';
@@ -941,7 +940,7 @@ export const calculateDirectLightingUnified = Fn( ( [
941
940
  ] ) => {
942
941
 
943
942
  const totalContribution = vec3( 0.0 ).toVar();
944
- const rayOrigin = hitPoint.add( calculateRayOffset( hitPoint, hitNormal, material ) ).toVar();
943
+ const rayOrigin = hitPoint.add( hitNormal.mul( 0.001 ) ).toVar();
945
944
 
946
945
  // Early exit for highly emissive surfaces
947
946
  If( material.emissiveIntensity.lessThanEqual( 10.0 ), () => {
@@ -1042,7 +1041,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1042
1041
 
1043
1042
  If( NoL.greaterThan( 0.0 ).and( lightImportance.mul( NoL ).greaterThan( importanceThreshold ) ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: hitNormal } ) ), () => {
1044
1043
 
1045
- const shadowDistance = min( lightSample.distance.mul( 0.999 ), float( 1000.0 ) ).toVar();
1044
+ const shadowDistance = min( lightSample.distance.sub( 0.001 ), float( 1000.0 ) ).toVar();
1046
1045
  const visibility = traceShadowRay(
1047
1046
  rayOrigin, lightSample.direction, shadowDistance, rngState,
1048
1047
  traverseBVHShadow,