@aethervtt/shard-gltf 0.0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/accessors.d.ts +20 -0
- package/dist/accessors.js +137 -0
- package/dist/document.d.ts +157 -0
- package/dist/document.js +49 -0
- package/dist/importer.d.ts +12 -0
- package/dist/importer.js +965 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +20 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +11 -0
- package/package.json +40 -0
package/dist/importer.js
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clipInfo,
|
|
3
|
+
encodeClip
|
|
4
|
+
} from "@aethervtt/shard-animation";
|
|
5
|
+
import { defineImporter } from "@aethervtt/shard-assets";
|
|
6
|
+
import { defineSchema, ShardError, t } from "@aethervtt/shard-core";
|
|
7
|
+
import { encodeMesh, Mesh } from "@aethervtt/shard-mesh";
|
|
8
|
+
import { MAX_JOINTS, StandardMaterial, skinArtifact } from "@aethervtt/shard-render";
|
|
9
|
+
import { importImageBytes } from "@aethervtt/shard-texture";
|
|
10
|
+
import { Accessors } from "./accessors.js";
|
|
11
|
+
import {
|
|
12
|
+
parseGltf
|
|
13
|
+
} from "./document.js";
|
|
14
|
+
const GltfImportSettings = defineSchema(
|
|
15
|
+
"gltf/ImportSettings",
|
|
16
|
+
{
|
|
17
|
+
scale: t.f32({
|
|
18
|
+
default: 1,
|
|
19
|
+
min: 0,
|
|
20
|
+
description: "Uniform scale applied to the model root (0.01 for centimeter files)."
|
|
21
|
+
}),
|
|
22
|
+
forward: t.enum(["+z", "-z"], {
|
|
23
|
+
description: `Which way the model faces after import. "+z" keeps the file as authored (glTF models face +Z); "-z" turns it around to face Shard's forward, the way ships fly.`
|
|
24
|
+
}),
|
|
25
|
+
emissiveLuminance: t.f32({
|
|
26
|
+
default: 1e3,
|
|
27
|
+
min: 0,
|
|
28
|
+
unit: "cd/m\xB2",
|
|
29
|
+
description: "Luminance of emissiveFactor 1 at strength 1 (glTF emission has no unit)."
|
|
30
|
+
}),
|
|
31
|
+
cameras: t.bool({ description: "Spawn the file's cameras." }),
|
|
32
|
+
lights: t.bool({ description: "Spawn the file's lights (KHR_lights_punctual)." }),
|
|
33
|
+
generateNormals: t.enum(["missing", "always", "never"], {
|
|
34
|
+
description: 'Flat normals for primitives without them ("missing"), for all, or never.'
|
|
35
|
+
}),
|
|
36
|
+
lods: t.enum(["auto", "none"], {
|
|
37
|
+
description: 'Level of detail: "auto" turns MSFT_lod chains and sibling nodes named <name>_LOD0, _LOD1\u2026 into one entity with a render/Lod component; "none" imports every node as is.'
|
|
38
|
+
}),
|
|
39
|
+
materialTypes: t.json({
|
|
40
|
+
default: {},
|
|
41
|
+
description: `Material types by glTF material name: { "Lava*": "my-game/Lava" } (* matches anything). A material's extras.shardMaterial names a type directly. Imported materials are StandardMaterial otherwise.`
|
|
42
|
+
})
|
|
43
|
+
},
|
|
44
|
+
{ description: "Import settings for .gltf and .glb files." }
|
|
45
|
+
);
|
|
46
|
+
function materialTypeFor(material, patterns) {
|
|
47
|
+
const extra = material.extras?.shardMaterial;
|
|
48
|
+
if (typeof extra === "string") return extra;
|
|
49
|
+
if (!patterns || typeof patterns !== "object") return void 0;
|
|
50
|
+
const name = material.name ?? "";
|
|
51
|
+
for (const [pattern, type] of Object.entries(patterns)) {
|
|
52
|
+
if (typeof type !== "string") continue;
|
|
53
|
+
const re = new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
|
54
|
+
if (re.test(name)) return type;
|
|
55
|
+
}
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
const SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
59
|
+
"KHR_lights_punctual",
|
|
60
|
+
"KHR_materials_emissive_strength",
|
|
61
|
+
"KHR_mesh_quantization",
|
|
62
|
+
"KHR_texture_transform",
|
|
63
|
+
"MSFT_lod"
|
|
64
|
+
]);
|
|
65
|
+
function sanitize(name) {
|
|
66
|
+
return name.replace(/[^A-Za-z0-9_.-]/g, "_") || "_";
|
|
67
|
+
}
|
|
68
|
+
function labels(items) {
|
|
69
|
+
const list = items ?? [];
|
|
70
|
+
const counts = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const item of list) {
|
|
72
|
+
if (item.name) counts.set(sanitize(item.name), (counts.get(sanitize(item.name)) ?? 0) + 1);
|
|
73
|
+
}
|
|
74
|
+
return list.map((item, i) => {
|
|
75
|
+
const name = item.name ? sanitize(item.name) : void 0;
|
|
76
|
+
return name && counts.get(name) === 1 && !/^\d+$/.test(name) ? name : String(i);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function decodeDataUri(uri) {
|
|
80
|
+
const comma = uri.indexOf(",");
|
|
81
|
+
const meta = uri.slice(5, comma);
|
|
82
|
+
const data = uri.slice(comma + 1);
|
|
83
|
+
if (meta.endsWith(";base64")) return Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
|
84
|
+
return new TextEncoder().encode(decodeURIComponent(data));
|
|
85
|
+
}
|
|
86
|
+
function quatMul(a, b) {
|
|
87
|
+
return [
|
|
88
|
+
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
|
89
|
+
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
|
90
|
+
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
|
91
|
+
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
function rotate(q, v) {
|
|
95
|
+
const [x, y, z, w] = q;
|
|
96
|
+
const ix = w * v[0] + y * v[2] - z * v[1];
|
|
97
|
+
const iy = w * v[1] + z * v[0] - x * v[2];
|
|
98
|
+
const iz = w * v[2] + x * v[1] - y * v[0];
|
|
99
|
+
const iw = -x * v[0] - y * v[1] - z * v[2];
|
|
100
|
+
return [
|
|
101
|
+
ix * w + iw * -x + iy * -z - iz * -y,
|
|
102
|
+
iy * w + iw * -y + iz * -x - ix * -z,
|
|
103
|
+
iz * w + iw * -z + ix * -y - iy * -x
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
function trs(node) {
|
|
107
|
+
if (!node.matrix) {
|
|
108
|
+
return {
|
|
109
|
+
t: node.translation ?? [0, 0, 0],
|
|
110
|
+
r: node.rotation ?? [0, 0, 0, 1],
|
|
111
|
+
s: node.scale ?? [1, 1, 1]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const m = node.matrix;
|
|
115
|
+
let sx = Math.hypot(m[0], m[1], m[2]);
|
|
116
|
+
const sy = Math.hypot(m[4], m[5], m[6]);
|
|
117
|
+
const sz = Math.hypot(m[8], m[9], m[10]);
|
|
118
|
+
const det = m[0] * (m[5] * m[10] - m[6] * m[9]) - m[4] * (m[1] * m[10] - m[2] * m[9]) + m[8] * (m[1] * m[6] - m[2] * m[5]);
|
|
119
|
+
if (det < 0) sx = -sx;
|
|
120
|
+
const r00 = m[0] / sx;
|
|
121
|
+
const r10 = m[1] / sx;
|
|
122
|
+
const r20 = m[2] / sx;
|
|
123
|
+
const r01 = m[4] / sy;
|
|
124
|
+
const r11 = m[5] / sy;
|
|
125
|
+
const r21 = m[6] / sy;
|
|
126
|
+
const r02 = m[8] / sz;
|
|
127
|
+
const r12 = m[9] / sz;
|
|
128
|
+
const r22 = m[10] / sz;
|
|
129
|
+
const trace = r00 + r11 + r22;
|
|
130
|
+
let q;
|
|
131
|
+
if (trace > 0) {
|
|
132
|
+
const s = Math.sqrt(trace + 1) * 2;
|
|
133
|
+
q = [(r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s, s / 4];
|
|
134
|
+
} else if (r00 > r11 && r00 > r22) {
|
|
135
|
+
const s = Math.sqrt(1 + r00 - r11 - r22) * 2;
|
|
136
|
+
q = [s / 4, (r01 + r10) / s, (r02 + r20) / s, (r21 - r12) / s];
|
|
137
|
+
} else if (r11 > r22) {
|
|
138
|
+
const s = Math.sqrt(1 + r11 - r00 - r22) * 2;
|
|
139
|
+
q = [(r01 + r10) / s, s / 4, (r12 + r21) / s, (r02 - r20) / s];
|
|
140
|
+
} else {
|
|
141
|
+
const s = Math.sqrt(1 + r22 - r00 - r11) * 2;
|
|
142
|
+
q = [(r02 + r20) / s, (r12 + r21) / s, s / 4, (r10 - r01) / s];
|
|
143
|
+
}
|
|
144
|
+
return { t: [m[12], m[13], m[14]], r: q, s: [sx, sy, sz] };
|
|
145
|
+
}
|
|
146
|
+
function matrixOf(t2, r, s) {
|
|
147
|
+
const [x, y, z, w] = r;
|
|
148
|
+
const xx = x * x;
|
|
149
|
+
const yy = y * y;
|
|
150
|
+
const zz = z * z;
|
|
151
|
+
const xy = x * y;
|
|
152
|
+
const xz = x * z;
|
|
153
|
+
const yz = y * z;
|
|
154
|
+
const wx = w * x;
|
|
155
|
+
const wy = w * y;
|
|
156
|
+
const wz = w * z;
|
|
157
|
+
return [
|
|
158
|
+
(1 - 2 * (yy + zz)) * s[0],
|
|
159
|
+
2 * (xy + wz) * s[0],
|
|
160
|
+
2 * (xz - wy) * s[0],
|
|
161
|
+
0,
|
|
162
|
+
2 * (xy - wz) * s[1],
|
|
163
|
+
(1 - 2 * (xx + zz)) * s[1],
|
|
164
|
+
2 * (yz + wx) * s[1],
|
|
165
|
+
0,
|
|
166
|
+
2 * (xz + wy) * s[2],
|
|
167
|
+
2 * (yz - wx) * s[2],
|
|
168
|
+
(1 - 2 * (xx + yy)) * s[2],
|
|
169
|
+
0,
|
|
170
|
+
t2[0],
|
|
171
|
+
t2[1],
|
|
172
|
+
t2[2],
|
|
173
|
+
1
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
function mul4(a, b) {
|
|
177
|
+
const out = new Array(16);
|
|
178
|
+
for (let c = 0; c < 4; c++) {
|
|
179
|
+
for (let r = 0; r < 4; r++) {
|
|
180
|
+
out[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function flatShade(data) {
|
|
186
|
+
const tri = data.indices ?? Uint32Array.from({ length: data.positions.length / 3 }, (_, i) => i);
|
|
187
|
+
const n = tri.length;
|
|
188
|
+
const expand = (arr, width) => {
|
|
189
|
+
if (!arr) return void 0;
|
|
190
|
+
const out2 = new arr.constructor(n * width);
|
|
191
|
+
for (let i = 0; i < n; i++)
|
|
192
|
+
for (let c = 0; c < width; c++) out2[i * width + c] = arr[tri[i] * width + c];
|
|
193
|
+
return out2;
|
|
194
|
+
};
|
|
195
|
+
const positions = expand(data.positions, 3);
|
|
196
|
+
const normals = new Float32Array(n * 3);
|
|
197
|
+
for (let i = 0; i < n; i += 3) {
|
|
198
|
+
const a = i * 3;
|
|
199
|
+
const ux = positions[a + 3] - positions[a];
|
|
200
|
+
const uy = positions[a + 4] - positions[a + 1];
|
|
201
|
+
const uz = positions[a + 5] - positions[a + 2];
|
|
202
|
+
const vx = positions[a + 6] - positions[a];
|
|
203
|
+
const vy = positions[a + 7] - positions[a + 1];
|
|
204
|
+
const vz = positions[a + 8] - positions[a + 2];
|
|
205
|
+
let nx = uy * vz - uz * vy;
|
|
206
|
+
let ny = uz * vx - ux * vz;
|
|
207
|
+
let nz = ux * vy - uy * vx;
|
|
208
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
209
|
+
nx /= len;
|
|
210
|
+
ny /= len;
|
|
211
|
+
nz /= len;
|
|
212
|
+
for (let k = 0; k < 3; k++) {
|
|
213
|
+
normals[a + k * 3] = nx;
|
|
214
|
+
normals[a + k * 3 + 1] = ny;
|
|
215
|
+
normals[a + k * 3 + 2] = nz;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const out = { positions, normals };
|
|
219
|
+
const uvs = expand(data.uvs, 2);
|
|
220
|
+
if (uvs) out.uvs = uvs;
|
|
221
|
+
const uvs1 = expand(data.uvs1, 2);
|
|
222
|
+
if (uvs1) out.uvs1 = uvs1;
|
|
223
|
+
const colors = expand(data.colors, 4);
|
|
224
|
+
if (colors) out.colors = colors;
|
|
225
|
+
const joints = expand(data.joints, 4);
|
|
226
|
+
if (joints) out.joints = joints;
|
|
227
|
+
const weights = expand(data.weights, 4);
|
|
228
|
+
if (weights) out.weights = weights;
|
|
229
|
+
if (data.targets) out.targets = data.targets.map((t2) => expandTarget(t2, expand));
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function expandTarget(target, expand) {
|
|
233
|
+
const out = { positions: expand(target.positions, 3) };
|
|
234
|
+
if (target.normals) out.normals = expand(target.normals, 3);
|
|
235
|
+
if (target.tangents) out.tangents = expand(target.tangents, 3);
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
function toTriangleList(mode, source) {
|
|
239
|
+
const out = [];
|
|
240
|
+
if (mode === 5) {
|
|
241
|
+
for (let i = 0; i + 2 < source.length; i++) {
|
|
242
|
+
if (i % 2 === 0) out.push(source[i], source[i + 1], source[i + 2]);
|
|
243
|
+
else out.push(source[i + 1], source[i], source[i + 2]);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
for (let i = 1; i + 1 < source.length; i++) out.push(source[0], source[i], source[i + 1]);
|
|
247
|
+
}
|
|
248
|
+
return Uint32Array.from(out);
|
|
249
|
+
}
|
|
250
|
+
async function loadBuffers(doc, bin, ctx) {
|
|
251
|
+
const buffers = [];
|
|
252
|
+
for (const [i, buffer] of (doc.buffers ?? []).entries()) {
|
|
253
|
+
const ptr = `/buffers/${i}`;
|
|
254
|
+
if (buffer.uri === void 0) {
|
|
255
|
+
if (i === 0 && bin) buffers.push(bin);
|
|
256
|
+
else throw new ShardError("gltf/buffer-missing", `Buffer ${i} has no data`, { path: ptr });
|
|
257
|
+
} else if (buffer.uri.startsWith("data:")) {
|
|
258
|
+
buffers.push(decodeDataUri(buffer.uri));
|
|
259
|
+
} else {
|
|
260
|
+
try {
|
|
261
|
+
buffers.push(await ctx.read(decodeURIComponent(buffer.uri)));
|
|
262
|
+
} catch (cause) {
|
|
263
|
+
throw new ShardError("gltf/buffer-missing", `Can't read buffer file "${buffer.uri}"`, {
|
|
264
|
+
path: ptr,
|
|
265
|
+
hint: "Keep .bin files next to the .gltf, under the same name the file references.",
|
|
266
|
+
cause
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return buffers;
|
|
272
|
+
}
|
|
273
|
+
const WRAP = { 33071: "clamp", 33648: "mirror", 10497: "repeat" };
|
|
274
|
+
function convertMaterial(m, settings, textures, warn, ptr) {
|
|
275
|
+
const pbr = m.pbrMetallicRoughness ?? {};
|
|
276
|
+
const factor = m.emissiveFactor ?? [0, 0, 0];
|
|
277
|
+
const strength = m.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
|
|
278
|
+
const peak = Math.max(factor[0], factor[1], factor[2]);
|
|
279
|
+
if (m.alphaMode === "BLEND") {
|
|
280
|
+
warn(
|
|
281
|
+
"Blended transparency arrives in M5; this material imports as an alpha mask.",
|
|
282
|
+
`${ptr}/alphaMode`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const dependencies = [];
|
|
286
|
+
const slot = (info) => {
|
|
287
|
+
if (!info) return void 0;
|
|
288
|
+
const path = textures.pathOf(info.index);
|
|
289
|
+
if (!path) return void 0;
|
|
290
|
+
dependencies.push(path);
|
|
291
|
+
const tt = info.extensions?.KHR_texture_transform;
|
|
292
|
+
return {
|
|
293
|
+
texture: { path },
|
|
294
|
+
uv: tt?.texCoord ?? info.texCoord ?? 0,
|
|
295
|
+
offset: tt?.offset ?? [0, 0],
|
|
296
|
+
scale: tt?.scale ?? [1, 1],
|
|
297
|
+
rotation: tt?.rotation ?? 0,
|
|
298
|
+
...textures.samplerOf(info.index)
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
const value = {
|
|
302
|
+
baseColor: pbr.baseColorFactor ?? [1, 1, 1, 1],
|
|
303
|
+
metallic: pbr.metallicFactor ?? 1,
|
|
304
|
+
roughness: pbr.roughnessFactor ?? 1,
|
|
305
|
+
emissive: peak > 0 ? [factor[0] / peak, factor[1] / peak, factor[2] / peak, 1] : [1, 1, 1, 1],
|
|
306
|
+
// An emissive texture with no factor still means "emissive": glTF's factor defaults to 0 though.
|
|
307
|
+
emissiveLuminance: peak * strength * settings.emissiveLuminance,
|
|
308
|
+
doubleSided: m.doubleSided ?? false,
|
|
309
|
+
alphaMode: m.alphaMode === "MASK" || m.alphaMode === "BLEND" ? "mask" : "opaque",
|
|
310
|
+
alphaCutoff: m.alphaMode === "BLEND" ? 0.5 : m.alphaCutoff ?? 0.5,
|
|
311
|
+
normalScale: m.normalTexture?.scale ?? 1,
|
|
312
|
+
occlusionStrength: m.occlusionTexture?.strength ?? 1
|
|
313
|
+
};
|
|
314
|
+
const slots = {
|
|
315
|
+
baseColorTexture: slot(pbr.baseColorTexture),
|
|
316
|
+
metallicRoughnessTexture: slot(pbr.metallicRoughnessTexture),
|
|
317
|
+
normalTexture: slot(m.normalTexture),
|
|
318
|
+
occlusionTexture: slot(m.occlusionTexture),
|
|
319
|
+
emissiveTexture: slot(m.emissiveTexture)
|
|
320
|
+
};
|
|
321
|
+
for (const [k, v] of Object.entries(slots)) if (v) value[k] = v;
|
|
322
|
+
return { json: StandardMaterial.serialize(StandardMaterial.deserialize(value)), dependencies };
|
|
323
|
+
}
|
|
324
|
+
async function generateTangents(data) {
|
|
325
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
326
|
+
throw new ShardError(
|
|
327
|
+
"gltf/tangents-unavailable",
|
|
328
|
+
"Generating tangents needs a Node host (the CLI or shard dev)"
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const { createRequire } = await import("node:module");
|
|
332
|
+
const mikk = createRequire(import.meta.url)("mikktspace");
|
|
333
|
+
const tri = data.indices ?? Uint32Array.from({ length: data.positions.length / 3 }, (_, i) => i);
|
|
334
|
+
const expand = (arr, width) => {
|
|
335
|
+
if (!arr) return void 0;
|
|
336
|
+
const out2 = new arr.constructor(tri.length * width);
|
|
337
|
+
for (let i = 0; i < tri.length; i++)
|
|
338
|
+
for (let c = 0; c < width; c++) out2[i * width + c] = arr[tri[i] * width + c];
|
|
339
|
+
return out2;
|
|
340
|
+
};
|
|
341
|
+
const out = { positions: expand(data.positions, 3) };
|
|
342
|
+
out.normals = expand(data.normals, 3);
|
|
343
|
+
out.uvs = expand(data.uvs, 2);
|
|
344
|
+
if (data.uvs1) out.uvs1 = expand(data.uvs1, 2);
|
|
345
|
+
if (data.colors) out.colors = expand(data.colors, 4);
|
|
346
|
+
if (data.joints) out.joints = expand(data.joints, 4);
|
|
347
|
+
if (data.weights) out.weights = expand(data.weights, 4);
|
|
348
|
+
if (data.targets) out.targets = data.targets.map((t2) => expandTarget(t2, (a, w) => expand(a, w)));
|
|
349
|
+
const tangents = mikk.generateTangents(out.positions, out.normals, out.uvs);
|
|
350
|
+
for (let i = 3; i < tangents.length; i += 4) tangents[i] = -tangents[i];
|
|
351
|
+
out.tangents = tangents;
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
function buildPrimitive(prim, acc, settings, warn, ptr) {
|
|
355
|
+
const mode = prim.mode ?? 4;
|
|
356
|
+
if (mode < 4) {
|
|
357
|
+
warn(`Primitive mode ${mode} (points/lines) isn't supported; skipped.`, `${ptr}/mode`);
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
if (prim.attributes.POSITION === void 0) {
|
|
361
|
+
throw new ShardError("gltf/invalid", "Primitive has no POSITION attribute", {
|
|
362
|
+
path: `${ptr}/attributes`
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const a = prim.attributes;
|
|
366
|
+
const data = { positions: acc.floats(a.POSITION) };
|
|
367
|
+
if (a.NORMAL !== void 0) data.normals = acc.floats(a.NORMAL);
|
|
368
|
+
if (a.TEXCOORD_0 !== void 0) data.uvs = acc.floats(a.TEXCOORD_0);
|
|
369
|
+
if (a.TEXCOORD_1 !== void 0) data.uvs1 = acc.floats(a.TEXCOORD_1);
|
|
370
|
+
if (a.TANGENT !== void 0) data.tangents = acc.floats(a.TANGENT);
|
|
371
|
+
if (a.WEIGHTS_0 !== void 0) data.weights = acc.floats(a.WEIGHTS_0);
|
|
372
|
+
if (a.JOINTS_0 !== void 0) data.joints = Uint16Array.from(acc.read(a.JOINTS_0).values);
|
|
373
|
+
if (a.COLOR_0 !== void 0) {
|
|
374
|
+
const c = acc.read(a.COLOR_0);
|
|
375
|
+
if (c.components === 4) data.colors = Float32Array.from(c.values);
|
|
376
|
+
else {
|
|
377
|
+
const colors = new Float32Array(c.count * 4);
|
|
378
|
+
for (let i = 0; i < c.count; i++) {
|
|
379
|
+
colors[i * 4] = c.values[i * 3];
|
|
380
|
+
colors[i * 4 + 1] = c.values[i * 3 + 1];
|
|
381
|
+
colors[i * 4 + 2] = c.values[i * 3 + 2];
|
|
382
|
+
colors[i * 4 + 3] = 1;
|
|
383
|
+
}
|
|
384
|
+
data.colors = colors;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const vertexCount = data.positions.length / 3;
|
|
388
|
+
if (prim.targets?.length) {
|
|
389
|
+
data.targets = prim.targets.map((target, k) => {
|
|
390
|
+
if (target.POSITION === void 0) {
|
|
391
|
+
throw new ShardError("gltf/invalid", "Morph target has no POSITION", {
|
|
392
|
+
path: `${ptr}/targets/${k}`
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const out2 = { positions: acc.floats(target.POSITION) };
|
|
396
|
+
if (target.NORMAL !== void 0) out2.normals = acc.floats(target.NORMAL);
|
|
397
|
+
if (target.TANGENT !== void 0) out2.tangents = acc.floats(target.TANGENT);
|
|
398
|
+
return out2;
|
|
399
|
+
});
|
|
400
|
+
if (data.targets.length > 8)
|
|
401
|
+
warn(
|
|
402
|
+
`${data.targets.length} morph targets: the 8 heaviest at a time are applied.`,
|
|
403
|
+
`${ptr}/targets`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
let indices = prim.indices === void 0 ? void 0 : Uint32Array.from(acc.read(prim.indices).values);
|
|
407
|
+
if (mode === 5 || mode === 6) {
|
|
408
|
+
indices = toTriangleList(
|
|
409
|
+
mode,
|
|
410
|
+
indices ?? Uint32Array.from({ length: vertexCount }, (_, i) => i)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (indices) data.indices = indices;
|
|
414
|
+
let out = data;
|
|
415
|
+
if (settings.generateNormals === "always" || settings.generateNormals === "missing" && !data.normals) {
|
|
416
|
+
out = flatShade(data);
|
|
417
|
+
if (data.tangents) warn("Tangents were dropped when generating flat normals.", ptr);
|
|
418
|
+
}
|
|
419
|
+
if (out.indices && out.positions.length / 3 <= 65536) out.indices = Uint16Array.from(out.indices);
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
const GltfImporter = defineImporter({
|
|
423
|
+
name: "gltf",
|
|
424
|
+
version: 3,
|
|
425
|
+
extensions: [".gltf", ".glb"],
|
|
426
|
+
settings: GltfImportSettings,
|
|
427
|
+
async import(source, ctx) {
|
|
428
|
+
const settings = ctx.settings;
|
|
429
|
+
const { json: doc, bin } = parseGltf(source.bytes);
|
|
430
|
+
for (const [i, ext] of (doc.extensionsRequired ?? []).entries()) {
|
|
431
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
432
|
+
throw new ShardError(
|
|
433
|
+
"gltf/unsupported-extension",
|
|
434
|
+
`This file requires ${ext}, which isn't supported`,
|
|
435
|
+
{
|
|
436
|
+
path: `/extensionsRequired/${i}`,
|
|
437
|
+
hint: ext.includes("draco") || ext.includes("meshopt") ? "Re-export without mesh compression." : "Re-export without this extension."
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const ext of doc.extensionsUsed ?? []) {
|
|
443
|
+
if (!SUPPORTED_EXTENSIONS.has(ext) && !(doc.extensionsRequired ?? []).includes(ext)) {
|
|
444
|
+
ctx.warn(`Optional extension ${ext} is ignored.`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const buffers = await loadBuffers(doc, bin, ctx);
|
|
448
|
+
const acc = new Accessors(doc, buffers);
|
|
449
|
+
const assets = [];
|
|
450
|
+
const imageUsage = /* @__PURE__ */ new Map();
|
|
451
|
+
const imageOf = (textureIndex) => {
|
|
452
|
+
const tex = doc.textures?.[textureIndex];
|
|
453
|
+
const basisu = tex?.extensions?.KHR_texture_basisu?.source;
|
|
454
|
+
return basisu ?? tex?.source;
|
|
455
|
+
};
|
|
456
|
+
const use = (info, usage, ptr) => {
|
|
457
|
+
if (!info) return;
|
|
458
|
+
const image = imageOf(info.index);
|
|
459
|
+
if (image === void 0) return;
|
|
460
|
+
const previous = imageUsage.get(image);
|
|
461
|
+
if (previous && previous !== usage) {
|
|
462
|
+
ctx.warn(
|
|
463
|
+
`Image ${image} is used as both ${previous} and ${usage}; importing it as ${previous}.`,
|
|
464
|
+
ptr
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
imageUsage.set(image, usage);
|
|
469
|
+
};
|
|
470
|
+
for (const [i, m] of (doc.materials ?? []).entries()) {
|
|
471
|
+
const ptr = `/materials/${i}`;
|
|
472
|
+
use(
|
|
473
|
+
m.pbrMetallicRoughness?.baseColorTexture,
|
|
474
|
+
"color",
|
|
475
|
+
`${ptr}/pbrMetallicRoughness/baseColorTexture`
|
|
476
|
+
);
|
|
477
|
+
use(m.emissiveTexture, "color", `${ptr}/emissiveTexture`);
|
|
478
|
+
use(
|
|
479
|
+
m.pbrMetallicRoughness?.metallicRoughnessTexture,
|
|
480
|
+
"data",
|
|
481
|
+
`${ptr}/pbrMetallicRoughness/metallicRoughnessTexture`
|
|
482
|
+
);
|
|
483
|
+
use(m.occlusionTexture, "data", `${ptr}/occlusionTexture`);
|
|
484
|
+
use(m.normalTexture, "normal", `${ptr}/normalTexture`);
|
|
485
|
+
}
|
|
486
|
+
const imageLabels = labels(doc.images).map((l) => `Texture/${l}`);
|
|
487
|
+
const imagePaths = /* @__PURE__ */ new Map();
|
|
488
|
+
for (const [i, image] of (doc.images ?? []).entries()) {
|
|
489
|
+
const ptr = `/images/${i}`;
|
|
490
|
+
const usage = imageUsage.get(i) ?? "color";
|
|
491
|
+
let bytes;
|
|
492
|
+
if (image.bufferView !== void 0) {
|
|
493
|
+
const bv = doc.bufferViews?.[image.bufferView];
|
|
494
|
+
const buffer = bv ? buffers[bv.buffer] : void 0;
|
|
495
|
+
if (!bv || !buffer)
|
|
496
|
+
throw new ShardError("gltf/invalid", `Image ${i} has an invalid bufferView`, {
|
|
497
|
+
path: ptr
|
|
498
|
+
});
|
|
499
|
+
bytes = buffer.subarray(bv.byteOffset ?? 0, (bv.byteOffset ?? 0) + bv.byteLength);
|
|
500
|
+
} else if (image.uri?.startsWith("data:")) {
|
|
501
|
+
bytes = decodeDataUri(image.uri);
|
|
502
|
+
} else if (image.uri) {
|
|
503
|
+
const path = ctx.resolve(decodeURIComponent(image.uri));
|
|
504
|
+
imagePaths.set(i, path);
|
|
505
|
+
try {
|
|
506
|
+
const meta = JSON.parse(
|
|
507
|
+
new TextDecoder().decode(await ctx.read(`${decodeURIComponent(image.uri)}.meta`))
|
|
508
|
+
);
|
|
509
|
+
const declared = meta?.settings?.usage;
|
|
510
|
+
if (declared && declared !== usage) {
|
|
511
|
+
ctx.warn(
|
|
512
|
+
`gltf/texture-usage-mismatch: ${path} is imported as "${declared}" but this model uses it as "${usage}"`,
|
|
513
|
+
ptr
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (!bytes) continue;
|
|
521
|
+
const { bytes: artifact, info } = await importImageBytes(
|
|
522
|
+
bytes,
|
|
523
|
+
{
|
|
524
|
+
usage,
|
|
525
|
+
mipmaps: true,
|
|
526
|
+
compression: "none",
|
|
527
|
+
maxSize: 4096,
|
|
528
|
+
flipY: false,
|
|
529
|
+
premultiplyAlpha: false
|
|
530
|
+
},
|
|
531
|
+
(m) => ctx.warn(m, ptr)
|
|
532
|
+
);
|
|
533
|
+
imagePaths.set(i, `#${imageLabels[i]}`);
|
|
534
|
+
assets.push({ label: imageLabels[i], type: "Texture", bytes: artifact, info });
|
|
535
|
+
}
|
|
536
|
+
const textureRefs = {
|
|
537
|
+
pathOf: (index) => {
|
|
538
|
+
const image = imageOf(index);
|
|
539
|
+
return image === void 0 ? void 0 : imagePaths.get(image);
|
|
540
|
+
},
|
|
541
|
+
samplerOf: (index) => {
|
|
542
|
+
const sampler = doc.samplers?.[doc.textures?.[index]?.sampler ?? -1];
|
|
543
|
+
return {
|
|
544
|
+
wrap: WRAP[sampler?.wrapS ?? 10497] ?? "repeat",
|
|
545
|
+
filter: sampler?.magFilter === 9728 ? "nearest" : "linear"
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const materialLabels = labels(doc.materials).map((l) => `Material/${l}`);
|
|
550
|
+
for (const [i, m] of (doc.materials ?? []).entries()) {
|
|
551
|
+
const { json, dependencies } = convertMaterial(
|
|
552
|
+
m,
|
|
553
|
+
settings,
|
|
554
|
+
textureRefs,
|
|
555
|
+
ctx.warn,
|
|
556
|
+
`/materials/${i}`
|
|
557
|
+
);
|
|
558
|
+
const materialType = materialTypeFor(m, settings.materialTypes);
|
|
559
|
+
assets.push({
|
|
560
|
+
label: materialLabels[i],
|
|
561
|
+
type: "Material",
|
|
562
|
+
json: materialType ? { type: materialType, ...json } : json,
|
|
563
|
+
...dependencies.length ? { dependencies } : {}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const meshLabels = labels(doc.meshes);
|
|
567
|
+
const primLabels = [];
|
|
568
|
+
for (const [mi, mesh] of (doc.meshes ?? []).entries()) {
|
|
569
|
+
const out = [];
|
|
570
|
+
for (const [pi, prim] of mesh.primitives.entries()) {
|
|
571
|
+
const ptr = `/meshes/${mi}/primitives/${pi}`;
|
|
572
|
+
let data = buildPrimitive(prim, acc, settings, ctx.warn, ptr);
|
|
573
|
+
if (!data) {
|
|
574
|
+
out.push(void 0);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const material = prim.material === void 0 ? void 0 : doc.materials?.[prim.material];
|
|
578
|
+
if (material?.normalTexture && !data.tangents && data.normals && data.uvs) {
|
|
579
|
+
data = await generateTangents(data);
|
|
580
|
+
}
|
|
581
|
+
let built;
|
|
582
|
+
try {
|
|
583
|
+
built = Mesh.create(data);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
throw new ShardError("gltf/invalid", `${ptr}: ${err.message}`, {
|
|
586
|
+
path: ptr,
|
|
587
|
+
cause: err
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
const label = mesh.primitives.length === 1 ? `Mesh/${meshLabels[mi]}` : `Mesh/${meshLabels[mi]}/${pi}`;
|
|
591
|
+
out.push(label);
|
|
592
|
+
const b = built.bounds;
|
|
593
|
+
assets.push({
|
|
594
|
+
label,
|
|
595
|
+
type: "Mesh",
|
|
596
|
+
bytes: encodeMesh(built),
|
|
597
|
+
info: {
|
|
598
|
+
vertices: built.vertexCount,
|
|
599
|
+
triangles: Math.floor(built.drawCount / 3),
|
|
600
|
+
bounds: { min: [b[0], b[1], b[2]], max: [b[3], b[4], b[5]] }
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
primLabels.push(out);
|
|
605
|
+
}
|
|
606
|
+
const nodes = doc.nodes ?? [];
|
|
607
|
+
const skinLabels = labels(doc.skins);
|
|
608
|
+
const primitivePaths = /* @__PURE__ */ new Map();
|
|
609
|
+
const nodePaths = /* @__PURE__ */ new Map();
|
|
610
|
+
const rootRotation = settings.forward === "-z" ? [0, 1, 0, 0] : [0, 0, 0, 1];
|
|
611
|
+
const rootScale = settings.scale;
|
|
612
|
+
const lights = doc.extensions?.KHR_lights_punctual?.lights ?? [];
|
|
613
|
+
const lodOn = settings.lods !== "none";
|
|
614
|
+
const msftLower = /* @__PURE__ */ new Set();
|
|
615
|
+
if (lodOn) {
|
|
616
|
+
for (const node of nodes) {
|
|
617
|
+
const ids = node.extensions?.MSFT_lod?.ids;
|
|
618
|
+
for (const id of ids ?? []) msftLower.add(id);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const LOD_NAME = /^(.*)_LOD(\d+)$/i;
|
|
622
|
+
const lodGroups = (indices) => {
|
|
623
|
+
const chains = /* @__PURE__ */ new Map();
|
|
624
|
+
if (!lodOn) return { build: indices, chains };
|
|
625
|
+
const build = [];
|
|
626
|
+
const byName = /* @__PURE__ */ new Map();
|
|
627
|
+
for (const ni of indices) {
|
|
628
|
+
if (msftLower.has(ni)) continue;
|
|
629
|
+
const node = nodes[ni];
|
|
630
|
+
const ids = node?.extensions?.MSFT_lod?.ids;
|
|
631
|
+
if (ids?.length) {
|
|
632
|
+
const coverage = node?.extras?.MSFT_screencoverage;
|
|
633
|
+
chains.set(ni, { ids, coverage });
|
|
634
|
+
build.push(ni);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const match = LOD_NAME.exec(node?.name ?? "");
|
|
638
|
+
if (match && node?.mesh !== void 0) {
|
|
639
|
+
const group = byName.get(match[1]) ?? [];
|
|
640
|
+
group.push({ level: Number(match[2]), ni });
|
|
641
|
+
byName.set(match[1], group);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
build.push(ni);
|
|
645
|
+
}
|
|
646
|
+
for (const group of byName.values()) {
|
|
647
|
+
group.sort((a, b) => a.level - b.level);
|
|
648
|
+
const [base, ...lower] = group;
|
|
649
|
+
build.push(base.ni);
|
|
650
|
+
if (lower.length) chains.set(base.ni, { ids: lower.map((l) => l.ni), coverage: void 0 });
|
|
651
|
+
}
|
|
652
|
+
return { build, chains };
|
|
653
|
+
};
|
|
654
|
+
const lodScreenSize = (k, n, coverage) => {
|
|
655
|
+
const c = coverage?.[k];
|
|
656
|
+
if (c !== void 0) return Math.sqrt(Math.max(0, c));
|
|
657
|
+
return k === n - 1 ? 0 : 0.25 / 4 ** k;
|
|
658
|
+
};
|
|
659
|
+
const buildScene = (roots, record) => {
|
|
660
|
+
const deps = /* @__PURE__ */ new Set();
|
|
661
|
+
const min = [Infinity, Infinity, Infinity];
|
|
662
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
663
|
+
const paths = [];
|
|
664
|
+
const build = (ni, siblings, parentPath, parentMatrix, top2, stack, lod) => {
|
|
665
|
+
const node = nodes[ni];
|
|
666
|
+
if (!node) throw new ShardError("gltf/invalid", `No node ${ni}`, { path: `/nodes/${ni}` });
|
|
667
|
+
if (stack.has(ni))
|
|
668
|
+
throw new ShardError("gltf/invalid", `Node ${ni} is its own ancestor`, {
|
|
669
|
+
path: `/nodes/${ni}`
|
|
670
|
+
});
|
|
671
|
+
const baseName = lod && LOD_NAME.test(node.name ?? "") ? LOD_NAME.exec(node.name)[1] : node.name;
|
|
672
|
+
let name = sanitize(baseName ?? `Node${ni}`);
|
|
673
|
+
for (let k = 2; siblings.has(name); k++) name = `${sanitize(baseName ?? `Node${ni}`)}_${k}`;
|
|
674
|
+
siblings.add(name);
|
|
675
|
+
const path = parentPath ? `${parentPath}/${name}` : name;
|
|
676
|
+
if (record && !nodePaths.has(ni)) nodePaths.set(ni, path);
|
|
677
|
+
paths.push(path);
|
|
678
|
+
let { t: tr, r, s: sc } = trs(node);
|
|
679
|
+
if (top2 && (rootScale !== 1 || settings.forward === "-z")) {
|
|
680
|
+
tr = rotate(rootRotation, [tr[0] * rootScale, tr[1] * rootScale, tr[2] * rootScale]);
|
|
681
|
+
r = quatMul(rootRotation, r);
|
|
682
|
+
sc = [sc[0] * rootScale, sc[1] * rootScale, sc[2] * rootScale];
|
|
683
|
+
}
|
|
684
|
+
const world = mul4(parentMatrix, matrixOf(tr, r, sc));
|
|
685
|
+
const components = {
|
|
686
|
+
"core/Transform": { translation: [...tr], rotation: [...r], scale: [...sc] }
|
|
687
|
+
};
|
|
688
|
+
const children = [];
|
|
689
|
+
if (node.mesh !== void 0) {
|
|
690
|
+
const mesh = doc.meshes?.[node.mesh];
|
|
691
|
+
const prims = primLabels[node.mesh] ?? [];
|
|
692
|
+
let first = true;
|
|
693
|
+
for (const [pi, label] of prims.entries()) {
|
|
694
|
+
if (!label) continue;
|
|
695
|
+
const materialIndex = mesh?.primitives[pi]?.material;
|
|
696
|
+
const comps = {
|
|
697
|
+
"render/Mesh3d": { mesh: { path: `#${label}` } }
|
|
698
|
+
};
|
|
699
|
+
deps.add(`#${label}`);
|
|
700
|
+
if (node.skin !== void 0 && skinLabels[node.skin] !== void 0) {
|
|
701
|
+
comps["render/SkinnedMesh"] = { skin: { path: `#Skin/${skinLabels[node.skin]}` } };
|
|
702
|
+
deps.add(`#Skin/${skinLabels[node.skin]}`);
|
|
703
|
+
}
|
|
704
|
+
const targets = mesh?.primitives[pi]?.targets?.length ?? 0;
|
|
705
|
+
if (targets > 0) {
|
|
706
|
+
const initial = node.weights ?? mesh?.weights ?? [];
|
|
707
|
+
comps["render/MorphWeights"] = {
|
|
708
|
+
weights: Array.from({ length: targets }, (_, k) => initial[k] ?? 0)
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (lod) {
|
|
712
|
+
const levels = [label];
|
|
713
|
+
for (const id of lod.ids) {
|
|
714
|
+
const lower = nodes[id]?.mesh;
|
|
715
|
+
const lowerLabel = lower === void 0 ? void 0 : primLabels[lower]?.[pi];
|
|
716
|
+
if (lowerLabel) levels.push(lowerLabel);
|
|
717
|
+
}
|
|
718
|
+
if (levels.length > 1) {
|
|
719
|
+
comps["render/Lod"] = {
|
|
720
|
+
levels: levels.map((l, k) => {
|
|
721
|
+
deps.add(`#${l}`);
|
|
722
|
+
return {
|
|
723
|
+
mesh: { path: `#${l}` },
|
|
724
|
+
screenSize: lodScreenSize(k, levels.length, lod.coverage)
|
|
725
|
+
};
|
|
726
|
+
})
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (materialIndex !== void 0 && materialLabels[materialIndex]) {
|
|
731
|
+
comps["render/MeshMaterial"] = {
|
|
732
|
+
material: { path: `#${materialLabels[materialIndex]}` }
|
|
733
|
+
};
|
|
734
|
+
deps.add(`#${materialLabels[materialIndex]}`);
|
|
735
|
+
}
|
|
736
|
+
const info = assets.find((x) => x.label === label)?.info;
|
|
737
|
+
for (let c = 0; c < 8; c++) {
|
|
738
|
+
const p = [
|
|
739
|
+
c & 1 ? info.bounds.max[0] : info.bounds.min[0],
|
|
740
|
+
c & 2 ? info.bounds.max[1] : info.bounds.min[1],
|
|
741
|
+
c & 4 ? info.bounds.max[2] : info.bounds.min[2]
|
|
742
|
+
];
|
|
743
|
+
for (let k = 0; k < 3; k++) {
|
|
744
|
+
const v = world[k] * p[0] + world[4 + k] * p[1] + world[8 + k] * p[2] + world[12 + k];
|
|
745
|
+
min[k] = Math.min(min[k], v);
|
|
746
|
+
max[k] = Math.max(max[k], v);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (record) {
|
|
750
|
+
const list = primitivePaths.get(ni) ?? [];
|
|
751
|
+
list.push(first ? path : `${path}/${pi}`);
|
|
752
|
+
primitivePaths.set(ni, list);
|
|
753
|
+
}
|
|
754
|
+
if (first) {
|
|
755
|
+
Object.assign(components, comps);
|
|
756
|
+
first = false;
|
|
757
|
+
} else {
|
|
758
|
+
children.push({ name: String(pi), components: { "core/Transform": {}, ...comps } });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (node.camera !== void 0 && settings.cameras) {
|
|
763
|
+
const cam = doc.cameras?.[node.camera];
|
|
764
|
+
if (cam?.type === "perspective" && cam.perspective) {
|
|
765
|
+
components["render/Camera3d"] = {
|
|
766
|
+
fovY: cam.perspective.yfov * 180 / Math.PI,
|
|
767
|
+
near: cam.perspective.znear
|
|
768
|
+
};
|
|
769
|
+
} else if (cam?.orthographic) {
|
|
770
|
+
components["render/Camera3d"] = {
|
|
771
|
+
projection: "orthographic",
|
|
772
|
+
orthoHeight: cam.orthographic.ymag * 2,
|
|
773
|
+
near: cam.orthographic.znear,
|
|
774
|
+
far: cam.orthographic.zfar
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const lightIndex = node.extensions?.KHR_lights_punctual?.light;
|
|
779
|
+
if (lightIndex !== void 0 && settings.lights) {
|
|
780
|
+
const light = lights[lightIndex];
|
|
781
|
+
if (light?.type === "directional") {
|
|
782
|
+
components["render/DirectionalLight"] = {
|
|
783
|
+
illuminance: light.intensity ?? 1,
|
|
784
|
+
color: [...light.color ?? [1, 1, 1], 1]
|
|
785
|
+
};
|
|
786
|
+
} else if (light?.type === "point" || light?.type === "spot") {
|
|
787
|
+
const candela = light.intensity ?? 1;
|
|
788
|
+
const range = light.range ?? Math.min(1e3, Math.max(1, Math.sqrt(candela / 0.01)));
|
|
789
|
+
const value = {
|
|
790
|
+
intensity: candela * 4 * Math.PI,
|
|
791
|
+
color: [...light.color ?? [1, 1, 1], 1],
|
|
792
|
+
range: range * (typeof rootScale === "number" ? rootScale : 1)
|
|
793
|
+
};
|
|
794
|
+
if (light.type === "spot") {
|
|
795
|
+
const deg = 180 / Math.PI;
|
|
796
|
+
const outer = light.spot?.outerConeAngle ?? Math.PI / 4;
|
|
797
|
+
value.outerAngle = Math.min(89.9, Math.max(0.1, outer * deg));
|
|
798
|
+
value.innerAngle = Math.min(
|
|
799
|
+
value.outerAngle,
|
|
800
|
+
(light.spot?.innerConeAngle ?? 0) * deg
|
|
801
|
+
);
|
|
802
|
+
components["render/SpotLight"] = value;
|
|
803
|
+
} else {
|
|
804
|
+
components["render/PointLight"] = value;
|
|
805
|
+
}
|
|
806
|
+
} else if (light) {
|
|
807
|
+
ctx.warn(`Unknown light type "${light.type}"; skipped.`, `/nodes/${ni}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const next = /* @__PURE__ */ new Set();
|
|
811
|
+
const childStack = new Set(stack).add(ni);
|
|
812
|
+
const groups = lodGroups(node.children ?? []);
|
|
813
|
+
for (const child of groups.build)
|
|
814
|
+
children.push(
|
|
815
|
+
build(child, next, path, world, false, childStack, groups.chains.get(child))
|
|
816
|
+
);
|
|
817
|
+
return {
|
|
818
|
+
name,
|
|
819
|
+
components,
|
|
820
|
+
...children.length ? { children } : {}
|
|
821
|
+
};
|
|
822
|
+
};
|
|
823
|
+
const top = /* @__PURE__ */ new Set();
|
|
824
|
+
const rootGroups = lodGroups(roots);
|
|
825
|
+
const entities = rootGroups.build.map(
|
|
826
|
+
(ni) => build(
|
|
827
|
+
ni,
|
|
828
|
+
top,
|
|
829
|
+
"",
|
|
830
|
+
matrixOf([0, 0, 0], [0, 0, 0, 1], [1, 1, 1]),
|
|
831
|
+
true,
|
|
832
|
+
/* @__PURE__ */ new Set(),
|
|
833
|
+
rootGroups.chains.get(ni)
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
const file = { version: 1, entities };
|
|
837
|
+
const bounds = Number.isFinite(min[0]) ? { min, max } : null;
|
|
838
|
+
return {
|
|
839
|
+
file,
|
|
840
|
+
deps: [...deps],
|
|
841
|
+
info: { nodes: paths.length, bounds, paths: paths.slice(0, 200) }
|
|
842
|
+
};
|
|
843
|
+
};
|
|
844
|
+
const scenes = doc.scenes?.length ? doc.scenes : [
|
|
845
|
+
{
|
|
846
|
+
nodes: nodes.map((_, i) => i).filter((i) => !nodes.some((n) => n.children?.includes(i)))
|
|
847
|
+
}
|
|
848
|
+
];
|
|
849
|
+
const defaultScene = doc.scene ?? 0;
|
|
850
|
+
const sceneLabels = labels(scenes);
|
|
851
|
+
const order = [defaultScene, ...scenes.map((_, i) => i).filter((i) => i !== defaultScene)];
|
|
852
|
+
for (const si of order) {
|
|
853
|
+
const scene = scenes[si];
|
|
854
|
+
if (!scene) continue;
|
|
855
|
+
const built = buildScene(scene.nodes ?? [], si === defaultScene);
|
|
856
|
+
const payload = {
|
|
857
|
+
json: built.file,
|
|
858
|
+
dependencies: built.deps,
|
|
859
|
+
info: built.info
|
|
860
|
+
};
|
|
861
|
+
if (si === defaultScene) assets.push({ label: "Scene", type: "Scene", ...payload });
|
|
862
|
+
if (scenes.length > 1)
|
|
863
|
+
assets.push({ label: `Scene/${sceneLabels[si]}`, type: "Scene", ...payload });
|
|
864
|
+
}
|
|
865
|
+
const pathOf = (ni) => nodePaths.get(ni) ?? sanitize(nodes[ni]?.name ?? `Node${ni}`);
|
|
866
|
+
for (const [i, skin] of (doc.skins ?? []).entries()) {
|
|
867
|
+
if (skin.joints.length > MAX_JOINTS) {
|
|
868
|
+
throw new ShardError(
|
|
869
|
+
"render/too-many-joints",
|
|
870
|
+
`Skin ${i} has ${skin.joints.length} joints; skinning supports ${MAX_JOINTS}`,
|
|
871
|
+
{
|
|
872
|
+
path: `/skins/${i}/joints`,
|
|
873
|
+
hint: "Split the mesh, or remove helper bones before exporting."
|
|
874
|
+
}
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
const matrices = skin.inverseBindMatrices === void 0 ? new Float32Array(skin.joints.length * 16).map((_, k) => k % 17 === 0 ? 1 : 0) : acc.floats(skin.inverseBindMatrices);
|
|
878
|
+
const header = skinArtifact({
|
|
879
|
+
name: skin.name ?? skinLabels[i],
|
|
880
|
+
joints: skin.joints.map(pathOf),
|
|
881
|
+
...skin.skeleton !== void 0 ? { skeleton: pathOf(skin.skeleton) } : {},
|
|
882
|
+
restPose: skin.joints.map((j) => {
|
|
883
|
+
const { t: tr, r, s } = trs(nodes[j] ?? {});
|
|
884
|
+
return { translation: tr, rotation: r, scale: s };
|
|
885
|
+
})
|
|
886
|
+
});
|
|
887
|
+
assets.push({
|
|
888
|
+
label: `Skin/${skinLabels[i]}`,
|
|
889
|
+
type: "Skin",
|
|
890
|
+
json: header,
|
|
891
|
+
bytes: new Uint8Array(matrices.buffer.slice(0)),
|
|
892
|
+
info: { joints: skin.joints.length }
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
const INTERPOLATION = {
|
|
896
|
+
LINEAR: "linear",
|
|
897
|
+
STEP: "step",
|
|
898
|
+
CUBICSPLINE: "cubic"
|
|
899
|
+
};
|
|
900
|
+
const animationLabels = labels(doc.animations);
|
|
901
|
+
for (const [i, anim] of (doc.animations ?? []).entries()) {
|
|
902
|
+
let duration = 0;
|
|
903
|
+
const channels = [];
|
|
904
|
+
for (const [ci, ch] of anim.channels.entries()) {
|
|
905
|
+
const ptr = `/animations/${i}/channels/${ci}`;
|
|
906
|
+
const sampler = anim.samplers[ch.sampler];
|
|
907
|
+
if (!sampler || ch.target.node === void 0) {
|
|
908
|
+
ctx.warn("Animation channel without a node or sampler; skipped.", ptr);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const interpolation = INTERPOLATION[sampler.interpolation ?? "LINEAR"];
|
|
912
|
+
if (!interpolation) {
|
|
913
|
+
ctx.warn(`Unknown interpolation "${sampler.interpolation}"; skipped.`, ptr);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
const times = acc.floats(sampler.input);
|
|
917
|
+
const out = acc.read(sampler.output);
|
|
918
|
+
const values = Float32Array.from(out.values);
|
|
919
|
+
duration = Math.max(duration, times[times.length - 1] ?? 0);
|
|
920
|
+
const keys = Math.max(1, times.length) * (interpolation === "cubic" ? 3 : 1);
|
|
921
|
+
const path = ch.target.path;
|
|
922
|
+
if (path === "weights") {
|
|
923
|
+
const width = Math.round(values.length / keys);
|
|
924
|
+
const targets = primitivePaths.get(ch.target.node) ?? [];
|
|
925
|
+
if (targets.length === 0) ctx.warn("Weights channel on a node without a mesh.", ptr);
|
|
926
|
+
for (const target of targets) {
|
|
927
|
+
channels.push({
|
|
928
|
+
target,
|
|
929
|
+
component: "render/MorphWeights",
|
|
930
|
+
field: "weights",
|
|
931
|
+
interpolation,
|
|
932
|
+
times,
|
|
933
|
+
values,
|
|
934
|
+
width
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
} else if (path === "translation" || path === "rotation" || path === "scale") {
|
|
938
|
+
channels.push({
|
|
939
|
+
target: pathOf(ch.target.node),
|
|
940
|
+
component: "core/Transform",
|
|
941
|
+
field: path,
|
|
942
|
+
interpolation,
|
|
943
|
+
times,
|
|
944
|
+
values,
|
|
945
|
+
width: out.components
|
|
946
|
+
});
|
|
947
|
+
} else {
|
|
948
|
+
ctx.warn(`Animation path "${path}" isn't supported; skipped.`, `${ptr}/target/path`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const clip = { name: anim.name ?? animationLabels[i], duration, channels, events: [] };
|
|
952
|
+
assets.push({
|
|
953
|
+
label: `Animation/${animationLabels[i]}`,
|
|
954
|
+
type: "AnimationClip",
|
|
955
|
+
...encodeClip(clip),
|
|
956
|
+
info: clipInfo(clip)
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return { assets };
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
export {
|
|
963
|
+
GltfImportSettings,
|
|
964
|
+
GltfImporter
|
|
965
|
+
};
|