@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,280 @@
|
|
|
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
|
+
import { axisAngleMatrix, compose, eulerMatrix, multiply, multiply3, quaternionMatrix, translation, } from './blendMatrix.js';
|
|
22
|
+
export const OB_EMPTY = 0;
|
|
23
|
+
export const OB_MESH = 1;
|
|
24
|
+
export const OB_LAMP = 10;
|
|
25
|
+
export const OB_CAMERA = 11;
|
|
26
|
+
export const OB_ARMATURE = 25;
|
|
27
|
+
/** Object types that draw nothing and so need nothing evaluated: cameras, speakers, probes. */
|
|
28
|
+
const INERT = new Set([OB_CAMERA, 12, 13, 22]);
|
|
29
|
+
const NEEDS_EVALUATION = {
|
|
30
|
+
2: 'a curve',
|
|
31
|
+
3: 'a NURBS surface',
|
|
32
|
+
4: 'a text object',
|
|
33
|
+
5: 'a metaball',
|
|
34
|
+
26: 'a grease pencil object',
|
|
35
|
+
27: 'a hair curves object',
|
|
36
|
+
28: 'a point cloud',
|
|
37
|
+
29: 'a volume',
|
|
38
|
+
30: 'a grease pencil object',
|
|
39
|
+
};
|
|
40
|
+
const OB_HIDE_RENDER = 1 << 2;
|
|
41
|
+
const COLLECTION_HIDE_RENDER = 1 << 3;
|
|
42
|
+
const LAYER_COLLECTION_EXCLUDE = 1 << 4;
|
|
43
|
+
const OB_DUPLICOLLECTION = 1 << 8;
|
|
44
|
+
const OB_DUPLIVERTS = 1 << 4;
|
|
45
|
+
const OB_DUPLIFACES = 1 << 9;
|
|
46
|
+
const PARTYPE = 15;
|
|
47
|
+
const PAROBJECT = 0;
|
|
48
|
+
const PARSKEL = 4;
|
|
49
|
+
const PARBONE = 7;
|
|
50
|
+
/** The scene the file was saved looking at. */
|
|
51
|
+
export function activeScene(blend) {
|
|
52
|
+
const glob = blend.idsOf('GLOB')[0];
|
|
53
|
+
const scene = glob?.has('curscene') === true ? glob.deref('curscene') : null;
|
|
54
|
+
return scene ?? blend.idsOf('SC')[0] ?? null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The object's transform relative to its parent: location, rotation and scale, deltas folded in.
|
|
58
|
+
*
|
|
59
|
+
* `values`, where given, overrides what the file stores with what an animation says at a frame,
|
|
60
|
+
* keyed by Blender's data path and index — `location[0]`, `rotation_quaternion[3]` — which is what
|
|
61
|
+
* `Animation.values` fills (`blendAnimation.ts`).
|
|
62
|
+
*/
|
|
63
|
+
export function localMatrix(object, values) {
|
|
64
|
+
const read = (field, path, count, offset = 0) => {
|
|
65
|
+
const stored = object.has(field)
|
|
66
|
+
? object.floats(field)
|
|
67
|
+
: new Array(count).fill(field.includes('size') || field.includes('scale') ? 1 : 0);
|
|
68
|
+
if (values === undefined)
|
|
69
|
+
return stored;
|
|
70
|
+
return stored.map((v, i) => values.get(`${path}[${i + offset}]`) ?? v);
|
|
71
|
+
};
|
|
72
|
+
const location = read('loc', 'location', 3);
|
|
73
|
+
const deltaLocation = read('dloc', 'delta_location', 3);
|
|
74
|
+
const scale = read('size', 'scale', 3);
|
|
75
|
+
const deltaScale = object.has('dscale')
|
|
76
|
+
? read('dscale', 'delta_scale', 3)
|
|
77
|
+
: read('dsize', 'delta_scale', 3);
|
|
78
|
+
const mode = object.int('rotmode');
|
|
79
|
+
let rotation;
|
|
80
|
+
let delta;
|
|
81
|
+
if (mode === 0) {
|
|
82
|
+
rotation = quaternionMatrix(read('quat', 'rotation_quaternion', 4));
|
|
83
|
+
delta = quaternionMatrix(read('dquat', 'delta_rotation_quaternion', 4));
|
|
84
|
+
}
|
|
85
|
+
else if (mode === -1) {
|
|
86
|
+
/* Blender keys an axis-angle as four values, the angle first. */
|
|
87
|
+
const angle = values?.get('rotation_axis_angle[0]') ?? object.float('rotAngle');
|
|
88
|
+
rotation = axisAngleMatrix(read('rotAxis', 'rotation_axis_angle', 3, 1), angle);
|
|
89
|
+
delta = axisAngleMatrix(object.floats('drotAxis'), object.float('drotAngle'));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
rotation = eulerMatrix(read('rot', 'rotation_euler', 3), mode);
|
|
93
|
+
delta = eulerMatrix(read('drot', 'delta_rotation_euler', 3), mode);
|
|
94
|
+
}
|
|
95
|
+
return compose([0, 1, 2].map((i) => location[i] + deltaLocation[i]), multiply3(delta, rotation), [0, 1, 2].map((i) => scale[i] * deltaScale[i]));
|
|
96
|
+
}
|
|
97
|
+
/** Walk the scene into placed objects, naming in `refusals` anything that needs Blender to run. */
|
|
98
|
+
export function placeObjects(blend, refusals, bonePose) {
|
|
99
|
+
const scene = activeScene(blend);
|
|
100
|
+
if (scene === null) {
|
|
101
|
+
refusals.push('the file has no scene');
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const worlds = new Map();
|
|
105
|
+
const world = (object, depth = 0) => {
|
|
106
|
+
const cached = worlds.get(object.offset);
|
|
107
|
+
if (cached !== undefined)
|
|
108
|
+
return cached;
|
|
109
|
+
let matrix = localMatrix(object);
|
|
110
|
+
const parent = depth < 64 ? object.deref('parent') : null;
|
|
111
|
+
if (parent !== null) {
|
|
112
|
+
const type = object.int('partype') & PARTYPE;
|
|
113
|
+
let base = world(parent, depth + 1);
|
|
114
|
+
if (type === PARBONE) {
|
|
115
|
+
const pose = bonePose(parent, object.string('parsubstr'));
|
|
116
|
+
if (pose === null)
|
|
117
|
+
refusals.push(`"${object.idName()}" is parented to a bone that is not there`);
|
|
118
|
+
else
|
|
119
|
+
base = multiply(base, pose);
|
|
120
|
+
}
|
|
121
|
+
else if (type !== PAROBJECT && type !== PARSKEL) {
|
|
122
|
+
refusals.push(`"${object.idName()}" is parented to vertices, which Blender evaluates`);
|
|
123
|
+
}
|
|
124
|
+
matrix = multiply(base, multiply(object.floats('parentinv'), matrix));
|
|
125
|
+
}
|
|
126
|
+
if (object.list('constraints').length > 0) {
|
|
127
|
+
refusals.push(`"${object.idName()}" has constraints, which Blender evaluates`);
|
|
128
|
+
}
|
|
129
|
+
worlds.set(object.offset, matrix);
|
|
130
|
+
return matrix;
|
|
131
|
+
};
|
|
132
|
+
const placed = [];
|
|
133
|
+
const index = new Map();
|
|
134
|
+
const place = (object, outer, holder, depth) => {
|
|
135
|
+
const kind = object.int('type');
|
|
136
|
+
if (INERT.has(kind))
|
|
137
|
+
return;
|
|
138
|
+
const reason = NEEDS_EVALUATION[kind];
|
|
139
|
+
if (reason !== undefined)
|
|
140
|
+
refusals.push(`"${object.idName()}" is ${reason}, which Blender evaluates`);
|
|
141
|
+
if (object.list('particlesystem').length > 0) {
|
|
142
|
+
refusals.push(`"${object.idName()}" has a particle system, which Blender evaluates`);
|
|
143
|
+
}
|
|
144
|
+
const transflag = object.int('transflag');
|
|
145
|
+
if (transflag & (OB_DUPLIVERTS | OB_DUPLIFACES)) {
|
|
146
|
+
refusals.push(`"${object.idName()}" instances onto its vertices or faces, which Blender evaluates`);
|
|
147
|
+
}
|
|
148
|
+
const own = world(object);
|
|
149
|
+
const matrix = outer === null ? own : multiply(outer, own);
|
|
150
|
+
let parent = holder;
|
|
151
|
+
for (let up = object.deref('parent'), hops = 0; up !== null && hops < 64; hops++) {
|
|
152
|
+
const found = outer === null ? index.get(up.offset) : undefined;
|
|
153
|
+
if (found !== undefined) {
|
|
154
|
+
parent = found;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
up = up.deref('parent');
|
|
158
|
+
}
|
|
159
|
+
/*
|
|
160
|
+
* An instancer carries its collection's offset in its own matrix, so its members sit at their
|
|
161
|
+
* own world matrices beneath it. Blender's exporter writes the instancer the same way, which
|
|
162
|
+
* keeps the two node trees comparable; the empty draws nothing, so nothing moves on screen.
|
|
163
|
+
*/
|
|
164
|
+
const collection = transflag & OB_DUPLICOLLECTION ? object.deref('dup_group') : null;
|
|
165
|
+
const offset = collection?.floats('dupli_ofs') ?? [0, 0, 0];
|
|
166
|
+
const held = collection === null
|
|
167
|
+
? matrix
|
|
168
|
+
: multiply(matrix, translation(-offset[0], -offset[1], -offset[2]));
|
|
169
|
+
const at = placed.length;
|
|
170
|
+
placed.push({ object, name: object.idName(), kind, world: held, parent });
|
|
171
|
+
if (outer === null)
|
|
172
|
+
index.set(object.offset, at);
|
|
173
|
+
if (collection !== null && depth < 16) {
|
|
174
|
+
for (const member of collectionObjects(collection, true))
|
|
175
|
+
place(member, held, at, depth + 1);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const roots = sceneObjects(scene, blend);
|
|
179
|
+
/* Parents first, so a child finds its parent's index already assigned. */
|
|
180
|
+
const order = [...roots].sort((a, b) => depthOf(a) - depthOf(b));
|
|
181
|
+
for (const object of order)
|
|
182
|
+
place(object, null, -1, 0);
|
|
183
|
+
return placed;
|
|
184
|
+
}
|
|
185
|
+
function depthOf(object) {
|
|
186
|
+
let depth = 0;
|
|
187
|
+
for (let up = object.deref('parent'); up !== null && depth < 64; up = up.deref('parent'))
|
|
188
|
+
depth++;
|
|
189
|
+
return depth;
|
|
190
|
+
}
|
|
191
|
+
/** The objects a collection holds directly and through its children, those that render. */
|
|
192
|
+
function collectionObjects(collection, nested) {
|
|
193
|
+
const out = [];
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
const visit = (current, depth) => {
|
|
196
|
+
for (const link of current.list('gobject')) {
|
|
197
|
+
const object = link.deref('ob');
|
|
198
|
+
if (object === null || seen.has(object.offset))
|
|
199
|
+
continue;
|
|
200
|
+
seen.add(object.offset);
|
|
201
|
+
if ((object.int('restrictflag') & OB_HIDE_RENDER) === 0)
|
|
202
|
+
out.push(object);
|
|
203
|
+
}
|
|
204
|
+
if (!nested || depth > 32)
|
|
205
|
+
return;
|
|
206
|
+
for (const child of current.list('children')) {
|
|
207
|
+
const inner = child.deref('collection');
|
|
208
|
+
if (inner !== null && (inner.int('flag') & COLLECTION_HIDE_RENDER) === 0)
|
|
209
|
+
visit(inner, depth + 1);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
visit(collection, 0);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/** Every object the scene renders, once each. */
|
|
216
|
+
function sceneObjects(scene, blend) {
|
|
217
|
+
if (!scene.has('master_collection')) {
|
|
218
|
+
/* Before 2.8: bases on the scene, drawn when one of their layers is a visible scene layer. */
|
|
219
|
+
const layers = scene.int('lay');
|
|
220
|
+
return scene
|
|
221
|
+
.list('base')
|
|
222
|
+
.filter((base) => (base.int('lay') & layers) !== 0)
|
|
223
|
+
.map((base) => base.deref('object'))
|
|
224
|
+
.filter((object) => object !== null)
|
|
225
|
+
.filter((object) => (object.int('restrictflag') & OB_HIDE_RENDER) === 0);
|
|
226
|
+
}
|
|
227
|
+
const master = scene.deref('master_collection');
|
|
228
|
+
if (master === null)
|
|
229
|
+
return [];
|
|
230
|
+
const glob = blend.idsOf('GLOB')[0];
|
|
231
|
+
const layers = scene.list('view_layers');
|
|
232
|
+
const active = glob?.has('cur_view_layer') === true
|
|
233
|
+
? layers.find((layer) => layer.offset === glob.deref('cur_view_layer')?.offset)
|
|
234
|
+
: undefined;
|
|
235
|
+
const layer = active ?? layers[0];
|
|
236
|
+
const top = layer?.list('layer_collections')[0];
|
|
237
|
+
const out = [];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
const take = (collection) => {
|
|
240
|
+
for (const link of collection.list('gobject')) {
|
|
241
|
+
const object = link.deref('ob');
|
|
242
|
+
if (object === null || seen.has(object.offset))
|
|
243
|
+
continue;
|
|
244
|
+
if ((object.int('restrictflag') & OB_HIDE_RENDER) !== 0)
|
|
245
|
+
continue;
|
|
246
|
+
seen.add(object.offset);
|
|
247
|
+
out.push(object);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
/*
|
|
251
|
+
* Walk the layer collections where the view layer has them, since exclusion lives there; walk the
|
|
252
|
+
* collections themselves where it does not, which is a file written without a synchronised view
|
|
253
|
+
* layer and has nothing excluded.
|
|
254
|
+
*/
|
|
255
|
+
const walkLayer = (lc, depth) => {
|
|
256
|
+
if (depth > 64 || (lc.int('flag') & LAYER_COLLECTION_EXCLUDE) !== 0)
|
|
257
|
+
return;
|
|
258
|
+
const collection = lc.deref('collection');
|
|
259
|
+
if (collection === null || (collection.int('flag') & COLLECTION_HIDE_RENDER) !== 0)
|
|
260
|
+
return;
|
|
261
|
+
take(collection);
|
|
262
|
+
for (const child of lc.list('layer_collections'))
|
|
263
|
+
walkLayer(child, depth + 1);
|
|
264
|
+
};
|
|
265
|
+
const walkCollection = (collection, depth) => {
|
|
266
|
+
if (depth > 64 || (collection.int('flag') & COLLECTION_HIDE_RENDER) !== 0)
|
|
267
|
+
return;
|
|
268
|
+
take(collection);
|
|
269
|
+
for (const child of collection.list('children')) {
|
|
270
|
+
const inner = child.deref('collection');
|
|
271
|
+
if (inner !== null)
|
|
272
|
+
walkCollection(inner, depth + 1);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
if (top !== undefined)
|
|
276
|
+
walkLayer(top, 0);
|
|
277
|
+
else
|
|
278
|
+
walkCollection(master, 0);
|
|
279
|
+
return out;
|
|
280
|
+
}
|
package/dist/gltf.js
CHANGED
|
@@ -455,8 +455,14 @@ function materialOf(material) {
|
|
|
455
455
|
cutout: masked ? Math.min(1, Math.max(0, material?.alphaCutoff ?? 0.5)) : 0,
|
|
456
456
|
/* Metalness is glTF's statement about reflection: a metal mirrors its surroundings and
|
|
457
457
|
a dielectric mostly does not. It also feeds `specular`, which is not double counting,
|
|
458
|
-
because the two describe different halves of the same fact.
|
|
459
|
-
|
|
458
|
+
because the two describe different halves of the same fact.
|
|
459
|
+
|
|
460
|
+
**An absent factor is 1 here as it is everywhere else in glTF**, with the decal guard
|
|
461
|
+
`highlight` applies above: an image-coloured surface naming no metallic stays a dielectric.
|
|
462
|
+
It read 0 for every surface until a Blender export showed it, because Blender writes no
|
|
463
|
+
`metallicFactor` for a metal — 1 is the default — so every untextured metal it exported
|
|
464
|
+
arrived taking a highlight and mirroring nothing. */
|
|
465
|
+
reflectivity: Math.min(1, Math.max(0, modulated ? 0 : pbr?.baseColorTexture === undefined ? metallic : (pbr?.metallicFactor ?? 0))),
|
|
460
466
|
normalMap: material?.normalTexture?.index ?? -1,
|
|
461
467
|
ormMap,
|
|
462
468
|
emissiveMap: material?.emissiveTexture?.index ?? -1,
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,27 @@ export type { Recognised } from './recognise.ts';
|
|
|
48
48
|
export { fbxToMeshes } from './fbx.ts';
|
|
49
49
|
export type { Inflate } from './fbx.ts';
|
|
50
50
|
export { readModel, readerFor, extensionOf, MODEL_FORMATS } from './readModel.ts';
|
|
51
|
+
export { BlendNeedsBlender, openBlend, readBlend } from './blend.ts';
|
|
52
|
+
export { BlendData, BlendStruct } from './blendData.ts';
|
|
53
|
+
export { blendToGltf, blackbody, imageMime, imagePath, materialSlots, packedImage, } from './blendGltf.ts';
|
|
54
|
+
export type { BlendGltf } from './blendGltf.ts';
|
|
55
|
+
export { placeObjects, localMatrix, activeScene, OB_ARMATURE, OB_EMPTY, OB_LAMP, OB_MESH, } from './blendScene.ts';
|
|
56
|
+
export type { BlendPlaced } from './blendScene.ts';
|
|
57
|
+
export { readBlendMesh } from './blendMesh.ts';
|
|
58
|
+
export type { BlendMeshData, BlendColorLayer } from './blendMesh.ts';
|
|
59
|
+
export { cornerNormals, faceNormals } from './blendNormals.ts';
|
|
60
|
+
export { meshPrimitives, triangulateFace } from './blendGeometry.ts';
|
|
61
|
+
export type { BlendPrimitive, SlotNeeds } from './blendGeometry.ts';
|
|
62
|
+
export { readBlendSurface, customProperties } from './blendMaterial.ts';
|
|
63
|
+
export type { BlendSurface, ChannelSource } from './blendMaterial.ts';
|
|
64
|
+
export { judgeModifiers } from './blendModifiers.ts';
|
|
65
|
+
export { Animation, actionCurves, readCurve } from './blendAnimation.ts';
|
|
66
|
+
export type { Curve } from './blendAnimation.ts';
|
|
67
|
+
export type { ModifierVerdict } from './blendModifiers.ts';
|
|
68
|
+
export { blendCompression, isBlend } from './blendFile.ts';
|
|
69
|
+
export type { BlendBlock, BlendHeader, DnaField, DnaStruct } from './blendFile.ts';
|
|
70
|
+
export { unpackBlend, zstdFrames } from './blendPacked.ts';
|
|
71
|
+
export type { BlendDecompress } from './blendPacked.ts';
|
|
51
72
|
export type { ModelSource, ModelImport } from './readModel.ts';
|
|
52
73
|
export { browserInflate, browserInflateRaw, prepareFbxInflate, prepareZipInflate, } from './fbxInflate.ts';
|
|
53
74
|
export type { AsyncInflate } from './fbxInflate.ts';
|
package/dist/index.js
CHANGED
|
@@ -72,6 +72,23 @@ export { fbxToMeshes } from './fbx.js';
|
|
|
72
72
|
* decompressors and the resolution of a name a file states are all the caller's to supply.
|
|
73
73
|
*/
|
|
74
74
|
export { readModel, readerFor, extensionOf, MODEL_FORMATS } from './readModel.js';
|
|
75
|
+
/*
|
|
76
|
+
* Blender's own format, read directly. `readModel` takes a `.blend` like any other model; the rest
|
|
77
|
+
* is for a bake that keeps a scene's structure — its instancing, its materials' channels — rather
|
|
78
|
+
* than flattening it into world-space meshes.
|
|
79
|
+
*/
|
|
80
|
+
export { BlendNeedsBlender, openBlend, readBlend } from './blend.js';
|
|
81
|
+
export { BlendData, BlendStruct } from './blendData.js';
|
|
82
|
+
export { blendToGltf, blackbody, imageMime, imagePath, materialSlots, packedImage, } from './blendGltf.js';
|
|
83
|
+
export { placeObjects, localMatrix, activeScene, OB_ARMATURE, OB_EMPTY, OB_LAMP, OB_MESH, } from './blendScene.js';
|
|
84
|
+
export { readBlendMesh } from './blendMesh.js';
|
|
85
|
+
export { cornerNormals, faceNormals } from './blendNormals.js';
|
|
86
|
+
export { meshPrimitives, triangulateFace } from './blendGeometry.js';
|
|
87
|
+
export { readBlendSurface, customProperties } from './blendMaterial.js';
|
|
88
|
+
export { judgeModifiers } from './blendModifiers.js';
|
|
89
|
+
export { Animation, actionCurves, readCurve } from './blendAnimation.js';
|
|
90
|
+
export { blendCompression, isBlend } from './blendFile.js';
|
|
91
|
+
export { unpackBlend, zstdFrames } from './blendPacked.js';
|
|
75
92
|
/*
|
|
76
93
|
* The browser's decompressor, and the two-pass trick that makes an async one usable by a
|
|
77
94
|
* synchronous parser. `DecompressionStream` is all a page has and it only works as a stream, so
|
package/dist/readModel.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { AnimationClip, DrftLight, DrftMaterial, DrftNode, DrftSkin } from
|
|
|
25
25
|
import type { AssetReference } from './assetPath.ts';
|
|
26
26
|
import type { Handedness, UpAxis } from './orient.ts';
|
|
27
27
|
import type { Inflate } from './fbx.ts';
|
|
28
|
+
import type { BlendDecompress } from './blendPacked.ts';
|
|
28
29
|
/**
|
|
29
30
|
* Every extension this project reads, in the order a bundle should be picked from.
|
|
30
31
|
*
|
|
@@ -65,6 +66,8 @@ export interface ModelSource {
|
|
|
65
66
|
readonly inflate?: Inflate;
|
|
66
67
|
/** Raw deflate inflate, for the zip inside a `.3mf`. Not the same function as `inflate`. */
|
|
67
68
|
readonly inflateRaw?: Inflate;
|
|
69
|
+
/** gzip and zstd, for a `.blend` saved compressed. See `blendPacked.ts`. */
|
|
70
|
+
readonly decompress?: BlendDecompress;
|
|
68
71
|
/**
|
|
69
72
|
* Derive a tangent frame where a material declares a normal map and the file supplies none.
|
|
70
73
|
*
|
|
@@ -128,6 +131,14 @@ export interface ModelImport {
|
|
|
128
131
|
lights?: readonly DrftLight[];
|
|
129
132
|
/** Which way round the source's coordinate system was. `orient.ts` is what acts on it. */
|
|
130
133
|
declaredHand?: Handedness;
|
|
134
|
+
/**
|
|
135
|
+
* Where the reader knows its result differs from what the authoring tool would export: a node
|
|
136
|
+
* graph glTF cannot hold, animation it does not read. Drawn anyway, and named.
|
|
137
|
+
*
|
|
138
|
+
* Absent for every format but `.blend`, whose authoring tool can be asked instead: the baker
|
|
139
|
+
* reads a non-empty list as the reason to hand the file to Blender when Blender is there.
|
|
140
|
+
*/
|
|
141
|
+
approximations?: readonly string[];
|
|
131
142
|
}
|
|
132
143
|
/**
|
|
133
144
|
* Read one model file into meshes.
|
|
@@ -138,6 +149,8 @@ export interface ModelImport {
|
|
|
138
149
|
* whole call in a worker and keep the parse off the main thread.
|
|
139
150
|
*/
|
|
140
151
|
export declare function readModel(source: ModelSource): Promise<ModelImport>;
|
|
152
|
+
/** The separate levels of detail a bundle ships beside its model, finest first. */
|
|
153
|
+
export declare function levelsBeside(model: string, names: readonly string[]): string[];
|
|
141
154
|
/**
|
|
142
155
|
* The collision hull a vehicle folder ships beside its model, or null.
|
|
143
156
|
*
|
|
@@ -151,5 +164,4 @@ export declare function readModel(source: ModelSource): Promise<ModelImport>;
|
|
|
151
164
|
* reader and no writer — a format decision about whether a hull is triangle soup or a set of
|
|
152
165
|
* convex hulls, and not one an import should make in passing.
|
|
153
166
|
*/
|
|
154
|
-
export declare function levelsBeside(model: string, names: readonly string[]): string[];
|
|
155
167
|
export declare function colliderBeside(names: readonly string[]): string | null;
|
package/dist/readModel.js
CHANGED
|
@@ -31,6 +31,7 @@ import { parseUsda, usdzToMeshes } from './usd.js';
|
|
|
31
31
|
import { threeMfToMeshes } from './threemf.js';
|
|
32
32
|
import { kn5ToMeshes, toDrftNodes } from './kn5.js';
|
|
33
33
|
import { describeRecognised, recognise } from './recognise.js';
|
|
34
|
+
import { readBlend } from './blend.js';
|
|
34
35
|
/**
|
|
35
36
|
* Every extension this project reads, in the order a bundle should be picked from.
|
|
36
37
|
*
|
|
@@ -49,6 +50,11 @@ export const MODEL_FORMATS = [
|
|
|
49
50
|
{ ext: '.3mf', tier: 1, note: '3MF, geometry and colour, with no UVs in the core format' },
|
|
50
51
|
{ ext: '.fbx', tier: 2, note: 'FBX, experimental reader, geometry, materials, skins and clips' },
|
|
51
52
|
{ ext: '.kn5', tier: 2, note: 'experimental reader, geometry, materials and hierarchy' },
|
|
53
|
+
{
|
|
54
|
+
ext: '.blend',
|
|
55
|
+
tier: 2,
|
|
56
|
+
note: 'Blender, read directly; modifiers, rigs and constraints are refused and baked through Blender',
|
|
57
|
+
},
|
|
52
58
|
];
|
|
53
59
|
/** Whether this project has a reader for an extension. Lower case or not. */
|
|
54
60
|
export function readerFor(ext) {
|
|
@@ -305,6 +311,29 @@ async function dispatch(source) {
|
|
|
305
311
|
unitScale: result.unitScale,
|
|
306
312
|
};
|
|
307
313
|
}
|
|
314
|
+
if (ext === '.blend') {
|
|
315
|
+
/*
|
|
316
|
+
* Read into a glTF document and through the glTF reader, so a `.blend` and Blender's export of
|
|
317
|
+
* it share every step after the document. A file needing evaluation throws
|
|
318
|
+
* `BlendNeedsBlender`, which the baker answers by asking Blender.
|
|
319
|
+
*/
|
|
320
|
+
const result = await readBlend(bytes, source.decompress);
|
|
321
|
+
const parts = [result.binary];
|
|
322
|
+
const read = gltfToMeshes(result.doc, parts, options);
|
|
323
|
+
const said = [...result.approximations, ...result.warnings];
|
|
324
|
+
const shown = said.slice(0, 8);
|
|
325
|
+
return complete({
|
|
326
|
+
...read,
|
|
327
|
+
...readGltfSkins(result.doc, parts),
|
|
328
|
+
warnings: [
|
|
329
|
+
...read.warnings,
|
|
330
|
+
...shown,
|
|
331
|
+
...(said.length > shown.length ? [`…and ${said.length - shown.length} more`] : []),
|
|
332
|
+
'blend: direct reader (tier 2), not covered by the compatibility promise.',
|
|
333
|
+
],
|
|
334
|
+
approximations: result.approximations,
|
|
335
|
+
}, result.notes);
|
|
336
|
+
}
|
|
308
337
|
if (ext === '.kn5') {
|
|
309
338
|
/*
|
|
310
339
|
* No capability of any kind: a `.kn5` embeds every texture it uses, so this is the one binary
|
|
@@ -354,19 +383,7 @@ function base64Bytes(base64) {
|
|
|
354
383
|
out[i] = binary.charCodeAt(i);
|
|
355
384
|
return out;
|
|
356
385
|
}
|
|
357
|
-
/**
|
|
358
|
-
* The collision hull a vehicle folder ships beside its model, or null.
|
|
359
|
-
*
|
|
360
|
-
* **A convention, not a statement in any file**, which is why it is one named function instead of
|
|
361
|
-
* a rule spread through the baker: a folder naming its hull differently gets no collider and the
|
|
362
|
-
* baker says what it looked for, rather than leaving a consumer to wonder why physics has nothing
|
|
363
|
-
* to work with.
|
|
364
|
-
*
|
|
365
|
-
* It is read by the ordinary reader and baked as its own asset. Carrying a hull *inside* the
|
|
366
|
-
* model's own file would mean defining `COLL`, which is a declared FourCC with no payload, no
|
|
367
|
-
* reader and no writer — a format decision about whether a hull is triangle soup or a set of
|
|
368
|
-
* convex hulls, and not one an import should make in passing.
|
|
369
|
-
*/
|
|
386
|
+
/** The separate levels of detail a bundle ships beside its model, finest first. */
|
|
370
387
|
export function levelsBeside(model, names) {
|
|
371
388
|
/*
|
|
372
389
|
* `<name>_lod_<letter>.kn5` beside `<name>.kn5`, finest first, which is the order the letters
|
|
@@ -381,6 +398,19 @@ export function levelsBeside(model, names) {
|
|
|
381
398
|
.sort((a, b) => a.match[1].toLowerCase().localeCompare(b.match[1].toLowerCase()))
|
|
382
399
|
.map((entry) => entry.name);
|
|
383
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* The collision hull a vehicle folder ships beside its model, or null.
|
|
403
|
+
*
|
|
404
|
+
* **A convention, not a statement in any file**, which is why it is one named function instead of
|
|
405
|
+
* a rule spread through the baker: a folder naming its hull differently gets no collider and the
|
|
406
|
+
* baker says what it looked for, rather than leaving a consumer to wonder why physics has nothing
|
|
407
|
+
* to work with.
|
|
408
|
+
*
|
|
409
|
+
* It is read by the ordinary reader and baked as its own asset. Carrying a hull *inside* the
|
|
410
|
+
* model's own file would mean defining `COLL`, which is a declared FourCC with no payload, no
|
|
411
|
+
* reader and no writer — a format decision about whether a hull is triangle soup or a set of
|
|
412
|
+
* convex hulls, and not one an import should make in passing.
|
|
413
|
+
*/
|
|
384
414
|
export function colliderBeside(names) {
|
|
385
415
|
return names.find((name) => name.toLowerCase() === 'collider.kn5') ?? null;
|
|
386
416
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/assets",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.1",
|
|
4
4
|
"description": "Model readers and the streaming loader for the .drft container",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@driftengine/drft": "4.
|
|
31
|
-
"@driftengine/texture": "4.
|
|
30
|
+
"@driftengine/drft": "4.7.1",
|
|
31
|
+
"@driftengine/texture": "4.7.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@driftengine/core": "4.
|
|
34
|
+
"@driftengine/core": "4.7.1"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
37
37
|
"repository": {
|
package/src/blend.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a `.blend` directly: Blender's own file, with no Blender installed.
|
|
3
|
+
*
|
|
4
|
+
* The route is container → catalogue → scene → glTF document → the glTF reader, each step in its
|
|
5
|
+
* own module, and this one only chains them and decides what a refusal looks like. See
|
|
6
|
+
* `blendGltf.ts` for why the document in the middle is glTF.
|
|
7
|
+
*
|
|
8
|
+
* **A refusal is an error with its reasons attached**, `BlendNeedsBlender`, because the caller
|
|
9
|
+
* that can act on it — the baker, with Blender on the machine — needs the list rather than a
|
|
10
|
+
* sentence to parse: it hands the file to Blender and says why. A browser has no Blender, and shows
|
|
11
|
+
* the message.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { DrftError } from '@driftengine/drft';
|
|
15
|
+
import { BlendData } from './blendData.ts';
|
|
16
|
+
import { blendToGltf } from './blendGltf.ts';
|
|
17
|
+
import type { BlendGltf } from './blendGltf.ts';
|
|
18
|
+
import { unpackBlend } from './blendPacked.ts';
|
|
19
|
+
import type { BlendDecompress } from './blendPacked.ts';
|
|
20
|
+
|
|
21
|
+
/** Thrown when a `.blend` holds something only Blender can evaluate, with each thing named. */
|
|
22
|
+
export class BlendNeedsBlender extends DrftError {
|
|
23
|
+
readonly reasons: readonly string[];
|
|
24
|
+
|
|
25
|
+
constructor(reasons: readonly string[]) {
|
|
26
|
+
const shown = reasons
|
|
27
|
+
.slice(0, 6)
|
|
28
|
+
.map((reason) => ` ${reason}`)
|
|
29
|
+
.join('\n');
|
|
30
|
+
const more = reasons.length > 6 ? `\n …and ${reasons.length - 6} more` : '';
|
|
31
|
+
super(
|
|
32
|
+
`blend: this file needs Blender to evaluate it, and the direct reader draws nothing rather than ` +
|
|
33
|
+
`the wrong shape:\n${shown}${more}\nBake it with \`npm run bake\` on a machine with Blender ` +
|
|
34
|
+
'installed, which hands it to Blender, or apply these in Blender and save again.',
|
|
35
|
+
);
|
|
36
|
+
this.name = 'BlendNeedsBlender';
|
|
37
|
+
this.reasons = reasons;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Open a `.blend`'s bytes, decompressing them first where the file was saved compressed. */
|
|
42
|
+
export async function openBlend(
|
|
43
|
+
bytes: Uint8Array,
|
|
44
|
+
decompress?: BlendDecompress,
|
|
45
|
+
): Promise<BlendData> {
|
|
46
|
+
return new BlendData(await unpackBlend(bytes, decompress));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A `.blend` as a glTF document, refusing it whole when any part of it needs evaluating. */
|
|
50
|
+
export async function readBlend(
|
|
51
|
+
bytes: Uint8Array,
|
|
52
|
+
decompress?: BlendDecompress,
|
|
53
|
+
): Promise<BlendGltf> {
|
|
54
|
+
const result = blendToGltf(await openBlend(bytes, decompress));
|
|
55
|
+
if (result.refusals.length > 0) throw new BlendNeedsBlender(result.refusals);
|
|
56
|
+
return result;
|
|
57
|
+
}
|