@tinypoly/gpu-renderer 0.0.0 → 0.0.2
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 +7 -2
- package/README.md +29 -3
- package/dist/bvh.d.ts.map +1 -1
- package/dist/bvh.js +178 -89
- package/dist/bvh.js.map +1 -1
- package/dist/bvhBuilder.d.ts +72 -0
- package/dist/bvhBuilder.d.ts.map +1 -0
- package/dist/bvhBuilder.js +126 -0
- package/dist/bvhBuilder.js.map +1 -0
- package/dist/bvhWorker.d.ts +2 -0
- package/dist/bvhWorker.d.ts.map +1 -0
- package/dist/bvhWorker.js +8 -0
- package/dist/bvhWorker.js.map +1 -0
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +128 -0
- package/dist/geometry.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +20 -11
- package/dist/renderer.js.map +1 -1
- package/package.json +3 -2
- package/src/bvh.ts +167 -103
- package/src/bvhBuilder.ts +178 -0
- package/src/bvhWorker.ts +13 -0
- package/src/geometry.ts +168 -0
- package/src/index.ts +3 -0
- package/src/renderer.ts +42 -13
package/src/bvh.ts
CHANGED
|
@@ -35,47 +35,6 @@ export interface PackedBvh {
|
|
|
35
35
|
maxDepth: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
interface Range {
|
|
39
|
-
start: number;
|
|
40
|
-
count: number;
|
|
41
|
-
node: number;
|
|
42
|
-
depth: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Axis aligned bounds under construction, in doubles so the sweep does not accumulate rounding. */
|
|
46
|
-
class Bounds {
|
|
47
|
-
min = [Infinity, Infinity, Infinity];
|
|
48
|
-
max = [-Infinity, -Infinity, -Infinity];
|
|
49
|
-
|
|
50
|
-
reset() {
|
|
51
|
-
this.min = [Infinity, Infinity, Infinity];
|
|
52
|
-
this.max = [-Infinity, -Infinity, -Infinity];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
addPoint(x: number, y: number, z: number) {
|
|
56
|
-
const point = [x, y, z];
|
|
57
|
-
|
|
58
|
-
for (let axis = 0; axis < 3; axis++) {
|
|
59
|
-
if (point[axis] < this.min[axis]) this.min[axis] = point[axis];
|
|
60
|
-
if (point[axis] > this.max[axis]) this.max[axis] = point[axis];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
addBounds(other: Bounds) {
|
|
65
|
-
for (let axis = 0; axis < 3; axis++) {
|
|
66
|
-
if (other.min[axis] < this.min[axis]) this.min[axis] = other.min[axis];
|
|
67
|
-
if (other.max[axis] > this.max[axis]) this.max[axis] = other.max[axis];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Surface area, the cost the split heuristic weighs each side by. Empty bounds cost nothing. */
|
|
72
|
-
area(): number {
|
|
73
|
-
const x = this.max[0] - this.min[0], y = this.max[1] - this.min[1], z = this.max[2] - this.min[2];
|
|
74
|
-
|
|
75
|
-
return x < 0 || y < 0 || z < 0 ? 0 : 2 * (x * y + y * z + z * x);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
38
|
/** Per-triangle bounds and centroids, the only inputs the split needs. */
|
|
80
39
|
function triangleBounds(positions: Float32Array, triangleCount: number) {
|
|
81
40
|
const min = new Float32Array(triangleCount * 3);
|
|
@@ -123,41 +82,81 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
|
|
|
123
82
|
ints = new Int32Array(nodes);
|
|
124
83
|
};
|
|
125
84
|
|
|
126
|
-
|
|
127
|
-
|
|
85
|
+
// The pending ranges, four integers each: start, count, node and depth. Typed, so the loop allocates nothing.
|
|
86
|
+
let stack = new Int32Array(4 * 2 * MAX_BVH_DEPTH + 8);
|
|
87
|
+
let top = 1;
|
|
88
|
+
const bins = new Bins();
|
|
128
89
|
let nodeCount = 1;
|
|
129
90
|
let maxDepth = 0;
|
|
130
91
|
|
|
131
|
-
|
|
132
|
-
|
|
92
|
+
stack[0] = 0;
|
|
93
|
+
stack[1] = triangleCount;
|
|
94
|
+
stack[2] = 0;
|
|
95
|
+
stack[3] = 0;
|
|
133
96
|
|
|
134
|
-
|
|
135
|
-
|
|
97
|
+
while (top) {
|
|
98
|
+
top--;
|
|
136
99
|
|
|
137
|
-
|
|
138
|
-
|
|
100
|
+
const start = stack[top * 4], count = stack[top * 4 + 1], node = stack[top * 4 + 2], depth = stack[top * 4 + 3];
|
|
101
|
+
const end = start + count;
|
|
139
102
|
|
|
140
|
-
|
|
141
|
-
bounds.addPoint(max[triangle], max[triangle + 1], max[triangle + 2]);
|
|
142
|
-
}
|
|
103
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
143
104
|
|
|
144
|
-
|
|
105
|
+
// The node's bounds and its centroids' bounds, in one pass. Doubles, so the corners are exactly the floats read.
|
|
106
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
107
|
+
let cMinX = Infinity, cMinY = Infinity, cMinZ = Infinity, cMaxX = -Infinity, cMaxY = -Infinity, cMaxZ = -Infinity;
|
|
145
108
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
109
|
+
for (let i = start; i < end; i++) {
|
|
110
|
+
const triangle = order[i] * 3;
|
|
111
|
+
let value = min[triangle];
|
|
112
|
+
|
|
113
|
+
if (value < minX) minX = value;
|
|
114
|
+
value = min[triangle + 1];
|
|
115
|
+
if (value < minY) minY = value;
|
|
116
|
+
value = min[triangle + 2];
|
|
117
|
+
if (value < minZ) minZ = value;
|
|
118
|
+
value = max[triangle];
|
|
119
|
+
if (value > maxX) maxX = value;
|
|
120
|
+
value = max[triangle + 1];
|
|
121
|
+
if (value > maxY) maxY = value;
|
|
122
|
+
value = max[triangle + 2];
|
|
123
|
+
if (value > maxZ) maxZ = value;
|
|
124
|
+
value = centroid[triangle];
|
|
125
|
+
if (value < cMinX) cMinX = value;
|
|
126
|
+
if (value > cMaxX) cMaxX = value;
|
|
127
|
+
value = centroid[triangle + 1];
|
|
128
|
+
if (value < cMinY) cMinY = value;
|
|
129
|
+
if (value > cMaxY) cMaxY = value;
|
|
130
|
+
value = centroid[triangle + 2];
|
|
131
|
+
if (value < cMinZ) cMinZ = value;
|
|
132
|
+
if (value > cMaxZ) cMaxZ = value;
|
|
133
|
+
}
|
|
152
134
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
135
|
+
const word = node * NODE_WORDS;
|
|
136
|
+
|
|
137
|
+
floats[word] = minX;
|
|
138
|
+
floats[word + 1] = minY;
|
|
139
|
+
floats[word + 2] = minZ;
|
|
140
|
+
floats[word + 4] = maxX;
|
|
141
|
+
floats[word + 5] = maxY;
|
|
142
|
+
floats[word + 6] = maxZ;
|
|
143
|
+
|
|
144
|
+
let split = -1;
|
|
145
|
+
|
|
146
|
+
if (count > MAX_LEAF_SIZE && depth + 1 < MAX_BVH_DEPTH) {
|
|
147
|
+
bins.centroidMin[0] = cMinX;
|
|
148
|
+
bins.centroidMin[1] = cMinY;
|
|
149
|
+
bins.centroidMin[2] = cMinZ;
|
|
150
|
+
bins.centroidMax[0] = cMaxX;
|
|
151
|
+
bins.centroidMax[1] = cMaxY;
|
|
152
|
+
bins.centroidMax[2] = cMaxZ;
|
|
153
|
+
split = partition(order, centroid, min, max, start, end, area(minX, minY, minZ, maxX, maxY, maxZ), bins);
|
|
154
|
+
}
|
|
156
155
|
|
|
157
156
|
if (split < 0) {
|
|
158
157
|
// A leaf points into `order` instead of into the node array, which the sign of the first link tells apart.
|
|
159
|
-
ints[word + 3] = -1 -
|
|
160
|
-
ints[word + 7] =
|
|
158
|
+
ints[word + 3] = -1 - start;
|
|
159
|
+
ints[word + 7] = count;
|
|
161
160
|
continue;
|
|
162
161
|
}
|
|
163
162
|
|
|
@@ -168,92 +167,157 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
|
|
|
168
167
|
|
|
169
168
|
ints[word + 3] = left;
|
|
170
169
|
ints[word + 7] = right;
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
|
|
171
|
+
if ((top + 2) * 4 > stack.length) {
|
|
172
|
+
const grown = new Int32Array(stack.length * 2);
|
|
173
|
+
|
|
174
|
+
grown.set(stack);
|
|
175
|
+
stack = grown;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Left first, then right, so the right side is built first, as the order the tree was always built in.
|
|
179
|
+
stack.set([start, split - start, left, depth + 1, split, end - split, right, depth + 1], top * 4);
|
|
180
|
+
top += 2;
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
return { nodes: nodes.slice(0, nodeCount * NODE_WORDS * 4), nodeCount, order, maxDepth };
|
|
176
184
|
}
|
|
177
185
|
|
|
186
|
+
/** Surface area, the cost the split heuristic weighs each side by. Empty bounds cost nothing. */
|
|
187
|
+
function area(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number {
|
|
188
|
+
const x = maxX - minX, y = maxY - minY, z = maxZ - minZ;
|
|
189
|
+
|
|
190
|
+
return x < 0 || y < 0 || z < 0 ? 0 : 2 * (x * y + y * z + z * x);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Scratch space for one split, kept across every node so binning allocates nothing. */
|
|
194
|
+
class Bins {
|
|
195
|
+
readonly centroidMin = new Float64Array(3);
|
|
196
|
+
readonly centroidMax = new Float64Array(3);
|
|
197
|
+
/** Six doubles per bin and axis: the minimum corner, then the maximum. */
|
|
198
|
+
readonly bounds = new Float64Array(3 * BIN_COUNT * 6);
|
|
199
|
+
readonly counts = new Int32Array(3 * BIN_COUNT);
|
|
200
|
+
readonly rightArea = new Float64Array(BIN_COUNT);
|
|
201
|
+
readonly rightCount = new Int32Array(BIN_COUNT);
|
|
202
|
+
readonly scale = new Float64Array(3);
|
|
203
|
+
|
|
204
|
+
reset() {
|
|
205
|
+
const bounds = this.bounds;
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < bounds.length; i += 6) {
|
|
208
|
+
bounds[i] = bounds[i + 1] = bounds[i + 2] = Infinity;
|
|
209
|
+
bounds[i + 3] = bounds[i + 4] = bounds[i + 5] = -Infinity;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.counts.fill(0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
/**
|
|
179
217
|
* Reorders `order` in place so the range splits in two, and returns where the right side starts, or -1 when no split
|
|
180
|
-
* beats keeping the range as a leaf. Bins the centroids on all three axes and sweeps the surface area
|
|
218
|
+
* beats keeping the range as a leaf. Bins the centroids on all three axes in one pass and sweeps the surface area
|
|
219
|
+
* heuristic on each.
|
|
181
220
|
*/
|
|
182
221
|
function partition(order: Uint32Array, centroid: Float64Array, min: Float32Array, max: Float32Array,
|
|
183
|
-
|
|
222
|
+
start: number, end: number, parentArea: number, bins: Bins): number {
|
|
184
223
|
if (parentArea <= 0) return -1;
|
|
185
224
|
|
|
186
|
-
const
|
|
187
|
-
|
|
225
|
+
const { centroidMin, centroidMax, bounds, counts, rightArea, rightCount, scale } = bins;
|
|
226
|
+
let binned = false;
|
|
188
227
|
|
|
189
|
-
for (let
|
|
190
|
-
const
|
|
228
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
229
|
+
const extent = centroidMax[axis] - centroidMin[axis];
|
|
191
230
|
|
|
192
|
-
|
|
231
|
+
// Every centroid sits at the same place on this axis: no split here separates anything.
|
|
232
|
+
scale[axis] = extent > 0 ? BIN_COUNT / extent : 0;
|
|
233
|
+
binned ||= extent > 0;
|
|
193
234
|
}
|
|
194
235
|
|
|
195
|
-
|
|
196
|
-
let bestCost = range.count, bestAxis = -1, bestBin = -1, bestScale = 0;
|
|
236
|
+
if (!binned) return -1;
|
|
197
237
|
|
|
198
|
-
|
|
199
|
-
const extent = centroidBounds.max[axis] - centroidBounds.min[axis];
|
|
238
|
+
bins.reset();
|
|
200
239
|
|
|
201
|
-
|
|
202
|
-
|
|
240
|
+
for (let i = start; i < end; i++) {
|
|
241
|
+
const triangle = order[i] * 3;
|
|
242
|
+
const x0 = min[triangle], y0 = min[triangle + 1], z0 = min[triangle + 2];
|
|
243
|
+
const x1 = max[triangle], y1 = max[triangle + 1], z1 = max[triangle + 2];
|
|
203
244
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const binCounts = new Int32Array(BIN_COUNT);
|
|
245
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
246
|
+
if (scale[axis] === 0) continue;
|
|
207
247
|
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
248
|
+
const bin = axis * BIN_COUNT + binOf(centroid[triangle + axis], centroidMin[axis], scale[axis]);
|
|
249
|
+
const at = bin * 6;
|
|
250
|
+
|
|
251
|
+
counts[bin]++;
|
|
211
252
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
253
|
+
if (x0 < bounds[at]) bounds[at] = x0;
|
|
254
|
+
if (y0 < bounds[at + 1]) bounds[at + 1] = y0;
|
|
255
|
+
if (z0 < bounds[at + 2]) bounds[at + 2] = z0;
|
|
256
|
+
if (x1 > bounds[at + 3]) bounds[at + 3] = x1;
|
|
257
|
+
if (y1 > bounds[at + 4]) bounds[at + 4] = y1;
|
|
258
|
+
if (z1 > bounds[at + 5]) bounds[at + 5] = z1;
|
|
215
259
|
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Splitting costs a traversal step, so a split only wins when it beats shading the whole range as one leaf.
|
|
263
|
+
let bestCost = end - start, bestAxis = -1, bestBin = -1;
|
|
264
|
+
|
|
265
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
266
|
+
if (scale[axis] === 0) continue;
|
|
216
267
|
|
|
217
268
|
// Sweeping once from each side turns the heuristic into two passes instead of one per candidate split.
|
|
218
|
-
|
|
219
|
-
const rightCount = new Int32Array(BIN_COUNT);
|
|
220
|
-
const accumulated = new Bounds();
|
|
269
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
221
270
|
let running = 0;
|
|
222
271
|
|
|
223
272
|
for (let bin = BIN_COUNT - 1; bin > 0; bin--) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
273
|
+
const at = (axis * BIN_COUNT + bin) * 6;
|
|
274
|
+
|
|
275
|
+
if (bounds[at] < minX) minX = bounds[at];
|
|
276
|
+
if (bounds[at + 1] < minY) minY = bounds[at + 1];
|
|
277
|
+
if (bounds[at + 2] < minZ) minZ = bounds[at + 2];
|
|
278
|
+
if (bounds[at + 3] > maxX) maxX = bounds[at + 3];
|
|
279
|
+
if (bounds[at + 4] > maxY) maxY = bounds[at + 4];
|
|
280
|
+
if (bounds[at + 5] > maxZ) maxZ = bounds[at + 5];
|
|
281
|
+
running += counts[axis * BIN_COUNT + bin];
|
|
282
|
+
rightArea[bin] = area(minX, minY, minZ, maxX, maxY, maxZ);
|
|
227
283
|
rightCount[bin] = running;
|
|
228
284
|
}
|
|
229
285
|
|
|
230
|
-
|
|
286
|
+
minX = minY = minZ = Infinity;
|
|
287
|
+
maxX = maxY = maxZ = -Infinity;
|
|
231
288
|
running = 0;
|
|
232
289
|
|
|
233
290
|
for (let bin = 0; bin < BIN_COUNT - 1; bin++) {
|
|
234
|
-
|
|
235
|
-
|
|
291
|
+
const at = (axis * BIN_COUNT + bin) * 6;
|
|
292
|
+
|
|
293
|
+
if (bounds[at] < minX) minX = bounds[at];
|
|
294
|
+
if (bounds[at + 1] < minY) minY = bounds[at + 1];
|
|
295
|
+
if (bounds[at + 2] < minZ) minZ = bounds[at + 2];
|
|
296
|
+
if (bounds[at + 3] > maxX) maxX = bounds[at + 3];
|
|
297
|
+
if (bounds[at + 4] > maxY) maxY = bounds[at + 4];
|
|
298
|
+
if (bounds[at + 5] > maxZ) maxZ = bounds[at + 5];
|
|
299
|
+
running += counts[axis * BIN_COUNT + bin];
|
|
236
300
|
|
|
237
301
|
if (!running || !rightCount[bin + 1]) continue;
|
|
238
302
|
|
|
239
303
|
const cost = TRAVERSAL_COST
|
|
240
|
-
+ (
|
|
304
|
+
+ (area(minX, minY, minZ, maxX, maxY, maxZ) * running + rightArea[bin + 1] * rightCount[bin + 1]) / parentArea;
|
|
241
305
|
|
|
242
306
|
if (cost < bestCost) {
|
|
243
307
|
bestCost = cost;
|
|
244
308
|
bestAxis = axis;
|
|
245
309
|
bestBin = bin;
|
|
246
|
-
bestScale = scale;
|
|
247
310
|
}
|
|
248
311
|
}
|
|
249
312
|
}
|
|
250
313
|
|
|
251
314
|
if (bestAxis < 0) return -1;
|
|
252
315
|
|
|
253
|
-
const pivot =
|
|
254
|
-
|
|
316
|
+
const pivot = centroidMin[bestAxis];
|
|
317
|
+
const bestScale = scale[bestAxis];
|
|
318
|
+
let left = start;
|
|
255
319
|
|
|
256
|
-
for (let i =
|
|
320
|
+
for (let i = start; i < end; i++) {
|
|
257
321
|
if (binOf(centroid[order[i] * 3 + bestAxis], pivot, bestScale) <= bestBin) {
|
|
258
322
|
const swap = order[i];
|
|
259
323
|
|
|
@@ -264,7 +328,7 @@ function partition(order: Uint32Array, centroid: Float64Array, min: Float32Array
|
|
|
264
328
|
}
|
|
265
329
|
|
|
266
330
|
// The sweep only proposes splits both sides occupy, so this is a guard against rounding, not an expected outcome.
|
|
267
|
-
return left ===
|
|
331
|
+
return left === start || left === end ? -1 : left;
|
|
268
332
|
}
|
|
269
333
|
|
|
270
334
|
function binOf(value: number, origin: number, scale: number): number {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { buildBvh, type PackedBvh } from "./bvh.js";
|
|
2
|
+
|
|
3
|
+
/** What the page sends the worker: its own copy of the positions, transferred. */
|
|
4
|
+
export interface BvhRequest {
|
|
5
|
+
id: number;
|
|
6
|
+
positions: Float32Array;
|
|
7
|
+
triangleCount: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The hierarchy back, its buffers transferred, or why it could not be built. */
|
|
11
|
+
export type BvhResponse =
|
|
12
|
+
| { id: number; nodes: ArrayBuffer; nodeCount: number; order: Uint32Array; maxDepth: number }
|
|
13
|
+
| { id: number; error: string };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What the renderer needs from a worker: the shape of a Web `Worker`. Any transport that carries structured clones and
|
|
17
|
+
* transfer lists fits, such as a `worker_threads` thread behind an adapter.
|
|
18
|
+
*/
|
|
19
|
+
export interface BvhWorker {
|
|
20
|
+
postMessage(message: BvhRequest, transfer?: Transferable[]): void;
|
|
21
|
+
terminate(): void;
|
|
22
|
+
onmessage: ((event: { data: BvhResponse }) => void) | null;
|
|
23
|
+
onerror: ((event: { message?: string; preventDefault?(): void }) => void) | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type BvhWorkerFactory = () => BvhWorker;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Below this many triangles the hierarchy is built on the page: it takes a few milliseconds, less than copying the
|
|
30
|
+
* positions to a worker and the hierarchy back.
|
|
31
|
+
*/
|
|
32
|
+
export const BVH_WORKER_MIN_TRIANGLES = 50_000;
|
|
33
|
+
|
|
34
|
+
/** The worker this package ships, next to this module. Replace it when the bundler cannot follow the URL. */
|
|
35
|
+
export function createDefaultBvhWorker(): BvhWorker {
|
|
36
|
+
if (typeof Worker === "undefined")
|
|
37
|
+
throw new Error("Web Workers are unavailable here: pass `createBvhWorker`, or `bvhWorker: false`.");
|
|
38
|
+
|
|
39
|
+
return new Worker(new URL("./bvhWorker.js", import.meta.url), { type: "module" }) as unknown as BvhWorker;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The worker's side of the exchange, apart from the worker itself so it can be run anywhere. */
|
|
43
|
+
export function respondToBvhRequest(request: BvhRequest): { response: BvhResponse; transfer: Transferable[] } {
|
|
44
|
+
try {
|
|
45
|
+
const bvh = buildBvh(request.positions, request.triangleCount);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
response: { id: request.id, ...bvh },
|
|
49
|
+
transfer: [bvh.nodes, bvh.order.buffer],
|
|
50
|
+
};
|
|
51
|
+
} catch(error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
|
|
54
|
+
return { response: { id: request.id, error: message }, transfer: [] };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface Pending {
|
|
59
|
+
positions: Float32Array;
|
|
60
|
+
triangleCount: number;
|
|
61
|
+
resolve: (bvh: PackedBvh) => void;
|
|
62
|
+
reject: (error: Error) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds hierarchies in a worker, so a large scene does not hold the page while it is split. The worker is started on
|
|
67
|
+
* the first build that needs it and kept for the next ones. If it cannot be started or fails to load, every build from
|
|
68
|
+
* then on happens on the page, with the same result: the worker only moves the work, it never changes it.
|
|
69
|
+
*/
|
|
70
|
+
export class BvhBuilder {
|
|
71
|
+
private worker: BvhWorker | null = null;
|
|
72
|
+
private failed = false;
|
|
73
|
+
private disposed = false;
|
|
74
|
+
private next = 0;
|
|
75
|
+
private readonly pending = new Map<number, Pending>();
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
private readonly enabled: boolean,
|
|
79
|
+
private readonly createWorker: BvhWorkerFactory = createDefaultBvhWorker,
|
|
80
|
+
private readonly minTriangles = BVH_WORKER_MIN_TRIANGLES) {}
|
|
81
|
+
|
|
82
|
+
/** Whether builds go to the worker: it is enabled and has not failed. */
|
|
83
|
+
get usesWorker(): boolean {
|
|
84
|
+
return this.enabled && !this.failed && !this.disposed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
build(positions: Float32Array, triangleCount: number): Promise<PackedBvh> {
|
|
88
|
+
if (this.disposed) return Promise.reject(abortError());
|
|
89
|
+
|
|
90
|
+
const worker = this.usesWorker && triangleCount >= this.minTriangles ? this.connect() : null;
|
|
91
|
+
|
|
92
|
+
if (!worker) return Promise.resolve(buildBvh(positions, triangleCount));
|
|
93
|
+
|
|
94
|
+
const id = this.next++;
|
|
95
|
+
// The page still uploads the positions, so the worker gets a copy it can own.
|
|
96
|
+
const copy = positions.slice(0, triangleCount * 9);
|
|
97
|
+
|
|
98
|
+
return new Promise<PackedBvh>((resolve, reject) => {
|
|
99
|
+
this.pending.set(id, { positions, triangleCount, resolve, reject });
|
|
100
|
+
worker.postMessage({ id, positions: copy, triangleCount }, [copy.buffer]);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Stops the worker. Builds still waiting for it reject with an `AbortError`. */
|
|
105
|
+
dispose() {
|
|
106
|
+
this.disposed = true;
|
|
107
|
+
this.worker?.terminate();
|
|
108
|
+
this.worker = null;
|
|
109
|
+
|
|
110
|
+
for (const pending of this.pending.values()) pending.reject(abortError());
|
|
111
|
+
|
|
112
|
+
this.pending.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private connect(): BvhWorker | null {
|
|
116
|
+
if (this.worker) return this.worker;
|
|
117
|
+
|
|
118
|
+
let worker: BvhWorker;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
worker = this.createWorker();
|
|
122
|
+
} catch(error) {
|
|
123
|
+
this.fail(error instanceof Error ? error.message : String(error));
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
worker.onmessage = event => this.receive(event.data);
|
|
129
|
+
|
|
130
|
+
worker.onerror = event => {
|
|
131
|
+
event.preventDefault?.();
|
|
132
|
+
this.fail(event.message || "the worker failed to load");
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.worker = worker;
|
|
136
|
+
|
|
137
|
+
return worker;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private receive(response: BvhResponse) {
|
|
141
|
+
const pending = this.pending.get(response.id);
|
|
142
|
+
|
|
143
|
+
if (!pending) return;
|
|
144
|
+
|
|
145
|
+
this.pending.delete(response.id);
|
|
146
|
+
|
|
147
|
+
if ("error" in response) pending.reject(new Error(`GPU renderer: building the BVH failed: ${response.error}`));
|
|
148
|
+
else pending.resolve({
|
|
149
|
+
nodes: response.nodes, nodeCount: response.nodeCount, order: response.order, maxDepth: response.maxDepth,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The worker is gone for good: what it was building, and everything after, is built on the page instead. */
|
|
154
|
+
private fail(reason: string) {
|
|
155
|
+
if (!this.failed)
|
|
156
|
+
console.warn(`GPU renderer: the BVH worker is unavailable (${reason}); building on the main thread instead.`);
|
|
157
|
+
|
|
158
|
+
this.failed = true;
|
|
159
|
+
this.worker?.terminate();
|
|
160
|
+
this.worker = null;
|
|
161
|
+
|
|
162
|
+
const waiting = [...this.pending.values()];
|
|
163
|
+
|
|
164
|
+
this.pending.clear();
|
|
165
|
+
|
|
166
|
+
for (const pending of waiting) {
|
|
167
|
+
try {
|
|
168
|
+
pending.resolve(buildBvh(pending.positions, pending.triangleCount));
|
|
169
|
+
} catch(error) {
|
|
170
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function abortError(): Error {
|
|
177
|
+
return Object.assign(new Error("The renderer was disposed."), { name: "AbortError" });
|
|
178
|
+
}
|
package/src/bvhWorker.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The worker `createDefaultBvhWorker` starts: it builds one hierarchy per message, off the page's thread.
|
|
2
|
+
import { respondToBvhRequest, type BvhRequest, type BvhResponse } from "./bvhBuilder.js";
|
|
3
|
+
|
|
4
|
+
const scope = self as unknown as {
|
|
5
|
+
onmessage: ((event: MessageEvent<BvhRequest>) => void) | null;
|
|
6
|
+
postMessage(message: BvhResponse, transfer: Transferable[]): void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
scope.onmessage = event => {
|
|
10
|
+
const { response, transfer } = respondToBvhRequest(event.data);
|
|
11
|
+
|
|
12
|
+
scope.postMessage(response, transfer);
|
|
13
|
+
};
|