@volter/blender-engine 0.1.91 → 0.1.93
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.
|
@@ -9,16 +9,17 @@ const CHUNK_INDICES = 49_152;
|
|
|
9
9
|
const COPY_ITEMS = 8_192;
|
|
10
10
|
const cancelled = Symbol('navigation geometry replaced');
|
|
11
11
|
type Range = {start: number; count: number; materialIndex?: number | undefined};
|
|
12
|
-
type Entry = {geometry: THREE.BufferGeometry; error: number; bytes: number};
|
|
12
|
+
type Entry = {geometry: THREE.BufferGeometry; error: number; bytes: number; sharedAttributes: boolean};
|
|
13
|
+
type Job = {geometry: THREE.BufferGeometry; fine: boolean};
|
|
13
14
|
|
|
14
15
|
/** A disposable navigation drawing, never Blender's evaluated geometry.
|
|
15
16
|
* Copies are derived off-thread, in importance order, one bounded mesh at a
|
|
16
17
|
* time. Full meshes remain resident and are restored after EVERY draw. */
|
|
17
18
|
export class BlenderMotionGeometry {
|
|
18
19
|
private groups = new Map<THREE.BufferGeometry, THREE.Mesh[]>();
|
|
19
|
-
private readonly cache = new Map<THREE.BufferGeometry, Entry>();
|
|
20
|
+
private readonly cache = new Map<THREE.BufferGeometry, Entry[]>();
|
|
20
21
|
private readonly swapped: {mesh: THREE.Mesh; geometry: THREE.BufferGeometry}[] = [];
|
|
21
|
-
private queue:
|
|
22
|
+
private queue: Job[] = [];
|
|
22
23
|
private worker: Worker | null = null;
|
|
23
24
|
private pending: {id: number; resolve: (response: MotionMeshResponse) => void} | null = null;
|
|
24
25
|
private generation = 0;
|
|
@@ -29,7 +30,7 @@ export class BlenderMotionGeometry {
|
|
|
29
30
|
private nextIdentity = 0;
|
|
30
31
|
private bytes = 0;
|
|
31
32
|
private unavailable: string | null = null;
|
|
32
|
-
private reductions: {vertices: number; fullTriangles: number; reducedTriangles: number; error: number; accepted: boolean; bytes?: number}[] = [];
|
|
33
|
+
private reductions: {vertices: number; fullTriangles: number; reducedTriangles: number; error: number; fine: boolean; accepted: boolean; bytes?: number}[] = [];
|
|
33
34
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
34
35
|
private readonly cameraWorld = new THREE.Matrix4();
|
|
35
36
|
private readonly cameraProjection = new THREE.Matrix4();
|
|
@@ -92,14 +93,15 @@ export class BlenderMotionGeometry {
|
|
|
92
93
|
// that costs most draws, not its name or its order in the .blend.
|
|
93
94
|
if (candidates.reduce((n, [g, meshes]) => n + g.index!.count * meshes.length, 0) < 6_000_000) return;
|
|
94
95
|
candidates.sort(([a, am], [b, bm]) => b.index!.count * bm.length - a.index!.count * am.length);
|
|
95
|
-
this.queue = candidates.map(([
|
|
96
|
+
this.queue = candidates.map(([geometry]) => ({geometry, fine: false}));
|
|
96
97
|
this.next();
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
private async next(): Promise<void> {
|
|
100
101
|
if (this.building || this.unavailable || this.bytes >= CACHE_BYTES) return;
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
102
|
+
const job = this.queue.shift();
|
|
103
|
+
if (!job) { this.worker?.terminate(); this.worker = null; return; }
|
|
104
|
+
const {geometry, fine} = job;
|
|
103
105
|
this.building = true;
|
|
104
106
|
const generation = this.generation;
|
|
105
107
|
const current = () => {
|
|
@@ -175,7 +177,7 @@ export class BlenderMotionGeometry {
|
|
|
175
177
|
}
|
|
176
178
|
if (!geometry.boundingSphere) return;
|
|
177
179
|
const absoluteError = Math.max(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y,
|
|
178
|
-
bounds.max.z - bounds.min.z) * 0.002;
|
|
180
|
+
bounds.max.z - bounds.min.z) * (fine ? 0.0005 : 0.002);
|
|
179
181
|
const parts: Uint32Array[] = [];
|
|
180
182
|
const outputGroups: Range[] = [];
|
|
181
183
|
let count = 0, error = 0;
|
|
@@ -228,12 +230,14 @@ export class BlenderMotionGeometry {
|
|
|
228
230
|
if (multi) outputGroups.push({start: groupStart, count: count - groupStart, materialIndex: range.materialIndex});
|
|
229
231
|
}
|
|
230
232
|
const reduction: (typeof this.reductions)[number] = {vertices: position.count, fullTriangles: geometry.index!.count / 3,
|
|
231
|
-
reducedTriangles: count / 3, error, accepted: false};
|
|
233
|
+
reducedTriangles: count / 3, error, fine, accepted: false};
|
|
232
234
|
if (this.reductions.length < 32) this.reductions.push(reduction);
|
|
233
235
|
if (!count || count >= geometry.index!.count * 0.75) return;
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
236
|
+
// A finer level borrows the resident vertex columns and owns only its
|
|
237
|
+
// indices. This avoids another large attribute allocation beside the
|
|
238
|
+
// coarse copy; source versions still invalidate both levels together.
|
|
239
|
+
// Coarse copies remain compact for bounded packed draw families.
|
|
240
|
+
const remap = fine ? null : new Int32Array(position.count).fill(-1);
|
|
237
241
|
const originals: number[] = [];
|
|
238
242
|
const indices = new Uint32Array(count);
|
|
239
243
|
let offset = 0;
|
|
@@ -242,20 +246,21 @@ export class BlenderMotionGeometry {
|
|
|
242
246
|
await yieldCopy();
|
|
243
247
|
for (let i = first; i < Math.min(first + COPY_ITEMS, part.length); i++) {
|
|
244
248
|
const original = part[i]!;
|
|
245
|
-
let local = remap[original]
|
|
246
|
-
if (local < 0) { local = originals.length; originals.push(original); remap[original] = local; }
|
|
249
|
+
let local = remap ? remap[original]! : original;
|
|
250
|
+
if (local < 0) { local = originals.length; originals.push(original); remap![original] = local; }
|
|
247
251
|
indices[offset + i] = local;
|
|
248
252
|
}
|
|
249
253
|
}
|
|
250
254
|
offset += part.length;
|
|
251
255
|
}
|
|
252
|
-
const bytes = indices.byteLength + Object.values(geometry.attributes).reduce((n, a) =>
|
|
253
|
-
n + originals.length * a.itemSize * a.array.BYTES_PER_ELEMENT, 0);
|
|
256
|
+
const bytes = indices.byteLength + (fine ? 0 : Object.values(geometry.attributes).reduce((n, a) =>
|
|
257
|
+
n + originals.length * a.itemSize * a.array.BYTES_PER_ELEMENT, 0));
|
|
254
258
|
reduction.bytes = bytes;
|
|
255
259
|
if (this.bytes + bytes > CACHE_BYTES) return;
|
|
256
260
|
const copy = new THREE.BufferGeometry();
|
|
257
261
|
copy.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
258
262
|
for (const [name, source] of Object.entries(geometry.attributes)) {
|
|
263
|
+
if (fine) { copy.setAttribute(name, source); continue; }
|
|
259
264
|
const attribute = source as THREE.BufferAttribute;
|
|
260
265
|
const ArrayType = attribute.array.constructor as {new(length: number): THREE.TypedArray};
|
|
261
266
|
const data = new ArrayType(originals.length * attribute.itemSize);
|
|
@@ -272,8 +277,15 @@ export class BlenderMotionGeometry {
|
|
|
272
277
|
for (const group of outputGroups) copy.addGroup(group.start, group.count, group.materialIndex);
|
|
273
278
|
copy.userData = {...geometry.userData};
|
|
274
279
|
reduction.accepted = true;
|
|
275
|
-
this.cache.
|
|
280
|
+
const levels = this.cache.get(geometry) ?? [];
|
|
281
|
+
levels.push({geometry: copy, error, bytes, sharedAttributes: fine});
|
|
282
|
+
this.cache.set(geometry, levels);
|
|
276
283
|
this.bytes += bytes;
|
|
284
|
+
// Large repeated shapes can be cheap at distance yet still cost millions
|
|
285
|
+
// of triangles just beyond the coarse copy's pixel gate. Build one finer
|
|
286
|
+
// level after the initial queue; every placement keeps the same gate.
|
|
287
|
+
if (!fine && geometry.index!.count >= 300_000 && count < geometry.index!.count * 0.25)
|
|
288
|
+
this.queue.push({geometry, fine: true});
|
|
277
289
|
} catch (error) {
|
|
278
290
|
if (error === cancelled) return;
|
|
279
291
|
this.unavailable = String(error);
|
|
@@ -306,10 +318,10 @@ export class BlenderMotionGeometry {
|
|
|
306
318
|
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
307
319
|
this.frustum.setFromProjectionMatrix(this.projection);
|
|
308
320
|
let tooClose = 0;
|
|
309
|
-
for (const [source,
|
|
321
|
+
for (const [source, levels] of this.cache) {
|
|
310
322
|
// The copy carries the ORIGINAL bounds derived during its yielding
|
|
311
323
|
// build. Do not scan vertices or use the smaller simplified bounds.
|
|
312
|
-
const bounds =
|
|
324
|
+
const bounds = levels[0]!.geometry.boundingBox!;
|
|
313
325
|
bounds.getSize(this.halfExtent).multiplyScalar(0.5);
|
|
314
326
|
for (const mesh of this.groups.get(source) ?? []) {
|
|
315
327
|
if (!instanceObjectShown(mesh) || (mesh.frustumCulled && !this.frustum.intersectsObject(mesh))) continue;
|
|
@@ -342,8 +354,12 @@ export class BlenderMotionGeometry {
|
|
|
342
354
|
// camera. This tightens the bound without relaxing the pixel gate.
|
|
343
355
|
depth = Math.max(sphereDepth, -this.boxCentre.z - radiusZ);
|
|
344
356
|
}
|
|
345
|
-
const
|
|
346
|
-
|
|
357
|
+
const pixelsPerUnit = scale * Math.abs(camera.projectionMatrix.elements[5]!) * height / (2 * depth);
|
|
358
|
+
let entry: Entry | undefined;
|
|
359
|
+
if (depth > 0) for (const level of levels) {
|
|
360
|
+
if (level.error * pixelsPerUnit <= 1) { entry = level; break; }
|
|
361
|
+
}
|
|
362
|
+
if (!entry) { tooClose++; continue; }
|
|
347
363
|
// Decide per placement: one near-camera blade must not veto thousands
|
|
348
364
|
// of distant placements of its shared geometry. The following draw
|
|
349
365
|
// planner sees these geometry identities, so transparency still batches
|
|
@@ -366,7 +382,8 @@ export class BlenderMotionGeometry {
|
|
|
366
382
|
this.swapped.length = 0;
|
|
367
383
|
}
|
|
368
384
|
inspect() {
|
|
369
|
-
return {copies: this.cache.size,
|
|
385
|
+
return {copies: this.cache.size, levels: [...this.cache.values()].reduce((n, levels) => n + levels.length, 0),
|
|
386
|
+
bytes: this.bytes, queued: this.queue.length, pending: this.building,
|
|
370
387
|
unavailable: this.unavailable, reductions: this.reductions.map(r => ({...r})), lastNavigation: {...this.lastNavigation}};
|
|
371
388
|
}
|
|
372
389
|
private releaseCopies(): void {
|
|
@@ -377,7 +394,13 @@ export class BlenderMotionGeometry {
|
|
|
377
394
|
this.worker?.terminate(); this.worker = null;
|
|
378
395
|
this.pending?.resolve({id: this.pending.id, refusal: 'Navigation geometry replaced'});
|
|
379
396
|
this.pending = null; this.building = false; this.queue = [];
|
|
380
|
-
for (const
|
|
397
|
+
for (const levels of this.cache.values()) for (const entry of levels) {
|
|
398
|
+
// Geometry.dispose releases its attributes' GPU buffers. A borrowed
|
|
399
|
+
// column remains owned by the original geometry and must stay resident.
|
|
400
|
+
if (entry.sharedAttributes)
|
|
401
|
+
for (const name of Object.keys(entry.geometry.attributes)) entry.geometry.deleteAttribute(name);
|
|
402
|
+
entry.geometry.dispose();
|
|
403
|
+
}
|
|
381
404
|
this.cache.clear(); this.bytes = 0; this.reductions = [];
|
|
382
405
|
}
|
|
383
406
|
clear(): void {
|