@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.
@@ -171,23 +171,31 @@ export class FrameBuilder {
171
171
  */
172
172
  build(p) {
173
173
  const { scene, camera, pool, meshOf, options } = p;
174
- const n = scene.count;
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 (let i = 0; i < n; i++) if (options.selection.has(scene.uid[i])) selCount++;
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 nMat = Math.max(1, scene.materials.length);
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
- for (let i = 0; i < n; i++) {
201
- keys[i] = -1;
202
- const ln = scene.lodNode[i];
203
- if (ln >= 0 && !(sw.visible[ln] && sw.active[ln] === scene.lodChild[i])) continue;
204
- const cx = S[i * 4], cy = S[i * 4 + 1], cz = S[i * 4 + 2], r = S[i * 4 + 3];
205
- if (planes) {
206
- let out = false;
207
- for (let k = 0; k < 5; k++) {
208
- const pl = planes[k];
209
- if (pl[0] * cx + pl[1] * cy + pl[2] * cz + pl[3] < -r) {
210
- out = true;
211
- break;
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
- if (out) {
215
- culled++;
216
- continue;
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
- const gi = scene.geom[i];
220
- const entry = scene.geometries[gi];
221
- let lod = 0;
222
- if (entry.lods > 1) {
223
- if (fixedLod >= 0) lod = Math.min(fixedLod, entry.lods - 1);
224
- else {
225
- const dx = cx - camPos[0], dy = cy - camPos[1], dz = cz - camPos[2];
226
- const d = length3(dx, dy, dz);
227
- const ppu = instancePixelsPerUnit(lodK, cx, cy, cz, d);
228
- lod = selectLod(entry.lodDeviation, entry.lods, axisScale(scene.matrices, i), ppu, bias);
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
- lods[i] = lod;
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 M = scene.matrices;
292
- const transparentInstances = [];
293
- for (let i = 0; i < n; i++) {
294
- const k = keys[i];
295
- if (k < 0) continue;
296
- const b = batches[k];
297
- if (b.transparent) {
298
- transparentInstances.push(i);
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
- const o = b.cursor++ * 16;
302
- for (let j = 0; j < 16; j++) data[o + j] = M[i * 16 + j];
328
+ base += P.count;
303
329
  }
304
- if (transparentInstances.length) {
305
- const depthOf = (i) => -length3(S[i * 4] - camPos[0], S[i * 4 + 1] - camPos[1], S[i * 4 + 2] - camPos[2]);
306
- transparentInstances.sort((a, b) => depthOf(a) - depthOf(b));
307
- for (const i of transparentInstances) {
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[i * 16 + j];
311
- b.depth = Math.max(b.depth, -depthOf(i));
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 = (i, o) => {
330
- const e = scene.geometries[scene.geom[i]];
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 = M.subarray(i * 16, i * 16 + 16);
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
- for (let i = 0; i < n; i++) {
350
- if (keys[i] < 0) continue;
351
- boxMatrix(i, offset * 16);
352
- offset++;
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
- for (let i = 0; i < n; i++) {
360
- if (!sel.has(scene.uid[i])) continue;
361
- // hidden level-of-detail alternatives are not highlighted
362
- if (keys[i] < 0 && scene.lodNode[i] >= 0) continue;
363
- boxMatrix(i, offset * 16);
364
- offset++;
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 (or everything after a reallocation).
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.vertices = new Float32Array(vertexCapacity * VERTEX_FLOATS);
18
- this.indices = new Uint32Array(indexCapacity);
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 reallocation
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
- let grew = false;
47
- if ((this.vertexCount + extraV) * VERTEX_FLOATS > this.vertices.length) {
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
- let o = base * VERTEX_FLOATS;
76
- for (let i = 0; i < n; i++) {
77
- v[o] = p[i * 3];
78
- v[o + 1] = p[i * 3 + 1];
79
- v[o + 2] = p[i * 3 + 2];
80
- v[o + 3] = nr[i * 3];
81
- v[o + 4] = nr[i * 3 + 1];
82
- v[o + 5] = nr[i * 3 + 2];
83
- v[o + 6] = t[i * 2];
84
- v[o + 7] = t[i * 2 + 1];
85
- o += VERTEX_FLOATS;
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
- const dst = this.indices;
94
- for (let i = 0; i < src.length; i++) dst[first + i] = src[i] + base;
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);
@@ -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