@es3d/es3d 1.0.0 → 1.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/LICENSE +201 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
package/src/scene/flatten.js
CHANGED
|
@@ -1,754 +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
|
-
}
|
|
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
|
+
}
|