@volter/blender-engine 0.1.78 → 0.1.80
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,100 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
const CACHE_BYTES = 8 * 1024 * 1024;
|
|
4
|
+
type Copy = {geometry: THREE.BufferGeometry; signature: string; bytes: number; before: number; after: number};
|
|
5
|
+
|
|
6
|
+
/** Three sorts opaque groups by material id before drawing. Consolidate the
|
|
7
|
+
* ranges that are already consecutive in that render order, preserving index
|
|
8
|
+
* order inside each material. No transparency, vertices or authored slots change. */
|
|
9
|
+
export class BlenderMaterialRanges {
|
|
10
|
+
private objects: THREE.Mesh[] = [];
|
|
11
|
+
private readonly copies = new Map<string, Copy>();
|
|
12
|
+
private readonly swapped: {mesh: THREE.Mesh; geometry: THREE.BufferGeometry}[] = [];
|
|
13
|
+
private bytes = 0;
|
|
14
|
+
private savedDraws = 0;
|
|
15
|
+
|
|
16
|
+
setObjects(objects: Iterable<THREE.Object3D>): void {
|
|
17
|
+
this.clear();
|
|
18
|
+
this.objects = [...objects].filter((o): o is THREE.Mesh => (o as THREE.Mesh).isMesh === true);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
prepare(): void {
|
|
22
|
+
this.savedDraws = 0;
|
|
23
|
+
for (const mesh of this.objects) {
|
|
24
|
+
const source = mesh.geometry;
|
|
25
|
+
const materials = mesh.material;
|
|
26
|
+
if (mesh.layers.mask === 0 || !Array.isArray(materials) || source.groups.length < 8 || !source.index ||
|
|
27
|
+
(mesh as THREE.SkinnedMesh).isSkinnedMesh || (mesh as THREE.InstancedMesh).isInstancedMesh ||
|
|
28
|
+
mesh.morphTargetInfluences || Object.keys(source.morphAttributes).length ||
|
|
29
|
+
source.drawRange.start !== 0 || source.drawRange.count < source.index.count ||
|
|
30
|
+
materials.some(m => m.transparent || (m as THREE.MeshPhysicalMaterial).transmission > 0 ||
|
|
31
|
+
(m as THREE.ShaderMaterial).isShaderMaterial)) continue;
|
|
32
|
+
const signature = JSON.stringify([source.id, source.index.id, source.index.version,
|
|
33
|
+
materials.map(m => m.uuid), source.groups,
|
|
34
|
+
Object.entries(source.attributes).map(([name, a]) => [name, (a as THREE.BufferAttribute).id])]);
|
|
35
|
+
let copy = this.copies.get(signature);
|
|
36
|
+
if (!copy) {
|
|
37
|
+
// Partial/overlapping/out-of-order ranges have independent semantics;
|
|
38
|
+
// retain them verbatim instead of inferring missing triangles.
|
|
39
|
+
let end = 0, valid = true;
|
|
40
|
+
const grouped = new Map<string, {slot: number; groups: THREE.BufferGeometry['groups']}>();
|
|
41
|
+
for (const group of source.groups) {
|
|
42
|
+
const slot = group.materialIndex ?? 0;
|
|
43
|
+
const material = materials[slot];
|
|
44
|
+
if (!material || group.start !== end || group.count <= 0 || group.count % 3 !== 0) { valid = false; break; }
|
|
45
|
+
end += group.count;
|
|
46
|
+
const entry = grouped.get(material.uuid);
|
|
47
|
+
if (entry) entry.groups.push(group);
|
|
48
|
+
else grouped.set(material.uuid, {slot, groups: [group]});
|
|
49
|
+
}
|
|
50
|
+
if (!valid || end !== source.index.count || grouped.size >= source.groups.length) continue;
|
|
51
|
+
const bytes = source.index.array.byteLength;
|
|
52
|
+
if (this.bytes + bytes > CACHE_BYTES) continue;
|
|
53
|
+
const geometry = new THREE.BufferGeometry();
|
|
54
|
+
for (const [name, attribute] of Object.entries(source.attributes)) geometry.setAttribute(name, attribute);
|
|
55
|
+
const index = source.index.clone();
|
|
56
|
+
let offset = 0;
|
|
57
|
+
for (const entry of grouped.values()) {
|
|
58
|
+
const start = offset;
|
|
59
|
+
for (const group of entry.groups) {
|
|
60
|
+
index.array.set(source.index.array.subarray(group.start, group.start + group.count), offset);
|
|
61
|
+
offset += group.count;
|
|
62
|
+
}
|
|
63
|
+
geometry.addGroup(start, offset - start, entry.slot);
|
|
64
|
+
}
|
|
65
|
+
geometry.setIndex(index);
|
|
66
|
+
geometry.boundingBox = source.boundingBox?.clone() ?? null;
|
|
67
|
+
geometry.boundingSphere = source.boundingSphere?.clone() ?? null;
|
|
68
|
+
geometry.userData = {...source.userData};
|
|
69
|
+
copy = {geometry, signature, bytes, before: source.groups.length, after: grouped.size};
|
|
70
|
+
this.copies.set(signature, copy);
|
|
71
|
+
this.bytes += bytes;
|
|
72
|
+
}
|
|
73
|
+
this.swapped.push({mesh, geometry: source});
|
|
74
|
+
mesh.geometry = copy.geometry;
|
|
75
|
+
this.savedDraws += copy.before - copy.after;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
finish(): void {
|
|
80
|
+
for (const {mesh, geometry} of this.swapped) mesh.geometry = geometry;
|
|
81
|
+
this.swapped.length = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
inspect() { return {copies: this.copies.size, bytes: this.bytes, savedRanges: this.savedDraws}; }
|
|
85
|
+
|
|
86
|
+
private release(copy: Copy): void {
|
|
87
|
+
// Only the reordered index is owned; vertex attributes stay canonical.
|
|
88
|
+
for (const name of Object.keys(copy.geometry.attributes)) copy.geometry.deleteAttribute(name);
|
|
89
|
+
copy.geometry.dispose();
|
|
90
|
+
this.bytes -= copy.bytes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
clear(): void {
|
|
94
|
+
this.finish();
|
|
95
|
+
for (const copy of this.copies.values()) this.release(copy);
|
|
96
|
+
this.copies.clear();
|
|
97
|
+
this.objects = [];
|
|
98
|
+
this.savedDraws = 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -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
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import { DEFAULT_VIEWPORT_DISPLAY, workbenchMaterial } from './blender-workbench-material';
|
|
45
45
|
import { BlenderRuntimeInstances } from './blender-runtime-instances';
|
|
46
46
|
import { BlenderTransparentInstances } from './blender-transparent-instances';
|
|
47
|
+
import { BlenderMaterialRanges } from './blender-material-ranges';
|
|
47
48
|
import { BlenderMotionGeometry } from './blender-motion-geometry';
|
|
48
49
|
|
|
49
50
|
/** A saved `View3DShading.type` as the stage's draw mode: Material Preview is `preview`. */
|
|
@@ -644,6 +645,7 @@ export interface PhotographRecord {
|
|
|
644
645
|
export class BlenderRuntimeView {
|
|
645
646
|
readonly root = new THREE.Group();
|
|
646
647
|
private readonly instances = new BlenderRuntimeInstances(this.root);
|
|
648
|
+
private readonly materialRanges = new BlenderMaterialRanges();
|
|
647
649
|
private readonly transparentInstances = new BlenderTransparentInstances(this.root);
|
|
648
650
|
private readonly motionGeometry = new BlenderMotionGeometry(presenterChanged);
|
|
649
651
|
private drawBatching = true;
|
|
@@ -1233,6 +1235,7 @@ export class BlenderRuntimeView {
|
|
|
1233
1235
|
this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
|
|
1234
1236
|
if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
|
|
1235
1237
|
this.motionGeometry.setObjects(this.objects.values());
|
|
1238
|
+
this.materialRanges.setObjects(this.drawBatching && !this.rendered ? this.objects.values() : []);
|
|
1236
1239
|
}
|
|
1237
1240
|
|
|
1238
1241
|
/** Presentation-only comparison door; never changes Blender or its data. */
|
|
@@ -1244,8 +1247,8 @@ export class BlenderRuntimeView {
|
|
|
1244
1247
|
}
|
|
1245
1248
|
|
|
1246
1249
|
private drawPreparation: {
|
|
1247
|
-
last: {opaqueMs: number; motionMs: number; transparentMs: number} | null;
|
|
1248
|
-
navigation: {opaqueMs: number; motionMs: number; transparentMs: number} | null;
|
|
1250
|
+
last: {opaqueMs: number; motionMs: number; rangesMs: number; transparentMs: number} | null;
|
|
1251
|
+
navigation: {opaqueMs: number; motionMs: number; rangesMs: number; transparentMs: number} | null;
|
|
1249
1252
|
} = {last: null, navigation: null};
|
|
1250
1253
|
|
|
1251
1254
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
@@ -1264,18 +1267,22 @@ export class BlenderRuntimeView {
|
|
|
1264
1267
|
// their sorting barriers and compatible geometry grouping.
|
|
1265
1268
|
if (motion) this.instances.sync();
|
|
1266
1269
|
const motionDone = performance.now();
|
|
1270
|
+
this.materialRanges.prepare();
|
|
1271
|
+
const rangesDone = performance.now();
|
|
1267
1272
|
this.transparentInstances.prepare(camera, options?.multiDraw === true);
|
|
1268
1273
|
const timing = {opaqueMs: opaqueDone - start, motionMs: motionDone - opaqueDone,
|
|
1269
|
-
transparentMs: performance.now() -
|
|
1274
|
+
rangesMs: rangesDone - motionDone, transparentMs: performance.now() - rangesDone};
|
|
1270
1275
|
this.drawPreparation.last = timing;
|
|
1271
1276
|
if (motion) this.drawPreparation.navigation = timing;
|
|
1272
1277
|
} catch (error) {
|
|
1278
|
+
this.materialRanges.finish();
|
|
1273
1279
|
this.motionGeometry.finish();
|
|
1274
1280
|
this.instances.finishDraw();
|
|
1275
1281
|
this.transparentInstances.finishDraw();
|
|
1276
1282
|
throw error;
|
|
1277
1283
|
}
|
|
1278
1284
|
return () => {
|
|
1285
|
+
this.materialRanges.finish();
|
|
1279
1286
|
this.motionGeometry.finish();
|
|
1280
1287
|
this.instances.finishDraw();
|
|
1281
1288
|
this.transparentInstances.finishDraw();
|
|
@@ -2087,7 +2094,7 @@ export class BlenderRuntimeView {
|
|
|
2087
2094
|
}
|
|
2088
2095
|
|
|
2089
2096
|
drawStatistics() {
|
|
2090
|
-
return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, 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()};
|
|
2091
2098
|
}
|
|
2092
2099
|
|
|
2093
2100
|
snapshot() {
|
|
@@ -2297,6 +2304,7 @@ export class BlenderRuntimeView {
|
|
|
2297
2304
|
}
|
|
2298
2305
|
|
|
2299
2306
|
private clear() {
|
|
2307
|
+
this.materialRanges.clear();
|
|
2300
2308
|
this.motionGeometry.clear();
|
|
2301
2309
|
this.transparentInstances.clear();
|
|
2302
2310
|
this.instances.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);
|