@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,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `.blend` material's node graph says each surface channel is: a value, or a channel of an
|
|
3
|
+
* image times a value.
|
|
4
|
+
*
|
|
5
|
+
* **One reading, two consumers.** `blendGltf.ts` turns a `BlendSurface` into a glTF material, the
|
|
6
|
+
* way Blender's glTF exporter would; a bake that composes its own images reads the same surface
|
|
7
|
+
* and can pack channels glTF cannot, such as roughness and metallic drawn from two images. So the
|
|
8
|
+
* graph is walked once, here, into channels, and what glTF can and cannot hold is decided later.
|
|
9
|
+
*
|
|
10
|
+
* **The patterns are the ones Blender itself writes**, because those are the graphs real files
|
|
11
|
+
* carry: Blender's glTF importer builds exactly this wiring — an image straight into a socket, a
|
|
12
|
+
* Separate Color between an image and roughness, metallic or occlusion, a Math Multiply for a
|
|
13
|
+
* factor, a Normal Map node, a Mapping node for a texture transform, and a "glTF Material Output"
|
|
14
|
+
* group for occlusion — and so does a person wiring a Principled BSDF by hand. Reroutes are
|
|
15
|
+
* walked through. A city assembled from imported assets used nothing else in 1,809 materials.
|
|
16
|
+
*
|
|
17
|
+
* **What it cannot read it names**, in `approximations`, and uses the socket's own value: a
|
|
18
|
+
* procedural texture, a Mix Shader, a math chain, a bump node.
|
|
19
|
+
*
|
|
20
|
+
* Old files mean what Blender makes of them now: a material saved before 4.2 has its blend mode
|
|
21
|
+
* from `blend_method`, which is what Blender converts on opening, and one with no node tree is its
|
|
22
|
+
* viewport colour, metallic and roughness.
|
|
23
|
+
*/
|
|
24
|
+
import { eulerMatrix, compose, invert, multiply } from './blendMatrix.js';
|
|
25
|
+
const NODE_DO_OUTPUT = 1 << 6;
|
|
26
|
+
const NODE_MUTED = 1 << 9;
|
|
27
|
+
const NODE_LINK_MUTED = 1 << 4;
|
|
28
|
+
const MA_BL_CULL_BACKFACE = 1 << 2;
|
|
29
|
+
const MA_BM_SOLID = 0;
|
|
30
|
+
const MA_BM_CLIP = 3;
|
|
31
|
+
const RENDER_BLENDED = 1;
|
|
32
|
+
const SOCK_FLOAT = 0;
|
|
33
|
+
const SOCK_RGBA = 2;
|
|
34
|
+
const MA_RAMP_MULT = 2;
|
|
35
|
+
/* Math node operations, by the number `custom1` stores. */
|
|
36
|
+
const MATH_SUBTRACT = 1;
|
|
37
|
+
const MATH_MULTIPLY = 2;
|
|
38
|
+
const MATH_ROUND = 14;
|
|
39
|
+
const MATH_LESS_THAN = 15;
|
|
40
|
+
const MATH_GREATER_THAN = 16;
|
|
41
|
+
/* Mapping node vector types. */
|
|
42
|
+
const MAPPING_POINT = 0;
|
|
43
|
+
const MAPPING_TEXTURE = 1;
|
|
44
|
+
const idname = (node) => node.string('idname');
|
|
45
|
+
/** A node graph, indexed for following links backwards from an input. */
|
|
46
|
+
class Graph {
|
|
47
|
+
nodes;
|
|
48
|
+
into = new Map();
|
|
49
|
+
constructor(tree) {
|
|
50
|
+
this.nodes = tree.list('nodes');
|
|
51
|
+
for (const link of tree.list('links')) {
|
|
52
|
+
if ((link.int('flag') & NODE_LINK_MUTED) !== 0)
|
|
53
|
+
continue;
|
|
54
|
+
const to = link.deref('tonode');
|
|
55
|
+
const toSocket = link.deref('tosock');
|
|
56
|
+
const from = link.deref('fromnode');
|
|
57
|
+
const fromSocket = link.deref('fromsock');
|
|
58
|
+
if (to === null || toSocket === null || from === null || fromSocket === null)
|
|
59
|
+
continue;
|
|
60
|
+
this.into.set(`${to.offset}:${toSocket.string('identifier')}`, {
|
|
61
|
+
node: from,
|
|
62
|
+
socket: fromSocket.string('identifier'),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The link into an input, walked back through reroutes, which forward their one input. */
|
|
67
|
+
follow(link) {
|
|
68
|
+
let current = link;
|
|
69
|
+
for (let hops = 0; current !== null && idname(current.node) === 'NodeReroute' && hops < 64; hops++) {
|
|
70
|
+
current = this.into.get(`${current.node.offset}:Input`) ?? null;
|
|
71
|
+
}
|
|
72
|
+
return current;
|
|
73
|
+
}
|
|
74
|
+
input(node, ...identifiers) {
|
|
75
|
+
for (const socket of node.list('inputs')) {
|
|
76
|
+
const id = socket.string('identifier');
|
|
77
|
+
if (!identifiers.includes(id) && !identifiers.includes(socket.string('name')))
|
|
78
|
+
continue;
|
|
79
|
+
return { socket, link: this.follow(this.into.get(`${node.offset}:${id}`) ?? null) };
|
|
80
|
+
}
|
|
81
|
+
return { socket: null, link: null };
|
|
82
|
+
}
|
|
83
|
+
/** The `index`-th input socket of a node, by position. */
|
|
84
|
+
at(node, index) {
|
|
85
|
+
const socket = node.list('inputs')[index] ?? null;
|
|
86
|
+
if (socket === null)
|
|
87
|
+
return { socket: null, link: null };
|
|
88
|
+
return {
|
|
89
|
+
socket,
|
|
90
|
+
link: this.follow(this.into.get(`${node.offset}:${socket.string('identifier')}`) ?? null),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function socketValue(socket) {
|
|
95
|
+
if (socket === null)
|
|
96
|
+
return null;
|
|
97
|
+
const type = socket.int('type');
|
|
98
|
+
const value = socket.deref('default_value', type === SOCK_RGBA
|
|
99
|
+
? 'bNodeSocketValueRGBA'
|
|
100
|
+
: type === SOCK_FLOAT
|
|
101
|
+
? 'bNodeSocketValueFloat'
|
|
102
|
+
: 'bNodeSocketValueVector');
|
|
103
|
+
if (value === null)
|
|
104
|
+
return null;
|
|
105
|
+
return type === SOCK_FLOAT ? [value.float('value')] : value.floats('value');
|
|
106
|
+
}
|
|
107
|
+
const scalarOf = (input, fallback) => socketValue(input.socket)?.[0] ?? fallback;
|
|
108
|
+
/** The material's active output, the one Blender would render with EEVEE. */
|
|
109
|
+
function outputNode(graph) {
|
|
110
|
+
const outputs = graph.nodes.filter((node) => idname(node) === 'ShaderNodeOutputMaterial');
|
|
111
|
+
return (outputs.find((node) => (node.int('flag') & NODE_DO_OUTPUT) !== 0 && node.int('custom1') !== 2) ??
|
|
112
|
+
outputs.find((node) => node.int('custom1') !== 2) ??
|
|
113
|
+
null);
|
|
114
|
+
}
|
|
115
|
+
class Reader {
|
|
116
|
+
graph;
|
|
117
|
+
name;
|
|
118
|
+
approximations = [];
|
|
119
|
+
uvMap = null;
|
|
120
|
+
uvTransform = null;
|
|
121
|
+
uvSeen = false;
|
|
122
|
+
constructor(graph, name) {
|
|
123
|
+
this.graph = graph;
|
|
124
|
+
this.name = name;
|
|
125
|
+
}
|
|
126
|
+
note(what) {
|
|
127
|
+
this.approximations.push(`material "${this.name}": ${what}`);
|
|
128
|
+
}
|
|
129
|
+
/** A colour socket: a constant, an image, an image times a constant, or a colour attribute. */
|
|
130
|
+
colour(node, ids, label) {
|
|
131
|
+
const { socket, link } = this.graph.input(node, ...ids);
|
|
132
|
+
if (link === null)
|
|
133
|
+
return { map: null, factor: socketValue(socket), attribute: null };
|
|
134
|
+
return this.traceColour(link, label);
|
|
135
|
+
}
|
|
136
|
+
traceColour(link, label) {
|
|
137
|
+
const kind = idname(link.node);
|
|
138
|
+
if (kind === 'ShaderNodeTexImage')
|
|
139
|
+
return {
|
|
140
|
+
map: this.image(link.node, link.socket === 'Alpha' ? 'a' : 'rgb'),
|
|
141
|
+
factor: null,
|
|
142
|
+
attribute: null,
|
|
143
|
+
};
|
|
144
|
+
if (kind === 'ShaderNodeVertexColor' || kind === 'ShaderNodeAttribute') {
|
|
145
|
+
const storage = link.node.blockOf('storage') === null ? null : link.node.deref('storage');
|
|
146
|
+
const layer = storage?.has('layer_name') === true
|
|
147
|
+
? storage.string('layer_name')
|
|
148
|
+
: storage?.has('name') === true
|
|
149
|
+
? storage.string('name')
|
|
150
|
+
: '';
|
|
151
|
+
return { map: null, factor: null, attribute: layer };
|
|
152
|
+
}
|
|
153
|
+
if (kind === 'ShaderNodeMix' || kind === 'ShaderNodeMixRGB') {
|
|
154
|
+
/* The legacy node keeps its blend mode in `custom1`; its successor keeps mode and type in storage. */
|
|
155
|
+
const storage = kind === 'ShaderNodeMix' ? link.node.deref('storage', 'NodeShaderMix') : null;
|
|
156
|
+
const multiply = kind === 'ShaderNodeMixRGB'
|
|
157
|
+
? link.node.int('custom1') === MA_RAMP_MULT
|
|
158
|
+
: storage !== null &&
|
|
159
|
+
storage.int('data_type') === SOCK_RGBA &&
|
|
160
|
+
storage.int('blend_type') === MA_RAMP_MULT;
|
|
161
|
+
const fac = scalarOf(this.graph.input(link.node, 'Factor_Float', 'Fac'), 1);
|
|
162
|
+
if (multiply && fac === 1) {
|
|
163
|
+
const sides = [
|
|
164
|
+
this.graph.input(link.node, 'A_Color', 'Color1'),
|
|
165
|
+
this.graph.input(link.node, 'B_Color', 'Color2'),
|
|
166
|
+
].map((side) => side.link === null
|
|
167
|
+
? { map: null, factor: socketValue(side.socket), attribute: null }
|
|
168
|
+
: this.traceColour(side.link, label));
|
|
169
|
+
const maps = sides.filter((s) => s.map !== null);
|
|
170
|
+
if (maps.length > 1)
|
|
171
|
+
this.note(`its ${label} multiplies two images, and only the first is kept`);
|
|
172
|
+
const factors = sides.map((s) => s.factor).filter((f) => f !== null);
|
|
173
|
+
const factor = factors.length === 0 ? null : factors.reduce((a, b) => a.map((v, i) => v * (b[i] ?? 1)));
|
|
174
|
+
return {
|
|
175
|
+
map: maps[0]?.map ?? null,
|
|
176
|
+
factor,
|
|
177
|
+
attribute: sides.find((s) => s.attribute !== null)?.attribute ?? null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.note(`its ${label} comes from ${kind}, which glTF cannot express, so the socket's own value is used`);
|
|
182
|
+
return { map: null, factor: null, attribute: null };
|
|
183
|
+
}
|
|
184
|
+
/** A scalar socket: a constant, or one channel of an image times a constant. */
|
|
185
|
+
scalar(node, ids, label, fallback) {
|
|
186
|
+
const input = this.graph.input(node, ...ids);
|
|
187
|
+
if (input.link === null)
|
|
188
|
+
return { value: scalarOf(input, fallback), map: null };
|
|
189
|
+
return this.traceScalar(input.link, label, fallback);
|
|
190
|
+
}
|
|
191
|
+
traceScalar(link, label, fallback) {
|
|
192
|
+
const kind = idname(link.node);
|
|
193
|
+
if (kind === 'ShaderNodeSeparateColor' || kind === 'ShaderNodeSeparateRGB') {
|
|
194
|
+
const channel = { Red: 'r', R: 'r', Green: 'g', G: 'g', Blue: 'b', B: 'b' }[link.socket];
|
|
195
|
+
const source = this.graph.input(link.node, 'Color', 'Image').link;
|
|
196
|
+
if (channel !== undefined &&
|
|
197
|
+
source !== null &&
|
|
198
|
+
idname(source.node) === 'ShaderNodeTexImage') {
|
|
199
|
+
return { value: 1, map: this.image(source.node, channel) };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (kind === 'ShaderNodeTexImage') {
|
|
203
|
+
/* A grey image straight into a scalar: its colour is read as one channel, the first. */
|
|
204
|
+
return { value: 1, map: this.image(link.node, link.socket === 'Alpha' ? 'a' : 'r') };
|
|
205
|
+
}
|
|
206
|
+
else if (kind === 'ShaderNodeMath' && link.node.int('custom1') === MATH_MULTIPLY) {
|
|
207
|
+
const a = this.graph.at(link.node, 0);
|
|
208
|
+
const b = this.graph.at(link.node, 1);
|
|
209
|
+
const constant = a.link === null ? a : b.link === null ? b : null;
|
|
210
|
+
const other = constant === a ? b : a;
|
|
211
|
+
if (constant !== null && other.link !== null) {
|
|
212
|
+
const inner = this.traceScalar(other.link, label, fallback);
|
|
213
|
+
return { value: inner.value * scalarOf(constant, 1), map: inner.map };
|
|
214
|
+
}
|
|
215
|
+
if (a.link === null && b.link === null)
|
|
216
|
+
return { value: scalarOf(a, 1) * scalarOf(b, 1), map: null };
|
|
217
|
+
}
|
|
218
|
+
this.note(`its ${label} comes from ${kind}, which glTF cannot express, so the socket's own value is used`);
|
|
219
|
+
return { value: fallback, map: null };
|
|
220
|
+
}
|
|
221
|
+
/** An image node's picture, and the UV map and transform it is placed by. */
|
|
222
|
+
image(node, channel) {
|
|
223
|
+
const image = node.deref('id');
|
|
224
|
+
if (image === null)
|
|
225
|
+
return null;
|
|
226
|
+
const placement = this.placement(this.graph.input(node, 'Vector').link, image.idName());
|
|
227
|
+
if (!this.uvSeen) {
|
|
228
|
+
this.uvSeen = true;
|
|
229
|
+
this.uvMap = placement.map;
|
|
230
|
+
this.uvTransform = placement.transform;
|
|
231
|
+
}
|
|
232
|
+
else if (placement.map !== this.uvMap ||
|
|
233
|
+
JSON.stringify(placement.transform) !== JSON.stringify(this.uvTransform)) {
|
|
234
|
+
this.note(`image "${image.idName()}" is placed differently from the material's other images, and glTF gives a material one placement`);
|
|
235
|
+
}
|
|
236
|
+
return { image, channel };
|
|
237
|
+
}
|
|
238
|
+
placement(link, image) {
|
|
239
|
+
if (link === null)
|
|
240
|
+
return { map: null, transform: null };
|
|
241
|
+
const kind = idname(link.node);
|
|
242
|
+
if (kind === 'ShaderNodeTexCoord' && link.socket === 'UV')
|
|
243
|
+
return { map: null, transform: null };
|
|
244
|
+
if (kind === 'ShaderNodeUVMap') {
|
|
245
|
+
const storage = link.node.deref('storage', 'NodeShaderUVMap');
|
|
246
|
+
const name = storage?.string('uv_map') ?? '';
|
|
247
|
+
return { map: name === '' ? null : name, transform: null };
|
|
248
|
+
}
|
|
249
|
+
if (kind === 'ShaderNodeMapping') {
|
|
250
|
+
const inner = this.placement(this.graph.input(link.node, 'Vector').link, image);
|
|
251
|
+
const vector = (id, fallback) => {
|
|
252
|
+
const input = this.graph.input(link.node, id);
|
|
253
|
+
if (input.link !== null)
|
|
254
|
+
this.note(`image "${image}" is placed by a Mapping whose ${id} is driven by a node, which is ignored`);
|
|
255
|
+
return socketValue(input.socket)?.slice(0, 3) ?? fallback;
|
|
256
|
+
};
|
|
257
|
+
const mode = link.node.int('custom1');
|
|
258
|
+
let matrix = compose(vector('Location', [0, 0, 0]), eulerMatrix(vector('Rotation', [0, 0, 0]), 1), vector('Scale', [1, 1, 1]));
|
|
259
|
+
if (mode === MAPPING_TEXTURE)
|
|
260
|
+
matrix = invert(matrix);
|
|
261
|
+
else if (mode !== MAPPING_POINT) {
|
|
262
|
+
this.note(`image "${image}" is placed by a Mapping of a kind that only turns directions, which is ignored`);
|
|
263
|
+
return inner;
|
|
264
|
+
}
|
|
265
|
+
if (inner.transform !== null)
|
|
266
|
+
matrix = multiply(matrix, from3(inner.transform));
|
|
267
|
+
return {
|
|
268
|
+
map: inner.map,
|
|
269
|
+
transform: [
|
|
270
|
+
matrix[0],
|
|
271
|
+
matrix[1],
|
|
272
|
+
0,
|
|
273
|
+
matrix[4],
|
|
274
|
+
matrix[5],
|
|
275
|
+
0,
|
|
276
|
+
matrix[12],
|
|
277
|
+
matrix[13],
|
|
278
|
+
1,
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
this.note(`image "${image}" is placed by ${kind}, which is ignored`);
|
|
283
|
+
return { map: null, transform: null };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** A 3x3 UV affine as the 4x4 it is a slice of. */
|
|
287
|
+
function from3(t) {
|
|
288
|
+
return [
|
|
289
|
+
t[0],
|
|
290
|
+
t[1],
|
|
291
|
+
0,
|
|
292
|
+
0,
|
|
293
|
+
t[3],
|
|
294
|
+
t[4],
|
|
295
|
+
0,
|
|
296
|
+
0,
|
|
297
|
+
0,
|
|
298
|
+
0,
|
|
299
|
+
1,
|
|
300
|
+
0,
|
|
301
|
+
t[6],
|
|
302
|
+
t[7],
|
|
303
|
+
0,
|
|
304
|
+
1,
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
/** Read one material's surface. */
|
|
308
|
+
export function readBlendSurface(material, version) {
|
|
309
|
+
const name = material.idName();
|
|
310
|
+
const blendFlag = material.has('blend_flag') ? material.int('blend_flag') : 0;
|
|
311
|
+
const surface = {
|
|
312
|
+
name,
|
|
313
|
+
baseColor: [0.8, 0.8, 0.8],
|
|
314
|
+
baseColorMap: null,
|
|
315
|
+
alpha: 1,
|
|
316
|
+
alphaMap: null,
|
|
317
|
+
alphaMode: 'OPAQUE',
|
|
318
|
+
alphaCutoff: 0.5,
|
|
319
|
+
vertexColor: null,
|
|
320
|
+
metallic: 0,
|
|
321
|
+
metallicMap: null,
|
|
322
|
+
roughness: 0.5,
|
|
323
|
+
roughnessMap: null,
|
|
324
|
+
occlusionMap: null,
|
|
325
|
+
normalMap: null,
|
|
326
|
+
normalStrength: 1,
|
|
327
|
+
emission: [0, 0, 0],
|
|
328
|
+
emissionStrength: 1,
|
|
329
|
+
emissionMap: null,
|
|
330
|
+
transmission: 0,
|
|
331
|
+
doubleSided: (blendFlag & MA_BL_CULL_BACKFACE) === 0,
|
|
332
|
+
uvMap: null,
|
|
333
|
+
uvTransform: null,
|
|
334
|
+
extras: customProperties(material),
|
|
335
|
+
approximations: [],
|
|
336
|
+
};
|
|
337
|
+
const tree = material.has('nodetree') ? material.deref('nodetree') : null;
|
|
338
|
+
const useNodes = !material.has('use_nodes') || material.int('use_nodes') !== 0 || version >= 500;
|
|
339
|
+
if (tree === null || !useNodes) {
|
|
340
|
+
surface.baseColor = [material.float('r'), material.float('g'), material.float('b')];
|
|
341
|
+
surface.alpha = material.has('a') ? material.float('a') : 1;
|
|
342
|
+
if (material.has('metallic'))
|
|
343
|
+
surface.metallic = material.float('metallic');
|
|
344
|
+
if (material.has('roughness'))
|
|
345
|
+
surface.roughness = material.float('roughness');
|
|
346
|
+
return surface;
|
|
347
|
+
}
|
|
348
|
+
const graph = new Graph(tree);
|
|
349
|
+
const reader = new Reader(graph, name);
|
|
350
|
+
const output = outputNode(graph);
|
|
351
|
+
const surfaceLink = output === null ? null : graph.input(output, 'Surface').link;
|
|
352
|
+
if (surfaceLink === null) {
|
|
353
|
+
reader.note("no surface shader is connected, so it takes glTF's default");
|
|
354
|
+
return { ...surface, approximations: reader.approximations };
|
|
355
|
+
}
|
|
356
|
+
const shader = surfaceLink.node;
|
|
357
|
+
const kind = idname(shader);
|
|
358
|
+
if ((shader.int('flag') & NODE_MUTED) !== 0)
|
|
359
|
+
reader.note(`its ${kind} is muted`);
|
|
360
|
+
if (kind === 'ShaderNodeBsdfPrincipled' || kind === 'ShaderNodeBsdfDiffuse') {
|
|
361
|
+
const base = reader.colour(shader, ['Base Color', 'Color'], 'Base Color');
|
|
362
|
+
const factor = base.factor ??
|
|
363
|
+
(base.map !== null || base.attribute !== null ? [1, 1, 1, 1] : [0.8, 0.8, 0.8, 1]);
|
|
364
|
+
surface.baseColor = [factor[0] ?? 1, factor[1] ?? 1, factor[2] ?? 1];
|
|
365
|
+
surface.baseColorMap = base.map;
|
|
366
|
+
surface.vertexColor = base.attribute;
|
|
367
|
+
if (kind === 'ShaderNodeBsdfPrincipled') {
|
|
368
|
+
const metallic = reader.scalar(shader, ['Metallic'], 'Metallic', 0);
|
|
369
|
+
surface.metallic = metallic.value;
|
|
370
|
+
surface.metallicMap = metallic.map;
|
|
371
|
+
}
|
|
372
|
+
const roughness = reader.scalar(shader, ['Roughness'], 'Roughness', 0.5);
|
|
373
|
+
surface.roughness = roughness.value;
|
|
374
|
+
surface.roughnessMap = roughness.map;
|
|
375
|
+
const normal = graph.input(shader, 'Normal').link;
|
|
376
|
+
if (normal !== null) {
|
|
377
|
+
if (idname(normal.node) === 'ShaderNodeNormalMap') {
|
|
378
|
+
const map = reader.colour(normal.node, ['Color'], 'Normal Map');
|
|
379
|
+
surface.normalMap = map.map;
|
|
380
|
+
surface.normalStrength = scalarOf(graph.input(normal.node, 'Strength'), 1);
|
|
381
|
+
if (normal.node.int('custom1') !== 0)
|
|
382
|
+
reader.note('its normal map is not in tangent space, which glTF requires');
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
reader.note(`its Normal input comes from ${idname(normal.node)}, which glTF has no slot for`);
|
|
386
|
+
}
|
|
387
|
+
const emission = reader.colour(shader, ['Emission Color', 'Emission'], 'Emission');
|
|
388
|
+
const strength = graph.input(shader, 'Emission Strength');
|
|
389
|
+
surface.emissionStrength = strength.socket === null ? 1 : scalarOf(strength, 1);
|
|
390
|
+
if (strength.link !== null)
|
|
391
|
+
reader.note('its Emission Strength is driven by a node, so its value is used');
|
|
392
|
+
surface.emission = toRgb(emission.factor ?? (emission.map !== null ? [1, 1, 1] : [0, 0, 0]));
|
|
393
|
+
surface.emissionMap = emission.map;
|
|
394
|
+
const transmission = graph.input(shader, 'Transmission Weight', 'Transmission');
|
|
395
|
+
if (transmission.link !== null)
|
|
396
|
+
reader.note('its Transmission is driven by a node, so it is treated as opaque');
|
|
397
|
+
else
|
|
398
|
+
surface.transmission = scalarOf(transmission, 0);
|
|
399
|
+
alphaOf(shader, graph, reader, material, version, surface);
|
|
400
|
+
occlusionOf(graph, reader, output, surface);
|
|
401
|
+
}
|
|
402
|
+
else if (kind === 'ShaderNodeEmission') {
|
|
403
|
+
const emission = reader.colour(shader, ['Color'], 'Emission');
|
|
404
|
+
surface.baseColor = [0, 0, 0];
|
|
405
|
+
surface.emission = toRgb(emission.factor ?? [1, 1, 1]);
|
|
406
|
+
surface.emissionMap = emission.map;
|
|
407
|
+
surface.emissionStrength = scalarOf(graph.input(shader, 'Strength'), 1);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
reader.note(`its surface is a ${kind}, which glTF cannot express, so it takes glTF's default`);
|
|
411
|
+
}
|
|
412
|
+
surface.uvMap = reader.uvMap;
|
|
413
|
+
surface.uvTransform = reader.uvTransform;
|
|
414
|
+
surface.approximations = reader.approximations;
|
|
415
|
+
return surface;
|
|
416
|
+
}
|
|
417
|
+
const toRgb = (v) => [v[0] ?? 0, v[1] ?? 0, v[2] ?? 0];
|
|
418
|
+
/** Occlusion from the "glTF Material Output" group Blender's importer adds, wherever it is wired. */
|
|
419
|
+
function occlusionOf(graph, reader, output, surface) {
|
|
420
|
+
void output;
|
|
421
|
+
for (const node of graph.nodes) {
|
|
422
|
+
if (idname(node) !== 'ShaderNodeGroup')
|
|
423
|
+
continue;
|
|
424
|
+
const tree = node.deref('id');
|
|
425
|
+
const groupName = tree?.idName() ?? '';
|
|
426
|
+
if (!groupName.startsWith('glTF Material Output') && !groupName.startsWith('glTF Settings'))
|
|
427
|
+
continue;
|
|
428
|
+
const input = graph.input(node, 'Occlusion', 'Socket_0');
|
|
429
|
+
if (input.link !== null)
|
|
430
|
+
surface.occlusionMap = reader.traceScalar(input.link, 'Occlusion', 1).map;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function alphaOf(shader, graph, reader, material, version, surface) {
|
|
434
|
+
const alpha = graph.input(shader, 'Alpha');
|
|
435
|
+
let cutoff = null;
|
|
436
|
+
let source = alpha.link;
|
|
437
|
+
if (source !== null && idname(source.node) === 'ShaderNodeMath') {
|
|
438
|
+
const clip = alphaClip(source.node, graph);
|
|
439
|
+
if (clip !== null) {
|
|
440
|
+
cutoff = clip.cutoff;
|
|
441
|
+
source = clip.source;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (source === null)
|
|
445
|
+
surface.alpha = alpha.link === null ? scalarOf(alpha, 1) : 1;
|
|
446
|
+
else {
|
|
447
|
+
const traced = reader.traceScalar(source, 'Alpha', 1);
|
|
448
|
+
surface.alpha = traced.value;
|
|
449
|
+
surface.alphaMap = traced.map;
|
|
450
|
+
}
|
|
451
|
+
if (version < 402) {
|
|
452
|
+
/* Before 4.2 the mode was the material's own setting, and that is what Blender converts. */
|
|
453
|
+
const method = material.has('blend_method') ? material.int('blend_method') : MA_BM_SOLID;
|
|
454
|
+
if (method === MA_BM_SOLID) {
|
|
455
|
+
surface.alpha = 1;
|
|
456
|
+
surface.alphaMap = null;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (method === MA_BM_CLIP)
|
|
460
|
+
cutoff = material.has('alpha_threshold') ? material.float('alpha_threshold') : 0.5;
|
|
461
|
+
}
|
|
462
|
+
const varies = surface.alphaMap !== null || surface.alpha < 1;
|
|
463
|
+
if (!varies && cutoff === null)
|
|
464
|
+
return;
|
|
465
|
+
const dithered = version >= 402 &&
|
|
466
|
+
material.has('surface_render_method') &&
|
|
467
|
+
material.int('surface_render_method') !== RENDER_BLENDED;
|
|
468
|
+
if (cutoff !== null) {
|
|
469
|
+
surface.alphaMode = 'MASK';
|
|
470
|
+
surface.alphaCutoff = cutoff;
|
|
471
|
+
}
|
|
472
|
+
else if (dithered && surface.alphaMap !== null) {
|
|
473
|
+
/*
|
|
474
|
+
* **A dithered surface with an alpha map is a cut-out**, where Blender's glTF exporter writes
|
|
475
|
+
* it as blended. EEVEE draws dithered alpha as a screen door — each pixel drawn fully or not at
|
|
476
|
+
* all — so the surface is opaque wherever it shows, which an alpha test at the middle is, and a
|
|
477
|
+
* blend is not: a facade whose mask cuts its gaps would be sorted, drawn over and seen through.
|
|
478
|
+
* A deliberate difference from the exporter, for what the surface looks like in Blender.
|
|
479
|
+
*/
|
|
480
|
+
surface.alphaMode = 'MASK';
|
|
481
|
+
surface.alphaCutoff = 0.5;
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
surface.alphaMode = 'BLEND';
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* An alpha clip built from Math nodes, in the four shapes Blender's exporter knows: `round(x)`,
|
|
489
|
+
* `x > c`, `c < x`, and `1 - (x < c)`. Its cutoff and the link to `x`, or null.
|
|
490
|
+
*/
|
|
491
|
+
function alphaClip(node, graph) {
|
|
492
|
+
const operation = node.int('custom1');
|
|
493
|
+
const a = graph.at(node, 0);
|
|
494
|
+
const b = graph.at(node, 1);
|
|
495
|
+
if (operation === MATH_ROUND)
|
|
496
|
+
return { cutoff: 0.5, source: a.link };
|
|
497
|
+
if (operation === MATH_GREATER_THAN && b.link === null)
|
|
498
|
+
return { cutoff: scalarOf(b, 0.5), source: a.link };
|
|
499
|
+
if (operation === MATH_LESS_THAN && a.link === null)
|
|
500
|
+
return { cutoff: scalarOf(a, 0.5), source: b.link };
|
|
501
|
+
if (operation === MATH_SUBTRACT &&
|
|
502
|
+
a.link === null &&
|
|
503
|
+
scalarOf(a, 0) === 1 &&
|
|
504
|
+
b.link !== null &&
|
|
505
|
+
idname(b.link.node) === 'ShaderNodeMath') {
|
|
506
|
+
const inner = b.link.node;
|
|
507
|
+
const op = inner.int('custom1');
|
|
508
|
+
const x = graph.at(inner, 0);
|
|
509
|
+
const y = graph.at(inner, 1);
|
|
510
|
+
if (op === MATH_LESS_THAN && x.link !== null && y.link === null)
|
|
511
|
+
return { cutoff: scalarOf(y, 0.5), source: x.link };
|
|
512
|
+
if (op === MATH_GREATER_THAN && x.link === null && y.link !== null)
|
|
513
|
+
return { cutoff: scalarOf(x, 0.5), source: y.link };
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
/** An ID's custom properties that are plain values, for glTF's `extras`. */
|
|
518
|
+
export function customProperties(id) {
|
|
519
|
+
const header = id.sub('id');
|
|
520
|
+
if (!header.has('properties'))
|
|
521
|
+
return null;
|
|
522
|
+
const group = header.deref('properties', 'IDProperty');
|
|
523
|
+
if (group === null)
|
|
524
|
+
return null;
|
|
525
|
+
const out = {};
|
|
526
|
+
for (const property of group.sub('data').list('group', 'IDProperty')) {
|
|
527
|
+
const name = property.string('name');
|
|
528
|
+
const type = property.int('type');
|
|
529
|
+
const data = property.sub('data');
|
|
530
|
+
/* IDP_STRING 0, IDP_INT 1, IDP_FLOAT 2, IDP_DOUBLE 8, IDP_BOOLEAN 10. */
|
|
531
|
+
if (type === 0)
|
|
532
|
+
out[name] = property.file.text(data.ptr('pointer'), property.scope);
|
|
533
|
+
else if (type === 1)
|
|
534
|
+
out[name] = data.int('val');
|
|
535
|
+
else if (type === 10)
|
|
536
|
+
out[name] = data.int('val') !== 0;
|
|
537
|
+
else if (type === 2)
|
|
538
|
+
out[name] = new DataView(new Int32Array([data.int('val')]).buffer).getFloat32(0, true);
|
|
539
|
+
else if (type === 8) {
|
|
540
|
+
const bits = new Int32Array([data.int('val'), data.int('val2')]);
|
|
541
|
+
out[name] = new DataView(bits.buffer).getFloat64(0, true);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
545
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The little matrix arithmetic a `.blend` scene needs, in the conventions Blender stores it in.
|
|
3
|
+
*
|
|
4
|
+
* Column-major 4x4 as plain number arrays, which is both how Blender lays out `float[4][4]` in a
|
|
5
|
+
* file and how glTF writes `matrix`, so a matrix read from one is written to the other without a
|
|
6
|
+
* transpose. Doubles throughout: these run once per object at import, and a parent chain composes
|
|
7
|
+
* enough of them that single precision drifts visibly on a deep rig.
|
|
8
|
+
*
|
|
9
|
+
* **Every rotation here is Blender's definition, not a generic one**, because a rotation mode is a
|
|
10
|
+
* claim about order: Blender's `XYZ` turns about X first, so its matrix is `Rz · Ry · Rx`. The
|
|
11
|
+
* fixtures under `scripts/fixtures/blend` hold an object in each of the eight modes and Blender's
|
|
12
|
+
* own export of where they ended up.
|
|
13
|
+
*/
|
|
14
|
+
export type Mat4 = number[];
|
|
15
|
+
export type Mat3 = number[];
|
|
16
|
+
export declare const identity: () => Mat4;
|
|
17
|
+
export declare function multiply(a: Mat4, b: Mat4): Mat4;
|
|
18
|
+
export declare function multiply3(a: Mat3, b: Mat3): Mat3;
|
|
19
|
+
export declare function invert(m: Mat4): Mat4;
|
|
20
|
+
export declare function translation(x: number, y: number, z: number): Mat4;
|
|
21
|
+
/** Rotation and scale in a 3x3, translation beside it: `T · R · S`. */
|
|
22
|
+
export declare function compose(location: readonly number[], rotation: Mat3, scale: readonly number[]): Mat4;
|
|
23
|
+
export declare function eulerMatrix(e: readonly number[], mode: number): Mat3;
|
|
24
|
+
/** A quaternion as Blender stores it, `w` first, normalised before use as Blender does. */
|
|
25
|
+
export declare function quaternionMatrix(q: readonly number[]): Mat3;
|
|
26
|
+
export declare function axisAngleMatrix(direction: readonly number[], angle: number): Mat3;
|
|
27
|
+
/** Transform a point by a column-major 4x4. */
|
|
28
|
+
export declare function transformPoint(m: Mat4, x: number, y: number, z: number): [number, number, number];
|