rayzee 7.28.2 → 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.
@@ -8,7 +8,7 @@ import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
8
8
  import { traverseBVH } from '../TSL/BVHTraversal.js';
9
9
  import { cameraRayDirection } from '../TSL/CameraRay.js';
10
10
  import { getMaterial } from '../TSL/Common.js';
11
- import { computeUVCache, processNormal, processBump, triangleUVTangent, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
11
+ import { computeUVCache, processNormal, processBump, processMetalnessRoughness, triangleUVTangent, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
12
12
 
13
13
  /**
14
14
  * NormalDepth — primary-ray G-buffer for SVGF gates.
@@ -29,9 +29,10 @@ import { computeUVCache, processNormal, processBump, triangleUVTangent, buildBuc
29
29
  * map, recomputed from the SAME deterministic hit — no extra ray) so the
30
30
  * spatial denoiser's edge-stop can see normal-map detail the flat geometric
31
31
  * normal hides. Deterministic ⇒ jitter-free, so it's safe for the gates.
32
+ * Its .w is the sampled material roughness (NRD's normal+roughness guide).
32
33
  *
33
34
  * Publishes: pathtracer:normalDepth, pathtracer:prevNormalDepth,
34
- * pathtracer:shadingNormal
35
+ * pathtracer:shadingNormal (rgb = shading normal·0.5+0.5, a = roughness)
35
36
  */
36
37
  export class NormalDepth extends RenderStage {
37
38
 
@@ -257,6 +258,7 @@ export class NormalDepth extends RenderStage {
257
258
  // Shading normal: perturb the geometric normal by the normal/bump map
258
259
  // from the SAME hit (deterministic UV → jitter-free). Miss → geo default.
259
260
  const shadingNormal = hit.normal.toVar();
261
+ const roughness = float( 1.0 ).toVar();
260
262
  If( hit.didHit, () => {
261
263
 
262
264
  const material = RayTracingMaterial.wrap(
@@ -274,12 +276,14 @@ export class NormalDepth extends RenderStage {
274
276
 
275
277
  const mapped = processNormal( hit.normal, material, uvCache, uvTangent ).toVar();
276
278
  shadingNormal.assign( processBump( mapped, material, uvCache ) );
279
+ // Same floor the Shade kernel applies before shading.
280
+ roughness.assign( processMetalnessRoughness( material, uvCache ).y.clamp( 0.05, 1.0 ) );
277
281
 
278
282
  } );
279
283
 
280
284
  const shadingResult = hit.didHit.select(
281
- vec4( shadingNormal.mul( 0.5 ).add( 0.5 ), depth ),
282
- vec4( 0.0, 0.0, 0.0, float( 65504.0 ) )
285
+ vec4( shadingNormal.mul( 0.5 ).add( 0.5 ), roughness ),
286
+ vec4( 0.0, 0.0, 0.0, 1.0 )
283
287
  );
284
288
 
285
289
  textureStore(
@@ -1157,6 +1157,9 @@ export class PathTracerStage extends RenderStage {
1157
1157
  context.setTexture( 'pathtracer:normalDepth', writeTex.normalDepth );
1158
1158
  context.setTexture( 'pathtracer:albedo', writeTex.albedo );
1159
1159
 
1160
+ // Not the same as the context's own `accumulatedFrames`, which counts pipeline renders — this
1161
+ // freezes when the frame retires or the camera moves, which is what a denoiser needs.
1162
+ context.setState( 'pathtracer:samples', this.frameCount );
1160
1163
  context.setState( 'interactionMode', this.cameraOptimizer?.isInInteractionMode() ?? false );
1161
1164
  context.setState( 'renderMode', this.renderMode.value );
1162
1165
 
@@ -1224,6 +1227,7 @@ export class PathTracerStage extends RenderStage {
1224
1227
  // touched here — it is owned by ASVGF_QUALITY_PRESETS, and overwriting it
1225
1228
  // with a hardcoded value meant `medium` and `high` never took effect.
1226
1229
  this.emit( 'asvgf:reset' );
1230
+ this.emit( 'denoiser:reset' );
1227
1231
 
1228
1232
  }
1229
1233
 
package/src/TSL/Common.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp } from 'three/tsl';
1
+ import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp, select } from 'three/tsl';
2
2
 
3
3
  import {
4
4
  AnisoFrame,
@@ -17,6 +17,13 @@ export const MIN_CLEARCOAT_ROUGHNESS = 0.089;
17
17
  export const MAX_ROUGHNESS = 1.0;
18
18
  export const MIN_PDF = 0.001;
19
19
  export const REC709_LUMINANCE_COEFFICIENTS = vec3( 0.2126, 0.7152, 0.0722 );
20
+
21
+ export const FP16_MAX = 65504.0;
22
+
23
+ // NaN/±Inf guard: mix() propagates NaN, so one firefly would poison a temporal history forever.
24
+ export const sanitize1 = ( x, ceiling = 1e7 ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, ceiling );
25
+ export const sanitizeRGB = ( c, ceiling = 1e7 ) =>
26
+ vec3( sanitize1( c.x, ceiling ), sanitize1( c.y, ceiling ), sanitize1( c.z, ceiling ) );
20
27
  import { MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
21
28
 
22
29
  export const MATERIAL_SLOTS = MATERIAL_DATA_LAYOUT.SLOTS_PER_MATERIAL;
@@ -14,7 +14,7 @@ import {
14
14
  } from 'three/tsl';
15
15
 
16
16
  import {
17
- readRayRadiance, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
17
+ readRayRadiance, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo, gbDecodeHitDist,
18
18
  } from '../Processor/PackedRayBuffer.js';
19
19
  import { luminance } from './Common.js';
20
20
 
@@ -96,11 +96,14 @@ export function buildFinalWriteKernel( params ) {
96
96
  // auxOn gates the decode + stores so a no-denoiser frame does no G-buffer read and no aux writes.
97
97
  const finalNormalDepth = vec4( 0.0 ).toVar();
98
98
  const finalAlbedo = vec4( 0.0 ).xyz.toVar();
99
+ // Albedo .w carries the hit distance — OIDN reads albedo as 3 channels, so it is free.
100
+ const finalHitDist = float( 0.0 ).toVar();
99
101
  If( auxOn, () => {
100
102
 
101
103
  const gbuf = readGBuffer( gBufferRO, rayID );
102
104
  finalNormalDepth.assign( gbDecodeNormalDepth( gbuf ) );
103
105
  finalAlbedo.assign( vec4( gbDecodeAlbedo( gbuf ), 0.0 ).xyz );
106
+ finalHitDist.assign( gbDecodeHitDist( gbuf ) );
104
107
 
105
108
  } );
106
109
 
@@ -117,8 +120,10 @@ export function buildFinalWriteKernel( params ) {
117
120
  finalColor.assign( select( wasFrozen, prevAccumSample.xyz, mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) ) );
118
121
  If( auxOn.and( hasPreviousAux ), () => {
119
122
 
120
- // Albedo averages cleanly (it's a colour).
121
- finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, auxAccumulationAlpha ) );
123
+ // Albedo averages cleanly (it's a colour); so does the normalized hit distance in .w.
124
+ const prevAlbedoSample = texture( prevAlbedoTexture, prevUV, 0 ).toVar();
125
+ finalAlbedo.assign( mix( prevAlbedoSample.xyz, finalAlbedo, auxAccumulationAlpha ) );
126
+ finalHitDist.assign( mix( prevAlbedoSample.w, finalHitDist, auxAccumulationAlpha ) );
122
127
 
123
128
  // NORMAL: by default keep this frame's POINT-SAMPLED normal — it varies with the bump,
124
129
  // which fast/ASVGF want to preserve edge detail. But a CLEAN-AUX OIDN model (calb_cnrm/high,
@@ -251,7 +256,7 @@ export function buildFinalWriteKernel( params ) {
251
256
  If( auxOn, () => {
252
257
 
253
258
  textureStore( writeNDTex, uintCoord, finalNormalDepth ).toWriteOnly();
254
- textureStore( writeAlbedoTex, uintCoord, vec4( finalAlbedo, 1.0 ) ).toWriteOnly();
259
+ textureStore( writeAlbedoTex, uintCoord, vec4( finalAlbedo, finalHitDist ) ).toWriteOnly();
255
260
 
256
261
  } );
257
262
 
@@ -96,7 +96,7 @@ export function buildGenerateKernel( params ) {
96
96
  If( auxOn, () => {
97
97
 
98
98
  // default: normal +Z, depth 1 (far), black albedo (background/miss). Per-CHUNK G-buffer → LOCAL slot.
99
- writeGBuffer( gBufferRW, rayID, vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
99
+ writeGBuffer( gBufferRW, rayID, vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ), float( 0.0 ) );
100
100
  // DDFA: seed the see-through aux tint to white (no tint yet). RMW preserves slot-5 xyz (sigmaA).
101
101
  writeFeatureThroughput( rayBufferRW, rayID, vec3( 1.0 ) );
102
102
 
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  Fn, float, vec2, vec3, vec4, int, uint,
9
9
  bool as tslBool,
10
- If, Loop, normalize, max, exp, log, clamp, dot, length, select, smoothstep, mix,
10
+ If, Loop, normalize, max, min, exp, log, clamp, dot, length, select, smoothstep, mix,
11
11
  instanceIndex,
12
12
  sampler,
13
13
  atomicAdd, atomicLoad, atomicStore, uintBitsToFloat,
@@ -29,6 +29,7 @@ import { IndirectLightingResult, sampleCone } from './LightsCore.js';
29
29
  import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
30
30
  import { evaluateDFG } from './MaterialProperties.js';
31
31
  import { dielectricF0 } from './Fresnel.js';
32
+ import { NRD_HIT_DIST_A, NRD_HIT_DIST_B } from '../EngineDefaults.js';
32
33
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
33
34
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
34
35
  import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
@@ -57,7 +58,7 @@ import {
57
58
  readHitDistance, readHitBarycentrics, readHitNormal,
58
59
  readHitMaterialIndex, readHitTriangleIndex,
59
60
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
60
- writeGBuffer, readGBuffer, gbDecodeNormalDepth,
61
+ writeGBuffer, writeGBufferHitDist, readGBuffer, gbDecodeNormalDepth,
61
62
  readRayRadiance,
62
63
  readFeatureThroughput, writeFeatureThroughput,
63
64
  } from '../Processor/PackedRayBuffer.js';
@@ -226,6 +227,17 @@ export function buildShadeKernel( params ) {
226
227
  const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
227
228
  const transparentCount = readTransparentCount( rayBufferRW, rayID ).toVar();
228
229
 
230
+ // NRD guide: first segment after the primary scatter, plus any alpha-skip run so a cutout hole
231
+ // doesn't shorten it. Unconditional at depth 1, so the post-skip segment wins.
232
+ If( auxOn.and( cameraDepth.equal( 1 ) ), () => {
233
+
234
+ const scatterViewZ = cameraViewMatrix.mul( vec4( origin, 1.0 ) ).z.abs();
235
+ const norm = scatterViewZ.mul( NRD_HIT_DIST_B ).add( NRD_HIT_DIST_A );
236
+ const total = readMisRayT( rayBufferRW, rayID ).add( min( hitDist, float( 1e6 ) ) );
237
+ writeGBufferHitDist( gBufferRW, pixelIndex, total.div( max( norm, float( 1e-4 ) ) ).clamp( 0.0, 1.0 ) );
238
+
239
+ } );
240
+
229
241
  // ── Analytic ground-plane shadow catcher (primary ray only, no geometry) ──
230
242
  // A horizontal plane at y = groundCatcherHeight. For a bounce-0 ray that crosses it
231
243
  // closer than any BVH hit (hitDist is MISS_DIST on sky rays, so the plane wins over the
package/src/index.js CHANGED
@@ -39,6 +39,11 @@ export { Logger, createLogger, fmt, LOG_LEVELS } from './utils/Logger.js';
39
39
  export {
40
40
  ENGINE_DEFAULTS,
41
41
  ASVGF_QUALITY_PRESETS,
42
+ NRD_DEFAULTS,
43
+ NRD_QUALITY_PRESETS,
44
+ NRD_PRESET_KEYS,
45
+ NRD_HIT_DIST_A,
46
+ NRD_HIT_DIST_B,
42
47
  CAMERA_PRESETS,
43
48
  CAMERA_RANGES,
44
49
  SKY_PRESETS,
@@ -2,11 +2,11 @@ import { EventDispatcher } from 'three';
2
2
  import { OIDNDenoiser } from '../Passes/OIDNDenoiser.js';
3
3
  import { AIUpscaler } from '../Passes/AIUpscaler.js';
4
4
  import { EngineEvents } from '../EngineEvents.js';
5
- import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS } from '../EngineDefaults.js';
5
+ import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS } from '../EngineDefaults.js';
6
6
 
7
7
  /**
8
8
  * Orchestrates all denoising, post-processing, and AI upscaling:
9
- * - Real-time denoiser strategy switching (ASVGF / EdgeAware / None)
9
+ * - Real-time denoiser strategy switching (ASVGF / NRD / EdgeAware / None)
10
10
  * - OIDN (offline denoise on render completion)
11
11
  * - AI Upscaler
12
12
  * - Auto-exposure coordination
@@ -41,7 +41,7 @@ export class DenoisingManager extends EventDispatcher {
41
41
  this.pipeline = pipeline;
42
42
 
43
43
  // Stage references — only used internally for orchestration
44
- this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, edgeFilter, autoExposure, compositor }
44
+ this._stages = stages; // { pathTracer, asvgf, nrd, variance, bilateralFilter, edgeFilter, autoExposure, compositor }
45
45
 
46
46
  this._getExposure = getExposure;
47
47
  this._getSaturation = getSaturation;
@@ -237,30 +237,32 @@ export class DenoisingManager extends EventDispatcher {
237
237
 
238
238
  // ── Denoiser Strategy ─────────────────────────────────────────
239
239
 
240
- /**
241
- * Switches the real-time denoiser strategy.
242
- * @param {string} strategy - 'none' | 'asvgf' | 'edgeaware'
243
- * @param {string} [asvgfPreset] - ASVGF quality preset when strategy is 'asvgf'
244
- */
245
240
  /**
246
241
  * Active real-time denoiser, derived from stage state so it can't drift from
247
242
  * setDenoiserStrategy / setASVGFEnabled.
248
- * @returns {'asvgf'|'edgeaware'|'none'}
243
+ * @returns {'asvgf'|'nrd'|'edgeaware'|'none'}
249
244
  */
250
245
  get denoiserStrategy() {
251
246
 
252
247
  if ( this._stages.asvgf?.enabled ) return 'asvgf';
248
+ if ( this._stages.nrd?.enabled ) return 'nrd';
253
249
  if ( this._stages.edgeFilter?.enabled ) return 'edgeaware';
254
250
  return 'none';
255
251
 
256
252
  }
257
253
 
258
- setDenoiserStrategy( strategy, asvgfPreset ) {
254
+ /**
255
+ * Switches the real-time denoiser strategy.
256
+ * @param {string} strategy - 'none' | 'asvgf' | 'nrd' | 'edgeaware'
257
+ * @param {string} [preset] - quality preset for 'asvgf' (ASVGF_QUALITY_PRESETS) or 'nrd' (NRD_QUALITY_PRESETS)
258
+ */
259
+ setDenoiserStrategy( strategy, preset ) {
259
260
 
260
261
  const s = this._stages;
261
262
 
262
263
  // Disable all real-time denoisers first
263
264
  if ( s.asvgf ) s.asvgf.enabled = false;
265
+ if ( s.nrd ) s.nrd.enabled = false;
264
266
  if ( s.variance ) s.variance.enabled = false;
265
267
  if ( s.bilateralFilter ) s.bilateralFilter.enabled = false;
266
268
  if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( false );
@@ -274,7 +276,19 @@ export class DenoisingManager extends EventDispatcher {
274
276
  if ( s.variance ) s.variance.enabled = true;
275
277
  if ( s.bilateralFilter ) s.bilateralFilter.enabled = true;
276
278
  s.asvgf.setTemporalEnabled?.( true );
277
- this._applyASVGFPreset( asvgfPreset || 'medium' );
279
+ this._applyASVGFPreset( preset || 'medium' );
280
+ break;
281
+
282
+ case 'nrd':
283
+ if ( s.nrd ) {
284
+
285
+ s.nrd.enabled = true;
286
+ // Stale history from the last time it ran describes another view.
287
+ s.nrd.resetHistory?.();
288
+ this._applyNRDPreset( preset || 'medium' );
289
+
290
+ }
291
+
278
292
  break;
279
293
 
280
294
  case 'edgeaware':
@@ -304,6 +318,8 @@ export class DenoisingManager extends EventDispatcher {
304
318
 
305
319
  if ( enabled ) {
306
320
 
321
+ // One real-time denoiser at a time.
322
+ if ( s.nrd ) s.nrd.enabled = false;
307
323
  s.asvgf?.setTemporalEnabled?.( true );
308
324
  this._applyASVGFPreset( qualityPreset || 'medium' );
309
325
 
@@ -353,19 +369,25 @@ export class DenoisingManager extends EventDispatcher {
353
369
  * navigation and frees their textures. Call after any consumer toggle.
354
370
  *
355
371
  * MotionVector requires NormalDepth (reads pathtracer:normalDepth) and its
356
- * consumers (ASVGF) are a subset of NormalDepth's, so NormalDepth is
372
+ * consumers (ASVGF, NRD) are a subset of NormalDepth's, so NormalDepth is
357
373
  * always enabled whenever MotionVector is. Adaptive sampling / Variance / OIDN
358
374
  * do NOT read these signals, so they don't keep the G-buffer alive.
359
375
  */
376
+ /** True while a strategy that reprojects through motion vectors is active. */
377
+ get requiresMotionVectors() {
378
+
379
+ return !! ( this._stages.asvgf?.enabled || this._stages.nrd?.enabled );
380
+
381
+ }
382
+
360
383
  _syncGBufferStages() {
361
384
 
362
385
  const s = this._stages;
363
386
  const nd = s.normalDepth;
364
387
  const mv = s.motionVector;
365
388
 
366
- // motionVector:* consumed by ASVGF
367
- const motionNeeded = !! ( s.asvgf?.enabled );
368
- // pathtracer:normalDepth consumed by ASVGF, EdgeFilter, BilateralFilter
389
+ const motionNeeded = this.requiresMotionVectors;
390
+ // pathtracer:normalDepth consumed by ASVGF, NRD, EdgeFilter, BilateralFilter
369
391
  const normalNeeded = motionNeeded || !! ( s.edgeFilter?.enabled || s.bilateralFilter?.enabled );
370
392
 
371
393
  if ( nd ) {
@@ -406,7 +428,7 @@ export class DenoisingManager extends EventDispatcher {
406
428
  // disabled (lazily re-created on the next dispatch after re-enable). Every strategy/denoiser
407
429
  // toggle funnels through here after the enabled flags above are settled, so this is the one
408
430
  // choke point. dispose() is idempotent, so re-running it for an already-released stage is a no-op.
409
- for ( const stage of [ s.asvgf, s.variance, s.bilateralFilter, s.edgeFilter, nd, mv ] ) {
431
+ for ( const stage of [ s.asvgf, s.nrd, s.variance, s.bilateralFilter, s.edgeFilter, nd, mv ] ) {
410
432
 
411
433
  if ( stage && ! stage.enabled ) stage.releaseGPUMemory?.();
412
434
 
@@ -691,16 +713,42 @@ export class DenoisingManager extends EventDispatcher {
691
713
 
692
714
  /**
693
715
  * Switches strategy with automatic reset (convenience wrapper).
694
- * @param {'none'|'asvgf'|'edgeaware'} strategy
695
- * @param {string} [asvgfPreset]
716
+ * @param {'none'|'asvgf'|'nrd'|'edgeaware'} strategy
717
+ * @param {string} [preset]
696
718
  */
697
- setStrategy( strategy, asvgfPreset ) {
719
+ setStrategy( strategy, preset ) {
698
720
 
699
- this.setDenoiserStrategy( strategy, asvgfPreset );
721
+ this.setDenoiserStrategy( strategy, preset );
700
722
  this._onReset?.();
701
723
 
702
724
  }
703
725
 
726
+ // ── NRD (ReBLUR) ─────────────────────────────────────────────
727
+
728
+ /** Updates NRD stage parameters (nrd::ReblurSettings names, see NRD_DEFAULTS). */
729
+ setNRDParams( params ) {
730
+
731
+ this._stages.nrd?.updateParameters( params );
732
+
733
+ }
734
+
735
+ /**
736
+ * Applies an NRD quality preset.
737
+ * @param {string} presetName - 'low' | 'medium' | 'high'
738
+ */
739
+ applyNRDPreset( presetName ) {
740
+
741
+ this._applyNRDPreset( presetName );
742
+
743
+ }
744
+
745
+ /** Selects the NRD debug view (0 = beauty; see NRD.js for the others). */
746
+ setNRDDebugMode( mode ) {
747
+
748
+ this._stages.nrd?.updateParameters( { debugMode: mode } );
749
+
750
+ }
751
+
704
752
  // ── Private ───────────────────────────────────────────────────
705
753
 
706
754
  _setTileHelper( enabled ) {
@@ -737,12 +785,23 @@ export class DenoisingManager extends EventDispatcher {
737
785
  const keys = [
738
786
  'asvgf:output', 'asvgf:demodulated', 'asvgf:gradient',
739
787
  'variance:output', 'bilateralFiltering:output',
740
- 'edgeFiltering:output',
788
+ 'edgeFiltering:output', 'nrd:output',
741
789
  ];
742
790
  keys.forEach( k => ctx.removeTexture( k ) );
743
791
 
744
792
  }
745
793
 
794
+ _applyNRDPreset( presetName ) {
795
+
796
+ const preset = NRD_QUALITY_PRESETS[ presetName ];
797
+ if ( ! preset ) return;
798
+
799
+ const params = {};
800
+ for ( const key of NRD_PRESET_KEYS ) params[ key ] = NRD_DEFAULTS[ key ];
801
+ this._stages.nrd?.updateParameters( { ...params, ...preset } );
802
+
803
+ }
804
+
746
805
  _applyASVGFPreset( presetName ) {
747
806
 
748
807
  const preset = ASVGF_QUALITY_PRESETS[ presetName ];