@volter/blender-engine 0.1.67 → 0.1.68

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.
@@ -12,7 +12,7 @@ function sameMaterials(a: THREE.Material | THREE.Material[], b: THREE.Material |
12
12
  : a === b;
13
13
  }
14
14
 
15
- function shown(object: THREE.Object3D): boolean {
15
+ export function instanceObjectShown(object: THREE.Object3D): boolean {
16
16
  for (let node: THREE.Object3D | null = object; node; node = node.parent)
17
17
  if (!node.visible) return false;
18
18
  return true;
@@ -20,7 +20,7 @@ function shown(object: THREE.Object3D): boolean {
20
20
 
21
21
  /** Three's instance normal transform supports positive, orthogonal scale axes.
22
22
  * Mirrored, singular and sheared placements keep the ordinary Mesh draw. */
23
- function supported(matrix: THREE.Matrix4): boolean {
23
+ export function instanceMatrixSupported(matrix: THREE.Matrix4): boolean {
24
24
  if (matrix.determinant() <= 0) return false;
25
25
  const e = matrix.elements;
26
26
  const x = Math.hypot(e[0]!, e[1]!, e[2]!);
@@ -68,7 +68,7 @@ export class BlenderRuntimeInstances {
68
68
  // UVs/colours must not disable batching. No channel is deleted to fit.
69
69
  if (materials.some(m => graphDrawAttributes(m, mesh.geometry).size > 12)) { exclude('attributeBudget'); continue; }
70
70
  this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
71
- if (!supported(this.matrix)) { exclude('transform'); continue; }
71
+ if (!instanceMatrixSupported(this.matrix)) { exclude('transform'); continue; }
72
72
  this.eligible++;
73
73
  const key = `${mesh.geometry.id}:${Array.isArray(mesh.material) ? 'slots' : 'single'}:` +
74
74
  materials.map(m => m.uuid).join(',') + `:${mesh.renderOrder}`;
@@ -104,7 +104,7 @@ export class BlenderRuntimeInstances {
104
104
  // Return the canonical mesh, not the internal batch or an unstable slot.
105
105
  draw.raycast = (raycaster, hits) => {
106
106
  for (const {mesh} of batch.members)
107
- if (mesh.layers.mask === 0 && shown(mesh)) mesh.raycast(raycaster, hits);
107
+ if (mesh.layers.mask === 0 && instanceObjectShown(mesh)) mesh.raycast(raycaster, hits);
108
108
  };
109
109
  this.root.add(draw);
110
110
  draw.updateMatrixWorld(true);
@@ -130,13 +130,13 @@ export class BlenderRuntimeInstances {
130
130
  // A drag can introduce negative scale/shear, or a skin can replace a
131
131
  // mesh between frames. Fall back immediately without touching its data.
132
132
  const compatible = mesh.parent !== null && mesh.geometry === draw.geometry &&
133
- sameMaterials(mesh.material, draw.material) && supported(this.matrix);
133
+ sameMaterials(mesh.material, draw.material) && instanceMatrixSupported(this.matrix);
134
134
  if (!compatible) {
135
135
  mesh.layers.mask |= member.layers;
136
136
  continue;
137
137
  }
138
138
  mesh.layers.mask &= ~member.layers;
139
- if (!shown(mesh)) continue;
139
+ if (!instanceObjectShown(mesh)) continue;
140
140
  const offset = count * 16;
141
141
  if (this.matrix.elements.some((value, i) => Math.fround(value) !== draw.instanceMatrix.array[offset + i])) {
142
142
  draw.setMatrixAt(count, this.matrix);
@@ -43,6 +43,7 @@ import {
43
43
  } from './blender-runtime-camera-view';
44
44
  import { DEFAULT_VIEWPORT_DISPLAY, workbenchMaterial } from './blender-workbench-material';
45
45
  import { BlenderRuntimeInstances } from './blender-runtime-instances';
46
+ import { BlenderTransparentInstances } from './blender-transparent-instances';
46
47
 
47
48
  /** A saved `View3DShading.type` as the stage's draw mode: Material Preview is `preview`. */
48
49
  const SAVED_SHADING = {
@@ -642,6 +643,7 @@ export interface PhotographRecord {
642
643
  export class BlenderRuntimeView {
643
644
  readonly root = new THREE.Group();
644
645
  private readonly instances = new BlenderRuntimeInstances(this.root);
646
+ private readonly transparentInstances = new BlenderTransparentInstances(this.root);
645
647
  private readonly objects = new Map<string, THREE.Object3D>();
646
648
  private readonly meshes = new Map<
647
649
  string,
@@ -1036,7 +1038,7 @@ export class BlenderRuntimeView {
1036
1038
  for (const child of children) next.add(child);
1037
1039
  parent?.add(next);
1038
1040
  this.objects.set(id, next);
1039
- this.instances.rebuild(this.objects.values(), !this.rendered);
1041
+ this.rebuildInstances();
1040
1042
  return;
1041
1043
  }
1042
1044
  }
@@ -1220,7 +1222,26 @@ export class BlenderRuntimeView {
1220
1222
  material.dispose();
1221
1223
  this.workbenchMaterials.delete(key);
1222
1224
  }
1225
+ this.rebuildInstances();
1226
+ }
1227
+
1228
+ private rebuildInstances(): void {
1229
+ this.transparentInstances.clear();
1223
1230
  this.instances.rebuild(this.objects.values(), !this.rendered);
1231
+ if (!this.rendered) this.transparentInstances.setObjects(this.objects.values());
1232
+ }
1233
+
1234
+ /** Before the renderer uploads attributes/builds its queues, for this area's
1235
+ * actual camera. Transparent instance runs are camera-order dependent. */
1236
+ prepareDraw(camera: THREE.Camera): () => void {
1237
+ this.root.updateMatrixWorld(true);
1238
+ try {
1239
+ this.transparentInstances.prepare(camera);
1240
+ } catch (error) {
1241
+ this.transparentInstances.finishDraw();
1242
+ throw error;
1243
+ }
1244
+ return () => this.transparentInstances.finishDraw();
1224
1245
  }
1225
1246
 
1226
1247
  private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
@@ -2016,7 +2037,7 @@ export class BlenderRuntimeView {
2016
2037
  active: this.frame?.active,
2017
2038
  mode: this.frame?.mode,
2018
2039
  geometryBuilds: this.geometryBuilds,
2019
- instancing: this.instances.inspect(),
2040
+ instancing: this.drawStatistics(),
2020
2041
  objects: [...this.objects].map(([id, object]) => ({
2021
2042
  id,
2022
2043
  name: object.name,
@@ -2028,7 +2049,7 @@ export class BlenderRuntimeView {
2028
2049
  }
2029
2050
 
2030
2051
  drawStatistics() {
2031
- return this.instances.inspect();
2052
+ return {opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
2032
2053
  }
2033
2054
 
2034
2055
  snapshot() {
@@ -2238,6 +2259,7 @@ export class BlenderRuntimeView {
2238
2259
  }
2239
2260
 
2240
2261
  private clear() {
2262
+ this.transparentInstances.clear();
2241
2263
  this.instances.clear();
2242
2264
  for (const entry of this.staged?.meshes.values() ?? []) entry.geometry.dispose();
2243
2265
  this.staged = null;
@@ -0,0 +1,184 @@
1
+ import * as THREE from 'three';
2
+ import {graphDrawAttributes} from './blender-graph-material';
3
+ import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
4
+
5
+ const CAPACITY = 64;
6
+ type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; z: number; centre: THREE.Vector3};
7
+ type Draw = {mesh: THREE.InstancedMesh; members: THREE.Mesh[]};
8
+
9
+ /** Preserve Three's transparent object order. An instance run may contain only
10
+ * consecutive, compatible, single-pass surfaces in that order. Incompatible
11
+ * surfaces remain barriers; ties at a run boundary stay ordinary draws because
12
+ * Three breaks equal-depth ties by object id, and an internal batch has a new id.
13
+ * This does not turn blended foliage into cutouts or change its opacity. */
14
+ export class BlenderTransparentInstances {
15
+ private objects: THREE.Mesh[] = [];
16
+ private readonly pools = new Map<string, Draw[]>();
17
+ private readonly hidden: THREE.Mesh[] = [];
18
+ private readonly inverse = new THREE.Matrix4();
19
+ private readonly matrix = new THREE.Matrix4();
20
+ private readonly projection = new THREE.Matrix4();
21
+ private readonly frustum = new THREE.Frustum();
22
+ private readonly centre = new THREE.Vector3();
23
+ private readonly depth = new THREE.Vector4();
24
+ private batches = 0;
25
+ private instances = 0;
26
+
27
+ constructor(private readonly root: THREE.Group) {}
28
+
29
+ setObjects(objects: Iterable<THREE.Object3D>): void {
30
+ this.clear();
31
+ this.objects = [...objects].filter((object): object is THREE.Mesh => (object as THREE.Mesh).isMesh === true);
32
+ }
33
+
34
+ prepare(camera: THREE.Camera): void {
35
+ for (const mesh of this.hidden) mesh.layers.enable(0);
36
+ this.hidden.length = 0;
37
+ for (const pool of this.pools.values()) for (const draw of pool) {
38
+ draw.mesh.visible = false;
39
+ draw.members.length = 0;
40
+ }
41
+ this.batches = this.instances = 0;
42
+ if (!this.objects.length) return;
43
+ camera.updateMatrixWorld();
44
+ this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
45
+ this.frustum.setFromProjectionMatrix(this.projection);
46
+ this.inverse.copy(this.root.matrixWorld).invert();
47
+ const entries: Entry[] = [];
48
+ const eligibility = new Map<string, boolean>();
49
+ for (const mesh of this.objects) {
50
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
51
+ if (!materials.some(m => m.visible && m.transparent)) continue;
52
+ if (!instanceObjectShown(mesh) || !mesh.layers.test(camera.layers)) continue;
53
+ if (mesh.frustumCulled && !this.frustum.intersectsObject(mesh)) continue;
54
+ // Match WebGLRenderer.projectObject's sorting centre, including a skin's
55
+ // own sphere. Positions alone are not the centre of off-origin geometry.
56
+ const bounded = mesh as THREE.Mesh & {boundingSphere?: THREE.Sphere | null; computeBoundingSphere?: () => void};
57
+ if (bounded.boundingSphere !== undefined) {
58
+ if (bounded.boundingSphere === null) bounded.computeBoundingSphere?.();
59
+ this.centre.copy(bounded.boundingSphere!.center);
60
+ } else {
61
+ if (!mesh.geometry.boundingSphere) mesh.geometry.computeBoundingSphere();
62
+ this.centre.copy(mesh.geometry.boundingSphere!.center);
63
+ }
64
+ this.centre.applyMatrix4(mesh.matrixWorld);
65
+ const centre = this.centre.clone();
66
+ // WebGLRenderer sorts homogeneous clip z, before the perspective divide.
67
+ const z = this.depth.set(centre.x, centre.y, centre.z, 1).applyMatrix4(this.projection).z;
68
+ let material: THREE.Material | null = null;
69
+ let key: string | null = null;
70
+ const candidate = materials.length === 1 ? materials[0]! : null;
71
+ if (candidate && candidate.visible && candidate.transparent &&
72
+ !(candidate as THREE.ShaderMaterial).isShaderMaterial &&
73
+ !((candidate as THREE.MeshPhysicalMaterial).transmission > 0) &&
74
+ (candidate.side !== THREE.DoubleSide || candidate.forceSinglePass) &&
75
+ !(mesh as THREE.SkinnedMesh).isSkinnedMesh && !mesh.morphTargetInfluences &&
76
+ !(mesh as THREE.InstancedMesh).isInstancedMesh && mesh.layers.mask === 1) {
77
+ const geometry = mesh.geometry;
78
+ // A single material draws the whole geometry. An array does so only
79
+ // when its sole group covers the same range; preserve all other cases.
80
+ const count = geometry.index?.count ?? geometry.getAttribute('position').count;
81
+ const whole = !Array.isArray(mesh.material) || (geometry.groups.length === 1 &&
82
+ geometry.groups[0]!.start === 0 && geometry.groups[0]!.count === count &&
83
+ geometry.groups[0]!.materialIndex === 0);
84
+ const groupKey = `${geometry.id}:${candidate.uuid}:${mesh.renderOrder}`;
85
+ let fits = eligibility.get(groupKey);
86
+ if (fits === undefined) {
87
+ fits = graphDrawAttributes(candidate, geometry).size <= 12;
88
+ eligibility.set(groupKey, fits);
89
+ }
90
+ this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
91
+ if (whole && fits && instanceMatrixSupported(this.matrix)) {
92
+ material = candidate;
93
+ key = groupKey;
94
+ }
95
+ }
96
+ entries.push({mesh, material, key, z, centre});
97
+ }
98
+ entries.sort((a, b) => a.mesh.renderOrder - b.mesh.renderOrder || b.z - a.z || a.mesh.id - b.mesh.id);
99
+ const used = new Map<string, number>();
100
+ for (let begin = 0; begin < entries.length;) {
101
+ const first = entries[begin]!;
102
+ let end = begin + 1;
103
+ if (first.key !== null) while (end < entries.length && entries[end]!.key === first.key) end++;
104
+ const previous = entries[begin - 1];
105
+ const following = entries[end];
106
+ const tied = (previous?.z === first.z && previous.mesh.renderOrder === first.mesh.renderOrder) ||
107
+ (following?.z === entries[end - 1]!.z && following.mesh.renderOrder === first.mesh.renderOrder);
108
+ if (first.key !== null && end - begin >= 2 && !tied) {
109
+ // Equal-depth members may not straddle separate batches: their new
110
+ // object ids must never become the tie breaker. Keep such a run whole
111
+ // on the ordinary path if a capacity boundary would split a tie.
112
+ let safe = true;
113
+ for (let i = begin + CAPACITY; i < end; i += CAPACITY)
114
+ if (entries[i - 1]!.z === entries[i]!.z) safe = false;
115
+ if (safe) for (let offset = begin; offset < end; offset += CAPACITY) {
116
+ const length = Math.min(CAPACITY, end - offset);
117
+ if (length < 2) continue;
118
+ const head = entries[offset]!;
119
+ const index = used.get(first.key) ?? 0;
120
+ used.set(first.key, index + 1);
121
+ let pool = this.pools.get(first.key);
122
+ if (!pool) { pool = []; this.pools.set(first.key, pool); }
123
+ let draw = pool[index];
124
+ if (!draw) {
125
+ const mesh = new THREE.InstancedMesh(first.mesh.geometry, first.material!, CAPACITY);
126
+ mesh.name = 'Blender ordered transparency';
127
+ mesh.userData['engineInternal'] = true;
128
+ mesh.matrixAutoUpdate = false;
129
+ // Each member was culled above. The sphere is a sorting anchor,
130
+ // deliberately not an aggregate sphere that changes draw order.
131
+ mesh.frustumCulled = false;
132
+ mesh.boundingSphere = new THREE.Sphere();
133
+ mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
134
+ draw = {mesh, members: []};
135
+ const held = draw;
136
+ mesh.raycast = (raycaster, hits) => {
137
+ if (!mesh.visible) return;
138
+ for (const member of held.members)
139
+ if (instanceObjectShown(member)) member.raycast(raycaster, hits);
140
+ };
141
+ pool.push(draw);
142
+ this.root.add(mesh);
143
+ }
144
+ draw.mesh.visible = true;
145
+ draw.mesh.renderOrder = head.mesh.renderOrder;
146
+ draw.mesh.boundingSphere!.center.copy(head.centre).applyMatrix4(this.inverse);
147
+ draw.mesh.updateMatrixWorld(true);
148
+ for (let i = 0; i < length; i++) {
149
+ const member = entries[offset + i]!.mesh;
150
+ this.matrix.multiplyMatrices(this.inverse, member.matrixWorld);
151
+ draw.mesh.setMatrixAt(i, this.matrix);
152
+ draw.members.push(member);
153
+ member.layers.disable(0);
154
+ this.hidden.push(member);
155
+ }
156
+ draw.mesh.count = length;
157
+ draw.mesh.instanceMatrix.needsUpdate = true;
158
+ this.batches++;
159
+ this.instances += length;
160
+ }
161
+ }
162
+ begin = end;
163
+ }
164
+ }
165
+
166
+ inspect() { return {batches: this.batches, instances: this.instances}; }
167
+
168
+ finishDraw(): void {
169
+ for (const mesh of this.hidden) mesh.layers.enable(0);
170
+ this.hidden.length = 0;
171
+ for (const pool of this.pools.values()) for (const {mesh} of pool) mesh.visible = false;
172
+ }
173
+
174
+ clear(): void {
175
+ this.finishDraw();
176
+ for (const pool of this.pools.values()) for (const {mesh} of pool) {
177
+ mesh.removeFromParent();
178
+ mesh.dispose();
179
+ }
180
+ this.pools.clear();
181
+ this.objects = [];
182
+ this.batches = this.instances = 0;
183
+ }
184
+ }
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.67",
5
+ "version": "0.1.68",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },