rayzee 7.4.0 → 7.5.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/README.md +1 -1
- package/dist/rayzee.es.js +1028 -746
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +112 -21
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/Passes/OIDNDenoiser.js +27 -3
- package/src/Processor/AssetLoader.js +7 -1
- package/src/Processor/LightSerializer.js +56 -12
- package/src/Processor/PackedRayBuffer.js +9 -29
- package/src/Processor/StorageTexturePool.js +11 -3
- package/src/Processor/VRAMTracker.js +50 -8
- package/src/Processor/blackbody.js +74 -0
- package/src/Stages/ASVGF.js +27 -0
- package/src/Stages/BilateralFilter.js +12 -0
- package/src/Stages/EdgeFilter.js +12 -0
- package/src/Stages/MotionVector.js +10 -0
- package/src/Stages/NormalDepth.js +10 -0
- package/src/Stages/PathTracer.js +4 -2
- package/src/Stages/PathTracerStage.js +2 -2
- package/src/Stages/SSRC.js +16 -0
- package/src/Stages/Variance.js +13 -0
- package/src/TSL/GenerateKernel.js +1 -3
- package/src/TSL/LightsCore.js +139 -12
- package/src/TSL/LightsSampling.js +210 -17
- package/src/TSL/ShadeKernel.js +2 -9
- package/src/managers/DenoisingManager.js +10 -0
- package/src/managers/LightManager.js +26 -4
- package/src/managers/UniformManager.js +1 -1
package/package.json
CHANGED
|
@@ -137,6 +137,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
137
137
|
|
|
138
138
|
// Track in-flight tile staging buffers so they can be destroyed on abort
|
|
139
139
|
this._pendingStagingBuffers = new Set();
|
|
140
|
+
// Per-run tile-blit promises; done() awaits these so capture waits for every tile to paint.
|
|
141
|
+
this._pendingTileBlits = [];
|
|
140
142
|
|
|
141
143
|
this.currentTZAUrl = null;
|
|
142
144
|
this.unet = null;
|
|
@@ -629,6 +631,9 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
629
631
|
|
|
630
632
|
let abortDenoise = null;
|
|
631
633
|
|
|
634
|
+
// Fresh per-run list of tile-blit promises (the progress callback appends to it).
|
|
635
|
+
this._pendingTileBlits = [];
|
|
636
|
+
|
|
632
637
|
const abortHandler = () => {
|
|
633
638
|
|
|
634
639
|
if ( abortDenoise ) {
|
|
@@ -653,7 +658,23 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
653
658
|
|
|
654
659
|
try {
|
|
655
660
|
|
|
656
|
-
|
|
661
|
+
if ( this._pendingTileBlits.length > 0 ) {
|
|
662
|
+
|
|
663
|
+
// Normal path: the progress callback already painted every tile progressively, with
|
|
664
|
+
// the same exposure/saturation/tonemap/sRGB math the full-frame readback uses (verified:
|
|
665
|
+
// tiles tile the image exactly, no overlap). Just wait for those blits — no redundant
|
|
666
|
+
// full-frame re-read + re-tonemap.
|
|
667
|
+
await Promise.allSettled( this._pendingTileBlits );
|
|
668
|
+
|
|
669
|
+
} else {
|
|
670
|
+
|
|
671
|
+
// Degenerate fallback (no per-tile progress was emitted): one authoritative full paint.
|
|
672
|
+
await this._displayGPUOutput( output );
|
|
673
|
+
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// DENOISING_END (which gates screenshot/video capture) fires only after this resolves,
|
|
677
|
+
// so the captured canvas is always complete.
|
|
657
678
|
resolve();
|
|
658
679
|
|
|
659
680
|
} catch ( err ) {
|
|
@@ -703,8 +724,9 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
703
724
|
|
|
704
725
|
device.queue.submit( [ enc.finish() ] );
|
|
705
726
|
|
|
706
|
-
// Map and blit asynchronously — GPU copy is already queued
|
|
707
|
-
|
|
727
|
+
// Map and blit asynchronously — GPU copy is already queued. Track the whole chain so
|
|
728
|
+
// done() can await every tile paint before resolving (replaces the full-frame readback).
|
|
729
|
+
const tileBlit = staging.mapAsync( GPUMapMode.READ ).then( () => {
|
|
708
730
|
|
|
709
731
|
const f32 = new Float32Array( staging.getMappedRange() );
|
|
710
732
|
const tileImageData = new ImageData( clampedW, clampedH );
|
|
@@ -766,6 +788,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
766
788
|
|
|
767
789
|
} );
|
|
768
790
|
|
|
791
|
+
this._pendingTileBlits.push( tileBlit );
|
|
792
|
+
|
|
769
793
|
}
|
|
770
794
|
} );
|
|
771
795
|
|
|
@@ -1446,12 +1446,18 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1446
1446
|
|
|
1447
1447
|
if ( userData.type === 'RectAreaLight' ) {
|
|
1448
1448
|
|
|
1449
|
+
// Intensity is authored in Watts (Blender-style); emitted radiance
|
|
1450
|
+
// is computed at render time as power/(π·area). Blender emission
|
|
1451
|
+
// defaults: power-normalized, full Lambertian spread (π), rectangle.
|
|
1449
1452
|
const light = new RectAreaLight(
|
|
1450
1453
|
new Color( ...userData.color ),
|
|
1451
|
-
userData.intensity * 0.1
|
|
1454
|
+
userData.intensity * 0.1,
|
|
1452
1455
|
userData.width,
|
|
1453
1456
|
userData.height
|
|
1454
1457
|
);
|
|
1458
|
+
light.userData.normalize = userData.normalize ?? true;
|
|
1459
|
+
light.userData.spread = Number.isFinite( userData.spread ) ? userData.spread : Math.PI;
|
|
1460
|
+
light.userData.shape = userData.shape ?? 'rectangle';
|
|
1455
1461
|
light.position.z = - 2;
|
|
1456
1462
|
light.name = userData.name;
|
|
1457
1463
|
object.add( light );
|
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import { Vector3, Quaternion } from 'three';
|
|
2
|
+
import { blackbodyToLinearRGB } from './blackbody.js';
|
|
3
|
+
|
|
4
|
+
// Point & spot lights specify Power in Watts; the shader uses radiant intensity
|
|
5
|
+
// (W/sr) as `intensity / dist²`, so convert with I = P / 4π (isotropic emitter),
|
|
6
|
+
// matching Blender. Area lights are already radiant power; the Sun is irradiance.
|
|
7
|
+
const INV_4PI = 1 / ( 4 * Math.PI );
|
|
8
|
+
|
|
9
|
+
// Effective emission colour + intensity after per-light Exposure (EV stops) and
|
|
10
|
+
// optional blackbody Temperature tint (Blender-style). Both fold into the values
|
|
11
|
+
// already sent to the GPU — no shader or struct change needed.
|
|
12
|
+
function effectiveEmission( light ) {
|
|
13
|
+
|
|
14
|
+
const ud = light.userData || {};
|
|
15
|
+
const exposure = Number.isFinite( ud.exposure ) ? ud.exposure : 0;
|
|
16
|
+
const intensity = light.intensity * Math.pow( 2, exposure );
|
|
17
|
+
|
|
18
|
+
let r = light.color.r, g = light.color.g, b = light.color.b;
|
|
19
|
+
if ( ud.useTemperature ) {
|
|
20
|
+
|
|
21
|
+
const [ tr, tg, tb ] = blackbodyToLinearRGB( ud.temperature ?? 6500 );
|
|
22
|
+
r *= tr; g *= tg; b *= tb;
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { r, g, b, intensity };
|
|
27
|
+
|
|
28
|
+
}
|
|
2
29
|
|
|
3
30
|
export class LightSerializer {
|
|
4
31
|
|
|
@@ -90,6 +117,8 @@ export class LightSerializer {
|
|
|
90
117
|
// Get angle parameter from light (default to 0 for sharp shadows)
|
|
91
118
|
const angle = light.userData.angle || light.angle || 0.0; // In radians
|
|
92
119
|
|
|
120
|
+
const em = effectiveEmission( light );
|
|
121
|
+
|
|
93
122
|
// Optional projection mask. Sign of intensity carries the inverted flag.
|
|
94
123
|
const gobo = light.userData?.gobo;
|
|
95
124
|
const goboIndex = ( gobo && Number.isInteger( gobo.index ) ) ? gobo.index : - 1;
|
|
@@ -101,8 +130,8 @@ export class LightSerializer {
|
|
|
101
130
|
this.directionalLightCache.push( {
|
|
102
131
|
data: [
|
|
103
132
|
direction.x, direction.y, direction.z, // direction toward light (3)
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
134
|
+
em.intensity, // intensity (1) — incl. exposure
|
|
106
135
|
angle, // angular diameter in radians (1)
|
|
107
136
|
goboIndex, // gobo layer index, -1 = none (1)
|
|
108
137
|
goboIntensity, // signed gobo strength (1)
|
|
@@ -133,14 +162,25 @@ export class LightSerializer {
|
|
|
133
162
|
// Calculate importance for sorting
|
|
134
163
|
const importance = this.calculateLightImportance( light, 'area' );
|
|
135
164
|
|
|
136
|
-
//
|
|
165
|
+
// Blender-style emission controls (stored on userData; sensible defaults).
|
|
166
|
+
// normalize ON → radiance ∝ 1/area (resizing keeps total power constant).
|
|
167
|
+
const normalize = ( light.userData?.normalize ?? true ) ? 1.0 : 0.0;
|
|
168
|
+
const spread = Number.isFinite( light.userData?.spread ) ? light.userData.spread : Math.PI;
|
|
169
|
+
const shape = ( light.userData?.shape === 'ellipse' || light.userData?.shape === 'disk' || light.userData?.shape === 1 ) ? 1.0 : 0.0;
|
|
170
|
+
|
|
171
|
+
const em = effectiveEmission( light );
|
|
172
|
+
|
|
173
|
+
// Store in cache with importance (16 floats, vec4-aligned)
|
|
137
174
|
this.areaLightCache.push( {
|
|
138
175
|
data: [
|
|
139
176
|
position.x, position.y, position.z, // position (3)
|
|
140
|
-
u.x, u.y, u.z, // u vector (3)
|
|
141
|
-
v.x, v.y, v.z, // v vector (3)
|
|
142
|
-
|
|
143
|
-
|
|
177
|
+
u.x, u.y, u.z, // u half-vector (3)
|
|
178
|
+
v.x, v.y, v.z, // v half-vector (3)
|
|
179
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
180
|
+
em.intensity, // radiant power in Watts (1) — incl. exposure
|
|
181
|
+
normalize, // power-normalize flag (1)
|
|
182
|
+
spread, // emission spread in radians (1)
|
|
183
|
+
shape, // 0 = rect, 1 = disk/ellipse (1)
|
|
144
184
|
],
|
|
145
185
|
importance: importance,
|
|
146
186
|
light: light
|
|
@@ -159,12 +199,14 @@ export class LightSerializer {
|
|
|
159
199
|
// Calculate importance for sorting
|
|
160
200
|
const importance = this.calculateLightImportance( light, 'point' );
|
|
161
201
|
|
|
202
|
+
const em = effectiveEmission( light );
|
|
203
|
+
|
|
162
204
|
// Store in cache with importance
|
|
163
205
|
this.pointLightCache.push( {
|
|
164
206
|
data: [
|
|
165
207
|
position.x, position.y, position.z, // position (3)
|
|
166
|
-
|
|
167
|
-
|
|
208
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
209
|
+
em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
|
|
168
210
|
light.distance || 0.0, // cutoff distance (0 = infinite) (1)
|
|
169
211
|
light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
|
|
170
212
|
],
|
|
@@ -198,13 +240,15 @@ export class LightSerializer {
|
|
|
198
240
|
const iesIndex = ( ies && Number.isInteger( ies.index ) ) ? ies.index : - 1;
|
|
199
241
|
const iesIntensity = ( ies && typeof ies.intensity === 'number' ) ? ies.intensity : 1.0;
|
|
200
242
|
|
|
243
|
+
const em = effectiveEmission( light );
|
|
244
|
+
|
|
201
245
|
// Store in cache with importance
|
|
202
246
|
this.spotLightCache.push( {
|
|
203
247
|
data: [
|
|
204
248
|
position.x, position.y, position.z, // position (3)
|
|
205
249
|
direction.x, direction.y, direction.z, // direction (3)
|
|
206
|
-
|
|
207
|
-
|
|
250
|
+
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
251
|
+
em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
|
|
208
252
|
light.angle || Math.PI / 4, // cone half-angle in radians (1)
|
|
209
253
|
light.penumbra || 0.0, // penumbra [0,1] (1)
|
|
210
254
|
light.distance || 0.0, // cutoff distance (0 = infinite) (1)
|
|
@@ -290,7 +334,7 @@ export class LightSerializer {
|
|
|
290
334
|
|
|
291
335
|
// Divide flat array lengths by per-light stride to get actual light counts
|
|
292
336
|
const directionalCount = Math.floor( this.lightData.directional.length / 12 );
|
|
293
|
-
const areaCount = Math.floor( this.lightData.rectArea.length /
|
|
337
|
+
const areaCount = Math.floor( this.lightData.rectArea.length / 16 );
|
|
294
338
|
const pointCount = Math.floor( this.lightData.point.length / 9 );
|
|
295
339
|
const spotCount = Math.floor( this.lightData.spot.length / 20 );
|
|
296
340
|
|
|
@@ -11,13 +11,12 @@ import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
|
11
11
|
|
|
12
12
|
export const RAY_STRIDE = 7;
|
|
13
13
|
export const HIT_STRIDE = 2;
|
|
14
|
-
// Per-pixel G-buffer (first-hit MRT staging):
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// lane 1 — primary-hit surface ID for A-SVGF correlated-gradient re-projection (Tier 1); written at the
|
|
18
|
-
// bounce-0 hit, valid=0 on miss (Generate inits): .x=triIndex .y=meshIndex .z=packUnorm2x16(bary.u,bary.v) .w=valid
|
|
14
|
+
// Per-pixel G-buffer (first-hit MRT staging): 1 uvec4/pixel — half-packed normal/depth/albedo
|
|
15
|
+
// (pack2x16, no f32 bitcast); read by FinalWrite:
|
|
16
|
+
// .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg) .w=packUnorm2x16(albedo.b, 0)
|
|
19
17
|
// Separate buffer from RAY (per-pixel, not per-ray×S) — written by Generate/Shade bounce-0.
|
|
20
|
-
|
|
18
|
+
// (A second lane reserved for an A-SVGF Tier-1 surface ID was removed — write-only, never read.)
|
|
19
|
+
export const GBUFFER_STRIDE = 1;
|
|
21
20
|
|
|
22
21
|
export const RAY = {
|
|
23
22
|
ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex == rayID
|
|
@@ -152,36 +151,17 @@ export const readRayRadiance = ( buf, id ) =>
|
|
|
152
151
|
// ── Per-pixel G-buffer (first-hit MRT). 2 uvec4/pixel (AoS), pack2x16 lanes. ──
|
|
153
152
|
// normal: raw unit vec3; depth: linear [0,1]; albedo: vec3 [0,1]. Packed values live in u32 lanes
|
|
154
153
|
// verbatim (no f32 bitcast) so NaN-range bit patterns (snorm ±1 → 0x7FFF) survive store/load intact.
|
|
155
|
-
//
|
|
156
|
-
const gbLane = ( pixelIndex
|
|
157
|
-
|
|
158
|
-
const base = uint( pixelIndex ).mul( GBUFFER_STRIDE );
|
|
159
|
-
return lane === 0 ? base : base.add( lane );
|
|
160
|
-
|
|
161
|
-
};
|
|
154
|
+
// One uvec4 per pixel (stride 1); AoS base = pixelIndex * GBUFFER_STRIDE.
|
|
155
|
+
const gbLane = ( pixelIndex ) => uint( pixelIndex ).mul( GBUFFER_STRIDE );
|
|
162
156
|
|
|
163
157
|
export const writeGBuffer = ( buf, pixelIndex, normal, depth, albedo ) =>
|
|
164
|
-
buf.element( gbLane( pixelIndex
|
|
158
|
+
buf.element( gbLane( pixelIndex ) ).assign( uvec4(
|
|
165
159
|
packSnorm2x16( vec2( normal.x, normal.y ) ),
|
|
166
160
|
packSnorm2x16( vec2( normal.z, depth ) ),
|
|
167
161
|
packUnorm2x16( vec2( albedo.x, albedo.y ) ),
|
|
168
162
|
packUnorm2x16( vec2( albedo.z, 0.0 ) ),
|
|
169
163
|
) );
|
|
170
|
-
export const readGBuffer = ( buf, pixelIndex ) => buf.element( gbLane( pixelIndex
|
|
171
|
-
|
|
172
|
-
// Lane 1 — primary-hit surface ID for A-SVGF correlated gradient re-projection (Tier 1).
|
|
173
|
-
// valid=0 marks a miss (no primary surface); bary packed unorm (both in [0,1]).
|
|
174
|
-
export const writeGBufferSurfaceID = ( buf, pixelIndex, triIndex, meshIndex, baryU, baryV, valid ) =>
|
|
175
|
-
buf.element( gbLane( pixelIndex, 1 ) ).assign( uvec4(
|
|
176
|
-
uint( triIndex ), uint( meshIndex ), packUnorm2x16( vec2( baryU, baryV ) ), uint( valid ),
|
|
177
|
-
) );
|
|
178
|
-
export const readGBufferSurfaceID = ( buf, pixelIndex ) => {
|
|
179
|
-
|
|
180
|
-
const p = buf.element( gbLane( pixelIndex, 1 ) );
|
|
181
|
-
const bary = unpackUnorm2x16( p.z );
|
|
182
|
-
return { triIndex: p.x, meshIndex: p.y, baryU: bary.x, baryV: bary.y, valid: p.w };
|
|
183
|
-
|
|
184
|
-
};
|
|
164
|
+
export const readGBuffer = ( buf, pixelIndex ) => buf.element( gbLane( pixelIndex ) );
|
|
185
165
|
|
|
186
166
|
// Decode for FinalWrite. normalDepth.xyz matches the prior path (normal*0.5+0.5), .w = raw depth.
|
|
187
167
|
export const gbDecodeNormalDepth = ( packed ) => {
|
|
@@ -141,16 +141,24 @@ export class StorageTexturePool {
|
|
|
141
141
|
* Copy StorageTextures → RenderTarget textures via GPU copy.
|
|
142
142
|
* Must be called after each compute dispatch.
|
|
143
143
|
* @param {WebGPURenderer} renderer
|
|
144
|
+
* @param {boolean} [includeAux=true] Copy the normalDepth + albedo attachments too. When no
|
|
145
|
+
* denoiser/OIDN consumes the aux MRT (default interactive), the wavefront doesn't write those
|
|
146
|
+
* StorageTextures, so skipping their copies saves two full-res GPU copies/frame. Color is always copied.
|
|
144
147
|
*/
|
|
145
|
-
copyToReadTargets( renderer ) {
|
|
148
|
+
copyToReadTargets( renderer, includeAux = true ) {
|
|
146
149
|
|
|
147
150
|
// Source write StorageTextures are over-allocated at the max size; the Box2 region
|
|
148
151
|
// restricts the copy to the active render size so source and destination extents match.
|
|
149
152
|
this._srcRegion.max.set( this.renderWidth, this.renderHeight );
|
|
150
153
|
|
|
151
154
|
renderer.copyTextureToTexture( this.writeColor, this.readTarget.textures[ 0 ], this._srcRegion );
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
|
|
156
|
+
if ( includeAux ) {
|
|
157
|
+
|
|
158
|
+
renderer.copyTextureToTexture( this.writeNormalDepth, this.readTarget.textures[ 1 ], this._srcRegion );
|
|
159
|
+
renderer.copyTextureToTexture( this.writeAlbedo, this.readTarget.textures[ 2 ], this._srcRegion );
|
|
160
|
+
|
|
161
|
+
}
|
|
154
162
|
|
|
155
163
|
}
|
|
156
164
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VRAMTracker.js — current/peak GPU memory accounting.
|
|
3
3
|
*
|
|
4
|
-
* Measures
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Measures live GPU bytes: buffer attributes by backing-array byteLength, textures by
|
|
5
|
+
* dims × format/type — but ONLY when the backend actually holds a GPUTexture. three.js
|
|
6
|
+
* allocates a texture's GPUTexture lazily on first use, so a constructed-but-never-dispatched
|
|
7
|
+
* StorageTexture (e.g. a disabled stage's render targets) costs 0 here even though its JS
|
|
8
|
+
* `image` dimensions are set. Residency probing needs a renderer (constructor arg or
|
|
9
|
+
* setRenderer); without one it falls back to counting by dimensions (legacy behavior).
|
|
10
|
+
* Providers are thunks that read current state, so they survive reallocation (resize,
|
|
11
|
+
* scene/material/env reload). A per-pass WeakSet dedupes by resource identity, so
|
|
12
|
+
* overlapping registrations never double-count.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import {
|
|
@@ -60,15 +64,23 @@ export function textureBytes( tex ) {
|
|
|
60
64
|
|
|
61
65
|
export class VRAMTracker {
|
|
62
66
|
|
|
63
|
-
constructor() {
|
|
67
|
+
constructor( renderer = null ) {
|
|
64
68
|
|
|
65
69
|
this._providers = [];
|
|
70
|
+
this._renderer = renderer;
|
|
66
71
|
this.current = 0;
|
|
67
72
|
this.peak = 0;
|
|
68
73
|
this.byCategory = {};
|
|
69
74
|
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
/** Late-bind the renderer so texture accounting can probe actual GPU residency. */
|
|
78
|
+
setRenderer( renderer ) {
|
|
79
|
+
|
|
80
|
+
this._renderer = renderer;
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
/**
|
|
73
85
|
* @param {string} category - grouping label in the report
|
|
74
86
|
* @param {Function} fn - returns a resource or array of resources: buffer
|
|
@@ -138,12 +150,12 @@ export class VRAMTracker {
|
|
|
138
150
|
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
// texture / render target — dedupe by object identity
|
|
153
|
+
// texture / render target — dedupe by object identity; count only GPU-resident bytes
|
|
142
154
|
if ( r.isRenderTarget || r.isTexture ) {
|
|
143
155
|
|
|
144
156
|
if ( seen.has( r ) ) return 0;
|
|
145
157
|
seen.add( r );
|
|
146
|
-
return
|
|
158
|
+
return this._residentTextureBytes( r );
|
|
147
159
|
|
|
148
160
|
}
|
|
149
161
|
|
|
@@ -151,6 +163,36 @@ export class VRAMTracker {
|
|
|
151
163
|
|
|
152
164
|
}
|
|
153
165
|
|
|
166
|
+
// three.js allocates a texture's GPUTexture lazily on first dispatch; a never-used StorageTexture
|
|
167
|
+
// (disabled stage) has none. Count only when the backend holds a real GPUTexture so the report is
|
|
168
|
+
// resident VRAM, not JS-declared dimensions. No renderer bound → assume resident (legacy behavior).
|
|
169
|
+
_isResident( tex ) {
|
|
170
|
+
|
|
171
|
+
const backend = this._renderer?.backend;
|
|
172
|
+
if ( ! backend || typeof backend.get !== 'function' ) return true;
|
|
173
|
+
if ( typeof backend.has === 'function' && ! backend.has( tex ) ) return false;
|
|
174
|
+
const data = backend.get( tex );
|
|
175
|
+
return !! ( data && ( data.texture || data.gpuTexture ) );
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// RenderTarget bytes are attributed to its underlying texture(s); count each only if resident.
|
|
180
|
+
_residentTextureBytes( tex ) {
|
|
181
|
+
|
|
182
|
+
if ( tex.isRenderTarget ) {
|
|
183
|
+
|
|
184
|
+
const list = tex.textures?.length ? tex.textures : [ tex.texture ];
|
|
185
|
+
const w = tex.width || 0, h = tex.height || 0, d = tex.depth || 1;
|
|
186
|
+
let sum = 0;
|
|
187
|
+
for ( const t of list ) if ( t && this._isResident( t ) ) sum += w * h * d * texelBytes( t );
|
|
188
|
+
return sum;
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return this._isResident( tex ) ? textureBytes( tex ) : 0;
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
154
196
|
/** Drop the high-water mark to the current value (call when a new render begins). */
|
|
155
197
|
resetPeak() {
|
|
156
198
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Blackbody temperature → linear Rec.709 (= linear sRGB) RGB.
|
|
2
|
+
//
|
|
3
|
+
// Ported verbatim from Blender/Cycles `svm_math_blackbody_color_rec709`
|
|
4
|
+
// (Lukas Stockner's 7-segment rational/cubic fit; coefficients from
|
|
5
|
+
// intern/cycles/kernel/tables.h). The fit is luminance-balanced — the Rec.709
|
|
6
|
+
// luminance of the returned colour is ≈1.0 across the whole range — so it can be
|
|
7
|
+
// multiplied straight into a light's colour without altering its power: the
|
|
8
|
+
// temperature shifts hue only, brightness stays governed by intensity. This is
|
|
9
|
+
// exactly how Cycles applies it (raw multiply into the light colour, no
|
|
10
|
+
// renormalisation).
|
|
11
|
+
|
|
12
|
+
const BB_R = [
|
|
13
|
+
[ 1.61919106e3, - 2.05010916e-3, 5.02995757e0 ],
|
|
14
|
+
[ 2.48845471e3, - 1.11330907e-3, 3.22621544e0 ],
|
|
15
|
+
[ 3.34143193e3, - 4.86551192e-4, 1.76486769e0 ],
|
|
16
|
+
[ 4.09461742e3, - 1.27446582e-4, 7.25731635e-1 ],
|
|
17
|
+
[ 4.67028036e3, 2.91258199e-5, 1.26703442e-1 ],
|
|
18
|
+
[ 4.59509185e3, 2.87495649e-5, 1.50345020e-1 ],
|
|
19
|
+
[ 3.78717450e3, 9.35907826e-6, 3.99075871e-1 ],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const BB_G = [
|
|
23
|
+
[ - 4.88999748e2, 6.04330754e-4, - 7.55807526e-2 ],
|
|
24
|
+
[ - 7.55994277e2, 3.16730098e-4, 4.78306139e-1 ],
|
|
25
|
+
[ - 1.02363977e3, 1.20223470e-4, 9.36662319e-1 ],
|
|
26
|
+
[ - 1.26571316e3, 4.87340896e-6, 1.27054498e0 ],
|
|
27
|
+
[ - 1.42529332e3, - 4.01150431e-5, 1.43972784e0 ],
|
|
28
|
+
[ - 1.17554822e3, - 2.16378048e-5, 1.30408023e0 ],
|
|
29
|
+
[ - 5.00799571e2, - 4.59832026e-6, 1.09098763e0 ],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const BB_B = [
|
|
33
|
+
[ 5.96945309e-11, - 4.85742887e-8, - 9.70622247e-5, - 4.07936148e-3 ],
|
|
34
|
+
[ 2.40430366e-11, 5.55021075e-8, - 1.98503712e-4, 2.89312858e-2 ],
|
|
35
|
+
[ - 1.40949732e-11, 1.89878968e-7, - 3.56632824e-4, 9.10767778e-2 ],
|
|
36
|
+
[ - 3.61460868e-11, 2.84822009e-7, - 4.93211319e-4, 1.56723440e-1 ],
|
|
37
|
+
[ - 1.97075738e-11, 1.75359352e-7, - 2.50542825e-4, - 2.22783266e-2 ],
|
|
38
|
+
[ - 1.61997957e-13, - 1.64216008e-8, 3.86216271e-4, - 7.38077418e-1 ],
|
|
39
|
+
[ 6.72650283e-13, - 2.73078809e-8, 4.24098264e-4, - 7.52335691e-1 ],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a colour temperature in Kelvin to linear Rec.709 / linear-sRGB RGB.
|
|
44
|
+
* Negative out-of-gamut channels (very low temperatures) are clamped to 0.
|
|
45
|
+
* @param {number} kelvin
|
|
46
|
+
* @returns {[number, number, number]}
|
|
47
|
+
*/
|
|
48
|
+
export function blackbodyToLinearRGB( kelvin ) {
|
|
49
|
+
|
|
50
|
+
let r, g, b;
|
|
51
|
+
|
|
52
|
+
if ( kelvin >= 12000 ) {
|
|
53
|
+
|
|
54
|
+
r = 0.8262954810464208; g = 0.9945080501520986; b = 1.566307710274283;
|
|
55
|
+
|
|
56
|
+
} else if ( kelvin < 800 ) {
|
|
57
|
+
|
|
58
|
+
r = 5.413294490189271; g = - 0.20319390035873933; b = - 0.0822535242887164;
|
|
59
|
+
|
|
60
|
+
} else {
|
|
61
|
+
|
|
62
|
+
const i = kelvin >= 6365 ? 6 : kelvin >= 3315 ? 5 : kelvin >= 1902 ? 4
|
|
63
|
+
: kelvin >= 1449 ? 3 : kelvin >= 1167 ? 2 : kelvin >= 965 ? 1 : 0;
|
|
64
|
+
const cr = BB_R[ i ], cg = BB_G[ i ], cb = BB_B[ i ];
|
|
65
|
+
const tInv = 1 / kelvin;
|
|
66
|
+
r = cr[ 0 ] * tInv + cr[ 1 ] * kelvin + cr[ 2 ];
|
|
67
|
+
g = cg[ 0 ] * tInv + cg[ 1 ] * kelvin + cg[ 2 ];
|
|
68
|
+
b = ( ( cb[ 0 ] * kelvin + cb[ 1 ] ) * kelvin + cb[ 2 ] ) * kelvin + cb[ 3 ];
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [ Math.max( 0, r ), Math.max( 0, g ), Math.max( 0, b ) ];
|
|
73
|
+
|
|
74
|
+
}
|
package/src/Stages/ASVGF.js
CHANGED
|
@@ -832,6 +832,33 @@ export class ASVGF extends RenderStage {
|
|
|
832
832
|
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
+
// Free the over-allocated 2048² StorageTextures' GPU memory when the stage is disabled, so toggling
|
|
836
|
+
// the denoiser off reclaims VRAM. The JS texture objects + compiled compute nodes are kept; three.js
|
|
837
|
+
// lazily re-creates each GPUTexture on the next dispatch after re-enable. History re-anchors via the
|
|
838
|
+
// warm reset (the RTs are render-res, not 2048² — left alone).
|
|
839
|
+
releaseGPUMemory() {
|
|
840
|
+
|
|
841
|
+
this._temporalTexA?.dispose();
|
|
842
|
+
this._temporalTexB?.dispose();
|
|
843
|
+
this._outputModulatedTex?.dispose();
|
|
844
|
+
this._gradientStorageTex?.dispose();
|
|
845
|
+
this._heatmapStorageTex?.dispose();
|
|
846
|
+
// Render-res RT textures (sized to render resolution → meaningful VRAM at high res). Dispose the
|
|
847
|
+
// .texture, not the RenderTarget (RT.dispose() doesn't free the backing GPUTexture here); these RTs
|
|
848
|
+
// are copyTextureToTexture targets, so three.js reallocates on the next dispatch after re-enable.
|
|
849
|
+
// Drop the published context outputs first so the Compositor fallback (reads asvgf:output) can't
|
|
850
|
+
// sample a freed texture; render() re-publishes on re-enable.
|
|
851
|
+
this.context?.removeTexture( 'asvgf:demodulated' );
|
|
852
|
+
this.context?.removeTexture( 'asvgf:output' );
|
|
853
|
+
this.context?.removeTexture( 'asvgf:gradient' );
|
|
854
|
+
this._demodulatedRT?.texture?.dispose();
|
|
855
|
+
this._outputRT?.texture?.dispose();
|
|
856
|
+
this._gradientRT?.texture?.dispose();
|
|
857
|
+
this.heatmapTarget?.texture?.dispose();
|
|
858
|
+
this.resetTemporalData();
|
|
859
|
+
|
|
860
|
+
}
|
|
861
|
+
|
|
835
862
|
dispose() {
|
|
836
863
|
|
|
837
864
|
this._gradientNode?.dispose();
|
|
@@ -367,6 +367,18 @@ export class BilateralFilter extends RenderStage {
|
|
|
367
367
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
// Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
|
|
371
|
+
// after re-enable (no temporal state to re-anchor). See ASVGF.releaseGPUMemory.
|
|
372
|
+
releaseGPUMemory() {
|
|
373
|
+
|
|
374
|
+
this._storageTexA?.dispose();
|
|
375
|
+
this._storageTexB?.dispose();
|
|
376
|
+
// Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
|
|
377
|
+
this.context?.removeTexture( 'bilateralFiltering:output' );
|
|
378
|
+
this._outputTarget?.texture?.dispose();
|
|
379
|
+
|
|
380
|
+
}
|
|
381
|
+
|
|
370
382
|
reset() {
|
|
371
383
|
|
|
372
384
|
// No temporal state to reset
|
package/src/Stages/EdgeFilter.js
CHANGED
|
@@ -267,6 +267,18 @@ export class EdgeFilter extends RenderStage {
|
|
|
267
267
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
// Free the 2048² StorageTexture when disabled; three.js re-creates it on the next dispatch
|
|
271
|
+
// after re-enable, and reset() clears the iteration history. See ASVGF.releaseGPUMemory.
|
|
272
|
+
releaseGPUMemory() {
|
|
273
|
+
|
|
274
|
+
this._outputStorageTex?.dispose();
|
|
275
|
+
// Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
|
|
276
|
+
this.context?.removeTexture( 'edgeFiltering:output' );
|
|
277
|
+
this.outputTarget?.texture?.dispose();
|
|
278
|
+
this.reset();
|
|
279
|
+
|
|
280
|
+
}
|
|
281
|
+
|
|
270
282
|
reset() {
|
|
271
283
|
|
|
272
284
|
this._iterations = 0;
|
|
@@ -495,6 +495,16 @@ export class MotionVector extends RenderStage {
|
|
|
495
495
|
* Reset — intentionally does NOT reset matricesInitialized.
|
|
496
496
|
* Motion vectors must track camera motion across pipeline resets.
|
|
497
497
|
*/
|
|
498
|
+
// Free the 2048² StorageTextures when disabled (no consumer); three.js re-creates them on the next
|
|
499
|
+
// dispatch after re-enable. _syncGBufferStages additionally reseeds camera history on re-enable.
|
|
500
|
+
releaseGPUMemory() {
|
|
501
|
+
|
|
502
|
+
this._screenSpaceStorageTex?.dispose();
|
|
503
|
+
this._worldSpaceStorageTex?.dispose();
|
|
504
|
+
this.reset();
|
|
505
|
+
|
|
506
|
+
}
|
|
507
|
+
|
|
498
508
|
reset() {
|
|
499
509
|
|
|
500
510
|
if ( ! this.matricesInitialized ) {
|
|
@@ -367,6 +367,16 @@ export class NormalDepth extends RenderStage {
|
|
|
367
367
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
// Free the 2048² StorageTextures when disabled (no consumer); three.js re-creates them on the next
|
|
371
|
+
// dispatch after re-enable, and reset() re-arms the dirty/history fast-path. See ASVGF.releaseGPUMemory.
|
|
372
|
+
releaseGPUMemory() {
|
|
373
|
+
|
|
374
|
+
this._outputStorageTex?.dispose();
|
|
375
|
+
this._shadingStorageTex?.dispose();
|
|
376
|
+
this.reset();
|
|
377
|
+
|
|
378
|
+
}
|
|
379
|
+
|
|
370
380
|
reset() {
|
|
371
381
|
|
|
372
382
|
this._dirty = true;
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -64,7 +64,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
64
64
|
|
|
65
65
|
// VRAM accounting — providers are thunks reading CURRENT live resources,
|
|
66
66
|
// so they survive buffer/texture reallocation (resize, scene/material reload).
|
|
67
|
-
this.vramTracker = new VRAMTracker();
|
|
67
|
+
this.vramTracker = new VRAMTracker( this.renderer );
|
|
68
68
|
this._registerVRAMProviders();
|
|
69
69
|
|
|
70
70
|
console.log( 'PathTracer: initialized (wavefront)' );
|
|
@@ -322,7 +322,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
322
322
|
|
|
323
323
|
this._maybeReadbackCounters();
|
|
324
324
|
|
|
325
|
-
|
|
325
|
+
// Skip the normalDepth/albedo copies when aux is off — the wavefront didn't write them and
|
|
326
|
+
// no stage reads them; saves two full-res GPU copies/frame in the default interactive path.
|
|
327
|
+
this.storageTextures.copyToReadTargets( this.renderer, this._auxGBufferEnabled );
|
|
326
328
|
|
|
327
329
|
const readTex = this.storageTextures.getReadTextures();
|
|
328
330
|
if ( context ) this._publishTexturesToContext( context, readTex );
|
|
@@ -571,11 +571,11 @@ export class PathTracerStage extends RenderStage {
|
|
|
571
571
|
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
// Area lights (
|
|
574
|
+
// Area lights (16 floats per light — 13 base + normalize/spread/shape)
|
|
575
575
|
if ( this.areaLightsData && this.areaLightsData.length > 0 ) {
|
|
576
576
|
|
|
577
577
|
this.areaLightsBufferNode.array = Array.from( this.areaLightsData );
|
|
578
|
-
this.numAreaLights.value = Math.floor( this.areaLightsData.length /
|
|
578
|
+
this.numAreaLights.value = Math.floor( this.areaLightsData.length / 16 );
|
|
579
579
|
|
|
580
580
|
} else {
|
|
581
581
|
|
package/src/Stages/SSRC.js
CHANGED
|
@@ -171,6 +171,22 @@ export class SSRC extends RenderStage {
|
|
|
171
171
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
|
|
175
|
+
// after re-enable, and reset() clears the temporal cache. See ASVGF.releaseGPUMemory.
|
|
176
|
+
releaseGPUMemory() {
|
|
177
|
+
|
|
178
|
+
this._cacheTexA?.dispose();
|
|
179
|
+
this._cacheTexB?.dispose();
|
|
180
|
+
this._prevNDTexA?.dispose();
|
|
181
|
+
this._prevNDTexB?.dispose();
|
|
182
|
+
this._outputTex?.dispose();
|
|
183
|
+
// Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
|
|
184
|
+
this.context?.removeTexture( 'ssrc:output' );
|
|
185
|
+
this.outputTarget?.texture?.dispose();
|
|
186
|
+
this.reset();
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
174
190
|
reset() {
|
|
175
191
|
|
|
176
192
|
this._resetCache();
|