@volter/blender-engine 0.1.65 → 0.1.66
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.
|
@@ -322,8 +322,12 @@ ${compiled.uvs.map(n => `varying vec2 ${uvVarying(n)};`).join('\n')}`;
|
|
|
322
322
|
'#include <project_vertex>',
|
|
323
323
|
`vBlenderObjectPosition = transformed;
|
|
324
324
|
vBlenderObjectNormal = objectNormal;
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
mat4 blenderWorldMatrix = modelMatrix;
|
|
326
|
+
#ifdef USE_INSTANCING
|
|
327
|
+
blenderWorldMatrix = modelMatrix * instanceMatrix;
|
|
328
|
+
#endif
|
|
329
|
+
vBlenderWorldPosition = (blenderWorldMatrix * vec4(transformed, 1.0)).xyz;
|
|
330
|
+
vBlenderWorldNormal = normalize(transpose(inverse(mat3(blenderWorldMatrix))) * objectNormal);
|
|
327
331
|
vBlenderOrco = blenderOrco;
|
|
328
332
|
${compiled.attributes.map(n => `${attributeVarying(n)} = ${graphAttributeName(n)};`).join('\n')}
|
|
329
333
|
${compiled.uvs.map(n => `${uvVarying(n)} = ${attribute(channels[n] ?? 0)};`).join('\n')}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
// Small spatial batches retain frustum rejection without duplicating geometry.
|
|
4
|
+
const BATCH_SIZE = 64;
|
|
5
|
+
type Member = { mesh: THREE.Mesh; layers: number };
|
|
6
|
+
type Batch = { draw: THREE.InstancedMesh; members: Member[] };
|
|
7
|
+
|
|
8
|
+
function sameMaterials(a: THREE.Material | THREE.Material[], b: THREE.Material | THREE.Material[]): boolean {
|
|
9
|
+
return Array.isArray(a) && Array.isArray(b)
|
|
10
|
+
? a.length === b.length && a.every((material, i) => material === b[i])
|
|
11
|
+
: a === b;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function shown(object: THREE.Object3D): boolean {
|
|
15
|
+
for (let node: THREE.Object3D | null = object; node; node = node.parent)
|
|
16
|
+
if (!node.visible) return false;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Three's instance normal transform supports positive, orthogonal scale axes.
|
|
21
|
+
* Mirrored, singular and sheared placements keep the ordinary Mesh draw. */
|
|
22
|
+
function supported(matrix: THREE.Matrix4): boolean {
|
|
23
|
+
if (matrix.determinant() <= 0) return false;
|
|
24
|
+
const e = matrix.elements;
|
|
25
|
+
const x = Math.hypot(e[0]!, e[1]!, e[2]!);
|
|
26
|
+
const y = Math.hypot(e[4]!, e[5]!, e[6]!);
|
|
27
|
+
const z = Math.hypot(e[8]!, e[9]!, e[10]!);
|
|
28
|
+
const dot = (a: number, b: number) => e[a]! * e[b]! + e[a + 1]! * e[b + 1]! + e[a + 2]! * e[b + 2]!;
|
|
29
|
+
return Number.isFinite(x + y + z) && Math.min(x, y, z) > 0 &&
|
|
30
|
+
Math.abs(dot(0, 4)) <= 1e-7 * x * y &&
|
|
31
|
+
Math.abs(dot(0, 8)) <= 1e-7 * x * z &&
|
|
32
|
+
Math.abs(dot(4, 8)) <= 1e-7 * y * z;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Disposable draws only. The original objects, geometry, material slots and
|
|
36
|
+
* parent graph remain the authority for hierarchy, bounds, editing and picking.
|
|
37
|
+
* Layer zero-mask suppresses their ordinary draw; batch raycasts return those
|
|
38
|
+
* same original objects. Selection passes can still temporarily add their mask
|
|
39
|
+
* layer to an original. No vertices or authored data are reduced or replaced. */
|
|
40
|
+
export class BlenderRuntimeInstances {
|
|
41
|
+
private readonly batches: Batch[] = [];
|
|
42
|
+
private readonly inverse = new THREE.Matrix4();
|
|
43
|
+
private readonly matrix = new THREE.Matrix4();
|
|
44
|
+
private readonly box = new THREE.Box3();
|
|
45
|
+
|
|
46
|
+
constructor(private readonly root: THREE.Group) {}
|
|
47
|
+
|
|
48
|
+
rebuild(objects: Iterable<THREE.Object3D>, enabled: boolean): void {
|
|
49
|
+
this.clear();
|
|
50
|
+
if (!enabled) return;
|
|
51
|
+
const groups = new Map<string, THREE.Mesh[]>();
|
|
52
|
+
this.inverse.copy(this.root.matrixWorld).invert();
|
|
53
|
+
for (const object of objects) {
|
|
54
|
+
const mesh = object as THREE.Mesh;
|
|
55
|
+
if (!mesh.isMesh || (mesh as THREE.SkinnedMesh).isSkinnedMesh ||
|
|
56
|
+
(mesh as THREE.InstancedMesh).isInstancedMesh || mesh.morphTargetInfluences ||
|
|
57
|
+
mesh.layers.mask !== 1) continue;
|
|
58
|
+
// WebGL2 guarantees sixteen vertex attributes; the instance matrix uses
|
|
59
|
+
// four. Keep richly attributed meshes on the ordinary path rather than
|
|
60
|
+
// dropping UV/colour channels or risking a refused shader.
|
|
61
|
+
if (Object.values(mesh.geometry.attributes).reduce((count, attribute) => count + Math.ceil(attribute.itemSize / 4), 0) > 12) continue;
|
|
62
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
63
|
+
if (materials.some(m => m.transparent || (m as THREE.MeshPhysicalMaterial).transmission > 0 ||
|
|
64
|
+
(m as THREE.ShaderMaterial).isShaderMaterial)) continue;
|
|
65
|
+
this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
|
|
66
|
+
if (!supported(this.matrix)) continue;
|
|
67
|
+
const key = `${mesh.geometry.id}:${Array.isArray(mesh.material) ? 'slots' : 'single'}:` +
|
|
68
|
+
materials.map(m => m.uuid).join(',') + `:${mesh.renderOrder}`;
|
|
69
|
+
const group = groups.get(key);
|
|
70
|
+
if (group) group.push(mesh);
|
|
71
|
+
else groups.set(key, [mesh]);
|
|
72
|
+
}
|
|
73
|
+
for (const group of groups.values()) {
|
|
74
|
+
if (group.length < 2) continue;
|
|
75
|
+
// Partition along the widest centre axis, recursively. Adjacent authored
|
|
76
|
+
// names need not be adjacent in space (linked trees span the whole Bridge).
|
|
77
|
+
const partition = (members: THREE.Mesh[]): void => {
|
|
78
|
+
if (members.length > BATCH_SIZE) {
|
|
79
|
+
this.box.makeEmpty();
|
|
80
|
+
for (const mesh of members) this.box.expandByPoint(new THREE.Vector3().setFromMatrixPosition(mesh.matrixWorld));
|
|
81
|
+
const size = this.box.getSize(new THREE.Vector3());
|
|
82
|
+
const axis = size.x >= size.y && size.x >= size.z ? 12 : size.y >= size.z ? 13 : 14;
|
|
83
|
+
members.sort((a, b) => a.matrixWorld.elements[axis]! - b.matrixWorld.elements[axis]!);
|
|
84
|
+
const middle = Math.floor(members.length / 2);
|
|
85
|
+
partition(members.slice(0, middle));
|
|
86
|
+
partition(members.slice(middle));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (members.length < 2) return;
|
|
90
|
+
const first = members[0]!;
|
|
91
|
+
const draw = new THREE.InstancedMesh(first.geometry, first.material, members.length);
|
|
92
|
+
draw.name = 'Blender repeated geometry';
|
|
93
|
+
draw.userData['engineInternal'] = true;
|
|
94
|
+
draw.matrixAutoUpdate = false;
|
|
95
|
+
draw.renderOrder = first.renderOrder;
|
|
96
|
+
draw.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
97
|
+
const batch: Batch = {draw, members: members.map(mesh => ({mesh, layers: mesh.layers.mask}))};
|
|
98
|
+
// Return the canonical mesh, not the internal batch or an unstable slot.
|
|
99
|
+
draw.raycast = (raycaster, hits) => {
|
|
100
|
+
for (const {mesh} of batch.members)
|
|
101
|
+
if (mesh.layers.mask === 0 && shown(mesh)) mesh.raycast(raycaster, hits);
|
|
102
|
+
};
|
|
103
|
+
this.root.add(draw);
|
|
104
|
+
draw.updateMatrixWorld(true);
|
|
105
|
+
this.batches.push(batch);
|
|
106
|
+
};
|
|
107
|
+
partition(group);
|
|
108
|
+
}
|
|
109
|
+
this.sync();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Called after the root updates its descendants, before Three builds its
|
|
113
|
+
* render list/uploads attributes. Live gizmo transforms therefore appear on
|
|
114
|
+
* the very next draw, including parent transforms and visibility changes. */
|
|
115
|
+
sync(): void {
|
|
116
|
+
if (!this.batches.length) return;
|
|
117
|
+
this.inverse.copy(this.root.matrixWorld).invert();
|
|
118
|
+
for (const {draw, members} of this.batches) {
|
|
119
|
+
let count = 0;
|
|
120
|
+
let changed = false;
|
|
121
|
+
for (const member of members) {
|
|
122
|
+
const {mesh} = member;
|
|
123
|
+
this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
|
|
124
|
+
// A drag can introduce negative scale/shear, or a skin can replace a
|
|
125
|
+
// mesh between frames. Fall back immediately without touching its data.
|
|
126
|
+
const compatible = mesh.parent !== null && mesh.geometry === draw.geometry &&
|
|
127
|
+
sameMaterials(mesh.material, draw.material) && supported(this.matrix);
|
|
128
|
+
if (!compatible) {
|
|
129
|
+
mesh.layers.mask |= member.layers;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
mesh.layers.mask &= ~member.layers;
|
|
133
|
+
if (!shown(mesh)) continue;
|
|
134
|
+
const offset = count * 16;
|
|
135
|
+
if (this.matrix.elements.some((value, i) => Math.fround(value) !== draw.instanceMatrix.array[offset + i])) {
|
|
136
|
+
draw.setMatrixAt(count, this.matrix);
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
count++;
|
|
140
|
+
}
|
|
141
|
+
changed ||= draw.count !== count || draw.boundingSphere === null;
|
|
142
|
+
draw.count = count;
|
|
143
|
+
if (changed) {
|
|
144
|
+
draw.instanceMatrix.needsUpdate = true;
|
|
145
|
+
draw.computeBoundingBox();
|
|
146
|
+
draw.computeBoundingSphere();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
clear(): void {
|
|
152
|
+
for (const {draw, members} of this.batches) {
|
|
153
|
+
for (const {mesh, layers} of members) mesh.layers.mask |= layers;
|
|
154
|
+
draw.removeFromParent();
|
|
155
|
+
draw.dispose(); // releases instance attributes, never shared geometry/material
|
|
156
|
+
}
|
|
157
|
+
this.batches.length = 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
inspect() {
|
|
161
|
+
return {batches: this.batches.length, instances: this.batches.reduce((count, {draw}) => count + draw.count, 0)};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
panCameraView,
|
|
43
43
|
} from './blender-runtime-camera-view';
|
|
44
44
|
import { DEFAULT_VIEWPORT_DISPLAY, workbenchMaterial } from './blender-workbench-material';
|
|
45
|
+
import { BlenderRuntimeInstances } from './blender-runtime-instances';
|
|
45
46
|
|
|
46
47
|
/** A saved `View3DShading.type` as the stage's draw mode: Material Preview is `preview`. */
|
|
47
48
|
const SAVED_SHADING = {
|
|
@@ -640,6 +641,7 @@ export interface PhotographRecord {
|
|
|
640
641
|
|
|
641
642
|
export class BlenderRuntimeView {
|
|
642
643
|
readonly root = new THREE.Group();
|
|
644
|
+
private readonly instances = new BlenderRuntimeInstances(this.root);
|
|
643
645
|
private readonly objects = new Map<string, THREE.Object3D>();
|
|
644
646
|
private readonly meshes = new Map<
|
|
645
647
|
string,
|
|
@@ -1034,11 +1036,17 @@ export class BlenderRuntimeView {
|
|
|
1034
1036
|
for (const child of children) next.add(child);
|
|
1035
1037
|
parent?.add(next);
|
|
1036
1038
|
this.objects.set(id, next);
|
|
1039
|
+
this.instances.rebuild(this.objects.values(), !this.rendered);
|
|
1037
1040
|
return;
|
|
1038
1041
|
}
|
|
1039
1042
|
}
|
|
1040
1043
|
|
|
1041
1044
|
constructor() {
|
|
1045
|
+
const updateMatrixWorld = this.root.updateMatrixWorld.bind(this.root);
|
|
1046
|
+
this.root.updateMatrixWorld = (force?: boolean) => {
|
|
1047
|
+
updateMatrixWorld(force);
|
|
1048
|
+
this.instances.sync();
|
|
1049
|
+
};
|
|
1042
1050
|
this.root.name = 'Model';
|
|
1043
1051
|
// BLENDER IS Z-UP, THREE IS Y-UP: (x, y, z) -> (x, z, -y). Written as the
|
|
1044
1052
|
// matrix rather than `rotation.x = -Math.PI / 2`, because that Euler is a
|
|
@@ -1212,6 +1220,7 @@ export class BlenderRuntimeView {
|
|
|
1212
1220
|
material.dispose();
|
|
1213
1221
|
this.workbenchMaterials.delete(key);
|
|
1214
1222
|
}
|
|
1223
|
+
this.instances.rebuild(this.objects.values(), !this.rendered);
|
|
1215
1224
|
}
|
|
1216
1225
|
|
|
1217
1226
|
private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
|
|
@@ -2007,6 +2016,7 @@ export class BlenderRuntimeView {
|
|
|
2007
2016
|
active: this.frame?.active,
|
|
2008
2017
|
mode: this.frame?.mode,
|
|
2009
2018
|
geometryBuilds: this.geometryBuilds,
|
|
2019
|
+
instancing: this.instances.inspect(),
|
|
2010
2020
|
objects: [...this.objects].map(([id, object]) => ({
|
|
2011
2021
|
id,
|
|
2012
2022
|
name: object.name,
|
|
@@ -2224,6 +2234,7 @@ export class BlenderRuntimeView {
|
|
|
2224
2234
|
}
|
|
2225
2235
|
|
|
2226
2236
|
private clear() {
|
|
2237
|
+
this.instances.clear();
|
|
2227
2238
|
for (const entry of this.staged?.meshes.values() ?? []) entry.geometry.dispose();
|
|
2228
2239
|
this.staged = null;
|
|
2229
2240
|
for (const light of this.lights.values()) {
|