@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/frameLayout.ts
CHANGED
|
@@ -35,6 +35,10 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
35
35
|
{ name: "localLightCount", type: "u32", doc: "How many leading lights sit in the scene, which the light tree picks." },
|
|
36
36
|
{ name: "localLightPick", type: "f32", doc: "The chance of picking one of them, the last one's cumulative probability." },
|
|
37
37
|
{ name: "lightTree", type: "u32", doc: "Where the light tree's nodes start in the texture buffer; zero picks by power alone." },
|
|
38
|
+
{ name: "portals", type: "u32", doc: "Where the tree of the windows the sky is sampled through starts in the texture buffer." },
|
|
39
|
+
{ name: "portalCount", type: "u32", doc: "How many window triangles there are; zero samples the sky by its brightness alone." },
|
|
40
|
+
{ name: "portalList", type: "u32", doc: "Where the windows of that tree's leaves are listed in the texture buffer." },
|
|
41
|
+
{ name: "lightList", type: "u32", doc: "Where the lights of the tree's leaves are listed in the texture buffer." },
|
|
38
42
|
{ name: "lightLeaves", type: "u32", doc: "Where the leaf of every local light starts in the texture buffer." },
|
|
39
43
|
{ name: "clampIndirect", type: "f32", doc: "Maximum luminance one indirect contribution may add; zero leaves it." },
|
|
40
44
|
{ name: "shadowSpread", type: "f32", doc: "Tangent of half the angular diameter shadow rays spread over." },
|
|
@@ -54,7 +58,7 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
54
58
|
{ name: "skyProgram", type: "u32", doc: "A procedural sky's foreground program, or NO_PROGRAM." },
|
|
55
59
|
{ name: "skyData", type: "u32" },
|
|
56
60
|
{ name: "skyBackground", type: "u32", doc: "Whether a procedural sky paints the background (1) or leaves it black (0)." },
|
|
57
|
-
{ name: "skyIntensity", type: "f32", doc: "The strength a procedural sky lights with,
|
|
61
|
+
{ name: "skyIntensity", type: "f32", doc: "The strength a procedural sky lights with, and shows with as the background." },
|
|
58
62
|
{ name: "pixelCount", type: "u32", doc: "Where the alpha half of the accumulation buffer starts." },
|
|
59
63
|
{ name: "volumetricCount", type: "u32", doc: "Lights that scatter in the air, and where their records start." },
|
|
60
64
|
{ name: "volumetricData", type: "u32" },
|
|
@@ -63,6 +67,7 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
63
67
|
{ name: "lensDistortion", type: "f32", doc: "Radial distortion of the camera ray: positive is barrel, negative pincushion." },
|
|
64
68
|
{ name: "rouletteThreshold", type: "f32", doc: "Throughput under which a path may be ended by chance; zero never." },
|
|
65
69
|
{ name: "rouletteStart", type: "u32", doc: "Bounces every path takes before the roulette may end it." },
|
|
70
|
+
{ name: "subsurfaceDepth", type: "u32", doc: "Bounces at which light still walks inside a material; deeper it reflects." },
|
|
66
71
|
{ name: "subsurfaceEnabled", type: "u32", doc: "Whether materials that scatter light inside do; 0 renders them opaque." },
|
|
67
72
|
{ name: "bevelEnabled", type: "u32", doc: "Whether materials with a bevel radius round their edges." },
|
|
68
73
|
{ name: "volumesEnabled", type: "u32", doc: "Whether volumes scatter light; 0 leaves their meshes out altogether." },
|
package/src/imageResolve.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface ResolveSettings {
|
|
|
62
62
|
exposure: number;
|
|
63
63
|
toneMapping: RenderToneMapping;
|
|
64
64
|
vignette: number;
|
|
65
|
+
contrast: number;
|
|
65
66
|
transparent: boolean;
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -92,6 +93,7 @@ export class ImageResolvePass {
|
|
|
92
93
|
floats[4] = settings.vignette;
|
|
93
94
|
integers[7] = settings.transparent ? 1 : 0;
|
|
94
95
|
integers[8] = denoised ? 1 : 0;
|
|
96
|
+
floats[9] = settings.contrast;
|
|
95
97
|
this.device.queue.writeBuffer(this.uniforms, 0, this.values);
|
|
96
98
|
|
|
97
99
|
const bytes = width * height * 4;
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,8 @@ export type {
|
|
|
19
19
|
export { attachCanvas } from "./canvas.js";
|
|
20
20
|
|
|
21
21
|
export { DEFAULT_DENOISER } from "./denoise.js";
|
|
22
|
-
export
|
|
22
|
+
export { DEFAULT_OIDN_WEIGHTS } from "./oidnPass.js";
|
|
23
|
+
export type { DenoiserAlgorithm, DenoiserSettings } from "./denoise.js";
|
|
23
24
|
|
|
24
25
|
// The settings only this renderer reads, beside the CPU renderer's, and the defaults it renders with.
|
|
25
26
|
export {
|
package/src/lensPass.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface LensEffects {
|
|
|
27
27
|
glareAngle: number;
|
|
28
28
|
/** A linear sRGB matrix, row by row, or `null` for a neutral white balance. */
|
|
29
29
|
whiteBalance: number[] | null;
|
|
30
|
+
/** Whether the image is metered and exposed to middle grey before the display transform. */
|
|
31
|
+
autoExposure: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** The lens effects these settings ask for, or `null` when they ask for none and the pass can be skipped. */
|
|
@@ -38,7 +40,9 @@ export function lensEffects(settings: GpuRenderSettings): LensEffects | null {
|
|
|
38
40
|
const neutral = !Number.isFinite(camera.whiteBalance) || camera.whiteBalance === NEUTRAL_WHITE_BALANCE;
|
|
39
41
|
const blades = Math.min(16, Math.max(3, Math.round(Number(glare.blades) || 6)));
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
const autoExposure = camera.autoExposure === true;
|
|
44
|
+
|
|
45
|
+
if (intensity <= 0 && streaks <= 0 && aberration <= 0 && neutral && !autoExposure) return null;
|
|
42
46
|
|
|
43
47
|
return {
|
|
44
48
|
bloom: intensity,
|
|
@@ -50,6 +54,7 @@ export function lensEffects(settings: GpuRenderSettings): LensEffects | null {
|
|
|
50
54
|
glareLength: Math.min(1, Math.max(0, Number(glare.length) || 0)),
|
|
51
55
|
glareAngle: (Number(glare.rotation) || 0) * Math.PI / 180,
|
|
52
56
|
whiteBalance: neutral ? null : whiteBalanceMatrix(camera.whiteBalance),
|
|
57
|
+
autoExposure,
|
|
53
58
|
};
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -93,6 +98,8 @@ export class LensPass {
|
|
|
93
98
|
private readonly upsample: GPUComputePipeline;
|
|
94
99
|
private readonly composite: GPUComputePipeline;
|
|
95
100
|
private readonly streaks: GPUComputePipeline;
|
|
101
|
+
private readonly meter: GPUComputePipeline;
|
|
102
|
+
private readonly metered: GPUBuffer;
|
|
96
103
|
/** One uniform buffer per dispatch, so every one of them keeps its own values until the queue runs it. */
|
|
97
104
|
private readonly uniforms: GPUBuffer[] = [];
|
|
98
105
|
private readonly values = new ArrayBuffer(UNIFORM_BYTES);
|
|
@@ -116,6 +123,7 @@ export class LensPass {
|
|
|
116
123
|
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
117
124
|
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
118
125
|
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
126
|
+
{ binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
119
127
|
],
|
|
120
128
|
});
|
|
121
129
|
|
|
@@ -126,6 +134,8 @@ export class LensPass {
|
|
|
126
134
|
this.upsample = pipeline("upsample");
|
|
127
135
|
this.composite = pipeline("composite");
|
|
128
136
|
this.streaks = pipeline("streaks");
|
|
137
|
+
this.meter = pipeline("meter");
|
|
138
|
+
this.metered = device.createBuffer({ label: "lens meter", size: 16, usage: GPUBufferUsage.STORAGE });
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
/** The graded image of the last `encode`, one vec4 of linear colour per pixel. */
|
|
@@ -201,6 +211,7 @@ export class LensPass {
|
|
|
201
211
|
integers[32] = streak.width;
|
|
202
212
|
integers[33] = streak.height;
|
|
203
213
|
floats[34] = effects.glareAngle;
|
|
214
|
+
floats[35] = effects.autoExposure ? 1 : 0;
|
|
204
215
|
for (let row = 0; row < 3; row++) for (let column = 0; column < 3; column++)
|
|
205
216
|
floats[20 + row * 4 + column] = white[row * 3 + column];
|
|
206
217
|
fill();
|
|
@@ -222,6 +233,7 @@ export class LensPass {
|
|
|
222
233
|
{ binding: 4, resource: { buffer: this.up as GPUBuffer } },
|
|
223
234
|
{ binding: 5, resource: { buffer: this.graded as GPUBuffer } },
|
|
224
235
|
{ binding: 6, resource: { buffer: this.streaked as GPUBuffer } },
|
|
236
|
+
{ binding: 7, resource: { buffer: this.metered } },
|
|
225
237
|
],
|
|
226
238
|
}));
|
|
227
239
|
pass.dispatchWorkgroups(Math.ceil(target.width / WORKGROUP), Math.ceil(target.height / WORKGROUP));
|
|
@@ -278,6 +290,9 @@ export class LensPass {
|
|
|
278
290
|
}
|
|
279
291
|
}
|
|
280
292
|
|
|
293
|
+
// The meter is one workgroup, whatever the size: dispatched as a single tile of the grid it reads.
|
|
294
|
+
if (effects.autoExposure) dispatch(this.meter, { width: 1, height: 1 }, () => {});
|
|
295
|
+
|
|
281
296
|
dispatch(this.composite, { width, height }, () => {});
|
|
282
297
|
|
|
283
298
|
return this.graded as GPUBuffer;
|
|
@@ -293,6 +308,7 @@ export class LensPass {
|
|
|
293
308
|
|
|
294
309
|
destroy() {
|
|
295
310
|
this.destroyBuffers();
|
|
311
|
+
this.metered.destroy();
|
|
296
312
|
for (const buffer of this.uniforms) buffer.destroy();
|
|
297
313
|
this.uniforms.length = 0;
|
|
298
314
|
}
|
package/src/lightTree.ts
CHANGED
|
@@ -24,9 +24,23 @@ export interface TreeLight {
|
|
|
24
24
|
/** Sixteen words a node: low and power, high and cos theta_o, axis and cos theta_e, then the links. */
|
|
25
25
|
export const TREE_NODE_WORDS = 16;
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The high bit of a node's first link marks a leaf, whose low bits say where its lights start in the list after the
|
|
29
|
+
* nodes; its second link says how many there are.
|
|
30
|
+
*/
|
|
28
31
|
export const TREE_LEAF = 0x80000000;
|
|
29
32
|
|
|
33
|
+
/** Four words a light in a leaf's list: the light, its chance within the leaf, its area density, the running chance. */
|
|
34
|
+
export const TREE_ENTRY_WORDS = 4;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A leaf holds up to this many lights, when they are so close together, against how far the lights spread, that from
|
|
38
|
+
* any point the tree could not tell them apart: the triangles of one small lamp. Lights further apart each keep a leaf,
|
|
39
|
+
* which keeps every choice between them by distance and facing.
|
|
40
|
+
*/
|
|
41
|
+
const LEAF_LIGHTS = 8;
|
|
42
|
+
const LEAF_EXTENT = 0.02;
|
|
43
|
+
|
|
30
44
|
export const TREE_ROOT_PARENT = 0xffffffff;
|
|
31
45
|
|
|
32
46
|
interface Cone {
|
|
@@ -77,18 +91,22 @@ interface Node {
|
|
|
77
91
|
left: number;
|
|
78
92
|
right: number;
|
|
79
93
|
parent: number;
|
|
80
|
-
/** The
|
|
81
|
-
|
|
82
|
-
density: number;
|
|
94
|
+
/** The lights of a leaf, or none. */
|
|
95
|
+
lights: number[];
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
/**
|
|
86
|
-
* The tree over these lights, as words for the texture buffer,
|
|
87
|
-
*
|
|
99
|
+
* The tree over these lights, as words for the texture buffer, the leaf and list slot each light ended in, and where
|
|
100
|
+
* the list of the leaves' lights starts. Split at the middle of the longest axis of the centres, which needs no cost
|
|
101
|
+
* model and keeps a tight group, such as one lamp's triangles, on one side to share a leaf.
|
|
88
102
|
*/
|
|
89
|
-
export function buildLightTree(lights: readonly TreeLight[]): {
|
|
103
|
+
export function buildLightTree(lights: readonly TreeLight[]): {
|
|
104
|
+
words: Uint32Array; leafOf: Uint32Array; listAt: number;
|
|
105
|
+
} {
|
|
90
106
|
const nodes: Node[] = [];
|
|
91
|
-
|
|
107
|
+
// Two words a light: its leaf, and where it sits in the list of lights after the nodes.
|
|
108
|
+
const leafOf = new Uint32Array(lights.length * 2);
|
|
109
|
+
let span = 0;
|
|
92
110
|
|
|
93
111
|
const build = (indices: number[], parent: number): number => {
|
|
94
112
|
const index = nodes.length;
|
|
@@ -111,29 +129,35 @@ export function buildLightTree(lights: readonly TreeLight[]): { words: Uint32Arr
|
|
|
111
129
|
cone = cone ? unionCone(cone, own) : own;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
const node: Node = { low, high, power, cone: cone as Cone, left: -1, right: -1, parent,
|
|
132
|
+
const node: Node = { low, high, power, cone: cone as Cone, left: -1, right: -1, parent, lights: [] };
|
|
115
133
|
|
|
116
134
|
nodes.push(node);
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
const extent = Math.hypot(high[0] - low[0], high[1] - low[1], high[2] - low[2]);
|
|
137
|
+
|
|
138
|
+
if (index === 0) span = extent;
|
|
139
|
+
|
|
140
|
+
if (indices.length === 1 || (indices.length <= LEAF_LIGHTS && extent <= span * LEAF_EXTENT)) {
|
|
141
|
+
node.lights = indices;
|
|
122
142
|
|
|
123
143
|
return index;
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
const centre = (i: number, k: number) => (lights[i].low[k] + lights[i].high[k]) / 2;
|
|
127
147
|
|
|
128
|
-
const
|
|
148
|
+
const spread = [0, 1, 2].map(k => {
|
|
129
149
|
const values = indices.map(i => centre(i, k));
|
|
130
150
|
|
|
131
151
|
return Math.max(...values) - Math.min(...values);
|
|
132
152
|
});
|
|
133
153
|
|
|
134
|
-
const axis =
|
|
154
|
+
const axis = spread.indexOf(Math.max(...spread));
|
|
135
155
|
const sorted = [...indices].sort((a, b) => centre(a, axis) - centre(b, axis));
|
|
136
|
-
|
|
156
|
+
// Split at the middle of the extent, which keeps a tight group of lights together on one side; the median only
|
|
157
|
+
// when that leaves a side empty, as lights all at one place do.
|
|
158
|
+
const middle = (centre(sorted[0], axis) + centre(sorted[sorted.length - 1], axis)) / 2;
|
|
159
|
+
const below = sorted.findIndex(i => centre(i, axis) > middle);
|
|
160
|
+
const half = below > 0 ? below : sorted.length >> 1;
|
|
137
161
|
|
|
138
162
|
node.left = build(sorted.slice(0, half), index);
|
|
139
163
|
node.right = build(sorted.slice(half), index);
|
|
@@ -143,8 +167,11 @@ export function buildLightTree(lights: readonly TreeLight[]): { words: Uint32Arr
|
|
|
143
167
|
|
|
144
168
|
if (lights.length) build(lights.map((_, i) => i), TREE_ROOT_PARENT);
|
|
145
169
|
|
|
146
|
-
const
|
|
170
|
+
const listAt = nodes.length * TREE_NODE_WORDS;
|
|
171
|
+
const entries = nodes.reduce((sum, node) => sum + node.lights.length, 0);
|
|
172
|
+
const words = new Uint32Array(listAt + entries * TREE_ENTRY_WORDS);
|
|
147
173
|
const floats = new Float32Array(words.buffer);
|
|
174
|
+
let entry = 0;
|
|
148
175
|
|
|
149
176
|
nodes.forEach((node, k) => {
|
|
150
177
|
const at = k * TREE_NODE_WORDS;
|
|
@@ -157,9 +184,28 @@ export function buildLightTree(lights: readonly TreeLight[]): { words: Uint32Arr
|
|
|
157
184
|
floats.set(node.cone.axis, at + 8);
|
|
158
185
|
floats[at + 11] = Math.cos(Math.min(node.cone.thetaE, Math.PI));
|
|
159
186
|
|
|
160
|
-
if (node.
|
|
161
|
-
words[at + 12] = (TREE_LEAF |
|
|
162
|
-
|
|
187
|
+
if (node.lights.length) {
|
|
188
|
+
words[at + 12] = (TREE_LEAF | entry) >>> 0;
|
|
189
|
+
words[at + 13] = node.lights.length;
|
|
190
|
+
|
|
191
|
+
// Within a leaf, by power: the lights are too close together for anything else to tell them apart.
|
|
192
|
+
const total = node.lights.reduce((sum, i) => sum + lights[i].power, 0);
|
|
193
|
+
let running = 0;
|
|
194
|
+
|
|
195
|
+
node.lights.forEach((light, j) => {
|
|
196
|
+
const chance = total > 0 ? lights[light].power / total : 1 / node.lights.length;
|
|
197
|
+
const slot = listAt + entry * TREE_ENTRY_WORDS;
|
|
198
|
+
|
|
199
|
+
running += chance;
|
|
200
|
+
words[slot] = light;
|
|
201
|
+
floats[slot + 1] = chance;
|
|
202
|
+
floats[slot + 2] = lights[light].density;
|
|
203
|
+
// The last closes the range exactly, so a unit sample never falls past it.
|
|
204
|
+
floats[slot + 3] = j === node.lights.length - 1 ? 1 : running;
|
|
205
|
+
leafOf[light * 2] = k;
|
|
206
|
+
leafOf[light * 2 + 1] = entry;
|
|
207
|
+
entry++;
|
|
208
|
+
});
|
|
163
209
|
} else {
|
|
164
210
|
words[at + 12] = node.left;
|
|
165
211
|
words[at + 13] = node.right;
|
|
@@ -168,5 +214,5 @@ export function buildLightTree(lights: readonly TreeLight[]): { words: Uint32Arr
|
|
|
168
214
|
words[at + 14] = node.parent >>> 0;
|
|
169
215
|
});
|
|
170
216
|
|
|
171
|
-
return { words, leafOf };
|
|
217
|
+
return { words, leafOf, listAt };
|
|
172
218
|
}
|
package/src/lights.ts
CHANGED
|
@@ -71,6 +71,51 @@ export interface PackedLights {
|
|
|
71
71
|
/** Where the light tree's nodes start in the texture buffer, and the leaf of every local entry; zero for no tree. */
|
|
72
72
|
treeAt: number;
|
|
73
73
|
leavesAt: number;
|
|
74
|
+
/** Where the lights of the tree's leaves are listed in the texture buffer. */
|
|
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 };
|
|
74
119
|
}
|
|
75
120
|
|
|
76
121
|
const luminance = (color: ArrayLike<number>) => 0.2126 * color[0] + 0.7152 * color[1] + 0.0722 * color[2];
|
|
@@ -425,22 +470,41 @@ export function packLights(
|
|
|
425
470
|
|
|
426
471
|
for (let index = 0; index < areas.length; index++) extra.push(words[index * LIGHT_WORDS + 3]);
|
|
427
472
|
|
|
428
|
-
let treeAt = 0, leavesAt = 0;
|
|
473
|
+
let treeAt = 0, leavesAt = 0, listAt = 0;
|
|
429
474
|
|
|
430
475
|
if (localCount > 0) {
|
|
431
476
|
const tree = buildLightTree(candidates.slice(0, localCount).map(candidate => candidate.tree as TreeLight));
|
|
432
477
|
|
|
433
478
|
treeAt = base + extra.length;
|
|
479
|
+
listAt = treeAt + tree.listAt;
|
|
434
480
|
for (const word of tree.words) extra.push(word);
|
|
435
481
|
leavesAt = base + extra.length;
|
|
436
482
|
for (const leaf of tree.leafOf) extra.push(leaf);
|
|
437
483
|
}
|
|
438
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
|
+
|
|
439
499
|
return {
|
|
500
|
+
portalTreeAt,
|
|
501
|
+
portalListAt,
|
|
502
|
+
portalCount: portals.triangles.length,
|
|
440
503
|
localCount,
|
|
441
504
|
localPick: localCount > 0 ? floats[(localCount - 1) * LIGHT_WORDS + 1] : 0,
|
|
442
505
|
treeAt,
|
|
443
506
|
leavesAt,
|
|
507
|
+
listAt,
|
|
444
508
|
cdfAt,
|
|
445
509
|
trianglesAt,
|
|
446
510
|
entries,
|
package/src/oidnPass.ts
ADDED
|
@@ -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
|
+
}
|