@tinypoly/gpu-renderer 0.0.4 → 0.1.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 +81 -10
- package/README.md +53 -5
- package/dist/atmosphere.d.ts +21 -0
- package/dist/atmosphere.d.ts.map +1 -0
- package/dist/atmosphere.js +72 -0
- package/dist/atmosphere.js.map +1 -0
- package/dist/bvh.d.ts +11 -6
- package/dist/bvh.d.ts.map +1 -1
- package/dist/bvh.js +64 -11
- package/dist/bvh.js.map +1 -1
- package/dist/denoise.d.ts +17 -2
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +95 -27
- package/dist/denoise.js.map +1 -1
- package/dist/dispatchScheduler.d.ts +11 -4
- package/dist/dispatchScheduler.d.ts.map +1 -1
- package/dist/dispatchScheduler.js +14 -7
- package/dist/dispatchScheduler.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +18 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +6 -3
- package/dist/geometry.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +60 -0
- package/dist/lensPass.d.ts.map +1 -0
- package/dist/lensPass.js +235 -0
- package/dist/lensPass.js.map +1 -0
- package/dist/lightTree.d.ts +43 -0
- package/dist/lightTree.d.ts.map +1 -0
- package/dist/lightTree.js +107 -0
- package/dist/lightTree.js.map +1 -0
- package/dist/lights.d.ts +19 -1
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +63 -1
- package/dist/lights.js.map +1 -1
- package/dist/materialLayout.d.ts +24 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +36 -1
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +6 -3
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +16 -10
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +19 -7
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +198 -36
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +19 -1
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +25 -3
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +136 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +100 -0
- package/dist/settings.js.map +1 -0
- package/dist/subsurface.d.ts +76 -0
- package/dist/subsurface.d.ts.map +1 -0
- package/dist/subsurface.js +70 -0
- package/dist/subsurface.js.map +1 -0
- package/dist/texturePack.d.ts +16 -4
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +61 -8
- package/dist/texturePack.js.map +1 -1
- package/dist/volumeContainment.d.ts +17 -0
- package/dist/volumeContainment.d.ts.map +1 -0
- package/dist/volumeContainment.js +61 -0
- package/dist/volumeContainment.js.map +1 -0
- package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bsdf.wgsl.js +35 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +8 -2
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +167 -96
- package/dist/wgsl/bvh.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +24 -7
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +17 -0
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
- package/dist/wgsl/lens.wgsl.js +315 -0
- package/dist/wgsl/lens.wgsl.js.map +1 -0
- 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 +206 -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 +410 -25
- package/dist/wgsl/pathTrace.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 +9 -17
- package/dist/wgsl/random.wgsl.js.map +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
- package/dist/wgsl/sampling.wgsl.js +25 -0
- package/dist/wgsl/sampling.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +47 -4
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/dist/wgsl/texture.wgsl.d.ts +1 -1
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
- package/dist/wgsl/texture.wgsl.js +10 -11
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/atmosphere.ts +95 -0
- package/src/bvh.ts +81 -12
- package/src/denoise.ts +110 -27
- package/src/dispatchScheduler.ts +16 -7
- package/src/frameLayout.ts +18 -0
- package/src/geometry.ts +6 -2
- package/src/index.ts +13 -0
- package/src/lensPass.ts +299 -0
- package/src/lightTree.ts +172 -0
- package/src/lights.ts +90 -2
- package/src/materialLayout.ts +52 -1
- package/src/pathTracePass.ts +19 -10
- package/src/renderer.ts +258 -43
- package/src/sceneUpload.ts +41 -4
- package/src/settings.ts +234 -0
- package/src/subsurface.ts +153 -0
- package/src/texturePack.ts +80 -13
- package/src/volumeContainment.ts +81 -0
- package/src/wgsl/bsdf.wgsl.ts +35 -3
- package/src/wgsl/bvh.wgsl.ts +167 -96
- package/src/wgsl/common.wgsl.ts +24 -7
- package/src/wgsl/lens.wgsl.ts +314 -0
- package/src/wgsl/lights.wgsl.ts +206 -33
- package/src/wgsl/pathTrace.wgsl.ts +410 -25
- package/src/wgsl/random.wgsl.ts +9 -17
- package/src/wgsl/sampling.wgsl.ts +25 -0
- package/src/wgsl/surface.wgsl.ts +47 -4
- package/src/wgsl/texture.wgsl.ts +10 -11
package/src/bvh.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Binned SAH bounding volume hierarchy
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Binned SAH bounding volume hierarchy. It is built as a binary tree of boxes, eight words a node, and then laid out
|
|
3
|
+
* the way the shader reads it: each node holds the boxes of its two children, so one read tells the traversal both
|
|
4
|
+
* what it may enter and where to go, and a leaf is tested without being read at all. That is sixteen words a node,
|
|
5
|
+
* matching `struct BvhNode` in `wgsl/common.wgsl.ts`: per child a minimum corner and a link, then a maximum corner and
|
|
6
|
+
* a triangle count. A link of zero or more is the node to go to; a negative one is a leaf, `-1 - start` into `order`.
|
|
5
7
|
*
|
|
6
8
|
* The bounds are computed from the same `Float32Array` the shader traverses, so every corner is already a float and
|
|
7
9
|
* needs no widening. Building from double precision positions, as the CPU renderer does, would need each minimum
|
|
8
10
|
* rounded down and each maximum rounded up before they reach the GPU.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
/** Words per node in the packed buffer. */
|
|
12
|
-
export const NODE_WORDS =
|
|
13
|
+
/** Words per node in the packed buffer: both children's corners, links and counts. */
|
|
14
|
+
export const NODE_WORDS = 16;
|
|
15
|
+
|
|
16
|
+
/** Words per node of the binary tree the builder works on: its own corners, and its children or its leaf. */
|
|
17
|
+
const BINARY_WORDS = 8;
|
|
13
18
|
|
|
14
19
|
/** Triangles a leaf may hold. */
|
|
15
20
|
const MAX_LEAF_SIZE = 4;
|
|
@@ -27,7 +32,10 @@ const TRAVERSAL_COST = 0.125;
|
|
|
27
32
|
export const MAX_BVH_DEPTH = 32;
|
|
28
33
|
|
|
29
34
|
export interface PackedBvh {
|
|
30
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* `nodeCount * NODE_WORDS` words, read as floats for the corners and as signed integers for the links. Node zero
|
|
37
|
+
* holds the root's two children, or the root itself beside an empty child when the whole scene is one leaf.
|
|
38
|
+
*/
|
|
31
39
|
nodes: ArrayBuffer;
|
|
32
40
|
nodeCount: number;
|
|
33
41
|
/** Triangle indices in leaf order: a leaf covers `order[start] .. order[start + count - 1]`. */
|
|
@@ -66,15 +74,15 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
|
|
|
66
74
|
|
|
67
75
|
const { min, max, centroid } = triangleBounds(positions, triangleCount);
|
|
68
76
|
// The heuristic may split down to one triangle per leaf, so the pool starts at the common case and grows.
|
|
69
|
-
let nodes = new ArrayBuffer(Math.max(4, Math.ceil(triangleCount / 2)) *
|
|
77
|
+
let nodes = new ArrayBuffer(Math.max(4, Math.ceil(triangleCount / 2)) * BINARY_WORDS * 4);
|
|
70
78
|
let floats = new Float32Array(nodes);
|
|
71
79
|
let ints = new Int32Array(nodes);
|
|
72
80
|
|
|
73
81
|
const reserve = (needed: number) => {
|
|
74
|
-
if (needed *
|
|
82
|
+
if (needed * BINARY_WORDS * 4 <= nodes.byteLength) return;
|
|
75
83
|
|
|
76
|
-
const grown = new ArrayBuffer(Math.max(needed, Math.ceil(nodes.byteLength / (
|
|
77
|
-
*
|
|
84
|
+
const grown = new ArrayBuffer(Math.max(needed, Math.ceil(nodes.byteLength / (BINARY_WORDS * 4) * 1.6))
|
|
85
|
+
* BINARY_WORDS * 4);
|
|
78
86
|
|
|
79
87
|
new Float32Array(grown).set(floats);
|
|
80
88
|
nodes = grown;
|
|
@@ -132,7 +140,7 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
|
|
|
132
140
|
if (value > cMaxZ) cMaxZ = value;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
const word = node *
|
|
143
|
+
const word = node * BINARY_WORDS;
|
|
136
144
|
|
|
137
145
|
floats[word] = minX;
|
|
138
146
|
floats[word + 1] = minY;
|
|
@@ -180,7 +188,68 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
|
|
|
180
188
|
top += 2;
|
|
181
189
|
}
|
|
182
190
|
|
|
183
|
-
return {
|
|
191
|
+
return { ...widen(floats, ints, nodeCount), order, maxDepth };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The binary tree laid out for the shader: one node per interior node of it, holding both children's boxes. Interior
|
|
196
|
+
* nodes are numbered depth first, left before right, so a node and the child it leads to tend to share a cache line.
|
|
197
|
+
*/
|
|
198
|
+
function widen(floats: Float32Array, ints: Int32Array, binaryCount: number): { nodes: ArrayBuffer; nodeCount: number } {
|
|
199
|
+
const leaf = (node: number) => ints[node * BINARY_WORDS + 3] < 0;
|
|
200
|
+
const rootLeaf = leaf(0);
|
|
201
|
+
// A full binary tree has one fewer interior node than leaves; a single leaf still needs a node to stand in.
|
|
202
|
+
const nodeCount = rootLeaf ? 1 : (binaryCount - 1) / 2;
|
|
203
|
+
const nodes = new ArrayBuffer(nodeCount * NODE_WORDS * 4);
|
|
204
|
+
const outFloats = new Float32Array(nodes);
|
|
205
|
+
const outInts = new Int32Array(nodes);
|
|
206
|
+
const index = new Int32Array(binaryCount);
|
|
207
|
+
|
|
208
|
+
const writeChild = (word: number, child: number) => {
|
|
209
|
+
const from = child * BINARY_WORDS;
|
|
210
|
+
|
|
211
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
212
|
+
outFloats[word + axis] = floats[from + axis];
|
|
213
|
+
outFloats[word + 4 + axis] = floats[from + 4 + axis];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
outInts[word + 3] = leaf(child) ? ints[from + 3] : index[child];
|
|
217
|
+
outInts[word + 7] = leaf(child) ? ints[from + 7] : 0;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (rootLeaf) {
|
|
221
|
+
writeChild(0, 0);
|
|
222
|
+
|
|
223
|
+
// A box at the far corner of the float range, which no ray reaches, around a leaf of nothing.
|
|
224
|
+
for (let axis = 0; axis < 3; axis++) outFloats[8 + axis] = outFloats[12 + axis] = 3.4028234663852886e38;
|
|
225
|
+
|
|
226
|
+
outInts[11] = -1;
|
|
227
|
+
outInts[15] = 0;
|
|
228
|
+
|
|
229
|
+
return { nodes, nodeCount };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const interior: number[] = [];
|
|
233
|
+
const pending = [0];
|
|
234
|
+
|
|
235
|
+
while (pending.length) {
|
|
236
|
+
const node = pending.pop() as number;
|
|
237
|
+
|
|
238
|
+
index[node] = interior.length;
|
|
239
|
+
interior.push(node);
|
|
240
|
+
|
|
241
|
+
const left = ints[node * BINARY_WORDS + 3], right = ints[node * BINARY_WORDS + 7];
|
|
242
|
+
|
|
243
|
+
if (!leaf(right)) pending.push(right);
|
|
244
|
+
if (!leaf(left)) pending.push(left);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interior.forEach((node, at) => {
|
|
248
|
+
writeChild(at * NODE_WORDS, ints[node * BINARY_WORDS + 3]);
|
|
249
|
+
writeChild(at * NODE_WORDS + 8, ints[node * BINARY_WORDS + 7]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return { nodes, nodeCount };
|
|
184
253
|
}
|
|
185
254
|
|
|
186
255
|
/** Surface area, the cost the split heuristic weighs each side by. Empty bounds cost nothing. */
|
package/src/denoise.ts
CHANGED
|
@@ -15,6 +15,16 @@ export interface DenoiserSettings {
|
|
|
15
15
|
onlyNoisy: boolean;
|
|
16
16
|
/** Clamp a pixel brighter than every one of its neighbours to the brightest of them, before filtering. */
|
|
17
17
|
antiFirefly: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* How far the filter lets go of the normal and depth tests over a material that scatters light inside, from 0 to 1.
|
|
20
|
+
* Light leaves such a surface after wandering under it, so its shading hardly follows the normal at each point.
|
|
21
|
+
*/
|
|
22
|
+
subsurface: number;
|
|
23
|
+
/**
|
|
24
|
+
* The smallest footprint a smooth reflection is filtered over, in pixels; 0 leaves a mirror alone. Within it, taps
|
|
25
|
+
* also have to agree on how far away the reflected thing is, so the edge of a reflected object stays sharp.
|
|
26
|
+
*/
|
|
27
|
+
mirrorRadius: number;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
export const DEFAULT_DENOISER: Readonly<DenoiserSettings> = Object.freeze({
|
|
@@ -23,6 +33,8 @@ export const DEFAULT_DENOISER: Readonly<DenoiserSettings> = Object.freeze({
|
|
|
23
33
|
edge: 2,
|
|
24
34
|
onlyNoisy: true,
|
|
25
35
|
antiFirefly: true,
|
|
36
|
+
subsurface: 1,
|
|
37
|
+
mirrorRadius: 3,
|
|
26
38
|
});
|
|
27
39
|
|
|
28
40
|
/** The settings with `input` applied, each field kept in range. */
|
|
@@ -35,6 +47,8 @@ export function mergeDenoiser(base: DenoiserSettings, input: Partial<DenoiserSet
|
|
|
35
47
|
edge: Math.max(0.01, Number(merged.edge) || DEFAULT_DENOISER.edge),
|
|
36
48
|
onlyNoisy: Boolean(merged.onlyNoisy),
|
|
37
49
|
antiFirefly: Boolean(merged.antiFirefly),
|
|
50
|
+
subsurface: Math.min(1, Math.max(0, Number(merged.subsurface) || 0)),
|
|
51
|
+
mirrorRadius: Math.min(16, Math.max(0, Number(merged.mirrorRadius) || 0)),
|
|
38
52
|
};
|
|
39
53
|
}
|
|
40
54
|
|
|
@@ -77,7 +91,8 @@ struct Params {
|
|
|
77
91
|
converged: f32,
|
|
78
92
|
pixelScale: f32,
|
|
79
93
|
orthographic: u32,
|
|
80
|
-
|
|
94
|
+
subsurface: f32,
|
|
95
|
+
mirrorRadius: f32,
|
|
81
96
|
};
|
|
82
97
|
|
|
83
98
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
@@ -86,6 +101,10 @@ struct Params {
|
|
|
86
101
|
@group(0) @binding(3) var<storage, read> specularIn: array<vec4<f32>>;
|
|
87
102
|
@group(0) @binding(4) var<storage, read_write> diffuseOut: array<vec4<f32>>;
|
|
88
103
|
@group(0) @binding(5) var<storage, read_write> specularOut: array<vec4<f32>>;
|
|
104
|
+
// The guides each tap compares, worked out once per pixel by prepare: the normal and the depth, then the roughness, or
|
|
105
|
+
// -1 where no sample has landed. The same numbers the accumulation gives, two reads a tap instead of four.
|
|
106
|
+
@group(0) @binding(6) var<storage, read_write> guides: array<vec4<f32>>;
|
|
107
|
+
@group(0) @binding(7) var<storage, read_write> roughnesses: array<f32>;
|
|
89
108
|
|
|
90
109
|
fn luminance(color: vec3<f32>) -> f32 {
|
|
91
110
|
return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
@@ -128,6 +147,11 @@ fn guideRoughness(index: u32) -> f32 {
|
|
|
128
147
|
return accumulation[2u * params.count + index].w / samples(index);
|
|
129
148
|
}
|
|
130
149
|
|
|
150
|
+
/** How much of the first surface scatters light inside, averaged over the pixel's samples. */
|
|
151
|
+
fn guideSubsurface(index: u32) -> f32 {
|
|
152
|
+
return accumulation[5u * params.count + index].w / samples(index);
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
fn specularDistance(index: u32) -> f32 {
|
|
132
156
|
return accumulation[4u * params.count + index].w / samples(index);
|
|
133
157
|
}
|
|
@@ -153,10 +177,15 @@ fn prepare(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
153
177
|
if (count <= 0.0) {
|
|
154
178
|
diffuseOut[index] = vec4<f32>(0.0);
|
|
155
179
|
specularOut[index] = vec4<f32>(0.0);
|
|
180
|
+
guides[index] = vec4<f32>(0.0);
|
|
181
|
+
roughnesses[index] = -1.0;
|
|
156
182
|
|
|
157
183
|
return;
|
|
158
184
|
}
|
|
159
185
|
|
|
186
|
+
guides[index] = vec4<f32>(guideNormal(index), guideDepth(index));
|
|
187
|
+
roughnesses[index] = guideRoughness(index);
|
|
188
|
+
|
|
160
189
|
let squares = accumulation[params.count + index];
|
|
161
190
|
let diffuse = diffuseMean(index);
|
|
162
191
|
let specular = specularMean(index);
|
|
@@ -269,14 +298,26 @@ fn atrous(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
269
298
|
return;
|
|
270
299
|
}
|
|
271
300
|
|
|
272
|
-
let
|
|
273
|
-
let
|
|
274
|
-
let
|
|
275
|
-
let
|
|
301
|
+
let guide = guides[index];
|
|
302
|
+
let normal = guide.xyz;
|
|
303
|
+
let depth = guide.w;
|
|
304
|
+
let roughness = roughnesses[index];
|
|
305
|
+
let diffuseSigma = params.edge * sqrt(max(localVariance(pixel, false), 0.0))
|
|
306
|
+
* (1.0 + guideSubsurface(index) * params.subsurface) + 1e-6;
|
|
276
307
|
let specularSigma = params.edge * sqrt(max(localVariance(pixel, true), 0.0)) + 1e-6;
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
308
|
+
// A smooth reflection is filtered over at least the mirror radius, by taps that see the reflected thing at the same
|
|
309
|
+
// distance: that test keeps the edges of what it reflects, which lets the normal test relax over a curved mirror,
|
|
310
|
+
// where neighbouring normals part fast and would otherwise leave the reflection unfiltered.
|
|
311
|
+
let mirror = params.mirrorRadius > 0.0 && roughness < 0.3;
|
|
312
|
+
let radius = select(specularRadius(index, depth), max(specularRadius(index, depth), params.mirrorRadius), mirror);
|
|
313
|
+
let reflected = specularDistance(index);
|
|
314
|
+
// A smoother surface's reflection turns faster with its normal: the normal test sharpens with it, short of shutting
|
|
315
|
+
// out the neighbours of a curved mirror altogether once the reflected distance guards it.
|
|
316
|
+
let specularPower = min(128.0 / max(roughness * roughness, 0.03), select(4096.0, 64.0, mirror));
|
|
317
|
+
// Under a surface that scatters light inside, the normal and the depth say little about the light that leaves it.
|
|
318
|
+
let scattering = guideSubsurface(index) * params.subsurface;
|
|
319
|
+
let diffusePower = mix(128.0, 1.0, scattering);
|
|
320
|
+
let depthSlack = mix(1.0, 4.0, scattering);
|
|
280
321
|
let step = i32(params.step);
|
|
281
322
|
var diffuse = vec4<f32>(0.0);
|
|
282
323
|
var specular = vec4<f32>(0.0);
|
|
@@ -299,21 +340,30 @@ fn atrous(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
299
340
|
var ws = base;
|
|
300
341
|
|
|
301
342
|
if (dx != 0 || dy != 0) {
|
|
302
|
-
|
|
343
|
+
let otherRoughness = roughnesses[other];
|
|
344
|
+
|
|
345
|
+
// No sample has landed there.
|
|
346
|
+
if (otherRoughness < 0.0) {
|
|
303
347
|
continue;
|
|
304
348
|
}
|
|
305
349
|
|
|
350
|
+
let otherGuide = guides[other];
|
|
306
351
|
let offset = length(vec2<f32>(f32(dx * step), f32(dy * step)));
|
|
307
|
-
let alignment = max(dot(normal,
|
|
308
|
-
let depthWeight = exp(-abs(depth -
|
|
352
|
+
let alignment = max(dot(normal, otherGuide.xyz), 0.0);
|
|
353
|
+
let depthWeight = exp(-abs(depth - otherGuide.w) / (0.02 * depth * offset + 1e-3));
|
|
354
|
+
let diffuseDepth = exp(-abs(depth - otherGuide.w) / (0.02 * depthSlack * depth * offset + 1e-3));
|
|
309
355
|
|
|
310
|
-
wd = wd * pow(alignment,
|
|
356
|
+
wd = wd * pow(alignment, diffusePower) * diffuseDepth
|
|
311
357
|
* exp(-abs(luminance(d.rgb) - luminance(diffuseCenter.rgb)) / diffuseSigma);
|
|
312
358
|
|
|
313
359
|
ws = ws * pow(alignment, specularPower) * depthWeight
|
|
314
|
-
* exp(-abs(
|
|
360
|
+
* exp(-abs(otherRoughness - roughness) * 10.0)
|
|
315
361
|
* exp(-abs(luminance(s.rgb) - luminance(specularCenter.rgb)) / specularSigma)
|
|
316
362
|
* select(0.0, exp(-offset * offset / (radius * radius)), radius >= 0.5);
|
|
363
|
+
|
|
364
|
+
if (mirror) {
|
|
365
|
+
ws = ws * exp(-abs(specularDistance(other) - reflected) / (0.05 * reflected + 1e-3));
|
|
366
|
+
}
|
|
317
367
|
}
|
|
318
368
|
|
|
319
369
|
diffuse = diffuse + vec4<f32>(d.rgb * wd, d.w * wd * wd);
|
|
@@ -381,8 +431,21 @@ export class DenoisePass {
|
|
|
381
431
|
private readonly finish: GPUComputePipeline;
|
|
382
432
|
/** One uniform per stage, so a whole denoise is a single submit. */
|
|
383
433
|
private readonly params: GPUBuffer[];
|
|
384
|
-
/** Two ping-pong pairs, diffuse and specular, and the
|
|
434
|
+
/** Two ping-pong pairs, diffuse and specular, the output, then the guides and the roughness. */
|
|
385
435
|
private buffers: GPUBuffer[] = [];
|
|
436
|
+
/** Bind groups by the buffers they bind, and a number for each buffer seen, which is what the key is made of. */
|
|
437
|
+
private readonly groups = new Map<string, GPUBindGroup>();
|
|
438
|
+
private readonly ids = new WeakMap<GPUBuffer, number>();
|
|
439
|
+
private nextId = 0;
|
|
440
|
+
|
|
441
|
+
private idOf(buffer: GPUBuffer): number {
|
|
442
|
+
let id = this.ids.get(buffer);
|
|
443
|
+
|
|
444
|
+
if (id === undefined) this.ids.set(buffer, id = this.nextId++);
|
|
445
|
+
|
|
446
|
+
return id;
|
|
447
|
+
}
|
|
448
|
+
|
|
386
449
|
private width = 0;
|
|
387
450
|
private height = 0;
|
|
388
451
|
|
|
@@ -402,6 +465,8 @@ export class DenoisePass {
|
|
|
402
465
|
{ binding: 3, visibility, buffer: storage("read-only-storage") },
|
|
403
466
|
{ binding: 4, visibility, buffer: storage("storage") },
|
|
404
467
|
{ binding: 5, visibility, buffer: storage("storage") },
|
|
468
|
+
{ binding: 6, visibility, buffer: storage("storage") },
|
|
469
|
+
{ binding: 7, visibility, buffer: storage("storage") },
|
|
405
470
|
],
|
|
406
471
|
});
|
|
407
472
|
|
|
@@ -423,9 +488,12 @@ export class DenoisePass {
|
|
|
423
488
|
|
|
424
489
|
const size = Math.max(16, width * height * 16);
|
|
425
490
|
|
|
426
|
-
this.buffers = Array.from({ length:
|
|
491
|
+
this.buffers = Array.from({ length: 6 }, () =>
|
|
427
492
|
this.device.createBuffer({ size, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC }));
|
|
493
|
+
this.buffers.push(
|
|
494
|
+
this.device.createBuffer({ size: Math.max(16, width * height * 4), usage: GPUBufferUsage.STORAGE }));
|
|
428
495
|
this.output = this.buffers[4];
|
|
496
|
+
this.groups.clear();
|
|
429
497
|
this.width = width;
|
|
430
498
|
this.height = height;
|
|
431
499
|
}
|
|
@@ -445,6 +513,8 @@ export class DenoisePass {
|
|
|
445
513
|
floats[7] = CONVERGED_ERROR;
|
|
446
514
|
floats[8] = view.pixelScale;
|
|
447
515
|
words[9] = view.orthographic ? 1 : 0;
|
|
516
|
+
floats[10] = settings.subsurface;
|
|
517
|
+
floats[11] = settings.mirrorRadius;
|
|
448
518
|
this.device.queue.writeBuffer(this.params[k], 0, values);
|
|
449
519
|
});
|
|
450
520
|
}
|
|
@@ -460,22 +530,34 @@ export class DenoisePass {
|
|
|
460
530
|
this.resize(width, height);
|
|
461
531
|
this.writeParams(width, height, settings, view);
|
|
462
532
|
|
|
463
|
-
const [diffuseA, diffuseB, specularA, specularB, output] = this.buffers;
|
|
533
|
+
const [diffuseA, diffuseB, specularA, specularB, output, guides, roughnesses] = this.buffers;
|
|
464
534
|
const groups = [Math.ceil(width / 8), Math.ceil(height / 8)] as const;
|
|
465
535
|
|
|
466
536
|
const run = (pipeline: GPUComputePipeline, params: GPUBuffer,
|
|
467
537
|
sources: [GPUBuffer, GPUBuffer], targets: [GPUBuffer, GPUBuffer]) => {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
538
|
+
// The same few groups serve every denoise of an image size: they are made once and kept.
|
|
539
|
+
const key = [params, accumulation, ...sources, ...targets].map(buffer => this.idOf(buffer)).join();
|
|
540
|
+
let group = this.groups.get(key);
|
|
541
|
+
|
|
542
|
+
if (!group) {
|
|
543
|
+
group = this.device.createBindGroup({
|
|
544
|
+
layout: this.layout,
|
|
545
|
+
entries: [
|
|
546
|
+
{ binding: 0, resource: { buffer: params } },
|
|
547
|
+
{ binding: 1, resource: { buffer: accumulation } },
|
|
548
|
+
{ binding: 2, resource: { buffer: sources[0] } },
|
|
549
|
+
{ binding: 3, resource: { buffer: sources[1] } },
|
|
550
|
+
{ binding: 4, resource: { buffer: targets[0] } },
|
|
551
|
+
{ binding: 5, resource: { buffer: targets[1] } },
|
|
552
|
+
{ binding: 6, resource: { buffer: guides } },
|
|
553
|
+
{ binding: 7, resource: { buffer: roughnesses } },
|
|
554
|
+
],
|
|
555
|
+
});
|
|
556
|
+
// A new accumulation buffer comes with a new size, which clears these; the cap is only a guard.
|
|
557
|
+
if (this.groups.size > 64) this.groups.clear();
|
|
558
|
+
|
|
559
|
+
this.groups.set(key, group);
|
|
560
|
+
}
|
|
479
561
|
|
|
480
562
|
const pass = encoder.beginComputePass({ label: "denoise" });
|
|
481
563
|
|
|
@@ -509,6 +591,7 @@ export class DenoisePass {
|
|
|
509
591
|
for (const buffer of [...this.buffers, ...this.params]) buffer.destroy();
|
|
510
592
|
|
|
511
593
|
this.buffers = [];
|
|
594
|
+
this.groups.clear();
|
|
512
595
|
this.output = null;
|
|
513
596
|
}
|
|
514
597
|
}
|
package/src/dispatchScheduler.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface DispatchSchedulerOptions {
|
|
|
13
13
|
initialRows?: number;
|
|
14
14
|
/** Rows in the whole image. */
|
|
15
15
|
maxRows: number;
|
|
16
|
+
/** The most paths per pixel one submit may take, which is how far past the whole image a band may grow. */
|
|
17
|
+
maxSamples?: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** The most one measurement may widen the band, so an unmeasured machine meets the watchdog gradually. */
|
|
@@ -24,20 +26,26 @@ export class DispatchScheduler {
|
|
|
24
26
|
private readonly targetMs: number;
|
|
25
27
|
private readonly initialRows: number;
|
|
26
28
|
private readonly maxRows: number;
|
|
29
|
+
private readonly maxSamples: number;
|
|
30
|
+
/** The work of the next submit, in rows times paths per pixel. */
|
|
27
31
|
private rows: number;
|
|
28
|
-
/** Rows a millisecond, as far as anything has shown. Zero until the first submit says something. */
|
|
32
|
+
/** Rows a millisecond, one path each, as far as anything has shown. Zero until the first submit says something. */
|
|
29
33
|
private throughput = 0;
|
|
30
34
|
|
|
31
35
|
constructor(options: DispatchSchedulerOptions) {
|
|
32
36
|
this.targetMs = options.targetMs ?? 12;
|
|
33
37
|
this.maxRows = Math.max(1, Math.floor(options.maxRows));
|
|
38
|
+
this.maxSamples = Math.max(1, Math.floor(options.maxSamples ?? 1));
|
|
34
39
|
this.initialRows = Math.max(1, Math.min(this.maxRows, Math.floor(options.initialRows ?? 16)));
|
|
35
40
|
this.rows = this.initialRows;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Rows the next submit should cover, never more than what is left. A submit that takes several paths per pixel
|
|
45
|
+
* covers that many times fewer rows, so it still takes about the target time.
|
|
46
|
+
*/
|
|
47
|
+
next(remaining: number, samples = 1): number {
|
|
48
|
+
return Math.max(1, Math.min(Math.floor(this.rows / Math.max(1, samples)), Math.floor(remaining)));
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
/**
|
|
@@ -52,15 +60,16 @@ export class DispatchScheduler {
|
|
|
52
60
|
* The rate is measured from the rows actually submitted rather than from the current guess, so one band clamped by
|
|
53
61
|
* what was left of the image does not stop the next one from growing.
|
|
54
62
|
*/
|
|
55
|
-
record(durationMs: number, rows: number) {
|
|
63
|
+
record(durationMs: number, rows: number, samples = 1) {
|
|
56
64
|
if (!Number.isFinite(durationMs) || durationMs <= 0 || rows <= 0) return;
|
|
57
65
|
|
|
58
|
-
const
|
|
66
|
+
const work = rows * Math.max(1, samples);
|
|
67
|
+
const observed = work / durationMs;
|
|
59
68
|
|
|
60
69
|
// Good news is believed at once, since it can only mean the band was cheaper than the clock suggested. Bad news
|
|
61
70
|
// is taken gradually, so one submit that happened to land behind something else does not collapse the band.
|
|
62
71
|
this.throughput = observed > this.throughput ? observed : this.throughput * DECAY + observed * (1 - DECAY);
|
|
63
|
-
this.rows = Math.max(1, Math.min(this.maxRows,
|
|
72
|
+
this.rows = Math.max(1, Math.min(this.maxRows * this.maxSamples, work * GROW, this.targetMs * this.throughput));
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
/** Back to the unmeasured band, for a new scene or a new size. */
|
package/src/frameLayout.ts
CHANGED
|
@@ -30,11 +30,18 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
30
30
|
{ name: "triangleCount", type: "u32" },
|
|
31
31
|
{ name: "lightCount", type: "u32" },
|
|
32
32
|
{ name: "areaLightCount", type: "u32", doc: "How many leading lights are emissive triangles." },
|
|
33
|
+
{ name: "lightCdf", type: "u32", doc: "Where every light's cumulative probability starts in the texture buffer." },
|
|
34
|
+
{ name: "lightTriangles", type: "u32", doc: "Where every emissive light's triangle starts in the texture buffer." },
|
|
35
|
+
{ name: "localLightCount", type: "u32", doc: "How many leading lights sit in the scene, which the light tree picks." },
|
|
36
|
+
{ name: "localLightPick", type: "f32", doc: "The chance of picking one of them, the last one's cumulative probability." },
|
|
37
|
+
{ name: "lightTree", type: "u32", doc: "Where the light tree's nodes start in the texture buffer; zero picks by power alone." },
|
|
38
|
+
{ name: "lightLeaves", type: "u32", doc: "Where the leaf of every local light starts in the texture buffer." },
|
|
33
39
|
{ name: "clampIndirect", type: "f32", doc: "Maximum luminance one indirect contribution may add; zero leaves it." },
|
|
34
40
|
{ name: "shadowSpread", type: "f32", doc: "Tangent of half the angular diameter shadow rays spread over." },
|
|
35
41
|
{ name: "shadowsEnabled", type: "u32" },
|
|
36
42
|
{ name: "uvOffset", type: "u32", doc: "Where the UV sets start inside the attribute buffer." },
|
|
37
43
|
{ name: "orderOffset", type: "u32", doc: "Where the hierarchy's triangle order starts inside the index buffer." },
|
|
44
|
+
{ name: "leafOffset", type: "u32", doc: "Where each triangle's position in that order starts inside the index buffer." },
|
|
38
45
|
{ name: "colorOffset", type: "u32", doc: "Where vertex colours start inside the attribute buffer." },
|
|
39
46
|
{ name: "hasColors", type: "u32" },
|
|
40
47
|
{ name: "varyingOffset", type: "u32", doc: "Where shader varyings start inside the attribute buffer." },
|
|
@@ -53,6 +60,17 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
53
60
|
{ name: "volumetricData", type: "u32" },
|
|
54
61
|
{ name: "adaptiveMin", type: "u32", doc: "Samples a pixel takes before it may stop as converged; 0 never stops." },
|
|
55
62
|
{ name: "adaptiveThreshold", type: "f32", doc: "The error under which a pixel and its neighbours count as converged." },
|
|
63
|
+
{ name: "lensDistortion", type: "f32", doc: "Radial distortion of the camera ray: positive is barrel, negative pincushion." },
|
|
64
|
+
{ name: "rouletteThreshold", type: "f32", doc: "Throughput under which a path may be ended by chance; zero never." },
|
|
65
|
+
{ name: "rouletteStart", type: "u32", doc: "Bounces every path takes before the roulette may end it." },
|
|
66
|
+
{ name: "subsurfaceEnabled", type: "u32", doc: "Whether materials that scatter light inside do; 0 renders them opaque." },
|
|
67
|
+
{ name: "bevelEnabled", type: "u32", doc: "Whether materials with a bevel radius round their edges." },
|
|
68
|
+
{ name: "volumesEnabled", type: "u32", doc: "Whether volumes scatter light; 0 leaves their meshes out altogether." },
|
|
69
|
+
{ name: "cameraVolume", type: "u32", doc: "The volume material the camera stands inside, or NO_MATERIAL." },
|
|
70
|
+
{ name: "atmosphereColor", type: "vec4f", doc: "rgb is the radiance the air scatters towards the camera, w 1 when it is on." },
|
|
71
|
+
{ name: "atmosphereExtinction", type: "vec4f", doc: "rgb is how much of each colour the air takes per scene unit, w the haze." },
|
|
72
|
+
{ name: "atmosphereSun", type: "vec4f", doc: "xyz is the direction towards the brightest directional light." },
|
|
73
|
+
{ name: "atmosphereSunColor", type: "vec4f", doc: "rgb is that light's irradiance, which the air scatters." },
|
|
56
74
|
{ name: "clipPlane", type: "vec4f", doc: "A plane the camera ray drops surfaces behind, for planar captures; w = 0 and xyz = 0 is none." },
|
|
57
75
|
]);
|
|
58
76
|
|
package/src/geometry.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { clipToWorld, transform4, worldToClip, type CameraMatrices } from "./cam
|
|
|
2
2
|
import {
|
|
3
3
|
isMaterialVisible,
|
|
4
4
|
MATERIAL_KIND,
|
|
5
|
+
MATERIAL_UNLIT_LIGHT,
|
|
5
6
|
materialProperties,
|
|
6
7
|
NO_PLACEMENT,
|
|
7
8
|
NO_PROGRAM,
|
|
@@ -525,9 +526,12 @@ export function packGeometry(
|
|
|
525
526
|
materials: new Uint32Array(triangleMaterials),
|
|
526
527
|
triangleCount: triangleMaterials.length,
|
|
527
528
|
materialColors: new Float32Array(properties.flatMap(m => [...m.color, m.opacity])),
|
|
528
|
-
// Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs.
|
|
529
|
+
// Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs. An unlit
|
|
530
|
+
// material of a fixed colour emits that colour, so it is one too.
|
|
529
531
|
materialEmissive: new Float32Array(properties.flatMap(m =>
|
|
530
|
-
m.kind === MATERIAL_KIND.shader ? [0, 0, 0, 0]
|
|
532
|
+
m.kind === MATERIAL_KIND.shader ? [0, 0, 0, 0]
|
|
533
|
+
: (m.flags & MATERIAL_UNLIT_LIGHT) !== 0 ? [...m.color, 1]
|
|
534
|
+
: [...m.emissive, m.emissiveIntensity])),
|
|
531
535
|
materialProperties: properties,
|
|
532
536
|
materialPlacements: entries.map(entry => entry.placement),
|
|
533
537
|
materialCount: properties.length,
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,19 @@ export { attachCanvas } from "./canvas.js";
|
|
|
21
21
|
export { DEFAULT_DENOISER } from "./denoise.js";
|
|
22
22
|
export type { DenoiserSettings } from "./denoise.js";
|
|
23
23
|
|
|
24
|
+
// The settings only this renderer reads, beside the CPU renderer's, and the defaults it renders with.
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_GPU_RENDER_SETTINGS,
|
|
27
|
+
effectiveExposure,
|
|
28
|
+
mergeGpuSettings,
|
|
29
|
+
physicalExposure,
|
|
30
|
+
whiteBalanceMatrix,
|
|
31
|
+
} from "./settings.js";
|
|
32
|
+
|
|
33
|
+
export type { GpuRenderSettings, GpuRenderSettingsInput } from "./settings.js";
|
|
34
|
+
export { subsurfaceMedium } from "./subsurface.js";
|
|
35
|
+
export type { SubsurfaceInput, SubsurfaceMaterial, SubsurfaceMedium } from "./subsurface.js";
|
|
36
|
+
|
|
24
37
|
export type { Bucket, Listener } from "./upstream.js";
|
|
25
38
|
|
|
26
39
|
// Scene packing, published because the sizes it produces are what a host checks against a device's limits.
|