@tinypoly/gpu-renderer 0.1.0 → 0.2.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/ARCHITECTURE.md +31 -3
- package/README.md +13 -4
- package/dist/denoise.d.ts +10 -1
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +4 -0
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +8 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +6 -1
- package/dist/frameLayout.js.map +1 -1
- package/dist/imageResolve.d.ts +2 -1
- package/dist/imageResolve.d.ts.map +1 -1
- package/dist/imageResolve.js +1 -0
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +4 -0
- package/dist/lensPass.d.ts.map +1 -1
- package/dist/lensPass.js +14 -1
- package/dist/lensPass.js.map +1 -1
- package/dist/lightTree.d.ts +10 -3
- package/dist/lightTree.d.ts.map +1 -1
- package/dist/lightTree.js +56 -17
- package/dist/lightTree.js.map +1 -1
- package/dist/lights.d.ts +20 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +48 -1
- package/dist/lights.js.map +1 -1
- package/dist/oidnPass.d.ts +48 -0
- package/dist/oidnPass.d.ts.map +1 -0
- package/dist/oidnPass.js +255 -0
- package/dist/oidnPass.js.map +1 -0
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +9 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/presentPass.d.ts +2 -0
- package/dist/presentPass.d.ts.map +1 -1
- package/dist/presentPass.js +1 -0
- package/dist/presentPass.js.map +1 -1
- package/dist/renderer.d.ts +20 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +138 -19
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +5 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +4 -0
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +29 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +15 -6
- package/dist/settings.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +1 -1
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lens.wgsl.js +110 -17
- package/dist/wgsl/lens.wgsl.js.map +1 -1
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +197 -33
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +173 -131
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/present.wgsl.d.ts +1 -1
- package/dist/wgsl/present.wgsl.d.ts.map +1 -1
- package/dist/wgsl/present.wgsl.js +11 -1
- package/dist/wgsl/present.wgsl.js.map +1 -1
- package/dist/wgsl/random.wgsl.d.ts +1 -1
- package/dist/wgsl/random.wgsl.d.ts.map +1 -1
- package/dist/wgsl/random.wgsl.js +28 -0
- package/dist/wgsl/random.wgsl.js.map +1 -1
- package/package.json +4 -2
- package/src/denoise.ts +15 -1
- package/src/device.ts +9 -0
- package/src/frameLayout.ts +6 -1
- package/src/imageResolve.ts +2 -0
- package/src/index.ts +2 -1
- package/src/lensPass.ts +17 -1
- package/src/lightTree.ts +67 -21
- package/src/lights.ts +65 -1
- package/src/oidnPass.ts +284 -0
- package/src/oidnWebgpu.d.ts +5 -0
- package/src/pathTracePass.ts +11 -9
- package/src/presentPass.ts +3 -0
- package/src/renderer.ts +161 -20
- package/src/sceneUpload.ts +9 -0
- package/src/settings.ts +40 -6
- package/src/wgsl/lens.wgsl.ts +110 -17
- package/src/wgsl/lights.wgsl.ts +197 -33
- package/src/wgsl/pathTrace.wgsl.ts +173 -131
- package/src/wgsl/present.wgsl.ts +11 -1
- package/src/wgsl/random.wgsl.ts +28 -0
- package/weights/LICENSE.txt +202 -0
- package/weights/rt_hdr_alb_nrm.tza +0 -0
package/src/pathTracePass.ts
CHANGED
|
@@ -24,11 +24,12 @@ export type FrameUniforms = Record<keyof typeof FRAME_LAYOUT.offsets & string, n
|
|
|
24
24
|
* them.
|
|
25
25
|
*/
|
|
26
26
|
export function pathTraceSource(
|
|
27
|
-
programs: ShaderPrograms | null = null, subsurface = true, volumes = true, bevel = true
|
|
27
|
+
programs: ShaderPrograms | null = null, subsurface = true, volumes = true, bevel = true,
|
|
28
|
+
portals = true): string {
|
|
28
29
|
// The random walk inside materials and the media inside volumes are compiled in only for a scene that has them: the
|
|
29
30
|
// kernel is bound by its size, and either would cost every other scene a few percent for code that never runs.
|
|
30
31
|
return `const SUBSURFACE_WALK: bool = ${subsurface};\nconst VOLUMES: bool = ${volumes};\nconst BEVEL: bool = ${bevel};\n`
|
|
31
|
-
+ COMMON_WGSL + RANDOM_WGSL + BVH_WGSL + SAMPLING_WGSL + TEXTURE_WGSL + SHADER_SUPPORT_WGSL
|
|
32
|
+
+ `const PORTALS: bool = ${portals};\n` + COMMON_WGSL + RANDOM_WGSL + BVH_WGSL + SAMPLING_WGSL + TEXTURE_WGSL + SHADER_SUPPORT_WGSL
|
|
32
33
|
+ programsWgsl(programs) + ENVIRONMENT_WGSL + SURFACE_WGSL + BSDF_WGSL + LIGHTS_WGSL + PATH_TRACE_WGSL;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -41,10 +42,10 @@ export class PathTracePass {
|
|
|
41
42
|
private readonly uniforms: GPUBuffer;
|
|
42
43
|
private readonly values = new ArrayBuffer(FRAME_BYTES);
|
|
43
44
|
private bindGroup: GPUBindGroup | null = null;
|
|
44
|
-
private programKey = "|false|false|false";
|
|
45
|
+
private programKey = "|false|false|false|false|false";
|
|
45
46
|
|
|
46
47
|
constructor(private readonly device: GPUDevice) {
|
|
47
|
-
this.pipeline = this.build(null, false, false, false);
|
|
48
|
+
this.pipeline = this.build(null, false, false, false, false);
|
|
48
49
|
this.uniforms = device.createBuffer({ size: FRAME_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -52,20 +53,21 @@ export class PathTracePass {
|
|
|
52
53
|
* Recompiles the kernel when the scene's shaders changed, or whether it has a material that scatters light inside;
|
|
53
54
|
* the same set keeps the pipeline it has.
|
|
54
55
|
*/
|
|
55
|
-
setPrograms(programs: ShaderPrograms | null, subsurface = false, volumes = false, bevel = false
|
|
56
|
-
|
|
56
|
+
setPrograms(programs: ShaderPrograms | null, subsurface = false, volumes = false, bevel = false,
|
|
57
|
+
portals = false) {
|
|
58
|
+
const key = `${programs?.key ?? ""}|${subsurface}|${volumes}|${bevel}|${portals}`;
|
|
57
59
|
|
|
58
60
|
if (key === this.programKey) return;
|
|
59
61
|
|
|
60
62
|
this.programKey = key;
|
|
61
|
-
this.pipeline = this.build(programs, subsurface, volumes, bevel);
|
|
63
|
+
this.pipeline = this.build(programs, subsurface, volumes, bevel, portals);
|
|
62
64
|
this.bindGroup = null;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
private build(programs: ShaderPrograms | null, subsurface: boolean, volumes: boolean,
|
|
66
|
-
bevel: boolean): GPUComputePipeline {
|
|
68
|
+
bevel: boolean, portals: boolean): GPUComputePipeline {
|
|
67
69
|
const module = createShaderModule(this.device, "path trace",
|
|
68
|
-
pathTraceSource(programs, subsurface, volumes, bevel));
|
|
70
|
+
pathTraceSource(programs, subsurface, volumes, bevel, portals));
|
|
69
71
|
|
|
70
72
|
return this.device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
|
|
71
73
|
}
|
package/src/presentPass.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface PresentUniforms {
|
|
|
9
9
|
toneMapping: RenderToneMapping;
|
|
10
10
|
vignette: number;
|
|
11
11
|
grain: number;
|
|
12
|
+
/** AgX's look, from its base curve to Punchy. */
|
|
13
|
+
contrast: number;
|
|
12
14
|
frameIndex: number;
|
|
13
15
|
transparent: boolean;
|
|
14
16
|
/** The denoiser's output, which the colour is read from instead of the accumulation when given. */
|
|
@@ -84,6 +86,7 @@ export class PresentPass {
|
|
|
84
86
|
this.integers[6] = present.frameIndex;
|
|
85
87
|
this.integers[7] = present.transparent ? 1 : 0;
|
|
86
88
|
this.integers[8] = present.denoised ? 1 : 0;
|
|
89
|
+
this.floats[9] = present.contrast;
|
|
87
90
|
this.device.queue.writeBuffer(this.uniforms, 0, this.values);
|
|
88
91
|
|
|
89
92
|
const pass = encoder.beginRenderPass({
|
package/src/renderer.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type DenoiserSettings,
|
|
41
41
|
type DenoiseView,
|
|
42
42
|
} from "./denoise.js";
|
|
43
|
+
import { DEFAULT_OIDN_WEIGHTS, OidnPass } from "./oidnPass.js";
|
|
43
44
|
import { CaptureResolvePass, planarCaptures, type PlanarCaptures } from "./planarCapture.js";
|
|
44
45
|
import { prepareSceneAssets, type SceneAssets } from "./sceneAssets.js";
|
|
45
46
|
import { packVolumetricLights, VOLUMETRIC_WORDS } from "./volumetric.js";
|
|
@@ -123,6 +124,13 @@ interface PendingFrame {
|
|
|
123
124
|
reject: (reason: Error) => void;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/** AgX's look, which a missing or broken setting leaves at the base curve's own. */
|
|
128
|
+
function lookContrast(settings: GpuRenderSettings): number {
|
|
129
|
+
const contrast = Number(settings.camera.contrast);
|
|
130
|
+
|
|
131
|
+
return Number.isFinite(contrast) ? Math.min(1, Math.max(0, contrast)) : 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
function abortError(message: string): Error {
|
|
127
135
|
return Object.assign(new Error(message), { name: "AbortError" });
|
|
128
136
|
}
|
|
@@ -156,12 +164,30 @@ const SHOW_EVERY_MS = 16;
|
|
|
156
164
|
*/
|
|
157
165
|
const BANDS_IN_FLIGHT = 2;
|
|
158
166
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Throughput under which Russian roulette may end a path, with the chance of carrying on equal to the throughput. One
|
|
169
|
+
* plays it on every path past the first bounces, as PBRT does: a path off a light wall carries on as long as it
|
|
170
|
+
* carries light, instead of seven bounces before the roulette ever looks at it. The mean stays exact.
|
|
171
|
+
*/
|
|
172
|
+
const ROULETTE_THRESHOLD = 1;
|
|
173
|
+
|
|
174
|
+
/** How often the lens pass grades the canvas again while a frame traces without the denoiser. */
|
|
175
|
+
const LENS_EVERY_MS = 100;
|
|
161
176
|
|
|
162
|
-
/** How often the canvas takes a freshly denoised image while a frame traces. */
|
|
177
|
+
/** How often the canvas takes a freshly denoised image while a frame traces, for an image of DENOISE_REFERENCE work. */
|
|
163
178
|
const DENOISE_EVERY_MS = 200;
|
|
164
179
|
|
|
180
|
+
/** The filtering of a 1080p image with three passes, which the interval above was set for. */
|
|
181
|
+
const DENOISE_REFERENCE = 1920 * 1080 * 3;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* How long the canvas waits between denoised images: longer for a larger image or more passes, in proportion, so the
|
|
185
|
+
* filter takes the same share of the device from tracing whatever the size. The finished image is filtered regardless.
|
|
186
|
+
*/
|
|
187
|
+
export function denoiseInterval(width: number, height: number, passes: number): number {
|
|
188
|
+
return DENOISE_EVERY_MS * Math.max(1, width * height * Math.max(1, passes) / DENOISE_REFERENCE);
|
|
189
|
+
}
|
|
190
|
+
|
|
165
191
|
/** The most paths per pixel one dispatch takes, while no pixel can stop yet. */
|
|
166
192
|
const BATCH_PATHS = 4;
|
|
167
193
|
|
|
@@ -273,7 +299,19 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
273
299
|
private planar = false;
|
|
274
300
|
private resolve: CaptureResolvePass | null = null;
|
|
275
301
|
private denoisePass: DenoisePass | null = null;
|
|
302
|
+
private oidnPass: OidnPass | null = null;
|
|
303
|
+
// The buffer the last denoise wrote, whichever denoiser ran it.
|
|
304
|
+
private denoiseOutput: GPUBuffer | null = null;
|
|
305
|
+
// The network denoises one image at a time: each run waits for the one before it.
|
|
306
|
+
private oidnRun: Promise<unknown> = Promise.resolve();
|
|
307
|
+
private oidnBusy = false;
|
|
308
|
+
// Set once the network failed to load or run; SVGF stands in until the denoiser settings change.
|
|
309
|
+
private oidnFailed = false;
|
|
276
310
|
private lensPass: LensPass | null = null;
|
|
311
|
+
// What the lens pass last graded, and when: its input only changes when the denoiser runs or the accumulation grows.
|
|
312
|
+
private lensKey = "";
|
|
313
|
+
private lensAt = -Infinity;
|
|
314
|
+
private denoiseVersion = 0;
|
|
277
315
|
private denoiserSettings: DenoiserSettings = { ...DEFAULT_DENOISER };
|
|
278
316
|
// The size of the last frame that finished, which setDenoiser filters again; null while a frame traces.
|
|
279
317
|
private finished: { width: number; height: number } | null = null;
|
|
@@ -329,6 +367,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
329
367
|
*/
|
|
330
368
|
setDenoiser(input: Partial<DenoiserSettings>) {
|
|
331
369
|
this.denoiserSettings = mergeDenoiser(this.denoiserSettings, input);
|
|
370
|
+
this.oidnFailed = false;
|
|
332
371
|
this.refilter();
|
|
333
372
|
}
|
|
334
373
|
|
|
@@ -342,16 +381,18 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
342
381
|
const denoise = this.current.globalIllumination.denoise;
|
|
343
382
|
|
|
344
383
|
void (async() => {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (denoise) this.encodeDenoise(gpu, encoder, finished.width, finished.height);
|
|
384
|
+
if (denoise) await this.denoiseNow(gpu, finished.width, finished.height);
|
|
348
385
|
else this.denoised = false;
|
|
349
386
|
|
|
387
|
+
if (revision !== this.revision || this.rendering) return;
|
|
388
|
+
|
|
389
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
390
|
+
|
|
350
391
|
this.encodePresent(encoder, finished.width, finished.height);
|
|
351
392
|
gpu.device.queue.submit([encoder.finish()]);
|
|
352
393
|
|
|
353
394
|
const image = await this.readImage(gpu, finished.width, finished.height,
|
|
354
|
-
denoise
|
|
395
|
+
denoise && this.denoised ? this.denoiseOutput : null);
|
|
355
396
|
|
|
356
397
|
// A frame started meanwhile owns the image now.
|
|
357
398
|
if (revision !== this.revision || this.rendering) return;
|
|
@@ -536,6 +577,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
536
577
|
this.buffers?.destroy();
|
|
537
578
|
this.accumulator?.destroy();
|
|
538
579
|
this.denoisePass?.destroy();
|
|
580
|
+
this.oidnPass?.destroy();
|
|
539
581
|
this.lensPass?.destroy();
|
|
540
582
|
this.lensPass = null;
|
|
541
583
|
this.imageResolve?.dispose();
|
|
@@ -716,13 +758,30 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
716
758
|
|
|
717
759
|
// The canvas shows the denoised image, refreshed a few times a second while the bands refine underneath it:
|
|
718
760
|
// filtering the whole image after every pass would cost a cheap scene as much as tracing it.
|
|
719
|
-
if (denoise && this.canvas && this.context && performance.now() - denoisedAt
|
|
720
|
-
|
|
761
|
+
if (denoise && this.canvas && this.context && !this.oidnBusy && performance.now() - denoisedAt
|
|
762
|
+
>= denoiseInterval(width, height, this.denoiserSettings.passes)) {
|
|
763
|
+
if (this.usesOidn()) {
|
|
764
|
+
// The network takes longer than a pass: it runs beside the tracing, and the canvas takes its image when done.
|
|
765
|
+
this.oidnBusy = true;
|
|
766
|
+
void this.denoiseNow(gpu, width, height).then(() => {
|
|
767
|
+
if (revision !== this.revision || !this.denoised) return;
|
|
768
|
+
|
|
769
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
770
|
+
|
|
771
|
+
this.encodePresent(encoder, width, height);
|
|
772
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
773
|
+
}).finally(() => {
|
|
774
|
+
this.oidnBusy = false;
|
|
775
|
+
denoisedAt = performance.now();
|
|
776
|
+
});
|
|
777
|
+
} else {
|
|
778
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
721
779
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
780
|
+
this.encodeDenoise(gpu, encoder, width, height);
|
|
781
|
+
this.encodePresent(encoder, width, height);
|
|
782
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
783
|
+
denoisedAt = performance.now();
|
|
784
|
+
}
|
|
726
785
|
}
|
|
727
786
|
|
|
728
787
|
this.emit("sample", { samples: Math.min(target, Math.round(spent / height)), target });
|
|
@@ -743,14 +802,16 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
743
802
|
this.check(revision);
|
|
744
803
|
|
|
745
804
|
if (denoise) {
|
|
805
|
+
await this.denoiseNow(gpu, width, height);
|
|
806
|
+
this.check(revision);
|
|
807
|
+
|
|
746
808
|
const encoder = gpu.device.createCommandEncoder();
|
|
747
809
|
|
|
748
|
-
this.encodeDenoise(gpu, encoder, width, height);
|
|
749
810
|
this.encodePresent(encoder, width, height);
|
|
750
811
|
gpu.device.queue.submit([encoder.finish()]);
|
|
751
812
|
}
|
|
752
813
|
|
|
753
|
-
const image = await this.readImage(gpu, width, height, denoise
|
|
814
|
+
const image = await this.readImage(gpu, width, height, denoise && this.denoised ? this.denoiseOutput : null);
|
|
754
815
|
|
|
755
816
|
this.check(revision);
|
|
756
817
|
this.output = image;
|
|
@@ -808,7 +869,11 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
808
869
|
&& subsurfaceOf(material) !== null),
|
|
809
870
|
// Even turned off, a volume's boundary has to stay invisible, which takes the same code.
|
|
810
871
|
materials.some(material => volumeOf(material) !== null),
|
|
811
|
-
this.current.bevel.enabled && materials.some(material => bevelOf(material) > 0)
|
|
872
|
+
this.current.bevel.enabled && materials.some(material => bevelOf(material) > 0),
|
|
873
|
+
// Portals need windows: thin glass, which is what a physical material with transmission and no thickness is.
|
|
874
|
+
this.current.globalIllumination.portals !== false && materials.some(material =>
|
|
875
|
+
material.type === "MeshPhysicalMaterial" && Number(material.values.transmission) > 0
|
|
876
|
+
&& !(Number(material.values.thickness) > 0)));
|
|
812
877
|
|
|
813
878
|
// A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
|
|
814
879
|
const environment = assets.sky?.environment ?? image;
|
|
@@ -1048,10 +1113,11 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1048
1113
|
toneMapping: this.current.toneMapping,
|
|
1049
1114
|
vignette: this.current.vignette,
|
|
1050
1115
|
grain: this.current.grain,
|
|
1116
|
+
contrast: lookContrast(this.current),
|
|
1051
1117
|
frameIndex: this.frameIndex,
|
|
1052
1118
|
transparent: this.current.background.mode === "transparent",
|
|
1053
1119
|
denoised: this.encodeLens(this.gpu, encoder, width, height,
|
|
1054
|
-
this.denoised ? this.
|
|
1120
|
+
this.denoised ? this.denoiseOutput : null),
|
|
1055
1121
|
});
|
|
1056
1122
|
}
|
|
1057
1123
|
|
|
@@ -1060,13 +1126,25 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1060
1126
|
* buffer of graded colour the display transform reads instead, or the input as it was.
|
|
1061
1127
|
*/
|
|
1062
1128
|
private encodeLens(gpu: RenderDevice, encoder: GPUCommandEncoder, width: number, height: number,
|
|
1063
|
-
denoised: GPUBuffer | null): GPUBuffer | null {
|
|
1129
|
+
denoised: GPUBuffer | null, force = false): GPUBuffer | null {
|
|
1064
1130
|
const effects = lensEffects(this.current);
|
|
1065
1131
|
|
|
1066
1132
|
if (!effects) return denoised;
|
|
1067
1133
|
|
|
1068
1134
|
this.lensPass ??= new LensPass(gpu.device);
|
|
1069
1135
|
|
|
1136
|
+
// The canvas is drawn about once a display frame, but a denoised image only changes a few times a second: grading
|
|
1137
|
+
// the same one again would take the device from tracing for nothing. Straight from the accumulation, the image
|
|
1138
|
+
// grows with every pass, and is graded again at most every LENS_EVERY_MS.
|
|
1139
|
+
const key = JSON.stringify([this.revision, width, height, denoised ? this.denoiseVersion : -1, effects]);
|
|
1140
|
+
const now = performance.now();
|
|
1141
|
+
const fresh = key === this.lensKey && (denoised !== null || now - this.lensAt < LENS_EVERY_MS);
|
|
1142
|
+
|
|
1143
|
+
if (!force && fresh && this.lensPass.output) return this.lensPass.output;
|
|
1144
|
+
|
|
1145
|
+
this.lensKey = key;
|
|
1146
|
+
this.lensAt = now;
|
|
1147
|
+
|
|
1070
1148
|
return this.lensPass.encode(encoder, (this.accumulator as Accumulator).buffer, denoised, width, height, effects);
|
|
1071
1149
|
}
|
|
1072
1150
|
|
|
@@ -1075,7 +1153,65 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1075
1153
|
this.denoisePass ??= new DenoisePass(gpu.device);
|
|
1076
1154
|
this.denoisePass.encode(encoder, (this.accumulator as Accumulator).buffer, width, height, this.denoiserSettings,
|
|
1077
1155
|
this.denoiseView);
|
|
1156
|
+
this.denoiseOutput = this.denoisePass.output;
|
|
1078
1157
|
this.denoised = true;
|
|
1158
|
+
this.denoiseVersion++;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private usesOidn(): boolean {
|
|
1162
|
+
return this.denoiserSettings.algorithm === "oidn" && !this.oidnFailed;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Denoises the accumulation as it stands with the chosen denoiser, and resolves once the output holds it. SVGF is
|
|
1167
|
+
* submitted at once; OIDN runs after any run still in flight, and is dropped when a newer frame starts meanwhile.
|
|
1168
|
+
*/
|
|
1169
|
+
private async denoiseNow(gpu: RenderDevice, width: number, height: number): Promise<void> {
|
|
1170
|
+
if (this.usesOidn()) {
|
|
1171
|
+
const revision = this.revision;
|
|
1172
|
+
const run = this.oidnRun.then(() => this.runOidn(gpu, width, height, revision));
|
|
1173
|
+
|
|
1174
|
+
this.oidnRun = run.catch(() => undefined);
|
|
1175
|
+
|
|
1176
|
+
// Denoised, or replaced by a newer frame: either way nothing is left to do here.
|
|
1177
|
+
if (await run || !this.oidnFailed) return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
1181
|
+
|
|
1182
|
+
this.encodeDenoise(gpu, encoder, width, height);
|
|
1183
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private async runOidn(gpu: RenderDevice, width: number, height: number, revision: number): Promise<boolean> {
|
|
1187
|
+
if (revision !== this.revision || !this.accumulator) return false;
|
|
1188
|
+
|
|
1189
|
+
const signal = { cancelled: false };
|
|
1190
|
+
|
|
1191
|
+
const watch = () => { signal.cancelled = revision !== this.revision; };
|
|
1192
|
+
|
|
1193
|
+
this.oidnPass ??= new OidnPass(gpu.device);
|
|
1194
|
+
|
|
1195
|
+
try {
|
|
1196
|
+
const { strength, oidnWeights } = this.denoiserSettings;
|
|
1197
|
+
const timer = setInterval(watch, 50);
|
|
1198
|
+
|
|
1199
|
+
const done = await this.oidnPass.run(this.accumulator.buffer, width, height, strength,
|
|
1200
|
+
oidnWeights || DEFAULT_OIDN_WEIGHTS, signal).finally(() => clearInterval(timer));
|
|
1201
|
+
|
|
1202
|
+
if (!done || revision !== this.revision) return false;
|
|
1203
|
+
|
|
1204
|
+
this.denoiseOutput = this.oidnPass.output;
|
|
1205
|
+
this.denoised = true;
|
|
1206
|
+
this.denoiseVersion++;
|
|
1207
|
+
|
|
1208
|
+
return true;
|
|
1209
|
+
} catch(error) {
|
|
1210
|
+
this.oidnFailed = true;
|
|
1211
|
+
console.warn("GPU renderer: OIDN could not denoise, so SVGF does.", error);
|
|
1212
|
+
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1079
1215
|
}
|
|
1080
1216
|
|
|
1081
1217
|
private frameUniforms(
|
|
@@ -1145,6 +1281,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1145
1281
|
rouletteStart: Math.max(0, Math.round(this.current.globalIllumination.rouletteStart ?? 0)),
|
|
1146
1282
|
lensDistortion: Number.isFinite(this.current.lens.distortion) ? this.current.lens.distortion : 0,
|
|
1147
1283
|
subsurfaceEnabled: this.current.subsurface.enabled,
|
|
1284
|
+
subsurfaceDepth: Math.max(0, Math.round(Number(this.current.subsurface.depth ?? 2))),
|
|
1148
1285
|
bevelEnabled: this.current.bevel.enabled,
|
|
1149
1286
|
volumesEnabled: this.current.volumes.enabled,
|
|
1150
1287
|
cameraVolume: this.current.volumes.enabled
|
|
@@ -1163,6 +1300,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1163
1300
|
localLightPick: buffers?.localLightPick ?? 0,
|
|
1164
1301
|
lightTree: this.current.globalIllumination.lightTree === false ? 0 : buffers?.lightTree ?? 0,
|
|
1165
1302
|
lightLeaves: buffers?.lightLeaves ?? 0,
|
|
1303
|
+
lightList: buffers?.lightList ?? 0,
|
|
1304
|
+
portals: buffers?.portals ?? 0,
|
|
1305
|
+
portalCount: this.current.globalIllumination.portals === false ? 0 : buffers?.portalCount ?? 0,
|
|
1306
|
+
portalList: buffers?.portalList ?? 0,
|
|
1166
1307
|
clampIndirect: Math.max(0, this.current.globalIllumination.clamp),
|
|
1167
1308
|
// The setting is the angular diameter a light covers, in degrees, as it is for the CPU renderer's shadow blur.
|
|
1168
1309
|
shadowSpread: shadows.enabled ? Math.tan(Math.max(0, shadows.softness) * Math.PI / 360) : 0,
|
|
@@ -1206,12 +1347,12 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1206
1347
|
|
|
1207
1348
|
// The lens's effects are encoded on their own, ahead of the resolve, which reads the colour they leave.
|
|
1208
1349
|
const encoder = gpu.device.createCommandEncoder({ label: "lens" });
|
|
1209
|
-
const graded = this.encodeLens(gpu, encoder, width, height, denoised);
|
|
1350
|
+
const graded = this.encodeLens(gpu, encoder, width, height, denoised, true);
|
|
1210
1351
|
|
|
1211
1352
|
gpu.device.queue.submit([encoder.finish()]);
|
|
1212
1353
|
|
|
1213
1354
|
const data = await this.imageResolve.read(accumulator.buffer, graded, width, height,
|
|
1214
|
-
{ exposure, toneMapping, vignette, transparent });
|
|
1355
|
+
{ exposure, toneMapping, vignette, contrast: lookContrast(this.current), transparent });
|
|
1215
1356
|
|
|
1216
1357
|
return { width, height, data };
|
|
1217
1358
|
}
|
package/src/sceneUpload.ts
CHANGED
|
@@ -52,6 +52,11 @@ export interface SceneBuffers {
|
|
|
52
52
|
localLightPick: number;
|
|
53
53
|
lightTree: number;
|
|
54
54
|
lightLeaves: number;
|
|
55
|
+
lightList: number;
|
|
56
|
+
/** The windows the sky is sampled through: see lights.ts. */
|
|
57
|
+
portals: number;
|
|
58
|
+
portalCount: number;
|
|
59
|
+
portalList: number;
|
|
55
60
|
/** Radiance an ambient or hemisphere light adds, which no shadow ray can reach. */
|
|
56
61
|
ambient: PackedLights["ambient"];
|
|
57
62
|
/** Where the surrounding image and its distributions ended up, for the frame uniform. */
|
|
@@ -206,6 +211,10 @@ export function uploadScene(
|
|
|
206
211
|
localLightPick: lights.localPick,
|
|
207
212
|
lightTree: lights.treeAt,
|
|
208
213
|
lightLeaves: lights.leavesAt,
|
|
214
|
+
lightList: lights.listAt,
|
|
215
|
+
portals: lights.portalTreeAt,
|
|
216
|
+
portalCount: lights.portalCount,
|
|
217
|
+
portalList: lights.portalListAt,
|
|
209
218
|
ambient: lights.ambient,
|
|
210
219
|
environment: {
|
|
211
220
|
texture: environment ? textures.indexOf.get(environment.texture.id) ?? NO_TEXTURE : NO_TEXTURE,
|
package/src/settings.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface GpuRenderSettings extends Omit<RenderSettings, "globalIlluminat
|
|
|
20
20
|
* gets the samples a large panel far away would have taken.
|
|
21
21
|
*/
|
|
22
22
|
lightTree: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Inside a room, aims at the sky through its windows (every thin transmissive material) as well as by its
|
|
25
|
+
* brightness, as Cycles' portals do, so daylight that enters through glass is found instead of stumbled upon.
|
|
26
|
+
* A point counts as inside when something is above it; elsewhere the sky is sampled as before.
|
|
27
|
+
*/
|
|
28
|
+
portals: boolean;
|
|
23
29
|
};
|
|
24
30
|
/**
|
|
25
31
|
* The light a real lens and sensor scatter around anything bright: a soft halo that fades with distance. Energy
|
|
@@ -60,11 +66,24 @@ export interface GpuRenderSettings extends Omit<RenderSettings, "globalIlluminat
|
|
|
60
66
|
};
|
|
61
67
|
/** A camera's exposure triangle and white balance. */
|
|
62
68
|
camera: {
|
|
69
|
+
/**
|
|
70
|
+
* The contrast and saturation of AgX's look, `0` to `1`: `0` is its base curve, flat on purpose, and `1` a look
|
|
71
|
+
* like Blender's Punchy, pivoted on middle grey so the image keeps its brightness. The other tone mappings ignore it.
|
|
72
|
+
*/
|
|
73
|
+
contrast: number;
|
|
63
74
|
/**
|
|
64
75
|
* Exposes from `iso`, `shutterSpeed` and `depthOfField.aperture` as a camera would, with `exposure` as a
|
|
65
76
|
* compensation on top. The scene's lights then have to be in physical units (lux, candela, nits).
|
|
66
77
|
*/
|
|
67
78
|
physicalExposure: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Meters the image as a camera in automatic does and exposes it to middle grey, from the mean of the logarithms of
|
|
81
|
+
* its luminance, weighed towards the centre: a bright window or sky cannot darken the rest. It takes the place of
|
|
82
|
+
* `exposure` and of the physical exposure, with `exposureCompensation` on top.
|
|
83
|
+
*/
|
|
84
|
+
autoExposure: boolean;
|
|
85
|
+
/** Stops added to the metered exposure: positive brightens, as on a camera's dial. */
|
|
86
|
+
exposureCompensation: number;
|
|
68
87
|
iso: number;
|
|
69
88
|
/** Seconds. */
|
|
70
89
|
shutterSpeed: number;
|
|
@@ -78,6 +97,12 @@ export interface GpuRenderSettings extends Omit<RenderSettings, "globalIlluminat
|
|
|
78
97
|
subsurface: {
|
|
79
98
|
/** Materials with `userData.cpuRenderer.subsurface` scatter light inside themselves. Off renders them opaque. */
|
|
80
99
|
enabled: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Bounces at which light still walks inside such a material: past them it reflects off it as a diffuse surface of
|
|
102
|
+
* the same colour. Light that has already bounced that often hardly shows the difference, and every walk costs as
|
|
103
|
+
* much as many bounces.
|
|
104
|
+
*/
|
|
105
|
+
depth: number;
|
|
81
106
|
};
|
|
82
107
|
/**
|
|
83
108
|
* Aerial perspective: the air between the camera and what it sees, which fades distant things towards the colour of
|
|
@@ -123,13 +148,16 @@ export const DEFAULT_GPU_RENDER_SETTINGS: Readonly<GpuRenderSettings> = Object.f
|
|
|
123
148
|
...DEFAULT_RENDER_SETTINGS,
|
|
124
149
|
// AgX rolls highlights off as film does, where ACES pushes saturated colours to the gamut's edge.
|
|
125
150
|
toneMapping: "agx",
|
|
126
|
-
globalIllumination: { ...DEFAULT_RENDER_SETTINGS.globalIllumination, russianRoulette: true, rouletteStart:
|
|
127
|
-
lightTree: true },
|
|
151
|
+
globalIllumination: { ...DEFAULT_RENDER_SETTINGS.globalIllumination, russianRoulette: true, rouletteStart: 1,
|
|
152
|
+
lightTree: true, portals: true },
|
|
128
153
|
bloom: { enabled: true, intensity: 0.04, radius: 0.65 },
|
|
129
154
|
glare: { enabled: true, intensity: 0.06, blades: 6, length: 0.25, rotation: 15 },
|
|
130
|
-
lens: { chromaticAberration: 0.
|
|
131
|
-
camera: {
|
|
132
|
-
|
|
155
|
+
lens: { chromaticAberration: 0.05, distortion: 0 },
|
|
156
|
+
camera: {
|
|
157
|
+
contrast: 1, physicalExposure: false, autoExposure: false, exposureCompensation: 0, iso: 100,
|
|
158
|
+
shutterSpeed: 1 / 125, whiteBalance: 6500,
|
|
159
|
+
},
|
|
160
|
+
subsurface: { enabled: true, depth: 2 },
|
|
133
161
|
atmosphere: { enabled: true, visibility: 20000, haze: 0.6, color: "" },
|
|
134
162
|
volumes: { enabled: true },
|
|
135
163
|
bevel: { enabled: true },
|
|
@@ -154,10 +182,16 @@ export function physicalExposure(aperture: number, shutterSpeed: number, iso: nu
|
|
|
154
182
|
return 1 / (1.2 * 2 ** ev100);
|
|
155
183
|
}
|
|
156
184
|
|
|
157
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* The exposure the image is multiplied by: the setting itself, or a camera's on top of it as compensation, or with
|
|
187
|
+
* the automatic exposure only its compensation, the meter's own exposure being applied on the device.
|
|
188
|
+
*/
|
|
158
189
|
export function effectiveExposure(settings: GpuRenderSettings): number {
|
|
159
190
|
const { camera, depthOfField, exposure } = settings;
|
|
160
191
|
|
|
192
|
+
// The meter's exposure is applied on the device, by the lens pass; what is left here is the compensation.
|
|
193
|
+
if (camera.autoExposure) return 2 ** (Number(camera.exposureCompensation) || 0);
|
|
194
|
+
|
|
161
195
|
return camera.physicalExposure
|
|
162
196
|
? exposure * physicalExposure(depthOfField.aperture, camera.shutterSpeed, camera.iso)
|
|
163
197
|
: exposure;
|
package/src/wgsl/lens.wgsl.ts
CHANGED
|
@@ -46,7 +46,8 @@ struct Lens {
|
|
|
46
46
|
// The size of the level the rays are gathered at, and the angle of the first ray, in radians.
|
|
47
47
|
streakSize: vec2<u32>,
|
|
48
48
|
glareAngle: f32,
|
|
49
|
-
|
|
49
|
+
// Whether the camera meters the image and exposes it to middle grey, which the meter pass works out.
|
|
50
|
+
autoExposure: f32,
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
const PI: f32 = 3.14159265358979;
|
|
@@ -61,6 +62,108 @@ const STREAK_TAPS: i32 = 24;
|
|
|
61
62
|
@group(0) @binding(4) var<storage, read_write> up: array<vec4<f32>>;
|
|
62
63
|
@group(0) @binding(5) var<storage, read_write> graded: array<vec4<f32>>;
|
|
63
64
|
@group(0) @binding(6) var<storage, read_write> streaked: array<vec4<f32>>;
|
|
65
|
+
// What the camera's meter read: the exposure that brings the image's average to middle grey, in x.
|
|
66
|
+
@group(0) @binding(7) var<storage, read_write> metered: array<vec4<f32>>;
|
|
67
|
+
|
|
68
|
+
/** What a light meter calibrates to: an average scene reflects about eighteen percent. */
|
|
69
|
+
const MIDDLE_GREY: f32 = 0.18;
|
|
70
|
+
const METER_GRID: vec2<u32> = vec2<u32>(64u, 36u);
|
|
71
|
+
const METER_THREADS: u32 = 256u;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* How far above middle grey the meter lets the bright end of the image land: three stops, where AgX still shows
|
|
75
|
+
* detail, three and a half under where it reaches white.
|
|
76
|
+
*/
|
|
77
|
+
const HIGHLIGHT_CEILING: f32 = 8.0;
|
|
78
|
+
/** The share of the metered points that counts as the bright end: the light through a window, not a lamp's own pixels. */
|
|
79
|
+
const HIGHLIGHT_SHARE: f32 = 0.03;
|
|
80
|
+
/** Half stops of luminance, from 2^-16 to 2^16, which the bright end is found in. */
|
|
81
|
+
const METER_BINS: u32 = 64u;
|
|
82
|
+
|
|
83
|
+
var<workgroup> meterSums: array<vec2<f32>, 256>;
|
|
84
|
+
var<workgroup> meterHistogram: array<atomic<u32>, 64>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The camera's meter: the image's average luminance over a grid of points, taken as the mean of the logarithms so that
|
|
88
|
+
* a bright window or a patch of sky cannot darken everything else, weighed towards the centre as a camera's
|
|
89
|
+
* centre-weighted metering is. What no path has reached yet, or is black, does not count. One workgroup reads the
|
|
90
|
+
* grid and sums it in shared memory.
|
|
91
|
+
*
|
|
92
|
+
* Then it guards the highlights, as a camera's matrix metering does: the average alone takes a dim attic with a shaft
|
|
93
|
+
* of sun in it to middle grey and burns the shaft out, where a photographer keeps the room dim. The brightest few
|
|
94
|
+
* percent of the points, found in a histogram of half stops, may land no more than {@link HIGHLIGHT_CEILING} times
|
|
95
|
+
* brighter; an even scene, whose bright end is close to its average, is left as the average says.
|
|
96
|
+
*/
|
|
97
|
+
@compute @workgroup_size(256, 1, 1)
|
|
98
|
+
fn meter(@builtin(local_invocation_index) thread: u32) {
|
|
99
|
+
var sum = vec2<f32>(0.0);
|
|
100
|
+
let count = METER_GRID.x * METER_GRID.y;
|
|
101
|
+
|
|
102
|
+
if (thread < METER_BINS) {
|
|
103
|
+
atomicStore(&meterHistogram[thread], 0u);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
workgroupBarrier();
|
|
107
|
+
|
|
108
|
+
for (var k = thread; k < count; k = k + METER_THREADS) {
|
|
109
|
+
let cell = vec2<f32>(f32(k % METER_GRID.x), f32(k / METER_GRID.x)) + 0.5;
|
|
110
|
+
let uv = cell / vec2<f32>(METER_GRID);
|
|
111
|
+
let luminance = dot(imageTexel(i32(uv.x * f32(lens.resolution.x)), i32(uv.y * f32(lens.resolution.y))),
|
|
112
|
+
vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
113
|
+
let away = (uv - 0.5) * 2.0;
|
|
114
|
+
let weight = select(0.0, 1.0 - 0.6 * min(dot(away, away) * 0.5, 1.0), luminance > 0.0);
|
|
115
|
+
|
|
116
|
+
sum = sum + vec2<f32>(log(max(luminance, 1e-6)) * weight, weight);
|
|
117
|
+
|
|
118
|
+
if (luminance > 0.0) {
|
|
119
|
+
let bin = u32(clamp((log2(luminance) + 16.0) * 2.0, 0.0, f32(METER_BINS - 1u)));
|
|
120
|
+
|
|
121
|
+
atomicAdd(&meterHistogram[bin], 1u);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
meterSums[thread] = sum;
|
|
126
|
+
workgroupBarrier();
|
|
127
|
+
|
|
128
|
+
for (var stride = METER_THREADS / 2u; stride > 0u; stride = stride / 2u) {
|
|
129
|
+
if (thread < stride) {
|
|
130
|
+
meterSums[thread] = meterSums[thread] + meterSums[thread + stride];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
workgroupBarrier();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (thread == 0u) {
|
|
137
|
+
let total = meterSums[0];
|
|
138
|
+
let average = select(MIDDLE_GREY, exp(total.x / max(total.y, 1e-6)), total.y > 0.0);
|
|
139
|
+
var exposure = MIDDLE_GREY / average;
|
|
140
|
+
|
|
141
|
+
// From the top down, the first half stop by which the bright share is reached; its upper edge is that end.
|
|
142
|
+
var points = 0u;
|
|
143
|
+
|
|
144
|
+
for (var bin = 0u; bin < METER_BINS; bin = bin + 1u) {
|
|
145
|
+
points = points + atomicLoad(&meterHistogram[bin]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var above = 0u;
|
|
149
|
+
var bright = 0.0;
|
|
150
|
+
|
|
151
|
+
for (var bin = i32(METER_BINS) - 1; bin >= 0 && points > 0u; bin = bin - 1) {
|
|
152
|
+
above = above + atomicLoad(&meterHistogram[u32(bin)]);
|
|
153
|
+
|
|
154
|
+
if (f32(above) >= HIGHLIGHT_SHARE * f32(points)) {
|
|
155
|
+
bright = exp2(f32(bin + 1) * 0.5 - 16.0);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (bright > 0.0) {
|
|
161
|
+
exposure = min(exposure, HIGHLIGHT_CEILING * MIDDLE_GREY / bright);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
metered[0] = vec4<f32>(clamp(exposure, 1e-4, 1e4), average, bright, 0.0);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
64
167
|
|
|
65
168
|
/** The image's own texel: its mean so far, or nothing where no path has landed yet. */
|
|
66
169
|
fn imageTexel(x: i32, y: i32) -> vec3<f32> {
|
|
@@ -260,21 +363,6 @@ fn streakBilinear(position: vec2<f32>) -> vec3<f32> {
|
|
|
260
363
|
return mix(mix(streakTexel(x, y), streakTexel(x + 1, y), b.z), mix(streakTexel(x, y + 1), streakTexel(x + 1, y + 1), b.z), b.w);
|
|
261
364
|
}
|
|
262
365
|
|
|
263
|
-
/** The folded halo at full resolution: a tent over its largest level, which is half the image's size. */
|
|
264
|
-
fn tentHalo(position: vec2<f32>) -> vec3<f32> {
|
|
265
|
-
var sum = vec3<f32>(0.0);
|
|
266
|
-
|
|
267
|
-
for (var dy = -1; dy <= 1; dy = dy + 1) {
|
|
268
|
-
for (var dx = -1; dx <= 1; dx = dx + 1) {
|
|
269
|
-
let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;
|
|
270
|
-
|
|
271
|
-
sum = sum + upBilinear(lens.haloOffset, lens.haloSize, position + vec2<f32>(f32(dx), f32(dy)) * 0.5) * weight;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return sum;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
366
|
/** The image as the lens delivers it: fringed towards the corners, with its halo, balanced for the camera's white. */
|
|
279
367
|
@compute @workgroup_size(8, 8, 1)
|
|
280
368
|
fn composite(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
@@ -298,7 +386,8 @@ fn composite(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
298
386
|
}
|
|
299
387
|
|
|
300
388
|
if (lens.intensity > 0.0) {
|
|
301
|
-
|
|
389
|
+
// The largest folded level is already a tent over the levels below it, so one bilinear read of it is smooth.
|
|
390
|
+
let halo = upBilinear(lens.haloOffset, lens.haloSize, position * vec2<f32>(lens.haloSize) / size);
|
|
302
391
|
|
|
303
392
|
color = mix(color, halo, lens.intensity);
|
|
304
393
|
}
|
|
@@ -307,6 +396,10 @@ fn composite(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
307
396
|
color = mix(color, streakBilinear(position * vec2<f32>(lens.streakSize) / size), lens.glare);
|
|
308
397
|
}
|
|
309
398
|
|
|
399
|
+
if (lens.autoExposure > 0.0) {
|
|
400
|
+
color = color * metered[0].x;
|
|
401
|
+
}
|
|
402
|
+
|
|
310
403
|
let balanced = vec3<f32>(dot(lens.white0.xyz, color), dot(lens.white1.xyz, color), dot(lens.white2.xyz, color));
|
|
311
404
|
|
|
312
405
|
graded[id.y * lens.resolution.x + id.x] = vec4<f32>(max(balanced, vec3<f32>(0.0)), 1.0);
|