@starcried/starlite-vrm 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +18 -0
- package/dist/adjustments.d.ts +14 -0
- package/dist/adjustments.js +106 -0
- package/dist/config.cjs +30 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.js +25 -1
- package/dist/gaze.d.ts +2 -1
- package/dist/gaze.js +17 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +240 -197
- package/dist/inspect.cjs +213 -74
- package/dist/inspect.d.ts +56 -9
- package/dist/inspect.js +257 -84
- package/dist/playback.js +25 -3
- package/dist/prepare.cjs +211 -74
- package/dist/types.d.ts +20 -1
- package/package.json +2 -2
package/dist/inspect.cjs
CHANGED
|
@@ -21,153 +21,266 @@ var inspect_exports = {};
|
|
|
21
21
|
__export(inspect_exports, {
|
|
22
22
|
MAX_ANIMATION_BYTES: () => MAX_ANIMATION_BYTES,
|
|
23
23
|
MAX_MODEL_BYTES: () => MAX_MODEL_BYTES,
|
|
24
|
+
VrmInspectionError: () => VrmInspectionError,
|
|
24
25
|
embeddedAnimationCatalog: () => embeddedAnimationCatalog,
|
|
25
26
|
inspectVrm: () => inspectVrm,
|
|
26
27
|
inspectVrma: () => inspectVrma,
|
|
27
28
|
parseGlb: () => parseGlb
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(inspect_exports);
|
|
31
|
+
var VrmInspectionError = class extends Error {
|
|
32
|
+
code;
|
|
33
|
+
details;
|
|
34
|
+
constructor(code, message, details = {}) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "VrmInspectionError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.details = details;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var requiredBones = ["hips", "spine", "head", ...["left", "right"].flatMap((side) => ["UpperLeg", "LowerLeg", "Foot", "UpperArm", "LowerArm", "Hand"].map((part) => side + part))];
|
|
42
|
+
var optionalBones = ["chest", "upperChest", "neck", "jaw", ...["left", "right"].flatMap((side) => ["Eye", "Shoulder", "Toes", ...["ThumbMetacarpal", "ThumbProximal", "ThumbIntermediate", "ThumbDistal", ...["Index", "Middle", "Ring", "Little"].flatMap((finger) => ["Proximal", "Intermediate", "Distal"].map((part) => finger + part))]].map((part) => side + part))];
|
|
30
43
|
var MAX_MODEL_BYTES = 64 * 1024 * 1024;
|
|
31
44
|
var MAX_ANIMATION_BYTES = 16 * 1024 * 1024;
|
|
32
45
|
function parseGlb(bytes, kind) {
|
|
46
|
+
try {
|
|
47
|
+
return parseGlbContents(bytes, kind);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof VrmInspectionError) throw error;
|
|
50
|
+
throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF structure; re-export a self-contained VRM/VRMA file");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function parseGlbContents(bytes, kind) {
|
|
33
54
|
const limit = kind === "vrm" ? MAX_MODEL_BYTES : MAX_ANIMATION_BYTES;
|
|
34
|
-
if (bytes.byteLength < 20 || bytes.byteLength > limit) throw new
|
|
55
|
+
if (bytes.byteLength < 20 || bytes.byteLength > limit) throw new VrmInspectionError("INVALID_GLB", `invalid GLB size; ${kind.toUpperCase()} must be a binary glTF 2.0 file no larger than ${limit / 1024 / 1024} MiB`);
|
|
35
56
|
const view = new DataView(bytes);
|
|
36
|
-
if (view.getUint32(0, true) !== 1179937895 || view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== bytes.byteLength) throw new
|
|
57
|
+
if (view.getUint32(0, true) !== 1179937895 || view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== bytes.byteLength) throw new VrmInspectionError("INVALID_GLB", "invalid GLB header; export binary glTF 2.0 VRM/VRMA, not FBX or JSON glTF");
|
|
37
58
|
let json;
|
|
38
59
|
let binSize = 0;
|
|
39
60
|
let binStart = 0;
|
|
40
61
|
let chunkCount = 0;
|
|
41
62
|
for (let offset = 12; offset < bytes.byteLength; ) {
|
|
42
|
-
if (offset + 8 > bytes.byteLength) throw new
|
|
63
|
+
if (offset + 8 > bytes.byteLength) throw new VrmInspectionError("INVALID_GLB", "truncated GLB chunk; download or export the complete file again");
|
|
43
64
|
const size = view.getUint32(offset, true), type = view.getUint32(offset + 4, true);
|
|
44
65
|
offset += 8;
|
|
45
|
-
if (size % 4 || size > bytes.byteLength - offset) throw new
|
|
66
|
+
if (size % 4 || size > bytes.byteLength - offset) throw new VrmInspectionError("INVALID_GLB", "invalid GLB chunk length; download or export the complete file again");
|
|
46
67
|
if (chunkCount === 0 && type === 1313821514) {
|
|
47
|
-
|
|
68
|
+
try {
|
|
69
|
+
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes, offset, size)));
|
|
70
|
+
} catch {
|
|
71
|
+
throw new VrmInspectionError("INVALID_GLB", "invalid GLB JSON; re-export the file with a VRM exporter");
|
|
72
|
+
}
|
|
48
73
|
} else if (chunkCount === 1 && type === 5130562) {
|
|
49
74
|
binSize = size;
|
|
50
75
|
binStart = offset;
|
|
51
|
-
} else throw new
|
|
76
|
+
} else throw new VrmInspectionError("INVALID_GLB", "unexpected GLB chunk; export a self-contained VRM/VRMA file");
|
|
52
77
|
offset += size;
|
|
53
78
|
chunkCount++;
|
|
54
79
|
}
|
|
55
|
-
if (!json || json.asset?.version !== "2.0") throw new
|
|
80
|
+
if (!json || typeof json !== "object" || Array.isArray(json) || json.asset?.version !== "2.0") throw new VrmInspectionError("UNSUPPORTED_FORMAT", "glTF 2.0 is required; use a VRM/VRMA exporter");
|
|
56
81
|
const expected = kind === "vrm" ? json.extensions?.VRM ?? json.extensions?.VRMC_vrm : json.extensions?.VRMC_vrm_animation;
|
|
57
|
-
if (!expected) throw new
|
|
58
|
-
if (kind === "vrm" && json.extensions?.VRM && json.extensions.VRM.specVersion !== "0.0") throw new
|
|
59
|
-
if (kind === "vrm" && json.extensions?.VRMC_vrm && json.extensions.VRMC_vrm.specVersion !== "1.0") throw new
|
|
60
|
-
if (kind === "vrma" && expected.specVersion !== "1.0") throw new
|
|
82
|
+
if (!expected) throw new VrmInspectionError("UNSUPPORTED_FORMAT", `missing ${kind} extension; export ${kind === "vrm" ? "VRM 0.0 or 1.0" : "VRMA 1.0"}, not a plain GLB`);
|
|
83
|
+
if (kind === "vrm" && json.extensions?.VRM && json.extensions.VRM.specVersion !== "0.0") throw new VrmInspectionError("UNSUPPORTED_VERSION", "unsupported VRM version; export VRM 0.0 or 1.0");
|
|
84
|
+
if (kind === "vrm" && json.extensions?.VRMC_vrm && json.extensions.VRMC_vrm.specVersion !== "1.0") throw new VrmInspectionError("UNSUPPORTED_VERSION", "unsupported VRM version; export VRM 0.0 or 1.0");
|
|
85
|
+
if (kind === "vrma" && expected.specVersion !== "1.0") throw new VrmInspectionError("UNSUPPORTED_VERSION", "VRMA 1.0 is required");
|
|
61
86
|
const allowed = /* @__PURE__ */ new Set(["VRM", "VRMC_vrm", "VRMC_vrm_animation", "VRMC_materials_mtoon", "VRMC_springBone", "VRMC_node_constraint", "VRMC_materials_hdr_emissiveMultiplier", "KHR_materials_unlit", "KHR_texture_transform", "KHR_materials_emissive_strength"]);
|
|
62
|
-
if ((json.extensionsRequired ?? []).some((name) => !allowed.has(name))) throw new
|
|
87
|
+
if ((json.extensionsRequired ?? []).some((name) => !allowed.has(name))) throw new VrmInspectionError("UNSUPPORTED_EXTENSION", "unsupported required glTF extension; re-export without geometry or texture compression extensions");
|
|
63
88
|
const visit = (value, depth = 0) => {
|
|
64
|
-
if (depth > 64) throw new
|
|
89
|
+
if (depth > 64) throw new VrmInspectionError("RESOURCE_LIMIT", "glTF nesting limit exceeded");
|
|
65
90
|
if (!value || typeof value !== "object") return;
|
|
66
91
|
for (const [key, child] of Object.entries(value)) {
|
|
67
|
-
if (key === "uri") throw new
|
|
68
|
-
if (typeof child === "number" && !Number.isFinite(child)) throw new
|
|
92
|
+
if (key === "uri") throw new VrmInspectionError("EXTERNAL_RESOURCE", "external glTF resources are not supported; embed all textures and buffers in one VRM/VRMA file");
|
|
93
|
+
if (typeof child === "number" && !Number.isFinite(child)) throw new VrmInspectionError("INVALID_STRUCTURE", "non-finite glTF value");
|
|
69
94
|
visit(child, depth + 1);
|
|
70
95
|
}
|
|
71
96
|
};
|
|
72
97
|
visit(json);
|
|
73
|
-
if ((json.buffers?.length ?? 0) > 1 || (json.buffers?.[0]?.byteLength ?? 0) > binSize) throw new
|
|
98
|
+
if ((json.buffers?.length ?? 0) > 1 || (json.buffers?.[0]?.byteLength ?? 0) > binSize) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid GLB buffer");
|
|
74
99
|
for (const buffer of json.bufferViews ?? []) {
|
|
75
100
|
const start = buffer.byteOffset ?? 0, length = buffer.byteLength;
|
|
76
|
-
if (buffer.buffer !== 0 || !Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 0 || start + length > (json.buffers?.[0]?.byteLength ?? 0)) throw new
|
|
101
|
+
if (buffer.buffer !== 0 || !Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 0 || start + length > (json.buffers?.[0]?.byteLength ?? 0)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF buffer view");
|
|
77
102
|
}
|
|
78
|
-
if ((json.nodes?.length ?? 0) > 4096 || (json.accessors?.length ?? 0) > 32768) throw new
|
|
103
|
+
if ((json.nodes?.length ?? 0) > 4096 || (json.accessors?.length ?? 0) > 32768) throw new VrmInspectionError("RESOURCE_LIMIT", "glTF resource count exceeded");
|
|
79
104
|
validateStructure(json, new DataView(bytes, binStart, binSize));
|
|
80
105
|
return json;
|
|
81
106
|
}
|
|
82
107
|
function embeddedAnimationCatalog(json) {
|
|
83
108
|
const animations = json.animations ?? [];
|
|
84
|
-
if (!Array.isArray(animations) || animations.length > 64) throw new
|
|
109
|
+
if (!Array.isArray(animations) || animations.length > 64) throw new VrmInspectionError("INVALID_ANIMATION", "at most 64 embedded animations are supported");
|
|
85
110
|
const names = /* @__PURE__ */ new Set();
|
|
86
111
|
return animations.map((animation, index) => {
|
|
87
112
|
const name = animation.name ?? "Animation_" + (index + 1);
|
|
88
|
-
if (typeof name !== "string" || !name.trim() || new TextEncoder().encode(name).length > 128 || /[\u0000-\u001f\u007f]/.test(name) || name === "StarLiteIdle" || names.has(name)) throw new
|
|
113
|
+
if (typeof name !== "string" || !name.trim() || new TextEncoder().encode(name).length > 128 || /[\u0000-\u001f\u007f]/.test(name) || name === "StarLiteIdle" || names.has(name)) throw new VrmInspectionError("INVALID_ANIMATION", "invalid or duplicate embedded animation name; use unique names of at most 128 UTF-8 bytes, excluding StarLiteIdle");
|
|
89
114
|
names.add(name);
|
|
90
115
|
let duration = 0;
|
|
91
116
|
for (const sampler of animation.samplers ?? []) {
|
|
92
117
|
const accessor = json.accessors?.[sampler.input];
|
|
93
|
-
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new
|
|
118
|
+
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new VrmInspectionError("INVALID_ANIMATION", "embedded animation has invalid animation times");
|
|
94
119
|
duration = Math.max(duration, accessor.max[0]);
|
|
95
120
|
}
|
|
96
|
-
if (!(duration > 0) || duration > 600) throw new
|
|
121
|
+
if (!(duration > 0) || duration > 600) throw new VrmInspectionError("INVALID_ANIMATION", "embedded animation duration must be between 0 and 600 seconds");
|
|
97
122
|
return { name, durationMs: duration * 1e3 };
|
|
98
123
|
});
|
|
99
124
|
}
|
|
100
125
|
function inspectVrm(bytes) {
|
|
101
126
|
const json = parseGlb(bytes, "vrm");
|
|
102
127
|
const v0 = json.extensions.VRM;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const used = /* @__PURE__ */ new Set();
|
|
107
|
-
for (const name2 of required) {
|
|
108
|
-
const node = bones?.[name2]?.node;
|
|
109
|
-
if (!Number.isSafeInteger(node) || !json.nodes?.[node] || used.has(node)) throw new Error("invalid required humanoid bone");
|
|
110
|
-
used.add(node);
|
|
111
|
-
}
|
|
128
|
+
const extension = v0 ?? json.extensions.VRMC_vrm;
|
|
129
|
+
if (Array.isArray(json.extensionsUsed) && !json.extensionsUsed.includes(v0 ? "VRM" : "VRMC_vrm")) throw new VrmInspectionError("UNSUPPORTED_FORMAT", "missing VRM extension declaration; re-export with the VRM exporter");
|
|
130
|
+
const bones = inspectHumanBones(json, extension.humanoid?.humanBones, Boolean(v0));
|
|
112
131
|
const blinkNames = v0 ? ["blink", "blink_l", "blink_r"] : ["blink", "blinkLeft", "blinkRight"];
|
|
113
132
|
const groups = v0?.blendShapeMaster?.blendShapeGroups;
|
|
114
|
-
if (groups !== void 0 && !Array.isArray(groups)) throw new
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const binds = v0 ? expression.binds : expression.morphTargetBinds;
|
|
122
|
-
if (binds === void 0) continue;
|
|
123
|
-
if (!Array.isArray(binds)) throw new Error("invalid VRM blink bindings");
|
|
124
|
-
for (const bind of binds) {
|
|
125
|
-
const mesh = v0 ? bind?.mesh : json.nodes?.[bind?.node]?.mesh;
|
|
126
|
-
const primitives = json.meshes?.[mesh]?.primitives;
|
|
127
|
-
if (!bind || !Number.isSafeInteger(mesh) || mesh < 0 || !v0 && (!Number.isSafeInteger(bind.node) || bind.node < 0) || !Number.isSafeInteger(bind.index) || bind.index < 0 || !Number.isFinite(bind.weight) || bind.weight < 0 || bind.weight > (v0 ? 100 : 1) || !Array.isArray(primitives) || !primitives.some((primitive) => primitive.targets?.[bind.index] !== void 0)) {
|
|
128
|
-
throw new Error("invalid VRM blink morph binding");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
133
|
+
if (groups !== void 0 && !Array.isArray(groups)) throw new VrmInspectionError("INVALID_BLINK", "invalid VRM blink expressions; re-export the blink presets");
|
|
134
|
+
const aliases = { a: "aa", e: "ee", i: "ih", o: "oh", u: "ou", joy: "happy", angry: "angry", sorrow: "sad", fun: "relaxed", neutral: "neutral", blink: "blink", blink_l: "blinkLeft", blink_r: "blinkRight", lookup: "lookUp", lookdown: "lookDown", lookleft: "lookLeft", lookright: "lookRight" };
|
|
135
|
+
const registeredName = (item) => aliases[item?.presetName] ?? item?.name;
|
|
136
|
+
const expression = (name2) => v0 ? (groups ?? []).find((item) => registeredName(item) === (aliases[name2] ?? name2)) : extension.expressions?.preset?.[name2];
|
|
137
|
+
for (const preset of blinkNames) {
|
|
138
|
+
const presets = v0 ? (groups ?? []).filter((item) => registeredName(item) === aliases[preset]) : expression(preset) === void 0 ? [] : [expression(preset)];
|
|
139
|
+
for (const item of presets) inspectMorphExpression(json, item, Boolean(v0), "INVALID_BLINK", preset);
|
|
131
140
|
}
|
|
132
|
-
const
|
|
141
|
+
const [combined, left, right] = blinkNames.map((preset) => inspectMorphExpression(json, expression(preset), Boolean(v0), "INVALID_BLINK", preset));
|
|
142
|
+
const meta = extension.meta ?? {};
|
|
133
143
|
const name = v0 ? meta.title : meta.name;
|
|
134
144
|
return {
|
|
135
145
|
version: v0 ? "0" : "1",
|
|
136
|
-
...typeof name === "string" ? { name } : {},
|
|
146
|
+
...typeof name === "string" ? { name: safeLabel(name) } : {},
|
|
137
147
|
animations: embeddedAnimationCatalog(json),
|
|
138
|
-
expressions: v0 ? (
|
|
148
|
+
expressions: (v0 ? (groups ?? []).map((group) => group?.name) : Object.keys({ ...extension.expressions?.preset, ...extension.expressions?.custom })).filter((value) => typeof value === "string").map(safeLabel),
|
|
149
|
+
capabilities: {
|
|
150
|
+
humanoid: { requiredBones: [...requiredBones], optionalBones: optionalBones.filter((name2) => bones.has(name2)) },
|
|
151
|
+
blink: { automatic: Boolean(combined || left && right), combined: Boolean(combined), left: Boolean(left), right: Boolean(right) },
|
|
152
|
+
lookAt: inspectLookAt(json, extension, Boolean(v0), bones, expression)
|
|
153
|
+
},
|
|
154
|
+
metrics: inspectMetrics(json, bytes.byteLength)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function safeLabel(value) {
|
|
158
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 128);
|
|
159
|
+
}
|
|
160
|
+
function inspectHumanBones(json, humanoid, v0) {
|
|
161
|
+
const known = /* @__PURE__ */ new Set([...requiredBones, ...optionalBones]), result = /* @__PURE__ */ new Map();
|
|
162
|
+
const invalid = [], duplicates = [], used = /* @__PURE__ */ new Map();
|
|
163
|
+
const entries = v0 ? Array.isArray(humanoid) ? humanoid.filter((item) => item && known.has(item.bone)).map((item) => [item.bone, item]) : [] : humanoid && typeof humanoid === "object" && !Array.isArray(humanoid) ? Object.entries(humanoid).filter(([name]) => known.has(name)) : [];
|
|
164
|
+
for (const [name, value] of entries) {
|
|
165
|
+
const node = value?.node;
|
|
166
|
+
if (result.has(name)) duplicates.push(name);
|
|
167
|
+
if (!Number.isSafeInteger(node) || node < 0 || !json.nodes?.[node]) {
|
|
168
|
+
invalid.push(name);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const previous = used.get(node);
|
|
172
|
+
if (previous && previous !== name) duplicates.push(previous, name);
|
|
173
|
+
used.set(node, name);
|
|
174
|
+
result.set(name, node);
|
|
175
|
+
}
|
|
176
|
+
const missing = requiredBones.filter((name) => !result.has(name) && !invalid.includes(name));
|
|
177
|
+
if (missing.length || invalid.length || duplicates.length) {
|
|
178
|
+
const details = { missingBones: missing, invalidBones: [...new Set(invalid)], duplicateBones: [...new Set(duplicates)] };
|
|
179
|
+
const summary = [missing.length ? `missing ${missing.join(", ")}` : "", invalid.length ? `invalid node for ${details.invalidBones.join(", ")}` : "", duplicates.length ? `duplicate mapping for ${details.duplicateBones.join(", ")}` : ""].filter(Boolean).join("; ");
|
|
180
|
+
throw new VrmInspectionError("HUMANOID_BONES", `invalid required humanoid bone: ${summary}; assign separate human bones in the VRM exporter and export again`, details);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
function inspectMorphExpression(json, expression, v0, code, preset) {
|
|
185
|
+
if (expression === void 0) return false;
|
|
186
|
+
const label = code === "INVALID_BLINK" ? "blink" : "look-at";
|
|
187
|
+
if (!expression || typeof expression !== "object" || Array.isArray(expression)) throw new VrmInspectionError(code, `invalid VRM ${label} expression; re-export the ${preset} preset`, { preset });
|
|
188
|
+
const binds = v0 ? expression.binds : expression.morphTargetBinds;
|
|
189
|
+
if (binds === void 0) return false;
|
|
190
|
+
if (!Array.isArray(binds)) throw new VrmInspectionError(code, `invalid VRM ${label} bindings; re-export the ${preset} preset`, { preset });
|
|
191
|
+
let usable = false;
|
|
192
|
+
for (const bind of binds) {
|
|
193
|
+
const mesh = v0 ? bind?.mesh : json.nodes?.[bind?.node]?.mesh;
|
|
194
|
+
const primitives = json.meshes?.[mesh]?.primitives;
|
|
195
|
+
if (!bind || !Number.isSafeInteger(mesh) || mesh < 0 || !v0 && (!Number.isSafeInteger(bind.node) || bind.node < 0) || !Number.isSafeInteger(bind.index) || bind.index < 0 || !Number.isFinite(bind.weight) || bind.weight < 0 || bind.weight > (v0 ? 100 : 1) || !Array.isArray(primitives) || !primitives.length || !primitives.every((primitive) => primitive?.targets?.[bind.index] !== void 0)) {
|
|
196
|
+
throw new VrmInspectionError(code, `invalid VRM ${label} morph binding: ${preset}; check its mesh/node, morph target index and weight in the VRM exporter`, { preset });
|
|
197
|
+
}
|
|
198
|
+
const instantiated = !v0 || (json.nodes ?? []).some((node) => node?.mesh === mesh);
|
|
199
|
+
const morphs = primitives.every((primitive) => primitive.targets.some((target) => ["POSITION", "NORMAL", "COLOR_0"].some((semantic) => Number.isSafeInteger(target?.[semantic]) && json.accessors?.[target[semantic]])));
|
|
200
|
+
usable ||= bind.weight > 0 && instantiated && morphs;
|
|
201
|
+
}
|
|
202
|
+
return usable;
|
|
203
|
+
}
|
|
204
|
+
function inspectLookAt(json, extension, v0, bones, expression) {
|
|
205
|
+
const lookAt = v0 ? extension.firstPerson : extension.lookAt;
|
|
206
|
+
const empty = { mode: "none", head: true, eyes: false, horizontal: false, vertical: false };
|
|
207
|
+
if (lookAt === void 0) return empty;
|
|
208
|
+
if (!lookAt || typeof lookAt !== "object" || Array.isArray(lookAt)) throw new VrmInspectionError("INVALID_LOOK_AT", "invalid VRM look-at configuration; re-export eye tracking");
|
|
209
|
+
const expressionManager = v0 ? extension.blendShapeMaster : extension.expressions;
|
|
210
|
+
if (!expressionManager) return empty;
|
|
211
|
+
const type = v0 ? lookAt.lookAtTypeName : lookAt.type;
|
|
212
|
+
if (type !== void 0 && !(v0 ? ["Bone", "BlendShape"] : ["bone", "expression"]).includes(type)) throw new VrmInspectionError("INVALID_LOOK_AT", "invalid VRM look-at type; use bone or expression eye tracking in the VRM exporter");
|
|
213
|
+
const mode = type === (v0 ? "BlendShape" : "expression") ? "expression" : "bone";
|
|
214
|
+
const scale = (name) => {
|
|
215
|
+
const range = lookAt[name], value = range?.[v0 ? "yRange" : "outputScale"] ?? (mode === "expression" ? 1 : 10);
|
|
216
|
+
const input = range?.[v0 ? "xRange" : "inputMaxValue"] ?? 90;
|
|
217
|
+
if (range !== void 0 && (!range || typeof range !== "object" || Array.isArray(range)) || !Number.isFinite(value) || value < 0 || !Number.isFinite(input) || input < 0) throw new VrmInspectionError("INVALID_LOOK_AT", "invalid VRM look-at range; use finite, non-negative input and output ranges");
|
|
218
|
+
return value;
|
|
139
219
|
};
|
|
220
|
+
const horizontalRange = scale(v0 ? "lookAtHorizontalOuter" : "rangeMapHorizontalOuter");
|
|
221
|
+
const innerRange = scale(v0 ? "lookAtHorizontalInner" : "rangeMapHorizontalInner");
|
|
222
|
+
const upRange = scale(v0 ? "lookAtVerticalUp" : "rangeMapVerticalUp"), downRange = scale(v0 ? "lookAtVerticalDown" : "rangeMapVerticalDown");
|
|
223
|
+
let horizontal, vertical;
|
|
224
|
+
if (mode === "bone") {
|
|
225
|
+
const eyes = bones.has("leftEye") || bones.has("rightEye");
|
|
226
|
+
horizontal = eyes && (horizontalRange > 0 || innerRange > 0);
|
|
227
|
+
vertical = eyes && (upRange > 0 || downRange > 0);
|
|
228
|
+
} else {
|
|
229
|
+
const usable = (name) => inspectMorphExpression(json, expression(name), v0, "INVALID_LOOK_AT", name);
|
|
230
|
+
const left = usable(v0 ? "lookleft" : "lookLeft"), right = usable(v0 ? "lookright" : "lookRight"), up = usable(v0 ? "lookup" : "lookUp"), down = usable(v0 ? "lookdown" : "lookDown");
|
|
231
|
+
horizontal = horizontalRange > 0 && left && right;
|
|
232
|
+
vertical = upRange > 0 && downRange > 0 && up && down;
|
|
233
|
+
}
|
|
234
|
+
return { mode, head: true, eyes: horizontal || vertical, horizontal, vertical };
|
|
235
|
+
}
|
|
236
|
+
function inspectMetrics(json, fileBytes) {
|
|
237
|
+
const primitives = (json.meshes ?? []).flatMap((mesh) => Array.isArray(mesh?.primitives) ? mesh.primitives : []);
|
|
238
|
+
let vertexCount = 0, triangleCount = 0, morphTargetCount = 0;
|
|
239
|
+
for (const primitive of primitives) {
|
|
240
|
+
const vertices = json.accessors?.[primitive?.attributes?.POSITION]?.count ?? 0;
|
|
241
|
+
const indices = primitive?.indices === void 0 ? vertices : json.accessors?.[primitive.indices]?.count ?? 0;
|
|
242
|
+
vertexCount += vertices;
|
|
243
|
+
if (primitive?.mode === void 0 || primitive.mode === 4) triangleCount += Math.floor(indices / 3);
|
|
244
|
+
else if (primitive.mode === 5 || primitive.mode === 6) triangleCount += Math.max(0, indices - 2);
|
|
245
|
+
morphTargetCount += Array.isArray(primitive?.targets) ? primitive.targets.length : 0;
|
|
246
|
+
}
|
|
247
|
+
const widths = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 }, components = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
248
|
+
const decodedAccessorBytes = (json.accessors ?? []).reduce((sum, accessor) => {
|
|
249
|
+
const width = widths[accessor.componentType], dimension = accessor.type === "MAT2" ? 2 : accessor.type === "MAT3" ? 3 : accessor.type === "MAT4" ? 4 : 0;
|
|
250
|
+
return sum + accessor.count * (dimension ? Math.ceil(dimension * width / 4) * 4 * dimension : width * components[accessor.type]);
|
|
251
|
+
}, 0);
|
|
252
|
+
return { fileBytes, nodeCount: json.nodes?.length ?? 0, meshCount: json.meshes?.length ?? 0, primitiveCount: primitives.length, materialCount: json.materials?.length ?? 0, textureCount: json.textures?.length ?? 0, imageCount: json.images?.length ?? 0, vertexCount, triangleCount, morphTargetCount, decodedAccessorBytes };
|
|
140
253
|
}
|
|
141
254
|
function inspectVrma(bytes) {
|
|
142
255
|
const json = parseGlb(bytes, "vrma");
|
|
143
|
-
if (json.animations?.length !== 1) throw new
|
|
256
|
+
if (json.animations?.length !== 1) throw new VrmInspectionError("INVALID_STRUCTURE", "each VRMA file must contain exactly one animation");
|
|
144
257
|
let duration = 0;
|
|
145
258
|
for (const sampler of json.animations[0].samplers ?? []) {
|
|
146
259
|
const accessor = json.accessors?.[sampler.input];
|
|
147
|
-
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new
|
|
260
|
+
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new VrmInspectionError("INVALID_STRUCTURE", "VRMA has invalid animation times");
|
|
148
261
|
duration = Math.max(duration, accessor.max[0]);
|
|
149
262
|
}
|
|
150
|
-
if (!(duration > 0) || duration > 600) throw new
|
|
263
|
+
if (!(duration > 0) || duration > 600) throw new VrmInspectionError("INVALID_STRUCTURE", "VRMA duration must be between 0 and 600 seconds");
|
|
151
264
|
return { durationMs: duration * 1e3 };
|
|
152
265
|
}
|
|
153
266
|
function validateStructure(json, binary) {
|
|
154
|
-
for (const key of ["nodes", "accessors", "bufferViews", "buffers", "meshes", "images", "animations", "skins", "scenes"]) {
|
|
155
|
-
if (json[key] !== void 0 && !Array.isArray(json[key])) throw new
|
|
267
|
+
for (const key of ["nodes", "accessors", "bufferViews", "buffers", "meshes", "materials", "textures", "images", "animations", "skins", "scenes", "extensionsUsed", "extensionsRequired"]) {
|
|
268
|
+
if (json[key] !== void 0 && !Array.isArray(json[key])) throw new VrmInspectionError("INVALID_STRUCTURE", `invalid glTF ${key}`);
|
|
156
269
|
}
|
|
157
270
|
const views = json.bufferViews ?? [], nodes = json.nodes ?? [], accessors = json.accessors ?? [];
|
|
158
271
|
const integer = (n, min, max) => Number.isSafeInteger(n) && n >= min && n <= max;
|
|
159
272
|
const index = (n, array) => integer(n, 0, array.length - 1);
|
|
160
273
|
const seen = /* @__PURE__ */ new Set(), active = /* @__PURE__ */ new Set(), parents = /* @__PURE__ */ new Set();
|
|
161
274
|
const walk = (i, depth) => {
|
|
162
|
-
if (!index(i, nodes) || active.has(i) || depth > 128) throw new
|
|
275
|
+
if (!index(i, nodes) || active.has(i) || depth > 128) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF node hierarchy");
|
|
163
276
|
if (seen.has(i)) return;
|
|
164
277
|
const node = nodes[i];
|
|
165
|
-
if (!node || typeof node !== "object") throw new
|
|
278
|
+
if (!node || typeof node !== "object") throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF node");
|
|
166
279
|
seen.add(i);
|
|
167
280
|
active.add(i);
|
|
168
|
-
if (node.children !== void 0 && !Array.isArray(node.children)) throw new
|
|
281
|
+
if (node.children !== void 0 && !Array.isArray(node.children)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid node children");
|
|
169
282
|
for (const child of node.children ?? []) {
|
|
170
|
-
if (parents.has(child)) throw new
|
|
283
|
+
if (parents.has(child)) throw new VrmInspectionError("INVALID_STRUCTURE", "node has multiple parents");
|
|
171
284
|
parents.add(child);
|
|
172
285
|
walk(child, depth + 1);
|
|
173
286
|
}
|
|
@@ -178,21 +291,21 @@ function validateStructure(json, binary) {
|
|
|
178
291
|
const sizes = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
|
179
292
|
let decoded = 0;
|
|
180
293
|
const span = (viewIndex, offset, count, element, alignment, strided = false) => {
|
|
181
|
-
if (!index(viewIndex, views)) throw new
|
|
294
|
+
if (!index(viewIndex, views)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid accessor buffer view");
|
|
182
295
|
const v = views[viewIndex], stride = strided ? v.byteStride ?? element : element;
|
|
183
|
-
if (!integer(offset, 0, v.byteLength) || offset % alignment || !integer(stride, element, 256) || stride % alignment || offset + (count - 1) * stride + element > v.byteLength) throw new
|
|
296
|
+
if (!integer(offset, 0, v.byteLength) || offset % alignment || !integer(stride, element, 256) || stride % alignment || offset + (count - 1) * stride + element > v.byteLength) throw new VrmInspectionError("INVALID_STRUCTURE", "accessor exceeds buffer view");
|
|
184
297
|
return (v.byteOffset ?? 0) + offset;
|
|
185
298
|
};
|
|
186
299
|
for (const a of accessors) {
|
|
187
300
|
const width = sizes[a?.componentType], count = a?.count, componentsCount = components[a?.type];
|
|
188
|
-
if (!width || !componentsCount || !integer(count, 1, 2e6)) throw new
|
|
301
|
+
if (!width || !componentsCount || !integer(count, 1, 2e6)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid accessor allocation");
|
|
189
302
|
const dimension = a.type === "MAT2" ? 2 : a.type === "MAT3" ? 3 : a.type === "MAT4" ? 4 : 0;
|
|
190
303
|
const element = dimension ? Math.ceil(dimension * width / 4) * 4 * dimension : width * componentsCount;
|
|
191
304
|
decoded += element * count;
|
|
192
|
-
if (decoded > 256 * 1024 * 1024) throw new
|
|
305
|
+
if (decoded > 256 * 1024 * 1024) throw new VrmInspectionError("RESOURCE_LIMIT", "decoded geometry exceeds 256 MiB");
|
|
193
306
|
const checkFloats = (at, n, stride) => {
|
|
194
307
|
if (a.componentType !== 5126) return;
|
|
195
|
-
for (let k = 0; k < n; k++) for (let c = 0; c < componentsCount; c++) if (!Number.isFinite(binary.getFloat32(at + k * stride + c * 4, true))) throw new
|
|
308
|
+
for (let k = 0; k < n; k++) for (let c = 0; c < componentsCount; c++) if (!Number.isFinite(binary.getFloat32(at + k * stride + c * 4, true))) throw new VrmInspectionError("INVALID_STRUCTURE", "non-finite accessor value");
|
|
196
309
|
};
|
|
197
310
|
if (a.bufferView !== void 0) {
|
|
198
311
|
const at = span(a.bufferView, a.byteOffset ?? 0, count, element, width, true);
|
|
@@ -200,7 +313,7 @@ function validateStructure(json, binary) {
|
|
|
200
313
|
}
|
|
201
314
|
if (a.sparse) {
|
|
202
315
|
const sparse = a.sparse, n = sparse.count, iw = sizes[sparse.indices?.componentType];
|
|
203
|
-
if (!integer(n, 1, count) || ![5121, 5123, 5125].includes(sparse.indices?.componentType)) throw new
|
|
316
|
+
if (!integer(n, 1, count) || ![5121, 5123, 5125].includes(sparse.indices?.componentType)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid sparse accessor");
|
|
204
317
|
const at = span(sparse.indices.bufferView, sparse.indices.byteOffset ?? 0, n, iw, iw);
|
|
205
318
|
const valuesAt = span(sparse.values?.bufferView, sparse.values?.byteOffset ?? 0, n, element, width);
|
|
206
319
|
checkFloats(valuesAt, n, element);
|
|
@@ -208,29 +321,54 @@ function validateStructure(json, binary) {
|
|
|
208
321
|
for (let k = 0; k < n; k++) {
|
|
209
322
|
const offset = at + k * iw;
|
|
210
323
|
const next = iw === 1 ? binary.getUint8(offset) : iw === 2 ? binary.getUint16(offset, true) : binary.getUint32(offset, true);
|
|
211
|
-
if (next <= previous || next >= count) throw new
|
|
324
|
+
if (next <= previous || next >= count) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid sparse index");
|
|
212
325
|
previous = next;
|
|
213
326
|
}
|
|
214
327
|
}
|
|
215
328
|
}
|
|
329
|
+
let primitiveCount = 0;
|
|
330
|
+
for (const mesh of json.meshes ?? []) {
|
|
331
|
+
if (!mesh || !Array.isArray(mesh.primitives) || !mesh.primitives.length) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF mesh primitives");
|
|
332
|
+
primitiveCount += mesh.primitives.length;
|
|
333
|
+
if (primitiveCount > 32768) throw new VrmInspectionError("RESOURCE_LIMIT", "glTF primitive count exceeded");
|
|
334
|
+
for (const primitive of mesh.primitives) {
|
|
335
|
+
const position = accessors[primitive?.attributes?.POSITION];
|
|
336
|
+
if (!primitive || !position || position.type !== "VEC3" || position.componentType !== 5126) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF primitive POSITION accessor");
|
|
337
|
+
if (primitive.indices !== void 0) {
|
|
338
|
+
const indices = accessors[primitive.indices];
|
|
339
|
+
if (!index(primitive.indices, accessors) || indices.type !== "SCALAR" || ![5121, 5123, 5125].includes(indices.componentType)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF primitive index accessor");
|
|
340
|
+
}
|
|
341
|
+
if (primitive.mode !== void 0 && !integer(primitive.mode, 0, 6)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF primitive mode");
|
|
342
|
+
if (primitive.targets !== void 0 && (!Array.isArray(primitive.targets) || primitive.targets.length > 1024)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF morph target list");
|
|
343
|
+
for (const target of primitive.targets ?? []) {
|
|
344
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF morph target");
|
|
345
|
+
for (const semantic of ["POSITION", "NORMAL", "COLOR_0"]) {
|
|
346
|
+
if (target[semantic] === void 0) continue;
|
|
347
|
+
const accessor = accessors[target[semantic]];
|
|
348
|
+
if (!index(target[semantic], accessors) || accessor.componentType !== 5126 || !(semantic === "COLOR_0" ? ["VEC3", "VEC4"] : ["VEC3"]).includes(accessor.type) || accessor.count !== position.count) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF morph target accessor");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (const node of nodes) if (node.mesh !== void 0 && !index(node.mesh, json.meshes ?? [])) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF node mesh reference");
|
|
216
354
|
for (const animation of json.animations ?? []) {
|
|
217
|
-
if (!Array.isArray(animation.samplers) || !Array.isArray(animation.channels)) throw new
|
|
355
|
+
if (!Array.isArray(animation.samplers) || !Array.isArray(animation.channels)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation");
|
|
218
356
|
for (const channel of animation.channels) {
|
|
219
|
-
if (!index(channel.sampler, animation.samplers) || !index(channel.target?.node, nodes) || !["translation", "rotation", "scale", "weights"].includes(channel.target?.path)) throw new
|
|
357
|
+
if (!index(channel.sampler, animation.samplers) || !index(channel.target?.node, nodes) || !["translation", "rotation", "scale", "weights"].includes(channel.target?.path)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation channel");
|
|
220
358
|
}
|
|
221
359
|
for (const sampler of animation.samplers) {
|
|
222
|
-
if (!index(sampler.input, accessors) || !index(sampler.output, accessors)) throw new
|
|
360
|
+
if (!index(sampler.input, accessors) || !index(sampler.output, accessors)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation accessor");
|
|
223
361
|
const a = accessors[sampler.input];
|
|
224
|
-
if (a.type !== "SCALAR" || a.componentType !== 5126 || a.sparse || a.bufferView === void 0) throw new
|
|
362
|
+
if (a.type !== "SCALAR" || a.componentType !== 5126 || a.sparse || a.bufferView === void 0) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation timeline");
|
|
225
363
|
const start = span(a.bufferView, a.byteOffset ?? 0, a.count, 4, 4);
|
|
226
364
|
let previous = -1;
|
|
227
365
|
for (let i = 0; i < a.count; i++) {
|
|
228
366
|
const time = binary.getFloat32(start + i * 4, true);
|
|
229
|
-
if (!Number.isFinite(time) || time <= previous || time < 0 || time > 600) throw new
|
|
367
|
+
if (!Number.isFinite(time) || time <= previous || time < 0 || time > 600) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation times");
|
|
230
368
|
previous = time;
|
|
231
369
|
}
|
|
232
|
-
if (!Array.isArray(a.max) || Math.abs(a.max[0] - previous) > 1e-3) throw new
|
|
233
|
-
if (sampler.interpolation !== void 0 && !["LINEAR", "STEP", "CUBICSPLINE"].includes(sampler.interpolation)) throw new
|
|
370
|
+
if (!Array.isArray(a.max) || Math.abs(a.max[0] - previous) > 1e-3) throw new VrmInspectionError("INVALID_STRUCTURE", "animation duration does not match timeline");
|
|
371
|
+
if (sampler.interpolation !== void 0 && !["LINEAR", "STEP", "CUBICSPLINE"].includes(sampler.interpolation)) throw new VrmInspectionError("INVALID_STRUCTURE", "unsupported animation interpolation");
|
|
234
372
|
}
|
|
235
373
|
}
|
|
236
374
|
}
|
|
@@ -238,6 +376,7 @@ function validateStructure(json, binary) {
|
|
|
238
376
|
0 && (module.exports = {
|
|
239
377
|
MAX_ANIMATION_BYTES,
|
|
240
378
|
MAX_MODEL_BYTES,
|
|
379
|
+
VrmInspectionError,
|
|
241
380
|
embeddedAnimationCatalog,
|
|
242
381
|
inspectVrm,
|
|
243
382
|
inspectVrma,
|
package/dist/inspect.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
type Json = Record<string, any>;
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
2
|
+
export type VrmInspectionErrorCode = "INVALID_GLB" | "UNSUPPORTED_FORMAT" | "UNSUPPORTED_VERSION" | "UNSUPPORTED_EXTENSION" | "EXTERNAL_RESOURCE" | "RESOURCE_LIMIT" | "INVALID_STRUCTURE" | "HUMANOID_BONES" | "INVALID_BLINK" | "INVALID_LOOK_AT" | "INVALID_ANIMATION";
|
|
3
|
+
export interface VrmInspectionErrorDetails {
|
|
4
|
+
missingBones?: readonly string[];
|
|
5
|
+
invalidBones?: readonly string[];
|
|
6
|
+
duplicateBones?: readonly string[];
|
|
7
|
+
preset?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Controlled diagnostic text only: never include input paths, URIs or JSON text. */
|
|
10
|
+
export declare class VrmInspectionError extends Error {
|
|
11
|
+
readonly code: VrmInspectionErrorCode;
|
|
12
|
+
readonly details: Readonly<VrmInspectionErrorDetails>;
|
|
13
|
+
constructor(code: VrmInspectionErrorCode, message: string, details?: VrmInspectionErrorDetails);
|
|
14
|
+
}
|
|
15
|
+
export interface VrmInspection {
|
|
10
16
|
version: "0" | "1";
|
|
11
17
|
name?: string;
|
|
12
18
|
expressions: string[];
|
|
@@ -14,7 +20,48 @@ export declare function inspectVrm(bytes: ArrayBuffer): {
|
|
|
14
20
|
name: string;
|
|
15
21
|
durationMs: number;
|
|
16
22
|
}[];
|
|
17
|
-
|
|
23
|
+
capabilities: {
|
|
24
|
+
humanoid: {
|
|
25
|
+
requiredBones: string[];
|
|
26
|
+
optionalBones: string[];
|
|
27
|
+
};
|
|
28
|
+
blink: {
|
|
29
|
+
automatic: boolean;
|
|
30
|
+
combined: boolean;
|
|
31
|
+
left: boolean;
|
|
32
|
+
right: boolean;
|
|
33
|
+
};
|
|
34
|
+
lookAt: {
|
|
35
|
+
mode: "bone" | "expression" | "none";
|
|
36
|
+
head: boolean;
|
|
37
|
+
eyes: boolean;
|
|
38
|
+
horizontal: boolean;
|
|
39
|
+
vertical: boolean;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/** Logical accessor bytes and aggregate mesh primitive counts, not GPU memory. */
|
|
43
|
+
metrics: {
|
|
44
|
+
fileBytes: number;
|
|
45
|
+
nodeCount: number;
|
|
46
|
+
meshCount: number;
|
|
47
|
+
primitiveCount: number;
|
|
48
|
+
materialCount: number;
|
|
49
|
+
textureCount: number;
|
|
50
|
+
imageCount: number;
|
|
51
|
+
vertexCount: number;
|
|
52
|
+
triangleCount: number;
|
|
53
|
+
morphTargetCount: number;
|
|
54
|
+
decodedAccessorBytes: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export declare const MAX_MODEL_BYTES: number;
|
|
58
|
+
export declare const MAX_ANIMATION_BYTES: number;
|
|
59
|
+
export declare function parseGlb(bytes: ArrayBuffer, kind: "vrm" | "vrma"): Json;
|
|
60
|
+
export declare function embeddedAnimationCatalog(json: Json): {
|
|
61
|
+
name: string;
|
|
62
|
+
durationMs: number;
|
|
63
|
+
}[];
|
|
64
|
+
export declare function inspectVrm(bytes: ArrayBuffer): VrmInspection;
|
|
18
65
|
export declare function inspectVrma(bytes: ArrayBuffer): {
|
|
19
66
|
durationMs: number;
|
|
20
67
|
};
|