@multiplekex/shallot 0.1.12 → 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.
Files changed (62) hide show
  1. package/package.json +3 -4
  2. package/src/core/builder.ts +71 -32
  3. package/src/core/component.ts +25 -11
  4. package/src/core/index.ts +14 -13
  5. package/src/core/math.ts +135 -0
  6. package/src/core/runtime.ts +0 -1
  7. package/src/core/state.ts +9 -68
  8. package/src/core/xml.ts +381 -265
  9. package/src/editor/format.ts +5 -0
  10. package/src/editor/index.ts +101 -0
  11. package/src/extras/arrows/index.ts +28 -69
  12. package/src/extras/gradient/index.ts +36 -52
  13. package/src/extras/lines/index.ts +51 -122
  14. package/src/extras/orbit/index.ts +40 -15
  15. package/src/extras/text/font.ts +546 -0
  16. package/src/extras/text/index.ts +158 -204
  17. package/src/extras/text/sdf.ts +429 -0
  18. package/src/standard/activity/index.ts +172 -0
  19. package/src/standard/compute/graph.ts +23 -23
  20. package/src/standard/compute/index.ts +76 -61
  21. package/src/standard/defaults.ts +8 -5
  22. package/src/standard/index.ts +1 -0
  23. package/src/standard/input/index.ts +30 -19
  24. package/src/standard/loading/index.ts +18 -13
  25. package/src/standard/render/bvh/blas.ts +752 -0
  26. package/src/standard/render/bvh/radix.ts +476 -0
  27. package/src/standard/render/bvh/structs.ts +167 -0
  28. package/src/standard/render/bvh/tlas.ts +886 -0
  29. package/src/standard/render/bvh/traverse.ts +467 -0
  30. package/src/standard/render/camera.ts +302 -27
  31. package/src/standard/render/data.ts +93 -0
  32. package/src/standard/render/depth.ts +117 -0
  33. package/src/standard/render/forward/index.ts +259 -0
  34. package/src/standard/render/forward/raster.ts +228 -0
  35. package/src/standard/render/index.ts +443 -70
  36. package/src/standard/render/indirect.ts +40 -0
  37. package/src/standard/render/instance.ts +214 -0
  38. package/src/standard/render/intersection.ts +72 -0
  39. package/src/standard/render/light.ts +16 -16
  40. package/src/standard/render/mesh/index.ts +67 -75
  41. package/src/standard/render/mesh/unified.ts +96 -0
  42. package/src/standard/render/{transparent.ts → overlay.ts} +14 -15
  43. package/src/standard/render/pass.ts +10 -4
  44. package/src/standard/render/postprocess.ts +142 -64
  45. package/src/standard/render/ray.ts +61 -0
  46. package/src/standard/render/scene.ts +38 -164
  47. package/src/standard/render/shaders.ts +484 -0
  48. package/src/standard/render/surface/compile.ts +3 -10
  49. package/src/standard/render/surface/index.ts +60 -30
  50. package/src/standard/render/surface/noise.ts +45 -0
  51. package/src/standard/render/surface/structs.ts +60 -19
  52. package/src/standard/render/surface/wgsl.ts +573 -0
  53. package/src/standard/render/triangle.ts +84 -0
  54. package/src/standard/transforms/index.ts +4 -6
  55. package/src/standard/tween/index.ts +10 -1
  56. package/src/standard/tween/sequence.ts +24 -16
  57. package/src/standard/tween/tween.ts +67 -16
  58. package/src/core/types.ts +0 -37
  59. package/src/standard/compute/inspect.ts +0 -201
  60. package/src/standard/compute/pass.ts +0 -23
  61. package/src/standard/compute/timing.ts +0 -139
  62. package/src/standard/render/forward.ts +0 -273
@@ -0,0 +1,752 @@
1
+ import type { Vec3, BVHNode, MortonPair, AABB } from "./structs";
2
+ import { LEAF_FLAG, isLeaf, leafIndex } from "./structs";
3
+ import type { MeshData } from "../mesh";
4
+
5
+ export interface BLASTriangle {
6
+ v0: Vec3;
7
+ e1: Vec3;
8
+ e2: Vec3;
9
+ n0: Vec3;
10
+ n1: Vec3;
11
+ n2: Vec3;
12
+ }
13
+
14
+ export interface BLASData {
15
+ nodes: BVHNode[];
16
+ sortedTriIds: number[];
17
+ aabbMin: Vec3;
18
+ aabbMax: Vec3;
19
+ triCount: number;
20
+ }
21
+
22
+ export interface BLASMeta {
23
+ nodeOffset: number;
24
+ triIdOffset: number;
25
+ nodeCount: number;
26
+ triCount: number;
27
+ }
28
+
29
+ export interface BLASAtlas {
30
+ blasData: Map<number, BLASData>;
31
+ nodesBuffer: GPUBuffer;
32
+ triIdsBuffer: GPUBuffer;
33
+ metaBuffer: GPUBuffer;
34
+ trianglesBuffer: GPUBuffer;
35
+ triangles: Map<number, BLASTriangle[]>;
36
+ shapeAABBs: GPUBuffer;
37
+ }
38
+
39
+ function vec3(x: number, y: number, z: number): Vec3 {
40
+ return { x, y, z };
41
+ }
42
+
43
+ function vec3Min(a: Vec3, b: Vec3): Vec3 {
44
+ return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), z: Math.min(a.z, b.z) };
45
+ }
46
+
47
+ function vec3Max(a: Vec3, b: Vec3): Vec3 {
48
+ return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y), z: Math.max(a.z, b.z) };
49
+ }
50
+
51
+ function vec3Add(a: Vec3, b: Vec3): Vec3 {
52
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
53
+ }
54
+
55
+ function vec3Sub(a: Vec3, b: Vec3): Vec3 {
56
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
57
+ }
58
+
59
+ function vec3Scale(v: Vec3, s: number): Vec3 {
60
+ return { x: v.x * s, y: v.y * s, z: v.z * s };
61
+ }
62
+
63
+ function octEncode(n: Vec3): number {
64
+ const absSum = Math.abs(n.x) + Math.abs(n.y) + Math.abs(n.z);
65
+ let vx = n.x / absSum;
66
+ let vy = n.y / absSum;
67
+ const vz = n.z / absSum;
68
+ if (vz < 0) {
69
+ const signX = vx >= 0 ? 1 : -1;
70
+ const signY = vy >= 0 ? 1 : -1;
71
+ const newVx = (1 - Math.abs(vy)) * signX;
72
+ const newVy = (1 - Math.abs(vx)) * signY;
73
+ vx = newVx;
74
+ vy = newVy;
75
+ }
76
+ const x = Math.floor(Math.max(0, Math.min(65535, (vx * 0.5 + 0.5) * 65535)));
77
+ const y = Math.floor(Math.max(0, Math.min(65535, (vy * 0.5 + 0.5) * 65535)));
78
+ return ((y << 16) | x) >>> 0;
79
+ }
80
+
81
+ export function extractShapeTriangles(mesh: MeshData): BLASTriangle[] {
82
+ const triangles: BLASTriangle[] = [];
83
+ const { vertices, indices, indexCount } = mesh;
84
+ const stride = 6;
85
+
86
+ for (let i = 0; i < indexCount; i += 3) {
87
+ const i0 = indices[i];
88
+ const i1 = indices[i + 1];
89
+ const i2 = indices[i + 2];
90
+
91
+ const v0 = vec3(
92
+ vertices[i0 * stride],
93
+ vertices[i0 * stride + 1],
94
+ vertices[i0 * stride + 2]
95
+ );
96
+ const v1 = vec3(
97
+ vertices[i1 * stride],
98
+ vertices[i1 * stride + 1],
99
+ vertices[i1 * stride + 2]
100
+ );
101
+ const v2 = vec3(
102
+ vertices[i2 * stride],
103
+ vertices[i2 * stride + 1],
104
+ vertices[i2 * stride + 2]
105
+ );
106
+
107
+ const n0 = vec3(
108
+ vertices[i0 * stride + 3],
109
+ vertices[i0 * stride + 4],
110
+ vertices[i0 * stride + 5]
111
+ );
112
+ const n1 = vec3(
113
+ vertices[i1 * stride + 3],
114
+ vertices[i1 * stride + 4],
115
+ vertices[i1 * stride + 5]
116
+ );
117
+ const n2 = vec3(
118
+ vertices[i2 * stride + 3],
119
+ vertices[i2 * stride + 4],
120
+ vertices[i2 * stride + 5]
121
+ );
122
+
123
+ triangles.push({
124
+ v0,
125
+ e1: vec3Sub(v1, v0),
126
+ e2: vec3Sub(v2, v0),
127
+ n0,
128
+ n1,
129
+ n2,
130
+ });
131
+ }
132
+
133
+ return triangles;
134
+ }
135
+
136
+ function computeBounds(triangles: BLASTriangle[]): AABB {
137
+ if (triangles.length === 0) {
138
+ return { min: vec3(0, 0, 0), max: vec3(0, 0, 0) };
139
+ }
140
+
141
+ let min = vec3(Infinity, Infinity, Infinity);
142
+ let max = vec3(-Infinity, -Infinity, -Infinity);
143
+
144
+ for (const tri of triangles) {
145
+ const v0 = tri.v0;
146
+ const v1 = vec3Add(v0, tri.e1);
147
+ const v2 = vec3Add(v0, tri.e2);
148
+ min = vec3Min(min, v0);
149
+ min = vec3Min(min, v1);
150
+ min = vec3Min(min, v2);
151
+ max = vec3Max(max, v0);
152
+ max = vec3Max(max, v1);
153
+ max = vec3Max(max, v2);
154
+ }
155
+
156
+ return { min, max };
157
+ }
158
+
159
+ function expandBits(v: number): number {
160
+ let x = v & 0x3ff;
161
+ x = (x | (x << 16)) & 0x030000ff;
162
+ x = (x | (x << 8)) & 0x0300f00f;
163
+ x = (x | (x << 4)) & 0x030c30c3;
164
+ x = (x | (x << 2)) & 0x09249249;
165
+ return x >>> 0;
166
+ }
167
+
168
+ function mortonCode3D(x: number, y: number, z: number): number {
169
+ return ((expandBits(x) << 2) | (expandBits(y) << 1) | expandBits(z)) >>> 0;
170
+ }
171
+
172
+ function computeMortonCode(centroid: Vec3, bounds: AABB): number {
173
+ const size = {
174
+ x: bounds.max.x - bounds.min.x,
175
+ y: bounds.max.y - bounds.min.y,
176
+ z: bounds.max.z - bounds.min.z,
177
+ };
178
+
179
+ const safeSize = {
180
+ x: Math.max(size.x, 1e-6),
181
+ y: Math.max(size.y, 1e-6),
182
+ z: Math.max(size.z, 1e-6),
183
+ };
184
+
185
+ const normalized = {
186
+ x: (centroid.x - bounds.min.x) / safeSize.x,
187
+ y: (centroid.y - bounds.min.y) / safeSize.y,
188
+ z: (centroid.z - bounds.min.z) / safeSize.z,
189
+ };
190
+
191
+ const clamped = {
192
+ x: Math.max(0, Math.min(1, normalized.x)),
193
+ y: Math.max(0, Math.min(1, normalized.y)),
194
+ z: Math.max(0, Math.min(1, normalized.z)),
195
+ };
196
+
197
+ const quantized = {
198
+ x: Math.floor(clamped.x * 1023),
199
+ y: Math.floor(clamped.y * 1023),
200
+ z: Math.floor(clamped.z * 1023),
201
+ };
202
+
203
+ return mortonCode3D(quantized.x, quantized.y, quantized.z);
204
+ }
205
+
206
+ function buildMortonPairs(triangles: BLASTriangle[], bounds: AABB): MortonPair[] {
207
+ return triangles.map((tri, i) => {
208
+ const centroid = vec3Add(tri.v0, vec3Scale(vec3Add(tri.e1, tri.e2), 1 / 3));
209
+ return {
210
+ code: computeMortonCode(centroid, bounds),
211
+ triangleId: i,
212
+ };
213
+ });
214
+ }
215
+
216
+ function radixSort(pairs: MortonPair[]): MortonPair[] {
217
+ const n = pairs.length;
218
+ if (n === 0) return [];
219
+
220
+ let input = [...pairs];
221
+ let output = new Array<MortonPair>(n);
222
+
223
+ for (let pass = 0; pass < 4; pass++) {
224
+ const bitOffset = pass * 8;
225
+ const histogram = new Array<number>(256).fill(0);
226
+
227
+ for (const pair of input) {
228
+ const digit = (pair.code >>> bitOffset) & 0xff;
229
+ histogram[digit]++;
230
+ }
231
+
232
+ let sum = 0;
233
+ for (let i = 0; i < 256; i++) {
234
+ const count = histogram[i];
235
+ histogram[i] = sum;
236
+ sum += count;
237
+ }
238
+
239
+ for (const pair of input) {
240
+ const digit = (pair.code >>> bitOffset) & 0xff;
241
+ output[histogram[digit]] = pair;
242
+ histogram[digit]++;
243
+ }
244
+
245
+ [input, output] = [output, input];
246
+ }
247
+
248
+ return input;
249
+ }
250
+
251
+ function clz32(x: number): number {
252
+ if (x === 0) return 32;
253
+ return Math.clz32(x >>> 0);
254
+ }
255
+
256
+ function delta(sortedPairs: MortonPair[], i: number, j: number): number {
257
+ const n = sortedPairs.length;
258
+ if (j < 0 || j >= n) {
259
+ return -1;
260
+ }
261
+ const codeI = sortedPairs[i].code >>> 0;
262
+ const codeJ = sortedPairs[j].code >>> 0;
263
+ if (codeI === codeJ) {
264
+ return clz32((i ^ j) >>> 0) + 32;
265
+ }
266
+ return clz32((codeI ^ codeJ) >>> 0);
267
+ }
268
+
269
+ function determineRange(sortedPairs: MortonPair[], i: number): [number, number] {
270
+ const n = sortedPairs.length;
271
+
272
+ if (i === 0) {
273
+ return [0, n - 1];
274
+ }
275
+
276
+ const deltaLeft = delta(sortedPairs, i, i - 1);
277
+ const deltaRight = delta(sortedPairs, i, i + 1);
278
+ const d = deltaRight > deltaLeft ? 1 : -1;
279
+
280
+ const deltaMin = Math.min(deltaLeft, deltaRight);
281
+
282
+ let lmax = 2;
283
+ while (delta(sortedPairs, i, i + lmax * d) > deltaMin) {
284
+ lmax *= 2;
285
+ }
286
+
287
+ let l = 0;
288
+ let t = Math.floor(lmax / 2);
289
+ while (t >= 1) {
290
+ if (delta(sortedPairs, i, i + (l + t) * d) > deltaMin) {
291
+ l += t;
292
+ }
293
+ t = Math.floor(t / 2);
294
+ }
295
+
296
+ const j = i + l * d;
297
+ const first = Math.min(i, j);
298
+ const last = Math.max(i, j);
299
+
300
+ return [first, last];
301
+ }
302
+
303
+ function findSplit(sortedPairs: MortonPair[], first: number, last: number): number {
304
+ const firstCode = sortedPairs[first].code >>> 0;
305
+ const lastCode = sortedPairs[last].code >>> 0;
306
+
307
+ if (firstCode === lastCode) {
308
+ return Math.floor((first + last) / 2);
309
+ }
310
+
311
+ const deltaNode = clz32((firstCode ^ lastCode) >>> 0);
312
+
313
+ let split = first;
314
+ let stride = last - first;
315
+
316
+ do {
317
+ stride = Math.floor((stride + 1) / 2);
318
+ const middle = split + stride;
319
+
320
+ if (middle < last) {
321
+ const splitCode = sortedPairs[middle].code >>> 0;
322
+ const splitDelta = clz32((firstCode ^ splitCode) >>> 0);
323
+
324
+ if (splitDelta > deltaNode) {
325
+ split = middle;
326
+ }
327
+ }
328
+ } while (stride > 1);
329
+
330
+ return split;
331
+ }
332
+
333
+ function buildKarrasTree(sortedPairs: MortonPair[]): {
334
+ nodes: BVHNode[];
335
+ parents: number[];
336
+ } {
337
+ const n = sortedPairs.length;
338
+
339
+ if (n === 0) {
340
+ return { nodes: [], parents: [] };
341
+ }
342
+
343
+ if (n === 1) {
344
+ return { nodes: [], parents: [-1] };
345
+ }
346
+
347
+ const numInternal = n - 1;
348
+ const nodes: BVHNode[] = new Array(numInternal);
349
+ const parents: number[] = new Array(2 * n).fill(-1);
350
+
351
+ for (let i = 0; i < numInternal; i++) {
352
+ const [first, last] = determineRange(sortedPairs, i);
353
+ const gamma = findSplit(sortedPairs, first, last);
354
+
355
+ const leftIsLeaf = Math.min(first, last) === gamma;
356
+ const rightIsLeaf = Math.max(first, last) === gamma + 1;
357
+
358
+ let leftChild: number;
359
+ let rightChild: number;
360
+
361
+ if (leftIsLeaf) {
362
+ leftChild = (gamma | LEAF_FLAG) >>> 0;
363
+ parents[gamma] = i;
364
+ } else {
365
+ leftChild = gamma;
366
+ parents[n + gamma] = i;
367
+ }
368
+
369
+ if (rightIsLeaf) {
370
+ rightChild = ((gamma + 1) | LEAF_FLAG) >>> 0;
371
+ parents[gamma + 1] = i;
372
+ } else {
373
+ rightChild = gamma + 1;
374
+ parents[n + (gamma + 1)] = i;
375
+ }
376
+
377
+ nodes[i] = {
378
+ min: vec3(1e30, 1e30, 1e30),
379
+ max: vec3(-1e30, -1e30, -1e30),
380
+ leftChild,
381
+ rightChild,
382
+ };
383
+ }
384
+
385
+ return { nodes, parents };
386
+ }
387
+
388
+ function getTriangleBounds(tri: BLASTriangle): AABB {
389
+ const v0 = tri.v0;
390
+ const v1 = vec3Add(v0, tri.e1);
391
+ const v2 = vec3Add(v0, tri.e2);
392
+ const min = vec3Min(vec3Min(v0, v1), v2);
393
+ const max = vec3Max(vec3Max(v0, v1), v2);
394
+ return { min, max };
395
+ }
396
+
397
+ function propagateBounds(
398
+ nodes: BVHNode[],
399
+ triangles: BLASTriangle[],
400
+ pairs: MortonPair[],
401
+ parents: number[]
402
+ ): void {
403
+ const n = triangles.length;
404
+ if (n <= 1) return;
405
+
406
+ const boundsFlags = new Array(n - 1).fill(0);
407
+
408
+ for (let leafIdx = 0; leafIdx < n; leafIdx++) {
409
+ const tri = triangles[pairs[leafIdx].triangleId];
410
+
411
+ let current = leafIdx;
412
+ let isLeafNode = true;
413
+
414
+ for (let iter = 0; iter < 64; iter++) {
415
+ const parent = isLeafNode ? parents[current] : parents[n + current];
416
+
417
+ if (parent === -1 || parent === undefined) {
418
+ break;
419
+ }
420
+
421
+ const oldFlag = boundsFlags[parent];
422
+ boundsFlags[parent]++;
423
+
424
+ if (oldFlag === 0) {
425
+ break;
426
+ }
427
+
428
+ const node = nodes[parent];
429
+ const left = node.leftChild;
430
+ const right = node.rightChild;
431
+
432
+ let leftBounds: AABB;
433
+ let rightBounds: AABB;
434
+
435
+ if (isLeaf(left)) {
436
+ const leftTri = triangles[pairs[leafIndex(left)].triangleId];
437
+ leftBounds = getTriangleBounds(leftTri);
438
+ } else {
439
+ leftBounds = { min: nodes[left].min, max: nodes[left].max };
440
+ }
441
+
442
+ if (isLeaf(right)) {
443
+ const rightTri = triangles[pairs[leafIndex(right)].triangleId];
444
+ rightBounds = getTriangleBounds(rightTri);
445
+ } else {
446
+ rightBounds = { min: nodes[right].min, max: nodes[right].max };
447
+ }
448
+
449
+ nodes[parent].min = vec3Min(leftBounds.min, rightBounds.min);
450
+ nodes[parent].max = vec3Max(leftBounds.max, rightBounds.max);
451
+
452
+ current = parent;
453
+ isLeafNode = false;
454
+
455
+ if (parent === 0) {
456
+ break;
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ export function buildShapeBLAS(triangles: BLASTriangle[]): BLASData {
463
+ const triCount = triangles.length;
464
+
465
+ if (triCount === 0) {
466
+ return {
467
+ nodes: [],
468
+ sortedTriIds: [],
469
+ aabbMin: vec3(0, 0, 0),
470
+ aabbMax: vec3(0, 0, 0),
471
+ triCount: 0,
472
+ };
473
+ }
474
+
475
+ const bounds = computeBounds(triangles);
476
+
477
+ if (triCount === 1) {
478
+ return {
479
+ nodes: [],
480
+ sortedTriIds: [0],
481
+ aabbMin: bounds.min,
482
+ aabbMax: bounds.max,
483
+ triCount: 1,
484
+ };
485
+ }
486
+
487
+ const pairs = buildMortonPairs(triangles, bounds);
488
+ const sortedPairs = radixSort(pairs);
489
+ const { nodes, parents } = buildKarrasTree(sortedPairs);
490
+
491
+ propagateBounds(nodes, triangles, sortedPairs, parents);
492
+
493
+ const sortedTriIds = sortedPairs.map((p) => p.triangleId);
494
+
495
+ const rootBounds = nodes.length > 0 ? { min: nodes[0].min, max: nodes[0].max } : bounds;
496
+
497
+ return {
498
+ nodes,
499
+ sortedTriIds,
500
+ aabbMin: rootBounds.min,
501
+ aabbMax: rootBounds.max,
502
+ triCount,
503
+ };
504
+ }
505
+
506
+ export function validateBLASBounds(blas: BLASData, triangles: BLASTriangle[]): boolean {
507
+ if (blas.triCount === 0) return true;
508
+
509
+ const epsilon = 1e-5;
510
+
511
+ for (const tri of triangles) {
512
+ const v0 = tri.v0;
513
+ const v1 = vec3Add(v0, tri.e1);
514
+ const v2 = vec3Add(v0, tri.e2);
515
+
516
+ for (const v of [v0, v1, v2]) {
517
+ if (
518
+ v.x < blas.aabbMin.x - epsilon ||
519
+ v.y < blas.aabbMin.y - epsilon ||
520
+ v.z < blas.aabbMin.z - epsilon ||
521
+ v.x > blas.aabbMax.x + epsilon ||
522
+ v.y > blas.aabbMax.y + epsilon ||
523
+ v.z > blas.aabbMax.z + epsilon
524
+ ) {
525
+ return false;
526
+ }
527
+ }
528
+ }
529
+
530
+ return true;
531
+ }
532
+
533
+ export function validateBLASNodes(blas: BLASData): { valid: boolean; errors: string[] } {
534
+ const errors: string[] = [];
535
+
536
+ if (blas.triCount <= 1) {
537
+ return { valid: true, errors: [] };
538
+ }
539
+
540
+ const expectedNodes = blas.triCount - 1;
541
+ if (blas.nodes.length !== expectedNodes) {
542
+ errors.push(`Expected ${expectedNodes} nodes, got ${blas.nodes.length}`);
543
+ }
544
+
545
+ for (let i = 0; i < blas.nodes.length; i++) {
546
+ const node = blas.nodes[i];
547
+
548
+ if (node.min.x > node.max.x || node.min.y > node.max.y || node.min.z > node.max.z) {
549
+ errors.push(`Node ${i} has invalid bounds: min > max`);
550
+ }
551
+
552
+ const left = node.leftChild;
553
+ const right = node.rightChild;
554
+
555
+ if (isLeaf(left)) {
556
+ const idx = leafIndex(left);
557
+ if (idx >= blas.triCount) {
558
+ errors.push(`Node ${i} left child leaf index ${idx} >= triCount ${blas.triCount}`);
559
+ }
560
+ } else {
561
+ if (left >= blas.nodes.length) {
562
+ errors.push(`Node ${i} left child ${left} >= node count ${blas.nodes.length}`);
563
+ }
564
+ }
565
+
566
+ if (isLeaf(right)) {
567
+ const idx = leafIndex(right);
568
+ if (idx >= blas.triCount) {
569
+ errors.push(`Node ${i} right child leaf index ${idx} >= triCount ${blas.triCount}`);
570
+ }
571
+ } else {
572
+ if (right >= blas.nodes.length) {
573
+ errors.push(`Node ${i} right child ${right} >= node count ${blas.nodes.length}`);
574
+ }
575
+ }
576
+ }
577
+
578
+ return { valid: errors.length === 0, errors };
579
+ }
580
+
581
+ const MAX_SHAPES = 16;
582
+ const TREE_NODE_SIZE = 32;
583
+ const BLAS_TRIANGLE_SIZE = 64;
584
+
585
+ export interface BLASMetaExtended {
586
+ nodeOffset: number;
587
+ triIdOffset: number;
588
+ triOffset: number;
589
+ triCount: number;
590
+ }
591
+
592
+ export function createBLASAtlas(
593
+ device: GPUDevice,
594
+ getMesh: (id: number) => MeshData | undefined
595
+ ): BLASAtlas {
596
+ const blasData = new Map<number, BLASData>();
597
+ const trianglesMap = new Map<number, BLASTriangle[]>();
598
+ const metas: BLASMetaExtended[] = [];
599
+
600
+ let totalNodes = 0;
601
+ let totalTriIds = 0;
602
+ let totalTris = 0;
603
+
604
+ for (let shapeId = 0; shapeId < MAX_SHAPES; shapeId++) {
605
+ const mesh = getMesh(shapeId);
606
+ if (!mesh || mesh.indexCount === 0) {
607
+ metas.push({ nodeOffset: 0, triIdOffset: 0, triOffset: 0, triCount: 0 });
608
+ continue;
609
+ }
610
+
611
+ const triangles = extractShapeTriangles(mesh);
612
+ const blas = buildShapeBLAS(triangles);
613
+ blasData.set(shapeId, blas);
614
+ trianglesMap.set(shapeId, triangles);
615
+
616
+ metas.push({
617
+ nodeOffset: totalNodes,
618
+ triIdOffset: totalTriIds,
619
+ triOffset: totalTris,
620
+ triCount: blas.triCount,
621
+ });
622
+
623
+ totalNodes += blas.nodes.length;
624
+ totalTriIds += blas.sortedTriIds.length;
625
+ totalTris += triangles.length;
626
+ }
627
+
628
+ const nodesData = new Float32Array(Math.max(totalNodes * 8, 8));
629
+ const triIdsData = new Uint32Array(Math.max(totalTriIds, 1));
630
+ const metaData = new Uint32Array(MAX_SHAPES * 4);
631
+ const trianglesData = new Uint32Array(Math.max(totalTris * 16, 16));
632
+
633
+ let nodeOffset = 0;
634
+ let triIdOffset = 0;
635
+ let triOffset = 0;
636
+
637
+ for (let shapeId = 0; shapeId < MAX_SHAPES; shapeId++) {
638
+ const blas = blasData.get(shapeId);
639
+ const triangles = trianglesMap.get(shapeId);
640
+ if (!blas || !triangles) continue;
641
+
642
+ for (const node of blas.nodes) {
643
+ nodesData[nodeOffset * 8 + 0] = node.min.x;
644
+ nodesData[nodeOffset * 8 + 1] = node.min.y;
645
+ nodesData[nodeOffset * 8 + 2] = node.min.z;
646
+ nodesData[nodeOffset * 8 + 3] = new Float32Array(
647
+ new Uint32Array([node.leftChild]).buffer
648
+ )[0];
649
+ nodesData[nodeOffset * 8 + 4] = node.max.x;
650
+ nodesData[nodeOffset * 8 + 5] = node.max.y;
651
+ nodesData[nodeOffset * 8 + 6] = node.max.z;
652
+ nodesData[nodeOffset * 8 + 7] = new Float32Array(
653
+ new Uint32Array([node.rightChild]).buffer
654
+ )[0];
655
+ nodeOffset++;
656
+ }
657
+
658
+ for (const triId of blas.sortedTriIds) {
659
+ triIdsData[triIdOffset++] = triId;
660
+ }
661
+
662
+ for (const tri of triangles) {
663
+ const base = triOffset * 16;
664
+ const floatView = new Float32Array(trianglesData.buffer, base * 4, 16);
665
+ floatView[0] = tri.v0.x;
666
+ floatView[1] = tri.v0.y;
667
+ floatView[2] = tri.v0.z;
668
+ trianglesData[base + 3] = 0;
669
+ floatView[4] = tri.e1.x;
670
+ floatView[5] = tri.e1.y;
671
+ floatView[6] = tri.e1.z;
672
+ trianglesData[base + 7] = 0;
673
+ floatView[8] = tri.e2.x;
674
+ floatView[9] = tri.e2.y;
675
+ floatView[10] = tri.e2.z;
676
+ trianglesData[base + 11] = 0;
677
+ trianglesData[base + 12] = octEncode(tri.n0);
678
+ trianglesData[base + 13] = octEncode(tri.n1);
679
+ trianglesData[base + 14] = octEncode(tri.n2);
680
+ trianglesData[base + 15] = 0;
681
+ triOffset++;
682
+ }
683
+ }
684
+
685
+ for (let i = 0; i < metas.length; i++) {
686
+ metaData[i * 4 + 0] = metas[i].nodeOffset;
687
+ metaData[i * 4 + 1] = metas[i].triIdOffset;
688
+ metaData[i * 4 + 2] = metas[i].triOffset;
689
+ metaData[i * 4 + 3] = metas[i].triCount;
690
+ }
691
+
692
+ const nodesBuffer = device.createBuffer({
693
+ label: "blas-nodes",
694
+ size: Math.max(nodesData.byteLength, 32),
695
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
696
+ });
697
+ device.queue.writeBuffer(nodesBuffer, 0, nodesData);
698
+
699
+ const triIdsBuffer = device.createBuffer({
700
+ label: "blas-triIds",
701
+ size: Math.max(triIdsData.byteLength, 4),
702
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
703
+ });
704
+ device.queue.writeBuffer(triIdsBuffer, 0, triIdsData);
705
+
706
+ const metaBuffer = device.createBuffer({
707
+ label: "blas-meta",
708
+ size: metaData.byteLength,
709
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
710
+ });
711
+ device.queue.writeBuffer(metaBuffer, 0, metaData);
712
+
713
+ const trianglesBuffer = device.createBuffer({
714
+ label: "blas-triangles",
715
+ size: Math.max(totalTris * BLAS_TRIANGLE_SIZE, BLAS_TRIANGLE_SIZE),
716
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
717
+ });
718
+ device.queue.writeBuffer(trianglesBuffer, 0, trianglesData);
719
+
720
+ const shapeAABBsData = new Float32Array(MAX_SHAPES * 8);
721
+ for (let shapeId = 0; shapeId < MAX_SHAPES; shapeId++) {
722
+ const blas = blasData.get(shapeId);
723
+ const offset = shapeId * 8;
724
+ if (blas) {
725
+ shapeAABBsData[offset + 0] = blas.aabbMin.x;
726
+ shapeAABBsData[offset + 1] = blas.aabbMin.y;
727
+ shapeAABBsData[offset + 2] = blas.aabbMin.z;
728
+ shapeAABBsData[offset + 3] = 0;
729
+ shapeAABBsData[offset + 4] = blas.aabbMax.x;
730
+ shapeAABBsData[offset + 5] = blas.aabbMax.y;
731
+ shapeAABBsData[offset + 6] = blas.aabbMax.z;
732
+ shapeAABBsData[offset + 7] = 0;
733
+ }
734
+ }
735
+
736
+ const shapeAABBs = device.createBuffer({
737
+ label: "shape-aabbs",
738
+ size: shapeAABBsData.byteLength,
739
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
740
+ });
741
+ device.queue.writeBuffer(shapeAABBs, 0, shapeAABBsData);
742
+
743
+ return {
744
+ blasData,
745
+ nodesBuffer,
746
+ triIdsBuffer,
747
+ metaBuffer,
748
+ trianglesBuffer,
749
+ triangles: trianglesMap,
750
+ shapeAABBs,
751
+ };
752
+ }