@starcried/starlite-vrm 0.0.0-stage → 0.1.0-alpha.2
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/DEFAULT_IDLE.md +21 -0
- package/LICENSE +25 -0
- package/README.md +45 -2
- package/THIRD_PARTY_LICENSES.txt +80 -0
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/dist/camera.d.ts +17 -0
- package/dist/camera.js +112 -0
- package/dist/character.d.ts +10 -0
- package/dist/character.js +145 -0
- package/dist/config.cjs +152 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +114 -0
- package/dist/default-idle-data.d.ts +1 -0
- package/dist/default-idle-data.js +2 -0
- package/dist/default-idle.d.ts +2 -0
- package/dist/default-idle.js +5 -0
- package/dist/embedded.d.ts +11 -0
- package/dist/embedded.js +154 -0
- package/dist/gaze.d.ts +28 -0
- package/dist/gaze.js +102 -0
- package/dist/host-motion.d.ts +14 -0
- package/dist/host-motion.js +97 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5243 -0
- package/dist/inspect.cjs +245 -0
- package/dist/inspect.d.ts +21 -0
- package/dist/inspect.js +272 -0
- package/dist/playback.d.ts +17 -0
- package/dist/playback.js +122 -0
- package/dist/pose-bounds.d.ts +8 -0
- package/dist/pose-bounds.js +80 -0
- package/dist/prepare.cjs +387 -0
- package/dist/prepare.d.ts +42 -0
- package/dist/prepare.js +134 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +1 -0
- package/dist/widget.css +69 -0
- package/package.json +56 -4
package/dist/playback.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { AnimationMixer, AnimationClip, PropertyBinding, QuaternionKeyframeTrack, VectorKeyframeTrack, NumberKeyframeTrack, LoopOnce, LoopRepeat } from "three";
|
|
3
|
+
/** One semantic clip, with a short captured-pose transition on interruption. */
|
|
4
|
+
export class VrmAnimationPlayer {
|
|
5
|
+
mixer;
|
|
6
|
+
#clips;
|
|
7
|
+
#restore;
|
|
8
|
+
#returnTo;
|
|
9
|
+
#active;
|
|
10
|
+
#finished = false;
|
|
11
|
+
#transition;
|
|
12
|
+
static transitionSeconds = .4;
|
|
13
|
+
constructor(root, clips, restore, returnTo) {
|
|
14
|
+
if (returnTo && !clips.has(returnTo))
|
|
15
|
+
throw new Error(`return animation does not exist: ${returnTo}`);
|
|
16
|
+
this.mixer = new AnimationMixer(root);
|
|
17
|
+
this.#clips = clips;
|
|
18
|
+
this.#restore = restore;
|
|
19
|
+
this.#returnTo = returnTo;
|
|
20
|
+
this.mixer.addEventListener("finished", (event) => { if (event.action === this.#active?.action)
|
|
21
|
+
this.#finished = true; });
|
|
22
|
+
}
|
|
23
|
+
get activeName() { return this.#active?.name; }
|
|
24
|
+
get isOneShot() { return this.#active?.action.loop === LoopOnce; }
|
|
25
|
+
get hasLookAtTrack() { return this.#clips.get(this.#active?.name ?? "")?.tracks.some((track) => track.name.includes("LookAt")) ?? false; }
|
|
26
|
+
get hasBlinkTrack() { return this.#clips.get(this.#active?.name ?? "")?.tracks.some((track) => /VRMExpression_blink(?:Left|Right)?\.weight$/.test(track.name)) ?? false; }
|
|
27
|
+
play(name, options = {}) {
|
|
28
|
+
const clip = this.#clips.get(name);
|
|
29
|
+
if (!clip)
|
|
30
|
+
return Promise.reject(new Error(`animation does not exist: ${name}`));
|
|
31
|
+
if (this.#active?.name === name && !options.restart)
|
|
32
|
+
return this.#active.promise;
|
|
33
|
+
const pose = this.#active ? this.#capturePose() : undefined;
|
|
34
|
+
this.stop("interrupted");
|
|
35
|
+
const action = this.mixer.clipAction(clip).reset().setEffectiveWeight(1);
|
|
36
|
+
action.setLoop(options.mode === "loop" ? LoopRepeat : LoopOnce, options.mode === "loop" ? Infinity : 1);
|
|
37
|
+
action.clampWhenFinished = true;
|
|
38
|
+
let resolve;
|
|
39
|
+
const promise = new Promise((done) => { resolve = done; });
|
|
40
|
+
this.#active = { name, action, promise, resolve };
|
|
41
|
+
action.play();
|
|
42
|
+
if (pose) {
|
|
43
|
+
const outgoing = this.mixer.clipAction(pose);
|
|
44
|
+
outgoing.setLoop(LoopOnce, 1);
|
|
45
|
+
outgoing.clampWhenFinished = true;
|
|
46
|
+
outgoing.setEffectiveWeight(1).play();
|
|
47
|
+
action.setEffectiveWeight(0);
|
|
48
|
+
this.#transition = { clip: pose, action: outgoing, elapsed: 0 };
|
|
49
|
+
}
|
|
50
|
+
this.mixer.update(0);
|
|
51
|
+
return promise;
|
|
52
|
+
}
|
|
53
|
+
update(delta) {
|
|
54
|
+
if (this.#transition) {
|
|
55
|
+
this.#transition.elapsed += delta;
|
|
56
|
+
const t = Math.max(0, Math.min(1, this.#transition.elapsed / _a.transitionSeconds));
|
|
57
|
+
// Ease the blend at both ends without overshooting a bone's target pose.
|
|
58
|
+
// This shapes the weights; the frozen snapshot does not retain velocity.
|
|
59
|
+
const weight = t * t * (3 - 2 * t);
|
|
60
|
+
this.#transition.action.setEffectiveWeight(1 - weight);
|
|
61
|
+
this.#active?.action.setEffectiveWeight(weight);
|
|
62
|
+
}
|
|
63
|
+
this.mixer.update(delta);
|
|
64
|
+
if (this.#transition && this.#transition.elapsed >= _a.transitionSeconds)
|
|
65
|
+
this.#clearTransition();
|
|
66
|
+
if (this.#finished) {
|
|
67
|
+
if (this.#returnTo) {
|
|
68
|
+
// Finish at the authored endpoint, then capture it before starting Idle.
|
|
69
|
+
// Restoring neutral first would erase the pose needed for the exit blend.
|
|
70
|
+
this.#active?.resolve("completed");
|
|
71
|
+
void this.play(this.#returnTo, { mode: "loop", restart: true });
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
this.stop("completed");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
stop(result = "stopped") {
|
|
78
|
+
const active = this.#active;
|
|
79
|
+
this.#active = undefined;
|
|
80
|
+
this.#finished = false;
|
|
81
|
+
this.#clearTransition();
|
|
82
|
+
this.mixer.stopAllAction();
|
|
83
|
+
this.#restore();
|
|
84
|
+
active?.resolve(result);
|
|
85
|
+
}
|
|
86
|
+
#capturePose() {
|
|
87
|
+
// Capture the currently mixed pose, including an interrupted transition.
|
|
88
|
+
// Freezing this snapshot avoids a neutral-frame snap or unbounded outgoing
|
|
89
|
+
// actions when DSH changes state several times inside the transition.
|
|
90
|
+
const tracks = new Map();
|
|
91
|
+
for (const clip of this.#clips.values()) {
|
|
92
|
+
for (const source of clip.tracks) {
|
|
93
|
+
if (tracks.has(source.name))
|
|
94
|
+
continue;
|
|
95
|
+
const Track = source.ValueTypeName === "quaternion" ? QuaternionKeyframeTrack
|
|
96
|
+
: source.ValueTypeName === "vector" ? VectorKeyframeTrack : NumberKeyframeTrack;
|
|
97
|
+
const size = source.ValueTypeName === "quaternion" ? 4 : source.ValueTypeName === "vector" ? 3 : 1;
|
|
98
|
+
const value = new Float32Array(size);
|
|
99
|
+
const binding = new PropertyBinding(this.mixer.getRoot(), source.name);
|
|
100
|
+
// Three assigns this getter dynamically when the binding is bound;
|
|
101
|
+
// its type declarations only expose bind/unbind.
|
|
102
|
+
binding.bind();
|
|
103
|
+
binding.getValue(value, 0);
|
|
104
|
+
binding.unbind();
|
|
105
|
+
// A snapshot is always constant, even if the source used GLTF cubic
|
|
106
|
+
// splines with tangent triplets and a custom interpolant factory.
|
|
107
|
+
const track = new Track(source.name, [0, _a.transitionSeconds], [...value, ...value]);
|
|
108
|
+
tracks.set(track.name, track);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return new AnimationClip("transition-pose", _a.transitionSeconds, [...tracks.values()]);
|
|
112
|
+
}
|
|
113
|
+
#clearTransition() {
|
|
114
|
+
if (!this.#transition)
|
|
115
|
+
return;
|
|
116
|
+
this.#transition.action.stop();
|
|
117
|
+
this.mixer.uncacheClip(this.#transition.clip);
|
|
118
|
+
this.#transition = undefined;
|
|
119
|
+
}
|
|
120
|
+
destroy() { this.stop(); this.mixer.uncacheRoot(this.mixer.getRoot()); }
|
|
121
|
+
}
|
|
122
|
+
_a = VrmAnimationPlayer;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Box3, type Object3D } from "three";
|
|
2
|
+
/** Conservative skin bounds: build vertex clusters once, then transform one box
|
|
3
|
+
* per influencing bone. This avoids reskinning every vertex on every frame. */
|
|
4
|
+
export declare class PoseBounds {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(root: Object3D);
|
|
7
|
+
measure(root: Object3D, result?: Box3): Box3;
|
|
8
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Box3, Matrix3, Matrix4, Mesh, SkinnedMesh, Vector3 } from "three";
|
|
2
|
+
/** Conservative skin bounds: build vertex clusters once, then transform one box
|
|
3
|
+
* per influencing bone. This avoids reskinning every vertex on every frame. */
|
|
4
|
+
export class PoseBounds {
|
|
5
|
+
#meshes = [];
|
|
6
|
+
#matrix = new Matrix4();
|
|
7
|
+
#box = new Box3();
|
|
8
|
+
constructor(root) {
|
|
9
|
+
const point = new Vector3(), local = new Vector3(), offset = new Vector3(), matrix = new Matrix4(), linear = new Matrix3();
|
|
10
|
+
root.traverseVisible(object => {
|
|
11
|
+
if (!(object instanceof Mesh))
|
|
12
|
+
return;
|
|
13
|
+
const mesh = object, geometry = mesh.geometry, positions = geometry.getAttribute("position");
|
|
14
|
+
if (!positions)
|
|
15
|
+
return;
|
|
16
|
+
const boxes = new Map();
|
|
17
|
+
const skin = mesh instanceof SkinnedMesh;
|
|
18
|
+
const indices = geometry.getAttribute("skinIndex"), weights = geometry.getAttribute("skinWeight");
|
|
19
|
+
for (let vertex = 0; vertex < positions.count; vertex++) {
|
|
20
|
+
point.fromBufferAttribute(positions, vertex);
|
|
21
|
+
if (skin && (!indices || !weights))
|
|
22
|
+
throw new Error("skin bounds require joint indices and weights");
|
|
23
|
+
for (let axis = 0; axis < (skin ? 4 : 1); axis++) {
|
|
24
|
+
if (skin && weights.getComponent(vertex, axis) <= 0)
|
|
25
|
+
continue;
|
|
26
|
+
const joint = skin ? indices.getComponent(vertex, axis) : -1;
|
|
27
|
+
matrix.identity();
|
|
28
|
+
if (skin) {
|
|
29
|
+
const inverse = mesh.skeleton.boneInverses[joint];
|
|
30
|
+
if (!inverse)
|
|
31
|
+
throw new Error("skin bounds reference a missing joint");
|
|
32
|
+
matrix.copy(inverse).multiply(mesh.bindMatrix);
|
|
33
|
+
}
|
|
34
|
+
linear.setFromMatrix4(matrix);
|
|
35
|
+
const box = boxes.get(joint) ?? { base: new Box3(), morphs: new Map() };
|
|
36
|
+
box.base.expandByPoint(local.copy(point).applyMatrix4(matrix));
|
|
37
|
+
for (const [index, morph] of (geometry.morphAttributes.position ?? []).entries()) {
|
|
38
|
+
offset.fromBufferAttribute(morph, vertex);
|
|
39
|
+
if (!geometry.morphTargetsRelative)
|
|
40
|
+
offset.sub(point);
|
|
41
|
+
if (offset.lengthSq() === 0)
|
|
42
|
+
continue;
|
|
43
|
+
offset.applyMatrix3(linear);
|
|
44
|
+
// Include zero: other vertices in this cluster may not use a morph.
|
|
45
|
+
const delta = box.morphs.get(index) ?? new Box3(new Vector3(), new Vector3());
|
|
46
|
+
delta.expandByPoint(offset);
|
|
47
|
+
box.morphs.set(index, delta);
|
|
48
|
+
}
|
|
49
|
+
boxes.set(joint, box);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.#meshes.push({ mesh, boxes });
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
measure(root, result = new Box3()) {
|
|
56
|
+
root.updateMatrixWorld(true);
|
|
57
|
+
result.makeEmpty();
|
|
58
|
+
for (const { mesh, boxes } of this.#meshes) {
|
|
59
|
+
if (!mesh.visible)
|
|
60
|
+
continue;
|
|
61
|
+
for (const [joint, box] of boxes) {
|
|
62
|
+
this.#matrix.copy(mesh.matrixWorld);
|
|
63
|
+
if (mesh instanceof SkinnedMesh)
|
|
64
|
+
this.#matrix.multiply(mesh.bindMatrixInverse).multiply(mesh.skeleton.bones[joint].matrixWorld);
|
|
65
|
+
this.#box.copy(box.base);
|
|
66
|
+
// Only active morphs contribute, rather than sizing the camera for all
|
|
67
|
+
// mutually exclusive facial expressions simultaneously.
|
|
68
|
+
for (const [index, delta] of box.morphs) {
|
|
69
|
+
const weight = mesh.morphTargetInfluences?.[index] ?? 0;
|
|
70
|
+
if (!Number.isFinite(weight))
|
|
71
|
+
throw new Error("non-finite morph weight");
|
|
72
|
+
this.#box.min.addScaledVector(weight < 0 ? delta.max : delta.min, weight);
|
|
73
|
+
this.#box.max.addScaledVector(weight < 0 ? delta.min : delta.max, weight);
|
|
74
|
+
}
|
|
75
|
+
result.union(this.#box.applyMatrix4(this.#matrix));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/prepare.cjs
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/prepare.ts
|
|
20
|
+
var prepare_exports = {};
|
|
21
|
+
__export(prepare_exports, {
|
|
22
|
+
prepareModel: () => prepareModel,
|
|
23
|
+
readPreparedBundle: () => readPreparedBundle,
|
|
24
|
+
readPreparedBundleSync: () => readPreparedBundleSync
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(prepare_exports);
|
|
27
|
+
var import_node_fs = require("node:fs");
|
|
28
|
+
var import_node_crypto = require("node:crypto");
|
|
29
|
+
var import_promises = require("node:fs/promises");
|
|
30
|
+
var import_node_path = require("node:path");
|
|
31
|
+
|
|
32
|
+
// src/inspect.ts
|
|
33
|
+
var MAX_MODEL_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
var MAX_ANIMATION_BYTES = 16 * 1024 * 1024;
|
|
35
|
+
function parseGlb(bytes, kind) {
|
|
36
|
+
const limit = kind === "vrm" ? MAX_MODEL_BYTES : MAX_ANIMATION_BYTES;
|
|
37
|
+
if (bytes.byteLength < 20 || bytes.byteLength > limit) throw new Error("invalid GLB size");
|
|
38
|
+
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 Error("invalid GLB header");
|
|
40
|
+
let json;
|
|
41
|
+
let binSize = 0;
|
|
42
|
+
let binStart = 0;
|
|
43
|
+
let chunkCount = 0;
|
|
44
|
+
for (let offset = 12; offset < bytes.byteLength; ) {
|
|
45
|
+
if (offset + 8 > bytes.byteLength) throw new Error("truncated GLB chunk");
|
|
46
|
+
const size = view.getUint32(offset, true), type = view.getUint32(offset + 4, true);
|
|
47
|
+
offset += 8;
|
|
48
|
+
if (size % 4 || size > bytes.byteLength - offset) throw new Error("invalid GLB chunk length");
|
|
49
|
+
if (chunkCount === 0 && type === 1313821514) {
|
|
50
|
+
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes, offset, size)));
|
|
51
|
+
} else if (chunkCount === 1 && type === 5130562) {
|
|
52
|
+
binSize = size;
|
|
53
|
+
binStart = offset;
|
|
54
|
+
} else throw new Error("unexpected GLB chunk");
|
|
55
|
+
offset += size;
|
|
56
|
+
chunkCount++;
|
|
57
|
+
}
|
|
58
|
+
if (!json || json.asset?.version !== "2.0") throw new Error("glTF 2.0 is required");
|
|
59
|
+
const expected = kind === "vrm" ? json.extensions?.VRM ?? json.extensions?.VRMC_vrm : json.extensions?.VRMC_vrm_animation;
|
|
60
|
+
if (!expected) throw new Error(`missing ${kind} extension`);
|
|
61
|
+
if (kind === "vrm" && json.extensions?.VRM && json.extensions.VRM.specVersion !== "0.0") throw new Error("unsupported VRM version");
|
|
62
|
+
if (kind === "vrm" && json.extensions?.VRMC_vrm && json.extensions.VRMC_vrm.specVersion !== "1.0") throw new Error("unsupported VRM version");
|
|
63
|
+
if (kind === "vrma" && expected.specVersion !== "1.0") throw new Error("VRMA 1.0 is required");
|
|
64
|
+
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 Error("unsupported required glTF extension");
|
|
66
|
+
const visit = (value, depth = 0) => {
|
|
67
|
+
if (depth > 64) throw new Error("glTF nesting limit exceeded");
|
|
68
|
+
if (!value || typeof value !== "object") return;
|
|
69
|
+
for (const [key, child] of Object.entries(value)) {
|
|
70
|
+
if (key === "uri") throw new Error("external glTF resources are not supported");
|
|
71
|
+
if (typeof child === "number" && !Number.isFinite(child)) throw new Error("non-finite glTF value");
|
|
72
|
+
visit(child, depth + 1);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
visit(json);
|
|
76
|
+
if ((json.buffers?.length ?? 0) > 1 || (json.buffers?.[0]?.byteLength ?? 0) > binSize) throw new Error("invalid GLB buffer");
|
|
77
|
+
for (const buffer of json.bufferViews ?? []) {
|
|
78
|
+
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 Error("invalid glTF buffer view");
|
|
80
|
+
}
|
|
81
|
+
if ((json.nodes?.length ?? 0) > 4096 || (json.accessors?.length ?? 0) > 32768) throw new Error("glTF resource count exceeded");
|
|
82
|
+
validateStructure(json, new DataView(bytes, binStart, binSize));
|
|
83
|
+
return json;
|
|
84
|
+
}
|
|
85
|
+
function embeddedAnimationCatalog(json) {
|
|
86
|
+
const animations = json.animations ?? [];
|
|
87
|
+
if (!Array.isArray(animations) || animations.length > 64) throw new Error("at most 64 embedded animations are supported");
|
|
88
|
+
const names = /* @__PURE__ */ new Set();
|
|
89
|
+
return animations.map((animation, index) => {
|
|
90
|
+
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 === "StarLiteIdle" || names.has(name)) throw new Error("invalid or duplicate embedded animation name");
|
|
92
|
+
names.add(name);
|
|
93
|
+
let duration = 0;
|
|
94
|
+
for (const sampler of animation.samplers ?? []) {
|
|
95
|
+
const accessor = json.accessors?.[sampler.input];
|
|
96
|
+
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new Error("embedded animation has invalid animation times");
|
|
97
|
+
duration = Math.max(duration, accessor.max[0]);
|
|
98
|
+
}
|
|
99
|
+
if (!(duration > 0) || duration > 600) throw new Error("embedded animation duration must be between 0 and 600 seconds");
|
|
100
|
+
return { name, durationMs: duration * 1e3 };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function inspectVrm(bytes) {
|
|
104
|
+
const json = parseGlb(bytes, "vrm");
|
|
105
|
+
const v0 = json.extensions.VRM;
|
|
106
|
+
const humanoid = (v0 ?? json.extensions.VRMC_vrm).humanoid?.humanBones;
|
|
107
|
+
const bones = v0 ? Object.fromEntries((humanoid ?? []).map((b) => [b.bone, b])) : humanoid;
|
|
108
|
+
const required = ["hips", "spine", "head", ...["left", "right"].flatMap((side) => ["UpperLeg", "LowerLeg", "Foot", "UpperArm", "LowerArm", "Hand"].map((part) => side + part))];
|
|
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
|
+
}
|
|
115
|
+
const blinkNames = v0 ? ["blink", "blink_l", "blink_r"] : ["blink", "blinkLeft", "blinkRight"];
|
|
116
|
+
const groups = v0?.blendShapeMaster?.blendShapeGroups;
|
|
117
|
+
if (groups !== void 0 && !Array.isArray(groups)) throw new Error("invalid VRM blink expressions");
|
|
118
|
+
const blinkPresets = v0 ? (groups ?? []).filter((item) => blinkNames.includes(item?.presetName)) : blinkNames.flatMap((name2) => {
|
|
119
|
+
const expression = json.extensions.VRMC_vrm.expressions?.preset?.[name2];
|
|
120
|
+
return expression === void 0 ? [] : [expression];
|
|
121
|
+
});
|
|
122
|
+
for (const expression of blinkPresets) {
|
|
123
|
+
if (!expression || typeof expression !== "object" || Array.isArray(expression)) throw new Error("invalid VRM blink expression");
|
|
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
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const meta = (v0 ?? json.extensions.VRMC_vrm).meta ?? {};
|
|
136
|
+
const name = v0 ? meta.title : meta.name;
|
|
137
|
+
return {
|
|
138
|
+
version: v0 ? "0" : "1",
|
|
139
|
+
...typeof name === "string" ? { name } : {},
|
|
140
|
+
animations: embeddedAnimationCatalog(json),
|
|
141
|
+
expressions: v0 ? (v0.blendShapeMaster?.blendShapeGroups ?? []).map((group) => group.name) : Object.keys({ ...json.extensions.VRMC_vrm.expressions?.preset, ...json.extensions.VRMC_vrm.expressions?.custom })
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function inspectVrma(bytes) {
|
|
145
|
+
const json = parseGlb(bytes, "vrma");
|
|
146
|
+
if (json.animations?.length !== 1) throw new Error("each VRMA file must contain exactly one animation");
|
|
147
|
+
let duration = 0;
|
|
148
|
+
for (const sampler of json.animations[0].samplers ?? []) {
|
|
149
|
+
const accessor = json.accessors?.[sampler.input];
|
|
150
|
+
if (!accessor || accessor.type !== "SCALAR" || accessor.componentType !== 5126 || !Number.isFinite(accessor.max?.[0])) throw new Error("VRMA has invalid animation times");
|
|
151
|
+
duration = Math.max(duration, accessor.max[0]);
|
|
152
|
+
}
|
|
153
|
+
if (!(duration > 0) || duration > 600) throw new Error("VRMA duration must be between 0 and 600 seconds");
|
|
154
|
+
return { durationMs: duration * 1e3 };
|
|
155
|
+
}
|
|
156
|
+
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 Error(`invalid glTF ${key}`);
|
|
159
|
+
}
|
|
160
|
+
const views = json.bufferViews ?? [], nodes = json.nodes ?? [], accessors = json.accessors ?? [];
|
|
161
|
+
const integer = (n, min, max) => Number.isSafeInteger(n) && n >= min && n <= max;
|
|
162
|
+
const index = (n, array) => integer(n, 0, array.length - 1);
|
|
163
|
+
const seen = /* @__PURE__ */ new Set(), active = /* @__PURE__ */ new Set(), parents = /* @__PURE__ */ new Set();
|
|
164
|
+
const walk = (i, depth) => {
|
|
165
|
+
if (!index(i, nodes) || active.has(i) || depth > 128) throw new Error("invalid glTF node hierarchy");
|
|
166
|
+
if (seen.has(i)) return;
|
|
167
|
+
const node = nodes[i];
|
|
168
|
+
if (!node || typeof node !== "object") throw new Error("invalid glTF node");
|
|
169
|
+
seen.add(i);
|
|
170
|
+
active.add(i);
|
|
171
|
+
if (node.children !== void 0 && !Array.isArray(node.children)) throw new Error("invalid node children");
|
|
172
|
+
for (const child of node.children ?? []) {
|
|
173
|
+
if (parents.has(child)) throw new Error("node has multiple parents");
|
|
174
|
+
parents.add(child);
|
|
175
|
+
walk(child, depth + 1);
|
|
176
|
+
}
|
|
177
|
+
active.delete(i);
|
|
178
|
+
};
|
|
179
|
+
for (let i = 0; i < nodes.length; i++) walk(i, 0);
|
|
180
|
+
const components = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
181
|
+
const sizes = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 };
|
|
182
|
+
let decoded = 0;
|
|
183
|
+
const span = (viewIndex, offset, count, element, alignment, strided = false) => {
|
|
184
|
+
if (!index(viewIndex, views)) throw new Error("invalid accessor buffer view");
|
|
185
|
+
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 Error("accessor exceeds buffer view");
|
|
187
|
+
return (v.byteOffset ?? 0) + offset;
|
|
188
|
+
};
|
|
189
|
+
for (const a of accessors) {
|
|
190
|
+
const width = sizes[a?.componentType], count = a?.count, componentsCount = components[a?.type];
|
|
191
|
+
if (!width || !componentsCount || !integer(count, 1, 2e6)) throw new Error("invalid accessor allocation");
|
|
192
|
+
const dimension = a.type === "MAT2" ? 2 : a.type === "MAT3" ? 3 : a.type === "MAT4" ? 4 : 0;
|
|
193
|
+
const element = dimension ? Math.ceil(dimension * width / 4) * 4 * dimension : width * componentsCount;
|
|
194
|
+
decoded += element * count;
|
|
195
|
+
if (decoded > 256 * 1024 * 1024) throw new Error("decoded geometry exceeds 256 MiB");
|
|
196
|
+
const checkFloats = (at, n, stride) => {
|
|
197
|
+
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 Error("non-finite accessor value");
|
|
199
|
+
};
|
|
200
|
+
if (a.bufferView !== void 0) {
|
|
201
|
+
const at = span(a.bufferView, a.byteOffset ?? 0, count, element, width, true);
|
|
202
|
+
checkFloats(at, count, views[a.bufferView].byteStride ?? element);
|
|
203
|
+
}
|
|
204
|
+
if (a.sparse) {
|
|
205
|
+
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 Error("invalid sparse accessor");
|
|
207
|
+
const at = span(sparse.indices.bufferView, sparse.indices.byteOffset ?? 0, n, iw, iw);
|
|
208
|
+
const valuesAt = span(sparse.values?.bufferView, sparse.values?.byteOffset ?? 0, n, element, width);
|
|
209
|
+
checkFloats(valuesAt, n, element);
|
|
210
|
+
let previous = -1;
|
|
211
|
+
for (let k = 0; k < n; k++) {
|
|
212
|
+
const offset = at + k * iw;
|
|
213
|
+
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 Error("invalid sparse index");
|
|
215
|
+
previous = next;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const animation of json.animations ?? []) {
|
|
220
|
+
if (!Array.isArray(animation.samplers) || !Array.isArray(animation.channels)) throw new Error("invalid animation");
|
|
221
|
+
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 Error("invalid animation channel");
|
|
223
|
+
}
|
|
224
|
+
for (const sampler of animation.samplers) {
|
|
225
|
+
if (!index(sampler.input, accessors) || !index(sampler.output, accessors)) throw new Error("invalid animation accessor");
|
|
226
|
+
const a = accessors[sampler.input];
|
|
227
|
+
if (a.type !== "SCALAR" || a.componentType !== 5126 || a.sparse || a.bufferView === void 0) throw new Error("invalid animation timeline");
|
|
228
|
+
const start = span(a.bufferView, a.byteOffset ?? 0, a.count, 4, 4);
|
|
229
|
+
let previous = -1;
|
|
230
|
+
for (let i = 0; i < a.count; i++) {
|
|
231
|
+
const time = binary.getFloat32(start + i * 4, true);
|
|
232
|
+
if (!Number.isFinite(time) || time <= previous || time < 0 || time > 600) throw new Error("invalid animation times");
|
|
233
|
+
previous = time;
|
|
234
|
+
}
|
|
235
|
+
if (!Array.isArray(a.max) || Math.abs(a.max[0] - previous) > 1e-3) throw new Error("animation duration does not match timeline");
|
|
236
|
+
if (sampler.interpolation !== void 0 && !["LINEAR", "STEP", "CUBICSPLINE"].includes(sampler.interpolation)) throw new Error("unsupported animation interpolation");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/config.ts
|
|
242
|
+
function finite(value, min, max) {
|
|
243
|
+
return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
244
|
+
}
|
|
245
|
+
function validateCharacterProfile(input) {
|
|
246
|
+
if (!input || typeof input !== "object") throw new Error("invalid character profile");
|
|
247
|
+
const profile = input;
|
|
248
|
+
if (profile.schemaVersion !== 1 || profile.type !== "weiguang") throw new Error("unsupported character profile");
|
|
249
|
+
const record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
250
|
+
const pose = (value) => {
|
|
251
|
+
if (!record(value) || Object.keys(value).length > 256) return false;
|
|
252
|
+
return Object.values(value).every((item) => Array.isArray(item?.rotation) && item.rotation.length === 4 && item.rotation.every((v) => finite(v, -1, 1)) && Math.abs(Math.hypot(...item.rotation) - 1) < 0.01 && (item.position === void 0 || Array.isArray(item.position) && item.position.length === 3 && item.position.every((v) => finite(v, -10, 10))));
|
|
253
|
+
};
|
|
254
|
+
if (!pose(profile.neutralPose) || !record(profile.earPoses) || !Object.values(profile.earPoses).every(pose)) throw new Error("invalid character bone poses");
|
|
255
|
+
if (!Array.isArray(profile.earBones) || profile.earBones.length > 32 || profile.earBones.some((name) => typeof name !== "string" || !name)) throw new Error("invalid ear bones");
|
|
256
|
+
if (!record(profile.expressions) || Object.keys(profile.expressions).length > 64 || !Object.values(profile.expressions).every((v) => typeof v?.expression === "string" && (v.blink === void 0 || typeof v.blink === "string"))) throw new Error("invalid expressions");
|
|
257
|
+
if (profile.neutralAnimation !== void 0 && (typeof profile.neutralAnimation !== "string" || !profile.neutralAnimation.trim() || profile.neutralAnimation.length > 128)) throw new Error("invalid neutral animation");
|
|
258
|
+
for (const emotion of ["Neutral", ...Object.keys(profile.expressions)]) if (!profile.earPoses[emotion] || profile.earBones.some((name) => !profile.earPoses[emotion]?.[name])) throw new Error("incomplete ear poses");
|
|
259
|
+
const a = profile.accessories;
|
|
260
|
+
if (!a || typeof a.collar !== "string" || typeof a.cone !== "string" || typeof a.collarVisible !== "boolean" || typeof a.coneVisible !== "boolean") throw new Error("invalid accessories");
|
|
261
|
+
const blink = profile.blink;
|
|
262
|
+
if (!blink || !finite(blink.period, 1, 60) || !Array.isArray(blink.curve) || blink.curve.length < 2 || blink.curve.length > 64) throw new Error("invalid blink timing");
|
|
263
|
+
let previous = -1;
|
|
264
|
+
for (const point of blink.curve) {
|
|
265
|
+
if (!Array.isArray(point) || point.length !== 2 || !finite(point[0], 0, blink.period) || point[0] <= previous || !finite(point[1], 0, 1)) throw new Error("invalid blink curve");
|
|
266
|
+
previous = point[0];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/prepare.ts
|
|
271
|
+
var hash = (bytes) => (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
272
|
+
var arrayBuffer = (bytes) => Uint8Array.from(bytes).buffer;
|
|
273
|
+
async function bounded(path, maximum) {
|
|
274
|
+
const info = await (0, import_promises.lstat)(path);
|
|
275
|
+
if (!info.isFile() || info.size > maximum) throw new Error("invalid asset size or file type");
|
|
276
|
+
const bytes = await (0, import_promises.readFile)(path);
|
|
277
|
+
if (bytes.byteLength > maximum) throw new Error("asset exceeds byte limit");
|
|
278
|
+
return bytes;
|
|
279
|
+
}
|
|
280
|
+
function animationNames(animations) {
|
|
281
|
+
if (!Array.isArray(animations) || animations.length > 64) throw new Error("invalid animation collection");
|
|
282
|
+
const names = /* @__PURE__ */ new Set();
|
|
283
|
+
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");
|
|
285
|
+
names.add(a.name);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function profileNames(profile, animations) {
|
|
289
|
+
if (profile.neutralAnimation && !animations.some((a) => a.name === profile.neutralAnimation)) throw new Error("profile neutral animation is missing");
|
|
290
|
+
}
|
|
291
|
+
async function prepareModel(request) {
|
|
292
|
+
const bytes = await bounded(request.model, MAX_MODEL_BYTES);
|
|
293
|
+
const inspected = inspectVrm(arrayBuffer(bytes));
|
|
294
|
+
const animations = request.animations ?? [];
|
|
295
|
+
animationNames(animations);
|
|
296
|
+
const files = /* @__PURE__ */ new Map([["model.vrm", bytes]]);
|
|
297
|
+
const record = (file, data) => ({ file, sha256: hash(data), bytes: data.byteLength });
|
|
298
|
+
const manifest = { schemaVersion: 1, tool: "vrm-companion-assets", backend: "vrm", model: { ...record("model.vrm", bytes), sourceSha256: hash(bytes) }, animations: [], publication: { allowRedistribution: request.allowRedistribution === true } };
|
|
299
|
+
let total = 0;
|
|
300
|
+
for (const [i, source] of animations.entries()) {
|
|
301
|
+
const data = await bounded(source.path, MAX_ANIMATION_BYTES);
|
|
302
|
+
total += data.byteLength;
|
|
303
|
+
if (total > 64 * 1024 * 1024) throw new Error("animation collection exceeds 64 MiB");
|
|
304
|
+
const { durationMs } = inspectVrma(arrayBuffer(data));
|
|
305
|
+
const file = `motions/${i}.vrma`;
|
|
306
|
+
files.set(file, data);
|
|
307
|
+
manifest.animations.push({ ...record(file, data), name: source.name, durationMs });
|
|
308
|
+
}
|
|
309
|
+
if (request.profile) {
|
|
310
|
+
const data = await bounded(request.profile, 1024 * 1024);
|
|
311
|
+
const profile = JSON.parse(data.toString("utf8"));
|
|
312
|
+
validateCharacterProfile(profile);
|
|
313
|
+
profileNames(profile, [...animations, ...inspected.animations, { name: "StarLiteIdle" }]);
|
|
314
|
+
files.set("profile.json", data);
|
|
315
|
+
manifest.profile = record("profile.json", data);
|
|
316
|
+
}
|
|
317
|
+
const output = (0, import_node_path.resolve)(request.output);
|
|
318
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(output), { recursive: true });
|
|
319
|
+
try {
|
|
320
|
+
await (0, import_promises.lstat)(output);
|
|
321
|
+
throw new Error("output already exists");
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error.code !== "ENOENT") throw error;
|
|
324
|
+
}
|
|
325
|
+
const stage = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_path.dirname)(output), ".vrm-stage-"));
|
|
326
|
+
try {
|
|
327
|
+
for (const [file, data] of files) {
|
|
328
|
+
const path = (0, import_node_path.join)(stage, file);
|
|
329
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
330
|
+
await (0, import_promises.writeFile)(path, data, { flag: "wx" });
|
|
331
|
+
}
|
|
332
|
+
await (0, import_promises.writeFile)((0, import_node_path.join)(stage, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { flag: "wx" });
|
|
333
|
+
await (0, import_promises.rename)(stage, output);
|
|
334
|
+
return manifest;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
await (0, import_promises.rm)(stage, { recursive: true, force: true });
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function readPreparedBundle(directory) {
|
|
341
|
+
return readPreparedBundleSync(directory);
|
|
342
|
+
}
|
|
343
|
+
function boundedSync(path, maximum) {
|
|
344
|
+
const info = (0, import_node_fs.lstatSync)(path);
|
|
345
|
+
if (!info.isFile() || info.size > maximum) throw new Error("invalid asset size or file type");
|
|
346
|
+
const bytes = (0, import_node_fs.readFileSync)(path);
|
|
347
|
+
if (bytes.byteLength > maximum) throw new Error("asset exceeds byte limit");
|
|
348
|
+
return bytes;
|
|
349
|
+
}
|
|
350
|
+
function readPreparedBundleSync(directory) {
|
|
351
|
+
const root = (0, import_node_fs.realpathSync)(directory), raw = boundedSync((0, import_node_path.join)(root, "manifest.json"), 1024 * 1024);
|
|
352
|
+
const manifest = JSON.parse(raw.toString("utf8"));
|
|
353
|
+
if (manifest.schemaVersion !== 1 || manifest.tool !== "vrm-companion-assets" || manifest.backend !== "vrm" || typeof manifest.publication?.allowRedistribution !== "boolean" || !/^[a-f0-9]{64}$/.test(manifest.model?.sourceSha256)) throw new Error("invalid VRM manifest");
|
|
354
|
+
animationNames(manifest.animations);
|
|
355
|
+
const files = /* @__PURE__ */ new Map();
|
|
356
|
+
const load = (item, maximum) => {
|
|
357
|
+
if (!item || typeof item.file !== "string" || !item.file || (0, import_node_path.isAbsolute)(item.file) || /[\\:?%#]/.test(item.file) || item.file.split("/").some((p) => !p || p === "." || p === "..") || files.has(item.file) || !/^[a-f0-9]{64}$/.test(item.sha256) || !Number.isSafeInteger(item.bytes) || item.bytes < 1 || item.bytes > maximum) throw new Error("invalid manifest asset");
|
|
358
|
+
const path = (0, import_node_fs.realpathSync)((0, import_node_path.join)(root, item.file)), part = (0, import_node_path.relative)(root, path);
|
|
359
|
+
if ((0, import_node_path.isAbsolute)(part) || part === ".." || part.startsWith("../") || part.startsWith("..\\")) throw new Error("manifest asset leaves bundle");
|
|
360
|
+
const bytes = boundedSync(path, maximum);
|
|
361
|
+
if (bytes.byteLength !== item.bytes || hash(bytes) !== item.sha256) throw new Error("manifest asset hash mismatch");
|
|
362
|
+
files.set(item.file, bytes);
|
|
363
|
+
return bytes;
|
|
364
|
+
};
|
|
365
|
+
const inspected = inspectVrm(arrayBuffer(load(manifest.model, MAX_MODEL_BYTES)));
|
|
366
|
+
let total = 0;
|
|
367
|
+
for (const item of manifest.animations) {
|
|
368
|
+
const bytes = load(item, MAX_ANIMATION_BYTES);
|
|
369
|
+
total += bytes.byteLength;
|
|
370
|
+
if (total > 64 * 1024 * 1024) throw new Error("animation collection exceeds 64 MiB");
|
|
371
|
+
const inspected2 = inspectVrma(arrayBuffer(bytes));
|
|
372
|
+
if (!Number.isFinite(item.durationMs) || Math.abs(inspected2.durationMs - item.durationMs) > 1) throw new Error("manifest animation duration mismatch");
|
|
373
|
+
}
|
|
374
|
+
if (manifest.profile) {
|
|
375
|
+
const bytes = load(manifest.profile, 1024 * 1024);
|
|
376
|
+
const profile = JSON.parse(bytes.toString("utf8"));
|
|
377
|
+
validateCharacterProfile(profile);
|
|
378
|
+
profileNames(profile, [...manifest.animations, ...inspected.animations, { name: "StarLiteIdle" }]);
|
|
379
|
+
}
|
|
380
|
+
return { manifest, files };
|
|
381
|
+
}
|
|
382
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
383
|
+
0 && (module.exports = {
|
|
384
|
+
prepareModel,
|
|
385
|
+
readPreparedBundle,
|
|
386
|
+
readPreparedBundleSync
|
|
387
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface AssetRecord {
|
|
2
|
+
file: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
bytes: number;
|
|
5
|
+
}
|
|
6
|
+
export interface VrmManifest {
|
|
7
|
+
schemaVersion: 1;
|
|
8
|
+
tool: "vrm-companion-assets";
|
|
9
|
+
backend: "vrm";
|
|
10
|
+
model: AssetRecord & {
|
|
11
|
+
sourceSha256: string;
|
|
12
|
+
};
|
|
13
|
+
animations: (AssetRecord & {
|
|
14
|
+
name: string;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
})[];
|
|
17
|
+
profile?: AssetRecord;
|
|
18
|
+
publication: {
|
|
19
|
+
allowRedistribution: boolean;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface PrepareRequest {
|
|
23
|
+
model: string;
|
|
24
|
+
animations?: {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
}[];
|
|
28
|
+
profile?: string;
|
|
29
|
+
output: string;
|
|
30
|
+
allowRedistribution?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Validate every source before creating output. Never overwrite an existing bundle. */
|
|
33
|
+
export declare function prepareModel(request: PrepareRequest): Promise<VrmManifest>;
|
|
34
|
+
/** Read only declared, hashed files contained in the prepared bundle. */
|
|
35
|
+
export declare function readPreparedBundle(directory: string): Promise<{
|
|
36
|
+
manifest: VrmManifest;
|
|
37
|
+
files: Map<string, Buffer>;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function readPreparedBundleSync(directory: string): {
|
|
40
|
+
manifest: VrmManifest;
|
|
41
|
+
files: Map<string, Buffer>;
|
|
42
|
+
};
|