rayzee 11.7.0 → 11.9.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 +31 -6
- package/dist/{ArchiveImporter-DYcRXeKE.js → ArchiveImporter-BJxszU_k.js} +2353 -1356
- package/dist/{BidirectionalIntegrator-BvjRpbTP.js → BidirectionalIntegrator-DWg66vAd.js} +302 -301
- package/dist/{BufferLayout-xPRH9jYD.js → BufferLayout-Bzgccg1o.js} +1 -0
- package/dist/{ColorManagement-oeEk7oSu.js → ColorManagement-B_HjAiy2.js} +1 -1
- package/dist/{EngineIssues-CIZnXHsF.js → EngineIssues-CfJiBaNj.js} +19 -8
- package/dist/{NeuralRetouch-ClaMiKYg.js → NeuralRetouch-BD9E-Yht.js} +2 -2
- package/dist/{NeuralSuperRes-WNEXun9s.js → NeuralSuperRes-C8Mf3tJX.js} +2 -2
- package/dist/{PathTracer-B9wZPPVL.js → PathTracer-DeE9UJ1n.js} +3530 -3402
- package/dist/{PhysicalSky-DcjItqKi.js → PhysicalSky-cKy75-oR.js} +45 -45
- package/dist/{RayzeeRenderer-p-NeE6E2.js → RayzeeRenderer-CzLyBA3G.js} +378 -374
- package/dist/SceneMetadata-C17UfOEp.js +183 -0
- package/dist/{SolarLimb-bZE6y63S.js → SolarLimb-DRYy8tln.js} +184 -172
- package/dist/{ToneMapGPU-sAs8wdgI.js → ToneMapGPU-4GPwoG_c.js} +13 -6
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/color.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/addons/storage.es.js +1 -1
- package/dist/{openStorage-Fe9BoS1_.js → openStorage-CdyVjmps.js} +1 -1
- package/dist/rayzee-core.es.js +7 -7
- package/dist/rayzee.es.js +821 -794
- package/dist/rayzee.umd.js +112 -103
- package/dist/{sceneCachePolicy-B9w3BnaP.js → sceneCachePolicy-ywbGKHD3.js} +9 -6
- package/package.json +1 -1
- package/src/EngineDefaults.js +1 -0
- package/src/EngineIssues.js +21 -6
- package/src/Passes/AIUpscaler.js +4 -0
- package/src/Passes/OIDNDenoiser.js +8 -6
- package/src/Passes/OIDNTemporalHistory.js +1 -1
- package/src/PathTracerApp.js +10 -1
- package/src/Processor/ArchiveImporter.js +106 -27
- package/src/Processor/AssetLoader.js +22 -0
- package/src/Processor/BufferLayout.js +3 -3
- package/src/Processor/GeometryExtractor.js +12 -4
- package/src/Processor/LightSerializer.js +6 -2
- package/src/Processor/MaterialPacking.js +2 -2
- package/src/Processor/PBRT/PBRTMaterials.js +49 -13
- package/src/Processor/PBRT/PBRTParser.js +144 -22
- package/src/Processor/PBRT/PBRTSceneBuilder.js +287 -67
- package/src/Processor/PBRT/PBRTTextureBake.js +191 -9
- package/src/Processor/PBRT/index.js +19 -4
- package/src/Processor/PlatformImageLoader.js +9 -0
- package/src/Processor/SceneProcessor.js +42 -26
- package/src/Processor/TextureCreator.js +44 -4
- package/src/Processor/ToneMapCPU.js +34 -5
- package/src/Processor/ToneMapGPU.js +15 -9
- package/src/Processor/USD/USDScene.js +981 -17
- package/src/RayzeeRenderer.js +85 -28
- package/src/Stages/AutoExposure.js +21 -8
- package/src/Stages/Compositor.js +39 -1
- package/src/Stages/DenoiserSettings.js +0 -4
- package/src/Stages/LocalExposure.js +44 -18
- package/src/Stages/NRD.js +53 -24
- package/src/Stages/NormalDepth.js +12 -6
- package/src/Stages/PathTracer.js +27 -1
- package/src/Stages/PathTracerStage.js +14 -2
- package/src/Storage/SceneGraphCodec.js +6 -3
- package/src/TSL/BVHTraversal.js +33 -20
- package/src/TSL/CameraRay.js +12 -1
- package/src/TSL/Common.js +25 -8
- package/src/TSL/DebugKernel.js +3 -2
- package/src/TSL/Displacement.js +66 -113
- package/src/TSL/FinalWriteKernel.js +4 -7
- package/src/TSL/GenerateKernel.js +6 -2
- package/src/TSL/LightSplatKernel.js +7 -4
- package/src/TSL/LightsCore.js +6 -2
- package/src/TSL/LightsDirect.js +33 -11
- package/src/TSL/LightsSampling.js +45 -12
- package/src/TSL/SceneResources.js +19 -0
- package/src/TSL/ShadeKernel.js +3 -4
- package/src/TSL/Struct.js +1 -0
- package/src/integrators/BidirectionalIntegrator.js +1 -0
- package/src/managers/CameraManager.js +9 -0
- package/src/managers/DenoisingManager.js +1 -0
- package/src/managers/InteractionManager.js +6 -0
- package/src/managers/MaterialDataManager.js +11 -0
- package/src/managers/UniformManager.js +3 -1
- package/dist/SceneMetadata-D2Sh-0e-.js +0 -110
package/src/RayzeeRenderer.js
CHANGED
|
@@ -123,6 +123,15 @@ function attachDeviceLostHandler( device, holder ) {
|
|
|
123
123
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// three.js's GPUFeatureName, which it does not export: what its own device request enables.
|
|
127
|
+
const THREE_DEVICE_FEATURES = [
|
|
128
|
+
'core-features-and-limits', 'depth-clip-control', 'depth32float-stencil8', 'texture-compression-bc',
|
|
129
|
+
'texture-compression-bc-sliced-3d', 'texture-compression-etc2', 'texture-compression-astc',
|
|
130
|
+
'texture-compression-astc-sliced-3d', 'timestamp-query', 'indirect-first-instance', 'shader-f16',
|
|
131
|
+
'rg11b10ufloat-renderable', 'bgra8unorm-storage', 'float32-filterable', 'float32-blendable', 'clip-distances',
|
|
132
|
+
'dual-source-blending', 'subgroups', 'texture-formats-tier1', 'texture-formats-tier2',
|
|
133
|
+
];
|
|
134
|
+
|
|
126
135
|
const SOFTWARE_ADAPTER = /swiftshader|llvmpipe|lavapipe|basic render|microsoft basic|warp/i;
|
|
127
136
|
|
|
128
137
|
/**
|
|
@@ -822,9 +831,10 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
822
831
|
// disposed graph stays reachable until another app claims the same canvas.
|
|
823
832
|
if ( this.canvas && _appsByCanvas.get( this.canvas ) === this ) _appsByCanvas.delete( this.canvas );
|
|
824
833
|
|
|
825
|
-
|
|
834
|
+
const device = this._gpuDevice;
|
|
835
|
+
if ( device ) {
|
|
826
836
|
|
|
827
|
-
|
|
837
|
+
device.onuncapturederror = null;
|
|
828
838
|
this._gpuDevice = null;
|
|
829
839
|
|
|
830
840
|
}
|
|
@@ -836,7 +846,8 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
836
846
|
|
|
837
847
|
}
|
|
838
848
|
|
|
839
|
-
|
|
849
|
+
// three.js destroys only a device it requested itself.
|
|
850
|
+
Promise.resolve( disposeRenderer( this.renderer ) ).finally( () => device?.destroy() );
|
|
840
851
|
this.renderer = null;
|
|
841
852
|
|
|
842
853
|
this.stages = {};
|
|
@@ -1222,7 +1233,7 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
1222
1233
|
this._modelReplaced();
|
|
1223
1234
|
// Before the scene's own environment starts loading: a spilling scene's is narrowed as it decodes.
|
|
1224
1235
|
this._planSpill();
|
|
1225
|
-
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
1236
|
+
const built = await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
1226
1237
|
this._maybeSpill();
|
|
1227
1238
|
this.pipeline?.eventBus.emit( 'pipeline:lightingChanged' );
|
|
1228
1239
|
// Not held: the first denoise of a new scene lands ~1.1 s after the load (upload and
|
|
@@ -1231,6 +1242,7 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
1231
1242
|
this.reset();
|
|
1232
1243
|
this.dispatchEvent( eventPayload );
|
|
1233
1244
|
this._modelAnnounced();
|
|
1245
|
+
if ( built ) this.assetLoader?.commitPendingGraph( this.sceneModel );
|
|
1234
1246
|
|
|
1235
1247
|
} catch ( error ) {
|
|
1236
1248
|
|
|
@@ -2724,7 +2736,8 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2724
2736
|
* @param {string} [options.colorSpace='srgb'] - `'srgb'` for display bytes through the active
|
|
2725
2737
|
* view transform, `'linear'` for the raw working-space accumulation, or the name of a colour
|
|
2726
2738
|
* space in the loaded OCIO config for a delivery buffer (float, e.g. `'ACES2065-1'`)
|
|
2727
|
-
* @param {boolean} [options.preserveAlpha=false] - srgb only
|
|
2739
|
+
* @param {boolean} [options.preserveAlpha=false] - srgb only: coverage as alpha while the background is
|
|
2740
|
+
* transparent, else 255. Float readbacks carry coverage then and 1 otherwise, as the canvas shows it.
|
|
2728
2741
|
* @param {'accumulation'|'display'} [options.source='accumulation'] - `'display'` reads what the
|
|
2729
2742
|
* viewport is showing — denoised when a denoiser has run — instead of the raw
|
|
2730
2743
|
* accumulation
|
|
@@ -2750,6 +2763,8 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2750
2763
|
|
|
2751
2764
|
// The pool over-allocates to the reserve, so the texture is larger than the frame.
|
|
2752
2765
|
const { width, height } = stage;
|
|
2766
|
+
// The accumulation's alpha is always coverage; an opaque backdrop reads back opaque, as the Compositor draws it.
|
|
2767
|
+
const transparent = !! this.settings.get( 'transparentBackground' );
|
|
2753
2768
|
|
|
2754
2769
|
if ( colorSpace === 'srgb' ) {
|
|
2755
2770
|
|
|
@@ -2758,15 +2773,16 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2758
2773
|
exposure: this.renderer.toneMappingExposure,
|
|
2759
2774
|
toneMapping: this.renderer.toneMapping,
|
|
2760
2775
|
saturation: this.settings.get( 'saturation' ) ?? 1,
|
|
2761
|
-
preserveAlpha,
|
|
2776
|
+
preserveAlpha: preserveAlpha && transparent,
|
|
2762
2777
|
gain: this._displayGain(),
|
|
2763
2778
|
};
|
|
2764
2779
|
|
|
2765
2780
|
const gpu = await this._toneMapOnGPU( shown?.texture ?? null, target, width, height, tone );
|
|
2766
|
-
const fn = ! gpu && tone.gain ? await tone.gain.cpu() : null;
|
|
2781
|
+
const fn = ! gpu && tone.gain?.cpu ? await tone.gain.cpu() : null;
|
|
2782
|
+
const backdrop = tone.gain?.backdrop ?? 0;
|
|
2767
2783
|
const data = gpu ?? toneMapToRGBA8( shown
|
|
2768
2784
|
? await this._readTexture( shown.texture, width, height )
|
|
2769
|
-
: await readPixels( this.renderer, target, width, height ), { ...tone, pixelGain: fn
|
|
2785
|
+
: await readPixels( this.renderer, target, width, height ), { ...tone, pixelGain: ( fn || backdrop ) ? { fn, width, height, backdrop } : null } );
|
|
2770
2786
|
return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation', toneMappedOn: gpu ? 'gpu' : 'cpu' };
|
|
2771
2787
|
|
|
2772
2788
|
}
|
|
@@ -2774,6 +2790,12 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2774
2790
|
const { data: linear, source: read } = source === 'display'
|
|
2775
2791
|
? await this._readDisplaySource( target, width, height )
|
|
2776
2792
|
: { data: await readPixels( this.renderer, target, width, height ), source: 'accumulation' };
|
|
2793
|
+
if ( ! transparent ) {
|
|
2794
|
+
|
|
2795
|
+
const one = linear instanceof Uint16Array ? 0x3C00 : 1; // a half-float target reads back as its bits
|
|
2796
|
+
for ( let i = 3; i < linear.length; i += 4 ) linear[ i ] = one;
|
|
2797
|
+
|
|
2798
|
+
}
|
|
2777
2799
|
|
|
2778
2800
|
// `colorSpace` stays 'linear' — callers branch on it. `workingSpace` is the new, additive
|
|
2779
2801
|
// answer to "linear in what primaries", which only means something once a config is loaded.
|
|
@@ -3654,14 +3676,25 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
3654
3676
|
|
|
3655
3677
|
if ( typeof navigator === 'undefined' || ! navigator.gpu ) {
|
|
3656
3678
|
|
|
3657
|
-
|
|
3679
|
+
this._issues.fail( ISSUE_CODES.WEBGPU_UNAVAILABLE, 'WebGPU is not available — outside a browser, install one as navigator.gpu (for Node, the `webgpu` package)' );
|
|
3680
|
+
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
let adapter = null;
|
|
3684
|
+
let adapterError;
|
|
3685
|
+
try {
|
|
3686
|
+
|
|
3687
|
+
adapter = await navigator.gpu.requestAdapter( { powerPreference: 'high-performance' } );
|
|
3688
|
+
|
|
3689
|
+
} catch ( error ) {
|
|
3690
|
+
|
|
3691
|
+
adapterError = error;
|
|
3658
3692
|
|
|
3659
3693
|
}
|
|
3660
3694
|
|
|
3661
|
-
const adapter = await navigator.gpu.requestAdapter( { powerPreference: 'high-performance' } );
|
|
3662
3695
|
if ( ! adapter ) {
|
|
3663
3696
|
|
|
3664
|
-
|
|
3697
|
+
this._issues.fail( ISSUE_CODES.ADAPTER_UNAVAILABLE, `no WebGPU adapter${adapterError ? ': ' + adapterError.message : ''}`, {}, adapterError );
|
|
3665
3698
|
|
|
3666
3699
|
}
|
|
3667
3700
|
|
|
@@ -3711,22 +3744,43 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
3711
3744
|
// buys nothing, and 10 is already this adapter's ceiling.
|
|
3712
3745
|
requiredLimits.maxStorageBuffersPerShaderStage = Math.min( adapterLimits.maxStorageBuffersPerShaderStage, 10 );
|
|
3713
3746
|
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3747
|
+
// Requested here rather than by three.js, so a failure is typed where it happens.
|
|
3748
|
+
let device;
|
|
3749
|
+
try {
|
|
3750
|
+
|
|
3751
|
+
device = await adapter.requestDevice( {
|
|
3752
|
+
requiredFeatures: THREE_DEVICE_FEATURES.filter( ( name ) => adapter.features.has( name ) ),
|
|
3753
|
+
requiredLimits,
|
|
3754
|
+
} );
|
|
3720
3755
|
|
|
3721
|
-
|
|
3756
|
+
} catch ( error ) {
|
|
3757
|
+
|
|
3758
|
+
this._issues.fail( ISSUE_CODES.DEVICE_UNAVAILABLE, `WebGPU device request failed: ${error.message}`, { adapter: { ...this.adapterInfo } }, error );
|
|
3759
|
+
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
this.renderer = new WebGPURenderer( { canvas: this.canvas, alpha: true, device } );
|
|
3763
|
+
|
|
3764
|
+
try {
|
|
3765
|
+
|
|
3766
|
+
await initWithoutFrameLoop( this.renderer, this._headless );
|
|
3767
|
+
|
|
3768
|
+
} catch ( error ) {
|
|
3769
|
+
|
|
3770
|
+
device.destroy();
|
|
3771
|
+
throw error;
|
|
3772
|
+
|
|
3773
|
+
}
|
|
3722
3774
|
|
|
3723
3775
|
// WebGPURenderer swaps in WebGL2 on failure with only a warn(). The wavefront path is
|
|
3724
3776
|
// compute-only, so every frame would fail against an empty canvas.
|
|
3725
3777
|
if ( ! this.renderer.backend?.isWebGPUBackend ) {
|
|
3726
3778
|
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3779
|
+
device.destroy();
|
|
3780
|
+
this._issues.fail(
|
|
3781
|
+
ISSUE_CODES.WEBGPU_UNAVAILABLE,
|
|
3782
|
+
'three.js fell back to WebGL2, which cannot run the path tracer\'s compute kernels. ' +
|
|
3783
|
+
'Check GPU process flags and driver support.'
|
|
3730
3784
|
);
|
|
3731
3785
|
|
|
3732
3786
|
}
|
|
@@ -3743,13 +3797,12 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
3743
3797
|
// BVH and CPU-side texture data per create/dispose cycle. `lost` is a pending promise
|
|
3744
3798
|
// whose reaction cannot be unregistered, so it reads the app out of a holder that
|
|
3745
3799
|
// dispose() empties; `onuncapturederror` is cleared there directly.
|
|
3746
|
-
|
|
3747
|
-
if (
|
|
3800
|
+
this._gpuDevice = device;
|
|
3801
|
+
if ( device.lost ) {
|
|
3748
3802
|
|
|
3749
|
-
this._gpuDevice = gpuDevice;
|
|
3750
3803
|
this._deviceLostHolder = { app: this };
|
|
3751
|
-
attachDeviceLostHandler(
|
|
3752
|
-
|
|
3804
|
+
attachDeviceLostHandler( device, this._deviceLostHolder );
|
|
3805
|
+
device.onuncapturederror = ( event ) => log.error( 'WebGPU uncaptured error:', event.error );
|
|
3753
3806
|
|
|
3754
3807
|
}
|
|
3755
3808
|
|
|
@@ -3860,7 +3913,8 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
3860
3913
|
|
|
3861
3914
|
// Drag-drop / file load is a replace: clear any appended models first.
|
|
3862
3915
|
this._clearAppendedModels();
|
|
3863
|
-
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
3916
|
+
const built = await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
3917
|
+
if ( built ) this.assetLoader?.commitPendingGraph( event.model );
|
|
3864
3918
|
|
|
3865
3919
|
} else if ( event.texture ) {
|
|
3866
3920
|
|
|
@@ -4072,7 +4126,10 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
4072
4126
|
|
|
4073
4127
|
}
|
|
4074
4128
|
|
|
4075
|
-
/**
|
|
4129
|
+
/**
|
|
4130
|
+
* A per-pixel gain the shown picture carries before the view (the compositor's), for tone-mapped readbacks; or null.
|
|
4131
|
+
* Its `backdrop`, when set, is the gain a solid backdrop is held at (`PackedToneMapper`'s `gain`).
|
|
4132
|
+
*/
|
|
4076
4133
|
_displayGain() {
|
|
4077
4134
|
|
|
4078
4135
|
return null;
|
|
@@ -26,7 +26,7 @@ const BINS = 256;
|
|
|
26
26
|
const LOG2_MIN = - 16;
|
|
27
27
|
const LOG2_MAX = 12;
|
|
28
28
|
const BIN_STOPS = ( LOG2_MAX - LOG2_MIN ) / BINS;
|
|
29
|
-
// A tile below this has no light to meter
|
|
29
|
+
// A tile below this has no light to meter (a black sky, say): it is not part of the exposure.
|
|
30
30
|
const BLACK = 2 ** - 24;
|
|
31
31
|
const WEIGHT_SCALE = 65536;
|
|
32
32
|
const MAX_TILE_READS = 256;
|
|
@@ -42,6 +42,10 @@ const MOTION_HOLD = 0.3; // a metering of a new view counts as motion for this l
|
|
|
42
42
|
const DAMP_UNTIL = 1.5; // stops from the manual exposure: the room level is damped up to here...
|
|
43
43
|
const FOLLOW_FROM = 3; // ...and followed fully from here
|
|
44
44
|
const REC709_Y = [ 0.2126, 0.7152, 0.0722 ];
|
|
45
|
+
// A pixel this covered saw only the scene. One less carries the backdrop's own colour: an edge, a window onto it.
|
|
46
|
+
const FULL_COVERAGE = 0.999;
|
|
47
|
+
// How the backdrop counts: weighed out by coverage (transparent), in full (the environment), not at all (a solid colour).
|
|
48
|
+
const BACKDROP_MODES = { transparent: 0, environment: 1, color: 2 };
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* One adaptation step in stops: constant speed while far, exponential near the target, never past it.
|
|
@@ -93,15 +97,19 @@ const meterInterval = samples => samples < 16 ? 1 : samples < 64 ? 4 : 16;
|
|
|
93
97
|
/**
|
|
94
98
|
* Auto exposure: meters the accumulated image on the GPU and adapts on the CPU.
|
|
95
99
|
*
|
|
96
|
-
* Tiles are averaged in linear light,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
100
|
+
* Tiles are averaged in linear light, so a one-sample image meters like the converged one; the
|
|
101
|
+
* percentile-clipped mean of their log2 histogram is read back. The sky, also seen through windows, is
|
|
102
|
+
* part of what is lit and is metered; a transparent backdrop is weighed out by the path tracer's coverage
|
|
103
|
+
* (alpha); under a solid colour only pixels that saw the scene alone count, so its colour moves nothing.
|
|
104
|
+
*
|
|
105
|
+
* It aims at a room level learned while the view changes plus a damped share of each view
|
|
106
|
+
* (`blendExposureEV`), moves in stops every loop frame (`update()`: eased while the image restarts
|
|
107
|
+
* every frame, landing in ~0.3 s once it accumulates), in video time (`advance()`, eased), or with each
|
|
108
|
+
* metering (`instant`), and is applied as 2^EV × the manual exposure.
|
|
102
109
|
*
|
|
103
110
|
* Events listened: pipeline:reset, pipeline:lightingChanged (lands on the next metering)
|
|
104
111
|
* Textures read: pathtracer:color
|
|
112
|
+
* State read: pathtracer:samples, pathtracer:backdrop
|
|
105
113
|
* State published: autoexposure:value, autoexposure:avgLuminance
|
|
106
114
|
*/
|
|
107
115
|
export class AutoExposure extends RenderStage {
|
|
@@ -133,6 +141,7 @@ export class AutoExposure extends RenderStage {
|
|
|
133
141
|
this._falloffU = uniform( new Vector2() );
|
|
134
142
|
this._pointU = uniform( new Vector2( 0.5, 0.5 ) );
|
|
135
143
|
this._lumWeightsU = uniform( new Vector3().fromArray( REC709_Y ) );
|
|
144
|
+
this._backdropModeU = uniform( BACKDROP_MODES.transparent, 'int' );
|
|
136
145
|
this._widthU = uniform( 1, 'int' );
|
|
137
146
|
this._heightU = uniform( 1, 'int' );
|
|
138
147
|
this._strideU = uniform( 1, 'int' );
|
|
@@ -178,6 +187,7 @@ export class AutoExposure extends RenderStage {
|
|
|
178
187
|
const hist = this._histogram;
|
|
179
188
|
const width = this._widthU, height = this._heightU, stride = this._strideU, gridX = this._gridXU, gridY = this._gridYU;
|
|
180
189
|
const lumWeights = this._lumWeightsU, falloff = this._falloffU, point = this._pointU;
|
|
190
|
+
const backdropMode = this._backdropModeU;
|
|
181
191
|
|
|
182
192
|
return Fn( () => {
|
|
183
193
|
|
|
@@ -199,7 +209,9 @@ export class AutoExposure extends RenderStage {
|
|
|
199
209
|
const lum = dot( c.xyz, lumWeights ).toVar();
|
|
200
210
|
// NaN or Inf would poison the whole tile.
|
|
201
211
|
const finite = floatBitsToUint( lum ).bitAnd( uint( 0x7f800000 ) ).notEqual( uint( 0x7f800000 ) );
|
|
202
|
-
const
|
|
212
|
+
const coverage = c.w.clamp( 0.0, 1.0 );
|
|
213
|
+
const alpha = select( backdropMode.equal( int( BACKDROP_MODES.environment ) ), float( 1 ),
|
|
214
|
+
select( backdropMode.equal( int( BACKDROP_MODES.color ) ), select( coverage.greaterThanEqual( FULL_COVERAGE ), float( 1 ), float( 0 ) ), coverage ) );
|
|
203
215
|
sum.addAssign( select( finite, lum.clamp( 0.0, 1e6 ), float( 0 ) ).mul( alpha ) );
|
|
204
216
|
cover.addAssign( alpha );
|
|
205
217
|
|
|
@@ -388,6 +400,7 @@ export class AutoExposure extends RenderStage {
|
|
|
388
400
|
this._strideU.value = Math.max( 1, Math.round( Math.sqrt( tileReads / MAX_TILE_READS ) ) );
|
|
389
401
|
this._widthU.value = width;
|
|
390
402
|
this._heightU.value = height;
|
|
403
|
+
this._backdropModeU.value = BACKDROP_MODES[ context.getState( 'pathtracer:backdrop' ) ] ?? BACKDROP_MODES.transparent;
|
|
391
404
|
this._updateMeteringPattern( width / height );
|
|
392
405
|
this._inputNode.value = input;
|
|
393
406
|
|
package/src/Stages/Compositor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { MeshBasicNodeMaterial, QuadMesh, TextureNode } from 'three/webgpu';
|
|
|
3
3
|
import { NoBlending, NoToneMapping } from 'three';
|
|
4
4
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
5
5
|
import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
|
|
6
|
+
import { backdropGain } from '../Processor/ToneMapCPU.js';
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -51,6 +52,11 @@ export class Compositor extends RenderStage {
|
|
|
51
52
|
this._sourceTexNode = new TextureNode();
|
|
52
53
|
this._displayGain = null;
|
|
53
54
|
|
|
55
|
+
// A solid backdrop held at this exposure, or null: see setBackdropExposure().
|
|
56
|
+
this._backdropExposure = null;
|
|
57
|
+
this._holdBackdrop = uniform( 0, 'int' );
|
|
58
|
+
this._backdropGain = uniform( 1.0 );
|
|
59
|
+
|
|
54
60
|
this.compositorMaterial = new MeshBasicNodeMaterial();
|
|
55
61
|
this.compositorMaterial.blending = NoBlending;
|
|
56
62
|
this._buildColorNode();
|
|
@@ -62,7 +68,17 @@ export class Compositor extends RenderStage {
|
|
|
62
68
|
_buildColorNode() {
|
|
63
69
|
|
|
64
70
|
const texSample = this._sourceTexNode.sample( uv() );
|
|
65
|
-
|
|
71
|
+
let gain = this._displayGain ? this._displayGain( texSample.xyz, screenUV ) : null;
|
|
72
|
+
if ( this._backdropExposure !== null ) {
|
|
73
|
+
|
|
74
|
+
// Alpha is coverage: the backdrop takes its own gain, a partly covered pixel a blend (ToneMapCPU's heldGain).
|
|
75
|
+
const g = gain ?? float( 1 );
|
|
76
|
+
const held = g.add( this._backdropGain.sub( g ).mul( texSample.w.clamp( 0, 1 ).oneMinus() ) );
|
|
77
|
+
gain = select( this._holdBackdrop.greaterThan( int( 0 ) ), held, g );
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const color = gain ? texSample.xyz.mul( gain ) : texSample.xyz;
|
|
66
82
|
|
|
67
83
|
const luma = dot( color, REC709_LUMINANCE_COEFFICIENTS );
|
|
68
84
|
const gradedColor = mix( vec3( luma ), color, this.saturation );
|
|
@@ -86,6 +102,20 @@ export class Compositor extends RenderStage {
|
|
|
86
102
|
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Holds a solid-colour backdrop at `exposure` (the manual one): whatever the renderer's exposure does — auto
|
|
107
|
+
* exposure — and whatever display gain the scene takes, the backdrop is shown as at that exposure. Applies while
|
|
108
|
+
* the path tracer reports a 'color' backdrop. Null removes it.
|
|
109
|
+
* @param {number|null} exposure
|
|
110
|
+
*/
|
|
111
|
+
setBackdropExposure( exposure ) {
|
|
112
|
+
|
|
113
|
+
const changes = ( exposure === null ) !== ( this._backdropExposure === null );
|
|
114
|
+
this._backdropExposure = exposure;
|
|
115
|
+
if ( changes ) this._buildColorNode();
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
/**
|
|
90
120
|
* The picture to show: the first published of the display sources, else `pathtracer:color` — the newest
|
|
91
121
|
* scene-referred image the pipeline holds, denoised when a denoiser has run.
|
|
@@ -224,6 +254,14 @@ export class Compositor extends RenderStage {
|
|
|
224
254
|
const sourceTexture = this.resolveLightTexture( context );
|
|
225
255
|
if ( ! sourceTexture ) return;
|
|
226
256
|
|
|
257
|
+
if ( this._backdropExposure !== null ) {
|
|
258
|
+
|
|
259
|
+
const hold = context.getState( 'pathtracer:backdrop' ) === 'color';
|
|
260
|
+
this._holdBackdrop.value = hold ? 1 : 0;
|
|
261
|
+
if ( hold ) this._backdropGain.value = backdropGain( this._backdropExposure, this.renderer.toneMappingExposure, this.renderer.toneMapping );
|
|
262
|
+
|
|
263
|
+
}
|
|
264
|
+
|
|
227
265
|
this._sourceTexNode.value = sourceTexture;
|
|
228
266
|
|
|
229
267
|
this.renderer.setRenderTarget( null );
|
|
@@ -4,10 +4,6 @@
|
|
|
4
4
|
export const GBUFFER_MISS_DEPTH = 65504.0;
|
|
5
5
|
export const GBUFFER_MISS_THRESHOLD = 6e4;
|
|
6
6
|
|
|
7
|
-
// Albedo demodulation safety floor. Demod (`color / safeAlbedo`) and remod (`lighting * safeAlbedo`)
|
|
8
|
-
// only round-trip exactly when both sides use the same value.
|
|
9
|
-
export const ALBEDO_EPS = 0.01;
|
|
10
|
-
|
|
11
7
|
// The live view's denoisers, for setDenoiserStrategy.
|
|
12
8
|
export const DENOISER_STRATEGIES = Object.freeze( [ 'none', 'nrd', 'oidn' ] );
|
|
13
9
|
|
|
@@ -26,6 +26,7 @@ const BLUR_TEXEL = 32;
|
|
|
26
26
|
const BLUR_BLEND = 0.6;
|
|
27
27
|
const BLUR_PERCENT = 50;
|
|
28
28
|
const MIDDLE_GREY = Math.log2( 0.18 );
|
|
29
|
+
const FULL_COVERAGE = 0.999; // under a solid backdrop, a pixel counts only when it saw the scene alone
|
|
29
30
|
const DARKEST = 2 ** - 24;
|
|
30
31
|
const MAX_CELLS = Math.ceil( MAX_RESERVABLE_RENDER_SIZE / CELL );
|
|
31
32
|
const MAX_BLUR = Math.ceil( MAX_RESERVABLE_RENDER_SIZE / BLUR_TEXEL );
|
|
@@ -127,9 +128,12 @@ fn rayzee_gain( uv: vec2<f32>, linear: vec3<f32>, exposure: f32 ) -> f32 {
|
|
|
127
128
|
* shadows apart, and the detail kept. It changes only what is shown: the compositor and every tone-mapped
|
|
128
129
|
* readback apply the same per-pixel gain (`gainNode`, `toneGain`), so a saved picture matches the canvas.
|
|
129
130
|
*
|
|
130
|
-
* Built from `pathtracer:color` when the image restarts, less often as it converges.
|
|
131
|
+
* Built from `pathtracer:color` when the image restarts, less often as it converges. While the backdrop is a solid
|
|
132
|
+
* colour, both are built from the pixels that saw the scene alone (full coverage in alpha): the backdrop is not part of
|
|
133
|
+
* what is lit, and the compositor holds it at the manual exposure without this gain.
|
|
131
134
|
* Events listened: pipeline:reset
|
|
132
135
|
* Textures read: pathtracer:color
|
|
136
|
+
* State read: pathtracer:samples, pathtracer:backdrop
|
|
133
137
|
*/
|
|
134
138
|
export class LocalExposure extends RenderStage {
|
|
135
139
|
|
|
@@ -146,7 +150,9 @@ export class LocalExposure extends RenderStage {
|
|
|
146
150
|
|
|
147
151
|
this._grid = attributeArray( MAX_CELLS * MAX_CELLS * BINS, 'vec2' );
|
|
148
152
|
this._blur = attributeArray( MAX_BLUR * MAX_BLUR, 'float' );
|
|
149
|
-
|
|
153
|
+
// The blur runs on ( log2 luminance × share, share ), divided out at the end: see the blur source kernel.
|
|
154
|
+
this._blurSource = attributeArray( MAX_BLUR * MAX_BLUR, 'vec2' );
|
|
155
|
+
this._blurTmp = attributeArray( MAX_BLUR * MAX_BLUR, 'vec2' );
|
|
150
156
|
this._inputNode = new TextureNode();
|
|
151
157
|
this._widthU = uniform( 1, 'int' );
|
|
152
158
|
this._heightU = uniform( 1, 'int' );
|
|
@@ -155,10 +161,11 @@ export class LocalExposure extends RenderStage {
|
|
|
155
161
|
this._blurHU = uniform( 1, 'int' );
|
|
156
162
|
this._radiusU = uniform( 1 );
|
|
157
163
|
this._logExposureU = uniform( 0 );
|
|
164
|
+
this._sceneOnlyU = uniform( 0, 'int' ); // 1: only pixels that saw the scene alone, a solid backdrop left out
|
|
158
165
|
this._p = [ new Vector4(), new Vector4(), new Vector4(), new Vector4() ].map( v => uniform( v ) );
|
|
159
166
|
this._params = new Float32Array( 16 );
|
|
160
167
|
|
|
161
|
-
this._kernels = [ this._buildGridKernel(), this._buildBlurSourceKernel(), this._buildBlurKernel( this.
|
|
168
|
+
this._kernels = [ this._buildGridKernel(), this._buildBlurSourceKernel(), this._buildBlurKernel( this._blurSource, this._blurTmp, true ), this._buildBlurKernel( this._blurTmp, this._blur, false ) ];
|
|
162
169
|
|
|
163
170
|
this._built = false;
|
|
164
171
|
this._restarts = 0;
|
|
@@ -175,6 +182,7 @@ export class LocalExposure extends RenderStage {
|
|
|
175
182
|
|
|
176
183
|
const input = this._inputNode, grid = this._grid;
|
|
177
184
|
const width = this._widthU, height = this._heightU, cellsX = this._cellsXU, logExposure = this._logExposureU;
|
|
185
|
+
const sceneOnly = this._sceneOnlyU.greaterThan( int( 0 ) );
|
|
178
186
|
const yw = vec4( ...Y, 0 ).xyz;
|
|
179
187
|
|
|
180
188
|
return Fn( () => {
|
|
@@ -202,20 +210,24 @@ export class LocalExposure extends RenderStage {
|
|
|
202
210
|
If( x.lessThan( width ).and( y.lessThan( height ) ), () => {
|
|
203
211
|
|
|
204
212
|
const x1 = min( x.add( int( 1 ) ), width.sub( int( 1 ) ) ), y1 = min( y.add( int( 1 ) ), height.sub( int( 1 ) ) );
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
const t00 = textureLoad( input, ivec2( x, y ) ).toVar(), t10 = textureLoad( input, ivec2( x1, y ) ).toVar();
|
|
214
|
+
const t01 = textureLoad( input, ivec2( x, y1 ) ).toVar(), t11 = textureLoad( input, ivec2( x1, y1 ) ).toVar();
|
|
215
|
+
const coverage = ( t ) => select( sceneOnly.and( t.w.lessThan( FULL_COVERAGE ) ), float( 0 ), float( 1 ) );
|
|
216
|
+
const a00 = coverage( t00 ), a10 = coverage( t10 ), a01 = coverage( t01 ), a11 = coverage( t11 );
|
|
217
|
+
const covered = a00.add( a10 ).add( a01 ).add( a11 ).toVar();
|
|
218
|
+
const lum = dot( t00.xyz, yw ).mul( a00 ).add( dot( t10.xyz, yw ).mul( a10 ) )
|
|
219
|
+
.add( dot( t01.xyz, yw ).mul( a01 ) ).add( dot( t11.xyz, yw ).mul( a11 ) ).div( max( covered, 1e-6 ) ).toVar();
|
|
220
|
+
const share = covered.mul( 0.25 ).toVar();
|
|
209
221
|
// A NaN or infinite texel is left out, as the meter leaves it out: max() may keep a NaN on some
|
|
210
222
|
// GPUs, and an infinite one would read as 2^40 and darken its whole cell.
|
|
211
|
-
If( floatBitsToUint( lum ).bitAnd( uint( 0x7f800000 ) ).notEqual( uint( 0x7f800000 ) ), () => {
|
|
223
|
+
If( floatBitsToUint( lum ).bitAnd( uint( 0x7f800000 ) ).notEqual( uint( 0x7f800000 ) ).and( covered.greaterThan( 0.0 ) ), () => {
|
|
212
224
|
|
|
213
225
|
const logL = log2( max( lum, DARKEST ) ).clamp( LOG_FLOOR, LOG_CEIL ).toVar();
|
|
214
226
|
const f = logL.add( logExposure ).sub( BIN_MIN ).div( BIN_MAX - BIN_MIN ).clamp( 0.0, 1.0 ).mul( BINS - 1 ).toVar();
|
|
215
227
|
const b0 = uint( floor( f ) ).min( uint( BINS - 1 ) );
|
|
216
228
|
const b1 = b0.add( uint( 1 ) ).min( uint( BINS - 1 ) );
|
|
217
|
-
const w1 = f.sub( floor( f ) );
|
|
218
|
-
const w0 = float( 1 ).sub(
|
|
229
|
+
const w1 = f.sub( floor( f ) ).mul( share );
|
|
230
|
+
const w0 = float( 1 ).sub( f.sub( floor( f ) ) ).mul( share );
|
|
219
231
|
const level = logL.sub( LOG_FLOOR ).mul( FIXED );
|
|
220
232
|
atomicAdd( sums.element( b0 ), uint( level.mul( w0 ).add( 0.5 ) ) );
|
|
221
233
|
atomicAdd( weights.element( b0 ), uint( w0.mul( FIXED ).add( 0.5 ) ) );
|
|
@@ -246,10 +258,12 @@ export class LocalExposure extends RenderStage {
|
|
|
246
258
|
}
|
|
247
259
|
|
|
248
260
|
// The log of each 32×32 block's mean luminance: averaged in linear light, like the meter, so noise does not darken it.
|
|
261
|
+
// Stored as ( log × share, share ), share the fraction of its pixels counted: 1 unless a solid backdrop is left out.
|
|
249
262
|
_buildBlurSourceKernel() {
|
|
250
263
|
|
|
251
|
-
const input = this._inputNode, out = this.
|
|
264
|
+
const input = this._inputNode, out = this._blurSource;
|
|
252
265
|
const width = this._widthU, height = this._heightU, blurW = this._blurWU;
|
|
266
|
+
const sceneOnly = this._sceneOnlyU.greaterThan( int( 0 ) );
|
|
253
267
|
const yw = vec4( ...Y, 0 ).xyz;
|
|
254
268
|
|
|
255
269
|
return Fn( () => {
|
|
@@ -257,6 +271,7 @@ export class LocalExposure extends RenderStage {
|
|
|
257
271
|
const lid = localId.y.mul( uint( 8 ) ).add( localId.x );
|
|
258
272
|
const ox = int( workgroupId.x ).mul( int( BLUR_TEXEL ) ), oy = int( workgroupId.y ).mul( int( BLUR_TEXEL ) );
|
|
259
273
|
const sum = float( 0 ).toVar();
|
|
274
|
+
const covered = float( 0 ).toVar();
|
|
260
275
|
const count = float( 0 ).toVar();
|
|
261
276
|
|
|
262
277
|
Loop( { start: int( localId.y ), end: int( BLUR_TEXEL ), type: 'int', condition: '<', update: 8, name: 'by' }, ( { by } ) => {
|
|
@@ -266,9 +281,12 @@ export class LocalExposure extends RenderStage {
|
|
|
266
281
|
const x = ox.add( bx ), y = oy.add( by );
|
|
267
282
|
If( x.lessThan( width ).and( y.lessThan( height ) ), () => {
|
|
268
283
|
|
|
269
|
-
const
|
|
284
|
+
const texel = textureLoad( input, ivec2( x, y ) ).toVar();
|
|
285
|
+
const lum = dot( texel.xyz, yw ).toVar();
|
|
270
286
|
const finite = floatBitsToUint( lum ).bitAnd( uint( 0x7f800000 ) ).notEqual( uint( 0x7f800000 ) );
|
|
271
|
-
|
|
287
|
+
const a = select( sceneOnly.and( texel.w.lessThan( FULL_COVERAGE ) ), float( 0 ), float( 1 ) );
|
|
288
|
+
sum.addAssign( select( finite, lum.max( 0.0 ).mul( a ), float( 0 ) ) );
|
|
289
|
+
covered.addAssign( select( finite, a, float( 0 ) ) );
|
|
272
290
|
count.addAssign( select( finite, float( 1 ), float( 0 ) ) );
|
|
273
291
|
|
|
274
292
|
} );
|
|
@@ -278,8 +296,10 @@ export class LocalExposure extends RenderStage {
|
|
|
278
296
|
} );
|
|
279
297
|
|
|
280
298
|
const sharedSum = workgroupArray( 'float', 64 );
|
|
299
|
+
const sharedCovered = workgroupArray( 'float', 64 );
|
|
281
300
|
const sharedCount = workgroupArray( 'float', 64 );
|
|
282
301
|
sharedSum.element( lid ).assign( sum );
|
|
302
|
+
sharedCovered.element( lid ).assign( covered );
|
|
283
303
|
sharedCount.element( lid ).assign( count );
|
|
284
304
|
workgroupBarrier();
|
|
285
305
|
|
|
@@ -288,6 +308,7 @@ export class LocalExposure extends RenderStage {
|
|
|
288
308
|
If( lid.lessThan( uint( s ) ), () => {
|
|
289
309
|
|
|
290
310
|
sharedSum.element( lid ).addAssign( sharedSum.element( lid.add( uint( s ) ) ) );
|
|
311
|
+
sharedCovered.element( lid ).addAssign( sharedCovered.element( lid.add( uint( s ) ) ) );
|
|
291
312
|
sharedCount.element( lid ).addAssign( sharedCount.element( lid.add( uint( s ) ) ) );
|
|
292
313
|
|
|
293
314
|
} );
|
|
@@ -297,8 +318,10 @@ export class LocalExposure extends RenderStage {
|
|
|
297
318
|
|
|
298
319
|
If( lid.equal( uint( 0 ) ), () => {
|
|
299
320
|
|
|
300
|
-
const mean = sharedSum.element( uint( 0 ) ).div( max(
|
|
301
|
-
|
|
321
|
+
const mean = sharedSum.element( uint( 0 ) ).div( max( sharedCovered.element( uint( 0 ) ), 1.0 ) );
|
|
322
|
+
const level = log2( max( mean, DARKEST ) ).clamp( LOG_FLOOR, LOG_CEIL );
|
|
323
|
+
const share = select( sceneOnly, sharedCovered.element( uint( 0 ) ).div( max( sharedCount.element( uint( 0 ) ), 1.0 ) ), float( 1 ) ).toVar();
|
|
324
|
+
out.element( uint( int( workgroupId.y ).mul( blurW ).add( int( workgroupId.x ) ) ) ).assign( vec2( level.mul( share ), share ) );
|
|
302
325
|
|
|
303
326
|
} );
|
|
304
327
|
|
|
@@ -306,7 +329,8 @@ export class LocalExposure extends RenderStage {
|
|
|
306
329
|
|
|
307
330
|
}
|
|
308
331
|
|
|
309
|
-
// Unreal's Gaussian, exp( -16.7 ( x / r )² ), along one axis, mirrored at the edges.
|
|
332
|
+
// Unreal's Gaussian, exp( -16.7 ( x / r )² ), along one axis, mirrored at the edges. Runs on ( level × share,
|
|
333
|
+
// share ); the second pass divides it out, so backdrop the build left out takes no part.
|
|
310
334
|
_buildBlurKernel( src, dst, ax ) {
|
|
311
335
|
|
|
312
336
|
const blurW = this._blurWU, blurH = this._blurHU, radius = this._radiusU;
|
|
@@ -320,7 +344,7 @@ export class LocalExposure extends RenderStage {
|
|
|
320
344
|
const size = ax ? blurW : blurH;
|
|
321
345
|
const at = ax ? x : y;
|
|
322
346
|
const reach = int( radius.ceil() );
|
|
323
|
-
const sum =
|
|
347
|
+
const sum = vec2( 0 ).toVar();
|
|
324
348
|
const total = float( 0 ).toVar();
|
|
325
349
|
|
|
326
350
|
Loop( { start: reach.negate(), end: reach.add( int( 1 ) ), type: 'int', condition: '<', name: 'o' }, ( { o } ) => {
|
|
@@ -342,7 +366,8 @@ export class LocalExposure extends RenderStage {
|
|
|
342
366
|
|
|
343
367
|
} );
|
|
344
368
|
|
|
345
|
-
|
|
369
|
+
const blurred = sum.div( total ).toVar();
|
|
370
|
+
dst.element( uint( y.mul( blurW ).add( x ) ) ).assign( ax ? blurred : blurred.x.div( max( blurred.y, 1e-30 ) ) );
|
|
346
371
|
|
|
347
372
|
} );
|
|
348
373
|
|
|
@@ -390,6 +415,7 @@ export class LocalExposure extends RenderStage {
|
|
|
390
415
|
this._blurHU.value = blurH;
|
|
391
416
|
this._radiusU.value = Math.max( 1, BLUR_PERCENT / 100 * blurW );
|
|
392
417
|
this._logExposureU.value = logExposure;
|
|
418
|
+
this._sceneOnlyU.value = context.getState( 'pathtracer:backdrop' ) === 'color' ? 1 : 0;
|
|
393
419
|
this._kernels[ 0 ].dispatchSize = [ cellsX, cellsY, 1 ];
|
|
394
420
|
this._kernels[ 1 ].dispatchSize = [ blurW, blurH, 1 ];
|
|
395
421
|
this._kernels[ 2 ].dispatchSize = this._kernels[ 3 ].dispatchSize = [ Math.ceil( blurW / 8 ), Math.ceil( blurH / 8 ), 1 ];
|