rayzee 7.10.3 → 7.11.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": "7.10.3",
3
+ "version": "7.11.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",
@@ -390,8 +390,8 @@ export const TRIANGLE_DATA_LAYOUT = {
390
390
  // Shared between CPU writers (TextureCreator, MaterialDataManager) and GPU readers (Common.js getMaterial).
391
391
  export const MATERIAL_DATA_LAYOUT = {
392
392
 
393
- SLOTS_PER_MATERIAL: 30, // vec4 slots per material
394
- FLOATS_PER_MATERIAL: 120, // total floats per material (30 × 4)
393
+ SLOTS_PER_MATERIAL: 33, // vec4 slots per material
394
+ FLOATS_PER_MATERIAL: 132, // total floats per material (33 × 4)
395
395
 
396
396
  // ── Flat float offsets (CPU side) ────────────────────────────────
397
397
  // Used as: data[ materialIndex * FLOATS_PER_MATERIAL + offset ]
@@ -438,8 +438,14 @@ export const MATERIAL_DATA_LAYOUT = {
438
438
  SUBSURFACE_COLOR: 108, SUBSURFACE: 111,
439
439
  // Slot 28: subsurfaceRadius.rgb (mean free path) + radius scale
440
440
  SUBSURFACE_RADIUS: 112, SUBSURFACE_RADIUS_SCALE: 115,
441
- // Slot 29: anisotropy g (floats 117-119 reserved for future SSS)
442
- SUBSURFACE_ANISOTROPY: 116,
441
+ // Slot 29: subsurfaceAnisotropy g (116) + surface anisotropy (strength 117, rotation 118, map index 119)
442
+ SUBSURFACE_ANISOTROPY: 116, ANISOTROPY: 117, ANISOTROPY_ROTATION: 118, ANISOTROPY_MAP_INDEX: 119,
443
+ // Slot 30: extension-texture map indices A (transmission, clearcoat, clearcoatRoughness, sheenColor)
444
+ TRANSMISSION_MAP_INDEX: 120, CLEARCOAT_MAP_INDEX: 121, CLEARCOAT_ROUGHNESS_MAP_INDEX: 122, SHEEN_COLOR_MAP_INDEX: 123,
445
+ // Slot 31: extension-texture map indices B (sheenRoughness, iridescence, iridescenceThickness, specularIntensity)
446
+ SHEEN_ROUGHNESS_MAP_INDEX: 124, IRIDESCENCE_MAP_INDEX: 125, IRIDESCENCE_THICKNESS_MAP_INDEX: 126, SPECULAR_INTENSITY_MAP_INDEX: 127,
447
+ // Slot 32: extension-texture map indices C (specularColor + 3 reserved)
448
+ SPECULAR_COLOR_MAP_INDEX: 128,
443
449
 
444
450
  // ── Vec4 slot indices (GPU/TSL side) ─────────────────────────────
445
451
  // Used with getDatafromStorageBuffer( buf, matIdx, int(slot), int(SLOTS_PER_MATERIAL) )
@@ -466,7 +472,10 @@ export const MATERIAL_DATA_LAYOUT = {
466
472
  DISPLACEMENT_TRANSFORM_A: 25, DISPLACEMENT_TRANSFORM_B: 26,
467
473
  SUBSURFACE_A: 27, // subsurfaceColor.rgb, subsurface weight
468
474
  SUBSURFACE_B: 28, // subsurfaceRadius.rgb, subsurfaceRadiusScale
469
- SUBSURFACE_C: 29, // subsurfaceAnisotropy, reserved
475
+ SUBSURFACE_C: 29, // subsurfaceAnisotropy g, anisotropy, anisotropyRotation, anisotropyMapIndex
476
+ EXT_MAP_INDICES_A: 30, // transmission, clearcoat, clearcoatRoughness, sheenColor map indices
477
+ EXT_MAP_INDICES_B: 31, // sheenRoughness, iridescence, iridescenceThickness, specularIntensity map indices
478
+ EXT_MAP_INDICES_C: 32, // specularColor map index + 3 reserved
470
479
  },
471
480
 
472
481
  };
@@ -1590,6 +1590,17 @@ export class AssetLoader extends EventDispatcher {
1590
1590
 
1591
1591
  }
1592
1592
 
1593
+ // glTF punctual point/spot intensity is candela (W/sr); the engine's light
1594
+ // model is Blender-style Power (W) that LightSerializer divides by 4π. Convert
1595
+ // glTF candela → Power once at import (×4π) so it nets to the correct I/d².
1596
+ // Directional (lux ≈ irradiance) is used directly and needs no conversion.
1597
+ if ( ( object.isPointLight || object.isSpotLight ) && ! userData.__candelaConverted ) {
1598
+
1599
+ object.intensity *= 4 * Math.PI;
1600
+ userData.__candelaConverted = true;
1601
+
1602
+ }
1603
+
1593
1604
  // Process ceiling lights
1594
1605
  if ( object.name.startsWith( 'RectAreaLightPlaceholder' ) &&
1595
1606
  userData.name
@@ -292,7 +292,10 @@ export class GeometryExtractor {
292
292
  subsurfaceColor: new Color( 0xffffff ),
293
293
  subsurfaceRadius: [ 1.0, 0.2, 0.1 ], // skin-like: red travels furthest
294
294
  subsurfaceRadiusScale: 1.0,
295
- subsurfaceAnisotropy: 0.0
295
+ subsurfaceAnisotropy: 0.0,
296
+ // Surface specular anisotropy (native MeshPhysicalMaterial / KHR_materials_anisotropy)
297
+ anisotropy: 0.0,
298
+ anisotropyRotation: 0.0
296
299
  };
297
300
 
298
301
  }
@@ -399,7 +402,9 @@ export class GeometryExtractor {
399
402
  emissiveIntensity: legacyMapping.emissiveIntensity ?? material.emissiveIntensity ?? defaults.emissiveIntensity,
400
403
 
401
404
  // Surface properties
402
- roughness: Math.max( 0.05, legacyMapping.roughness ?? material.roughness ?? defaults.roughness ),
405
+ // Floor at 0.02 (the sampler's own VNDF-PDF clamp, MaterialProperties.js) rather than
406
+ // 0.05, so near-mirror metals stay sharp without entering a new firefly regime.
407
+ roughness: Math.max( 0.02, legacyMapping.roughness ?? material.roughness ?? defaults.roughness ),
403
408
  metalness: legacyMapping.metalness ?? material.metalness ?? defaults.metalness,
404
409
 
405
410
  // Optical properties
@@ -430,6 +435,10 @@ export class GeometryExtractor {
430
435
  subsurfaceRadiusScale: material.subsurfaceRadiusScale ?? defaults.subsurfaceRadiusScale,
431
436
  subsurfaceAnisotropy: material.subsurfaceAnisotropy ?? defaults.subsurfaceAnisotropy,
432
437
 
438
+ // Surface specular anisotropy (native MeshPhysicalMaterial / KHR_materials_anisotropy)
439
+ anisotropy: material.anisotropy ?? defaults.anisotropy,
440
+ anisotropyRotation: material.anisotropyRotation ?? defaults.anisotropyRotation,
441
+
433
442
  // Specular properties (for compatibility)
434
443
  specularIntensity: legacyMapping.specularIntensity ?? material.specularIntensity ?? defaults.specularIntensity,
435
444
  specularColor: legacyMapping.specularColor ?? material.specularColor ?? defaults.specularColor,
@@ -456,18 +465,21 @@ export class GeometryExtractor {
456
465
  metalnessMap: this.processTexture( material.metalnessMap, this.metalnessMaps ),
457
466
  emissiveMap: this.processTexture( material.emissiveMap, this.emissiveMaps ),
458
467
  displacementMap: this.processTexture( material.displacementMap, this.displacementMaps ),
459
-
460
- // Advanced texture maps (MeshPhysicalMaterial only)
461
- clearcoatMap: this.processTexture( material.clearcoatMap, [] ),
462
- clearcoatRoughnessMap: this.processTexture( material.clearcoatRoughnessMap, [] ),
463
- transmissionMap: this.processTexture( material.transmissionMap, [] ),
468
+ anisotropyMap: this.processTexture( material.anisotropyMap, this.anisotropyMaps ),
469
+
470
+ // Advanced texture maps (MeshPhysicalMaterial only). Folded into their scalar factors
471
+ // in ShadeKernel (applyExtensionMaps). thicknessMap stays dropped — thickness has no
472
+ // render effect yet (see gap-plan Phase 4.4).
473
+ clearcoatMap: this.processTexture( material.clearcoatMap, this.clearcoatMaps ),
474
+ clearcoatRoughnessMap: this.processTexture( material.clearcoatRoughnessMap, this.clearcoatRoughnessMaps ),
475
+ transmissionMap: this.processTexture( material.transmissionMap, this.transmissionMaps ),
464
476
  thicknessMap: this.processTexture( material.thicknessMap, [] ),
465
- sheenColorMap: this.processTexture( material.sheenColorMap, [] ),
466
- sheenRoughnessMap: this.processTexture( material.sheenRoughnessMap, [] ),
467
- specularIntensityMap: this.processTexture( material.specularIntensityMap, [] ),
468
- specularColorMap: this.processTexture( material.specularColorMap, [] ),
469
- iridescenceMap: this.processTexture( material.iridescenceMap, [] ),
470
- iridescenceThicknessMap: this.processTexture( material.iridescenceThicknessMap, [] ),
477
+ sheenColorMap: this.processTexture( material.sheenColorMap, this.sheenColorMaps ),
478
+ sheenRoughnessMap: this.processTexture( material.sheenRoughnessMap, this.sheenRoughnessMaps ),
479
+ specularIntensityMap: this.processTexture( material.specularIntensityMap, this.specularIntensityMaps ),
480
+ specularColorMap: this.processTexture( material.specularColorMap, this.specularColorMaps ),
481
+ iridescenceMap: this.processTexture( material.iridescenceMap, this.iridescenceMaps ),
482
+ iridescenceThicknessMap: this.processTexture( material.iridescenceThicknessMap, this.iridescenceThicknessMaps ),
471
483
 
472
484
  // Texture transformation matrices
473
485
  mapMatrix: this.getTextureMatrix( material.map ),
@@ -819,6 +831,16 @@ export class GeometryExtractor {
819
831
  this.emissiveMaps = [];
820
832
  this.roughnessMaps = [];
821
833
  this.displacementMaps = [];
834
+ this.anisotropyMaps = [];
835
+ this.transmissionMaps = [];
836
+ this.clearcoatMaps = [];
837
+ this.clearcoatRoughnessMaps = [];
838
+ this.sheenColorMaps = [];
839
+ this.sheenRoughnessMaps = [];
840
+ this.iridescenceMaps = [];
841
+ this.iridescenceThicknessMaps = [];
842
+ this.specularIntensityMaps = [];
843
+ this.specularColorMaps = [];
822
844
  this.directionalLights = [];
823
845
  this.cameras = [];
824
846
 
@@ -857,6 +879,16 @@ export class GeometryExtractor {
857
879
  emissiveMaps: this.emissiveMaps,
858
880
  roughnessMaps: this.roughnessMaps,
859
881
  displacementMaps: this.displacementMaps,
882
+ anisotropyMaps: this.anisotropyMaps,
883
+ transmissionMaps: this.transmissionMaps,
884
+ clearcoatMaps: this.clearcoatMaps,
885
+ clearcoatRoughnessMaps: this.clearcoatRoughnessMaps,
886
+ sheenColorMaps: this.sheenColorMaps,
887
+ sheenRoughnessMaps: this.sheenRoughnessMaps,
888
+ iridescenceMaps: this.iridescenceMaps,
889
+ iridescenceThicknessMaps: this.iridescenceThicknessMaps,
890
+ specularIntensityMaps: this.specularIntensityMaps,
891
+ specularColorMaps: this.specularColorMaps,
860
892
  directionalLights: this.directionalLights,
861
893
  cameras: this.cameras,
862
894
  sceneFeatures: this.sceneFeatures // Scene-wide material feature flags
@@ -63,6 +63,16 @@ export class SceneProcessor {
63
63
  this.metalnessMaps = [];
64
64
  this.emissiveMaps = [];
65
65
  this.displacementMaps = [];
66
+ this.anisotropyMaps = [];
67
+ this.transmissionMaps = [];
68
+ this.clearcoatMaps = [];
69
+ this.clearcoatRoughnessMaps = [];
70
+ this.sheenColorMaps = [];
71
+ this.sheenRoughnessMaps = [];
72
+ this.iridescenceMaps = [];
73
+ this.iridescenceThicknessMaps = [];
74
+ this.specularIntensityMaps = [];
75
+ this.specularColorMaps = [];
66
76
  this.directionalLights = [];
67
77
  this.cameras = [];
68
78
  this.spheres = [];
@@ -320,6 +330,16 @@ export class SceneProcessor {
320
330
  this.metalnessMaps = extractedData.metalnessMaps;
321
331
  this.emissiveMaps = extractedData.emissiveMaps;
322
332
  this.displacementMaps = extractedData.displacementMaps;
333
+ this.anisotropyMaps = extractedData.anisotropyMaps;
334
+ this.transmissionMaps = extractedData.transmissionMaps;
335
+ this.clearcoatMaps = extractedData.clearcoatMaps;
336
+ this.clearcoatRoughnessMaps = extractedData.clearcoatRoughnessMaps;
337
+ this.sheenColorMaps = extractedData.sheenColorMaps;
338
+ this.sheenRoughnessMaps = extractedData.sheenRoughnessMaps;
339
+ this.iridescenceMaps = extractedData.iridescenceMaps;
340
+ this.iridescenceThicknessMaps = extractedData.iridescenceThicknessMaps;
341
+ this.specularIntensityMaps = extractedData.specularIntensityMaps;
342
+ this.specularColorMaps = extractedData.specularColorMaps;
323
343
  this.directionalLights = extractedData.directionalLights;
324
344
  this.cameras = extractedData.cameras;
325
345
  this.sceneFeatures = extractedData.sceneFeatures; // Store material feature flags for shader optimization
@@ -883,6 +903,17 @@ export class SceneProcessor {
883
903
  roughness: remapType( this.roughnessMaps, linearLists, linearDedup, this._linearTexPacked ),
884
904
  metalness: remapType( this.metalnessMaps, linearLists, linearDedup, this._linearTexPacked ),
885
905
  displacement: remapType( this.displacementMaps, linearLists, linearDedup, this._linearTexPacked ),
906
+ anisotropy: remapType( this.anisotropyMaps, linearLists, linearDedup, this._linearTexPacked ),
907
+ // Extension maps — data maps → linear pool; color maps (sheenColor, specularColor) → sRGB pool.
908
+ transmission: remapType( this.transmissionMaps, linearLists, linearDedup, this._linearTexPacked ),
909
+ clearcoat: remapType( this.clearcoatMaps, linearLists, linearDedup, this._linearTexPacked ),
910
+ clearcoatRoughness: remapType( this.clearcoatRoughnessMaps, linearLists, linearDedup, this._linearTexPacked ),
911
+ sheenColor: remapType( this.sheenColorMaps, srgbLists, srgbDedup, this._srgbTexPacked ),
912
+ sheenRoughness: remapType( this.sheenRoughnessMaps, linearLists, linearDedup, this._linearTexPacked ),
913
+ iridescence: remapType( this.iridescenceMaps, linearLists, linearDedup, this._linearTexPacked ),
914
+ iridescenceThickness: remapType( this.iridescenceThicknessMaps, linearLists, linearDedup, this._linearTexPacked ),
915
+ specularIntensity: remapType( this.specularIntensityMaps, linearLists, linearDedup, this._linearTexPacked ),
916
+ specularColor: remapType( this.specularColorMaps, srgbLists, srgbDedup, this._srgbTexPacked ),
886
917
  };
887
918
 
888
919
  return { srgbLists, linearLists, remap };
@@ -907,6 +938,16 @@ export class SceneProcessor {
907
938
  mat.roughnessMap = fix( mat.roughnessMap, remap.roughness );
908
939
  mat.metalnessMap = fix( mat.metalnessMap, remap.metalness );
909
940
  mat.displacementMap = fix( mat.displacementMap, remap.displacement );
941
+ mat.anisotropyMap = fix( mat.anisotropyMap, remap.anisotropy );
942
+ mat.transmissionMap = fix( mat.transmissionMap, remap.transmission );
943
+ mat.clearcoatMap = fix( mat.clearcoatMap, remap.clearcoat );
944
+ mat.clearcoatRoughnessMap = fix( mat.clearcoatRoughnessMap, remap.clearcoatRoughness );
945
+ mat.sheenColorMap = fix( mat.sheenColorMap, remap.sheenColor );
946
+ mat.sheenRoughnessMap = fix( mat.sheenRoughnessMap, remap.sheenRoughness );
947
+ mat.iridescenceMap = fix( mat.iridescenceMap, remap.iridescence );
948
+ mat.iridescenceThicknessMap = fix( mat.iridescenceThicknessMap, remap.iridescenceThickness );
949
+ mat.specularIntensityMap = fix( mat.specularIntensityMap, remap.specularIntensity );
950
+ mat.specularColorMap = fix( mat.specularColorMap, remap.specularColor );
910
951
 
911
952
  }
912
953
 
@@ -984,6 +1025,16 @@ export class SceneProcessor {
984
1025
  this.metalnessMaps = [];
985
1026
  this.emissiveMaps = [];
986
1027
  this.displacementMaps = [];
1028
+ this.anisotropyMaps = [];
1029
+ this.transmissionMaps = [];
1030
+ this.clearcoatMaps = [];
1031
+ this.clearcoatRoughnessMaps = [];
1032
+ this.sheenColorMaps = [];
1033
+ this.sheenRoughnessMaps = [];
1034
+ this.iridescenceMaps = [];
1035
+ this.iridescenceThicknessMaps = [];
1036
+ this.specularIntensityMaps = [];
1037
+ this.specularColorMaps = [];
987
1038
  this.directionalLights = [];
988
1039
  this.cameras = [];
989
1040
  this.spheres = [];
@@ -1,4 +1,4 @@
1
- import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace } from "three";
1
+ import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, RepeatWrapping } from "three";
2
2
  import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
3
3
  import { fetchAsWorker } from './Workers/fetchAsWorker.js';
4
4
  import TEXTURES_WORKER_URL from './Workers/TexturesWorker.js?worker&url';
@@ -983,25 +983,31 @@ export class TextureCreator {
983
983
  // Slot 12: displacement
984
984
  mat.bumpScale, mat.displacementScale, mat.displacementMap, 0,
985
985
  mapMatrix[ 0 ], mapMatrix[ 1 ], mapMatrix[ 2 ], mapMatrix[ 3 ],
986
- mapMatrix[ 4 ], mapMatrix[ 5 ], mapMatrix[ 6 ], 1,
986
+ mapMatrix[ 4 ], mapMatrix[ 5 ], mapMatrix[ 6 ], mapMatrix[ 7 ],
987
987
  normalMapMatrices[ 0 ], normalMapMatrices[ 1 ], normalMapMatrices[ 2 ], normalMapMatrices[ 3 ],
988
- normalMapMatrices[ 4 ], normalMapMatrices[ 5 ], normalMapMatrices[ 6 ], 1,
988
+ normalMapMatrices[ 4 ], normalMapMatrices[ 5 ], normalMapMatrices[ 6 ], normalMapMatrices[ 7 ],
989
989
  roughnessMapMatrices[ 0 ], roughnessMapMatrices[ 1 ], roughnessMapMatrices[ 2 ], roughnessMapMatrices[ 3 ],
990
- roughnessMapMatrices[ 4 ], roughnessMapMatrices[ 5 ], roughnessMapMatrices[ 6 ], 1,
990
+ roughnessMapMatrices[ 4 ], roughnessMapMatrices[ 5 ], roughnessMapMatrices[ 6 ], roughnessMapMatrices[ 7 ],
991
991
  metalnessMapMatrices[ 0 ], metalnessMapMatrices[ 1 ], metalnessMapMatrices[ 2 ], metalnessMapMatrices[ 3 ],
992
- metalnessMapMatrices[ 4 ], metalnessMapMatrices[ 5 ], metalnessMapMatrices[ 6 ], 1,
992
+ metalnessMapMatrices[ 4 ], metalnessMapMatrices[ 5 ], metalnessMapMatrices[ 6 ], metalnessMapMatrices[ 7 ],
993
993
  emissiveMapMatrices[ 0 ], emissiveMapMatrices[ 1 ], emissiveMapMatrices[ 2 ], emissiveMapMatrices[ 3 ],
994
- emissiveMapMatrices[ 4 ], emissiveMapMatrices[ 5 ], emissiveMapMatrices[ 6 ], 1,
994
+ emissiveMapMatrices[ 4 ], emissiveMapMatrices[ 5 ], emissiveMapMatrices[ 6 ], emissiveMapMatrices[ 7 ],
995
995
  bumpMapMatrices[ 0 ], bumpMapMatrices[ 1 ], bumpMapMatrices[ 2 ], bumpMapMatrices[ 3 ],
996
- bumpMapMatrices[ 4 ], bumpMapMatrices[ 5 ], bumpMapMatrices[ 6 ], 1,
996
+ bumpMapMatrices[ 4 ], bumpMapMatrices[ 5 ], bumpMapMatrices[ 6 ], bumpMapMatrices[ 7 ],
997
997
  displacementMapMatrices[ 0 ], displacementMapMatrices[ 1 ], displacementMapMatrices[ 2 ], displacementMapMatrices[ 3 ],
998
- displacementMapMatrices[ 4 ], displacementMapMatrices[ 5 ], displacementMapMatrices[ 6 ], 1,
998
+ displacementMapMatrices[ 4 ], displacementMapMatrices[ 5 ], displacementMapMatrices[ 6 ], displacementMapMatrices[ 7 ],
999
999
  // Slot 27: subsurface (subsurfaceColor.rgb, subsurface weight)
1000
1000
  mat.subsurfaceColor?.r ?? 1, mat.subsurfaceColor?.g ?? 1, mat.subsurfaceColor?.b ?? 1, mat.subsurface ?? 0,
1001
1001
  // Slot 28: subsurface (subsurfaceRadius.rgb, subsurfaceRadiusScale)
1002
1002
  mat.subsurfaceRadius?.[ 0 ] ?? 1, mat.subsurfaceRadius?.[ 1 ] ?? 0.2, mat.subsurfaceRadius?.[ 2 ] ?? 0.1, mat.subsurfaceRadiusScale ?? 1,
1003
- // Slot 29: subsurface (anisotropy g, reserved)
1004
- mat.subsurfaceAnisotropy ?? 0, 0, 0, 0,
1003
+ // Slot 29: subsurfaceAnisotropy g + surface anisotropy (strength, rotation, map index)
1004
+ mat.subsurfaceAnisotropy ?? 0, mat.anisotropy ?? 0, mat.anisotropyRotation ?? 0, mat.anisotropyMap ?? - 1,
1005
+ // Slot 30: extension map indices A (transmission, clearcoat, clearcoatRoughness, sheenColor)
1006
+ mat.transmissionMap ?? - 1, mat.clearcoatMap ?? - 1, mat.clearcoatRoughnessMap ?? - 1, mat.sheenColorMap ?? - 1,
1007
+ // Slot 31: extension map indices B (sheenRoughness, iridescence, iridescenceThickness, specularIntensity)
1008
+ mat.sheenRoughnessMap ?? - 1, mat.iridescenceMap ?? - 1, mat.iridescenceThicknessMap ?? - 1, mat.specularIntensityMap ?? - 1,
1009
+ // Slot 32: extension map indices C (specularColor + 3 reserved)
1010
+ mat.specularColorMap ?? - 1, 0, 0, 0,
1005
1011
  ];
1006
1012
 
1007
1013
  data.set( materialData, stride );
@@ -1314,6 +1320,11 @@ export class TextureCreator {
1314
1320
 
1315
1321
  texture.minFilter = LinearFilter;
1316
1322
  texture.magFilter = LinearFilter;
1323
+ // glTF's default sampler wrap is REPEAT; array layers are independent (not an atlas),
1324
+ // so per-layer repeat is safe and lets tiling UVs (outside [0,1]) tile instead of
1325
+ // clamping. MIRRORED_REPEAT / per-texture wrap is a follow-up (not stored per-slot yet).
1326
+ texture.wrapS = RepeatWrapping;
1327
+ texture.wrapT = RepeatWrapping;
1317
1328
  texture.format = RGBAFormat;
1318
1329
  texture.type = UnsignedByteType;
1319
1330
  texture.needsUpdate = true;
@@ -2,6 +2,7 @@ import {
2
2
  Fn,
3
3
  vec3,
4
4
  float,
5
+ dot,
5
6
  normalize,
6
7
  reflect,
7
8
  max,
@@ -10,10 +11,10 @@ import {
10
11
 
11
12
  import { struct } from './patches.js';
12
13
 
13
- import { DotProducts } from './Struct.js';
14
- import { PI, MIN_CLEARCOAT_ROUGHNESS, computeDotProducts } from './Common.js';
15
- import { DistributionGGX } from './MaterialProperties.js';
16
- import { ImportanceSampleGGX, ImportanceSampleCosine } from './MaterialSampling.js';
14
+ import { AnisoFrame, DotProducts } from './Struct.js';
15
+ import { PI, MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso, anisoTangentFrame } from './Common.js';
16
+ import { DistributionGGX, computeAnisoAlphas, calculateVNDFPDFAniso } from './MaterialProperties.js';
17
+ import { ImportanceSampleGGX, ImportanceSampleCosine, sampleGGXVNDFAniso } from './MaterialSampling.js';
17
18
  import { evaluateLayeredBRDF } from './MaterialEvaluation.js';
18
19
  import { RandomValue } from './Random.js';
19
20
 
@@ -62,8 +63,20 @@ export const sampleClearcoat = Fn( ( [
62
63
 
63
64
  } ).ElseIf( rand.lessThan( clearcoatWeight.add( specularWeight ) ), () => {
64
65
 
65
- // Sample base specular
66
- H.assign( ImportanceSampleGGX( { N, roughness: baseRoughness, Xi: randomSample } ) );
66
+ // Sample base specular (anisotropic VNDF when anisotropy > 0)
67
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
68
+
69
+ const f = AnisoFrame.wrap( anisoTangentFrame( N, material.anisotropyRotation ) );
70
+ const localV = vec3( dot( V, f.Ta ), dot( V, f.Ba ), dot( V, N ) );
71
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
72
+ const localH = sampleGGXVNDFAniso( { V: localV, alphaX: a.x, alphaY: a.y, Xi: randomSample } );
73
+ H.assign( f.Ta.mul( localH.x ).add( f.Ba.mul( localH.y ) ).add( N.mul( localH.z ) ) );
74
+
75
+ } ).Else( () => {
76
+
77
+ H.assign( ImportanceSampleGGX( { N, roughness: baseRoughness, Xi: randomSample } ) );
78
+
79
+ } );
67
80
  L.assign( reflect( V.negate(), H ) );
68
81
 
69
82
  } ).Else( () => {
@@ -74,12 +87,23 @@ export const sampleClearcoat = Fn( ( [
74
87
 
75
88
  } );
76
89
 
77
- // Calculate dot products
78
- const dots = DotProducts.wrap( computeDotProducts( N, V, L ) );
90
+ // Calculate dot products (aniso-aware: also projects onto the anisotropy frame)
91
+ const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
79
92
 
80
- // Calculate individual PDFs
93
+ // Calculate individual PDFs. Clearcoat layer is always isotropic; the base specular
94
+ // matches its sampler — VNDF-aniso when anisotropy>0, else the half-vector GGX pdf.
81
95
  const clearcoatPDF = DistributionGGX( dots.NoH, clearcoatRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( clearcoatWeight );
82
- const specularPDF = DistributionGGX( dots.NoH, baseRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( specularWeight );
96
+ const specularPDF = float( 0.0 ).toVar();
97
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
98
+
99
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
100
+ specularPDF.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ).mul( specularWeight ) );
101
+
102
+ } ).Else( () => {
103
+
104
+ specularPDF.assign( DistributionGGX( dots.NoH, baseRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( specularWeight ) );
105
+
106
+ } );
83
107
  const diffusePDF = dots.NoL.div( PI ).mul( diffuseWeight );
84
108
 
85
109
  // Combined PDF using MIS
package/src/TSL/Common.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp, bool as tslBool } from 'three/tsl';
2
2
 
3
3
  import {
4
+ AnisoFrame,
4
5
  DotProducts,
5
6
  MaterialClassification,
6
7
  MISStrategy,
@@ -155,6 +156,58 @@ export const computeDotProducts = Fn( ( [ N, V, L ] ) => {
155
156
  NoH: max( dot( N, H ), 0.001 ),
156
157
  VoH: max( dot( V, H ), 0.001 ),
157
158
  LoH: max( dot( L, H ), 0.001 ),
159
+ ToH: float( 0.0 ), BoH: float( 0.0 ),
160
+ ToV: float( 0.0 ), BoV: float( 0.0 ),
161
+ ToL: float( 0.0 ), BoL: float( 0.0 ),
162
+ } );
163
+
164
+ } );
165
+
166
+ // Anisotropy tangent frame: arbitrary ONB from N (matching constructTBN + normal mapping),
167
+ // rotated by anisotropyRotation. Single source of truth for sampler AND eval/PDF so their
168
+ // frames are bit-identical (required for MIS consistency).
169
+ export const anisoTangentFrame = Fn( ( [ N, rotation ] ) => {
170
+
171
+ const majorAxis = N.x.abs().lessThan( 0.999 ).select( vec3( 1.0, 0.0, 0.0 ), vec3( 0.0, 1.0, 0.0 ) );
172
+ const T0 = N.cross( majorAxis ).normalize();
173
+ const B0 = N.cross( T0 ).normalize();
174
+ const c = rotation.cos();
175
+ const s = rotation.sin();
176
+ return AnisoFrame( { Ta: T0.mul( c ).add( B0.mul( s ) ), Ba: B0.mul( c ).sub( T0.mul( s ) ) } );
177
+
178
+ } );
179
+
180
+ // Anisotropy-aware dot products: computeDotProducts + tangent-frame projections of H/V/L
181
+ // (0 for isotropic materials, so aniso branches gated on anisotropy>0 are a no-op).
182
+ export const computeDotProductsAniso = Fn( ( [ N, V, L, material ] ) => {
183
+
184
+ const H = V.add( L ).toVar();
185
+ const lenSq = dot( H, H ).toVar();
186
+ H.assign( lenSq.greaterThan( EPSILON ).select( H.div( lenSq.sqrt() ), vec3( 0.0, 0.0, 1.0 ) ) );
187
+
188
+ const ToH = float( 0.0 ).toVar();
189
+ const BoH = float( 0.0 ).toVar();
190
+ const ToV = float( 0.0 ).toVar();
191
+ const BoV = float( 0.0 ).toVar();
192
+ const ToL = float( 0.0 ).toVar();
193
+ const BoL = float( 0.0 ).toVar();
194
+
195
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
196
+
197
+ const f = AnisoFrame.wrap( anisoTangentFrame( N, material.anisotropyRotation ) );
198
+ ToH.assign( dot( f.Ta, H ) ); BoH.assign( dot( f.Ba, H ) );
199
+ ToV.assign( dot( f.Ta, V ) ); BoV.assign( dot( f.Ba, V ) );
200
+ ToL.assign( dot( f.Ta, L ) ); BoL.assign( dot( f.Ba, L ) );
201
+
202
+ } );
203
+
204
+ return DotProducts( {
205
+ NoL: max( dot( N, L ), 0.001 ),
206
+ NoV: max( dot( N, V ), 0.001 ),
207
+ NoH: max( dot( N, H ), 0.001 ),
208
+ VoH: max( dot( V, H ), 0.001 ),
209
+ LoH: max( dot( L, H ), 0.001 ),
210
+ ToH, BoH, ToV, BoV, ToL, BoL,
158
211
  } );
159
212
 
160
213
  } );
@@ -345,6 +398,9 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
345
398
  const data27 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SUBSURFACE_A ), int( MATERIAL_SLOTS ) ).toVar();
346
399
  const data28 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SUBSURFACE_B ), int( MATERIAL_SLOTS ) ).toVar();
347
400
  const data29 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SUBSURFACE_C ), int( MATERIAL_SLOTS ) ).toVar();
401
+ const data30 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EXT_MAP_INDICES_A ), int( MATERIAL_SLOTS ) ).toVar();
402
+ const data31 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EXT_MAP_INDICES_B ), int( MATERIAL_SLOTS ) ).toVar();
403
+ const data32 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EXT_MAP_INDICES_C ), int( MATERIAL_SLOTS ) ).toVar();
348
404
 
349
405
  return RayTracingMaterial( {
350
406
  color: vec4( data0.rgb, 1.0 ),
@@ -371,6 +427,18 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
371
427
  subsurfaceRadius: data28.rgb,
372
428
  subsurfaceRadiusScale: data28.a,
373
429
  subsurfaceAnisotropy: data29.r,
430
+ anisotropy: data29.g,
431
+ anisotropyRotation: data29.b,
432
+ anisotropyMapIndex: int( data29.a ),
433
+ transmissionMapIndex: int( data30.r ),
434
+ clearcoatMapIndex: int( data30.g ),
435
+ clearcoatRoughnessMapIndex: int( data30.b ),
436
+ sheenColorMapIndex: int( data30.a ),
437
+ sheenRoughnessMapIndex: int( data31.r ),
438
+ iridescenceMapIndex: int( data31.g ),
439
+ iridescenceThicknessMapIndex: int( data31.b ),
440
+ specularIntensityMapIndex: int( data31.a ),
441
+ specularColorMapIndex: int( data32.r ),
374
442
  albedoMapIndex: int( data8.r ),
375
443
  normalMapIndex: int( data8.g ),
376
444
  roughnessMapIndex: int( data8.b ),
@@ -458,6 +526,18 @@ export const diffuseGroundMaterial = Fn( () => {
458
526
  subsurfaceRadius: vec3( 1.0, 0.2, 0.1 ),
459
527
  subsurfaceRadiusScale: float( 1.0 ),
460
528
  subsurfaceAnisotropy: float( 0.0 ),
529
+ anisotropy: float( 0.0 ),
530
+ anisotropyRotation: float( 0.0 ),
531
+ anisotropyMapIndex: int( - 1 ),
532
+ transmissionMapIndex: int( - 1 ),
533
+ clearcoatMapIndex: int( - 1 ),
534
+ clearcoatRoughnessMapIndex: int( - 1 ),
535
+ sheenColorMapIndex: int( - 1 ),
536
+ sheenRoughnessMapIndex: int( - 1 ),
537
+ iridescenceMapIndex: int( - 1 ),
538
+ iridescenceThicknessMapIndex: int( - 1 ),
539
+ specularIntensityMapIndex: int( - 1 ),
540
+ specularColorMapIndex: int( - 1 ),
461
541
  } );
462
542
 
463
543
  } );
@@ -26,7 +26,7 @@ import {
26
26
  } from 'three/tsl';
27
27
 
28
28
  import { struct } from './patches.js';
29
- import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT, computeDotProducts } from './Common.js';
29
+ import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT, computeDotProductsAniso } from './Common.js';
30
30
  import { RandomValue } from './Random.js';
31
31
  import { calculateMaterialPDFFromDots } from './LightsSampling.js';
32
32
  import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
@@ -573,7 +573,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
573
573
 
574
574
  // Share H + dot products between BRDF eval and PDF (computeDotProducts
575
575
  // would otherwise run twice with identical inputs).
576
- const dots = DotProducts.wrap( computeDotProducts( normal, viewDir, emissiveSample.direction ) );
576
+ const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, emissiveSample.direction, material ) );
577
577
  const brdfValue = evaluateMaterialResponseFromDots( material, dots );
578
578
  const brdfPdf = calculateMaterialPDFFromDots( material, dots );
579
579
 
@@ -144,8 +144,9 @@ export const traceShadowRay = Fn( ( [
144
144
 
145
145
  If( shadowMaterial.alphaMode.equal( int( 1 ) ), () => {
146
146
 
147
- // MASK mode: binary alpha cutout
148
- const effectiveAlpha = shadowMaterial.color.a.mul( texAlpha );
147
+ // MASK mode: binary alpha cutout. Include opacity (=baseColorFactor.a) so
148
+ // factor-only MASK cuts out; mirrors the BLEND branch and the camera path.
149
+ const effectiveAlpha = shadowMaterial.color.a.mul( texAlpha ).mul( shadowMaterial.opacity );
149
150
  const cutoff = select( shadowMaterial.alphaTest.greaterThan( 0.0 ), shadowMaterial.alphaTest, float( 0.5 ) );
150
151
  If( effectiveAlpha.lessThan( cutoff ), () => {
151
152
 
@@ -25,7 +25,6 @@ import {
25
25
  abs,
26
26
  sqrt,
27
27
  dot,
28
- normalize,
29
28
  If,
30
29
  select,
31
30
  } from 'three/tsl';
@@ -41,7 +40,7 @@ import {
41
40
  EPSILON,
42
41
  MIN_PDF,
43
42
  } from './Common.js';
44
- import { DistributionGGX, calculateVNDFPDF } from './MaterialProperties.js';
43
+ import { DistributionGGX, calculateVNDFPDF, specularVNDFPDFForDir } from './MaterialProperties.js';
45
44
  import { evaluateMaterialResponse } from './MaterialEvaluation.js';
46
45
  import { RandomValue } from './Random.js';
47
46
  import { sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
@@ -342,11 +341,9 @@ export const calculateIndirectLighting = Fn( ( [
342
341
 
343
342
  If( weights.useSpecular, () => {
344
343
 
345
- // Evaluate specular PDF at sampleDir (not brdfSampleDirection which may differ)
346
- const H_spec = normalize( V.add( sampleDir ) );
347
- const NoH_spec = max( dot( N, H_spec ), 0.001 );
348
- const NoV_spec = max( dot( N, V ), 0.001 );
349
- const specPdfAtSampleDir = calculateVNDFPDF( NoH_spec, NoV_spec, material.roughness );
344
+ // Evaluate specular PDF at sampleDir (not brdfSampleDirection which may differ);
345
+ // auto-selects iso/aniso to stay consistent with the BRDF sampler.
346
+ const specPdfAtSampleDir = specularVNDFPDFForDir( material, N, V, sampleDir );
350
347
  combinedPdf.addAssign( weights.specularWeight.mul( specPdfAtSampleDir ) );
351
348
 
352
349
  } );
@@ -72,7 +72,7 @@ import {
72
72
 
73
73
  import { traverseBVHShadow } from './BVHTraversal.js';
74
74
  import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
75
- import { calculateVNDFPDF } from './MaterialProperties.js';
75
+ import { calculateVNDFPDF, calculateVNDFPDFAniso, computeAnisoAlphas } from './MaterialProperties.js';
76
76
  import { RandomValue } from './Random.js';
77
77
  import {
78
78
  selectOptimalMISStrategy,
@@ -81,7 +81,7 @@ import {
81
81
  EPSILON,
82
82
  powerHeuristic,
83
83
  balanceHeuristic,
84
- computeDotProducts,
84
+ computeDotProductsAniso,
85
85
  } from './Common.js';
86
86
  import {
87
87
  sampleEquirectProbability,
@@ -737,7 +737,18 @@ export const calculateMaterialPDFFromDots = Fn( ( [ material, dots ] ) => {
737
737
  If( specularWeight.greaterThan( 0.0 ).and( NoL.greaterThan( 0.0 ) ), () => {
738
738
 
739
739
  const roughness = max( material.roughness, 0.02 );
740
- pdf.addAssign( specularWeight.mul( calculateVNDFPDF( NoH, NoV, roughness ) ) );
740
+ const specPdf = float( 0.0 ).toVar();
741
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
742
+
743
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
744
+ specPdf.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ) );
745
+
746
+ } ).Else( () => {
747
+
748
+ specPdf.assign( calculateVNDFPDF( NoH, NoV, roughness ) );
749
+
750
+ } );
751
+ pdf.addAssign( specularWeight.mul( specPdf ) );
741
752
 
742
753
  } );
743
754
 
@@ -751,7 +762,7 @@ export const calculateMaterialPDFFromDots = Fn( ( [ material, dots ] ) => {
751
762
  // already have dots; otherwise prefer calculateMaterialPDFFromDots.
752
763
  export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material ] ) => {
753
764
 
754
- const dots = DotProducts.wrap( computeDotProducts( normal, viewDir, lightDir ) );
765
+ const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, lightDir, material ) );
755
766
  return calculateMaterialPDFFromDots( material, dots );
756
767
 
757
768
  } );
@@ -985,7 +996,7 @@ export const calculateDirectLightingUnified = Fn( ( [
985
996
 
986
997
  // Share H + dot products between BRDF eval and PDF — otherwise each
987
998
  // would recompute normalize(V+L) + 5 dot products independently.
988
- const sharedDots = DotProducts.wrap( computeDotProducts( hitNormal, viewDir, lightSample.direction ) );
999
+ const sharedDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, lightSample.direction, material ) );
989
1000
  const brdfValue = evaluateMaterialResponseFromDots( material, sharedDots );
990
1001
  const bPdf = calculateMaterialPDFFromDots( material, sharedDots ).toVar();
991
1002
 
@@ -1187,7 +1198,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1187
1198
 
1188
1199
  // Share H + dots between env BRDF/PDF — same redundancy fix as the
1189
1200
  // discrete-light path above.
1190
- const envDots = DotProducts.wrap( computeDotProducts( hitNormal, viewDir, envDirection ) );
1201
+ const envDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, envDirection, material ) );
1191
1202
  const brdfValue = evaluateMaterialResponseFromDots( material, envDots );
1192
1203
  const bPdf = calculateMaterialPDFFromDots( material, envDots ).toVar();
1193
1204