@es3d/es3d 0.0.0-stage → 1.0.1

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 (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +85 -2
  3. package/docs/REFERENCE.md +581 -0
  4. package/docs/SPEC.md +417 -0
  5. package/package.json +50 -4
  6. package/scripts/es3d-cli.mjs +377 -0
  7. package/src/cache/geometry-store.js +392 -0
  8. package/src/cache/mesh-bundle.js +123 -0
  9. package/src/cache/sha256.js +73 -0
  10. package/src/format/ast.js +238 -0
  11. package/src/format/colors.js +93 -0
  12. package/src/format/compact-parser.js +171 -0
  13. package/src/format/compact-tokenizer.js +165 -0
  14. package/src/format/errors.js +247 -0
  15. package/src/format/json.js +55 -0
  16. package/src/format/limits.js +14 -0
  17. package/src/format/parse.js +92 -0
  18. package/src/format/schema.js +617 -0
  19. package/src/format/serializer.js +333 -0
  20. package/src/format/validator.js +330 -0
  21. package/src/format/value-error.js +12 -0
  22. package/src/format/value-format.js +314 -0
  23. package/src/format/values.js +459 -0
  24. package/src/format/xml-parser.js +136 -0
  25. package/src/format/xml-tokenizer.js +237 -0
  26. package/src/geometry/contour.js +86 -0
  27. package/src/geometry/extrude.js +175 -0
  28. package/src/geometry/gear.js +85 -0
  29. package/src/geometry/geometry-cache.js +159 -0
  30. package/src/geometry/lod-error.js +178 -0
  31. package/src/geometry/mesh.js +458 -0
  32. package/src/geometry/primitives.js +299 -0
  33. package/src/geometry/registry.js +645 -0
  34. package/src/geometry/revolve.js +136 -0
  35. package/src/geometry/sweep.js +441 -0
  36. package/src/index.js +47 -0
  37. package/src/io/compression.js +67 -0
  38. package/src/io/convert.js +236 -0
  39. package/src/io/download.js +13 -0
  40. package/src/io/external-scene.js +127 -0
  41. package/src/io/file-loader.js +28 -0
  42. package/src/io/gltf.js +630 -0
  43. package/src/io/mesh-import.js +104 -0
  44. package/src/io/obj.js +352 -0
  45. package/src/io/step-brep.js +439 -0
  46. package/src/io/step-exact.js +1149 -0
  47. package/src/io/step.js +1418 -0
  48. package/src/io/stl.js +193 -0
  49. package/src/io/svg-import.js +787 -0
  50. package/src/material/materials.js +154 -0
  51. package/src/math/mat4.js +475 -0
  52. package/src/math/random.js +30 -0
  53. package/src/math/trig.js +65 -0
  54. package/src/math/vec3.js +81 -0
  55. package/src/optimize/optimizer.js +579 -0
  56. package/src/path/path-flatten.js +322 -0
  57. package/src/path/path-parser.js +332 -0
  58. package/src/path/shapes.js +129 -0
  59. package/src/path/triangulate.js +517 -0
  60. package/src/pipeline.js +74 -0
  61. package/src/render/camera.js +168 -0
  62. package/src/render/frame-builder.js +362 -0
  63. package/src/render/geometry-pool.js +149 -0
  64. package/src/render/lighting.js +149 -0
  65. package/src/render/orbit-controls.js +237 -0
  66. package/src/render/renderer.js +258 -0
  67. package/src/render/scroll-timeline.js +104 -0
  68. package/src/render/shaders/glsl.js +285 -0
  69. package/src/render/shaders/wgsl.js +252 -0
  70. package/src/render/stage.js +364 -0
  71. package/src/render/texture-cache.js +40 -0
  72. package/src/render/textures.js +223 -0
  73. package/src/render/webgl2/webgl2-backend.js +586 -0
  74. package/src/render/webgpu/webgpu-backend.js +482 -0
  75. package/src/scene/animation.js +171 -0
  76. package/src/scene/camera-constraints.js +117 -0
  77. package/src/scene/flatten.js +754 -0
  78. package/src/scene/picking.js +161 -0
  79. package/src/scene/scene-graph.js +320 -0
  80. package/src/transform/transform.js +211 -0
  81. package/src/wasm/kernels-binary.js +3 -0
  82. package/src/wasm/kernels.js +385 -0
@@ -0,0 +1,754 @@
1
+ // Flattening: scene graph -> RenderScene (a structure-of-arrays instance table).
2
+ //
3
+ // The traversal evaluates hierarchical transforms, resolves <use> references,
4
+ // expands <repeat>/<scatter>, records <lod> switches, inherits materials and
5
+ // collects lights and cameras in world space. Each drawable instance becomes
6
+ // one row: world matrix, geometry, material, selectable element and LOD switch.
7
+ //
8
+ // Animated subtrees are recorded as "dynamic segments": per frame only those
9
+ // subtrees are re-traversed and their rows overwritten in place.
10
+ //
11
+ // <repeat> and <scatter> are expanded by a WebAssembly kernel where they can
12
+ // be: the subtree below one placement is recorded once (recordTemplate) and
13
+ // the kernel runs it for every placement, with the same operations in the
14
+ // same order as the walker, so both give the same rows bit for bit.
15
+ //
16
+ // The RenderScene is renderer-agnostic; the OBJ and glTF importers produce the
17
+ // same structure, which lets the benchmark render all formats identically.
18
+
19
+ import * as mat4 from '../math/mat4.js';
20
+ import { mulberry32 } from '../math/random.js';
21
+ import { es3dCos, es3dSin } from '../math/trig.js';
22
+ import { localMatrix } from '../transform/transform.js';
23
+ import { sampleAnimation } from './animation.js';
24
+ import { colorToLinear } from '../format/colors.js';
25
+ import { instanceKernelEnabled, instancesWasm, INSTANCE_HEADER as H, INSTANCE_PARAMS as F } from '../wasm/kernels.js';
26
+
27
+ /** Instance flag bits (RenderScene.flags). */
28
+ export const FLAG_MIRRORED = 1;
29
+ export const FLAG_NO_COLLIDE = 2; // under collide="false": the camera may pass through it
30
+
31
+ export class RenderScene {
32
+ constructor(capacity = 256) {
33
+ this.count = 0;
34
+ this.capacity = 0;
35
+ /** GeometryEntry-like objects: {id, kind, lods, doubleSided, bounds, meshes, ...} */
36
+ this.geometries = [];
37
+ this.geomIndex = new Map();
38
+ this.materials = [];
39
+ this.lights = [];
40
+ this.cameras = [];
41
+ this.lodSwitches = [];
42
+ this.environment = null;
43
+ this.controls = null; // <controls> settings, or null for the viewer's defaults
44
+ this.animation = { time: false, scroll: false }; // clocks the animations use
45
+ this.dynamic = [];
46
+ this.bounds = { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] };
47
+ this.version = 0; // bumps whenever instance rows change
48
+ this.templates = new Map(); // repeat or scatter node -> its recorded subtree (Walker.template)
49
+ this.alloc(capacity);
50
+ }
51
+
52
+ alloc(cap) {
53
+ const grow = (Ctor, old, per) => {
54
+ const a = new Ctor(cap * per);
55
+ if (old) a.set(old.subarray(0, Math.min(old.length, a.length)));
56
+ return a;
57
+ };
58
+ this.matrices = grow(Float32Array, this.matrices, 16);
59
+ this.geom = grow(Uint32Array, this.geom, 1);
60
+ this.mat = grow(Uint32Array, this.mat, 1);
61
+ this.uid = grow(Uint32Array, this.uid, 1);
62
+ this.lodNode = grow(Int32Array, this.lodNode, 1);
63
+ this.lodChild = grow(Uint16Array, this.lodChild, 1);
64
+ this.flags = grow(Uint8Array, this.flags, 1);
65
+ this.spheres = grow(Float32Array, this.spheres, 4);
66
+ this.capacity = cap;
67
+ }
68
+
69
+ geometryIndex(entry) {
70
+ let i = this.geomIndex.get(entry);
71
+ if (i === undefined) {
72
+ i = this.geometries.length;
73
+ this.geometries.push(entry);
74
+ this.geomIndex.set(entry, i);
75
+ }
76
+ return i;
77
+ }
78
+
79
+ /** Write an instance row at index i (growing as needed). */
80
+ setInstance(i, gi, mi, world, uid, lodNode, lodChild, flags = 0) {
81
+ if (i >= this.capacity) this.alloc(Math.max(this.capacity * 2, i + 1));
82
+ const m = this.matrices;
83
+ const o = i * 16;
84
+ for (let k = 0; k < 16; k++) m[o + k] = world[k];
85
+ this.geom[i] = gi;
86
+ this.mat[i] = mi;
87
+ this.uid[i] = uid;
88
+ this.lodNode[i] = lodNode;
89
+ this.lodChild[i] = lodChild;
90
+ const det = mat4.determinant3(world);
91
+ this.flags[i] = (det < 0 ? FLAG_MIRRORED : 0) | flags;
92
+ // world bounding sphere from the canonical bounds
93
+ const b = this.geometries[gi].bounds;
94
+ let cx = 0, cy = 0, cz = 0, r = 0;
95
+ if (b) {
96
+ const lx = (b.min[0] + b.max[0]) / 2, ly = (b.min[1] + b.max[1]) / 2, lz = (b.min[2] + b.max[2]) / 2;
97
+ cx = world[0] * lx + world[4] * ly + world[8] * lz + world[12];
98
+ cy = world[1] * lx + world[5] * ly + world[9] * lz + world[13];
99
+ cz = world[2] * lx + world[6] * ly + world[10] * lz + world[14];
100
+ const hx = (b.max[0] - b.min[0]) / 2, hy = (b.max[1] - b.min[1]) / 2, hz = (b.max[2] - b.min[2]) / 2;
101
+ // half extents of the transformed box
102
+ const ex = Math.abs(world[0]) * hx + Math.abs(world[4]) * hy + Math.abs(world[8]) * hz;
103
+ const ey = Math.abs(world[1]) * hx + Math.abs(world[5]) * hy + Math.abs(world[9]) * hz;
104
+ const ez = Math.abs(world[2]) * hx + Math.abs(world[6]) * hy + Math.abs(world[10]) * hz;
105
+ // a culling bound, not output: plain sqrt is several times faster than Math.hypot
106
+ r = Math.sqrt(ex * ex + ey * ey + ez * ez);
107
+ const bb = this.bounds;
108
+ if (cx - ex < bb.min[0]) bb.min[0] = cx - ex;
109
+ if (cy - ey < bb.min[1]) bb.min[1] = cy - ey;
110
+ if (cz - ez < bb.min[2]) bb.min[2] = cz - ez;
111
+ if (cx + ex > bb.max[0]) bb.max[0] = cx + ex;
112
+ if (cy + ey > bb.max[1]) bb.max[1] = cy + ey;
113
+ if (cz + ez > bb.max[2]) bb.max[2] = cz + ez;
114
+ }
115
+ const s = this.spheres;
116
+ s[i * 4] = cx;
117
+ s[i * 4 + 1] = cy;
118
+ s[i * 4 + 2] = cz;
119
+ s[i * 4 + 3] = r;
120
+ }
121
+
122
+ /** Room for n rows, grown as setInstance grows it row by row. */
123
+ reserve(n) {
124
+ let cap = this.capacity;
125
+ while (cap < n) cap = Math.max(cap * 2, cap + 1);
126
+ if (cap !== this.capacity) this.alloc(cap);
127
+ }
128
+
129
+ /** Release unused capacity (after a full build). */
130
+ trim() {
131
+ if (this.capacity > this.count * 2 + 64) this.alloc(Math.max(this.count, 1));
132
+ }
133
+
134
+ get empty() {
135
+ return this.count === 0;
136
+ }
137
+ }
138
+
139
+ const IDENTITY = mat4.createF64();
140
+
141
+ class Walker {
142
+ constructor(scene, graph, time, progress = 0) {
143
+ this.scene = scene;
144
+ this.graph = graph;
145
+ this.time = time;
146
+ this.progress = progress;
147
+ this.noCollide = 0; // depth of collide="false" ancestors
148
+ this.cursor = 0;
149
+ this.lightCursor = 0;
150
+ this.cameraCursor = 0;
151
+ this.switchCursor = 0;
152
+ this.overwrite = false;
153
+ this.stack = [];
154
+ this.tmp = mat4.createF64();
155
+ this.segmentDepth = 0;
156
+ }
157
+
158
+ mat(depth) {
159
+ while (this.stack.length <= depth) this.stack.push(new Float64Array(16));
160
+ return this.stack[depth];
161
+ }
162
+
163
+ /** Local matrix of a node at the current time. */
164
+ localOf(node, out) {
165
+ if (!node.anims.length) {
166
+ if (node.local) out.set(node.local);
167
+ else mat4.identity(out);
168
+ return out;
169
+ }
170
+ const ov = {};
171
+ for (const a of node.anims) {
172
+ const v = sampleAnimation(a, this.time, this.progress);
173
+ if (v) ov[a.attr] = v;
174
+ }
175
+ return localMatrix(node.el.values, out, ov);
176
+ }
177
+
178
+ walk(node, parent, material, color, sel, lodNode, lodChild, depth) {
179
+ if (!node || !node.visible) return;
180
+ const g = this.graph;
181
+ const isSegmentRoot = node.anims.length > 0 && this.segmentDepth === 0 && !this.overwrite;
182
+ let seg = null;
183
+ if (isSegmentRoot) {
184
+ seg = {
185
+ node,
186
+ parent: new Float64Array(parent),
187
+ material,
188
+ color,
189
+ sel,
190
+ lodNode,
191
+ lodChild,
192
+ noCollide: this.noCollide,
193
+ start: this.cursor,
194
+ end: 0,
195
+ lightStart: this.lightCursor,
196
+ cameraStart: this.cameraCursor,
197
+ switchStart: this.switchCursor,
198
+ };
199
+ }
200
+ if (node.anims.length) this.segmentDepth++;
201
+ if (node.collide === false) this.noCollide++;
202
+
203
+ // inherited style: an element that sets a material starts from that material's own color
204
+ let mat = material;
205
+ let col = color;
206
+ if (node.material !== null) {
207
+ mat = node.material;
208
+ col = null;
209
+ }
210
+ if (node.color !== null) col = node.color;
211
+
212
+ if (node.kind === 'light') this.emitLight(node, parent);
213
+ else if (node.kind === 'camera') this.emitCamera(node, parent);
214
+ else {
215
+ let world;
216
+ if (!node.local && !node.anims.length) {
217
+ // identity local transform: children write only to deeper stack slots, so the parent can be shared
218
+ world = parent;
219
+ } else {
220
+ world = this.mat(depth);
221
+ // static nodes multiply by their stored matrix directly (no copy)
222
+ const local = node.anims.length ? this.localOf(node, this.tmp) : node.local;
223
+ mat4.multiply(world, parent, local);
224
+ }
225
+ switch (node.kind) {
226
+ case 'shape':
227
+ this.emitShape(node, world, mat, col, sel === 0 ? node.uid : sel, lodNode, lodChild, depth);
228
+ break;
229
+ case 'group':
230
+ for (const c of node.children) this.walk(c, world, mat, col, sel, lodNode, lodChild, depth + 1);
231
+ break;
232
+ case 'use':
233
+ if (node.target) this.walk(node.target, world, mat, col, sel === 0 ? node.uid : sel, lodNode, lodChild, depth + 1);
234
+ for (const c of node.children) this.walk(c, world, mat, col, sel, lodNode, lodChild, depth + 1);
235
+ break;
236
+ case 'repeat':
237
+ this.emitRepeat(node, world, mat, col, sel, lodNode, lodChild, depth);
238
+ break;
239
+ case 'scatter':
240
+ this.emitScatter(node, world, mat, col, sel, lodNode, lodChild, depth);
241
+ break;
242
+ case 'lod':
243
+ this.emitLod(node, world, mat, col, sel, lodNode, lodChild, depth);
244
+ break;
245
+ default:
246
+ break;
247
+ }
248
+ }
249
+
250
+ if (node.anims.length) this.segmentDepth--;
251
+ if (node.collide === false) this.noCollide--;
252
+ if (seg) {
253
+ seg.end = this.cursor;
254
+ seg.lightEnd = this.lightCursor;
255
+ seg.cameraEnd = this.cameraCursor;
256
+ seg.switchEnd = this.switchCursor;
257
+ this.scene.dynamic.push(seg);
258
+ }
259
+ }
260
+
261
+ emitShape(node, world, material, color, uid, lodNode, lodChild, depth) {
262
+ if (!node.geometry) return;
263
+ const s = this.scene;
264
+ let w = world;
265
+ if (node.unit) {
266
+ w = this.mat(depth + 1);
267
+ mat4.multiply(w, world, node.unit);
268
+ }
269
+ const gi = s.geometryIndex(node.geometry);
270
+ const mi = this.graph.materials.resolve(material, color);
271
+ s.setInstance(this.cursor++, gi, mi, w, uid, lodNode, lodChild, this.noCollide ? FLAG_NO_COLLIDE : 0);
272
+ }
273
+
274
+ emitRepeat(node, world, material, color, sel, lodNode, lodChild, depth) {
275
+ if (this.expand(node, node.count, world, material, color, sel, lodNode, lodChild)) return;
276
+ const acc = mat4.createF64();
277
+ const tmp = mat4.createF64();
278
+ // children use stack slots from depth + 2 on, so slot depth + 1 stays intact
279
+ const childWorld = this.mat(depth + 1);
280
+ for (let i = 0; i < node.count; i++) {
281
+ mat4.multiply(childWorld, world, acc);
282
+ for (const c of node.children) this.walk(c, childWorld, material, color, sel, lodNode, lodChild, depth + 2);
283
+ mat4.multiply(tmp, acc, node.step);
284
+ acc.set(tmp);
285
+ }
286
+ }
287
+
288
+ emitScatter(node, world, material, color, sel, lodNode, lodChild, depth) {
289
+ const p = node.params;
290
+ if (this.expand(node, p.count, world, material, color, sel, lodNode, lodChild)) return;
291
+ const rand = mulberry32(p.seed);
292
+ const place = mat4.createF64();
293
+ const u = new Float64Array(9);
294
+ const lerp = (r, t) => r[0] + (r[1] - r[0]) * t;
295
+ const nx = p.grid ? p.grid[0] : 0;
296
+ const nz = p.grid ? p.grid[1] : 0;
297
+ const childWorld = new Float64Array(16);
298
+ const pos = [0, 0, 0];
299
+ const rot = [0, 0, 0];
300
+ const scl = [1, 1, 1];
301
+ for (let i = 0; i < p.count; i++) {
302
+ for (let k = 0; k < 9; k++) u[k] = rand();
303
+ if (p.grid) {
304
+ const ix = i % nx, iz = Math.floor(i / nx);
305
+ pos[0] = (ix - (nx - 1) / 2) * p.spacing[0] + (u[0] - 0.5) * p.jitter * p.spacing[0];
306
+ pos[2] = (iz - (nz - 1) / 2) * p.spacing[1] + (u[1] - 0.5) * p.jitter * p.spacing[1];
307
+ } else if (p.radius) {
308
+ const r = p.radius * Math.sqrt(u[0]);
309
+ const a = u[1] * Math.PI * 2;
310
+ pos[0] = r * es3dCos(a);
311
+ pos[2] = r * es3dSin(a);
312
+ } else {
313
+ pos[0] = (u[0] - 0.5) * p.area[0];
314
+ pos[2] = (u[1] - 0.5) * p.area[1];
315
+ }
316
+ if (p.randomRotation) {
317
+ const rr = p.randomRotation.ranges;
318
+ rot[0] = lerp(rr[0], u[2]);
319
+ rot[1] = lerp(rr[1], u[3]);
320
+ rot[2] = lerp(rr[2], u[4]);
321
+ }
322
+ if (p.randomScale) {
323
+ const rs = p.randomScale.ranges;
324
+ if (p.randomScale.uniform) scl[0] = scl[1] = scl[2] = lerp(rs[0], u[5]);
325
+ else {
326
+ scl[0] = lerp(rs[0], u[5]);
327
+ scl[1] = lerp(rs[1], u[6]);
328
+ scl[2] = lerp(rs[2], u[7]);
329
+ }
330
+ }
331
+ mat4.compose(place, pos, rot, scl);
332
+ mat4.multiply(childWorld, world, place);
333
+ let m = material;
334
+ let c = color;
335
+ if (p.randomMaterial) {
336
+ m = p.randomMaterial[Math.min(p.randomMaterial.length - 1, Math.floor(u[8] * p.randomMaterial.length))];
337
+ c = null;
338
+ }
339
+ for (const ch of node.children) this.walk(ch, childWorld, m, c, sel, lodNode, lodChild, depth + 1);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * A <repeat> or <scatter> expanded by the instance kernel
345
+ * (wasm/simd/instances.c). False when the walker must expand it: no
346
+ * kernel, few rows, or a subtree or parameters the kernel does not handle.
347
+ */
348
+ expand(node, count, world, material, color, sel, lodNode, lodChild) {
349
+ if (!instanceKernelEnabled() || !Number.isInteger(count) || count < 1) return false;
350
+ const t = this.template(node, sel, lodChild);
351
+ if (!t || t.rows.length * count < KERNEL_ROWS) return false;
352
+ const scatter = node.kind === 'scatter';
353
+ const p = node.params;
354
+ const h = new Int32Array(H.SIZE);
355
+ const f = new Float64Array(F.SIZE);
356
+ if (scatter && !scatterHeader(p, h, f)) return false;
357
+
358
+ const s = this.scene;
359
+ const nrows = t.rows.length;
360
+ const nlods = t.lods.length;
361
+ if (!t.geometries) {
362
+ // where the walker would add them: before the first row of the first placement
363
+ t.geometries = true;
364
+ t.rows.forEach((r, k) => {
365
+ const b = r.entry.bounds;
366
+ t.table.set([s.geometryIndex(r.entry), r.uid, r.lod, r.child, r.flags, b ? 1 : 0], k * 6);
367
+ if (b) {
368
+ const half = [(b.max[0] - b.min[0]) / 2, (b.max[1] - b.min[1]) / 2, (b.max[2] - b.min[2]) / 2];
369
+ t.bounds.set([(b.min[0] + b.max[0]) / 2, (b.min[1] + b.max[1]) / 2, (b.min[2] + b.max[2]) / 2, ...half], k * 6);
370
+ }
371
+ });
372
+ }
373
+ h[H.KIND] = scatter ? SCATTER : REPEAT;
374
+ h[H.NOPS] = t.ops.length;
375
+ h[H.NROWS] = nrows;
376
+ h[H.LODS] = nlods;
377
+ h[H.OUTER_LOD] = lodNode;
378
+ h[H.SLOTS] = t.slots;
379
+ f.set(world, F.WORLD);
380
+ if (!scatter) {
381
+ f.set(IDENTITY, F.ACC);
382
+ f.set(node.step, F.STEP);
383
+ }
384
+ const bb = s.bounds;
385
+ f.set(bb.min, F.BOUNDS);
386
+ f.set(bb.max, F.BOUNDS + 3);
387
+
388
+ // the kernel asks for each material where the walker would: at the first row that needs it
389
+ const choices = scatter && p.randomMaterial;
390
+ const materials = new Int32Array(nrows * (choices ? choices.length : 1)).fill(-1);
391
+ const resolveMaterial = (code) => {
392
+ const choice = Math.floor(code / nrows);
393
+ const r = t.rows[code - choice * nrows];
394
+ const m = choices ? choices[choice] : material;
395
+ const c = choices ? null : color;
396
+ return this.graph.materials.resolve(r.material === INHERIT ? m : r.material, r.color === INHERIT ? c : r.color);
397
+ };
398
+
399
+ const per = Math.max(1, Math.floor(CHUNK_ROWS / nrows));
400
+ let cursor = this.cursor;
401
+ let sw = this.switchCursor;
402
+ for (let first = 0; first < count; first += per) {
403
+ h[H.FIRST] = first;
404
+ h[H.COUNT] = Math.min(per, count - first);
405
+ h[H.SWITCH] = sw;
406
+ const out = instancesWasm(h, f, t, materials, resolveMaterial);
407
+ if (!out) {
408
+ if (first > 0) throw new Error('WebAssembly memory ran out while expanding instances');
409
+ return false;
410
+ }
411
+ const n = out.mat.length;
412
+ s.reserve(cursor + n);
413
+ s.matrices.set(out.matrices, cursor * 16);
414
+ s.spheres.set(out.spheres, cursor * 4);
415
+ s.geom.set(out.geom, cursor);
416
+ s.mat.set(out.mat, cursor);
417
+ s.uid.set(out.uid, cursor);
418
+ s.lodNode.set(out.lodNode, cursor);
419
+ s.lodChild.set(out.lodChild, cursor);
420
+ s.flags.set(out.flags, cursor);
421
+ const v = out.switches;
422
+ for (let k = 0; k < v.length / 4; k++) {
423
+ const lod = t.lods[k % nlods];
424
+ const scale = Math.cbrt(Math.abs(v[k * 4 + 3]) || 1);
425
+ s.lodSwitches[sw + k] = {
426
+ pos: [v[k * 4], v[k * 4 + 1], v[k * 4 + 2]],
427
+ distances: lod.distances.map((d) => d * scale),
428
+ parent: lod.ref >= 0 ? sw + k - (k % nlods) + lod.ref : lodNode,
429
+ parentChild: lod.child,
430
+ };
431
+ }
432
+ cursor += n;
433
+ sw += v.length / 4;
434
+ }
435
+ for (let k = 0; k < 3; k++) {
436
+ bb.min[k] = f[F.BOUNDS + k];
437
+ bb.max[k] = f[F.BOUNDS + 3 + k];
438
+ }
439
+ this.cursor = cursor;
440
+ this.switchCursor = sw;
441
+ return true;
442
+ }
443
+
444
+ /** The subtree below one placement of a repeat or scatter node (recordTemplate), recorded once per scene. */
445
+ template(node, sel, lodChild) {
446
+ const noCollide = this.noCollide > 0;
447
+ let e = this.scene.templates.get(node);
448
+ if (!e || e.sel !== sel || e.lodChild !== lodChild || e.noCollide !== noCollide) {
449
+ e = { sel, lodChild, noCollide, template: recordTemplate(node.children, sel, lodChild, noCollide) };
450
+ this.scene.templates.set(node, e);
451
+ }
452
+ return e.template;
453
+ }
454
+
455
+ emitLod(node, world, material, color, sel, lodNode, lodChild, depth) {
456
+ const s = this.scene;
457
+ const sw = {
458
+ pos: [world[12], world[13], world[14]],
459
+ // distances are measured in the lod node's coordinate system: scale them to world units
460
+ distances: node.params.distances.map((d) => d * Math.cbrt(Math.abs(mat4.determinant3(world)) || 1)),
461
+ parent: lodNode,
462
+ parentChild: lodChild,
463
+ };
464
+ const si = this.switchCursor++;
465
+ s.lodSwitches[si] = sw;
466
+ node.children.forEach((c, ci) => this.walk(c, world, material, color, sel, si, ci, depth + 1));
467
+ }
468
+
469
+ emitLight(node, parent) {
470
+ const p = node.params;
471
+ const lw = mat4.createF64();
472
+ lw.set(parent);
473
+ if (p.transform) mat4.multiply(lw, lw, p.transform);
474
+ const pos = node.anims.length ? sampleAnimations(node, this, 'position', p.position) : p.position;
475
+ const rotation = node.anims.length ? sampleAnimations(node, this, 'rotation', p.rotation || [0, 0, 0]) : p.rotation;
476
+ const placed = mat4.createF64();
477
+ mat4.compose(placed, pos, rotation || [0, 0, 0], [1, 1, 1]);
478
+ mat4.multiply(placed, lw, placed);
479
+ const P = [placed[12], placed[13], placed[14]];
480
+ let dirLocal = p.direction;
481
+ let dirSpace = placed;
482
+ if (!dirLocal && p.target) {
483
+ // target is given in the parent's coordinate system
484
+ const t = [0, 0, 0];
485
+ mat4.transformPoint(t, lw, p.target[0], p.target[1], p.target[2]);
486
+ dirLocal = [t[0] - P[0], t[1] - P[1], t[2] - P[2]];
487
+ dirSpace = IDENTITY;
488
+ }
489
+ if (!dirLocal) dirLocal = [0, -1, 0];
490
+ const D = [0, 0, 0];
491
+ mat4.transformDir(D, dirSpace, dirLocal[0], dirLocal[1], dirLocal[2]);
492
+ const dl = Math.hypot(D[0], D[1], D[2]) || 1;
493
+ const lin = colorToLinear(p.color);
494
+ const light = {
495
+ type: p.type,
496
+ color: [lin[0] * p.intensity, lin[1] * p.intensity, lin[2] * p.intensity],
497
+ position: P,
498
+ direction: [D[0] / dl, D[1] / dl, D[2] / dl],
499
+ range: p.range,
500
+ outer: Math.cos(((p.angle / 2) * Math.PI) / 180),
501
+ inner: Math.cos((((p.angle / 2) * (1 - p.penumbra)) * Math.PI) / 180),
502
+ ground: colorToLinear(p.groundColor).map((v) => v * p.intensity),
503
+ uid: node.uid,
504
+ };
505
+ this.scene.lights[this.lightCursor++] = light;
506
+ }
507
+
508
+ emitCamera(node, parent) {
509
+ const p = node.params;
510
+ const cw = mat4.createF64();
511
+ cw.set(parent);
512
+ if (p.transform) mat4.multiply(cw, cw, p.transform);
513
+ const animated = node.anims.length > 0;
514
+ const pos = animated ? sampleAnimations(node, this, 'position', p.position) : p.position;
515
+ const eye = [0, 0, 0];
516
+ mat4.transformPoint(eye, cw, pos[0], pos[1], pos[2]);
517
+ let target;
518
+ const up = [0, 0, 0];
519
+ const aim = animated ? sampleAnimations(node, this, 'target', p.target) : p.target;
520
+ if (aim) {
521
+ target = [0, 0, 0];
522
+ mat4.transformPoint(target, cw, aim[0], aim[1], aim[2]);
523
+ mat4.transformDir(up, cw, p.up[0], p.up[1], p.up[2]);
524
+ } else {
525
+ const r = mat4.createF64();
526
+ const rot = animated ? sampleAnimations(node, this, 'rotation', p.rotation || [0, 0, 0]) : p.rotation || [0, 0, 0];
527
+ mat4.fromEulerDeg(r, rot[0], rot[1], rot[2]);
528
+ mat4.multiply(r, cw, r);
529
+ const f = [0, 0, 0];
530
+ mat4.transformDir(f, r, 0, 0, -1);
531
+ target = [eye[0] + f[0], eye[1] + f[1], eye[2] + f[2]];
532
+ mat4.transformDir(up, r, p.up[0], p.up[1], p.up[2]);
533
+ }
534
+ this.scene.cameras[this.cameraCursor++] = {
535
+ type: p.type,
536
+ position: eye,
537
+ target,
538
+ up: up.map((v) => v / (Math.hypot(up[0], up[1], up[2]) || 1)),
539
+ fov: animated ? sampleAnimations(node, this, 'fov', [p.fov])[0] : p.fov,
540
+ height: p.height,
541
+ near: p.near,
542
+ far: p.far,
543
+ aspect: p.aspect,
544
+ fit: p.fit,
545
+ name: p.name,
546
+ uid: node.uid,
547
+ };
548
+ }
549
+ }
550
+
551
+ // keeps the walker's hidden class alive between scenes (see MeshBuilder.shapeKeeper);
552
+ // fractional clocks, so its fields have the representation an animated scene gives them
553
+ Walker.shapeKeeper = new Walker(null, null, 0.5, 0.5);
554
+
555
+ // --- the instance kernel's input (see Walker.expand and wasm/simd/instances.c) ---
556
+
557
+ const KERNEL_ROWS = 16; // fewer rows are cheaper to walk than to hand over
558
+ const CHUNK_ROWS = 16384; // rows per kernel call, bounding its memory
559
+ const TEMPLATE_ROWS = 4096; // larger subtrees are walked per placement
560
+ const TEMPLATE_SLOTS = 4 * TEMPLATE_ROWS;
561
+ // the enums of wasm/simd/instances.c
562
+ const OP_MUL = 0, OP_ROW = 1, OP_LOD = 2;
563
+ const REPEAT = 0, SCATTER = 1;
564
+ const GRID = 0, RADIUS = 1, AREA = 2;
565
+ const RANDOM_ROTATION = 1, RANDOM_SCALE = 2, UNIFORM_SCALE = 4;
566
+ /** The material or colour of the placement: what the element inherits, or the scatter's random material. */
567
+ const INHERIT = Symbol('inherit');
568
+
569
+ /** A scatter's placement parameters in the kernel headers; false when the walker must place it. */
570
+ function scatterHeader(p, h, f) {
571
+ h[H.STATE] = p.seed >>> 0;
572
+ if (p.grid) {
573
+ const [nx, nz] = p.grid;
574
+ if (!Number.isInteger(nx) || !Number.isInteger(nz) || nx < 1 || !p.spacing) return false;
575
+ h[H.MODE] = GRID;
576
+ h[H.NX] = nx;
577
+ h[H.NZ] = nz;
578
+ f[F.SPACING] = p.spacing[0];
579
+ f[F.SPACING + 1] = p.spacing[1];
580
+ f[F.JITTER] = p.jitter;
581
+ } else if (p.radius) {
582
+ h[H.MODE] = RADIUS;
583
+ f[F.RADIUS] = p.radius;
584
+ } else {
585
+ if (!p.area) return false;
586
+ h[H.MODE] = AREA;
587
+ f[F.AREA] = p.area[0];
588
+ f[F.AREA + 1] = p.area[1];
589
+ }
590
+ const ranges = (r, at) => {
591
+ for (let k = 0; k < r.length; k++) f.set(r[k], at + k * 2);
592
+ };
593
+ if (p.randomRotation) {
594
+ if (p.randomRotation.ranges.length !== 3) return false;
595
+ h[H.RANDOM] |= RANDOM_ROTATION;
596
+ ranges(p.randomRotation.ranges, F.ROTATION);
597
+ }
598
+ if (p.randomScale) {
599
+ const rs = p.randomScale;
600
+ if (rs.ranges.length < (rs.uniform ? 1 : 3)) return false;
601
+ h[H.RANDOM] |= rs.uniform ? RANDOM_SCALE | UNIFORM_SCALE : RANDOM_SCALE;
602
+ ranges(rs.uniform ? rs.ranges.slice(0, 1) : rs.ranges.slice(0, 3), F.SCALE);
603
+ }
604
+ h[H.CHOICES] = p.randomMaterial ? p.randomMaterial.length : 0;
605
+ return true;
606
+ }
607
+
608
+ /**
609
+ * The subtree below one placement of a <repeat> or <scatter> as a program
610
+ * for the instance kernel: products into matrix slots (slot 0 is the
611
+ * placement's world matrix), instance rows and lod switches, in the order
612
+ * the walker produces them; a nested <repeat> is unrolled. Null when the
613
+ * subtree holds what only the walker handles: animation, lights, cameras,
614
+ * a nested <scatter>, more rows or products than TEMPLATE_ROWS and
615
+ * TEMPLATE_SLOTS allow.
616
+ */
617
+ function recordTemplate(children, sel, lodChild, noCollide) {
618
+ const t = { ops: [], matrices: [], index: new Map(), slots: 1, rows: [], lods: [], geometries: false };
619
+ for (const c of children) if (!record(t, c, 0, INHERIT, INHERIT, sel, -1, lodChild, noCollide)) return null;
620
+ t.ops = Int32Array.from(t.ops);
621
+ t.mats = new Float64Array(Math.max(1, t.matrices.length) * 16);
622
+ t.matrices.forEach((m, k) => t.mats.set(m, k * 16));
623
+ t.table = new Int32Array(t.rows.length * 6);
624
+ t.bounds = new Float64Array(Math.max(1, t.rows.length * 6));
625
+ return t;
626
+ }
627
+
628
+ /** slot = src * matrix */
629
+ function product(t, src, matrix) {
630
+ let k = t.index.get(matrix);
631
+ if (k === undefined) {
632
+ k = t.matrices.length;
633
+ t.matrices.push(matrix);
634
+ t.index.set(matrix, k);
635
+ }
636
+ const slot = t.slots++;
637
+ t.ops.push(OP_MUL, slot, src, k);
638
+ return slot;
639
+ }
640
+
641
+ /** Walker.walk for a node below a placement, as template operations (lod: a template lod, or -1 for the outer one). */
642
+ function record(t, node, slot, material, color, sel, lod, child, noCollide) {
643
+ if (!node || !node.visible) return true;
644
+ const kind = node.kind;
645
+ if (node.anims.length || kind === 'light' || kind === 'camera' || kind === 'scatter') return false;
646
+ if (t.rows.length > TEMPLATE_ROWS || t.slots > TEMPLATE_SLOTS) return false;
647
+ if (node.collide === false) noCollide = true;
648
+ let mat = material;
649
+ let col = color;
650
+ if (node.material !== null) {
651
+ mat = node.material;
652
+ col = null;
653
+ }
654
+ if (node.color !== null) col = node.color;
655
+ const world = node.local ? product(t, slot, node.local) : slot;
656
+ const walk = (c) => record(t, c, world, mat, col, sel, lod, child, noCollide);
657
+ switch (kind) {
658
+ case 'shape':
659
+ if (node.geometry) {
660
+ const w = node.unit ? product(t, world, node.unit) : world;
661
+ t.ops.push(OP_ROW, t.rows.length, w, 0);
662
+ t.rows.push({ entry: node.geometry, uid: sel === 0 ? node.uid : sel, lod, child, flags: noCollide ? FLAG_NO_COLLIDE : 0, material: mat, color: col });
663
+ }
664
+ return true;
665
+ case 'group':
666
+ return node.children.every(walk);
667
+ case 'use':
668
+ if (node.target && !record(t, node.target, world, mat, col, sel === 0 ? node.uid : sel, lod, child, noCollide)) return false;
669
+ return node.children.every(walk);
670
+ case 'repeat': {
671
+ // Walker.emitRepeat: the accumulated steps do not depend on the placement
672
+ const acc = mat4.createF64();
673
+ const tmp = mat4.createF64();
674
+ for (let i = 0; i < node.count; i++) {
675
+ if (t.slots > TEMPLATE_SLOTS) return false;
676
+ const copy = product(t, world, new Float64Array(acc));
677
+ if (!node.children.every((c) => record(t, c, copy, mat, col, sel, lod, child, noCollide))) return false;
678
+ mat4.multiply(tmp, acc, node.step);
679
+ acc.set(tmp);
680
+ }
681
+ return true;
682
+ }
683
+ case 'lod': {
684
+ const k = t.lods.length;
685
+ t.lods.push({ ref: lod, child, distances: node.params.distances });
686
+ t.ops.push(OP_LOD, k, world, 0);
687
+ return node.children.every((c, ci) => record(t, c, world, mat, col, sel, k, ci, noCollide));
688
+ }
689
+ default:
690
+ return true;
691
+ }
692
+ }
693
+
694
+ function sampleAnimations(node, walker, attr, base) {
695
+ let v = base;
696
+ for (const a of node.anims) {
697
+ if (a.attr !== attr) continue;
698
+ const s = sampleAnimation(a, walker.time, walker.progress);
699
+ if (s) v = s;
700
+ }
701
+ return v;
702
+ }
703
+
704
+ /**
705
+ * Flatten a scene graph into a RenderScene.
706
+ * @param {object} graph result of buildSceneGraph
707
+ * @param {{time?: number, progress?: number}} [opts] progress: scroll timeline position 0..1
708
+ */
709
+ export function flattenScene(graph, opts = {}) {
710
+ const scene = new RenderScene(Math.min(Math.max(64, graph.expanded || 64), 1 << 20));
711
+ scene.environment = graph.environment;
712
+ scene.controls = graph.controls || null;
713
+ if (graph.animation) scene.animation = { ...graph.animation };
714
+ if (graph.tooLarge || !graph.root) {
715
+ scene.materials = graph.materials.list;
716
+ return scene;
717
+ }
718
+ const w = new Walker(scene, graph, opts.time || 0, opts.progress || 0);
719
+ w.walk(graph.root, IDENTITY, null, null, 0, -1, 0, 0);
720
+ scene.count = w.cursor;
721
+ scene.lights.length = w.lightCursor;
722
+ scene.cameras.length = w.cameraCursor;
723
+ scene.lodSwitches.length = w.switchCursor;
724
+ scene.materials = graph.materials.list;
725
+ scene.trim();
726
+ if (!(scene.bounds.min[0] <= scene.bounds.max[0])) scene.bounds = { min: [-1, -1, -1], max: [1, 1, 1] };
727
+ scene.version++;
728
+ return scene;
729
+ }
730
+
731
+ /** Re-evaluate animated subtrees at time t and scroll progress, overwriting their rows. */
732
+ export function updateDynamic(scene, graph, time, progress = 0) {
733
+ if (!scene.dynamic.length) return false;
734
+ const w = new Walker(scene, graph, time, progress);
735
+ w.overwrite = true;
736
+ for (const seg of scene.dynamic) {
737
+ w.cursor = seg.start;
738
+ w.lightCursor = seg.lightStart;
739
+ w.cameraCursor = seg.cameraStart;
740
+ w.switchCursor = seg.switchStart;
741
+ w.segmentDepth = 0;
742
+ w.noCollide = seg.noCollide;
743
+ w.walk(seg.node, seg.parent, seg.material, seg.color, seg.sel, seg.lodNode, seg.lodChild, 0);
744
+ }
745
+ scene.version++;
746
+ return true;
747
+ }
748
+
749
+ /** Instances per geometry (for statistics). */
750
+ export function instanceHistogram(scene) {
751
+ const counts = new Uint32Array(scene.geometries.length);
752
+ for (let i = 0; i < scene.count; i++) counts[scene.geom[i]]++;
753
+ return counts;
754
+ }