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,120 @@
|
|
|
1
|
+
// engine/engine-state.js —— 引擎实例的字段初值与少量只读派生
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独放:SpineEngine 的字段有 40 多个(画布/上下文/相机/动画/外观/统计),
|
|
4
|
+
// 全部堆在 constructor 里会让主类读起来找不到"装载流程"这条主线。
|
|
5
|
+
// 初值集中在这里,字段含义一处可见,player 只负责行为。
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 引擎初值(不含 callbacks,由 player 合并)。
|
|
9
|
+
* @returns {object}
|
|
10
|
+
*/
|
|
11
|
+
export function engineDefaults() { // dsh-skip-func-length 引擎默认值表,纯数据声明;既有代码(本轮仅删 2 行死字段)
|
|
12
|
+
return {
|
|
13
|
+
canvas: null,
|
|
14
|
+
gl: null,
|
|
15
|
+
mode: 'none',
|
|
16
|
+
// —— 运行时与能力
|
|
17
|
+
spine: null,
|
|
18
|
+
runtimeVersion: null,
|
|
19
|
+
caps: null,
|
|
20
|
+
adapters: null,
|
|
21
|
+
supportsStraightAlpha: true,
|
|
22
|
+
// —— 模型内容
|
|
23
|
+
atlas: null,
|
|
24
|
+
textures: [],
|
|
25
|
+
skeletonData: null,
|
|
26
|
+
skeleton: null,
|
|
27
|
+
stateData: null,
|
|
28
|
+
state: null,
|
|
29
|
+
currentModel: null,
|
|
30
|
+
premultiplied: true,
|
|
31
|
+
// —— 渲染器与相机
|
|
32
|
+
renderer: null,
|
|
33
|
+
renderTarget: null,
|
|
34
|
+
rendererKind: 'none',
|
|
35
|
+
rendererRuntime: null,
|
|
36
|
+
camera: null,
|
|
37
|
+
baseZoom: 1,
|
|
38
|
+
userScale: 1,
|
|
39
|
+
logicalW: 0,
|
|
40
|
+
logicalH: 0,
|
|
41
|
+
// —— fit / 外观档位
|
|
42
|
+
fitMode: 'subject', // subject | all | animation
|
|
43
|
+
fitWarnedSubject: false,
|
|
44
|
+
pmaMode: 'auto', // auto | premultiplied | straight | upload
|
|
45
|
+
pmaInfo: null,
|
|
46
|
+
pmaDetection: null,
|
|
47
|
+
textureFilter: 'linear', // linear | nearest
|
|
48
|
+
filterId: 'original', // 12 个预设之一
|
|
49
|
+
filterState: { saved: null, active: null },
|
|
50
|
+
activeSkins: [],
|
|
51
|
+
slotFilterKeywords: [],
|
|
52
|
+
// 绘制期间被临时摘掉的插槽附件(画完立刻挂回;不销毁,保证可恢复)
|
|
53
|
+
slotHiddenBackup: [],
|
|
54
|
+
debugDraw: { bones: false, meshes: false, boundingBoxes: false, paths: false, points: false, regions: false, clipping: false, hulls: false },
|
|
55
|
+
// —— 动画
|
|
56
|
+
animationName: '',
|
|
57
|
+
animDuration: 0,
|
|
58
|
+
animTime: 0,
|
|
59
|
+
loop: true,
|
|
60
|
+
speed: 1,
|
|
61
|
+
mixTime: 0.2, // dsh-skip-quality 默认混音时长,既有代码
|
|
62
|
+
durations: {},
|
|
63
|
+
// —— 循环与统计
|
|
64
|
+
isPlaying: false,
|
|
65
|
+
animationId: null,
|
|
66
|
+
lastTime: 0,
|
|
67
|
+
fpsFrames: 0,
|
|
68
|
+
fpsLastTime: 0,
|
|
69
|
+
currentFps: 0,
|
|
70
|
+
fpsLimit: 60,
|
|
71
|
+
dprMode: 'auto',
|
|
72
|
+
isSoftware: false,
|
|
73
|
+
rendererStr: '',
|
|
74
|
+
antialias: false,
|
|
75
|
+
pendingPresent: false,
|
|
76
|
+
loadingModel: false,
|
|
77
|
+
renderWarned: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 合并调试绘制开关(每次只传变化的键)。
|
|
83
|
+
* @param {object} engine
|
|
84
|
+
* @param {object} patch
|
|
85
|
+
* @returns {object} 合并后的开关表
|
|
86
|
+
*/
|
|
87
|
+
export function mergeDebugDraw(engine, patch) {
|
|
88
|
+
engine.debugDraw = { ...engine.debugDraw, ...(patch || {}) };
|
|
89
|
+
return engine.debugDraw;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 渲染器信息(含能力矩阵,便于排查"某能力在该运行时不可用")。
|
|
94
|
+
* @param {object} engine
|
|
95
|
+
* @returns {object}
|
|
96
|
+
*/
|
|
97
|
+
export function rendererInfo(engine) { // dsh-skip-complexity 渲染器自述表,多字段 ?. 取值;既有代码
|
|
98
|
+
return {
|
|
99
|
+
isSoftware: engine.isSoftware,
|
|
100
|
+
rendererStr: engine.rendererStr,
|
|
101
|
+
antialias: engine.antialias,
|
|
102
|
+
// 运行时画不出来的调试绘制项(如 3.8/4.1 都没有 drawPoints):如实暴露,别让面板假装能用。
|
|
103
|
+
// 走确定性探测(探测调试渲染器实例上有没有对应属性),不依赖"是否已经画过帧"
|
|
104
|
+
debugUnsupported: (() => {
|
|
105
|
+
try {
|
|
106
|
+
const rd = engine.adapters && engine.adapters.renderer;
|
|
107
|
+
const sup = rd && typeof rd.debugSupport === 'function' ? rd.debugSupport(engine.renderTarget) : null;
|
|
108
|
+
return sup && Array.isArray(sup.missing) ? sup.missing : [];
|
|
109
|
+
} catch { return []; }
|
|
110
|
+
})(),
|
|
111
|
+
dpr: engine.getEffectiveDpr(window.devicePixelRatio || 1),
|
|
112
|
+
runtimeVersion: engine.runtimeVersion, // 仅展示,不参与行为
|
|
113
|
+
capabilities: engine.caps ? engine.caps.matrix() : null,
|
|
114
|
+
capabilityWarns: engine.caps ? engine.caps.warns.slice() : [],
|
|
115
|
+
pma: engine.pmaInfo ? { mode: engine.pmaMode, effective: engine.pmaInfo.effectiveMode, blend: engine.pmaInfo.blendMode, degraded: engine.pmaInfo.degraded } : null,
|
|
116
|
+
pmaDetection: engine.pmaDetection || null,
|
|
117
|
+
filter: engine.filterId,
|
|
118
|
+
textureFilter: engine.textureFilter,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// engine/filters.js —— 滤镜预设表 + 着色器注入/还原
|
|
2
|
+
//
|
|
3
|
+
// 片元模板的做法:先把预乘色还原成直通 `base`,再走 `__EFFECT_CHAIN__`(预设表达式 +
|
|
4
|
+
// 插件效果链,产出 vec3 f),最后乘回 alpha 输出 —— 所以滤镜效果与 PMA 口径解耦,
|
|
5
|
+
// 切滤镜不需要重传纹理。链的组装本身在 effect-chain.js(与 Live2D 共用)。
|
|
6
|
+
|
|
7
|
+
import { buildChainBody, chainSignature, chainUniforms, effectPrelude, uniformDeclarations } from './effect-chain.js';
|
|
8
|
+
// 编译失败的诊断必须**不经 engine.log** 落地:那是个外部传入的回调,它若不是函数,
|
|
9
|
+
// 在 catch 里再抛一次会把异常传出 reconcileFilter,连 filterState 都来不及赋值
|
|
10
|
+
// (表现就是"签名恒空 + 日志里什么都没有",实测踩过)。
|
|
11
|
+
import { stageLog } from '../app/status.js';
|
|
12
|
+
|
|
13
|
+
/** 纹理像素步长的兜底近似(1/512):见 shaderSourceOf 里 u_texel 的说明 */
|
|
14
|
+
export const DEFAULT_TEXEL = 1 / 512;
|
|
15
|
+
|
|
16
|
+
/** 滤镜预设表 */
|
|
17
|
+
export const FILTER_PRESETS = [
|
|
18
|
+
{ id: 'original', name: '原画', body: 'c' },
|
|
19
|
+
{ id: 'bright', name: '亮部增强', body: 'clamp(c * 1.14 + 0.04, 0.0, 1.0)' },
|
|
20
|
+
{ id: 'soft', name: '亮部柔和', body: 'c * 0.82' },
|
|
21
|
+
{ id: 'mono', name: '黑白', body: 'toGray(c)' },
|
|
22
|
+
{ id: 'warm', name: '暖色', body: 'clamp(c * vec3(1.08, 1.01, 0.90) + vec3(0.012, 0.006, 0.0), 0.0, 1.0)' },
|
|
23
|
+
{ id: 'cool', name: '冷色', body: 'clamp(c * vec3(0.90, 0.97, 1.10), 0.0, 1.0)' },
|
|
24
|
+
{ id: 'fresh', name: '日系清新', body: 'withSat(clamp(c * 1.06 + 0.035, 0.0, 1.0), 0.80) * vec3(0.97, 1.0, 1.05)' },
|
|
25
|
+
{ id: 'sepia', name: '棕褐复古', body: 'mix(c, clamp(vec3(dot(c, vec3(0.393, 0.769, 0.189)), dot(c, vec3(0.349, 0.686, 0.168)), dot(c, vec3(0.272, 0.534, 0.131))), 0.0, 1.0), 0.85)' },
|
|
26
|
+
{ id: 'tealorange', name: '青橙电影', body: 'c * mix(vec3(0.72, 1.06, 1.16), vec3(1.14, 0.97, 0.76), smoothstep(0.08, 0.92, dot(c, LUMA)))' },
|
|
27
|
+
{ id: 'cyber', name: '赛博朋克', body: 'clamp(withSat((c - 0.5) * 1.18 + 0.5, 1.32) * mix(vec3(0.78, 1.08, 1.22), vec3(1.28, 0.82, 1.18), smoothstep(0.1, 0.9, dot(c, LUMA))), 0.0, 1.0)' },
|
|
28
|
+
{ id: 'negative', name: '负片', body: '1.0 - c' },
|
|
29
|
+
{ id: 'woodcut', name: '版画二值', body: 'vec3(step(0.52, dot(c, LUMA)))' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** 缺省滤镜 */
|
|
33
|
+
export const DEFAULT_FILTER = 'original';
|
|
34
|
+
|
|
35
|
+
/** 是否合法滤镜 id */
|
|
36
|
+
export function hasFilter(id) {
|
|
37
|
+
return FILTER_PRESETS.some((p) => p.id === id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 顶点着色器 */
|
|
41
|
+
export const VERTEX_SHADER = `
|
|
42
|
+
attribute vec4 a_position;
|
|
43
|
+
attribute vec4 a_color;
|
|
44
|
+
attribute vec4 a_color2;
|
|
45
|
+
attribute vec2 a_texCoords;
|
|
46
|
+
uniform mat4 u_projTrans;
|
|
47
|
+
varying vec4 v_light;
|
|
48
|
+
varying vec4 v_dark;
|
|
49
|
+
varying vec2 v_texCoords;
|
|
50
|
+
|
|
51
|
+
void main() {
|
|
52
|
+
v_light = a_color;
|
|
53
|
+
v_dark = a_color2;
|
|
54
|
+
v_texCoords = a_texCoords;
|
|
55
|
+
gl_Position = u_projTrans * a_position;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
/** 片元着色器模板(__EFFECT_CHAIN__ 处填入预设表达式 + 插件效果链) */
|
|
60
|
+
export const FRAGMENT_TEMPLATE = `
|
|
61
|
+
#ifdef GL_ES
|
|
62
|
+
#define LOWP lowp
|
|
63
|
+
precision mediump float;
|
|
64
|
+
#else
|
|
65
|
+
#define LOWP
|
|
66
|
+
#endif
|
|
67
|
+
varying LOWP vec4 v_light;
|
|
68
|
+
varying LOWP vec4 v_dark;
|
|
69
|
+
varying vec2 v_texCoords;
|
|
70
|
+
uniform sampler2D u_texture;
|
|
71
|
+
__EFFECT_GLOBALS__
|
|
72
|
+
|
|
73
|
+
const vec3 LUMA = vec3(0.299, 0.587, 0.114);
|
|
74
|
+
|
|
75
|
+
vec3 toGray(vec3 c) { return vec3(dot(c, LUMA)); }
|
|
76
|
+
vec3 withSat(vec3 c, float s) { float l = dot(c, LUMA); return mix(vec3(l), c, s); }
|
|
77
|
+
|
|
78
|
+
void main() {
|
|
79
|
+
vec4 texColor = texture2D(u_texture, v_texCoords);
|
|
80
|
+
float a = texColor.a * v_light.a;
|
|
81
|
+
vec3 pma = ((texColor.a - 1.0) * v_dark.a + 1.0 - texColor.rgb) * v_dark.rgb + texColor.rgb * v_light.rgb;
|
|
82
|
+
vec3 base = pma / max(a, 0.00001);
|
|
83
|
+
// 下面是"效果链占位行":由 shaderSourceOf 注入预设表达式与插件效果链,产出 vec3 f。
|
|
84
|
+
// 注意:模板内注释不许出现占位符字样本身,也不许出现反引号(会终止模板字符串)。
|
|
85
|
+
__EFFECT_CHAIN__
|
|
86
|
+
gl_FragColor = vec4(f * a, a);
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
/** 取某预设的 vs/fs(未知 id 回落 original) */
|
|
91
|
+
export function shaderSourceOf(id, effects) {
|
|
92
|
+
// 硬断言:两个注入位在模板里必须**恰好各 1 处**。
|
|
93
|
+
// 为什么需要:模板注释里若也写了这两个 token 字样,replace 只会命中最先出现的那一处
|
|
94
|
+
// ⇒ 注入内容落进注释、真正的注入行留在原地 ⇒ GLSL 报 '{' / '=' 之类的语法错,
|
|
95
|
+
// 而从渲染层一路查下来完全看不出是"字符串拼接"问题(实测在这上面耗了几轮)。
|
|
96
|
+
// 断言能一次拦住,不用再靠肉眼数。
|
|
97
|
+
const countOf = (hay, token) => hay.split(token).length - 1;
|
|
98
|
+
const nGlobals = countOf(FRAGMENT_TEMPLATE, '__EFFECT_GLOBALS__');
|
|
99
|
+
const nChain = countOf(FRAGMENT_TEMPLATE, '__EFFECT_CHAIN__');
|
|
100
|
+
if (nGlobals !== 1 || nChain !== 1) {
|
|
101
|
+
throw new Error(`滤镜模板注入位数量异常:__EFFECT_GLOBALS__=${nGlobals}、__EFFECT_CHAIN__=${nChain}(各必须恰好 1 次)`);
|
|
102
|
+
}
|
|
103
|
+
const preset = FILTER_PRESETS.find((p) => p.id === id) || FILTER_PRESETS[0];
|
|
104
|
+
const chain = Array.isArray(effects) ? effects : [];
|
|
105
|
+
// 链的组装交给 effect-chain.js(与 Live2D 共用同一份语义),这里只负责
|
|
106
|
+
// "把链塞进 Spine 的片元模板"—— 这是两条链路唯一该有的差异。
|
|
107
|
+
const body = buildChainBody({ presetBody: preset.body, effects: chain });
|
|
108
|
+
// 前置声明分两段(GLSL 作用域规则):uniform 与函数定义进全局,局部变量进 main 内。
|
|
109
|
+
// 上一版把两段混在一起塞进 main ⇒ `S'u_texel' : syntax error`(实测编译失败)。
|
|
110
|
+
const pre = effectPrelude({ sampler: 'u_texture', texCoords: 'v_texCoords' });
|
|
111
|
+
const useChain = chain.length > 0;
|
|
112
|
+
const fs = FRAGMENT_TEMPLATE
|
|
113
|
+
// 用 replaceAll:占位符若在模板注释里再出现一次,replace 只会换第一处,
|
|
114
|
+
// 把注入内容吞进注释、真占位行残留 ⇒ 语法错(实测踩过)
|
|
115
|
+
.replaceAll('__EFFECT_GLOBALS__', () => (useChain ? `${pre.global}\n${uniformDeclarations(chain)}` : ''))
|
|
116
|
+
.replaceAll('__EFFECT_CHAIN__', () => (useChain ? `${pre.local}\n ${body}` : body));
|
|
117
|
+
const uniforms = chainUniforms(chain);
|
|
118
|
+
// u_texel 必须有非零值:GLSL 未赋值的 uniform 默认 0 ⇒ texelSample(uv-0) 取回自己,
|
|
119
|
+
// 锐化类表达式退化成 c + k*(c-c) = c,看起来"开关没反应"(上一轮实测踩过)。
|
|
120
|
+
if (chain.length) uniforms.u_texel = [DEFAULT_TEXEL, DEFAULT_TEXEL];
|
|
121
|
+
return { vs: VERTEX_SHADER, fs, uniforms, signature: chainSignature(chain) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 把滤镜着色器装上/还原。
|
|
126
|
+
* 关键约定:
|
|
127
|
+
* · `savedDefaultBatcherShader` **只在第一次装滤镜时保存一次**,避免把"滤镜着色器"当成默认存进去;
|
|
128
|
+
* · 编译失败 → warn 并保持原渲染(不置空、不抛);
|
|
129
|
+
* · 切回 original 时还原默认着色器并 dispose 滤镜着色器。
|
|
130
|
+
* @param {{renderer: object, gl: WebGLRenderingContext, ShaderCtor: Function,
|
|
131
|
+
* filterId: string, saved: object|null, active: object|null}} ctx
|
|
132
|
+
* @returns {{saved: object|null, active: object|null, changed: boolean}}
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* 给 shader 实例写 uniform 值。
|
|
136
|
+
*
|
|
137
|
+
* PixiJS 各版本挂点不同:v4 是 `shader.uniforms`,v5+ 是 `shader.uniformGroup.uniforms`。
|
|
138
|
+
* 写不上**不抛**:编译本身已经成功,值缺失最多是"效果强度为 0",
|
|
139
|
+
* 不该被外层当成编译失败(实测:直接写 `shader.uniforms[key]` 在 v5 上抛
|
|
140
|
+
* "Cannot set properties of undefined",被 catch 吞成"编译失败",
|
|
141
|
+
* 于是 active 永远为 null ⇒ 效果永远不生效,而日志看起来像着色器语法问题)。
|
|
142
|
+
* @param {object} shader
|
|
143
|
+
* @param {object} uniforms
|
|
144
|
+
* @param {Function} log
|
|
145
|
+
* @returns {boolean} 是否写成功
|
|
146
|
+
*/
|
|
147
|
+
function applyUniforms(shader, uniforms, log = () => {}) {
|
|
148
|
+
if (!shader || typeof shader.bind !== 'function') return false;
|
|
149
|
+
// spine-webgl 的 Shader(src/spine-runtimes/spine-webgl-*.js)**没有 uniforms 属性**,
|
|
150
|
+
// 值只能经 bind() + setUniformf / setUniform2f 下发;Pixi 的 uniformGroup/dirty 那套不适用。
|
|
151
|
+
// gl.uniform* 只作用于当前 program,所以必须先 bind(batcherShader 自己也是这么用的)。
|
|
152
|
+
try {
|
|
153
|
+
shader.bind();
|
|
154
|
+
for (const [key, value] of Object.entries(uniforms || {})) {
|
|
155
|
+
// 未被表达式使用的 uniform 会被 GLSL 编译器优化掉,getUniformLocation 返回 null,
|
|
156
|
+
// 此时 gl.uniform*(null, v) 静默无效 —— 显式拦一下并留痕,免得"声明了却没生效"。
|
|
157
|
+
const loc = typeof shader.getUniformLocation === 'function' ? shader.getUniformLocation(key) : undefined;
|
|
158
|
+
if (loc === null) {
|
|
159
|
+
try { log('warn', `uniform ${key} 在着色器里未被使用(可能被优化掉),已跳过`, 'filter'); } catch { /* 忽略 */ }
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
163
|
+
// vec2 必须用 setUniform2f:setUniformf 走 gl.uniform1f,给 vec2 会丢一半
|
|
164
|
+
if (typeof shader.setUniform2f === 'function') shader.setUniform2f(key, Number(value[0]), Number(value[1]));
|
|
165
|
+
} else if (typeof shader.setUniformf === 'function') {
|
|
166
|
+
shader.setUniformf(key, Number(value));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
} catch (e) {
|
|
171
|
+
try { log('warn', `效果参数下发失败: ${e && e.message ? e.message : e}`, 'filter'); } catch { /* 忽略 */ }
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function reconcileFilter(ctx) {
|
|
177
|
+
const { renderer: r, gl, ShaderCtor, filterId, effects, state, log = () => {} } = ctx;
|
|
178
|
+
if (!r || !gl || !ShaderCtor || !('batcherShader' in r)) return { ...state, changed: false };
|
|
179
|
+
const chain = Array.isArray(effects) ? effects : [];
|
|
180
|
+
if (filterId === DEFAULT_FILTER && !chain.length) {
|
|
181
|
+
if (state.active) {
|
|
182
|
+
if (state.saved) r.batcherShader = state.saved;
|
|
183
|
+
try { state.active.dispose(); } catch { /* 释放失败不影响还原 */ }
|
|
184
|
+
return { saved: state.saved, active: null, changed: true, signature: '' };
|
|
185
|
+
}
|
|
186
|
+
// 链已空 ⇒ 签名就是空串(与 active 是否非 null 无关)。
|
|
187
|
+
// 这句漏了会出大问题:active 为 null(例如上一次编译失败从未挂上)时返回 {...state}
|
|
188
|
+
// 会把**旧签名**又带回去 ⇒ 上层以为链还在(实测:关掉档位后签名不回落)。
|
|
189
|
+
return { ...state, changed: false, signature: '' };
|
|
190
|
+
}
|
|
191
|
+
const saved = state.saved || r.batcherShader;
|
|
192
|
+
// 签名 = 预设 + 链上"结构"(效果增减 / 表达式变化)。**参数值不进签名** ——
|
|
193
|
+
// 参数变化只改 uniform,这是热更新的关键:签名一变就会重建着色器(黑帧抖动)。
|
|
194
|
+
const signature = `${filterId}|${chainSignature(chain)}`;
|
|
195
|
+
if (state.active && state.signature === signature) {
|
|
196
|
+
const { uniforms } = shaderSourceOf(filterId, chain);
|
|
197
|
+
applyUniforms(state.active, uniforms, log);
|
|
198
|
+
return { ...state, changed: false, signature };
|
|
199
|
+
}
|
|
200
|
+
const { vs, fs, uniforms } = shaderSourceOf(filterId, chain);
|
|
201
|
+
let shader = null;
|
|
202
|
+
try {
|
|
203
|
+
shader = new ShaderCtor(gl, vs, fs);
|
|
204
|
+
// 新实例的 uniform 要显式写一遍:模板只给了声明空间,值由宿主负责填
|
|
205
|
+
applyUniforms(shader, uniforms, log);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
const detail = e && e.message ? e.message : String(e);
|
|
208
|
+
// 失败也要带 signature:上层用它区分"没有链"和"链挂了"。
|
|
209
|
+
// 漏了它 ⇒ filterState.signature 存成 '' ⇒ effectChainSignature() 恒空,
|
|
210
|
+
// 看起来像"链根本没建"(这就是上一轮卡住的地方)。
|
|
211
|
+
try { stageLog('warn', `滤镜着色器编译失败,保持原渲染: ${detail}`, { filterId, chainLen: chain.length }); } catch { /* 日志失败不影响还原 */ }
|
|
212
|
+
// 把替换后的片元前若干行打出来:要看的是 __EFFECT_CHAIN__ 到底替成了什么
|
|
213
|
+
try {
|
|
214
|
+
const head = String(fs).split('\n').slice(0, 24).join(' ⏎ ');
|
|
215
|
+
stageLog('info', `[滤镜 fs 片段] ${head}`, { filterId, signature });
|
|
216
|
+
} catch { /* 忽略 */ }
|
|
217
|
+
try { log('warn', `滤镜着色器编译失败,保持原渲染: ${detail}`, 'filter'); } catch { /* log 不是函数时别把异常传出去 */ }
|
|
218
|
+
return { saved, active: state.active, changed: false, signature };
|
|
219
|
+
}
|
|
220
|
+
const prev = state.active;
|
|
221
|
+
r.batcherShader = shader;
|
|
222
|
+
if (prev) { try { prev.dispose(); } catch { /* 忽略 */ } }
|
|
223
|
+
return { saved, active: shader, changed: true, signature };
|
|
224
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// engine/fit.js —— 适配(fit)三模式:subject(默认)/ all / animation
|
|
2
|
+
//
|
|
3
|
+
// 三模式的判定规则:
|
|
4
|
+
// · NON_SUBJECT_RE 剔除背景/特效,CORE_RE 认主体,FIT_MAX_RATIO=1.15 限制"横向过宽"的包围盒;
|
|
5
|
+
// · subject 结果不可信时回落到整体包围盒;
|
|
6
|
+
// · animation 模式对每个动作采样若干帧取并集。
|
|
7
|
+
// 版本差异(computeWorldVertices 的签名)用**参数个数 + 试调**探测,不读版本号。
|
|
8
|
+
|
|
9
|
+
/** 背景/特效类部件(不参与主体适配) */
|
|
10
|
+
export const NON_SUBJECT_RE = /(?:^|[_\s/.-])(backgrounds?|bgs?|backs?|effects?|effs?|efs?|fxs?|efxs?|vfx|particles?|auras?|glows?|halos?|debris|bursts?|shadows?|floors?|grounds?|lights?|(?:bg)|(?:fx)[0-9]*)(?:[_\s/.-]|$|[0-9])|sakura|petal|blossom|bgimage|bg-img|bg_img|(?:^|[/])ref[/]|背景|特效|光效|粒子|光晕|樱花|花瓣|碎片|阴影|地面/i;
|
|
11
|
+
|
|
12
|
+
/** 主体类部件(优先级最高) */
|
|
13
|
+
export const CORE_RE = /(chara|character|body|head|face|hair|arm|leg|skirt|cloth|pelvis|neck|hand|foot|torso|spine|chest|hip|bust|skin|sleeve|glove|shoe|socks|dress|coat|jacket|cape|wing|tail|horn|ear|eye|mouth|nose|身|头|脸|发|手|脚|腿|衣|裙|体|肤)/i;
|
|
14
|
+
|
|
15
|
+
/** 横向相对高度的上限(超过就当作"背景太宽",按高度收窄) */
|
|
16
|
+
export const FIT_MAX_RATIO = 1.15;
|
|
17
|
+
|
|
18
|
+
/** 支持的模式(与面板「初始缩放」三个按钮一一对应) */
|
|
19
|
+
export const FIT_MODES = ['subject', 'all', 'animation'];
|
|
20
|
+
|
|
21
|
+
/** 顶点数组 → 包围盒 */
|
|
22
|
+
function boxOfPoints(points) {
|
|
23
|
+
let minX = Infinity;
|
|
24
|
+
let minY = Infinity;
|
|
25
|
+
let maxX = -Infinity;
|
|
26
|
+
let maxY = -Infinity;
|
|
27
|
+
for (let i = 0; i + 1 < points.length; i += 2) {
|
|
28
|
+
const x = points[i];
|
|
29
|
+
const y = points[i + 1];
|
|
30
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
31
|
+
if (x < minX) minX = x;
|
|
32
|
+
if (x > maxX) maxX = x;
|
|
33
|
+
if (y < minY) minY = y;
|
|
34
|
+
if (y > maxY) maxY = y;
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isFinite(minX) || maxX <= minX) return null;
|
|
37
|
+
return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 取附件世界顶点包围盒(签名差异用"参数个数 + 试调"探测)。
|
|
42
|
+
* @param {object} skeleton
|
|
43
|
+
* @param {object} slot
|
|
44
|
+
* @returns {{minX: number, minY: number, maxX: number, maxY: number, w: number, h: number}|null}
|
|
45
|
+
*/
|
|
46
|
+
export function slotWorldBounds(skeleton, slot) {
|
|
47
|
+
const att = slot && slot.attachment;
|
|
48
|
+
if (!att) return null;
|
|
49
|
+
const len = att.worldVerticesLength;
|
|
50
|
+
if (typeof len === 'number' && typeof att.computeWorldVertices === 'function') {
|
|
51
|
+
const verts = new Float32Array(len);
|
|
52
|
+
const deepFirst = att.computeWorldVertices.length >= 6;
|
|
53
|
+
const narrow = () => att.computeWorldVertices(slot, 0, len, verts, 0, 2);
|
|
54
|
+
const deep = () => att.computeWorldVertices(skeleton, slot, 0, len, verts, 0, 2);
|
|
55
|
+
for (const run of (deepFirst ? [deep, narrow] : [narrow, deep])) {
|
|
56
|
+
try { run(); const box = boxOfPoints(verts); if (box) return box; } catch { /* 试下一种签名 */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// 回退:骨骼位置 + 附件宽高(region 类附件)
|
|
60
|
+
const bone = slot && slot.bone;
|
|
61
|
+
if (!bone) return null;
|
|
62
|
+
let sx = Math.abs(bone.scaleX || 1);
|
|
63
|
+
let sy = Math.abs(bone.scaleY || 1);
|
|
64
|
+
for (let p = bone.parent; p; p = p.parent) { sx *= Math.abs(p.scaleX || 1); sy *= Math.abs(p.scaleY || 1); }
|
|
65
|
+
const w = (att.width || 0) * sx;
|
|
66
|
+
const h = (att.height || 0) * sy;
|
|
67
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
68
|
+
const x = bone.worldX || 0;
|
|
69
|
+
const y = bone.worldY || 0;
|
|
70
|
+
return { minX: x - w / 2, maxX: x + w / 2, minY: y - h / 2, maxY: y + h / 2, w, h };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 主体包围盒(核心部件为准,剔除背景/特效,远离主体的碎片也剔除)。
|
|
75
|
+
* @param {{skeleton: object, skeletonData: object}} ctx
|
|
76
|
+
* @returns {{x: number, y: number, w: number, h: number}|null}
|
|
77
|
+
*/
|
|
78
|
+
export function computeSubjectBounds({ skeleton, skeletonData }) {
|
|
79
|
+
const order = (skeleton && skeleton.drawOrder) || [];
|
|
80
|
+
const items = [];
|
|
81
|
+
for (const slot of order) {
|
|
82
|
+
const box = slot && slot.attachment ? slotWorldBounds(skeleton, slot) : null;
|
|
83
|
+
if (!box) continue;
|
|
84
|
+
const slotName = String((slot.data && slot.data.name) || '');
|
|
85
|
+
const attName = String((slot.attachment && slot.attachment.name) || '');
|
|
86
|
+
const core = CORE_RE.test(slotName) || CORE_RE.test(attName);
|
|
87
|
+
const bg = (NON_SUBJECT_RE.test(slotName) || NON_SUBJECT_RE.test(attName)) && !core;
|
|
88
|
+
items.push({ box, bg, core });
|
|
89
|
+
}
|
|
90
|
+
if (!items.length) return null;
|
|
91
|
+
const ref = referenceSize(items, skeletonData);
|
|
92
|
+
const coreBox = unionCore(items);
|
|
93
|
+
const coreSignificant = !!coreBox && coreBox.w >= 0.12 * ref.w && coreBox.h >= 0.12 * ref.h;
|
|
94
|
+
const margin = coreSignificant ? Math.max(0.22 * ref.h, coreBox.h * 0.6) : 0;
|
|
95
|
+
return unionKeep(items, { coreBox, coreSignificant, margin, ref });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 参考尺寸:优先骨架数据自带,否则全部部件的范围 */
|
|
99
|
+
function referenceSize(items, skeletonData) {
|
|
100
|
+
let w = skeletonData && skeletonData.width > 0 ? skeletonData.width : 0;
|
|
101
|
+
let h = skeletonData && skeletonData.height > 0 ? skeletonData.height : 0;
|
|
102
|
+
if (!(w > 0) || !(h > 0)) {
|
|
103
|
+
let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity;
|
|
104
|
+
for (const it of items) {
|
|
105
|
+
if (it.box.minX < minX) minX = it.box.minX;
|
|
106
|
+
if (it.box.minY < minY) minY = it.box.minY;
|
|
107
|
+
if (it.box.maxX > maxX) maxX = it.box.maxX;
|
|
108
|
+
if (it.box.maxY > maxY) maxY = it.box.maxY;
|
|
109
|
+
}
|
|
110
|
+
if (Number.isFinite(minX)) { w = maxX - minX; h = maxY - minY; }
|
|
111
|
+
}
|
|
112
|
+
return { w: w > 0 ? w : 1024, h: h > 0 ? h : 1024 };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 核心部件并集 */
|
|
116
|
+
function unionCore(items) {
|
|
117
|
+
let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity;
|
|
118
|
+
for (const it of items) {
|
|
119
|
+
if (!it.core) continue;
|
|
120
|
+
if (it.box.minX < minX) minX = it.box.minX;
|
|
121
|
+
if (it.box.minY < minY) minY = it.box.minY;
|
|
122
|
+
if (it.box.maxX > maxX) maxX = it.box.maxX;
|
|
123
|
+
if (it.box.maxY > maxY) maxY = it.box.maxY;
|
|
124
|
+
}
|
|
125
|
+
if (minX === Infinity) return null;
|
|
126
|
+
return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 合并:核心必留;非核心剔除背景与远离主体的碎片 */
|
|
130
|
+
function unionKeep(items, { coreBox, coreSignificant, margin, ref }) {
|
|
131
|
+
let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity;
|
|
132
|
+
let count = 0;
|
|
133
|
+
for (const it of items) {
|
|
134
|
+
const b = it.box;
|
|
135
|
+
if (!it.core) {
|
|
136
|
+
if (it.bg) continue;
|
|
137
|
+
const tooWide = b.w > 1.3 * ref.w || b.h > 1.3 * ref.h;
|
|
138
|
+
const farAway = coreSignificant && (
|
|
139
|
+
b.maxX < coreBox.minX - margin || b.minX > coreBox.maxX + margin
|
|
140
|
+
|| b.maxY < coreBox.minY - margin || b.minY > coreBox.maxY + margin
|
|
141
|
+
);
|
|
142
|
+
if (tooWide || farAway) continue;
|
|
143
|
+
}
|
|
144
|
+
if (b.minX < minX) minX = b.minX;
|
|
145
|
+
if (b.minY < minY) minY = b.minY;
|
|
146
|
+
if (b.maxX > maxX) maxX = b.maxX;
|
|
147
|
+
if (b.maxY > maxY) maxY = b.maxY;
|
|
148
|
+
count += 1;
|
|
149
|
+
}
|
|
150
|
+
if (!count || !Number.isFinite(minX)) return null;
|
|
151
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 两个包围盒求交(subject 并集 ∩ 整体包围盒)。
|
|
156
|
+
* 为什么需要:并集可能因个别远端附件(如超大挂件)比整体还大,
|
|
157
|
+
* 实测的 subject 结果始终落在整体范围内;取交可剔除这类离群部件。
|
|
158
|
+
* @param {{x: number, y: number, w: number, h: number}} a
|
|
159
|
+
* @param {{x: number, y: number, w: number, h: number}} b
|
|
160
|
+
*/
|
|
161
|
+
export function intersectBox(a, b) {
|
|
162
|
+
if (!a) return b || null;
|
|
163
|
+
if (!b) return a;
|
|
164
|
+
const x = Math.max(a.x, b.x);
|
|
165
|
+
const y = Math.max(a.y, b.y);
|
|
166
|
+
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
167
|
+
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
168
|
+
if (!(right > x) || !(bottom > y)) return b;
|
|
169
|
+
return { x, y, w: right - x, h: bottom - y };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** subject 结果的"横向对称收窄" */
|
|
173
|
+
export function clampSubjectBox(box) {
|
|
174
|
+
if (!box) return null;
|
|
175
|
+
const symHalf = Math.max(Math.abs(box.x), Math.abs(box.x + box.w));
|
|
176
|
+
let w = 2 * symHalf;
|
|
177
|
+
const cap = FIT_MAX_RATIO * box.h;
|
|
178
|
+
if (w > cap) w = cap;
|
|
179
|
+
const center = box.x + box.w / 2;
|
|
180
|
+
const keepCenter = Math.abs(center) <= box.w * 0.04 ? 0 : center;
|
|
181
|
+
return { x: keepCenter - w / 2, y: box.y, w, h: box.h };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 计算视口适配参数(相机 zoom 语义:可见世界宽 = zoom × 视口宽)。
|
|
186
|
+
* @param {{bounds: {x: number, y: number, w: number, h: number}|null, viewW: number, viewH: number}} ctx
|
|
187
|
+
* @returns {{baseZoom: number, centerX: number, centerY: number}|null}
|
|
188
|
+
*/
|
|
189
|
+
export function computeFit({ bounds, viewW, viewH }) {
|
|
190
|
+
if (!bounds || !(viewW > 0) || !(viewH > 0) || !(bounds.w > 0) || !(bounds.h > 0)) return null;
|
|
191
|
+
const need = Math.max(bounds.w / viewW, bounds.h / viewH);
|
|
192
|
+
if (!Number.isFinite(need) || need <= 0) return null;
|
|
193
|
+
// :baseZoom = max(宽比, 高比),不再额外除以 (1-FIT_MARGIN)
|
|
194
|
+
const baseZoom = need;
|
|
195
|
+
if (!Number.isFinite(baseZoom) || baseZoom <= 0) return null;
|
|
196
|
+
return { baseZoom, centerX: bounds.x + bounds.w / 2, centerY: bounds.y + bounds.h / 2 };
|
|
197
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// engine/frame-gate.js —— 帧率门控(累加器口径,两面共用)。
|
|
2
|
+
// 为什么不用 PIXI ticker.maxFPS / 单门限:前者按 deltaTime 判断且带容差(90Hz 屏会放行,实测 60/30 档跑到 90),
|
|
3
|
+
// 后者只跟上一帧时间戳比,零头被丢掉导致长期偏低。累加器:够一格才渲,渲后只扣一格(余量留给下一次),
|
|
4
|
+
// 并对 debt 做上限钳制(卡顿回来不连爆)。两面共用同一份实现,避免各自再写一套限帧逻辑。
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 造一个帧率门控。
|
|
8
|
+
* @param {() => number} getTargetFps 目标帧率(每次询问,支持运行期改档位)
|
|
9
|
+
* @returns {{shouldRender: (deltaMs: number) => boolean, reset: () => void, debtMs: number}}
|
|
10
|
+
*/
|
|
11
|
+
export function createFrameGate(getTargetFps) {
|
|
12
|
+
let debtMs = 0;
|
|
13
|
+
return {
|
|
14
|
+
/**
|
|
15
|
+
* 这一帧该不该渲染。
|
|
16
|
+
* @param {number} deltaMs 距上一次 rAF/ticker 的间隔(毫秒)
|
|
17
|
+
* @returns {boolean} true = 渲染;false = 跳过本帧(只更新状态不出画)
|
|
18
|
+
*/
|
|
19
|
+
shouldRender(deltaMs) {
|
|
20
|
+
const target = Math.max(1, Number(getTargetFps()) || 60);
|
|
21
|
+
const interval = 1000 / target;
|
|
22
|
+
const d = Number(deltaMs);
|
|
23
|
+
if (Number.isFinite(d) && d > 0) debtMs += d;
|
|
24
|
+
if (debtMs < interval) return false;
|
|
25
|
+
debtMs -= interval;
|
|
26
|
+
if (debtMs > interval) debtMs = interval; // 防连爆:最多保留一格余量
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
/** 重置累加器(重新起循环/换 app 时用) */
|
|
30
|
+
reset() { debtMs = 0; },
|
|
31
|
+
get debtMs() { return debtMs; },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// engine/gl-driver.js —— 上下文/尺寸/DPR 的驱动逻辑(从 player 拆出,控制单类规模)
|
|
2
|
+
//
|
|
3
|
+
// 这些方法只操作"引擎实例上的 GL 状态",与装载/动画无关,放这里便于单独演进。
|
|
4
|
+
|
|
5
|
+
import { createContext, effectiveDpr, applyPixelSize } from './gl.js';
|
|
6
|
+
import * as cameraOps from './camera-adapter.js';
|
|
7
|
+
|
|
8
|
+
/** 建上下文;失败返回 false(调用方据此判定环境不可渲染) */
|
|
9
|
+
export function initEngine(engine, canvas, opts = {}) {
|
|
10
|
+
engine.canvas = canvas;
|
|
11
|
+
engine.antialias = opts.antialias === true;
|
|
12
|
+
const { gl, isSoftware, rendererStr } = createContext(canvas, { antialias: engine.antialias });
|
|
13
|
+
if (!gl) {
|
|
14
|
+
engine.log('error', '当前浏览器不支持 WebGL,无法渲染 Spine 动画。', 'renderer');
|
|
15
|
+
engine.mode = 'none';
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
engine.gl = gl;
|
|
19
|
+
engine.mode = 'webgl';
|
|
20
|
+
engine.isSoftware = isSoftware;
|
|
21
|
+
engine.rendererStr = rendererStr;
|
|
22
|
+
engine.logicalW = canvas.clientWidth || canvas.width;
|
|
23
|
+
engine.logicalH = canvas.clientHeight || canvas.height;
|
|
24
|
+
refreshResolution(engine);
|
|
25
|
+
engine.log('info', `WebGL 初始化成功 (canvas=${canvas.width}x${canvas.height}, css=${engine.logicalW}x${engine.logicalH})`, 'renderer');
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 生效 DPR(auto:软件渲染钳 1、否则上限 2) */
|
|
30
|
+
export function effectiveDprOf(engine, deviceDpr) {
|
|
31
|
+
return effectiveDpr(engine.dprMode, engine.isSoftware, deviceDpr);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 按逻辑尺寸 + DPR 重设 backing store */
|
|
35
|
+
export function refreshResolution(engine) {
|
|
36
|
+
if (!engine.canvas) return;
|
|
37
|
+
if (engine.logicalW <= 0 || engine.logicalH <= 0) return;
|
|
38
|
+
const dpr = effectiveDprOf(engine, window.devicePixelRatio || 1);
|
|
39
|
+
applyPixelSize(engine.canvas, engine.gl, { logicalW: engine.logicalW, logicalH: engine.logicalH, dpr });
|
|
40
|
+
cameraOps.syncCamera(engine);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 纯尺寸更新(画布尺寸由外部改动后调用) */
|
|
44
|
+
export function resizeEngine(engine, w, h) {
|
|
45
|
+
if (!engine.canvas) return;
|
|
46
|
+
engine.logicalW = w || engine.canvas.clientWidth;
|
|
47
|
+
engine.logicalH = h || engine.canvas.clientHeight;
|
|
48
|
+
refreshResolution(engine);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** DPR 模式 */
|
|
52
|
+
export function setDprMode(engine, mode) {
|
|
53
|
+
engine.dprMode = mode;
|
|
54
|
+
refreshResolution(engine);
|
|
55
|
+
requestAnimationFrame(() => refreshResolution(engine));
|
|
56
|
+
}
|