rayzee 6.3.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 +3314 -1861
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +27 -25
- 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/AssetLoader.js +151 -1
- package/src/Processor/GeometryExtractor.js +16 -1
- package/src/Processor/PBRT/PBRTMaterials.js +350 -0
- package/src/Processor/PBRT/PBRTMath.js +173 -0
- package/src/Processor/PBRT/PBRTParser.js +642 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +578 -0
- package/src/Processor/PBRT/PBRTTokenizer.js +141 -0
- package/src/Processor/PBRT/index.js +179 -0
- 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 +151 -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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PBRT-v4 scene loader.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates: virtual filesystem (from a zip) → tokenize/parse the entry
|
|
5
|
+
* .pbrt (following Include/Import) → build a THREE scene graph. Geometry,
|
|
6
|
+
* image, and HDR decoding are injected by the host (AssetLoader owns the
|
|
7
|
+
* three/examples loaders) so this module stays dependency-light.
|
|
8
|
+
*
|
|
9
|
+
* Usage (from AssetLoader):
|
|
10
|
+
* const { group, environment, warnings } = await loadPBRTScene({
|
|
11
|
+
* vfs, entryPath, plyParser, imageFromBytes, envFromBytes
|
|
12
|
+
* });
|
|
13
|
+
* scene.environment = environment?.texture ?? scene.environment;
|
|
14
|
+
* await loadObject3D(group);
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { PBRTParser } from './PBRTParser.js';
|
|
18
|
+
import { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
|
|
19
|
+
|
|
20
|
+
export { PBRTParser } from './PBRTParser.js';
|
|
21
|
+
export { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
|
|
22
|
+
export { tokenize } from './PBRTTokenizer.js';
|
|
23
|
+
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
|
|
26
|
+
/** Normalize a path: forward slashes, collapse "./" and "../". */
|
|
27
|
+
function normalizePath( p ) {
|
|
28
|
+
|
|
29
|
+
const parts = p.replace( /\\/g, '/' ).split( '/' );
|
|
30
|
+
const out = [];
|
|
31
|
+
for ( const part of parts ) {
|
|
32
|
+
|
|
33
|
+
if ( part === '' || part === '.' ) continue;
|
|
34
|
+
if ( part === '..' ) out.pop();
|
|
35
|
+
else out.push( part );
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return out.join( '/' );
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Join a base directory with a relative path. */
|
|
44
|
+
function joinPath( dir, rel ) {
|
|
45
|
+
|
|
46
|
+
if ( ! dir ) return normalizePath( rel );
|
|
47
|
+
if ( rel.startsWith( '/' ) ) return normalizePath( rel );
|
|
48
|
+
return normalizePath( `${dir}/${rel}` );
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wraps the zip contents with tolerant, case-insensitive lookup that falls back
|
|
54
|
+
* to a basename match — pbrt scenes are inconsistent about path roots.
|
|
55
|
+
*/
|
|
56
|
+
class VirtualFS {
|
|
57
|
+
|
|
58
|
+
constructor( entries ) {
|
|
59
|
+
|
|
60
|
+
// entries: { path: Uint8Array }
|
|
61
|
+
this.byPath = new Map(); // normalized lowercase -> { norm, bytes }
|
|
62
|
+
this.byBase = new Map(); // basename lowercase -> [ { norm, bytes } ] (insertion order)
|
|
63
|
+
for ( const key in entries ) {
|
|
64
|
+
|
|
65
|
+
const norm = normalizePath( key ).toLowerCase();
|
|
66
|
+
const rec = { norm, bytes: entries[ key ] };
|
|
67
|
+
this.byPath.set( norm, rec );
|
|
68
|
+
const base = norm.split( '/' ).pop();
|
|
69
|
+
const bucket = this.byBase.get( base );
|
|
70
|
+
if ( bucket ) bucket.push( rec );
|
|
71
|
+
else this.byBase.set( base, [ rec ] );
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
find( path ) {
|
|
78
|
+
|
|
79
|
+
const norm = normalizePath( path ).toLowerCase();
|
|
80
|
+
if ( this.byPath.has( norm ) ) return this.byPath.get( norm ).bytes;
|
|
81
|
+
|
|
82
|
+
// Resolve by basename (O(1)); among collisions prefer a path-suffix match,
|
|
83
|
+
// else fall back to the first entry with that name. pbrt scenes are
|
|
84
|
+
// inconsistent about path roots, so this tolerates relative/absolute drift.
|
|
85
|
+
const bucket = this.byBase.get( norm.split( '/' ).pop() );
|
|
86
|
+
if ( ! bucket ) return null;
|
|
87
|
+
const suffixHit = bucket.find( rec => rec.norm.endsWith( '/' + norm ) );
|
|
88
|
+
return ( suffixHit || bucket[ 0 ] ).bytes;
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Pick the top-level .pbrt entry: shallowest path, preferring scene/main names. */
|
|
95
|
+
export function pickEntryPath( entries ) {
|
|
96
|
+
|
|
97
|
+
const pbrts = Object.keys( entries ).filter( k => k.toLowerCase().endsWith( '.pbrt' ) );
|
|
98
|
+
if ( pbrts.length === 0 ) return null;
|
|
99
|
+
|
|
100
|
+
const preferred = pbrts.filter( k => /(^|\/)(scene|main)\.pbrt$/i.test( k ) );
|
|
101
|
+
const pool = preferred.length ? preferred : pbrts;
|
|
102
|
+
|
|
103
|
+
// Shallowest (fewest path segments), then shortest name.
|
|
104
|
+
pool.sort( ( a, b ) => {
|
|
105
|
+
|
|
106
|
+
const da = a.split( '/' ).length, db = b.split( '/' ).length;
|
|
107
|
+
return da !== db ? da - db : a.length - b.length;
|
|
108
|
+
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
return pool[ 0 ];
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {object} args
|
|
117
|
+
* @param {Object<string,Uint8Array>} args.vfs - zip entries (path → bytes)
|
|
118
|
+
* @param {string} [args.entryPath] - top .pbrt; auto-detected if omitted
|
|
119
|
+
* @param {(buf:ArrayBuffer)=>import('three').BufferGeometry} args.plyParser
|
|
120
|
+
* @param {(bytes:Uint8Array, filename:string)=>Promise<import('three').Texture>} args.imageFromBytes
|
|
121
|
+
* @param {(bytes:Uint8Array, filename:string)=>Promise<import('three').Texture>} [args.envFromBytes]
|
|
122
|
+
* @param {boolean} [args.convertHandedness=true]
|
|
123
|
+
* @returns {Promise<{group, camera, environment, warnings, entryPath}>}
|
|
124
|
+
*/
|
|
125
|
+
export async function loadPBRTScene( args ) {
|
|
126
|
+
|
|
127
|
+
const { vfs: rawEntries, plyParser, imageFromBytes, envFromBytes, convertHandedness } = args;
|
|
128
|
+
const vfs = new VirtualFS( rawEntries );
|
|
129
|
+
|
|
130
|
+
const entryPath = args.entryPath || pickEntryPath( rawEntries );
|
|
131
|
+
if ( ! entryPath ) throw new Error( 'PBRT loader: no .pbrt file found in archive' );
|
|
132
|
+
|
|
133
|
+
const entryBytes = vfs.find( entryPath );
|
|
134
|
+
if ( ! entryBytes ) throw new Error( `PBRT loader: entry "${entryPath}" not readable` );
|
|
135
|
+
|
|
136
|
+
const baseDir = entryPath.includes( '/' ) ? entryPath.slice( 0, entryPath.lastIndexOf( '/' ) ) : '';
|
|
137
|
+
|
|
138
|
+
// Parse (with Include resolution)
|
|
139
|
+
const parser = new PBRTParser( {
|
|
140
|
+
resolveInclude: ( path, currentDir ) => {
|
|
141
|
+
|
|
142
|
+
const bytes = vfs.find( joinPath( currentDir, path ) ) || vfs.find( path );
|
|
143
|
+
return bytes ? decoder.decode( bytes ) : null;
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
} );
|
|
147
|
+
const ir = parser.parse( decoder.decode( entryBytes ), baseDir );
|
|
148
|
+
|
|
149
|
+
// Build scene graph
|
|
150
|
+
const sliceBuf = ( bytes ) => bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength );
|
|
151
|
+
const builder = new PBRTSceneBuilder( {
|
|
152
|
+
convertHandedness,
|
|
153
|
+
resolvePLY: async ( filename ) => {
|
|
154
|
+
|
|
155
|
+
const bytes = vfs.find( filename );
|
|
156
|
+
if ( ! bytes ) return null;
|
|
157
|
+
return plyParser( sliceBuf( bytes ) );
|
|
158
|
+
|
|
159
|
+
},
|
|
160
|
+
resolveImage: async ( filename ) => {
|
|
161
|
+
|
|
162
|
+
const bytes = vfs.find( filename );
|
|
163
|
+
if ( ! bytes ) return null;
|
|
164
|
+
return imageFromBytes( bytes, filename );
|
|
165
|
+
|
|
166
|
+
},
|
|
167
|
+
resolveEnvironment: async ( filename ) => {
|
|
168
|
+
|
|
169
|
+
const bytes = vfs.find( filename );
|
|
170
|
+
if ( ! bytes ) return null;
|
|
171
|
+
return ( envFromBytes || imageFromBytes )( bytes, filename );
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
} );
|
|
175
|
+
|
|
176
|
+
const result = await builder.build( ir );
|
|
177
|
+
return { ...result, entryPath };
|
|
178
|
+
|
|
179
|
+
}
|
|
@@ -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 ) );
|
|
@@ -918,6 +1045,18 @@ export const Trace = Fn( ( [
|
|
|
918
1045
|
const randomSample = getRandomSample( pixelCoord, rayIndex, bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
|
|
919
1046
|
|
|
920
1047
|
const V = rayDirection.negate().toVar();
|
|
1048
|
+
|
|
1049
|
+
// Two-sided shading: flip the shading normal into the viewer's hemisphere.
|
|
1050
|
+
// This is the opaque reflection path (transmissive rays already Continue'd),
|
|
1051
|
+
// so it never disturbs dielectric entering/exiting. No-op when N already
|
|
1052
|
+
// faces V; rescues meshes with inward-facing normals (common in imported
|
|
1053
|
+
// scenes, e.g. pbrt PLY assets) that would otherwise shade black.
|
|
1054
|
+
If( dot( N, V ).lessThan( 0.0 ), () => {
|
|
1055
|
+
|
|
1056
|
+
N.assign( N.negate() );
|
|
1057
|
+
|
|
1058
|
+
} );
|
|
1059
|
+
|
|
921
1060
|
material.sheenRoughness.assign( clamp( material.sheenRoughness, MIN_ROUGHNESS, MAX_ROUGHNESS ) );
|
|
922
1061
|
|
|
923
1062
|
// Sync material classification cache up front — the materialCache, BRDF
|
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
|