rayzee 7.10.3 → 7.11.1
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/README.md +1 -1
- package/dist/rayzee.es.js +1688 -1398
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +69 -54
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +14 -5
- package/src/Processor/AssetLoader.js +11 -0
- package/src/Processor/GeometryExtractor.js +45 -13
- package/src/Processor/SceneProcessor.js +51 -0
- package/src/Processor/ShaderBuilder.js +3 -0
- package/src/Processor/TextureCreator.js +21 -10
- package/src/Stages/PathTracer.js +22 -0
- package/src/TSL/Clearcoat.js +34 -10
- package/src/TSL/Common.js +80 -0
- package/src/TSL/EmissiveSampling.js +2 -2
- package/src/TSL/FinalWriteKernel.js +25 -6
- package/src/TSL/LightsDirect.js +3 -2
- package/src/TSL/LightsIndirect.js +4 -7
- package/src/TSL/LightsSampling.js +17 -6
- package/src/TSL/MaterialEvaluation.js +37 -9
- package/src/TSL/MaterialProperties.js +78 -2
- package/src/TSL/MaterialSampling.js +19 -0
- package/src/TSL/MaterialTransmission.js +3 -1
- package/src/TSL/PathTracerCore.js +35 -10
- package/src/TSL/ShadeKernel.js +97 -3
- package/src/TSL/Struct.js +39 -0
- package/src/TSL/TextureSampling.js +103 -1
- package/src/managers/DenoisingManager.js +6 -0
- package/src/managers/MaterialDataManager.js +36 -3
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
Fn, wgslFn, float, vec2, vec4, int, uint, uvec2,
|
|
7
|
-
If, mix, select, texture, textureStore,
|
|
7
|
+
If, mix, select, texture, textureStore, length,
|
|
8
8
|
localId, workgroupId,
|
|
9
9
|
} from 'three/tsl';
|
|
10
10
|
|
|
@@ -32,15 +32,19 @@ export function buildFinalWriteKernel( params ) {
|
|
|
32
32
|
resolution, frame,
|
|
33
33
|
enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
|
|
34
34
|
transparentBackground,
|
|
35
|
-
prevAccumTexture, prevAlbedoTexture,
|
|
35
|
+
prevAccumTexture, prevAlbedoTexture, prevNormalDepthTexture,
|
|
36
36
|
renderWidth, renderHeight,
|
|
37
37
|
visMode,
|
|
38
38
|
// Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
|
|
39
39
|
// on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
|
|
40
40
|
auxGBufferEnabled,
|
|
41
|
+
// Clean-aux normal (1 = temporally accumulate + renormalize the aux normal). On only for clean-aux
|
|
42
|
+
// OIDN models (calb_cnrm/high, alb_nrm/balanced); off for fast/ASVGF which want the bump normal.
|
|
43
|
+
cleanAuxNormalEnabled,
|
|
41
44
|
} = params;
|
|
42
45
|
|
|
43
46
|
const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
|
|
47
|
+
const cleanAuxNormalOn = cleanAuxNormalEnabled.greaterThan( uint( 0 ) );
|
|
44
48
|
|
|
45
49
|
const computeFn = Fn( () => {
|
|
46
50
|
|
|
@@ -80,12 +84,27 @@ export function buildFinalWriteKernel( params ) {
|
|
|
80
84
|
finalColor.assign( mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) );
|
|
81
85
|
If( auxOn, () => {
|
|
82
86
|
|
|
83
|
-
// Albedo averages cleanly (it's a colour).
|
|
84
|
-
// unit normals collapses toward the flat geometric mean (worse at distance), which made
|
|
85
|
-
// OIDN smooth bump detail away. Keep this frame's point-sampled normal — it varies with
|
|
86
|
-
// the bump, which is exactly what OIDN's edge-stop needs to preserve it.
|
|
87
|
+
// Albedo averages cleanly (it's a colour).
|
|
87
88
|
finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
|
|
88
89
|
|
|
90
|
+
// NORMAL: by default keep this frame's POINT-SAMPLED normal — it varies with the bump,
|
|
91
|
+
// which fast/ASVGF want to preserve edge detail. But a CLEAN-AUX OIDN model (calb_cnrm/high,
|
|
92
|
+
// alb_nrm/balanced) trusts the aux and is fed per-frame point-sampled NOISE → leaked noise
|
|
93
|
+
// (high) / over-smoothing (balanced). When cleanAuxNormalOn, temporally accumulate it like
|
|
94
|
+
// the colour. Average the RAW unit normals (decode 0.5+0.5 → [-1,1]) and RENORMALIZE — a
|
|
95
|
+
// plain encoded-space mix would bias toward the flat (0.5,0.5,1) mean and collapse detail.
|
|
96
|
+
// Depth (.w) stays this frame's value; guard the degenerate near-zero mean (opposing normals).
|
|
97
|
+
If( cleanAuxNormalOn, () => {
|
|
98
|
+
|
|
99
|
+
const prevN = texture( prevNormalDepthTexture, prevUV, 0 ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
100
|
+
const curN = finalNormalDepth.xyz.mul( 2.0 ).sub( 1.0 ).toVar();
|
|
101
|
+
const mixedN = mix( prevN, curN, accumulationAlpha ).toVar();
|
|
102
|
+
const len = length( mixedN );
|
|
103
|
+
const avgN = select( len.greaterThan( 1e-4 ), mixedN.div( len ), curN );
|
|
104
|
+
finalNormalDepth.assign( vec4( avgN.mul( 0.5 ).add( 0.5 ), finalNormalDepth.w ) );
|
|
105
|
+
|
|
106
|
+
} );
|
|
107
|
+
|
|
89
108
|
} );
|
|
90
109
|
|
|
91
110
|
If( transparentBackground, () => {
|
package/src/TSL/LightsDirect.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
347
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
const localV = TBN.transpose().mul( V );
|
|
132
|
+
If( material.anisotropy.greaterThan( 0.0 ), () => {
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -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,
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
53
54
|
readHitDistance, readHitBarycentrics, readHitNormal,
|
|
54
55
|
readHitMaterialIndex, readHitTriangleIndex,
|
|
55
56
|
writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
|
|
56
|
-
writeGBuffer, readGBuffer, gbDecodeNormalDepth,
|
|
57
|
+
writeGBuffer, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
|
|
57
58
|
readRayRadiance,
|
|
58
59
|
} from '../Processor/PackedRayBuffer.js';
|
|
59
60
|
|
|
@@ -319,6 +320,11 @@ export function buildShadeKernel( params ) {
|
|
|
319
320
|
const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
|
|
320
321
|
const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
|
|
321
322
|
|
|
323
|
+
// Fix ①: OIDN clean-aux for a redirected ray that escapes to the environment (e.g. smooth
|
|
324
|
+
// glass over sky). Holds the clamped env colour this ray sees; written into the aux G-buffer
|
|
325
|
+
// before the miss Return below, but only when the pixel still carries the black bounce-0 default.
|
|
326
|
+
const escapedAuxAlbedo = vec3( 0.0 ).toVar();
|
|
327
|
+
|
|
322
328
|
If( wantBackdrop.or( wantEnvLight ), () => {
|
|
323
329
|
|
|
324
330
|
// Ground projection bends the backdrop-view env lookup onto a projected sphere+disk so the lower
|
|
@@ -357,6 +363,10 @@ export function buildShadeKernel( params ) {
|
|
|
357
363
|
|
|
358
364
|
} );
|
|
359
365
|
|
|
366
|
+
// Fix ①: remember the (clamped) env colour this escaped ray sees, so a redirected ray
|
|
367
|
+
// that never captured a surface aux can feed OIDN a meaningful albedo below.
|
|
368
|
+
escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
369
|
+
|
|
360
370
|
// MIS weight for implicit env hit — prevents double-counting with NEE
|
|
361
371
|
const envMisWeight = float( 1.0 ).toVar();
|
|
362
372
|
If( isBackdropView.not().and( useEnvMapIS ), () => {
|
|
@@ -424,6 +434,25 @@ export function buildShadeKernel( params ) {
|
|
|
424
434
|
|
|
425
435
|
} );
|
|
426
436
|
|
|
437
|
+
// Fix ①: fill the black bounce-0 default aux for a redirected ray that reached the
|
|
438
|
+
// environment without ever locking a surface guide (smooth glass / specular transmission
|
|
439
|
+
// over sky). Only when the stored albedo is still the black default (dot < 1e-4), so a real
|
|
440
|
+
// specular-surface aux written upstream is never clobbered. Normal = the escaped ray
|
|
441
|
+
// direction (varies per pixel → OIDN sees structure that tracks the refracted view);
|
|
442
|
+
// albedo = the clamped env colour it sees; depth kept at the primary hit.
|
|
443
|
+
If( auxOn
|
|
444
|
+
.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
|
|
445
|
+
.and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
|
|
446
|
+
|
|
447
|
+
const gPrev = readGBuffer( gBufferRW, pixelIndex );
|
|
448
|
+
If( dot( gbDecodeAlbedo( gPrev ), vec3( 1.0 ) ).lessThan( float( 1e-4 ) ), () => {
|
|
449
|
+
|
|
450
|
+
writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, escapedAuxAlbedo );
|
|
451
|
+
|
|
452
|
+
} );
|
|
453
|
+
|
|
454
|
+
} );
|
|
455
|
+
|
|
427
456
|
writeRayRadiance( rayBufferRW, rayID, currentRadiance );
|
|
428
457
|
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
429
458
|
Return();
|
|
@@ -549,6 +578,35 @@ export function buildShadeKernel( params ) {
|
|
|
549
578
|
material.roughness.assign( matSamples.roughness.clamp( 0.05, 1.0 ) );
|
|
550
579
|
material.sheenRoughness.assign( material.sheenRoughness.clamp( 0.05, 1.0 ) ); // megakernel parity (PathTracerCore.js:1060): sample/PDF mismatch at sheenRoughness~0
|
|
551
580
|
|
|
581
|
+
// Anisotropy + extension maps carry no per-slot transform of their own, so reuse the material's
|
|
582
|
+
// albedo UV-transform (KHR_texture_transform). Extension/aniso textures nearly always share the
|
|
583
|
+
// base UV set + transform, so this aligns tiled/offset maps (e.g. SheenCloth's 30× sheen weave)
|
|
584
|
+
// instead of sampling at raw 1× UV. Identity albedo transform → same as raw UV (safe no-op).
|
|
585
|
+
const extUV = getTransformedUV( { uv: samplingUV, transform: material.albedoTransform } ).toVar();
|
|
586
|
+
|
|
587
|
+
// Fold the anisotropy texture (if any) into the scalar anisotropy/rotation used by the BRDF
|
|
588
|
+
If( material.anisotropyMapIndex.greaterThanEqual( int( 0 ) ), () => {
|
|
589
|
+
|
|
590
|
+
const aniso = processAnisotropyMap( material, extUV ).toVar();
|
|
591
|
+
material.anisotropy.assign( aniso.x );
|
|
592
|
+
material.anisotropyRotation.assign( aniso.y );
|
|
593
|
+
|
|
594
|
+
} );
|
|
595
|
+
|
|
596
|
+
// Fold the glTF extension textures (transmission/clearcoat/sheen/iridescence/specular) into
|
|
597
|
+
// their scalar factors. `material` is the shared mutable struct that flows into BOTH the
|
|
598
|
+
// BRDF-sample path and the NEE evaluator, so modulating here covers every consumer at once.
|
|
599
|
+
const extMaps = ExtMapResult.wrap( applyExtensionMaps( material, extUV ) ).toVar();
|
|
600
|
+
material.transmission.assign( extMaps.transmission );
|
|
601
|
+
material.clearcoat.assign( extMaps.clearcoat );
|
|
602
|
+
material.clearcoatRoughness.assign( extMaps.clearcoatRoughness );
|
|
603
|
+
material.sheenColor.assign( extMaps.sheenColor );
|
|
604
|
+
material.sheenRoughness.assign( extMaps.sheenRoughness );
|
|
605
|
+
material.iridescence.assign( extMaps.iridescence );
|
|
606
|
+
material.iridescenceThicknessRange.assign( vec2( material.iridescenceThicknessRange.x, extMaps.iridescenceThickness ) );
|
|
607
|
+
material.specularIntensity.assign( extMaps.specularIntensity );
|
|
608
|
+
material.specularColor.assign( extMaps.specularColor );
|
|
609
|
+
|
|
552
610
|
const albedo = matSamples.albedo.toVar();
|
|
553
611
|
If(
|
|
554
612
|
material.displacementMapIndex.greaterThanEqual( int( 0 ) )
|
|
@@ -577,7 +635,23 @@ export function buildShadeKernel( params ) {
|
|
|
577
635
|
// (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
|
|
578
636
|
// block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
|
|
579
637
|
// (objectNormal/objectColor stay at their init for transmissive-then-miss).
|
|
580
|
-
|
|
638
|
+
//
|
|
639
|
+
// Fix ③: BLEND (semi-transparent) glass instead LOCKS its OWN normal + albedo here. Its per-frame
|
|
640
|
+
// stochastic alpha-skip-vs-opaque-shade outcome (MaterialTransmission.js:522-540) otherwise writes a
|
|
641
|
+
// DIFFERENT surface into the aux each frame (wall-behind on skip vs glass on opaque); with the normal
|
|
642
|
+
// not temporally averaged that becomes the speckle OIDN smears on — the visible artefact on
|
|
643
|
+
// alpha-transparent glass. A deterministic glass-surface guide every frame removes it. MASK / opaque
|
|
644
|
+
// materials keep the default + rely on the downstream aux-extend.
|
|
645
|
+
If( material.alphaMode.equal( int( 2 ) ), () => {
|
|
646
|
+
|
|
647
|
+
writeGBuffer( gBufferRW, pixelIndex, N, linearDepth, albedo.xyz );
|
|
648
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
649
|
+
|
|
650
|
+
} ).Else( () => {
|
|
651
|
+
|
|
652
|
+
writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
|
|
653
|
+
|
|
654
|
+
} );
|
|
581
655
|
|
|
582
656
|
} );
|
|
583
657
|
|
|
@@ -714,6 +788,26 @@ export function buildShadeKernel( params ) {
|
|
|
714
788
|
|
|
715
789
|
throughput.mulAssign( interaction.throughput );
|
|
716
790
|
|
|
791
|
+
// Fix ②: OIDN clean-aux for STOCHASTIC glass (rough / frosted / dispersive), whose refracted
|
|
792
|
+
// ray hits a different behind-surface every frame. Extending aux through it (the aux-extend on
|
|
793
|
+
// the opaque-hit path) captures that per-frame surface — albedo then temporally averaged, normal
|
|
794
|
+
// point-sampled — an inconsistent guide OIDN smears on. Instead lock the glass surface's OWN
|
|
795
|
+
// normal + tint here (deterministic, spatially coherent). Smooth non-dispersive glass is left
|
|
796
|
+
// alone: its refraction is a per-pixel delta, so the behind-surface aux captured downstream is
|
|
797
|
+
// already stable. Gated to a genuine dielectric event (not alpha-cutout passthrough or SSS).
|
|
798
|
+
If( auxOn
|
|
799
|
+
.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
|
|
800
|
+
.and( interaction.isTransmissive )
|
|
801
|
+
.and( interaction.isAlphaSkip.not() )
|
|
802
|
+
.and( interaction.isSubsurface.not() )
|
|
803
|
+
.and( material.roughness.greaterThan( 0.05 ).or( material.dispersion.greaterThan( 0.0 ) ) ), () => {
|
|
804
|
+
|
|
805
|
+
const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
|
|
806
|
+
writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
|
|
807
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
808
|
+
|
|
809
|
+
} );
|
|
810
|
+
|
|
717
811
|
// reflection stays on same side, transmission pushes through
|
|
718
812
|
const reflectOffsetDir = select( interaction.entering, N, N.negate() );
|
|
719
813
|
const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
|