@driftengine/assets 4.6.1 → 4.7.1
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/README.md +1 -1
- package/dist/blend.d.ts +25 -0
- package/dist/blend.js +43 -0
- package/dist/blendAnimation.d.ts +45 -0
- package/dist/blendAnimation.js +294 -0
- package/dist/blendData.d.ts +114 -0
- package/dist/blendData.js +366 -0
- package/dist/blendFile.d.ts +69 -0
- package/dist/blendFile.js +182 -0
- package/dist/blendGeometry.d.ts +45 -0
- package/dist/blendGeometry.js +210 -0
- package/dist/blendGltf.d.ts +65 -0
- package/dist/blendGltf.js +545 -0
- package/dist/blendMaterial.d.ts +68 -0
- package/dist/blendMaterial.js +545 -0
- package/dist/blendMatrix.d.ts +28 -0
- package/dist/blendMatrix.js +245 -0
- package/dist/blendMesh.d.ts +70 -0
- package/dist/blendMesh.js +409 -0
- package/dist/blendModifiers.d.ts +25 -0
- package/dist/blendModifiers.js +83 -0
- package/dist/blendNormals.d.ts +44 -0
- package/dist/blendNormals.js +458 -0
- package/dist/blendPacked.d.ts +35 -0
- package/dist/blendPacked.js +82 -0
- package/dist/blendScene.d.ts +50 -0
- package/dist/blendScene.js +280 -0
- package/dist/gltf.js +8 -2
- package/dist/index.d.ts +21 -0
- package/dist/index.js +17 -0
- package/dist/readModel.d.ts +13 -1
- package/dist/readModel.js +43 -13
- package/package.json +4 -4
- package/src/blend.ts +57 -0
- package/src/blendAnimation.ts +328 -0
- package/src/blendData.ts +375 -0
- package/src/blendFile.ts +233 -0
- package/src/blendGeometry.ts +263 -0
- package/src/blendGltf.ts +621 -0
- package/src/blendMaterial.ts +676 -0
- package/src/blendMatrix.ts +257 -0
- package/src/blendMesh.ts +496 -0
- package/src/blendModifiers.ts +96 -0
- package/src/blendNormals.ts +481 -0
- package/src/blendPacked.ts +93 -0
- package/src/blendScene.ts +307 -0
- package/src/gltf.ts +14 -2
- package/src/index.ts +41 -0
- package/src/readModel.ts +58 -13
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `.blend` mesh as triangle primitives, one per material slot, in glTF's conventions.
|
|
3
|
+
*
|
|
4
|
+
* **The same conversion serves every route out of a `.blend`.** `blendGltf.ts` writes these into a
|
|
5
|
+
* glTF document for `readModel`, and a bake that keeps a world's instancing calls this once per
|
|
6
|
+
* distinct mesh rather than once per object. So what a corner becomes — its position turned Y up,
|
|
7
|
+
* its UV flipped to glTF's origin, its colour linear — is decided here and nowhere else.
|
|
8
|
+
*
|
|
9
|
+
* **Vertices are shared where every attribute agrees exactly**, which is how Blender's exporter
|
|
10
|
+
* builds them, found by walking the exported copies of each source vertex rather than by hashing,
|
|
11
|
+
* so a fifty-million-corner city costs a scan rather than fifty million string keys.
|
|
12
|
+
*
|
|
13
|
+
* **Faces are triangulated as Blender does it**: a quad split from its first corner, an ngon by ear
|
|
14
|
+
* clipping in its own plane. Blender clips ears in a different order, so an ngon's diagonals can
|
|
15
|
+
* differ from Blender's; on a planar face that changes no pixel, on a bent one it changes the bend.
|
|
16
|
+
*/
|
|
17
|
+
import { POINT } from './blendMesh.js';
|
|
18
|
+
import { faceNormals } from './blendNormals.js';
|
|
19
|
+
/** Blender Z up to glTF Y up: (x, y, z) becomes (x, z, -y). */
|
|
20
|
+
export function toYUp(out, at, x, y, z) {
|
|
21
|
+
out[at] = x;
|
|
22
|
+
out[at + 1] = z;
|
|
23
|
+
out[at + 2] = -y;
|
|
24
|
+
}
|
|
25
|
+
/** Triangles of one face, as corner indices, appended to `out`. */
|
|
26
|
+
export function triangulateFace(mesh, face, normal, out) {
|
|
27
|
+
const start = mesh.faceOffsets[face];
|
|
28
|
+
const end = mesh.faceOffsets[face + 1];
|
|
29
|
+
const n = end - start;
|
|
30
|
+
if (n < 3)
|
|
31
|
+
return;
|
|
32
|
+
if (n === 3) {
|
|
33
|
+
out.push(start, start + 1, start + 2);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (n === 4) {
|
|
37
|
+
out.push(start, start + 1, start + 2, start, start + 2, start + 3);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
/* Project onto the face's plane, then clip ears. */
|
|
41
|
+
const [nx, ny, nz] = normal;
|
|
42
|
+
const ax = Math.abs(nx);
|
|
43
|
+
const ay = Math.abs(ny);
|
|
44
|
+
const az = Math.abs(nz);
|
|
45
|
+
const drop = az >= ax && az >= ay ? 2 : ax >= ay ? 0 : 1;
|
|
46
|
+
const flip = (drop === 2 ? nz : drop === 0 ? nx : ny) < 0;
|
|
47
|
+
const p = mesh.positions;
|
|
48
|
+
const xs = [];
|
|
49
|
+
const ys = [];
|
|
50
|
+
for (let c = start; c < end; c++) {
|
|
51
|
+
const v = mesh.cornerVerts[c] * 3;
|
|
52
|
+
/* The two axes left after dropping one, in right-handed order: (y, z), (z, x) or (x, y). */
|
|
53
|
+
const a = p[v + ((drop + 1) % 3)];
|
|
54
|
+
const b = p[v + ((drop + 2) % 3)];
|
|
55
|
+
xs.push(flip ? b : a);
|
|
56
|
+
ys.push(flip ? a : b);
|
|
57
|
+
}
|
|
58
|
+
const ring = Array.from({ length: n }, (_, i) => i);
|
|
59
|
+
const cross = (i, j, k) => (xs[j] - xs[i]) * (ys[k] - ys[i]) -
|
|
60
|
+
(ys[j] - ys[i]) * (xs[k] - xs[i]);
|
|
61
|
+
const inside = (i, j, k, q) => cross(i, j, q) >= 0 && cross(j, k, q) >= 0 && cross(k, i, q) >= 0;
|
|
62
|
+
let guard = 0;
|
|
63
|
+
while (ring.length > 3 && guard++ < n * n) {
|
|
64
|
+
let clipped = false;
|
|
65
|
+
for (let r = 0; r < ring.length; r++) {
|
|
66
|
+
const i = ring[(r + ring.length - 1) % ring.length];
|
|
67
|
+
const j = ring[r];
|
|
68
|
+
const k = ring[(r + 1) % ring.length];
|
|
69
|
+
if (cross(i, j, k) <= 0)
|
|
70
|
+
continue;
|
|
71
|
+
let blocked = false;
|
|
72
|
+
for (const q of ring) {
|
|
73
|
+
if (q !== i && q !== j && q !== k && inside(i, j, k, q)) {
|
|
74
|
+
blocked = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (blocked)
|
|
79
|
+
continue;
|
|
80
|
+
out.push(start + i, start + j, start + k);
|
|
81
|
+
ring.splice(r, 1);
|
|
82
|
+
clipped = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
/* A face too bent or degenerate to clip: fan the rest, which is what remains drawable. */
|
|
86
|
+
if (!clipped)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
for (let r = 1; r + 1 < ring.length; r++)
|
|
90
|
+
out.push(start + ring[0], start + ring[r], start + ring[r + 1]);
|
|
91
|
+
}
|
|
92
|
+
/** Split a mesh into one primitive per material slot it uses. */
|
|
93
|
+
export function meshPrimitives(mesh, normals, slots, needs) {
|
|
94
|
+
const faces = mesh.faceOffsets.length - 1;
|
|
95
|
+
const face = faceNormals(mesh);
|
|
96
|
+
const bySlot = new Map();
|
|
97
|
+
for (let f = 0; f < faces; f++) {
|
|
98
|
+
const raw = mesh.materialIndex === null ? 0 : mesh.materialIndex[f];
|
|
99
|
+
const slot = slots === 0 ? -1 : Math.max(0, Math.min(slots - 1, raw));
|
|
100
|
+
let list = bySlot.get(slot);
|
|
101
|
+
if (list === undefined)
|
|
102
|
+
bySlot.set(slot, (list = []));
|
|
103
|
+
list.push(f);
|
|
104
|
+
}
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const slot of [...bySlot.keys()].sort((a, b) => a - b)) {
|
|
107
|
+
const need = slot < 0 ? { vertexColor: null, uvMap: null, uvTransform: null } : needs(slot);
|
|
108
|
+
const corners = [];
|
|
109
|
+
for (const f of bySlot.get(slot))
|
|
110
|
+
triangulateFace(mesh, f, [face[f * 3], face[f * 3 + 1], face[f * 3 + 2]], corners);
|
|
111
|
+
out.push(build(mesh, normals, slot, need, corners));
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function uvLayer(mesh, name) {
|
|
116
|
+
if (mesh.uvs.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const found = name === null ? -1 : mesh.uvs.findIndex((u) => u.name === name);
|
|
119
|
+
return (mesh.uvs[found >= 0 ? found : mesh.renderUv] ?? mesh.uvs[0])?.uv ?? null;
|
|
120
|
+
}
|
|
121
|
+
function colorLayer(mesh, name) {
|
|
122
|
+
if (name === null || mesh.colors.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
const found = name === '' ? -1 : mesh.colors.findIndex((c) => c.name === name);
|
|
125
|
+
return mesh.colors[found >= 0 ? found : mesh.renderColor] ?? null;
|
|
126
|
+
}
|
|
127
|
+
function build(mesh, normals, slot, need, corners) {
|
|
128
|
+
const uv = uvLayer(mesh, need.uvMap);
|
|
129
|
+
const color = colorLayer(mesh, need.vertexColor);
|
|
130
|
+
const t = need.uvTransform;
|
|
131
|
+
const verts = mesh.positions.length / 3;
|
|
132
|
+
/* Exported copies of each source vertex, as a chain: head per vertex, next per copy. */
|
|
133
|
+
const head = new Int32Array(verts).fill(-1);
|
|
134
|
+
const next = [];
|
|
135
|
+
const corner = [];
|
|
136
|
+
const source = [];
|
|
137
|
+
const indices = new Uint32Array(corners.length);
|
|
138
|
+
const same = (a, b) => {
|
|
139
|
+
for (let k = 0; k < 3; k++)
|
|
140
|
+
if (normals[a * 3 + k] !== normals[b * 3 + k])
|
|
141
|
+
return false;
|
|
142
|
+
if (uv !== null)
|
|
143
|
+
for (let k = 0; k < 2; k++)
|
|
144
|
+
if (uv[a * 2 + k] !== uv[b * 2 + k])
|
|
145
|
+
return false;
|
|
146
|
+
if (color !== null && color.domain !== POINT)
|
|
147
|
+
for (let k = 0; k < 4; k++)
|
|
148
|
+
if (color.rgba[a * 4 + k] !== color.rgba[b * 4 + k])
|
|
149
|
+
return false;
|
|
150
|
+
return true;
|
|
151
|
+
};
|
|
152
|
+
for (let i = 0; i < corners.length; i++) {
|
|
153
|
+
const c = corners[i];
|
|
154
|
+
const v = mesh.cornerVerts[c];
|
|
155
|
+
let found = -1;
|
|
156
|
+
for (let e = head[v]; e >= 0; e = next[e]) {
|
|
157
|
+
if (same(corner[e], c)) {
|
|
158
|
+
found = e;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (found < 0) {
|
|
163
|
+
found = corner.length;
|
|
164
|
+
corner.push(c);
|
|
165
|
+
source.push(v);
|
|
166
|
+
next.push(head[v]);
|
|
167
|
+
head[v] = found;
|
|
168
|
+
}
|
|
169
|
+
indices[i] = found;
|
|
170
|
+
}
|
|
171
|
+
const count = corner.length;
|
|
172
|
+
const positions = new Float32Array(count * 3);
|
|
173
|
+
const outNormals = new Float32Array(count * 3);
|
|
174
|
+
const uvs = uv === null ? null : new Float32Array(count * 2);
|
|
175
|
+
const colors = color === null ? null : new Float32Array(count * 4);
|
|
176
|
+
const p = mesh.positions;
|
|
177
|
+
for (let e = 0; e < count; e++) {
|
|
178
|
+
const c = corner[e];
|
|
179
|
+
const v = source[e];
|
|
180
|
+
toYUp(positions, e * 3, p[v * 3], p[v * 3 + 1], p[v * 3 + 2]);
|
|
181
|
+
toYUp(outNormals, e * 3, normals[c * 3], normals[c * 3 + 1], normals[c * 3 + 2]);
|
|
182
|
+
if (uvs !== null) {
|
|
183
|
+
let u = uv?.[c * 2];
|
|
184
|
+
let w = uv?.[c * 2 + 1];
|
|
185
|
+
if (t !== null) {
|
|
186
|
+
const tu = t[0] * u + t[3] * w + t[6];
|
|
187
|
+
const tv = t[1] * u + t[4] * w + t[7];
|
|
188
|
+
u = tu;
|
|
189
|
+
w = tv;
|
|
190
|
+
}
|
|
191
|
+
uvs[e * 2] = u;
|
|
192
|
+
uvs[e * 2 + 1] = 1 - w;
|
|
193
|
+
}
|
|
194
|
+
if (colors !== null && color !== null) {
|
|
195
|
+
const at = color.domain === POINT ? v : c;
|
|
196
|
+
for (let k = 0; k < 4; k++)
|
|
197
|
+
colors[e * 4 + k] = color.rgba[at * 4 + k];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
slot,
|
|
202
|
+
positions,
|
|
203
|
+
normals: outNormals,
|
|
204
|
+
uvs,
|
|
205
|
+
colors,
|
|
206
|
+
indices,
|
|
207
|
+
sourceVertex: Int32Array.from(source),
|
|
208
|
+
sourceCorner: Int32Array.from(corner),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `.blend` as a glTF document in memory, so that `readModel` reads it through the glTF reader.
|
|
3
|
+
*
|
|
4
|
+
* **Why glTF in the middle.** Blender's own exporter writes glTF, and the baker hands a file it
|
|
5
|
+
* cannot read directly to that exporter. Building the same document here means the two routes share
|
|
6
|
+
* every step after it — materials, textures, skins, lights, the flattening into world space — and
|
|
7
|
+
* so cannot drift apart on any of them. It also makes Blender's export of a fixture an independent
|
|
8
|
+
* check on this one: the two documents should say the same thing.
|
|
9
|
+
*
|
|
10
|
+
* **What it refuses** it lists in `refusals`, and `readBlend` declines the whole file: a modifier,
|
|
11
|
+
* a constraint, a rig, a curve. Half a scene is worse than an error, because nobody can see what is
|
|
12
|
+
* missing. **What it approximates** — a node graph glTF cannot hold, animation it does not read —
|
|
13
|
+
* it lists in `approximations` and draws anyway, as Blender's exporter would.
|
|
14
|
+
*/
|
|
15
|
+
import type { BlendData, BlendStruct } from './blendData.ts';
|
|
16
|
+
import type { GltfDocument } from './gltf.ts';
|
|
17
|
+
export interface BlendGltf {
|
|
18
|
+
readonly doc: GltfDocument;
|
|
19
|
+
readonly binary: Uint8Array;
|
|
20
|
+
/** What only Blender can evaluate. `readBlend` refuses the file over any of these. */
|
|
21
|
+
readonly refusals: string[];
|
|
22
|
+
/** What Blender's own exporter would do better: animation, a channel glTF needs repacked. */
|
|
23
|
+
readonly approximations: string[];
|
|
24
|
+
/** What glTF cannot hold by either route, said once each. */
|
|
25
|
+
readonly warnings: string[];
|
|
26
|
+
readonly notes: string[];
|
|
27
|
+
}
|
|
28
|
+
/** The bytes a packed image holds, or null where it lives in a file beside the `.blend`. */
|
|
29
|
+
export declare function packedImage(image: BlendStruct): Uint8Array | null;
|
|
30
|
+
/**
|
|
31
|
+
* Whether an image stores one channel: a grey PNG, or a JPEG of one component. An image that is not
|
|
32
|
+
* packed cannot be looked at here and is taken to be grey, which is what a scalar map usually is.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isGreyImage(image: BlendStruct): boolean;
|
|
35
|
+
/** A packed image's media type, from its first bytes. */
|
|
36
|
+
export declare function imageMime(bytes: Uint8Array): string | null;
|
|
37
|
+
/** Where an unpacked image lives, relative to the `.blend` when Blender wrote it that way. */
|
|
38
|
+
export declare function imagePath(image: BlendStruct): string;
|
|
39
|
+
/** What `blendToGltf` reads, where the whole open scene is not wanted. */
|
|
40
|
+
export interface BlendGltfOptions {
|
|
41
|
+
/**
|
|
42
|
+
* One object alone at the origin rather than the scene: a bake that keeps a world's instancing
|
|
43
|
+
* converts each distinct mesh once this way and places the copies itself.
|
|
44
|
+
*/
|
|
45
|
+
readonly only?: BlendStruct;
|
|
46
|
+
/**
|
|
47
|
+
* Name each image by `imageUri` rather than carrying its bytes. A bake converting thousands of
|
|
48
|
+
* objects that share a few thousand images reads each image once, itself, rather than once a
|
|
49
|
+
* document.
|
|
50
|
+
*/
|
|
51
|
+
readonly imageUri?: (image: BlendStruct) => string;
|
|
52
|
+
}
|
|
53
|
+
/** Build the document. */
|
|
54
|
+
export declare function blendToGltf(blend: BlendData, options?: BlendGltfOptions): BlendGltf;
|
|
55
|
+
/** An object's material slots: the object's own material where its slot says so, else the mesh's. */
|
|
56
|
+
export declare function materialSlots(object: BlendStruct, mesh: BlendStruct): (BlendStruct | null)[];
|
|
57
|
+
/**
|
|
58
|
+
* The colour of a black body at `kelvin`, in linear Rec. 709, scaled to unit luminance: what
|
|
59
|
+
* Blender multiplies a light's colour by when the light takes its colour from a temperature.
|
|
60
|
+
*
|
|
61
|
+
* Planck's law through the CIE 1931 observer, by the multi-lobe fit of Wyman, Sloan and Shirley.
|
|
62
|
+
* **Within 1% of Blender's table, not equal to it**: Blender's is fitted per range to its own colour
|
|
63
|
+
* configuration, and at 3000 K reads (1.771, 0.844, 0.272) where this reads (1.752, 0.850, 0.271).
|
|
64
|
+
*/
|
|
65
|
+
export declare function blackbody(kelvin: number): [number, number, number];
|