@volter/blender-engine 0.1.74 → 0.1.76
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.
|
@@ -4,6 +4,11 @@ import type {MotionMeshRequest, MotionMeshResponse} from './blender-motion-worke
|
|
|
4
4
|
|
|
5
5
|
const CACHE_BYTES = 64 * 1024 * 1024;
|
|
6
6
|
const QUIET_MS = 150;
|
|
7
|
+
// At most 16,384 triangles are copied/simplified in a single worker request.
|
|
8
|
+
const CHUNK_INDICES = 49_152;
|
|
9
|
+
const COPY_ITEMS = 8_192;
|
|
10
|
+
const cancelled = Symbol('navigation geometry replaced');
|
|
11
|
+
type Range = {start: number; count: number; materialIndex?: number | undefined};
|
|
7
12
|
type Entry = {geometry: THREE.BufferGeometry; error: number; bytes: number};
|
|
8
13
|
|
|
9
14
|
/** A disposable navigation drawing, never Blender's evaluated geometry.
|
|
@@ -15,13 +20,16 @@ export class BlenderMotionGeometry {
|
|
|
15
20
|
private readonly swapped: {mesh: THREE.Mesh; geometry: THREE.BufferGeometry}[] = [];
|
|
16
21
|
private queue: THREE.BufferGeometry[] = [];
|
|
17
22
|
private worker: Worker | null = null;
|
|
18
|
-
private pending: {id: number;
|
|
23
|
+
private pending: {id: number; resolve: (response: MotionMeshResponse) => void} | null = null;
|
|
24
|
+
private generation = 0;
|
|
25
|
+
private building = false;
|
|
19
26
|
private nextId = 0;
|
|
20
27
|
private signature = '';
|
|
21
28
|
private readonly identities = new WeakMap<object, number>();
|
|
22
29
|
private nextIdentity = 0;
|
|
23
30
|
private bytes = 0;
|
|
24
31
|
private unavailable: string | null = null;
|
|
32
|
+
private reductions: {vertices: number; fullTriangles: number; reducedTriangles: number; error: number; accepted: boolean; bytes?: number}[] = [];
|
|
25
33
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
26
34
|
private readonly cameraWorld = new THREE.Matrix4();
|
|
27
35
|
private readonly cameraProjection = new THREE.Matrix4();
|
|
@@ -45,15 +53,22 @@ export class BlenderMotionGeometry {
|
|
|
45
53
|
(mesh as THREE.InstancedMesh).isInstancedMesh || mesh.morphTargetInfluences) continue;
|
|
46
54
|
const g = mesh.geometry;
|
|
47
55
|
const position = g.getAttribute('position');
|
|
48
|
-
if (!g.index || g.index.count < 6144 || !position || position.count >
|
|
56
|
+
if (!g.index || g.index.count < 6144 || !position || position.count > 8_000_000 ||
|
|
49
57
|
Object.values(g.attributes).some(a => !(a instanceof THREE.BufferAttribute) ||
|
|
50
58
|
(a as THREE.BufferAttribute & {isFloat16BufferAttribute?: boolean}).isFloat16BufferAttribute ||
|
|
51
59
|
(a as THREE.InstancedBufferAttribute).isInstancedBufferAttribute) ||
|
|
52
60
|
Object.keys(g.morphAttributes).length || g.drawRange.start !== 0 || g.drawRange.count < g.index.count) continue;
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
if (Array.isArray(mesh.material)
|
|
56
|
-
|
|
61
|
+
// Support ordered, disjoint material ranges. Overlaps/gaps or a shared
|
|
62
|
+
// geometry interpreted differently by two objects retain full detail.
|
|
63
|
+
if (Array.isArray(mesh.material)) {
|
|
64
|
+
let end = 0;
|
|
65
|
+
if (!g.groups.length || g.groups.some(range => {
|
|
66
|
+
const valid = range.start === end && range.count > 0 && range.count % 3 === 0 &&
|
|
67
|
+
Number.isInteger(range.materialIndex) && range.materialIndex! >= 0 && range.materialIndex! < (mesh.material as THREE.Material[]).length;
|
|
68
|
+
end = range.start + range.count;
|
|
69
|
+
return !valid;
|
|
70
|
+
}) || end !== g.index.count) continue;
|
|
71
|
+
}
|
|
57
72
|
const held = groups.get(g);
|
|
58
73
|
if (held) held.push(mesh); else groups.set(g, [mesh]);
|
|
59
74
|
}
|
|
@@ -63,7 +78,7 @@ export class BlenderMotionGeometry {
|
|
|
63
78
|
if (id === undefined) { id = ++this.nextIdentity; this.identities.set(object, id); }
|
|
64
79
|
return id;
|
|
65
80
|
};
|
|
66
|
-
const signature = [...groups].map(([g]) => `${g.id}:${identity(g.index!)}:${g.index!.version}:` +
|
|
81
|
+
const signature = [...groups].map(([g, meshes]) => `${g.id}:${identity(g.index!)}:${g.index!.version}:${Array.isArray(meshes[0]!.material)}:${JSON.stringify(g.groups)}:` +
|
|
67
82
|
Object.entries(g.attributes).map(([name, a]) => `${name}:${identity(a)}:${(a as THREE.BufferAttribute).version}`).join(',')).join(';');
|
|
68
83
|
if (signature === this.signature) return;
|
|
69
84
|
this.releaseCopies();
|
|
@@ -78,95 +93,194 @@ export class BlenderMotionGeometry {
|
|
|
78
93
|
this.next();
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
private next(): void {
|
|
82
|
-
if (this.
|
|
96
|
+
private async next(): Promise<void> {
|
|
97
|
+
if (this.building || this.unavailable || this.bytes >= CACHE_BYTES) return;
|
|
83
98
|
const geometry = this.queue.shift();
|
|
84
99
|
if (!geometry) { this.worker?.terminate(); this.worker = null; return; }
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
this.building = true;
|
|
101
|
+
const generation = this.generation;
|
|
102
|
+
const current = () => {
|
|
103
|
+
if (generation !== this.generation) throw cancelled;
|
|
104
|
+
if (this.unavailable) throw new Error(this.unavailable);
|
|
105
|
+
};
|
|
106
|
+
const yieldCopy = async () => { await new Promise<void>(resolve => setTimeout(resolve, 0)); current(); };
|
|
107
|
+
try {
|
|
108
|
+
if (!this.worker) {
|
|
87
109
|
this.worker = new Worker(new URL('./blender-motion-worker.ts', import.meta.url), {type: 'module'});
|
|
88
|
-
this.worker.onmessage = (event: MessageEvent<MotionMeshResponse>) =>
|
|
110
|
+
this.worker.onmessage = (event: MessageEvent<MotionMeshResponse>) => {
|
|
111
|
+
if (this.pending?.id === event.data.id) {
|
|
112
|
+
const pending = this.pending; this.pending = null; pending.resolve(event.data);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
89
115
|
this.worker.onerror = event => {
|
|
90
116
|
this.unavailable = event.message || 'Navigation geometry worker unavailable';
|
|
91
|
-
this.worker?.terminate(); this.worker = null;
|
|
92
|
-
this.
|
|
117
|
+
this.worker?.terminate(); this.worker = null;
|
|
118
|
+
const pending = this.pending; this.pending = null;
|
|
119
|
+
pending?.resolve({id: pending.id, refusal: this.unavailable});
|
|
93
120
|
};
|
|
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
121
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
122
|
+
const position = geometry.getAttribute('position') as THREE.BufferAttribute;
|
|
123
|
+
const meshes = this.groups.get(geometry)!;
|
|
124
|
+
const multi = Array.isArray(meshes[0]!.material);
|
|
125
|
+
if (meshes.some(mesh => Array.isArray(mesh.material) !== multi)) return;
|
|
126
|
+
const ranges: Range[] = multi ? geometry.groups : [{start: 0, count: geometry.index!.count}];
|
|
127
|
+
const chunks = ranges.flatMap(range => {
|
|
128
|
+
const chunks: Range[] = [];
|
|
129
|
+
for (let start = range.start; start < range.start + range.count; start += CHUNK_INDICES)
|
|
130
|
+
chunks.push({start, count: Math.min(CHUNK_INDICES, range.start + range.count - start), materialIndex: range.materialIndex});
|
|
131
|
+
return chunks;
|
|
132
|
+
});
|
|
133
|
+
// Lock only the boundaries introduced by partitioning. LockBorder would
|
|
134
|
+
// also pin every vertex on a real leaf's silhouette, defeating LOD even
|
|
135
|
+
// when its edge could simplify within the allowed appearance error.
|
|
136
|
+
// Blender's source-point map joins UV/normal splits at the same point.
|
|
137
|
+
const sourcePoints = geometry.getAttribute('blenderVertex') as THREE.BufferAttribute | undefined;
|
|
138
|
+
const partition = new Uint32Array(position.count);
|
|
139
|
+
const boundary = new Uint8Array(position.count);
|
|
140
|
+
const pointIndex = (vertex: number) => sourcePoints ? sourcePoints.getX(vertex) : vertex;
|
|
141
|
+
for (let chunk = 0; chunk < chunks.length; chunk++) {
|
|
142
|
+
const range = chunks[chunk]!;
|
|
143
|
+
for (let first = range.start; first < range.start + range.count; first += COPY_ITEMS) {
|
|
144
|
+
await yieldCopy();
|
|
145
|
+
for (let i = first; i < Math.min(first + COPY_ITEMS, range.start + range.count); i++) {
|
|
146
|
+
const point = pointIndex(geometry.index!.getX(i));
|
|
147
|
+
if (!Number.isInteger(point) || point < 0 || point >= partition.length) return;
|
|
148
|
+
if (!partition[point]) partition[point] = chunk + 1;
|
|
149
|
+
else if (partition[point] !== chunk + 1) boundary[point] = 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const appearance = Object.entries(geometry.attributes).filter(([name]) => name !== 'position' && name !== 'blenderVertex');
|
|
154
|
+
const stride = appearance.reduce((n, [, a]) => n + a.itemSize, 0);
|
|
155
|
+
if (stride > 32) return; // meshoptimizer's attribute component limit
|
|
156
|
+
const protectedComponents: number[] = [];
|
|
157
|
+
let component = 0;
|
|
158
|
+
for (const [name, attribute] of appearance) {
|
|
159
|
+
for (let c = 0; c < attribute.itemSize; c++, component++) if (name !== 'normal') protectedComponents.push(component);
|
|
160
|
+
}
|
|
161
|
+
const weights = appearance.flatMap(([name, a]) => Array(a.itemSize).fill(name === 'normal' ? 0.5 : 1));
|
|
162
|
+
// Reuse bounds when present, otherwise derive the error scale in
|
|
163
|
+
// yielding slices. Never scan a large mesh in one UI-thread call.
|
|
164
|
+
const bounds = geometry.boundingBox?.clone() ?? new THREE.Box3();
|
|
165
|
+
if (!geometry.boundingBox) {
|
|
166
|
+
const point = new THREE.Vector3();
|
|
167
|
+
for (let first = 0; first < position.count; first += COPY_ITEMS) {
|
|
168
|
+
await yieldCopy();
|
|
169
|
+
for (let i = first; i < Math.min(first + COPY_ITEMS, position.count); i++)
|
|
170
|
+
bounds.expandByPoint(point.fromBufferAttribute(position, i));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!geometry.boundingSphere) return;
|
|
174
|
+
const absoluteError = Math.max(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y,
|
|
175
|
+
bounds.max.z - bounds.min.z) * 0.002;
|
|
176
|
+
const parts: Uint32Array[] = [];
|
|
177
|
+
const outputGroups: Range[] = [];
|
|
178
|
+
let count = 0, error = 0;
|
|
179
|
+
for (const range of ranges) {
|
|
180
|
+
const groupStart = count;
|
|
181
|
+
for (let start = range.start; start < range.start + range.count; start += CHUNK_INDICES) {
|
|
182
|
+
await yieldCopy();
|
|
183
|
+
const end = Math.min(start + CHUNK_INDICES, range.start + range.count);
|
|
184
|
+
const originals: number[] = [], remap = new Map<number, number>();
|
|
185
|
+
const indices = new Uint32Array(end - start);
|
|
186
|
+
for (let i = start; i < end; i++) {
|
|
187
|
+
const original = geometry.index!.getX(i);
|
|
188
|
+
let local = remap.get(original);
|
|
189
|
+
if (local === undefined) { local = originals.length; originals.push(original); remap.set(original, local); }
|
|
190
|
+
indices[i - start] = local;
|
|
191
|
+
}
|
|
192
|
+
const locks = new Uint8Array(originals.length);
|
|
193
|
+
const positions = new Float32Array(originals.length * 3);
|
|
194
|
+
const values = new Float32Array(originals.length * stride);
|
|
195
|
+
for (let first = 0; first < originals.length; first += COPY_ITEMS) {
|
|
196
|
+
await yieldCopy();
|
|
197
|
+
for (let i = first; i < Math.min(first + COPY_ITEMS, originals.length); i++) {
|
|
198
|
+
const original = originals[i]!;
|
|
199
|
+
locks[i] = boundary[pointIndex(original)]!;
|
|
200
|
+
for (let c = 0; c < 3; c++) positions[i * 3 + c] = position.getComponent(original, c);
|
|
201
|
+
let offset = 0;
|
|
202
|
+
for (const [, attribute] of appearance) {
|
|
203
|
+
for (let c = 0; c < attribute.itemSize; c++)
|
|
204
|
+
values[i * stride + offset + c] = (attribute as THREE.BufferAttribute).getComponent(original, c);
|
|
205
|
+
offset += attribute.itemSize;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const id = ++this.nextId;
|
|
210
|
+
const response = await new Promise<MotionMeshResponse>(resolve => {
|
|
211
|
+
this.pending = {id, resolve};
|
|
212
|
+
const request: MotionMeshRequest = {id, positions, indices, attributes: values, stride, weights, absoluteError, protectedComponents, locks};
|
|
213
|
+
this.worker!.postMessage(request, [positions.buffer, indices.buffer, values.buffer, locks.buffer]);
|
|
214
|
+
});
|
|
215
|
+
current();
|
|
216
|
+
if (response.refusal) throw new Error(response.refusal);
|
|
217
|
+
if (!response.indices || !Number.isFinite(response.error)) throw new Error('Invalid navigation geometry response');
|
|
218
|
+
const reduced = response.indices;
|
|
219
|
+
for (let i = 0; i < reduced.length; i++) reduced[i] = originals[reduced[i]!]!;
|
|
220
|
+
count += reduced.length;
|
|
221
|
+
if (count * 4 > CACHE_BYTES - this.bytes) return;
|
|
222
|
+
parts.push(reduced);
|
|
223
|
+
error = Math.max(error, response.error!);
|
|
224
|
+
}
|
|
225
|
+
if (multi) outputGroups.push({start: groupStart, count: count - groupStart, materialIndex: range.materialIndex});
|
|
226
|
+
}
|
|
227
|
+
const reduction: (typeof this.reductions)[number] = {vertices: position.count, fullTriangles: geometry.index!.count / 3,
|
|
228
|
+
reducedTriangles: count / 3, error, accepted: false};
|
|
229
|
+
if (this.reductions.length < 32) this.reductions.push(reduction);
|
|
230
|
+
if (!count || count >= geometry.index!.count * 0.75) return;
|
|
231
|
+
// Compact only after all material ranges succeed. Yield while remapping
|
|
232
|
+
// and copying; cancellation never publishes a partial surface.
|
|
233
|
+
const remap = new Int32Array(position.count).fill(-1);
|
|
137
234
|
const originals: number[] = [];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
let
|
|
142
|
-
|
|
143
|
-
|
|
235
|
+
const indices = new Uint32Array(count);
|
|
236
|
+
let offset = 0;
|
|
237
|
+
for (const part of parts) {
|
|
238
|
+
for (let first = 0; first < part.length; first += COPY_ITEMS) {
|
|
239
|
+
await yieldCopy();
|
|
240
|
+
for (let i = first; i < Math.min(first + COPY_ITEMS, part.length); i++) {
|
|
241
|
+
const original = part[i]!;
|
|
242
|
+
let local = remap[original]!;
|
|
243
|
+
if (local < 0) { local = originals.length; originals.push(original); remap[original] = local; }
|
|
244
|
+
indices[offset + i] = local;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
offset += part.length;
|
|
144
248
|
}
|
|
145
249
|
const bytes = indices.byteLength + Object.values(geometry.attributes).reduce((n, a) =>
|
|
146
250
|
n + originals.length * a.itemSize * a.array.BYTES_PER_ELEMENT, 0);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
251
|
+
reduction.bytes = bytes;
|
|
252
|
+
if (this.bytes + bytes > CACHE_BYTES) return;
|
|
253
|
+
const copy = new THREE.BufferGeometry();
|
|
254
|
+
copy.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
255
|
+
for (const [name, source] of Object.entries(geometry.attributes)) {
|
|
256
|
+
const attribute = source as THREE.BufferAttribute;
|
|
257
|
+
const ArrayType = attribute.array.constructor as {new(length: number): THREE.TypedArray};
|
|
258
|
+
const data = new ArrayType(originals.length * attribute.itemSize);
|
|
259
|
+
for (let first = 0; first < originals.length; first += COPY_ITEMS) {
|
|
260
|
+
await yieldCopy();
|
|
261
|
+
for (let i = first; i < Math.min(first + COPY_ITEMS, originals.length); i++)
|
|
262
|
+
for (let c = 0; c < attribute.itemSize; c++)
|
|
263
|
+
data[i * attribute.itemSize + c] = attribute.array[originals[i]! * attribute.itemSize + c]!;
|
|
157
264
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
265
|
+
copy.setAttribute(name, new THREE.BufferAttribute(data, attribute.itemSize, attribute.normalized));
|
|
266
|
+
}
|
|
267
|
+
copy.boundingSphere = geometry.boundingSphere.clone();
|
|
268
|
+
copy.boundingBox = bounds.clone();
|
|
269
|
+
for (const group of outputGroups) copy.addGroup(group.start, group.count, group.materialIndex);
|
|
270
|
+
copy.userData = {...geometry.userData};
|
|
271
|
+
reduction.accepted = true;
|
|
272
|
+
this.cache.set(geometry, {geometry: copy, error, bytes});
|
|
273
|
+
this.bytes += bytes;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error === cancelled) return;
|
|
276
|
+
this.unavailable = String(error);
|
|
277
|
+
this.worker?.terminate(); this.worker = null; this.pending = null; this.queue.length = 0;
|
|
278
|
+
} finally {
|
|
279
|
+
if (generation === this.generation) {
|
|
280
|
+
this.building = false;
|
|
281
|
+
this.timer = setTimeout(() => { this.timer = null; void this.next(); }, 0);
|
|
166
282
|
}
|
|
167
283
|
}
|
|
168
|
-
// Yield between meshes, including copying their attributes for the worker.
|
|
169
|
-
this.timer = setTimeout(() => { this.timer = null; this.next(); }, 0);
|
|
170
284
|
}
|
|
171
285
|
|
|
172
286
|
prepare(camera: THREE.Camera, interactive: boolean, height: number): boolean {
|
|
@@ -224,16 +338,19 @@ export class BlenderMotionGeometry {
|
|
|
224
338
|
this.swapped.length = 0;
|
|
225
339
|
}
|
|
226
340
|
inspect() {
|
|
227
|
-
return {copies: this.cache.size, bytes: this.bytes, queued: this.queue.length, pending: this.
|
|
228
|
-
unavailable: this.unavailable, lastNavigation: {...this.lastNavigation}};
|
|
341
|
+
return {copies: this.cache.size, bytes: this.bytes, queued: this.queue.length, pending: this.building,
|
|
342
|
+
unavailable: this.unavailable, reductions: this.reductions.map(r => ({...r})), lastNavigation: {...this.lastNavigation}};
|
|
229
343
|
}
|
|
230
344
|
private releaseCopies(): void {
|
|
231
345
|
this.finish();
|
|
232
346
|
if (this.timer) clearTimeout(this.timer);
|
|
233
347
|
this.timer = null;
|
|
234
|
-
this.
|
|
348
|
+
this.generation++;
|
|
349
|
+
this.worker?.terminate(); this.worker = null;
|
|
350
|
+
this.pending?.resolve({id: this.pending.id, refusal: 'Navigation geometry replaced'});
|
|
351
|
+
this.pending = null; this.building = false; this.queue = [];
|
|
235
352
|
for (const entry of this.cache.values()) entry.geometry.dispose();
|
|
236
|
-
this.cache.clear(); this.bytes = 0;
|
|
353
|
+
this.cache.clear(); this.bytes = 0; this.reductions = [];
|
|
237
354
|
}
|
|
238
355
|
clear(): void {
|
|
239
356
|
this.releaseCopies();
|
|
@@ -7,24 +7,37 @@ export interface MotionMeshRequest {
|
|
|
7
7
|
attributes: Float32Array;
|
|
8
8
|
stride: number;
|
|
9
9
|
weights: number[];
|
|
10
|
+
absoluteError: number;
|
|
11
|
+
protectedComponents: number[];
|
|
12
|
+
locks: Uint8Array;
|
|
10
13
|
}
|
|
11
14
|
export interface MotionMeshResponse {id: number; indices?: Uint32Array; error?: number; refusal?: string}
|
|
12
15
|
|
|
13
16
|
// One copied mesh at a time. The worker never owns or detaches a resident
|
|
14
17
|
// Blender column, and never writes positions or attributes back to the model.
|
|
15
18
|
self.onmessage = async (event: MessageEvent<MotionMeshRequest>) => {
|
|
16
|
-
const {id, positions, indices, attributes, stride, weights} = event.data;
|
|
19
|
+
const {id, positions, indices, attributes, stride, weights, absoluteError, protectedComponents, locks} = event.data;
|
|
17
20
|
try {
|
|
18
21
|
await MeshoptSimplifier.ready;
|
|
19
|
-
|
|
22
|
+
// Permit collapses across normal splits, while explicitly protecting UV
|
|
23
|
+
// and other shader-attribute discontinuities. This follows meshoptimizer
|
|
24
|
+
// 1.2's attribute-aware permissive recipe; no position/attribute is edited.
|
|
25
|
+
const positionsRemap = MeshoptSimplifier.generatePositionRemap(positions, 3);
|
|
26
|
+
for (let i = 0; i < positionsRemap.length; i++) {
|
|
27
|
+
const other = positionsRemap[i]!;
|
|
28
|
+
if (other !== i && protectedComponents.some(c => attributes[i * stride + c] !== attributes[other * stride + c])) {
|
|
29
|
+
locks[i]! |= 2; locks[other]! |= 2; // meshopt_SimplifyVertex_Protect
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const target = Math.min(indices.length, Math.max(384, Math.floor(indices.length * 0.08 / 3) * 3));
|
|
20
33
|
const [reduced, error] = MeshoptSimplifier.simplifyWithAttributes(
|
|
21
|
-
indices, positions, 3, attributes, stride, weights,
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
['
|
|
34
|
+
indices, positions, 3, attributes, stride, weights, locks, target, absoluteError,
|
|
35
|
+
// Explicit locks preserve material/chunk borders. The appearance error
|
|
36
|
+
// includes normals; unsuccessful reductions retain complete originals.
|
|
37
|
+
['ErrorAbsolute', 'Permissive'],
|
|
25
38
|
);
|
|
26
39
|
const response: MotionMeshResponse = {
|
|
27
|
-
id, indices: reduced, error
|
|
40
|
+
id, indices: reduced, error,
|
|
28
41
|
};
|
|
29
42
|
self.postMessage(response, {transfer: [reduced.buffer]});
|
|
30
43
|
} catch (error) {
|