rayzee 6.4.0 → 6.5.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/README.md +1 -1
- package/dist/rayzee.es.js +1744 -1556
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +50 -50
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +17 -4
- package/src/PathTracerApp.js +1 -0
- package/src/Processor/GeometryExtractor.js +16 -1
- package/src/Processor/ShaderBuilder.js +1 -0
- package/src/Processor/TextureCreator.js +6 -0
- package/src/RenderSettings.js +1 -0
- package/src/TSL/BVHTraversal.js +7 -1
- package/src/TSL/Common.js +12 -2
- package/src/TSL/MaterialTransmission.js +32 -2
- package/src/TSL/PathTracer.js +2 -2
- package/src/TSL/PathTracerCore.js +139 -12
- package/src/TSL/Struct.js +5 -0
- package/src/TSL/Subsurface.js +232 -0
- package/src/managers/MaterialDataManager.js +60 -1
- package/src/managers/UniformManager.js +1 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subsurface.js - Random-walk subsurface scattering.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the refraction interface + medium stack (PathTracerCore.js). The new physics
|
|
5
|
+
* is inside the medium: a ray collides mid-flight (sigma_s > 0) and scatters via a
|
|
6
|
+
* Henyey-Greenstein phase function instead of flying straight + absorbing (glass).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Fn,
|
|
11
|
+
float,
|
|
12
|
+
vec2,
|
|
13
|
+
vec3,
|
|
14
|
+
If,
|
|
15
|
+
select,
|
|
16
|
+
abs,
|
|
17
|
+
dot,
|
|
18
|
+
clamp,
|
|
19
|
+
max,
|
|
20
|
+
exp,
|
|
21
|
+
log,
|
|
22
|
+
sqrt,
|
|
23
|
+
cos,
|
|
24
|
+
sin,
|
|
25
|
+
normalize,
|
|
26
|
+
reflect,
|
|
27
|
+
refract,
|
|
28
|
+
} from 'three/tsl';
|
|
29
|
+
|
|
30
|
+
import { struct } from './patches.js';
|
|
31
|
+
import { TWO_PI, EPSILON, constructTBN } from './Common.js';
|
|
32
|
+
import { RandomValue } from './Random.js';
|
|
33
|
+
import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
|
|
34
|
+
import { ImportanceSampleGGX } from './MaterialSampling.js';
|
|
35
|
+
|
|
36
|
+
// ================================================================================
|
|
37
|
+
// STRUCTS
|
|
38
|
+
// ================================================================================
|
|
39
|
+
|
|
40
|
+
export const CollisionSample = struct( {
|
|
41
|
+
didScatter: 'bool',
|
|
42
|
+
t: 'float', // collision distance (clamped to surfaceDist when no scatter)
|
|
43
|
+
weight: 'vec3', // throughput multiplier for the segment (chromatic-MIS)
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
export const MediumCoeffs = struct( {
|
|
47
|
+
sigmaT: 'vec3',
|
|
48
|
+
sigmaS: 'vec3',
|
|
49
|
+
sigmaA: 'vec3',
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
export const SubsurfaceEntryResult = struct( {
|
|
53
|
+
direction: 'vec3',
|
|
54
|
+
throughput: 'vec3',
|
|
55
|
+
didReflect: 'bool',
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
// ================================================================================
|
|
59
|
+
// HENYEY-GREENSTEIN PHASE SAMPLING
|
|
60
|
+
// ================================================================================
|
|
61
|
+
|
|
62
|
+
// Returns a scattered direction (unit). cosTheta is relative to the propagation dir
|
|
63
|
+
// `wi`, so g > 0 is forward scattering. Inverse-CDF sampling is exact, so the brute-force
|
|
64
|
+
// walk needs no extra weight at the vertex (hence no pdf is returned).
|
|
65
|
+
export const sampleHenyeyGreenstein = Fn( ( [ wi, g, xi ] ) => {
|
|
66
|
+
|
|
67
|
+
const cosTheta = float( 0.0 ).toVar();
|
|
68
|
+
|
|
69
|
+
If( abs( g ).lessThan( 0.001 ), () => {
|
|
70
|
+
|
|
71
|
+
cosTheta.assign( float( 1.0 ).sub( xi.x.mul( 2.0 ) ) ); // isotropic; avoids 1/(2g)
|
|
72
|
+
|
|
73
|
+
} ).Else( () => {
|
|
74
|
+
|
|
75
|
+
const denom = max( float( 1.0 ).sub( g ).add( g.mul( 2.0 ).mul( xi.x ) ), 1e-4 );
|
|
76
|
+
const sqrTerm = float( 1.0 ).sub( g.mul( g ) ).div( denom );
|
|
77
|
+
cosTheta.assign( float( 1.0 ).add( g.mul( g ) ).sub( sqrTerm.mul( sqrTerm ) ).div( g.mul( 2.0 ) ) );
|
|
78
|
+
|
|
79
|
+
} );
|
|
80
|
+
|
|
81
|
+
cosTheta.assign( clamp( cosTheta, - 1.0, 1.0 ) );
|
|
82
|
+
const sinTheta = sqrt( max( float( 0.0 ), float( 1.0 ).sub( cosTheta.mul( cosTheta ) ) ) );
|
|
83
|
+
const phi = float( TWO_PI ).mul( xi.y );
|
|
84
|
+
|
|
85
|
+
// Basis with wi as 3rd column → result is already unit length.
|
|
86
|
+
const TBN = constructTBN( { N: wi } );
|
|
87
|
+
return TBN.mul( vec3( sinTheta.mul( cos( phi ) ), sinTheta.mul( sin( phi ) ), cosTheta ) );
|
|
88
|
+
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
// ================================================================================
|
|
92
|
+
// CHROMATIC COLLISION-DISTANCE SAMPLING (hero-channel spectral MIS)
|
|
93
|
+
// ================================================================================
|
|
94
|
+
|
|
95
|
+
// Per-channel sigma_t can't be represented by one scalar distance. Pick a channel
|
|
96
|
+
// ∝ throughput, sample t against it, and weight by the balance-heuristic combined pdf
|
|
97
|
+
// p̄ = Σ pmf_c·p_c — the shared scalar p̄ is what suppresses color fireflies.
|
|
98
|
+
export const sampleChromaticCollision = Fn( ( [ sigmaT, sigmaS, beta, surfaceDist, rngState ] ) => {
|
|
99
|
+
|
|
100
|
+
const w = max( beta, vec3( 1e-4 ) ); // floor so no channel goes unsampled
|
|
101
|
+
const pmf = w.div( w.x.add( w.y ).add( w.z ) ).toVar();
|
|
102
|
+
|
|
103
|
+
// .toVar() pins the single RNG draw (else it re-executes per comparison → state drift).
|
|
104
|
+
const u = RandomValue( rngState ).toVar();
|
|
105
|
+
const cSigmaT = float( 0.0 ).toVar();
|
|
106
|
+
If( u.lessThan( pmf.x ), () => {
|
|
107
|
+
|
|
108
|
+
cSigmaT.assign( sigmaT.x );
|
|
109
|
+
|
|
110
|
+
} ).ElseIf( u.lessThan( pmf.x.add( pmf.y ) ), () => {
|
|
111
|
+
|
|
112
|
+
cSigmaT.assign( sigmaT.y );
|
|
113
|
+
|
|
114
|
+
} ).Else( () => {
|
|
115
|
+
|
|
116
|
+
cSigmaT.assign( sigmaT.z );
|
|
117
|
+
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
const xi = RandomValue( rngState ).toVar();
|
|
121
|
+
const t = log( max( float( 1.0 ).sub( xi ), 1e-6 ) ).negate().div( max( cSigmaT, 1e-6 ) ).toVar();
|
|
122
|
+
|
|
123
|
+
const didScatter = t.lessThan( surfaceDist ).toVar();
|
|
124
|
+
const tOut = t.toVar();
|
|
125
|
+
const weight = vec3( 0.0 ).toVar();
|
|
126
|
+
|
|
127
|
+
If( didScatter, () => {
|
|
128
|
+
|
|
129
|
+
const Tr = exp( sigmaT.mul( t ).negate() ).toVar();
|
|
130
|
+
const pBar = dot( pmf, sigmaT.mul( Tr ) );
|
|
131
|
+
weight.assign( sigmaS.mul( Tr ).div( max( pBar, 1e-6 ) ) );
|
|
132
|
+
|
|
133
|
+
} ).Else( () => {
|
|
134
|
+
|
|
135
|
+
const Tr = exp( sigmaT.mul( surfaceDist ).negate() ).toVar();
|
|
136
|
+
const pBar = dot( pmf, Tr );
|
|
137
|
+
weight.assign( Tr.div( max( pBar, 1e-6 ) ) );
|
|
138
|
+
tOut.assign( surfaceDist );
|
|
139
|
+
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
return CollisionSample( { didScatter, t: tOut, weight } );
|
|
143
|
+
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
// ================================================================================
|
|
147
|
+
// PARAMETER → COEFFICIENT MAPPING (Cycles-style)
|
|
148
|
+
// ================================================================================
|
|
149
|
+
|
|
150
|
+
// sigma_t = 1/(radius·scale), sigma_s = albedo·sigma_t, sigma_a = sigma_t - sigma_s.
|
|
151
|
+
// subsurfaceColor is the single-scatter albedo, so the per-event weight carries the tint.
|
|
152
|
+
export const subsurfaceCoefficients = Fn( ( [ subsurfaceColor, subsurfaceRadius, radiusScale ] ) => {
|
|
153
|
+
|
|
154
|
+
const r = max( subsurfaceRadius.mul( radiusScale ), vec3( 1e-4 ) );
|
|
155
|
+
const sigmaT = vec3( 1.0 ).div( r );
|
|
156
|
+
const sigmaS = subsurfaceColor.mul( sigmaT );
|
|
157
|
+
const sigmaA = max( sigmaT.sub( sigmaS ), vec3( 0.0 ) );
|
|
158
|
+
|
|
159
|
+
return MediumCoeffs( { sigmaT, sigmaS, sigmaA } );
|
|
160
|
+
|
|
161
|
+
} );
|
|
162
|
+
|
|
163
|
+
// ================================================================================
|
|
164
|
+
// DIELECTRIC BOUNDARY (enter / exit the SSS medium)
|
|
165
|
+
// ================================================================================
|
|
166
|
+
|
|
167
|
+
// Dielectric interface driven by material.ior: reflect (Fresnel/TIR) or refract across
|
|
168
|
+
// the boundary. No color tint — the scattering color lives in sigma_s.
|
|
169
|
+
export const handleSubsurfaceEntry = Fn( ( [
|
|
170
|
+
rayDir, normal, material, entering, rngState, currentMediumIOR, previousMediumIOR,
|
|
171
|
+
] ) => {
|
|
172
|
+
|
|
173
|
+
const result = SubsurfaceEntryResult( {
|
|
174
|
+
direction: vec3( 0.0 ),
|
|
175
|
+
throughput: vec3( 1.0 ),
|
|
176
|
+
didReflect: false,
|
|
177
|
+
} ).toVar();
|
|
178
|
+
|
|
179
|
+
const N = select( entering, normal, normal.negate() ).toVar();
|
|
180
|
+
const n1 = select( entering, currentMediumIOR, material.ior ).toVar();
|
|
181
|
+
const n2 = select( entering, material.ior, previousMediumIOR ).toVar();
|
|
182
|
+
|
|
183
|
+
const cosThetaI = abs( dot( N, rayDir ) );
|
|
184
|
+
const sinThetaT2 = n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) ).mul( float( 1.0 ).sub( cosThetaI.mul( cosThetaI ) ) );
|
|
185
|
+
const tir = sinThetaT2.greaterThan( 1.0 ).toVar();
|
|
186
|
+
|
|
187
|
+
const F0 = iorToFresnel0( n2, n1 );
|
|
188
|
+
const Fr = select( tir, float( 1.0 ), fresnelSchlickFloat( cosThetaI, F0 ) ).toVar();
|
|
189
|
+
const reflectProb = clamp( Fr, 0.02, 0.98 ).toVar();
|
|
190
|
+
|
|
191
|
+
const doReflect = tir.or( RandomValue( rngState ).lessThan( reflectProb ) ).toVar();
|
|
192
|
+
result.didReflect.assign( doReflect );
|
|
193
|
+
|
|
194
|
+
If( doReflect, () => {
|
|
195
|
+
|
|
196
|
+
// GGX-sampled reflection: a perfect mirror here makes SSS surfaces read as polished ceramic.
|
|
197
|
+
const xiR = vec2( RandomValue( rngState ), RandomValue( rngState ) );
|
|
198
|
+
const H = ImportanceSampleGGX( { N, roughness: material.roughness, Xi: xiR } );
|
|
199
|
+
const reflDir = reflect( rayDir, H ).toVar();
|
|
200
|
+
If( dot( reflDir, N ).lessThanEqual( 0.0 ), () => {
|
|
201
|
+
|
|
202
|
+
reflDir.assign( reflect( rayDir, N ) ); // rough sample dipped below surface
|
|
203
|
+
|
|
204
|
+
} );
|
|
205
|
+
result.direction.assign( reflDir );
|
|
206
|
+
result.throughput.assign( vec3( Fr.div( max( reflectProb, 0.02 ) ) ) );
|
|
207
|
+
|
|
208
|
+
} ).Else( () => {
|
|
209
|
+
|
|
210
|
+
const refrDir = refract( rayDir, N, n1.div( max( n2, EPSILON ) ) ).toVar();
|
|
211
|
+
|
|
212
|
+
If( dot( refrDir, refrDir ).lessThan( 0.0001 ), () => {
|
|
213
|
+
|
|
214
|
+
result.direction.assign( reflect( rayDir, N ) );
|
|
215
|
+
result.didReflect.assign( true );
|
|
216
|
+
|
|
217
|
+
} ).Else( () => {
|
|
218
|
+
|
|
219
|
+
result.direction.assign( normalize( refrDir ) );
|
|
220
|
+
// (1-Fr) transmission + (n1/n2)² radiance scale (cancels round-trip).
|
|
221
|
+
const radianceScale = n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) );
|
|
222
|
+
result.throughput.assign( vec3(
|
|
223
|
+
float( 1.0 ).sub( Fr ).div( max( float( 1.0 ).sub( reflectProb ), 0.02 ) ).mul( radianceScale )
|
|
224
|
+
) );
|
|
225
|
+
|
|
226
|
+
} );
|
|
227
|
+
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
|
|
232
|
+
} );
|
|
@@ -307,6 +307,41 @@ export class MaterialDataManager {
|
|
|
307
307
|
break;
|
|
308
308
|
case 'bumpScale': data[ stride + M.BUMP_SCALE ] = value; break;
|
|
309
309
|
case 'displacementScale': data[ stride + M.DISPLACEMENT_SCALE ] = value; break;
|
|
310
|
+
case 'subsurface': data[ stride + M.SUBSURFACE ] = value; break;
|
|
311
|
+
case 'subsurfaceRadiusScale': data[ stride + M.SUBSURFACE_RADIUS_SCALE ] = value; break;
|
|
312
|
+
case 'subsurfaceAnisotropy': data[ stride + M.SUBSURFACE_ANISOTROPY ] = value; break;
|
|
313
|
+
case 'subsurfaceColor':
|
|
314
|
+
if ( value.r !== undefined ) {
|
|
315
|
+
|
|
316
|
+
data[ stride + M.SUBSURFACE_COLOR ] = value.r;
|
|
317
|
+
data[ stride + M.SUBSURFACE_COLOR + 1 ] = value.g;
|
|
318
|
+
data[ stride + M.SUBSURFACE_COLOR + 2 ] = value.b;
|
|
319
|
+
|
|
320
|
+
} else if ( Array.isArray( value ) ) {
|
|
321
|
+
|
|
322
|
+
data[ stride + M.SUBSURFACE_COLOR ] = value[ 0 ];
|
|
323
|
+
data[ stride + M.SUBSURFACE_COLOR + 1 ] = value[ 1 ];
|
|
324
|
+
data[ stride + M.SUBSURFACE_COLOR + 2 ] = value[ 2 ];
|
|
325
|
+
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
break;
|
|
329
|
+
case 'subsurfaceRadius':
|
|
330
|
+
if ( Array.isArray( value ) ) {
|
|
331
|
+
|
|
332
|
+
data[ stride + M.SUBSURFACE_RADIUS ] = value[ 0 ];
|
|
333
|
+
data[ stride + M.SUBSURFACE_RADIUS + 1 ] = value[ 1 ];
|
|
334
|
+
data[ stride + M.SUBSURFACE_RADIUS + 2 ] = value[ 2 ];
|
|
335
|
+
|
|
336
|
+
} else if ( value.x !== undefined ) {
|
|
337
|
+
|
|
338
|
+
data[ stride + M.SUBSURFACE_RADIUS ] = value.x;
|
|
339
|
+
data[ stride + M.SUBSURFACE_RADIUS + 1 ] = value.y;
|
|
340
|
+
data[ stride + M.SUBSURFACE_RADIUS + 2 ] = value.z;
|
|
341
|
+
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
break;
|
|
310
345
|
default:
|
|
311
346
|
console.warn( `Unknown material property: ${property}` );
|
|
312
347
|
return;
|
|
@@ -322,7 +357,7 @@ export class MaterialDataManager {
|
|
|
322
357
|
|
|
323
358
|
}
|
|
324
359
|
|
|
325
|
-
const featureProperties = [ 'transmission', 'clearcoat', 'sheen', 'iridescence', 'dispersion', 'transparent', 'opacity', 'alphaTest' ];
|
|
360
|
+
const featureProperties = [ 'transmission', 'clearcoat', 'sheen', 'iridescence', 'dispersion', 'transparent', 'opacity', 'alphaTest', 'subsurface' ];
|
|
326
361
|
if ( featureProperties.includes( property ) ) {
|
|
327
362
|
|
|
328
363
|
const featuresChanged = this.rescanMaterialFeatures();
|
|
@@ -446,6 +481,27 @@ export class MaterialDataManager {
|
|
|
446
481
|
data[ stride + M.DISPLACEMENT_SCALE ] = materialData.displacementScale ?? 1;
|
|
447
482
|
data[ stride + M.DISPLACEMENT_MAP_INDEX ] = materialData.displacementMap ?? - 1;
|
|
448
483
|
|
|
484
|
+
// Subsurface scattering
|
|
485
|
+
data[ stride + M.SUBSURFACE ] = materialData.subsurface ?? 0;
|
|
486
|
+
if ( materialData.subsurfaceColor ) {
|
|
487
|
+
|
|
488
|
+
data[ stride + M.SUBSURFACE_COLOR ] = materialData.subsurfaceColor.r ?? materialData.subsurfaceColor[ 0 ] ?? 1;
|
|
489
|
+
data[ stride + M.SUBSURFACE_COLOR + 1 ] = materialData.subsurfaceColor.g ?? materialData.subsurfaceColor[ 1 ] ?? 1;
|
|
490
|
+
data[ stride + M.SUBSURFACE_COLOR + 2 ] = materialData.subsurfaceColor.b ?? materialData.subsurfaceColor[ 2 ] ?? 1;
|
|
491
|
+
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if ( materialData.subsurfaceRadius ) {
|
|
495
|
+
|
|
496
|
+
data[ stride + M.SUBSURFACE_RADIUS ] = materialData.subsurfaceRadius[ 0 ] ?? 1;
|
|
497
|
+
data[ stride + M.SUBSURFACE_RADIUS + 1 ] = materialData.subsurfaceRadius[ 1 ] ?? 0.2;
|
|
498
|
+
data[ stride + M.SUBSURFACE_RADIUS + 2 ] = materialData.subsurfaceRadius[ 2 ] ?? 0.1;
|
|
499
|
+
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
data[ stride + M.SUBSURFACE_RADIUS_SCALE ] = materialData.subsurfaceRadiusScale ?? 1;
|
|
503
|
+
data[ stride + M.SUBSURFACE_ANISOTROPY ] = materialData.subsurfaceAnisotropy ?? 0;
|
|
504
|
+
|
|
449
505
|
// Texture transformation matrices (9 floats each, identity if missing)
|
|
450
506
|
const identity = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
|
|
451
507
|
const transformEntries = [
|
|
@@ -574,6 +630,7 @@ export class MaterialDataManager {
|
|
|
574
630
|
hasIridescence: false,
|
|
575
631
|
hasSheen: false,
|
|
576
632
|
hasTransparency: false,
|
|
633
|
+
hasSubsurface: false,
|
|
577
634
|
hasMultiLobeMaterials: false,
|
|
578
635
|
hasMRTOutputs: true
|
|
579
636
|
};
|
|
@@ -590,6 +647,7 @@ export class MaterialDataManager {
|
|
|
590
647
|
const opacity = data[ stride + M.OPACITY ];
|
|
591
648
|
const transparent = data[ stride + M.TRANSPARENT ];
|
|
592
649
|
const alphaTest = data[ stride + M.ALPHA_TEST ];
|
|
650
|
+
const subsurface = data[ stride + M.SUBSURFACE ];
|
|
593
651
|
|
|
594
652
|
if ( clearcoat > 0 ) newFeatures.hasClearcoat = true;
|
|
595
653
|
if ( transmission > 0 ) newFeatures.hasTransmission = true;
|
|
@@ -597,6 +655,7 @@ export class MaterialDataManager {
|
|
|
597
655
|
if ( iridescence > 0 ) newFeatures.hasIridescence = true;
|
|
598
656
|
if ( sheen > 0 ) newFeatures.hasSheen = true;
|
|
599
657
|
if ( transparent > 0 || opacity < 1.0 || alphaTest > 0 ) newFeatures.hasTransparency = true;
|
|
658
|
+
if ( subsurface > 0 ) newFeatures.hasSubsurface = true;
|
|
600
659
|
|
|
601
660
|
const featureCount = [
|
|
602
661
|
clearcoat > 0,
|
|
@@ -164,6 +164,7 @@ export class UniformManager {
|
|
|
164
164
|
u( 'samplesPerPixel', DEFAULT_STATE.samplesPerPixel, 'int' );
|
|
165
165
|
u( 'maxSamples', DEFAULT_STATE.maxSamples, 'int' );
|
|
166
166
|
u( 'transmissiveBounces', DEFAULT_STATE.transmissiveBounces, 'int' );
|
|
167
|
+
u( 'maxSubsurfaceSteps', DEFAULT_STATE.maxSubsurfaceSteps, 'int' );
|
|
167
168
|
u( 'visMode', DEFAULT_STATE.debugMode, 'int' );
|
|
168
169
|
u( 'debugVisScale', DEFAULT_STATE.debugVisScale, 'float' );
|
|
169
170
|
|