@driftengine/assets 4.6.1 → 4.7.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/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 +11 -0
- package/dist/readModel.js +29 -0
- 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 +44 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which objects of a `.blend` are drawn, and where: the scene walk, in Blender's coordinates.
|
|
3
|
+
*
|
|
4
|
+
* **"Drawn" means what a render of the open scene shows**, the same rule `scripts/blender/
|
|
5
|
+
* export_gltf.py` applies before it hands the scene to Blender's exporter, so both routes into
|
|
6
|
+
* the engine agree on what an asset contains. An object is drawn when it is linked into the scene
|
|
7
|
+
* through at least one collection that is neither excluded from the view layer nor disabled for
|
|
8
|
+
* rendering, and is not itself hidden from renders. Parenting does not hide: a child of a hidden
|
|
9
|
+
* object is drawn where its parent would have put it, which is what Blender does.
|
|
10
|
+
*
|
|
11
|
+
* **A collection instance is expanded** into its objects, placed by the instancer and shifted by
|
|
12
|
+
* the collection's instance offset. Before 2.8 there were no collections, and the scene's bases and
|
|
13
|
+
* layer bits say the same thing.
|
|
14
|
+
*
|
|
15
|
+
* **What has to be evaluated is refused rather than guessed.** A constraint, a vertex parent or a
|
|
16
|
+
* particle instancer changes where things are by running Blender, so the walk names it in
|
|
17
|
+
* `refusals` and the caller declines the whole file. Animation does not refuse: Blender writes the
|
|
18
|
+
* evaluated value of every animated property back to the datablock, so a saved file holds the
|
|
19
|
+
* pose of the frame it was saved on.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { BlendData, BlendStruct } from './blendData.ts';
|
|
23
|
+
import type { Mat4 } from './blendMatrix.ts';
|
|
24
|
+
import {
|
|
25
|
+
axisAngleMatrix,
|
|
26
|
+
compose,
|
|
27
|
+
eulerMatrix,
|
|
28
|
+
multiply,
|
|
29
|
+
multiply3,
|
|
30
|
+
quaternionMatrix,
|
|
31
|
+
translation,
|
|
32
|
+
} from './blendMatrix.ts';
|
|
33
|
+
|
|
34
|
+
/** One drawn object, or an empty that carries others, at its place in the world. */
|
|
35
|
+
export interface BlendPlaced {
|
|
36
|
+
readonly object: BlendStruct;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
/** Blender's object type: 0 empty, 1 mesh, 10 light, 25 armature, and the rest. */
|
|
39
|
+
readonly kind: number;
|
|
40
|
+
/** World matrix, Blender space (Z up), column-major. */
|
|
41
|
+
readonly world: Mat4;
|
|
42
|
+
/** Index of the nearest placed ancestor, or -1. */
|
|
43
|
+
readonly parent: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const OB_EMPTY = 0;
|
|
47
|
+
export const OB_MESH = 1;
|
|
48
|
+
export const OB_LAMP = 10;
|
|
49
|
+
export const OB_CAMERA = 11;
|
|
50
|
+
export const OB_ARMATURE = 25;
|
|
51
|
+
|
|
52
|
+
/** Object types that draw nothing and so need nothing evaluated: cameras, speakers, probes. */
|
|
53
|
+
const INERT = new Set([OB_CAMERA, 12, 13, 22]);
|
|
54
|
+
const NEEDS_EVALUATION: Record<number, string> = {
|
|
55
|
+
2: 'a curve',
|
|
56
|
+
3: 'a NURBS surface',
|
|
57
|
+
4: 'a text object',
|
|
58
|
+
5: 'a metaball',
|
|
59
|
+
26: 'a grease pencil object',
|
|
60
|
+
27: 'a hair curves object',
|
|
61
|
+
28: 'a point cloud',
|
|
62
|
+
29: 'a volume',
|
|
63
|
+
30: 'a grease pencil object',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const OB_HIDE_RENDER = 1 << 2;
|
|
67
|
+
const COLLECTION_HIDE_RENDER = 1 << 3;
|
|
68
|
+
const LAYER_COLLECTION_EXCLUDE = 1 << 4;
|
|
69
|
+
const OB_DUPLICOLLECTION = 1 << 8;
|
|
70
|
+
const OB_DUPLIVERTS = 1 << 4;
|
|
71
|
+
const OB_DUPLIFACES = 1 << 9;
|
|
72
|
+
const PARTYPE = 15;
|
|
73
|
+
const PAROBJECT = 0;
|
|
74
|
+
const PARSKEL = 4;
|
|
75
|
+
const PARBONE = 7;
|
|
76
|
+
|
|
77
|
+
/** The scene the file was saved looking at. */
|
|
78
|
+
export function activeScene(blend: BlendData): BlendStruct | null {
|
|
79
|
+
const glob = blend.idsOf('GLOB')[0];
|
|
80
|
+
const scene = glob?.has('curscene') === true ? glob.deref('curscene') : null;
|
|
81
|
+
return scene ?? blend.idsOf('SC')[0] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The object's transform relative to its parent: location, rotation and scale, deltas folded in.
|
|
86
|
+
*
|
|
87
|
+
* `values`, where given, overrides what the file stores with what an animation says at a frame,
|
|
88
|
+
* keyed by Blender's data path and index — `location[0]`, `rotation_quaternion[3]` — which is what
|
|
89
|
+
* `Animation.values` fills (`blendAnimation.ts`).
|
|
90
|
+
*/
|
|
91
|
+
export function localMatrix(object: BlendStruct, values?: ReadonlyMap<string, number>): Mat4 {
|
|
92
|
+
const read = (field: string, path: string, count: number, offset = 0): number[] => {
|
|
93
|
+
const stored = object.has(field)
|
|
94
|
+
? object.floats(field)
|
|
95
|
+
: new Array<number>(count).fill(field.includes('size') || field.includes('scale') ? 1 : 0);
|
|
96
|
+
if (values === undefined) return stored;
|
|
97
|
+
return stored.map((v, i) => values.get(`${path}[${i + offset}]`) ?? v);
|
|
98
|
+
};
|
|
99
|
+
const location = read('loc', 'location', 3);
|
|
100
|
+
const deltaLocation = read('dloc', 'delta_location', 3);
|
|
101
|
+
const scale = read('size', 'scale', 3);
|
|
102
|
+
const deltaScale = object.has('dscale')
|
|
103
|
+
? read('dscale', 'delta_scale', 3)
|
|
104
|
+
: read('dsize', 'delta_scale', 3);
|
|
105
|
+
const mode = object.int('rotmode');
|
|
106
|
+
let rotation;
|
|
107
|
+
let delta;
|
|
108
|
+
if (mode === 0) {
|
|
109
|
+
rotation = quaternionMatrix(read('quat', 'rotation_quaternion', 4));
|
|
110
|
+
delta = quaternionMatrix(read('dquat', 'delta_rotation_quaternion', 4));
|
|
111
|
+
} else if (mode === -1) {
|
|
112
|
+
/* Blender keys an axis-angle as four values, the angle first. */
|
|
113
|
+
const angle = values?.get('rotation_axis_angle[0]') ?? object.float('rotAngle');
|
|
114
|
+
rotation = axisAngleMatrix(read('rotAxis', 'rotation_axis_angle', 3, 1), angle);
|
|
115
|
+
delta = axisAngleMatrix(object.floats('drotAxis'), object.float('drotAngle'));
|
|
116
|
+
} else {
|
|
117
|
+
rotation = eulerMatrix(read('rot', 'rotation_euler', 3), mode);
|
|
118
|
+
delta = eulerMatrix(read('drot', 'delta_rotation_euler', 3), mode);
|
|
119
|
+
}
|
|
120
|
+
return compose(
|
|
121
|
+
[0, 1, 2].map((i) => (location[i] as number) + (deltaLocation[i] as number)),
|
|
122
|
+
multiply3(delta, rotation),
|
|
123
|
+
[0, 1, 2].map((i) => (scale[i] as number) * (deltaScale[i] as number)),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Walk the scene into placed objects, naming in `refusals` anything that needs Blender to run. */
|
|
128
|
+
export function placeObjects(
|
|
129
|
+
blend: BlendData,
|
|
130
|
+
refusals: string[],
|
|
131
|
+
bonePose: (armature: BlendStruct, bone: string) => Mat4 | null,
|
|
132
|
+
): BlendPlaced[] {
|
|
133
|
+
const scene = activeScene(blend);
|
|
134
|
+
if (scene === null) {
|
|
135
|
+
refusals.push('the file has no scene');
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const worlds = new Map<number, Mat4>();
|
|
139
|
+
const world = (object: BlendStruct, depth = 0): Mat4 => {
|
|
140
|
+
const cached = worlds.get(object.offset);
|
|
141
|
+
if (cached !== undefined) return cached;
|
|
142
|
+
let matrix = localMatrix(object);
|
|
143
|
+
const parent = depth < 64 ? object.deref('parent') : null;
|
|
144
|
+
if (parent !== null) {
|
|
145
|
+
const type = object.int('partype') & PARTYPE;
|
|
146
|
+
let base = world(parent, depth + 1);
|
|
147
|
+
if (type === PARBONE) {
|
|
148
|
+
const pose = bonePose(parent, object.string('parsubstr'));
|
|
149
|
+
if (pose === null)
|
|
150
|
+
refusals.push(`"${object.idName()}" is parented to a bone that is not there`);
|
|
151
|
+
else base = multiply(base, pose);
|
|
152
|
+
} else if (type !== PAROBJECT && type !== PARSKEL) {
|
|
153
|
+
refusals.push(`"${object.idName()}" is parented to vertices, which Blender evaluates`);
|
|
154
|
+
}
|
|
155
|
+
matrix = multiply(base, multiply(object.floats('parentinv'), matrix));
|
|
156
|
+
}
|
|
157
|
+
if (object.list('constraints').length > 0) {
|
|
158
|
+
refusals.push(`"${object.idName()}" has constraints, which Blender evaluates`);
|
|
159
|
+
}
|
|
160
|
+
worlds.set(object.offset, matrix);
|
|
161
|
+
return matrix;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const placed: BlendPlaced[] = [];
|
|
165
|
+
const index = new Map<number, number>();
|
|
166
|
+
|
|
167
|
+
const place = (object: BlendStruct, outer: Mat4 | null, holder: number, depth: number): void => {
|
|
168
|
+
const kind = object.int('type');
|
|
169
|
+
if (INERT.has(kind)) return;
|
|
170
|
+
const reason = NEEDS_EVALUATION[kind];
|
|
171
|
+
if (reason !== undefined)
|
|
172
|
+
refusals.push(`"${object.idName()}" is ${reason}, which Blender evaluates`);
|
|
173
|
+
if (object.list('particlesystem').length > 0) {
|
|
174
|
+
refusals.push(`"${object.idName()}" has a particle system, which Blender evaluates`);
|
|
175
|
+
}
|
|
176
|
+
const transflag = object.int('transflag');
|
|
177
|
+
if (transflag & (OB_DUPLIVERTS | OB_DUPLIFACES)) {
|
|
178
|
+
refusals.push(
|
|
179
|
+
`"${object.idName()}" instances onto its vertices or faces, which Blender evaluates`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const own = world(object);
|
|
183
|
+
const matrix = outer === null ? own : multiply(outer, own);
|
|
184
|
+
let parent = holder;
|
|
185
|
+
for (let up = object.deref('parent'), hops = 0; up !== null && hops < 64; hops++) {
|
|
186
|
+
const found = outer === null ? index.get(up.offset) : undefined;
|
|
187
|
+
if (found !== undefined) {
|
|
188
|
+
parent = found;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
up = up.deref('parent');
|
|
192
|
+
}
|
|
193
|
+
/*
|
|
194
|
+
* An instancer carries its collection's offset in its own matrix, so its members sit at their
|
|
195
|
+
* own world matrices beneath it. Blender's exporter writes the instancer the same way, which
|
|
196
|
+
* keeps the two node trees comparable; the empty draws nothing, so nothing moves on screen.
|
|
197
|
+
*/
|
|
198
|
+
const collection = transflag & OB_DUPLICOLLECTION ? object.deref('dup_group') : null;
|
|
199
|
+
const offset = collection?.floats('dupli_ofs') ?? [0, 0, 0];
|
|
200
|
+
const held =
|
|
201
|
+
collection === null
|
|
202
|
+
? matrix
|
|
203
|
+
: multiply(
|
|
204
|
+
matrix,
|
|
205
|
+
translation(-(offset[0] as number), -(offset[1] as number), -(offset[2] as number)),
|
|
206
|
+
);
|
|
207
|
+
const at = placed.length;
|
|
208
|
+
placed.push({ object, name: object.idName(), kind, world: held, parent });
|
|
209
|
+
if (outer === null) index.set(object.offset, at);
|
|
210
|
+
if (collection !== null && depth < 16) {
|
|
211
|
+
for (const member of collectionObjects(collection, true)) place(member, held, at, depth + 1);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const roots = sceneObjects(scene, blend);
|
|
216
|
+
/* Parents first, so a child finds its parent's index already assigned. */
|
|
217
|
+
const order = [...roots].sort((a, b) => depthOf(a) - depthOf(b));
|
|
218
|
+
for (const object of order) place(object, null, -1, 0);
|
|
219
|
+
return placed;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function depthOf(object: BlendStruct): number {
|
|
223
|
+
let depth = 0;
|
|
224
|
+
for (let up = object.deref('parent'); up !== null && depth < 64; up = up.deref('parent')) depth++;
|
|
225
|
+
return depth;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The objects a collection holds directly and through its children, those that render. */
|
|
229
|
+
function collectionObjects(collection: BlendStruct, nested: boolean): BlendStruct[] {
|
|
230
|
+
const out: BlendStruct[] = [];
|
|
231
|
+
const seen = new Set<number>();
|
|
232
|
+
const visit = (current: BlendStruct, depth: number): void => {
|
|
233
|
+
for (const link of current.list('gobject')) {
|
|
234
|
+
const object = link.deref('ob');
|
|
235
|
+
if (object === null || seen.has(object.offset)) continue;
|
|
236
|
+
seen.add(object.offset);
|
|
237
|
+
if ((object.int('restrictflag') & OB_HIDE_RENDER) === 0) out.push(object);
|
|
238
|
+
}
|
|
239
|
+
if (!nested || depth > 32) return;
|
|
240
|
+
for (const child of current.list('children')) {
|
|
241
|
+
const inner = child.deref('collection');
|
|
242
|
+
if (inner !== null && (inner.int('flag') & COLLECTION_HIDE_RENDER) === 0)
|
|
243
|
+
visit(inner, depth + 1);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
visit(collection, 0);
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Every object the scene renders, once each. */
|
|
251
|
+
function sceneObjects(scene: BlendStruct, blend: BlendData): BlendStruct[] {
|
|
252
|
+
if (!scene.has('master_collection')) {
|
|
253
|
+
/* Before 2.8: bases on the scene, drawn when one of their layers is a visible scene layer. */
|
|
254
|
+
const layers = scene.int('lay');
|
|
255
|
+
return scene
|
|
256
|
+
.list('base')
|
|
257
|
+
.filter((base) => (base.int('lay') & layers) !== 0)
|
|
258
|
+
.map((base) => base.deref('object'))
|
|
259
|
+
.filter((object): object is BlendStruct => object !== null)
|
|
260
|
+
.filter((object) => (object.int('restrictflag') & OB_HIDE_RENDER) === 0);
|
|
261
|
+
}
|
|
262
|
+
const master = scene.deref('master_collection');
|
|
263
|
+
if (master === null) return [];
|
|
264
|
+
const glob = blend.idsOf('GLOB')[0];
|
|
265
|
+
const layers = scene.list('view_layers');
|
|
266
|
+
const active =
|
|
267
|
+
glob?.has('cur_view_layer') === true
|
|
268
|
+
? layers.find((layer) => layer.offset === glob.deref('cur_view_layer')?.offset)
|
|
269
|
+
: undefined;
|
|
270
|
+
const layer = active ?? layers[0];
|
|
271
|
+
const top = layer?.list('layer_collections')[0];
|
|
272
|
+
|
|
273
|
+
const out: BlendStruct[] = [];
|
|
274
|
+
const seen = new Set<number>();
|
|
275
|
+
const take = (collection: BlendStruct): void => {
|
|
276
|
+
for (const link of collection.list('gobject')) {
|
|
277
|
+
const object = link.deref('ob');
|
|
278
|
+
if (object === null || seen.has(object.offset)) continue;
|
|
279
|
+
if ((object.int('restrictflag') & OB_HIDE_RENDER) !== 0) continue;
|
|
280
|
+
seen.add(object.offset);
|
|
281
|
+
out.push(object);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/*
|
|
285
|
+
* Walk the layer collections where the view layer has them, since exclusion lives there; walk the
|
|
286
|
+
* collections themselves where it does not, which is a file written without a synchronised view
|
|
287
|
+
* layer and has nothing excluded.
|
|
288
|
+
*/
|
|
289
|
+
const walkLayer = (lc: BlendStruct, depth: number): void => {
|
|
290
|
+
if (depth > 64 || (lc.int('flag') & LAYER_COLLECTION_EXCLUDE) !== 0) return;
|
|
291
|
+
const collection = lc.deref('collection');
|
|
292
|
+
if (collection === null || (collection.int('flag') & COLLECTION_HIDE_RENDER) !== 0) return;
|
|
293
|
+
take(collection);
|
|
294
|
+
for (const child of lc.list('layer_collections')) walkLayer(child, depth + 1);
|
|
295
|
+
};
|
|
296
|
+
const walkCollection = (collection: BlendStruct, depth: number): void => {
|
|
297
|
+
if (depth > 64 || (collection.int('flag') & COLLECTION_HIDE_RENDER) !== 0) return;
|
|
298
|
+
take(collection);
|
|
299
|
+
for (const child of collection.list('children')) {
|
|
300
|
+
const inner = child.deref('collection');
|
|
301
|
+
if (inner !== null) walkCollection(inner, depth + 1);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
if (top !== undefined) walkLayer(top, 0);
|
|
305
|
+
else walkCollection(master, 0);
|
|
306
|
+
return out;
|
|
307
|
+
}
|
package/src/gltf.ts
CHANGED
|
@@ -692,8 +692,20 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
|
|
|
692
692
|
cutout: masked ? Math.min(1, Math.max(0, material?.alphaCutoff ?? 0.5)) : 0,
|
|
693
693
|
/* Metalness is glTF's statement about reflection: a metal mirrors its surroundings and
|
|
694
694
|
a dielectric mostly does not. It also feeds `specular`, which is not double counting,
|
|
695
|
-
because the two describe different halves of the same fact.
|
|
696
|
-
|
|
695
|
+
because the two describe different halves of the same fact.
|
|
696
|
+
|
|
697
|
+
**An absent factor is 1 here as it is everywhere else in glTF**, with the decal guard
|
|
698
|
+
`highlight` applies above: an image-coloured surface naming no metallic stays a dielectric.
|
|
699
|
+
It read 0 for every surface until a Blender export showed it, because Blender writes no
|
|
700
|
+
`metallicFactor` for a metal — 1 is the default — so every untextured metal it exported
|
|
701
|
+
arrived taking a highlight and mirroring nothing. */
|
|
702
|
+
reflectivity: Math.min(
|
|
703
|
+
1,
|
|
704
|
+
Math.max(
|
|
705
|
+
0,
|
|
706
|
+
modulated ? 0 : pbr?.baseColorTexture === undefined ? metallic : (pbr?.metallicFactor ?? 0),
|
|
707
|
+
),
|
|
708
|
+
),
|
|
697
709
|
normalMap: material?.normalTexture?.index ?? -1,
|
|
698
710
|
ormMap,
|
|
699
711
|
emissiveMap: material?.emissiveTexture?.index ?? -1,
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,47 @@ export type { Inflate } from './fbx.ts';
|
|
|
91
91
|
* decompressors and the resolution of a name a file states are all the caller's to supply.
|
|
92
92
|
*/
|
|
93
93
|
export { readModel, readerFor, extensionOf, MODEL_FORMATS } from './readModel.ts';
|
|
94
|
+
/*
|
|
95
|
+
* Blender's own format, read directly. `readModel` takes a `.blend` like any other model; the rest
|
|
96
|
+
* is for a bake that keeps a scene's structure — its instancing, its materials' channels — rather
|
|
97
|
+
* than flattening it into world-space meshes.
|
|
98
|
+
*/
|
|
99
|
+
export { BlendNeedsBlender, openBlend, readBlend } from './blend.ts';
|
|
100
|
+
export { BlendData, BlendStruct } from './blendData.ts';
|
|
101
|
+
export {
|
|
102
|
+
blendToGltf,
|
|
103
|
+
blackbody,
|
|
104
|
+
imageMime,
|
|
105
|
+
imagePath,
|
|
106
|
+
materialSlots,
|
|
107
|
+
packedImage,
|
|
108
|
+
} from './blendGltf.ts';
|
|
109
|
+
export type { BlendGltf } from './blendGltf.ts';
|
|
110
|
+
export {
|
|
111
|
+
placeObjects,
|
|
112
|
+
localMatrix,
|
|
113
|
+
activeScene,
|
|
114
|
+
OB_ARMATURE,
|
|
115
|
+
OB_EMPTY,
|
|
116
|
+
OB_LAMP,
|
|
117
|
+
OB_MESH,
|
|
118
|
+
} from './blendScene.ts';
|
|
119
|
+
export type { BlendPlaced } from './blendScene.ts';
|
|
120
|
+
export { readBlendMesh } from './blendMesh.ts';
|
|
121
|
+
export type { BlendMeshData, BlendColorLayer } from './blendMesh.ts';
|
|
122
|
+
export { cornerNormals, faceNormals } from './blendNormals.ts';
|
|
123
|
+
export { meshPrimitives, triangulateFace } from './blendGeometry.ts';
|
|
124
|
+
export type { BlendPrimitive, SlotNeeds } from './blendGeometry.ts';
|
|
125
|
+
export { readBlendSurface, customProperties } from './blendMaterial.ts';
|
|
126
|
+
export type { BlendSurface, ChannelSource } from './blendMaterial.ts';
|
|
127
|
+
export { judgeModifiers } from './blendModifiers.ts';
|
|
128
|
+
export { Animation, actionCurves, readCurve } from './blendAnimation.ts';
|
|
129
|
+
export type { Curve } from './blendAnimation.ts';
|
|
130
|
+
export type { ModifierVerdict } from './blendModifiers.ts';
|
|
131
|
+
export { blendCompression, isBlend } from './blendFile.ts';
|
|
132
|
+
export type { BlendBlock, BlendHeader, DnaField, DnaStruct } from './blendFile.ts';
|
|
133
|
+
export { unpackBlend, zstdFrames } from './blendPacked.ts';
|
|
134
|
+
export type { BlendDecompress } from './blendPacked.ts';
|
|
94
135
|
export type { ModelSource, ModelImport } from './readModel.ts';
|
|
95
136
|
/*
|
|
96
137
|
* The browser's decompressor, and the two-pass trick that makes an async one usable by a
|
package/src/readModel.ts
CHANGED
|
@@ -38,6 +38,8 @@ import { parseUsda, usdzToMeshes } from './usd.ts';
|
|
|
38
38
|
import { threeMfToMeshes } from './threemf.ts';
|
|
39
39
|
import { kn5ToMeshes, toDrftNodes } from './kn5.ts';
|
|
40
40
|
import { describeRecognised, recognise } from './recognise.ts';
|
|
41
|
+
import { readBlend } from './blend.ts';
|
|
42
|
+
import type { BlendDecompress } from './blendPacked.ts';
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Every extension this project reads, in the order a bundle should be picked from.
|
|
@@ -57,6 +59,11 @@ export const MODEL_FORMATS: readonly { ext: string; tier: number; note: string }
|
|
|
57
59
|
{ ext: '.3mf', tier: 1, note: '3MF, geometry and colour, with no UVs in the core format' },
|
|
58
60
|
{ ext: '.fbx', tier: 2, note: 'FBX, experimental reader, geometry, materials, skins and clips' },
|
|
59
61
|
{ ext: '.kn5', tier: 2, note: 'experimental reader, geometry, materials and hierarchy' },
|
|
62
|
+
{
|
|
63
|
+
ext: '.blend',
|
|
64
|
+
tier: 2,
|
|
65
|
+
note: 'Blender, read directly; modifiers, rigs and constraints are refused and baked through Blender',
|
|
66
|
+
},
|
|
60
67
|
];
|
|
61
68
|
|
|
62
69
|
/** Whether this project has a reader for an extension. Lower case or not. */
|
|
@@ -93,6 +100,8 @@ export interface ModelSource {
|
|
|
93
100
|
readonly inflate?: Inflate;
|
|
94
101
|
/** Raw deflate inflate, for the zip inside a `.3mf`. Not the same function as `inflate`. */
|
|
95
102
|
readonly inflateRaw?: Inflate;
|
|
103
|
+
/** gzip and zstd, for a `.blend` saved compressed. See `blendPacked.ts`. */
|
|
104
|
+
readonly decompress?: BlendDecompress;
|
|
96
105
|
/**
|
|
97
106
|
* Derive a tangent frame where a material declares a normal map and the file supplies none.
|
|
98
107
|
*
|
|
@@ -154,6 +163,14 @@ export interface ModelImport {
|
|
|
154
163
|
lights?: readonly DrftLight[];
|
|
155
164
|
/** Which way round the source's coordinate system was. `orient.ts` is what acts on it. */
|
|
156
165
|
declaredHand?: Handedness;
|
|
166
|
+
/**
|
|
167
|
+
* Where the reader knows its result differs from what the authoring tool would export: a node
|
|
168
|
+
* graph glTF cannot hold, animation it does not read. Drawn anyway, and named.
|
|
169
|
+
*
|
|
170
|
+
* Absent for every format but `.blend`, whose authoring tool can be asked instead: the baker
|
|
171
|
+
* reads a non-empty list as the reason to hand the file to Blender when Blender is there.
|
|
172
|
+
*/
|
|
173
|
+
approximations?: readonly string[];
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
function needed(ext: string, capability: string): DrftError {
|
|
@@ -419,6 +436,33 @@ async function dispatch(source: ModelSource): Promise<ModelImport> {
|
|
|
419
436
|
};
|
|
420
437
|
}
|
|
421
438
|
|
|
439
|
+
if (ext === '.blend') {
|
|
440
|
+
/*
|
|
441
|
+
* Read into a glTF document and through the glTF reader, so a `.blend` and Blender's export of
|
|
442
|
+
* it share every step after the document. A file needing evaluation throws
|
|
443
|
+
* `BlendNeedsBlender`, which the baker answers by asking Blender.
|
|
444
|
+
*/
|
|
445
|
+
const result = await readBlend(bytes, source.decompress);
|
|
446
|
+
const parts = [result.binary];
|
|
447
|
+
const read = gltfToMeshes(result.doc, parts, options);
|
|
448
|
+
const said = [...result.approximations, ...result.warnings];
|
|
449
|
+
const shown = said.slice(0, 8);
|
|
450
|
+
return complete(
|
|
451
|
+
{
|
|
452
|
+
...read,
|
|
453
|
+
...readGltfSkins(result.doc, parts),
|
|
454
|
+
warnings: [
|
|
455
|
+
...read.warnings,
|
|
456
|
+
...shown,
|
|
457
|
+
...(said.length > shown.length ? [`…and ${said.length - shown.length} more`] : []),
|
|
458
|
+
'blend: direct reader (tier 2), not covered by the compatibility promise.',
|
|
459
|
+
],
|
|
460
|
+
approximations: result.approximations,
|
|
461
|
+
},
|
|
462
|
+
result.notes,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
422
466
|
if (ext === '.kn5') {
|
|
423
467
|
/*
|
|
424
468
|
* No capability of any kind: a `.kn5` embeds every texture it uses, so this is the one binary
|