@volter/blender-engine 0.1.68 → 0.1.69
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.
|
@@ -43,6 +43,9 @@ export class BlenderRuntimeInstances {
|
|
|
43
43
|
private readonly inverse = new THREE.Matrix4();
|
|
44
44
|
private readonly matrix = new THREE.Matrix4();
|
|
45
45
|
private readonly box = new THREE.Box3();
|
|
46
|
+
private readonly projection = new THREE.Matrix4();
|
|
47
|
+
private readonly frustum = new THREE.Frustum();
|
|
48
|
+
private camera: THREE.Camera | null = null;
|
|
46
49
|
private eligible = 0;
|
|
47
50
|
private excluded: Record<string, number> = {};
|
|
48
51
|
|
|
@@ -137,6 +140,10 @@ export class BlenderRuntimeInstances {
|
|
|
137
140
|
}
|
|
138
141
|
mesh.layers.mask &= ~member.layers;
|
|
139
142
|
if (!instanceObjectShown(mesh)) continue;
|
|
143
|
+
// Aggregate batch bounds alone admit off-screen members. Retain the
|
|
144
|
+
// same per-object rejection as ordinary draws before compacting slots.
|
|
145
|
+
if (this.camera && (!this.camera.layers.isEnabled(0) ||
|
|
146
|
+
(mesh.frustumCulled && !this.frustum.intersectsObject(mesh)))) continue;
|
|
140
147
|
const offset = count * 16;
|
|
141
148
|
if (this.matrix.elements.some((value, i) => Math.fround(value) !== draw.instanceMatrix.array[offset + i])) {
|
|
142
149
|
draw.setMatrixAt(count, this.matrix);
|
|
@@ -154,7 +161,17 @@ export class BlenderRuntimeInstances {
|
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
|
|
164
|
+
prepareDraw(camera: THREE.Camera): void {
|
|
165
|
+
camera.updateMatrixWorld();
|
|
166
|
+
this.camera = camera;
|
|
167
|
+
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
168
|
+
this.frustum.setFromProjectionMatrix(this.projection);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
finishDraw(): void { this.camera = null; }
|
|
172
|
+
|
|
157
173
|
clear(): void {
|
|
174
|
+
this.camera = null;
|
|
158
175
|
for (const {draw, members} of this.batches) {
|
|
159
176
|
for (const {mesh, layers} of members) mesh.layers.mask |= layers;
|
|
160
177
|
draw.removeFromParent();
|
|
@@ -644,6 +644,7 @@ export class BlenderRuntimeView {
|
|
|
644
644
|
readonly root = new THREE.Group();
|
|
645
645
|
private readonly instances = new BlenderRuntimeInstances(this.root);
|
|
646
646
|
private readonly transparentInstances = new BlenderTransparentInstances(this.root);
|
|
647
|
+
private drawBatching = true;
|
|
647
648
|
private readonly objects = new Map<string, THREE.Object3D>();
|
|
648
649
|
private readonly meshes = new Map<
|
|
649
650
|
string,
|
|
@@ -1227,21 +1228,34 @@ export class BlenderRuntimeView {
|
|
|
1227
1228
|
|
|
1228
1229
|
private rebuildInstances(): void {
|
|
1229
1230
|
this.transparentInstances.clear();
|
|
1230
|
-
this.instances.rebuild(this.objects.values(), !this.rendered);
|
|
1231
|
-
if (!this.rendered) this.transparentInstances.setObjects(this.objects.values());
|
|
1231
|
+
this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
|
|
1232
|
+
if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Presentation-only comparison door; never changes Blender or its data. */
|
|
1236
|
+
setDrawBatching(enabled: boolean): void {
|
|
1237
|
+
if (enabled === this.drawBatching) return;
|
|
1238
|
+
this.drawBatching = enabled;
|
|
1239
|
+
this.rebuildInstances();
|
|
1240
|
+
presenterChanged();
|
|
1232
1241
|
}
|
|
1233
1242
|
|
|
1234
1243
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1235
1244
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1236
1245
|
prepareDraw(camera: THREE.Camera): () => void {
|
|
1237
|
-
this.root.updateMatrixWorld(true);
|
|
1238
1246
|
try {
|
|
1247
|
+
this.instances.prepareDraw(camera);
|
|
1248
|
+
this.root.updateMatrixWorld(true);
|
|
1239
1249
|
this.transparentInstances.prepare(camera);
|
|
1240
1250
|
} catch (error) {
|
|
1251
|
+
this.instances.finishDraw();
|
|
1241
1252
|
this.transparentInstances.finishDraw();
|
|
1242
1253
|
throw error;
|
|
1243
1254
|
}
|
|
1244
|
-
return () =>
|
|
1255
|
+
return () => {
|
|
1256
|
+
this.instances.finishDraw();
|
|
1257
|
+
this.transparentInstances.finishDraw();
|
|
1258
|
+
};
|
|
1245
1259
|
}
|
|
1246
1260
|
|
|
1247
1261
|
private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
|
|
@@ -2049,7 +2063,7 @@ export class BlenderRuntimeView {
|
|
|
2049
2063
|
}
|
|
2050
2064
|
|
|
2051
2065
|
drawStatistics() {
|
|
2052
|
-
return {opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
|
|
2066
|
+
return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
|
|
2053
2067
|
}
|
|
2054
2068
|
|
|
2055
2069
|
snapshot() {
|
|
@@ -3,9 +3,15 @@ import {graphDrawAttributes} from './blender-graph-material';
|
|
|
3
3
|
import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
|
|
4
4
|
|
|
5
5
|
const CAPACITY = 64;
|
|
6
|
-
type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; z: number; centre: THREE.Vector3};
|
|
6
|
+
type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; groupOrder: number; z: number; centre: THREE.Vector3};
|
|
7
7
|
type Draw = {mesh: THREE.InstancedMesh; members: THREE.Mesh[]};
|
|
8
8
|
|
|
9
|
+
function groupOrder(mesh: THREE.Object3D): number {
|
|
10
|
+
for (let parent = mesh.parent; parent; parent = parent.parent)
|
|
11
|
+
if ((parent as THREE.Group).isGroup) return parent.renderOrder;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
/** Preserve Three's transparent object order. An instance run may contain only
|
|
10
16
|
* consecutive, compatible, single-pass surfaces in that order. Incompatible
|
|
11
17
|
* surfaces remain barriers; ties at a run boundary stay ordinary draws because
|
|
@@ -23,6 +29,10 @@ export class BlenderTransparentInstances {
|
|
|
23
29
|
private readonly depth = new THREE.Vector4();
|
|
24
30
|
private batches = 0;
|
|
25
31
|
private instances = 0;
|
|
32
|
+
private readonly planState: (number | string)[] = [];
|
|
33
|
+
private readonly planned: Draw[] = [];
|
|
34
|
+
private stateIndex = 0;
|
|
35
|
+
private stateChanged = false;
|
|
26
36
|
|
|
27
37
|
constructor(private readonly root: THREE.Group) {}
|
|
28
38
|
|
|
@@ -32,16 +42,29 @@ export class BlenderTransparentInstances {
|
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
prepare(camera: THREE.Camera): void {
|
|
35
|
-
|
|
36
|
-
this.
|
|
45
|
+
this.finishDraw();
|
|
46
|
+
if (!this.objects.length) return;
|
|
47
|
+
camera.updateMatrixWorld();
|
|
48
|
+
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
49
|
+
if (!this.planChanged(camera)) {
|
|
50
|
+
// The draw plan is a disposable presentation, not evaluated scene data.
|
|
51
|
+
// Reuse only after exact comparison of every input to culling/ordering;
|
|
52
|
+
// a camera move, drag, visibility or material change rebuilds it below.
|
|
53
|
+
for (const draw of this.planned) {
|
|
54
|
+
draw.mesh.visible = true;
|
|
55
|
+
for (const member of draw.members) {
|
|
56
|
+
member.layers.disable(0);
|
|
57
|
+
this.hidden.push(member);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.planned.length = 0;
|
|
37
63
|
for (const pool of this.pools.values()) for (const draw of pool) {
|
|
38
64
|
draw.mesh.visible = false;
|
|
39
65
|
draw.members.length = 0;
|
|
40
66
|
}
|
|
41
67
|
this.batches = this.instances = 0;
|
|
42
|
-
if (!this.objects.length) return;
|
|
43
|
-
camera.updateMatrixWorld();
|
|
44
|
-
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
45
68
|
this.frustum.setFromProjectionMatrix(this.projection);
|
|
46
69
|
this.inverse.copy(this.root.matrixWorld).invert();
|
|
47
70
|
const entries: Entry[] = [];
|
|
@@ -68,12 +91,14 @@ export class BlenderTransparentInstances {
|
|
|
68
91
|
let material: THREE.Material | null = null;
|
|
69
92
|
let key: string | null = null;
|
|
70
93
|
const candidate = materials.length === 1 ? materials[0]! : null;
|
|
94
|
+
const order = groupOrder(mesh);
|
|
71
95
|
if (candidate && candidate.visible && candidate.transparent &&
|
|
72
96
|
!(candidate as THREE.ShaderMaterial).isShaderMaterial &&
|
|
73
97
|
!((candidate as THREE.MeshPhysicalMaterial).transmission > 0) &&
|
|
74
98
|
(candidate.side !== THREE.DoubleSide || candidate.forceSinglePass) &&
|
|
75
99
|
!(mesh as THREE.SkinnedMesh).isSkinnedMesh && !mesh.morphTargetInfluences &&
|
|
76
|
-
!(mesh as THREE.InstancedMesh).isInstancedMesh && mesh.layers.mask === 1
|
|
100
|
+
!(mesh as THREE.InstancedMesh).isInstancedMesh && mesh.layers.mask === 1 &&
|
|
101
|
+
order === this.root.renderOrder) {
|
|
77
102
|
const geometry = mesh.geometry;
|
|
78
103
|
// A single material draws the whole geometry. An array does so only
|
|
79
104
|
// when its sole group covers the same range; preserve all other cases.
|
|
@@ -93,9 +118,9 @@ export class BlenderTransparentInstances {
|
|
|
93
118
|
key = groupKey;
|
|
94
119
|
}
|
|
95
120
|
}
|
|
96
|
-
entries.push({mesh, material, key, z, centre});
|
|
121
|
+
entries.push({mesh, material, key, groupOrder: order, z, centre});
|
|
97
122
|
}
|
|
98
|
-
entries.sort((a, b) => a.mesh.renderOrder - b.mesh.renderOrder || b.z - a.z || a.mesh.id - b.mesh.id);
|
|
123
|
+
entries.sort((a, b) => a.groupOrder - b.groupOrder || a.mesh.renderOrder - b.mesh.renderOrder || b.z - a.z || a.mesh.id - b.mesh.id);
|
|
99
124
|
const used = new Map<string, number>();
|
|
100
125
|
for (let begin = 0; begin < entries.length;) {
|
|
101
126
|
const first = entries[begin]!;
|
|
@@ -155,6 +180,7 @@ export class BlenderTransparentInstances {
|
|
|
155
180
|
}
|
|
156
181
|
draw.mesh.count = length;
|
|
157
182
|
draw.mesh.instanceMatrix.needsUpdate = true;
|
|
183
|
+
this.planned.push(draw);
|
|
158
184
|
this.batches++;
|
|
159
185
|
this.instances += length;
|
|
160
186
|
}
|
|
@@ -165,6 +191,60 @@ export class BlenderTransparentInstances {
|
|
|
165
191
|
|
|
166
192
|
inspect() { return {batches: this.batches, instances: this.instances}; }
|
|
167
193
|
|
|
194
|
+
private observe(value: number | string): void {
|
|
195
|
+
if (this.planState[this.stateIndex] !== value) {
|
|
196
|
+
this.planState[this.stateIndex] = value;
|
|
197
|
+
this.stateChanged = true;
|
|
198
|
+
}
|
|
199
|
+
this.stateIndex++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Compare inputs without cloning 10,000 centres, allocating entries or
|
|
203
|
+
* sorting/uploading matrices again on an unchanged frame. No lossy hash. */
|
|
204
|
+
private planChanged(camera: THREE.Camera): boolean {
|
|
205
|
+
this.stateIndex = 0;
|
|
206
|
+
this.stateChanged = this.planState.length === 0;
|
|
207
|
+
this.observe(camera.layers.mask);
|
|
208
|
+
for (const n of this.projection.elements) this.observe(n);
|
|
209
|
+
for (const n of this.root.matrixWorld.elements) this.observe(n);
|
|
210
|
+
for (const mesh of this.objects) {
|
|
211
|
+
this.observe(mesh.parent?.id ?? -1);
|
|
212
|
+
this.observe(instanceObjectShown(mesh) ? 1 : 0);
|
|
213
|
+
this.observe(mesh.layers.mask);
|
|
214
|
+
this.observe(mesh.renderOrder);
|
|
215
|
+
this.observe(groupOrder(mesh));
|
|
216
|
+
this.observe(mesh.frustumCulled ? 1 : 0);
|
|
217
|
+
this.observe(mesh.morphTargetInfluences ? 1 : 0);
|
|
218
|
+
for (const n of mesh.matrixWorld.elements) this.observe(n);
|
|
219
|
+
const geometry = mesh.geometry;
|
|
220
|
+
this.observe(geometry.id);
|
|
221
|
+
this.observe(geometry.drawRange.start);
|
|
222
|
+
this.observe(geometry.drawRange.count);
|
|
223
|
+
this.observe(geometry.index?.count ?? geometry.getAttribute('position').count);
|
|
224
|
+
this.observe(geometry.groups.length);
|
|
225
|
+
for (const group of geometry.groups) {
|
|
226
|
+
this.observe(group.start); this.observe(group.count); this.observe(group.materialIndex ?? 0);
|
|
227
|
+
}
|
|
228
|
+
const sphere = (mesh as THREE.Mesh & {boundingSphere?: THREE.Sphere | null}).boundingSphere ?? geometry.boundingSphere;
|
|
229
|
+
this.observe(sphere?.center.x ?? 0); this.observe(sphere?.center.y ?? 0);
|
|
230
|
+
this.observe(sphere?.center.z ?? 0); this.observe(sphere?.radius ?? -1);
|
|
231
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : null;
|
|
232
|
+
this.observe(materials?.length ?? -1);
|
|
233
|
+
if (materials) for (const material of materials) this.observeMaterial(material);
|
|
234
|
+
else this.observeMaterial(mesh.material as THREE.Material);
|
|
235
|
+
}
|
|
236
|
+
this.stateChanged ||= this.planState.length !== this.stateIndex;
|
|
237
|
+
this.planState.length = this.stateIndex;
|
|
238
|
+
return this.stateChanged;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private observeMaterial(material: THREE.Material): void {
|
|
242
|
+
this.observe(material.uuid); this.observe(material.version);
|
|
243
|
+
this.observe(material.visible ? 1 : 0); this.observe(material.transparent ? 1 : 0);
|
|
244
|
+
this.observe(material.side); this.observe(material.forceSinglePass ? 1 : 0);
|
|
245
|
+
this.observe((material as THREE.MeshPhysicalMaterial).transmission ?? 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
168
248
|
finishDraw(): void {
|
|
169
249
|
for (const mesh of this.hidden) mesh.layers.enable(0);
|
|
170
250
|
this.hidden.length = 0;
|
|
@@ -178,6 +258,8 @@ export class BlenderTransparentInstances {
|
|
|
178
258
|
mesh.dispose();
|
|
179
259
|
}
|
|
180
260
|
this.pools.clear();
|
|
261
|
+
this.planned.length = 0;
|
|
262
|
+
this.planState.length = 0;
|
|
181
263
|
this.objects = [];
|
|
182
264
|
this.batches = this.instances = 0;
|
|
183
265
|
}
|