@volter/blender-engine 0.1.76 → 0.1.78
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.
- package/browser/three/blender-graph-material.ts +4 -1
- package/browser/three/blender-motion-geometry.ts +11 -3
- package/browser/three/blender-ordered-batches.ts +156 -0
- package/browser/three/blender-runtime-view.ts +15 -3
- package/browser/three/blender-transparent-instances.ts +81 -10
- package/package.json +1 -1
|
@@ -347,8 +347,11 @@ ${compiled.uvs.map(n => `varying vec2 ${uvVarying(n)};`).join('\n')}`;
|
|
|
347
347
|
`vBlenderObjectPosition = transformed;
|
|
348
348
|
vBlenderObjectNormal = objectNormal;
|
|
349
349
|
mat4 blenderWorldMatrix = modelMatrix;
|
|
350
|
+
#ifdef USE_BATCHING
|
|
351
|
+
blenderWorldMatrix = blenderWorldMatrix * batchingMatrix;
|
|
352
|
+
#endif
|
|
350
353
|
#ifdef USE_INSTANCING
|
|
351
|
-
blenderWorldMatrix =
|
|
354
|
+
blenderWorldMatrix = blenderWorldMatrix * instanceMatrix;
|
|
352
355
|
#endif
|
|
353
356
|
vBlenderWorldPosition = (blenderWorldMatrix * vec4(transformed, 1.0)).xyz;
|
|
354
357
|
vBlenderWorldNormal = normalize(transpose(inverse(mat3(blenderWorldMatrix))) * objectNormal);
|
|
@@ -306,10 +306,18 @@ export class BlenderMotionGeometry {
|
|
|
306
306
|
for (const [source, entry] of this.cache) {
|
|
307
307
|
for (const mesh of this.groups.get(source) ?? []) {
|
|
308
308
|
if (!instanceObjectShown(mesh) || (mesh.frustumCulled && !this.frustum.intersectsObject(mesh))) continue;
|
|
309
|
-
//
|
|
310
|
-
//
|
|
309
|
+
// Bound the largest singular value by the maximum absolute row sum
|
|
310
|
+
// of AᵀA. Unlike Frobenius this is tight for orthogonal TRS columns,
|
|
311
|
+
// while still bounding shear. A uniform rotation must not inflate
|
|
312
|
+
// both error and radius by sqrt(3) and veto nearby placements.
|
|
311
313
|
const e = mesh.matrixWorld.elements;
|
|
312
|
-
const
|
|
314
|
+
const x2 = e[0]! ** 2 + e[1]! ** 2 + e[2]! ** 2;
|
|
315
|
+
const y2 = e[4]! ** 2 + e[5]! ** 2 + e[6]! ** 2;
|
|
316
|
+
const z2 = e[8]! ** 2 + e[9]! ** 2 + e[10]! ** 2;
|
|
317
|
+
const xy = Math.abs(e[0]! * e[4]! + e[1]! * e[5]! + e[2]! * e[6]!);
|
|
318
|
+
const xz = Math.abs(e[0]! * e[8]! + e[1]! * e[9]! + e[2]! * e[10]!);
|
|
319
|
+
const yz = Math.abs(e[4]! * e[8]! + e[5]! * e[9]! + e[6]! * e[10]!);
|
|
320
|
+
const scale = Math.sqrt(Math.max(x2 + xy + xz, y2 + xy + yz, z2 + xz + yz));
|
|
313
321
|
const sphere = source.boundingSphere!;
|
|
314
322
|
this.centre.copy(sphere.center).applyMatrix4(mesh.matrixWorld).applyMatrix4(camera.matrixWorldInverse);
|
|
315
323
|
const depth = (camera as THREE.PerspectiveCamera).isPerspectiveCamera
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
export const ORDERED_BATCH_CAPACITY = 64;
|
|
4
|
+
const FAMILY_BYTES = 4 * 1024 * 1024;
|
|
5
|
+
const CACHE_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
const MAX_DRAWS = 1024;
|
|
7
|
+
type Shape = {geometry: THREE.BufferGeometry; stamp: string};
|
|
8
|
+
type Family = {material: THREE.Material; shapes: Map<number, Shape>; template: THREE.BatchedMesh | null;
|
|
9
|
+
ids: Map<number, number>; draws: THREE.BatchedMesh[]; dirty: boolean; bytes: number; refused: boolean};
|
|
10
|
+
|
|
11
|
+
/** Immutable packed attributes are shared between ordered draw runs. Only the
|
|
12
|
+
* matrix/indirection textures and command lists belong to an individual run.
|
|
13
|
+
* All interaction and authored geometry remain on the canonical meshes. */
|
|
14
|
+
export class BlenderOrderedBatches {
|
|
15
|
+
private readonly families = new Map<string, Family>();
|
|
16
|
+
private readonly registrations = new Map<string, string | null>();
|
|
17
|
+
private bytes = 0;
|
|
18
|
+
private drawCount = 0;
|
|
19
|
+
private created = 0;
|
|
20
|
+
pending = false;
|
|
21
|
+
|
|
22
|
+
begin(): void { this.created = 0; this.pending = false; this.registrations.clear(); }
|
|
23
|
+
|
|
24
|
+
register(geometry: THREE.BufferGeometry, material: THREE.Material, order: number): string | null {
|
|
25
|
+
const registration = `${geometry.id}:${material.uuid}:${order}`;
|
|
26
|
+
if (this.registrations.has(registration)) return this.registrations.get(registration)!;
|
|
27
|
+
this.registrations.set(registration, null);
|
|
28
|
+
const position = geometry.getAttribute('position');
|
|
29
|
+
const count = geometry.index?.count ?? position?.count ?? 0;
|
|
30
|
+
if (!position || geometry.drawRange.start !== 0 || geometry.drawRange.count < count ||
|
|
31
|
+
Object.keys(geometry.morphAttributes).length) return null;
|
|
32
|
+
const layout: unknown[] = [!!geometry.index, geometry.userData['blenderUvChannels'] ?? null];
|
|
33
|
+
const versions: unknown[] = [geometry.index?.id, geometry.index?.version];
|
|
34
|
+
let bytes = (geometry.index?.count ?? 0) * 4;
|
|
35
|
+
for (const name of Object.keys(geometry.attributes).sort()) {
|
|
36
|
+
const attribute = geometry.getAttribute(name);
|
|
37
|
+
if (!(attribute instanceof THREE.BufferAttribute) ||
|
|
38
|
+
(attribute as THREE.InstancedBufferAttribute).isInstancedBufferAttribute ||
|
|
39
|
+
attribute instanceof THREE.Float16BufferAttribute ||
|
|
40
|
+
attribute.count !== position.count) return null;
|
|
41
|
+
layout.push([name, attribute.array.constructor.name, attribute.itemSize, attribute.normalized, attribute.gpuType]);
|
|
42
|
+
versions.push(name, attribute.id, attribute.version);
|
|
43
|
+
bytes += attribute.array.byteLength;
|
|
44
|
+
}
|
|
45
|
+
if (bytes > FAMILY_BYTES) return null;
|
|
46
|
+
const key = `multi:${material.uuid}:${order}:${JSON.stringify(layout)}`;
|
|
47
|
+
let family = this.families.get(key);
|
|
48
|
+
if (!family) {
|
|
49
|
+
family = {material, shapes: new Map(), template: null, ids: new Map(), draws: [], dirty: true, bytes: 0, refused: false};
|
|
50
|
+
this.families.set(key, family);
|
|
51
|
+
}
|
|
52
|
+
const stamp = JSON.stringify(versions);
|
|
53
|
+
const held = family.shapes.get(geometry.id);
|
|
54
|
+
if (!held || held.stamp !== stamp) {
|
|
55
|
+
family.shapes.set(geometry.id, {geometry, stamp});
|
|
56
|
+
family.dirty = true;
|
|
57
|
+
}
|
|
58
|
+
this.registrations.set(registration, key);
|
|
59
|
+
return key;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pack(): void {
|
|
63
|
+
for (const family of this.families.values()) {
|
|
64
|
+
if (!family.dirty) continue;
|
|
65
|
+
this.release(family);
|
|
66
|
+
family.dirty = false;
|
|
67
|
+
let vertices = 0, indices = 0, bytes = 0;
|
|
68
|
+
for (const {geometry} of family.shapes.values()) {
|
|
69
|
+
vertices += geometry.getAttribute('position').count;
|
|
70
|
+
indices += geometry.index?.count ?? 0;
|
|
71
|
+
for (const attribute of Object.values(geometry.attributes)) bytes += attribute.array.byteLength;
|
|
72
|
+
}
|
|
73
|
+
bytes += indices * (vertices > 65535 ? 4 : 2);
|
|
74
|
+
family.refused = bytes > FAMILY_BYTES || this.bytes + bytes > CACHE_BYTES;
|
|
75
|
+
if (family.refused) continue;
|
|
76
|
+
const template = new THREE.BatchedMesh(ORDERED_BATCH_CAPACITY, vertices, indices, family.material);
|
|
77
|
+
template.sortObjects = false;
|
|
78
|
+
template.perObjectFrustumCulled = false;
|
|
79
|
+
template.frustumCulled = false;
|
|
80
|
+
template.matrixAutoUpdate = false;
|
|
81
|
+
template.boundingSphere = new THREE.Sphere();
|
|
82
|
+
template.name = 'Blender ordered multi-draw';
|
|
83
|
+
template.userData['engineInternal'] = true;
|
|
84
|
+
for (const {geometry} of family.shapes.values()) family.ids.set(geometry.id, template.addGeometry(geometry));
|
|
85
|
+
// Named UV selection is a geometry-level material input. The family key
|
|
86
|
+
// requires identical maps; every per-vertex graph attribute is copied.
|
|
87
|
+
const first = family.shapes.values().next().value!.geometry;
|
|
88
|
+
template.geometry.userData['blenderUvChannels'] = first.userData['blenderUvChannels'];
|
|
89
|
+
for (const name of Object.keys(first.attributes))
|
|
90
|
+
(template.geometry.getAttribute(name) as THREE.BufferAttribute).gpuType = (first.getAttribute(name) as THREE.BufferAttribute).gpuType;
|
|
91
|
+
family.template = template;
|
|
92
|
+
family.bytes = bytes;
|
|
93
|
+
this.bytes += bytes;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
available(key: string): boolean { return !!this.families.get(key)?.template; }
|
|
98
|
+
|
|
99
|
+
get(key: string, index: number): THREE.BatchedMesh | null {
|
|
100
|
+
const family = this.families.get(key)!;
|
|
101
|
+
const template = family.template;
|
|
102
|
+
if (!template) return null;
|
|
103
|
+
let draw = family.draws[index];
|
|
104
|
+
if (!draw) {
|
|
105
|
+
if (this.drawCount >= MAX_DRAWS) return null;
|
|
106
|
+
// copy() uses public Three APIs and briefly copies the packed arrays.
|
|
107
|
+
// Bound that transient work per frame; ordinary meshes fill every gap.
|
|
108
|
+
if (this.created >= 4) { this.pending = true; return null; }
|
|
109
|
+
draw = new THREE.BatchedMesh(1, 0, 0, family.material);
|
|
110
|
+
draw.dispose();
|
|
111
|
+
draw.copy(template);
|
|
112
|
+
for (const name of Object.keys(template.geometry.attributes))
|
|
113
|
+
draw.geometry.setAttribute(name, template.geometry.getAttribute(name));
|
|
114
|
+
draw.geometry.setIndex(template.geometry.index);
|
|
115
|
+
const firstId = family.ids.values().next().value!;
|
|
116
|
+
for (let i = 0; i < ORDERED_BATCH_CAPACITY; i++) {
|
|
117
|
+
draw.addInstance(firstId);
|
|
118
|
+
draw.setVisibleAt(i, false);
|
|
119
|
+
}
|
|
120
|
+
family.draws.push(draw);
|
|
121
|
+
this.created++;
|
|
122
|
+
this.drawCount++;
|
|
123
|
+
}
|
|
124
|
+
return draw;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
geometryId(key: string, geometry: THREE.BufferGeometry): number { return this.families.get(key)!.ids.get(geometry.id)!; }
|
|
128
|
+
|
|
129
|
+
inspect() { return {bytes: this.bytes, draws: this.drawCount, pending: this.pending,
|
|
130
|
+
refusedFamilies: [...this.families.values()].filter(f => f.refused).length}; }
|
|
131
|
+
|
|
132
|
+
private release(family: Family): void {
|
|
133
|
+
for (const draw of family.draws) {
|
|
134
|
+
draw.removeFromParent();
|
|
135
|
+
// BatchedMesh.dispose owns its geometry. Detach shared buffer attributes
|
|
136
|
+
// first, so retiring one run cannot delete another run's GPU buffers.
|
|
137
|
+
for (const name of Object.keys(draw.geometry.attributes)) draw.geometry.deleteAttribute(name);
|
|
138
|
+
draw.geometry.setIndex(null);
|
|
139
|
+
draw.dispose();
|
|
140
|
+
this.drawCount--;
|
|
141
|
+
}
|
|
142
|
+
family.draws.length = 0;
|
|
143
|
+
family.template?.dispose();
|
|
144
|
+
family.template = null;
|
|
145
|
+
family.ids.clear();
|
|
146
|
+
this.bytes -= family.bytes;
|
|
147
|
+
family.bytes = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
clear(): void {
|
|
151
|
+
for (const family of this.families.values()) this.release(family);
|
|
152
|
+
this.families.clear();
|
|
153
|
+
this.registrations.clear();
|
|
154
|
+
this.pending = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -1243,12 +1243,19 @@ export class BlenderRuntimeView {
|
|
|
1243
1243
|
presenterChanged();
|
|
1244
1244
|
}
|
|
1245
1245
|
|
|
1246
|
+
private drawPreparation: {
|
|
1247
|
+
last: {opaqueMs: number; motionMs: number; transparentMs: number} | null;
|
|
1248
|
+
navigation: {opaqueMs: number; motionMs: number; transparentMs: number} | null;
|
|
1249
|
+
} = {last: null, navigation: null};
|
|
1250
|
+
|
|
1246
1251
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1247
1252
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1248
|
-
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number}): () => void {
|
|
1253
|
+
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean}): () => void {
|
|
1249
1254
|
try {
|
|
1255
|
+
const start = performance.now();
|
|
1250
1256
|
this.instances.prepareDraw(camera);
|
|
1251
1257
|
this.root.updateMatrixWorld(true);
|
|
1258
|
+
const opaqueDone = performance.now();
|
|
1252
1259
|
const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
|
|
1253
1260
|
options?.interactive === true, options?.height ?? 0);
|
|
1254
1261
|
// A changed navigation geometry is an ordinary-draw fallback for an
|
|
@@ -1256,7 +1263,12 @@ export class BlenderRuntimeView {
|
|
|
1256
1263
|
// runs are planned AFTER per-placement geometry selection, preserving
|
|
1257
1264
|
// their sorting barriers and compatible geometry grouping.
|
|
1258
1265
|
if (motion) this.instances.sync();
|
|
1259
|
-
|
|
1266
|
+
const motionDone = performance.now();
|
|
1267
|
+
this.transparentInstances.prepare(camera, options?.multiDraw === true);
|
|
1268
|
+
const timing = {opaqueMs: opaqueDone - start, motionMs: motionDone - opaqueDone,
|
|
1269
|
+
transparentMs: performance.now() - motionDone};
|
|
1270
|
+
this.drawPreparation.last = timing;
|
|
1271
|
+
if (motion) this.drawPreparation.navigation = timing;
|
|
1260
1272
|
} catch (error) {
|
|
1261
1273
|
this.motionGeometry.finish();
|
|
1262
1274
|
this.instances.finishDraw();
|
|
@@ -2075,7 +2087,7 @@ export class BlenderRuntimeView {
|
|
|
2075
2087
|
}
|
|
2076
2088
|
|
|
2077
2089
|
drawStatistics() {
|
|
2078
|
-
return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2090
|
+
return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2079
2091
|
}
|
|
2080
2092
|
|
|
2081
2093
|
snapshot() {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import {BlenderOrderedBatches, ORDERED_BATCH_CAPACITY} from './blender-ordered-batches';
|
|
3
|
+
import {presenterChanged} from './blender-presenter-change';
|
|
2
4
|
import {graphDrawAttributes} from './blender-graph-material';
|
|
3
5
|
import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
|
|
4
6
|
|
|
5
|
-
const CAPACITY =
|
|
6
|
-
type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; groupOrder: number; z: number; centre: THREE.Vector3};
|
|
7
|
-
type Draw = {mesh: THREE.InstancedMesh; members: THREE.Mesh[]};
|
|
7
|
+
const CAPACITY = ORDERED_BATCH_CAPACITY;
|
|
8
|
+
type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; batchKey: string | null; groupOrder: number; z: number; centre: THREE.Vector3};
|
|
9
|
+
type Draw = {mesh: THREE.InstancedMesh | THREE.BatchedMesh; members: THREE.Mesh[]};
|
|
8
10
|
|
|
9
11
|
function groupOrder(mesh: THREE.Object3D): number {
|
|
10
12
|
for (let parent = mesh.parent; parent; parent = parent.parent)
|
|
@@ -19,6 +21,9 @@ function groupOrder(mesh: THREE.Object3D): number {
|
|
|
19
21
|
* This does not turn blended foliage into cutouts or change its opacity. */
|
|
20
22
|
export class BlenderTransparentInstances {
|
|
21
23
|
private objects: THREE.Mesh[] = [];
|
|
24
|
+
private readonly ordered = new BlenderOrderedBatches();
|
|
25
|
+
private readonly mixedDraws = new Map<THREE.BatchedMesh, Draw>();
|
|
26
|
+
private multiDraw = false;
|
|
22
27
|
private readonly pools = new Map<string, Draw[]>();
|
|
23
28
|
private readonly hidden: THREE.Mesh[] = [];
|
|
24
29
|
private readonly inverse = new THREE.Matrix4();
|
|
@@ -29,6 +34,7 @@ export class BlenderTransparentInstances {
|
|
|
29
34
|
private readonly depth = new THREE.Vector4();
|
|
30
35
|
private batches = 0;
|
|
31
36
|
private instances = 0;
|
|
37
|
+
private excluded: Record<string, number> = {};
|
|
32
38
|
private readonly planState: (number | string)[] = [];
|
|
33
39
|
private readonly planned: Draw[] = [];
|
|
34
40
|
private stateIndex = 0;
|
|
@@ -41,12 +47,15 @@ export class BlenderTransparentInstances {
|
|
|
41
47
|
this.objects = [...objects].filter((object): object is THREE.Mesh => (object as THREE.Mesh).isMesh === true);
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
prepare(camera: THREE.Camera): void {
|
|
50
|
+
prepare(camera: THREE.Camera, multiDraw = false): void {
|
|
45
51
|
this.finishDraw();
|
|
46
52
|
if (!this.objects.length) return;
|
|
47
53
|
camera.updateMatrixWorld();
|
|
48
54
|
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
49
|
-
|
|
55
|
+
const capabilityChanged = this.multiDraw !== multiDraw;
|
|
56
|
+
this.multiDraw = multiDraw;
|
|
57
|
+
const inputsChanged = this.planChanged(camera);
|
|
58
|
+
if (!inputsChanged && !capabilityChanged && !this.ordered.pending) {
|
|
50
59
|
// The draw plan is a disposable presentation, not evaluated scene data.
|
|
51
60
|
// Reuse only after exact comparison of every input to culling/ordering;
|
|
52
61
|
// a camera move, drag, visibility or material change rebuilds it below.
|
|
@@ -60,11 +69,14 @@ export class BlenderTransparentInstances {
|
|
|
60
69
|
return;
|
|
61
70
|
}
|
|
62
71
|
this.planned.length = 0;
|
|
72
|
+
for (const draw of this.mixedDraws.values()) draw.members.length = 0;
|
|
73
|
+
this.ordered.begin();
|
|
63
74
|
for (const pool of this.pools.values()) for (const draw of pool) {
|
|
64
75
|
draw.mesh.visible = false;
|
|
65
76
|
draw.members.length = 0;
|
|
66
77
|
}
|
|
67
78
|
this.batches = this.instances = 0;
|
|
79
|
+
this.excluded = {};
|
|
68
80
|
this.frustum.setFromProjectionMatrix(this.projection);
|
|
69
81
|
this.inverse.copy(this.root.matrixWorld).invert();
|
|
70
82
|
const entries: Entry[] = [];
|
|
@@ -118,8 +130,22 @@ export class BlenderTransparentInstances {
|
|
|
118
130
|
key = groupKey;
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
|
-
|
|
133
|
+
if (key === null) {
|
|
134
|
+
const reason = !candidate ? 'multipleMaterials'
|
|
135
|
+
: (candidate as THREE.ShaderMaterial).isShaderMaterial ? 'customShader'
|
|
136
|
+
: (candidate as THREE.MeshPhysicalMaterial).transmission > 0 ? 'transmission'
|
|
137
|
+
: candidate.side === THREE.DoubleSide && !candidate.forceSinglePass ? 'twoPass'
|
|
138
|
+
: 'geometryLayersOrTransform';
|
|
139
|
+
this.excluded[reason] = (this.excluded[reason] ?? 0) + 1;
|
|
140
|
+
}
|
|
141
|
+
const batchKey = multiDraw && key && material ? this.ordered.register(mesh.geometry, material, mesh.renderOrder) : null;
|
|
142
|
+
entries.push({mesh, material, key, batchKey, groupOrder: order, z, centre});
|
|
122
143
|
}
|
|
144
|
+
this.ordered.pack();
|
|
145
|
+
// A family may exceed the bounded cache. Retain its original instancing
|
|
146
|
+
// key rather than merging incompatible geometries on the fallback path.
|
|
147
|
+
for (const entry of entries) if (entry.batchKey && this.ordered.available(entry.batchKey)) entry.key = entry.batchKey;
|
|
148
|
+
for (const [mesh] of this.mixedDraws) if (!mesh.parent) this.mixedDraws.delete(mesh);
|
|
123
149
|
entries.sort((a, b) => a.groupOrder - b.groupOrder || a.mesh.renderOrder - b.mesh.renderOrder || b.z - a.z || a.mesh.id - b.mesh.id);
|
|
124
150
|
const used = new Map<string, number>();
|
|
125
151
|
for (let begin = 0; begin < entries.length;) {
|
|
@@ -143,9 +169,26 @@ export class BlenderTransparentInstances {
|
|
|
143
169
|
const head = entries[offset]!;
|
|
144
170
|
const index = used.get(first.key) ?? 0;
|
|
145
171
|
used.set(first.key, index + 1);
|
|
172
|
+
const mixed = first.key.startsWith('multi:');
|
|
173
|
+
let draw: Draw | undefined;
|
|
174
|
+
if (mixed) {
|
|
175
|
+
const mesh = this.ordered.get(first.key, index);
|
|
176
|
+
if (!mesh) continue;
|
|
177
|
+
draw = this.mixedDraws.get(mesh);
|
|
178
|
+
if (!draw) {
|
|
179
|
+
draw = {mesh, members: []};
|
|
180
|
+
this.mixedDraws.set(mesh, draw);
|
|
181
|
+
const held = draw;
|
|
182
|
+
mesh.raycast = (raycaster, hits) => {
|
|
183
|
+
if (mesh.visible) for (const member of held.members)
|
|
184
|
+
if (instanceObjectShown(member)) member.raycast(raycaster, hits);
|
|
185
|
+
};
|
|
186
|
+
this.root.add(mesh);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
146
189
|
let pool = this.pools.get(first.key);
|
|
147
190
|
if (!pool) { pool = []; this.pools.set(first.key, pool); }
|
|
148
|
-
|
|
191
|
+
if (!mixed) draw = pool[index];
|
|
149
192
|
if (!draw) {
|
|
150
193
|
const mesh = new THREE.InstancedMesh(first.mesh.geometry, first.material!, CAPACITY);
|
|
151
194
|
mesh.name = 'Blender ordered transparency';
|
|
@@ -174,12 +217,20 @@ export class BlenderTransparentInstances {
|
|
|
174
217
|
const member = entries[offset + i]!.mesh;
|
|
175
218
|
this.matrix.multiplyMatrices(this.inverse, member.matrixWorld);
|
|
176
219
|
draw.mesh.setMatrixAt(i, this.matrix);
|
|
220
|
+
if (draw.mesh instanceof THREE.BatchedMesh) {
|
|
221
|
+
draw.mesh.setGeometryIdAt(i, this.ordered.geometryId(first.key, member.geometry));
|
|
222
|
+
draw.mesh.setVisibleAt(i, true);
|
|
223
|
+
}
|
|
177
224
|
draw.members.push(member);
|
|
178
225
|
member.layers.disable(0);
|
|
179
226
|
this.hidden.push(member);
|
|
180
227
|
}
|
|
181
|
-
draw.mesh.
|
|
182
|
-
|
|
228
|
+
if (draw.mesh instanceof THREE.BatchedMesh) {
|
|
229
|
+
for (let i = length; i < CAPACITY; i++) draw.mesh.setVisibleAt(i, false);
|
|
230
|
+
} else {
|
|
231
|
+
draw.mesh.count = length;
|
|
232
|
+
draw.mesh.instanceMatrix.needsUpdate = true;
|
|
233
|
+
}
|
|
183
234
|
this.planned.push(draw);
|
|
184
235
|
this.batches++;
|
|
185
236
|
this.instances += length;
|
|
@@ -187,9 +238,12 @@ export class BlenderTransparentInstances {
|
|
|
187
238
|
}
|
|
188
239
|
begin = end;
|
|
189
240
|
}
|
|
241
|
+
// A quiet viewport must also finish bounded pool construction. Notify only
|
|
242
|
+
// after this draw's temporary layer/geometry swaps have been restored.
|
|
243
|
+
if (this.ordered.pending) queueMicrotask(presenterChanged);
|
|
190
244
|
}
|
|
191
245
|
|
|
192
|
-
inspect() { return {batches: this.batches, instances: this.instances}; }
|
|
246
|
+
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect()}; }
|
|
193
247
|
|
|
194
248
|
private observe(value: number | string): void {
|
|
195
249
|
if (this.planState[this.stateIndex] !== value) {
|
|
@@ -207,6 +261,7 @@ export class BlenderTransparentInstances {
|
|
|
207
261
|
this.observe(camera.layers.mask);
|
|
208
262
|
for (const n of this.projection.elements) this.observe(n);
|
|
209
263
|
for (const n of this.root.matrixWorld.elements) this.observe(n);
|
|
264
|
+
const observedGeometry = new Set<THREE.BufferGeometry>();
|
|
210
265
|
for (const mesh of this.objects) {
|
|
211
266
|
this.observe(mesh.parent?.id ?? -1);
|
|
212
267
|
this.observe(instanceObjectShown(mesh) ? 1 : 0);
|
|
@@ -218,6 +273,19 @@ export class BlenderTransparentInstances {
|
|
|
218
273
|
for (const n of mesh.matrixWorld.elements) this.observe(n);
|
|
219
274
|
const geometry = mesh.geometry;
|
|
220
275
|
this.observe(geometry.id);
|
|
276
|
+
if (!observedGeometry.has(geometry)) {
|
|
277
|
+
observedGeometry.add(geometry);
|
|
278
|
+
this.observe(geometry.index?.id ?? -1);
|
|
279
|
+
this.observe(geometry.index?.version ?? -1);
|
|
280
|
+
this.observe(JSON.stringify(geometry.userData['blenderUvChannels'] ?? null));
|
|
281
|
+
for (const name of Object.keys(geometry.attributes).sort()) {
|
|
282
|
+
const attribute = geometry.getAttribute(name);
|
|
283
|
+
this.observe(name);
|
|
284
|
+
if (attribute instanceof THREE.BufferAttribute) {
|
|
285
|
+
this.observe(attribute.id); this.observe(attribute.version);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
221
289
|
this.observe(geometry.drawRange.start);
|
|
222
290
|
this.observe(geometry.drawRange.count);
|
|
223
291
|
this.observe(geometry.index?.count ?? geometry.getAttribute('position').count);
|
|
@@ -249,6 +317,7 @@ export class BlenderTransparentInstances {
|
|
|
249
317
|
for (const mesh of this.hidden) mesh.layers.enable(0);
|
|
250
318
|
this.hidden.length = 0;
|
|
251
319
|
for (const pool of this.pools.values()) for (const {mesh} of pool) mesh.visible = false;
|
|
320
|
+
for (const {mesh} of this.mixedDraws.values()) mesh.visible = false;
|
|
252
321
|
}
|
|
253
322
|
|
|
254
323
|
clear(): void {
|
|
@@ -258,6 +327,8 @@ export class BlenderTransparentInstances {
|
|
|
258
327
|
mesh.dispose();
|
|
259
328
|
}
|
|
260
329
|
this.pools.clear();
|
|
330
|
+
this.ordered.clear();
|
|
331
|
+
this.mixedDraws.clear();
|
|
261
332
|
this.planned.length = 0;
|
|
262
333
|
this.planState.length = 0;
|
|
263
334
|
this.objects = [];
|