@volter/blender-engine 0.1.55 → 0.1.56

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.
@@ -79,6 +79,8 @@ export const ATTRIBUTE_LAYOUT: Record<AttributeType, { size: number }> = {
79
79
  export interface MeshColumns {
80
80
  /** 3 * nv */
81
81
  co: Float32Array | Float64Array;
82
+ /** Blender's tessellation, triangle corner indices in face order. */
83
+ cornerTri?: Uint32Array | undefined;
82
84
  /** Blender-evaluated normals, 3 per corner, independent of UV splits. */
83
85
  cornerNormal?: Float32Array | undefined;
84
86
  /** 3 * nv, a deformed mesh's Generated coordinates in Blender's stored
@@ -313,8 +313,113 @@ export function geometryFromDrawArrays(
313
313
  return geometry;
314
314
  }
315
315
 
316
+ /** Blender already supplied corner normals and triangulation. Read those
317
+ * columns directly: no polygon triangulation, adjacency graph, vector objects
318
+ * or stringified floating-point keys. Two passes size final buffers exactly. */
319
+ function drawNativeColumns(c: MeshColumns, hash: string): DrawArrays {
320
+ const triangles = c.cornerTri!, normal = c.cornerNormal!;
321
+ const nv = c.co.length / 3, nc = c.corner.length, nf = c.faceStart.length - 1;
322
+ const maps = c.attributes.filter(a => a.type === 'FLOAT2' && a.domain === 'CORNER');
323
+ const mapName = c.renderUv ?? c.activeUv ?? maps[0]?.name;
324
+ const activeMap = maps.findIndex(a => a.name === mapName);
325
+ if (mapName && activeMap < 0) throw new Error(`Runtime UV map ${mapName} is missing`);
326
+ const attributes = graphAttributeLayers(c.attributes, data => data as ArrayLike<number>);
327
+ const width = 3 + maps.length * 2 + attributes.length * 4;
328
+ const scratch = new Float32Array(width), comparison = new Float32Array(width);
329
+ const bits = new Uint32Array(scratch.buffer), comparisonBits = new Uint32Array(comparison.buffer);
330
+ const faces = new Uint32Array(nc), cornerIndex = new Uint32Array(nc);
331
+ const representative = new Uint32Array(nc), links = new Uint32Array(nc);
332
+ const used = new Uint8Array(nv);
333
+ let tableSize = 1;
334
+ while (tableSize < Math.max(1, nc * 2)) tableSize *= 2;
335
+ const buckets = new Uint32Array(tableSize);
336
+ const read = (corner: number, face: number, vertex: number, target: Float32Array): void => {
337
+ let offset = 0;
338
+ for (let k = 0; k < 3; k++) target[offset++] = normal[corner * 3 + k]!;
339
+ for (const map of maps) for (let k = 0; k < 2; k++) target[offset++] = Number(map.data[corner * 2 + k]);
340
+ for (const layer of attributes) {
341
+ const value = layer.read(vertex, corner, face);
342
+ for (let k = 0; k < 4; k++) target[offset++] = Number(value[k]);
343
+ }
344
+ };
345
+ let count = 0;
346
+ for (let face = 0; face < nf; face++) {
347
+ for (let corner = c.faceStart[face]!; corner < c.faceStart[face + 1]!; corner++) {
348
+ const vertex = c.corner[corner]!;
349
+ if (vertex >= nv) throw new Error('Native mesh corner references a missing vertex');
350
+ faces[corner] = face; used[vertex] = 1;
351
+ read(corner, face, vertex, scratch);
352
+ let digest = Math.imul(vertex ^ 2166136261, 16777619);
353
+ for (const word of bits) digest = Math.imul(digest ^ word, 16777619);
354
+ const bucket = (digest >>> 0) & (tableSize - 1);
355
+ let found = -1;
356
+ for (let entry = buckets[bucket]!; entry; entry = links[entry - 1]!) {
357
+ const index = entry - 1, previous = representative[index]!;
358
+ if (c.corner[previous] !== vertex) continue;
359
+ read(previous, faces[previous]!, vertex, comparison);
360
+ let equal = true;
361
+ for (let k = 0; k < width; k++) if (bits[k] !== comparisonBits[k]) { equal = false; break; }
362
+ if (equal) { found = index; break; }
363
+ }
364
+ if (found < 0) {
365
+ found = count++; representative[found] = corner;
366
+ links[found] = buckets[bucket]!; buckets[bucket] = found + 1;
367
+ }
368
+ cornerIndex[corner] = found;
369
+ }
370
+ }
371
+ let loose = 0; for (const flag of used) if (!flag) loose++;
372
+ const total = count + loose;
373
+ const positions = new Float32Array(total * 3), normals = new Float32Array(total * 3);
374
+ const sourceVertex = new Uint32Array(total);
375
+ const uvLayers = maps.map(map => ({ name: map.name, data: new Float32Array(total * 2) }));
376
+ const attributeLayers = attributes.map(layer => ({ name: layer.name, data: new Float32Array(total * 4) }));
377
+ const orco = c.orco ? new Float32Array(total * 3) : null;
378
+ const position = (index: number, vertex: number): void => {
379
+ sourceVertex[index] = vertex;
380
+ for (let k = 0; k < 3; k++) {
381
+ positions[index * 3 + k] = c.co[vertex * 3 + k]!;
382
+ if (orco) orco[index * 3 + k] = c.orco![vertex * 3 + k]! * 0.5 + 0.5;
383
+ }
384
+ };
385
+ for (let index = 0; index < count; index++) {
386
+ const corner = representative[index]!, vertex = c.corner[corner]!;
387
+ position(index, vertex); read(corner, faces[corner]!, vertex, scratch);
388
+ normals.set(scratch.subarray(0, 3), index * 3);
389
+ let offset = 3;
390
+ for (const layer of uvLayers) { layer.data.set(scratch.subarray(offset, offset + 2), index * 2); offset += 2; }
391
+ for (const layer of attributeLayers) { layer.data.set(scratch.subarray(offset, offset + 4), index * 4); offset += 4; }
392
+ }
393
+ let index = count;
394
+ for (let vertex = 0; vertex < nv; vertex++) if (!used[vertex]) {
395
+ position(index, vertex);
396
+ for (let layer = 0; layer < attributes.length; layer++) attributeLayers[layer]!.data.set(attributes[layer]!.read(vertex, undefined, undefined), index * 4);
397
+ index++;
398
+ }
399
+ const indices = new Uint32Array(triangles.length), groups: DrawArrays['groups'] = [];
400
+ let offset = 0;
401
+ for (let face = 0; face < nf; face++) {
402
+ const start = c.faceStart[face]!, end = c.faceStart[face + 1]!;
403
+ const length = Math.max(0, end - start - 2) * 3;
404
+ if (offset + length > triangles.length) throw new Error('Native mesh triangulation is incomplete');
405
+ for (let j = 0; j < length; j++) {
406
+ const corner = triangles[offset + j]!;
407
+ if (corner < start || corner >= end) throw new Error('Native triangle references a different face');
408
+ indices[offset + j] = cornerIndex[corner]!;
409
+ }
410
+ const materialIndex = c.material[face]!, previous = groups.at(-1);
411
+ if (previous?.materialIndex === materialIndex) previous.count += length;
412
+ else groups.push({ start: offset, count: length, materialIndex });
413
+ offset += length;
414
+ }
415
+ if (offset !== triangles.length) throw new Error('Native mesh triangulation has extra triangles');
416
+ return { positions, normals, sourceVertex, uv: activeMap < 0 ? null : uvLayers[activeMap]!.data,
417
+ uvLayers, attributeLayers, orco, indices, groups, hash };
418
+ }
419
+
316
420
  /** The worker's door: the mesh store's columns, drawn into buffers. */
317
421
  export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays {
422
+ if (c.cornerTri && c.cornerNormal) return drawNativeColumns(c, hash);
318
423
  const nv = c.co.length / 3;
319
424
  const maps = c.attributes.filter((a) => a.type === 'FLOAT2' && a.domain === 'CORNER');
320
425
  const mapName = c.renderUv ?? c.activeUv ?? maps[0]?.name;
@@ -186,6 +186,7 @@ const columnsSchema = z
186
186
  .object({
187
187
  co: z.union([z.instanceof(Float32Array), z.instanceof(Float64Array)]),
188
188
  cornerNormal: z.instanceof(Float32Array).optional(),
189
+ cornerTri: z.instanceof(Uint32Array).optional(),
189
190
  orco: z.instanceof(Float32Array).optional(),
190
191
  faceStart: z.instanceof(Uint32Array),
191
192
  corner: z.instanceof(Uint32Array),
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.55",
5
+ "version": "0.1.56",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },