@volter/blender-engine 0.1.75 → 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.
|
@@ -29,7 +29,7 @@ export class BlenderMotionGeometry {
|
|
|
29
29
|
private nextIdentity = 0;
|
|
30
30
|
private bytes = 0;
|
|
31
31
|
private unavailable: string | null = null;
|
|
32
|
-
private reductions: {vertices: number; fullTriangles: number; reducedTriangles: number; error: number; accepted: boolean}[] = [];
|
|
32
|
+
private reductions: {vertices: number; fullTriangles: number; reducedTriangles: number; error: number; accepted: boolean; bytes?: number}[] = [];
|
|
33
33
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
34
34
|
private readonly cameraWorld = new THREE.Matrix4();
|
|
35
35
|
private readonly cameraProjection = new THREE.Matrix4();
|
|
@@ -124,8 +124,40 @@ export class BlenderMotionGeometry {
|
|
|
124
124
|
const multi = Array.isArray(meshes[0]!.material);
|
|
125
125
|
if (meshes.some(mesh => Array.isArray(mesh.material) !== multi)) return;
|
|
126
126
|
const ranges: Range[] = multi ? geometry.groups : [{start: 0, count: geometry.index!.count}];
|
|
127
|
-
const
|
|
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');
|
|
128
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
|
+
}
|
|
129
161
|
const weights = appearance.flatMap(([name, a]) => Array(a.itemSize).fill(name === 'normal' ? 0.5 : 1));
|
|
130
162
|
// Reuse bounds when present, otherwise derive the error scale in
|
|
131
163
|
// yielding slices. Never scan a large mesh in one UI-thread call.
|
|
@@ -157,12 +189,14 @@ export class BlenderMotionGeometry {
|
|
|
157
189
|
if (local === undefined) { local = originals.length; originals.push(original); remap.set(original, local); }
|
|
158
190
|
indices[i - start] = local;
|
|
159
191
|
}
|
|
192
|
+
const locks = new Uint8Array(originals.length);
|
|
160
193
|
const positions = new Float32Array(originals.length * 3);
|
|
161
194
|
const values = new Float32Array(originals.length * stride);
|
|
162
195
|
for (let first = 0; first < originals.length; first += COPY_ITEMS) {
|
|
163
196
|
await yieldCopy();
|
|
164
197
|
for (let i = first; i < Math.min(first + COPY_ITEMS, originals.length); i++) {
|
|
165
198
|
const original = originals[i]!;
|
|
199
|
+
locks[i] = boundary[pointIndex(original)]!;
|
|
166
200
|
for (let c = 0; c < 3; c++) positions[i * 3 + c] = position.getComponent(original, c);
|
|
167
201
|
let offset = 0;
|
|
168
202
|
for (const [, attribute] of appearance) {
|
|
@@ -175,8 +209,8 @@ export class BlenderMotionGeometry {
|
|
|
175
209
|
const id = ++this.nextId;
|
|
176
210
|
const response = await new Promise<MotionMeshResponse>(resolve => {
|
|
177
211
|
this.pending = {id, resolve};
|
|
178
|
-
const request: MotionMeshRequest = {id, positions, indices, attributes: values, stride, weights, absoluteError};
|
|
179
|
-
this.worker!.postMessage(request, [positions.buffer, indices.buffer, values.buffer]);
|
|
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]);
|
|
180
214
|
});
|
|
181
215
|
current();
|
|
182
216
|
if (response.refusal) throw new Error(response.refusal);
|
|
@@ -190,7 +224,7 @@ export class BlenderMotionGeometry {
|
|
|
190
224
|
}
|
|
191
225
|
if (multi) outputGroups.push({start: groupStart, count: count - groupStart, materialIndex: range.materialIndex});
|
|
192
226
|
}
|
|
193
|
-
const reduction = {vertices: position.count, fullTriangles: geometry.index!.count / 3,
|
|
227
|
+
const reduction: (typeof this.reductions)[number] = {vertices: position.count, fullTriangles: geometry.index!.count / 3,
|
|
194
228
|
reducedTriangles: count / 3, error, accepted: false};
|
|
195
229
|
if (this.reductions.length < 32) this.reductions.push(reduction);
|
|
196
230
|
if (!count || count >= geometry.index!.count * 0.75) return;
|
|
@@ -214,6 +248,7 @@ export class BlenderMotionGeometry {
|
|
|
214
248
|
}
|
|
215
249
|
const bytes = indices.byteLength + Object.values(geometry.attributes).reduce((n, a) =>
|
|
216
250
|
n + originals.length * a.itemSize * a.array.BYTES_PER_ELEMENT, 0);
|
|
251
|
+
reduction.bytes = bytes;
|
|
217
252
|
if (this.bytes + bytes > CACHE_BYTES) return;
|
|
218
253
|
const copy = new THREE.BufferGeometry();
|
|
219
254
|
copy.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
@@ -8,21 +8,33 @@ export interface MotionMeshRequest {
|
|
|
8
8
|
stride: number;
|
|
9
9
|
weights: number[];
|
|
10
10
|
absoluteError: number;
|
|
11
|
+
protectedComponents: number[];
|
|
12
|
+
locks: Uint8Array;
|
|
11
13
|
}
|
|
12
14
|
export interface MotionMeshResponse {id: number; indices?: Uint32Array; error?: number; refusal?: string}
|
|
13
15
|
|
|
14
16
|
// One copied mesh at a time. The worker never owns or detaches a resident
|
|
15
17
|
// Blender column, and never writes positions or attributes back to the model.
|
|
16
18
|
self.onmessage = async (event: MessageEvent<MotionMeshRequest>) => {
|
|
17
|
-
const {id, positions, indices, attributes, stride, weights, absoluteError} = event.data;
|
|
19
|
+
const {id, positions, indices, attributes, stride, weights, absoluteError, protectedComponents, locks} = event.data;
|
|
18
20
|
try {
|
|
19
21
|
await MeshoptSimplifier.ready;
|
|
20
|
-
|
|
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));
|
|
21
33
|
const [reduced, error] = MeshoptSimplifier.simplifyWithAttributes(
|
|
22
|
-
indices, positions, 3, attributes, stride, weights,
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
['
|
|
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'],
|
|
26
38
|
);
|
|
27
39
|
const response: MotionMeshResponse = {
|
|
28
40
|
id, indices: reduced, error,
|