@tinypoly/gpu-renderer 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/ARCHITECTURE.md +31 -3
  2. package/README.md +11 -3
  3. package/dist/denoise.d.ts +10 -1
  4. package/dist/denoise.d.ts.map +1 -1
  5. package/dist/denoise.js +4 -0
  6. package/dist/denoise.js.map +1 -1
  7. package/dist/device.d.ts.map +1 -1
  8. package/dist/device.js +8 -0
  9. package/dist/device.js.map +1 -1
  10. package/dist/frameLayout.d.ts.map +1 -1
  11. package/dist/frameLayout.js +4 -1
  12. package/dist/frameLayout.js.map +1 -1
  13. package/dist/imageResolve.d.ts +2 -1
  14. package/dist/imageResolve.d.ts.map +1 -1
  15. package/dist/imageResolve.js +1 -0
  16. package/dist/imageResolve.js.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/lensPass.d.ts +4 -0
  22. package/dist/lensPass.d.ts.map +1 -1
  23. package/dist/lensPass.js +14 -1
  24. package/dist/lensPass.js.map +1 -1
  25. package/dist/lights.d.ts +18 -0
  26. package/dist/lights.d.ts.map +1 -1
  27. package/dist/lights.js +45 -0
  28. package/dist/lights.js.map +1 -1
  29. package/dist/oidnPass.d.ts +48 -0
  30. package/dist/oidnPass.d.ts.map +1 -0
  31. package/dist/oidnPass.js +255 -0
  32. package/dist/oidnPass.js.map +1 -0
  33. package/dist/pathTracePass.d.ts +2 -2
  34. package/dist/pathTracePass.d.ts.map +1 -1
  35. package/dist/pathTracePass.js +9 -9
  36. package/dist/pathTracePass.js.map +1 -1
  37. package/dist/presentPass.d.ts +2 -0
  38. package/dist/presentPass.d.ts.map +1 -1
  39. package/dist/presentPass.js +1 -0
  40. package/dist/presentPass.js.map +1 -1
  41. package/dist/renderer.d.ts +16 -1
  42. package/dist/renderer.d.ts.map +1 -1
  43. package/dist/renderer.js +111 -15
  44. package/dist/renderer.js.map +1 -1
  45. package/dist/sceneUpload.d.ts +4 -0
  46. package/dist/sceneUpload.d.ts.map +1 -1
  47. package/dist/sceneUpload.js +3 -0
  48. package/dist/sceneUpload.js.map +1 -1
  49. package/dist/settings.d.ts +23 -1
  50. package/dist/settings.d.ts.map +1 -1
  51. package/dist/settings.js +13 -4
  52. package/dist/settings.js.map +1 -1
  53. package/dist/wgsl/lens.wgsl.d.ts +1 -1
  54. package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
  55. package/dist/wgsl/lens.wgsl.js +108 -1
  56. package/dist/wgsl/lens.wgsl.js.map +1 -1
  57. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  58. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  59. package/dist/wgsl/lights.wgsl.js +147 -17
  60. package/dist/wgsl/lights.wgsl.js.map +1 -1
  61. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  62. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  63. package/dist/wgsl/pathTrace.wgsl.js +123 -91
  64. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  65. package/dist/wgsl/present.wgsl.d.ts +1 -1
  66. package/dist/wgsl/present.wgsl.d.ts.map +1 -1
  67. package/dist/wgsl/present.wgsl.js +11 -1
  68. package/dist/wgsl/present.wgsl.js.map +1 -1
  69. package/package.json +4 -2
  70. package/src/denoise.ts +15 -1
  71. package/src/device.ts +9 -0
  72. package/src/frameLayout.ts +4 -1
  73. package/src/imageResolve.ts +2 -0
  74. package/src/index.ts +2 -1
  75. package/src/lensPass.ts +17 -1
  76. package/src/lights.ts +60 -0
  77. package/src/oidnPass.ts +284 -0
  78. package/src/oidnWebgpu.d.ts +5 -0
  79. package/src/pathTracePass.ts +11 -9
  80. package/src/presentPass.ts +3 -0
  81. package/src/renderer.ts +133 -16
  82. package/src/sceneUpload.ts +7 -0
  83. package/src/settings.ts +32 -4
  84. package/src/wgsl/lens.wgsl.ts +108 -1
  85. package/src/wgsl/lights.wgsl.ts +147 -17
  86. package/src/wgsl/pathTrace.wgsl.ts +123 -91
  87. package/src/wgsl/present.wgsl.ts +11 -1
  88. package/weights/LICENSE.txt +202 -0
  89. package/weights/rt_hdr_alb_nrm.tza +0 -0
package/src/lights.ts CHANGED
@@ -73,6 +73,49 @@ export interface PackedLights {
73
73
  leavesAt: number;
74
74
  /** Where the lights of the tree's leaves are listed in the texture buffer. */
75
75
  listAt: number;
76
+ /**
77
+ * The windows the sky is also sampled through, from inside: the triangles of every thin transmissive material, in a
78
+ * tree of their own, as the lights have, so a point aims at the windows near it rather than at every one in the
79
+ * building alike. Where its nodes start, where its leaves' list starts, and how many windows there are.
80
+ */
81
+ portalTreeAt: number;
82
+ portalListAt: number;
83
+ portalCount: number;
84
+ }
85
+
86
+ /**
87
+ * The triangles of thin glass, which is where daylight enters a room, as the tree sees them: their bounds, their area
88
+ * as power, so a window weighs by the solid angle it takes from a point, and no orientation, since a pane lets the sky
89
+ * through from either side.
90
+ */
91
+ export function portalTriangles(geometry: PackedGeometry): { triangles: number[]; lights: TreeLight[] } {
92
+ const windows = geometry.materialProperties.map(p => p.transmission > 0 && p.thickness <= 0);
93
+ const triangles: number[] = [], lights: TreeLight[] = [];
94
+
95
+ for (let triangle = 0; triangle < geometry.triangleCount; triangle++) {
96
+ if (!windows[geometry.materials[triangle]]) continue;
97
+
98
+ const at = triangle * 9;
99
+ const area = doubleArea(geometry.positions, at) / 2;
100
+
101
+ if (!(area > 0)) continue;
102
+
103
+ const corner = (k: number) => [0, 1, 2].map(c => geometry.positions[at + k * 3 + c]);
104
+ const points = [corner(0), corner(1), corner(2)];
105
+
106
+ triangles.push(triangle);
107
+ lights.push({
108
+ low: [0, 1, 2].map(c => Math.min(points[0][c], points[1][c], points[2][c])),
109
+ high: [0, 1, 2].map(c => Math.max(points[0][c], points[1][c], points[2][c])),
110
+ power: area,
111
+ axis: [0, 1, 0],
112
+ thetaO: Math.PI,
113
+ thetaE: Math.PI / 2,
114
+ density: 1 / area,
115
+ });
116
+ }
117
+
118
+ return { triangles, lights };
76
119
  }
77
120
 
78
121
  const luminance = (color: ArrayLike<number>) => 0.2126 * color[0] + 0.7152 * color[1] + 0.0722 * color[2];
@@ -439,7 +482,24 @@ export function packLights(
439
482
  for (const leaf of tree.leafOf) extra.push(leaf);
440
483
  }
441
484
 
485
+ const portals = environment && environment.power > 0 ? portalTriangles(geometry) : { triangles: [], lights: [] };
486
+ let portalTreeAt = 0, portalListAt = 0;
487
+
488
+ if (portals.triangles.length) {
489
+ const tree = buildLightTree(portals.lights);
490
+
491
+ portalTreeAt = base + extra.length;
492
+ portalListAt = portalTreeAt + tree.listAt;
493
+
494
+ // The leaves list window numbers; the kernel wants the triangles they stand for.
495
+ for (let k = tree.listAt; k < tree.words.length; k += 4) tree.words[k] = portals.triangles[tree.words[k]];
496
+ for (const word of tree.words) extra.push(word);
497
+ }
498
+
442
499
  return {
500
+ portalTreeAt,
501
+ portalListAt,
502
+ portalCount: portals.triangles.length,
443
503
  localCount,
444
504
  localPick: localCount > 0 ? floats[(localCount - 1) * LIGHT_WORDS + 1] : 0,
445
505
  treeAt,
@@ -0,0 +1,284 @@
1
+ import type { UNet } from "oidn-webgpu";
2
+ import { initUNetFromURL } from "oidn-webgpu/dist/oidn.js";
3
+ import { createShaderModule } from "./shaderModule.js";
4
+
5
+ /**
6
+ * The weights the OIDN denoiser loads by default: the HDR ray tracing network that takes the albedo and the normal as
7
+ * well as the colour (RenderKit's oidn-weights, Apache-2.0), shipped with the package. A bundler that moves the package
8
+ * elsewhere, or an app that serves its own copy, passes `denoiser.oidnWeights` instead.
9
+ */
10
+ export const DEFAULT_OIDN_WEIGHTS = new URL("../weights/rt_hdr_alb_nrm.tza", import.meta.url).href;
11
+
12
+ const WORKGROUP = 8;
13
+ const METER_GRID = [64, 36] as const;
14
+
15
+ /**
16
+ * Reads the accumulation into the three images OIDN takes, and back.
17
+ *
18
+ * The network was trained on images whose average sits near middle grey, which its own CPU path reaches by an
19
+ * automatic exposure; on the device that scale is left to the caller. `meter` takes the mean of the logarithms of the
20
+ * image's luminance over a grid, as the lens pass's meter does, `prepare` writes the colour times the scale that brings
21
+ * it to middle grey, the albedo (diffuse and specular, so a mirror is not taken for black) and the normal, and `finish`
22
+ * divides the network's output by the same scale and mixes it with the traced image by the strength.
23
+ */
24
+ export const OIDN_WGSL = /* wgsl */ `
25
+ struct Params {
26
+ size: vec2<u32>,
27
+ count: u32,
28
+ strength: f32,
29
+ };
30
+
31
+ @group(0) @binding(0) var<uniform> params: Params;
32
+ @group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;
33
+ @group(0) @binding(2) var<storage, read_write> color: array<vec4<f32>>;
34
+ @group(0) @binding(3) var<storage, read_write> albedo: array<vec4<f32>>;
35
+ @group(0) @binding(4) var<storage, read_write> normal: array<vec4<f32>>;
36
+ @group(0) @binding(5) var<storage, read_write> scale: array<f32>;
37
+
38
+ const MIDDLE_GREY: f32 = 0.18;
39
+ const GRID: vec2<u32> = vec2<u32>(${METER_GRID[0]}u, ${METER_GRID[1]}u);
40
+
41
+ var<workgroup> sums: array<vec2<f32>, 256>;
42
+
43
+ fn luminance(value: vec3<f32>) -> f32 {
44
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
45
+ }
46
+
47
+ fn mean(index: u32) -> vec3<f32> {
48
+ let stored = accumulation[index];
49
+
50
+ return select(vec3<f32>(0.0), stored.rgb / stored.w, stored.w > 0.0);
51
+ }
52
+
53
+ @compute @workgroup_size(256, 1, 1)
54
+ fn meter(@builtin(local_invocation_index) thread: u32) {
55
+ var sum = vec2<f32>(0.0);
56
+
57
+ for (var k = thread; k < GRID.x * GRID.y; k = k + 256u) {
58
+ let uv = (vec2<f32>(f32(k % GRID.x), f32(k / GRID.x)) + 0.5) / vec2<f32>(GRID);
59
+ let pixel = min(vec2<u32>(uv * vec2<f32>(params.size)), params.size - 1u);
60
+ let value = luminance(mean(pixel.y * params.size.x + pixel.x));
61
+
62
+ if (value > 0.0) {
63
+ sum = sum + vec2<f32>(log(value), 1.0);
64
+ }
65
+ }
66
+
67
+ sums[thread] = sum;
68
+ workgroupBarrier();
69
+
70
+ for (var stride = 128u; stride > 0u; stride = stride / 2u) {
71
+ if (thread < stride) {
72
+ sums[thread] = sums[thread] + sums[thread + stride];
73
+ }
74
+
75
+ workgroupBarrier();
76
+ }
77
+
78
+ if (thread == 0u) {
79
+ let total = sums[0];
80
+
81
+ scale[0] = select(1.0, clamp(MIDDLE_GREY / exp(total.x / total.y), 1e-6, 1e6), total.y > 0.0);
82
+ }
83
+ }
84
+
85
+ @compute @workgroup_size(${WORKGROUP}, ${WORKGROUP})
86
+ fn prepare(@builtin(global_invocation_id) id: vec3<u32>) {
87
+ if (id.x >= params.size.x || id.y >= params.size.y) {
88
+ return;
89
+ }
90
+
91
+ let index = id.y * params.size.x + id.x;
92
+ let samples = max(accumulation[index].w, 1.0);
93
+ let traced = mean(index) * scale[0];
94
+ let normalSum = accumulation[3u * params.count + index].xyz;
95
+ let size = length(normalSum);
96
+ let surface = size > 1e-6;
97
+ let reflectance = (accumulation[2u * params.count + index].rgb + accumulation[5u * params.count + index].rgb)
98
+ / samples;
99
+
100
+ color[index] = vec4<f32>(max(traced, vec3<f32>(0.0)), 1.0);
101
+ // Where the camera sees no surface, the sky, the albedo is the colour itself, as OIDN asks of the background.
102
+ albedo[index] = vec4<f32>(select(clamp(traced, vec3<f32>(0.0), vec3<f32>(1.0)),
103
+ clamp(reflectance, vec3<f32>(0.0), vec3<f32>(1.0)), surface), 1.0);
104
+ normal[index] = vec4<f32>(select(vec3<f32>(0.0), normalSum / size, surface), 0.0);
105
+ }
106
+
107
+ // The network's output arrives in the colour binding; the albedo binding takes the result.
108
+ @compute @workgroup_size(${WORKGROUP}, ${WORKGROUP})
109
+ fn finish(@builtin(global_invocation_id) id: vec3<u32>) {
110
+ if (id.x >= params.size.x || id.y >= params.size.y) {
111
+ return;
112
+ }
113
+
114
+ let index = id.y * params.size.x + id.x;
115
+ let traced = mean(index);
116
+ let denoised = max(color[index].rgb, vec3<f32>(0.0)) / scale[0];
117
+
118
+ albedo[index] = vec4<f32>(mix(traced, denoised, clamp(params.strength, 0.0, 1.0)), 1.0);
119
+ }
120
+ `;
121
+
122
+ const networks = new WeakMap<GPUDevice, Map<string, Promise<UNet>>>();
123
+
124
+ /** One network per device and weights file, kept for as long as the device lives: loading it compiles its kernels. */
125
+ function network(device: GPUDevice, weights: string): Promise<UNet> {
126
+ let byUrl = networks.get(device);
127
+
128
+ if (!byUrl) networks.set(device, byUrl = new Map());
129
+
130
+ let unet = byUrl.get(weights);
131
+
132
+ if (!unet) {
133
+ unet = initUNetFromURL(weights, { device }, { aux: true, hdr: true });
134
+ // A file that failed to load may load on a later attempt.
135
+ unet.catch(() => byUrl.delete(weights));
136
+ byUrl.set(weights, unet);
137
+ }
138
+
139
+ return unet;
140
+ }
141
+
142
+ /**
143
+ * Open Image Denoise (Intel's neural denoiser, through oidn-webgpu): the image, its albedo and its normal through a
144
+ * U-Net trained on path traced images, entirely on the device. It keeps texture and fine detail that a hand-made
145
+ * filter blurs, at the cost of a fraction of a second for a large image.
146
+ */
147
+ export class OidnPass {
148
+ private readonly layout: GPUBindGroupLayout;
149
+ private readonly meter: GPUComputePipeline;
150
+ private readonly prepare: GPUComputePipeline;
151
+ private readonly finish: GPUComputePipeline;
152
+ private readonly params: GPUBuffer;
153
+ private readonly scale: GPUBuffer;
154
+ private buffers: GPUBuffer[] = [];
155
+ private width = 0;
156
+ private height = 0;
157
+ /** The denoised radiance, one vec4 per pixel, once `run` has resolved. */
158
+ output: GPUBuffer | null = null;
159
+
160
+ constructor(private readonly device: GPUDevice) {
161
+ const module = createShaderModule(device, "oidn", OIDN_WGSL);
162
+ const visibility = GPUShaderStage.COMPUTE;
163
+ const storage = (type: GPUBufferBindingType) => ({ binding: 0, visibility, buffer: { type } });
164
+
165
+ this.layout = device.createBindGroupLayout({
166
+ entries: [
167
+ { binding: 0, visibility, buffer: { type: "uniform" } },
168
+ { ...storage("read-only-storage"), binding: 1 },
169
+ { ...storage("storage"), binding: 2 },
170
+ { ...storage("storage"), binding: 3 },
171
+ { ...storage("storage"), binding: 4 },
172
+ { ...storage("storage"), binding: 5 },
173
+ ],
174
+ });
175
+
176
+ const layout = device.createPipelineLayout({ bindGroupLayouts: [this.layout] });
177
+ const pipeline = (entryPoint: string) => device.createComputePipeline({ layout, compute: { module, entryPoint } });
178
+
179
+ this.meter = pipeline("meter");
180
+ this.prepare = pipeline("prepare");
181
+ this.finish = pipeline("finish");
182
+ this.params = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
183
+ this.scale = device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE });
184
+ }
185
+
186
+ private resize(width: number, height: number) {
187
+ if (width === this.width && height === this.height && this.buffers.length) return;
188
+
189
+ for (const buffer of this.buffers) buffer.destroy();
190
+
191
+ const size = Math.max(16, width * height * 16);
192
+ const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST;
193
+
194
+ // Colour, albedo and normal for the network, then the result.
195
+ this.buffers = Array.from({ length: 4 }, () => this.device.createBuffer({ size, usage }));
196
+ this.output = this.buffers[3];
197
+ this.width = width;
198
+ this.height = height;
199
+ }
200
+
201
+ private group(accumulation: GPUBuffer, color: GPUBuffer, albedo: GPUBuffer, normal: GPUBuffer): GPUBindGroup {
202
+ return this.device.createBindGroup({
203
+ layout: this.layout,
204
+ entries: [
205
+ { binding: 0, resource: { buffer: this.params } },
206
+ { binding: 1, resource: { buffer: accumulation } },
207
+ { binding: 2, resource: { buffer: color } },
208
+ { binding: 3, resource: { buffer: albedo } },
209
+ { binding: 4, resource: { buffer: normal } },
210
+ { binding: 5, resource: { buffer: this.scale } },
211
+ ],
212
+ });
213
+ }
214
+
215
+ private dispatch(encoder: GPUCommandEncoder, pipeline: GPUComputePipeline, group: GPUBindGroup, x: number,
216
+ y: number) {
217
+ const pass = encoder.beginComputePass({ label: "oidn" });
218
+
219
+ pass.setPipeline(pipeline);
220
+ pass.setBindGroup(0, group);
221
+ pass.dispatchWorkgroups(x, y);
222
+ pass.end();
223
+ }
224
+
225
+ /**
226
+ * Denoises what `accumulation` holds into `output`. The images are read from it as it stands on the queue when this
227
+ * is called; the promise resolves once the result has been written, and rejects when the network cannot load.
228
+ */
229
+ async run(accumulation: GPUBuffer, width: number, height: number, strength: number, weights: string,
230
+ signal?: { cancelled: boolean }): Promise<boolean> {
231
+ const unet = await network(this.device, weights);
232
+
233
+ if (signal?.cancelled) return false;
234
+
235
+ this.resize(width, height);
236
+
237
+ const [color, albedo, normal, output] = this.buffers;
238
+ const values = new ArrayBuffer(16);
239
+
240
+ new Uint32Array(values).set([width, height, width * height]);
241
+ new Float32Array(values)[3] = strength;
242
+ this.device.queue.writeBuffer(this.params, 0, values);
243
+
244
+ const groups = [Math.ceil(width / WORKGROUP), Math.ceil(height / WORKGROUP)] as const;
245
+ const inputs = this.group(accumulation, color, albedo, normal);
246
+ const encoder = this.device.createCommandEncoder({ label: "oidn prepare" });
247
+
248
+ this.dispatch(encoder, this.meter, inputs, 1, 1);
249
+ this.dispatch(encoder, this.prepare, inputs, ...groups);
250
+ this.device.queue.submit([encoder.finish()]);
251
+
252
+ const denoised = await new Promise<GPUBuffer | null>((resolve, reject) => {
253
+ const abort = unet.tileExecute({
254
+ color: { data: color, width, height },
255
+ albedo: { data: albedo, width, height },
256
+ normal: { data: normal, width, height },
257
+ done: result => resolve(result.data),
258
+ error: reject,
259
+ progress: () => {
260
+ if (signal?.cancelled) {
261
+ abort();
262
+ resolve(null);
263
+ }
264
+ },
265
+ });
266
+ });
267
+
268
+ if (!denoised || signal?.cancelled) return false;
269
+
270
+ const finishing = this.device.createCommandEncoder({ label: "oidn finish" });
271
+
272
+ this.dispatch(finishing, this.finish, this.group(accumulation, denoised, output, normal), ...groups);
273
+ this.device.queue.submit([finishing.finish()]);
274
+
275
+ return true;
276
+ }
277
+
278
+ destroy() {
279
+ for (const buffer of [...this.buffers, this.params, this.scale]) buffer.destroy();
280
+
281
+ this.buffers = [];
282
+ this.output = null;
283
+ }
284
+ }
@@ -0,0 +1,5 @@
1
+ // The package's lib/ imports its modules without extensions, which only a bundler resolves; its dist/ is one ES module
2
+ // that a browser and Node both load, with the same exports as lib/main.
3
+ declare module "oidn-webgpu/dist/oidn.js" {
4
+ export * from "oidn-webgpu";
5
+ }
@@ -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): string {
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
- const key = `${programs?.key ?? ""}|${subsurface}|${volumes}|${bevel}`;
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
  }
@@ -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({