rayzee 5.7.0 → 5.7.1

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": "5.7.0",
3
+ "version": "5.7.1",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -1,4 +1,4 @@
1
- import { WebGPURenderer, RectAreaLightNode } from 'three/webgpu';
1
+ import { WebGPURenderer, RectAreaLightNode, LinearSRGBColorSpace, SRGBColorSpace } from 'three/webgpu';
2
2
  import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
3
3
  import {
4
4
  ACESFilmicToneMapping, Scene, EventDispatcher, TimestampQuery
@@ -16,7 +16,7 @@ import { AdaptiveSampling } from './Stages/AdaptiveSampling.js';
16
16
  import { EdgeFilter } from './Stages/EdgeFilter.js';
17
17
  import { AutoExposure } from './Stages/AutoExposure.js';
18
18
  import { SSRC } from './Stages/SSRC.js';
19
- import { Display } from './Stages/Display.js';
19
+ import { Compositor } from './Stages/Compositor.js';
20
20
  import { RenderPipeline } from './Pipeline/RenderPipeline.js';
21
21
  import { CompletionTracker } from './Pipeline/CompletionTracker.js';
22
22
  import { ENGINE_DEFAULTS as DEFAULT_STATE, FINAL_RENDER_CONFIG, PREVIEW_RENDER_CONFIG } from './EngineDefaults.js';
@@ -286,7 +286,7 @@ export class PathTracerApp extends EventDispatcher {
286
286
  if ( this._needsDisplayRefresh ) {
287
287
 
288
288
  this._needsDisplayRefresh = false;
289
- this.stages.display.render( this.pipeline.context );
289
+ this.stages.compositor.render( this.pipeline.context );
290
290
  this._renderHelperOverlay();
291
291
 
292
292
  }
@@ -775,7 +775,7 @@ export class PathTracerApp extends EventDispatcher {
775
775
  // Apply all settings to stages in one shot
776
776
  timer.start( 'Apply settings' );
777
777
  this.settings.applyAll();
778
- this.stages.display.setTransparentBackground( this.settings.get( 'transparentBackground' ) );
778
+ this.stages.compositor.setTransparentBackground( this.settings.get( 'transparentBackground' ) );
779
779
  timer.end( 'Apply settings' );
780
780
 
781
781
  timer.print();
@@ -1006,10 +1006,10 @@ export class PathTracerApp extends EventDispatcher {
1006
1006
 
1007
1007
  if ( usePostProcess ) return dm.denoiserCanvas;
1008
1008
 
1009
- // Re-render display stage so the WebGPU canvas has valid content
1010
- if ( this.stages.display && this.pipeline?.context ) {
1009
+ // Re-render compositor stage so the WebGPU canvas has valid content
1010
+ if ( this.stages.compositor && this.pipeline?.context ) {
1011
1011
 
1012
- this.stages.display.render( this.pipeline.context );
1012
+ this.stages.compositor.render( this.pipeline.context );
1013
1013
 
1014
1014
  }
1015
1015
 
@@ -1208,6 +1208,8 @@ export class PathTracerApp extends EventDispatcher {
1208
1208
 
1209
1209
  RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
1210
1210
 
1211
+ this.renderer.workingColorSpace = SRGBColorSpace;
1212
+ this.renderer.outputColorSpace = SRGBColorSpace;
1211
1213
  this.renderer.toneMapping = ACESFilmicToneMapping;
1212
1214
  this.renderer.toneMappingExposure = 1.0;
1213
1215
  this.renderer.setPixelRatio( 1.0 );
@@ -1261,7 +1263,7 @@ export class PathTracerApp extends EventDispatcher {
1261
1263
  this.pipeline.addStage( this.stages.adaptiveSampling );
1262
1264
  this.pipeline.addStage( this.stages.edgeFilter );
1263
1265
  this.pipeline.addStage( this.stages.autoExposure );
1264
- this.pipeline.addStage( this.stages.display );
1266
+ this.pipeline.addStage( this.stages.compositor );
1265
1267
 
1266
1268
  const initRenderW = this.canvas.clientWidth || 1;
1267
1269
  const initRenderH = this.canvas.clientHeight || 1;
@@ -1367,10 +1369,13 @@ export class PathTracerApp extends EventDispatcher {
1367
1369
  // Bind settings to pipeline stages
1368
1370
  this.settings.bind( {
1369
1371
  stages: this.stages,
1372
+ renderer: this.renderer,
1370
1373
  resetCallback: () => this.reset(),
1371
1374
  reconcileCompletion: () => this._reconcileCompletion(),
1372
1375
  } );
1373
1376
 
1377
+ this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
1378
+
1374
1379
  // Resize handling
1375
1380
  this.onResize();
1376
1381
  this.resizeHandler = () => this.onResize();
@@ -1474,8 +1479,7 @@ export class PathTracerApp extends EventDispatcher {
1474
1479
  this.stages.edgeFilter = new EdgeFilter( this.renderer, { enabled: false } );
1475
1480
  this.stages.autoExposure = new AutoExposure( this.renderer, { enabled: DEFAULT_STATE.autoExposure ?? false } );
1476
1481
 
1477
- this.stages.display = new Display( this.renderer, {
1478
- exposure: ( DEFAULT_STATE.autoExposure ) ? 1.0 : ( this.settings.get( 'exposure' ) ?? 1.0 ),
1482
+ this.stages.compositor = new Compositor( this.renderer, {
1479
1483
  saturation: this.settings.get( 'saturation' ) ?? DEFAULT_STATE.saturation,
1480
1484
  } );
1481
1485
 
@@ -1497,7 +1501,7 @@ export class PathTracerApp extends EventDispatcher {
1497
1501
  edgeFilter: this.stages.edgeFilter,
1498
1502
  ssrc: this.stages.ssrc,
1499
1503
  autoExposure: this.stages.autoExposure,
1500
- display: this.stages.display,
1504
+ compositor: this.stages.compositor,
1501
1505
  },
1502
1506
  pipeline: this.pipeline,
1503
1507
  getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
@@ -104,16 +104,16 @@ export class RenderSettings extends EventDispatcher {
104
104
  * Wires internal references. Called by PathTracerApp after init().
105
105
  *
106
106
  * @param {Object} params
107
- * @param {Object} params.stages - Pipeline stages { pathTracer, display, autoExposure, ... }
107
+ * @param {Object} params.stages - Pipeline stages { pathTracer, compositor, autoExposure, ... }
108
108
  * @param {Function} params.resetCallback - Called to reset accumulation
109
109
  * @param {Function} [params.reconcileCompletion] - Called when completion limits change
110
110
  */
111
- bind( { stages, resetCallback, reconcileCompletion } ) {
111
+ bind( { stages, renderer, resetCallback, reconcileCompletion } ) {
112
112
 
113
113
  this._pathTracer = stages.pathTracer;
114
114
  this._resetCallback = resetCallback;
115
115
  this._delegates = {};
116
- this._handlers = this._buildHandlers( stages, reconcileCompletion );
116
+ this._handlers = this._buildHandlers( stages, renderer, reconcileCompletion );
117
117
 
118
118
  }
119
119
 
@@ -121,22 +121,24 @@ export class RenderSettings extends EventDispatcher {
121
121
  * Builds handler functions for multi-stage settings that can't
122
122
  * be routed with a simple uniform forward.
123
123
  */
124
- _buildHandlers( stages, reconcileCompletion ) {
124
+ _buildHandlers( stages, renderer, reconcileCompletion ) {
125
125
 
126
126
  return {
127
127
 
128
128
  handleTransparentBackground: ( value ) => {
129
129
 
130
130
  stages.pathTracer?.setUniform( 'transparentBackground', value );
131
- stages.display?.setTransparentBackground( value );
131
+ stages.compositor?.setTransparentBackground( value );
132
132
 
133
133
  },
134
134
 
135
135
  handleExposure: ( value ) => {
136
136
 
137
- if ( ! stages.autoExposure?.enabled ) {
137
+ // Three.js applies toneMappingExposure inside the tone-mapping branch,
138
+ // so this has no effect when renderer.toneMapping === NoToneMapping.
139
+ if ( ! stages.autoExposure?.enabled && renderer ) {
138
140
 
139
- stages.display?.setExposure( value );
141
+ renderer.toneMappingExposure = value;
140
142
 
141
143
  }
142
144
 
@@ -144,7 +146,7 @@ export class RenderSettings extends EventDispatcher {
144
146
 
145
147
  handleSaturation: ( value ) => {
146
148
 
147
- stages.display?.setSaturation( value );
149
+ stages.compositor?.setSaturation( value );
148
150
 
149
151
  },
150
152
 
@@ -0,0 +1,101 @@
1
+ import { vec4, vec3, uv, uniform, select, dot, mix } from 'three/tsl';
2
+ import { MeshBasicNodeMaterial, QuadMesh, TextureNode } from 'three/webgpu';
3
+ import { NoBlending } from 'three';
4
+ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
5
+ import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
6
+
7
+ /**
8
+ * Compositor — Terminal pipeline stage.
9
+ *
10
+ * Selects the latest upstream texture via a priority fallback chain, applies
11
+ * a saturation grade, sets alpha, and hands the linear HDR result to the
12
+ * renderer's output pass (tone mapping + sRGB gamma happen there).
13
+ *
14
+ * Exposure is not applied here — `renderer.toneMappingExposure` owns it,
15
+ * and Three.js applies it inside the tone-mapping branch of the output pass
16
+ * (so it has no effect when `renderer.toneMapping === NoToneMapping`).
17
+ */
18
+ export class Compositor extends RenderStage {
19
+
20
+ constructor( renderer, options = {} ) {
21
+
22
+ super( 'Compositor', {
23
+ ...options,
24
+ executionMode: StageExecutionMode.ALWAYS
25
+ } );
26
+
27
+ this.renderer = renderer;
28
+
29
+ // 1.0 = neutral; >1 boosts to compensate for ACES/AgX desaturation.
30
+ this.saturation = uniform( options.saturation ?? 1.0 );
31
+
32
+ this._transparentBackground = uniform( 0, 'int' );
33
+
34
+ // TextureNode reused across frames — only `.value` mutates, shader doesn't recompile.
35
+ this._sourceTexNode = new TextureNode();
36
+
37
+ const texSample = this._sourceTexNode.sample( uv() );
38
+
39
+ const luma = dot( texSample.xyz, REC709_LUMINANCE_COEFFICIENTS );
40
+ const gradedColor = mix( vec3( luma ), texSample.xyz, this.saturation );
41
+
42
+ const outputAlpha = select( this._transparentBackground, texSample.w, 1.0 );
43
+
44
+ this.compositorMaterial = new MeshBasicNodeMaterial();
45
+ this.compositorMaterial.colorNode = vec4( gradedColor, outputAlpha );
46
+ this.compositorMaterial.blending = NoBlending;
47
+
48
+ this.compositorQuad = new QuadMesh( this.compositorMaterial );
49
+
50
+ }
51
+
52
+ /**
53
+ * Later stages in the chain take priority; `pathtracer:color` is the
54
+ * baseline fallback that is always present.
55
+ */
56
+ _resolveSourceTexture( context ) {
57
+
58
+ return context.getTexture( 'bloom:output' )
59
+ || context.getTexture( 'edgeFiltering:output' )
60
+ || context.getTexture( 'asvgf:output' )
61
+ || context.getTexture( 'ssrc:output' )
62
+ || context.getTexture( 'pathtracer:color' );
63
+
64
+ }
65
+
66
+ render( context ) {
67
+
68
+ if ( ! this.enabled ) return;
69
+
70
+ const sourceTexture = this._resolveSourceTexture( context );
71
+ if ( ! sourceTexture ) return;
72
+
73
+ this._sourceTexNode.value = sourceTexture;
74
+
75
+ this.renderer.setRenderTarget( null );
76
+ this.compositorQuad.render( this.renderer );
77
+
78
+ }
79
+
80
+ setSaturation( value ) {
81
+
82
+ this.saturation.value = value;
83
+
84
+ }
85
+
86
+ setTransparentBackground( enabled ) {
87
+
88
+ this._transparentBackground.value = enabled ? 1 : 0;
89
+
90
+ }
91
+
92
+ dispose() {
93
+
94
+ this._sourceTexNode?.dispose();
95
+ this.compositorMaterial?.dispose();
96
+ // QuadMesh extends Mesh — no dispose method; material already released above.
97
+ this.compositorQuad = null;
98
+
99
+ }
100
+
101
+ }
@@ -95,6 +95,14 @@ export class NormalDepth extends RenderStage {
95
95
  this._bvhStorageNode = null;
96
96
  this._matStorageNode = null;
97
97
 
98
+ // Last-seen attribute identities. PathTracer replaces these in-place
99
+ // across model load / BVH rebuild; the compute's bind group is locked
100
+ // to whatever buffer was bound at pipeline compile time, so we rebuild
101
+ // when any of them swaps to a new object.
102
+ this._lastTriAttr = null;
103
+ this._lastBvhAttr = null;
104
+ this._lastMatAttr = null;
105
+
98
106
  // Compute node — built once when storage buffers are ready
99
107
  this._computeNode = null;
100
108
  this._computeBuilt = false;
@@ -138,50 +146,59 @@ export class NormalDepth extends RenderStage {
138
146
  const pt = this.pathTracer;
139
147
  if ( ! pt ) return false;
140
148
 
141
- // Triangle storage
149
+ const matStorageAttr = pt.materialData.materialStorageAttr;
150
+
151
+ // Detect attribute identity swap (PathTracer.setTriangleData /
152
+ // setBVHData replace the attribute object on growth). The compute
153
+ // node's bind group is locked to the buffer bound at compile time —
154
+ // updating the storage node's .value alone leaves the GPU binding
155
+ // pointing at the now-discarded buffer, so every traversal misses.
156
+ const triSwapped = pt.triangleStorageAttr && pt.triangleStorageAttr !== this._lastTriAttr;
157
+ const bvhSwapped = pt.bvhStorageAttr && pt.bvhStorageAttr !== this._lastBvhAttr;
158
+ const matSwapped = matStorageAttr && matStorageAttr !== this._lastMatAttr;
159
+
160
+ if ( triSwapped || bvhSwapped || matSwapped ) {
161
+
162
+ // Drop compute + storage nodes so they get rebuilt against the
163
+ // current buffers. Cheap: this only happens on model load.
164
+ this._computeNode?.dispose?.();
165
+ this._computeNode = null;
166
+ this._computeBuilt = false;
167
+ this._triStorageNode = null;
168
+ this._bvhStorageNode = null;
169
+ this._matStorageNode = null;
170
+ this._dirty = true;
171
+
172
+ }
173
+
142
174
  if ( pt.triangleStorageAttr && ! this._triStorageNode ) {
143
175
 
144
176
  this._triStorageNode = storage(
145
177
  pt.triangleStorageAttr, 'vec4', pt.triangleStorageAttr.count
146
178
  ).toReadOnly();
147
179
 
148
- } else if ( pt.triangleStorageAttr && this._triStorageNode ) {
149
-
150
- // Data changed (new model loaded) — update in-place
151
- this._triStorageNode.value = pt.triangleStorageAttr;
152
- this._triStorageNode.bufferCount = pt.triangleStorageAttr.count;
153
-
154
180
  }
155
181
 
156
- // BVH storage
157
182
  if ( pt.bvhStorageAttr && ! this._bvhStorageNode ) {
158
183
 
159
184
  this._bvhStorageNode = storage(
160
185
  pt.bvhStorageAttr, 'vec4', pt.bvhStorageAttr.count
161
186
  ).toReadOnly();
162
187
 
163
- } else if ( pt.bvhStorageAttr && this._bvhStorageNode ) {
164
-
165
- this._bvhStorageNode.value = pt.bvhStorageAttr;
166
- this._bvhStorageNode.bufferCount = pt.bvhStorageAttr.count;
167
-
168
188
  }
169
189
 
170
- // Material storage
171
- const matStorageAttr = pt.materialData.materialStorageAttr;
172
190
  if ( matStorageAttr && ! this._matStorageNode ) {
173
191
 
174
192
  this._matStorageNode = storage(
175
193
  matStorageAttr, 'vec4', matStorageAttr.count
176
194
  ).toReadOnly();
177
195
 
178
- } else if ( matStorageAttr && this._matStorageNode ) {
179
-
180
- this._matStorageNode.value = matStorageAttr;
181
- this._matStorageNode.bufferCount = matStorageAttr.count;
182
-
183
196
  }
184
197
 
198
+ this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
199
+ this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
200
+ this._lastMatAttr = matStorageAttr || this._lastMatAttr;
201
+
185
202
  return !! ( this._triStorageNode && this._bvhStorageNode && this._matStorageNode );
186
203
 
187
204
  }
@@ -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, adaptiveSampling, edgeFilter, ssrc, autoExposure, display }
44
+ this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, adaptiveSampling, edgeFilter, ssrc, autoExposure, compositor }
45
45
 
46
46
  this._getExposure = getExposure;
47
47
  this._getSaturation = getSaturation;
@@ -295,9 +295,8 @@ export class DenoisingManager extends EventDispatcher {
295
295
  }
296
296
 
297
297
  /**
298
- * Enables/disables auto-exposure with proper exposure stacking management.
299
298
  * @param {boolean} enabled
300
- * @param {number} manualExposure - The manual exposure value to restore when disabling
299
+ * @param {number} manualExposure - Restored to renderer.toneMappingExposure when disabling.
301
300
  */
302
301
  setAutoExposureEnabled( enabled, manualExposure ) {
303
302
 
@@ -306,19 +305,10 @@ export class DenoisingManager extends EventDispatcher {
306
305
 
307
306
  s.autoExposure.enabled = enabled;
308
307
 
309
- if ( enabled ) {
310
-
311
- // Neutralize Display manual exposure to avoid stacking
312
- s.display?.setExposure( 1.0 );
313
-
314
- } else {
308
+ // AutoExposure overwrites renderer.toneMappingExposure each frame; restore manual on disable.
309
+ if ( ! enabled && this.renderer ) {
315
310
 
316
- s.display?.setExposure( manualExposure );
317
- if ( s.display && this.renderer ) {
318
-
319
- this.renderer.toneMappingExposure = 1.0;
320
-
321
- }
311
+ this.renderer.toneMappingExposure = manualExposure;
322
312
 
323
313
  }
324
314
 
@@ -417,10 +407,10 @@ export class DenoisingManager extends EventDispatcher {
417
407
 
418
408
  } else {
419
409
 
420
- // Re-render display stage so WebGPU canvas has valid content
421
- if ( this.upscaler?.enabled && this._stages.display && context ) {
410
+ // Re-render compositor stage so WebGPU canvas has valid content
411
+ if ( this.upscaler?.enabled && this._stages.compositor && context ) {
422
412
 
423
- this._stages.display.render( context );
413
+ this._stages.compositor.render( context );
424
414
 
425
415
  }
426
416
 
@@ -1,120 +0,0 @@
1
- import { vec4, vec3, uv, uniform, select, dot, mix } from 'three/tsl';
2
- import { MeshBasicNodeMaterial, QuadMesh, TextureNode } from 'three/webgpu';
3
- import { NoBlending } from 'three';
4
- import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
5
- import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
6
-
7
- /**
8
- * Display — Terminal pipeline stage for WebGPU.
9
- *
10
- * Reads the final colour texture from the pipeline context (using a
11
- * priority fallback chain), applies exposure, and renders to screen.
12
- *
13
- * When new post-processing stages are added between PathTracer and
14
- * Display, the fallback chain automatically picks up the latest output
15
- * without any wiring changes.
16
- */
17
- export class Display extends RenderStage {
18
-
19
- constructor( renderer, options = {} ) {
20
-
21
- super( 'Display', {
22
- ...options,
23
- executionMode: StageExecutionMode.ALWAYS
24
- } );
25
-
26
- this.renderer = renderer;
27
-
28
- // Exposure uniform — linear multiplier (consistent with auto-exposure)
29
- this.exposure = uniform( options.exposure ?? 1.0 );
30
-
31
- // Pre-tonemapping saturation — compensates for ACES/AgX desaturation (1.0 = neutral)
32
- this.saturation = uniform( options.saturation ?? 1.0 );
33
-
34
- // Transparent background toggle
35
- this._transparentBackground = uniform( 0, 'int' );
36
-
37
- // Updatable texture node — swap .value each frame, no shader recompile
38
- this._displayTexNode = new TextureNode();
39
-
40
- const texSample = this._displayTexNode.sample( uv() );
41
-
42
- // Build material once (TSL compiles on first render)
43
- const exposed = texSample.xyz.mul( this.exposure );
44
-
45
- // Saturation adjustment (before tonemapping): mix between luminance and color
46
- const luma = dot( exposed, REC709_LUMINANCE_COEFFICIENTS );
47
- let displayShader = mix( vec3( luma ), exposed, this.saturation );
48
-
49
- // Alpha: pass through source alpha when transparent, otherwise 1.0
50
- const outputAlpha = select( this._transparentBackground, texSample.w, 1.0 );
51
-
52
- this.displayMaterial = new MeshBasicNodeMaterial();
53
- this.displayMaterial.colorNode = vec4( displayShader, outputAlpha );
54
- this.displayMaterial.blending = NoBlending;
55
- this.displayMaterial.toneMapped = true;
56
-
57
- this.displayQuad = new QuadMesh( this.displayMaterial );
58
-
59
- }
60
-
61
- /**
62
- * Resolve the best available output texture from the pipeline context.
63
- * Later stages in the chain take priority; pathtracer:color is the
64
- * baseline fallback that is always present.
65
- */
66
- _resolveDisplayTexture( context ) {
67
-
68
- return context.getTexture( 'bloom:output' )
69
- || context.getTexture( 'edgeFiltering:output' )
70
- || context.getTexture( 'asvgf:output' )
71
- || context.getTexture( 'ssrc:output' )
72
- || context.getTexture( 'pathtracer:color' );
73
-
74
- }
75
-
76
- render( context ) {
77
-
78
- if ( ! this.enabled ) return;
79
-
80
- const displayTexture = this._resolveDisplayTexture( context );
81
-
82
- if ( ! displayTexture ) return;
83
-
84
- // Swap texture reference (no shader recompilation)
85
- this._displayTexNode.value = displayTexture;
86
-
87
- // Render to screen
88
- this.renderer.setRenderTarget( null );
89
- this.displayQuad.render( this.renderer );
90
-
91
- }
92
-
93
- setExposure( value ) {
94
-
95
- this.exposure.value = value;
96
-
97
- }
98
-
99
- setSaturation( value ) {
100
-
101
- this.saturation.value = value;
102
-
103
- }
104
-
105
- setTransparentBackground( enabled ) {
106
-
107
- this._transparentBackground.value = enabled ? 1 : 0;
108
-
109
- }
110
-
111
- dispose() {
112
-
113
- this._displayTexNode?.dispose();
114
- this.displayMaterial?.dispose();
115
- // QuadMesh extends Mesh — no dispose method; material already disposed.
116
- this.displayQuad = null;
117
-
118
- }
119
-
120
- }