@volter/blender-engine 0.1.72 → 0.1.74
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,244 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {instanceObjectShown} from './blender-runtime-instances';
|
|
3
|
+
import type {MotionMeshRequest, MotionMeshResponse} from './blender-motion-worker';
|
|
4
|
+
|
|
5
|
+
const CACHE_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
const QUIET_MS = 150;
|
|
7
|
+
type Entry = {geometry: THREE.BufferGeometry; error: number; bytes: number};
|
|
8
|
+
|
|
9
|
+
/** A disposable navigation drawing, never Blender's evaluated geometry.
|
|
10
|
+
* Copies are derived off-thread, in importance order, one bounded mesh at a
|
|
11
|
+
* time. Full meshes remain resident and are restored after EVERY draw. */
|
|
12
|
+
export class BlenderMotionGeometry {
|
|
13
|
+
private groups = new Map<THREE.BufferGeometry, THREE.Mesh[]>();
|
|
14
|
+
private readonly cache = new Map<THREE.BufferGeometry, Entry>();
|
|
15
|
+
private readonly swapped: {mesh: THREE.Mesh; geometry: THREE.BufferGeometry}[] = [];
|
|
16
|
+
private queue: THREE.BufferGeometry[] = [];
|
|
17
|
+
private worker: Worker | null = null;
|
|
18
|
+
private pending: {id: number; geometry: THREE.BufferGeometry} | null = null;
|
|
19
|
+
private nextId = 0;
|
|
20
|
+
private signature = '';
|
|
21
|
+
private readonly identities = new WeakMap<object, number>();
|
|
22
|
+
private nextIdentity = 0;
|
|
23
|
+
private bytes = 0;
|
|
24
|
+
private unavailable: string | null = null;
|
|
25
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
+
private readonly cameraWorld = new THREE.Matrix4();
|
|
27
|
+
private readonly cameraProjection = new THREE.Matrix4();
|
|
28
|
+
private cameraKnown = false;
|
|
29
|
+
private movedAt = -Infinity;
|
|
30
|
+
private readonly projection = new THREE.Matrix4();
|
|
31
|
+
private readonly frustum = new THREE.Frustum();
|
|
32
|
+
private readonly centre = new THREE.Vector3();
|
|
33
|
+
private drawnMeshes = 0;
|
|
34
|
+
private fullTriangles = 0;
|
|
35
|
+
private motionTriangles = 0;
|
|
36
|
+
private lastNavigation = {meshes: 0, fullTriangles: 0, motionTriangles: 0, tooClose: 0};
|
|
37
|
+
|
|
38
|
+
constructor(private readonly invalidate: () => void) {}
|
|
39
|
+
|
|
40
|
+
setObjects(objects: Iterable<THREE.Object3D>): void {
|
|
41
|
+
const groups = new Map<THREE.BufferGeometry, THREE.Mesh[]>();
|
|
42
|
+
for (const object of objects) {
|
|
43
|
+
const mesh = object as THREE.Mesh;
|
|
44
|
+
if (!mesh.isMesh || (mesh as THREE.SkinnedMesh).isSkinnedMesh ||
|
|
45
|
+
(mesh as THREE.InstancedMesh).isInstancedMesh || mesh.morphTargetInfluences) continue;
|
|
46
|
+
const g = mesh.geometry;
|
|
47
|
+
const position = g.getAttribute('position');
|
|
48
|
+
if (!g.index || g.index.count < 6144 || !position || position.count > 200_000 ||
|
|
49
|
+
Object.values(g.attributes).some(a => !(a instanceof THREE.BufferAttribute) ||
|
|
50
|
+
(a as THREE.BufferAttribute & {isFloat16BufferAttribute?: boolean}).isFloat16BufferAttribute ||
|
|
51
|
+
(a as THREE.InstancedBufferAttribute).isInstancedBufferAttribute) ||
|
|
52
|
+
Object.keys(g.morphAttributes).length || g.drawRange.start !== 0 || g.drawRange.count < g.index.count) continue;
|
|
53
|
+
// Only a whole, single material surface. Material boundaries are never
|
|
54
|
+
// crossed by the simplifier, nor are authored group ranges shortened.
|
|
55
|
+
if (Array.isArray(mesh.material) && (mesh.material.length !== 1 || g.groups.length !== 1 ||
|
|
56
|
+
g.groups[0]!.start !== 0 || g.groups[0]!.count !== g.index.count || g.groups[0]!.materialIndex !== 0)) continue;
|
|
57
|
+
const held = groups.get(g);
|
|
58
|
+
if (held) held.push(mesh); else groups.set(g, [mesh]);
|
|
59
|
+
}
|
|
60
|
+
this.groups = groups;
|
|
61
|
+
const identity = (object: object): number => {
|
|
62
|
+
let id = this.identities.get(object);
|
|
63
|
+
if (id === undefined) { id = ++this.nextIdentity; this.identities.set(object, id); }
|
|
64
|
+
return id;
|
|
65
|
+
};
|
|
66
|
+
const signature = [...groups].map(([g]) => `${g.id}:${identity(g.index!)}:${g.index!.version}:` +
|
|
67
|
+
Object.entries(g.attributes).map(([name, a]) => `${name}:${identity(a)}:${(a as THREE.BufferAttribute).version}`).join(',')).join(';');
|
|
68
|
+
if (signature === this.signature) return;
|
|
69
|
+
this.releaseCopies();
|
|
70
|
+
this.signature = signature;
|
|
71
|
+
this.unavailable = null;
|
|
72
|
+
const candidates = [...groups].filter(([, meshes]) => meshes.length >= 3);
|
|
73
|
+
// Small scenes use their originals throughout. Prioritize the geometry
|
|
74
|
+
// that costs most draws, not its name or its order in the .blend.
|
|
75
|
+
if (candidates.reduce((n, [g, meshes]) => n + g.index!.count * meshes.length, 0) < 6_000_000) return;
|
|
76
|
+
candidates.sort(([a, am], [b, bm]) => b.index!.count * bm.length - a.index!.count * am.length);
|
|
77
|
+
this.queue = candidates.map(([g]) => g);
|
|
78
|
+
this.next();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private next(): void {
|
|
82
|
+
if (this.pending || this.unavailable || this.bytes >= CACHE_BYTES) return;
|
|
83
|
+
const geometry = this.queue.shift();
|
|
84
|
+
if (!geometry) { this.worker?.terminate(); this.worker = null; return; }
|
|
85
|
+
if (!this.worker) {
|
|
86
|
+
try {
|
|
87
|
+
this.worker = new Worker(new URL('./blender-motion-worker.ts', import.meta.url), {type: 'module'});
|
|
88
|
+
this.worker.onmessage = (event: MessageEvent<MotionMeshResponse>) => this.receive(event.data);
|
|
89
|
+
this.worker.onerror = event => {
|
|
90
|
+
this.unavailable = event.message || 'Navigation geometry worker unavailable';
|
|
91
|
+
this.worker?.terminate(); this.worker = null; this.pending = null;
|
|
92
|
+
this.queue.length = 0;
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// This is an optional presentation. Refusal retains full geometry,
|
|
96
|
+
// with a visible diagnostic; never fall back to blocking the UI.
|
|
97
|
+
this.unavailable = String(error); return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const position = geometry.getAttribute('position') as THREE.BufferAttribute;
|
|
101
|
+
const positions = new Float32Array(position.count * 3);
|
|
102
|
+
for (let i = 0; i < position.count; i++) {
|
|
103
|
+
positions[i * 3] = position.getX(i); positions[i * 3 + 1] = position.getY(i); positions[i * 3 + 2] = position.getZ(i);
|
|
104
|
+
}
|
|
105
|
+
// Normals and UVs contribute appearance error. All attributes, including
|
|
106
|
+
// those not used to rank collapses, are retained in the resulting copy.
|
|
107
|
+
const attributes = Object.entries(geometry.attributes).filter(([name]) => name === 'normal' || /^uv\d*$/.test(name));
|
|
108
|
+
const stride = attributes.reduce((n, [, a]) => n + a.itemSize, 0);
|
|
109
|
+
const values = new Float32Array(position.count * stride);
|
|
110
|
+
const weights: number[] = [];
|
|
111
|
+
let offset = 0;
|
|
112
|
+
for (const [name, a] of attributes) {
|
|
113
|
+
for (let c = 0; c < a.itemSize; c++) weights.push(name === 'normal' ? 0.5 : 1);
|
|
114
|
+
for (let i = 0; i < position.count; i++) for (let c = 0; c < a.itemSize; c++)
|
|
115
|
+
values[i * stride + offset + c] = (a as THREE.BufferAttribute).getComponent(i, c);
|
|
116
|
+
offset += a.itemSize;
|
|
117
|
+
}
|
|
118
|
+
const indices = new Uint32Array(geometry.index!.array);
|
|
119
|
+
const id = ++this.nextId;
|
|
120
|
+
this.pending = {id, geometry};
|
|
121
|
+
const request: MotionMeshRequest = {id, positions, indices, attributes: values, stride, weights};
|
|
122
|
+
this.worker.postMessage(request, [positions.buffer, indices.buffer, values.buffer]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private receive(response: MotionMeshResponse): void {
|
|
126
|
+
const pending = this.pending;
|
|
127
|
+
if (!pending || pending.id !== response.id) return;
|
|
128
|
+
this.pending = null;
|
|
129
|
+
const {geometry} = pending;
|
|
130
|
+
const indices = response.indices;
|
|
131
|
+
if (response.refusal) {
|
|
132
|
+
this.unavailable = response.refusal;
|
|
133
|
+
this.worker?.terminate(); this.worker = null; this.queue.length = 0;
|
|
134
|
+
}
|
|
135
|
+
if (indices && response.error !== undefined && Number.isFinite(response.error) &&
|
|
136
|
+
indices.length > 0 && indices.length < geometry.index!.count * 0.75) {
|
|
137
|
+
const originals: number[] = [];
|
|
138
|
+
const remap = new Map<number, number>();
|
|
139
|
+
for (let i = 0; i < indices.length; i++) {
|
|
140
|
+
const original = indices[i]!;
|
|
141
|
+
let index = remap.get(original);
|
|
142
|
+
if (index === undefined) { index = originals.length; originals.push(original); remap.set(original, index); }
|
|
143
|
+
indices[i] = index;
|
|
144
|
+
}
|
|
145
|
+
const bytes = indices.byteLength + Object.values(geometry.attributes).reduce((n, a) =>
|
|
146
|
+
n + originals.length * a.itemSize * a.array.BYTES_PER_ELEMENT, 0);
|
|
147
|
+
if (this.bytes + bytes <= CACHE_BYTES) {
|
|
148
|
+
const copy = new THREE.BufferGeometry();
|
|
149
|
+
copy.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
150
|
+
for (const [name, source] of Object.entries(geometry.attributes)) {
|
|
151
|
+
const attribute = source as THREE.BufferAttribute;
|
|
152
|
+
const ArrayType = attribute.array.constructor as {new(length: number): THREE.TypedArray};
|
|
153
|
+
const data = new ArrayType(originals.length * attribute.itemSize);
|
|
154
|
+
for (let i = 0; i < originals.length; i++) for (let c = 0; c < attribute.itemSize; c++)
|
|
155
|
+
data[i * attribute.itemSize + c] = attribute.array[originals[i]! * attribute.itemSize + c]!;
|
|
156
|
+
copy.setAttribute(name, new THREE.BufferAttribute(data, attribute.itemSize, attribute.normalized));
|
|
157
|
+
}
|
|
158
|
+
// Retain the exact culling/sorting bounds and material assignment.
|
|
159
|
+
if (!geometry.boundingSphere) geometry.computeBoundingSphere();
|
|
160
|
+
copy.boundingSphere = geometry.boundingSphere!.clone();
|
|
161
|
+
copy.boundingBox = geometry.boundingBox?.clone() ?? null;
|
|
162
|
+
if (geometry.groups.length === 1) copy.addGroup(0, indices.length, geometry.groups[0]!.materialIndex);
|
|
163
|
+
copy.userData = {...geometry.userData};
|
|
164
|
+
this.cache.set(geometry, {geometry: copy, error: response.error, bytes});
|
|
165
|
+
this.bytes += bytes;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Yield between meshes, including copying their attributes for the worker.
|
|
169
|
+
this.timer = setTimeout(() => { this.timer = null; this.next(); }, 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
prepare(camera: THREE.Camera, interactive: boolean, height: number): boolean {
|
|
173
|
+
this.finish();
|
|
174
|
+
this.drawnMeshes = this.fullTriangles = this.motionTriangles = 0;
|
|
175
|
+
if (!interactive || !(height > 0)) return false;
|
|
176
|
+
const now = performance.now();
|
|
177
|
+
const moved = this.cameraKnown && (!camera.matrixWorld.equals(this.cameraWorld) ||
|
|
178
|
+
!camera.projectionMatrix.equals(this.cameraProjection));
|
|
179
|
+
this.cameraKnown = true;
|
|
180
|
+
this.cameraWorld.copy(camera.matrixWorld); this.cameraProjection.copy(camera.projectionMatrix);
|
|
181
|
+
if (moved) {
|
|
182
|
+
this.movedAt = now;
|
|
183
|
+
// The host draws on change. Ensure a full-detail frame after damping
|
|
184
|
+
// stops even when no more input or scene-change notification arrives.
|
|
185
|
+
if (this.quietTimer) clearTimeout(this.quietTimer);
|
|
186
|
+
this.quietTimer = setTimeout(() => { this.quietTimer = null; this.invalidate(); }, QUIET_MS + 1);
|
|
187
|
+
}
|
|
188
|
+
if (now - this.movedAt > QUIET_MS) return false;
|
|
189
|
+
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
190
|
+
this.frustum.setFromProjectionMatrix(this.projection);
|
|
191
|
+
let tooClose = 0;
|
|
192
|
+
for (const [source, entry] of this.cache) {
|
|
193
|
+
for (const mesh of this.groups.get(source) ?? []) {
|
|
194
|
+
if (!instanceObjectShown(mesh) || (mesh.frustumCulled && !this.frustum.intersectsObject(mesh))) continue;
|
|
195
|
+
// Frobenius norm bounds the linear transform even under shear; the
|
|
196
|
+
// longest axis alone can understate the projected simplifier error.
|
|
197
|
+
const e = mesh.matrixWorld.elements;
|
|
198
|
+
const scale = Math.hypot(e[0]!, e[1]!, e[2]!, e[4]!, e[5]!, e[6]!, e[8]!, e[9]!, e[10]!);
|
|
199
|
+
const sphere = source.boundingSphere!;
|
|
200
|
+
this.centre.copy(sphere.center).applyMatrix4(mesh.matrixWorld).applyMatrix4(camera.matrixWorldInverse);
|
|
201
|
+
const depth = (camera as THREE.PerspectiveCamera).isPerspectiveCamera
|
|
202
|
+
? -this.centre.z - sphere.radius * scale : 1;
|
|
203
|
+
const pixels = entry.error * scale * Math.abs(camera.projectionMatrix.elements[5]!) * height / (2 * depth);
|
|
204
|
+
if (!(depth > 0) || pixels > 1) { tooClose++; continue; }
|
|
205
|
+
// Decide per placement: one near-camera blade must not veto thousands
|
|
206
|
+
// of distant placements of its shared geometry. The following draw
|
|
207
|
+
// planner sees these geometry identities, so transparency still batches
|
|
208
|
+
// only consecutive compatible surfaces in the original camera order.
|
|
209
|
+
this.swapped.push({mesh, geometry: source});
|
|
210
|
+
mesh.geometry = entry.geometry;
|
|
211
|
+
this.drawnMeshes++;
|
|
212
|
+
this.fullTriangles += source.index!.count / 3;
|
|
213
|
+
this.motionTriangles += entry.geometry.index!.count / 3;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.lastNavigation = {meshes: this.drawnMeshes, fullTriangles: this.fullTriangles,
|
|
217
|
+
motionTriangles: this.motionTriangles, tooClose};
|
|
218
|
+
return this.swapped.length > 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private quietTimer: ReturnType<typeof setTimeout> | null = null;
|
|
222
|
+
finish(): void {
|
|
223
|
+
for (const {mesh, geometry} of this.swapped) mesh.geometry = geometry;
|
|
224
|
+
this.swapped.length = 0;
|
|
225
|
+
}
|
|
226
|
+
inspect() {
|
|
227
|
+
return {copies: this.cache.size, bytes: this.bytes, queued: this.queue.length, pending: this.pending !== null,
|
|
228
|
+
unavailable: this.unavailable, lastNavigation: {...this.lastNavigation}};
|
|
229
|
+
}
|
|
230
|
+
private releaseCopies(): void {
|
|
231
|
+
this.finish();
|
|
232
|
+
if (this.timer) clearTimeout(this.timer);
|
|
233
|
+
this.timer = null;
|
|
234
|
+
this.worker?.terminate(); this.worker = null; this.pending = null; this.queue = [];
|
|
235
|
+
for (const entry of this.cache.values()) entry.geometry.dispose();
|
|
236
|
+
this.cache.clear(); this.bytes = 0;
|
|
237
|
+
}
|
|
238
|
+
clear(): void {
|
|
239
|
+
this.releaseCopies();
|
|
240
|
+
if (this.quietTimer) clearTimeout(this.quietTimer);
|
|
241
|
+
this.quietTimer = null;
|
|
242
|
+
this.groups.clear(); this.signature = ''; this.cameraKnown = false; this.movedAt = -Infinity;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {MeshoptSimplifier} from 'meshoptimizer/simplifier';
|
|
2
|
+
|
|
3
|
+
export interface MotionMeshRequest {
|
|
4
|
+
id: number;
|
|
5
|
+
positions: Float32Array;
|
|
6
|
+
indices: Uint32Array;
|
|
7
|
+
attributes: Float32Array;
|
|
8
|
+
stride: number;
|
|
9
|
+
weights: number[];
|
|
10
|
+
}
|
|
11
|
+
export interface MotionMeshResponse {id: number; indices?: Uint32Array; error?: number; refusal?: string}
|
|
12
|
+
|
|
13
|
+
// One copied mesh at a time. The worker never owns or detaches a resident
|
|
14
|
+
// Blender column, and never writes positions or attributes back to the model.
|
|
15
|
+
self.onmessage = async (event: MessageEvent<MotionMeshRequest>) => {
|
|
16
|
+
const {id, positions, indices, attributes, stride, weights} = event.data;
|
|
17
|
+
try {
|
|
18
|
+
await MeshoptSimplifier.ready;
|
|
19
|
+
const target = Math.max(384, Math.floor(indices.length * 0.08 / 3) * 3);
|
|
20
|
+
const [reduced, error] = MeshoptSimplifier.simplifyWithAttributes(
|
|
21
|
+
indices, positions, 3, attributes, stride, weights, null, target, 0.002,
|
|
22
|
+
// Preserve open boundaries and attribute seams. If that prevents a
|
|
23
|
+
// useful reduction, keep the original; never sample/drop triangles.
|
|
24
|
+
['LockBorder'],
|
|
25
|
+
);
|
|
26
|
+
const response: MotionMeshResponse = {
|
|
27
|
+
id, indices: reduced, error: error * MeshoptSimplifier.getScale(positions, 3),
|
|
28
|
+
};
|
|
29
|
+
self.postMessage(response, {transfer: [reduced.buffer]});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
self.postMessage({id, refusal: error instanceof Error ? error.message : String(error)} satisfies MotionMeshResponse);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -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 { BlenderMotionGeometry } from './blender-motion-geometry';
|
|
47
48
|
|
|
48
49
|
/** A saved `View3DShading.type` as the stage's draw mode: Material Preview is `preview`. */
|
|
49
50
|
const SAVED_SHADING = {
|
|
@@ -644,6 +645,7 @@ export class BlenderRuntimeView {
|
|
|
644
645
|
readonly root = new THREE.Group();
|
|
645
646
|
private readonly instances = new BlenderRuntimeInstances(this.root);
|
|
646
647
|
private readonly transparentInstances = new BlenderTransparentInstances(this.root);
|
|
648
|
+
private readonly motionGeometry = new BlenderMotionGeometry(presenterChanged);
|
|
647
649
|
private drawBatching = true;
|
|
648
650
|
private readonly objects = new Map<string, THREE.Object3D>();
|
|
649
651
|
private readonly meshes = new Map<
|
|
@@ -1230,6 +1232,7 @@ export class BlenderRuntimeView {
|
|
|
1230
1232
|
this.transparentInstances.clear();
|
|
1231
1233
|
this.instances.rebuild(this.objects.values(), this.drawBatching && !this.rendered);
|
|
1232
1234
|
if (this.drawBatching && !this.rendered) this.transparentInstances.setObjects(this.objects.values());
|
|
1235
|
+
this.motionGeometry.setObjects(this.objects.values());
|
|
1233
1236
|
}
|
|
1234
1237
|
|
|
1235
1238
|
/** Presentation-only comparison door; never changes Blender or its data. */
|
|
@@ -1242,17 +1245,26 @@ export class BlenderRuntimeView {
|
|
|
1242
1245
|
|
|
1243
1246
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1244
1247
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1245
|
-
prepareDraw(camera: THREE.Camera): () => void {
|
|
1248
|
+
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number}): () => void {
|
|
1246
1249
|
try {
|
|
1247
1250
|
this.instances.prepareDraw(camera);
|
|
1248
1251
|
this.root.updateMatrixWorld(true);
|
|
1252
|
+
const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
|
|
1253
|
+
options?.interactive === true, options?.height ?? 0);
|
|
1254
|
+
// A changed navigation geometry is an ordinary-draw fallback for an
|
|
1255
|
+
// opaque batch; its full-detail members remain instanced. Transparent
|
|
1256
|
+
// runs are planned AFTER per-placement geometry selection, preserving
|
|
1257
|
+
// their sorting barriers and compatible geometry grouping.
|
|
1258
|
+
if (motion) this.instances.sync();
|
|
1249
1259
|
this.transparentInstances.prepare(camera);
|
|
1250
1260
|
} catch (error) {
|
|
1261
|
+
this.motionGeometry.finish();
|
|
1251
1262
|
this.instances.finishDraw();
|
|
1252
1263
|
this.transparentInstances.finishDraw();
|
|
1253
1264
|
throw error;
|
|
1254
1265
|
}
|
|
1255
1266
|
return () => {
|
|
1267
|
+
this.motionGeometry.finish();
|
|
1256
1268
|
this.instances.finishDraw();
|
|
1257
1269
|
this.transparentInstances.finishDraw();
|
|
1258
1270
|
};
|
|
@@ -2063,7 +2075,7 @@ export class BlenderRuntimeView {
|
|
|
2063
2075
|
}
|
|
2064
2076
|
|
|
2065
2077
|
drawStatistics() {
|
|
2066
|
-
return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
|
|
2078
|
+
return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2067
2079
|
}
|
|
2068
2080
|
|
|
2069
2081
|
snapshot() {
|
|
@@ -2273,6 +2285,7 @@ export class BlenderRuntimeView {
|
|
|
2273
2285
|
}
|
|
2274
2286
|
|
|
2275
2287
|
private clear() {
|
|
2288
|
+
this.motionGeometry.clear();
|
|
2276
2289
|
this.transparentInstances.clear();
|
|
2277
2290
|
this.instances.clear();
|
|
2278
2291
|
for (const entry of this.staged?.meshes.values() ?? []) entry.geometry.dispose();
|
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.
|
|
5
|
+
"version": "0.1.74",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -44,5 +44,8 @@
|
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build:blender-three": "vite build --config vite.release.config.ts",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.browser.json"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"meshoptimizer": "1.2.0"
|
|
47
50
|
}
|
|
48
51
|
}
|