@volter/blender-engine 0.1.77 → 0.1.79
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-material-ranges.ts +100 -0
- package/browser/three/blender-ordered-batches.ts +156 -0
- package/browser/three/blender-runtime-view.ts +14 -6
- package/browser/three/blender-transparent-instances.ts +71 -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);
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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,13 +1247,13 @@ 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
|
|
1252
1255
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1253
|
-
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number}): () => void {
|
|
1256
|
+
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean}): () => void {
|
|
1254
1257
|
try {
|
|
1255
1258
|
const start = performance.now();
|
|
1256
1259
|
this.instances.prepareDraw(camera);
|
|
@@ -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();
|
|
1267
|
-
this.
|
|
1270
|
+
this.materialRanges.prepare();
|
|
1271
|
+
const rangesDone = performance.now();
|
|
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();
|
|
@@ -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();
|
|
@@ -42,12 +47,15 @@ export class BlenderTransparentInstances {
|
|
|
42
47
|
this.objects = [...objects].filter((object): object is THREE.Mesh => (object as THREE.Mesh).isMesh === true);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
prepare(camera: THREE.Camera): void {
|
|
50
|
+
prepare(camera: THREE.Camera, multiDraw = false): void {
|
|
46
51
|
this.finishDraw();
|
|
47
52
|
if (!this.objects.length) return;
|
|
48
53
|
camera.updateMatrixWorld();
|
|
49
54
|
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
50
|
-
|
|
55
|
+
const capabilityChanged = this.multiDraw !== multiDraw;
|
|
56
|
+
this.multiDraw = multiDraw;
|
|
57
|
+
const inputsChanged = this.planChanged(camera);
|
|
58
|
+
if (!inputsChanged && !capabilityChanged && !this.ordered.pending) {
|
|
51
59
|
// The draw plan is a disposable presentation, not evaluated scene data.
|
|
52
60
|
// Reuse only after exact comparison of every input to culling/ordering;
|
|
53
61
|
// a camera move, drag, visibility or material change rebuilds it below.
|
|
@@ -61,6 +69,8 @@ export class BlenderTransparentInstances {
|
|
|
61
69
|
return;
|
|
62
70
|
}
|
|
63
71
|
this.planned.length = 0;
|
|
72
|
+
for (const draw of this.mixedDraws.values()) draw.members.length = 0;
|
|
73
|
+
this.ordered.begin();
|
|
64
74
|
for (const pool of this.pools.values()) for (const draw of pool) {
|
|
65
75
|
draw.mesh.visible = false;
|
|
66
76
|
draw.members.length = 0;
|
|
@@ -128,8 +138,14 @@ export class BlenderTransparentInstances {
|
|
|
128
138
|
: 'geometryLayersOrTransform';
|
|
129
139
|
this.excluded[reason] = (this.excluded[reason] ?? 0) + 1;
|
|
130
140
|
}
|
|
131
|
-
|
|
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});
|
|
132
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);
|
|
133
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);
|
|
134
150
|
const used = new Map<string, number>();
|
|
135
151
|
for (let begin = 0; begin < entries.length;) {
|
|
@@ -153,9 +169,26 @@ export class BlenderTransparentInstances {
|
|
|
153
169
|
const head = entries[offset]!;
|
|
154
170
|
const index = used.get(first.key) ?? 0;
|
|
155
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
|
+
}
|
|
156
189
|
let pool = this.pools.get(first.key);
|
|
157
190
|
if (!pool) { pool = []; this.pools.set(first.key, pool); }
|
|
158
|
-
|
|
191
|
+
if (!mixed) draw = pool[index];
|
|
159
192
|
if (!draw) {
|
|
160
193
|
const mesh = new THREE.InstancedMesh(first.mesh.geometry, first.material!, CAPACITY);
|
|
161
194
|
mesh.name = 'Blender ordered transparency';
|
|
@@ -184,12 +217,20 @@ export class BlenderTransparentInstances {
|
|
|
184
217
|
const member = entries[offset + i]!.mesh;
|
|
185
218
|
this.matrix.multiplyMatrices(this.inverse, member.matrixWorld);
|
|
186
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
|
+
}
|
|
187
224
|
draw.members.push(member);
|
|
188
225
|
member.layers.disable(0);
|
|
189
226
|
this.hidden.push(member);
|
|
190
227
|
}
|
|
191
|
-
draw.mesh.
|
|
192
|
-
|
|
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
|
+
}
|
|
193
234
|
this.planned.push(draw);
|
|
194
235
|
this.batches++;
|
|
195
236
|
this.instances += length;
|
|
@@ -197,9 +238,12 @@ export class BlenderTransparentInstances {
|
|
|
197
238
|
}
|
|
198
239
|
begin = end;
|
|
199
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);
|
|
200
244
|
}
|
|
201
245
|
|
|
202
|
-
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}}; }
|
|
246
|
+
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect()}; }
|
|
203
247
|
|
|
204
248
|
private observe(value: number | string): void {
|
|
205
249
|
if (this.planState[this.stateIndex] !== value) {
|
|
@@ -217,6 +261,7 @@ export class BlenderTransparentInstances {
|
|
|
217
261
|
this.observe(camera.layers.mask);
|
|
218
262
|
for (const n of this.projection.elements) this.observe(n);
|
|
219
263
|
for (const n of this.root.matrixWorld.elements) this.observe(n);
|
|
264
|
+
const observedGeometry = new Set<THREE.BufferGeometry>();
|
|
220
265
|
for (const mesh of this.objects) {
|
|
221
266
|
this.observe(mesh.parent?.id ?? -1);
|
|
222
267
|
this.observe(instanceObjectShown(mesh) ? 1 : 0);
|
|
@@ -228,6 +273,19 @@ export class BlenderTransparentInstances {
|
|
|
228
273
|
for (const n of mesh.matrixWorld.elements) this.observe(n);
|
|
229
274
|
const geometry = mesh.geometry;
|
|
230
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
|
+
}
|
|
231
289
|
this.observe(geometry.drawRange.start);
|
|
232
290
|
this.observe(geometry.drawRange.count);
|
|
233
291
|
this.observe(geometry.index?.count ?? geometry.getAttribute('position').count);
|
|
@@ -259,6 +317,7 @@ export class BlenderTransparentInstances {
|
|
|
259
317
|
for (const mesh of this.hidden) mesh.layers.enable(0);
|
|
260
318
|
this.hidden.length = 0;
|
|
261
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;
|
|
262
321
|
}
|
|
263
322
|
|
|
264
323
|
clear(): void {
|
|
@@ -268,6 +327,8 @@ export class BlenderTransparentInstances {
|
|
|
268
327
|
mesh.dispose();
|
|
269
328
|
}
|
|
270
329
|
this.pools.clear();
|
|
330
|
+
this.ordered.clear();
|
|
331
|
+
this.mixedDraws.clear();
|
|
271
332
|
this.planned.length = 0;
|
|
272
333
|
this.planState.length = 0;
|
|
273
334
|
this.objects = [];
|