@volter/blender-engine 0.1.80 → 0.1.81

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.
@@ -0,0 +1,139 @@
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
+ }
@@ -43,6 +43,7 @@ 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';
46
47
  import { BlenderTransparentInstances } from './blender-transparent-instances';
47
48
  import { BlenderMaterialRanges } from './blender-material-ranges';
48
49
  import { BlenderMotionGeometry } from './blender-motion-geometry';
@@ -645,6 +646,7 @@ export interface PhotographRecord {
645
646
  export class BlenderRuntimeView {
646
647
  readonly root = new THREE.Group();
647
648
  private readonly instances = new BlenderRuntimeInstances(this.root);
649
+ private readonly depthInstances = new BlenderDepthInstances(this.root);
648
650
  private readonly materialRanges = new BlenderMaterialRanges();
649
651
  private readonly transparentInstances = new BlenderTransparentInstances(this.root);
650
652
  private readonly motionGeometry = new BlenderMotionGeometry(presenterChanged);
@@ -1231,6 +1233,7 @@ export class BlenderRuntimeView {
1231
1233
  }
1232
1234
 
1233
1235
  private rebuildInstances(): void {
1236
+ this.depthInstances.setObjects(this.drawBatching && !this.rendered ? this.objects.values() : []);
1234
1237
  this.transparentInstances.clear();
1235
1238
  this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
1236
1239
  if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
@@ -1289,6 +1292,15 @@ export class BlenderRuntimeView {
1289
1292
  };
1290
1293
  }
1291
1294
 
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
+
1292
1304
  private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
1293
1305
  const display = (id !== null ? this.frame?.materials[id]?.viewport : undefined) ?? DEFAULT_VIEWPORT_DISPLAY;
1294
1306
  const key = `${display.color.join(',')}|${display.roughness}|${display.metallic}|${side}`;
@@ -2094,7 +2106,7 @@ export class BlenderRuntimeView {
2094
2106
  }
2095
2107
 
2096
2108
  drawStatistics() {
2097
- return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
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()};
2098
2110
  }
2099
2111
 
2100
2112
  snapshot() {
@@ -2304,6 +2316,7 @@ export class BlenderRuntimeView {
2304
2316
  }
2305
2317
 
2306
2318
  private clear() {
2319
+ this.depthInstances.clear();
2307
2320
  this.materialRanges.clear();
2308
2321
  this.motionGeometry.clear();
2309
2322
  this.transparentInstances.clear();
@@ -318,6 +318,19 @@ 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
+
321
334
  finishDraw(): void {
322
335
  for (const mesh of this.hidden) mesh.layers.enable(0);
323
336
  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.80",
5
+ "version": "0.1.81",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },