rayzee 9.2.1 → 9.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/dist/rayzee.es.js +2773 -2759
- package/dist/rayzee.umd.js +44 -44
- package/package.json +1 -1
- package/src/Processor/QueueManager.js +1 -0
- package/src/TSL/BVHTraversal.js +13 -12
- package/src/TSL/Debugger.js +3 -0
- package/src/TSL/ExtendKernel.js +6 -2
- package/src/TSL/MaterialEvaluation.js +11 -6
- package/src/TSL/PathTracerCore.js +25 -4
- package/src/TSL/ShadeKernel.js +34 -8
package/package.json
CHANGED
|
@@ -51,6 +51,7 @@ export const RAY_FLAG = {
|
|
|
51
51
|
AUX_LOCKED: 1 << 17, // bit 17: OIDN aux (normal/albedo) locked onto first non-specular hit (megakernel auxLocked)
|
|
52
52
|
REDIRECTED: 1 << 18, // bit 18: ray has been redirected (refraction/reflection/SSS/opaque scatter) since the camera, so env it reaches is transported light (sharp), NOT the direct backdrop. NOT set by pure alpha/transparent passthrough (direction unchanged) → env through cutout holes is still the backdrop (blur/intensity/show/color/ground-projection). Set via bitOr only (positive mask) so it never disturbs ACTIVE/bounce bits.
|
|
53
53
|
SUN_NEE: 1 << 19, // bit 19: the last opaque scatter's sun NEE could have drawn this direction (MIS partner for a sun hit on miss)
|
|
54
|
+
UNDER_SURFACE: 1 << 20, // bit 20: the last opaque scatter left below its own facet (shading normal allowed it), so the ray culls back faces to leave that shell
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
export class QueueManager {
|
package/src/TSL/BVHTraversal.js
CHANGED
|
@@ -212,8 +212,14 @@ const fastRayAABBDst = wgslFn( `
|
|
|
212
212
|
// ================================================================================
|
|
213
213
|
// MAIN BVH TRAVERSAL
|
|
214
214
|
// ================================================================================
|
|
215
|
-
|
|
216
|
-
//
|
|
215
|
+
|
|
216
|
+
// Side from a triangle's flags word (row 4 .z): 0 front, 1 back, 2 double.
|
|
217
|
+
export const triangleSide = ( flags ) => int( flags.shiftRight( uint( TRI_SIDE_SHIFT ) ).bitAnd( uint( 3 ) ) );
|
|
218
|
+
|
|
219
|
+
// Whether a face of this side is seen by a ray; rayDotNormal < 0 is a front hit.
|
|
220
|
+
export const sideAccepts = ( side, rayDotNormal ) => side.equal( int( 2 ) )
|
|
221
|
+
.or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
|
|
222
|
+
.or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
|
|
217
223
|
|
|
218
224
|
// Factory: the boxTests/triTests debug counters are compiled OUT of the hot closest-hit
|
|
219
225
|
// path (extend/normalDepth use traverseBVH = trackStats false) — only the debug viz reads
|
|
@@ -223,11 +229,11 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
223
229
|
bvhBuffer,
|
|
224
230
|
triangleBuffer,
|
|
225
231
|
insideMedium, // optional: when true (ray inside a medium), bypass front/back culling
|
|
232
|
+
cullBackFaces, // optional, default true: false hits either side, as shadow rays do
|
|
226
233
|
] ) => {
|
|
227
234
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
const inMedium = insideMedium ?? tslBool( false );
|
|
235
|
+
// Culling is for what the camera sees through; a bounce culling it leaks light through hollow models.
|
|
236
|
+
const eitherSide = ( insideMedium ?? tslBool( false ) ).or( ( cullBackFaces ?? tslBool( true ) ).not() ).toVar();
|
|
231
237
|
|
|
232
238
|
// Folded leaves cost up to 4 % of traversal, so only a BVH built with them gets that code.
|
|
233
239
|
const folded = bvhBuffer.value?.foldedLeaves === true;
|
|
@@ -308,8 +314,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
308
314
|
const nA = unpackTriangleNormal( recA.w );
|
|
309
315
|
const nB = unpackTriangleNormal( recB.w );
|
|
310
316
|
const nC = unpackTriangleNormal( recC.w );
|
|
311
|
-
const
|
|
312
|
-
const side = int( flags.shiftRight( uint( TRI_SIDE_SHIFT ) ).bitAnd( uint( 3 ) ) ).toVar();
|
|
317
|
+
const side = triangleSide( triangleRow( triangleBuffer, triIndex, 4 ).z ).toVar();
|
|
313
318
|
|
|
314
319
|
// Interpolate normal for the side-culling dot product (kept local,
|
|
315
320
|
// not stored on closestHit — re-derived post-loop from closestTriIdx).
|
|
@@ -319,11 +324,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
319
324
|
);
|
|
320
325
|
|
|
321
326
|
// Side culling (inline; per-mesh visibility is at the BLAS-pointer level).
|
|
322
|
-
|
|
323
|
-
const sidePass = inMedium.or( side.equal( int( 2 ) ) )
|
|
324
|
-
.or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
|
|
325
|
-
.or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
|
|
326
|
-
If( sidePass, () => {
|
|
327
|
+
If( eitherSide.or( sideAccepts( side, rayDotNormal ) ), () => {
|
|
327
328
|
|
|
328
329
|
closestHit.didHit.assign( true );
|
|
329
330
|
closestHit.dst.assign( t );
|
package/src/TSL/Debugger.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
If,
|
|
19
19
|
select,
|
|
20
20
|
sampler,
|
|
21
|
+
bool as tslBool,
|
|
21
22
|
} from 'three/tsl';
|
|
22
23
|
|
|
23
24
|
import { Ray, HitInfo, RayTracingMaterial, MaterialSamples } from './Struct.js';
|
|
@@ -343,6 +344,8 @@ export const TraceDebugMode = Fn( ( [
|
|
|
343
344
|
bounceRay,
|
|
344
345
|
bvhBuffer,
|
|
345
346
|
triangleBuffer,
|
|
347
|
+
tslBool( false ),
|
|
348
|
+
tslBool( false ),
|
|
346
349
|
).toVar() );
|
|
347
350
|
|
|
348
351
|
const incoming = vec3( 0.0 ).toVar();
|
package/src/TSL/ExtendKernel.js
CHANGED
|
@@ -49,7 +49,8 @@ export function buildExtendKernel( params ) {
|
|
|
49
49
|
|
|
50
50
|
// Parity with Shade's guard. Free — the flags share DIR_FLAGS.w with the direction read below.
|
|
51
51
|
// Only culls work on the pinned-dispatch path, whose identity list still holds dead rays.
|
|
52
|
-
|
|
52
|
+
const flags = readRayBounceFlags( rayBufferRO, rayID ).toVar();
|
|
53
|
+
If( flags.bitAnd( uint( RAY_FLAG.ACTIVE ) ).equal( uint( 0 ) ), () => {
|
|
53
54
|
|
|
54
55
|
Return();
|
|
55
56
|
|
|
@@ -62,8 +63,11 @@ export function buildExtendKernel( params ) {
|
|
|
62
63
|
|
|
63
64
|
// insideMedium bypasses front/back culling so the ray can hit a glass/SSS back-facing boundary.
|
|
64
65
|
const insideMedium = readMediumStack( rayBufferRO, rayID ).stackDepth.greaterThan( uint( 0 ) );
|
|
66
|
+
// Only the camera's own view culls back faces, and a bounce that dipped under its surface.
|
|
67
|
+
const cull = flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).equal( uint( 0 ) )
|
|
68
|
+
.or( flags.bitAnd( uint( RAY_FLAG.UNDER_SURFACE ) ).notEqual( uint( 0 ) ) );
|
|
65
69
|
const hitInfo = HitInfo.wrap( traverseBVH(
|
|
66
|
-
ray, bvhBuffer, triangleBuffer, insideMedium,
|
|
70
|
+
ray, bvhBuffer, triangleBuffer, insideMedium, cull,
|
|
67
71
|
) ).toVar();
|
|
68
72
|
|
|
69
73
|
const facet = hitFacet( {
|
|
@@ -24,7 +24,9 @@ import { evalIridescence } from './MaterialProperties.js';
|
|
|
24
24
|
// Body of evaluateMaterialResponse taking precomputed dot products. Callers
|
|
25
25
|
// that also need calculateMaterialPDF for the same (V, L, N) should share dots
|
|
26
26
|
// to save one computeDotProducts call.
|
|
27
|
-
|
|
27
|
+
// Roughness 0 marks an exact mirror (ShadeKernel): its delta lobe is left out of this BSDF, and
|
|
28
|
+
// deltaOnly returns that lobe's reflectance instead, under the same sheen and coat attenuation.
|
|
29
|
+
const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] ) => {
|
|
28
30
|
|
|
29
31
|
const result = vec3( 0.0 ).toVar();
|
|
30
32
|
|
|
@@ -92,7 +94,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
92
94
|
const Va = VisibilityGGXAniso( a.x, a.y, dots.ToV, dots.BoV, dots.ToL, dots.BoL, dots.NoV, dots.NoL );
|
|
93
95
|
specularSS.assign( F.mul( Da.mul( Va ) ) );
|
|
94
96
|
|
|
95
|
-
} ).
|
|
97
|
+
} ).ElseIf( material.roughness.greaterThan( 0.0 ), () => {
|
|
96
98
|
|
|
97
99
|
const D = DistributionGGX( dots.NoH, material.roughness );
|
|
98
100
|
const Vis = VisibilityGGXSmithCorrelated( dots.NoV, dots.NoL, material.roughness );
|
|
@@ -105,7 +107,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
105
107
|
bf.f0, bf.f90, bf.eta, bf.F0m, material.metalness, iridF, material.iridescence, F0,
|
|
106
108
|
dots.NoV, material.roughness,
|
|
107
109
|
) );
|
|
108
|
-
const specular = specularSS.mul( dfg.compensation );
|
|
110
|
+
const specular = ( deltaOnly ? F : specularSS ).mul( dfg.compensation );
|
|
109
111
|
|
|
110
112
|
// Diffuse energy budget from hemisphere-integrated specular albedo (includes multiscatter)
|
|
111
113
|
// Transmission removes energy from diffuse just as metalness does — KHR_materials_transmission
|
|
@@ -113,7 +115,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
113
115
|
const kD = vec3( 1.0 ).sub( dfg.E_total )
|
|
114
116
|
.mul( float( 1.0 ).sub( material.metalness ) )
|
|
115
117
|
.mul( float( 1.0 ).sub( material.transmission ) );
|
|
116
|
-
const diffuse = kD.mul( materialColor ).mul( PI_INV );
|
|
118
|
+
const diffuse = deltaOnly ? vec3( 0.0 ) : kD.mul( materialColor ).mul( PI_INV );
|
|
117
119
|
|
|
118
120
|
const baseLayer = diffuse.add( specular ).toVar();
|
|
119
121
|
|
|
@@ -131,7 +133,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
131
133
|
const sheenReflectance = clamp( material.sheenColor.mul( material.sheen ).mul( sheenE ), vec3( 0.0 ), vec3( 1.0 ) );
|
|
132
134
|
const sheenAttenuation = vec3( 1.0 ).sub( sheenReflectance );
|
|
133
135
|
|
|
134
|
-
result.assign( baseLayer.mul( sheenAttenuation ).add( sheenTerm ) );
|
|
136
|
+
result.assign( deltaOnly ? baseLayer.mul( sheenAttenuation ) : baseLayer.mul( sheenAttenuation ).add( sheenTerm ) );
|
|
135
137
|
|
|
136
138
|
} ).Else( () => {
|
|
137
139
|
|
|
@@ -161,7 +163,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
161
163
|
|
|
162
164
|
const ccAttenuation = vec3( 1.0 ).sub( ccDfg.E_total.mul( material.clearcoat ) );
|
|
163
165
|
|
|
164
|
-
result.assign( result.mul( ccAttenuation ).add( ccLobe.mul( material.clearcoat ) ) );
|
|
166
|
+
result.assign( deltaOnly ? result.mul( ccAttenuation ) : result.mul( ccAttenuation ).add( ccLobe.mul( material.clearcoat ) ) );
|
|
165
167
|
|
|
166
168
|
} );
|
|
167
169
|
|
|
@@ -171,6 +173,9 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
171
173
|
|
|
172
174
|
} );
|
|
173
175
|
|
|
176
|
+
export const evaluateMaterialResponseFromDots = /*@__PURE__*/ makeEvaluateMaterialResponse( false );
|
|
177
|
+
export const evaluateSpecularDeltaFromDots = /*@__PURE__*/ makeEvaluateMaterialResponse( true );
|
|
178
|
+
|
|
174
179
|
// Wrapper that computes dot products internally. Use this when you don't already
|
|
175
180
|
// have dots; otherwise prefer evaluateMaterialResponseFromDots to share the work.
|
|
176
181
|
export const evaluateMaterialResponse = Fn( ( [ V, L, N, material ] ) => {
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
calculateBRDFWeights,
|
|
47
47
|
sheenSamplingRoughness,
|
|
48
48
|
} from './MaterialProperties.js';
|
|
49
|
-
import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
|
|
49
|
+
import { evaluateMaterialResponseFromDots, evaluateSpecularDeltaFromDots } from './MaterialEvaluation.js';
|
|
50
50
|
|
|
51
51
|
import {
|
|
52
52
|
ImportanceSampleCosine,
|
|
@@ -59,6 +59,9 @@ import {
|
|
|
59
59
|
// BRDF Direction Sampling
|
|
60
60
|
// =============================================================================
|
|
61
61
|
|
|
62
|
+
// Stand-in density for a delta (exact mirror) sample; squared in the power heuristic, so it stays f32-safe.
|
|
63
|
+
export const DELTA_PDF = 1e6;
|
|
64
|
+
|
|
62
65
|
export const generateSampledDirection = Fn( ( [
|
|
63
66
|
V, N, material, xi, lobeXi, rngState,
|
|
64
67
|
pixelCoord, resolution, frame, dimBase,
|
|
@@ -75,6 +78,7 @@ export const generateSampledDirection = Fn( ( [
|
|
|
75
78
|
const resultPdf = float( 0.0 ).toVar();
|
|
76
79
|
const resultIsTransmission = tslBool( false ).toVar();
|
|
77
80
|
const resultColorWeight = vec3( 1.0 ).toVar();
|
|
81
|
+
const resultIsDelta = tslBool( false ).toVar();
|
|
78
82
|
|
|
79
83
|
// Compute BRDF weights
|
|
80
84
|
const weights = cachedBrdfWeights.toVar();
|
|
@@ -134,7 +138,7 @@ export const generateSampledDirection = Fn( ( [
|
|
|
134
138
|
|
|
135
139
|
resultDirection.assign( reflect( V.negate(), H ) );
|
|
136
140
|
|
|
137
|
-
} ).
|
|
141
|
+
} ).ElseIf( material.roughness.greaterThan( 0.0 ), () => {
|
|
138
142
|
|
|
139
143
|
const TBN = constructTBN( { N } );
|
|
140
144
|
const localV = TBN.transpose().mul( V );
|
|
@@ -145,6 +149,11 @@ export const generateSampledDirection = Fn( ( [
|
|
|
145
149
|
|
|
146
150
|
resultDirection.assign( reflect( V.negate(), H ) );
|
|
147
151
|
|
|
152
|
+
} ).Else( () => {
|
|
153
|
+
|
|
154
|
+
resultDirection.assign( reflect( V.negate(), N ) );
|
|
155
|
+
resultIsDelta.assign( true );
|
|
156
|
+
|
|
148
157
|
} );
|
|
149
158
|
|
|
150
159
|
} ).ElseIf( rand.lessThan( cumulativeSheen ), () => {
|
|
@@ -189,8 +198,20 @@ export const generateSampledDirection = Fn( ( [
|
|
|
189
198
|
If( resultIsTransmission.not(), () => {
|
|
190
199
|
|
|
191
200
|
const dotsOut = DotProducts.wrap( computeDotProductsAniso( N, V, resultDirection, material ) );
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
If( resultIsDelta, () => {
|
|
202
|
+
|
|
203
|
+
// A delta lobe has no density to share, so report one large enough that every MIS weight
|
|
204
|
+
// against it is 1, and scale the value so value·NoL/pdf is reflectance / selection chance.
|
|
205
|
+
resultPdf.assign( DELTA_PDF );
|
|
206
|
+
resultValue.assign( evaluateSpecularDeltaFromDots( material, dotsOut )
|
|
207
|
+
.mul( DELTA_PDF ).div( max( weights.specular.mul( dotsOut.NoL ), 1e-6 ) ) );
|
|
208
|
+
|
|
209
|
+
} ).Else( () => {
|
|
210
|
+
|
|
211
|
+
resultValue.assign( evaluateMaterialResponseFromDots( material, dotsOut ) );
|
|
212
|
+
resultPdf.assign( calculateBSDFSamplingPDF( material, weights, dotsOut ) );
|
|
213
|
+
|
|
214
|
+
} );
|
|
194
215
|
|
|
195
216
|
} );
|
|
196
217
|
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from 'three/tsl';
|
|
16
16
|
|
|
17
17
|
import { sampleEnvironment, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
|
|
18
|
-
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial, offsetRayOrigin, SHADOW_END } from './Common.js';
|
|
18
|
+
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial, offsetRayOrigin, SHADOW_END, triangleRow, MIN_ROUGHNESS } from './Common.js';
|
|
19
19
|
import { cosineWeightedSample } from './MaterialSampling.js';
|
|
20
20
|
import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV, triangleUVTangent } from './TextureSampling.js';
|
|
21
21
|
import { evaluateMaterialResponse } from './MaterialEvaluation.js';
|
|
@@ -23,7 +23,7 @@ import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSa
|
|
|
23
23
|
import { traceShadowRay } from './LightsDirect.js';
|
|
24
24
|
import { shadowTerminatorOrigin } from './ShadowTerminator.js';
|
|
25
25
|
import { unpackHitFacet } from './HitFacet.js';
|
|
26
|
-
import { traverseBVHShadow } from './BVHTraversal.js';
|
|
26
|
+
import { traverseBVHShadow, triangleSide, sideAccepts } from './BVHTraversal.js';
|
|
27
27
|
import { handleMaterialTransparency, MaterialInteractionResult } from './MaterialTransmission.js';
|
|
28
28
|
import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficients, CollisionSample, MediumCoeffs } from './Subsurface.js';
|
|
29
29
|
import { calculateIndirectLighting } from './LightsIndirect.js';
|
|
@@ -637,7 +637,7 @@ export function buildShadeKernel( params ) {
|
|
|
637
637
|
|
|
638
638
|
// free-bounce continuation: ray stays in the same medium, so medium stack + coeffs persist
|
|
639
639
|
// SSS scatter changes direction → no longer the direct backdrop view.
|
|
640
|
-
flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
|
|
640
|
+
flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.UNDER_SURFACE ) ) ).bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
|
|
641
641
|
writeRayOriginMeta( rayBufferRW, rayID, scatterPoint, cameraDepth, sssSteps, transparentCount );
|
|
642
642
|
writeRayDirFlags( rayBufferRW, rayID, newDir, flags );
|
|
643
643
|
// Free bounce: preserve prevBouncePdf (megakernel leaves it untouched across SSS scatter,
|
|
@@ -705,7 +705,7 @@ export function buildShadeKernel( params ) {
|
|
|
705
705
|
// BRDF functions read material.color/metalness/roughness, so apply samples here
|
|
706
706
|
material.color.assign( matSamples.albedo );
|
|
707
707
|
material.metalness.assign( matSamples.metalness.clamp( 0.0, 1.0 ) );
|
|
708
|
-
material.roughness.assign( matSamples.roughness.clamp(
|
|
708
|
+
material.roughness.assign( matSamples.roughness.clamp( MIN_ROUGHNESS, 1.0 ) );
|
|
709
709
|
material.sheenRoughness.assign( material.sheenRoughness.clamp( 0.05, 1.0 ) ); // megakernel parity (PathTracerCore.js:1060): sample/PDF mismatch at sheenRoughness~0
|
|
710
710
|
|
|
711
711
|
// Anisotropy + extension maps carry no per-slot transform of their own, so reuse the material's
|
|
@@ -737,6 +737,15 @@ export function buildShadeKernel( params ) {
|
|
|
737
737
|
material.specularIntensity.assign( extMaps.specularIntensity );
|
|
738
738
|
material.specularColor.assign( extMaps.specularColor );
|
|
739
739
|
|
|
740
|
+
// Below the floor the GGX peak is past f32, so a plain reflector reflects exactly: roughness 0
|
|
741
|
+
// is the delta-lobe marker the sampler and evaluator read. The other lobes keep the floor.
|
|
742
|
+
If( rawRough.lessThan( MIN_ROUGHNESS ).and( material.anisotropy.equal( 0.0 ) ).and( material.clearcoat.equal( 0.0 ) )
|
|
743
|
+
.and( material.transmission.equal( 0.0 ) ).and( material.subsurface.equal( 0.0 ) ), () => {
|
|
744
|
+
|
|
745
|
+
material.roughness.assign( 0.0 );
|
|
746
|
+
|
|
747
|
+
} );
|
|
748
|
+
|
|
740
749
|
const albedo = matSamples.albedo.toVar();
|
|
741
750
|
If(
|
|
742
751
|
material.displacementMapIndex.greaterThanEqual( int( 0 ) )
|
|
@@ -987,7 +996,8 @@ export function buildShadeKernel( params ) {
|
|
|
987
996
|
|
|
988
997
|
// Off the side of the facet the new ray leaves on: reflection stays, transmission and alpha skip cross.
|
|
989
998
|
const Ng = unpackHitFacet( readHitFacet( hitBufferRW, rayID ) ).faceN;
|
|
990
|
-
const
|
|
999
|
+
const crossesFacet = dot( Ng, interaction.direction ).lessThan( 0.0 ).toVar();
|
|
1000
|
+
const newOrigin = offsetRayOrigin( hitPoint, select( crossesFacet, Ng.negate(), Ng ) );
|
|
991
1001
|
|
|
992
1002
|
// SSS = free bounce (depth unchanged); transmission advances camera-bounce depth.
|
|
993
1003
|
// Transmissive / alpha-skip / SSS-boundary are all FREE bounces — they do NOT advance camera depth (megakernel parity, gap #4). cameraDepth advances only on opaque scatter (below).
|
|
@@ -996,7 +1006,9 @@ export function buildShadeKernel( params ) {
|
|
|
996
1006
|
// backdrop while env through glass becomes sharp redirected light.
|
|
997
1007
|
If( interaction.isAlphaSkip.not(), () => {
|
|
998
1008
|
|
|
999
|
-
|
|
1009
|
+
const underSurface = interaction.didReflect.and( crossesFacet );
|
|
1010
|
+
flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.UNDER_SURFACE ) ) ).bitOr( uint( RAY_FLAG.REDIRECTED ) )
|
|
1011
|
+
.bitOr( select( underSurface, uint( RAY_FLAG.UNDER_SURFACE ), uint( 0 ) ) ) );
|
|
1000
1012
|
|
|
1001
1013
|
} ).Else( () => {
|
|
1002
1014
|
|
|
@@ -1044,6 +1056,16 @@ export function buildShadeKernel( params ) {
|
|
|
1044
1056
|
flags.assign( flags.bitOr( uint( RAY_FLAG.HAS_HIT_OPAQUE | RAY_FLAG.REDIRECTED ) ) );
|
|
1045
1057
|
|
|
1046
1058
|
const emissive = matSamples.emissive.toVar();
|
|
1059
|
+
// NEE samples only a single-sided emitter's lit side, so from behind (bounce rays) it is a dark occluder.
|
|
1060
|
+
If( isBackdropView.not().and( length( emissive ).greaterThan( 0.0 ) ), () => {
|
|
1061
|
+
|
|
1062
|
+
If( sideAccepts( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ), dot( direction, hitNormal ) ).not(), () => {
|
|
1063
|
+
|
|
1064
|
+
emissive.assign( vec3( 0.0 ) );
|
|
1065
|
+
|
|
1066
|
+
} );
|
|
1067
|
+
|
|
1068
|
+
} );
|
|
1047
1069
|
If( length( emissive ).greaterThan( 0.0 ), () => {
|
|
1048
1070
|
|
|
1049
1071
|
// Key on backdrop-view (not bounceIndex>0) so an emitter seen DIRECTLY through an alpha-cutout hole
|
|
@@ -1417,11 +1439,15 @@ export function buildShadeKernel( params ) {
|
|
|
1417
1439
|
|
|
1418
1440
|
} );
|
|
1419
1441
|
|
|
1420
|
-
const
|
|
1442
|
+
const underFacet = dot( facetN, bounceDir ).lessThan( 0.0 ).toVar();
|
|
1443
|
+
const newOrigin = offsetRayOrigin( hitPoint, select( underFacet, facetN.negate(), facetN ) );
|
|
1421
1444
|
|
|
1422
1445
|
// Whether this vertex's sun NEE could have drawn bounceDir, so a sun hit at the miss knows its MIS partner.
|
|
1423
1446
|
const sunNEE = brdfIsTransmission.not().and( dot( N, bounceDir ).greaterThan( 0.0 ) );
|
|
1424
|
-
|
|
1447
|
+
const underSurface = brdfIsTransmission.not().and( underFacet );
|
|
1448
|
+
flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.SUN_NEE | RAY_FLAG.UNDER_SURFACE ) ) )
|
|
1449
|
+
.bitOr( select( sunNEE, uint( RAY_FLAG.SUN_NEE ), uint( 0 ) ) )
|
|
1450
|
+
.bitOr( select( underSurface, uint( RAY_FLAG.UNDER_SURFACE ), uint( 0 ) ) ) );
|
|
1425
1451
|
|
|
1426
1452
|
// Opaque scatter: the only bounce that advances camera depth.
|
|
1427
1453
|
writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth.add( 1 ), sssSteps, transparentCount );
|