@volter/blender-engine 0.1.55 → 0.1.57

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.57",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
package/wasm/BUNDLE.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "blender": "5.2.0 LTS",
3
- "source": "e5d7eec67572dfc7c73f8f4d3f601d03442546f9",
4
- "reviewedSource": "e5d7eec67572dfc7c73f8f4d3f601d03442546f9",
3
+ "source": "54e4af619a285c6d76c56ef960dc99d45aca5084",
5
4
  "upstream": "fbe6228777e7d9afefcd61a413844e790ae75db7",
6
5
  "sourceRepository": "https://github.com/volter-ai/blender",
7
- "sourceRecipe": "https://github.com/volter-ai/blender/blob/e5d7eec67572dfc7c73f8f4d3f601d03442546f9/build_files/emscripten/RELEASE.md",
6
+ "sourceRecipe": "https://github.com/volter-ai/blender/blob/54e4af619a285c6d76c56ef960dc99d45aca5084/build_files/emscripten/RELEASE.md",
8
7
  "factoryEngine": "BLENDER_EEVEE",
9
8
  "files": {
10
9
  "blender_browser.js": "raw",
@@ -17,8 +16,8 @@
17
16
  "sha256": "0c63c29e7d4b1ba1fce4e94057778b0757767b25ea4f6f01d329e99351568f30"
18
17
  },
19
18
  "blender_browser.wasm": {
20
- "bytes": 86317655,
21
- "sha256": "0ba94ac8388de893bcec4da70516aea69128addaa490ef27f17ca06262ee6546"
19
+ "bytes": 86332914,
20
+ "sha256": "15c4b7072ebc655418849da79a6fefed8bff26774e7446ade8d3a54bd6ad7a61"
22
21
  },
23
22
  "blender_browser.data": {
24
23
  "bytes": 52698568,
@@ -31,8 +30,8 @@
31
30
  "sha256": "0c63c29e7d4b1ba1fce4e94057778b0757767b25ea4f6f01d329e99351568f30"
32
31
  },
33
32
  "blender_browser.wasm.br": {
34
- "bytes": 18596913,
35
- "sha256": "64af23954e1911ffcb879f69a9e8c9bbd83b5531939b586228f44fcea8211e3b"
33
+ "bytes": 18601239,
34
+ "sha256": "8cd29a87fcce046c292c52cc62523fe0ebbd1db8f05b19503d56311ae12c84c5"
36
35
  },
37
36
  "blender_browser.data.br": {
38
37
  "bytes": 8056563,
@@ -43,9 +42,9 @@
43
42
  "sha256": "4085e9336266cf66f061d4d5c962a5929cc96d5732c746b5c13ea05cd104a75b"
44
43
  }
45
44
  },
46
- "note": "Unreleased Stoneguard candidate. Clean source build at the named commit; Release -O3 objects and -O2 link, Emscripten 6.0.5. Focused geometry regression matches shipped engine across original, vertex edit and subdivision-level edit. Full historical battery is absent from current editor checkout; no full-battery claim.",
45
+ "note": "Stoneguard c7/c14/c15 engine. Emscripten 6.0.5, Release -O3 objects and -O2 link. Portable native arm64 matrix reciprocal plus fused generic OpenSubdiv stencil and patch accumulation; bodice four-stage comparison 28/28 arrays exact. Full-scene comparison and memory scope recorded in provenance/stoneguard-memory.json. Historical battery absent; no full-battery or hosted acceptance claim.",
47
46
  "callTrampoline": "CPython 3.13.13 is compiled with PY_CALL_TRAMPOLINE for Blender CPython slot arity casts. This applies to CPython dispatch, not arbitrary indirect calls.",
48
47
  "engineIds": "The existing session registers Python render engines under CYCLES, BLENDER_EEVEE and BLENDER_WORKBENCH; the headless build permits built-in IDs to be shadowed.",
49
- "exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build. CPU evaluators adopt stencil tables without cloning; browser subdivision descriptors are released immediately after CPU tessellation/deformation.",
48
+ "exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build. CPU evaluators adopt stencil tables without cloning; browser subdivision descriptors are released immediately after CPU tessellation/deformation. Native evaluated corner triangles accompany corner normals. Consumed local conversion stencils are released before later UV channels are constructed.",
50
49
  "preloadedPayload": "releasePreloadedFileData releases the JS preload buffer after session ready. The measured working payload exclusions are retained."
51
50
  }
Binary file