@es3d/es3d 1.1.2 → 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.
@@ -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
  }
@@ -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