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.
@@ -6,11 +6,12 @@ import {
6
6
  import { DotProducts, DFGResult } from './Struct.js';
7
7
  import {
8
8
  PI, PI_INV, EPSILON, MIN_CLEARCOAT_ROUGHNESS,
9
- computeDotProducts,
9
+ computeDotProductsAniso,
10
10
  } from './Common.js';
11
11
  import { fresnelSchlick, fresnelSchlickFloat, dielectricF0 } from './Fresnel.js';
12
12
  import {
13
13
  DistributionGGX, SheenDistribution, GeometrySmith, evaluateDFG,
14
+ computeAnisoAlphas, DistributionGGXAniso, VisibilityGGXAniso,
14
15
  } from './MaterialProperties.js';
15
16
  import { evalIridescence } from './MaterialProperties.js';
16
17
 
@@ -75,12 +76,25 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
75
76
  } );
76
77
 
77
78
  // Precalculate shared terms
78
- const D = DistributionGGX( dots.NoH, material.roughness );
79
- const G = GeometrySmith( dots.NoV, dots.NoL, material.roughness );
80
79
  const F = fresnelSchlick( dots.VoH, F0 );
81
80
 
82
- // Single-scatter specular BRDF
83
- const specularSS = D.mul( G ).mul( F ).div( max( float( 4.0 ).mul( dots.NoV ).mul( dots.NoL ), EPSILON ) );
81
+ // Single-scatter specular BRDF (anisotropic when material.anisotropy > 0; the aniso
82
+ // visibility term already carries the 1/(4·NoV·NoL) denominator)
83
+ const specularSS = vec3( 0.0 ).toVar();
84
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
85
+
86
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
87
+ const Da = DistributionGGXAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH );
88
+ const Va = VisibilityGGXAniso( a.x, a.y, dots.ToV, dots.BoV, dots.ToL, dots.BoL, dots.NoV, dots.NoL );
89
+ specularSS.assign( F.mul( Da.mul( Va ) ) );
90
+
91
+ } ).Else( () => {
92
+
93
+ const D = DistributionGGX( dots.NoH, material.roughness );
94
+ const G = GeometrySmith( dots.NoV, dots.NoL, material.roughness );
95
+ specularSS.assign( D.mul( G ).mul( F ).div( max( float( 4.0 ).mul( dots.NoV ).mul( dots.NoL ), EPSILON ) ) );
96
+
97
+ } );
84
98
 
85
99
  // Shared DFG evaluation — compensation factor and total directional albedo
86
100
  // come from the same polynomial.
@@ -123,7 +137,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
123
137
  // have dots; otherwise prefer evaluateMaterialResponseFromDots to share the work.
124
138
  export const evaluateMaterialResponse = Fn( ( [ V, L, N, material ] ) => {
125
139
 
126
- const dots = DotProducts.wrap( computeDotProducts( N, V, L ) );
140
+ const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
127
141
  return evaluateMaterialResponseFromDots( material, dots );
128
142
 
129
143
  } );
@@ -143,10 +157,24 @@ export const evaluateLayeredBRDF = Fn( ( [ dots, material ] ) => {
143
157
  vec3( 0.0 ), vec3( 1.0 )
144
158
  ).toVar();
145
159
 
146
- const D = DistributionGGX( dots.NoH, material.roughness );
147
- const G = GeometrySmith( dots.NoV, dots.NoL, material.roughness );
148
160
  const F = fresnelSchlick( dots.VoH, F0 );
149
- const baseBRDFSS = D.mul( G ).mul( F ).div( max( float( 4.0 ).mul( dots.NoV ).mul( dots.NoL ), EPSILON ) );
161
+
162
+ // Base specular (anisotropic when anisotropy > 0; aniso V term carries 1/(4·NoV·NoL))
163
+ const baseBRDFSS = vec3( 0.0 ).toVar();
164
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
165
+
166
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
167
+ const Da = DistributionGGXAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH );
168
+ const Va = VisibilityGGXAniso( a.x, a.y, dots.ToV, dots.BoV, dots.ToL, dots.BoL, dots.NoV, dots.NoL );
169
+ baseBRDFSS.assign( F.mul( Da.mul( Va ) ) );
170
+
171
+ } ).Else( () => {
172
+
173
+ const D = DistributionGGX( dots.NoH, material.roughness );
174
+ const G = GeometrySmith( dots.NoV, dots.NoL, material.roughness );
175
+ baseBRDFSS.assign( D.mul( G ).mul( F ).div( max( float( 4.0 ).mul( dots.NoV ).mul( dots.NoL ), EPSILON ) ) );
176
+
177
+ } );
150
178
 
151
179
  // Shared DFG evaluation — compensation factor and total directional albedo
152
180
  // come from the same polynomial.
@@ -1,16 +1,17 @@
1
- import { Fn, float, vec3, int, If, dot, max, min, sqrt, cos, exp, mix, clamp, smoothstep } from 'three/tsl';
1
+ import { Fn, float, vec2, vec3, int, If, dot, max, min, sqrt, cos, exp, mix, clamp, smoothstep } from 'three/tsl';
2
2
 
3
3
  import {
4
4
  BRDFWeights,
5
5
  MaterialCache,
6
6
  ImportanceSamplingInfo,
7
7
  DFGResult,
8
+ DotProducts,
8
9
 
9
10
  } from './Struct.js';
10
11
 
11
12
  import {
12
13
  PI, TWO_PI, EPSILON, MIN_ROUGHNESS,
13
- XYZ_TO_REC709, square,
14
+ XYZ_TO_REC709, square, computeDotProductsAniso,
14
15
  } from './Common.js';
15
16
 
16
17
  import {
@@ -124,6 +125,81 @@ export const calculateVNDFPDF = Fn( ( [ NoH, NoV, roughness ] ) => {
124
125
 
125
126
  } );
126
127
 
128
+ // -----------------------------------------------------------------------------
129
+ // Anisotropic GGX (Filament / three.js convention — matches WebGLRenderer/glTF)
130
+ // -----------------------------------------------------------------------------
131
+ // alphaB = roughness² (bitangent axis), alphaT = mix(roughness², 1, anisotropy²) (tangent axis).
132
+ // Returns vec2( alphaT, alphaB ). Both floored so smooth anisotropic surfaces stay stable.
133
+ export const computeAnisoAlphas = Fn( ( [ roughness, anisotropy ] ) => {
134
+
135
+ const alphaB = max( roughness.mul( roughness ), 1e-4 ).toVar();
136
+ const alphaT = max( mix( alphaB, float( 1.0 ), anisotropy.mul( anisotropy ) ), 1e-4 );
137
+ return vec2( alphaT, alphaB );
138
+
139
+ } );
140
+
141
+ // Anisotropic GGX normal distribution (Filament D_GGX_Anisotropic). Reduces exactly to
142
+ // DistributionGGX when alphaT == alphaB.
143
+ export const DistributionGGXAniso = Fn( ( [ alphaT, alphaB, NoH, ToH, BoH ] ) => {
144
+
145
+ const a2 = alphaT.mul( alphaB );
146
+ const v = vec3( alphaB.mul( ToH ), alphaT.mul( BoH ), a2.mul( NoH ) );
147
+ const v2 = max( dot( v, v ), EPSILON );
148
+ const w2 = a2.div( v2 );
149
+ return a2.mul( w2.mul( w2 ) ).div( PI );
150
+
151
+ } );
152
+
153
+ // Anisotropic Smith height-correlated visibility (three.js V_GGX_SmithCorrelated_Anisotropic).
154
+ // Already includes the 1/(4·NoV·NoL) denominator — multiply directly by D and F.
155
+ export const VisibilityGGXAniso = Fn( ( [ alphaT, alphaB, ToV, BoV, ToL, BoL, NoV, NoL ] ) => {
156
+
157
+ const gv = NoL.mul( vec3( alphaT.mul( ToV ), alphaB.mul( BoV ), NoV ).length() );
158
+ const gl = NoV.mul( vec3( alphaT.mul( ToL ), alphaB.mul( BoL ), NoL ).length() );
159
+ return float( 0.5 ).div( max( gv.add( gl ), EPSILON ) );
160
+
161
+ } );
162
+
163
+ // Anisotropic Smith masking G1 (for the VNDF pdf). G1 = 1 / (1 + Λ).
164
+ export const smithG1Aniso = Fn( ( [ alphaT, alphaB, ToV, BoV, NoV ] ) => {
165
+
166
+ const t = alphaT.mul( ToV );
167
+ const b = alphaB.mul( BoV );
168
+ const num = t.mul( t ).add( b.mul( b ) );
169
+ const nov2 = max( NoV.mul( NoV ), EPSILON );
170
+ const lambda = sqrt( float( 1.0 ).add( num.div( nov2 ) ) ).sub( 1.0 ).mul( 0.5 );
171
+ return float( 1.0 ).div( lambda.add( 1.0 ) );
172
+
173
+ } );
174
+
175
+ // Anisotropic VNDF pdf over the reflected direction: G1(V)·D(H) / (4·NoV).
176
+ export const calculateVNDFPDFAniso = Fn( ( [ alphaT, alphaB, NoH, ToH, BoH, NoV, ToV, BoV ] ) => {
177
+
178
+ const D = DistributionGGXAniso( alphaT, alphaB, NoH, ToH, BoH );
179
+ const G1 = smithG1Aniso( alphaT, alphaB, ToV, BoV, NoV );
180
+ return D.mul( G1 ).div( max( NoV.mul( 4.0 ), EPSILON ) );
181
+
182
+ } );
183
+
184
+ // Specular VNDF pdf (iso/aniso auto-select) for MIS sites that only have (N, V, L).
185
+ export const specularVNDFPDFForDir = Fn( ( [ material, N, V, L ] ) => {
186
+
187
+ const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
188
+ const pdf = float( 0.0 ).toVar();
189
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
190
+
191
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
192
+ pdf.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ) );
193
+
194
+ } ).Else( () => {
195
+
196
+ pdf.assign( calculateVNDFPDF( dots.NoH, dots.NoV, max( material.roughness, 0.02 ) ) );
197
+
198
+ } );
199
+ return pdf;
200
+
201
+ } );
202
+
127
203
  // -----------------------------------------------------------------------------
128
204
  // Iridescence Evaluation
129
205
  // -----------------------------------------------------------------------------
@@ -74,3 +74,22 @@ export const sampleGGXVNDF = /*@__PURE__*/ wgslFn( `
74
74
  return normalize( vec3f( alpha * Nh.x, alpha * Nh.y, max( 0.0f, Nh.z ) ) );
75
75
  }
76
76
  ` );
77
+
78
+ // Anisotropic VNDF: identical to sampleGGXVNDF but stretches x/y by separate alphas.
79
+ // V is in the anisotropy tangent frame (x=tangent, y=bitangent, z=normal); returns local H.
80
+ export const sampleGGXVNDFAniso = /*@__PURE__*/ wgslFn( `
81
+ fn sampleGGXVNDFAniso( V: vec3f, alphaX: f32, alphaY: f32, Xi: vec2f ) -> vec3f {
82
+ let Vh = normalize( vec3f( alphaX * V.x, alphaY * V.y, V.z ) );
83
+ let lensq = Vh.x * Vh.x + Vh.y * Vh.y;
84
+ let T1 = select( vec3f( 1.0f, 0.0f, 0.0f ), vec3f( -Vh.y, Vh.x, 0.0f ) / sqrt( lensq ), lensq > 1e-8f );
85
+ let T2 = cross( Vh, T1 );
86
+ let r = sqrt( Xi.x );
87
+ let phi = 6.28318530717958647692f * Xi.y;
88
+ let t1 = r * cos( phi );
89
+ let t2tmp = r * sin( phi );
90
+ let s = 0.5f * ( 1.0f + Vh.z );
91
+ let t2 = ( 1.0f - s ) * sqrt( 1.0f - t1 * t1 ) + s * t2tmp;
92
+ let Nh = T1 * t1 + T2 * t2 + Vh * sqrt( max( 0.0f, 1.0f - t1 * t1 - t2 * t2 ) );
93
+ return normalize( vec3f( alphaX * Nh.x, alphaY * Nh.y, max( 0.0f, Nh.z ) ) );
94
+ }
95
+ ` );
@@ -546,7 +546,9 @@ export const handleMaterialTransparency = Fn( ( [
546
546
 
547
547
  const cutoff = select( material.alphaTest.greaterThan( 0.0 ), material.alphaTest, float( 0.5 ) );
548
548
 
549
- If( material.color.a.lessThan( cutoff ), () => {
549
+ // Effective alpha = textureAlpha (in color.a) × baseColorFactor.a (opacity).
550
+ // color.a base is 1.0, so factor-only MASK (no albedo map) relies on opacity.
551
+ If( material.color.a.mul( material.opacity ).lessThan( cutoff ), () => {
550
552
 
551
553
  result.continueRay.assign( true );
552
554
  result.direction.assign( ray.direction );
@@ -33,15 +33,18 @@ import {
33
33
  MIN_CLEARCOAT_ROUGHNESS,
34
34
  MIN_PDF,
35
35
  constructTBN,
36
+ anisoTangentFrame,
36
37
  calculateFireflyThreshold,
37
38
  applySoftSuppressionRGB,
38
39
  } from './Common.js';
39
- import { DirectionSample, MaterialCache } from './Struct.js';
40
+ import { AnisoFrame, DirectionSample, MaterialCache } from './Struct.js';
40
41
  import { RandomValue } from './Random.js';
41
42
  import { sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
42
43
  import {
43
44
  SheenDistribution,
44
45
  calculateVNDFPDF,
46
+ calculateVNDFPDFAniso,
47
+ computeAnisoAlphas,
45
48
  calculateBRDFWeights,
46
49
  } from './MaterialProperties.js';
47
50
  import { evaluateMaterialResponse } from './MaterialEvaluation.js';
@@ -50,6 +53,7 @@ import {
50
53
  ImportanceSampleCosine,
51
54
  ImportanceSampleGGX,
52
55
  sampleGGXVNDF,
56
+ sampleGGXVNDFAniso,
53
57
  } from './MaterialSampling.js';
54
58
 
55
59
  // =============================================================================
@@ -125,18 +129,39 @@ export const generateSampledDirection = Fn( ( [
125
129
 
126
130
  } ).ElseIf( rand.lessThan( cumulativeSpecular ), () => {
127
131
 
128
- const TBN = constructTBN( { N } );
129
- const localV = TBN.transpose().mul( V );
132
+ If( material.anisotropy.greaterThan( 0.0 ), () => {
130
133
 
131
- // VNDF sampling
132
- const localH = sampleGGXVNDF( { V: localV, roughness: material.roughness, Xi: xi } );
133
- H.assign( TBN.mul( localH ) );
134
+ // Shared frame → sampler and eval/PDF stay bit-identical (MIS consistency)
135
+ const f = AnisoFrame.wrap( anisoTangentFrame( N, material.anisotropyRotation ) );
136
+ const Ta = f.Ta;
137
+ const Ba = f.Ba;
134
138
 
135
- const NoH = clamp( dot( N, H ), 0.001, 1.0 );
139
+ const localV = vec3( dot( V, Ta ), dot( V, Ba ), dot( V, N ) );
140
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
141
+ const localH = sampleGGXVNDFAniso( { V: localV, alphaX: a.x, alphaY: a.y, Xi: xi } );
142
+ H.assign( Ta.mul( localH.x ).add( Ba.mul( localH.y ) ).add( N.mul( localH.z ) ) );
136
143
 
137
- resultDirection.assign( reflect( V.negate(), H ) );
138
- resultPdf.assign( calculateVNDFPDF( NoH, NoV, material.roughness ) );
139
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
144
+ const NoH = clamp( dot( N, H ), 0.001, 1.0 );
145
+ resultDirection.assign( reflect( V.negate(), H ) );
146
+ resultPdf.assign( calculateVNDFPDFAniso( a.x, a.y, NoH, dot( Ta, H ), dot( Ba, H ), NoV, dot( Ta, V ), dot( Ba, V ) ) );
147
+ resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
148
+
149
+ } ).Else( () => {
150
+
151
+ const TBN = constructTBN( { N } );
152
+ const localV = TBN.transpose().mul( V );
153
+
154
+ // VNDF sampling
155
+ const localH = sampleGGXVNDF( { V: localV, roughness: material.roughness, Xi: xi } );
156
+ H.assign( TBN.mul( localH ) );
157
+
158
+ const NoH = clamp( dot( N, H ), 0.001, 1.0 );
159
+
160
+ resultDirection.assign( reflect( V.negate(), H ) );
161
+ resultPdf.assign( calculateVNDFPDF( NoH, NoV, material.roughness ) );
162
+ resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
163
+
164
+ } );
140
165
 
141
166
  } ).ElseIf( rand.lessThan( cumulativeSheen ), () => {
142
167
 
@@ -17,7 +17,7 @@ import {
17
17
  import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
18
18
  import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial } from './Common.js';
19
19
  import { cosineWeightedSample } from './MaterialSampling.js';
20
- import { sampleAllMaterialTextures } from './TextureSampling.js';
20
+ import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV } from './TextureSampling.js';
21
21
  import { evaluateMaterialResponse } from './MaterialEvaluation.js';
22
22
  import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
23
23
  import { traceShadowRay, calculateRayOffset } from './LightsDirect.js';
@@ -37,6 +37,7 @@ import {
37
37
  HitInfo,
38
38
  RayTracingMaterial,
39
39
  MaterialSamples,
40
+ ExtMapResult,
40
41
  DirectionSample,
41
42
  ImportanceSamplingInfo,
42
43
  MaterialClassification,
@@ -549,6 +550,35 @@ export function buildShadeKernel( params ) {
549
550
  material.roughness.assign( matSamples.roughness.clamp( 0.05, 1.0 ) );
550
551
  material.sheenRoughness.assign( material.sheenRoughness.clamp( 0.05, 1.0 ) ); // megakernel parity (PathTracerCore.js:1060): sample/PDF mismatch at sheenRoughness~0
551
552
 
553
+ // Anisotropy + extension maps carry no per-slot transform of their own, so reuse the material's
554
+ // albedo UV-transform (KHR_texture_transform). Extension/aniso textures nearly always share the
555
+ // base UV set + transform, so this aligns tiled/offset maps (e.g. SheenCloth's 30× sheen weave)
556
+ // instead of sampling at raw 1× UV. Identity albedo transform → same as raw UV (safe no-op).
557
+ const extUV = getTransformedUV( { uv: samplingUV, transform: material.albedoTransform } ).toVar();
558
+
559
+ // Fold the anisotropy texture (if any) into the scalar anisotropy/rotation used by the BRDF
560
+ If( material.anisotropyMapIndex.greaterThanEqual( int( 0 ) ), () => {
561
+
562
+ const aniso = processAnisotropyMap( material, extUV ).toVar();
563
+ material.anisotropy.assign( aniso.x );
564
+ material.anisotropyRotation.assign( aniso.y );
565
+
566
+ } );
567
+
568
+ // Fold the glTF extension textures (transmission/clearcoat/sheen/iridescence/specular) into
569
+ // their scalar factors. `material` is the shared mutable struct that flows into BOTH the
570
+ // BRDF-sample path and the NEE evaluator, so modulating here covers every consumer at once.
571
+ const extMaps = ExtMapResult.wrap( applyExtensionMaps( material, extUV ) ).toVar();
572
+ material.transmission.assign( extMaps.transmission );
573
+ material.clearcoat.assign( extMaps.clearcoat );
574
+ material.clearcoatRoughness.assign( extMaps.clearcoatRoughness );
575
+ material.sheenColor.assign( extMaps.sheenColor );
576
+ material.sheenRoughness.assign( extMaps.sheenRoughness );
577
+ material.iridescence.assign( extMaps.iridescence );
578
+ material.iridescenceThicknessRange.assign( vec2( material.iridescenceThicknessRange.x, extMaps.iridescenceThickness ) );
579
+ material.specularIntensity.assign( extMaps.specularIntensity );
580
+ material.specularColor.assign( extMaps.specularColor );
581
+
552
582
  const albedo = matSamples.albedo.toVar();
553
583
  If(
554
584
  material.displacementMapIndex.greaterThanEqual( int( 0 ) )
package/src/TSL/Struct.js CHANGED
@@ -55,6 +55,33 @@ export const RayTracingMaterial = struct( {
55
55
  subsurfaceRadius: 'vec3', // per-channel mean free path
56
56
  subsurfaceRadiusScale: 'float', // scalar multiplier on radius
57
57
  subsurfaceAnisotropy: 'float', // Henyey-Greenstein g (-1..1)
58
+ anisotropy: 'float', // surface specular anisotropy strength (0..1)
59
+ anisotropyRotation: 'float', // anisotropy tangent rotation (radians)
60
+ anisotropyMapIndex: 'int', // packed linear-bucket index, -1 if none
61
+ // Extension-texture map indices (packed bucket index, -1 if none). Fold applied in ShadeKernel.
62
+ transmissionMapIndex: 'int',
63
+ clearcoatMapIndex: 'int',
64
+ clearcoatRoughnessMapIndex: 'int',
65
+ sheenColorMapIndex: 'int',
66
+ sheenRoughnessMapIndex: 'int',
67
+ iridescenceMapIndex: 'int',
68
+ iridescenceThicknessMapIndex: 'int',
69
+ specularIntensityMapIndex: 'int',
70
+ specularColorMapIndex: 'int',
71
+ } );
72
+
73
+ // Result of folding the glTF extension textures into their scalar factors (applyExtensionMaps).
74
+ // Each field is the material's scalar × the sampled map channel (or the scalar unchanged if no map).
75
+ export const ExtMapResult = struct( {
76
+ transmission: 'float',
77
+ clearcoat: 'float',
78
+ clearcoatRoughness: 'float',
79
+ sheenColor: 'vec3',
80
+ sheenRoughness: 'float',
81
+ iridescence: 'float',
82
+ iridescenceThickness: 'float', // resolved thin-film thickness → written into iridescenceThicknessRange.y
83
+ specularIntensity: 'float',
84
+ specularColor: 'vec3',
58
85
  } );
59
86
 
60
87
  // Lightweight material for shadow ray evaluation — only the fields needed
@@ -142,12 +169,24 @@ export const ImportanceSamplingInfo = struct( {
142
169
  clearcoatImportance: 'float',
143
170
  } );
144
171
 
172
+ // Anisotropy tangent frame in world space (rotated ONB). Shared by the sampler and
173
+ // the eval/PDF so both derive the identical frame — see anisoTangentFrame().
174
+ export const AnisoFrame = struct( {
175
+ Ta: 'vec3', // anisotropy tangent
176
+ Ba: 'vec3', // anisotropy bitangent
177
+ } );
178
+
145
179
  export const DotProducts = struct( {
146
180
  NoL: 'float', // Normal • Light
147
181
  NoV: 'float', // Normal • View
148
182
  NoH: 'float', // Normal • Half
149
183
  VoH: 'float', // View • Half
150
184
  LoH: 'float', // Light • Half
185
+ // Anisotropy tangent-frame projections (0 unless computed via computeDotProductsAniso).
186
+ // T = anisotropy tangent, B = anisotropy bitangent.
187
+ ToH: 'float', BoH: 'float',
188
+ ToV: 'float', BoV: 'float',
189
+ ToL: 'float', BoL: 'float',
151
190
  } );
152
191
 
153
192
  // Kulla-Conty DFG approximation outputs (computed once, consumed by both
@@ -1,9 +1,10 @@
1
- import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, abs, mix, clamp, texture, textureSize } from 'three/tsl';
1
+ import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, abs, atan, mix, clamp, texture, textureSize } from 'three/tsl';
2
2
  import { DataArrayTexture, LinearFilter } from 'three';
3
3
 
4
4
  import {
5
5
  UVCache,
6
6
  MaterialSamples,
7
+ ExtMapResult,
7
8
  } from './Struct.js';
8
9
  import { TEXTURE_CONSTANTS } from '../EngineDefaults.js';
9
10
 
@@ -431,6 +432,107 @@ export const processBump = Fn( ( [ currentNormal, material, uvCache ] ) => {
431
432
 
432
433
  } );
433
434
 
435
+ // Fold the glTF anisotropyTexture (RG = tangent-space direction, B = strength) into scalar
436
+ // (strength, rotationRadians) per three.js: anisotropy · R(rotation) · (normalize(rg·2−1)·b).
437
+ // Caller guarantees anisotropyMapIndex >= 0. `uv` is pre-transformed by the caller (albedo transform).
438
+ export const processAnisotropyMap = Fn( ( [ material, uv ] ) => {
439
+
440
+ const result = vec2( material.anisotropy, material.anisotropyRotation ).toVar();
441
+
442
+ const s = sampleBucket( _linearBuckets, material.anisotropyMapIndex, uv ).toVar();
443
+ const texDir = s.rg.mul( 2.0 ).sub( 1.0 ).toVar();
444
+ const dlen = texDir.length().toVar();
445
+
446
+ If( dlen.greaterThan( 1e-4 ), () => {
447
+
448
+ const u = texDir.div( dlen ).mul( s.b ).toVar(); // unit direction × texture strength
449
+ const c = material.anisotropyRotation.cos();
450
+ const sn = material.anisotropyRotation.sin();
451
+ const vx = material.anisotropy.mul( c.mul( u.x ).sub( sn.mul( u.y ) ) );
452
+ const vy = material.anisotropy.mul( sn.mul( u.x ).add( c.mul( u.y ) ) );
453
+ result.assign( vec2( clamp( vx.mul( vx ).add( vy.mul( vy ) ).sqrt(), 0.0, 1.0 ), atan( vy, vx ) ) );
454
+
455
+ } ).Else( () => {
456
+
457
+ result.assign( vec2( 0.0, material.anisotropyRotation ) );
458
+
459
+ } );
460
+
461
+ return result;
462
+
463
+ } );
464
+
465
+ // Fold the glTF extension textures into their scalar factors, per KHR_materials_* / three.js
466
+ // channel conventions. Returns the modulated scalars (unchanged where a map is absent). `uv` is
467
+ // pre-transformed by the caller with the material's albedo KHR_texture_transform (extension maps
468
+ // share the base UV set/transform in practice). Color maps (sheenColor, specularColor) read the
469
+ // sRGB pool; data maps read the linear pool.
470
+ export const applyExtensionMaps = Fn( ( [ material, uv ] ) => {
471
+
472
+ const r = ExtMapResult( {
473
+ transmission: material.transmission,
474
+ clearcoat: material.clearcoat,
475
+ clearcoatRoughness: material.clearcoatRoughness,
476
+ sheenColor: material.sheenColor,
477
+ sheenRoughness: material.sheenRoughness,
478
+ iridescence: material.iridescence,
479
+ iridescenceThickness: material.iridescenceThicknessRange.y, // default = max (no-texture behavior)
480
+ specularIntensity: material.specularIntensity,
481
+ specularColor: material.specularColor,
482
+ } ).toVar();
483
+
484
+ If( material.transmissionMapIndex.greaterThanEqual( int( 0 ) ), () => {
485
+
486
+ r.transmission.assign( r.transmission.mul( sampleBucket( _linearBuckets, material.transmissionMapIndex, uv ).r ) );
487
+
488
+ } );
489
+ If( material.clearcoatMapIndex.greaterThanEqual( int( 0 ) ), () => {
490
+
491
+ r.clearcoat.assign( r.clearcoat.mul( sampleBucket( _linearBuckets, material.clearcoatMapIndex, uv ).r ) );
492
+
493
+ } );
494
+ If( material.clearcoatRoughnessMapIndex.greaterThanEqual( int( 0 ) ), () => {
495
+
496
+ r.clearcoatRoughness.assign( r.clearcoatRoughness.mul( sampleBucket( _linearBuckets, material.clearcoatRoughnessMapIndex, uv ).g ) );
497
+
498
+ } );
499
+ If( material.sheenColorMapIndex.greaterThanEqual( int( 0 ) ), () => {
500
+
501
+ r.sheenColor.assign( r.sheenColor.mul( sampleBucket( _srgbBuckets, material.sheenColorMapIndex, uv ).rgb ) );
502
+
503
+ } );
504
+ If( material.sheenRoughnessMapIndex.greaterThanEqual( int( 0 ) ), () => {
505
+
506
+ // clamp to [0.05,1] to keep parity with the sample/PDF floor applied in ShadeKernel
507
+ r.sheenRoughness.assign( clamp( r.sheenRoughness.mul( sampleBucket( _linearBuckets, material.sheenRoughnessMapIndex, uv ).a ), 0.05, 1.0 ) );
508
+
509
+ } );
510
+ If( material.iridescenceMapIndex.greaterThanEqual( int( 0 ) ), () => {
511
+
512
+ r.iridescence.assign( r.iridescence.mul( sampleBucket( _linearBuckets, material.iridescenceMapIndex, uv ).r ) );
513
+
514
+ } );
515
+ If( material.iridescenceThicknessMapIndex.greaterThanEqual( int( 0 ) ), () => {
516
+
517
+ const g = sampleBucket( _linearBuckets, material.iridescenceThicknessMapIndex, uv ).g;
518
+ r.iridescenceThickness.assign( mix( material.iridescenceThicknessRange.x, material.iridescenceThicknessRange.y, g ) );
519
+
520
+ } );
521
+ If( material.specularIntensityMapIndex.greaterThanEqual( int( 0 ) ), () => {
522
+
523
+ r.specularIntensity.assign( r.specularIntensity.mul( sampleBucket( _linearBuckets, material.specularIntensityMapIndex, uv ).a ) );
524
+
525
+ } );
526
+ If( material.specularColorMapIndex.greaterThanEqual( int( 0 ) ), () => {
527
+
528
+ r.specularColor.assign( r.specularColor.mul( sampleBucket( _srgbBuckets, material.specularColorMapIndex, uv ).rgb ) );
529
+
530
+ } );
531
+
532
+ return r;
533
+
534
+ } );
535
+
434
536
  export const processEmissive = Fn( ( [ material, uvCache ] ) => {
435
537
 
436
538
  const emissionBase = material.emissive.mul( material.emissiveIntensity ).toVar();
@@ -329,6 +329,8 @@ export class MaterialDataManager {
329
329
  case 'subsurface': data[ stride + M.SUBSURFACE ] = value; break;
330
330
  case 'subsurfaceRadiusScale': data[ stride + M.SUBSURFACE_RADIUS_SCALE ] = value; break;
331
331
  case 'subsurfaceAnisotropy': data[ stride + M.SUBSURFACE_ANISOTROPY ] = value; break;
332
+ case 'anisotropy': data[ stride + M.ANISOTROPY ] = value; break;
333
+ case 'anisotropyRotation': data[ stride + M.ANISOTROPY_ROTATION ] = value; break;
332
334
  case 'subsurfaceColor':
333
335
  if ( value.r !== undefined ) {
334
336
 
@@ -521,7 +523,25 @@ export class MaterialDataManager {
521
523
  data[ stride + M.SUBSURFACE_RADIUS_SCALE ] = materialData.subsurfaceRadiusScale ?? 1;
522
524
  data[ stride + M.SUBSURFACE_ANISOTROPY ] = materialData.subsurfaceAnisotropy ?? 0;
523
525
 
524
- // Texture transformation matrices (9 floats each, identity if missing)
526
+ // Surface specular anisotropy (map index defaults to -1 = none)
527
+ data[ stride + M.ANISOTROPY ] = materialData.anisotropy ?? 0;
528
+ data[ stride + M.ANISOTROPY_ROTATION ] = materialData.anisotropyRotation ?? 0;
529
+ data[ stride + M.ANISOTROPY_MAP_INDEX ] = materialData.anisotropyMap ?? - 1;
530
+
531
+ // Extension-texture map indices (packed bucket index, -1 = none)
532
+ data[ stride + M.TRANSMISSION_MAP_INDEX ] = materialData.transmissionMap ?? - 1;
533
+ data[ stride + M.CLEARCOAT_MAP_INDEX ] = materialData.clearcoatMap ?? - 1;
534
+ data[ stride + M.CLEARCOAT_ROUGHNESS_MAP_INDEX ] = materialData.clearcoatRoughnessMap ?? - 1;
535
+ data[ stride + M.SHEEN_COLOR_MAP_INDEX ] = materialData.sheenColorMap ?? - 1;
536
+ data[ stride + M.SHEEN_ROUGHNESS_MAP_INDEX ] = materialData.sheenRoughnessMap ?? - 1;
537
+ data[ stride + M.IRIDESCENCE_MAP_INDEX ] = materialData.iridescenceMap ?? - 1;
538
+ data[ stride + M.IRIDESCENCE_THICKNESS_MAP_INDEX ] = materialData.iridescenceThicknessMap ?? - 1;
539
+ data[ stride + M.SPECULAR_INTENSITY_MAP_INDEX ] = materialData.specularIntensityMap ?? - 1;
540
+ data[ stride + M.SPECULAR_COLOR_MAP_INDEX ] = materialData.specularColorMap ?? - 1;
541
+
542
+ // Texture transformation matrices (8 floats per slot = matrix elements[0..7];
543
+ // element[8]=1 is reconstructed on the GPU by arrayToMat3, so it is NOT stored —
544
+ // writing a 9th float here would spill into the next slot / subsurfaceColor).
525
545
  const identity = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
526
546
  const transformEntries = [
527
547
  { key: 'mapMatrix', offset: M.ALBEDO_TRANSFORM },
@@ -536,7 +556,7 @@ export class MaterialDataManager {
536
556
  for ( const { key, offset } of transformEntries ) {
537
557
 
538
558
  const matrix = materialData[ key ] ?? identity;
539
- for ( let i = 0; i < 9; i ++ ) {
559
+ for ( let i = 0; i < 8; i ++ ) {
540
560
 
541
561
  if ( stride + offset + i < data.length ) {
542
562
 
@@ -582,6 +602,17 @@ export class MaterialDataManager {
582
602
  completeMaterialData.roughnessMap = this.getPackedTextureIndex( material.roughnessMap, false );
583
603
  completeMaterialData.metalnessMap = this.getPackedTextureIndex( material.metalnessMap, false );
584
604
  completeMaterialData.displacementMap = this.getPackedTextureIndex( material.displacementMap, false );
605
+ completeMaterialData.anisotropyMap = this.getPackedTextureIndex( material.anisotropyMap, false );
606
+ // Extension maps — color maps (sheenColor, specularColor) are sRGB; the rest are data (linear)
607
+ completeMaterialData.transmissionMap = this.getPackedTextureIndex( material.transmissionMap, false );
608
+ completeMaterialData.clearcoatMap = this.getPackedTextureIndex( material.clearcoatMap, false );
609
+ completeMaterialData.clearcoatRoughnessMap = this.getPackedTextureIndex( material.clearcoatRoughnessMap, false );
610
+ completeMaterialData.sheenColorMap = this.getPackedTextureIndex( material.sheenColorMap, true );
611
+ completeMaterialData.sheenRoughnessMap = this.getPackedTextureIndex( material.sheenRoughnessMap, false );
612
+ completeMaterialData.iridescenceMap = this.getPackedTextureIndex( material.iridescenceMap, false );
613
+ completeMaterialData.iridescenceThicknessMap = this.getPackedTextureIndex( material.iridescenceThicknessMap, false );
614
+ completeMaterialData.specularIntensityMap = this.getPackedTextureIndex( material.specularIntensityMap, false );
615
+ completeMaterialData.specularColorMap = this.getPackedTextureIndex( material.specularColorMap, true );
585
616
 
586
617
  }
587
618
 
@@ -625,7 +656,9 @@ export class MaterialDataManager {
625
656
 
626
657
  }
627
658
 
628
- for ( let i = 0; i < 9; i ++ ) {
659
+ // 8 floats per slot (matrix elements[0..7]); element[8]=1 is GPU-reconstructed.
660
+ // Writing 9 would clobber the next transform slot's first element.
661
+ for ( let i = 0; i < 8; i ++ ) {
629
662
 
630
663
  if ( stride + offset + i < data.length ) {
631
664