rayzee 9.2.0 → 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.
@@ -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
- } ).Else( () => {
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
- resultValue.assign( evaluateMaterialResponseFromDots( material, dotsOut ) );
193
- resultPdf.assign( calculateBSDFSamplingPDF( material, weights, dotsOut ) );
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
 
@@ -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( 0.05, 1.0 ) );
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 newOrigin = offsetRayOrigin( hitPoint, select( dot( Ng, interaction.direction ).lessThan( 0.0 ), Ng.negate(), Ng ) );
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
- flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
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 newOrigin = offsetRayOrigin( hitPoint, select( dot( facetN, bounceDir ).lessThan( 0.0 ), facetN.negate(), facetN ) );
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
- flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ) ).bitOr( select( sunNEE, uint( RAY_FLAG.SUN_NEE ), uint( 0 ) ) ) );
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 );