@driftengine/assets 3.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/assetPath.d.ts +51 -0
- package/dist/assetPath.js +59 -0
- package/dist/coarseLevel.d.ts +94 -0
- package/dist/coarseLevel.js +606 -0
- package/dist/dds.d.ts +41 -0
- package/dist/dds.js +425 -0
- package/dist/drawKey.d.ts +64 -0
- package/dist/drawKey.js +42 -0
- package/dist/drftLoader.d.ts +420 -0
- package/dist/drftLoader.js +996 -0
- package/dist/drftTextures.d.ts +87 -0
- package/dist/drftTextures.js +138 -0
- package/dist/fbx.d.ts +86 -0
- package/dist/fbx.js +792 -0
- package/dist/fbxHarness.d.ts +68 -0
- package/dist/fbxHarness.js +185 -0
- package/dist/fbxInflate.d.ts +60 -0
- package/dist/fbxInflate.js +204 -0
- package/dist/fbxMaterial.d.ts +87 -0
- package/dist/fbxMaterial.js +345 -0
- package/dist/fbxRig.d.ts +130 -0
- package/dist/fbxRig.js +601 -0
- package/dist/gltf.d.ts +242 -0
- package/dist/gltf.js +904 -0
- package/dist/gltfAccessor.d.ts +2 -0
- package/dist/gltfAccessor.js +106 -0
- package/dist/gltfSkin.d.ts +32 -0
- package/dist/gltfSkin.js +144 -0
- package/dist/iesPhotometry.d.ts +44 -0
- package/dist/iesPhotometry.js +94 -0
- package/dist/imageInfo.d.ts +31 -0
- package/dist/imageInfo.js +119 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +132 -0
- package/dist/kn5.d.ts +158 -0
- package/dist/kn5.js +557 -0
- package/dist/loadProgress.d.ts +118 -0
- package/dist/loadProgress.js +1 -0
- package/dist/localise.d.ts +35 -0
- package/dist/localise.js +253 -0
- package/dist/obj.d.ts +54 -0
- package/dist/obj.js +335 -0
- package/dist/orient.d.ts +104 -0
- package/dist/orient.js +241 -0
- package/dist/radianceHdr.d.ts +36 -0
- package/dist/radianceHdr.js +172 -0
- package/dist/readModel.d.ts +153 -0
- package/dist/readModel.js +386 -0
- package/dist/recognise.d.ts +38 -0
- package/dist/recognise.js +97 -0
- package/dist/stl.d.ts +22 -0
- package/dist/stl.js +131 -0
- package/dist/tangentOrder.d.ts +24 -0
- package/dist/tangentOrder.js +31 -0
- package/dist/threemf.d.ts +46 -0
- package/dist/threemf.js +254 -0
- package/dist/uploadBudget.d.ts +82 -0
- package/dist/uploadBudget.js +86 -0
- package/dist/usd.d.ts +49 -0
- package/dist/usd.js +542 -0
- package/dist/weld.d.ts +44 -0
- package/dist/weld.js +237 -0
- package/dist/zip.d.ts +35 -0
- package/dist/zip.js +110 -0
- package/package.json +61 -0
- package/src/assetPath.ts +79 -0
- package/src/coarseLevel.ts +720 -0
- package/src/dds.ts +507 -0
- package/src/drawKey.ts +91 -0
- package/src/drftLoader.ts +1197 -0
- package/src/drftTextures.ts +153 -0
- package/src/fbx.ts +934 -0
- package/src/fbxHarness.ts +215 -0
- package/src/fbxInflate.ts +258 -0
- package/src/fbxMaterial.ts +410 -0
- package/src/fbxRig.ts +652 -0
- package/src/gltf.ts +1161 -0
- package/src/gltfAccessor.ts +127 -0
- package/src/gltfSkin.ts +210 -0
- package/src/iesPhotometry.ts +136 -0
- package/src/imageInfo.ts +130 -0
- package/src/index.ts +162 -0
- package/src/kn5.ts +705 -0
- package/src/loadProgress.ts +131 -0
- package/src/localise.ts +275 -0
- package/src/obj.ts +386 -0
- package/src/orient.ts +262 -0
- package/src/radianceHdr.ts +195 -0
- package/src/readModel.ts +515 -0
- package/src/recognise.ts +124 -0
- package/src/stl.ts +147 -0
- package/src/tangentOrder.ts +32 -0
- package/src/threemf.ts +303 -0
- package/src/uploadBudget.ts +90 -0
- package/src/usd.ts +642 -0
- package/src/weld.ts +264 -0
- package/src/zip.ts +130 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FBX materials and the textures they name. One responsibility: what a surface looks like.
|
|
3
|
+
*
|
|
4
|
+
* Split from `fbx.ts` because reading a record tree and interpreting a shading model are
|
|
5
|
+
* different jobs with different ways of being wrong. The container either parses or it does
|
|
6
|
+
* not; a material silently comes out the wrong colour.
|
|
7
|
+
*
|
|
8
|
+
* **This resolves nothing from disk.** A texture is reported as the string the file wrote,
|
|
9
|
+
* and turning that into bytes belongs to the caller, which is the only party that knows
|
|
10
|
+
* whether it is reading a folder, a zip or a URL. The same reasoning as the injected
|
|
11
|
+
* `Inflate` in `fbx.ts`, for the same reason: this module is part of an engine that runs in
|
|
12
|
+
* a browser and must not reach for a filesystem.
|
|
13
|
+
*
|
|
14
|
+
* **Why the declared path is the only one worth keeping.** These records carry an absolute
|
|
15
|
+
* `FileName` as well, and it is the author's own machine: the asset this was built against
|
|
16
|
+
* says `G:\SketchfabRipper_v1.18.0-b2\tools\blender-292\_sfTemp\textures\carbon.jpg`. That
|
|
17
|
+
* path is dead everywhere except one computer that no longer has the file either. The
|
|
18
|
+
* `RelativeFilename` beside it says `textures\carbon.jpg`, which is true relative to the
|
|
19
|
+
* model and is what every other reader uses.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { FbxNode } from './fbx.ts';
|
|
23
|
+
import type { AssetReference } from './assetPath.ts';
|
|
24
|
+
|
|
25
|
+
/** A surface, in the terms this engine's vertex attributes speak. */
|
|
26
|
+
export interface FbxMaterial {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly color: readonly [number, number, number];
|
|
29
|
+
/** Highlight strength, 0 to 1. */
|
|
30
|
+
readonly specular: number;
|
|
31
|
+
/** Highlight width, 0 to 1, where 0 is a mirror. */
|
|
32
|
+
readonly roughness: number;
|
|
33
|
+
/** 1 is opaque. FBX states the inverse, when it states it at all. */
|
|
34
|
+
readonly opacity: number;
|
|
35
|
+
/** Self-illumination, 0 to 1. */
|
|
36
|
+
readonly emissive: number;
|
|
37
|
+
readonly emissiveColor: readonly [number, number, number];
|
|
38
|
+
/** Index into `FbxMaterials.textures` for the albedo map, or -1 for an untextured surface. */
|
|
39
|
+
readonly albedo: number;
|
|
40
|
+
/** How much of the environment this surface mirrors, from `ReflectionFactor`. */
|
|
41
|
+
readonly reflectivity: number;
|
|
42
|
+
/**
|
|
43
|
+
* The three map indices `DrftMaterial` carries, all -1 here.
|
|
44
|
+
*
|
|
45
|
+
* This reader resolves no maps but a normal, ORM or emissive one; an FBX that embeds them names
|
|
46
|
+
* them in its own texture connections and wiring that up is its own piece of work. The fields
|
|
47
|
+
* exist because `fbx.ts` pushes an `FbxMaterial` straight into a `DrftMaterial[]`, so the two
|
|
48
|
+
* shapes have to agree.
|
|
49
|
+
*/
|
|
50
|
+
readonly normalMap: number;
|
|
51
|
+
readonly ormMap: number;
|
|
52
|
+
readonly emissiveMap: number;
|
|
53
|
+
/** The scales that go with an `ormMap`. Identity here, since there is never one. */
|
|
54
|
+
readonly roughnessScale: number;
|
|
55
|
+
readonly metallicScale: number;
|
|
56
|
+
readonly occlusionStrength: number;
|
|
57
|
+
/** Zero: an FBX material states no alpha test, so nothing is discarded. */
|
|
58
|
+
readonly cutout: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FbxMaterials {
|
|
62
|
+
readonly byId: ReadonlyMap<number, FbxMaterial>;
|
|
63
|
+
/**
|
|
64
|
+
* Model id to its materials, in the order the file connected them.
|
|
65
|
+
*
|
|
66
|
+
* That order is load-bearing: `LayerElementMaterial` stores an *index into this list*,
|
|
67
|
+
* not an object id, so reordering it repaints the model.
|
|
68
|
+
*/
|
|
69
|
+
readonly ofModel: ReadonlyMap<number, readonly number[]>;
|
|
70
|
+
/** Every texture the file names, with bytes where it embedded them itself. */
|
|
71
|
+
readonly textures: readonly AssetReference[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The engine's own defaults, for a property a material does not state. */
|
|
75
|
+
const DEFAULT_COLOR: readonly [number, number, number] = [0.8, 0.8, 0.8];
|
|
76
|
+
const DEFAULT_ROUGHNESS = 0.4277;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* What a surface with no material at all is shaded as.
|
|
80
|
+
*
|
|
81
|
+
* Exported so the geometry reader has no null to branch on. A geometry can genuinely reach
|
|
82
|
+
* the reader wearing nothing, and a fallback stated once here is better than the same three
|
|
83
|
+
* defaults spelled out at each use, which is how two of them end up disagreeing.
|
|
84
|
+
*/
|
|
85
|
+
export const FALLBACK_MATERIAL: FbxMaterial = {
|
|
86
|
+
name: '',
|
|
87
|
+
color: DEFAULT_COLOR,
|
|
88
|
+
specular: 0,
|
|
89
|
+
roughness: DEFAULT_ROUGHNESS,
|
|
90
|
+
emissive: 0,
|
|
91
|
+
opacity: 1,
|
|
92
|
+
reflectivity: 0,
|
|
93
|
+
normalMap: -1,
|
|
94
|
+
ormMap: -1,
|
|
95
|
+
emissiveMap: -1,
|
|
96
|
+
roughnessScale: 1,
|
|
97
|
+
metallicScale: 1,
|
|
98
|
+
occlusionStrength: 0,
|
|
99
|
+
cutout: 0,
|
|
100
|
+
emissiveColor: [-1, -1, -1],
|
|
101
|
+
albedo: -1,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** A record's first property, when it is a string. */
|
|
105
|
+
function stringChild(node: FbxNode, name: string): string | undefined {
|
|
106
|
+
const value = node.children.find((child) => child.name === name)?.properties[0];
|
|
107
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The bytes of an embedded image, if this video carries one.
|
|
112
|
+
*
|
|
113
|
+
* A `Content` record with nothing in it is common and means "this image lives outside the
|
|
114
|
+
* file" rather than "this image is empty", so a zero length payload has to read as absent.
|
|
115
|
+
* Treating it as present embeds a zero byte texture, which decodes to nothing and takes the
|
|
116
|
+
* place of the path that would have worked.
|
|
117
|
+
*/
|
|
118
|
+
function contentOf(video: FbxNode): Uint8Array | undefined {
|
|
119
|
+
const value = video.children.find((child) => child.name === 'Content')?.properties[0];
|
|
120
|
+
return value instanceof Uint8Array && value.length > 0 ? value : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function propertiesOf(node: FbxNode): Map<string, unknown[]> {
|
|
124
|
+
const out = new Map<string, unknown[]>();
|
|
125
|
+
for (const child of node.children) {
|
|
126
|
+
if (child.name !== 'Properties70') continue;
|
|
127
|
+
for (const property of child.children) {
|
|
128
|
+
const name = property.properties[0];
|
|
129
|
+
if (typeof name === 'string') out.set(name, property.properties.slice(4));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function numberAt(values: unknown[] | undefined, at: number, fallback: number): number {
|
|
136
|
+
const value = values?.[at];
|
|
137
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function triple(
|
|
141
|
+
values: unknown[] | undefined,
|
|
142
|
+
fallback: readonly [number, number, number],
|
|
143
|
+
scale = 1,
|
|
144
|
+
): [number, number, number] {
|
|
145
|
+
if (values === undefined) return [fallback[0], fallback[1], fallback[2]];
|
|
146
|
+
return normaliseColor([
|
|
147
|
+
numberAt(values, 0, fallback[0]) * scale,
|
|
148
|
+
numberAt(values, 1, fallback[1]) * scale,
|
|
149
|
+
numberAt(values, 2, fallback[2]) * scale,
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A colour whose components exceed one is a fraction that lost its decimal point.
|
|
155
|
+
*
|
|
156
|
+
* Measured in the asset this was built against, which stores `silver` as
|
|
157
|
+
* `815686, 815686, 815686` and `copper` as `235294, 184314, 137255`. Those are not colours
|
|
158
|
+
* and they are not a scale factor either: they are `0.815686` and `0.235294, 0.184314,
|
|
159
|
+
* 0.137255` written as their fractional digits alone. Every one is six digits, which is what
|
|
160
|
+
* makes the repair well defined rather than a guess, and `calipers` proves the reading —
|
|
161
|
+
* it stores `8, 564706, 160784`, where the `8` is `000008` with its leading zeros lost the
|
|
162
|
+
* moment it was read as a number.
|
|
163
|
+
*
|
|
164
|
+
* **Each channel is divided by its own digit count**, because each is its own fraction. That
|
|
165
|
+
* is settled by the asset rather than argued: read this way the calipers come out
|
|
166
|
+
* `0.8, 0.564706, 0.160784`, a gold, and the vendor's own reference render shows gold brake
|
|
167
|
+
* calipers. A shared divisor taken from the largest channel was tried first, on the theory
|
|
168
|
+
* that the `8` was `000008` with its leading zeros lost, and it produces a green caliper.
|
|
169
|
+
* Looking at the reference is what decided it, which is the whole lesson of this pipeline.
|
|
170
|
+
*
|
|
171
|
+
* A component above one is never legitimate here: FBX's `DiffuseColor` is defined over nought
|
|
172
|
+
* to one, and this engine has no high dynamic range material input for it to mean instead. So
|
|
173
|
+
* the test cannot fire on a colour somebody meant.
|
|
174
|
+
*/
|
|
175
|
+
function normaliseColor(color: [number, number, number]): [number, number, number] {
|
|
176
|
+
const digits = (value: number): number => {
|
|
177
|
+
if (!(value > 1)) return value;
|
|
178
|
+
let divisor = 1;
|
|
179
|
+
while (value / divisor > 1) divisor *= 10;
|
|
180
|
+
return value / divisor;
|
|
181
|
+
};
|
|
182
|
+
return [digits(color[0]), digits(color[1]), digits(color[2])];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A Phong exponent as a roughness.
|
|
187
|
+
*
|
|
188
|
+
* The two describe the same thing from opposite ends: the exponent says how *tightly* a
|
|
189
|
+
* highlight is gathered and rises without bound, roughness says how widely it is spread over
|
|
190
|
+
* nought to one. `sqrt(2 / (n + 2))` is the usual correspondence and it lands where intuition
|
|
191
|
+
* expects at both ends, with a mirror-like 1000 arriving at 0.04 and a flat 2 at 0.7.
|
|
192
|
+
*
|
|
193
|
+
* It is an approximation between two different shading models and is stated as one. What it
|
|
194
|
+
* must not do is default to zero: an unstated exponent read as a mirror makes every surface
|
|
195
|
+
* in an imported scene a black mirror, which reads as a lighting fault rather than a material
|
|
196
|
+
* one.
|
|
197
|
+
*/
|
|
198
|
+
/**
|
|
199
|
+
* How opaque a surface is, from whichever of the two spellings a writer used.
|
|
200
|
+
*
|
|
201
|
+
* They mean opposite things and that is the trap: `TransparencyFactor` is how *transparent*
|
|
202
|
+
* a surface is, so opacity is one minus it, while `Opacity` is already what it says. Reading
|
|
203
|
+
* one as the other turns every solid surface in an export invisible, and an invisible model
|
|
204
|
+
* reads as a failed load rather than as a material fault.
|
|
205
|
+
*
|
|
206
|
+
* Silence means opaque. Defaulting the other way would make every material in a file that
|
|
207
|
+
* omits both vanish, which is most files.
|
|
208
|
+
*/
|
|
209
|
+
function opacityOf(p: Map<string, unknown[]>): number {
|
|
210
|
+
const clamp = (value: number): number => Math.min(1, Math.max(0, value));
|
|
211
|
+
if (p.has('TransparencyFactor')) return clamp(1 - numberAt(p.get('TransparencyFactor'), 0, 0));
|
|
212
|
+
if (p.has('Opacity')) return clamp(numberAt(p.get('Opacity'), 0, 1));
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function roughnessFromShininess(exponent: number): number {
|
|
217
|
+
if (!(exponent > 0)) return DEFAULT_ROUGHNESS;
|
|
218
|
+
return Math.min(1, Math.max(0.02, Math.sqrt(2 / (exponent + 2))));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Materials, their textures, and which model wears which.
|
|
223
|
+
*
|
|
224
|
+
* Textures reach a material through an `OP` connection, which is FBX for "this object feeds
|
|
225
|
+
* that object's *named property*" — the property being `DiffuseColor`, `NormalMap`,
|
|
226
|
+
* `SpecularColor` and so on. Only the albedo is used, because it is the only one this
|
|
227
|
+
* renderer has a slot for; the rest are counted and reported rather than dropped in silence,
|
|
228
|
+
* so a model that looks flatter than it should says why.
|
|
229
|
+
*/
|
|
230
|
+
export function readMaterials(root: FbxNode, warnings: string[]): FbxMaterials {
|
|
231
|
+
const materialNodes = new Map<number, FbxNode>();
|
|
232
|
+
const textureNodes = new Map<number, FbxNode>();
|
|
233
|
+
const videoNodes = new Map<number, FbxNode>();
|
|
234
|
+
for (const objects of root.children.filter((child) => child.name === 'Objects')) {
|
|
235
|
+
for (const node of objects.children) {
|
|
236
|
+
const id = node.properties[0];
|
|
237
|
+
if (typeof id !== 'number') continue;
|
|
238
|
+
if (node.name === 'Material') materialNodes.set(id, node);
|
|
239
|
+
else if (node.name === 'Texture') textureNodes.set(id, node);
|
|
240
|
+
else if (node.name === 'Video') videoNodes.set(id, node);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/*
|
|
245
|
+
* Which video backs which texture, from the connection table.
|
|
246
|
+
*
|
|
247
|
+
* A `Media` property naming the video by string exists too and is what older writers use,
|
|
248
|
+
* so it is the fallback. The connection is preferred because a name is a name and an id is
|
|
249
|
+
* a fact: two videos may be called the same thing, and only one of them is connected.
|
|
250
|
+
*/
|
|
251
|
+
const videoOfTexture = new Map<number, FbxNode>();
|
|
252
|
+
for (const connections of root.children.filter((child) => child.name === 'Connections')) {
|
|
253
|
+
for (const c of connections.children) {
|
|
254
|
+
const [kind, from, to] = c.properties;
|
|
255
|
+
if (kind !== 'OO' || typeof from !== 'number' || typeof to !== 'number') continue;
|
|
256
|
+
const video = videoNodes.get(from);
|
|
257
|
+
if (video !== undefined && textureNodes.has(to)) videoOfTexture.set(to, video);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const [id, node] of textureNodes) {
|
|
261
|
+
if (videoOfTexture.has(id)) continue;
|
|
262
|
+
const media = stringChild(node, 'Media');
|
|
263
|
+
if (media === undefined) continue;
|
|
264
|
+
for (const [, video] of videoNodes) {
|
|
265
|
+
if (String(video.properties[1] ?? '') === media) {
|
|
266
|
+
videoOfTexture.set(id, video);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/*
|
|
273
|
+
* Texture id to an index in the reference list, deduplicated by what it resolves to.
|
|
274
|
+
*
|
|
275
|
+
* **FBX embeds its images as often as it points at them**, and the two look nothing alike.
|
|
276
|
+
* An embedded texture's `RelativeFilename` is `*0`, a reference into the file's own video
|
|
277
|
+
* list rather than a path, and the bytes live in the matching `Video` record's `Content`.
|
|
278
|
+
* Treating that as a filename produces a hunt for a file called `*0`, which fails, and a
|
|
279
|
+
* model that draws untextured while the image it needs sits inside the file being read.
|
|
280
|
+
*/
|
|
281
|
+
const textures: AssetReference[] = [];
|
|
282
|
+
const candidates = new Map<number, { reference: AssetReference; key: string }>();
|
|
283
|
+
for (const [id, node] of textureNodes) {
|
|
284
|
+
const declared = stringChild(node, 'RelativeFilename') ?? stringChild(node, 'FileName') ?? '';
|
|
285
|
+
const video = videoOfTexture.get(id);
|
|
286
|
+
const content = video === undefined ? undefined : contentOf(video);
|
|
287
|
+
|
|
288
|
+
/* An embedded image names itself by its video's filename, because `*0` names nothing. */
|
|
289
|
+
const name =
|
|
290
|
+
content !== undefined
|
|
291
|
+
? (stringChild(video as FbxNode, 'RelativeFilename') ??
|
|
292
|
+
stringChild(video as FbxNode, 'Filename') ??
|
|
293
|
+
declared)
|
|
294
|
+
: declared;
|
|
295
|
+
|
|
296
|
+
if (content === undefined && (name === '' || name.startsWith('*'))) {
|
|
297
|
+
warnings.push(
|
|
298
|
+
name.startsWith('*')
|
|
299
|
+
? `texture "${name}" is embedded but its image is missing from the file; it will draw untextured`
|
|
300
|
+
: 'a texture record states no filename and was skipped',
|
|
301
|
+
);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
* Held as a candidate, not embedded yet.
|
|
307
|
+
*
|
|
308
|
+
* **A file names far more textures than this renderer can use.** One test character
|
|
309
|
+
* carries normal, roughness and metallic maps beside every colour map, and embedding all
|
|
310
|
+
* of them produced a 401 MB asset against the 94 MB the same model makes through OBJ —
|
|
311
|
+
* three quarters of it maps that were reported as ignored in the same run. Nothing was
|
|
312
|
+
* wrong with the ignoring; the list was simply built from every `Texture` record rather
|
|
313
|
+
* than from the ones a material actually reaches. So an entry earns its place below, when
|
|
314
|
+
* something binds it.
|
|
315
|
+
*/
|
|
316
|
+
candidates.set(id, {
|
|
317
|
+
reference: content === undefined ? { name } : { name, bytes: content },
|
|
318
|
+
key: content === undefined ? `path:${name}` : `embedded:${id}`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Which texture feeds which material, and through which of the material's properties. */
|
|
323
|
+
const albedoOfMaterial = new Map<number, number>();
|
|
324
|
+
const ignoredSlots = new Map<string, number>();
|
|
325
|
+
/** Allocate a slot for a texture the first time something actually binds it. */
|
|
326
|
+
const claim = new Map<string, number>();
|
|
327
|
+
for (const connections of root.children.filter((child) => child.name === 'Connections')) {
|
|
328
|
+
for (const c of connections.children) {
|
|
329
|
+
const [kind, from, to, slot] = c.properties;
|
|
330
|
+
if (kind !== 'OP' || typeof from !== 'number' || typeof to !== 'number') continue;
|
|
331
|
+
const candidate = candidates.get(from);
|
|
332
|
+
if (candidate === undefined || !materialNodes.has(to)) continue;
|
|
333
|
+
if (slot !== 'DiffuseColor') {
|
|
334
|
+
if (typeof slot === 'string') ignoredSlots.set(slot, (ignoredSlots.get(slot) ?? 0) + 1);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
let at = claim.get(candidate.key);
|
|
338
|
+
if (at === undefined) {
|
|
339
|
+
at = textures.length;
|
|
340
|
+
claim.set(candidate.key, at);
|
|
341
|
+
textures.push(candidate.reference);
|
|
342
|
+
}
|
|
343
|
+
albedoOfMaterial.set(to, at);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const [slot, count] of ignoredSlots) {
|
|
347
|
+
warnings.push(
|
|
348
|
+
`${count} ${slot} texture${count === 1 ? '' : 's'} ignored; this renderer has one texture slot, for colour`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const byId = new Map<number, FbxMaterial>();
|
|
353
|
+
for (const [id, node] of materialNodes) {
|
|
354
|
+
const p = propertiesOf(node);
|
|
355
|
+
const emissiveColor = triple(
|
|
356
|
+
p.get('EmissiveColor'),
|
|
357
|
+
[0, 0, 0],
|
|
358
|
+
numberAt(p.get('EmissiveFactor'), 0, 1),
|
|
359
|
+
);
|
|
360
|
+
const emissive = Math.min(1, Math.max(emissiveColor[0], emissiveColor[1], emissiveColor[2]));
|
|
361
|
+
byId.set(id, {
|
|
362
|
+
name:
|
|
363
|
+
typeof node.properties[1] === 'string'
|
|
364
|
+
? ((node.properties[1] as string).split('\u0000')[0] ?? '')
|
|
365
|
+
: '',
|
|
366
|
+
color: triple(p.get('DiffuseColor'), DEFAULT_COLOR, numberAt(p.get('DiffuseFactor'), 0, 1)),
|
|
367
|
+
specular: Math.min(1, Math.max(0, numberAt(p.get('SpecularFactor'), 0, 0))),
|
|
368
|
+
roughness: roughnessFromShininess(
|
|
369
|
+
numberAt(p.get('ShininessExponent'), 0, numberAt(p.get('Shininess'), 0, 0)),
|
|
370
|
+
),
|
|
371
|
+
/*
|
|
372
|
+
* FBX states how transparent a surface is, so opacity is one minus it. Both
|
|
373
|
+
* spellings are read because writers disagree, and a file that says nothing is
|
|
374
|
+
* opaque rather than invisible: defaulting the other way would make every material
|
|
375
|
+
* in an export that omits it disappear.
|
|
376
|
+
*/
|
|
377
|
+
opacity: opacityOf(p),
|
|
378
|
+
/* FBX states this directly, and it is the property that decides whether paint reads as
|
|
379
|
+
paint. Read past until now, which is why an imported car looked flat. */
|
|
380
|
+
reflectivity: Math.min(1, Math.max(0, numberAt(p.get('ReflectionFactor'), 0, 0))),
|
|
381
|
+
normalMap: -1,
|
|
382
|
+
ormMap: -1,
|
|
383
|
+
emissiveMap: -1,
|
|
384
|
+
roughnessScale: 1,
|
|
385
|
+
metallicScale: 1,
|
|
386
|
+
occlusionStrength: 0,
|
|
387
|
+
cutout: 0,
|
|
388
|
+
emissive,
|
|
389
|
+
/* A negative component means "inherit the albedo", which is the engine's own default
|
|
390
|
+
and the right answer for a surface that names no emissive colour of its own. */
|
|
391
|
+
emissiveColor: emissive > 0 ? emissiveColor : [-1, -1, -1],
|
|
392
|
+
albedo: albedoOfMaterial.get(id) ?? -1,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* Model to its materials, in file order, because that order is what an index means. */
|
|
397
|
+
const ofModel = new Map<number, number[]>();
|
|
398
|
+
for (const connections of root.children.filter((child) => child.name === 'Connections')) {
|
|
399
|
+
for (const c of connections.children) {
|
|
400
|
+
const [kind, from, to] = c.properties;
|
|
401
|
+
if (kind !== 'OO' || typeof from !== 'number' || typeof to !== 'number') continue;
|
|
402
|
+
if (!byId.has(from)) continue;
|
|
403
|
+
const list = ofModel.get(to);
|
|
404
|
+
if (list === undefined) ofModel.set(to, [from]);
|
|
405
|
+
else list.push(from);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return { byId, ofModel, textures };
|
|
410
|
+
}
|