rayzee 7.20.2 → 7.20.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.20.2",
3
+ "version": "7.20.3",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -97,7 +97,7 @@ export const ENGINE_DEFAULTS = {
97
97
  pixelFreezeThreshold: 0.02, // per-pixel noise below which a pixel becomes a freeze candidate
98
98
  pixelFreezeStability: 8, // consecutive candidate frames before a pixel freezes
99
99
 
100
- samplingTechnique: 3,
100
+ samplingTechnique: 2,
101
101
  enableEmissiveTriangleSampling: false,
102
102
  emissiveBoost: 1.0,
103
103
 
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Fn,
3
3
  vec3,
4
- float,
4
+ int,
5
5
  dot,
6
6
  normalize,
7
7
  reflect,
@@ -11,12 +11,14 @@ import {
11
11
 
12
12
  import { struct } from './patches.js';
13
13
 
14
- import { AnisoFrame, DotProducts } from './Struct.js';
15
- import { PI, MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso, anisoTangentFrame } from './Common.js';
16
- import { DistributionGGX, computeAnisoAlphas, calculateVNDFPDFAniso } from './MaterialProperties.js';
17
- import { ImportanceSampleGGX, ImportanceSampleCosine, sampleGGXVNDFAniso } from './MaterialSampling.js';
18
- import { evaluateLayeredBRDF } from './MaterialEvaluation.js';
19
- import { RandomValue } from './Random.js';
14
+ import { AnisoFrame, DotProducts,
15
+ BRDFWeights,
16
+ } from './Struct.js';
17
+ import { MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso, anisoTangentFrame, constructTBN } from './Common.js';
18
+ import { computeAnisoAlphas, calculateBRDFWeightsFromMaterial, calculateBSDFSamplingPDF } from './MaterialProperties.js';
19
+ import { ImportanceSampleCosine, sampleGGXVNDF, sampleGGXVNDFAniso } from './MaterialSampling.js';
20
+ import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
21
+ import { getRandomSample1D } from './Random.js';
20
22
 
21
23
  export const ClearcoatResult = struct( {
22
24
  brdf: 'vec3',
@@ -29,6 +31,7 @@ export const ClearcoatResult = struct( {
29
31
  // L (light direction) is returned via the out pattern as a separate return
30
32
  export const sampleClearcoat = Fn( ( [
31
33
  ray, hitInfo, material, randomSample, rngState,
34
+ pixelCoord, resolution, frame, dimBase,
32
35
  ] ) => {
33
36
 
34
37
  const N = hitInfo.normal;
@@ -38,27 +41,26 @@ export const sampleClearcoat = Fn( ( [
38
41
  const clearcoatRoughness = max( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS );
39
42
  const baseRoughness = max( material.roughness, MIN_CLEARCOAT_ROUGHNESS );
40
43
 
41
- // Calculate sampling weights based on material properties
42
- const specularWeight = float( 1.0 ).sub( baseRoughness ).mul( float( 0.5 ).add( float( 0.5 ).mul( material.metalness ) ) ).toVar();
43
- const clearcoatWeight = material.clearcoat.mul( float( 1.0 ).sub( clearcoatRoughness ) ).toVar();
44
- const diffuseWeight = float( 1.0 ).sub( specularWeight ).mul( float( 1.0 ).sub( material.metalness ) ).toVar();
45
-
46
- // Normalize weights
47
- const total = specularWeight.add( clearcoatWeight ).add( diffuseWeight );
48
- specularWeight.divAssign( total );
49
- clearcoatWeight.divAssign( total );
50
- diffuseWeight.divAssign( total );
44
+ // The shared selection probabilities, NOT a fourth private scheme. This function used to
45
+ // derive its own specular/clearcoat/diffuse split, so a clearcoat material was sampled with
46
+ // one set of probabilities while every MIS site evaluated the density of another — the white
47
+ // furnace read the resulting mismatch as an energy error that flipped sign once the other
48
+ // sites were unified.
49
+ const weights = BRDFWeights.wrap( calculateBRDFWeightsFromMaterial( material ) ).toVar();
50
+ const clearcoatWeight = weights.clearcoat.toVar();
51
+ const specularWeight = weights.specular.toVar();
51
52
 
52
53
  // Choose which layer to sample
53
- const rand = RandomValue( rngState );
54
+ const rand = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 13 ) ), rngState, resolution, frame );
54
55
 
55
56
  const L = vec3( 0.0 ).toVar();
56
57
  const H = vec3( 0.0 ).toVar();
57
58
 
58
59
  If( rand.lessThan( clearcoatWeight ), () => {
59
60
 
60
- // Sample clearcoat layer
61
- H.assign( ImportanceSampleGGX( { N, roughness: clearcoatRoughness, Xi: randomSample } ) );
61
+ // Sample clearcoat layer (VNDF — see the base-specular note below)
62
+ const ccTBN = constructTBN( { N } );
63
+ H.assign( ccTBN.mul( sampleGGXVNDF( { V: ccTBN.transpose().mul( V ), roughness: clearcoatRoughness, Xi: randomSample } ) ) );
62
64
  L.assign( reflect( V.negate(), H ) );
63
65
 
64
66
  } ).ElseIf( rand.lessThan( clearcoatWeight.add( specularWeight ) ), () => {
@@ -74,7 +76,9 @@ export const sampleClearcoat = Fn( ( [
74
76
 
75
77
  } ).Else( () => {
76
78
 
77
- H.assign( ImportanceSampleGGX( { N, roughness: baseRoughness, Xi: randomSample } ) );
79
+ // VNDF to match the density the MIS sites evaluate for this lobe.
80
+ const tbn = constructTBN( { N } );
81
+ H.assign( tbn.mul( sampleGGXVNDF( { V: tbn.transpose().mul( V ), roughness: baseRoughness, Xi: randomSample } ) ) );
78
82
 
79
83
  } );
80
84
  L.assign( reflect( V.negate(), H ) );
@@ -90,27 +94,12 @@ export const sampleClearcoat = Fn( ( [
90
94
  // Calculate dot products (aniso-aware: also projects onto the anisotropy frame)
91
95
  const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
92
96
 
93
- // Calculate individual PDFs. Clearcoat layer is always isotropic; the base specular
94
- // matches its sampler — VNDF-aniso when anisotropy>0, else the half-vector GGX pdf.
95
- const clearcoatPDF = DistributionGGX( dots.NoH, clearcoatRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( clearcoatWeight );
96
- const specularPDF = float( 0.0 ).toVar();
97
- If( material.anisotropy.greaterThan( 0.0 ), () => {
98
-
99
- const a = computeAnisoAlphas( material.roughness, material.anisotropy );
100
- specularPDF.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ).mul( specularWeight ) );
101
-
102
- } ).Else( () => {
103
-
104
- specularPDF.assign( DistributionGGX( dots.NoH, baseRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( specularWeight ) );
105
-
106
- } );
107
- const diffusePDF = dots.NoL.div( PI ).mul( diffuseWeight );
108
-
109
- // Combined PDF using MIS
110
- const pdf = max( clearcoatPDF.add( specularPDF ).add( diffusePDF ), 0.001 );
97
+ // One density for every MIS site — see calculateBSDFSamplingPDF.
98
+ const pdf = max( calculateBSDFSamplingPDF( material, weights, dots ), 0.001 );
111
99
 
112
100
  // Evaluate complete BRDF
113
- const brdf = evaluateLayeredBRDF( dots, material );
101
+ // The same BRDF every other site evaluates — see the clearcoat note in MaterialEvaluation.
102
+ const brdf = evaluateMaterialResponseFromDots( material, dots );
114
103
 
115
104
  // Return brdf, L direction, and pdf packed together
116
105
  // Caller needs L and pdf - return as struct-like output
@@ -3,7 +3,6 @@
3
3
 
4
4
  import {
5
5
  Fn,
6
- vec2,
7
6
  vec3,
8
7
  float,
9
8
  int,
@@ -27,7 +26,7 @@ import {
27
26
 
28
27
  import { struct } from './patches.js';
29
28
  import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT, computeDotProductsAniso } from './Common.js';
30
- import { RandomValue } from './Random.js';
29
+ import { getRandomSample1D, getRandomSample2D } from './Random.js';
31
30
  import { calculateMaterialPDFFromDots } from './LightsSampling.js';
32
31
  import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
33
32
  import { DotProducts } from './Struct.js';
@@ -390,6 +389,7 @@ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissive
390
389
  export const sampleEmissiveTriangle = Fn( ( [
391
390
  hitPoint, surfaceNormal,
392
391
  rngState,
392
+ pixelCoord, resolution, frame, dimBase,
393
393
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
394
394
  triangleBuffer,
395
395
  ] ) => {
@@ -410,7 +410,7 @@ export const sampleEmissiveTriangle = Fn( ( [
410
410
  If( emissiveTriangleCount.greaterThan( int( 0 ) ), () => {
411
411
 
412
412
  // CDF importance-weighted triangle selection (brighter triangles sampled more)
413
- const randEmissive = RandomValue( rngState );
413
+ const randEmissive = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame );
414
414
  const emissiveIndex = binarySearchCDF( emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, randEmissive ).toVar();
415
415
 
416
416
  // Fetch emissive triangle data from packed light buffer (2 vec4s per entry)
@@ -427,10 +427,7 @@ export const sampleEmissiveTriangle = Fn( ( [
427
427
  // Fetch triangle geometry
428
428
  const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer ) );
429
429
 
430
- // Generate random numbers for sampling
431
- const xi_r1 = RandomValue( rngState ).toVar();
432
- const xi_r2 = RandomValue( rngState ).toVar();
433
- const xi = vec2( xi_r1, xi_r2 );
430
+ const xi = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame ).toVar();
434
431
 
435
432
  const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
436
433
 
@@ -532,6 +529,7 @@ export const sampleEmissiveTriangle = Fn( ( [
532
529
  export const calculateEmissiveTriangleContributionDebug = Fn( ( [
533
530
  hitPoint, normal, viewDir, material,
534
531
  bounceIndex, rngState,
532
+ pixelCoord, resolution, frame, dimBase,
535
533
  emissiveBoost,
536
534
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
537
535
  triangleBuffer,
@@ -555,6 +553,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
555
553
  // Sample emissive triangle (CDF importance-weighted)
556
554
  const emissiveSample = EmissiveSample.wrap( sampleEmissiveTriangle(
557
555
  hitPoint, normal, rngState,
556
+ pixelCoord, resolution, frame, dimBase,
558
557
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
559
558
  triangleBuffer,
560
559
  ) );
@@ -616,6 +615,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
616
615
  export const calculateEmissiveTriangleContribution = Fn( ( [
617
616
  hitPoint, normal, viewDir, material,
618
617
  bounceIndex, rngState,
618
+ pixelCoord, resolution, frame, dimBase,
619
619
  emissiveBoost,
620
620
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
621
621
  triangleBuffer,
@@ -626,6 +626,7 @@ export const calculateEmissiveTriangleContribution = Fn( ( [
626
626
  const result = EmissiveContributionResult.wrap( calculateEmissiveTriangleContributionDebug(
627
627
  hitPoint, normal, viewDir, material,
628
628
  bounceIndex, rngState,
629
+ pixelCoord, resolution, frame, dimBase,
629
630
  emissiveBoost,
630
631
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
631
632
  triangleBuffer,
@@ -3,7 +3,6 @@
3
3
 
4
4
  import {
5
5
  Fn,
6
- vec2,
7
6
  vec3,
8
7
  float,
9
8
  int,
@@ -21,7 +20,7 @@ import {
21
20
  length,
22
21
  } from 'three/tsl';
23
22
  import { MIN_PDF } from './Common.js';
24
- import { RandomValue } from './Random.js';
23
+ import { getRandomSample1D, getRandomSample2D } from './Random.js';
25
24
  import {
26
25
  EmissiveSample,
27
26
  sampleTriangle,
@@ -107,6 +106,7 @@ export const lbvhNodeImportance = Fn( ( [ nMin, power, nMax, coneAxis, cosThetaO
107
106
  export const sampleLightBVHTriangle = Fn( ( [
108
107
  hitPoint, surfaceNormal,
109
108
  rngState,
109
+ pixelCoord, resolution, frame, dimBase,
110
110
  lbvhBuffer,
111
111
  emissiveTriangleBuffer,
112
112
  emissiveVec4Offset,
@@ -130,6 +130,13 @@ export const sampleLightBVHTriangle = Fn( ( [
130
130
  const nodeIndex = int( 0 ).toVar();
131
131
  const foundLeaf = tslBool( false ).toVar();
132
132
 
133
+ // Hierarchical sample warping: ONE variate recycled down the descent and into the leaf scan,
134
+ // rescaled to [0,1) after each choice. A per-level draw has no fixed dimension to key on
135
+ // (depth varies per pixel), so the descent — which dominates emissive-NEE variance — would
136
+ // otherwise stay white-noise however well the point-on-triangle pair is sampled. Unbiased:
137
+ // the rescaled value is uniform conditional on the branch taken; selectionPdf is unchanged.
138
+ const u = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
139
+
133
140
  // Tree descent: at most MAX_LBVH_DEPTH iterations
134
141
  Loop( MAX_LBVH_DEPTH, () => {
135
142
 
@@ -179,19 +186,19 @@ export const sampleLightBVHTriangle = Fn( ( [
179
186
  // Probability of choosing left child
180
187
  const pLeft = lImportance.div( totalImportance );
181
188
 
182
- // Sample random value to pick child
183
- const rand = RandomValue( rngState );
184
-
185
- If( rand.lessThan( pLeft ), () => {
189
+ If( u.lessThan( pLeft ), () => {
186
190
 
187
191
  // Choose left child
192
+ u.assign( clamp( u.div( max( pLeft, float( 1e-6 ) ) ), 0.0, 0.999999 ) );
188
193
  selectionPdf.mulAssign( pLeft );
189
194
  nodeIndex.assign( leftChildIdx );
190
195
 
191
196
  } ).Else( () => {
192
197
 
193
198
  // Choose right child
194
- selectionPdf.mulAssign( float( 1.0 ).sub( pLeft ) );
199
+ const pRight = float( 1.0 ).sub( pLeft ).toVar();
200
+ u.assign( clamp( u.sub( pLeft ).div( max( pRight, float( 1e-6 ) ) ), 0.0, 0.999999 ) );
201
+ selectionPdf.mulAssign( pRight );
195
202
  nodeIndex.assign( rightChildIdx );
196
203
 
197
204
  } );
@@ -213,7 +220,7 @@ export const sampleLightBVHTriangle = Fn( ( [
213
220
 
214
221
  // Sample one triangle proportional to power within the leaf
215
222
  // Linear scan: pick random threshold against cumulative power sum
216
- const randLeaf = RandomValue( rngState ).mul( leafTotalPower );
223
+ const randLeaf = u.mul( leafTotalPower );
217
224
  const cumPower = float( 0.0 ).toVar();
218
225
 
219
226
  // Default to last entry as fallback
@@ -253,10 +260,7 @@ export const sampleLightBVHTriangle = Fn( ( [
253
260
  // Fetch triangle geometry
254
261
  const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer ) );
255
262
 
256
- // Generate random numbers for point sampling
257
- const xi_r1 = RandomValue( rngState ).toVar();
258
- const xi_r2 = RandomValue( rngState ).toVar();
259
- const xi = vec2( xi_r1, xi_r2 );
263
+ const xi = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame ).toVar();
260
264
 
261
265
  const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
262
266
 
@@ -88,8 +88,6 @@ export const LightSample = struct( {
88
88
  export const IndirectLightingResult = struct( {
89
89
  direction: 'vec3', // Sampled direction for next bounce
90
90
  throughput: 'vec3', // Light throughput along this path
91
- misWeight: 'float', // MIS weight for this sample
92
- pdf: 'float', // PDF of the selected strategy
93
91
  combinedPdf: 'float', // Weighted sum of all strategy PDFs (for NEE↔implicit MIS pairing)
94
92
  } );
95
93