@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 appearance = Object.entries(geometry.attributes).filter(([name]) => name === 'normal' || /^uv\d*$/.test(name));
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
- const target = Math.max(384, Math.floor(indices.length * 0.08 / 3) * 3);
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, null, target, absoluteError,
23
- // Preserve open boundaries and attribute seams. If that prevents a
24
- // useful reduction, keep the original; never sample/drop triangles.
25
- ['LockBorder', 'ErrorAbsolute'],
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,
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.75",
5
+ "version": "0.1.76",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },