rayzee 7.2.0 → 7.2.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 +857 -728
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +20 -20
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/PathTracerApp.js +6 -1
- package/src/Processor/EmissiveTriangleBuilder.js +39 -5
- package/src/Processor/LightBVHBuilder.js +113 -27
- package/src/Processor/SceneProcessor.js +19 -1
- package/src/Stages/PathTracer.js +1 -0
- package/src/Stages/PathTracerStage.js +14 -3
- package/src/TSL/EmissiveSampling.js +3 -2
- package/src/TSL/LightBVHSampling.js +216 -25
- package/src/TSL/ShadeKernel.js +21 -6
- package/src/managers/UniformManager.js +4 -0
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
dot,
|
|
15
15
|
sqrt,
|
|
16
16
|
max,
|
|
17
|
+
clamp,
|
|
18
|
+
select,
|
|
17
19
|
normalize,
|
|
18
20
|
cross,
|
|
19
21
|
length,
|
|
@@ -29,6 +31,8 @@ import {
|
|
|
29
31
|
sampleSphericalTriangle,
|
|
30
32
|
barycentricFromPoint,
|
|
31
33
|
useSphericalSampling,
|
|
34
|
+
sphericalTriangleSolidAngle,
|
|
35
|
+
triangleArea,
|
|
32
36
|
SphericalTriangleSampleResult,
|
|
33
37
|
} from './EmissiveSampling.js';
|
|
34
38
|
|
|
@@ -37,6 +41,58 @@ const LBVH_STRIDE = 4; // 4 vec4s per node
|
|
|
37
41
|
const EMISSIVE_STRIDE = 2; // 2 vec4s per emissive entry (matches EmissiveSampling.js)
|
|
38
42
|
const MAX_LBVH_DEPTH = 32;
|
|
39
43
|
|
|
44
|
+
// ================================================================================
|
|
45
|
+
// LIGHT-BVH NODE IMPORTANCE (Conty-Estevez & Kulla 2018 / PBRT-v4 BVHLightSampler)
|
|
46
|
+
// ================================================================================
|
|
47
|
+
// cos((thetaA - thetaB) clamped to >= 0). cosA > cosB ⇒ thetaA < thetaB ⇒ diff clamped to 0.
|
|
48
|
+
const cosSubClamped = Fn( ( [ sinThetaA, cosThetaA, sinThetaB, cosThetaB ] ) => {
|
|
49
|
+
|
|
50
|
+
return select( cosThetaA.greaterThan( cosThetaB ), float( 1.0 ), cosThetaA.mul( cosThetaB ).add( sinThetaA.mul( sinThetaB ) ) );
|
|
51
|
+
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
// sin((thetaA - thetaB) clamped to >= 0).
|
|
55
|
+
const sinSubClamped = Fn( ( [ sinThetaA, cosThetaA, sinThetaB, cosThetaB ] ) => {
|
|
56
|
+
|
|
57
|
+
return select( cosThetaA.greaterThan( cosThetaB ), float( 0.0 ), sinThetaA.mul( cosThetaB ).sub( cosThetaA.mul( sinThetaB ) ) );
|
|
58
|
+
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
// Importance of a Light-BVH node for a shading point (power × orientation × inverse-square),
|
|
62
|
+
// θ_e = π/2 (diffuse emitters). cosThetaO = -1 ⇒ whole-sphere cone (never culled by orientation).
|
|
63
|
+
// SHARED by both the stochastic descent and the MIS pdf re-walk — they MUST stay byte-identical.
|
|
64
|
+
export const lbvhNodeImportance = Fn( ( [ nMin, power, nMax, coneAxis, cosThetaO, hitPoint ] ) => {
|
|
65
|
+
|
|
66
|
+
const center = nMin.add( nMax ).mul( 0.5 );
|
|
67
|
+
const diagLen = length( nMax.sub( nMin ) );
|
|
68
|
+
const toCenter = hitPoint.sub( center );
|
|
69
|
+
const d2c = max( dot( toCenter, toCenter ), float( 1e-12 ) );
|
|
70
|
+
// PBRT distance clamp (note: clamps to diagLen/2, a length not a square — matches PBRT)
|
|
71
|
+
const d2 = max( d2c, diagLen.mul( 0.5 ) );
|
|
72
|
+
|
|
73
|
+
const wi = toCenter.div( sqrt( d2c ) ); // direction from cluster center to shading point
|
|
74
|
+
const cosThetaW = dot( coneAxis, wi );
|
|
75
|
+
const sinThetaW = sqrt( max( float( 1.0 ).sub( cosThetaW.mul( cosThetaW ) ), float( 0.0 ) ) );
|
|
76
|
+
|
|
77
|
+
// Half-angle subtended by the cluster's bounding sphere from the shading point.
|
|
78
|
+
const r2 = diagLen.mul( diagLen ).mul( 0.25 );
|
|
79
|
+
const sin2ThetaB = clamp( r2.div( d2c ), float( 0.0 ), float( 1.0 ) );
|
|
80
|
+
const cosThetaB = select( d2c.lessThan( r2 ), float( - 1.0 ), sqrt( max( float( 1.0 ).sub( sin2ThetaB ), float( 0.0 ) ) ) );
|
|
81
|
+
const sinThetaB = sqrt( max( float( 1.0 ).sub( cosThetaB.mul( cosThetaB ) ), float( 0.0 ) ) );
|
|
82
|
+
|
|
83
|
+
const sinThetaO = sqrt( max( float( 1.0 ).sub( cosThetaO.mul( cosThetaO ) ), float( 0.0 ) ) );
|
|
84
|
+
|
|
85
|
+
// cosThetap = cos( (theta_w - theta_o - theta_b) clamped >= 0 )
|
|
86
|
+
const cosThetaX = cosSubClamped( sinThetaW, cosThetaW, sinThetaO, cosThetaO );
|
|
87
|
+
const sinThetaX = sinSubClamped( sinThetaW, cosThetaW, sinThetaO, cosThetaO );
|
|
88
|
+
const cosThetap = cosSubClamped( sinThetaX, cosThetaX, sinThetaB, cosThetaB );
|
|
89
|
+
|
|
90
|
+
// θ_e = π/2 ⇒ cosThetaE = 0; cluster cannot illuminate the point when cosThetap <= 0.
|
|
91
|
+
const imp = select( cosThetap.greaterThan( float( 0.0 ) ), power.mul( cosThetap ).div( d2 ), float( 0.0 ) );
|
|
92
|
+
return max( imp, float( 0.0 ) );
|
|
93
|
+
|
|
94
|
+
} );
|
|
95
|
+
|
|
40
96
|
/**
|
|
41
97
|
* Sample one emissive triangle using the Light BVH for spatially-aware importance sampling.
|
|
42
98
|
*
|
|
@@ -105,31 +161,12 @@ export const sampleLightBVHTriangle = Fn( ( [
|
|
|
105
161
|
const rd0 = lbvhBuffer.element( rBase ); // [minX, minY, minZ, totalPower]
|
|
106
162
|
const rd1 = lbvhBuffer.element( rBase.add( int( 1 ) ) ); // [maxX, maxY, maxZ, isLeaf]
|
|
107
163
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
const rCenter = vec3(
|
|
115
|
-
rd0.x.add( rd1.x ).mul( 0.5 ),
|
|
116
|
-
rd0.y.add( rd1.y ).mul( 0.5 ),
|
|
117
|
-
rd0.z.add( rd1.z ).mul( 0.5 )
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
// Compute squared distance from hitPoint to each child center
|
|
121
|
-
const lDiff = lCenter.sub( hitPoint );
|
|
122
|
-
const rDiff = rCenter.sub( hitPoint );
|
|
123
|
-
const lDistSq = max( dot( lDiff, lDiff ), float( 0.01 ) );
|
|
124
|
-
const rDistSq = max( dot( rDiff, rDiff ), float( 0.01 ) );
|
|
125
|
-
|
|
126
|
-
// Child power
|
|
127
|
-
const lPower = max( ld0.w, float( 0.0 ) );
|
|
128
|
-
const rPower = max( rd0.w, float( 0.0 ) );
|
|
129
|
-
|
|
130
|
-
// Importance = power / dist²
|
|
131
|
-
const lImportance = lPower.div( lDistSq );
|
|
132
|
-
const rImportance = rPower.div( rDistSq );
|
|
164
|
+
// Conty-Kulla importance: power × orientation-cone × inverse-square (shared with the MIS re-walk).
|
|
165
|
+
// d3 = [coneAxis, cosThetaO]; cosThetaO = -1 ⇒ whole-sphere cone (never culled by orientation).
|
|
166
|
+
const ld3 = lbvhBuffer.element( lBase.add( int( 3 ) ) );
|
|
167
|
+
const rd3 = lbvhBuffer.element( rBase.add( int( 3 ) ) );
|
|
168
|
+
const lImportance = lbvhNodeImportance( ld0.xyz, ld0.w, ld1.xyz, ld3.xyz, ld3.w, hitPoint );
|
|
169
|
+
const rImportance = lbvhNodeImportance( rd0.xyz, rd0.w, rd1.xyz, rd3.xyz, rd3.w, hitPoint );
|
|
133
170
|
const totalImportance = lImportance.add( rImportance );
|
|
134
171
|
|
|
135
172
|
If( totalImportance.lessThanEqual( float( 0.0 ) ), () => {
|
|
@@ -307,3 +344,157 @@ export const sampleLightBVHTriangle = Fn( ( [
|
|
|
307
344
|
return result;
|
|
308
345
|
|
|
309
346
|
} );
|
|
347
|
+
|
|
348
|
+
// ================================================================================
|
|
349
|
+
// LIGHT-BVH MIS PDF (re-walk)
|
|
350
|
+
// ================================================================================
|
|
351
|
+
// Computes the solid-angle pdf that sampleLightBVHTriangle WOULD assign to a given emissive
|
|
352
|
+
// triangle hit by a BSDF bounce, by re-walking the exact stochastic descent along the triangle's
|
|
353
|
+
// stored bit-trail. Required so the bounce-hit MIS weight uses the SAME light pdf the NEE sampler
|
|
354
|
+
// used — without this the MIS partition-of-unity breaks (a real bias).
|
|
355
|
+
//
|
|
356
|
+
// lightBuffer packs [ LBVH nodes | emissive entries | bit-trail map ]. The bit-trail map holds one
|
|
357
|
+
// float per absolute triangleIndex (4 packed per vec4): the root→leaf left(0)/right(1) choices.
|
|
358
|
+
export const calculateLightBVHPdf = Fn( ( [
|
|
359
|
+
triangleIndex, hitDistance, rayDir, shadingPoint,
|
|
360
|
+
lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer,
|
|
361
|
+
] ) => {
|
|
362
|
+
|
|
363
|
+
const result = float( 0.0 ).toVar();
|
|
364
|
+
|
|
365
|
+
const triIdx = int( triangleIndex ).toVar();
|
|
366
|
+
|
|
367
|
+
// Fetch this triangle's bit-trail (4 packed per vec4). -1 ⇒ not in the BVH (shouldn't happen on
|
|
368
|
+
// an emissive hit) → leave pdf 0 (MIS falls back to BSDF-only).
|
|
369
|
+
const packed = lightBuffer.element( reverseMapVec4Offset.add( triIdx.shiftRight( int( 2 ) ) ) );
|
|
370
|
+
const lane = triIdx.bitAnd( int( 3 ) );
|
|
371
|
+
const trailF = select( lane.equal( int( 0 ) ), packed.x,
|
|
372
|
+
select( lane.equal( int( 1 ) ), packed.y,
|
|
373
|
+
select( lane.equal( int( 2 ) ), packed.z, packed.w ) ) ).toVar();
|
|
374
|
+
|
|
375
|
+
If( trailF.greaterThanEqual( float( 0.0 ) ), () => {
|
|
376
|
+
|
|
377
|
+
const trail = int( trailF ).toVar();
|
|
378
|
+
const selectionPdf = float( 1.0 ).toVar();
|
|
379
|
+
const nodeIndex = int( 0 ).toVar();
|
|
380
|
+
const depth = int( 0 ).toVar();
|
|
381
|
+
const foundLeaf = tslBool( false ).toVar();
|
|
382
|
+
|
|
383
|
+
Loop( MAX_LBVH_DEPTH, () => {
|
|
384
|
+
|
|
385
|
+
const base = nodeIndex.mul( int( LBVH_STRIDE ) );
|
|
386
|
+
const d1 = lightBuffer.element( base.add( int( 1 ) ) ); // [max, isLeaf]
|
|
387
|
+
|
|
388
|
+
If( d1.w.greaterThan( 0.5 ), () => {
|
|
389
|
+
|
|
390
|
+
foundLeaf.assign( tslBool( true ) );
|
|
391
|
+
Break();
|
|
392
|
+
|
|
393
|
+
} );
|
|
394
|
+
|
|
395
|
+
const d2 = lightBuffer.element( base.add( int( 2 ) ) ); // [left, right]
|
|
396
|
+
const leftChildIdx = int( d2.x );
|
|
397
|
+
const rightChildIdx = int( d2.y );
|
|
398
|
+
|
|
399
|
+
const lBase = leftChildIdx.mul( int( LBVH_STRIDE ) );
|
|
400
|
+
const ld0 = lightBuffer.element( lBase );
|
|
401
|
+
const ld1 = lightBuffer.element( lBase.add( int( 1 ) ) );
|
|
402
|
+
const ld3 = lightBuffer.element( lBase.add( int( 3 ) ) );
|
|
403
|
+
const rBase = rightChildIdx.mul( int( LBVH_STRIDE ) );
|
|
404
|
+
const rd0 = lightBuffer.element( rBase );
|
|
405
|
+
const rd1 = lightBuffer.element( rBase.add( int( 1 ) ) );
|
|
406
|
+
const rd3 = lightBuffer.element( rBase.add( int( 3 ) ) );
|
|
407
|
+
|
|
408
|
+
const lImp = lbvhNodeImportance( ld0.xyz, ld0.w, ld1.xyz, ld3.xyz, ld3.w, shadingPoint );
|
|
409
|
+
const rImp = lbvhNodeImportance( rd0.xyz, rd0.w, rd1.xyz, rd3.xyz, rd3.w, shadingPoint );
|
|
410
|
+
const totalImp = lImp.add( rImp );
|
|
411
|
+
|
|
412
|
+
// Trail bit at this depth: 0 → left, 1 → right. Mirror the descent's choice logic EXACTLY.
|
|
413
|
+
const goRight = trail.shiftRight( depth ).bitAnd( int( 1 ) ).equal( int( 1 ) );
|
|
414
|
+
|
|
415
|
+
If( totalImp.lessThanEqual( float( 0.0 ) ), () => {
|
|
416
|
+
|
|
417
|
+
// Descent forces left with no pdf update; if the trail goes right it is unreachable.
|
|
418
|
+
If( goRight, () => {
|
|
419
|
+
|
|
420
|
+
selectionPdf.assign( float( 0.0 ) );
|
|
421
|
+
nodeIndex.assign( rightChildIdx );
|
|
422
|
+
|
|
423
|
+
} ).Else( () => {
|
|
424
|
+
|
|
425
|
+
nodeIndex.assign( leftChildIdx );
|
|
426
|
+
|
|
427
|
+
} );
|
|
428
|
+
|
|
429
|
+
} ).Else( () => {
|
|
430
|
+
|
|
431
|
+
const pLeft = lImp.div( totalImp );
|
|
432
|
+
If( goRight, () => {
|
|
433
|
+
|
|
434
|
+
selectionPdf.mulAssign( float( 1.0 ).sub( pLeft ) );
|
|
435
|
+
nodeIndex.assign( rightChildIdx );
|
|
436
|
+
|
|
437
|
+
} ).Else( () => {
|
|
438
|
+
|
|
439
|
+
selectionPdf.mulAssign( pLeft );
|
|
440
|
+
nodeIndex.assign( leftChildIdx );
|
|
441
|
+
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
} );
|
|
445
|
+
|
|
446
|
+
depth.addAssign( int( 1 ) );
|
|
447
|
+
|
|
448
|
+
} );
|
|
449
|
+
|
|
450
|
+
If( foundLeaf.and( selectionPdf.greaterThan( float( 0.0 ) ) ), () => {
|
|
451
|
+
|
|
452
|
+
// Leaf: power-weighted within-leaf selection prob for the target triangle (matches the sampler).
|
|
453
|
+
const base = nodeIndex.mul( int( LBVH_STRIDE ) );
|
|
454
|
+
const d0 = lightBuffer.element( base );
|
|
455
|
+
const d2 = lightBuffer.element( base.add( int( 2 ) ) );
|
|
456
|
+
const emissiveStart = int( d2.x );
|
|
457
|
+
const emissiveCount = int( d2.y );
|
|
458
|
+
const leafTotalPower = max( d0.w, float( 1e-10 ) );
|
|
459
|
+
|
|
460
|
+
const targetPower = float( 0.0 ).toVar();
|
|
461
|
+
Loop( { start: int( 0 ), end: emissiveCount }, ( { i } ) => {
|
|
462
|
+
|
|
463
|
+
const entryIdx = emissiveStart.add( i );
|
|
464
|
+
const emData0 = lightBuffer.element( emissiveVec4Offset.add( entryIdx.mul( int( EMISSIVE_STRIDE ) ) ) );
|
|
465
|
+
If( int( emData0.r ).equal( triIdx ), () => {
|
|
466
|
+
|
|
467
|
+
targetPower.assign( max( emData0.g, float( 0.0 ) ) );
|
|
468
|
+
Break();
|
|
469
|
+
|
|
470
|
+
} );
|
|
471
|
+
|
|
472
|
+
} );
|
|
473
|
+
|
|
474
|
+
selectionPdf.mulAssign( targetPower.div( leafTotalPower ) );
|
|
475
|
+
|
|
476
|
+
// Convert selection pdf → solid-angle measure using the SAME heuristic as the sampler.
|
|
477
|
+
const triData = TriangleData.wrap( fetchTriangleData( triIdx, triangleBuffer ) );
|
|
478
|
+
If( useSphericalSampling( triData.v0, triData.v1, triData.v2, shadingPoint ), () => {
|
|
479
|
+
|
|
480
|
+
const solidAngle = sphericalTriangleSolidAngle( triData.v0, triData.v1, triData.v2, shadingPoint );
|
|
481
|
+
result.assign( selectionPdf.div( max( solidAngle, float( 1e-10 ) ) ) );
|
|
482
|
+
|
|
483
|
+
} ).Else( () => {
|
|
484
|
+
|
|
485
|
+
const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
|
|
486
|
+
const cosLight = max( dot( rayDir.negate(), geoNormal ), float( 0.001 ) );
|
|
487
|
+
const area = triangleArea( triData.v0, triData.v1, triData.v2 );
|
|
488
|
+
const distSq = hitDistance.mul( hitDistance );
|
|
489
|
+
const pdfArea = selectionPdf.div( max( area, float( 1e-10 ) ) );
|
|
490
|
+
result.assign( pdfArea.mul( distSq ).div( cosLight ) );
|
|
491
|
+
|
|
492
|
+
} );
|
|
493
|
+
|
|
494
|
+
} );
|
|
495
|
+
|
|
496
|
+
} );
|
|
497
|
+
|
|
498
|
+
return max( result, MIN_PDF );
|
|
499
|
+
|
|
500
|
+
} );
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -29,7 +29,7 @@ import { getImportanceSamplingInfo } from './MaterialProperties.js';
|
|
|
29
29
|
import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
|
|
30
30
|
import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
|
|
31
31
|
import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
|
|
32
|
-
import { sampleLightBVHTriangle } from './LightBVHSampling.js';
|
|
32
|
+
import { sampleLightBVHTriangle, calculateLightBVHPdf } from './LightBVHSampling.js';
|
|
33
33
|
import {
|
|
34
34
|
Ray,
|
|
35
35
|
HitInfo,
|
|
@@ -85,7 +85,7 @@ export function buildShadeKernel( params ) {
|
|
|
85
85
|
fireflyThreshold, frame, resolution,
|
|
86
86
|
emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
|
|
87
87
|
emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
|
|
88
|
-
lightBVHNodeCount,
|
|
88
|
+
lightBVHNodeCount, reverseMapVec4Offset,
|
|
89
89
|
maxRayCount,
|
|
90
90
|
} = params;
|
|
91
91
|
|
|
@@ -560,10 +560,25 @@ export function buildShadeKernel( params ) {
|
|
|
560
560
|
const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
|
|
561
561
|
If( prevBouncePdf.greaterThan( 0.0 ), () => {
|
|
562
562
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
);
|
|
563
|
+
// MIS partner pdf MUST match the actual NEE sampler: re-walk the Light BVH descent
|
|
564
|
+
// when it is active, else use the flat-CDF pdf. Mismatching them breaks MIS
|
|
565
|
+
// partition-of-unity → a real bias (see calculateLightBVHPdf).
|
|
566
|
+
const lightPdf = float( 0.0 ).toVar();
|
|
567
|
+
If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
|
|
568
|
+
|
|
569
|
+
lightPdf.assign( calculateLightBVHPdf(
|
|
570
|
+
int( hitTriIdx ), hitDist, direction, origin,
|
|
571
|
+
lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer,
|
|
572
|
+
) );
|
|
573
|
+
|
|
574
|
+
} ).Else( () => {
|
|
575
|
+
|
|
576
|
+
lightPdf.assign( calculateEmissiveLightPdf(
|
|
577
|
+
int( hitTriIdx ), hitDist, direction, origin,
|
|
578
|
+
triangleBuffer, materialBuffer, emissiveTotalPower,
|
|
579
|
+
) );
|
|
580
|
+
|
|
581
|
+
} );
|
|
567
582
|
emissiveMISWeight.assign( powerHeuristic( { pdf1: prevBouncePdf, pdf2: lightPdf } ) );
|
|
568
583
|
|
|
569
584
|
} );
|
|
@@ -242,6 +242,10 @@ export class UniformManager {
|
|
|
242
242
|
// Offset (in vec4 elements) within the packed light buffer where emissive
|
|
243
243
|
// triangle data starts. Equals lightBVHNodeCount * LBVH_STRIDE; computed on upload.
|
|
244
244
|
u( 'emissiveVec4Offset', 0, 'int' );
|
|
245
|
+
// Offset (in vec4 elements) within the packed light buffer where the per-triangle
|
|
246
|
+
// bit-trail map starts (4 trails packed per vec4); computed on upload. Used by the
|
|
247
|
+
// bounce-hit MIS path to re-walk the Light BVH descent pdf.
|
|
248
|
+
u( 'reverseMapVec4Offset', 0, 'int' );
|
|
245
249
|
|
|
246
250
|
// Render mode
|
|
247
251
|
u( 'renderMode', DEFAULT_STATE.renderMode, 'int' );
|