@volter/blender-engine 0.1.66 → 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.
|
@@ -148,6 +148,30 @@ export function graphUvChannels(material: THREE.MeshPhysicalMaterial): number[]
|
|
|
148
148
|
return Object.values(bindings.get(material)?.channels ?? {});
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/** Attributes a physical graph draw can read, including its pending program.
|
|
152
|
+
* Resident geometry retains many more authored layers than a shader consumes;
|
|
153
|
+
* only shader inputs count toward WebGL's vertex attribute limit. */
|
|
154
|
+
export function graphDrawAttributes(material: THREE.Material, geometry: THREE.BufferGeometry): Set<string> {
|
|
155
|
+
const names = new Set(['position', 'normal', 'uv']);
|
|
156
|
+
if (geometry.hasAttribute('tangent')) names.add('tangent');
|
|
157
|
+
if (material.vertexColors) names.add('color');
|
|
158
|
+
if (!(material instanceof THREE.MeshPhysicalMaterial)) return names;
|
|
159
|
+
const channels = geometry.userData['blenderUvChannels'] as Record<string, number> | undefined;
|
|
160
|
+
const uv = (name: string) => {
|
|
161
|
+
const channel = name === '' ? 0 : (channels?.[name] ?? 9);
|
|
162
|
+
names.add(channel === 0 ? 'uv' : `uv${channel}`);
|
|
163
|
+
};
|
|
164
|
+
for (const texture of [material.map, material.roughnessMap, material.normalMap])
|
|
165
|
+
if (texture) uv((texture.userData['blenderUvName'] as string | undefined) ?? '');
|
|
166
|
+
for (const binding of [bindings.get(material), pendings.get(material)?.binding]) {
|
|
167
|
+
if (!binding) continue;
|
|
168
|
+
names.add('blenderOrco');
|
|
169
|
+
for (const name of binding.compiled.uvs) uv(name);
|
|
170
|
+
for (const name of binding.compiled.attributes) names.add(graphAttributeName(name));
|
|
171
|
+
}
|
|
172
|
+
return names;
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
/** The channel each named UV layer is in on `geometry` (a layer the mesh
|
|
152
176
|
* lacks reads zeros from channel 9, Blender's missing attribute). */
|
|
153
177
|
function channelsFor(compiled: CompiledGraph, geometry: THREE.BufferGeometry): Record<string, number> {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import { graphDrawAttributes } from './blender-graph-material';
|
|
2
3
|
|
|
3
4
|
// Small spatial batches retain frustum rejection without duplicating geometry.
|
|
4
5
|
const BATCH_SIZE = 64;
|
|
@@ -11,7 +12,7 @@ function sameMaterials(a: THREE.Material | THREE.Material[], b: THREE.Material |
|
|
|
11
12
|
: a === b;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
function
|
|
15
|
+
export function instanceObjectShown(object: THREE.Object3D): boolean {
|
|
15
16
|
for (let node: THREE.Object3D | null = object; node; node = node.parent)
|
|
16
17
|
if (!node.visible) return false;
|
|
17
18
|
return true;
|
|
@@ -19,7 +20,7 @@ function shown(object: THREE.Object3D): boolean {
|
|
|
19
20
|
|
|
20
21
|
/** Three's instance normal transform supports positive, orthogonal scale axes.
|
|
21
22
|
* Mirrored, singular and sheared placements keep the ordinary Mesh draw. */
|
|
22
|
-
function
|
|
23
|
+
export function instanceMatrixSupported(matrix: THREE.Matrix4): boolean {
|
|
23
24
|
if (matrix.determinant() <= 0) return false;
|
|
24
25
|
const e = matrix.elements;
|
|
25
26
|
const x = Math.hypot(e[0]!, e[1]!, e[2]!);
|
|
@@ -42,6 +43,8 @@ export class BlenderRuntimeInstances {
|
|
|
42
43
|
private readonly inverse = new THREE.Matrix4();
|
|
43
44
|
private readonly matrix = new THREE.Matrix4();
|
|
44
45
|
private readonly box = new THREE.Box3();
|
|
46
|
+
private eligible = 0;
|
|
47
|
+
private excluded: Record<string, number> = {};
|
|
45
48
|
|
|
46
49
|
constructor(private readonly root: THREE.Group) {}
|
|
47
50
|
|
|
@@ -49,21 +52,24 @@ export class BlenderRuntimeInstances {
|
|
|
49
52
|
this.clear();
|
|
50
53
|
if (!enabled) return;
|
|
51
54
|
const groups = new Map<string, THREE.Mesh[]>();
|
|
55
|
+
const exclude = (reason: string) => { this.excluded[reason] = (this.excluded[reason] ?? 0) + 1; };
|
|
52
56
|
this.inverse.copy(this.root.matrixWorld).invert();
|
|
53
57
|
for (const object of objects) {
|
|
54
58
|
const mesh = object as THREE.Mesh;
|
|
55
|
-
if (!mesh.isMesh
|
|
59
|
+
if (!mesh.isMesh) continue;
|
|
60
|
+
if ((mesh as THREE.SkinnedMesh).isSkinnedMesh ||
|
|
56
61
|
(mesh as THREE.InstancedMesh).isInstancedMesh || mesh.morphTargetInfluences ||
|
|
57
|
-
mesh.layers.mask !== 1) continue;
|
|
58
|
-
// WebGL2 guarantees sixteen vertex attributes; the instance matrix uses
|
|
59
|
-
// four. Keep richly attributed meshes on the ordinary path rather than
|
|
60
|
-
// dropping UV/colour channels or risking a refused shader.
|
|
61
|
-
if (Object.values(mesh.geometry.attributes).reduce((count, attribute) => count + Math.ceil(attribute.itemSize / 4), 0) > 12) continue;
|
|
62
|
+
mesh.layers.mask !== 1) { exclude('deformationOrLayers'); continue; }
|
|
62
63
|
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
63
64
|
if (materials.some(m => m.transparent || (m as THREE.MeshPhysicalMaterial).transmission > 0 ||
|
|
64
|
-
(m as THREE.ShaderMaterial).isShaderMaterial)) continue;
|
|
65
|
+
(m as THREE.ShaderMaterial).isShaderMaterial)) { exclude('material'); continue; }
|
|
66
|
+
// Four slots for instanceMatrix, at most twelve for each actual program.
|
|
67
|
+
// Count active inputs, NOT every stored authored layer: retaining unused
|
|
68
|
+
// UVs/colours must not disable batching. No channel is deleted to fit.
|
|
69
|
+
if (materials.some(m => graphDrawAttributes(m, mesh.geometry).size > 12)) { exclude('attributeBudget'); continue; }
|
|
65
70
|
this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
|
|
66
|
-
if (!
|
|
71
|
+
if (!instanceMatrixSupported(this.matrix)) { exclude('transform'); continue; }
|
|
72
|
+
this.eligible++;
|
|
67
73
|
const key = `${mesh.geometry.id}:${Array.isArray(mesh.material) ? 'slots' : 'single'}:` +
|
|
68
74
|
materials.map(m => m.uuid).join(',') + `:${mesh.renderOrder}`;
|
|
69
75
|
const group = groups.get(key);
|
|
@@ -98,7 +104,7 @@ export class BlenderRuntimeInstances {
|
|
|
98
104
|
// Return the canonical mesh, not the internal batch or an unstable slot.
|
|
99
105
|
draw.raycast = (raycaster, hits) => {
|
|
100
106
|
for (const {mesh} of batch.members)
|
|
101
|
-
if (mesh.layers.mask === 0 &&
|
|
107
|
+
if (mesh.layers.mask === 0 && instanceObjectShown(mesh)) mesh.raycast(raycaster, hits);
|
|
102
108
|
};
|
|
103
109
|
this.root.add(draw);
|
|
104
110
|
draw.updateMatrixWorld(true);
|
|
@@ -124,13 +130,13 @@ export class BlenderRuntimeInstances {
|
|
|
124
130
|
// A drag can introduce negative scale/shear, or a skin can replace a
|
|
125
131
|
// mesh between frames. Fall back immediately without touching its data.
|
|
126
132
|
const compatible = mesh.parent !== null && mesh.geometry === draw.geometry &&
|
|
127
|
-
sameMaterials(mesh.material, draw.material) &&
|
|
133
|
+
sameMaterials(mesh.material, draw.material) && instanceMatrixSupported(this.matrix);
|
|
128
134
|
if (!compatible) {
|
|
129
135
|
mesh.layers.mask |= member.layers;
|
|
130
136
|
continue;
|
|
131
137
|
}
|
|
132
138
|
mesh.layers.mask &= ~member.layers;
|
|
133
|
-
if (!
|
|
139
|
+
if (!instanceObjectShown(mesh)) continue;
|
|
134
140
|
const offset = count * 16;
|
|
135
141
|
if (this.matrix.elements.some((value, i) => Math.fround(value) !== draw.instanceMatrix.array[offset + i])) {
|
|
136
142
|
draw.setMatrixAt(count, this.matrix);
|
|
@@ -155,9 +161,12 @@ export class BlenderRuntimeInstances {
|
|
|
155
161
|
draw.dispose(); // releases instance attributes, never shared geometry/material
|
|
156
162
|
}
|
|
157
163
|
this.batches.length = 0;
|
|
164
|
+
this.eligible = 0;
|
|
165
|
+
this.excluded = {};
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
inspect() {
|
|
161
|
-
return {batches: this.batches.length, instances: this.batches.reduce((count, {draw}) => count + draw.count, 0)
|
|
169
|
+
return {batches: this.batches.length, instances: this.batches.reduce((count, {draw}) => count + draw.count, 0),
|
|
170
|
+
eligible: this.eligible, excluded: {...this.excluded}};
|
|
162
171
|
}
|
|
163
172
|
}
|
|
@@ -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.
|
|
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.
|
|
2040
|
+
instancing: this.drawStatistics(),
|
|
2020
2041
|
objects: [...this.objects].map(([id, object]) => ({
|
|
2021
2042
|
id,
|
|
2022
2043
|
name: object.name,
|
|
@@ -2027,6 +2048,10 @@ export class BlenderRuntimeView {
|
|
|
2027
2048
|
};
|
|
2028
2049
|
}
|
|
2029
2050
|
|
|
2051
|
+
drawStatistics() {
|
|
2052
|
+
return {opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2030
2055
|
snapshot() {
|
|
2031
2056
|
return this.frame;
|
|
2032
2057
|
}
|
|
@@ -2234,6 +2259,7 @@ export class BlenderRuntimeView {
|
|
|
2234
2259
|
}
|
|
2235
2260
|
|
|
2236
2261
|
private clear() {
|
|
2262
|
+
this.transparentInstances.clear();
|
|
2237
2263
|
this.instances.clear();
|
|
2238
2264
|
for (const entry of this.staged?.meshes.values() ?? []) entry.geometry.dispose();
|
|
2239
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
|
+
}
|