@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.
@@ -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);
@@ -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
- // Frobenius norm bounds the linear transform even under shear; the
310
- // longest axis alone can understate the projected simplifier error.
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 scale = Math.hypot(e[0]!, e[1]!, e[2]!, e[4]!, e[5]!, e[6]!, e[8]!, e[9]!, e[10]!);
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
- this.transparentInstances.prepare(camera);
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 = 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();
@@ -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
- 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) {
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
- entries.push({mesh, material, key, groupOrder: order, z, centre});
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
- let draw = pool[index];
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.count = length;
182
- 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
+ }
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 = [];
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.76",
5
+ "version": "0.1.78",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },