@tinypoly/gpu-renderer 0.2.4 → 0.3.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 +48 -10
- package/README.md +2 -0
- package/dist/accumulator.d.ts.map +1 -1
- package/dist/accumulator.js +1 -0
- package/dist/accumulator.js.map +1 -1
- package/dist/denoise.js +2 -2
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +9 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +5 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts +48 -8
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +409 -134
- package/dist/geometry.js.map +1 -1
- package/dist/grass.d.ts.map +1 -1
- package/dist/grass.js +2 -1
- package/dist/grass.js.map +1 -1
- package/dist/imageResolve.js +2 -2
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instancing.d.ts +100 -0
- package/dist/instancing.d.ts.map +1 -0
- package/dist/instancing.js +286 -0
- package/dist/instancing.js.map +1 -0
- package/dist/lensPass.d.ts +2 -0
- package/dist/lensPass.d.ts.map +1 -1
- package/dist/lensPass.js +7 -4
- package/dist/lensPass.js.map +1 -1
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +8 -5
- package/dist/lights.js.map +1 -1
- package/dist/memoryProbe.d.ts +76 -0
- package/dist/memoryProbe.d.ts.map +1 -0
- package/dist/memoryProbe.js +197 -0
- package/dist/memoryProbe.js.map +1 -0
- package/dist/oidnPass.d.ts +6 -0
- package/dist/oidnPass.d.ts.map +1 -1
- package/dist/oidnPass.js +18 -1
- package/dist/oidnPass.js.map +1 -1
- package/dist/pathTracePass.d.ts +15 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +38 -16
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +248 -41
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +26 -8
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +97 -27
- package/dist/sceneUpload.js.map +1 -1
- package/dist/shaderModule.d.ts.map +1 -1
- package/dist/shaderModule.js +2 -1
- package/dist/shaderModule.js.map +1 -1
- package/dist/skyBake.d.ts.map +1 -1
- package/dist/skyBake.js +5 -3
- package/dist/skyBake.js.map +1 -1
- package/dist/texturePack.d.ts +43 -3
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +209 -37
- package/dist/texturePack.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +5 -1
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +105 -15
- 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 +92 -11
- package/dist/wgsl/common.wgsl.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 +14 -2
- package/dist/wgsl/lens.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 +24 -14
- 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 +11 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/accumulator.ts +1 -0
- package/src/denoise.ts +2 -2
- package/src/device.ts +12 -0
- package/src/frameLayout.ts +5 -0
- package/src/geometry.ts +527 -133
- package/src/grass.ts +2 -1
- package/src/imageResolve.ts +2 -2
- package/src/index.ts +1 -0
- package/src/instancing.ts +415 -0
- package/src/lensPass.ts +9 -4
- package/src/lights.ts +9 -5
- package/src/memoryProbe.ts +275 -0
- package/src/oidnPass.ts +21 -1
- package/src/pathTracePass.ts +42 -18
- package/src/renderer.ts +308 -47
- package/src/sceneUpload.ts +139 -36
- package/src/shaderModule.ts +2 -1
- package/src/skyBake.ts +5 -3
- package/src/texturePack.ts +274 -43
- package/src/wgsl/bvh.wgsl.ts +105 -15
- package/src/wgsl/common.wgsl.ts +92 -11
- package/src/wgsl/lens.wgsl.ts +14 -2
- package/src/wgsl/surface.wgsl.ts +24 -14
- package/src/wgsl/texture.wgsl.ts +11 -0
package/src/grass.ts
CHANGED
|
@@ -470,7 +470,8 @@ export function packGrass(geometry: PackedGeometry, base: number): PackedGrass |
|
|
|
470
470
|
if (length < 1e-12) continue;
|
|
471
471
|
|
|
472
472
|
// The side the surface faces: its vertex normals where it has them, which survive a winding turned around.
|
|
473
|
-
const
|
|
473
|
+
const [v0, v1, v2] = [0, 1, 2].map(corner => geometry.corners[triangle * 3 + corner] * 3);
|
|
474
|
+
const authored = [0, 1, 2].map(axis => n[v0 + axis] + n[v1 + axis] + n[v2 + axis]);
|
|
474
475
|
const facing = Math.hypot(authored[0], authored[1], authored[2]) > 1e-6 ? authored[1] : geometric[1];
|
|
475
476
|
const up = Math.abs(geometric[1]) / length;
|
|
476
477
|
|
package/src/imageResolve.ts
CHANGED
|
@@ -97,8 +97,8 @@ export class ImageResolvePass {
|
|
|
97
97
|
this.device.queue.writeBuffer(this.uniforms, 0, this.values);
|
|
98
98
|
|
|
99
99
|
const bytes = width * height * 4;
|
|
100
|
-
const output = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
|
|
101
|
-
const staging = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
100
|
+
const output = this.device.createBuffer({ label: "image readback", size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
|
|
101
|
+
const staging = this.device.createBuffer({ label: "image readback", size: bytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
102
102
|
|
|
103
103
|
try {
|
|
104
104
|
const encoder = this.device.createCommandEncoder({ label: "image resolve" });
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { isWebGpuAvailable, requestRenderDevice, requestWebGpuSupport } from "./
|
|
|
5
5
|
export type { RenderDevice, RenderDeviceOptions, WebGpuSupport } from "./device.js";
|
|
6
6
|
|
|
7
7
|
export { GpuRenderer, isAbortError } from "./renderer.js";
|
|
8
|
+
export { deviceMemory, setMemoryLogging } from "./memoryProbe.js";
|
|
8
9
|
|
|
9
10
|
export type {
|
|
10
11
|
GpuRendererEvents,
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geometry a scene repeats, kept once. A mesh whose triangles appear again elsewhere, the same positions, normals, UVs
|
|
3
|
+
* and colours under another transform, becomes an instance of a prototype: the prototype's triangles sit in object
|
|
4
|
+
* space with a hierarchy of their own, and each instance keeps only its transform and the material of each triangle.
|
|
5
|
+
* A tree over the instances' boxes leads into those hierarchies, so the kernel traverses one tree, entering an instance
|
|
6
|
+
* by taking the ray into its space.
|
|
7
|
+
*
|
|
8
|
+
* Only meshes nothing else reads triangle by triangle become instances: lights, windows, volumes and grass are
|
|
9
|
+
* found on the CPU among the scene's own triangles, and points, lines, sprites and vertex shaders depend on the camera.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NODE_WORDS, type PackedBvh } from "./bvh.js";
|
|
13
|
+
|
|
14
|
+
/** A mesh with fewer triangles than this is cheaper to copy than to enter. */
|
|
15
|
+
export const INSTANCE_MIN_TRIANGLES = 256;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Words per instance in the texture buffer: the object to world transform and its inverse, three rows of four floats
|
|
19
|
+
* each, then the root of the prototype's hierarchy, where its triangles start among the stored ones, and the number
|
|
20
|
+
* the first of this instance's triangles goes by.
|
|
21
|
+
*/
|
|
22
|
+
export const INSTANCE_WORDS = 28;
|
|
23
|
+
|
|
24
|
+
/** A prototype: a run of the stored triangles, in object space. */
|
|
25
|
+
export interface Prototype {
|
|
26
|
+
/** Its first triangle among the prototype triangles. */
|
|
27
|
+
first: number;
|
|
28
|
+
count: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Instance {
|
|
32
|
+
prototype: number;
|
|
33
|
+
/** Object to world, column major, as Three's `matrixWorld`. */
|
|
34
|
+
matrix: readonly number[];
|
|
35
|
+
/** The number this instance's first triangle goes by, after every triangle of the scene's own. */
|
|
36
|
+
firstTriangle: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Repeated geometry, as `packGeometry` leaves it. */
|
|
40
|
+
export interface PackedInstancing {
|
|
41
|
+
/** Nine coordinates per prototype triangle, in object space. */
|
|
42
|
+
positions: Float32Array;
|
|
43
|
+
triangleCount: number;
|
|
44
|
+
prototypes: Prototype[];
|
|
45
|
+
instances: Instance[];
|
|
46
|
+
/** One material index per instanced triangle, every instance's in turn. */
|
|
47
|
+
materials: Uint32Array;
|
|
48
|
+
/** The box around every instance, in world space. */
|
|
49
|
+
bounds: { low: number[]; high: number[] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A seed pair, mixed by every word: two independent 32 bit hashes, so a match is all but certain. */
|
|
53
|
+
interface Hash {
|
|
54
|
+
a: number;
|
|
55
|
+
b: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const scratch = new Float32Array(1);
|
|
59
|
+
const scratchWords = new Uint32Array(scratch.buffer);
|
|
60
|
+
|
|
61
|
+
function mix(hash: Hash, word: number) {
|
|
62
|
+
hash.a = Math.imul(hash.a ^ word, 0x01000193);
|
|
63
|
+
hash.b = Math.imul(hash.b + word, 0x5bd1e995);
|
|
64
|
+
hash.b ^= hash.b >>> 15;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mixArray(hash: Hash, data: ArrayLike<number> | null | undefined) {
|
|
68
|
+
if (!data) {
|
|
69
|
+
mix(hash, 0x9e3779b9);
|
|
70
|
+
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
mix(hash, data.length);
|
|
75
|
+
|
|
76
|
+
if (data instanceof Float32Array || data instanceof Uint32Array || data instanceof Int32Array) {
|
|
77
|
+
const words = new Uint32Array(data.buffer, data.byteOffset, data.length);
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < words.length; i++) mix(hash, words[i]);
|
|
80
|
+
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < data.length; i++) {
|
|
85
|
+
scratch[0] = data[i];
|
|
86
|
+
mix(hash, scratchWords[0]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What makes two meshes' geometry the same, as far as the triangles they pack: one value per array, then the rest. */
|
|
91
|
+
interface GeometrySource {
|
|
92
|
+
attributes: Record<string, { data: ArrayLike<number>; itemSize: number } | undefined>;
|
|
93
|
+
index: ArrayLike<number> | null;
|
|
94
|
+
groups: readonly { start: number; count: number; materialIndex: number }[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A mesh handed over twice shares its arrays: hashing each array once is enough.
|
|
98
|
+
const arrayHashes = new WeakMap<object, string>();
|
|
99
|
+
|
|
100
|
+
function arrayKey(data: ArrayLike<number> | null | undefined): string {
|
|
101
|
+
if (!data) return "-";
|
|
102
|
+
|
|
103
|
+
const known = arrayHashes.get(data as object);
|
|
104
|
+
|
|
105
|
+
if (known) return known;
|
|
106
|
+
|
|
107
|
+
const hash = { a: 0x811c9dc5, b: 0x2545f491 };
|
|
108
|
+
|
|
109
|
+
mixArray(hash, data);
|
|
110
|
+
|
|
111
|
+
const key = `${(hash.a >>> 0).toString(36)}.${(hash.b >>> 0).toString(36)}`;
|
|
112
|
+
|
|
113
|
+
arrayHashes.set(data as object, key);
|
|
114
|
+
|
|
115
|
+
return key;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A key equal for two meshes exactly when their triangles would pack the same in object space. */
|
|
119
|
+
export function geometryKey(mesh: GeometrySource, visibleGroups: string): string {
|
|
120
|
+
const { position, normal, uv, uv1, color } = mesh.attributes;
|
|
121
|
+
const groups = mesh.groups.map(group => `${group.start}:${group.count}:${group.materialIndex}`).join(",");
|
|
122
|
+
|
|
123
|
+
return [
|
|
124
|
+
arrayKey(position?.data), position?.itemSize,
|
|
125
|
+
arrayKey(normal?.data), normal?.itemSize,
|
|
126
|
+
arrayKey(uv?.data), uv?.itemSize,
|
|
127
|
+
arrayKey(uv1?.data), uv1?.itemSize,
|
|
128
|
+
arrayKey(color?.data), color?.itemSize,
|
|
129
|
+
arrayKey(mesh.index),
|
|
130
|
+
groups, visibleGroups,
|
|
131
|
+
].join("|");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Whether a transform keeps a prototype's triangles as they are: affine, invertible and not mirrored. */
|
|
135
|
+
export function instanceable(matrix: readonly number[]): boolean {
|
|
136
|
+
if (matrix[3] !== 0 || matrix[7] !== 0 || matrix[11] !== 0 || matrix[15] !== 1) return false;
|
|
137
|
+
|
|
138
|
+
const determinant = matrix[0] * (matrix[5] * matrix[10] - matrix[6] * matrix[9])
|
|
139
|
+
- matrix[4] * (matrix[1] * matrix[10] - matrix[2] * matrix[9])
|
|
140
|
+
+ matrix[8] * (matrix[1] * matrix[6] - matrix[2] * matrix[5]);
|
|
141
|
+
|
|
142
|
+
return determinant > 1e-12 && Number.isFinite(determinant);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The inverse of an affine transform, column major. */
|
|
146
|
+
export function inverseAffine(m: readonly number[]): number[] {
|
|
147
|
+
const a = m[0], b = m[1], c = m[2], d = m[4], e = m[5], f = m[6], g = m[8], h = m[9], i = m[10];
|
|
148
|
+
const c0 = e * i - f * h, c1 = f * g - d * i, c2 = d * h - e * g;
|
|
149
|
+
const s = 1 / (a * c0 + b * c1 + c * c2);
|
|
150
|
+
|
|
151
|
+
// Columns of the inverse 3x3: the adjugate over the determinant.
|
|
152
|
+
const r = [
|
|
153
|
+
c0 * s, (c * h - b * i) * s, (b * f - c * e) * s,
|
|
154
|
+
c1 * s, (a * i - c * g) * s, (c * d - a * f) * s,
|
|
155
|
+
c2 * s, (b * g - a * h) * s, (a * e - b * d) * s,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const tx = m[12], ty = m[13], tz = m[14];
|
|
159
|
+
|
|
160
|
+
return [
|
|
161
|
+
r[0], r[1], r[2], 0,
|
|
162
|
+
r[3], r[4], r[5], 0,
|
|
163
|
+
r[6], r[7], r[8], 0,
|
|
164
|
+
-(r[0] * tx + r[3] * ty + r[6] * tz), -(r[1] * tx + r[4] * ty + r[7] * tz), -(r[2] * tx + r[5] * ty + r[8] * tz), 1,
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A box transformed by a matrix: the box around its eight corners. */
|
|
169
|
+
export function transformBox(m: readonly number[], low: readonly number[], high: readonly number[]) {
|
|
170
|
+
const outLow = [Infinity, Infinity, Infinity], outHigh = [-Infinity, -Infinity, -Infinity];
|
|
171
|
+
|
|
172
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
173
|
+
const x = corner & 1 ? high[0] : low[0], y = corner & 2 ? high[1] : low[1], z = corner & 4 ? high[2] : low[2];
|
|
174
|
+
|
|
175
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
176
|
+
const value = m[axis] * x + m[4 + axis] * y + m[8 + axis] * z + m[12 + axis];
|
|
177
|
+
|
|
178
|
+
outLow[axis] = Math.min(outLow[axis], value);
|
|
179
|
+
outHigh[axis] = Math.max(outHigh[axis], value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { low: outLow, high: outHigh };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The box around a run of triangles, nine coordinates each. */
|
|
187
|
+
export function trianglesBox(positions: Float32Array, first: number, count: number) {
|
|
188
|
+
const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];
|
|
189
|
+
|
|
190
|
+
for (let at = first * 9; at < (first + count) * 9; at += 3) {
|
|
191
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
192
|
+
low[axis] = Math.min(low[axis], positions[at + axis]);
|
|
193
|
+
high[axis] = Math.max(high[axis], positions[at + axis]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { low, high };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The words of every instance, as the kernel reads them: see `INSTANCE_WORDS`. */
|
|
201
|
+
export function instanceWords(
|
|
202
|
+
instances: readonly Instance[], roots: readonly number[], storageFirst: readonly number[]): Uint32Array {
|
|
203
|
+
const words = new Uint32Array(instances.length * INSTANCE_WORDS);
|
|
204
|
+
const floats = new Float32Array(words.buffer);
|
|
205
|
+
|
|
206
|
+
instances.forEach((instance, k) => {
|
|
207
|
+
const at = k * INSTANCE_WORDS;
|
|
208
|
+
const m = instance.matrix;
|
|
209
|
+
const inverse = inverseAffine(m);
|
|
210
|
+
|
|
211
|
+
// Rows of the 3x4 transforms: a row times (x, y, z, 1) is one coordinate.
|
|
212
|
+
for (let row = 0; row < 3; row++) {
|
|
213
|
+
floats.set([m[row], m[4 + row], m[8 + row], m[12 + row]], at + row * 4);
|
|
214
|
+
floats.set([inverse[row], inverse[4 + row], inverse[8 + row], inverse[12 + row]], at + 12 + row * 4);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
words[at + 24] = roots[instance.prototype];
|
|
218
|
+
words[at + 25] = storageFirst[instance.prototype];
|
|
219
|
+
words[at + 26] = instance.firstTriangle;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return words;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** One node of the tree over instances, before it is laid out: a box, and two children or one instance. */
|
|
226
|
+
interface TopNode {
|
|
227
|
+
low: number[];
|
|
228
|
+
high: number[];
|
|
229
|
+
left: number;
|
|
230
|
+
right: number;
|
|
231
|
+
instance: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The tree over the instances' boxes, split at the median along the widest spread of their centres, one instance a
|
|
236
|
+
* leaf. Instances are few, so this is cheap, and the median split never fails the way a heuristic one can on boxes
|
|
237
|
+
* that share a centre: the depth is the logarithm of the count, which keeps the kernel's stack bounded.
|
|
238
|
+
*/
|
|
239
|
+
function buildTopTree(boxes: readonly { low: number[]; high: number[] }[]): TopNode[] {
|
|
240
|
+
const nodes: TopNode[] = [];
|
|
241
|
+
const centre = boxes.map(box => box.low.map((value, axis) => (value + box.high[axis]) / 2));
|
|
242
|
+
|
|
243
|
+
const build = (items: number[]): number => {
|
|
244
|
+
const index = nodes.length;
|
|
245
|
+
const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];
|
|
246
|
+
|
|
247
|
+
for (const item of items)
|
|
248
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
249
|
+
low[axis] = Math.min(low[axis], boxes[item].low[axis]);
|
|
250
|
+
high[axis] = Math.max(high[axis], boxes[item].high[axis]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
nodes.push({ low, high, left: -1, right: -1, instance: items.length === 1 ? items[0] : -1 });
|
|
254
|
+
|
|
255
|
+
if (items.length === 1) return index;
|
|
256
|
+
|
|
257
|
+
const spread = [0, 1, 2].map(axis => {
|
|
258
|
+
const values = items.map(item => centre[item][axis]);
|
|
259
|
+
|
|
260
|
+
return Math.max(...values) - Math.min(...values);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const axis = spread.indexOf(Math.max(...spread));
|
|
264
|
+
const sorted = [...items].sort((p, q) => centre[p][axis] - centre[q][axis] || p - q);
|
|
265
|
+
const half = sorted.length >> 1;
|
|
266
|
+
|
|
267
|
+
nodes[index].left = build(sorted.slice(0, half));
|
|
268
|
+
nodes[index].right = build(sorted.slice(half));
|
|
269
|
+
|
|
270
|
+
return index;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (boxes.length) build(boxes.map((_, k) => k));
|
|
274
|
+
|
|
275
|
+
return nodes;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The scene's triangles, then every prototype's, in one hierarchy: what the kernel traverses. */
|
|
279
|
+
export interface MergedHierarchy {
|
|
280
|
+
nodes: ArrayBuffer;
|
|
281
|
+
nodeCount: number;
|
|
282
|
+
/** The stored triangle at each position of the leaf order: the scene's own first, then each prototype's. */
|
|
283
|
+
order: Uint32Array;
|
|
284
|
+
/** Where each prototype's hierarchy starts among the nodes. */
|
|
285
|
+
roots: number[];
|
|
286
|
+
/** Where each prototype's triangles start among the stored ones. */
|
|
287
|
+
storageFirst: number[];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const FAR = 3.4028234663852886e38;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Joins the scene's own hierarchy, one per prototype and the tree over the instances into one node array. Node zero
|
|
294
|
+
* holds the scene's tree on one side and the instances' on the other. A leaf of the instances' tree has a count of -1
|
|
295
|
+
* and links to its instance; a prototype's leaves point into its run of the leaf order, after the scene's own.
|
|
296
|
+
*/
|
|
297
|
+
export function mergeHierarchies(scene: PackedBvh, sceneTriangles: number, prototypes: readonly PackedBvh[],
|
|
298
|
+
prototypeCounts: readonly number[], instanceBoxes: readonly { low: number[]; high: number[] }[]): MergedHierarchy {
|
|
299
|
+
const top = buildTopTree(instanceBoxes);
|
|
300
|
+
const topNodes = top.filter(node => node.instance < 0);
|
|
301
|
+
// A single instance is a tree of one leaf, which still takes a node to hold it.
|
|
302
|
+
const topCount = Math.max(1, topNodes.length);
|
|
303
|
+
const sceneBase = 1;
|
|
304
|
+
const topBase = sceneBase + scene.nodeCount;
|
|
305
|
+
const roots: number[] = [];
|
|
306
|
+
const storageFirst: number[] = [];
|
|
307
|
+
let nodeCount = topBase + topCount;
|
|
308
|
+
let stored = sceneTriangles;
|
|
309
|
+
|
|
310
|
+
prototypes.forEach((bvh, p) => {
|
|
311
|
+
roots.push(nodeCount);
|
|
312
|
+
storageFirst.push(stored);
|
|
313
|
+
nodeCount += bvh.nodeCount;
|
|
314
|
+
stored += prototypeCounts[p];
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const nodes = new ArrayBuffer(nodeCount * NODE_WORDS * 4);
|
|
318
|
+
const floats = new Float32Array(nodes);
|
|
319
|
+
const ints = new Int32Array(nodes);
|
|
320
|
+
const order = new Uint32Array(stored);
|
|
321
|
+
|
|
322
|
+
order.set(scene.order.subarray(0, sceneTriangles), 0);
|
|
323
|
+
|
|
324
|
+
// A tree copied in place: its nodes move by `base`, its leaves by `leafBase` along the leaf order.
|
|
325
|
+
const copyTree = (bvh: PackedBvh, base: number, leafBase: number) => {
|
|
326
|
+
const source = new Int32Array(bvh.nodes);
|
|
327
|
+
|
|
328
|
+
floats.set(new Float32Array(bvh.nodes), base * NODE_WORDS);
|
|
329
|
+
|
|
330
|
+
for (let node = 0; node < bvh.nodeCount; node++)
|
|
331
|
+
for (const child of [0, 8]) {
|
|
332
|
+
const word = (base + node) * NODE_WORDS + child;
|
|
333
|
+
const link = source[node * NODE_WORDS + child + 3];
|
|
334
|
+
const count = source[node * NODE_WORDS + child + 7];
|
|
335
|
+
|
|
336
|
+
if (link >= 0) ints[word + 3] = link + base;
|
|
337
|
+
else if (count > 0) ints[word + 3] = -1 - (-1 - link + leafBase);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const writeChild = (word: number, low: readonly number[], high: readonly number[], link: number, count: number) => {
|
|
342
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
343
|
+
floats[word + axis] = low[axis];
|
|
344
|
+
floats[word + 4 + axis] = high[axis];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
ints[word + 3] = link;
|
|
348
|
+
ints[word + 7] = count;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const empty = (word: number) => writeChild(word, [FAR, FAR, FAR], [FAR, FAR, FAR], -1, 0);
|
|
352
|
+
|
|
353
|
+
// The box a whole tree takes, from the two children of its first node; an empty child adds nothing.
|
|
354
|
+
const treeBox = (bvh: PackedBvh) => {
|
|
355
|
+
const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];
|
|
356
|
+
const source = new Float32Array(bvh.nodes), links = new Int32Array(bvh.nodes);
|
|
357
|
+
|
|
358
|
+
for (const child of [0, 8]) {
|
|
359
|
+
if (links[child + 3] < 0 && links[child + 7] === 0) continue;
|
|
360
|
+
|
|
361
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
362
|
+
low[axis] = Math.min(low[axis], source[child + axis]);
|
|
363
|
+
high[axis] = Math.max(high[axis], source[child + 4 + axis]);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { low, high };
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
if (sceneTriangles > 0 && scene.nodeCount > 0) {
|
|
371
|
+
const box = treeBox(scene);
|
|
372
|
+
|
|
373
|
+
copyTree(scene, sceneBase, 0);
|
|
374
|
+
writeChild(0, box.low, box.high, sceneBase, 0);
|
|
375
|
+
} else {
|
|
376
|
+
empty(0);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// The instances' tree, numbered as it was built without its leaves, which live in their parents.
|
|
380
|
+
const index = new Map<number, number>();
|
|
381
|
+
|
|
382
|
+
top.forEach((node, at) => { if (node.instance < 0) index.set(at, topBase + index.size); });
|
|
383
|
+
|
|
384
|
+
const topChild = (word: number, at: number) => {
|
|
385
|
+
const node = top[at];
|
|
386
|
+
|
|
387
|
+
if (node.instance >= 0) writeChild(word, node.low, node.high, -1 - node.instance, -1);
|
|
388
|
+
else writeChild(word, node.low, node.high, index.get(at) as number, 0);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (top.length === 1) {
|
|
392
|
+
topChild(topBase * NODE_WORDS, 0);
|
|
393
|
+
empty(topBase * NODE_WORDS + 8);
|
|
394
|
+
} else {
|
|
395
|
+
top.forEach((node, at) => {
|
|
396
|
+
if (node.instance >= 0) return;
|
|
397
|
+
|
|
398
|
+
const word = (index.get(at) as number) * NODE_WORDS;
|
|
399
|
+
|
|
400
|
+
topChild(word, node.left);
|
|
401
|
+
topChild(word + 8, node.right);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (top.length) writeChild(8, top[0].low, top[0].high, topBase, 0);
|
|
406
|
+
else empty(8);
|
|
407
|
+
|
|
408
|
+
prototypes.forEach((bvh, p) => {
|
|
409
|
+
copyTree(bvh, roots[p], storageFirst[p]);
|
|
410
|
+
|
|
411
|
+
for (let i = 0; i < prototypeCounts[p]; i++) order[storageFirst[p] + i] = storageFirst[p] + bvh.order[i];
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return { nodes, nodeCount, order, roots, storageFirst };
|
|
415
|
+
}
|
package/src/lensPass.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createShaderModule } from "./shaderModule.js";
|
|
2
|
-
import { NEUTRAL_WHITE_BALANCE, whiteBalanceMatrix, type GpuRenderSettings } from "./settings.js";
|
|
2
|
+
import { effectiveExposure, NEUTRAL_WHITE_BALANCE, whiteBalanceMatrix, type GpuRenderSettings } from "./settings.js";
|
|
3
3
|
import { LENS_WGSL } from "./wgsl/lens.wgsl.js";
|
|
4
4
|
|
|
5
5
|
const UNIFORM_BYTES = 160;
|
|
@@ -31,6 +31,8 @@ export interface LensEffects {
|
|
|
31
31
|
autoExposure: boolean;
|
|
32
32
|
/** The share of a dark scene's darkness the meter keeps, `0` to `1`. */
|
|
33
33
|
darkAdaptation: number;
|
|
34
|
+
/** What the image is multiplied by after the lens, the meter's own exposure aside: the rays are limited against it. */
|
|
35
|
+
exposure: number;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/** The lens effects these settings ask for, or `null` when they ask for none and the pass can be skipped. */
|
|
@@ -58,6 +60,7 @@ export function lensEffects(settings: GpuRenderSettings): LensEffects | null {
|
|
|
58
60
|
whiteBalance: neutral ? null : whiteBalanceMatrix(camera.whiteBalance),
|
|
59
61
|
autoExposure,
|
|
60
62
|
darkAdaptation: Math.min(1, Math.max(0, Number(camera.darkAdaptation) || 0)),
|
|
63
|
+
exposure: Math.max(effectiveExposure(settings), 0),
|
|
61
64
|
};
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -216,6 +219,7 @@ export class LensPass {
|
|
|
216
219
|
floats[34] = effects.glareAngle;
|
|
217
220
|
floats[35] = effects.autoExposure ? 1 : 0;
|
|
218
221
|
floats[36] = effects.darkAdaptation;
|
|
222
|
+
floats[37] = effects.exposure;
|
|
219
223
|
for (let row = 0; row < 3; row++) for (let column = 0; column < 3; column++)
|
|
220
224
|
floats[20 + row * 4 + column] = white[row * 3 + column];
|
|
221
225
|
fill();
|
|
@@ -256,6 +260,10 @@ export class LensPass {
|
|
|
256
260
|
integers[10] = level.offset;
|
|
257
261
|
};
|
|
258
262
|
|
|
263
|
+
// The meter is one workgroup, whatever the size: dispatched as a single tile of the grid it reads. It goes first,
|
|
264
|
+
// since the rays are limited against the exposure it finds.
|
|
265
|
+
if (effects.autoExposure) dispatch(this.meter, { width: 1, height: 1 }, () => {});
|
|
266
|
+
|
|
259
267
|
// The rays are gathered from a level of the same pyramid, so either effect builds it.
|
|
260
268
|
if (effects.bloom > 0 || effects.glare > 0) {
|
|
261
269
|
levels.forEach((level, k) => dispatch(this.downsample, level, () => {
|
|
@@ -294,9 +302,6 @@ export class LensPass {
|
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
|
|
297
|
-
// The meter is one workgroup, whatever the size: dispatched as a single tile of the grid it reads.
|
|
298
|
-
if (effects.autoExposure) dispatch(this.meter, { width: 1, height: 1 }, () => {});
|
|
299
|
-
|
|
300
305
|
dispatch(this.composite, { width, height }, () => {});
|
|
301
306
|
|
|
302
307
|
return this.graded as GPUBuffer;
|
package/src/lights.ts
CHANGED
|
@@ -148,11 +148,15 @@ interface Bounds {
|
|
|
148
148
|
radius: number;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function sceneBounds(
|
|
152
|
-
|
|
151
|
+
function sceneBounds(geometry: PackedGeometry): Bounds {
|
|
152
|
+
const positions = geometry.positions;
|
|
153
|
+
const instances = geometry.instancing?.bounds;
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
if (!positions.length && !instances) return { centre: [0, 0, 0], radius: 1 };
|
|
156
|
+
|
|
157
|
+
// Repeated geometry is kept in object space; the box around its instances stands in for it.
|
|
158
|
+
const low = instances ? [...instances.low] : [Infinity, Infinity, Infinity];
|
|
159
|
+
const high = instances ? [...instances.high] : [-Infinity, -Infinity, -Infinity];
|
|
156
160
|
|
|
157
161
|
for (let at = 0; at < positions.length; at += 3) {
|
|
158
162
|
for (let axis = 0; axis < 3; axis++) {
|
|
@@ -418,7 +422,7 @@ export function packLights(
|
|
|
418
422
|
lights: readonly SerializedLight[] = [],
|
|
419
423
|
environment?: { power: number } | null,
|
|
420
424
|
options: LightPackOptions = {}): PackedLights {
|
|
421
|
-
const bounds = sceneBounds(geometry
|
|
425
|
+
const bounds = sceneBounds(geometry);
|
|
422
426
|
const areas = emissiveTriangles(geometry, bounds);
|
|
423
427
|
const extra: number[] = [];
|
|
424
428
|
|