@es3d/es3d 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/browser/es3d-stage.min.js +243 -10232
- package/browser/es3d.min.js +534 -0
- 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 +114 -77
- package/src/render/geometry-pool.js +103 -41
- package/src/render/renderer.js +28 -2
- package/src/render/stage.js +305 -50
- package/src/render/webgl2/webgl2-backend.js +68 -16
- package/src/render/webgpu/webgpu-backend.js +42 -11
- 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
|
@@ -171,23 +171,31 @@ export class FrameBuilder {
|
|
|
171
171
|
*/
|
|
172
172
|
build(p) {
|
|
173
173
|
const { scene, camera, pool, meshOf, options } = p;
|
|
174
|
-
|
|
174
|
+
// A SceneSet draws several instance tables (the host and the fragments,
|
|
175
|
+
// scene-set.js); a RenderScene is one part whose indices need no map.
|
|
176
|
+
const parts = scene.parts ? scene.drawnParts() : [{ scene, geomMap: null, matMap: null, selectable: true }];
|
|
177
|
+
let n = 0;
|
|
178
|
+
for (const part of parts) n += part.scene.count;
|
|
175
179
|
// reserve instance rows for: batches (<= n), bounding boxes (<= n), selection boxes
|
|
176
180
|
let selCount = 0;
|
|
177
181
|
if (options.selection && options.selection.size) {
|
|
178
|
-
for (
|
|
182
|
+
for (const part of parts) {
|
|
183
|
+
if (!part.selectable) continue;
|
|
184
|
+
const P = part.scene;
|
|
185
|
+
for (let i = 0; i < P.count; i++) if (options.selection.has(P.uid[i])) selCount++;
|
|
186
|
+
}
|
|
179
187
|
}
|
|
180
188
|
this.ensure(n + (options.bbox ? n : 0) + selCount + 64);
|
|
181
189
|
const camPos = camera.position;
|
|
182
190
|
const dist = (c) => Math.hypot(c[0] - camPos[0], c[1] - camPos[1], c[2] - camPos[2]);
|
|
183
|
-
const sw = this.lodStates(scene, camPos, dist);
|
|
184
191
|
const planes = options.culling === false ? null : planesFrom(camera.viewProj);
|
|
185
|
-
const S = scene.spheres;
|
|
186
192
|
const keys = this.keys;
|
|
187
193
|
const lods = this.lods;
|
|
188
194
|
const batchMap = new Map();
|
|
189
195
|
const batches = [];
|
|
190
|
-
const
|
|
196
|
+
const geometries = scene.geometries;
|
|
197
|
+
const materials = scene.materials;
|
|
198
|
+
const nMat = Math.max(1, materials.length);
|
|
191
199
|
const lodHist = [0, 0, 0, 0];
|
|
192
200
|
let visibleCount = 0;
|
|
193
201
|
let culled = 0;
|
|
@@ -197,52 +205,64 @@ export class FrameBuilder {
|
|
|
197
205
|
// pixels per world unit: depth based for perspective, constant for orthographic
|
|
198
206
|
const lodK = lodFrameConstants(camera, vh);
|
|
199
207
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// rows are numbered across the parts: part k's row r is base(k) + r
|
|
209
|
+
let base = 0;
|
|
210
|
+
for (const part of parts) {
|
|
211
|
+
const P = part.scene;
|
|
212
|
+
const gm = part.geomMap;
|
|
213
|
+
const mm = part.matMap;
|
|
214
|
+
const pn = P.count;
|
|
215
|
+
const sw = this.lodStates(P, camPos, dist);
|
|
216
|
+
const S = P.spheres;
|
|
217
|
+
for (let r = 0; r < pn; r++) {
|
|
218
|
+
const i = base + r;
|
|
219
|
+
keys[i] = -1;
|
|
220
|
+
const ln = P.lodNode[r];
|
|
221
|
+
if (ln >= 0 && !(sw.visible[ln] && sw.active[ln] === P.lodChild[r])) continue;
|
|
222
|
+
const cx = S[r * 4], cy = S[r * 4 + 1], cz = S[r * 4 + 2], rad = S[r * 4 + 3];
|
|
223
|
+
if (planes) {
|
|
224
|
+
let out = false;
|
|
225
|
+
for (let k = 0; k < 5; k++) {
|
|
226
|
+
const pl = planes[k];
|
|
227
|
+
if (pl[0] * cx + pl[1] * cy + pl[2] * cz + pl[3] < -rad) {
|
|
228
|
+
out = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (out) {
|
|
233
|
+
culled++;
|
|
234
|
+
continue;
|
|
212
235
|
}
|
|
213
236
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
const gi = gm ? gm[P.geom[r]] : P.geom[r];
|
|
238
|
+
const entry = geometries[gi];
|
|
239
|
+
let lod = 0;
|
|
240
|
+
if (entry.lods > 1) {
|
|
241
|
+
if (fixedLod >= 0) lod = Math.min(fixedLod, entry.lods - 1);
|
|
242
|
+
else {
|
|
243
|
+
const dx = cx - camPos[0], dy = cy - camPos[1], dz = cz - camPos[2];
|
|
244
|
+
const d = length3(dx, dy, dz);
|
|
245
|
+
const ppu = instancePixelsPerUnit(lodK, cx, cy, cz, d);
|
|
246
|
+
lod = selectLod(entry.lodDeviation, entry.lods, axisScale(P.matrices, r), ppu, bias);
|
|
247
|
+
}
|
|
217
248
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
249
|
+
lods[i] = lod;
|
|
250
|
+
lodHist[lod]++;
|
|
251
|
+
const mirrored = P.flags[r] & 1;
|
|
252
|
+
const mi = mm ? mm[P.mat[r]] : P.mat[r];
|
|
253
|
+
const key = ((gi * 4 + lod) * nMat + mi) * 2 + mirrored;
|
|
254
|
+
let b = batchMap.get(key);
|
|
255
|
+
if (b === undefined) {
|
|
256
|
+
b = batches.length;
|
|
257
|
+
batchMap.set(key, b);
|
|
258
|
+
const material = materials[mi];
|
|
259
|
+
batches.push({ gi, entry, lod, mi, material, mirrored: !!mirrored, count: 0, firstInstance: 0, depth: 0, instances: null });
|
|
229
260
|
}
|
|
261
|
+
batches[b].count++;
|
|
262
|
+
keys[i] = b;
|
|
263
|
+
visibleCount++;
|
|
230
264
|
}
|
|
231
|
-
|
|
232
|
-
lodHist[lod]++;
|
|
233
|
-
const mirrored = scene.flags[i] & 1;
|
|
234
|
-
const mi = scene.mat[i];
|
|
235
|
-
const key = ((gi * 4 + lod) * nMat + mi) * 2 + mirrored;
|
|
236
|
-
let b = batchMap.get(key);
|
|
237
|
-
if (b === undefined) {
|
|
238
|
-
b = batches.length;
|
|
239
|
-
batchMap.set(key, b);
|
|
240
|
-
const material = scene.materials[mi];
|
|
241
|
-
batches.push({ gi, entry, lod, mi, material, mirrored: !!mirrored, count: 0, firstInstance: 0, depth: 0, instances: null });
|
|
242
|
-
}
|
|
243
|
-
batches[b].count++;
|
|
244
|
-
keys[i] = b;
|
|
245
|
-
visibleCount++;
|
|
265
|
+
base += pn;
|
|
246
266
|
}
|
|
247
267
|
|
|
248
268
|
// resolve meshes / pool ranges. Once this frame has spent its tessellation
|
|
@@ -288,27 +308,32 @@ export class FrameBuilder {
|
|
|
288
308
|
b.cursor = b.firstInstance;
|
|
289
309
|
}
|
|
290
310
|
const data = this.instanceData;
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
for (
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
continue;
|
|
311
|
+
const transparent = []; // {M, r, b, depth}: drawn back to front after the opaque rows
|
|
312
|
+
base = 0;
|
|
313
|
+
for (const part of parts) {
|
|
314
|
+
const P = part.scene;
|
|
315
|
+
const M = P.matrices;
|
|
316
|
+
const S = P.spheres;
|
|
317
|
+
for (let r = 0; r < P.count; r++) {
|
|
318
|
+
const k = keys[base + r];
|
|
319
|
+
if (k < 0) continue;
|
|
320
|
+
const b = batches[k];
|
|
321
|
+
if (b.transparent) {
|
|
322
|
+
transparent.push({ M, r, b, depth: -length3(S[r * 4] - camPos[0], S[r * 4 + 1] - camPos[1], S[r * 4 + 2] - camPos[2]) });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const o = b.cursor++ * 16;
|
|
326
|
+
for (let j = 0; j < 16; j++) data[o + j] = M[r * 16 + j];
|
|
300
327
|
}
|
|
301
|
-
|
|
302
|
-
for (let j = 0; j < 16; j++) data[o + j] = M[i * 16 + j];
|
|
328
|
+
base += P.count;
|
|
303
329
|
}
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const b = batches[keys[i]];
|
|
330
|
+
if (transparent.length) {
|
|
331
|
+
transparent.sort((a, b) => a.depth - b.depth);
|
|
332
|
+
for (const t of transparent) {
|
|
333
|
+
const b = t.b;
|
|
309
334
|
const o = b.cursor++ * 16;
|
|
310
|
-
for (let j = 0; j < 16; j++) data[o + j] = M[
|
|
311
|
-
b.depth = Math.max(b.depth, -
|
|
335
|
+
for (let j = 0; j < 16; j++) data[o + j] = t.M[t.r * 16 + j];
|
|
336
|
+
b.depth = Math.max(b.depth, -t.depth);
|
|
312
337
|
}
|
|
313
338
|
// farthest transparent batches first
|
|
314
339
|
const opaque = order.filter((b) => !b.transparent);
|
|
@@ -326,12 +351,13 @@ export class FrameBuilder {
|
|
|
326
351
|
if (options.normals) overlays.normals.push({ range: pool.normalRange(b.mesh), firstInstance: b.firstInstance, count: b.count });
|
|
327
352
|
}
|
|
328
353
|
}
|
|
329
|
-
const boxMatrix = (
|
|
330
|
-
const
|
|
354
|
+
const boxMatrix = (part, r, o) => {
|
|
355
|
+
const P = part.scene;
|
|
356
|
+
const e = geometries[part.geomMap ? part.geomMap[P.geom[r]] : P.geom[r]];
|
|
331
357
|
const bb = e.bounds;
|
|
332
358
|
const cx = (bb.min[0] + bb.max[0]) / 2, cy = (bb.min[1] + bb.max[1]) / 2, cz = (bb.min[2] + bb.max[2]) / 2;
|
|
333
359
|
const sx = Math.max(bb.max[0] - bb.min[0], 1e-4), sy = Math.max(bb.max[1] - bb.min[1], 1e-4), sz = Math.max(bb.max[2] - bb.min[2], 1e-4);
|
|
334
|
-
const m =
|
|
360
|
+
const m = P.matrices.subarray(r * 16, r * 16 + 16);
|
|
335
361
|
// world * T(c) * S(s)
|
|
336
362
|
for (let k = 0; k < 3; k++) {
|
|
337
363
|
data[o + k] = m[k] * sx;
|
|
@@ -346,22 +372,33 @@ export class FrameBuilder {
|
|
|
346
372
|
};
|
|
347
373
|
if (options.bbox) {
|
|
348
374
|
const first = offset;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
375
|
+
base = 0;
|
|
376
|
+
for (const part of parts) {
|
|
377
|
+
for (let r = 0; r < part.scene.count; r++) {
|
|
378
|
+
if (keys[base + r] < 0) continue;
|
|
379
|
+
boxMatrix(part, r, offset * 16);
|
|
380
|
+
offset++;
|
|
381
|
+
}
|
|
382
|
+
base += part.scene.count;
|
|
353
383
|
}
|
|
354
384
|
overlays.bbox = { firstInstance: first, count: offset - first };
|
|
355
385
|
}
|
|
356
386
|
if (selCount) {
|
|
357
387
|
const first = offset;
|
|
358
388
|
const sel = options.selection;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
389
|
+
base = 0;
|
|
390
|
+
for (const part of parts) {
|
|
391
|
+
const P = part.scene;
|
|
392
|
+
if (part.selectable) {
|
|
393
|
+
for (let r = 0; r < P.count; r++) {
|
|
394
|
+
if (!sel.has(P.uid[r])) continue;
|
|
395
|
+
// hidden level-of-detail alternatives are not highlighted
|
|
396
|
+
if (keys[base + r] < 0 && P.lodNode[r] >= 0) continue;
|
|
397
|
+
boxMatrix(part, r, offset * 16);
|
|
398
|
+
offset++;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
base += P.count;
|
|
365
402
|
}
|
|
366
403
|
overlays.selection = { firstInstance: first, count: offset - first };
|
|
367
404
|
}
|
|
@@ -7,26 +7,93 @@
|
|
|
7
7
|
// vertex. That keeps the WebGL2 and WebGPU paths the same.
|
|
8
8
|
//
|
|
9
9
|
// The pool is CPU-side; backends upload the ranges appended since their last
|
|
10
|
-
// sync
|
|
10
|
+
// sync. The CPU copy is kept so a lost GPU context can be rebuilt.
|
|
11
|
+
//
|
|
12
|
+
// Growing and resetting are different events:
|
|
13
|
+
// - Growing keeps every byte and adds room. The CPU side adds a chunk (old
|
|
14
|
+
// chunks are never copied), and backends grow their buffers with a GPU-side
|
|
15
|
+
// copy of what they already hold, then upload only what is new. Vertex and
|
|
16
|
+
// index storage grow independently.
|
|
17
|
+
// - Resetting (reset(), a repack) throws the content away. It bumps
|
|
18
|
+
// `generation`, and backends reallocate and upload from zero.
|
|
11
19
|
|
|
12
20
|
export const VERTEX_FLOATS = 8; // position xyz, normal xyz, uv
|
|
13
21
|
export const VERTEX_BYTES = VERTEX_FLOATS * 4;
|
|
14
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A growable typed array stored as chunks. Element `i` of the whole array is
|
|
25
|
+
* in the chunk whose `start <= i < start + length`. Growing appends a chunk
|
|
26
|
+
* and never moves existing data.
|
|
27
|
+
*/
|
|
28
|
+
class ChunkedArray {
|
|
29
|
+
constructor(Type, stride, capacity) {
|
|
30
|
+
this.Type = Type;
|
|
31
|
+
this.stride = stride; // array entries per element (8 floats per vertex, 1 per index)
|
|
32
|
+
this.chunks = [{ start: 0, length: capacity, data: new Type(capacity * stride) }];
|
|
33
|
+
this.capacity = capacity; // elements
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Make room for `count` elements in total. Returns true when it grew. */
|
|
37
|
+
reserve(count) {
|
|
38
|
+
if (count <= this.capacity) return false;
|
|
39
|
+
let cap = Math.max(1, this.capacity); // a zero capacity would never double
|
|
40
|
+
while (cap < count) cap *= 2;
|
|
41
|
+
const length = cap - this.capacity;
|
|
42
|
+
this.chunks.push({ start: this.capacity, length, data: new this.Type(length * this.stride) });
|
|
43
|
+
this.capacity = cap;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Visit the parts of elements [start, end) chunk by chunk:
|
|
49
|
+
* fn(firstElement, view), where view is the chunk's typed array restricted
|
|
50
|
+
* to that part.
|
|
51
|
+
*/
|
|
52
|
+
forEach(start, end, fn) {
|
|
53
|
+
const s = this.stride;
|
|
54
|
+
for (const c of this.chunks) {
|
|
55
|
+
const a = Math.max(start, c.start);
|
|
56
|
+
const b = Math.min(end, c.start + c.length);
|
|
57
|
+
if (a < b) fn(a, c.data.subarray((a - c.start) * s, (b - c.start) * s));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
15
62
|
export class GeometryPool {
|
|
16
63
|
constructor(vertexCapacity = 1 << 16, indexCapacity = 1 << 17) {
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
64
|
+
this.vertexStore = new ChunkedArray(Float32Array, VERTEX_FLOATS, vertexCapacity);
|
|
65
|
+
this.indexStore = new ChunkedArray(Uint32Array, 1, indexCapacity);
|
|
19
66
|
this.vertexCount = 0;
|
|
20
67
|
this.indexCount = 0;
|
|
21
68
|
this.ranges = new Map(); // key object -> {firstIndex, indexCount}
|
|
22
69
|
this.edgeRanges = new Map();
|
|
23
70
|
this.normalRanges = new Map();
|
|
24
71
|
// dirty tracking for incremental uploads
|
|
25
|
-
this.generation = 0; // bumps on
|
|
72
|
+
this.generation = 0; // bumps when the content is thrown away (reset, repack); NOT on growth
|
|
26
73
|
this.uploadedVertices = 0;
|
|
27
74
|
this.uploadedIndices = 0;
|
|
28
75
|
}
|
|
29
76
|
|
|
77
|
+
/** Vertex capacity in vertices. Backends size their vertex buffer to at least this. */
|
|
78
|
+
get vertexCapacity() {
|
|
79
|
+
return this.vertexStore.capacity;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Index capacity in indices. */
|
|
83
|
+
get indexCapacity() {
|
|
84
|
+
return this.indexStore.capacity;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Capacity in bytes of the vertex storage. */
|
|
88
|
+
get vertexCapacityBytes() {
|
|
89
|
+
return this.vertexStore.capacity * VERTEX_BYTES;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Capacity in bytes of the index storage. */
|
|
93
|
+
get indexCapacityBytes() {
|
|
94
|
+
return this.indexStore.capacity * 4;
|
|
95
|
+
}
|
|
96
|
+
|
|
30
97
|
reset() {
|
|
31
98
|
this.vertexCount = 0;
|
|
32
99
|
this.indexCount = 0;
|
|
@@ -42,60 +109,55 @@ export class GeometryPool {
|
|
|
42
109
|
return this.vertexCount * VERTEX_BYTES + this.indexCount * 4;
|
|
43
110
|
}
|
|
44
111
|
|
|
112
|
+
/** Make room for more vertices and indices. Growing keeps what is stored and uploaded. */
|
|
45
113
|
ensure(extraV, extraI) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let cap = this.vertices.length / VERTEX_FLOATS;
|
|
49
|
-
while (cap < this.vertexCount + extraV) cap *= 2;
|
|
50
|
-
const v = new Float32Array(cap * VERTEX_FLOATS);
|
|
51
|
-
v.set(this.vertices.subarray(0, this.vertexCount * VERTEX_FLOATS));
|
|
52
|
-
this.vertices = v;
|
|
53
|
-
grew = true;
|
|
54
|
-
}
|
|
55
|
-
if (this.indexCount + extraI > this.indices.length) {
|
|
56
|
-
let cap = this.indices.length;
|
|
57
|
-
while (cap < this.indexCount + extraI) cap *= 2;
|
|
58
|
-
const i = new Uint32Array(cap);
|
|
59
|
-
i.set(this.indices.subarray(0, this.indexCount));
|
|
60
|
-
this.indices = i;
|
|
61
|
-
grew = true;
|
|
62
|
-
}
|
|
63
|
-
if (grew) {
|
|
64
|
-
this.generation++;
|
|
65
|
-
this.uploadedVertices = 0;
|
|
66
|
-
this.uploadedIndices = 0;
|
|
67
|
-
}
|
|
114
|
+
this.vertexStore.reserve(this.vertexCount + extraV);
|
|
115
|
+
this.indexStore.reserve(this.indexCount + extraI);
|
|
68
116
|
}
|
|
69
117
|
|
|
70
118
|
appendVertices(mesh) {
|
|
71
119
|
const n = mesh.vertexCount;
|
|
72
120
|
const base = this.vertexCount;
|
|
73
|
-
const v = this.vertices;
|
|
74
121
|
const p = mesh.positions, nr = mesh.normals, t = mesh.uvs;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
this.vertexStore.forEach(base, base + n, (first, v) => {
|
|
123
|
+
const count = v.length / VERTEX_FLOATS;
|
|
124
|
+
let o = 0;
|
|
125
|
+
for (let i = first - base, end = i + count; i < end; i++) {
|
|
126
|
+
v[o] = p[i * 3];
|
|
127
|
+
v[o + 1] = p[i * 3 + 1];
|
|
128
|
+
v[o + 2] = p[i * 3 + 2];
|
|
129
|
+
v[o + 3] = nr[i * 3];
|
|
130
|
+
v[o + 4] = nr[i * 3 + 1];
|
|
131
|
+
v[o + 5] = nr[i * 3 + 2];
|
|
132
|
+
v[o + 6] = t[i * 2];
|
|
133
|
+
v[o + 7] = t[i * 2 + 1];
|
|
134
|
+
o += VERTEX_FLOATS;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
87
137
|
this.vertexCount += n;
|
|
88
138
|
return base;
|
|
89
139
|
}
|
|
90
140
|
|
|
91
141
|
appendIndices(src, base) {
|
|
92
142
|
const first = this.indexCount;
|
|
93
|
-
|
|
94
|
-
|
|
143
|
+
this.indexStore.forEach(first, first + src.length, (at, dst) => {
|
|
144
|
+
const off = at - first;
|
|
145
|
+
for (let i = 0; i < dst.length; i++) dst[i] = src[off + i] + base;
|
|
146
|
+
});
|
|
95
147
|
this.indexCount += src.length;
|
|
96
148
|
return first;
|
|
97
149
|
}
|
|
98
150
|
|
|
151
|
+
/** Visit stored vertices [start, end) chunk by chunk: fn(firstVertex, Float32Array). */
|
|
152
|
+
forEachVertices(start, end, fn) {
|
|
153
|
+
this.vertexStore.forEach(start, end, fn);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Visit stored indices [start, end) chunk by chunk: fn(firstIndex, Uint32Array). */
|
|
157
|
+
forEachIndices(start, end, fn) {
|
|
158
|
+
this.indexStore.forEach(start, end, fn);
|
|
159
|
+
}
|
|
160
|
+
|
|
99
161
|
/** Range of a mesh's primitives (triangles or lines). */
|
|
100
162
|
meshRange(mesh) {
|
|
101
163
|
let r = this.ranges.get(mesh);
|
package/src/render/renderer.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// The facade is the only renderer API used by the application and the
|
|
6
6
|
// benchmark; backends never see the ES3D document, only a RenderScene.
|
|
7
7
|
|
|
8
|
-
import { GeometryPool } from './geometry-pool.js';
|
|
8
|
+
import { GeometryPool, VERTEX_BYTES } from './geometry-pool.js';
|
|
9
9
|
import { FrameBuilder } from './frame-builder.js';
|
|
10
10
|
import { TextureCache } from './texture-cache.js';
|
|
11
11
|
import { FRAME_FLOATS, packFrameUniforms } from './lighting.js';
|
|
@@ -106,14 +106,40 @@ export class Renderer {
|
|
|
106
106
|
setScene(scene, meshOf, { keepGeometry = true } = {}) {
|
|
107
107
|
this.scene = scene;
|
|
108
108
|
this.meshOf = meshOf;
|
|
109
|
-
if (!scene || !keepGeometry || this.pool.byteSize > POOL_REPACK_BYTES) {
|
|
109
|
+
if (!scene || !keepGeometry || (!scene.parts && this.pool.byteSize > POOL_REPACK_BYTES)) {
|
|
110
110
|
this.pool.reset();
|
|
111
|
+
this.pool.unusedBytes = 0;
|
|
111
112
|
this.cubeRange = this.pool.meshRange(this.cube);
|
|
112
113
|
}
|
|
113
114
|
this.backend.releaseMaterials(new Set(scene ? scene.materials : []));
|
|
114
115
|
this.stats.totalUploadBytes = 0;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The scene set being drawn changed (a fragment placed, hidden or removed):
|
|
120
|
+
* keep every packed mesh, free the materials no part uses, and repack the
|
|
121
|
+
* pool only once most of what it holds is geometry no part draws any more.
|
|
122
|
+
* Cheap: nothing here is proportional to the scene.
|
|
123
|
+
* @param {import('../scene/scene-set.js').SceneSet} set
|
|
124
|
+
*/
|
|
125
|
+
updateScene(set) {
|
|
126
|
+
for (const entry of set.takeReleased()) {
|
|
127
|
+
if (set.geomIndex.has(entry)) continue; // taken again by another part
|
|
128
|
+
for (const mesh of entry.meshes) {
|
|
129
|
+
if (!mesh) continue;
|
|
130
|
+
const r = this.pool.ranges.get(mesh);
|
|
131
|
+
if (r) this.pool.unusedBytes = (this.pool.unusedBytes || 0) + mesh.vertexCount * VERTEX_BYTES + r.indexCount * 4;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (this.pool.byteSize > POOL_REPACK_BYTES && (this.pool.unusedBytes || 0) * 2 > this.pool.byteSize) {
|
|
135
|
+
this.pool.reset();
|
|
136
|
+
this.pool.unusedBytes = 0;
|
|
137
|
+
this.cubeRange = this.pool.meshRange(this.cube);
|
|
138
|
+
}
|
|
139
|
+
if (this.materialsSeen !== set.materials.length || set.freeMaterials.length) this.backend.releaseMaterials(new Set(set.materials));
|
|
140
|
+
this.materialsSeen = set.materials.length;
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
/**
|
|
118
144
|
* Render one frame.
|
|
119
145
|
* @param {import('./camera.js').Camera} camera
|