@volter/blender-engine 0.1.79 → 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
|
+
}
|
|
@@ -38,6 +38,9 @@ export class BlenderMotionGeometry {
|
|
|
38
38
|
private readonly projection = new THREE.Matrix4();
|
|
39
39
|
private readonly frustum = new THREE.Frustum();
|
|
40
40
|
private readonly centre = new THREE.Vector3();
|
|
41
|
+
private readonly boxCentre = new THREE.Vector3();
|
|
42
|
+
private readonly halfExtent = new THREE.Vector3();
|
|
43
|
+
private readonly viewWorld = new THREE.Matrix4();
|
|
41
44
|
private drawnMeshes = 0;
|
|
42
45
|
private fullTriangles = 0;
|
|
43
46
|
private motionTriangles = 0;
|
|
@@ -304,13 +307,19 @@ export class BlenderMotionGeometry {
|
|
|
304
307
|
this.frustum.setFromProjectionMatrix(this.projection);
|
|
305
308
|
let tooClose = 0;
|
|
306
309
|
for (const [source, entry] of this.cache) {
|
|
310
|
+
// The copy carries the ORIGINAL bounds derived during its yielding
|
|
311
|
+
// build. Do not scan vertices or use the smaller simplified bounds.
|
|
312
|
+
const bounds = entry.geometry.boundingBox!;
|
|
313
|
+
bounds.getSize(this.halfExtent).multiplyScalar(0.5);
|
|
307
314
|
for (const mesh of this.groups.get(source) ?? []) {
|
|
308
315
|
if (!instanceObjectShown(mesh) || (mesh.frustumCulled && !this.frustum.intersectsObject(mesh))) continue;
|
|
309
316
|
// Bound the largest singular value by the maximum absolute row sum
|
|
310
317
|
// of AᵀA. Unlike Frobenius this is tight for orthogonal TRS columns,
|
|
311
318
|
// while still bounding shear. A uniform rotation must not inflate
|
|
312
319
|
// both error and radius by sqrt(3) and veto nearby placements.
|
|
313
|
-
|
|
320
|
+
// Bound in view space, also accounting for an explicitly scaled camera.
|
|
321
|
+
this.viewWorld.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld);
|
|
322
|
+
const e = this.viewWorld.elements;
|
|
314
323
|
const x2 = e[0]! ** 2 + e[1]! ** 2 + e[2]! ** 2;
|
|
315
324
|
const y2 = e[4]! ** 2 + e[5]! ** 2 + e[6]! ** 2;
|
|
316
325
|
const z2 = e[8]! ** 2 + e[9]! ** 2 + e[10]! ** 2;
|
|
@@ -319,9 +328,20 @@ export class BlenderMotionGeometry {
|
|
|
319
328
|
const yz = Math.abs(e[4]! * e[8]! + e[5]! * e[9]! + e[6]! * e[10]!);
|
|
320
329
|
const scale = Math.sqrt(Math.max(x2 + xy + xz, y2 + xy + yz, z2 + xz + yz));
|
|
321
330
|
const sphere = source.boundingSphere!;
|
|
322
|
-
this.centre.copy(sphere.center).applyMatrix4(
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
this.centre.copy(sphere.center).applyMatrix4(this.viewWorld);
|
|
332
|
+
let depth = 1;
|
|
333
|
+
if ((camera as THREE.PerspectiveCamera).isPerspectiveCamera) {
|
|
334
|
+
const sphereDepth = -this.centre.z - sphere.radius * scale;
|
|
335
|
+
bounds.getCenter(this.boxCentre).applyMatrix4(this.viewWorld);
|
|
336
|
+
const v = this.viewWorld.elements;
|
|
337
|
+
const radiusZ = Math.abs(v[2]!) * this.halfExtent.x +
|
|
338
|
+
Math.abs(v[6]!) * this.halfExtent.y + Math.abs(v[10]!) * this.halfExtent.z;
|
|
339
|
+
// Both enclosures contain every original vertex. Their nearest
|
|
340
|
+
// depths are lower bounds, so the larger remains a lower bound.
|
|
341
|
+
// A tall shape's height need not inflate its radius toward a level
|
|
342
|
+
// camera. This tightens the bound without relaxing the pixel gate.
|
|
343
|
+
depth = Math.max(sphereDepth, -this.boxCentre.z - radiusZ);
|
|
344
|
+
}
|
|
325
345
|
const pixels = entry.error * scale * Math.abs(camera.projectionMatrix.elements[5]!) * height / (2 * depth);
|
|
326
346
|
if (!(depth > 0) || pixels > 1) { tooClose++; continue; }
|
|
327
347
|
// Decide per placement: one near-camera blade must not veto thousands
|
|
@@ -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();
|
|
@@ -263,6 +263,11 @@ export class BlenderTransparentInstances {
|
|
|
263
263
|
for (const n of this.root.matrixWorld.elements) this.observe(n);
|
|
264
264
|
const observedGeometry = new Set<THREE.BufferGeometry>();
|
|
265
265
|
for (const mesh of this.objects) {
|
|
266
|
+
// Check eligibility every frame, including opaque -> transparent
|
|
267
|
+
// transitions, but opaque geometry/attribute versions cannot affect a
|
|
268
|
+
// transparent draw plan. Large solid-only scenes need no such census.
|
|
269
|
+
const currentMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
270
|
+
if (!currentMaterials.some(material => material.visible && material.transparent)) continue;
|
|
266
271
|
this.observe(mesh.parent?.id ?? -1);
|
|
267
272
|
this.observe(instanceObjectShown(mesh) ? 1 : 0);
|
|
268
273
|
this.observe(mesh.layers.mask);
|
|
@@ -313,6 +318,19 @@ export class BlenderTransparentInstances {
|
|
|
313
318
|
this.observe((material as THREE.MeshPhysicalMaterial).transmission ?? 0);
|
|
314
319
|
}
|
|
315
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
|
+
|
|
316
334
|
finishDraw(): void {
|
|
317
335
|
for (const mesh of this.hidden) mesh.layers.enable(0);
|
|
318
336
|
this.hidden.length = 0;
|