@volter/blender-engine 0.1.71 → 0.1.73
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,248 @@
|
|
|
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
|
+
|
|
37
|
+
constructor(private readonly root: THREE.Group, private readonly invalidate: () => void) {}
|
|
38
|
+
|
|
39
|
+
setObjects(objects: Iterable<THREE.Object3D>): void {
|
|
40
|
+
const groups = new Map<THREE.BufferGeometry, THREE.Mesh[]>();
|
|
41
|
+
for (const object of objects) {
|
|
42
|
+
const mesh = object as THREE.Mesh;
|
|
43
|
+
if (!mesh.isMesh || (mesh as THREE.SkinnedMesh).isSkinnedMesh ||
|
|
44
|
+
(mesh as THREE.InstancedMesh).isInstancedMesh || mesh.morphTargetInfluences) continue;
|
|
45
|
+
const g = mesh.geometry;
|
|
46
|
+
const position = g.getAttribute('position');
|
|
47
|
+
if (!g.index || g.index.count < 6144 || !position || position.count > 200_000 ||
|
|
48
|
+
Object.values(g.attributes).some(a => !(a instanceof THREE.BufferAttribute)) ||
|
|
49
|
+
Object.keys(g.morphAttributes).length || g.drawRange.start !== 0 || g.drawRange.count < g.index.count) continue;
|
|
50
|
+
// Only a whole, single material surface. Material boundaries are never
|
|
51
|
+
// crossed by the simplifier, nor are authored group ranges shortened.
|
|
52
|
+
if (Array.isArray(mesh.material) && (mesh.material.length !== 1 || g.groups.length !== 1 ||
|
|
53
|
+
g.groups[0]!.start !== 0 || g.groups[0]!.count !== g.index.count || g.groups[0]!.materialIndex !== 0)) continue;
|
|
54
|
+
const held = groups.get(g);
|
|
55
|
+
if (held) held.push(mesh); else groups.set(g, [mesh]);
|
|
56
|
+
}
|
|
57
|
+
this.groups = groups;
|
|
58
|
+
const identity = (object: object): number => {
|
|
59
|
+
let id = this.identities.get(object);
|
|
60
|
+
if (id === undefined) { id = ++this.nextIdentity; this.identities.set(object, id); }
|
|
61
|
+
return id;
|
|
62
|
+
};
|
|
63
|
+
const signature = [...groups].map(([g]) => `${g.id}:${identity(g.index!)}:${g.index!.version}:` +
|
|
64
|
+
Object.entries(g.attributes).map(([name, a]) => `${name}:${identity(a)}:${(a as THREE.BufferAttribute).version}`).join(',')).join(';');
|
|
65
|
+
if (signature === this.signature) return;
|
|
66
|
+
this.releaseCopies();
|
|
67
|
+
this.signature = signature;
|
|
68
|
+
this.unavailable = null;
|
|
69
|
+
const candidates = [...groups].filter(([, meshes]) => meshes.length >= 3);
|
|
70
|
+
// Small scenes use their originals throughout. Prioritize the geometry
|
|
71
|
+
// that costs most draws, not its name or its order in the .blend.
|
|
72
|
+
if (candidates.reduce((n, [g, meshes]) => n + g.index!.count * meshes.length, 0) < 6_000_000) return;
|
|
73
|
+
candidates.sort(([a, am], [b, bm]) => b.index!.count * bm.length - a.index!.count * am.length);
|
|
74
|
+
this.queue = candidates.map(([g]) => g);
|
|
75
|
+
this.next();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private next(): void {
|
|
79
|
+
if (this.pending || this.unavailable || this.bytes >= CACHE_BYTES) return;
|
|
80
|
+
const geometry = this.queue.shift();
|
|
81
|
+
if (!geometry) { this.worker?.terminate(); this.worker = null; return; }
|
|
82
|
+
if (!this.worker) {
|
|
83
|
+
try {
|
|
84
|
+
this.worker = new Worker(new URL('./blender-motion-worker.ts', import.meta.url), {type: 'module'});
|
|
85
|
+
this.worker.onmessage = (event: MessageEvent<MotionMeshResponse>) => this.receive(event.data);
|
|
86
|
+
this.worker.onerror = event => {
|
|
87
|
+
this.unavailable = event.message || 'Navigation geometry worker unavailable';
|
|
88
|
+
this.worker?.terminate(); this.worker = null; this.pending = null;
|
|
89
|
+
this.queue.length = 0;
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// This is an optional presentation. Refusal retains full geometry,
|
|
93
|
+
// with a visible diagnostic; never fall back to blocking the UI.
|
|
94
|
+
this.unavailable = String(error); return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const position = geometry.getAttribute('position') as THREE.BufferAttribute;
|
|
98
|
+
const positions = new Float32Array(position.count * 3);
|
|
99
|
+
for (let i = 0; i < position.count; i++) {
|
|
100
|
+
positions[i * 3] = position.getX(i); positions[i * 3 + 1] = position.getY(i); positions[i * 3 + 2] = position.getZ(i);
|
|
101
|
+
}
|
|
102
|
+
// Normals and UVs contribute appearance error. All attributes, including
|
|
103
|
+
// those not used to rank collapses, are retained in the resulting copy.
|
|
104
|
+
const attributes = Object.entries(geometry.attributes).filter(([name]) => name === 'normal' || /^uv\d*$/.test(name));
|
|
105
|
+
const stride = attributes.reduce((n, [, a]) => n + a.itemSize, 0);
|
|
106
|
+
const values = new Float32Array(position.count * stride);
|
|
107
|
+
const weights: number[] = [];
|
|
108
|
+
let offset = 0;
|
|
109
|
+
for (const [name, a] of attributes) {
|
|
110
|
+
for (let c = 0; c < a.itemSize; c++) weights.push(name === 'normal' ? 0.5 : 1);
|
|
111
|
+
for (let i = 0; i < position.count; i++) for (let c = 0; c < a.itemSize; c++)
|
|
112
|
+
values[i * stride + offset + c] = (a as THREE.BufferAttribute).getComponent(i, c);
|
|
113
|
+
offset += a.itemSize;
|
|
114
|
+
}
|
|
115
|
+
const indices = new Uint32Array(geometry.index!.array);
|
|
116
|
+
const id = ++this.nextId;
|
|
117
|
+
this.pending = {id, geometry};
|
|
118
|
+
const request: MotionMeshRequest = {id, positions, indices, attributes: values, stride, weights};
|
|
119
|
+
this.worker.postMessage(request, [positions.buffer, indices.buffer, values.buffer]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private receive(response: MotionMeshResponse): void {
|
|
123
|
+
const pending = this.pending;
|
|
124
|
+
if (!pending || pending.id !== response.id) return;
|
|
125
|
+
this.pending = null;
|
|
126
|
+
const {geometry} = pending;
|
|
127
|
+
const indices = response.indices;
|
|
128
|
+
if (response.refusal) {
|
|
129
|
+
this.unavailable = response.refusal;
|
|
130
|
+
this.worker?.terminate(); this.worker = null; this.queue.length = 0;
|
|
131
|
+
}
|
|
132
|
+
if (indices && response.error !== undefined && Number.isFinite(response.error) &&
|
|
133
|
+
indices.length > 0 && indices.length < geometry.index!.count * 0.75) {
|
|
134
|
+
const originals: number[] = [];
|
|
135
|
+
const remap = new Map<number, number>();
|
|
136
|
+
for (let i = 0; i < indices.length; i++) {
|
|
137
|
+
const original = indices[i]!;
|
|
138
|
+
let index = remap.get(original);
|
|
139
|
+
if (index === undefined) { index = originals.length; originals.push(original); remap.set(original, index); }
|
|
140
|
+
indices[i] = index;
|
|
141
|
+
}
|
|
142
|
+
const bytes = indices.byteLength + Object.values(geometry.attributes).reduce((n, a) =>
|
|
143
|
+
n + originals.length * a.itemSize * a.array.BYTES_PER_ELEMENT, 0);
|
|
144
|
+
if (this.bytes + bytes <= CACHE_BYTES) {
|
|
145
|
+
const copy = new THREE.BufferGeometry();
|
|
146
|
+
copy.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
147
|
+
for (const [name, source] of Object.entries(geometry.attributes)) {
|
|
148
|
+
const attribute = source as THREE.BufferAttribute;
|
|
149
|
+
const ArrayType = attribute.array.constructor as {new(length: number): THREE.TypedArray};
|
|
150
|
+
const data = new ArrayType(originals.length * attribute.itemSize);
|
|
151
|
+
for (let i = 0; i < originals.length; i++) for (let c = 0; c < attribute.itemSize; c++)
|
|
152
|
+
data[i * attribute.itemSize + c] = attribute.array[originals[i]! * attribute.itemSize + c]!;
|
|
153
|
+
copy.setAttribute(name, new THREE.BufferAttribute(data, attribute.itemSize, attribute.normalized));
|
|
154
|
+
}
|
|
155
|
+
// Retain the exact culling/sorting bounds and material assignment.
|
|
156
|
+
if (!geometry.boundingSphere) geometry.computeBoundingSphere();
|
|
157
|
+
copy.boundingSphere = geometry.boundingSphere!.clone();
|
|
158
|
+
copy.boundingBox = geometry.boundingBox?.clone() ?? null;
|
|
159
|
+
if (geometry.groups.length === 1) copy.addGroup(0, indices.length, geometry.groups[0]!.materialIndex);
|
|
160
|
+
copy.userData = {...geometry.userData};
|
|
161
|
+
this.cache.set(geometry, {geometry: copy, error: response.error, bytes});
|
|
162
|
+
this.bytes += bytes;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Yield between meshes, including copying their attributes for the worker.
|
|
166
|
+
this.timer = setTimeout(() => { this.timer = null; this.next(); }, 0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
prepare(camera: THREE.Camera, interactive: boolean, height: number): void {
|
|
170
|
+
this.finish();
|
|
171
|
+
this.drawnMeshes = this.fullTriangles = this.motionTriangles = 0;
|
|
172
|
+
if (!interactive || !(height > 0)) return;
|
|
173
|
+
const now = performance.now();
|
|
174
|
+
const moved = this.cameraKnown && (!camera.matrixWorld.equals(this.cameraWorld) ||
|
|
175
|
+
!camera.projectionMatrix.equals(this.cameraProjection));
|
|
176
|
+
this.cameraKnown = true;
|
|
177
|
+
this.cameraWorld.copy(camera.matrixWorld); this.cameraProjection.copy(camera.projectionMatrix);
|
|
178
|
+
if (moved) {
|
|
179
|
+
this.movedAt = now;
|
|
180
|
+
// The host draws on change. Ensure a full-detail frame after damping
|
|
181
|
+
// stops even when no more input or scene-change notification arrives.
|
|
182
|
+
if (this.quietTimer) clearTimeout(this.quietTimer);
|
|
183
|
+
this.quietTimer = setTimeout(() => { this.quietTimer = null; this.invalidate(); }, QUIET_MS + 1);
|
|
184
|
+
}
|
|
185
|
+
if (now - this.movedAt > QUIET_MS) return;
|
|
186
|
+
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
187
|
+
this.frustum.setFromProjectionMatrix(this.projection);
|
|
188
|
+
const allowed = new Map<THREE.BufferGeometry, THREE.BufferGeometry>();
|
|
189
|
+
for (const [source, entry] of this.cache) {
|
|
190
|
+
let fits = true;
|
|
191
|
+
for (const mesh of this.groups.get(source) ?? []) {
|
|
192
|
+
if (!instanceObjectShown(mesh) || (mesh.frustumCulled && !this.frustum.intersectsObject(mesh))) continue;
|
|
193
|
+
const scale = mesh.matrixWorld.getMaxScaleOnAxis();
|
|
194
|
+
const sphere = source.boundingSphere!;
|
|
195
|
+
this.centre.copy(sphere.center).applyMatrix4(mesh.matrixWorld).applyMatrix4(camera.matrixWorldInverse);
|
|
196
|
+
const depth = (camera as THREE.PerspectiveCamera).isPerspectiveCamera
|
|
197
|
+
? -this.centre.z - sphere.radius * scale : 1;
|
|
198
|
+
const pixels = entry.error * scale * Math.abs(camera.projectionMatrix.elements[5]!) * height / (2 * depth);
|
|
199
|
+
if (!(depth > 0) || pixels > 1) { fits = false; break; }
|
|
200
|
+
}
|
|
201
|
+
if (fits) allowed.set(source, entry.geometry);
|
|
202
|
+
}
|
|
203
|
+
// After instancing/culling chose its exact members: both ordinary and
|
|
204
|
+
// internal draws select the same temporary geometry for this camera.
|
|
205
|
+
this.root.traverse(object => {
|
|
206
|
+
const mesh = object as THREE.Mesh;
|
|
207
|
+
if (!mesh.isMesh || (mesh as THREE.SkinnedMesh).isSkinnedMesh || mesh.morphTargetInfluences ||
|
|
208
|
+
((mesh as THREE.InstancedMesh).isInstancedMesh && mesh.userData['engineInternal'] !== true)) return;
|
|
209
|
+
if (Array.isArray(mesh.material) && (mesh.material.length !== 1 || mesh.geometry.groups.length !== 1 ||
|
|
210
|
+
mesh.geometry.groups[0]!.start !== 0 || mesh.geometry.groups[0]!.count !== mesh.geometry.index?.count ||
|
|
211
|
+
mesh.geometry.groups[0]!.materialIndex !== 0)) return;
|
|
212
|
+
const copy = allowed.get(mesh.geometry);
|
|
213
|
+
if (!copy) return;
|
|
214
|
+
this.swapped.push({mesh, geometry: mesh.geometry});
|
|
215
|
+
if (mesh.visible && mesh.layers.isEnabled(0)) {
|
|
216
|
+
const count = (mesh as THREE.InstancedMesh).isInstancedMesh ? (mesh as THREE.InstancedMesh).count : 1;
|
|
217
|
+
this.drawnMeshes += count;
|
|
218
|
+
this.fullTriangles += mesh.geometry.index!.count / 3 * count;
|
|
219
|
+
this.motionTriangles += copy.index!.count / 3 * count;
|
|
220
|
+
}
|
|
221
|
+
mesh.geometry = copy;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private quietTimer: ReturnType<typeof setTimeout> | null = null;
|
|
226
|
+
finish(): void {
|
|
227
|
+
for (const {mesh, geometry} of this.swapped) mesh.geometry = geometry;
|
|
228
|
+
this.swapped.length = 0;
|
|
229
|
+
}
|
|
230
|
+
inspect() {
|
|
231
|
+
return {copies: this.cache.size, bytes: this.bytes, queued: this.queue.length, pending: this.pending !== null,
|
|
232
|
+
unavailable: this.unavailable, lastDraw: {meshes: this.drawnMeshes, fullTriangles: this.fullTriangles, motionTriangles: this.motionTriangles}};
|
|
233
|
+
}
|
|
234
|
+
private releaseCopies(): void {
|
|
235
|
+
this.finish();
|
|
236
|
+
if (this.timer) clearTimeout(this.timer);
|
|
237
|
+
this.timer = null;
|
|
238
|
+
this.worker?.terminate(); this.worker = null; this.pending = null; this.queue = [];
|
|
239
|
+
for (const entry of this.cache.values()) entry.geometry.dispose();
|
|
240
|
+
this.cache.clear(); this.bytes = 0;
|
|
241
|
+
}
|
|
242
|
+
clear(): void {
|
|
243
|
+
this.releaseCopies();
|
|
244
|
+
if (this.quietTimer) clearTimeout(this.quietTimer);
|
|
245
|
+
this.quietTimer = null;
|
|
246
|
+
this.groups.clear(); this.signature = ''; this.cameraKnown = false; this.movedAt = -Infinity;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -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(this.root, 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,20 @@ 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);
|
|
1249
1252
|
this.transparentInstances.prepare(camera);
|
|
1253
|
+
this.motionGeometry.prepare(camera, !this.rendered && options?.interactive === true, options?.height ?? 0);
|
|
1250
1254
|
} catch (error) {
|
|
1255
|
+
this.motionGeometry.finish();
|
|
1251
1256
|
this.instances.finishDraw();
|
|
1252
1257
|
this.transparentInstances.finishDraw();
|
|
1253
1258
|
throw error;
|
|
1254
1259
|
}
|
|
1255
1260
|
return () => {
|
|
1261
|
+
this.motionGeometry.finish();
|
|
1256
1262
|
this.instances.finishDraw();
|
|
1257
1263
|
this.transparentInstances.finishDraw();
|
|
1258
1264
|
};
|
|
@@ -2063,7 +2069,7 @@ export class BlenderRuntimeView {
|
|
|
2063
2069
|
}
|
|
2064
2070
|
|
|
2065
2071
|
drawStatistics() {
|
|
2066
|
-
return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect()};
|
|
2072
|
+
return {enabled: this.drawBatching, opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2067
2073
|
}
|
|
2068
2074
|
|
|
2069
2075
|
snapshot() {
|
|
@@ -2273,6 +2279,7 @@ export class BlenderRuntimeView {
|
|
|
2273
2279
|
}
|
|
2274
2280
|
|
|
2275
2281
|
private clear() {
|
|
2282
|
+
this.motionGeometry.clear();
|
|
2276
2283
|
this.transparentInstances.clear();
|
|
2277
2284
|
this.instances.clear();
|
|
2278
2285
|
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.73",
|
|
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
|
}
|