@volter/blender-engine 0.1.81 → 0.1.82

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.
@@ -4,15 +4,16 @@ export const ORDERED_BATCH_CAPACITY = 64;
4
4
  const FAMILY_BYTES = 4 * 1024 * 1024;
5
5
  const CACHE_BYTES = 32 * 1024 * 1024;
6
6
  const MAX_DRAWS = 1024;
7
- type Shape = {geometry: THREE.BufferGeometry; stamp: string};
7
+ type Shape = {geometry: THREE.BufferGeometry; stamp: string; bytes: number};
8
8
  type Family = {material: THREE.Material; shapes: Map<number, Shape>; template: THREE.BatchedMesh | null;
9
- ids: Map<number, number>; draws: THREE.BatchedMesh[]; dirty: boolean; bytes: number; refused: boolean};
9
+ ids: Map<number, number>; draws: THREE.BatchedMesh[]; dirty: boolean; bytes: number; reservedBytes: number; refused: boolean};
10
10
 
11
11
  /** Immutable packed attributes are shared between ordered draw runs. Only the
12
12
  * matrix/indirection textures and command lists belong to an individual run.
13
13
  * All interaction and authored geometry remain on the canonical meshes. */
14
14
  export class BlenderOrderedBatches {
15
15
  private readonly families = new Map<string, Family>();
16
+ private readonly partitions = new Map<string, string[]>();
16
17
  private readonly registrations = new Map<string, string | null>();
17
18
  private bytes = 0;
18
19
  private drawCount = 0;
@@ -43,16 +44,26 @@ export class BlenderOrderedBatches {
43
44
  bytes += attribute.array.byteLength;
44
45
  }
45
46
  if (bytes > FAMILY_BYTES) return null;
46
- const key = `multi:${material.uuid}:${order}:${JSON.stringify(layout)}`;
47
+ const layoutKey = `multi:${material.uuid}:${order}:${JSON.stringify(layout)}`;
48
+ let keys = this.partitions.get(layoutKey);
49
+ if (!keys) { keys = []; this.partitions.set(layoutKey, keys); }
50
+ // A new compatible shape must not evict every existing run when their
51
+ // union exceeds one bounded allocation (notably full + navigation copies).
52
+ // Partition deterministically, preserving established geometry membership.
53
+ let key = keys.find(key => this.families.get(key)!.shapes.has(geometry.id));
54
+ key ??= keys.find(key => this.families.get(key)!.reservedBytes + bytes <= FAMILY_BYTES);
55
+ if (!key) { key = `${layoutKey}:${keys.length}`; keys.push(key); }
47
56
  let family = this.families.get(key);
48
57
  if (!family) {
49
- family = {material, shapes: new Map(), template: null, ids: new Map(), draws: [], dirty: true, bytes: 0, refused: false};
58
+ family = {material, shapes: new Map(), template: null, ids: new Map(), draws: [], dirty: true,
59
+ bytes: 0, reservedBytes: 0, refused: false};
50
60
  this.families.set(key, family);
51
61
  }
52
62
  const stamp = JSON.stringify(versions);
53
63
  const held = family.shapes.get(geometry.id);
54
64
  if (!held || held.stamp !== stamp) {
55
- family.shapes.set(geometry.id, {geometry, stamp});
65
+ family.reservedBytes += bytes - (held?.bytes ?? 0);
66
+ family.shapes.set(geometry.id, {geometry, stamp, bytes});
56
67
  family.dirty = true;
57
68
  }
58
69
  this.registrations.set(registration, key);
@@ -150,6 +161,7 @@ export class BlenderOrderedBatches {
150
161
  clear(): void {
151
162
  for (const family of this.families.values()) this.release(family);
152
163
  this.families.clear();
164
+ this.partitions.clear();
153
165
  this.registrations.clear();
154
166
  this.pending = false;
155
167
  }
@@ -43,7 +43,6 @@ import {
43
43
  } from './blender-runtime-camera-view';
44
44
  import { DEFAULT_VIEWPORT_DISPLAY, workbenchMaterial } from './blender-workbench-material';
45
45
  import { BlenderRuntimeInstances } from './blender-runtime-instances';
46
- import { BlenderDepthInstances } from './blender-depth-instances';
47
46
  import { BlenderTransparentInstances } from './blender-transparent-instances';
48
47
  import { BlenderMaterialRanges } from './blender-material-ranges';
49
48
  import { BlenderMotionGeometry } from './blender-motion-geometry';
@@ -646,7 +645,6 @@ export interface PhotographRecord {
646
645
  export class BlenderRuntimeView {
647
646
  readonly root = new THREE.Group();
648
647
  private readonly instances = new BlenderRuntimeInstances(this.root);
649
- private readonly depthInstances = new BlenderDepthInstances(this.root);
650
648
  private readonly materialRanges = new BlenderMaterialRanges();
651
649
  private readonly transparentInstances = new BlenderTransparentInstances(this.root);
652
650
  private readonly motionGeometry = new BlenderMotionGeometry(presenterChanged);
@@ -1233,7 +1231,6 @@ export class BlenderRuntimeView {
1233
1231
  }
1234
1232
 
1235
1233
  private rebuildInstances(): void {
1236
- this.depthInstances.setObjects(this.drawBatching && !this.rendered ? this.objects.values() : []);
1237
1234
  this.transparentInstances.clear();
1238
1235
  this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
1239
1236
  if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
@@ -1292,15 +1289,6 @@ export class BlenderRuntimeView {
1292
1289
  };
1293
1290
  }
1294
1291
 
1295
- prepareDepthDraw(camera: THREE.Camera, material: THREE.MeshDepthMaterial): void | (() => void) {
1296
- if (!this.drawBatching || this.rendered || !this.depthInstances.supports(material)) return;
1297
- const resumeColor = this.transparentInstances.suspendDraw();
1298
- const restore = () => { try { this.depthInstances.finish(); } finally { resumeColor(); } };
1299
- try { this.depthInstances.prepare(camera, material); }
1300
- catch (error) { restore(); throw error; }
1301
- return restore;
1302
- }
1303
-
1304
1292
  private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
1305
1293
  const display = (id !== null ? this.frame?.materials[id]?.viewport : undefined) ?? DEFAULT_VIEWPORT_DISPLAY;
1306
1294
  const key = `${display.color.join(',')}|${display.roughness}|${display.metallic}|${side}`;
@@ -2106,7 +2094,7 @@ export class BlenderRuntimeView {
2106
2094
  }
2107
2095
 
2108
2096
  drawStatistics() {
2109
- return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, depth: this.depthInstances.inspect(), materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
2097
+ return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
2110
2098
  }
2111
2099
 
2112
2100
  snapshot() {
@@ -2316,7 +2304,6 @@ export class BlenderRuntimeView {
2316
2304
  }
2317
2305
 
2318
2306
  private clear() {
2319
- this.depthInstances.clear();
2320
2307
  this.materialRanges.clear();
2321
2308
  this.motionGeometry.clear();
2322
2309
  this.transparentInstances.clear();
@@ -318,19 +318,6 @@ export class BlenderTransparentInstances {
318
318
  this.observe((material as THREE.MeshPhysicalMaterial).transmission ?? 0);
319
319
  }
320
320
 
321
- /** A plain depth override does not need color transparency ordering. Reveal
322
- * only this plan's canonical members, retaining exact masks for restoration. */
323
- suspendDraw(): () => void {
324
- const members = this.hidden.map(mesh => ({mesh, mask: mesh.layers.mask}));
325
- const draws = this.planned.map(({mesh}) => ({mesh, visible: mesh.visible}));
326
- for (const {mesh} of members) mesh.layers.enable(0);
327
- for (const {mesh} of draws) mesh.visible = false;
328
- return () => {
329
- for (const {mesh, mask} of members) mesh.layers.mask = mask;
330
- for (const {mesh, visible} of draws) mesh.visible = visible;
331
- };
332
- }
333
-
334
321
  finishDraw(): void {
335
322
  for (const mesh of this.hidden) mesh.layers.enable(0);
336
323
  this.hidden.length = 0;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/blender-engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "GPL-3.0-or-later",
5
- "version": "0.1.81",
5
+ "version": "0.1.82",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
@@ -1,139 +0,0 @@
1
- import * as THREE from 'three';
2
- import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
3
-
4
- const CAPACITY = 64;
5
- const MAX_DRAWS = 1024; // At most 4 MiB of instance matrices; no vertex copies.
6
-
7
- /** Opaque depth overrides discard authored color materials. Geometry-compatible
8
- * objects can share a draw here even when their color draws cannot be reordered.
9
- * Only the current pass changes; selection layers and canonical meshes survive. */
10
- export class BlenderDepthInstances {
11
- private objects: THREE.Mesh[] = [];
12
- private readonly pools = new Map<THREE.BufferGeometry, THREE.InstancedMesh[]>();
13
- private readonly hidden: {mesh: THREE.Mesh; mask: number}[] = [];
14
- private readonly active: THREE.InstancedMesh[] = [];
15
- private readonly inverse = new THREE.Matrix4();
16
- private readonly matrix = new THREE.Matrix4();
17
- private readonly projection = new THREE.Matrix4();
18
- private readonly frustum = new THREE.Frustum();
19
- private allocated = 0;
20
- private statistics = {batches: 0, instances: 0, preparationMs: 0};
21
-
22
- constructor(private readonly root: THREE.Group) {}
23
-
24
- supports(material: THREE.MeshDepthMaterial): boolean {
25
- return material.isMeshDepthMaterial && material.visible && !material.transparent &&
26
- !material.map && !material.alphaMap && !material.displacementMap &&
27
- !material.alphaHash && material.alphaTest === 0 && !material.wireframe &&
28
- !material.stencilWrite && material.depthWrite && material.depthTest &&
29
- !material.clippingPlanes?.length && !material.polygonOffset &&
30
- (material.depthFunc === THREE.LessDepth || material.depthFunc === THREE.LessEqualDepth) &&
31
- (material.blending === THREE.NormalBlending || material.blending === THREE.NoBlending) &&
32
- material.onBeforeCompile === THREE.Material.prototype.onBeforeCompile &&
33
- material.onBeforeRender === THREE.Material.prototype.onBeforeRender;
34
- }
35
-
36
- setObjects(objects: Iterable<THREE.Object3D>): void {
37
- this.clear();
38
- this.objects = [...objects].filter((object): object is THREE.Mesh => (object as THREE.Mesh).isMesh === true);
39
- }
40
-
41
- prepare(camera: THREE.Camera, material: THREE.MeshDepthMaterial): void {
42
- this.finish();
43
- const start = performance.now();
44
- this.statistics = {batches: 0, instances: 0, preparationMs: 0};
45
- if (!camera.layers.isEnabled(0)) return;
46
- this.inverse.copy(this.root.matrixWorld).invert();
47
- this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
48
- this.frustum.setFromProjectionMatrix(this.projection);
49
- const groups = new Map<THREE.BufferGeometry, THREE.Mesh[]>();
50
- for (const mesh of this.objects) {
51
- // A selected object has a separate mask layer, and the outline pass also
52
- // disables its default layer. Never pull it into the occluder draw.
53
- if (mesh.layers.mask !== 1 || !instanceObjectShown(mesh) ||
54
- (mesh as THREE.SkinnedMesh).isSkinnedMesh ||
55
- (mesh as THREE.InstancedMesh).isInstancedMesh ||
56
- (mesh as THREE.BatchedMesh).isBatchedMesh || mesh.morphTargetInfluences ||
57
- mesh.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender ||
58
- mesh.onAfterRender !== THREE.Object3D.prototype.onAfterRender) continue;
59
- const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
60
- if (materials.length !== 1 || !materials[0]?.visible) continue;
61
- const geometry = mesh.geometry;
62
- const count = geometry.index?.count ?? geometry.getAttribute('position')?.count ?? 0;
63
- if (!count || (Array.isArray(mesh.material) && !(geometry.groups.length === 1 &&
64
- geometry.groups[0]!.start === 0 && geometry.groups[0]!.count === count &&
65
- geometry.groups[0]!.materialIndex === 0))) continue;
66
- if (mesh.frustumCulled && !this.frustum.intersectsObject(mesh)) continue;
67
- this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
68
- if (!instanceMatrixSupported(this.matrix)) continue;
69
- const group = groups.get(geometry);
70
- if (group) group.push(mesh); else groups.set(geometry, [mesh]);
71
- }
72
- // Do not retain pools for obsolete LOD/canonical geometry indefinitely.
73
- for (const [geometry, pool] of this.pools) if (!groups.has(geometry)) {
74
- for (const draw of pool) { draw.removeFromParent(); draw.dispose(); this.allocated--; }
75
- this.pools.delete(geometry);
76
- }
77
- for (const [geometry, members] of groups) {
78
- if (members.length < 2) continue;
79
- let pool = this.pools.get(geometry);
80
- if (!pool) { pool = []; this.pools.set(geometry, pool); }
81
- for (let offset = 0; offset < members.length; offset += CAPACITY) {
82
- const count = Math.min(CAPACITY, members.length - offset);
83
- if (count < 2) continue;
84
- const slot = offset / CAPACITY;
85
- let draw = pool[slot];
86
- if (!draw) {
87
- if (this.allocated >= MAX_DRAWS) continue;
88
- draw = new THREE.InstancedMesh(geometry, material, CAPACITY);
89
- draw.name = 'Blender depth instances';
90
- draw.userData['engineInternal'] = true;
91
- draw.matrixAutoUpdate = false;
92
- draw.frustumCulled = false; // Members were individually culled above.
93
- draw.boundingSphere = new THREE.Sphere();
94
- draw.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
95
- draw.raycast = () => {};
96
- this.root.add(draw);
97
- pool.push(draw);
98
- this.allocated++;
99
- }
100
- draw.material = material;
101
- draw.matrixWorld.copy(this.root.matrixWorld);
102
- draw.count = count;
103
- draw.visible = true;
104
- this.active.push(draw);
105
- for (let i = 0; i < count; i++) {
106
- const mesh = members[offset + i]!;
107
- this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
108
- draw.setMatrixAt(i, this.matrix);
109
- this.hidden.push({mesh, mask: mesh.layers.mask});
110
- mesh.layers.disable(0);
111
- }
112
- draw.instanceMatrix.needsUpdate = true;
113
- this.statistics.batches++;
114
- this.statistics.instances += count;
115
- }
116
- }
117
- this.statistics.preparationMs = performance.now() - start;
118
- }
119
-
120
- finish(): void {
121
- for (const {mesh, mask} of this.hidden) mesh.layers.mask = mask;
122
- this.hidden.length = 0;
123
- for (const draw of this.active) draw.visible = false;
124
- this.active.length = 0;
125
- }
126
-
127
- clear(): void {
128
- this.finish();
129
- for (const pool of this.pools.values()) for (const draw of pool) {
130
- draw.removeFromParent();
131
- draw.dispose(); // Shared geometry and override material belong to callers.
132
- }
133
- this.pools.clear();
134
- this.objects = [];
135
- this.allocated = 0;
136
- }
137
-
138
- inspect() { return {...this.statistics, allocated: this.allocated}; }
139
- }