@starcried/starlite-vrm 0.1.0-alpha.2 → 0.1.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/BUNDLED_MOTIONS.json +63 -0
- package/BUNDLED_MOTIONS.md +41 -0
- package/README.en.md +65 -0
- package/README.md +31 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/dist/adjustments.d.ts +23 -0
- package/dist/adjustments.js +201 -0
- package/dist/builtin-motion-catalog.d.ts +16 -0
- package/dist/builtin-motion-catalog.js +10 -0
- package/dist/builtin-motion-data.d.ts +5 -0
- package/dist/builtin-motion-data.js +6 -0
- package/dist/builtin-motions.d.ts +3 -0
- package/dist/builtin-motions.js +10 -0
- package/dist/camera.d.ts +8 -0
- package/dist/camera.js +50 -0
- package/dist/config.cjs +71 -2
- package/dist/config.d.ts +4 -1
- package/dist/config.js +34 -3
- package/dist/environment-lighting.d.ts +12 -0
- package/dist/environment-lighting.js +82 -0
- package/dist/gaze.d.ts +2 -1
- package/dist/gaze.js +17 -6
- package/dist/index.d.ts +4 -1
- package/dist/index.js +240 -197
- package/dist/inspect.cjs +226 -74
- package/dist/inspect.d.ts +56 -9
- package/dist/inspect.js +259 -85
- package/dist/playback.js +25 -3
- package/dist/prepare.cjs +225 -77
- package/dist/prepare.js +4 -3
- package/dist/types.d.ts +22 -1
- package/package.json +6 -3
package/dist/prepare.cjs
CHANGED
|
@@ -29,148 +29,271 @@ var import_node_crypto = require("node:crypto");
|
|
|
29
29
|
var import_promises = require("node:fs/promises");
|
|
30
30
|
var import_node_path = require("node:path");
|
|
31
31
|
|
|
32
|
+
// src/builtin-motion-catalog.ts
|
|
33
|
+
var BUILTIN_ANIMATIONS = [
|
|
34
|
+
{ name: "StarLiteIdle", durationMs: 6e3 },
|
|
35
|
+
{ name: "StarLiteCuriousPeek", durationMs: 2100 },
|
|
36
|
+
{ name: "StarLiteHappyWave", durationMs: 4600 },
|
|
37
|
+
{ name: "StarLiteSleepyStartle", durationMs: 5500 }
|
|
38
|
+
];
|
|
39
|
+
function isBuiltinAnimation(name) {
|
|
40
|
+
return BUILTIN_ANIMATIONS.some((animation) => animation.name === name);
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
// src/inspect.ts
|
|
44
|
+
var VrmInspectionError = class extends Error {
|
|
45
|
+
code;
|
|
46
|
+
details;
|
|
47
|
+
constructor(code, message, details = {}) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "VrmInspectionError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.details = details;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var requiredBones = ["hips", "spine", "head", ...["left", "right"].flatMap((side) => ["UpperLeg", "LowerLeg", "Foot", "UpperArm", "LowerArm", "Hand"].map((part) => side + part))];
|
|
55
|
+
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))];
|
|
33
56
|
var MAX_MODEL_BYTES = 64 * 1024 * 1024;
|
|
34
57
|
var MAX_ANIMATION_BYTES = 16 * 1024 * 1024;
|
|
35
58
|
function parseGlb(bytes, kind) {
|
|
59
|
+
try {
|
|
60
|
+
return parseGlbContents(bytes, kind);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof VrmInspectionError) throw error;
|
|
63
|
+
throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF structure; re-export a self-contained VRM/VRMA file");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function parseGlbContents(bytes, kind) {
|
|
36
67
|
const limit = kind === "vrm" ? MAX_MODEL_BYTES : MAX_ANIMATION_BYTES;
|
|
37
|
-
if (bytes.byteLength < 20 || bytes.byteLength > limit) throw new
|
|
68
|
+
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`);
|
|
38
69
|
const view = new DataView(bytes);
|
|
39
|
-
if (view.getUint32(0, true) !== 1179937895 || view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== bytes.byteLength) throw new
|
|
70
|
+
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");
|
|
40
71
|
let json;
|
|
41
72
|
let binSize = 0;
|
|
42
73
|
let binStart = 0;
|
|
43
74
|
let chunkCount = 0;
|
|
44
75
|
for (let offset = 12; offset < bytes.byteLength; ) {
|
|
45
|
-
if (offset + 8 > bytes.byteLength) throw new
|
|
76
|
+
if (offset + 8 > bytes.byteLength) throw new VrmInspectionError("INVALID_GLB", "truncated GLB chunk; download or export the complete file again");
|
|
46
77
|
const size = view.getUint32(offset, true), type = view.getUint32(offset + 4, true);
|
|
47
78
|
offset += 8;
|
|
48
|
-
if (size % 4 || size > bytes.byteLength - offset) throw new
|
|
79
|
+
if (size % 4 || size > bytes.byteLength - offset) throw new VrmInspectionError("INVALID_GLB", "invalid GLB chunk length; download or export the complete file again");
|
|
49
80
|
if (chunkCount === 0 && type === 1313821514) {
|
|
50
|
-
|
|
81
|
+
try {
|
|
82
|
+
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes, offset, size)));
|
|
83
|
+
} catch {
|
|
84
|
+
throw new VrmInspectionError("INVALID_GLB", "invalid GLB JSON; re-export the file with a VRM exporter");
|
|
85
|
+
}
|
|
51
86
|
} else if (chunkCount === 1 && type === 5130562) {
|
|
52
87
|
binSize = size;
|
|
53
88
|
binStart = offset;
|
|
54
|
-
} else throw new
|
|
89
|
+
} else throw new VrmInspectionError("INVALID_GLB", "unexpected GLB chunk; export a self-contained VRM/VRMA file");
|
|
55
90
|
offset += size;
|
|
56
91
|
chunkCount++;
|
|
57
92
|
}
|
|
58
|
-
if (!json || json.asset?.version !== "2.0") throw new
|
|
93
|
+
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");
|
|
59
94
|
const expected = kind === "vrm" ? json.extensions?.VRM ?? json.extensions?.VRMC_vrm : json.extensions?.VRMC_vrm_animation;
|
|
60
|
-
if (!expected) throw new
|
|
61
|
-
if (kind === "vrm" && json.extensions?.VRM && json.extensions.VRM.specVersion !== "0.0") throw new
|
|
62
|
-
if (kind === "vrm" && json.extensions?.VRMC_vrm && json.extensions.VRMC_vrm.specVersion !== "1.0") throw new
|
|
63
|
-
if (kind === "vrma" && expected.specVersion !== "1.0") throw new
|
|
95
|
+
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`);
|
|
96
|
+
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");
|
|
97
|
+
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");
|
|
98
|
+
if (kind === "vrma" && expected.specVersion !== "1.0") throw new VrmInspectionError("UNSUPPORTED_VERSION", "VRMA 1.0 is required");
|
|
64
99
|
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"]);
|
|
65
|
-
if ((json.extensionsRequired ?? []).some((name) => !allowed.has(name))) throw new
|
|
100
|
+
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");
|
|
66
101
|
const visit = (value, depth = 0) => {
|
|
67
|
-
if (depth > 64) throw new
|
|
102
|
+
if (depth > 64) throw new VrmInspectionError("RESOURCE_LIMIT", "glTF nesting limit exceeded");
|
|
68
103
|
if (!value || typeof value !== "object") return;
|
|
69
104
|
for (const [key, child] of Object.entries(value)) {
|
|
70
|
-
if (key === "uri") throw new
|
|
71
|
-
if (typeof child === "number" && !Number.isFinite(child)) throw new
|
|
105
|
+
if (key === "uri") throw new VrmInspectionError("EXTERNAL_RESOURCE", "external glTF resources are not supported; embed all textures and buffers in one VRM/VRMA file");
|
|
106
|
+
if (typeof child === "number" && !Number.isFinite(child)) throw new VrmInspectionError("INVALID_STRUCTURE", "non-finite glTF value");
|
|
72
107
|
visit(child, depth + 1);
|
|
73
108
|
}
|
|
74
109
|
};
|
|
75
110
|
visit(json);
|
|
76
|
-
if ((json.buffers?.length ?? 0) > 1 || (json.buffers?.[0]?.byteLength ?? 0) > binSize) throw new
|
|
111
|
+
if ((json.buffers?.length ?? 0) > 1 || (json.buffers?.[0]?.byteLength ?? 0) > binSize) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid GLB buffer");
|
|
77
112
|
for (const buffer of json.bufferViews ?? []) {
|
|
78
113
|
const start = buffer.byteOffset ?? 0, length = buffer.byteLength;
|
|
79
|
-
if (buffer.buffer !== 0 || !Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 0 || start + length > (json.buffers?.[0]?.byteLength ?? 0)) throw new
|
|
114
|
+
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");
|
|
80
115
|
}
|
|
81
|
-
if ((json.nodes?.length ?? 0) > 4096 || (json.accessors?.length ?? 0) > 32768) throw new
|
|
116
|
+
if ((json.nodes?.length ?? 0) > 4096 || (json.accessors?.length ?? 0) > 32768) throw new VrmInspectionError("RESOURCE_LIMIT", "glTF resource count exceeded");
|
|
82
117
|
validateStructure(json, new DataView(bytes, binStart, binSize));
|
|
83
118
|
return json;
|
|
84
119
|
}
|
|
85
120
|
function embeddedAnimationCatalog(json) {
|
|
86
121
|
const animations = json.animations ?? [];
|
|
87
|
-
if (!Array.isArray(animations) || animations.length > 64) throw new
|
|
122
|
+
if (!Array.isArray(animations) || animations.length > 64) throw new VrmInspectionError("INVALID_ANIMATION", "at most 64 embedded animations are supported");
|
|
88
123
|
const names = /* @__PURE__ */ new Set();
|
|
89
124
|
return animations.map((animation, index) => {
|
|
90
125
|
const name = animation.name ?? "Animation_" + (index + 1);
|
|
91
|
-
if (typeof name !== "string" || !name.trim() || new TextEncoder().encode(name).length > 128 || /[\u0000-\u001f\u007f]/.test(name) || name
|
|
126
|
+
if (typeof name !== "string" || !name.trim() || new TextEncoder().encode(name).length > 128 || /[\u0000-\u001f\u007f]/.test(name) || isBuiltinAnimation(name) || 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 reserved StarLite program motion names");
|
|
92
127
|
names.add(name);
|
|
93
128
|
let duration = 0;
|
|
94
129
|
for (const sampler of animation.samplers ?? []) {
|
|
95
130
|
const accessor = json.accessors?.[sampler.input];
|
|
96
|
-
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new
|
|
131
|
+
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new VrmInspectionError("INVALID_ANIMATION", "embedded animation has invalid animation times");
|
|
97
132
|
duration = Math.max(duration, accessor.max[0]);
|
|
98
133
|
}
|
|
99
|
-
if (!(duration > 0) || duration > 600) throw new
|
|
134
|
+
if (!(duration > 0) || duration > 600) throw new VrmInspectionError("INVALID_ANIMATION", "embedded animation duration must be between 0 and 600 seconds");
|
|
100
135
|
return { name, durationMs: duration * 1e3 };
|
|
101
136
|
});
|
|
102
137
|
}
|
|
103
138
|
function inspectVrm(bytes) {
|
|
104
139
|
const json = parseGlb(bytes, "vrm");
|
|
105
140
|
const v0 = json.extensions.VRM;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const used = /* @__PURE__ */ new Set();
|
|
110
|
-
for (const name2 of required) {
|
|
111
|
-
const node = bones?.[name2]?.node;
|
|
112
|
-
if (!Number.isSafeInteger(node) || !json.nodes?.[node] || used.has(node)) throw new Error("invalid required humanoid bone");
|
|
113
|
-
used.add(node);
|
|
114
|
-
}
|
|
141
|
+
const extension = v0 ?? json.extensions.VRMC_vrm;
|
|
142
|
+
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");
|
|
143
|
+
const bones = inspectHumanBones(json, extension.humanoid?.humanBones, Boolean(v0));
|
|
115
144
|
const blinkNames = v0 ? ["blink", "blink_l", "blink_r"] : ["blink", "blinkLeft", "blinkRight"];
|
|
116
145
|
const groups = v0?.blendShapeMaster?.blendShapeGroups;
|
|
117
|
-
if (groups !== void 0 && !Array.isArray(groups)) throw new
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const binds = v0 ? expression.binds : expression.morphTargetBinds;
|
|
125
|
-
if (binds === void 0) continue;
|
|
126
|
-
if (!Array.isArray(binds)) throw new Error("invalid VRM blink bindings");
|
|
127
|
-
for (const bind of binds) {
|
|
128
|
-
const mesh = v0 ? bind?.mesh : json.nodes?.[bind?.node]?.mesh;
|
|
129
|
-
const primitives = json.meshes?.[mesh]?.primitives;
|
|
130
|
-
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)) {
|
|
131
|
-
throw new Error("invalid VRM blink morph binding");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
146
|
+
if (groups !== void 0 && !Array.isArray(groups)) throw new VrmInspectionError("INVALID_BLINK", "invalid VRM blink expressions; re-export the blink presets");
|
|
147
|
+
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" };
|
|
148
|
+
const registeredName = (item) => aliases[item?.presetName] ?? item?.name;
|
|
149
|
+
const expression = (name2) => v0 ? (groups ?? []).find((item) => registeredName(item) === (aliases[name2] ?? name2)) : extension.expressions?.preset?.[name2];
|
|
150
|
+
for (const preset of blinkNames) {
|
|
151
|
+
const presets = v0 ? (groups ?? []).filter((item) => registeredName(item) === aliases[preset]) : expression(preset) === void 0 ? [] : [expression(preset)];
|
|
152
|
+
for (const item of presets) inspectMorphExpression(json, item, Boolean(v0), "INVALID_BLINK", preset);
|
|
134
153
|
}
|
|
135
|
-
const
|
|
154
|
+
const [combined, left, right] = blinkNames.map((preset) => inspectMorphExpression(json, expression(preset), Boolean(v0), "INVALID_BLINK", preset));
|
|
155
|
+
const meta = extension.meta ?? {};
|
|
136
156
|
const name = v0 ? meta.title : meta.name;
|
|
137
157
|
return {
|
|
138
158
|
version: v0 ? "0" : "1",
|
|
139
|
-
...typeof name === "string" ? { name } : {},
|
|
159
|
+
...typeof name === "string" ? { name: safeLabel(name) } : {},
|
|
140
160
|
animations: embeddedAnimationCatalog(json),
|
|
141
|
-
expressions: v0 ? (
|
|
161
|
+
expressions: (v0 ? (groups ?? []).map((group) => group?.name) : Object.keys({ ...extension.expressions?.preset, ...extension.expressions?.custom })).filter((value) => typeof value === "string").map(safeLabel),
|
|
162
|
+
capabilities: {
|
|
163
|
+
humanoid: { requiredBones: [...requiredBones], optionalBones: optionalBones.filter((name2) => bones.has(name2)) },
|
|
164
|
+
blink: { automatic: Boolean(combined || left && right), combined: Boolean(combined), left: Boolean(left), right: Boolean(right) },
|
|
165
|
+
lookAt: inspectLookAt(json, extension, Boolean(v0), bones, expression)
|
|
166
|
+
},
|
|
167
|
+
metrics: inspectMetrics(json, bytes.byteLength)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function safeLabel(value) {
|
|
171
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 128);
|
|
172
|
+
}
|
|
173
|
+
function inspectHumanBones(json, humanoid, v0) {
|
|
174
|
+
const known = /* @__PURE__ */ new Set([...requiredBones, ...optionalBones]), result = /* @__PURE__ */ new Map();
|
|
175
|
+
const invalid = [], duplicates = [], used = /* @__PURE__ */ new Map();
|
|
176
|
+
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)) : [];
|
|
177
|
+
for (const [name, value] of entries) {
|
|
178
|
+
const node = value?.node;
|
|
179
|
+
if (result.has(name)) duplicates.push(name);
|
|
180
|
+
if (!Number.isSafeInteger(node) || node < 0 || !json.nodes?.[node]) {
|
|
181
|
+
invalid.push(name);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const previous = used.get(node);
|
|
185
|
+
if (previous && previous !== name) duplicates.push(previous, name);
|
|
186
|
+
used.set(node, name);
|
|
187
|
+
result.set(name, node);
|
|
188
|
+
}
|
|
189
|
+
const missing = requiredBones.filter((name) => !result.has(name) && !invalid.includes(name));
|
|
190
|
+
if (missing.length || invalid.length || duplicates.length) {
|
|
191
|
+
const details = { missingBones: missing, invalidBones: [...new Set(invalid)], duplicateBones: [...new Set(duplicates)] };
|
|
192
|
+
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("; ");
|
|
193
|
+
throw new VrmInspectionError("HUMANOID_BONES", `invalid required humanoid bone: ${summary}; assign separate human bones in the VRM exporter and export again`, details);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
function inspectMorphExpression(json, expression, v0, code, preset) {
|
|
198
|
+
if (expression === void 0) return false;
|
|
199
|
+
const label = code === "INVALID_BLINK" ? "blink" : "look-at";
|
|
200
|
+
if (!expression || typeof expression !== "object" || Array.isArray(expression)) throw new VrmInspectionError(code, `invalid VRM ${label} expression; re-export the ${preset} preset`, { preset });
|
|
201
|
+
const binds = v0 ? expression.binds : expression.morphTargetBinds;
|
|
202
|
+
if (binds === void 0) return false;
|
|
203
|
+
if (!Array.isArray(binds)) throw new VrmInspectionError(code, `invalid VRM ${label} bindings; re-export the ${preset} preset`, { preset });
|
|
204
|
+
let usable = false;
|
|
205
|
+
for (const bind of binds) {
|
|
206
|
+
const mesh = v0 ? bind?.mesh : json.nodes?.[bind?.node]?.mesh;
|
|
207
|
+
const primitives = json.meshes?.[mesh]?.primitives;
|
|
208
|
+
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)) {
|
|
209
|
+
throw new VrmInspectionError(code, `invalid VRM ${label} morph binding: ${preset}; check its mesh/node, morph target index and weight in the VRM exporter`, { preset });
|
|
210
|
+
}
|
|
211
|
+
const instantiated = !v0 || (json.nodes ?? []).some((node) => node?.mesh === mesh);
|
|
212
|
+
const morphs = primitives.every((primitive) => primitive.targets.some((target) => ["POSITION", "NORMAL", "COLOR_0"].some((semantic) => Number.isSafeInteger(target?.[semantic]) && json.accessors?.[target[semantic]])));
|
|
213
|
+
usable ||= bind.weight > 0 && instantiated && morphs;
|
|
214
|
+
}
|
|
215
|
+
return usable;
|
|
216
|
+
}
|
|
217
|
+
function inspectLookAt(json, extension, v0, bones, expression) {
|
|
218
|
+
const lookAt = v0 ? extension.firstPerson : extension.lookAt;
|
|
219
|
+
const empty = { mode: "none", head: true, eyes: false, horizontal: false, vertical: false };
|
|
220
|
+
if (lookAt === void 0) return empty;
|
|
221
|
+
if (!lookAt || typeof lookAt !== "object" || Array.isArray(lookAt)) throw new VrmInspectionError("INVALID_LOOK_AT", "invalid VRM look-at configuration; re-export eye tracking");
|
|
222
|
+
const expressionManager = v0 ? extension.blendShapeMaster : extension.expressions;
|
|
223
|
+
if (!expressionManager) return empty;
|
|
224
|
+
const type = v0 ? lookAt.lookAtTypeName : lookAt.type;
|
|
225
|
+
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");
|
|
226
|
+
const mode = type === (v0 ? "BlendShape" : "expression") ? "expression" : "bone";
|
|
227
|
+
const scale = (name) => {
|
|
228
|
+
const range = lookAt[name], value = range?.[v0 ? "yRange" : "outputScale"] ?? (mode === "expression" ? 1 : 10);
|
|
229
|
+
const input = range?.[v0 ? "xRange" : "inputMaxValue"] ?? 90;
|
|
230
|
+
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");
|
|
231
|
+
return value;
|
|
142
232
|
};
|
|
233
|
+
const horizontalRange = scale(v0 ? "lookAtHorizontalOuter" : "rangeMapHorizontalOuter");
|
|
234
|
+
const innerRange = scale(v0 ? "lookAtHorizontalInner" : "rangeMapHorizontalInner");
|
|
235
|
+
const upRange = scale(v0 ? "lookAtVerticalUp" : "rangeMapVerticalUp"), downRange = scale(v0 ? "lookAtVerticalDown" : "rangeMapVerticalDown");
|
|
236
|
+
let horizontal, vertical;
|
|
237
|
+
if (mode === "bone") {
|
|
238
|
+
const eyes = bones.has("leftEye") || bones.has("rightEye");
|
|
239
|
+
horizontal = eyes && (horizontalRange > 0 || innerRange > 0);
|
|
240
|
+
vertical = eyes && (upRange > 0 || downRange > 0);
|
|
241
|
+
} else {
|
|
242
|
+
const usable = (name) => inspectMorphExpression(json, expression(name), v0, "INVALID_LOOK_AT", name);
|
|
243
|
+
const left = usable(v0 ? "lookleft" : "lookLeft"), right = usable(v0 ? "lookright" : "lookRight"), up = usable(v0 ? "lookup" : "lookUp"), down = usable(v0 ? "lookdown" : "lookDown");
|
|
244
|
+
horizontal = horizontalRange > 0 && left && right;
|
|
245
|
+
vertical = upRange > 0 && downRange > 0 && up && down;
|
|
246
|
+
}
|
|
247
|
+
return { mode, head: true, eyes: horizontal || vertical, horizontal, vertical };
|
|
248
|
+
}
|
|
249
|
+
function inspectMetrics(json, fileBytes) {
|
|
250
|
+
const primitives = (json.meshes ?? []).flatMap((mesh) => Array.isArray(mesh?.primitives) ? mesh.primitives : []);
|
|
251
|
+
let vertexCount = 0, triangleCount = 0, morphTargetCount = 0;
|
|
252
|
+
for (const primitive of primitives) {
|
|
253
|
+
const vertices = json.accessors?.[primitive?.attributes?.POSITION]?.count ?? 0;
|
|
254
|
+
const indices = primitive?.indices === void 0 ? vertices : json.accessors?.[primitive.indices]?.count ?? 0;
|
|
255
|
+
vertexCount += vertices;
|
|
256
|
+
if (primitive?.mode === void 0 || primitive.mode === 4) triangleCount += Math.floor(indices / 3);
|
|
257
|
+
else if (primitive.mode === 5 || primitive.mode === 6) triangleCount += Math.max(0, indices - 2);
|
|
258
|
+
morphTargetCount += Array.isArray(primitive?.targets) ? primitive.targets.length : 0;
|
|
259
|
+
}
|
|
260
|
+
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 };
|
|
261
|
+
const decodedAccessorBytes = (json.accessors ?? []).reduce((sum, accessor) => {
|
|
262
|
+
const width = widths[accessor.componentType], dimension = accessor.type === "MAT2" ? 2 : accessor.type === "MAT3" ? 3 : accessor.type === "MAT4" ? 4 : 0;
|
|
263
|
+
return sum + accessor.count * (dimension ? Math.ceil(dimension * width / 4) * 4 * dimension : width * components[accessor.type]);
|
|
264
|
+
}, 0);
|
|
265
|
+
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 };
|
|
143
266
|
}
|
|
144
267
|
function inspectVrma(bytes) {
|
|
145
268
|
const json = parseGlb(bytes, "vrma");
|
|
146
|
-
if (json.animations?.length !== 1) throw new
|
|
269
|
+
if (json.animations?.length !== 1) throw new VrmInspectionError("INVALID_STRUCTURE", "each VRMA file must contain exactly one animation");
|
|
147
270
|
let duration = 0;
|
|
148
271
|
for (const sampler of json.animations[0].samplers ?? []) {
|
|
149
272
|
const accessor = json.accessors?.[sampler.input];
|
|
150
|
-
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new
|
|
273
|
+
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new VrmInspectionError("INVALID_STRUCTURE", "VRMA has invalid animation times");
|
|
151
274
|
duration = Math.max(duration, accessor.max[0]);
|
|
152
275
|
}
|
|
153
|
-
if (!(duration > 0) || duration > 600) throw new
|
|
276
|
+
if (!(duration > 0) || duration > 600) throw new VrmInspectionError("INVALID_STRUCTURE", "VRMA duration must be between 0 and 600 seconds");
|
|
154
277
|
return { durationMs: duration * 1e3 };
|
|
155
278
|
}
|
|
156
279
|
function validateStructure(json, binary) {
|
|
157
|
-
for (const key of ["nodes", "accessors", "bufferViews", "buffers", "meshes", "images", "animations", "skins", "scenes"]) {
|
|
158
|
-
if (json[key] !== void 0 && !Array.isArray(json[key])) throw new
|
|
280
|
+
for (const key of ["nodes", "accessors", "bufferViews", "buffers", "meshes", "materials", "textures", "images", "animations", "skins", "scenes", "extensionsUsed", "extensionsRequired"]) {
|
|
281
|
+
if (json[key] !== void 0 && !Array.isArray(json[key])) throw new VrmInspectionError("INVALID_STRUCTURE", `invalid glTF ${key}`);
|
|
159
282
|
}
|
|
160
283
|
const views = json.bufferViews ?? [], nodes = json.nodes ?? [], accessors = json.accessors ?? [];
|
|
161
284
|
const integer = (n, min, max) => Number.isSafeInteger(n) && n >= min && n <= max;
|
|
162
285
|
const index = (n, array) => integer(n, 0, array.length - 1);
|
|
163
286
|
const seen = /* @__PURE__ */ new Set(), active = /* @__PURE__ */ new Set(), parents = /* @__PURE__ */ new Set();
|
|
164
287
|
const walk = (i, depth) => {
|
|
165
|
-
if (!index(i, nodes) || active.has(i) || depth > 128) throw new
|
|
288
|
+
if (!index(i, nodes) || active.has(i) || depth > 128) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF node hierarchy");
|
|
166
289
|
if (seen.has(i)) return;
|
|
167
290
|
const node = nodes[i];
|
|
168
|
-
if (!node || typeof node !== "object") throw new
|
|
291
|
+
if (!node || typeof node !== "object") throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF node");
|
|
169
292
|
seen.add(i);
|
|
170
293
|
active.add(i);
|
|
171
|
-
if (node.children !== void 0 && !Array.isArray(node.children)) throw new
|
|
294
|
+
if (node.children !== void 0 && !Array.isArray(node.children)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid node children");
|
|
172
295
|
for (const child of node.children ?? []) {
|
|
173
|
-
if (parents.has(child)) throw new
|
|
296
|
+
if (parents.has(child)) throw new VrmInspectionError("INVALID_STRUCTURE", "node has multiple parents");
|
|
174
297
|
parents.add(child);
|
|
175
298
|
walk(child, depth + 1);
|
|
176
299
|
}
|
|
@@ -181,21 +304,21 @@ function validateStructure(json, binary) {
|
|
|
181
304
|
const sizes = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
|
182
305
|
let decoded = 0;
|
|
183
306
|
const span = (viewIndex, offset, count, element, alignment, strided = false) => {
|
|
184
|
-
if (!index(viewIndex, views)) throw new
|
|
307
|
+
if (!index(viewIndex, views)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid accessor buffer view");
|
|
185
308
|
const v = views[viewIndex], stride = strided ? v.byteStride ?? element : element;
|
|
186
|
-
if (!integer(offset, 0, v.byteLength) || offset % alignment || !integer(stride, element, 256) || stride % alignment || offset + (count - 1) * stride + element > v.byteLength) throw new
|
|
309
|
+
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");
|
|
187
310
|
return (v.byteOffset ?? 0) + offset;
|
|
188
311
|
};
|
|
189
312
|
for (const a of accessors) {
|
|
190
313
|
const width = sizes[a?.componentType], count = a?.count, componentsCount = components[a?.type];
|
|
191
|
-
if (!width || !componentsCount || !integer(count, 1, 2e6)) throw new
|
|
314
|
+
if (!width || !componentsCount || !integer(count, 1, 2e6)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid accessor allocation");
|
|
192
315
|
const dimension = a.type === "MAT2" ? 2 : a.type === "MAT3" ? 3 : a.type === "MAT4" ? 4 : 0;
|
|
193
316
|
const element = dimension ? Math.ceil(dimension * width / 4) * 4 * dimension : width * componentsCount;
|
|
194
317
|
decoded += element * count;
|
|
195
|
-
if (decoded > 256 * 1024 * 1024) throw new
|
|
318
|
+
if (decoded > 256 * 1024 * 1024) throw new VrmInspectionError("RESOURCE_LIMIT", "decoded geometry exceeds 256 MiB");
|
|
196
319
|
const checkFloats = (at, n, stride) => {
|
|
197
320
|
if (a.componentType !== 5126) return;
|
|
198
|
-
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
|
|
321
|
+
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");
|
|
199
322
|
};
|
|
200
323
|
if (a.bufferView !== void 0) {
|
|
201
324
|
const at = span(a.bufferView, a.byteOffset ?? 0, count, element, width, true);
|
|
@@ -203,7 +326,7 @@ function validateStructure(json, binary) {
|
|
|
203
326
|
}
|
|
204
327
|
if (a.sparse) {
|
|
205
328
|
const sparse = a.sparse, n = sparse.count, iw = sizes[sparse.indices?.componentType];
|
|
206
|
-
if (!integer(n, 1, count) || ![5121, 5123, 5125].includes(sparse.indices?.componentType)) throw new
|
|
329
|
+
if (!integer(n, 1, count) || ![5121, 5123, 5125].includes(sparse.indices?.componentType)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid sparse accessor");
|
|
207
330
|
const at = span(sparse.indices.bufferView, sparse.indices.byteOffset ?? 0, n, iw, iw);
|
|
208
331
|
const valuesAt = span(sparse.values?.bufferView, sparse.values?.byteOffset ?? 0, n, element, width);
|
|
209
332
|
checkFloats(valuesAt, n, element);
|
|
@@ -211,29 +334,54 @@ function validateStructure(json, binary) {
|
|
|
211
334
|
for (let k = 0; k < n; k++) {
|
|
212
335
|
const offset = at + k * iw;
|
|
213
336
|
const next = iw === 1 ? binary.getUint8(offset) : iw === 2 ? binary.getUint16(offset, true) : binary.getUint32(offset, true);
|
|
214
|
-
if (next <= previous || next >= count) throw new
|
|
337
|
+
if (next <= previous || next >= count) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid sparse index");
|
|
215
338
|
previous = next;
|
|
216
339
|
}
|
|
217
340
|
}
|
|
218
341
|
}
|
|
342
|
+
let primitiveCount = 0;
|
|
343
|
+
for (const mesh of json.meshes ?? []) {
|
|
344
|
+
if (!mesh || !Array.isArray(mesh.primitives) || !mesh.primitives.length) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF mesh primitives");
|
|
345
|
+
primitiveCount += mesh.primitives.length;
|
|
346
|
+
if (primitiveCount > 32768) throw new VrmInspectionError("RESOURCE_LIMIT", "glTF primitive count exceeded");
|
|
347
|
+
for (const primitive of mesh.primitives) {
|
|
348
|
+
const position = accessors[primitive?.attributes?.POSITION];
|
|
349
|
+
if (!primitive || !position || position.type !== "VEC3" || position.componentType !== 5126) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF primitive POSITION accessor");
|
|
350
|
+
if (primitive.indices !== void 0) {
|
|
351
|
+
const indices = accessors[primitive.indices];
|
|
352
|
+
if (!index(primitive.indices, accessors) || indices.type !== "SCALAR" || ![5121, 5123, 5125].includes(indices.componentType)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF primitive index accessor");
|
|
353
|
+
}
|
|
354
|
+
if (primitive.mode !== void 0 && !integer(primitive.mode, 0, 6)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF primitive mode");
|
|
355
|
+
if (primitive.targets !== void 0 && (!Array.isArray(primitive.targets) || primitive.targets.length > 1024)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF morph target list");
|
|
356
|
+
for (const target of primitive.targets ?? []) {
|
|
357
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid glTF morph target");
|
|
358
|
+
for (const semantic of ["POSITION", "NORMAL", "COLOR_0"]) {
|
|
359
|
+
if (target[semantic] === void 0) continue;
|
|
360
|
+
const accessor = accessors[target[semantic]];
|
|
361
|
+
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");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
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");
|
|
219
367
|
for (const animation of json.animations ?? []) {
|
|
220
|
-
if (!Array.isArray(animation.samplers) || !Array.isArray(animation.channels)) throw new
|
|
368
|
+
if (!Array.isArray(animation.samplers) || !Array.isArray(animation.channels)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation");
|
|
221
369
|
for (const channel of animation.channels) {
|
|
222
|
-
if (!index(channel.sampler, animation.samplers) || !index(channel.target?.node, nodes) || !["translation", "rotation", "scale", "weights"].includes(channel.target?.path)) throw new
|
|
370
|
+
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");
|
|
223
371
|
}
|
|
224
372
|
for (const sampler of animation.samplers) {
|
|
225
|
-
if (!index(sampler.input, accessors) || !index(sampler.output, accessors)) throw new
|
|
373
|
+
if (!index(sampler.input, accessors) || !index(sampler.output, accessors)) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation accessor");
|
|
226
374
|
const a = accessors[sampler.input];
|
|
227
|
-
if (a.type !== "SCALAR" || a.componentType !== 5126 || a.sparse || a.bufferView === void 0) throw new
|
|
375
|
+
if (a.type !== "SCALAR" || a.componentType !== 5126 || a.sparse || a.bufferView === void 0) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation timeline");
|
|
228
376
|
const start = span(a.bufferView, a.byteOffset ?? 0, a.count, 4, 4);
|
|
229
377
|
let previous = -1;
|
|
230
378
|
for (let i = 0; i < a.count; i++) {
|
|
231
379
|
const time = binary.getFloat32(start + i * 4, true);
|
|
232
|
-
if (!Number.isFinite(time) || time <= previous || time < 0 || time > 600) throw new
|
|
380
|
+
if (!Number.isFinite(time) || time <= previous || time < 0 || time > 600) throw new VrmInspectionError("INVALID_STRUCTURE", "invalid animation times");
|
|
233
381
|
previous = time;
|
|
234
382
|
}
|
|
235
|
-
if (!Array.isArray(a.max) || Math.abs(a.max[0] - previous) > 1e-3) throw new
|
|
236
|
-
if (sampler.interpolation !== void 0 && !["LINEAR", "STEP", "CUBICSPLINE"].includes(sampler.interpolation)) throw new
|
|
383
|
+
if (!Array.isArray(a.max) || Math.abs(a.max[0] - previous) > 1e-3) throw new VrmInspectionError("INVALID_STRUCTURE", "animation duration does not match timeline");
|
|
384
|
+
if (sampler.interpolation !== void 0 && !["LINEAR", "STEP", "CUBICSPLINE"].includes(sampler.interpolation)) throw new VrmInspectionError("INVALID_STRUCTURE", "unsupported animation interpolation");
|
|
237
385
|
}
|
|
238
386
|
}
|
|
239
387
|
}
|
|
@@ -281,7 +429,7 @@ function animationNames(animations) {
|
|
|
281
429
|
if (!Array.isArray(animations) || animations.length > 64) throw new Error("invalid animation collection");
|
|
282
430
|
const names = /* @__PURE__ */ new Set();
|
|
283
431
|
for (const a of animations) {
|
|
284
|
-
if (!a || typeof a.name !== "string" || !a.name.trim() || a.name.length > 128 || names.has(a.name)) throw new Error("duplicate or invalid animation name");
|
|
432
|
+
if (!a || typeof a.name !== "string" || !a.name.trim() || a.name.length > 128 || names.has(a.name) || a.name !== "StarLiteIdle" && isBuiltinAnimation(a.name)) throw new Error("duplicate or invalid animation name");
|
|
285
433
|
names.add(a.name);
|
|
286
434
|
}
|
|
287
435
|
}
|
|
@@ -310,7 +458,7 @@ async function prepareModel(request) {
|
|
|
310
458
|
const data = await bounded(request.profile, 1024 * 1024);
|
|
311
459
|
const profile = JSON.parse(data.toString("utf8"));
|
|
312
460
|
validateCharacterProfile(profile);
|
|
313
|
-
profileNames(profile, [...animations, ...inspected.animations,
|
|
461
|
+
profileNames(profile, [...animations, ...inspected.animations, ...BUILTIN_ANIMATIONS]);
|
|
314
462
|
files.set("profile.json", data);
|
|
315
463
|
manifest.profile = record("profile.json", data);
|
|
316
464
|
}
|
|
@@ -375,7 +523,7 @@ function readPreparedBundleSync(directory) {
|
|
|
375
523
|
const bytes = load(manifest.profile, 1024 * 1024);
|
|
376
524
|
const profile = JSON.parse(bytes.toString("utf8"));
|
|
377
525
|
validateCharacterProfile(profile);
|
|
378
|
-
profileNames(profile, [...manifest.animations, ...inspected.animations,
|
|
526
|
+
profileNames(profile, [...manifest.animations, ...inspected.animations, ...BUILTIN_ANIMATIONS]);
|
|
379
527
|
}
|
|
380
528
|
return { manifest, files };
|
|
381
529
|
}
|
package/dist/prepare.js
CHANGED
|
@@ -4,6 +4,7 @@ import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/
|
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { inspectVrm, inspectVrma, MAX_MODEL_BYTES, MAX_ANIMATION_BYTES } from "./inspect.js";
|
|
6
6
|
import { validateCharacterProfile } from "./config.js";
|
|
7
|
+
import { BUILTIN_ANIMATIONS, isBuiltinAnimation } from "./builtin-motion-catalog.js";
|
|
7
8
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
8
9
|
const arrayBuffer = (bytes) => Uint8Array.from(bytes).buffer;
|
|
9
10
|
async function bounded(path, maximum) {
|
|
@@ -20,7 +21,7 @@ function animationNames(animations) {
|
|
|
20
21
|
throw new Error("invalid animation collection");
|
|
21
22
|
const names = new Set();
|
|
22
23
|
for (const a of animations) {
|
|
23
|
-
if (!a || typeof a.name !== "string" || !a.name.trim() || a.name.length > 128 || names.has(a.name))
|
|
24
|
+
if (!a || typeof a.name !== "string" || !a.name.trim() || a.name.length > 128 || names.has(a.name) || (a.name !== "StarLiteIdle" && isBuiltinAnimation(a.name)))
|
|
24
25
|
throw new Error("duplicate or invalid animation name");
|
|
25
26
|
names.add(a.name);
|
|
26
27
|
}
|
|
@@ -53,7 +54,7 @@ export async function prepareModel(request) {
|
|
|
53
54
|
const data = await bounded(request.profile, 1024 * 1024);
|
|
54
55
|
const profile = JSON.parse(data.toString("utf8"));
|
|
55
56
|
validateCharacterProfile(profile);
|
|
56
|
-
profileNames(profile, [...animations, ...inspected.animations,
|
|
57
|
+
profileNames(profile, [...animations, ...inspected.animations, ...BUILTIN_ANIMATIONS]);
|
|
57
58
|
files.set("profile.json", data);
|
|
58
59
|
manifest.profile = record("profile.json", data);
|
|
59
60
|
}
|
|
@@ -128,7 +129,7 @@ export function readPreparedBundleSync(directory) {
|
|
|
128
129
|
const bytes = load(manifest.profile, 1024 * 1024);
|
|
129
130
|
const profile = JSON.parse(bytes.toString("utf8"));
|
|
130
131
|
validateCharacterProfile(profile);
|
|
131
|
-
profileNames(profile, [...manifest.animations, ...inspected.animations,
|
|
132
|
+
profileNames(profile, [...manifest.animations, ...inspected.animations, ...BUILTIN_ANIMATIONS]);
|
|
132
133
|
}
|
|
133
134
|
return { manifest, files };
|
|
134
135
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DialogueSceneConfig, ModelController } from "@starcried/starlite-core";
|
|
1
|
+
import type { EnvironmentLighting, StarLiteLocale, DialogueSceneConfig, ModelController, PerformanceConfig } from "@starcried/starlite-core";
|
|
2
2
|
export type { ModelController, AnimationPlaybackResult, AnimationPlaybackOptions } from "@starcried/starlite-core";
|
|
3
3
|
export interface VrmAnimationSource {
|
|
4
4
|
name: string;
|
|
@@ -9,6 +9,22 @@ export interface BonePose {
|
|
|
9
9
|
rotation: [number, number, number, number];
|
|
10
10
|
position?: [number, number, number];
|
|
11
11
|
}
|
|
12
|
+
/** Per-model calibration. Omitted values preserve the original pose and gaze. */
|
|
13
|
+
export interface ModelAdjustments {
|
|
14
|
+
idle?: {
|
|
15
|
+
armLiftDegrees?: number;
|
|
16
|
+
palmTwistDegrees?: number;
|
|
17
|
+
wristLiftDegrees?: number;
|
|
18
|
+
/** 0 opens mapped fingers; 1 closes them. Omit to keep the authored hands. */
|
|
19
|
+
handCurl?: number;
|
|
20
|
+
elbowBendDegrees?: number;
|
|
21
|
+
};
|
|
22
|
+
gaze?: {
|
|
23
|
+
headStrength?: number;
|
|
24
|
+
eyeStrength?: number;
|
|
25
|
+
smoothingMs?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
12
28
|
/** Data only: imported profiles cannot execute scripts. All rotations use glTF coordinates. */
|
|
13
29
|
export interface CharacterProfile {
|
|
14
30
|
schemaVersion: 1;
|
|
@@ -33,7 +49,11 @@ export interface CharacterProfile {
|
|
|
33
49
|
};
|
|
34
50
|
}
|
|
35
51
|
export interface VrmWidgetConfig {
|
|
52
|
+
locale?: StarLiteLocale;
|
|
36
53
|
backend?: "vrm";
|
|
54
|
+
adjustments?: ModelAdjustments;
|
|
55
|
+
performance?: PerformanceConfig;
|
|
56
|
+
environmentLighting?: EnvironmentLighting;
|
|
37
57
|
model: string;
|
|
38
58
|
fallback: string;
|
|
39
59
|
animations?: readonly VrmAnimationSource[];
|
|
@@ -140,6 +160,7 @@ export interface VrmDiagnostics {
|
|
|
140
160
|
}[];
|
|
141
161
|
}
|
|
142
162
|
export interface VrmController extends ModelController {
|
|
163
|
+
setModelAdjustments(adjustments: ModelAdjustments): void;
|
|
143
164
|
diagnostics(): VrmDiagnostics;
|
|
144
165
|
captureFrame(): string;
|
|
145
166
|
getExpressions(): readonly string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starcried/starlite-vrm",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "BSD-2-Clause",
|
|
6
6
|
"files": [
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"README.md",
|
|
10
10
|
"DEFAULT_IDLE.md",
|
|
11
11
|
"THIRD_PARTY_NOTICES.md",
|
|
12
|
-
"THIRD_PARTY_LICENSES.txt"
|
|
12
|
+
"THIRD_PARTY_LICENSES.txt",
|
|
13
|
+
"README.en.md",
|
|
14
|
+
"BUNDLED_MOTIONS.md",
|
|
15
|
+
"BUNDLED_MOTIONS.json"
|
|
13
16
|
],
|
|
14
17
|
"exports": {
|
|
15
18
|
".": {
|
|
@@ -40,7 +43,7 @@
|
|
|
40
43
|
"dependencies": {
|
|
41
44
|
"@pixiv/three-vrm": "3.5.5",
|
|
42
45
|
"@pixiv/three-vrm-animation": "3.5.5",
|
|
43
|
-
"@starcried/starlite-core": "0.1.0
|
|
46
|
+
"@starcried/starlite-core": "0.1.0",
|
|
44
47
|
"three": "0.186.0"
|
|
45
48
|
},
|
|
46
49
|
"devDependencies": {
|