@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.
@@ -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 = modelMatrix * instanceMatrix;
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.transparentInstances.prepare(camera);
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() - motionDone};
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 = 64;
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
- if (!this.planChanged(camera)) {
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
- entries.push({mesh, material, key, groupOrder: order, z, centre});
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
- let draw = pool[index];
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.count = length;
192
- draw.mesh.instanceMatrix.needsUpdate = true;
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 = [];
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/blender-engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "GPL-3.0-or-later",
5
- "version": "0.1.77",
5
+ "version": "0.1.79",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },