@volter/blender-engine 0.1.81 → 0.1.83
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.
- package/browser/three/blender-draw-materials.ts +55 -0
- package/browser/three/blender-graph-material.ts +17 -0
- package/browser/three/blender-ordered-batches.ts +17 -5
- package/browser/three/blender-physical-material.ts +22 -1
- package/browser/three/blender-runtime-view.ts +1 -14
- package/browser/three/blender-transparent-instances.ts +20 -14
- package/package.json +1 -1
- package/browser/three/blender-depth-instances.ts +0 -139
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {syncPhysicalDrawMaterial} from './blender-physical-material';
|
|
3
|
+
|
|
4
|
+
type Variant = {material: THREE.MeshPhysicalMaterial; sourceVersion: number; frame: number; ready: boolean};
|
|
5
|
+
const MAX_VARIANTS = 128;
|
|
6
|
+
|
|
7
|
+
/** Three caches one current shader form per Material. Alternating ordinary,
|
|
8
|
+
* instanced and batched objects under that material repeatedly rebuilds its
|
|
9
|
+
* program parameters/uniform list, even when all programs are already linked.
|
|
10
|
+
* These small, disposable material identities isolate those caches without
|
|
11
|
+
* changing canonical material slots, geometry, transparency order or values. */
|
|
12
|
+
export class BlenderDrawMaterials {
|
|
13
|
+
private readonly sources = new Map<THREE.Material, Map<string, Variant>>();
|
|
14
|
+
private readonly geometryKeys = new Map<THREE.BufferGeometry, string>();
|
|
15
|
+
private frame = 0;
|
|
16
|
+
private count = 0;
|
|
17
|
+
|
|
18
|
+
begin(): void { this.frame++; this.geometryKeys.clear(); }
|
|
19
|
+
|
|
20
|
+
get(source: THREE.Material, mesh: THREE.InstancedMesh | THREE.BatchedMesh): THREE.Material {
|
|
21
|
+
let geometryKey = this.geometryKeys.get(mesh.geometry);
|
|
22
|
+
if (geometryKey === undefined) {
|
|
23
|
+
geometryKey = JSON.stringify([mesh.geometry.userData['blenderUvChannels'] ?? null,
|
|
24
|
+
Object.keys(mesh.geometry.attributes).sort().map(name => [name, mesh.geometry.getAttribute(name).itemSize])]);
|
|
25
|
+
this.geometryKeys.set(mesh.geometry, geometryKey);
|
|
26
|
+
}
|
|
27
|
+
const key = `${mesh instanceof THREE.BatchedMesh ? 'batch' : 'instance'}:${geometryKey}`;
|
|
28
|
+
let variants = this.sources.get(source);
|
|
29
|
+
if (!variants) { variants = new Map(); this.sources.set(source, variants); }
|
|
30
|
+
let variant = variants.get(key);
|
|
31
|
+
if (!variant) {
|
|
32
|
+
if (this.count >= MAX_VARIANTS) return source;
|
|
33
|
+
const material = syncPhysicalDrawMaterial(source);
|
|
34
|
+
if (!material) return source;
|
|
35
|
+
variant = {material, sourceVersion: source.version, frame: this.frame, ready: true};
|
|
36
|
+
variants.set(key, variant);
|
|
37
|
+
this.count++;
|
|
38
|
+
} else if (variant.frame !== this.frame) {
|
|
39
|
+
variant.ready = syncPhysicalDrawMaterial(source, variant.material) !== null;
|
|
40
|
+
if (variant.sourceVersion !== source.version) variant.material.needsUpdate = true;
|
|
41
|
+
variant.sourceVersion = source.version;
|
|
42
|
+
variant.frame = this.frame;
|
|
43
|
+
}
|
|
44
|
+
return variant.ready ? variant.material : source;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clear(): void {
|
|
48
|
+
for (const variants of this.sources.values()) for (const {material} of variants.values()) material.dispose();
|
|
49
|
+
this.sources.clear();
|
|
50
|
+
this.geometryKeys.clear();
|
|
51
|
+
this.count = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
inspect() { return {variants: this.count, limit: MAX_VARIANTS}; }
|
|
55
|
+
}
|
|
@@ -105,6 +105,23 @@ export function setMaterialGraph(
|
|
|
105
105
|
pendings.set(material, {binding, started: false});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** A disposable draw material borrows the source's live uniform values, never
|
|
109
|
+
* its owned ramp textures or geometry-dependent UV selector. Pending graphs
|
|
110
|
+
* must first draw through their source so its asynchronous compile can finish. */
|
|
111
|
+
export function borrowGraphDrawBinding(source: THREE.MeshPhysicalMaterial, target: THREE.MeshPhysicalMaterial): boolean {
|
|
112
|
+
if (pendings.has(source)) return false;
|
|
113
|
+
const sourceBinding = bindings.get(source);
|
|
114
|
+
const binding = sourceBinding && !failed.has(sourceBinding.compiled.key) ? sourceBinding : undefined;
|
|
115
|
+
const held = bindings.get(target);
|
|
116
|
+
if (!binding) {
|
|
117
|
+
if (held) { bindings.delete(target); target.needsUpdate = true; }
|
|
118
|
+
} else if (!held || held.compiled.key !== binding.compiled.key || held.uniforms !== binding.uniforms) {
|
|
119
|
+
bindings.set(target, {compiled: binding.compiled, uniforms: binding.uniforms, ramps: [], channels: {}});
|
|
120
|
+
target.needsUpdate = true;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
/** A binding's values: uniforms, ramp tables and textures. */
|
|
109
126
|
function fill(binding: Binding, compiled: CompiledGraph, texture: (image: CompiledGraph['images'][number]) => ImageTexture): void {
|
|
110
127
|
binding.compiled = compiled;
|
|
@@ -4,15 +4,16 @@ export const ORDERED_BATCH_CAPACITY = 64;
|
|
|
4
4
|
const FAMILY_BYTES = 4 * 1024 * 1024;
|
|
5
5
|
const CACHE_BYTES = 32 * 1024 * 1024;
|
|
6
6
|
const MAX_DRAWS = 1024;
|
|
7
|
-
type Shape = {geometry: THREE.BufferGeometry; stamp: string};
|
|
7
|
+
type Shape = {geometry: THREE.BufferGeometry; stamp: string; bytes: number};
|
|
8
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};
|
|
9
|
+
ids: Map<number, number>; draws: THREE.BatchedMesh[]; dirty: boolean; bytes: number; reservedBytes: number; refused: boolean};
|
|
10
10
|
|
|
11
11
|
/** Immutable packed attributes are shared between ordered draw runs. Only the
|
|
12
12
|
* matrix/indirection textures and command lists belong to an individual run.
|
|
13
13
|
* All interaction and authored geometry remain on the canonical meshes. */
|
|
14
14
|
export class BlenderOrderedBatches {
|
|
15
15
|
private readonly families = new Map<string, Family>();
|
|
16
|
+
private readonly partitions = new Map<string, string[]>();
|
|
16
17
|
private readonly registrations = new Map<string, string | null>();
|
|
17
18
|
private bytes = 0;
|
|
18
19
|
private drawCount = 0;
|
|
@@ -43,16 +44,26 @@ export class BlenderOrderedBatches {
|
|
|
43
44
|
bytes += attribute.array.byteLength;
|
|
44
45
|
}
|
|
45
46
|
if (bytes > FAMILY_BYTES) return null;
|
|
46
|
-
const
|
|
47
|
+
const layoutKey = `multi:${material.uuid}:${order}:${JSON.stringify(layout)}`;
|
|
48
|
+
let keys = this.partitions.get(layoutKey);
|
|
49
|
+
if (!keys) { keys = []; this.partitions.set(layoutKey, keys); }
|
|
50
|
+
// A new compatible shape must not evict every existing run when their
|
|
51
|
+
// union exceeds one bounded allocation (notably full + navigation copies).
|
|
52
|
+
// Partition deterministically, preserving established geometry membership.
|
|
53
|
+
let key = keys.find(key => this.families.get(key)!.shapes.has(geometry.id));
|
|
54
|
+
key ??= keys.find(key => this.families.get(key)!.reservedBytes + bytes <= FAMILY_BYTES);
|
|
55
|
+
if (!key) { key = `${layoutKey}:${keys.length}`; keys.push(key); }
|
|
47
56
|
let family = this.families.get(key);
|
|
48
57
|
if (!family) {
|
|
49
|
-
family = {material, shapes: new Map(), template: null, ids: new Map(), draws: [], dirty: true,
|
|
58
|
+
family = {material, shapes: new Map(), template: null, ids: new Map(), draws: [], dirty: true,
|
|
59
|
+
bytes: 0, reservedBytes: 0, refused: false};
|
|
50
60
|
this.families.set(key, family);
|
|
51
61
|
}
|
|
52
62
|
const stamp = JSON.stringify(versions);
|
|
53
63
|
const held = family.shapes.get(geometry.id);
|
|
54
64
|
if (!held || held.stamp !== stamp) {
|
|
55
|
-
family.
|
|
65
|
+
family.reservedBytes += bytes - (held?.bytes ?? 0);
|
|
66
|
+
family.shapes.set(geometry.id, {geometry, stamp, bytes});
|
|
56
67
|
family.dirty = true;
|
|
57
68
|
}
|
|
58
69
|
this.registrations.set(registration, key);
|
|
@@ -150,6 +161,7 @@ export class BlenderOrderedBatches {
|
|
|
150
161
|
clear(): void {
|
|
151
162
|
for (const family of this.families.values()) this.release(family);
|
|
152
163
|
this.families.clear();
|
|
164
|
+
this.partitions.clear();
|
|
153
165
|
this.registrations.clear();
|
|
154
166
|
this.pending = false;
|
|
155
167
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* that MeshPhysicalMaterial does not expose as properties. */
|
|
5
5
|
import * as THREE from 'three';
|
|
6
6
|
import {z} from 'zod';
|
|
7
|
-
import {applyGraphShader, bindGraphDraw, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
7
|
+
import {applyGraphShader, bindGraphDraw, borrowGraphDrawBinding, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
8
8
|
import {bindNamedUvChannels} from './blender-texture-samplers';
|
|
9
9
|
|
|
10
10
|
const scalar = z.number().finite();
|
|
@@ -56,6 +56,27 @@ function graphShadow(material: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMa
|
|
|
56
56
|
return shadow;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** Copy physical values into an independently cached draw form. Textures and
|
|
60
|
+
* graph uniform values remain borrowed from the authoritative source material. */
|
|
61
|
+
export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMaterial | null {
|
|
62
|
+
if (!(source instanceof THREE.MeshPhysicalMaterial) || !uniforms.has(source)) return null;
|
|
63
|
+
const copy = target ?? graphShadow(source);
|
|
64
|
+
if (!borrowGraphDrawBinding(source, copy)) {
|
|
65
|
+
if (!target) copy.dispose();
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
copy.copy(source);
|
|
69
|
+
const held = uniforms.get(source)!;
|
|
70
|
+
const values = uniforms.get(copy)!;
|
|
71
|
+
values.blenderCoatIor.value = held.blenderCoatIor.value;
|
|
72
|
+
values.blenderCoatTint.value.copy(held.blenderCoatTint.value);
|
|
73
|
+
values.blenderMapClip.value = held.blenderMapClip.value;
|
|
74
|
+
values.blenderRoughnessClip.value = held.blenderRoughnessClip.value;
|
|
75
|
+
values.blenderNormalClip.value = held.blenderNormalClip.value;
|
|
76
|
+
values.blenderWorldExtinction.value.copy(held.blenderWorldExtinction.value);
|
|
77
|
+
return copy;
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, input?: Physical,
|
|
60
81
|
clips: {map?: boolean; roughness?: boolean; normal?: boolean} = {}): void {
|
|
61
82
|
const data = input ?? defaults;
|
|
@@ -43,7 +43,6 @@ 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 { BlenderDepthInstances } from './blender-depth-instances';
|
|
47
46
|
import { BlenderTransparentInstances } from './blender-transparent-instances';
|
|
48
47
|
import { BlenderMaterialRanges } from './blender-material-ranges';
|
|
49
48
|
import { BlenderMotionGeometry } from './blender-motion-geometry';
|
|
@@ -646,7 +645,6 @@ export interface PhotographRecord {
|
|
|
646
645
|
export class BlenderRuntimeView {
|
|
647
646
|
readonly root = new THREE.Group();
|
|
648
647
|
private readonly instances = new BlenderRuntimeInstances(this.root);
|
|
649
|
-
private readonly depthInstances = new BlenderDepthInstances(this.root);
|
|
650
648
|
private readonly materialRanges = new BlenderMaterialRanges();
|
|
651
649
|
private readonly transparentInstances = new BlenderTransparentInstances(this.root);
|
|
652
650
|
private readonly motionGeometry = new BlenderMotionGeometry(presenterChanged);
|
|
@@ -1233,7 +1231,6 @@ export class BlenderRuntimeView {
|
|
|
1233
1231
|
}
|
|
1234
1232
|
|
|
1235
1233
|
private rebuildInstances(): void {
|
|
1236
|
-
this.depthInstances.setObjects(this.drawBatching && !this.rendered ? this.objects.values() : []);
|
|
1237
1234
|
this.transparentInstances.clear();
|
|
1238
1235
|
this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
|
|
1239
1236
|
if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
|
|
@@ -1292,15 +1289,6 @@ export class BlenderRuntimeView {
|
|
|
1292
1289
|
};
|
|
1293
1290
|
}
|
|
1294
1291
|
|
|
1295
|
-
prepareDepthDraw(camera: THREE.Camera, material: THREE.MeshDepthMaterial): void | (() => void) {
|
|
1296
|
-
if (!this.drawBatching || this.rendered || !this.depthInstances.supports(material)) return;
|
|
1297
|
-
const resumeColor = this.transparentInstances.suspendDraw();
|
|
1298
|
-
const restore = () => { try { this.depthInstances.finish(); } finally { resumeColor(); } };
|
|
1299
|
-
try { this.depthInstances.prepare(camera, material); }
|
|
1300
|
-
catch (error) { restore(); throw error; }
|
|
1301
|
-
return restore;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
1292
|
private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
|
|
1305
1293
|
const display = (id !== null ? this.frame?.materials[id]?.viewport : undefined) ?? DEFAULT_VIEWPORT_DISPLAY;
|
|
1306
1294
|
const key = `${display.color.join(',')}|${display.roughness}|${display.metallic}|${side}`;
|
|
@@ -2106,7 +2094,7 @@ export class BlenderRuntimeView {
|
|
|
2106
2094
|
}
|
|
2107
2095
|
|
|
2108
2096
|
drawStatistics() {
|
|
2109
|
-
return {enabled: this.drawBatching, preparation: {...this.drawPreparation},
|
|
2097
|
+
return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2110
2098
|
}
|
|
2111
2099
|
|
|
2112
2100
|
snapshot() {
|
|
@@ -2316,7 +2304,6 @@ export class BlenderRuntimeView {
|
|
|
2316
2304
|
}
|
|
2317
2305
|
|
|
2318
2306
|
private clear() {
|
|
2319
|
-
this.depthInstances.clear();
|
|
2320
2307
|
this.materialRanges.clear();
|
|
2321
2308
|
this.motionGeometry.clear();
|
|
2322
2309
|
this.transparentInstances.clear();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import {BlenderOrderedBatches, ORDERED_BATCH_CAPACITY} from './blender-ordered-batches';
|
|
3
|
+
import {BlenderDrawMaterials} from './blender-draw-materials';
|
|
3
4
|
import {presenterChanged} from './blender-presenter-change';
|
|
4
5
|
import {graphDrawAttributes} from './blender-graph-material';
|
|
5
6
|
import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
|
|
@@ -21,6 +22,7 @@ function groupOrder(mesh: THREE.Object3D): number {
|
|
|
21
22
|
* This does not turn blended foliage into cutouts or change its opacity. */
|
|
22
23
|
export class BlenderTransparentInstances {
|
|
23
24
|
private objects: THREE.Mesh[] = [];
|
|
25
|
+
private readonly drawMaterials = new BlenderDrawMaterials();
|
|
24
26
|
private readonly ordered = new BlenderOrderedBatches();
|
|
25
27
|
private readonly mixedDraws = new Map<THREE.BatchedMesh, Draw>();
|
|
26
28
|
private multiDraw = false;
|
|
@@ -35,6 +37,8 @@ export class BlenderTransparentInstances {
|
|
|
35
37
|
private batches = 0;
|
|
36
38
|
private instances = 0;
|
|
37
39
|
private excluded: Record<string, number> = {};
|
|
40
|
+
private readonly plannedProjection = new THREE.Matrix4();
|
|
41
|
+
private projectionKnown = false;
|
|
38
42
|
private readonly planState: (number | string)[] = [];
|
|
39
43
|
private readonly planned: Draw[] = [];
|
|
40
44
|
private stateIndex = 0;
|
|
@@ -49,6 +53,7 @@ export class BlenderTransparentInstances {
|
|
|
49
53
|
|
|
50
54
|
prepare(camera: THREE.Camera, multiDraw = false): void {
|
|
51
55
|
this.finishDraw();
|
|
56
|
+
this.drawMaterials.begin();
|
|
52
57
|
if (!this.objects.length) return;
|
|
53
58
|
camera.updateMatrixWorld();
|
|
54
59
|
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
@@ -61,6 +66,8 @@ export class BlenderTransparentInstances {
|
|
|
61
66
|
// a camera move, drag, visibility or material change rebuilds it below.
|
|
62
67
|
for (const draw of this.planned) {
|
|
63
68
|
draw.mesh.visible = true;
|
|
69
|
+
const source = draw.members[0]!.material;
|
|
70
|
+
draw.mesh.material = this.drawMaterials.get(Array.isArray(source) ? source[0]! : source, draw.mesh);
|
|
64
71
|
for (const member of draw.members) {
|
|
65
72
|
member.layers.disable(0);
|
|
66
73
|
this.hidden.push(member);
|
|
@@ -209,6 +216,7 @@ export class BlenderTransparentInstances {
|
|
|
209
216
|
pool.push(draw);
|
|
210
217
|
this.root.add(mesh);
|
|
211
218
|
}
|
|
219
|
+
draw.mesh.material = this.drawMaterials.get(first.material!, draw.mesh);
|
|
212
220
|
draw.mesh.visible = true;
|
|
213
221
|
draw.mesh.renderOrder = head.mesh.renderOrder;
|
|
214
222
|
draw.mesh.boundingSphere!.center.copy(head.centre).applyMatrix4(this.inverse);
|
|
@@ -243,7 +251,7 @@ export class BlenderTransparentInstances {
|
|
|
243
251
|
if (this.ordered.pending) queueMicrotask(presenterChanged);
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect()}; }
|
|
254
|
+
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect(), materials: this.drawMaterials.inspect()}; }
|
|
247
255
|
|
|
248
256
|
private observe(value: number | string): void {
|
|
249
257
|
if (this.planState[this.stateIndex] !== value) {
|
|
@@ -256,6 +264,15 @@ export class BlenderTransparentInstances {
|
|
|
256
264
|
/** Compare inputs without cloning 10,000 centres, allocating entries or
|
|
257
265
|
* sorting/uploading matrices again on an unchanged frame. No lossy hash. */
|
|
258
266
|
private planChanged(camera: THREE.Camera): boolean {
|
|
267
|
+
// A moved camera already invalidates culling and ordering. Do not first
|
|
268
|
+
// compare every object's attributes/matrix only to rebuild the same list.
|
|
269
|
+
// Invalidate the saved inputs; the first quiet frame records them afresh.
|
|
270
|
+
if (!this.projectionKnown || !this.plannedProjection.equals(this.projection)) {
|
|
271
|
+
this.plannedProjection.copy(this.projection);
|
|
272
|
+
this.projectionKnown = true;
|
|
273
|
+
this.planState.length = 0;
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
259
276
|
this.stateIndex = 0;
|
|
260
277
|
this.stateChanged = this.planState.length === 0;
|
|
261
278
|
this.observe(camera.layers.mask);
|
|
@@ -318,19 +335,6 @@ export class BlenderTransparentInstances {
|
|
|
318
335
|
this.observe((material as THREE.MeshPhysicalMaterial).transmission ?? 0);
|
|
319
336
|
}
|
|
320
337
|
|
|
321
|
-
/** A plain depth override does not need color transparency ordering. Reveal
|
|
322
|
-
* only this plan's canonical members, retaining exact masks for restoration. */
|
|
323
|
-
suspendDraw(): () => void {
|
|
324
|
-
const members = this.hidden.map(mesh => ({mesh, mask: mesh.layers.mask}));
|
|
325
|
-
const draws = this.planned.map(({mesh}) => ({mesh, visible: mesh.visible}));
|
|
326
|
-
for (const {mesh} of members) mesh.layers.enable(0);
|
|
327
|
-
for (const {mesh} of draws) mesh.visible = false;
|
|
328
|
-
return () => {
|
|
329
|
-
for (const {mesh, mask} of members) mesh.layers.mask = mask;
|
|
330
|
-
for (const {mesh, visible} of draws) mesh.visible = visible;
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
338
|
finishDraw(): void {
|
|
335
339
|
for (const mesh of this.hidden) mesh.layers.enable(0);
|
|
336
340
|
this.hidden.length = 0;
|
|
@@ -346,9 +350,11 @@ export class BlenderTransparentInstances {
|
|
|
346
350
|
}
|
|
347
351
|
this.pools.clear();
|
|
348
352
|
this.ordered.clear();
|
|
353
|
+
this.drawMaterials.clear();
|
|
349
354
|
this.mixedDraws.clear();
|
|
350
355
|
this.planned.length = 0;
|
|
351
356
|
this.planState.length = 0;
|
|
357
|
+
this.projectionKnown = false;
|
|
352
358
|
this.objects = [];
|
|
353
359
|
this.batches = this.instances = 0;
|
|
354
360
|
}
|
package/package.json
CHANGED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
|
|
3
|
-
|
|
4
|
-
const CAPACITY = 64;
|
|
5
|
-
const MAX_DRAWS = 1024; // At most 4 MiB of instance matrices; no vertex copies.
|
|
6
|
-
|
|
7
|
-
/** Opaque depth overrides discard authored color materials. Geometry-compatible
|
|
8
|
-
* objects can share a draw here even when their color draws cannot be reordered.
|
|
9
|
-
* Only the current pass changes; selection layers and canonical meshes survive. */
|
|
10
|
-
export class BlenderDepthInstances {
|
|
11
|
-
private objects: THREE.Mesh[] = [];
|
|
12
|
-
private readonly pools = new Map<THREE.BufferGeometry, THREE.InstancedMesh[]>();
|
|
13
|
-
private readonly hidden: {mesh: THREE.Mesh; mask: number}[] = [];
|
|
14
|
-
private readonly active: THREE.InstancedMesh[] = [];
|
|
15
|
-
private readonly inverse = new THREE.Matrix4();
|
|
16
|
-
private readonly matrix = new THREE.Matrix4();
|
|
17
|
-
private readonly projection = new THREE.Matrix4();
|
|
18
|
-
private readonly frustum = new THREE.Frustum();
|
|
19
|
-
private allocated = 0;
|
|
20
|
-
private statistics = {batches: 0, instances: 0, preparationMs: 0};
|
|
21
|
-
|
|
22
|
-
constructor(private readonly root: THREE.Group) {}
|
|
23
|
-
|
|
24
|
-
supports(material: THREE.MeshDepthMaterial): boolean {
|
|
25
|
-
return material.isMeshDepthMaterial && material.visible && !material.transparent &&
|
|
26
|
-
!material.map && !material.alphaMap && !material.displacementMap &&
|
|
27
|
-
!material.alphaHash && material.alphaTest === 0 && !material.wireframe &&
|
|
28
|
-
!material.stencilWrite && material.depthWrite && material.depthTest &&
|
|
29
|
-
!material.clippingPlanes?.length && !material.polygonOffset &&
|
|
30
|
-
(material.depthFunc === THREE.LessDepth || material.depthFunc === THREE.LessEqualDepth) &&
|
|
31
|
-
(material.blending === THREE.NormalBlending || material.blending === THREE.NoBlending) &&
|
|
32
|
-
material.onBeforeCompile === THREE.Material.prototype.onBeforeCompile &&
|
|
33
|
-
material.onBeforeRender === THREE.Material.prototype.onBeforeRender;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
setObjects(objects: Iterable<THREE.Object3D>): void {
|
|
37
|
-
this.clear();
|
|
38
|
-
this.objects = [...objects].filter((object): object is THREE.Mesh => (object as THREE.Mesh).isMesh === true);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
prepare(camera: THREE.Camera, material: THREE.MeshDepthMaterial): void {
|
|
42
|
-
this.finish();
|
|
43
|
-
const start = performance.now();
|
|
44
|
-
this.statistics = {batches: 0, instances: 0, preparationMs: 0};
|
|
45
|
-
if (!camera.layers.isEnabled(0)) return;
|
|
46
|
-
this.inverse.copy(this.root.matrixWorld).invert();
|
|
47
|
-
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
48
|
-
this.frustum.setFromProjectionMatrix(this.projection);
|
|
49
|
-
const groups = new Map<THREE.BufferGeometry, THREE.Mesh[]>();
|
|
50
|
-
for (const mesh of this.objects) {
|
|
51
|
-
// A selected object has a separate mask layer, and the outline pass also
|
|
52
|
-
// disables its default layer. Never pull it into the occluder draw.
|
|
53
|
-
if (mesh.layers.mask !== 1 || !instanceObjectShown(mesh) ||
|
|
54
|
-
(mesh as THREE.SkinnedMesh).isSkinnedMesh ||
|
|
55
|
-
(mesh as THREE.InstancedMesh).isInstancedMesh ||
|
|
56
|
-
(mesh as THREE.BatchedMesh).isBatchedMesh || mesh.morphTargetInfluences ||
|
|
57
|
-
mesh.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender ||
|
|
58
|
-
mesh.onAfterRender !== THREE.Object3D.prototype.onAfterRender) continue;
|
|
59
|
-
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
60
|
-
if (materials.length !== 1 || !materials[0]?.visible) continue;
|
|
61
|
-
const geometry = mesh.geometry;
|
|
62
|
-
const count = geometry.index?.count ?? geometry.getAttribute('position')?.count ?? 0;
|
|
63
|
-
if (!count || (Array.isArray(mesh.material) && !(geometry.groups.length === 1 &&
|
|
64
|
-
geometry.groups[0]!.start === 0 && geometry.groups[0]!.count === count &&
|
|
65
|
-
geometry.groups[0]!.materialIndex === 0))) continue;
|
|
66
|
-
if (mesh.frustumCulled && !this.frustum.intersectsObject(mesh)) continue;
|
|
67
|
-
this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
|
|
68
|
-
if (!instanceMatrixSupported(this.matrix)) continue;
|
|
69
|
-
const group = groups.get(geometry);
|
|
70
|
-
if (group) group.push(mesh); else groups.set(geometry, [mesh]);
|
|
71
|
-
}
|
|
72
|
-
// Do not retain pools for obsolete LOD/canonical geometry indefinitely.
|
|
73
|
-
for (const [geometry, pool] of this.pools) if (!groups.has(geometry)) {
|
|
74
|
-
for (const draw of pool) { draw.removeFromParent(); draw.dispose(); this.allocated--; }
|
|
75
|
-
this.pools.delete(geometry);
|
|
76
|
-
}
|
|
77
|
-
for (const [geometry, members] of groups) {
|
|
78
|
-
if (members.length < 2) continue;
|
|
79
|
-
let pool = this.pools.get(geometry);
|
|
80
|
-
if (!pool) { pool = []; this.pools.set(geometry, pool); }
|
|
81
|
-
for (let offset = 0; offset < members.length; offset += CAPACITY) {
|
|
82
|
-
const count = Math.min(CAPACITY, members.length - offset);
|
|
83
|
-
if (count < 2) continue;
|
|
84
|
-
const slot = offset / CAPACITY;
|
|
85
|
-
let draw = pool[slot];
|
|
86
|
-
if (!draw) {
|
|
87
|
-
if (this.allocated >= MAX_DRAWS) continue;
|
|
88
|
-
draw = new THREE.InstancedMesh(geometry, material, CAPACITY);
|
|
89
|
-
draw.name = 'Blender depth instances';
|
|
90
|
-
draw.userData['engineInternal'] = true;
|
|
91
|
-
draw.matrixAutoUpdate = false;
|
|
92
|
-
draw.frustumCulled = false; // Members were individually culled above.
|
|
93
|
-
draw.boundingSphere = new THREE.Sphere();
|
|
94
|
-
draw.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
95
|
-
draw.raycast = () => {};
|
|
96
|
-
this.root.add(draw);
|
|
97
|
-
pool.push(draw);
|
|
98
|
-
this.allocated++;
|
|
99
|
-
}
|
|
100
|
-
draw.material = material;
|
|
101
|
-
draw.matrixWorld.copy(this.root.matrixWorld);
|
|
102
|
-
draw.count = count;
|
|
103
|
-
draw.visible = true;
|
|
104
|
-
this.active.push(draw);
|
|
105
|
-
for (let i = 0; i < count; i++) {
|
|
106
|
-
const mesh = members[offset + i]!;
|
|
107
|
-
this.matrix.multiplyMatrices(this.inverse, mesh.matrixWorld);
|
|
108
|
-
draw.setMatrixAt(i, this.matrix);
|
|
109
|
-
this.hidden.push({mesh, mask: mesh.layers.mask});
|
|
110
|
-
mesh.layers.disable(0);
|
|
111
|
-
}
|
|
112
|
-
draw.instanceMatrix.needsUpdate = true;
|
|
113
|
-
this.statistics.batches++;
|
|
114
|
-
this.statistics.instances += count;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
this.statistics.preparationMs = performance.now() - start;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
finish(): void {
|
|
121
|
-
for (const {mesh, mask} of this.hidden) mesh.layers.mask = mask;
|
|
122
|
-
this.hidden.length = 0;
|
|
123
|
-
for (const draw of this.active) draw.visible = false;
|
|
124
|
-
this.active.length = 0;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
clear(): void {
|
|
128
|
-
this.finish();
|
|
129
|
-
for (const pool of this.pools.values()) for (const draw of pool) {
|
|
130
|
-
draw.removeFromParent();
|
|
131
|
-
draw.dispose(); // Shared geometry and override material belong to callers.
|
|
132
|
-
}
|
|
133
|
-
this.pools.clear();
|
|
134
|
-
this.objects = [];
|
|
135
|
-
this.allocated = 0;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
inspect() { return {...this.statistics, allocated: this.allocated}; }
|
|
139
|
-
}
|