@volter/blender-engine 0.1.73 → 0.1.74

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.
@@ -33,8 +33,9 @@ export class BlenderMotionGeometry {
33
33
  private drawnMeshes = 0;
34
34
  private fullTriangles = 0;
35
35
  private motionTriangles = 0;
36
+ private lastNavigation = {meshes: 0, fullTriangles: 0, motionTriangles: 0, tooClose: 0};
36
37
 
37
- constructor(private readonly root: THREE.Group, private readonly invalidate: () => void) {}
38
+ constructor(private readonly invalidate: () => void) {}
38
39
 
39
40
  setObjects(objects: Iterable<THREE.Object3D>): void {
40
41
  const groups = new Map<THREE.BufferGeometry, THREE.Mesh[]>();
@@ -45,7 +46,9 @@ export class BlenderMotionGeometry {
45
46
  const g = mesh.geometry;
46
47
  const position = g.getAttribute('position');
47
48
  if (!g.index || g.index.count < 6144 || !position || position.count > 200_000 ||
48
- Object.values(g.attributes).some(a => !(a instanceof THREE.BufferAttribute)) ||
49
+ Object.values(g.attributes).some(a => !(a instanceof THREE.BufferAttribute) ||
50
+ (a as THREE.BufferAttribute & {isFloat16BufferAttribute?: boolean}).isFloat16BufferAttribute ||
51
+ (a as THREE.InstancedBufferAttribute).isInstancedBufferAttribute) ||
49
52
  Object.keys(g.morphAttributes).length || g.drawRange.start !== 0 || g.drawRange.count < g.index.count) continue;
50
53
  // Only a whole, single material surface. Material boundaries are never
51
54
  // crossed by the simplifier, nor are authored group ranges shortened.
@@ -166,10 +169,10 @@ export class BlenderMotionGeometry {
166
169
  this.timer = setTimeout(() => { this.timer = null; this.next(); }, 0);
167
170
  }
168
171
 
169
- prepare(camera: THREE.Camera, interactive: boolean, height: number): void {
172
+ prepare(camera: THREE.Camera, interactive: boolean, height: number): boolean {
170
173
  this.finish();
171
174
  this.drawnMeshes = this.fullTriangles = this.motionTriangles = 0;
172
- if (!interactive || !(height > 0)) return;
175
+ if (!interactive || !(height > 0)) return false;
173
176
  const now = performance.now();
174
177
  const moved = this.cameraKnown && (!camera.matrixWorld.equals(this.cameraWorld) ||
175
178
  !camera.projectionMatrix.equals(this.cameraProjection));
@@ -182,44 +185,37 @@ export class BlenderMotionGeometry {
182
185
  if (this.quietTimer) clearTimeout(this.quietTimer);
183
186
  this.quietTimer = setTimeout(() => { this.quietTimer = null; this.invalidate(); }, QUIET_MS + 1);
184
187
  }
185
- if (now - this.movedAt > QUIET_MS) return;
188
+ if (now - this.movedAt > QUIET_MS) return false;
186
189
  this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
187
190
  this.frustum.setFromProjectionMatrix(this.projection);
188
- const allowed = new Map<THREE.BufferGeometry, THREE.BufferGeometry>();
191
+ let tooClose = 0;
189
192
  for (const [source, entry] of this.cache) {
190
- let fits = true;
191
193
  for (const mesh of this.groups.get(source) ?? []) {
192
194
  if (!instanceObjectShown(mesh) || (mesh.frustumCulled && !this.frustum.intersectsObject(mesh))) continue;
193
- const scale = mesh.matrixWorld.getMaxScaleOnAxis();
195
+ // Frobenius norm bounds the linear transform even under shear; the
196
+ // longest axis alone can understate the projected simplifier error.
197
+ const e = mesh.matrixWorld.elements;
198
+ const scale = Math.hypot(e[0]!, e[1]!, e[2]!, e[4]!, e[5]!, e[6]!, e[8]!, e[9]!, e[10]!);
194
199
  const sphere = source.boundingSphere!;
195
200
  this.centre.copy(sphere.center).applyMatrix4(mesh.matrixWorld).applyMatrix4(camera.matrixWorldInverse);
196
201
  const depth = (camera as THREE.PerspectiveCamera).isPerspectiveCamera
197
202
  ? -this.centre.z - sphere.radius * scale : 1;
198
203
  const pixels = entry.error * scale * Math.abs(camera.projectionMatrix.elements[5]!) * height / (2 * depth);
199
- if (!(depth > 0) || pixels > 1) { fits = false; break; }
204
+ if (!(depth > 0) || pixels > 1) { tooClose++; continue; }
205
+ // Decide per placement: one near-camera blade must not veto thousands
206
+ // of distant placements of its shared geometry. The following draw
207
+ // planner sees these geometry identities, so transparency still batches
208
+ // only consecutive compatible surfaces in the original camera order.
209
+ this.swapped.push({mesh, geometry: source});
210
+ mesh.geometry = entry.geometry;
211
+ this.drawnMeshes++;
212
+ this.fullTriangles += source.index!.count / 3;
213
+ this.motionTriangles += entry.geometry.index!.count / 3;
200
214
  }
201
- if (fits) allowed.set(source, entry.geometry);
202
215
  }
203
- // After instancing/culling chose its exact members: both ordinary and
204
- // internal draws select the same temporary geometry for this camera.
205
- this.root.traverse(object => {
206
- const mesh = object as THREE.Mesh;
207
- if (!mesh.isMesh || (mesh as THREE.SkinnedMesh).isSkinnedMesh || mesh.morphTargetInfluences ||
208
- ((mesh as THREE.InstancedMesh).isInstancedMesh && mesh.userData['engineInternal'] !== true)) return;
209
- if (Array.isArray(mesh.material) && (mesh.material.length !== 1 || mesh.geometry.groups.length !== 1 ||
210
- mesh.geometry.groups[0]!.start !== 0 || mesh.geometry.groups[0]!.count !== mesh.geometry.index?.count ||
211
- mesh.geometry.groups[0]!.materialIndex !== 0)) return;
212
- const copy = allowed.get(mesh.geometry);
213
- if (!copy) return;
214
- this.swapped.push({mesh, geometry: mesh.geometry});
215
- if (mesh.visible && mesh.layers.isEnabled(0)) {
216
- const count = (mesh as THREE.InstancedMesh).isInstancedMesh ? (mesh as THREE.InstancedMesh).count : 1;
217
- this.drawnMeshes += count;
218
- this.fullTriangles += mesh.geometry.index!.count / 3 * count;
219
- this.motionTriangles += copy.index!.count / 3 * count;
220
- }
221
- mesh.geometry = copy;
222
- });
216
+ this.lastNavigation = {meshes: this.drawnMeshes, fullTriangles: this.fullTriangles,
217
+ motionTriangles: this.motionTriangles, tooClose};
218
+ return this.swapped.length > 0;
223
219
  }
224
220
 
225
221
  private quietTimer: ReturnType<typeof setTimeout> | null = null;
@@ -229,7 +225,7 @@ export class BlenderMotionGeometry {
229
225
  }
230
226
  inspect() {
231
227
  return {copies: this.cache.size, bytes: this.bytes, queued: this.queue.length, pending: this.pending !== null,
232
- unavailable: this.unavailable, lastDraw: {meshes: this.drawnMeshes, fullTriangles: this.fullTriangles, motionTriangles: this.motionTriangles}};
228
+ unavailable: this.unavailable, lastNavigation: {...this.lastNavigation}};
233
229
  }
234
230
  private releaseCopies(): void {
235
231
  this.finish();
@@ -645,7 +645,7 @@ export class BlenderRuntimeView {
645
645
  readonly root = new THREE.Group();
646
646
  private readonly instances = new BlenderRuntimeInstances(this.root);
647
647
  private readonly transparentInstances = new BlenderTransparentInstances(this.root);
648
- private readonly motionGeometry = new BlenderMotionGeometry(this.root, presenterChanged);
648
+ private readonly motionGeometry = new BlenderMotionGeometry(presenterChanged);
649
649
  private drawBatching = true;
650
650
  private readonly objects = new Map<string, THREE.Object3D>();
651
651
  private readonly meshes = new Map<
@@ -1249,8 +1249,14 @@ export class BlenderRuntimeView {
1249
1249
  try {
1250
1250
  this.instances.prepareDraw(camera);
1251
1251
  this.root.updateMatrixWorld(true);
1252
+ const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
1253
+ options?.interactive === true, options?.height ?? 0);
1254
+ // A changed navigation geometry is an ordinary-draw fallback for an
1255
+ // opaque batch; its full-detail members remain instanced. Transparent
1256
+ // runs are planned AFTER per-placement geometry selection, preserving
1257
+ // their sorting barriers and compatible geometry grouping.
1258
+ if (motion) this.instances.sync();
1252
1259
  this.transparentInstances.prepare(camera);
1253
- this.motionGeometry.prepare(camera, !this.rendered && options?.interactive === true, options?.height ?? 0);
1254
1260
  } catch (error) {
1255
1261
  this.motionGeometry.finish();
1256
1262
  this.instances.finishDraw();
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.73",
5
+ "version": "0.1.74",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },