@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.
- package/LICENSE +724 -0
- package/README.md +48 -0
- package/browser/blender-emscripten-engine.mts +289 -0
- package/browser/blender-engine.mts +412 -0
- package/browser/blender-wali-engine.mts +362 -0
- package/browser/index.ts +7 -0
- package/browser/protocol.ts +202 -0
- package/browser/rna.ts +697 -0
- package/browser/runtime.ts +511 -0
- package/browser/session-frame.mts +169 -0
- package/browser/session.py +4166 -0
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/attach-presenter.ts +140 -0
- package/browser/three/blender-agx.ts +235 -0
- package/browser/three/blender-base64.ts +42 -0
- package/browser/three/blender-corner-normals.ts +432 -0
- package/browser/three/blender-display-lut.ts +145 -0
- package/browser/three/blender-filmic.ts +49 -0
- package/browser/three/blender-frame-columns.ts +100 -0
- package/browser/three/blender-gradient-texture.ts +57 -0
- package/browser/three/blender-runtime-armature.ts +528 -0
- package/browser/three/blender-runtime-frame.ts +39 -0
- package/browser/three/blender-runtime-geometry.ts +342 -0
- package/browser/three/blender-runtime-lighting.ts +829 -0
- package/browser/three/blender-runtime-shadows.ts +107 -0
- package/browser/three/blender-runtime-view.ts +1481 -0
- package/browser/three/blender-runtime-volume.ts +128 -0
- package/browser/three/blender-runtime-weights.ts +306 -0
- package/browser/three/blender-sky.ts +461 -0
- package/browser/three/blender-standard.ts +68 -0
- package/browser/three/blender-triangulate.ts +181 -0
- package/browser/three/filmic-srgb.lut +0 -0
- package/browser/three/presenter.ts +265 -0
- package/browser/three/release.ts +27 -0
- package/browser/three/sky-precompute-worker.ts +45 -0
- package/browser/three/sky-worker.ts +79 -0
- package/browser/three/world-field-sampler.ts +358 -0
- package/browser/three/world-math.ts +59 -0
- package/browser/vgai_three.py +554 -0
- package/browser/worker.ts +648 -0
- package/package.json +48 -0
- package/wasm/BUNDLE.json +65 -0
- package/wasm/DEPENDENCY-LICENSES.txt +4879 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +2 -0
- 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
|
+
}
|