@volter/blender-engine 0.1.67 → 0.1.69
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
|
|
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
|
|
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]!);
|
|
@@ -43,6 +43,9 @@ export class BlenderRuntimeInstances {
|
|
|
43
43
|
private readonly inverse = new THREE.Matrix4();
|
|
44
44
|
private readonly matrix = new THREE.Matrix4();
|
|
45
45
|
private readonly box = new THREE.Box3();
|
|
46
|
+
private readonly projection = new THREE.Matrix4();
|
|
47
|
+
private readonly frustum = new THREE.Frustum();
|
|
48
|
+
private camera: THREE.Camera | null = null;
|
|
46
49
|
private eligible = 0;
|
|
47
50
|
private excluded: Record<string, number> = {};
|
|
48
51
|
|
|
@@ -68,7 +71,7 @@ export class BlenderRuntimeInstances {
|
|
|
68
71
|
// UVs/colours must not disable batching. No channel is deleted to fit.
|
|
69
72
|
if (materials.some(m => graphDrawAttributes(m, mesh.geometry).size > 12)) { exclude('attributeBudget'); continue; }
|
|
70
73
|
this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
|
|
71
|
-
if (!
|
|
74
|
+
if (!instanceMatrixSupported(this.matrix)) { exclude('transform'); continue; }
|
|
72
75
|
this.eligible++;
|
|
73
76
|
const key = `${mesh.geometry.id}:${Array.isArray(mesh.material) ? 'slots' : 'single'}:` +
|
|
74
77
|
materials.map(m => m.uuid).join(',') + `:${mesh.renderOrder}`;
|
|
@@ -104,7 +107,7 @@ export class BlenderRuntimeInstances {
|
|
|
104
107
|
// Return the canonical mesh, not the internal batch or an unstable slot.
|
|
105
108
|
draw.raycast = (raycaster, hits) => {
|
|
106
109
|
for (const {mesh} of batch.members)
|
|
107
|
-
if (mesh.layers.mask === 0 &&
|
|
110
|
+
if (mesh.layers.mask === 0 && instanceObjectShown(mesh)) mesh.raycast(raycaster, hits);
|
|
108
111
|
};
|
|
109
112
|
this.root.add(draw);
|
|
110
113
|
draw.updateMatrixWorld(true);
|
|
@@ -130,13 +133,17 @@ export class BlenderRuntimeInstances {
|
|
|
130
133
|
// A drag can introduce negative scale/shear, or a skin can replace a
|
|
131
134
|
// mesh between frames. Fall back immediately without touching its data.
|
|
132
135
|
const compatible = mesh.parent !== null && mesh.geometry === draw.geometry &&
|
|
133
|
-
sameMaterials(mesh.material, draw.material) &&
|
|
136
|
+
sameMaterials(mesh.material, draw.material) && instanceMatrixSupported(this.matrix);
|
|
134
137
|
if (!compatible) {
|
|
135
138
|
mesh.layers.mask |= member.layers;
|
|
136
139
|
continue;
|
|
137
140
|
}
|
|
138
141
|
mesh.layers.mask &= ~member.layers;
|
|
139
|
-
if (!
|
|
142
|
+
if (!instanceObjectShown(mesh)) continue;
|
|
143
|
+
// Aggregate batch bounds alone admit off-screen members. Retain the
|
|
144
|
+
// same per-object rejection as ordinary draws before compacting slots.
|
|
145
|
+
if (this.camera && (!this.camera.layers.isEnabled(0) ||
|
|
146
|
+
(mesh.frustumCulled && !this.frustum.intersectsObject(mesh)))) continue;
|
|
140
147
|
const offset = count * 16;
|
|
141
148
|
if (this.matrix.elements.some((value, i) => Math.fround(value) !== draw.instanceMatrix.array[offset + i])) {
|
|
142
149
|
draw.setMatrixAt(count, this.matrix);
|
|
@@ -154,7 +161,17 @@ export class BlenderRuntimeInstances {
|
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
|
|
164
|
+
prepareDraw(camera: THREE.Camera): void {
|
|
165
|
+
camera.updateMatrixWorld();
|
|
166
|
+
this.camera = camera;
|
|
167
|
+
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
168
|
+
this.frustum.setFromProjectionMatrix(this.projection);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
finishDraw(): void { this.camera = null; }
|
|
172
|
+
|
|
157
173
|
clear(): void {
|
|
174
|
+
this.camera = null;
|
|
158
175
|
for (const {draw, members} of this.batches) {
|
|
159
176
|
for (const {mesh, layers} of members) mesh.layers.mask |= layers;
|
|
160
177
|
draw.removeFromParent();
|
|
@@ -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,8 @@ 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);
|
|
647
|
+
private drawBatching = true;
|
|
645
648
|
private readonly objects = new Map<string, THREE.Object3D>();
|
|
646
649
|
private readonly meshes = new Map<
|
|
647
650
|
string,
|
|
@@ -1036,7 +1039,7 @@ export class BlenderRuntimeView {
|
|
|
1036
1039
|
for (const child of children) next.add(child);
|
|
1037
1040
|
parent?.add(next);
|
|
1038
1041
|
this.objects.set(id, next);
|
|
1039
|
-
this.
|
|
1042
|
+
this.rebuildInstances();
|
|
1040
1043
|
return;
|
|
1041
1044
|
}
|
|
1042
1045
|
}
|
|
@@ -1220,7 +1223,39 @@ export class BlenderRuntimeView {
|
|
|
1220
1223
|
material.dispose();
|
|
1221
1224
|
this.workbenchMaterials.delete(key);
|
|
1222
1225
|
}
|
|
1223
|
-
this.
|
|
1226
|
+
this.rebuildInstances();
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
private rebuildInstances(): void {
|
|
1230
|
+
this.transparentInstances.clear();
|
|
1231
|
+
this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
|
|
1232
|
+
if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Presentation-only comparison door; never changes Blender or its data. */
|
|
1236
|
+
setDrawBatching(enabled: boolean): void {
|
|
1237
|
+
if (enabled === this.drawBatching) return;
|
|
1238
|
+
this.drawBatching = enabled;
|
|
1239
|
+
this.rebuildInstances();
|
|
1240
|
+
presenterChanged();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1244
|
+
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1245
|
+
prepareDraw(camera: THREE.Camera): () => void {
|
|
1246
|
+
try {
|
|
1247
|
+
this.instances.prepareDraw(camera);
|
|
1248
|
+
this.root.updateMatrixWorld(true);
|
|
1249
|
+
this.transparentInstances.prepare(camera);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
this.instances.finishDraw();
|
|
1252
|
+
this.transparentInstances.finishDraw();
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
return () => {
|
|
1256
|
+
this.instances.finishDraw();
|
|
1257
|
+
this.transparentInstances.finishDraw();
|
|
1258
|
+
};
|
|
1224
1259
|
}
|
|
1225
1260
|
|
|
1226
1261
|
private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
|
|
@@ -2016,7 +2051,7 @@ export class BlenderRuntimeView {
|
|
|
2016
2051
|
active: this.frame?.active,
|
|
2017
2052
|
mode: this.frame?.mode,
|
|
2018
2053
|
geometryBuilds: this.geometryBuilds,
|
|
2019
|
-
instancing: this.
|
|
2054
|
+
instancing: this.drawStatistics(),
|
|
2020
2055
|
objects: [...this.objects].map(([id, object]) => ({
|
|
2021
2056
|
id,
|
|
2022
2057
|
name: object.name,
|
|
@@ -2028,7 +2063,7 @@ export class BlenderRuntimeView {
|
|
|
2028
2063
|
}
|
|
2029
2064
|
|
|
2030
2065
|
drawStatistics() {
|
|
2031
|
-
return this.instances.inspect();
|
|
2066
|
+
return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
|
|
2032
2067
|
}
|
|
2033
2068
|
|
|
2034
2069
|
snapshot() {
|
|
@@ -2238,6 +2273,7 @@ export class BlenderRuntimeView {
|
|
|
2238
2273
|
}
|
|
2239
2274
|
|
|
2240
2275
|
private clear() {
|
|
2276
|
+
this.transparentInstances.clear();
|
|
2241
2277
|
this.instances.clear();
|
|
2242
2278
|
for (const entry of this.staged?.meshes.values() ?? []) entry.geometry.dispose();
|
|
2243
2279
|
this.staged = null;
|
|
@@ -0,0 +1,266 @@
|
|
|
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; groupOrder: number; z: number; centre: THREE.Vector3};
|
|
7
|
+
type Draw = {mesh: THREE.InstancedMesh; members: THREE.Mesh[]};
|
|
8
|
+
|
|
9
|
+
function groupOrder(mesh: THREE.Object3D): number {
|
|
10
|
+
for (let parent = mesh.parent; parent; parent = parent.parent)
|
|
11
|
+
if ((parent as THREE.Group).isGroup) return parent.renderOrder;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Preserve Three's transparent object order. An instance run may contain only
|
|
16
|
+
* consecutive, compatible, single-pass surfaces in that order. Incompatible
|
|
17
|
+
* surfaces remain barriers; ties at a run boundary stay ordinary draws because
|
|
18
|
+
* Three breaks equal-depth ties by object id, and an internal batch has a new id.
|
|
19
|
+
* This does not turn blended foliage into cutouts or change its opacity. */
|
|
20
|
+
export class BlenderTransparentInstances {
|
|
21
|
+
private objects: THREE.Mesh[] = [];
|
|
22
|
+
private readonly pools = new Map<string, Draw[]>();
|
|
23
|
+
private readonly hidden: THREE.Mesh[] = [];
|
|
24
|
+
private readonly inverse = new THREE.Matrix4();
|
|
25
|
+
private readonly matrix = new THREE.Matrix4();
|
|
26
|
+
private readonly projection = new THREE.Matrix4();
|
|
27
|
+
private readonly frustum = new THREE.Frustum();
|
|
28
|
+
private readonly centre = new THREE.Vector3();
|
|
29
|
+
private readonly depth = new THREE.Vector4();
|
|
30
|
+
private batches = 0;
|
|
31
|
+
private instances = 0;
|
|
32
|
+
private readonly planState: (number | string)[] = [];
|
|
33
|
+
private readonly planned: Draw[] = [];
|
|
34
|
+
private stateIndex = 0;
|
|
35
|
+
private stateChanged = false;
|
|
36
|
+
|
|
37
|
+
constructor(private readonly root: THREE.Group) {}
|
|
38
|
+
|
|
39
|
+
setObjects(objects: Iterable<THREE.Object3D>): void {
|
|
40
|
+
this.clear();
|
|
41
|
+
this.objects = [...objects].filter((object): object is THREE.Mesh => (object as THREE.Mesh).isMesh === true);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
prepare(camera: THREE.Camera): void {
|
|
45
|
+
this.finishDraw();
|
|
46
|
+
if (!this.objects.length) return;
|
|
47
|
+
camera.updateMatrixWorld();
|
|
48
|
+
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
49
|
+
if (!this.planChanged(camera)) {
|
|
50
|
+
// The draw plan is a disposable presentation, not evaluated scene data.
|
|
51
|
+
// Reuse only after exact comparison of every input to culling/ordering;
|
|
52
|
+
// a camera move, drag, visibility or material change rebuilds it below.
|
|
53
|
+
for (const draw of this.planned) {
|
|
54
|
+
draw.mesh.visible = true;
|
|
55
|
+
for (const member of draw.members) {
|
|
56
|
+
member.layers.disable(0);
|
|
57
|
+
this.hidden.push(member);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.planned.length = 0;
|
|
63
|
+
for (const pool of this.pools.values()) for (const draw of pool) {
|
|
64
|
+
draw.mesh.visible = false;
|
|
65
|
+
draw.members.length = 0;
|
|
66
|
+
}
|
|
67
|
+
this.batches = this.instances = 0;
|
|
68
|
+
this.frustum.setFromProjectionMatrix(this.projection);
|
|
69
|
+
this.inverse.copy(this.root.matrixWorld).invert();
|
|
70
|
+
const entries: Entry[] = [];
|
|
71
|
+
const eligibility = new Map<string, boolean>();
|
|
72
|
+
for (const mesh of this.objects) {
|
|
73
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
74
|
+
if (!materials.some(m => m.visible && m.transparent)) continue;
|
|
75
|
+
if (!instanceObjectShown(mesh) || !mesh.layers.test(camera.layers)) continue;
|
|
76
|
+
if (mesh.frustumCulled && !this.frustum.intersectsObject(mesh)) continue;
|
|
77
|
+
// Match WebGLRenderer.projectObject's sorting centre, including a skin's
|
|
78
|
+
// own sphere. Positions alone are not the centre of off-origin geometry.
|
|
79
|
+
const bounded = mesh as THREE.Mesh & {boundingSphere?: THREE.Sphere | null; computeBoundingSphere?: () => void};
|
|
80
|
+
if (bounded.boundingSphere !== undefined) {
|
|
81
|
+
if (bounded.boundingSphere === null) bounded.computeBoundingSphere?.();
|
|
82
|
+
this.centre.copy(bounded.boundingSphere!.center);
|
|
83
|
+
} else {
|
|
84
|
+
if (!mesh.geometry.boundingSphere) mesh.geometry.computeBoundingSphere();
|
|
85
|
+
this.centre.copy(mesh.geometry.boundingSphere!.center);
|
|
86
|
+
}
|
|
87
|
+
this.centre.applyMatrix4(mesh.matrixWorld);
|
|
88
|
+
const centre = this.centre.clone();
|
|
89
|
+
// WebGLRenderer sorts homogeneous clip z, before the perspective divide.
|
|
90
|
+
const z = this.depth.set(centre.x, centre.y, centre.z, 1).applyMatrix4(this.projection).z;
|
|
91
|
+
let material: THREE.Material | null = null;
|
|
92
|
+
let key: string | null = null;
|
|
93
|
+
const candidate = materials.length === 1 ? materials[0]! : null;
|
|
94
|
+
const order = groupOrder(mesh);
|
|
95
|
+
if (candidate && candidate.visible && candidate.transparent &&
|
|
96
|
+
!(candidate as THREE.ShaderMaterial).isShaderMaterial &&
|
|
97
|
+
!((candidate as THREE.MeshPhysicalMaterial).transmission > 0) &&
|
|
98
|
+
(candidate.side !== THREE.DoubleSide || candidate.forceSinglePass) &&
|
|
99
|
+
!(mesh as THREE.SkinnedMesh).isSkinnedMesh && !mesh.morphTargetInfluences &&
|
|
100
|
+
!(mesh as THREE.InstancedMesh).isInstancedMesh && mesh.layers.mask === 1 &&
|
|
101
|
+
order === this.root.renderOrder) {
|
|
102
|
+
const geometry = mesh.geometry;
|
|
103
|
+
// A single material draws the whole geometry. An array does so only
|
|
104
|
+
// when its sole group covers the same range; preserve all other cases.
|
|
105
|
+
const count = geometry.index?.count ?? geometry.getAttribute('position').count;
|
|
106
|
+
const whole = !Array.isArray(mesh.material) || (geometry.groups.length === 1 &&
|
|
107
|
+
geometry.groups[0]!.start === 0 && geometry.groups[0]!.count === count &&
|
|
108
|
+
geometry.groups[0]!.materialIndex === 0);
|
|
109
|
+
const groupKey = `${geometry.id}:${candidate.uuid}:${mesh.renderOrder}`;
|
|
110
|
+
let fits = eligibility.get(groupKey);
|
|
111
|
+
if (fits === undefined) {
|
|
112
|
+
fits = graphDrawAttributes(candidate, geometry).size <= 12;
|
|
113
|
+
eligibility.set(groupKey, fits);
|
|
114
|
+
}
|
|
115
|
+
this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
|
|
116
|
+
if (whole && fits && instanceMatrixSupported(this.matrix)) {
|
|
117
|
+
material = candidate;
|
|
118
|
+
key = groupKey;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
entries.push({mesh, material, key, groupOrder: order, z, centre});
|
|
122
|
+
}
|
|
123
|
+
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
|
+
const used = new Map<string, number>();
|
|
125
|
+
for (let begin = 0; begin < entries.length;) {
|
|
126
|
+
const first = entries[begin]!;
|
|
127
|
+
let end = begin + 1;
|
|
128
|
+
if (first.key !== null) while (end < entries.length && entries[end]!.key === first.key) end++;
|
|
129
|
+
const previous = entries[begin - 1];
|
|
130
|
+
const following = entries[end];
|
|
131
|
+
const tied = (previous?.z === first.z && previous.mesh.renderOrder === first.mesh.renderOrder) ||
|
|
132
|
+
(following?.z === entries[end - 1]!.z && following.mesh.renderOrder === first.mesh.renderOrder);
|
|
133
|
+
if (first.key !== null && end - begin >= 2 && !tied) {
|
|
134
|
+
// Equal-depth members may not straddle separate batches: their new
|
|
135
|
+
// object ids must never become the tie breaker. Keep such a run whole
|
|
136
|
+
// on the ordinary path if a capacity boundary would split a tie.
|
|
137
|
+
let safe = true;
|
|
138
|
+
for (let i = begin + CAPACITY; i < end; i += CAPACITY)
|
|
139
|
+
if (entries[i - 1]!.z === entries[i]!.z) safe = false;
|
|
140
|
+
if (safe) for (let offset = begin; offset < end; offset += CAPACITY) {
|
|
141
|
+
const length = Math.min(CAPACITY, end - offset);
|
|
142
|
+
if (length < 2) continue;
|
|
143
|
+
const head = entries[offset]!;
|
|
144
|
+
const index = used.get(first.key) ?? 0;
|
|
145
|
+
used.set(first.key, index + 1);
|
|
146
|
+
let pool = this.pools.get(first.key);
|
|
147
|
+
if (!pool) { pool = []; this.pools.set(first.key, pool); }
|
|
148
|
+
let draw = pool[index];
|
|
149
|
+
if (!draw) {
|
|
150
|
+
const mesh = new THREE.InstancedMesh(first.mesh.geometry, first.material!, CAPACITY);
|
|
151
|
+
mesh.name = 'Blender ordered transparency';
|
|
152
|
+
mesh.userData['engineInternal'] = true;
|
|
153
|
+
mesh.matrixAutoUpdate = false;
|
|
154
|
+
// Each member was culled above. The sphere is a sorting anchor,
|
|
155
|
+
// deliberately not an aggregate sphere that changes draw order.
|
|
156
|
+
mesh.frustumCulled = false;
|
|
157
|
+
mesh.boundingSphere = new THREE.Sphere();
|
|
158
|
+
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
159
|
+
draw = {mesh, members: []};
|
|
160
|
+
const held = draw;
|
|
161
|
+
mesh.raycast = (raycaster, hits) => {
|
|
162
|
+
if (!mesh.visible) return;
|
|
163
|
+
for (const member of held.members)
|
|
164
|
+
if (instanceObjectShown(member)) member.raycast(raycaster, hits);
|
|
165
|
+
};
|
|
166
|
+
pool.push(draw);
|
|
167
|
+
this.root.add(mesh);
|
|
168
|
+
}
|
|
169
|
+
draw.mesh.visible = true;
|
|
170
|
+
draw.mesh.renderOrder = head.mesh.renderOrder;
|
|
171
|
+
draw.mesh.boundingSphere!.center.copy(head.centre).applyMatrix4(this.inverse);
|
|
172
|
+
draw.mesh.updateMatrixWorld(true);
|
|
173
|
+
for (let i = 0; i < length; i++) {
|
|
174
|
+
const member = entries[offset + i]!.mesh;
|
|
175
|
+
this.matrix.multiplyMatrices(this.inverse, member.matrixWorld);
|
|
176
|
+
draw.mesh.setMatrixAt(i, this.matrix);
|
|
177
|
+
draw.members.push(member);
|
|
178
|
+
member.layers.disable(0);
|
|
179
|
+
this.hidden.push(member);
|
|
180
|
+
}
|
|
181
|
+
draw.mesh.count = length;
|
|
182
|
+
draw.mesh.instanceMatrix.needsUpdate = true;
|
|
183
|
+
this.planned.push(draw);
|
|
184
|
+
this.batches++;
|
|
185
|
+
this.instances += length;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
begin = end;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
inspect() { return {batches: this.batches, instances: this.instances}; }
|
|
193
|
+
|
|
194
|
+
private observe(value: number | string): void {
|
|
195
|
+
if (this.planState[this.stateIndex] !== value) {
|
|
196
|
+
this.planState[this.stateIndex] = value;
|
|
197
|
+
this.stateChanged = true;
|
|
198
|
+
}
|
|
199
|
+
this.stateIndex++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Compare inputs without cloning 10,000 centres, allocating entries or
|
|
203
|
+
* sorting/uploading matrices again on an unchanged frame. No lossy hash. */
|
|
204
|
+
private planChanged(camera: THREE.Camera): boolean {
|
|
205
|
+
this.stateIndex = 0;
|
|
206
|
+
this.stateChanged = this.planState.length === 0;
|
|
207
|
+
this.observe(camera.layers.mask);
|
|
208
|
+
for (const n of this.projection.elements) this.observe(n);
|
|
209
|
+
for (const n of this.root.matrixWorld.elements) this.observe(n);
|
|
210
|
+
for (const mesh of this.objects) {
|
|
211
|
+
this.observe(mesh.parent?.id ?? -1);
|
|
212
|
+
this.observe(instanceObjectShown(mesh) ? 1 : 0);
|
|
213
|
+
this.observe(mesh.layers.mask);
|
|
214
|
+
this.observe(mesh.renderOrder);
|
|
215
|
+
this.observe(groupOrder(mesh));
|
|
216
|
+
this.observe(mesh.frustumCulled ? 1 : 0);
|
|
217
|
+
this.observe(mesh.morphTargetInfluences ? 1 : 0);
|
|
218
|
+
for (const n of mesh.matrixWorld.elements) this.observe(n);
|
|
219
|
+
const geometry = mesh.geometry;
|
|
220
|
+
this.observe(geometry.id);
|
|
221
|
+
this.observe(geometry.drawRange.start);
|
|
222
|
+
this.observe(geometry.drawRange.count);
|
|
223
|
+
this.observe(geometry.index?.count ?? geometry.getAttribute('position').count);
|
|
224
|
+
this.observe(geometry.groups.length);
|
|
225
|
+
for (const group of geometry.groups) {
|
|
226
|
+
this.observe(group.start); this.observe(group.count); this.observe(group.materialIndex ?? 0);
|
|
227
|
+
}
|
|
228
|
+
const sphere = (mesh as THREE.Mesh & {boundingSphere?: THREE.Sphere | null}).boundingSphere ?? geometry.boundingSphere;
|
|
229
|
+
this.observe(sphere?.center.x ?? 0); this.observe(sphere?.center.y ?? 0);
|
|
230
|
+
this.observe(sphere?.center.z ?? 0); this.observe(sphere?.radius ?? -1);
|
|
231
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : null;
|
|
232
|
+
this.observe(materials?.length ?? -1);
|
|
233
|
+
if (materials) for (const material of materials) this.observeMaterial(material);
|
|
234
|
+
else this.observeMaterial(mesh.material as THREE.Material);
|
|
235
|
+
}
|
|
236
|
+
this.stateChanged ||= this.planState.length !== this.stateIndex;
|
|
237
|
+
this.planState.length = this.stateIndex;
|
|
238
|
+
return this.stateChanged;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private observeMaterial(material: THREE.Material): void {
|
|
242
|
+
this.observe(material.uuid); this.observe(material.version);
|
|
243
|
+
this.observe(material.visible ? 1 : 0); this.observe(material.transparent ? 1 : 0);
|
|
244
|
+
this.observe(material.side); this.observe(material.forceSinglePass ? 1 : 0);
|
|
245
|
+
this.observe((material as THREE.MeshPhysicalMaterial).transmission ?? 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
finishDraw(): void {
|
|
249
|
+
for (const mesh of this.hidden) mesh.layers.enable(0);
|
|
250
|
+
this.hidden.length = 0;
|
|
251
|
+
for (const pool of this.pools.values()) for (const {mesh} of pool) mesh.visible = false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
clear(): void {
|
|
255
|
+
this.finishDraw();
|
|
256
|
+
for (const pool of this.pools.values()) for (const {mesh} of pool) {
|
|
257
|
+
mesh.removeFromParent();
|
|
258
|
+
mesh.dispose();
|
|
259
|
+
}
|
|
260
|
+
this.pools.clear();
|
|
261
|
+
this.planned.length = 0;
|
|
262
|
+
this.planState.length = 0;
|
|
263
|
+
this.objects = [];
|
|
264
|
+
this.batches = this.instances = 0;
|
|
265
|
+
}
|
|
266
|
+
}
|