@volter/blender-engine 0.1.68 → 0.1.70

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.
@@ -43,6 +43,9 @@ export class BlenderRuntimeInstances {
43
43
  private readonly inverse = new THREE.Matrix4();
44
44
  private readonly matrix = new THREE.Matrix4();
45
45
  private readonly box = new THREE.Box3();
46
+ private readonly projection = new THREE.Matrix4();
47
+ private readonly frustum = new THREE.Frustum();
48
+ private camera: THREE.Camera | null = null;
46
49
  private eligible = 0;
47
50
  private excluded: Record<string, number> = {};
48
51
 
@@ -137,6 +140,10 @@ export class BlenderRuntimeInstances {
137
140
  }
138
141
  mesh.layers.mask &= ~member.layers;
139
142
  if (!instanceObjectShown(mesh)) continue;
143
+ // Aggregate batch bounds alone admit off-screen members. Retain the
144
+ // same per-object rejection as ordinary draws before compacting slots.
145
+ if (this.camera && (!this.camera.layers.isEnabled(0) ||
146
+ (mesh.frustumCulled && !this.frustum.intersectsObject(mesh)))) continue;
140
147
  const offset = count * 16;
141
148
  if (this.matrix.elements.some((value, i) => Math.fround(value) !== draw.instanceMatrix.array[offset + i])) {
142
149
  draw.setMatrixAt(count, this.matrix);
@@ -154,7 +161,17 @@ export class BlenderRuntimeInstances {
154
161
  }
155
162
  }
156
163
 
164
+ prepareDraw(camera: THREE.Camera): void {
165
+ camera.updateMatrixWorld();
166
+ this.camera = camera;
167
+ this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
168
+ this.frustum.setFromProjectionMatrix(this.projection);
169
+ }
170
+
171
+ finishDraw(): void { this.camera = null; }
172
+
157
173
  clear(): void {
174
+ this.camera = null;
158
175
  for (const {draw, members} of this.batches) {
159
176
  for (const {mesh, layers} of members) mesh.layers.mask |= layers;
160
177
  draw.removeFromParent();
@@ -644,6 +644,7 @@ export class BlenderRuntimeView {
644
644
  readonly root = new THREE.Group();
645
645
  private readonly instances = new BlenderRuntimeInstances(this.root);
646
646
  private readonly transparentInstances = new BlenderTransparentInstances(this.root);
647
+ private drawBatching = true;
647
648
  private readonly objects = new Map<string, THREE.Object3D>();
648
649
  private readonly meshes = new Map<
649
650
  string,
@@ -1227,21 +1228,34 @@ export class BlenderRuntimeView {
1227
1228
 
1228
1229
  private rebuildInstances(): void {
1229
1230
  this.transparentInstances.clear();
1230
- this.instances.rebuild(this.objects.values(), !this.rendered);
1231
- if (!this.rendered) this.transparentInstances.setObjects(this.objects.values());
1231
+ this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
1232
+ if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
1233
+ }
1234
+
1235
+ /** Presentation-only comparison door; never changes Blender or its data. */
1236
+ setDrawBatching(enabled: boolean): void {
1237
+ if (enabled === this.drawBatching) return;
1238
+ this.drawBatching = enabled;
1239
+ this.rebuildInstances();
1240
+ presenterChanged();
1232
1241
  }
1233
1242
 
1234
1243
  /** Before the renderer uploads attributes/builds its queues, for this area's
1235
1244
  * actual camera. Transparent instance runs are camera-order dependent. */
1236
1245
  prepareDraw(camera: THREE.Camera): () => void {
1237
- this.root.updateMatrixWorld(true);
1238
1246
  try {
1247
+ this.instances.prepareDraw(camera);
1248
+ this.root.updateMatrixWorld(true);
1239
1249
  this.transparentInstances.prepare(camera);
1240
1250
  } catch (error) {
1251
+ this.instances.finishDraw();
1241
1252
  this.transparentInstances.finishDraw();
1242
1253
  throw error;
1243
1254
  }
1244
- return () => this.transparentInstances.finishDraw();
1255
+ return () => {
1256
+ this.instances.finishDraw();
1257
+ this.transparentInstances.finishDraw();
1258
+ };
1245
1259
  }
1246
1260
 
1247
1261
  private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
@@ -2049,7 +2063,7 @@ export class BlenderRuntimeView {
2049
2063
  }
2050
2064
 
2051
2065
  drawStatistics() {
2052
- return {opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
2066
+ return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
2053
2067
  }
2054
2068
 
2055
2069
  snapshot() {
@@ -3,9 +3,15 @@ import {graphDrawAttributes} from './blender-graph-material';
3
3
  import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
4
4
 
5
5
  const CAPACITY = 64;
6
- type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; z: number; centre: THREE.Vector3};
6
+ type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; groupOrder: number; z: number; centre: THREE.Vector3};
7
7
  type Draw = {mesh: THREE.InstancedMesh; members: THREE.Mesh[]};
8
8
 
9
+ function groupOrder(mesh: THREE.Object3D): number {
10
+ for (let parent = mesh.parent; parent; parent = parent.parent)
11
+ if ((parent as THREE.Group).isGroup) return parent.renderOrder;
12
+ return 0;
13
+ }
14
+
9
15
  /** Preserve Three's transparent object order. An instance run may contain only
10
16
  * consecutive, compatible, single-pass surfaces in that order. Incompatible
11
17
  * surfaces remain barriers; ties at a run boundary stay ordinary draws because
@@ -23,6 +29,10 @@ export class BlenderTransparentInstances {
23
29
  private readonly depth = new THREE.Vector4();
24
30
  private batches = 0;
25
31
  private instances = 0;
32
+ private readonly planState: (number | string)[] = [];
33
+ private readonly planned: Draw[] = [];
34
+ private stateIndex = 0;
35
+ private stateChanged = false;
26
36
 
27
37
  constructor(private readonly root: THREE.Group) {}
28
38
 
@@ -32,16 +42,29 @@ export class BlenderTransparentInstances {
32
42
  }
33
43
 
34
44
  prepare(camera: THREE.Camera): void {
35
- for (const mesh of this.hidden) mesh.layers.enable(0);
36
- this.hidden.length = 0;
45
+ this.finishDraw();
46
+ if (!this.objects.length) return;
47
+ camera.updateMatrixWorld();
48
+ this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
49
+ if (!this.planChanged(camera)) {
50
+ // The draw plan is a disposable presentation, not evaluated scene data.
51
+ // Reuse only after exact comparison of every input to culling/ordering;
52
+ // a camera move, drag, visibility or material change rebuilds it below.
53
+ for (const draw of this.planned) {
54
+ draw.mesh.visible = true;
55
+ for (const member of draw.members) {
56
+ member.layers.disable(0);
57
+ this.hidden.push(member);
58
+ }
59
+ }
60
+ return;
61
+ }
62
+ this.planned.length = 0;
37
63
  for (const pool of this.pools.values()) for (const draw of pool) {
38
64
  draw.mesh.visible = false;
39
65
  draw.members.length = 0;
40
66
  }
41
67
  this.batches = this.instances = 0;
42
- if (!this.objects.length) return;
43
- camera.updateMatrixWorld();
44
- this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
45
68
  this.frustum.setFromProjectionMatrix(this.projection);
46
69
  this.inverse.copy(this.root.matrixWorld).invert();
47
70
  const entries: Entry[] = [];
@@ -68,12 +91,14 @@ export class BlenderTransparentInstances {
68
91
  let material: THREE.Material | null = null;
69
92
  let key: string | null = null;
70
93
  const candidate = materials.length === 1 ? materials[0]! : null;
94
+ const order = groupOrder(mesh);
71
95
  if (candidate && candidate.visible && candidate.transparent &&
72
96
  !(candidate as THREE.ShaderMaterial).isShaderMaterial &&
73
97
  !((candidate as THREE.MeshPhysicalMaterial).transmission > 0) &&
74
98
  (candidate.side !== THREE.DoubleSide || candidate.forceSinglePass) &&
75
99
  !(mesh as THREE.SkinnedMesh).isSkinnedMesh && !mesh.morphTargetInfluences &&
76
- !(mesh as THREE.InstancedMesh).isInstancedMesh && mesh.layers.mask === 1) {
100
+ !(mesh as THREE.InstancedMesh).isInstancedMesh && mesh.layers.mask === 1 &&
101
+ order === this.root.renderOrder) {
77
102
  const geometry = mesh.geometry;
78
103
  // A single material draws the whole geometry. An array does so only
79
104
  // when its sole group covers the same range; preserve all other cases.
@@ -93,9 +118,9 @@ export class BlenderTransparentInstances {
93
118
  key = groupKey;
94
119
  }
95
120
  }
96
- entries.push({mesh, material, key, z, centre});
121
+ entries.push({mesh, material, key, groupOrder: order, z, centre});
97
122
  }
98
- entries.sort((a, b) => a.mesh.renderOrder - b.mesh.renderOrder || b.z - a.z || a.mesh.id - b.mesh.id);
123
+ entries.sort((a, b) => a.groupOrder - b.groupOrder || a.mesh.renderOrder - b.mesh.renderOrder || b.z - a.z || a.mesh.id - b.mesh.id);
99
124
  const used = new Map<string, number>();
100
125
  for (let begin = 0; begin < entries.length;) {
101
126
  const first = entries[begin]!;
@@ -155,6 +180,7 @@ export class BlenderTransparentInstances {
155
180
  }
156
181
  draw.mesh.count = length;
157
182
  draw.mesh.instanceMatrix.needsUpdate = true;
183
+ this.planned.push(draw);
158
184
  this.batches++;
159
185
  this.instances += length;
160
186
  }
@@ -165,6 +191,60 @@ export class BlenderTransparentInstances {
165
191
 
166
192
  inspect() { return {batches: this.batches, instances: this.instances}; }
167
193
 
194
+ private observe(value: number | string): void {
195
+ if (this.planState[this.stateIndex] !== value) {
196
+ this.planState[this.stateIndex] = value;
197
+ this.stateChanged = true;
198
+ }
199
+ this.stateIndex++;
200
+ }
201
+
202
+ /** Compare inputs without cloning 10,000 centres, allocating entries or
203
+ * sorting/uploading matrices again on an unchanged frame. No lossy hash. */
204
+ private planChanged(camera: THREE.Camera): boolean {
205
+ this.stateIndex = 0;
206
+ this.stateChanged = this.planState.length === 0;
207
+ this.observe(camera.layers.mask);
208
+ for (const n of this.projection.elements) this.observe(n);
209
+ for (const n of this.root.matrixWorld.elements) this.observe(n);
210
+ for (const mesh of this.objects) {
211
+ this.observe(mesh.parent?.id ?? -1);
212
+ this.observe(instanceObjectShown(mesh) ? 1 : 0);
213
+ this.observe(mesh.layers.mask);
214
+ this.observe(mesh.renderOrder);
215
+ this.observe(groupOrder(mesh));
216
+ this.observe(mesh.frustumCulled ? 1 : 0);
217
+ this.observe(mesh.morphTargetInfluences ? 1 : 0);
218
+ for (const n of mesh.matrixWorld.elements) this.observe(n);
219
+ const geometry = mesh.geometry;
220
+ this.observe(geometry.id);
221
+ this.observe(geometry.drawRange.start);
222
+ this.observe(geometry.drawRange.count);
223
+ this.observe(geometry.index?.count ?? geometry.getAttribute('position').count);
224
+ this.observe(geometry.groups.length);
225
+ for (const group of geometry.groups) {
226
+ this.observe(group.start); this.observe(group.count); this.observe(group.materialIndex ?? 0);
227
+ }
228
+ const sphere = (mesh as THREE.Mesh & {boundingSphere?: THREE.Sphere | null}).boundingSphere ?? geometry.boundingSphere;
229
+ this.observe(sphere?.center.x ?? 0); this.observe(sphere?.center.y ?? 0);
230
+ this.observe(sphere?.center.z ?? 0); this.observe(sphere?.radius ?? -1);
231
+ const materials = Array.isArray(mesh.material) ? mesh.material : null;
232
+ this.observe(materials?.length ?? -1);
233
+ if (materials) for (const material of materials) this.observeMaterial(material);
234
+ else this.observeMaterial(mesh.material as THREE.Material);
235
+ }
236
+ this.stateChanged ||= this.planState.length !== this.stateIndex;
237
+ this.planState.length = this.stateIndex;
238
+ return this.stateChanged;
239
+ }
240
+
241
+ private observeMaterial(material: THREE.Material): void {
242
+ this.observe(material.uuid); this.observe(material.version);
243
+ this.observe(material.visible ? 1 : 0); this.observe(material.transparent ? 1 : 0);
244
+ this.observe(material.side); this.observe(material.forceSinglePass ? 1 : 0);
245
+ this.observe((material as THREE.MeshPhysicalMaterial).transmission ?? 0);
246
+ }
247
+
168
248
  finishDraw(): void {
169
249
  for (const mesh of this.hidden) mesh.layers.enable(0);
170
250
  this.hidden.length = 0;
@@ -178,6 +258,8 @@ export class BlenderTransparentInstances {
178
258
  mesh.dispose();
179
259
  }
180
260
  this.pools.clear();
261
+ this.planned.length = 0;
262
+ this.planState.length = 0;
181
263
  this.objects = [];
182
264
  this.batches = this.instances = 0;
183
265
  }
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.68",
5
+ "version": "0.1.70",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },