rayzee 7.20.2 → 7.20.3

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.
@@ -4,13 +4,11 @@ import {
4
4
  } from 'three/tsl';
5
5
 
6
6
  import { DotProducts, DFGResult } from './Struct.js';
7
- import {
8
- PI, PI_INV, EPSILON, MIN_CLEARCOAT_ROUGHNESS,
9
- computeDotProductsAniso,
10
- } from './Common.js';
7
+ import { PI_INV, MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso } from './Common.js';
11
8
  import { fresnelSchlick, fresnelSchlickFloat, dielectricF0 } from './Fresnel.js';
12
9
  import {
13
- DistributionGGX, SheenDistribution, GeometrySmith, evaluateDFG,
10
+ DistributionGGX, SheenDistribution, VisibilitySheen, VisibilityGGXSmithCorrelated,
11
+ sheenDirectionalAlbedo, evaluateDFG,
14
12
  computeAnisoAlphas, DistributionGGXAniso, VisibilityGGXAniso,
15
13
  } from './MaterialProperties.js';
16
14
  import { evalIridescence } from './MaterialProperties.js';
@@ -31,10 +29,14 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
31
29
  const result = vec3( 0.0 ).toVar();
32
30
 
33
31
  // Early exit for purely diffuse materials (skip if iridescent)
32
+ // `sheen` is in the guard because this fast path returns a bare Lambert term: without it a
33
+ // rough sheen material lost its sheen lobe entirely — silently, since the fast path is only
34
+ // taken above roughness 0.98.
34
35
  If( material.roughness.greaterThan( 0.98 )
35
36
  .and( material.metalness.lessThan( 0.02 ) )
36
37
  .and( material.transmission.equal( 0.0 ) )
37
38
  .and( material.clearcoat.equal( 0.0 ) )
39
+ .and( material.sheen.equal( 0.0 ) )
38
40
  .and( material.iridescence.equal( 0.0 ) ), () => {
39
41
 
40
42
  result.assign( material.color.rgb.mul( float( 1.0 ).sub( material.metalness ) ).mul( PI_INV ) );
@@ -91,8 +93,8 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
91
93
  } ).Else( () => {
92
94
 
93
95
  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
+ const Vis = VisibilityGGXSmithCorrelated( dots.NoV, dots.NoL, material.roughness );
97
+ specularSS.assign( D.mul( Vis ).mul( F ) );
96
98
 
97
99
  } );
98
100
 
@@ -102,7 +104,11 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
102
104
  const specular = specularSS.mul( dfg.compensation );
103
105
 
104
106
  // Diffuse energy budget from hemisphere-integrated specular albedo (includes multiscatter)
105
- const kD = vec3( 1.0 ).sub( dfg.E_total ).mul( float( 1.0 ).sub( material.metalness ) );
107
+ // Transmission removes energy from diffuse just as metalness does — KHR_materials_transmission
108
+ // defines transmission as replacing the diffuse component.
109
+ const kD = vec3( 1.0 ).sub( dfg.E_total )
110
+ .mul( float( 1.0 ).sub( material.metalness ) )
111
+ .mul( float( 1.0 ).sub( material.transmission ) );
106
112
  const diffuse = kD.mul( materialColor ).mul( PI_INV );
107
113
 
108
114
  const baseLayer = diffuse.add( specular ).toVar();
@@ -110,13 +116,15 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
110
116
  // Optimize sheen calculation
111
117
  If( material.sheen.greaterThan( 0.0 ), () => {
112
118
 
119
+ // D · V, not · NoL — callers apply the cosine when they integrate.
113
120
  const sheenDist = SheenDistribution( dots.NoH, material.sheenRoughness );
114
- const sheenTerm = material.sheenColor.mul( material.sheen ).mul( sheenDist ).mul( dots.NoL );
121
+ const sheenVis = VisibilitySheen( dots.NoV, dots.NoL );
122
+ const sheenTerm = material.sheenColor.mul( material.sheen ).mul( sheenDist ).mul( sheenVis );
115
123
 
116
- // Hemisphere-averaged sheen reflectance for energy-conserving base layer attenuation
117
- // Uses roughness-dependent average rather than per-sample distribution to avoid directional bias
118
- const avgSheenFactor = float( 1.0 ).sub( material.sheenRoughness ).mul( 0.5 ).add( 0.25 );
119
- const sheenReflectance = clamp( material.sheenColor.mul( material.sheen ).mul( avgSheenFactor ), vec3( 0.0 ), vec3( 1.0 ) );
124
+ // Attenuate the base by the lobe's actual directional albedo, so what the base loses is
125
+ // what the coat returns. See gen-dfg-lut.mjs for the integral.
126
+ const sheenE = sheenDirectionalAlbedo( dots.NoV, material.sheenRoughness );
127
+ const sheenReflectance = clamp( material.sheenColor.mul( material.sheen ).mul( sheenE ), vec3( 0.0 ), vec3( 1.0 ) );
120
128
  const sheenAttenuation = vec3( 1.0 ).sub( sheenReflectance );
121
129
 
122
130
  result.assign( baseLayer.mul( sheenAttenuation ).add( sheenTerm ) );
@@ -127,76 +135,41 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
127
135
 
128
136
  } );
129
137
 
130
- } );
131
-
132
- return result;
138
+ // The coat lives here, not in a separate layered-BRDF function: every strategy has to
139
+ // evaluate the same integrand or MIS is biased however good the weights are.
140
+ If( material.clearcoat.greaterThan( 0.0 ), () => {
133
141
 
134
- } );
142
+ const ccRoughness = max( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS );
143
+ const ccF0 = vec3( 0.04 );
135
144
 
136
- // Wrapper that computes dot products internally. Use this when you don't already
137
- // have dots; otherwise prefer evaluateMaterialResponseFromDots to share the work.
138
- export const evaluateMaterialResponse = Fn( ( [ V, L, N, material ] ) => {
145
+ // A GGX lobe like any other, so same multiscatter treatment; its albedo is what the
146
+ // base underneath loses.
147
+ const ccDfg = DFGResult.wrap( evaluateDFG( ccF0, dots.NoV, ccRoughness ) );
139
148
 
140
- const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
141
- return evaluateMaterialResponseFromDots( material, dots );
142
-
143
- } );
144
-
145
- // -----------------------------------------------------------------------------
146
- // Layered BRDF Evaluation (for clearcoat)
147
- // -----------------------------------------------------------------------------
149
+ const ccD = DistributionGGX( dots.NoH, ccRoughness );
150
+ const ccVis = VisibilityGGXSmithCorrelated( dots.NoV, dots.NoL, ccRoughness );
151
+ const ccF = fresnelSchlickFloat( dots.VoH, float( 0.04 ) );
152
+ const ccLobe = vec3( ccD.mul( ccVis ).mul( ccF ) ).mul( ccDfg.compensation );
148
153
 
149
- // Evaluate both clearcoat and base layer BRDFs
150
- export const evaluateLayeredBRDF = Fn( ( [ dots, material ] ) => {
151
154
 
152
- // Base F0 calculation with specular parameters, clamped to physically valid range
153
- const baseF0 = dielectricF0( material.ior );
154
- const F0 = clamp(
155
- mix( baseF0.mul( material.specularColor ), material.color.rgb, material.metalness )
156
- .mul( material.specularIntensity ),
157
- vec3( 0.0 ), vec3( 1.0 )
158
- ).toVar();
155
+ const ccAttenuation = vec3( 1.0 ).sub( ccDfg.E_total.mul( material.clearcoat ) );
159
156
 
160
- const F = fresnelSchlick( dots.VoH, F0 );
157
+ result.assign( result.mul( ccAttenuation ).add( ccLobe.mul( material.clearcoat ) ) );
161
158
 
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 ) ) );
159
+ } );
176
160
 
177
161
  } );
178
162
 
179
- // Shared DFG evaluation — compensation factor and total directional albedo
180
- // come from the same polynomial.
181
- const dfg = DFGResult.wrap( evaluateDFG( F0, dots.NoV, material.roughness ) );
182
- const baseBRDF = baseBRDFSS.mul( dfg.compensation );
183
-
184
- // Diffuse energy budget from hemisphere-integrated specular albedo (includes multiscatter)
185
- const kD = vec3( 1.0 ).sub( dfg.E_total ).mul( float( 1.0 ).sub( material.metalness ) );
186
- const diffuse = kD.mul( material.color.rgb ).div( PI );
187
- const baseLayer = diffuse.add( baseBRDF );
163
+ return result;
188
164
 
189
- // Clearcoat layer
190
- const clearcoatRoughness = max( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS );
191
- const clearcoatD = DistributionGGX( dots.NoH, clearcoatRoughness );
192
- const clearcoatG = GeometrySmith( dots.NoV, dots.NoL, clearcoatRoughness );
193
- const clearcoatF = fresnelSchlickFloat( dots.VoH, float( 0.04 ) );
194
- const clearcoatBRDF = clearcoatD.mul( clearcoatG ).mul( clearcoatF )
195
- .div( max( float( 4.0 ).mul( dots.NoV ).mul( dots.NoL ), EPSILON ) );
165
+ } );
196
166
 
197
- // Energy conservation for clearcoat: two-interface model (1-F)² per clearcoat strength
198
- const clearcoatAttenuation = float( 1.0 ).sub( material.clearcoat.mul( clearcoatF ).mul( float( 2.0 ).sub( clearcoatF ) ) );
167
+ // Wrapper that computes dot products internally. Use this when you don't already
168
+ // have dots; otherwise prefer evaluateMaterialResponseFromDots to share the work.
169
+ export const evaluateMaterialResponse = Fn( ( [ V, L, N, material ] ) => {
199
170
 
200
- return baseLayer.mul( clearcoatAttenuation ).add( vec3( clearcoatBRDF ).mul( material.clearcoat ) );
171
+ const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
172
+ return evaluateMaterialResponseFromDots( material, dots );
201
173
 
202
174
  } );
175
+