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.
@@ -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() );
@@ -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
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',
@@ -312,7 +312,7 @@ export class EnvironmentManager {
312
312
  const startTime = performance.now();
313
313
  const textureForCDF = this.scene.environment;
314
314
 
315
- if ( ! textureForCDF.image || ! textureForCDF.image.data ) {
315
+ if ( ! textureForCDF.image ) {
316
316
 
317
317
  this._updateCDFStorageBuffers();
318
318
  this.uniforms.set( 'envTotalSum', 0.0 );
@@ -9,9 +9,9 @@
9
9
 
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
11
  import { storage } from 'three/tsl';
12
- import { TEXTURE_CONSTANTS } from '../EngineDefaults.js';
12
+ import { TEXTURE_CONSTANTS, MATERIAL_DATA_LAYOUT as M } from '../EngineDefaults.js';
13
13
 
14
- const PIXELS_PER_MATERIAL = 27;
14
+ const PIXELS_PER_MATERIAL = M.SLOTS_PER_MATERIAL;
15
15
 
16
16
  export class MaterialDataManager {
17
17
 
@@ -166,139 +166,136 @@ export class MaterialDataManager {
166
166
  }
167
167
 
168
168
  const data = this.materialStorageAttr.array;
169
- const pixelsRequired = TEXTURE_CONSTANTS.PIXELS_PER_MATERIAL;
170
- const dataInEachPixel = TEXTURE_CONSTANTS.RGBA_COMPONENTS;
171
- const dataLengthPerMaterial = pixelsRequired * dataInEachPixel;
172
- const stride = materialIndex * dataLengthPerMaterial;
169
+ const stride = materialIndex * M.FLOATS_PER_MATERIAL;
173
170
 
174
171
  switch ( property ) {
175
172
 
176
173
  case 'color':
177
174
  if ( value.r !== undefined ) {
178
175
 
179
- data[ stride + 0 ] = value.r;
180
- data[ stride + 1 ] = value.g;
181
- data[ stride + 2 ] = value.b;
176
+ data[ stride + M.COLOR ] = value.r;
177
+ data[ stride + M.COLOR + 1 ] = value.g;
178
+ data[ stride + M.COLOR + 2 ] = value.b;
182
179
 
183
180
  } else if ( Array.isArray( value ) ) {
184
181
 
185
- data[ stride + 0 ] = value[ 0 ];
186
- data[ stride + 1 ] = value[ 1 ];
187
- data[ stride + 2 ] = value[ 2 ];
182
+ data[ stride + M.COLOR ] = value[ 0 ];
183
+ data[ stride + M.COLOR + 1 ] = value[ 1 ];
184
+ data[ stride + M.COLOR + 2 ] = value[ 2 ];
188
185
 
189
186
  }
190
187
 
191
188
  break;
192
- case 'metalness': data[ stride + 3 ] = value; break;
189
+ case 'metalness': data[ stride + M.METALNESS ] = value; break;
193
190
  case 'emissive':
194
191
  if ( value.r !== undefined ) {
195
192
 
196
- data[ stride + 4 ] = value.r;
197
- data[ stride + 5 ] = value.g;
198
- data[ stride + 6 ] = value.b;
193
+ data[ stride + M.EMISSIVE ] = value.r;
194
+ data[ stride + M.EMISSIVE + 1 ] = value.g;
195
+ data[ stride + M.EMISSIVE + 2 ] = value.b;
199
196
 
200
197
  } else if ( Array.isArray( value ) ) {
201
198
 
202
- data[ stride + 4 ] = value[ 0 ];
203
- data[ stride + 5 ] = value[ 1 ];
204
- data[ stride + 6 ] = value[ 2 ];
199
+ data[ stride + M.EMISSIVE ] = value[ 0 ];
200
+ data[ stride + M.EMISSIVE + 1 ] = value[ 1 ];
201
+ data[ stride + M.EMISSIVE + 2 ] = value[ 2 ];
205
202
 
206
203
  }
207
204
 
208
205
  break;
209
- case 'roughness': data[ stride + 7 ] = value; break;
210
- case 'ior': data[ stride + 8 ] = value; break;
211
- case 'transmission': data[ stride + 9 ] = value; break;
212
- case 'thickness': data[ stride + 10 ] = value; break;
213
- case 'emissiveIntensity': data[ stride + 11 ] = value; break;
206
+ case 'roughness': data[ stride + M.ROUGHNESS ] = value; break;
207
+ case 'ior': data[ stride + M.IOR ] = value; break;
208
+ case 'transmission': data[ stride + M.TRANSMISSION ] = value; break;
209
+ case 'thickness': data[ stride + M.THICKNESS ] = value; break;
210
+ case 'emissiveIntensity': data[ stride + M.EMISSIVE_INTENSITY ] = value; break;
214
211
  case 'attenuationColor':
215
212
  if ( value.r !== undefined ) {
216
213
 
217
- data[ stride + 12 ] = value.r;
218
- data[ stride + 13 ] = value.g;
219
- data[ stride + 14 ] = value.b;
214
+ data[ stride + M.ATTENUATION_COLOR ] = value.r;
215
+ data[ stride + M.ATTENUATION_COLOR + 1 ] = value.g;
216
+ data[ stride + M.ATTENUATION_COLOR + 2 ] = value.b;
220
217
 
221
218
  } else if ( Array.isArray( value ) ) {
222
219
 
223
- data[ stride + 12 ] = value[ 0 ];
224
- data[ stride + 13 ] = value[ 1 ];
225
- data[ stride + 14 ] = value[ 2 ];
220
+ data[ stride + M.ATTENUATION_COLOR ] = value[ 0 ];
221
+ data[ stride + M.ATTENUATION_COLOR + 1 ] = value[ 1 ];
222
+ data[ stride + M.ATTENUATION_COLOR + 2 ] = value[ 2 ];
226
223
 
227
224
  }
228
225
 
229
226
  break;
230
- case 'attenuationDistance': data[ stride + 15 ] = value; break;
231
- case 'dispersion': data[ stride + 16 ] = value; break;
232
- case 'sheen': data[ stride + 18 ] = value; break;
233
- case 'sheenRoughness': data[ stride + 19 ] = value; break;
227
+ case 'attenuationDistance': data[ stride + M.ATTENUATION_DISTANCE ] = value; break;
228
+ case 'dispersion': data[ stride + M.DISPERSION ] = value; break;
229
+ case 'sheen': data[ stride + M.SHEEN ] = value; break;
230
+ case 'sheenRoughness': data[ stride + M.SHEEN_ROUGHNESS ] = value; break;
234
231
  case 'sheenColor':
235
232
  if ( value.r !== undefined ) {
236
233
 
237
- data[ stride + 20 ] = value.r;
238
- data[ stride + 21 ] = value.g;
239
- data[ stride + 22 ] = value.b;
234
+ data[ stride + M.SHEEN_COLOR ] = value.r;
235
+ data[ stride + M.SHEEN_COLOR + 1 ] = value.g;
236
+ data[ stride + M.SHEEN_COLOR + 2 ] = value.b;
240
237
 
241
238
  } else if ( Array.isArray( value ) ) {
242
239
 
243
- data[ stride + 20 ] = value[ 0 ];
244
- data[ stride + 21 ] = value[ 1 ];
245
- data[ stride + 22 ] = value[ 2 ];
240
+ data[ stride + M.SHEEN_COLOR ] = value[ 0 ];
241
+ data[ stride + M.SHEEN_COLOR + 1 ] = value[ 1 ];
242
+ data[ stride + M.SHEEN_COLOR + 2 ] = value[ 2 ];
246
243
 
247
244
  }
248
245
 
249
246
  break;
250
- case 'specularIntensity': data[ stride + 24 ] = value; break;
247
+ case 'specularIntensity': data[ stride + M.SPECULAR_INTENSITY ] = value; break;
251
248
  case 'specularColor':
252
249
  if ( value.r !== undefined ) {
253
250
 
254
- data[ stride + 25 ] = value.r;
255
- data[ stride + 26 ] = value.g;
256
- data[ stride + 27 ] = value.b;
251
+ data[ stride + M.SPECULAR_COLOR ] = value.r;
252
+ data[ stride + M.SPECULAR_COLOR + 1 ] = value.g;
253
+ data[ stride + M.SPECULAR_COLOR + 2 ] = value.b;
257
254
 
258
255
  } else if ( Array.isArray( value ) ) {
259
256
 
260
- data[ stride + 25 ] = value[ 0 ];
261
- data[ stride + 26 ] = value[ 1 ];
262
- data[ stride + 27 ] = value[ 2 ];
257
+ data[ stride + M.SPECULAR_COLOR ] = value[ 0 ];
258
+ data[ stride + M.SPECULAR_COLOR + 1 ] = value[ 1 ];
259
+ data[ stride + M.SPECULAR_COLOR + 2 ] = value[ 2 ];
263
260
 
264
261
  }
265
262
 
266
263
  break;
267
- case 'iridescence': data[ stride + 28 ] = value; break;
268
- case 'iridescenceIOR': data[ stride + 29 ] = value; break;
264
+ case 'iridescence': data[ stride + M.IRIDESCENCE ] = value; break;
265
+ case 'iridescenceIOR': data[ stride + M.IRIDESCENCE_IOR ] = value; break;
269
266
  case 'iridescenceThicknessRange':
270
267
  if ( Array.isArray( value ) ) {
271
268
 
272
- data[ stride + 30 ] = value[ 0 ];
273
- data[ stride + 31 ] = value[ 1 ];
269
+ data[ stride + M.IRIDESCENCE_THICKNESS_RANGE ] = value[ 0 ];
270
+ data[ stride + M.IRIDESCENCE_THICKNESS_RANGE + 1 ] = value[ 1 ];
274
271
 
275
272
  }
276
273
 
277
274
  break;
278
- case 'clearcoat': data[ stride + 38 ] = value; break;
279
- case 'clearcoatRoughness': data[ stride + 39 ] = value; break;
280
- case 'opacity': data[ stride + 40 ] = value; break;
281
- case 'side': data[ stride + 41 ] = value; break;
282
- case 'transparent': data[ stride + 42 ] = value; break;
283
- case 'alphaTest': data[ stride + 43 ] = value; break;
284
- case 'alphaMode': data[ stride + 44 ] = value; break;
285
- case 'depthWrite': data[ stride + 45 ] = value; break;
275
+ case 'clearcoat': data[ stride + M.CLEARCOAT ] = value; break;
276
+ case 'clearcoatRoughness': data[ stride + M.CLEARCOAT_ROUGHNESS ] = value; break;
277
+ case 'opacity': data[ stride + M.OPACITY ] = value; break;
278
+ case 'side': data[ stride + M.SIDE ] = value; break;
279
+ case 'transparent': data[ stride + M.TRANSPARENT ] = value; break;
280
+ case 'alphaTest': data[ stride + M.ALPHA_TEST ] = value; break;
281
+ case 'alphaMode': data[ stride + M.ALPHA_MODE ] = value; break;
282
+ case 'depthWrite': data[ stride + M.DEPTH_WRITE ] = value; break;
286
283
  case 'normalScale':
287
284
  if ( value.x !== undefined ) {
288
285
 
289
- data[ stride + 46 ] = value.x;
290
- data[ stride + 47 ] = value.y;
286
+ data[ stride + M.NORMAL_SCALE ] = value.x;
287
+ data[ stride + M.NORMAL_SCALE + 1 ] = value.y;
291
288
 
292
289
  } else if ( typeof value === 'number' ) {
293
290
 
294
- data[ stride + 46 ] = value;
295
- data[ stride + 47 ] = value;
291
+ data[ stride + M.NORMAL_SCALE ] = value;
292
+ data[ stride + M.NORMAL_SCALE + 1 ] = value;
296
293
 
297
294
  }
298
295
 
299
296
  break;
300
- case 'bumpScale': data[ stride + 48 ] = value; break;
301
- case 'displacementScale': data[ stride + 49 ] = value; break;
297
+ case 'bumpScale': data[ stride + M.BUMP_SCALE ] = value; break;
298
+ case 'displacementScale': data[ stride + M.DISPLACEMENT_SCALE ] = value; break;
302
299
  default:
303
300
  console.warn( `Unknown material property: ${property}` );
304
301
  return;
@@ -338,108 +335,105 @@ export class MaterialDataManager {
338
335
  }
339
336
 
340
337
  const data = this.materialStorageAttr.array;
341
- const pixelsRequired = TEXTURE_CONSTANTS.PIXELS_PER_MATERIAL;
342
- const dataInEachPixel = TEXTURE_CONSTANTS.RGBA_COMPONENTS;
343
- const dataLengthPerMaterial = pixelsRequired * dataInEachPixel;
344
- const stride = materialIndex * dataLengthPerMaterial;
338
+ const stride = materialIndex * M.FLOATS_PER_MATERIAL;
345
339
 
346
340
  if ( materialData.color ) {
347
341
 
348
- data[ stride + 0 ] = materialData.color.r ?? materialData.color[ 0 ] ?? 1;
349
- data[ stride + 1 ] = materialData.color.g ?? materialData.color[ 1 ] ?? 1;
350
- data[ stride + 2 ] = materialData.color.b ?? materialData.color[ 2 ] ?? 1;
342
+ data[ stride + M.COLOR ] = materialData.color.r ?? materialData.color[ 0 ] ?? 1;
343
+ data[ stride + M.COLOR + 1 ] = materialData.color.g ?? materialData.color[ 1 ] ?? 1;
344
+ data[ stride + M.COLOR + 2 ] = materialData.color.b ?? materialData.color[ 2 ] ?? 1;
351
345
 
352
346
  }
353
347
 
354
- data[ stride + 3 ] = materialData.metalness ?? 0;
348
+ data[ stride + M.METALNESS ] = materialData.metalness ?? 0;
355
349
 
356
350
  if ( materialData.emissive ) {
357
351
 
358
- data[ stride + 4 ] = materialData.emissive.r ?? materialData.emissive[ 0 ] ?? 0;
359
- data[ stride + 5 ] = materialData.emissive.g ?? materialData.emissive[ 1 ] ?? 0;
360
- data[ stride + 6 ] = materialData.emissive.b ?? materialData.emissive[ 2 ] ?? 0;
352
+ data[ stride + M.EMISSIVE ] = materialData.emissive.r ?? materialData.emissive[ 0 ] ?? 0;
353
+ data[ stride + M.EMISSIVE + 1 ] = materialData.emissive.g ?? materialData.emissive[ 1 ] ?? 0;
354
+ data[ stride + M.EMISSIVE + 2 ] = materialData.emissive.b ?? materialData.emissive[ 2 ] ?? 0;
361
355
 
362
356
  }
363
357
 
364
- data[ stride + 7 ] = materialData.roughness ?? 1;
365
- data[ stride + 8 ] = materialData.ior ?? 1.5;
366
- data[ stride + 9 ] = materialData.transmission ?? 0;
367
- data[ stride + 10 ] = materialData.thickness ?? 0.1;
368
- data[ stride + 11 ] = materialData.emissiveIntensity ?? 1;
358
+ data[ stride + M.ROUGHNESS ] = materialData.roughness ?? 1;
359
+ data[ stride + M.IOR ] = materialData.ior ?? 1.5;
360
+ data[ stride + M.TRANSMISSION ] = materialData.transmission ?? 0;
361
+ data[ stride + M.THICKNESS ] = materialData.thickness ?? 0.1;
362
+ data[ stride + M.EMISSIVE_INTENSITY ] = materialData.emissiveIntensity ?? 1;
369
363
 
370
364
  if ( materialData.attenuationColor ) {
371
365
 
372
- data[ stride + 12 ] = materialData.attenuationColor.r ?? materialData.attenuationColor[ 0 ] ?? 1;
373
- data[ stride + 13 ] = materialData.attenuationColor.g ?? materialData.attenuationColor[ 1 ] ?? 1;
374
- data[ stride + 14 ] = materialData.attenuationColor.b ?? materialData.attenuationColor[ 2 ] ?? 1;
366
+ data[ stride + M.ATTENUATION_COLOR ] = materialData.attenuationColor.r ?? materialData.attenuationColor[ 0 ] ?? 1;
367
+ data[ stride + M.ATTENUATION_COLOR + 1 ] = materialData.attenuationColor.g ?? materialData.attenuationColor[ 1 ] ?? 1;
368
+ data[ stride + M.ATTENUATION_COLOR + 2 ] = materialData.attenuationColor.b ?? materialData.attenuationColor[ 2 ] ?? 1;
375
369
 
376
370
  }
377
371
 
378
- data[ stride + 15 ] = materialData.attenuationDistance ?? Infinity;
379
- data[ stride + 16 ] = materialData.dispersion ?? 0;
380
- data[ stride + 17 ] = 1; // Reserved slot (per-mesh visibility handled at BLAS-pointer level)
381
- data[ stride + 18 ] = materialData.sheen ?? 0;
382
- data[ stride + 19 ] = materialData.sheenRoughness ?? 1;
372
+ data[ stride + M.ATTENUATION_DISTANCE ] = materialData.attenuationDistance ?? Infinity;
373
+ data[ stride + M.DISPERSION ] = materialData.dispersion ?? 0;
374
+ data[ stride + M.VISIBLE ] = 1; // Reserved slot (per-mesh visibility handled at BLAS-pointer level)
375
+ data[ stride + M.SHEEN ] = materialData.sheen ?? 0;
376
+ data[ stride + M.SHEEN_ROUGHNESS ] = materialData.sheenRoughness ?? 1;
383
377
 
384
378
  if ( materialData.sheenColor ) {
385
379
 
386
- data[ stride + 20 ] = materialData.sheenColor.r ?? materialData.sheenColor[ 0 ] ?? 0;
387
- data[ stride + 21 ] = materialData.sheenColor.g ?? materialData.sheenColor[ 1 ] ?? 0;
388
- data[ stride + 22 ] = materialData.sheenColor.b ?? materialData.sheenColor[ 2 ] ?? 0;
380
+ data[ stride + M.SHEEN_COLOR ] = materialData.sheenColor.r ?? materialData.sheenColor[ 0 ] ?? 0;
381
+ data[ stride + M.SHEEN_COLOR + 1 ] = materialData.sheenColor.g ?? materialData.sheenColor[ 1 ] ?? 0;
382
+ data[ stride + M.SHEEN_COLOR + 2 ] = materialData.sheenColor.b ?? materialData.sheenColor[ 2 ] ?? 0;
389
383
 
390
384
  }
391
385
 
392
- data[ stride + 24 ] = materialData.specularIntensity ?? 1;
386
+ data[ stride + M.SPECULAR_INTENSITY ] = materialData.specularIntensity ?? 1;
393
387
 
394
388
  if ( materialData.specularColor ) {
395
389
 
396
- data[ stride + 25 ] = materialData.specularColor.r ?? materialData.specularColor[ 0 ] ?? 1;
397
- data[ stride + 26 ] = materialData.specularColor.g ?? materialData.specularColor[ 1 ] ?? 1;
398
- data[ stride + 27 ] = materialData.specularColor.b ?? materialData.specularColor[ 2 ] ?? 1;
390
+ data[ stride + M.SPECULAR_COLOR ] = materialData.specularColor.r ?? materialData.specularColor[ 0 ] ?? 1;
391
+ data[ stride + M.SPECULAR_COLOR + 1 ] = materialData.specularColor.g ?? materialData.specularColor[ 1 ] ?? 1;
392
+ data[ stride + M.SPECULAR_COLOR + 2 ] = materialData.specularColor.b ?? materialData.specularColor[ 2 ] ?? 1;
399
393
 
400
394
  }
401
395
 
402
- data[ stride + 28 ] = materialData.iridescence ?? 0;
403
- data[ stride + 29 ] = materialData.iridescenceIOR ?? 1.3;
396
+ data[ stride + M.IRIDESCENCE ] = materialData.iridescence ?? 0;
397
+ data[ stride + M.IRIDESCENCE_IOR ] = materialData.iridescenceIOR ?? 1.3;
404
398
 
405
399
  if ( materialData.iridescenceThicknessRange ) {
406
400
 
407
- data[ stride + 30 ] = materialData.iridescenceThicknessRange[ 0 ] ?? 100;
408
- data[ stride + 31 ] = materialData.iridescenceThicknessRange[ 1 ] ?? 400;
401
+ data[ stride + M.IRIDESCENCE_THICKNESS_RANGE ] = materialData.iridescenceThicknessRange[ 0 ] ?? 100;
402
+ data[ stride + M.IRIDESCENCE_THICKNESS_RANGE + 1 ] = materialData.iridescenceThicknessRange[ 1 ] ?? 400;
409
403
 
410
404
  }
411
405
 
412
- data[ stride + 32 ] = materialData.map ?? - 1;
413
- data[ stride + 33 ] = materialData.normalMap ?? - 1;
414
- data[ stride + 34 ] = materialData.roughnessMap ?? - 1;
415
- data[ stride + 35 ] = materialData.metalnessMap ?? - 1;
416
- data[ stride + 36 ] = materialData.emissiveMap ?? - 1;
417
- data[ stride + 37 ] = materialData.bumpMap ?? - 1;
418
-
419
- data[ stride + 38 ] = materialData.clearcoat ?? 0;
420
- data[ stride + 39 ] = materialData.clearcoatRoughness ?? 0;
421
- data[ stride + 40 ] = materialData.opacity ?? 1;
422
- data[ stride + 41 ] = materialData.side ?? 0;
423
- data[ stride + 42 ] = materialData.transparent ?? 0;
424
- data[ stride + 43 ] = materialData.alphaTest ?? 0;
425
- data[ stride + 44 ] = materialData.alphaMode ?? 0;
426
- data[ stride + 45 ] = materialData.depthWrite ?? 1;
427
- data[ stride + 46 ] = materialData.normalScale?.x ?? ( typeof materialData.normalScale === 'number' ? materialData.normalScale : 1 );
428
- data[ stride + 47 ] = materialData.normalScale?.y ?? ( typeof materialData.normalScale === 'number' ? materialData.normalScale : 1 );
429
- data[ stride + 48 ] = materialData.bumpScale ?? 1;
430
- data[ stride + 49 ] = materialData.displacementScale ?? 1;
431
- data[ stride + 50 ] = materialData.displacementMap ?? - 1;
406
+ data[ stride + M.ALBEDO_MAP_INDEX ] = materialData.map ?? - 1;
407
+ data[ stride + M.NORMAL_MAP_INDEX ] = materialData.normalMap ?? - 1;
408
+ data[ stride + M.ROUGHNESS_MAP_INDEX ] = materialData.roughnessMap ?? - 1;
409
+ data[ stride + M.METALNESS_MAP_INDEX ] = materialData.metalnessMap ?? - 1;
410
+ data[ stride + M.EMISSIVE_MAP_INDEX ] = materialData.emissiveMap ?? - 1;
411
+ data[ stride + M.BUMP_MAP_INDEX ] = materialData.bumpMap ?? - 1;
412
+
413
+ data[ stride + M.CLEARCOAT ] = materialData.clearcoat ?? 0;
414
+ data[ stride + M.CLEARCOAT_ROUGHNESS ] = materialData.clearcoatRoughness ?? 0;
415
+ data[ stride + M.OPACITY ] = materialData.opacity ?? 1;
416
+ data[ stride + M.SIDE ] = materialData.side ?? 0;
417
+ data[ stride + M.TRANSPARENT ] = materialData.transparent ?? 0;
418
+ data[ stride + M.ALPHA_TEST ] = materialData.alphaTest ?? 0;
419
+ data[ stride + M.ALPHA_MODE ] = materialData.alphaMode ?? 0;
420
+ data[ stride + M.DEPTH_WRITE ] = materialData.depthWrite ?? 1;
421
+ data[ stride + M.NORMAL_SCALE ] = materialData.normalScale?.x ?? ( typeof materialData.normalScale === 'number' ? materialData.normalScale : 1 );
422
+ data[ stride + M.NORMAL_SCALE + 1 ] = materialData.normalScale?.y ?? ( typeof materialData.normalScale === 'number' ? materialData.normalScale : 1 );
423
+ data[ stride + M.BUMP_SCALE ] = materialData.bumpScale ?? 1;
424
+ data[ stride + M.DISPLACEMENT_SCALE ] = materialData.displacementScale ?? 1;
425
+ data[ stride + M.DISPLACEMENT_MAP_INDEX ] = materialData.displacementMap ?? - 1;
432
426
 
433
427
  // Texture transformation matrices (9 floats each, identity if missing)
434
428
  const identity = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
435
429
  const transformEntries = [
436
- { key: 'mapMatrix', offset: 52 },
437
- { key: 'normalMapMatrices', offset: 60 },
438
- { key: 'roughnessMapMatrices', offset: 68 },
439
- { key: 'metalnessMapMatrices', offset: 76 },
440
- { key: 'emissiveMapMatrices', offset: 84 },
441
- { key: 'bumpMapMatrices', offset: 92 },
442
- { key: 'displacementMapMatrices', offset: 100 }
430
+ { key: 'mapMatrix', offset: M.ALBEDO_TRANSFORM },
431
+ { key: 'normalMapMatrices', offset: M.NORMAL_TRANSFORM },
432
+ { key: 'roughnessMapMatrices', offset: M.ROUGHNESS_TRANSFORM },
433
+ { key: 'metalnessMapMatrices', offset: M.METALNESS_TRANSFORM },
434
+ { key: 'emissiveMapMatrices', offset: M.EMISSIVE_TRANSFORM },
435
+ { key: 'bumpMapMatrices', offset: M.BUMP_TRANSFORM },
436
+ { key: 'displacementMapMatrices', offset: M.DISPLACEMENT_TRANSFORM }
443
437
  ];
444
438
 
445
439
  for ( const { key, offset } of transformEntries ) {
@@ -497,20 +491,17 @@ export class MaterialDataManager {
497
491
 
498
492
  }
499
493
 
500
- const pixelsRequired = TEXTURE_CONSTANTS.PIXELS_PER_MATERIAL;
501
- const dataInEachPixel = TEXTURE_CONSTANTS.RGBA_COMPONENTS;
502
- const dataLengthPerMaterial = pixelsRequired * dataInEachPixel;
503
494
  const data = this.materialStorageAttr.array;
504
- const stride = materialIndex * dataLengthPerMaterial;
495
+ const stride = materialIndex * M.FLOATS_PER_MATERIAL;
505
496
 
506
497
  const transformOffsets = {
507
- 'map': 52,
508
- 'normalMap': 60,
509
- 'roughnessMap': 68,
510
- 'metalnessMap': 76,
511
- 'emissiveMap': 84,
512
- 'bumpMap': 92,
513
- 'displacementMap': 100
498
+ 'map': M.ALBEDO_TRANSFORM,
499
+ 'normalMap': M.NORMAL_TRANSFORM,
500
+ 'roughnessMap': M.ROUGHNESS_TRANSFORM,
501
+ 'metalnessMap': M.METALNESS_TRANSFORM,
502
+ 'emissiveMap': M.EMISSIVE_TRANSFORM,
503
+ 'bumpMap': M.BUMP_TRANSFORM,
504
+ 'displacementMap': M.DISPLACEMENT_TRANSFORM
514
505
  };
515
506
 
516
507
  const offset = transformOffsets[ textureName ];
@@ -552,9 +543,6 @@ export class MaterialDataManager {
552
543
  }
553
544
 
554
545
  const data = this.materialStorageAttr.array;
555
- const pixelsRequired = TEXTURE_CONSTANTS.PIXELS_PER_MATERIAL;
556
- const dataInEachPixel = TEXTURE_CONSTANTS.RGBA_COMPONENTS;
557
- const dataLengthPerMaterial = pixelsRequired * dataInEachPixel;
558
546
  const materialCount = this.sdfs.materialCount || 1;
559
547
 
560
548
  const newFeatures = {
@@ -570,16 +558,16 @@ export class MaterialDataManager {
570
558
 
571
559
  for ( let i = 0; i < materialCount; i ++ ) {
572
560
 
573
- const stride = i * dataLengthPerMaterial;
561
+ const stride = i * M.FLOATS_PER_MATERIAL;
574
562
 
575
- const transmission = data[ stride + 9 ];
576
- const dispersion = data[ stride + 16 ];
577
- const sheen = data[ stride + 18 ];
578
- const iridescence = data[ stride + 28 ];
579
- const clearcoat = data[ stride + 38 ];
580
- const opacity = data[ stride + 40 ];
581
- const transparent = data[ stride + 42 ];
582
- const alphaTest = data[ stride + 43 ];
563
+ const transmission = data[ stride + M.TRANSMISSION ];
564
+ const dispersion = data[ stride + M.DISPERSION ];
565
+ const sheen = data[ stride + M.SHEEN ];
566
+ const iridescence = data[ stride + M.IRIDESCENCE ];
567
+ const clearcoat = data[ stride + M.CLEARCOAT ];
568
+ const opacity = data[ stride + M.OPACITY ];
569
+ const transparent = data[ stride + M.TRANSPARENT ];
570
+ const alphaTest = data[ stride + M.ALPHA_TEST ];
583
571
 
584
572
  if ( clearcoat > 0 ) newFeatures.hasClearcoat = true;
585
573
  if ( transmission > 0 ) newFeatures.hasTransmission = true;
@@ -240,6 +240,7 @@ export class UniformManager {
240
240
 
241
241
  // Render mode
242
242
  u( 'renderMode', DEFAULT_STATE.renderMode, 'int' );
243
+ ub( 'enableAlphaShadows', DEFAULT_STATE.enableAlphaShadows );
243
244
 
244
245
  // Resolution (for RNG seeding)
245
246
  u( 'resolution', new Vector2( width, height ), 'vec2' );