rayzee 7.29.0 → 7.30.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.29.0",
3
+ "version": "7.30.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -224,6 +224,10 @@ export const ENGINE_DEFAULTS = {
224
224
  asvgfQualityPreset: 'medium',
225
225
  showAsvgfHeatmap: false,
226
226
 
227
+ // NRD (ReBLUR port) real-time denoiser — see NRD_DEFAULTS / NRD_QUALITY_PRESETS.
228
+ nrdQualityPreset: 'medium',
229
+ nrdDebugMode: 0,
230
+
227
231
  // Auto-exposure settings
228
232
  autoExposure: false,
229
233
  autoExposureKeyValue: 0.18,
@@ -233,6 +237,16 @@ export const ENGINE_DEFAULTS = {
233
237
  autoExposureAdaptSpeedDark: 0.5,
234
238
  };
235
239
 
240
+ // Ray distance NormalDepth writes on a miss. Max finite half-float, so miss−miss diffs stay 0
241
+ // rather than Inf−Inf=NaN.
242
+ export const GBUFFER_MISS_DEPTH = 65504.0;
243
+ export const GBUFFER_MISS_THRESHOLD = 6e4;
244
+
245
+ // normHitDist = hitDist / (A + B·viewZ). A constant, not a uniform: the Shade write and the NRD
246
+ // decode must agree, and the normalization is a pure round trip.
247
+ export const NRD_HIT_DIST_A = 3.0;
248
+ export const NRD_HIT_DIST_B = 0.1;
249
+
236
250
  // Albedo demodulation safety floor. ASVGF and BilateralFilter MUST use the
237
251
  // same value — demod (`color / safeAlbedo`) and remod (`lighting * safeAlbedo`)
238
252
  // only round-trip exactly when both sides agree.
@@ -307,6 +321,67 @@ export const ASVGF_QUALITY_PRESETS = {
307
321
  }
308
322
  };
309
323
 
324
+ // NRD ReBLUR port (Stages/NRD.js). Names follow nrd::ReblurSettings so NVIDIA's tuning notes apply.
325
+ export const NRD_DEFAULTS = {
326
+ maxAccumulatedFrameNum: 30,
327
+ maxFastAccumulatedFrameNum: 6,
328
+ maxStabilizedFrameNum: 63,
329
+ historyFixFrameNum: 3,
330
+ historyFixBasePixelStride: 14,
331
+ prepassBlurRadius: 30,
332
+ minBlurRadius: 1,
333
+ maxBlurRadius: 30,
334
+ lobeAngleFraction: 0.15,
335
+ roughnessFraction: 0.15,
336
+ planeDistanceSensitivity: 0.02,
337
+ minHitDistanceWeight: 0.1,
338
+ fastHistoryClampingSigmaScale: 2.0,
339
+ fireflySuppressorMinRelativeScale: 2.0,
340
+ enableAntiFirefly: true,
341
+ antilagLuminanceSigmaScale: 2.0,
342
+ antilagLuminanceSensitivity: 3.0,
343
+ disocclusionThreshold: 0.01,
344
+ convergenceS: 1.0,
345
+ convergenceB: 0.2,
346
+ convergenceP: 0.8,
347
+ // Share of the lobe the normal weight accepts before any history exists. NRD's own constant is
348
+ // 0.75, which its source flags as probably too much; at that width it smears curved surfaces.
349
+ lobeVolumePercent: 0.1,
350
+ // Progressive handover: input sample count at which the denoiser passes the render through untouched.
351
+ // 0 = 2 · maxAccumulatedFrameNum.
352
+ handoverFrames: 0,
353
+ };
354
+
355
+ // Keys a preset may override. Applying one resets every key to its default first, so a preset only
356
+ // states its deltas and `medium` can be empty.
357
+ export const NRD_PRESET_KEYS = [
358
+ 'maxAccumulatedFrameNum', 'maxFastAccumulatedFrameNum', 'maxStabilizedFrameNum',
359
+ 'historyFixFrameNum', 'prepassBlurRadius', 'maxBlurRadius', 'enableAntiFirefly',
360
+ ];
361
+
362
+ export const NRD_QUALITY_PRESETS = {
363
+ // Short history + no pre-pass: most responsive, noisiest.
364
+ low: {
365
+ maxAccumulatedFrameNum: 16,
366
+ maxFastAccumulatedFrameNum: 4,
367
+ maxStabilizedFrameNum: 16,
368
+ historyFixFrameNum: 2,
369
+ prepassBlurRadius: 0,
370
+ maxBlurRadius: 20,
371
+ enableAntiFirefly: false,
372
+ },
373
+ // nrd::ReblurSettings defaults.
374
+ medium: {},
375
+ // Longer history and wider kernels: smoother, more lag on lighting change.
376
+ high: {
377
+ maxAccumulatedFrameNum: 45,
378
+ maxFastAccumulatedFrameNum: 8,
379
+ historyFixFrameNum: 4,
380
+ prepassBlurRadius: 40,
381
+ maxBlurRadius: 40,
382
+ },
383
+ };
384
+
310
385
  export const CAMERA_RANGES = {
311
386
  fov: {
312
387
  min: 10,
@@ -9,6 +9,7 @@ import { PathTracer } from './Stages/PathTracer.js';
9
9
  import { NormalDepth } from './Stages/NormalDepth.js';
10
10
  import { MotionVector } from './Stages/MotionVector.js';
11
11
  import { ASVGF } from './Stages/ASVGF.js';
12
+ import { NRD } from './Stages/NRD.js';
12
13
  import { Variance } from './Stages/Variance.js';
13
14
  import { BilateralFilter } from './Stages/BilateralFilter.js';
14
15
  import { EdgeFilter } from './Stages/EdgeFilter.js';
@@ -524,7 +525,12 @@ export class PathTracerApp extends EventDispatcher {
524
525
  if ( this.pipeline ) {
525
526
 
526
527
  this.pipeline.reset();
527
- if ( ! soft ) this.pipeline.eventBus.emit( 'asvgf:reset' );
528
+ if ( ! soft ) {
529
+
530
+ this.pipeline.eventBus.emit( 'asvgf:reset' );
531
+ this.pipeline.eventBus.emit( 'denoiser:reset' );
532
+
533
+ }
528
534
 
529
535
  }
530
536
 
@@ -2857,6 +2863,7 @@ export class PathTracerApp extends EventDispatcher {
2857
2863
  this.pipeline.addStage( this.stages.pathTracer );
2858
2864
  this.pipeline.addStage( this.stages.normalDepth );
2859
2865
  this.pipeline.addStage( this.stages.motionVector );
2866
+ this.pipeline.addStage( this.stages.nrd );
2860
2867
  this.pipeline.addStage( this.stages.asvgf );
2861
2868
  this.pipeline.addStage( this.stages.variance );
2862
2869
  this.pipeline.addStage( this.stages.bilateralFilter );
@@ -3090,6 +3097,7 @@ export class PathTracerApp extends EventDispatcher {
3090
3097
  pathTracer: this.stages.pathTracer
3091
3098
  } );
3092
3099
  this.stages.asvgf = new ASVGF( this.renderer, { enabled: false } );
3100
+ this.stages.nrd = new NRD( this.renderer, { enabled: false, pathTracer: this.stages.pathTracer } );
3093
3101
  this.stages.variance = new Variance( this.renderer, { enabled: false } );
3094
3102
  this.stages.bilateralFilter = new BilateralFilter( this.renderer, { enabled: false } );
3095
3103
  this.stages.edgeFilter = new EdgeFilter( this.renderer, { enabled: false } );
@@ -3114,6 +3122,7 @@ export class PathTracerApp extends EventDispatcher {
3114
3122
  normalDepth: this.stages.normalDepth,
3115
3123
  motionVector: this.stages.motionVector,
3116
3124
  asvgf: this.stages.asvgf,
3125
+ nrd: this.stages.nrd,
3117
3126
  variance: this.stages.variance,
3118
3127
  bilateralFilter: this.stages.bilateralFilter,
3119
3128
  edgeFilter: this.stages.edgeFilter,
@@ -16,9 +16,10 @@ export const RAY_STRIDE = 7;
16
16
  export const HIT_STRIDE = 2;
17
17
  // Per-pixel G-buffer (first-hit MRT staging): 1 uvec4/pixel — half-packed normal/depth/albedo
18
18
  // (pack2x16, no f32 bitcast); read by FinalWrite:
19
- // .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg) .w=packUnorm2x16(albedo.b, 0)
19
+ // .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg)
20
+ // .w=packUnorm2x16(albedo.b, normHitDist)
21
+ // normHitDist is written by Shade; writeGBuffer preserves it so the DDFA albedo commits can't zero it.
20
22
  // Separate buffer from RAY (per-pixel, not per-ray×S) — written by Generate/Shade bounce-0.
21
- // (A second lane reserved for an A-SVGF Tier-1 surface ID was removed — write-only, never read.)
22
23
  export const GBUFFER_STRIDE = 1;
23
24
 
24
25
  export const RAY = {
@@ -162,15 +163,35 @@ export const readRayRadiance = ( buf, id ) =>
162
163
  // One uvec4 per pixel (stride 1); AoS base = pixelIndex * GBUFFER_STRIDE.
163
164
  const gbLane = ( pixelIndex ) => uint( pixelIndex ).mul( GBUFFER_STRIDE );
164
165
 
165
- export const writeGBuffer = ( buf, pixelIndex, normal, depth, albedo ) =>
166
- buf.element( gbLane( pixelIndex ) ).assign( uvec4(
166
+ // hitDist = null keeps the lane's current normHitDist.
167
+ export const writeGBuffer = ( buf, pixelIndex, normal, depth, albedo, hitDist = null ) => {
168
+
169
+ const lane = gbLane( pixelIndex );
170
+ const h = hitDist === null ? unpackUnorm2x16( buf.element( lane ).w ).y : float( hitDist );
171
+ return buf.element( lane ).assign( uvec4(
167
172
  packSnorm2x16( vec2( normal.x, normal.y ) ),
168
173
  packSnorm2x16( vec2( normal.z, depth ) ),
169
174
  packUnorm2x16( vec2( albedo.x, albedo.y ) ),
170
- packUnorm2x16( vec2( albedo.z, 0.0 ) ),
175
+ packUnorm2x16( vec2( albedo.z, h ) ),
176
+ ) );
177
+
178
+ };
179
+
180
+ export const writeGBufferHitDist = ( buf, pixelIndex, normHitDist ) => {
181
+
182
+ const lane = gbLane( pixelIndex );
183
+ const cur = buf.element( lane ).toVar();
184
+ return buf.element( lane ).assign( uvec4(
185
+ cur.x, cur.y, cur.z,
186
+ packUnorm2x16( vec2( unpackUnorm2x16( cur.w ).x, normHitDist ) ),
171
187
  ) );
188
+
189
+ };
190
+
172
191
  export const readGBuffer = ( buf, pixelIndex ) => buf.element( gbLane( pixelIndex ) );
173
192
 
193
+ export const gbDecodeHitDist = ( packed ) => unpackUnorm2x16( packed.w ).y;
194
+
174
195
  // Decode for FinalWrite. normalDepth.xyz matches the prior path (normal*0.5+0.5), .w = raw depth.
175
196
  export const gbDecodeNormalDepth = ( packed ) => {
176
197
 
@@ -19,17 +19,21 @@ import { createLogger, fmt } from '../utils/Logger.js';
19
19
 
20
20
  const log = createLogger( 'gpu' );
21
21
 
22
- function createWriteStorageTex() {
22
+ // Compute-output StorageTexture at the reserved size, never resized (see above). LinearFilter is
23
+ // load-bearing: textureLoad codegen requires it on a StorageTexture.
24
+ export function createStorageTexture( type = FloatType, filter = LinearFilter ) {
23
25
 
24
26
  const tex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
25
- tex.type = FloatType;
27
+ tex.type = type;
26
28
  tex.format = RGBAFormat;
27
- tex.minFilter = LinearFilter;
28
- tex.magFilter = LinearFilter;
29
+ tex.minFilter = filter;
30
+ tex.magFilter = filter;
29
31
  return tex;
30
32
 
31
33
  }
32
34
 
35
+ const createWriteStorageTex = () => createStorageTexture();
36
+
33
37
  export class StorageTexturePool {
34
38
 
35
39
  constructor( width, height ) {
@@ -182,10 +182,10 @@ export class RenderSettings extends EventDispatcher {
182
182
 
183
183
  if ( ! isPanorama ) return;
184
184
 
185
- // ASVGF is driven entirely by MotionVector, which unprojects through
186
- // projectionMatrixInverse — meaningless once every pixel is its own direction.
187
- // Fall back to the spatial-only denoiser rather than leaving no strategy.
188
- if ( denoisingManager?.denoiserStrategy === 'asvgf' ) denoisingManager.setDenoiserStrategy( 'edgeaware' );
185
+ // MotionVector unprojects through projectionMatrixInverse, which is meaningless once
186
+ // every pixel is its own direction. Fall back to the spatial-only denoiser rather
187
+ // than leaving no strategy.
188
+ if ( denoisingManager?.requiresMotionVectors ) denoisingManager.setDenoiserStrategy( 'edgeaware' );
189
189
  // Auto-focus raycasts via Raycaster.setFromCamera, which only knows the frustum.
190
190
  cameraManager?.setAutoFocusMode( 'manual' );
191
191
 
@@ -70,6 +70,7 @@ export class Compositor extends RenderStage {
70
70
  || context.getTexture( 'edgeFiltering:output' )
71
71
  || context.getTexture( 'bilateralFiltering:output' )
72
72
  || context.getTexture( 'asvgf:output' )
73
+ || context.getTexture( 'nrd:output' )
73
74
  || context.getTexture( 'pathtracer:color' );
74
75
 
75
76
  }