@volter/blender-engine 0.1.78 → 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.
@@ -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
+ }
@@ -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,8 +1247,8 @@ 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
@@ -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();
1270
+ this.materialRanges.prepare();
1271
+ const rangesDone = performance.now();
1267
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();
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.78",
5
+ "version": "0.1.79",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },