rayzee 7.2.0 → 7.3.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 +1599 -1348
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +45 -45
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +6 -0
- package/src/PathTracerApp.js +26 -2
- package/src/Processor/EmissiveTriangleBuilder.js +39 -5
- package/src/Processor/LightBVHBuilder.js +113 -27
- package/src/Processor/SceneProcessor.js +19 -1
- package/src/RenderSettings.js +3 -0
- package/src/Stages/PathTracer.js +4 -0
- package/src/Stages/PathTracerStage.js +14 -3
- package/src/TSL/Common.js +62 -0
- package/src/TSL/EmissiveSampling.js +3 -2
- package/src/TSL/Environment.js +22 -1
- package/src/TSL/LightBVHSampling.js +216 -25
- package/src/TSL/LightsSampling.js +35 -14
- package/src/TSL/ShadeKernel.js +148 -14
- package/src/TSL/Struct.js +8 -0
- package/src/managers/UniformManager.js +7 -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
|
+
} );
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
sampleIESProfile,
|
|
59
59
|
} from './LightsCore.js';
|
|
60
60
|
|
|
61
|
-
import { MISStrategy, DotProducts } from './Struct.js';
|
|
61
|
+
import { MISStrategy, DotProducts, DirectLightingDual } from './Struct.js';
|
|
62
62
|
import {
|
|
63
63
|
calculateDirectionalLightImportance,
|
|
64
64
|
estimateLightImportance,
|
|
@@ -707,9 +707,14 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
707
707
|
envCDFTexture,
|
|
708
708
|
envTotalSum, envCompensationDelta, envResolution,
|
|
709
709
|
enableEnvironmentLight,
|
|
710
|
+
// Shadow catcher: when true, also accumulate the unoccluded (visibility=1) reference
|
|
711
|
+
// at every light/env site so the caller can form a shadow ratio. Dead path otherwise.
|
|
712
|
+
wantUnoccluded,
|
|
710
713
|
] ) => {
|
|
711
714
|
|
|
712
715
|
const totalContribution = vec3( 0.0 ).toVar();
|
|
716
|
+
// Unoccluded reference (visibility forced to 1) — only filled when wantUnoccluded.
|
|
717
|
+
const unoccludedContribution = vec3( 0.0 ).toVar();
|
|
713
718
|
const rayOrigin = hitPoint.add( hitNormal.mul( 0.001 ) ).toVar();
|
|
714
719
|
|
|
715
720
|
// Binds BVH params so shadow-ray sites at varying call depths use a 3-arg call
|
|
@@ -819,7 +824,7 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
819
824
|
const shadowDistance = min( lightSample.distance.sub( 0.001 ), float( 1000.0 ) );
|
|
820
825
|
const visibility = shadow( rayOrigin, lightSample.direction, shadowDistance );
|
|
821
826
|
|
|
822
|
-
If( visibility.greaterThan( 0.0 ), () => {
|
|
827
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
823
828
|
|
|
824
829
|
// Share H + dot products between BRDF eval and PDF — otherwise each
|
|
825
830
|
// would recompute normalize(V+L) + 5 dot products independently.
|
|
@@ -846,9 +851,15 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
846
851
|
|
|
847
852
|
} );
|
|
848
853
|
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
854
|
+
// Base contribution WITHOUT visibility; shadowed = base × visibility (identical to the
|
|
855
|
+
// pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
|
|
856
|
+
const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( lightWeight, 1e-10 ) );
|
|
857
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
858
|
+
If( wantUnoccluded, () => {
|
|
859
|
+
|
|
860
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
861
|
+
|
|
862
|
+
} );
|
|
852
863
|
|
|
853
864
|
} );
|
|
854
865
|
|
|
@@ -918,7 +929,7 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
918
929
|
const shadowDistance = min( hitDistance.sub( 0.001 ), float( 1000.0 ) );
|
|
919
930
|
const visibility = shadow( rayOrigin, brdfSampleDirection, shadowDistance );
|
|
920
931
|
|
|
921
|
-
If( visibility.greaterThan( 0.0 ), () => {
|
|
932
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
922
933
|
|
|
923
934
|
const lightFacing = max( float( 0.0 ), dot( brdfSampleDirection, light.normal ).negate() ).toVar();
|
|
924
935
|
|
|
@@ -934,9 +945,14 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
934
945
|
const misW = powerHeuristic( { pdf1: brdfPdfWeighted, pdf2: lightPdfWeighted } ).toVar();
|
|
935
946
|
|
|
936
947
|
const lightEmission = light.color.mul( light.intensity );
|
|
937
|
-
//
|
|
938
|
-
const
|
|
939
|
-
totalContribution.addAssign(
|
|
948
|
+
// Base contribution WITHOUT visibility (see discrete-light site).
|
|
949
|
+
const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( brdfWeight, 1e-10 ) );
|
|
950
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
951
|
+
If( wantUnoccluded, () => {
|
|
952
|
+
|
|
953
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
954
|
+
|
|
955
|
+
} );
|
|
940
956
|
|
|
941
957
|
} );
|
|
942
958
|
|
|
@@ -984,7 +1000,7 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
984
1000
|
|
|
985
1001
|
const visibility = shadow( rayOrigin, envDirection, float( 1000.0 ) );
|
|
986
1002
|
|
|
987
|
-
If( visibility.greaterThan( 0.0 ), () => {
|
|
1003
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
988
1004
|
|
|
989
1005
|
// Share H + dots between env BRDF/PDF — same redundancy fix as the
|
|
990
1006
|
// discrete-light path above.
|
|
@@ -1000,9 +1016,14 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
1000
1016
|
float( 1.0 )
|
|
1001
1017
|
).toVar();
|
|
1002
1018
|
|
|
1003
|
-
//
|
|
1004
|
-
const
|
|
1005
|
-
totalContribution.addAssign(
|
|
1019
|
+
// Base contribution WITHOUT visibility (deterministic estimator; no stochastic scaling).
|
|
1020
|
+
const baseContribution = envColor.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( envPdf, 1e-10 ) );
|
|
1021
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
1022
|
+
If( wantUnoccluded, () => {
|
|
1023
|
+
|
|
1024
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
1025
|
+
|
|
1026
|
+
} );
|
|
1006
1027
|
|
|
1007
1028
|
} );
|
|
1008
1029
|
|
|
@@ -1018,6 +1039,6 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
1018
1039
|
// NOTE: Emissive triangle sampling is handled separately in pathtracer_core.fs
|
|
1019
1040
|
// to bypass firefly suppression. Do not add it here to avoid double-counting.
|
|
1020
1041
|
|
|
1021
|
-
return totalContribution;
|
|
1042
|
+
return DirectLightingDual( { shadowed: totalContribution, unoccluded: unoccludedContribution } );
|
|
1022
1043
|
|
|
1023
1044
|
} );
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
Fn, float, vec2, vec3, vec4, int, uint,
|
|
9
|
+
bool as tslBool,
|
|
9
10
|
If, normalize, max, exp, log, clamp, dot, length, select,
|
|
10
11
|
instanceIndex,
|
|
11
12
|
sampler,
|
|
@@ -13,8 +14,9 @@ import {
|
|
|
13
14
|
Return,
|
|
14
15
|
} from 'three/tsl';
|
|
15
16
|
|
|
16
|
-
import { sampleEnvironment, sampleEquirectProbability, sampleEquirect,
|
|
17
|
-
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial } from './Common.js';
|
|
17
|
+
import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
|
|
18
|
+
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial } from './Common.js';
|
|
19
|
+
import { cosineWeightedSample } from './MaterialSampling.js';
|
|
18
20
|
import { sampleAllMaterialTextures } from './TextureSampling.js';
|
|
19
21
|
import { evaluateMaterialResponse } from './MaterialEvaluation.js';
|
|
20
22
|
import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
|
|
@@ -29,7 +31,7 @@ import { getImportanceSamplingInfo } from './MaterialProperties.js';
|
|
|
29
31
|
import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
|
|
30
32
|
import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
|
|
31
33
|
import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
|
|
32
|
-
import { sampleLightBVHTriangle } from './LightBVHSampling.js';
|
|
34
|
+
import { sampleLightBVHTriangle, calculateLightBVHPdf } from './LightBVHSampling.js';
|
|
33
35
|
import {
|
|
34
36
|
Ray,
|
|
35
37
|
HitInfo,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
MaterialClassification,
|
|
41
43
|
BRDFWeights,
|
|
42
44
|
MaterialCache,
|
|
45
|
+
DirectLightingDual,
|
|
43
46
|
} from './Struct.js';
|
|
44
47
|
import { RandomValue, getRandomSample } from './Random.js';
|
|
45
48
|
import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
|
|
@@ -57,6 +60,14 @@ import {
|
|
|
57
60
|
const WG_SIZE = 256;
|
|
58
61
|
const MISS_DIST = 1e19;
|
|
59
62
|
|
|
63
|
+
// Shadow-catcher thresholds (named so the two distinct 1e-4 uses don't read as one value)
|
|
64
|
+
const CATCHER_T_MIN = 1e-4; // min ray-t for the analytic plane hit (skip behind/at the camera)
|
|
65
|
+
const CATCHER_LUMA_FLOOR = 1e-4; // denominator floor for the shadow ratio
|
|
66
|
+
const CATCHER_COVERAGE_MIN = 1e-3; // below this incident luma there is no shadow to catch
|
|
67
|
+
// Reserved G-buffer surface ID for catcher pixels (no real triangle); stable across frames so the
|
|
68
|
+
// denoiser treats the analytic plane as one coherent surface, not a background miss.
|
|
69
|
+
const CATCHER_SURFACE_ID = 0xFFFFFFFE;
|
|
70
|
+
|
|
60
71
|
export function buildShadeKernel( params ) {
|
|
61
72
|
|
|
62
73
|
const {
|
|
@@ -71,7 +82,8 @@ export function buildShadeKernel( params ) {
|
|
|
71
82
|
displacementMaps,
|
|
72
83
|
envTexture, environmentIntensity, envMatrix,
|
|
73
84
|
enableEnvironmentLight, useEnvMapIS,
|
|
74
|
-
groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
|
|
85
|
+
groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
86
|
+
enableGroundCatcher, groundCatcherHeight,
|
|
75
87
|
envTotalSum, envCompensationDelta, envResolution,
|
|
76
88
|
directionalLightsBuffer, numDirectionalLights,
|
|
77
89
|
areaLightsBuffer, numAreaLights,
|
|
@@ -85,7 +97,7 @@ export function buildShadeKernel( params ) {
|
|
|
85
97
|
fireflyThreshold, frame, resolution,
|
|
86
98
|
emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
|
|
87
99
|
emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
|
|
88
|
-
lightBVHNodeCount,
|
|
100
|
+
lightBVHNodeCount, reverseMapVec4Offset,
|
|
89
101
|
maxRayCount,
|
|
90
102
|
} = params;
|
|
91
103
|
|
|
@@ -136,6 +148,111 @@ export function buildShadeKernel( params ) {
|
|
|
136
148
|
const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
|
|
137
149
|
const sampleIndex = int( rayID.div( maxRaysPerSample ) ).toVar();
|
|
138
150
|
|
|
151
|
+
// ── Analytic ground-plane shadow catcher (primary ray only, no geometry) ──
|
|
152
|
+
// A horizontal plane at y = groundCatcherHeight. For a bounce-0 ray that crosses it
|
|
153
|
+
// closer than any BVH hit (hitDist is MISS_DIST on sky rays, so the plane wins over the
|
|
154
|
+
// bare environment), shade it as a diffuse Lambertian holdout: output a monochrome shadow
|
|
155
|
+
// ratio in alpha (rgb = 0) so it composites as bg·ratio over a transparent background.
|
|
156
|
+
// Shadows are caught by the EXISTING NEE shadow ray into real geometry — the plane itself
|
|
157
|
+
// never enters the BVH. Secondary bounces ignore it entirely.
|
|
158
|
+
If( enableGroundCatcher.and( bounceIndex.equal( 0 ) ), () => {
|
|
159
|
+
|
|
160
|
+
const dirY = direction.y.toVar();
|
|
161
|
+
If( dirY.abs().greaterThan( float( EPSILON ) ), () => {
|
|
162
|
+
|
|
163
|
+
const tPlane = groundCatcherHeight.sub( origin.y ).div( dirY ).toVar();
|
|
164
|
+
If( tPlane.greaterThan( float( CATCHER_T_MIN ) ).and( tPlane.lessThan( hitDist ) ), () => {
|
|
165
|
+
|
|
166
|
+
const planePoint = origin.add( direction.mul( tPlane ) ).toVar();
|
|
167
|
+
const planeN = vec3( 0.0, 1.0, 0.0 );
|
|
168
|
+
const planeV = direction.negate().toVar();
|
|
169
|
+
const planeMat = RayTracingMaterial.wrap( diffuseGroundMaterial() ).toVar();
|
|
170
|
+
|
|
171
|
+
// Cosine-weighted hemisphere BRDF sample about the plane normal (0,1,0); the helper
|
|
172
|
+
// puts cosθ along N, so bDir.y == cosθ. Per-component .toVar() on the two randoms
|
|
173
|
+
// (vec2(RandomValue,RandomValue) would collapse to u==v — TSL pitfall).
|
|
174
|
+
const u1 = RandomValue( rngState ).toVar();
|
|
175
|
+
const u2 = RandomValue( rngState ).toVar();
|
|
176
|
+
const bDir = cosineWeightedSample( planeN, vec2( u1, u2 ) ).toVar();
|
|
177
|
+
const bPdf = bDir.y.mul( PI_INV ).toVar(); // cosθ / π
|
|
178
|
+
const bVal = vec3( PI_INV ); // albedo(1) / π
|
|
179
|
+
|
|
180
|
+
// Reuse the full NEE estimator; the diffuse BRDF is constant and cancels in the
|
|
181
|
+
// ratio, so this yields an irradiance-weighted shadow density across all lights + env.
|
|
182
|
+
const dual = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
183
|
+
planePoint, planeN, planeMat, planeV,
|
|
184
|
+
bDir, bPdf, bVal,
|
|
185
|
+
bounceIndex, rngState,
|
|
186
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
187
|
+
areaLightsBuffer, numAreaLights,
|
|
188
|
+
pointLightsBuffer, numPointLights,
|
|
189
|
+
spotLightsBuffer, numSpotLights,
|
|
190
|
+
bvhBuffer, triangleBuffer, materialBuffer,
|
|
191
|
+
envTexture, environmentIntensity, envMatrix,
|
|
192
|
+
envCDFTexture,
|
|
193
|
+
envTotalSum, envCompensationDelta, envResolution,
|
|
194
|
+
enableEnvironmentLight,
|
|
195
|
+
tslBool( true ), // wantUnoccluded
|
|
196
|
+
) ).toVar();
|
|
197
|
+
|
|
198
|
+
const lumShad = max( dot( dual.shadowed, REC709_LUMINANCE_COEFFICIENTS ), float( 0.0 ) );
|
|
199
|
+
const lumLit = max( dot( dual.unoccluded, REC709_LUMINANCE_COEFFICIENTS ), float( 0.0 ) ).toVar();
|
|
200
|
+
const ratio = clamp( lumShad.div( max( lumLit, float( CATCHER_LUMA_FLOOR ) ) ), 0.0, 1.0 ).toVar();
|
|
201
|
+
// Coverage gate: where no light reaches the plane there is no shadow to catch —
|
|
202
|
+
// force ratio=1 (no darkening) so unlit ground reads as plain background, not spurious black.
|
|
203
|
+
If( lumLit.lessThan( float( CATCHER_COVERAGE_MIN ) ), () => {
|
|
204
|
+
|
|
205
|
+
ratio.assign( 1.0 );
|
|
206
|
+
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
// Adaptive output (matches Arnold's scene_background / Cycles shadow catcher): the catcher
|
|
210
|
+
// respects the current background mode instead of forcing transparency.
|
|
211
|
+
// • Transparent background → emit a matte (rgb=0, alpha=1−ratio); the shadow rides alpha
|
|
212
|
+
// and composites as backplate·ratio over an external plate.
|
|
213
|
+
// • Visible background → composite the shadow into RGB: show the environment this
|
|
214
|
+
// camera ray would otherwise see, darkened by the shadow ratio (alpha=1). The HDRI stays
|
|
215
|
+
// visible; at the horizon ratio→1 so the catcher meets the sky with no seam.
|
|
216
|
+
// Sample the background via the SAME shared helper the miss branch uses, so the
|
|
217
|
+
// catcher seamlessly continues the visible environment (with ground projection on it
|
|
218
|
+
// must bend identically, else a bright horizon seam / mismatched ground appears).
|
|
219
|
+
const catcherEnvDir = groundProjectedEnvDir(
|
|
220
|
+
origin, direction, groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
221
|
+
).toVar();
|
|
222
|
+
const envBehind = sampleEnvironment( {
|
|
223
|
+
tex: envTexture,
|
|
224
|
+
samp: sampler( envTexture ),
|
|
225
|
+
direction: catcherEnvDir,
|
|
226
|
+
environmentMatrix: envMatrix,
|
|
227
|
+
environmentIntensity,
|
|
228
|
+
enableEnvironmentLight,
|
|
229
|
+
} ).xyz.toVar();
|
|
230
|
+
const bgColor = select( showBackground, envBehind.mul( backgroundIntensity ), vec3( 0.0 ) );
|
|
231
|
+
const outRgb = select( transparentBackground, vec3( 0.0 ), bgColor.mul( ratio ) );
|
|
232
|
+
const outAlpha = select( transparentBackground, float( 1.0 ).sub( ratio ), float( 1.0 ) );
|
|
233
|
+
|
|
234
|
+
// The catcher is a real ground surface for the denoiser — write the plane's normal/depth
|
|
235
|
+
// + a neutral albedo (black albedo would break OIDN demodulation) and mark the pixel a
|
|
236
|
+
// valid surface, so OIDN/ASVGF don't smear the caught shadow as a background miss.
|
|
237
|
+
If( sampleIndex.equal( int( 0 ) ), () => {
|
|
238
|
+
|
|
239
|
+
const planeDepth = computeNDCDepth( { worldPos: planePoint, cameraProjectionMatrix, cameraViewMatrix } );
|
|
240
|
+
writeGBuffer( gBufferRW, pixelIndex, planeN, planeDepth, vec3( 1.0 ) );
|
|
241
|
+
writeGBufferSurfaceID( gBufferRW, pixelIndex, uint( CATCHER_SURFACE_ID ), uint( CATCHER_SURFACE_ID ), 0.5, 0.5, uint( 1 ) );
|
|
242
|
+
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
|
|
246
|
+
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
247
|
+
rngBufferRW.element( rayID ).assign( rngState );
|
|
248
|
+
Return();
|
|
249
|
+
|
|
250
|
+
} );
|
|
251
|
+
|
|
252
|
+
} );
|
|
253
|
+
|
|
254
|
+
} );
|
|
255
|
+
|
|
139
256
|
If( hitDist.greaterThan( MISS_DIST ), () => {
|
|
140
257
|
|
|
141
258
|
If( enableEnvironmentLight, () => {
|
|
@@ -143,11 +260,12 @@ export function buildShadeKernel( params ) {
|
|
|
143
260
|
// Ground projection bends the primary ray's background lookup onto a
|
|
144
261
|
// projected sphere+disk so the lower env hemisphere reads as a ground
|
|
145
262
|
// plane. Primary ray only; secondary bounces see the raw envmap as a light.
|
|
263
|
+
// Shared helper (also used by the shadow catcher) keeps the two in lockstep.
|
|
146
264
|
const envDir = direction.toVar();
|
|
147
|
-
If( bounceIndex.equal( 0 )
|
|
265
|
+
If( bounceIndex.equal( 0 ), () => {
|
|
148
266
|
|
|
149
|
-
envDir.assign(
|
|
150
|
-
origin, direction, groundProjectionRadius, groundProjectionHeight,
|
|
267
|
+
envDir.assign( groundProjectedEnvDir(
|
|
268
|
+
origin, direction, groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
151
269
|
) );
|
|
152
270
|
|
|
153
271
|
} );
|
|
@@ -560,10 +678,25 @@ export function buildShadeKernel( params ) {
|
|
|
560
678
|
const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
|
|
561
679
|
If( prevBouncePdf.greaterThan( 0.0 ), () => {
|
|
562
680
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
);
|
|
681
|
+
// MIS partner pdf MUST match the actual NEE sampler: re-walk the Light BVH descent
|
|
682
|
+
// when it is active, else use the flat-CDF pdf. Mismatching them breaks MIS
|
|
683
|
+
// partition-of-unity → a real bias (see calculateLightBVHPdf).
|
|
684
|
+
const lightPdf = float( 0.0 ).toVar();
|
|
685
|
+
If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
|
|
686
|
+
|
|
687
|
+
lightPdf.assign( calculateLightBVHPdf(
|
|
688
|
+
int( hitTriIdx ), hitDist, direction, origin,
|
|
689
|
+
lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer,
|
|
690
|
+
) );
|
|
691
|
+
|
|
692
|
+
} ).Else( () => {
|
|
693
|
+
|
|
694
|
+
lightPdf.assign( calculateEmissiveLightPdf(
|
|
695
|
+
int( hitTriIdx ), hitDist, direction, origin,
|
|
696
|
+
triangleBuffer, materialBuffer, emissiveTotalPower,
|
|
697
|
+
) );
|
|
698
|
+
|
|
699
|
+
} );
|
|
567
700
|
emissiveMISWeight.assign( powerHeuristic( { pdf1: prevBouncePdf, pdf2: lightPdf } ) );
|
|
568
701
|
|
|
569
702
|
} );
|
|
@@ -674,7 +807,7 @@ export function buildShadeKernel( params ) {
|
|
|
674
807
|
|
|
675
808
|
} );
|
|
676
809
|
|
|
677
|
-
const directLight = calculateDirectLightingUnified(
|
|
810
|
+
const directLight = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
678
811
|
hitPoint, N, material, V,
|
|
679
812
|
brdfDir, brdfPdf, brdfValue,
|
|
680
813
|
bounceIndex, rngState,
|
|
@@ -687,7 +820,8 @@ export function buildShadeKernel( params ) {
|
|
|
687
820
|
envCDFTexture,
|
|
688
821
|
envTotalSum, envCompensationDelta, envResolution,
|
|
689
822
|
enableEnvironmentLight,
|
|
690
|
-
|
|
823
|
+
tslBool( false ), // wantUnoccluded: false on real surfaces — dead-codes the unoccluded sum
|
|
824
|
+
) ).shadowed.toVar();
|
|
691
825
|
|
|
692
826
|
const giScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
|
|
693
827
|
// Per-term firefly suppression (megakernel parity: PathTracerCore.js:1164) — wrap the direct-light add
|
package/src/TSL/Struct.js
CHANGED
|
@@ -73,6 +73,14 @@ export const ShadowMaterial = struct( {
|
|
|
73
73
|
albedoTransform: 'mat3',
|
|
74
74
|
} );
|
|
75
75
|
|
|
76
|
+
// Dual direct-lighting result: shadowed (the normal NEE estimate) + unoccluded
|
|
77
|
+
// (same estimator with visibility forced to 1). Used by the shadow catcher to
|
|
78
|
+
// derive a shadow ratio = luma(shadowed) / luma(unoccluded).
|
|
79
|
+
export const DirectLightingDual = struct( {
|
|
80
|
+
shadowed: 'vec3',
|
|
81
|
+
unoccluded: 'vec3',
|
|
82
|
+
} );
|
|
83
|
+
|
|
76
84
|
export const Sphere = struct( {
|
|
77
85
|
position: 'vec3',
|
|
78
86
|
radius: 'float',
|
|
@@ -188,6 +188,9 @@ export class UniformManager {
|
|
|
188
188
|
ub( 'groundProjectionEnabled', DEFAULT_STATE.groundProjectionEnabled );
|
|
189
189
|
u( 'groundProjectionRadius', DEFAULT_STATE.groundProjectionRadius, 'float' );
|
|
190
190
|
u( 'groundProjectionHeight', DEFAULT_STATE.groundProjectionHeight, 'float' );
|
|
191
|
+
u( 'groundProjectionLevel', DEFAULT_STATE.groundProjectionLevel, 'float' );
|
|
192
|
+
ub( 'enableGroundCatcher', DEFAULT_STATE.enableGroundCatcher );
|
|
193
|
+
u( 'groundCatcherHeight', DEFAULT_STATE.groundCatcherHeight, 'float' );
|
|
191
194
|
|
|
192
195
|
// Sun parameters
|
|
193
196
|
u( 'sunDirection', new Vector3( 0, 1, 0 ), 'vec3' );
|
|
@@ -242,6 +245,10 @@ export class UniformManager {
|
|
|
242
245
|
// Offset (in vec4 elements) within the packed light buffer where emissive
|
|
243
246
|
// triangle data starts. Equals lightBVHNodeCount * LBVH_STRIDE; computed on upload.
|
|
244
247
|
u( 'emissiveVec4Offset', 0, 'int' );
|
|
248
|
+
// Offset (in vec4 elements) within the packed light buffer where the per-triangle
|
|
249
|
+
// bit-trail map starts (4 trails packed per vec4); computed on upload. Used by the
|
|
250
|
+
// bounce-hit MIS path to re-walk the Light BVH descent pdf.
|
|
251
|
+
u( 'reverseMapVec4Offset', 0, 'int' );
|
|
245
252
|
|
|
246
253
|
// Render mode
|
|
247
254
|
u( 'renderMode', DEFAULT_STATE.renderMode, 'int' );
|