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
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -65,6 +65,7 @@ export const ENGINE_DEFAULTS = {
|
|
|
65
65
|
bounces: 3,
|
|
66
66
|
samplesPerPixel: 1,
|
|
67
67
|
transmissiveBounces: 5,
|
|
68
|
+
maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
|
|
68
69
|
samplingTechnique: 3,
|
|
69
70
|
enableEmissiveTriangleSampling: false,
|
|
70
71
|
emissiveBoost: 1.0,
|
|
@@ -356,8 +357,8 @@ export const TRIANGLE_DATA_LAYOUT = {
|
|
|
356
357
|
// Shared between CPU writers (TextureCreator, MaterialDataManager) and GPU readers (Common.js getMaterial).
|
|
357
358
|
export const MATERIAL_DATA_LAYOUT = {
|
|
358
359
|
|
|
359
|
-
SLOTS_PER_MATERIAL:
|
|
360
|
-
FLOATS_PER_MATERIAL:
|
|
360
|
+
SLOTS_PER_MATERIAL: 30, // vec4 slots per material
|
|
361
|
+
FLOATS_PER_MATERIAL: 120, // total floats per material (30 × 4)
|
|
361
362
|
|
|
362
363
|
// ── Flat float offsets (CPU side) ────────────────────────────────
|
|
363
364
|
// Used as: data[ materialIndex * FLOATS_PER_MATERIAL + offset ]
|
|
@@ -399,6 +400,14 @@ export const MATERIAL_DATA_LAYOUT = {
|
|
|
399
400
|
BUMP_TRANSFORM: 92,
|
|
400
401
|
DISPLACEMENT_TRANSFORM: 100,
|
|
401
402
|
|
|
403
|
+
// ── Subsurface scattering (3 slots appended after transforms) ────
|
|
404
|
+
// Slot 27: subsurfaceColor.rgb (scatter albedo) + subsurface weight
|
|
405
|
+
SUBSURFACE_COLOR: 108, SUBSURFACE: 111,
|
|
406
|
+
// Slot 28: subsurfaceRadius.rgb (mean free path) + radius scale
|
|
407
|
+
SUBSURFACE_RADIUS: 112, SUBSURFACE_RADIUS_SCALE: 115,
|
|
408
|
+
// Slot 29: anisotropy g (floats 117-119 reserved for future SSS)
|
|
409
|
+
SUBSURFACE_ANISOTROPY: 116,
|
|
410
|
+
|
|
402
411
|
// ── Vec4 slot indices (GPU/TSL side) ─────────────────────────────
|
|
403
412
|
// Used with getDatafromStorageBuffer( buf, matIdx, int(slot), int(SLOTS_PER_MATERIAL) )
|
|
404
413
|
SLOT: {
|
|
@@ -422,6 +431,9 @@ export const MATERIAL_DATA_LAYOUT = {
|
|
|
422
431
|
EMISSIVE_TRANSFORM_A: 21, EMISSIVE_TRANSFORM_B: 22,
|
|
423
432
|
BUMP_TRANSFORM_A: 23, BUMP_TRANSFORM_B: 24,
|
|
424
433
|
DISPLACEMENT_TRANSFORM_A: 25, DISPLACEMENT_TRANSFORM_B: 26,
|
|
434
|
+
SUBSURFACE_A: 27, // subsurfaceColor.rgb, subsurface weight
|
|
435
|
+
SUBSURFACE_B: 28, // subsurfaceRadius.rgb, subsurfaceRadiusScale
|
|
436
|
+
SUBSURFACE_C: 29, // subsurfaceAnisotropy, reserved
|
|
425
437
|
},
|
|
426
438
|
|
|
427
439
|
};
|
|
@@ -434,7 +446,7 @@ export const BVH_LEAF_MARKERS = {
|
|
|
434
446
|
|
|
435
447
|
// Texture processing constants
|
|
436
448
|
export const TEXTURE_CONSTANTS = {
|
|
437
|
-
PIXELS_PER_MATERIAL:
|
|
449
|
+
PIXELS_PER_MATERIAL: 30,
|
|
438
450
|
RGBA_COMPONENTS: 4,
|
|
439
451
|
VEC4_PER_TRIANGLE: 8,
|
|
440
452
|
VEC4_PER_BVH_NODE: 4,
|
|
@@ -454,7 +466,7 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
|
|
|
454
466
|
// 'interactive' — low-sample, bounded bounces, no offline denoising, controls enabled.
|
|
455
467
|
// 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
|
|
456
468
|
export const PRODUCTION_RENDER_CONFIG = {
|
|
457
|
-
maxSamples: 30, bounces: 20, transmissiveBounces: 8, samplesPerPixel: 1,
|
|
469
|
+
maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64, samplesPerPixel: 1,
|
|
458
470
|
renderMode: 1, enableAlphaShadows: true, tiles: 3, tilesHelper: true,
|
|
459
471
|
enableOIDN: true, oidnQuality: 'balance',
|
|
460
472
|
interactionModeEnabled: false,
|
|
@@ -464,6 +476,7 @@ export const INTERACTIVE_RENDER_CONFIG = {
|
|
|
464
476
|
maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
|
|
465
477
|
samplesPerPixel: ENGINE_DEFAULTS.samplesPerPixel, renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
|
|
466
478
|
transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
|
|
479
|
+
maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
|
|
467
480
|
tiles: ENGINE_DEFAULTS.tiles, tilesHelper: ENGINE_DEFAULTS.tilesHelper,
|
|
468
481
|
enableOIDN: false, oidnQuality: 'fast',
|
|
469
482
|
interactionModeEnabled: true,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -922,6 +922,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
922
922
|
maxBounces: config.bounces,
|
|
923
923
|
samplesPerPixel: config.samplesPerPixel,
|
|
924
924
|
transmissiveBounces: config.transmissiveBounces,
|
|
925
|
+
maxSubsurfaceSteps: config.maxSubsurfaceSteps,
|
|
925
926
|
}, { silent: true } );
|
|
926
927
|
|
|
927
928
|
this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
|
|
@@ -160,6 +160,7 @@ export class GeometryExtractor {
|
|
|
160
160
|
if ( newMaterial.iridescence > 0 ) this.sceneFeatures.hasIridescence = true;
|
|
161
161
|
if ( newMaterial.sheen > 0 ) this.sceneFeatures.hasSheen = true;
|
|
162
162
|
if ( newMaterial.transparent || newMaterial.opacity < 1.0 || newMaterial.alphaTest > 0 ) this.sceneFeatures.hasTransparency = true;
|
|
163
|
+
if ( newMaterial.subsurface > 0 ) this.sceneFeatures.hasSubsurface = true;
|
|
163
164
|
|
|
164
165
|
// Detect multi-lobe materials (require multi-lobe MIS for optimal sampling)
|
|
165
166
|
const featureCount = [
|
|
@@ -259,7 +260,13 @@ export class GeometryExtractor {
|
|
|
259
260
|
normalScale: { x: 1, y: 1 },
|
|
260
261
|
bumpScale: 1.0,
|
|
261
262
|
displacementScale: 1.0,
|
|
262
|
-
alphaTest: 0.0
|
|
263
|
+
alphaTest: 0.0,
|
|
264
|
+
// Subsurface scattering (no native MeshPhysicalMaterial equivalent)
|
|
265
|
+
subsurface: 0.0,
|
|
266
|
+
subsurfaceColor: new Color( 0xffffff ),
|
|
267
|
+
subsurfaceRadius: [ 1.0, 0.2, 0.1 ], // skin-like: red travels furthest
|
|
268
|
+
subsurfaceRadiusScale: 1.0,
|
|
269
|
+
subsurfaceAnisotropy: 0.0
|
|
263
270
|
};
|
|
264
271
|
|
|
265
272
|
}
|
|
@@ -390,6 +397,13 @@ export class GeometryExtractor {
|
|
|
390
397
|
iridescenceIOR: material.iridescenceIOR ?? defaults.iridescenceIOR,
|
|
391
398
|
iridescenceThicknessRange: material.iridescenceThicknessRange ?? defaults.iridescenceThicknessRange,
|
|
392
399
|
|
|
400
|
+
// Subsurface scattering (custom props; MeshPhysicalMaterial has none)
|
|
401
|
+
subsurface: material.subsurface ?? defaults.subsurface,
|
|
402
|
+
subsurfaceColor: material.subsurfaceColor ?? defaults.subsurfaceColor,
|
|
403
|
+
subsurfaceRadius: material.subsurfaceRadius ?? defaults.subsurfaceRadius,
|
|
404
|
+
subsurfaceRadiusScale: material.subsurfaceRadiusScale ?? defaults.subsurfaceRadiusScale,
|
|
405
|
+
subsurfaceAnisotropy: material.subsurfaceAnisotropy ?? defaults.subsurfaceAnisotropy,
|
|
406
|
+
|
|
393
407
|
// Specular properties (for compatibility)
|
|
394
408
|
specularIntensity: legacyMapping.specularIntensity ?? material.specularIntensity ?? defaults.specularIntensity,
|
|
395
409
|
specularColor: legacyMapping.specularColor ?? material.specularColor ?? defaults.specularColor,
|
|
@@ -789,6 +803,7 @@ export class GeometryExtractor {
|
|
|
789
803
|
hasIridescence: false,
|
|
790
804
|
hasSheen: false,
|
|
791
805
|
hasTransparency: false,
|
|
806
|
+
hasSubsurface: false,
|
|
792
807
|
hasMultiLobeMaterials: false, // Materials with 2+ BRDF lobes
|
|
793
808
|
hasMRTOutputs: true // Always enabled for ASVGF/adaptive sampling support
|
|
794
809
|
};
|
|
@@ -411,6 +411,7 @@ export class ShaderBuilder {
|
|
|
411
411
|
groundProjectionHeight: stage.groundProjectionHeight,
|
|
412
412
|
maxBounceCount: stage.maxBounces,
|
|
413
413
|
transmissiveBounces: stage.transmissiveBounces,
|
|
414
|
+
maxSubsurfaceSteps: stage.maxSubsurfaceSteps,
|
|
414
415
|
showBackground: stage.showBackground,
|
|
415
416
|
transparentBackground: stage.transparentBackground,
|
|
416
417
|
backgroundIntensity: stage.backgroundIntensity,
|
|
@@ -965,6 +965,12 @@ export class TextureCreator {
|
|
|
965
965
|
bumpMapMatrices[ 4 ], bumpMapMatrices[ 5 ], bumpMapMatrices[ 6 ], 1,
|
|
966
966
|
displacementMapMatrices[ 0 ], displacementMapMatrices[ 1 ], displacementMapMatrices[ 2 ], displacementMapMatrices[ 3 ],
|
|
967
967
|
displacementMapMatrices[ 4 ], displacementMapMatrices[ 5 ], displacementMapMatrices[ 6 ], 1,
|
|
968
|
+
// Slot 27: subsurface (subsurfaceColor.rgb, subsurface weight)
|
|
969
|
+
mat.subsurfaceColor?.r ?? 1, mat.subsurfaceColor?.g ?? 1, mat.subsurfaceColor?.b ?? 1, mat.subsurface ?? 0,
|
|
970
|
+
// Slot 28: subsurface (subsurfaceRadius.rgb, subsurfaceRadiusScale)
|
|
971
|
+
mat.subsurfaceRadius?.[ 0 ] ?? 1, mat.subsurfaceRadius?.[ 1 ] ?? 0.2, mat.subsurfaceRadius?.[ 2 ] ?? 0.1, mat.subsurfaceRadiusScale ?? 1,
|
|
972
|
+
// Slot 29: subsurface (anisotropy g, reserved)
|
|
973
|
+
mat.subsurfaceAnisotropy ?? 0, 0, 0, 0,
|
|
968
974
|
];
|
|
969
975
|
|
|
970
976
|
data.set( materialData, stride );
|
package/src/RenderSettings.js
CHANGED
|
@@ -18,6 +18,7 @@ const SETTING_ROUTES = {
|
|
|
18
18
|
maxBounces: { uniform: 'maxBounces', reset: true },
|
|
19
19
|
samplesPerPixel: { uniform: 'samplesPerPixel', reset: true },
|
|
20
20
|
transmissiveBounces: { uniform: 'transmissiveBounces', reset: true },
|
|
21
|
+
maxSubsurfaceSteps: { uniform: 'maxSubsurfaceSteps', reset: true },
|
|
21
22
|
environmentIntensity: { uniform: 'environmentIntensity', reset: true },
|
|
22
23
|
backgroundIntensity: { uniform: 'backgroundIntensity', reset: true },
|
|
23
24
|
showBackground: { uniform: 'showBackground', reset: true },
|
package/src/TSL/BVHTraversal.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
lessThan,
|
|
20
20
|
mat3,
|
|
21
21
|
array,
|
|
22
|
+
bool as tslBool,
|
|
22
23
|
} from 'three/tsl';
|
|
23
24
|
|
|
24
25
|
import { Ray, HitInfo } from './Struct.js';
|
|
@@ -178,8 +179,13 @@ export const traverseBVH = Fn( ( [
|
|
|
178
179
|
ray,
|
|
179
180
|
bvhBuffer,
|
|
180
181
|
triangleBuffer,
|
|
182
|
+
insideMedium, // optional: when true (ray inside a medium), bypass front/back culling
|
|
181
183
|
] ) => {
|
|
182
184
|
|
|
185
|
+
// Interior medium rays (SSS/transmission) must be able to hit boundary faces from
|
|
186
|
+
// either side to find the exit; exterior rays honor the authored side as before.
|
|
187
|
+
const inMedium = insideMedium ?? tslBool( false );
|
|
188
|
+
|
|
183
189
|
const closestHit = HitInfo( {
|
|
184
190
|
didHit: false,
|
|
185
191
|
dst: float( 1e20 ),
|
|
@@ -280,7 +286,7 @@ export const traverseBVH = Fn( ( [
|
|
|
280
286
|
|
|
281
287
|
// Side culling (inline; per-mesh visibility is at the BLAS-pointer level).
|
|
282
288
|
// 0=front (reject back-facing), 1=back (reject front-facing), 2=double (pass).
|
|
283
|
-
const sidePass = side.equal( int( 2 ) )
|
|
289
|
+
const sidePass = inMedium.or( side.equal( int( 2 ) ) )
|
|
284
290
|
.or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
|
|
285
291
|
.or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
|
|
286
292
|
If( sidePass, () => {
|
package/src/TSL/Common.js
CHANGED
|
@@ -201,13 +201,14 @@ export const applySoftSuppressionRGB = wgslFn( `
|
|
|
201
201
|
`, [ applySoftSuppression ] );
|
|
202
202
|
|
|
203
203
|
// Pre-computed material classification for faster branching
|
|
204
|
-
export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clearcoat, emissive ] ) => {
|
|
204
|
+
export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clearcoat, emissive, subsurface ] ) => {
|
|
205
205
|
|
|
206
206
|
const isMetallic = metalness.greaterThan( 0.7 ).toVar();
|
|
207
207
|
const isRough = roughness.greaterThan( 0.8 );
|
|
208
208
|
const isSmooth = roughness.lessThan( 0.3 ).toVar();
|
|
209
209
|
const isTransmissive = transmission.greaterThan( 0.5 ).toVar();
|
|
210
210
|
const hasClearcoat = clearcoat.greaterThan( 0.5 ).toVar();
|
|
211
|
+
const isSubsurface = subsurface.greaterThan( 0.0 ); // only feeds complexityScore below
|
|
211
212
|
|
|
212
213
|
// Fast emissive check using sum
|
|
213
214
|
const emissiveMag = emissive.x.add( emissive.y ).add( emissive.z );
|
|
@@ -218,7 +219,8 @@ export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clea
|
|
|
218
219
|
.add( float( 0.25 ).mul( float( isSmooth ) ) )
|
|
219
220
|
.add( float( 0.45 ).mul( float( isTransmissive ) ) )
|
|
220
221
|
.add( float( 0.35 ).mul( float( hasClearcoat ) ) )
|
|
221
|
-
.add( float( 0.3 ).mul( float( isEmissive ) ) )
|
|
222
|
+
.add( float( 0.3 ).mul( float( isEmissive ) ) )
|
|
223
|
+
.add( float( 0.4 ).mul( float( isSubsurface ) ) ); // SSS walks are deep + high-value → keep alive in RR
|
|
222
224
|
|
|
223
225
|
// Add material interaction complexity
|
|
224
226
|
const interactionComplexity = float( 0.0 ).toVar();
|
|
@@ -340,6 +342,9 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
|
|
|
340
342
|
const data24 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.BUMP_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
|
|
341
343
|
const data25 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.DISPLACEMENT_TRANSFORM_A ), int( MATERIAL_SLOTS ) ).toVar();
|
|
342
344
|
const data26 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.DISPLACEMENT_TRANSFORM_B ), int( MATERIAL_SLOTS ) ).toVar();
|
|
345
|
+
const data27 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SUBSURFACE_A ), int( MATERIAL_SLOTS ) ).toVar();
|
|
346
|
+
const data28 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SUBSURFACE_B ), int( MATERIAL_SLOTS ) ).toVar();
|
|
347
|
+
const data29 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.SUBSURFACE_C ), int( MATERIAL_SLOTS ) ).toVar();
|
|
343
348
|
|
|
344
349
|
return RayTracingMaterial( {
|
|
345
350
|
color: vec4( data0.rgb, 1.0 ),
|
|
@@ -361,6 +366,11 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
|
|
|
361
366
|
iridescence: data7.r,
|
|
362
367
|
iridescenceIOR: data7.g,
|
|
363
368
|
iridescenceThicknessRange: data7.ba,
|
|
369
|
+
subsurfaceColor: data27.rgb,
|
|
370
|
+
subsurface: data27.a,
|
|
371
|
+
subsurfaceRadius: data28.rgb,
|
|
372
|
+
subsurfaceRadiusScale: data28.a,
|
|
373
|
+
subsurfaceAnisotropy: data29.r,
|
|
364
374
|
albedoMapIndex: int( data8.r ),
|
|
365
375
|
normalMapIndex: int( data8.g ),
|
|
366
376
|
roughnessMapIndex: int( data8.b ),
|
|
@@ -27,6 +27,7 @@ import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
|
|
|
27
27
|
import { DistributionGGX } from './MaterialProperties.js';
|
|
28
28
|
import { ImportanceSampleGGX } from './MaterialSampling.js';
|
|
29
29
|
import { RandomValue, pcgHash } from './Random.js';
|
|
30
|
+
import { handleSubsurfaceEntry, SubsurfaceEntryResult } from './Subsurface.js';
|
|
30
31
|
|
|
31
32
|
// ================================================================================
|
|
32
33
|
// STRUCTS (local to transmission)
|
|
@@ -43,6 +44,7 @@ export const MaterialInteractionResult = struct( {
|
|
|
43
44
|
continueRay: 'bool', // Whether the ray should continue without further BRDF evaluation
|
|
44
45
|
isTransmissive: 'bool', // Flag to indicate this was a transmissive interaction
|
|
45
46
|
isAlphaSkip: 'bool', // Flag to indicate this was an alpha skip
|
|
47
|
+
isSubsurface: 'bool', // Flag to indicate this entered/exited a subsurface medium
|
|
46
48
|
didReflect: 'bool', // Whether TIR/reflection occurred (for medium stack update)
|
|
47
49
|
entering: 'bool', // Whether ray is entering or exiting medium
|
|
48
50
|
direction: 'vec3', // New ray direction if continuing
|
|
@@ -493,6 +495,7 @@ export const handleMaterialTransparency = Fn( ( [
|
|
|
493
495
|
continueRay: false,
|
|
494
496
|
isTransmissive: false,
|
|
495
497
|
isAlphaSkip: false,
|
|
498
|
+
isSubsurface: false,
|
|
496
499
|
didReflect: false,
|
|
497
500
|
entering: false,
|
|
498
501
|
direction: ray.direction,
|
|
@@ -501,8 +504,9 @@ export const handleMaterialTransparency = Fn( ( [
|
|
|
501
504
|
pathWavelength: pathWavelength,
|
|
502
505
|
} ).toVar();
|
|
503
506
|
|
|
504
|
-
// Fast path for fully opaque materials (most common case)
|
|
505
|
-
|
|
507
|
+
// Fast path for fully opaque, non-scattering materials (most common case).
|
|
508
|
+
// Subsurface materials are opaque (transmission==0) but must NOT take this path.
|
|
509
|
+
If( material.alphaMode.equal( int( 0 ) ).and( material.transmission.lessThanEqual( 0.0 ) ).and( material.subsurface.lessThanEqual( 0.0 ) ), () => {
|
|
506
510
|
|
|
507
511
|
// no interaction needed
|
|
508
512
|
|
|
@@ -584,6 +588,32 @@ export const handleMaterialTransparency = Fn( ( [
|
|
|
584
588
|
|
|
585
589
|
} );
|
|
586
590
|
|
|
591
|
+
// Subsurface (independent of transmission; works at transmission==0). Entry is a lottery
|
|
592
|
+
// (prob = weight) so 1-weight falls through to the opaque BRDF; exit is deterministic.
|
|
593
|
+
If( handled.not().and( material.subsurface.greaterThan( 0.0 ) ), () => {
|
|
594
|
+
|
|
595
|
+
const entering = dot( ray.direction, normal ).lessThan( 0.0 );
|
|
596
|
+
const doEnter = entering.not().or( RandomValue( rngState ).lessThan( material.subsurface ) );
|
|
597
|
+
|
|
598
|
+
If( doEnter, () => {
|
|
599
|
+
|
|
600
|
+
const ssResult = SubsurfaceEntryResult.wrap( handleSubsurfaceEntry(
|
|
601
|
+
ray.direction, normal, material, entering, rngState,
|
|
602
|
+
currentMediumIOR, previousMediumIOR,
|
|
603
|
+
) ).toVar();
|
|
604
|
+
|
|
605
|
+
result.direction.assign( ssResult.direction );
|
|
606
|
+
result.throughput.assign( ssResult.throughput );
|
|
607
|
+
result.continueRay.assign( true );
|
|
608
|
+
result.isSubsurface.assign( true );
|
|
609
|
+
result.didReflect.assign( ssResult.didReflect );
|
|
610
|
+
result.entering.assign( entering );
|
|
611
|
+
result.alpha.assign( 1.0 );
|
|
612
|
+
|
|
613
|
+
} );
|
|
614
|
+
|
|
615
|
+
} );
|
|
616
|
+
|
|
587
617
|
} );
|
|
588
618
|
|
|
589
619
|
return result;
|
package/src/TSL/PathTracer.js
CHANGED
|
@@ -151,7 +151,7 @@ export const pathTracerMain = ( params ) => {
|
|
|
151
151
|
envTotalSum, envCompensationDelta, envResolution,
|
|
152
152
|
enableEnvironmentLight, useEnvMapIS,
|
|
153
153
|
groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
|
|
154
|
-
maxBounceCount, transmissiveBounces,
|
|
154
|
+
maxBounceCount, transmissiveBounces, maxSubsurfaceSteps,
|
|
155
155
|
showBackground, transparentBackground, backgroundIntensity,
|
|
156
156
|
fireflyThreshold, globalIlluminationIntensity,
|
|
157
157
|
enableEmissiveTriangleSampling,
|
|
@@ -299,7 +299,7 @@ export const pathTracerMain = ( params ) => {
|
|
|
299
299
|
envTotalSum, envCompensationDelta, envResolution,
|
|
300
300
|
enableEnvironmentLight, useEnvMapIS,
|
|
301
301
|
groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
|
|
302
|
-
maxBounceCount, transmissiveBounces,
|
|
302
|
+
maxBounceCount, transmissiveBounces, maxSubsurfaceSteps,
|
|
303
303
|
backgroundIntensity, showBackground, transparentBackground,
|
|
304
304
|
fireflyThreshold, globalIlluminationIntensity,
|
|
305
305
|
enableEmissiveTriangleSampling,
|
|
@@ -77,6 +77,7 @@ import { sampleEnvironment, sampleEquirect, getGroundProjectedDirection } from '
|
|
|
77
77
|
import { sampleAllMaterialTextures } from './TextureSampling.js';
|
|
78
78
|
import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
|
|
79
79
|
import { handleMaterialTransparency, MaterialInteractionResult, sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
|
|
80
|
+
import { subsurfaceCoefficients, sampleChromaticCollision, sampleHenyeyGreenstein, MediumCoeffs, CollisionSample } from './Subsurface.js';
|
|
80
81
|
import {
|
|
81
82
|
SheenDistribution,
|
|
82
83
|
calculateVNDFPDF,
|
|
@@ -140,7 +141,7 @@ export const getOrCreateMaterialClassification = Fn( ( [
|
|
|
140
141
|
result.assign( classifyMaterial(
|
|
141
142
|
material.metalness, material.roughness,
|
|
142
143
|
material.transmission, material.clearcoat,
|
|
143
|
-
material.emissive,
|
|
144
|
+
material.emissive, material.subsurface,
|
|
144
145
|
) );
|
|
145
146
|
|
|
146
147
|
} );
|
|
@@ -538,7 +539,7 @@ export const Trace = Fn( ( [
|
|
|
538
539
|
enableEnvironmentLight, useEnvMapIS,
|
|
539
540
|
groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
|
|
540
541
|
// Rendering parameters
|
|
541
|
-
maxBounceCount, transmissiveBounces,
|
|
542
|
+
maxBounceCount, transmissiveBounces, maxSubsurfaceSteps,
|
|
542
543
|
backgroundIntensity, showBackground, transparentBackground,
|
|
543
544
|
fireflyThreshold, globalIlluminationIntensity,
|
|
544
545
|
enableEmissiveTriangleSampling,
|
|
@@ -584,6 +585,18 @@ export const Trace = Fn( ( [
|
|
|
584
585
|
const mediumStack_sigmaA_1 = vec3( 0.0 ).toVar();
|
|
585
586
|
const mediumStack_sigmaA_2 = vec3( 0.0 ).toVar();
|
|
586
587
|
const mediumStack_sigmaA_3 = vec3( 0.0 ).toVar();
|
|
588
|
+
// Subsurface: sigma_s>0 makes the medium scatter (random walk) rather than absorb straight.
|
|
589
|
+
const mediumStack_sigmaS_1 = vec3( 0.0 ).toVar();
|
|
590
|
+
const mediumStack_sigmaS_2 = vec3( 0.0 ).toVar();
|
|
591
|
+
const mediumStack_sigmaS_3 = vec3( 0.0 ).toVar();
|
|
592
|
+
const mediumStack_sigmaT_1 = vec3( 0.0 ).toVar();
|
|
593
|
+
const mediumStack_sigmaT_2 = vec3( 0.0 ).toVar();
|
|
594
|
+
const mediumStack_sigmaT_3 = vec3( 0.0 ).toVar();
|
|
595
|
+
const mediumStack_g_1 = float( 0.0 ).toVar();
|
|
596
|
+
const mediumStack_g_2 = float( 0.0 ).toVar();
|
|
597
|
+
const mediumStack_g_3 = float( 0.0 ).toVar();
|
|
598
|
+
// Walk-step budget for the whole path (bounded per render mode + RR).
|
|
599
|
+
const sssSteps = int( 0 ).toVar();
|
|
587
600
|
|
|
588
601
|
// Locked at the first dispersive transmission; reused for subsequent transmissions on
|
|
589
602
|
// the path so multi-bounce dispersion doesn't collapse under repeated colorWeight ×.
|
|
@@ -633,7 +646,7 @@ export const Trace = Fn( ( [
|
|
|
633
646
|
const rayDirection = ray.direction.toVar();
|
|
634
647
|
|
|
635
648
|
// Main bounce loop
|
|
636
|
-
Loop( { start: int( 0 ), end: maxBounceCount.add( transmissiveBounces ).add( 1 ), type: 'int', condition: '<' }, ( { i: bounceIndex } ) => {
|
|
649
|
+
Loop( { start: int( 0 ), end: maxBounceCount.add( transmissiveBounces ).add( maxSubsurfaceSteps ).add( 1 ), type: 'int', condition: '<' }, ( { i: bounceIndex } ) => {
|
|
637
650
|
|
|
638
651
|
// Update state
|
|
639
652
|
stateTraversals.assign( maxBounceCount.sub( effectiveBounces ) );
|
|
@@ -656,30 +669,82 @@ export const Trace = Fn( ( [
|
|
|
656
669
|
currentRay,
|
|
657
670
|
bvhBuffer,
|
|
658
671
|
triangleBuffer,
|
|
672
|
+
mediumStackDepth.greaterThan( int( 0 ) ), // inside a medium → bypass front/back culling
|
|
659
673
|
) ).toVar();
|
|
660
674
|
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
// is currently in — depth==0 means air (no absorption). sigma_a was
|
|
664
|
-
// precomputed at push time, so this collapses to a single exp().
|
|
675
|
+
// In-medium transport: glass (sigma_s==0) absorbs along a straight line; subsurface
|
|
676
|
+
// (sigma_s>0) random-walks and may scatter mid-flight.
|
|
665
677
|
If( hitInfo.didHit.and( mediumStackDepth.greaterThan( int( 0 ) ) ), () => {
|
|
666
678
|
|
|
679
|
+
// Load current-medium coefficients (chained branch — divergence-safe).
|
|
667
680
|
const mSigmaA = vec3( 0.0 ).toVar();
|
|
681
|
+
const mSigmaS = vec3( 0.0 ).toVar();
|
|
682
|
+
const mSigmaT = vec3( 0.0 ).toVar();
|
|
683
|
+
const mG = float( 0.0 ).toVar();
|
|
668
684
|
If( mediumStackDepth.equal( int( 1 ) ), () => {
|
|
669
685
|
|
|
670
|
-
mSigmaA.assign( mediumStack_sigmaA_1 );
|
|
686
|
+
mSigmaA.assign( mediumStack_sigmaA_1 ); mSigmaS.assign( mediumStack_sigmaS_1 );
|
|
687
|
+
mSigmaT.assign( mediumStack_sigmaT_1 ); mG.assign( mediumStack_g_1 );
|
|
671
688
|
|
|
672
689
|
} ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
|
|
673
690
|
|
|
674
|
-
mSigmaA.assign( mediumStack_sigmaA_2 );
|
|
691
|
+
mSigmaA.assign( mediumStack_sigmaA_2 ); mSigmaS.assign( mediumStack_sigmaS_2 );
|
|
692
|
+
mSigmaT.assign( mediumStack_sigmaT_2 ); mG.assign( mediumStack_g_2 );
|
|
675
693
|
|
|
676
694
|
} ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
|
|
677
695
|
|
|
678
|
-
mSigmaA.assign( mediumStack_sigmaA_3 );
|
|
696
|
+
mSigmaA.assign( mediumStack_sigmaA_3 ); mSigmaS.assign( mediumStack_sigmaS_3 );
|
|
697
|
+
mSigmaT.assign( mediumStack_sigmaT_3 ); mG.assign( mediumStack_g_3 );
|
|
679
698
|
|
|
680
699
|
} );
|
|
681
700
|
|
|
682
|
-
|
|
701
|
+
If( maxComponent( { v: mSigmaS } ).lessThanEqual( 0.0 ), () => {
|
|
702
|
+
|
|
703
|
+
// Non-scattering medium (glass): straight-line Beer-Lambert absorption.
|
|
704
|
+
throughput.mulAssign( exp( mSigmaA.mul( hitInfo.dst ).negate() ) );
|
|
705
|
+
|
|
706
|
+
} ).Else( () => {
|
|
707
|
+
|
|
708
|
+
// Scattering medium (subsurface): chromatic distance sampling for this segment.
|
|
709
|
+
const coll = CollisionSample.wrap( sampleChromaticCollision(
|
|
710
|
+
mSigmaT, mSigmaS, throughput, hitInfo.dst, rngState,
|
|
711
|
+
) ).toVar();
|
|
712
|
+
throughput.mulAssign( coll.weight );
|
|
713
|
+
|
|
714
|
+
If( coll.didScatter, () => {
|
|
715
|
+
|
|
716
|
+
// Scatter: move to the collision point, redirect via the HG phase function,
|
|
717
|
+
// continue as a free bounce (no camera-bounce cost).
|
|
718
|
+
const xi2 = vec2( RandomValue( rngState ), RandomValue( rngState ) );
|
|
719
|
+
const scatterPoint = rayOrigin.add( rayDirection.mul( coll.t ) );
|
|
720
|
+
rayOrigin.assign( scatterPoint );
|
|
721
|
+
rayDirection.assign( sampleHenyeyGreenstein( rayDirection, mG, xi2 ) );
|
|
722
|
+
sssSteps.addAssign( 1 );
|
|
723
|
+
stateIsPrimaryRay.assign( tslBool( false ) );
|
|
724
|
+
|
|
725
|
+
// Per-mode step cap: bounded walk (terminate — energy-loss-only bias).
|
|
726
|
+
If( sssSteps.greaterThanEqual( maxSubsurfaceSteps ), () => {
|
|
727
|
+
|
|
728
|
+
Break();
|
|
729
|
+
|
|
730
|
+
} );
|
|
731
|
+
|
|
732
|
+
// Russian roulette so the walk self-terminates before the cap.
|
|
733
|
+
const rrP = clamp( maxComponent( { v: throughput } ), 0.02, 1.0 ).toVar();
|
|
734
|
+
If( RandomValue( rngState ).greaterThan( rrP ), () => {
|
|
735
|
+
|
|
736
|
+
Break();
|
|
737
|
+
|
|
738
|
+
} );
|
|
739
|
+
throughput.divAssign( rrP );
|
|
740
|
+
|
|
741
|
+
Continue();
|
|
742
|
+
|
|
743
|
+
} );
|
|
744
|
+
|
|
745
|
+
// No scatter: reached the boundary (weight applied); fall through to surface handling.
|
|
746
|
+
|
|
747
|
+
} );
|
|
683
748
|
|
|
684
749
|
} );
|
|
685
750
|
|
|
@@ -732,7 +797,7 @@ export const Trace = Fn( ( [
|
|
|
732
797
|
|
|
733
798
|
} );
|
|
734
799
|
|
|
735
|
-
// Get full material (
|
|
800
|
+
// Get full material (30 reads). Lazy transform loading was tested but regressed
|
|
736
801
|
// textured scenes due to identity-construct + conditional-assign overhead.
|
|
737
802
|
// Shadow rays use getShadowMaterial() (7 reads) — the real bandwidth win.
|
|
738
803
|
const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
|
|
@@ -833,6 +898,7 @@ export const Trace = Fn( ( [
|
|
|
833
898
|
mediumStack_attColor_1.assign( material.attenuationColor );
|
|
834
899
|
mediumStack_attDist_1.assign( material.attenuationDistance );
|
|
835
900
|
mediumStack_sigmaA_1.assign( mSigmaA );
|
|
901
|
+
mediumStack_sigmaS_1.assign( vec3( 0.0 ) ); // glass: no scattering
|
|
836
902
|
|
|
837
903
|
} ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
|
|
838
904
|
|
|
@@ -840,6 +906,7 @@ export const Trace = Fn( ( [
|
|
|
840
906
|
mediumStack_attColor_2.assign( material.attenuationColor );
|
|
841
907
|
mediumStack_attDist_2.assign( material.attenuationDistance );
|
|
842
908
|
mediumStack_sigmaA_2.assign( mSigmaA );
|
|
909
|
+
mediumStack_sigmaS_2.assign( vec3( 0.0 ) ); // glass: no scattering
|
|
843
910
|
|
|
844
911
|
} ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
|
|
845
912
|
|
|
@@ -847,6 +914,7 @@ export const Trace = Fn( ( [
|
|
|
847
914
|
mediumStack_attColor_3.assign( material.attenuationColor );
|
|
848
915
|
mediumStack_attDist_3.assign( material.attenuationDistance );
|
|
849
916
|
mediumStack_sigmaA_3.assign( mSigmaA );
|
|
917
|
+
mediumStack_sigmaS_3.assign( vec3( 0.0 ) ); // glass: no scattering
|
|
850
918
|
|
|
851
919
|
} );
|
|
852
920
|
|
|
@@ -865,6 +933,65 @@ export const Trace = Fn( ( [
|
|
|
865
933
|
|
|
866
934
|
} );
|
|
867
935
|
|
|
936
|
+
} ).ElseIf( interaction.isSubsurface, () => {
|
|
937
|
+
|
|
938
|
+
// Subsurface boundary: free bounce (SSS step budget, not camera bounces). Push on enter, pop on exit.
|
|
939
|
+
isFreeBounce.assign( tslBool( true ) );
|
|
940
|
+
stateRayType.assign( int( RAY_TYPE_DIFFUSE ) );
|
|
941
|
+
|
|
942
|
+
If( interaction.didReflect.not(), () => {
|
|
943
|
+
|
|
944
|
+
If( interaction.entering, () => {
|
|
945
|
+
|
|
946
|
+
If( mediumStackDepth.lessThan( int( 3 ) ), () => {
|
|
947
|
+
|
|
948
|
+
mediumStackDepth.addAssign( 1 );
|
|
949
|
+
|
|
950
|
+
const ssCoeffs = MediumCoeffs.wrap( subsurfaceCoefficients(
|
|
951
|
+
material.subsurfaceColor, material.subsurfaceRadius, material.subsurfaceRadiusScale,
|
|
952
|
+
) ).toVar();
|
|
953
|
+
const ssG = clamp( material.subsurfaceAnisotropy, - 0.99, 0.99 ).toVar();
|
|
954
|
+
|
|
955
|
+
If( mediumStackDepth.equal( int( 1 ) ), () => {
|
|
956
|
+
|
|
957
|
+
mediumStack_ior_1.assign( material.ior );
|
|
958
|
+
mediumStack_sigmaA_1.assign( ssCoeffs.sigmaA );
|
|
959
|
+
mediumStack_sigmaS_1.assign( ssCoeffs.sigmaS );
|
|
960
|
+
mediumStack_sigmaT_1.assign( ssCoeffs.sigmaT );
|
|
961
|
+
mediumStack_g_1.assign( ssG );
|
|
962
|
+
|
|
963
|
+
} ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
|
|
964
|
+
|
|
965
|
+
mediumStack_ior_2.assign( material.ior );
|
|
966
|
+
mediumStack_sigmaA_2.assign( ssCoeffs.sigmaA );
|
|
967
|
+
mediumStack_sigmaS_2.assign( ssCoeffs.sigmaS );
|
|
968
|
+
mediumStack_sigmaT_2.assign( ssCoeffs.sigmaT );
|
|
969
|
+
mediumStack_g_2.assign( ssG );
|
|
970
|
+
|
|
971
|
+
} ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
|
|
972
|
+
|
|
973
|
+
mediumStack_ior_3.assign( material.ior );
|
|
974
|
+
mediumStack_sigmaA_3.assign( ssCoeffs.sigmaA );
|
|
975
|
+
mediumStack_sigmaS_3.assign( ssCoeffs.sigmaS );
|
|
976
|
+
mediumStack_sigmaT_3.assign( ssCoeffs.sigmaT );
|
|
977
|
+
mediumStack_g_3.assign( ssG );
|
|
978
|
+
|
|
979
|
+
} );
|
|
980
|
+
|
|
981
|
+
} );
|
|
982
|
+
|
|
983
|
+
} ).Else( () => {
|
|
984
|
+
|
|
985
|
+
If( mediumStackDepth.greaterThan( int( 0 ) ), () => {
|
|
986
|
+
|
|
987
|
+
mediumStackDepth.subAssign( 1 );
|
|
988
|
+
|
|
989
|
+
} );
|
|
990
|
+
|
|
991
|
+
} );
|
|
992
|
+
|
|
993
|
+
} );
|
|
994
|
+
|
|
868
995
|
} ).ElseIf( interaction.isAlphaSkip, () => {
|
|
869
996
|
|
|
870
997
|
isFreeBounce.assign( tslBool( true ) );
|
package/src/TSL/Struct.js
CHANGED
|
@@ -50,6 +50,11 @@ export const RayTracingMaterial = struct( {
|
|
|
50
50
|
iridescence: 'float',
|
|
51
51
|
iridescenceIOR: 'float',
|
|
52
52
|
iridescenceThicknessRange: 'vec2',
|
|
53
|
+
subsurface: 'float', // 0 = off, blends opaque BRDF → random-walk SSS
|
|
54
|
+
subsurfaceColor: 'vec3', // single-scatter albedo (tint light picks up inside)
|
|
55
|
+
subsurfaceRadius: 'vec3', // per-channel mean free path
|
|
56
|
+
subsurfaceRadiusScale: 'float', // scalar multiplier on radius
|
|
57
|
+
subsurfaceAnisotropy: 'float', // Henyey-Greenstein g (-1..1)
|
|
53
58
|
} );
|
|
54
59
|
|
|
55
60
|
// Lightweight material for shadow ray evaluation — only the fields needed
|