@es3d/es3d 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/browser/es3d-stage.min.js +243 -10218
- package/browser/es3d.min.js +534 -0
- package/docs/SPEC.md +1 -1
- package/docs/STAGE.md +95 -0
- package/package.json +6 -3
- package/src/browser.js +16 -0
- package/src/cache/geometry-store.js +56 -18
- package/src/geometry/geometry-cache.js +93 -8
- package/src/io/compression.js +4 -1
- package/src/io/convert.js +14 -1
- package/src/pipeline.js +2 -1
- package/src/render/compile-worker.js +143 -0
- package/src/render/frame-builder.js +151 -79
- package/src/render/renderer.js +28 -2
- package/src/render/stage.js +305 -50
- package/src/render/worker-boot.js +14 -0
- package/src/scene/fragments.js +527 -0
- package/src/scene/picking.js +31 -2
- package/src/scene/prepared.js +398 -0
- package/src/scene/scene-graph.js +1 -1
- package/src/scene/scene-set.js +251 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// A document compiled somewhere else (a worker), ready to place.
|
|
2
|
+
//
|
|
3
|
+
// compile() makes objects that cannot cross to another thread: the scene
|
|
4
|
+
// graph's geometry entries hold generator closures over the parsed tree. So a
|
|
5
|
+
// worker runs the whole pipeline and also generates the meshes, and sends
|
|
6
|
+
// back only plain data, most of it as transferred buffers (nothing copied):
|
|
7
|
+
//
|
|
8
|
+
// - the instance table (RenderScene rows, exact length),
|
|
9
|
+
// - each geometry's descriptor fields (key, identity, levels, bounds) as
|
|
10
|
+
// columns and, for keys this worker has not sent before, its generated
|
|
11
|
+
// meshes with the diagnostics generation reported. A level identical to
|
|
12
|
+
// the one before it (a straight-sided extrusion has four) is sent once
|
|
13
|
+
// and shared, and every mesh of the document is packed into four buffers:
|
|
14
|
+
// a few transfers, not four per level (thousands for a large tile, each
|
|
15
|
+
// with a cost of its own when the message arrives).
|
|
16
|
+
// - material parameters, lights, cameras, lod switches, bounds,
|
|
17
|
+
// - the diagnostics as plain fields (Diagnostic is rebuilt here, so the
|
|
18
|
+
// codes, messages and locations are exactly those compile() reports).
|
|
19
|
+
//
|
|
20
|
+
// The main thread turns that back into a RenderScene whose entries live in
|
|
21
|
+
// the stage's GeometryCache (resultFromPayload): an entry whose key the cache
|
|
22
|
+
// has is reused, a new one adopts the worker's meshes the way the persistent
|
|
23
|
+
// store's are adopted, and a level the worker did not send is generated from
|
|
24
|
+
// the document on first use. The parsed tree and the scene graph are never
|
|
25
|
+
// sent: unpacking them would cost the main thread about what parsing does.
|
|
26
|
+
// When a page needs them (setTransforms on that document, or names for a
|
|
27
|
+
// pick), the document is compiled on the main thread then.
|
|
28
|
+
//
|
|
29
|
+
// A document the payload cannot carry (one with <animate>, which updateDynamic
|
|
30
|
+
// rewalks through the graph every frame, or with textures) comes back as
|
|
31
|
+
// `main: true`, and the main thread compiles it as it would without workers.
|
|
32
|
+
|
|
33
|
+
import { compile, elementDiagnostic } from '../pipeline.js';
|
|
34
|
+
import { Diagnostic, LineIndex } from '../format/errors.js';
|
|
35
|
+
import { GeometryCache } from '../geometry/geometry-cache.js';
|
|
36
|
+
import { Mesh } from '../geometry/mesh.js';
|
|
37
|
+
import { Material } from '../material/materials.js';
|
|
38
|
+
import { RenderScene } from './flatten.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Code of the stage's own warning for an element a fragment cannot set.
|
|
42
|
+
* It is not a format diagnostic: a document that has these elements is valid,
|
|
43
|
+
* and opened on its own it uses them.
|
|
44
|
+
*/
|
|
45
|
+
export const HOST_ONLY = 'STAGE_HOST_ONLY';
|
|
46
|
+
|
|
47
|
+
const HOST_ONLY_ELEMENTS = {
|
|
48
|
+
environment: 'the environment',
|
|
49
|
+
camera: 'a camera',
|
|
50
|
+
controls: 'the controls',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Warnings for the <environment>, <camera> and <controls> elements of a
|
|
55
|
+
* document shown as a fragment: they belong to the host and are ignored.
|
|
56
|
+
* @param {object} result a compile() result
|
|
57
|
+
* @returns {Diagnostic[]}
|
|
58
|
+
*/
|
|
59
|
+
export function hostOnlyWarnings(result) {
|
|
60
|
+
const doc = result && result.doc;
|
|
61
|
+
if (!doc || !doc.root) return [];
|
|
62
|
+
const out = [];
|
|
63
|
+
const visit = (el) => {
|
|
64
|
+
if (el.unknown) return;
|
|
65
|
+
const what = HOST_ONLY_ELEMENTS[el.name];
|
|
66
|
+
if (what) {
|
|
67
|
+
out.push(elementDiagnostic(doc, {
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
phase: 'render',
|
|
70
|
+
code: HOST_ONLY,
|
|
71
|
+
message: `A fragment cannot set ${what}: the stage's document does. This element is ignored.`,
|
|
72
|
+
element: el,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
for (const c of el.children) if (c.type === 'element') visit(c);
|
|
76
|
+
};
|
|
77
|
+
visit(doc.root);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const DIAGNOSTIC_FIELDS = ['severity', 'phase', 'code', 'message', 'offset', 'length', 'suggestion', 'hint', 'path', 'line', 'column', 'uid'];
|
|
82
|
+
|
|
83
|
+
/** A Diagnostic as plain fields (structured-clone safe). */
|
|
84
|
+
export function plainDiagnostic(d) {
|
|
85
|
+
const o = {};
|
|
86
|
+
for (const k of DIAGNOSTIC_FIELDS) o[k] = d[k] === undefined ? null : d[k];
|
|
87
|
+
return o;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The Diagnostic those fields describe, located as it was. */
|
|
91
|
+
export function diagnosticFrom(o) {
|
|
92
|
+
const d = new Diagnostic(o);
|
|
93
|
+
d.line = o.line ?? 0;
|
|
94
|
+
d.column = o.column ?? 0;
|
|
95
|
+
return d;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const KINDS = ['triangles', 'lines'];
|
|
99
|
+
const kindCode = (k) => Math.max(0, KINDS.indexOf(k));
|
|
100
|
+
const sameArray = (a, b) => {
|
|
101
|
+
if (a.length !== b.length) return false;
|
|
102
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
const sameMesh = (a, b) => a.kind === b.kind && sameArray(a.indices, b.indices) && sameArray(a.positions, b.positions) && sameArray(a.normals, b.normals) && sameArray(a.uvs, b.uvs);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Geometry descriptors as columns, and the meshes of keys not sent before
|
|
109
|
+
* packed into four buffers. levels[g * maxLods + l] is the mesh index of
|
|
110
|
+
* level l of geometry g, -1 when it is not sent.
|
|
111
|
+
*/
|
|
112
|
+
function packGeometries(entries, cache, sent) {
|
|
113
|
+
const n = entries.length;
|
|
114
|
+
const maxLods = entries.reduce((m, e) => Math.max(m, e.lods), 1);
|
|
115
|
+
const keys = new Array(n);
|
|
116
|
+
const identities = new Array(n);
|
|
117
|
+
const lods = new Uint8Array(n);
|
|
118
|
+
const flags = new Uint8Array(n); // 1 double-sided, 2 has bounds, 4 failed somewhere
|
|
119
|
+
const kinds = new Uint8Array(n);
|
|
120
|
+
const bounds = new Float64Array(n * 6);
|
|
121
|
+
const deviation = new Float64Array(n * maxLods);
|
|
122
|
+
const devLength = new Uint8Array(n);
|
|
123
|
+
const levels = new Int32Array(n * maxLods).fill(-1);
|
|
124
|
+
const reports = []; // [g, l, reports] for levels that reported something
|
|
125
|
+
const failed = []; // [g, l]
|
|
126
|
+
const meshes = [];
|
|
127
|
+
entries.forEach((e, g) => {
|
|
128
|
+
keys[g] = e.key;
|
|
129
|
+
identities[g] = e.identity;
|
|
130
|
+
lods[g] = e.lods;
|
|
131
|
+
kinds[g] = kindCode(e.kind);
|
|
132
|
+
flags[g] = (e.doubleSided ? 1 : 0) | (e.bounds ? 2 : 0);
|
|
133
|
+
if (e.bounds) bounds.set([...e.bounds.min, ...e.bounds.max], g * 6);
|
|
134
|
+
devLength[g] = Math.min(e.lodDeviation.length, maxLods);
|
|
135
|
+
for (let l = 0; l < devLength[g]; l++) deviation[g * maxLods + l] = e.lodDeviation[l];
|
|
136
|
+
const tag = `${cache.quality}:${e.key}`;
|
|
137
|
+
if (sent && sent.has(tag)) return;
|
|
138
|
+
let prev = null;
|
|
139
|
+
for (let l = 0; l < e.lods; l++) {
|
|
140
|
+
const m = cache.mesh(e, l);
|
|
141
|
+
if (e.reports[l]) reports.push([g, l, e.reports[l]]);
|
|
142
|
+
if (e.failedLods & (1 << l)) failed.push([g, l]);
|
|
143
|
+
if (prev && sameMesh(prev, m)) {
|
|
144
|
+
levels[g * maxLods + l] = levels[g * maxLods + l - 1];
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
levels[g * maxLods + l] = meshes.length;
|
|
148
|
+
meshes.push(m);
|
|
149
|
+
prev = m;
|
|
150
|
+
}
|
|
151
|
+
if (sent) sent.add(tag);
|
|
152
|
+
});
|
|
153
|
+
// one buffer per attribute; a mesh is a range of each (offsets in elements)
|
|
154
|
+
const table = new Uint32Array(meshes.length * 8);
|
|
155
|
+
const meshKinds = new Uint8Array(meshes.length);
|
|
156
|
+
const total = [0, 0, 0, 0];
|
|
157
|
+
meshes.forEach((m, k) => {
|
|
158
|
+
const arrays = [m.positions, m.normals, m.uvs, m.indices];
|
|
159
|
+
arrays.forEach((a, j) => {
|
|
160
|
+
table[k * 8 + j * 2] = total[j];
|
|
161
|
+
table[k * 8 + j * 2 + 1] = a.length;
|
|
162
|
+
total[j] += a.length;
|
|
163
|
+
});
|
|
164
|
+
meshKinds[k] = kindCode(m.kind);
|
|
165
|
+
});
|
|
166
|
+
const positions = new Float32Array(total[0]);
|
|
167
|
+
const normals = new Float32Array(total[1]);
|
|
168
|
+
const uvs = new Float32Array(total[2]);
|
|
169
|
+
const indices = new Uint32Array(total[3]);
|
|
170
|
+
meshes.forEach((m, k) => {
|
|
171
|
+
positions.set(m.positions, table[k * 8]);
|
|
172
|
+
normals.set(m.normals, table[k * 8 + 2]);
|
|
173
|
+
uvs.set(m.uvs, table[k * 8 + 4]);
|
|
174
|
+
indices.set(m.indices, table[k * 8 + 6]);
|
|
175
|
+
});
|
|
176
|
+
const columns = { keys, identities, lods, flags, kinds, bounds, deviation, devLength, levels, maxLods, reports, failed };
|
|
177
|
+
const packed = { table, meshKinds, positions, normals, uvs, indices };
|
|
178
|
+
const buffers = [lods, flags, kinds, bounds, deviation, devLength, levels, table, meshKinds, positions, normals, uvs, indices].map((a) => a.buffer);
|
|
179
|
+
return { columns, packed, buffers };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The meshes of a packed payload, as views into its four buffers. */
|
|
183
|
+
function unpackMeshes(packed) {
|
|
184
|
+
const { table, meshKinds, positions, normals, uvs, indices } = packed;
|
|
185
|
+
const out = new Array(meshKinds.length);
|
|
186
|
+
for (let k = 0; k < out.length; k++) {
|
|
187
|
+
const o = k * 8;
|
|
188
|
+
out[k] = new Mesh({
|
|
189
|
+
positions: positions.subarray(table[o], table[o] + table[o + 1]),
|
|
190
|
+
normals: normals.subarray(table[o + 2], table[o + 2] + table[o + 3]),
|
|
191
|
+
uvs: uvs.subarray(table[o + 4], table[o + 4] + table[o + 5]),
|
|
192
|
+
indices: indices.subarray(table[o + 6], table[o + 6] + table[o + 7]),
|
|
193
|
+
kind: KINDS[meshKinds[k]],
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const textured = (m) => {
|
|
200
|
+
const p = m.params;
|
|
201
|
+
return !!(p.map || p.normalMap || p.occlusionMap || p.metallicRoughnessMap || p.emissiveMap);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// A typed array that owns its whole buffer can be transferred; a view into a
|
|
205
|
+
// larger one (a WebAssembly heap, a shared pool) is copied first.
|
|
206
|
+
const own = (a) => (a.byteOffset === 0 && a.byteLength === a.buffer.byteLength ? a : a.slice());
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Compile a document and describe the result as transferable data. Runs in
|
|
210
|
+
* the worker (compile-worker.js); tests run it on one thread.
|
|
211
|
+
* @param {string|ArrayBuffer|Uint8Array} input
|
|
212
|
+
* @param {{time?: number, progress?: number, quality?: number, sent?: Set<string>}} [opts]
|
|
213
|
+
* sent: keys (with quality) whose meshes this worker already sent; their meshes are not sent again
|
|
214
|
+
* @returns {{message: object, transfer: ArrayBuffer[]}}
|
|
215
|
+
*/
|
|
216
|
+
export function preparePayload(input, opts = {}) {
|
|
217
|
+
const cache = new GeometryCache();
|
|
218
|
+
if (opts.quality) cache.quality = opts.quality;
|
|
219
|
+
const result = compile(input, { cache, time: opts.time || 0, progress: opts.progress || 0 });
|
|
220
|
+
const lineIndex = result.doc && result.doc.lineIndex;
|
|
221
|
+
const base = {
|
|
222
|
+
ok: result.ok,
|
|
223
|
+
diagnostics: result.diagnostics.map(plainDiagnostic),
|
|
224
|
+
hostOnly: result.ok ? hostOnlyWarnings(result).map(plainDiagnostic) : [],
|
|
225
|
+
timings: result.timings,
|
|
226
|
+
// the text diagnostics point into, when it is not the input itself (decoded bytes)
|
|
227
|
+
lineText: lineIndex && typeof lineIndex.text === 'string' && lineIndex.text !== input ? lineIndex.text : null,
|
|
228
|
+
lineIndex: !!lineIndex,
|
|
229
|
+
};
|
|
230
|
+
if (!result.ok) return { message: base, transfer: [] };
|
|
231
|
+
const scene = result.scene;
|
|
232
|
+
if (scene.dynamic.length || scene.materials.some(textured)) return { message: { ...base, main: true }, transfer: [] };
|
|
233
|
+
|
|
234
|
+
const transfer = new Set();
|
|
235
|
+
const take = (a) => {
|
|
236
|
+
const o = own(a);
|
|
237
|
+
transfer.add(o.buffer);
|
|
238
|
+
return o;
|
|
239
|
+
};
|
|
240
|
+
const n = scene.count;
|
|
241
|
+
const rows = {
|
|
242
|
+
count: n,
|
|
243
|
+
matrices: take(scene.matrices.slice(0, n * 16)),
|
|
244
|
+
geom: take(scene.geom.slice(0, n)),
|
|
245
|
+
mat: take(scene.mat.slice(0, n)),
|
|
246
|
+
uid: take(scene.uid.slice(0, n)),
|
|
247
|
+
lodNode: take(scene.lodNode.slice(0, n)),
|
|
248
|
+
lodChild: take(scene.lodChild.slice(0, n)),
|
|
249
|
+
flags: take(scene.flags.slice(0, n)),
|
|
250
|
+
spheres: take(scene.spheres.slice(0, n * 4)),
|
|
251
|
+
};
|
|
252
|
+
const sent = opts.sent;
|
|
253
|
+
const geometries = packGeometries(scene.geometries, cache, sent);
|
|
254
|
+
for (const b of geometries.buffers) transfer.add(b);
|
|
255
|
+
const message = {
|
|
256
|
+
...base,
|
|
257
|
+
scene: {
|
|
258
|
+
rows,
|
|
259
|
+
geometries: geometries.columns,
|
|
260
|
+
meshes: geometries.packed,
|
|
261
|
+
materials: scene.materials.map((m) => ({ params: m.params, name: m.name })),
|
|
262
|
+
lights: scene.lights,
|
|
263
|
+
cameras: scene.cameras,
|
|
264
|
+
lodSwitches: scene.lodSwitches,
|
|
265
|
+
environment: scene.environment,
|
|
266
|
+
controls: scene.controls,
|
|
267
|
+
animation: scene.animation,
|
|
268
|
+
bounds: scene.bounds,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
return { message, transfer: [...transfer] };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A document compiled by prepare(), ready for stage.add() or stage.show().
|
|
276
|
+
* `ok` and `diagnostics` are known at once; diagnostics are those compile()
|
|
277
|
+
* reports (and, for `as: 'fragment'`, the stage's warnings for elements a
|
|
278
|
+
* fragment cannot set).
|
|
279
|
+
*/
|
|
280
|
+
export class Prepared {
|
|
281
|
+
/**
|
|
282
|
+
* @param {object} o
|
|
283
|
+
* @param {'fragment'|'document'} o.as
|
|
284
|
+
* @param {string|ArrayBuffer|Uint8Array} o.source
|
|
285
|
+
* @param {object|null} [o.payload] from preparePayload (a worker)
|
|
286
|
+
* @param {object|null} [o.result] a compile() result made on this thread
|
|
287
|
+
* @param {*} [o.owner] the cache owner holding that result's geometry
|
|
288
|
+
* @param {Diagnostic[]} [o.hostOnly]
|
|
289
|
+
*/
|
|
290
|
+
constructor(o) {
|
|
291
|
+
this.as = o.as || 'fragment';
|
|
292
|
+
this.source = o.source;
|
|
293
|
+
this.payload = o.payload || null;
|
|
294
|
+
this.result = o.result || null;
|
|
295
|
+
this.owner = o.owner ?? null;
|
|
296
|
+
this.used = false;
|
|
297
|
+
const p = this.payload;
|
|
298
|
+
if (p) {
|
|
299
|
+
this.ok = p.ok;
|
|
300
|
+
this.documentDiagnostics = p.diagnostics.map(diagnosticFrom);
|
|
301
|
+
this.hostOnly = p.hostOnly.map(diagnosticFrom);
|
|
302
|
+
this.timings = p.timings;
|
|
303
|
+
this.lineText = p.lineText;
|
|
304
|
+
this.hasLineIndex = p.lineIndex;
|
|
305
|
+
} else {
|
|
306
|
+
this.ok = this.result.ok;
|
|
307
|
+
this.documentDiagnostics = this.result.diagnostics;
|
|
308
|
+
this.hostOnly = o.hostOnly || [];
|
|
309
|
+
this.timings = this.result.timings;
|
|
310
|
+
this.lineText = null;
|
|
311
|
+
this.hasLineIndex = !!(this.result.doc && this.result.doc.lineIndex);
|
|
312
|
+
}
|
|
313
|
+
this.lineIndexCache = undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Diagnostics as the stage reports them for this use. */
|
|
317
|
+
get diagnostics() {
|
|
318
|
+
return this.as === 'fragment' ? [...this.documentDiagnostics, ...this.hostOnly] : this.documentDiagnostics;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Line positions for formatDiagnostics (made on first use; errors only need them). */
|
|
322
|
+
get lineIndex() {
|
|
323
|
+
if (this.lineIndexCache !== undefined) return this.lineIndexCache;
|
|
324
|
+
if (this.result && this.result.doc) return (this.lineIndexCache = this.result.doc.lineIndex);
|
|
325
|
+
const text = this.lineText || (typeof this.source === 'string' ? this.source : null);
|
|
326
|
+
return (this.lineIndexCache = this.hasLineIndex && text ? new LineIndex(text) : null);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Rebuild a worker's scene on this thread, with its geometry in `cache` held
|
|
332
|
+
* by `owner`. Costs what the scene's rows and geometry list are: the rows are
|
|
333
|
+
* the transferred arrays themselves.
|
|
334
|
+
* @param {object} payload a preparePayload message with ok and scene
|
|
335
|
+
* @param {GeometryCache} cache
|
|
336
|
+
* @param {*} owner
|
|
337
|
+
* @param {(key: string) => object} realDescriptor descriptor of a key, from the document compiled
|
|
338
|
+
* here, for a level the worker did not send (called on first use only)
|
|
339
|
+
* @returns {import('./flatten.js').RenderScene}
|
|
340
|
+
*/
|
|
341
|
+
export function sceneFromPayload(payload, cache, owner, realDescriptor) {
|
|
342
|
+
const p = payload.scene;
|
|
343
|
+
const scene = new RenderScene(1);
|
|
344
|
+
const r = p.rows;
|
|
345
|
+
scene.count = r.count;
|
|
346
|
+
scene.capacity = r.count;
|
|
347
|
+
scene.matrices = r.matrices;
|
|
348
|
+
scene.geom = r.geom;
|
|
349
|
+
scene.mat = r.mat;
|
|
350
|
+
scene.uid = r.uid;
|
|
351
|
+
scene.lodNode = r.lodNode;
|
|
352
|
+
scene.lodChild = r.lodChild;
|
|
353
|
+
scene.flags = r.flags;
|
|
354
|
+
scene.spheres = r.spheres;
|
|
355
|
+
const c = p.geometries;
|
|
356
|
+
const meshes = unpackMeshes(p.meshes);
|
|
357
|
+
const M = c.maxLods;
|
|
358
|
+
const reports = new Map(c.reports.map(([g, l, r]) => [g * M + l, r]));
|
|
359
|
+
const failed = new Set(c.failed.map(([g, l]) => g * M + l));
|
|
360
|
+
cache.beginBuild(owner);
|
|
361
|
+
for (let g = 0; g < c.keys.length; g++) {
|
|
362
|
+
const key = c.keys[g];
|
|
363
|
+
const lodDeviation = Array.from(c.deviation.subarray(g * M, g * M + c.devLength[g]));
|
|
364
|
+
const b = c.bounds;
|
|
365
|
+
const desc = {
|
|
366
|
+
key,
|
|
367
|
+
identity: c.identities[g],
|
|
368
|
+
kind: KINDS[c.kinds[g]],
|
|
369
|
+
lods: c.lods[g],
|
|
370
|
+
doubleSided: !!(c.flags[g] & 1),
|
|
371
|
+
bounds: c.flags[g] & 2 ? { min: [b[g * 6], b[g * 6 + 1], b[g * 6 + 2]], max: [b[g * 6 + 3], b[g * 6 + 4], b[g * 6 + 5]] } : null,
|
|
372
|
+
lodDeviation,
|
|
373
|
+
generate: (lod, quality, report) => realDescriptor(key).generate(lod, quality, report),
|
|
374
|
+
};
|
|
375
|
+
const entry = cache.acquire(desc, null);
|
|
376
|
+
for (let l = 0; l < desc.lods; l++) {
|
|
377
|
+
const k = c.levels[g * M + l];
|
|
378
|
+
if (k < 0 || entry.meshes[l]) continue;
|
|
379
|
+
if (failed.has(g * M + l)) {
|
|
380
|
+
entry.failed = true;
|
|
381
|
+
entry.failedLods |= 1 << l;
|
|
382
|
+
}
|
|
383
|
+
cache.adopt(entry, l, meshes[k], reports.get(g * M + l) || null);
|
|
384
|
+
}
|
|
385
|
+
scene.geometryIndex(entry);
|
|
386
|
+
}
|
|
387
|
+
cache.endBuild();
|
|
388
|
+
scene.materials = p.materials.map((m, i) => new Material(m.params, i, m.name));
|
|
389
|
+
scene.lights = p.lights;
|
|
390
|
+
scene.cameras = p.cameras;
|
|
391
|
+
scene.lodSwitches = p.lodSwitches;
|
|
392
|
+
scene.environment = p.environment;
|
|
393
|
+
scene.controls = p.controls;
|
|
394
|
+
scene.animation = p.animation;
|
|
395
|
+
scene.bounds = p.bounds;
|
|
396
|
+
scene.version = 1;
|
|
397
|
+
return scene;
|
|
398
|
+
}
|
package/src/scene/scene-graph.js
CHANGED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// A scene set: the instance tables a stage draws together, the host
|
|
2
|
+
// document's and one per fragment (stage.add).
|
|
3
|
+
//
|
|
4
|
+
// Each part keeps the RenderScene that compile() made for it, unchanged, so
|
|
5
|
+
// setTransforms, animation and ids work inside a part exactly as in a single
|
|
6
|
+
// document. Rows refer to their own document's geometry and material lists;
|
|
7
|
+
// the set keeps one list of each for all parts and a small map per part from
|
|
8
|
+
// its indices to the set's. The frame builder and picking read every placed,
|
|
9
|
+
// visible part through those maps (frame-builder.js, picking.js), so adding,
|
|
10
|
+
// hiding or removing a part never copies, moves or renumbers anyone's rows.
|
|
11
|
+
//
|
|
12
|
+
// Materials with identical parameters are one material across all parts (the
|
|
13
|
+
// specification asks readers to deduplicate them), and a geometry entry the
|
|
14
|
+
// host and several tiles use is one entry. Both lists count their users: a
|
|
15
|
+
// slot no part uses is freed, and its geometry is reported to the renderer
|
|
16
|
+
// (takeReleased) so the pool can repack once enough of it is unused.
|
|
17
|
+
//
|
|
18
|
+
// While nothing but the host is placed, the order of both lists is the order
|
|
19
|
+
// the host's own lists have, and parts placed in turn extend them in the order
|
|
20
|
+
// one merged document would: the picture is the same as showing that document.
|
|
21
|
+
|
|
22
|
+
import { Material } from '../material/materials.js';
|
|
23
|
+
|
|
24
|
+
const EMPTY_BOUNDS = () => ({ min: [-1, -1, -1], max: [1, 1, 1] });
|
|
25
|
+
|
|
26
|
+
/** One document drawn by a scene set: the host, or a fragment. */
|
|
27
|
+
export class ScenePart {
|
|
28
|
+
/**
|
|
29
|
+
* @param {string|null} id fragment id (null for the host)
|
|
30
|
+
* @param {'host'|'fragment'} kind
|
|
31
|
+
*/
|
|
32
|
+
constructor(id, kind = 'fragment') {
|
|
33
|
+
this.id = id;
|
|
34
|
+
this.kind = kind;
|
|
35
|
+
/** @type {import('./flatten.js').RenderScene|null} null until placed */
|
|
36
|
+
this.scene = null;
|
|
37
|
+
this.geomMap = null; // Uint32Array: part geometry index -> set index
|
|
38
|
+
this.matMap = null; // Uint32Array: part material index -> set index
|
|
39
|
+
this.visible = true;
|
|
40
|
+
// renderer.selection holds the host's element uids; fragments have their own
|
|
41
|
+
this.selectable = kind === 'host';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Placed and drawn. */
|
|
45
|
+
get drawn() {
|
|
46
|
+
return !!(this.scene && this.visible);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SceneSet {
|
|
51
|
+
constructor() {
|
|
52
|
+
/** @type {ScenePart[]} host first, then fragments in the order added */
|
|
53
|
+
this.parts = [];
|
|
54
|
+
this.host = null;
|
|
55
|
+
this.geometries = [];
|
|
56
|
+
this.geomIndex = new Map(); // entry -> set index
|
|
57
|
+
this.geomUsers = [];
|
|
58
|
+
this.freeGeometries = [];
|
|
59
|
+
this.materials = [];
|
|
60
|
+
this.matIndex = new Map(); // material key -> set index
|
|
61
|
+
this.matUsers = [];
|
|
62
|
+
this.freeMaterials = [];
|
|
63
|
+
this.released = []; // geometry entries no part uses any more
|
|
64
|
+
this.version = 0; // bumps when parts are placed, hidden or removed
|
|
65
|
+
this.boundsDirty = true;
|
|
66
|
+
this.cachedBounds = EMPTY_BOUNDS();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Add a part at the end (fragments) or at the front (the host). */
|
|
70
|
+
insert(part) {
|
|
71
|
+
if (part.kind === 'host') {
|
|
72
|
+
if (this.host) throw new Error('A scene set has one host.');
|
|
73
|
+
this.host = part;
|
|
74
|
+
this.parts.unshift(part);
|
|
75
|
+
} else this.parts.push(part);
|
|
76
|
+
this.changed();
|
|
77
|
+
return part;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Take a part out of the set and free what only it used. */
|
|
81
|
+
remove(part) {
|
|
82
|
+
const i = this.parts.indexOf(part);
|
|
83
|
+
if (i < 0) return false;
|
|
84
|
+
this.unplace(part);
|
|
85
|
+
this.parts.splice(i, 1);
|
|
86
|
+
if (part === this.host) this.host = null;
|
|
87
|
+
this.changed();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Draw a compiled scene as this part (replacing what it drew before).
|
|
93
|
+
* Costs what the part's own geometry and material lists do.
|
|
94
|
+
* @param {ScenePart} part
|
|
95
|
+
* @param {import('./flatten.js').RenderScene} scene
|
|
96
|
+
*/
|
|
97
|
+
place(part, scene) {
|
|
98
|
+
const geomMap = new Uint32Array(scene.geometries.length);
|
|
99
|
+
for (let k = 0; k < scene.geometries.length; k++) geomMap[k] = this.useGeometry(scene.geometries[k]);
|
|
100
|
+
const matMap = new Uint32Array(scene.materials.length);
|
|
101
|
+
for (let k = 0; k < scene.materials.length; k++) matMap[k] = this.useMaterial(scene.materials[k]);
|
|
102
|
+
// release the old content after taking the new, so what both use stays
|
|
103
|
+
this.unplace(part);
|
|
104
|
+
part.scene = scene;
|
|
105
|
+
part.geomMap = geomMap;
|
|
106
|
+
part.matMap = matMap;
|
|
107
|
+
this.changed();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Stop drawing a part's content and free what only it used. */
|
|
111
|
+
unplace(part) {
|
|
112
|
+
if (!part.scene) return;
|
|
113
|
+
for (const gi of part.geomMap) this.dropGeometry(gi);
|
|
114
|
+
for (const mi of part.matMap) this.dropMaterial(mi);
|
|
115
|
+
part.scene = null;
|
|
116
|
+
part.geomMap = null;
|
|
117
|
+
part.matMap = null;
|
|
118
|
+
this.changed();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setVisible(part, visible) {
|
|
122
|
+
const v = !!visible;
|
|
123
|
+
if (part.visible === v) return;
|
|
124
|
+
part.visible = v;
|
|
125
|
+
this.changed();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Something that changes bounds, lights or the drawn rows happened. */
|
|
129
|
+
changed() {
|
|
130
|
+
this.version++;
|
|
131
|
+
this.boundsDirty = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Bounds moved inside a part (setTransforms, animation). */
|
|
135
|
+
invalidateBounds() {
|
|
136
|
+
this.boundsDirty = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
useGeometry(entry) {
|
|
140
|
+
let i = this.geomIndex.get(entry);
|
|
141
|
+
if (i === undefined) {
|
|
142
|
+
i = this.freeGeometries.length ? this.freeGeometries.pop() : this.geometries.length;
|
|
143
|
+
this.geometries[i] = entry;
|
|
144
|
+
this.geomUsers[i] = 0;
|
|
145
|
+
this.geomIndex.set(entry, i);
|
|
146
|
+
}
|
|
147
|
+
this.geomUsers[i]++;
|
|
148
|
+
return i;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
dropGeometry(i) {
|
|
152
|
+
if (--this.geomUsers[i] > 0) return;
|
|
153
|
+
const entry = this.geometries[i];
|
|
154
|
+
this.geomIndex.delete(entry);
|
|
155
|
+
this.geometries[i] = null;
|
|
156
|
+
this.freeGeometries.push(i);
|
|
157
|
+
this.released.push(entry);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
useMaterial(m) {
|
|
161
|
+
let i = this.matIndex.get(m.key);
|
|
162
|
+
if (i === undefined) {
|
|
163
|
+
i = this.freeMaterials.length ? this.freeMaterials.pop() : this.materials.length;
|
|
164
|
+
this.materials[i] = new Material(m.params, i, m.name);
|
|
165
|
+
this.matUsers[i] = 0;
|
|
166
|
+
this.matIndex.set(m.key, i);
|
|
167
|
+
}
|
|
168
|
+
this.matUsers[i]++;
|
|
169
|
+
return i;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
dropMaterial(i) {
|
|
173
|
+
if (--this.matUsers[i] > 0) return;
|
|
174
|
+
this.matIndex.delete(this.materials[i].key);
|
|
175
|
+
this.materials[i] = null;
|
|
176
|
+
this.freeMaterials.push(i);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Geometry entries no part uses since the last call. */
|
|
180
|
+
takeReleased() {
|
|
181
|
+
const r = this.released;
|
|
182
|
+
this.released = [];
|
|
183
|
+
return r;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Placed, visible parts in drawing order. */
|
|
187
|
+
drawnParts() {
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const p of this.parts) if (p.scene && p.visible) out.push(p);
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Instance rows drawn. */
|
|
194
|
+
get count() {
|
|
195
|
+
let n = 0;
|
|
196
|
+
for (const p of this.parts) if (p.scene && p.visible) n += p.scene.count;
|
|
197
|
+
return n;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Union of the drawn parts' bounds, recomputed when something changed. */
|
|
201
|
+
get bounds() {
|
|
202
|
+
if (!this.boundsDirty) return this.cachedBounds;
|
|
203
|
+
const min = [Infinity, Infinity, Infinity];
|
|
204
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
205
|
+
for (const p of this.parts) {
|
|
206
|
+
// an empty part reports the ±1 stand-in flattenScene gives an empty scene
|
|
207
|
+
if (!p.scene || !p.visible || !p.scene.count) continue;
|
|
208
|
+
const b = p.scene.bounds;
|
|
209
|
+
for (let k = 0; k < 3; k++) {
|
|
210
|
+
if (b.min[k] < min[k]) min[k] = b.min[k];
|
|
211
|
+
if (b.max[k] > max[k]) max[k] = b.max[k];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.cachedBounds = min[0] <= max[0] ? { min, max } : EMPTY_BOUNDS();
|
|
215
|
+
this.boundsDirty = false;
|
|
216
|
+
return this.cachedBounds;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Lights of the drawn parts, host first, as one merged document lists them. */
|
|
220
|
+
get lights() {
|
|
221
|
+
const parts = this.parts;
|
|
222
|
+
if (parts.length === 1 && parts[0].scene && parts[0].visible) return parts[0].scene.lights;
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const p of parts) if (p.scene && p.visible) for (const l of p.scene.lights) out.push(l);
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Clocks the drawn parts' animations use. */
|
|
229
|
+
get animation() {
|
|
230
|
+
const a = { time: false, scroll: false };
|
|
231
|
+
for (const p of this.parts) {
|
|
232
|
+
if (!p.scene || !p.visible) continue;
|
|
233
|
+
if (p.scene.animation.time) a.time = true;
|
|
234
|
+
if (p.scene.animation.scroll) a.scroll = true;
|
|
235
|
+
}
|
|
236
|
+
return a;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// the host's alone: a fragment's <environment>, <camera> and <controls> are ignored
|
|
240
|
+
get environment() {
|
|
241
|
+
return this.host && this.host.scene ? this.host.scene.environment : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
get controls() {
|
|
245
|
+
return this.host && this.host.scene ? this.host.scene.controls : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
get cameras() {
|
|
249
|
+
return this.host && this.host.scene ? this.host.scene.cameras : [];
|
|
250
|
+
}
|
|
251
|
+
}
|