@volter/blender-engine 0.1.0

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.
Files changed (48) hide show
  1. package/LICENSE +724 -0
  2. package/README.md +48 -0
  3. package/browser/blender-emscripten-engine.mts +289 -0
  4. package/browser/blender-engine.mts +412 -0
  5. package/browser/blender-wali-engine.mts +362 -0
  6. package/browser/index.ts +7 -0
  7. package/browser/protocol.ts +202 -0
  8. package/browser/rna.ts +697 -0
  9. package/browser/runtime.ts +511 -0
  10. package/browser/session-frame.mts +169 -0
  11. package/browser/session.py +4166 -0
  12. package/browser/three/agx-base-srgb.lut +0 -0
  13. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  14. package/browser/three/agx-look-punchy.lut +0 -0
  15. package/browser/three/attach-presenter.ts +140 -0
  16. package/browser/three/blender-agx.ts +235 -0
  17. package/browser/three/blender-base64.ts +42 -0
  18. package/browser/three/blender-corner-normals.ts +432 -0
  19. package/browser/three/blender-display-lut.ts +145 -0
  20. package/browser/three/blender-filmic.ts +49 -0
  21. package/browser/three/blender-frame-columns.ts +100 -0
  22. package/browser/three/blender-gradient-texture.ts +57 -0
  23. package/browser/three/blender-runtime-armature.ts +528 -0
  24. package/browser/three/blender-runtime-frame.ts +39 -0
  25. package/browser/three/blender-runtime-geometry.ts +342 -0
  26. package/browser/three/blender-runtime-lighting.ts +829 -0
  27. package/browser/three/blender-runtime-shadows.ts +107 -0
  28. package/browser/three/blender-runtime-view.ts +1481 -0
  29. package/browser/three/blender-runtime-volume.ts +128 -0
  30. package/browser/three/blender-runtime-weights.ts +306 -0
  31. package/browser/three/blender-sky.ts +461 -0
  32. package/browser/three/blender-standard.ts +68 -0
  33. package/browser/three/blender-triangulate.ts +181 -0
  34. package/browser/three/filmic-srgb.lut +0 -0
  35. package/browser/three/presenter.ts +265 -0
  36. package/browser/three/release.ts +27 -0
  37. package/browser/three/sky-precompute-worker.ts +45 -0
  38. package/browser/three/sky-worker.ts +79 -0
  39. package/browser/three/world-field-sampler.ts +358 -0
  40. package/browser/three/world-math.ts +59 -0
  41. package/browser/vgai_three.py +554 -0
  42. package/browser/worker.ts +648 -0
  43. package/package.json +48 -0
  44. package/wasm/BUNDLE.json +65 -0
  45. package/wasm/DEPENDENCY-LICENSES.txt +4879 -0
  46. package/wasm/blender_browser.data.br +0 -0
  47. package/wasm/blender_browser.js +2 -0
  48. package/wasm/blender_browser.wasm.br +0 -0
@@ -0,0 +1,39 @@
1
+ /** The runtime frame's INCREMENTAL contract — the one thing the worker that
2
+ * builds a frame (`runtime_present`) and the presenter that applies it
3
+ * (`blender-runtime-view.ts`) both have to agree on.
4
+ *
5
+ * A present ships the columns of the meshes whose store revision moved since
6
+ * the last frame that session sent, and a bare REFERENCE for the rest; the
7
+ * presenter already holds those as `BufferGeometry` and keeps them. This file
8
+ * has no dependencies on purpose: the worker must not pull three.js in to read
9
+ * a marker string.
10
+ */
11
+
12
+ /** A mesh the presenter is expected to already hold, named by the store and
13
+ * revision it was last sent as. `store`/`revision` are diagnostic — they are
14
+ * what a stale-geometry report would quote — and `unchanged` is the
15
+ * discriminator. */
16
+ export interface UnchangedMesh {
17
+ store: number;
18
+ revision: number;
19
+ unchanged: true;
20
+ }
21
+
22
+ /** The presenter's refusal when a reference names geometry it does not hold.
23
+ *
24
+ * It is a MARKER IN THE MESSAGE rather than an error subclass because the
25
+ * refusal crosses a `Function`-constructed page eval and an SSE channel before
26
+ * the worker sees it, and only the message survives that trip. The worker
27
+ * answers by re-sending the whole frame in full; a page that reloaded under a
28
+ * still-running Python session is the case this exists for. */
29
+ export const UNKNOWN_GEOMETRY = 'RUNTIME_FRAME_UNKNOWN_GEOMETRY';
30
+
31
+ /** The presenter's refusal when a material names a PICTURE it does not hold.
32
+ *
33
+ * The same contract one level up from the meshes: a frame carries an image's
34
+ * bytes once per `(name, revision)` and after that its materials name it
35
+ * (`blender/python/runtime_session.py`, `_stage_images`). The worker cannot
36
+ * answer this one — the bytes are Python's to re-stage — so unlike
37
+ * `UNKNOWN_GEOMETRY` it travels all the way back, and `RuntimeSession.present`
38
+ * drops its record and presents the frame again in full. */
39
+ export const UNKNOWN_IMAGE = 'RUNTIME_FRAME_UNKNOWN_IMAGE';
@@ -0,0 +1,342 @@
1
+ /** Draw a mesh snapshot without allocating editable BMesh topology.
2
+ * Triangulation, smooth connected fans, UV splits and material groups follow
3
+ * toBufferGeometry; only the temporary adjacency uses compact corner indices.
4
+ *
5
+ * Two doors, one algorithm. The worker that owns the mesh store draws from
6
+ * the COLUMNS into typed arrays (`drawArraysFromColumns`) and the tab turns
7
+ * those into a `BufferGeometry` (`geometryFromDrawArrays`) — the frame
8
+ * crosses as transferable buffers, never as nested number arrays. The
9
+ * boundary JSON door (`drawRuntimeGeometry`) remains for a frame that
10
+ * arrives as JSON.
11
+ *
12
+ * A drawn geometry carried a `userData.authoredUv` mark until 2026-09-19.
13
+ * MEASURED when this file became `@volter/editor-blender`'s: its only readers were the
14
+ * kit's own `bmesh.ts` and `unwrap.ts`, and a presented frame's geometry is
15
+ * never handed to the kit — it is the Model document's scene. Data with no
16
+ * reader is not authored, so the mark is gone.
17
+ */
18
+ import * as THREE from 'three';
19
+ import { customCornerNormals, type Normal } from './blender-corner-normals';
20
+ import { ATTRIBUTE_LAYOUT, type MeshColumns } from './blender-frame-columns';
21
+ import { triangulatePolygon } from './blender-triangulate';
22
+
23
+ type MeshData = {
24
+ v: [number, number, number][];
25
+ f: number[][];
26
+ e?: [number, number][] | undefined;
27
+ edge_order?: [number, number][] | undefined;
28
+ m?: number[] | undefined;
29
+ s?: boolean[] | undefined;
30
+ sharp?: [number, number][] | undefined;
31
+ attributes?: { name: string; domain: string; type: string; data: unknown[] }[] | undefined;
32
+ active_uv?: string | null | undefined;
33
+ render_uv?: string | null | undefined;
34
+ };
35
+
36
+ /** The draw geometry as buffers: what the worker posts and the tab uploads. */
37
+ export interface DrawArrays {
38
+ positions: Float32Array;
39
+ normals: Float32Array | null;
40
+ /** Per drawn vertex, or null when the mesh has no UV map. */
41
+ uv: Float32Array | null;
42
+ indices: Uint32Array;
43
+ groups: { start: number; count: number; materialIndex: number }[];
44
+ /** THE BLENDER VERTEX EACH DRAWN VERTEX CAME FROM, one per drawn vertex.
45
+ *
46
+ * The draw splits a Blender vertex into as many drawn ones as its corners
47
+ * need (a UV seam, a sharp edge, a flat face), so nothing downstream can
48
+ * recover the mapping from the arrays alone — and an overlay that colours by
49
+ * a PER-VERTEX fact off the engine (a vertex group's weights,
50
+ * `blender-runtime-weights.ts`) needs exactly this. `emit` is the one place
51
+ * a drawn vertex is created, so it is the one place that knows. */
52
+ sourceVertex: Uint32Array;
53
+ /** The store's content hash: the frame reuse key. */
54
+ hash: string;
55
+ }
56
+
57
+ /** What the draw algorithm reads, from either door. */
58
+ interface DrawView {
59
+ vertexCount: number;
60
+ normals: Normal[] | null;
61
+ co(i: number): THREE.Vector3;
62
+ /** CSR faces: corner `starts[i]..starts[i+1]` hold vertex indices. */
63
+ starts: Uint32Array;
64
+ cornerVerts: Uint32Array;
65
+ smooth(face: number): boolean;
66
+ material(face: number): number;
67
+ /** Every mesh edge as a vertex pair; `null` when only faces define them. */
68
+ edges: Iterable<[number, number]> | null;
69
+ sharp: Iterable<[number, number]>;
70
+ /** A corner's UV, or undefined when the mesh has no map. */
71
+ uv: ((corner: number) => [number, number]) | null;
72
+ }
73
+
74
+ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
75
+ const nv = view.vertexCount,
76
+ nf = view.starts.length - 1;
77
+ const starts = view.starts,
78
+ cornerVerts = view.cornerVerts;
79
+ const cornerCount = starts[nf]!;
80
+ let capacity = nv;
81
+ for (let i = 0; i < nf; i++) capacity += (starts[i + 1]! - starts[i]! - 2) * 3;
82
+ const next = new Uint32Array(cornerCount);
83
+ const smooth = new Uint8Array(cornerCount),
84
+ used = new Uint8Array(nv);
85
+ const parents = new Uint32Array(cornerCount);
86
+ let mixed = false;
87
+ for (let i = 0; i < nf; i++)
88
+ if (view.smooth(i)) {
89
+ mixed = true;
90
+ break;
91
+ }
92
+ const edgeKey = (a: number, b: number): number | string =>
93
+ nv < 94906265 ? Math.min(a, b) * nv + Math.max(a, b) : `${Math.min(a, b)}:${Math.max(a, b)}`;
94
+ const edges = new Map<number | string, number>();
95
+ const sharp = new Set<number | string>();
96
+ for (const [a, b] of view.sharp) sharp.add(edgeKey(a, b));
97
+ if (view.edges)
98
+ for (const [a, b] of view.edges) {
99
+ if (a === b) throw new Error('Runtime mesh edge needs distinct vertices');
100
+ edges.set(edgeKey(a, b), -1);
101
+ }
102
+ const find = (value: number): number => {
103
+ while (parents[value] !== value) {
104
+ parents[value] = parents[parents[value]!]!;
105
+ value = parents[value]!;
106
+ }
107
+ return value;
108
+ };
109
+ const union = (a: number, b: number) => {
110
+ const x = find(a),
111
+ y = find(b);
112
+ parents[Math.max(x, y)] = Math.min(x, y);
113
+ };
114
+ for (let i = 0; i < nf; i++) {
115
+ const start = starts[i]!,
116
+ length = starts[i + 1]! - start;
117
+ const s = view.smooth(i) ? 1 : 0;
118
+ for (let j = 0; j < length; j++) {
119
+ const c = start + j;
120
+ used[cornerVerts[c]!] = 1;
121
+ next[c] = start + ((j + 1) % length);
122
+ smooth[c] = s;
123
+ parents[c] = c;
124
+ }
125
+ }
126
+ for (let c = 0; c < cornerCount; c++) {
127
+ const end = next[c]!,
128
+ a = cornerVerts[c]!,
129
+ b = cornerVerts[end]!;
130
+ const key = edgeKey(a, b),
131
+ previous = edges.get(key);
132
+ if (
133
+ previous !== undefined &&
134
+ previous >= 0 &&
135
+ smooth[c] &&
136
+ smooth[previous] &&
137
+ !sharp.has(key)
138
+ ) {
139
+ const otherEnd = next[previous]!;
140
+ union(c, cornerVerts[previous] === a ? previous : otherEnd);
141
+ union(end, cornerVerts[previous] === a ? otherEnd : previous);
142
+ }
143
+ if (previous === undefined || previous < 0 || (!smooth[previous] && smooth[c]))
144
+ edges.set(key, c);
145
+ }
146
+ const uv = view.uv;
147
+ const positions = new Float32Array(capacity * 3),
148
+ texcoords = new Float32Array(capacity * 2);
149
+ const sourceVertex = new Uint32Array(capacity);
150
+ const indices = new Uint32Array(capacity),
151
+ shared = new Map<string, number>();
152
+ const groups: DrawArrays['groups'] = [];
153
+ const normals = view.normals ? new Float32Array(capacity * 3) : null;
154
+ let count = 0,
155
+ indexCount = 0;
156
+ const emit = (v: number, texture?: [number, number], normal?: Normal): number => {
157
+ const index = count++;
158
+ sourceVertex[index] = v;
159
+ view.co(v).toArray(positions, index * 3);
160
+ if (texture) texcoords.set(texture, index * 2);
161
+ if (normal) normals!.set(normal, index * 3);
162
+ return index;
163
+ };
164
+ const corner = (c: number): number => {
165
+ const texture = uv?.(c);
166
+ const normal = view.normals?.[c];
167
+ if (!mixed) return emit(cornerVerts[c]!, texture, normal);
168
+ const key = `${smooth[c] ? find(c) : c}${texture ? `:${texture[0]}:${texture[1]}` : ''}${normal ? `:n${normal.join(':')}` : ''}`;
169
+ let index = shared.get(key);
170
+ if (index === undefined) {
171
+ index = emit(cornerVerts[c]!, texture, normal);
172
+ shared.set(key, index);
173
+ }
174
+ return index;
175
+ };
176
+ for (let i = 0; i < nf; i++) {
177
+ const start = indexCount,
178
+ faceStart = starts[i]!,
179
+ length = starts[i + 1]! - faceStart;
180
+ const points: THREE.Vector3[] = [];
181
+ for (let j = 0; j < length; j++) points.push(view.co(cornerVerts[faceStart + j]!));
182
+ for (const triangle of triangulatePolygon(points).triangles)
183
+ for (const c of triangle) indices[indexCount++] = corner(faceStart + c);
184
+ const materialIndex = view.material(i),
185
+ previous = groups.at(-1);
186
+ if (previous?.materialIndex === materialIndex) previous.count += indexCount - start;
187
+ else groups.push({ start, count: indexCount - start, materialIndex });
188
+ }
189
+ for (let i = 0; i < nv; i++) if (!used[i]) emit(i);
190
+ return {
191
+ positions: positions.slice(0, count * 3),
192
+ normals: normals?.slice(0, count * 3) ?? null,
193
+ uv: uv ? texcoords.slice(0, count * 2) : null,
194
+ indices: indices.slice(0, indexCount),
195
+ sourceVertex: sourceVertex.slice(0, count),
196
+ groups,
197
+ };
198
+ }
199
+
200
+ export function geometryFromDrawArrays(
201
+ arrays: Omit<DrawArrays, 'hash' | 'sourceVertex'> & { sourceVertex?: Uint32Array },
202
+ ): THREE.BufferGeometry {
203
+ const geometry = new THREE.BufferGeometry();
204
+ geometry.setAttribute('position', new THREE.BufferAttribute(arrays.positions, 3));
205
+ geometry.setAttribute(
206
+ 'uv',
207
+ new THREE.BufferAttribute(arrays.uv ?? new Float32Array((arrays.positions.length / 3) * 2), 2),
208
+ );
209
+ geometry.setIndex(new THREE.BufferAttribute(arrays.indices, 1));
210
+ for (const group of arrays.groups)
211
+ geometry.addGroup(group.start, group.count, group.materialIndex);
212
+ if (arrays.normals) geometry.setAttribute('normal', new THREE.BufferAttribute(arrays.normals, 3));
213
+ else geometry.computeVertexNormals();
214
+ // The draw's own vertex map (see `DrawArrays.sourceVertex`). Optional here
215
+ // because a frame may arrive ALREADY DRAWN from the worker, whose buffers
216
+ // predate this attribute; a reader that needs it says so by name rather than
217
+ // colouring by a guessed index.
218
+ if (arrays.sourceVertex)
219
+ geometry.setAttribute('blenderVertex', new THREE.BufferAttribute(arrays.sourceVertex, 1));
220
+ return geometry;
221
+ }
222
+
223
+ /** The worker's door: the mesh store's columns, drawn into buffers. */
224
+ export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays {
225
+ const nv = c.co.length / 3;
226
+ const maps = c.attributes.filter((a) => a.type === 'FLOAT2' && a.domain === 'CORNER');
227
+ const mapName = c.renderUv ?? c.activeUv ?? maps[0]?.name;
228
+ const map = maps.find((a) => a.name === mapName);
229
+ if (mapName && !map) throw new Error(`Runtime UV map ${mapName} is missing`);
230
+ const uvData = map ? (map.data as Float32Array) : null;
231
+ const ne = c.edge.length / 2;
232
+ const edges: [number, number][] = new Array(ne);
233
+ for (let i = 0; i < ne; i++) edges[i] = [c.edge[i * 2]!, c.edge[i * 2 + 1]!];
234
+ const sharp: [number, number][] = [];
235
+ for (let i = 0; i < ne; i++) if (c.edgeSharp[i] === 1) sharp.push(edges[i]!);
236
+ const custom = c.attributes.find((a) => a.name === 'custom_normal');
237
+ const normalMesh = custom
238
+ ? {
239
+ v: Array.from(
240
+ { length: nv },
241
+ (_, i) => [c.co[i * 3]!, c.co[i * 3 + 1]!, c.co[i * 3 + 2]!] as Normal,
242
+ ),
243
+ f: Array.from({ length: c.faceStart.length - 1 }, (_, i) =>
244
+ Array.from(c.corner.subarray(c.faceStart[i], c.faceStart[i + 1])),
245
+ ),
246
+ s: Array.from(c.smooth, Boolean),
247
+ edge_order: edges,
248
+ sharp,
249
+ face_edges: Array.from({ length: c.faceStart.length - 1 }, (_, i) =>
250
+ Array.from(c.cornerEdge.subarray(c.faceStart[i], c.faceStart[i + 1])),
251
+ ),
252
+ edge_sharp: Array.from(c.edgeSharp, Boolean),
253
+ }
254
+ : null;
255
+ const customData = custom
256
+ ? {
257
+ type: custom.type,
258
+ domain: custom.domain,
259
+ data: Array.from(
260
+ { length: custom.data.length / ATTRIBUTE_LAYOUT[custom.type].size },
261
+ (_, i) =>
262
+ Array.from({ length: ATTRIBUTE_LAYOUT[custom.type].size }, (_, j) =>
263
+ Number(custom.data[i * ATTRIBUTE_LAYOUT[custom.type].size + j]),
264
+ ),
265
+ ),
266
+ }
267
+ : undefined;
268
+ const scratch = new THREE.Vector3();
269
+ const view: DrawView = {
270
+ vertexCount: nv,
271
+ normals: normalMesh ? customCornerNormals(normalMesh, customData) : null,
272
+ co: (i) => scratch.set(c.co[i * 3]!, c.co[i * 3 + 1]!, c.co[i * 3 + 2]!).clone(),
273
+ starts: c.faceStart,
274
+ cornerVerts: c.corner,
275
+ smooth: (i) => c.smooth[i] === 1,
276
+ material: (i) => c.material[i]!,
277
+ edges,
278
+ sharp,
279
+ uv: uvData ? (corner) => [uvData[corner * 2]!, uvData[corner * 2 + 1]!] : null,
280
+ };
281
+ if (map && ATTRIBUTE_LAYOUT.FLOAT2.size !== 2) throw new Error('FLOAT2 layout');
282
+ return { ...drawCore(view), hash };
283
+ }
284
+
285
+ /** The boundary JSON door. */
286
+ export function drawRuntimeGeometry(data: MeshData): THREE.BufferGeometry {
287
+ const vertices = data.v.map((co) => new THREE.Vector3(...co));
288
+ const vertex = (index: number) => {
289
+ const value = vertices[index];
290
+ if (!value) throw new Error(`Runtime mesh vertex ${index} is missing`);
291
+ return value;
292
+ };
293
+ const starts = new Uint32Array(data.f.length + 1);
294
+ data.f.forEach((face, i) => {
295
+ if (face.length < 3 || new Set(face).size !== face.length)
296
+ throw new Error('Runtime mesh face needs at least three distinct vertices');
297
+ starts[i + 1] = starts[i]! + face.length;
298
+ });
299
+ const cornerVerts = new Uint32Array(starts[data.f.length]!);
300
+ data.f.forEach((face, i) =>
301
+ face.forEach((v, j) => {
302
+ vertex(v);
303
+ cornerVerts[starts[i]! + j] = v;
304
+ }),
305
+ );
306
+ for (const [a, b] of data.edge_order ?? data.e ?? []) {
307
+ vertex(a);
308
+ vertex(b);
309
+ }
310
+ const counts: Record<string, number> = {
311
+ POINT: vertices.length,
312
+ EDGE: (data.edge_order ?? data.e ?? []).length,
313
+ FACE: data.f.length,
314
+ CORNER: cornerVerts.length,
315
+ };
316
+ for (const attribute of data.attributes ?? [])
317
+ if (attribute.domain !== 'EDGE' && attribute.data.length !== counts[attribute.domain])
318
+ throw new Error(
319
+ `Runtime attribute ${attribute.name} has ${attribute.data.length} values for ${counts[attribute.domain]} ${attribute.domain} elements`,
320
+ );
321
+ const maps = (data.attributes ?? []).filter((a) => a.type === 'FLOAT2' && a.domain === 'CORNER');
322
+ const mapName = data.render_uv ?? data.active_uv ?? maps[0]?.name;
323
+ const map = maps.find((a) => a.name === mapName);
324
+ if (mapName && !map) throw new Error(`Runtime UV map ${mapName} is missing`);
325
+ const uv = map?.data as [number, number][] | undefined;
326
+ const view: DrawView = {
327
+ vertexCount: vertices.length,
328
+ normals: customCornerNormals(
329
+ data,
330
+ data.attributes?.find((a) => a.name === 'custom_normal'),
331
+ ),
332
+ co: vertex,
333
+ starts,
334
+ cornerVerts,
335
+ smooth: (i) => Boolean(data.s?.[i]),
336
+ material: (i) => data.m?.[i] ?? 0,
337
+ edges: data.edge_order ?? data.e ?? null,
338
+ sharp: data.sharp ?? [],
339
+ uv: uv ? (corner) => uv[corner]! : null,
340
+ };
341
+ return geometryFromDrawArrays(drawCore(view));
342
+ }