modelstage 0.0.0-stage → 0.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +137 -0
- package/config/diag-upload.example.json +7 -0
- package/config/models/live2d.assetbundls.json +1 -0
- package/config/models/live2d.azurlane.json +1 -0
- package/config/models/live2d.azurlane_bundle.json +1 -0
- package/config/models/live2d.azurlane_spine.json +1 -0
- package/config/models/live2d.bd2.json +1 -0
- package/config/models/live2d.bluearchive.json +1 -0
- package/config/models/live2d.live2d.json +1 -0
- package/config/models/live2d.nikke.json +1 -0
- package/config/models/live2d.nikke_2.json +1 -0
- package/config/models/live2d.npc.json +1 -0
- package/config/models/spine.assetbundls.json +1 -0
- package/config/models/spine.azurlane.json +1 -0
- package/config/models/spine.azurlane_bundle.json +1 -0
- package/config/models/spine.azurlane_spine.json +1 -0
- package/config/models/spine.bd2.json +1 -0
- package/config/models/spine.bluearchive.json +1 -0
- package/config/models/spine.live2d.json +1 -0
- package/config/models/spine.nikke.json +1 -0
- package/config/models/spine.nikke_2.json +1 -0
- package/config/models/spine.npc.json +1 -0
- package/config/names/NIKKE.json +1058 -0
- package/config/names/README.md +47 -0
- package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
- package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
- package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
- package/config/plugins.json +13 -0
- package/config/resources.example.json +40 -0
- package/package.json +63 -4
- package/plugins/unity-live2d-bundle/README.md +98 -0
- package/plugins/unity-live2d-bundle/index.js +891 -0
- package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
- package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
- package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
- package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
- package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
- package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
- package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
- package/plugins/unity-live2d-bundle/lib/png.js +424 -0
- package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
- package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
- package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-live2d-bundle/plugin.json +81 -0
- package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
- package/plugins/unity-spine-bundle/index.js +535 -0
- package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
- package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
- package/plugins/unity-spine-bundle/lib/errors.js +79 -0
- package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
- package/plugins/unity-spine-bundle/lib/extract.js +242 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-spine-bundle/lib/png.js +424 -0
- package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
- package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
- package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-spine-bundle/plugin.json +75 -0
- package/src/server/README.md +69 -0
- package/src/server/assets/browse.mjs +56 -0
- package/src/server/assets/bundle-probe.mjs +94 -0
- package/src/server/assets/probe.mjs +184 -0
- package/src/server/core/config.mjs +173 -0
- package/src/server/core/env-num.mjs +60 -0
- package/src/server/core/env.mjs +49 -0
- package/src/server/core/fs-portable.mjs +37 -0
- package/src/server/core/log.mjs +107 -0
- package/src/server/core/paths.mjs +44 -0
- package/src/server/data/catalog.mjs +1091 -0
- package/src/server/data/entry-states.mjs +499 -0
- package/src/server/data/live2d.mjs +272 -0
- package/src/server/data/manifest-cache.mjs +58 -0
- package/src/server/data/manifest-stamp.mjs +135 -0
- package/src/server/data/model-meta.mjs +114 -0
- package/src/server/data/names.mjs +106 -0
- package/src/server/data/shape-probe-worker.mjs +33 -0
- package/src/server/data/shape-probe.mjs +626 -0
- package/src/server/data/spine.mjs +336 -0
- package/src/server/data/tree.mjs +126 -0
- package/src/server/http/atomic-json.mjs +145 -0
- package/src/server/http/background-solids.mjs +270 -0
- package/src/server/http/backgrounds.mjs +388 -0
- package/src/server/http/diag-upload.mjs +264 -0
- package/src/server/http/diag.mjs +173 -0
- package/src/server/http/ready.mjs +167 -0
- package/src/server/http/static.mjs +312 -0
- package/src/server/http/write.mjs +654 -0
- package/src/server/index.mjs +114 -0
- package/src/server/plugins/builtin-providers.json +30 -0
- package/src/server/plugins/registry.mjs +650 -0
- package/src/server/plugins/slot-catalog.mjs +286 -0
- package/src/server/routes/background-solids.mjs +56 -0
- package/src/server/routes/backgrounds.mjs +77 -0
- package/src/server/routes/config.mjs +38 -0
- package/src/server/routes/data.mjs +313 -0
- package/src/server/routes/diag.mjs +37 -0
- package/src/server/routes/entries.mjs +170 -0
- package/src/server/routes/ready.mjs +122 -0
- package/src/server/serve.mjs +396 -0
- package/src/server/unpack/capacity.mjs +217 -0
- package/src/server/unpack/mem-probe.mjs +27 -0
- package/src/server/unpack/plugin-runner.mjs +529 -0
- package/src/server/unpack/plugin-store.mjs +736 -0
- package/src/server/unpack/probe-store.mjs +73 -0
- package/src/server/unpack/routes.mjs +359 -0
- package/src/server/unpack/runtime/exec.mjs +558 -0
- package/src/server/unpack/runtime/index.mjs +141 -0
- package/src/server/unpack/runtime/node-vm.mjs +111 -0
- package/src/server/unpack/unpack-worker.mjs +105 -0
- package/src/server/unpack/worker-client.mjs +184 -0
- package/src/server/unpack/write-result.mjs +84 -0
- package/src/shared/bundle-shape.mjs +83 -0
- package/src/shared/entry-reasons.mjs +119 -0
- package/src/shared/names.mjs +45 -0
- package/src/shared/type-labels.mjs +31 -0
- package/src/web/README.md +193 -0
- package/src/web/app/anim-list.js +222 -0
- package/src/web/app/background-solids.js +150 -0
- package/src/web/app/backgrounds.js +152 -0
- package/src/web/app/camera-log.js +28 -0
- package/src/web/app/context.js +72 -0
- package/src/web/app/dir-picker.js +242 -0
- package/src/web/app/entry-report.js +71 -0
- package/src/web/app/memory-bars.js +91 -0
- package/src/web/app/memory-budget.js +228 -0
- package/src/web/app/memory-guard.js +204 -0
- package/src/web/app/model-actions.js +682 -0
- package/src/web/app/renderer-setting.js +70 -0
- package/src/web/app/root-manager.js +254 -0
- package/src/web/app/status.js +138 -0
- package/src/web/app/topbar-actions.js +357 -0
- package/src/web/app/transport-actions.js +297 -0
- package/src/web/app/ui-state.js +559 -0
- package/src/web/app/url-params.js +171 -0
- package/src/web/app/view-settings.js +186 -0
- package/src/web/components.css +524 -0
- package/src/web/debug/diag-hud.js +95 -0
- package/src/web/debug/drag.js +369 -0
- package/src/web/debug/log.js +320 -0
- package/src/web/debug/model-cmp.js +292 -0
- package/src/web/debug/panel.css +474 -0
- package/src/web/debug/panel.js +712 -0
- package/src/web/debug/race-probe.js +82 -0
- package/src/web/debug/upload.js +130 -0
- package/src/web/engine/adapters/atlas.js +47 -0
- package/src/web/engine/adapters/index.js +22 -0
- package/src/web/engine/adapters/renderer.js +223 -0
- package/src/web/engine/adapters/skeleton.js +105 -0
- package/src/web/engine/adapters/texture.js +64 -0
- package/src/web/engine/animation.js +181 -0
- package/src/web/engine/appearance.js +136 -0
- package/src/web/engine/camera-adapter.js +212 -0
- package/src/web/engine/effect-chain.js +138 -0
- package/src/web/engine/engine-state.js +120 -0
- package/src/web/engine/filters.js +224 -0
- package/src/web/engine/fit.js +197 -0
- package/src/web/engine/frame-gate.js +33 -0
- package/src/web/engine/gl-driver.js +56 -0
- package/src/web/engine/gl.js +77 -0
- package/src/web/engine/index.js +20 -0
- package/src/web/engine/loader.js +55 -0
- package/src/web/engine/model-info.js +26 -0
- package/src/web/engine/pipeline.js +200 -0
- package/src/web/engine/player.js +427 -0
- package/src/web/engine/pma/blend.js +76 -0
- package/src/web/engine/pma/detect.js +118 -0
- package/src/web/engine/pma/index.js +8 -0
- package/src/web/engine/pma/resolve.js +112 -0
- package/src/web/engine/pma/upload.js +127 -0
- package/src/web/engine/pma.js +5 -0
- package/src/web/engine/render-frame.js +93 -0
- package/src/web/engine/runtime-capabilities.js +209 -0
- package/src/web/engine/runtime.js +37 -0
- package/src/web/engine/skins.js +165 -0
- package/src/web/engine/textures.js +174 -0
- package/src/web/index.html +117 -0
- package/src/web/infra/api.js +531 -0
- package/src/web/infra/diag.js +509 -0
- package/src/web/infra/document.js +269 -0
- package/src/web/infra/dom.js +31 -0
- package/src/web/infra/events.mjs +27 -0
- package/src/web/infra/log.js +153 -0
- package/src/web/infra/mem-stats.js +112 -0
- package/src/web/infra/plugin-host-registration.mjs +285 -0
- package/src/web/infra/plugin-host-report.mjs +32 -0
- package/src/web/infra/plugin-host-unload.mjs +79 -0
- package/src/web/infra/plugin-host-unpack.mjs +899 -0
- package/src/web/infra/plugin-host-warmup.mjs +180 -0
- package/src/web/infra/plugin-host.js +628 -0
- package/src/web/infra/ready.js +461 -0
- package/src/web/infra/schedule.js +51 -0
- package/src/web/infra/shortcuts.js +129 -0
- package/src/web/infra/slot-registry.js +402 -0
- package/src/web/infra/vfs.js +247 -0
- package/src/web/live2d/bridge.js +400 -0
- package/src/web/live2d/model.js +321 -0
- package/src/web/live2d/overlay.js +142 -0
- package/src/web/live2d/pixi-app.js +111 -0
- package/src/web/live2d/playback-probe.js +92 -0
- package/src/web/live2d/probe-geometry.js +329 -0
- package/src/web/live2d/probe.js +119 -0
- package/src/web/live2d/release.js +66 -0
- package/src/web/live2d/runtime-adapter.js +289 -0
- package/src/web/live2d/stage-ctx.js +152 -0
- package/src/web/live2d/stage.js +204 -0
- package/src/web/live2d/vfs-settings.js +174 -0
- package/src/web/live2d/view.js +89 -0
- package/src/web/main.js +629 -0
- package/src/web/panel/background-list.js +373 -0
- package/src/web/panel/bg-source.js +291 -0
- package/src/web/panel/color-picker.css +199 -0
- package/src/web/panel/color-picker.js +278 -0
- package/src/web/panel/dialog.css +116 -0
- package/src/web/panel/dialog.js +173 -0
- package/src/web/panel/dom.js +35 -0
- package/src/web/panel/icons.js +58 -0
- package/src/web/panel/memory-bar.js +195 -0
- package/src/web/panel/panel-settings.js +1263 -0
- package/src/web/panel/panel.css +1304 -0
- package/src/web/panel/render-choice.js +104 -0
- package/src/web/panel/schema.js +784 -0
- package/src/web/panel/settings-io.js +291 -0
- package/src/web/panel/shell-controls.js +274 -0
- package/src/web/panel/slider.js +210 -0
- package/src/web/panel/toolbar-schema.js +111 -0
- package/src/web/panel/toolbar.js +195 -0
- package/src/web/panel/transport-picker.js +375 -0
- package/src/web/panel/transport.js +321 -0
- package/src/web/panel/view.js +553 -0
- package/src/web/plugin/unpack-worker.js +185 -0
- package/src/web/render/camera/constants.js +60 -0
- package/src/web/render/camera/index.js +123 -0
- package/src/web/render/camera/spine.js +124 -0
- package/src/web/render/capture.js +30 -0
- package/src/web/render/interaction.js +527 -0
- package/src/web/render/live2d.js +627 -0
- package/src/web/render/playback.js +47 -0
- package/src/web/render/reset-view.js +29 -0
- package/src/web/render/spine.js +386 -0
- package/src/web/render/surface-switch.js +176 -0
- package/src/web/render/surface.js +571 -0
- package/src/web/render/surfaces.js +70 -0
- package/src/web/roster/filter.js +31 -0
- package/src/web/roster/list.js +276 -0
- package/src/web/roster/persist.js +92 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +39 -0
- package/src/web/roster/type-filter.js +97 -0
- package/src/web/roster/view.js +761 -0
- package/src/web/spine/canvas-api-groups.js +267 -0
- package/src/web/spine/canvas-api.js +97 -0
- package/src/web/spine/canvas-document.js +80 -0
- package/src/web/spine/canvas-engine.js +193 -0
- package/src/web/spine/canvas-entry.js +28 -0
- package/src/web/spine/canvas-pixels.js +36 -0
- package/src/web/spine/canvas-runtime.js +127 -0
- package/src/web/spine/canvas-view.js +108 -0
- package/src/web/spine/canvas.js +7 -0
- package/src/web/spine/runtime-hint.js +73 -0
- package/src/web/spine/runtimes.js +212 -0
- package/src/web/style.css +493 -0
- package/tool/lib/bundle-probe-node.mjs +121 -0
- package/tool/lib/paths.mjs +20 -0
- package/tool/lib/runtime-guides.mjs +65 -0
- package/tool/lib/runtime-package.mjs +403 -0
- package/tool/lib/spine-meta.mjs +227 -0
- package/tool/lib/spine-motions.mjs +104 -0
- package/tool/parse-jsonc.mjs +86 -0
- package/tool/resolve-live2d-runtimes.mjs +276 -0
- package/tool/spine-motion-rules.json +101 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// plugins/unity-live2d-bundle/lib/anim-to-motion.js —— Unity AnimationClip → motion3.json
|
|
3
|
+
//
|
|
4
|
+
// 适用对象:Unity 集成版 Live2D 资源包(包内只有 AnimationClip,没有现成 motion3.json)。
|
|
5
|
+
// 原生 Cubism 包不需要本文件(包内本来就有 motion3.json,直接抽取即可)。
|
|
6
|
+
//
|
|
7
|
+
// 一、Unity 这边动画数据长什么样(本样本实测 + 与 AssetStudio 结构定义逐条对齐)
|
|
8
|
+
// AnimationClip.m_MuscleClip.m_Clip.data 里是三段之一:
|
|
9
|
+
// · StreamedClip :变曲线的关键帧流。data 是 **uint32 数组**,但里面混着 int 与 float32 的位模式:
|
|
10
|
+
// 帧 = { time: f32, keyCount: i32, keyCount × 关键帧 }
|
|
11
|
+
// 关键帧 = { index: i32, coeff: f32[4] }(coeff[2] = 出切线、coeff[3] = 值)
|
|
12
|
+
// —— 关键帧是 5 个 32 位数,这是本文件最容易看错的地方(初看像「步长 5 的计数器规律」)。
|
|
13
|
+
// · DenseClip :定频采样。m_SampleArray 长度 = m_FrameCount × m_CurveCount,
|
|
14
|
+
// 取值下标 = frame × m_CurveCount + curve,时间 = m_BeginTime + frame / m_SampleRate。
|
|
15
|
+
// · ConstantClip :恒定曲线的值,数组顺序就是绑定顺序的后半段。
|
|
16
|
+
// 三段的**曲线总数**等于 m_ClipBindingConstant.genericBindings.length,
|
|
17
|
+
// 且满足 streamed + dense + constant = bindings(本样本 34 个 clip 全部成立,已断言)。
|
|
18
|
+
//
|
|
19
|
+
// 二、曲线绑定到哪个参数
|
|
20
|
+
// genericBindings[i] = { path: u32, attribute: u32, typeID, customType, ... },两个都是 **CRC32 哈希**。
|
|
21
|
+
// 实测 crc32("Value") = 3702945584 = 本样本所有绑定的 attribute ⇒ 曲线写的是组件的 Value 字段;
|
|
22
|
+
// path 哈希即「Cubism 参数名」(如 ParamAngleX)——用候选名单(moc3 里的参数/部件名 + 物理文件里的名字)
|
|
23
|
+
// 逐个算 CRC32 反查,查不到就跳过该曲线并在统计里报数(绝不猜名字)。
|
|
24
|
+
//
|
|
25
|
+
// 三、输出 motion3.json 的取值与计数
|
|
26
|
+
// Segments 采用线性段的三元组排布:首点 (t0, v0),其后每个关键帧写成 (0, t, v),
|
|
27
|
+
// 末尾补一个 0 作为最后一段的类型占位 —— 这是运行时(Cubism 的 parse)实际的遍历方式:
|
|
28
|
+
// 第 0 次迭代取 (Segments[0], Segments[1]) 作为首点;之后每次读 Segments[l] 当段类型,
|
|
29
|
+
// 线性段再吃掉 (t, v) 两个值 ⇒ 数组形态 = [t0, v0, 0, t1, v1, 0, ...]。
|
|
30
|
+
// Meta:Duration 取 m_MuscleClip.m_StopTime(实测 = 动作时长),Fps 取 m_SampleRate,
|
|
31
|
+
// CurveCount/TotalSegmentCount/TotalPointCount 按实际产出统计。
|
|
32
|
+
//
|
|
33
|
+
// 挂载:globalThis.__ULB__.animToMotion
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
const root = globalThis; // 原 IIFE 的 root 引用(脚本级裸块内保持同名)
|
|
40
|
+
const NS = (root.__ULB__ = root.__ULB__ || {});
|
|
41
|
+
const B = NS.bytes;
|
|
42
|
+
|
|
43
|
+
const F32 = new Float32Array(1);
|
|
44
|
+
const I32 = new Int32Array(F32.buffer);
|
|
45
|
+
const U32 = new Uint32Array(F32.buffer);
|
|
46
|
+
|
|
47
|
+
/** uint32 位模式 → float(Unity 的流里 int 与 float 混用同一片 uint32 数据) */
|
|
48
|
+
function bitsToFloat(bits) {
|
|
49
|
+
U32[0] = bits >>> 0;
|
|
50
|
+
return F32[0];
|
|
51
|
+
}
|
|
52
|
+
/** f32 → int32 位模式 */
|
|
53
|
+
function floatToBits(f) {
|
|
54
|
+
F32[0] = f;
|
|
55
|
+
return I32[0];
|
|
56
|
+
}
|
|
57
|
+
/** uint32 位模式 → int32 */
|
|
58
|
+
function bitsToInt(bits) {
|
|
59
|
+
U32[0] = bits >>> 0;
|
|
60
|
+
return I32[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 解 StreamedClip。
|
|
65
|
+
* @param {Array<number>} data uint32 数组(data 字段原样)
|
|
66
|
+
* @returns {{frames:Array<{time:number,keys:Array<{index:number,value:number,outSlope:number,inSlope:number}>}>, curveFrames:Map<number,Array<{t:number,v:number}>>}}
|
|
67
|
+
*/
|
|
68
|
+
function decodeStreamedClip(data, opts) {
|
|
69
|
+
const frames = [];
|
|
70
|
+
let i = 0;
|
|
71
|
+
let guard = 0;
|
|
72
|
+
while (i + 1 < data.length) {
|
|
73
|
+
if (++guard > 1e6) throw NS.errors.fail('ULB_ERR_STREAMED_CLIP_INVALID', 'StreamedClip 帧数异常,疑似数据布局不符');
|
|
74
|
+
const time = bitsToFloat(data[i]);
|
|
75
|
+
const numKeys = bitsToInt(data[i + 1]);
|
|
76
|
+
if (numKeys < 0 || numKeys > 4096) {
|
|
77
|
+
throw NS.errors.fail('ULB_ERR_STREAMED_CLIP_INVALID', `StreamedClip 帧的关键帧数非法:${numKeys}(偏移 ${i})`);
|
|
78
|
+
}
|
|
79
|
+
i += 2;
|
|
80
|
+
const keys = [];
|
|
81
|
+
for (let k = 0; k < numKeys; k++) {
|
|
82
|
+
if (i + 4 >= data.length + 1) throw NS.errors.fail('ULB_ERR_STREAMED_CLIP_INVALID', 'StreamedClip 数据在关键帧中间结束');
|
|
83
|
+
const index = bitsToInt(data[i]);
|
|
84
|
+
const c0 = bitsToFloat(data[i + 1]);
|
|
85
|
+
const c1 = bitsToFloat(data[i + 2]);
|
|
86
|
+
const outSlope = bitsToFloat(data[i + 3]);
|
|
87
|
+
const value = bitsToFloat(data[i + 4]);
|
|
88
|
+
keys.push({ index, coeff: [c0, c1, outSlope, value], value, outSlope, inSlope: 0 });
|
|
89
|
+
i += 5;
|
|
90
|
+
}
|
|
91
|
+
frames.push({ time, keys });
|
|
92
|
+
}
|
|
93
|
+
// 按曲线整理:关键帧只在部分帧出现(稀疏),这里按出现顺序收集并**按时间区间过滤**。
|
|
94
|
+
// 哨兵帧:首帧 time = -FLT_MAX、末帧 time = +Inf(都是有限/无穷的极端值,不是真时间轴上的帧),
|
|
95
|
+
// 它们只用于给相邻帧算切线。区间取 [m_StartTime - 1ms, m_StopTime + 1ms],
|
|
96
|
+
// 既剔除哨兵帧,又保留真正的起始帧(time = 0)。
|
|
97
|
+
const t0 = (opts && opts.timeRange ? opts.timeRange[0] : 0);
|
|
98
|
+
const t1 = (opts && opts.timeRange ? opts.timeRange[1] : Number.MAX_VALUE);
|
|
99
|
+
const inRange = (t) => Number.isFinite(t) && t >= t0 - 1e-3 && t <= t1 + 1e-3;
|
|
100
|
+
const curveFrames = new Map();
|
|
101
|
+
for (const fr of frames) {
|
|
102
|
+
if (!inRange(fr.time)) continue;
|
|
103
|
+
for (const k of fr.keys) {
|
|
104
|
+
if (!curveFrames.has(k.index)) curveFrames.set(k.index, []);
|
|
105
|
+
curveFrames.get(k.index).push({ t: fr.time, v: k.value });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { frames, curveFrames };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 解 DenseClip → 每条曲线的采样点 */
|
|
112
|
+
function decodeDenseClip(dense) {
|
|
113
|
+
const out = new Map();
|
|
114
|
+
const count = Number(dense.m_CurveCount || 0);
|
|
115
|
+
const frames = Number(dense.m_FrameCount || 0);
|
|
116
|
+
const sample = dense.m_SampleArray || [];
|
|
117
|
+
if (!count || !frames) return out;
|
|
118
|
+
const rate = Number(dense.m_SampleRate) || 60;
|
|
119
|
+
const begin = Number(dense.m_BeginTime) || 0;
|
|
120
|
+
for (let c = 0; c < count; c++) {
|
|
121
|
+
const pts = [];
|
|
122
|
+
for (let f = 0; f < frames; f++) {
|
|
123
|
+
const idx = f * count + c;
|
|
124
|
+
if (idx >= sample.length) break;
|
|
125
|
+
pts.push({ t: begin + f / rate, v: Number(sample[idx]) });
|
|
126
|
+
}
|
|
127
|
+
out.set(c, pts);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 用候选名单反查 CRC32 哈希 → 名字。
|
|
134
|
+
*
|
|
135
|
+
* 路径前缀:本样本实测绑定路径是 **`Parameters/<参数名>`**(416/416 命中;不带前缀一个都不命中),
|
|
136
|
+
* 这是 Cubism 的 Unity 集成对「模型参数」的路径约定。故同时登记「带前缀」与「裸名」两种形态,
|
|
137
|
+
* 命中后统一返回**裸参数名**——运行时的 motion3 曲线用参数 ID 查找,ID 就是裸名。
|
|
138
|
+
*
|
|
139
|
+
* @param {string[]} candidates 裸名列表(moc3 里的参数/部件名等)
|
|
140
|
+
* @param {string[]} [prefixes] 路径前缀(默认 ['Parameters/', ''])
|
|
141
|
+
*/
|
|
142
|
+
function buildHashLookup(candidates, prefixes) {
|
|
143
|
+
const pre = prefixes || ['Parameters/', ''];
|
|
144
|
+
const map = new Map();
|
|
145
|
+
for (const name of candidates) {
|
|
146
|
+
for (const p of pre) {
|
|
147
|
+
const h = B.crc32(p + name);
|
|
148
|
+
if (!map.has(h)) map.set(h, name);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return map;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 把一条曲线的关键帧转成 motion3 的 Segments(全线性段)。
|
|
156
|
+
*
|
|
157
|
+
* 形态(与原生 motion3 完全一致,实测原生 idle.motion3.json 逐条核对):
|
|
158
|
+
* [t0, v0, 0, t1, v1, 0, t2, v2, …]
|
|
159
|
+
* —— 开头是首点 (t0,v0),其后**每段**是三元组 (段类型, t, v):0=线性,1=贝塞尔,2/3=阶跃。
|
|
160
|
+
* 运行时按「先吃 2 个,再按段类型吃 3/7/3/3 个」循环消费(index.min.js 的 ee.parse),
|
|
161
|
+
* 因此**末尾不能有任何多余值**:多一个 0 就会被当成"再来一段线性"而多消费 3 个值,
|
|
162
|
+
* 越过预分配的 segments 数组(实测报 TypeError: Cannot set properties of undefined (setting 'basePointIndex'))。
|
|
163
|
+
* 长度恒为 2 + 3×(点数-1) = 3×点数 - 1。
|
|
164
|
+
*
|
|
165
|
+
* 只做**一一对应**的搬运:不插值、不抽稀、不改值 —— Unity 的关键帧原样进 motion3。
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* 按**运行时的真实消费过程**走一遍 Segments,返回它会产生多少段、多少个点。
|
|
169
|
+
*
|
|
170
|
+
* 运行时(index.min.js 的 ee.parse)先按 Meta.TotalSegmentCount **预分配** segments 数组:
|
|
171
|
+
* this._motionData.segments = Array.from({length: getMotionTotalSegmentCount()})
|
|
172
|
+
* 然后每条曲线每消费一段就写一个 segments[o] 并 o++。所以:
|
|
173
|
+
* · Meta.TotalSegmentCount 必须 ≥ Σ(每曲线的消费轮数),否则写到数组外 —— 运行时报
|
|
174
|
+
* TypeError: Cannot set properties of undefined (setting 'basePointIndex')(实测就是这个)。
|
|
175
|
+
* · 原生导出件(abeikelongbi_3/motions/*.motion3.json 实测 14 个)一律 = Σ轮数 + 1,
|
|
176
|
+
* 这里跟它保持一致(多留 1 个)。
|
|
177
|
+
* 轮数规则:首轮先读首点 (t,v) 吃掉 2 个值,**同一轮**再读一个段类型并按类型吃掉 3(线性/阶跃)
|
|
178
|
+
* 或 7(贝塞尔)个值;类型位置读到 undefined 时这一轮不消费任何值(单点曲线就是这种情况)。
|
|
179
|
+
*/
|
|
180
|
+
function runtimeLayout(seg) {
|
|
181
|
+
let l = 0;
|
|
182
|
+
let iterations = 0;
|
|
183
|
+
let guard = 0;
|
|
184
|
+
while (l < seg.length && guard++ < seg.length + 4) {
|
|
185
|
+
iterations++;
|
|
186
|
+
if (l === 0) l += 2;
|
|
187
|
+
const t = seg[l];
|
|
188
|
+
if (t === 1) l += 7;
|
|
189
|
+
else if (t === 0 || t === 2 || t === 3) l += 3;
|
|
190
|
+
// 其它类型(含 undefined)不消费 —— 与运行时 switch 无匹配分支的行为一致
|
|
191
|
+
}
|
|
192
|
+
return { iterations, points: iterations + 1, ended: l };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function consumesExactly(seg) {
|
|
196
|
+
const r = runtimeLayout(seg);
|
|
197
|
+
return r.ended === seg.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function toLinearSegments(points) {
|
|
201
|
+
const seg = [];
|
|
202
|
+
let count = 0;
|
|
203
|
+
for (const p of points) {
|
|
204
|
+
if (!Number.isFinite(p.t) || !Number.isFinite(p.v)) continue;
|
|
205
|
+
if (count === 0) seg.push(p.t, p.v);
|
|
206
|
+
else seg.push(0, p.t, p.v); // 0 = 线性段
|
|
207
|
+
count++;
|
|
208
|
+
}
|
|
209
|
+
return { segments: seg, points: count };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 把一个 AnimationClip 转成 motion3.json 对象。
|
|
214
|
+
* @param {object} sf serialized 解析结果
|
|
215
|
+
* @param {object} clipObj AnimationClip 对象条目
|
|
216
|
+
* @param {object} opts { hashLookup:Map, loop:boolean, fadeIn:number, fadeOut:number, paramWhitelist?:Set, log?:function }
|
|
217
|
+
* paramWhitelist:**moc3 参数表**里的参数 Id 集合(来自 Cubism Core 的 parameters.ids)。
|
|
218
|
+
* 名字靠字符串表反查出来的结果里可能混进部件名/绘制名之类"不是参数"的名字,
|
|
219
|
+
* 那种 Id 写进 motion3 后运行时查不到参数对象而抛异常 —— 故这里以参数表为准,
|
|
220
|
+
* 只输出 Id 确实在表内的曲线,其余计入 skippedNotParameter(如实报数,不猜)。
|
|
221
|
+
* @returns {{json:object, stats:object}}
|
|
222
|
+
*/
|
|
223
|
+
/** 读 clip 头部数据并校验「曲线数 = 绑定数」,返回各数据源与计数。 */
|
|
224
|
+
function readClipSources(f) {
|
|
225
|
+
const data = f.m_MuscleClip.m_Clip.data;
|
|
226
|
+
const streamed = data.m_StreamedClip;
|
|
227
|
+
const dense = data.m_DenseClip;
|
|
228
|
+
const constant = data.m_ConstantClip || { data: [] };
|
|
229
|
+
const bindings = f.m_ClipBindingConstant.genericBindings || [];
|
|
230
|
+
const streamedCurveCount = Number(streamed.curveCount || 0);
|
|
231
|
+
const denseCurveCount = Number(dense.m_CurveCount || 0);
|
|
232
|
+
const constantData = (constant.data || []).map(Number);
|
|
233
|
+
if (streamedCurveCount + denseCurveCount + constantData.length !== bindings.length) {
|
|
234
|
+
throw NS.errors.fail('ULB_ERR_CURVE_BINDING_MISMATCH', `曲线数与绑定数不一致:streamed ${streamedCurveCount}`
|
|
235
|
+
+ ` + dense ${denseCurveCount} + constant ${constantData.length} != bindings ${bindings.length}`);
|
|
236
|
+
}
|
|
237
|
+
return { streamed, dense, bindings, streamedCurveCount, denseCurveCount, constantData };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 曲线统计初值(口径:分母 = 绑定条数,= 每条绑定最多一条曲线)。 */
|
|
241
|
+
function createCurveStats(f, src) {
|
|
242
|
+
return {
|
|
243
|
+
name: f.m_Name,
|
|
244
|
+
duration: Number(f.m_MuscleClip.m_StopTime || 0),
|
|
245
|
+
startTime: Number(f.m_MuscleClip.m_StartTime || 0),
|
|
246
|
+
fps: Number(f.m_SampleRate || 0),
|
|
247
|
+
wrapMode: Number(f.m_WrapMode || 0),
|
|
248
|
+
bindings: src.bindings.length,
|
|
249
|
+
streamedCurves: src.streamedCurveCount,
|
|
250
|
+
denseCurves: src.denseCurveCount,
|
|
251
|
+
constantCurves: src.constantData.length,
|
|
252
|
+
curvesTotal: 0,
|
|
253
|
+
emitted: 0,
|
|
254
|
+
skippedUnknownBinding: 0, // 路径哈希反查不到名字
|
|
255
|
+
skippedNotParameter: 0, // 反查到名字但不在 moc3 参数表里
|
|
256
|
+
skippedEmpty: 0,
|
|
257
|
+
expandedSinglePoint: 0, // 单点曲线补成两点(否则运行时求值每帧抛 undefined.time)
|
|
258
|
+
invalidSegments: 0, // 按运行时消费规则会越界的曲线数(必须为 0)
|
|
259
|
+
totalPoints: 0,
|
|
260
|
+
totalSegments: 0,
|
|
261
|
+
unknownHashes: [],
|
|
262
|
+
notParameterIds: [],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 逐条绑定取点:按索引分流到 streamed / dense / constant 三个数据源,
|
|
268
|
+
* 再统一排序、单点补两点、切成线性段,并累加统计。
|
|
269
|
+
* @returns {Array<{Target:string, Id:string, Segments:number[]}>} 曲线数组
|
|
270
|
+
*/
|
|
271
|
+
function collectCurves(f, src, decoded, denseMap, hashLookup, whitelist, stats) {
|
|
272
|
+
const { bindings, streamedCurveCount, denseCurveCount, constantData } = src;
|
|
273
|
+
const curves = [];
|
|
274
|
+
for (let i = 0; i < bindings.length; i++) {
|
|
275
|
+
const b = bindings[i];
|
|
276
|
+
stats.curvesTotal++;
|
|
277
|
+
const name = hashLookup.get(b.path >>> 0) || null;
|
|
278
|
+
if (!name) {
|
|
279
|
+
stats.skippedUnknownBinding++;
|
|
280
|
+
if (stats.unknownHashes.length < 12) stats.unknownHashes.push(b.path >>> 0);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (whitelist && !whitelist.has(name)) {
|
|
284
|
+
// 名字是从字符串表解出来的,但不在这份 moc3 的参数表里 —— 运行时不认识,跳过并计入
|
|
285
|
+
stats.skippedNotParameter++;
|
|
286
|
+
if (stats.notParameterIds.length < 12) stats.notParameterIds.push(name);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
let points;
|
|
290
|
+
if (i < streamedCurveCount) {
|
|
291
|
+
points = decoded.curveFrames.get(i) || [];
|
|
292
|
+
} else if (i < streamedCurveCount + denseCurveCount) {
|
|
293
|
+
points = denseMap.get(i - streamedCurveCount) || [];
|
|
294
|
+
} else {
|
|
295
|
+
const v = constantData[i - streamedCurveCount - denseCurveCount];
|
|
296
|
+
points = [{ t: 0, v: Number(v) }];
|
|
297
|
+
}
|
|
298
|
+
if (!points.length) { stats.skippedEmpty++; continue; }
|
|
299
|
+
points.sort((a, b2) => a.t - b2.t);
|
|
300
|
+
// 单点曲线补成两点:运行时的求值器对**每个段**都要取"后继点"
|
|
301
|
+
// (points[basePointIndex + 1]);只有 1 个点的曲线其唯一段没有后继点,
|
|
302
|
+
// 求值时就抛 Cannot read properties of undefined (reading 'time')——实测
|
|
303
|
+
// aimierbeierding_2 有 129 条这样的曲线(来自 m_ConstantClip 的常量),横跨全部 14 个动作。
|
|
304
|
+
// 补成 (t0,v) → (StopTime,v):语义等价(该参数在这段时长里保持常量),运行时可正常求值。
|
|
305
|
+
if (points.length === 1) {
|
|
306
|
+
const t0p = points[0].t;
|
|
307
|
+
const endT = stats.duration > t0p ? stats.duration : t0p + 1e-3;
|
|
308
|
+
points.push({ t: endT, v: points[0].v });
|
|
309
|
+
stats.expandedSinglePoint++;
|
|
310
|
+
}
|
|
311
|
+
const built = toLinearSegments(points);
|
|
312
|
+
// 结构自检:按运行时消费规则走一遍,必须**正好**停在数组末尾(末尾多一个值会被当成新段而多消费)
|
|
313
|
+
const layout = runtimeLayout(built.segments);
|
|
314
|
+
if (layout.ended !== built.segments.length) stats.invalidSegments++;
|
|
315
|
+
curves.push({ Target: 'Parameter', Id: name, Segments: built.segments });
|
|
316
|
+
stats.emitted++;
|
|
317
|
+
// 计数按**运行时口径**:段数 = 消费轮数(Meta.TotalSegmentCount 要为它预留空间),
|
|
318
|
+
// 点数 = 轮数 + 1(运行时每条曲线的 points 数组实际长度)
|
|
319
|
+
stats.totalPoints += layout.points;
|
|
320
|
+
stats.totalSegments += layout.iterations;
|
|
321
|
+
}
|
|
322
|
+
return curves;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function clipToMotion(sf, clipObj, opts) {
|
|
326
|
+
const o = opts || {};
|
|
327
|
+
const f = NS.typetree.readObject(sf, clipObj);
|
|
328
|
+
const src = readClipSources(f);
|
|
329
|
+
const dense = src.dense;
|
|
330
|
+
const decoded = decodeStreamedClip(src.streamed ? src.streamed.data || [] : [], {
|
|
331
|
+
timeRange: [Number(f.m_MuscleClip.m_StartTime || 0), Number(f.m_MuscleClip.m_StopTime || 0)],
|
|
332
|
+
});
|
|
333
|
+
const denseMap = decodeDenseClip(dense);
|
|
334
|
+
const hashLookup = o.hashLookup || new Map();
|
|
335
|
+
// 参数白名单:只认 moc3 参数表里的 Id(未提供时退化为不校验,并记录这一事实)
|
|
336
|
+
const whitelist = o.paramWhitelist instanceof Set ? o.paramWhitelist : null;
|
|
337
|
+
const stats = createCurveStats(f, src);
|
|
338
|
+
const curves = collectCurves(f, src, decoded, denseMap, hashLookup, whitelist, stats);
|
|
339
|
+
|
|
340
|
+
const duration = stats.duration || (stats.fps ? (dense.m_FrameCount || 0) / stats.fps : 0);
|
|
341
|
+
const json = {
|
|
342
|
+
Version: 3,
|
|
343
|
+
Meta: {
|
|
344
|
+
Duration: Number(duration.toFixed(6)),
|
|
345
|
+
Loop: !!o.loop,
|
|
346
|
+
AreBeziersRestricted: false,
|
|
347
|
+
CurveCount: curves.length,
|
|
348
|
+
// +1:运行时按这个数预分配 segments 数组,原生导出件也一律多留 1(实测 14 个原生 motion3 全部如此)
|
|
349
|
+
TotalSegmentCount: stats.totalSegments + 1,
|
|
350
|
+
TotalPointCount: stats.totalPoints,
|
|
351
|
+
Fps: stats.fps,
|
|
352
|
+
FadeInTime: o.fadeIn === undefined ? 0.5 : o.fadeIn,
|
|
353
|
+
FadeOutTime: o.fadeOut === undefined ? 0.5 : o.fadeOut,
|
|
354
|
+
},
|
|
355
|
+
Curves: curves,
|
|
356
|
+
};
|
|
357
|
+
if (o.log) o.log(f.m_Name, stats);
|
|
358
|
+
return { json, stats };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 批量转换一个资源包里的全部 AnimationClip。
|
|
363
|
+
* @param {object} sf
|
|
364
|
+
* @param {string[]} nameCandidates CRC32 候选名单(参数名/部件名)
|
|
365
|
+
* @param {object} opts { isLoop(name):boolean, fadeIn, fadeOut, paramWhitelist?:Set, log }
|
|
366
|
+
* @returns {{motions:Array, hashLookup:Map, dropped:Array}}
|
|
367
|
+
* motions:过滤后仍有曲线的动作;dropped:过滤后曲线为空而被丢弃的动作(含各自统计,便于报数)
|
|
368
|
+
*/
|
|
369
|
+
function clipsToMotions(sf, nameCandidates, opts) {
|
|
370
|
+
const o = opts || {};
|
|
371
|
+
const hashLookup = buildHashLookup(nameCandidates || []);
|
|
372
|
+
const out = [];
|
|
373
|
+
const dropped = [];
|
|
374
|
+
for (const obj of NS.serialized.byClass(sf, 74)) {
|
|
375
|
+
const probe = NS.typetree.readObject(sf, obj);
|
|
376
|
+
const name = probe.m_Name;
|
|
377
|
+
const { json, stats } = clipToMotion(sf, obj, {
|
|
378
|
+
hashLookup,
|
|
379
|
+
paramWhitelist: o.paramWhitelist,
|
|
380
|
+
loop: o.isLoop ? !!o.isLoop(name) : false,
|
|
381
|
+
fadeIn: o.fadeIn,
|
|
382
|
+
fadeOut: o.fadeOut,
|
|
383
|
+
});
|
|
384
|
+
// 过滤后没有任何曲线的动作**不产出**:白名单(moc3 参数表)会剔掉本模型不存在的参数,
|
|
385
|
+
// 若某动作的曲线全被剔掉,它就是个空动作——留在列表里只会是一个点开即失败/无效果的条目。
|
|
386
|
+
// 如实计入 dropped(含该动作自己的统计),由调用方报数。
|
|
387
|
+
if (!json.Curves.length) {
|
|
388
|
+
dropped.push({ name, stats });
|
|
389
|
+
if (o.log) o.log(name, stats);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
out.push({ name, json, stats });
|
|
393
|
+
if (o.log) o.log(name, stats);
|
|
394
|
+
}
|
|
395
|
+
return { motions: out, hashLookup, dropped };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
NS.animToMotion = {
|
|
399
|
+
decodeStreamedClip, decodeDenseClip, clipToMotion, clipsToMotions,
|
|
400
|
+
buildHashLookup, toLinearSegments, bitsToFloat, floatToBits, bitsToInt,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// lib/bundle-probe.js —— 资源包「装载前预检」(只做便宜的事)
|
|
3
|
+
//
|
|
4
|
+
// 目的:不装载、不解码纹理像素,就能回答"这个资源包能不能装 / 装起来会缺什么"。
|
|
5
|
+
// 预检内容:容器头 → 对象表 → moc3/physics3 存在性 → 纹理格式是否可解码 → 动画片段数。
|
|
6
|
+
//
|
|
7
|
+
// 为什么放在 lib 而不是插件/服务端各写一份:
|
|
8
|
+
// 浏览器侧(插件的 probe)与 Node 侧(/api/bundle/probe)必须给出**同一口径**的结论,
|
|
9
|
+
// 否则"预检说能装、装载却失败"这类漂移会反复出现。这里只依赖 NS.* 里已有的库,
|
|
10
|
+
// 两端各自加载同一份文件即可。
|
|
11
|
+
//
|
|
12
|
+
// 挂载:globalThis.__ULB__.bundleProbe = { run, formatSupport }
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
const NS = (globalThis.__ULB__ = globalThis.__ULB__ || {});
|
|
19
|
+
|
|
20
|
+
// 已知但暂未实现的格式:预检要让调用方看到"ASTC"而不是"未知格式 50",
|
|
21
|
+
// 这样才能据此判断"是这个资源用了新压缩格式"而不是"文件坏了"。
|
|
22
|
+
const KNOWN_UNSUPPORTED = {
|
|
23
|
+
24: 'BC6H', 25: 'BC7',
|
|
24
|
+
30: 'PVRTC_RGB2', 31: 'PVRTC_RGBA2', 32: 'PVRTC_RGB4', 33: 'PVRTC_RGBA4',
|
|
25
|
+
50: 'ASTC_RGBA', 51: 'ASTC_RGBA_HDR', 52: 'ASTC_RGB', 53: 'ASTC_RGB_HDR',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 查某个 Unity TextureFormat 是否**可解码**。
|
|
30
|
+
*
|
|
31
|
+
* 判据只有一个:`texdecode.canDecode(fmt)`(内部只认 DECODERS 表)。
|
|
32
|
+
* 为什么不再用 `!!FORMATS[fmt]`:FORMATS 是格式字典(名称/尺寸/block),含 48 ETC2_RGBA1
|
|
33
|
+
* 这种"有条目但从未实现解码"的格式 ⇒ 预检会报"支持"、真正解包才抛 ULB_ERR_TEX_DECODE,
|
|
34
|
+
* 同一件事两套判据。改名/尺寸仍走 FORMATS,但"能不能解"不再由它决定。
|
|
35
|
+
*/
|
|
36
|
+
function formatSupport(fmt) {
|
|
37
|
+
const table = NS.texdecode && NS.texdecode.FORMATS ? NS.texdecode.FORMATS : null;
|
|
38
|
+
const entry = table ? table[fmt] : null;
|
|
39
|
+
const raw = NS.texdecode && NS.texdecode.formatName ? NS.texdecode.formatName(fmt) : `未知格式 ${fmt}`;
|
|
40
|
+
const name = entry ? raw : (KNOWN_UNSUPPORTED[fmt] || raw);
|
|
41
|
+
const canDecode = NS.texdecode && typeof NS.texdecode.canDecode === 'function'
|
|
42
|
+
? NS.texdecode.canDecode(fmt)
|
|
43
|
+
: false; // 缺 canDecode 时按"不可解码"处理:宁可早拒绝,也不要报"支持"却在解包时炸
|
|
44
|
+
return { supported: canDecode, formatName: name };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 从序列化文件里按类名统计对象数(避免为了数动作而跑一遍动画转换) */
|
|
48
|
+
function countByClass(sf, className) {
|
|
49
|
+
try {
|
|
50
|
+
const stats = NS.serialized.typeStats(sf);
|
|
51
|
+
if (!stats) return null;
|
|
52
|
+
for (const k of Object.keys(stats)) {
|
|
53
|
+
if (k.toLowerCase() === String(className).toLowerCase()) return stats[k];
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Cubism 2.1 运行时数据 .moc 的魔数:**小写三字节** 'm' 'o' 'c',紧随其后的第 4 字节是
|
|
62
|
+
// **文件版本号**(不是第四位魔数)。取证来源:Cubism 2 官方运行时 live2d.min.js 的
|
|
63
|
+
// loadModel() 里逐字节校验 `109 && 111 && 99`,随后把下一字节当 version 与 SDK 版本比较。
|
|
64
|
+
// ⇒ 判据**不能**写成 "moc\0"(第 4 字节是版本号,通常非 0)。
|
|
65
|
+
const CUBISM2_MOC_MAGIC = [0x6d, 0x6f, 0x63];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 在这个包里找 Cubism 2.1 的 .moc 运行时数据(**只判定存在性,不解析**)。
|
|
69
|
+
*
|
|
70
|
+
* 为什么要它:.moc 是 Cubism 2.1 的模型二进制,Cubism 3+ 的 SDK/运行时**完全不支持**它
|
|
71
|
+
* (官方帮助文档明说不支持 .moc/.mtn)。这类包既没有 moc3、也没有 .skel,两个 asset-provider
|
|
72
|
+
* 都不会认领 ⇒ 用户在界面上只看到一句"插件判定不处理该文件",无法判断是自己资源有问题、
|
|
73
|
+
* 还是格式超出支持范围。本函数就是为了让上层能说清"这是什么、为什么不行、你能做什么"。
|
|
74
|
+
*
|
|
75
|
+
* 判据(**宁缺勿错**,三条全满足才算命中):
|
|
76
|
+
* ① MonoBehaviour(class 114)的字节里出现 'moc' 魔数;
|
|
77
|
+
* ② 魔数前 4 字节(u32)是有意义的长度 L,且 `魔数起点 + L === 对象末尾`
|
|
78
|
+
* —— 与 findMoc3 同款自洽校验,把"随机字节里凑巧出现 moc"挡掉;
|
|
79
|
+
* ③ 魔数后第 4 字节是本文件的版本号,落在合理区间 1..16。
|
|
80
|
+
* 扩展名不参与判定(只用来在文案里报出文件名,供用户核对)。
|
|
81
|
+
*
|
|
82
|
+
* @param {object} sf SerializedFile(NS.serialized.parse 的结果)
|
|
83
|
+
* @returns {{count:number, names:string[], sample:{object:object, magicOffset:number, version:number, bytes:number}|null}}
|
|
84
|
+
*/
|
|
85
|
+
function findCubism2Moc(sf) {
|
|
86
|
+
const out = { count: 0, names: [], sample: null };
|
|
87
|
+
if (!sf || !NS.serialized || !NS.extract) return out;
|
|
88
|
+
let monos;
|
|
89
|
+
try {
|
|
90
|
+
monos = NS.serialized.byClass(sf, 114);
|
|
91
|
+
} catch {
|
|
92
|
+
return out; // 对象表读不出来就当没发现(宁可漏报,也不把别的包误判成 Cubism 2)
|
|
93
|
+
}
|
|
94
|
+
// 第一路(主判据):MonoBehaviour 内嵌字节里的魔数 + 前置长度自洽 + 版本合理
|
|
95
|
+
for (const obj of monos) {
|
|
96
|
+
const bytes = sf.bytes.subarray(obj.dataOffset, obj.dataOffset + obj.byteSize);
|
|
97
|
+
let from = 0;
|
|
98
|
+
for (;;) {
|
|
99
|
+
const at = NS.extract.indexOfMagic(bytes, CUBISM2_MOC_MAGIC, from);
|
|
100
|
+
if (at < 0) break;
|
|
101
|
+
from = at + 1;
|
|
102
|
+
// ② 前置长度自洽:魔数前 4 字节是小端长度,且「魔数起点 + L」恰好到对象末尾
|
|
103
|
+
// (与 findMoc3 同款;这一条把"随机字节里凑巧出现 moc"挡掉)
|
|
104
|
+
if (at < 4) continue;
|
|
105
|
+
const len = bytes[at - 4] | (bytes[at - 3] << 8) | (bytes[at - 2] << 16) | (bytes[at - 1] << 24);
|
|
106
|
+
if (!(len > 16) || at + len !== bytes.length) continue;
|
|
107
|
+
// ③ 版本号合理(Cubism 2.1 的 .moc 版本是个小整数,见魔数注释里的取证)
|
|
108
|
+
const end = at + 3;
|
|
109
|
+
if (end >= bytes.length) continue;
|
|
110
|
+
const version = bytes[end];
|
|
111
|
+
if (version < 1 || version > 16) continue;
|
|
112
|
+
out.count += 1;
|
|
113
|
+
const name = readObjectName(sf, obj);
|
|
114
|
+
if (name) out.names.push(name);
|
|
115
|
+
if (!out.sample) out.sample = { object: obj, magicOffset: at, version, bytes: len, by: 'magic' };
|
|
116
|
+
break; // 一个对象里只认第一处(同一个 .moc 不会被切成两段)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 第二路(兜底,只作辅助):TextAsset 的名字以 .moc 结尾。
|
|
120
|
+
// 为什么要这一路:真 .moc 在包里的形态**没有样本可验**(当前资源 0 个)。
|
|
121
|
+
// 若它是以 TextAsset/独立文件形式存在(而非仿真 moc3 的内嵌 TypelessData),
|
|
122
|
+
// 就不存在"前置 u32 长度",第一路会漏报。此时扩展名是唯一线索。
|
|
123
|
+
// 安全性:本函数只在 run() 里"已确认没有 moc3"之后才被调用,所以不会把 moc3 包误判成 Cubism 2。
|
|
124
|
+
if (out.count === 0) {
|
|
125
|
+
try {
|
|
126
|
+
for (const ta of NS.extract.findTextAssets(sf)) {
|
|
127
|
+
const nm = ta && ta.name ? String(ta.name) : '';
|
|
128
|
+
if (!/\.moc$/i.test(nm)) continue;
|
|
129
|
+
out.count += 1;
|
|
130
|
+
out.names.push(nm);
|
|
131
|
+
if (!out.sample) out.sample = { object: ta.object, magicOffset: -1, version: null, bytes: ta.bytes ? ta.bytes.length : null, by: 'name' };
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
} catch { /* 类型树读不出来就不兜底,不影响主判据 */ }
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 读对象名(读不出来返回空串;名字只用于文案,不参与判定) */
|
|
140
|
+
function readObjectName(sf, obj) {
|
|
141
|
+
try {
|
|
142
|
+
const fields = NS.typetree ? NS.typetree.readObject(sf, obj) : null;
|
|
143
|
+
return (fields && (fields.m_Name || fields.m_PathName)) || '';
|
|
144
|
+
} catch {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 预检一个资源包。
|
|
151
|
+
* @param {Uint8Array} bytes 资源包字节
|
|
152
|
+
* @returns {object} { ok, code?, error?, container?, objects?, moc3?, physics3?, textures?, motions? }
|
|
153
|
+
*/
|
|
154
|
+
function run(bytes) {
|
|
155
|
+
const E = NS.errors;
|
|
156
|
+
if (!bytes || !bytes.length) {
|
|
157
|
+
return { ok: false, code: E.CODES.NO_FILE, error: '资源包为空或不存在' };
|
|
158
|
+
}
|
|
159
|
+
// 容器头:只看签名,不合格直接定性(不必进解析)
|
|
160
|
+
let sig = '';
|
|
161
|
+
for (let i = 0; i < Math.min(7, bytes.length); i++) sig += String.fromCharCode(bytes[i]);
|
|
162
|
+
if (sig !== 'UnityFS') {
|
|
163
|
+
const shown = sig.replace(/[^\x20-\x7e]/g, '?');
|
|
164
|
+
return { ok: false, code: E.CODES.NOT_UNITYFS, error: `不是 UnityFS 资源包(signature="${shown}")` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let parsed;
|
|
168
|
+
let sf;
|
|
169
|
+
let got;
|
|
170
|
+
try {
|
|
171
|
+
parsed = NS.unityfs.parse(bytes);
|
|
172
|
+
// 结构判据(不是名字前缀):CAB- + 32 位十六进制、无后缀。
|
|
173
|
+
// .resS / .resource 也以 CAB- 开头,但都不是 serialized file(后者是 FSB5 音频银行)。
|
|
174
|
+
const cab = parsed.nodes.find((n) => /^CAB-[0-9a-f]{32}$/i.test(n.path));
|
|
175
|
+
if (!cab) {
|
|
176
|
+
return { ok: false, code: E.CODES.NO_CAB, error: '资源包里没有 CAB-* 节点(不是资产包)' };
|
|
177
|
+
}
|
|
178
|
+
const resSNode = parsed.nodes.find((n) => /\.resS$/.test(n.path));
|
|
179
|
+
const cabBytes = NS.unityfs.nodeBytes(parsed, cab.path);
|
|
180
|
+
const resSBytes = resSNode ? NS.unityfs.nodeBytes(parsed, resSNode.path) : null;
|
|
181
|
+
sf = NS.serialized.parse(cabBytes);
|
|
182
|
+
got = NS.extract.extractAll({ cabBytes, resSBytes, sf });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return { ok: false, code: E.CODES.CONTAINER_PARSE, error: e.message };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 纹理:只判定格式是否可解码,**不解码像素**(解码是最贵的一步,预检故意跳过)
|
|
188
|
+
const textures = (got.textures || []).map((t) => {
|
|
189
|
+
const s = formatSupport(t.format);
|
|
190
|
+
return {
|
|
191
|
+
name: t.name,
|
|
192
|
+
width: t.width,
|
|
193
|
+
height: t.height,
|
|
194
|
+
format: t.format,
|
|
195
|
+
formatName: s.formatName,
|
|
196
|
+
supported: s.supported,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
const unsupported = textures.filter((t) => !t.supported).map((t) => t.formatName);
|
|
200
|
+
|
|
201
|
+
// Cubism 2.1 的 .moc 存在性:**只有"没有 moc3"时才有意义**(有 moc3 的包一律按 moc3 走,
|
|
202
|
+
// 哪怕字节里凑巧有 'moc' 片段也不该被说成 Cubism 2 —— 这是判据可靠性的关键)
|
|
203
|
+
const moc3Hit = !!got.moc3;
|
|
204
|
+
const cubism2 = moc3Hit ? { count: 0, names: [], sample: null } : findCubism2Moc(sf);
|
|
205
|
+
// 结构化"为什么没人认领",交给上层(宿主/调试日志)原样透出给用户
|
|
206
|
+
let reject = null;
|
|
207
|
+
if (!moc3Hit && cubism2.count > 0) {
|
|
208
|
+
const ver = cubism2.sample ? cubism2.sample.version : null;
|
|
209
|
+
const fileHint = cubism2.names.length ? `(${cubism2.names.join('、')})` : '';
|
|
210
|
+
reject = {
|
|
211
|
+
code: 'LIVE2D_CUBISM2_MOC_UNSUPPORTED',
|
|
212
|
+
// 三句话缺一不可:这是什么 / 为什么不行 / 用户能做什么
|
|
213
|
+
message: `这是 Cubism 2.1 的 .moc 模型数据${fileHint}${ver ? `,版本 ${ver}` : ''}。`
|
|
214
|
+
+ 'Cubism 3 及以后的 SDK/运行时已不支持 .moc 与 .mtn 这两种旧格式,本页面的 Live2D 渲染只支持 .moc3。'
|
|
215
|
+
+ '若你手上有该模型的 .cmox 工程文件,可用官方 Cubism Editor 打开并转换为 .moc3 后再放进来;'
|
|
216
|
+
+ '如果是从游戏包里直接拆出的 .moc,则没有可用的转换途径(需要原作者提供工程文件)。',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
container: {
|
|
223
|
+
version: parsed.version,
|
|
224
|
+
engine: parsed.engineVersion,
|
|
225
|
+
player: parsed.playerVersion,
|
|
226
|
+
nodes: parsed.nodes.length,
|
|
227
|
+
bytes: bytes.length,
|
|
228
|
+
},
|
|
229
|
+
objects: (sf.objects || []).length,
|
|
230
|
+
moc3: got.moc3 ? { bytes: got.moc3.length } : null,
|
|
231
|
+
cubism2Moc: cubism2.count > 0 ? { count: cubism2.count, names: cubism2.names, version: cubism2.sample ? cubism2.sample.version : null } : null,
|
|
232
|
+
reject,
|
|
233
|
+
physics3: got.physics3 ? { bytes: got.physics3.length } : null,
|
|
234
|
+
nativeFiles: (got.nativeFiles || []).map((f) => f.name),
|
|
235
|
+
textures,
|
|
236
|
+
// 动作数 = AnimationClip 对象数(与装载时的 motion 数同源,但不跑转换)
|
|
237
|
+
motions: countByClass(sf, 'AnimationClip'),
|
|
238
|
+
// 有纹理格式解不了时不算整体失败,只把结论带出来,交给调用方决定
|
|
239
|
+
warnings: unsupported.length ? [`有纹理格式暂不支持:${[...new Set(unsupported)].join('、')}`] : [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
NS.bundleProbe = { run, formatSupport, findCubism2Moc };
|
|
244
|
+
}
|