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/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,
@@ -747,7 +747,9 @@ export const Trace = Fn( ( [
747
747
 
748
748
  } );
749
749
 
750
- // Get material from texture
750
+ // Get full material (27 reads). Lazy transform loading was tested but regressed
751
+ // textured scenes due to identity-construct + conditional-assign overhead.
752
+ // Shadow rays use getShadowMaterial() (7 reads) — the real bandwidth win.
751
753
  const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
752
754
 
753
755
  // Tessellation-free displacement — refine intersection with ray-height field marching
@@ -880,8 +882,7 @@ export const Trace = Fn( ( [
880
882
  // For transmission: offset along the old ray direction to push through the surface
881
883
  const reflectOffsetDir = select( interaction.entering, N, N.negate() );
882
884
  const offsetDir = select( interaction.didReflect, reflectOffsetDir, rayDirection );
883
- const bounceEps = max( float( 1e-4 ), length( hitInfo.hitPoint ).mul( 1e-6 ) );
884
- rayOrigin.assign( hitInfo.hitPoint.add( offsetDir.mul( bounceEps ) ) );
885
+ rayOrigin.assign( hitInfo.hitPoint.add( offsetDir.mul( 0.001 ) ) );
885
886
  rayDirection.assign( interaction.direction );
886
887
 
887
888
  stateIsPrimaryRay.assign( tslBool( false ) );
@@ -1055,7 +1056,7 @@ export const Trace = Fn( ( [
1055
1056
 
1056
1057
  const rayOffset = calculateRayOffset( hitInfo.hitPoint, N, material );
1057
1058
  const rayOrigin = hitInfo.hitPoint.add( rayOffset );
1058
- const shadowDist = emissiveSample.distance.mul( 0.999 );
1059
+ const shadowDist = emissiveSample.distance.sub( 0.001 );
1059
1060
  const visibility = traceShadowRayWrapped( rayOrigin, emissiveSample.direction, shadowDist, rngState );
1060
1061
 
1061
1062
  If( visibility.greaterThan( 0.0 ), () => {
@@ -1138,7 +1139,7 @@ export const Trace = Fn( ( [
1138
1139
  throughput.mulAssign( indirectResult.throughput );
1139
1140
 
1140
1141
  // Prepare for next bounce
1141
- rayOrigin.assign( hitInfo.hitPoint.add( calculateRayOffset( hitInfo.hitPoint, N, material ) ) );
1142
+ rayOrigin.assign( hitInfo.hitPoint.add( N.mul( 0.001 ) ) );
1142
1143
  rayDirection.assign( indirectResult.direction );
1143
1144
  prevBouncePdf.assign( indirectResult.combinedPdf );
1144
1145
 
package/src/TSL/Struct.js CHANGED
@@ -52,6 +52,22 @@ export const RayTracingMaterial = struct( {
52
52
  iridescenceThicknessRange: 'vec2',
53
53
  } );
54
54
 
55
+ // Lightweight material for shadow ray evaluation — only the fields needed
56
+ // by traceShadowRay (alpha, transmission, transparency, attenuation).
57
+ export const ShadowMaterial = struct( {
58
+ color: 'vec4',
59
+ ior: 'float',
60
+ transmission: 'float',
61
+ attenuationColor: 'vec3',
62
+ attenuationDistance: 'float',
63
+ albedoMapIndex: 'int',
64
+ opacity: 'float',
65
+ transparent: 'bool',
66
+ alphaTest: 'float',
67
+ alphaMode: 'int',
68
+ albedoTransform: 'mat3',
69
+ } );
70
+
55
71
  export const Sphere = struct( {
56
72
  position: 'vec3',
57
73
  radius: 'float',