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,427 @@
|
|
|
1
|
+
// engine/player.js —— 引擎主体:装载 / 帧循环 / 相机 / 动画 / 外观的最小闭环(B1)
|
|
2
|
+
//
|
|
3
|
+
// 帧调度:**保留渲染器自建 rAF 主循环(方案 A)**,与现状一致。
|
|
4
|
+
// 理由:StageSurface 只管尺寸/背景,不接管渲染帧;改成统一调度需要同时改 surface 与 canvas.js,
|
|
5
|
+
// 属于 B6 的收尾工作,B1 不做(避免一次改两处导致帧问题无法归因)。
|
|
6
|
+
//
|
|
7
|
+
// 对外契约与旧 SpineEngine 同名同义(25 个方法),B1 覆盖:init/loadModel/resize/dispose/
|
|
8
|
+
// start/stop/seek/setAnimation/setMixTime/setSkins/getSkins/setDebugDraw/setSlotFilter/
|
|
9
|
+
// setFilter/getFilter/getCameraZoom/setCameraZoom/applyZoom/applyPan/resetCamera/
|
|
10
|
+
// refreshResolution/getEffectiveDpr/setDprMode/getMode/getRendererInfo/getAnimations/
|
|
11
|
+
// getRuntimeVersion/getCurrentFps/presentFirstFrame;rebuildTextures 暂为明确告警的空实现(B4 落地)。
|
|
12
|
+
|
|
13
|
+
import { createContext, effectiveDpr, applyPixelSize } from './gl.js';
|
|
14
|
+
import { resolveRuntime } from './runtime.js';
|
|
15
|
+
import { buildModel } from './loader.js';
|
|
16
|
+
import { capabilitiesFor } from './runtime-capabilities.js';
|
|
17
|
+
import { createAdapters } from './adapters/index.js';
|
|
18
|
+
import * as appearance from './appearance.js';
|
|
19
|
+
import { getSkins as skinList, setSkins as skinApply, pickInitialSkin, normalizeKeywords, countFilterableSlots } from './skins.js';
|
|
20
|
+
import { engineDefaults, mergeDebugDraw, rendererInfo } from './engine-state.js';
|
|
21
|
+
import * as cameraOps from './camera-adapter.js';
|
|
22
|
+
import { describeModel } from './model-info.js';
|
|
23
|
+
import * as anim from './animation.js';
|
|
24
|
+
import * as glDriver from './gl-driver.js';
|
|
25
|
+
import { renderFrame, presentFirstFrame, startLoop, stopLoop } from './render-frame.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 引擎(与旧 SpineEngine 同名构造:`new Engine({log, onLoadComplete, onError})`)。
|
|
29
|
+
*/
|
|
30
|
+
export class SpineEngine {
|
|
31
|
+
constructor(callbacks = {}) {
|
|
32
|
+
this.callbacks = callbacks || {};
|
|
33
|
+
// 运行时上下文与钩子(上下文丢失恢复在 B4b/B6 处理,与现状一致)
|
|
34
|
+
Object.assign(this, engineDefaults());
|
|
35
|
+
this.runtimeManager = callbacks.runtimeManager
|
|
36
|
+
|| (typeof window !== 'undefined' && window.Is && typeof window.Is.getInstance === 'function' ? window.Is.getInstance() : null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 统一日志 */
|
|
40
|
+
log(level, message, scope = 'player') {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof this.callbacks.log === 'function') this.callbacks.log(level, message, scope);
|
|
43
|
+
} catch { /* 日志失败不得影响渲染 */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 当前渲染模式(none / webgl) */
|
|
47
|
+
getMode() {
|
|
48
|
+
return this.mode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 建上下文(实现见 gl-driver.js);返回 false 表示当前环境不可渲染 */
|
|
52
|
+
init(canvas, opts = {}) {
|
|
53
|
+
return glDriver.initEngine(this, canvas, opts);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 生效 DPR */
|
|
57
|
+
getEffectiveDpr(deviceDpr) {
|
|
58
|
+
return glDriver.effectiveDprOf(this, deviceDpr);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 按逻辑尺寸 + DPR 重设 backing store */
|
|
62
|
+
refreshResolution() {
|
|
63
|
+
glDriver.refreshResolution(this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 纯尺寸更新 */
|
|
67
|
+
resize(w, h) {
|
|
68
|
+
glDriver.resizeEngine(this, w, h);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** DPR 模式 */
|
|
72
|
+
setDprMode(mode) {
|
|
73
|
+
glDriver.setDprMode(this, mode);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 渲染器信息(含能力矩阵:探测结果 + 告警),便于排查"某能力在旧版本不可用"。
|
|
78
|
+
* @returns {object}
|
|
79
|
+
*/
|
|
80
|
+
getRendererInfo() {
|
|
81
|
+
return rendererInfo(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 相机视口跟随画布 CSS 尺寸 */
|
|
85
|
+
syncCameraViewport() {
|
|
86
|
+
cameraOps.syncCamera(this);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 装载模型(描述符契约见 loader.js 头部注释) */
|
|
90
|
+
async loadModel(descriptor) {
|
|
91
|
+
if (this.mode !== 'webgl' || !this.gl) throw new Error('当前浏览器不支持 WebGL,无法渲染 Spine 动画。');
|
|
92
|
+
const rt = await resolveRuntime(this.runtimeManager, descriptor.version);
|
|
93
|
+
this.spine = rt.spine;
|
|
94
|
+
this.runtimeVersion = rt.version;
|
|
95
|
+
this.supportsStraightAlpha = rt.supportsStraightAlpha !== false;
|
|
96
|
+
// 能力探测(一次、按版本缓存):版本号只用于展示,行为全部由探测结果决定
|
|
97
|
+
this.caps = capabilitiesFor(rt.spine, rt.version);
|
|
98
|
+
for (const w of this.caps.warns || []) this.log('warn', `能力缺失 → ${w}`, 'runtime');
|
|
99
|
+
this.adapters = createAdapters({
|
|
100
|
+
spine: rt.spine,
|
|
101
|
+
caps: this.caps,
|
|
102
|
+
gl: this.gl,
|
|
103
|
+
canvas: this.canvas,
|
|
104
|
+
size: { w: this.logicalW, h: this.logicalH },
|
|
105
|
+
onWarn: (m) => this.log('warn', m, 'runtime'),
|
|
106
|
+
});
|
|
107
|
+
this.stop();
|
|
108
|
+
this.disposeContent();
|
|
109
|
+
// 换模型 = 换内容:动作名 / 皮肤列表 / 时长缓存都是「当前模型」的属性,不能跟着旧模型走进新模型。
|
|
110
|
+
// 不清的后果:动作下拉回显旧模型的动作名;纹理重建(PMA 档位)那条路径还会拿旧动作名去 setAnimation
|
|
111
|
+
// (新模型没有该动作 ⇒ 静默失败或播错),循环推进时长也会用旧模型的数字。
|
|
112
|
+
// 为什么不能塞进 disposeContent():纹理重建(appearance.js)也调它,而那条路径属于
|
|
113
|
+
// 「同一内容换口径」,必须保留这三个字段来"重建后重放当前状态" —— 塞进去就会把重放弄坏。
|
|
114
|
+
this.animationName = '';
|
|
115
|
+
this.activeSkins = [];
|
|
116
|
+
this.durations = {};
|
|
117
|
+
this.pendingPresent = true;
|
|
118
|
+
const model = await buildModel({
|
|
119
|
+
adapters: this.adapters,
|
|
120
|
+
spine: this.spine,
|
|
121
|
+
gl: this.gl,
|
|
122
|
+
descriptor,
|
|
123
|
+
pmaMode: this.pmaMode,
|
|
124
|
+
textureFilter: this.textureFilter,
|
|
125
|
+
supportsStraight: this.supportsStraightAlpha,
|
|
126
|
+
log: (l, m, s) => this.log(l, m, s),
|
|
127
|
+
});
|
|
128
|
+
this.bindModel(model);
|
|
129
|
+
// 渲染器与运行时绑定:换运行时必须重建,否则用旧版本绘制器画新骨架会静默不出画
|
|
130
|
+
if (!this.renderer || this.rendererRuntime !== this.runtimeVersion) {
|
|
131
|
+
this.disposeRenderer();
|
|
132
|
+
this.createRenderer();
|
|
133
|
+
this.rendererRuntime = this.runtimeVersion;
|
|
134
|
+
}
|
|
135
|
+
// fit 前必须先摆好姿态:未 updateWorldTransform 时 getBounds 可能返回空包围盒(4.x 上实测)
|
|
136
|
+
this.applyAppearance();
|
|
137
|
+
this.poseForFit();
|
|
138
|
+
this.applyFit();
|
|
139
|
+
this.presentFirstFrame();
|
|
140
|
+
// 时长缓存:装载时扫一次
|
|
141
|
+
this.cacheDurations();
|
|
142
|
+
const info = this.describeModel(descriptor);
|
|
143
|
+
this.currentModel = descriptor;
|
|
144
|
+
if (typeof this.callbacks.onLoadComplete === 'function') {
|
|
145
|
+
try { this.callbacks.onLoadComplete(info); } catch { /* 回调异常不影响渲染 */ }
|
|
146
|
+
}
|
|
147
|
+
this.start();
|
|
148
|
+
return info;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 摆到 setup pose 并更新世界变换(fit 取包围盒的前置条件) */
|
|
152
|
+
poseForFit() {
|
|
153
|
+
if (!this.adapters || !this.skeleton) return;
|
|
154
|
+
this.adapters.skeleton.setupPose(this.skeleton);
|
|
155
|
+
this.adapters.skeleton.updateWorldTransform(this.skeleton);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 释放渲染器(换运行时/换上下文时调用) */
|
|
159
|
+
disposeRenderer() {
|
|
160
|
+
try { if (this.adapters && this.renderTarget) this.adapters.renderer.dispose(this.renderTarget); } catch { /* 忽略 */ }
|
|
161
|
+
this.renderer = null;
|
|
162
|
+
this.rendererKind = 'none';
|
|
163
|
+
this.camera = null;
|
|
164
|
+
this.rendererRuntime = null;
|
|
165
|
+
// 默认着色器是渲染器持有的对象,渲染器没了就不能再当成"可还原目标"留着
|
|
166
|
+
this.filterState = { saved: null, active: null };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 建渲染器与相机(版本差异由 loader.buildRenderer 适配) */
|
|
170
|
+
createRenderer() {
|
|
171
|
+
const built = this.adapters.renderer.create();
|
|
172
|
+
this.renderer = built.renderer;
|
|
173
|
+
this.rendererKind = built.kind;
|
|
174
|
+
this.camera = built.camera;
|
|
175
|
+
this.renderTarget = built; // 适配器 draw() 需要的目标对象
|
|
176
|
+
this.syncCameraViewport();
|
|
177
|
+
this.applyAppearance();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 把装载产物挂到引擎上(装载与重建共用) */
|
|
181
|
+
bindModel(model) {
|
|
182
|
+
// PMA 结果随模型一起绑定(rebuildTextures 走这里),否则档位显示与实际口径分裂
|
|
183
|
+
this.pmaInfo = model.pmaInfo || null;
|
|
184
|
+
this.pmaDetection = model.pmaDetection || null;
|
|
185
|
+
this.atlas = model.atlas;
|
|
186
|
+
this.textures = model.textures || [];
|
|
187
|
+
this.skeletonData = model.skeletonData;
|
|
188
|
+
this.skeleton = model.skeleton;
|
|
189
|
+
this.stateData = model.stateData;
|
|
190
|
+
this.state = model.state;
|
|
191
|
+
this.premultiplied = model.premultiplied;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 应用外观(混合钩子 + 滤镜);必须在渲染器建好之后调用 */
|
|
195
|
+
applyAppearance() {
|
|
196
|
+
// 先定皮肤、再装 blend/filter:附件都没挂上时谈混合模式没有意义。
|
|
197
|
+
// 只在"当前没有已选皮肤"时挑默认值 —— 用户手选过的皮肤(activeSkins 非空)不许被覆盖;
|
|
198
|
+
// 纹理重建(appearance.js)走这条路径时 activeSkins 已非空 ⇒ 天然跳过,重建后仍按用户的选择重放。
|
|
199
|
+
if (!this.activeSkins || !this.activeSkins.length) {
|
|
200
|
+
const picked = pickInitialSkin(this);
|
|
201
|
+
if (picked) {
|
|
202
|
+
const ok = skinApply(this, [picked]);
|
|
203
|
+
if (ok) this.log('info', `默认皮肤:${picked}(按"该皮肤下有附件"自动选中)`, 'skin');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
appearance.installBlend(this);
|
|
207
|
+
appearance.applyFilter(this);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 装载结果摘要(实现见 model-info.js) */
|
|
211
|
+
describeModel(descriptor) {
|
|
212
|
+
return describeModel({ engine: this, descriptor });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 切换 fit 模式(subject / all / animation) */
|
|
216
|
+
setFitMode(mode) {
|
|
217
|
+
return cameraOps.setFitMode(this, mode);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 读当前 fit 模式 */
|
|
221
|
+
getFitMode() {
|
|
222
|
+
return cameraOps.getFitMode(this);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 按当前 fit 模式重置相机(实现委托 camera-adapter) */
|
|
226
|
+
applyFit() {
|
|
227
|
+
return cameraOps.applyFit(this);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 把 userScale 落到相机 */
|
|
231
|
+
applyUserScale() {
|
|
232
|
+
cameraOps.applyUserScale(this);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getCameraZoom() {
|
|
236
|
+
return cameraOps.getScale(this);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setCameraZoom(scale) {
|
|
240
|
+
cameraOps.setScale(this, scale);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
applyZoom(dir) {
|
|
244
|
+
cameraOps.zoomStep(this, dir);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
applyPan(dx, dy) {
|
|
248
|
+
cameraOps.pan(this, dx, dy);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
resetCamera() {
|
|
252
|
+
cameraOps.resetCamera(this);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 首帧:更新一次状态并立即绘制(实现见 render-frame.js) */
|
|
256
|
+
presentFirstFrame() {
|
|
257
|
+
presentFirstFrame(this);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 起主循环(方案 A:引擎自建 rAF) */
|
|
261
|
+
start() {
|
|
262
|
+
startLoop(this);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 停主循环 */
|
|
266
|
+
stop() {
|
|
267
|
+
stopLoop(this);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
update(dt) {
|
|
271
|
+
if (!this.state || !this.skeleton) return;
|
|
272
|
+
// **不要在 dt 上再乘 speed**:AnimationState.update 内部已经 `delta *= this.timeScale`,
|
|
273
|
+
// 而 setSpeed 又把 timeScale 设成同一个 v ⇒ 再乘一次就变成 v²(2× 实际 4×、0.5× 实际 0.25×)。
|
|
274
|
+
// 这里取 v² 形态(同一份运行时里同时存在 `state.timeScale=v` 与 `state.update(t*this.speed)`),
|
|
275
|
+
// 属于历史行为;本轮按"与 Live2D 线性语义对齐"的要求修成线性。
|
|
276
|
+
this.state.update(dt);
|
|
277
|
+
this.state.apply(this.skeleton);
|
|
278
|
+
this.skeleton.updateWorldTransform(2);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getCurrentFps() { return this.currentFps; }
|
|
282
|
+
|
|
283
|
+
setFpsLimit(fps) { this.fpsLimit = Math.max(1, Math.min(120, Number(fps) || 60)); } // dsh-skip-quality 帧率钳制上下限 1~120,既有代码(本轮仅删 1 行死字段)
|
|
284
|
+
|
|
285
|
+
/** 跳到指定时间(实现见 animation.js) */
|
|
286
|
+
seek(t) {
|
|
287
|
+
anim.seek(this, t);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** 切换动作 */
|
|
291
|
+
setAnimation(name, loop) {
|
|
292
|
+
return anim.setAnimation(this, name, loop);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 混合时长(0..1 秒) */
|
|
296
|
+
setMixTime(t) {
|
|
297
|
+
anim.setMixTime(this, t);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 播放速率(夹 0.1–3) */
|
|
301
|
+
setSpeed(speed) {
|
|
302
|
+
anim.setSpeed(this, speed);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 循环开关 */
|
|
306
|
+
setLoop(loop) {
|
|
307
|
+
anim.setLoop(this, loop);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** 全部动作名 */
|
|
311
|
+
getAnimations() {
|
|
312
|
+
return anim.animations(this);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 当前动作时长 */
|
|
316
|
+
getCurrentDuration() {
|
|
317
|
+
return anim.currentDuration(this);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 装载后缓存时长(不每帧扫) */
|
|
321
|
+
cacheDurations() {
|
|
322
|
+
return anim.cacheDurations(this);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 动画只读快照(对照脚本用) */
|
|
326
|
+
animationSnapshot() {
|
|
327
|
+
return anim.snapshot(this);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 订阅「当前动作播完一次」(跨模型轮播的推进源)。
|
|
332
|
+
* @param {(e: {name: string, source: string}) => void} cb
|
|
333
|
+
* @returns {() => void} 取消订阅
|
|
334
|
+
*/
|
|
335
|
+
onMotionComplete(cb) {
|
|
336
|
+
return anim.onMotionComplete(this, cb);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** 皮肤名列表 */
|
|
340
|
+
getSkins() {
|
|
341
|
+
return skinList(this);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** 应用皮肤 */
|
|
345
|
+
setSkins(list) {
|
|
346
|
+
return skinApply(this, list);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 插槽过滤关键词(绘制侧由 render-frame 的 hide/restore 落地) */
|
|
350
|
+
setSlotFilter(keywords) { this.slotFilterKeywords = normalizeKeywords(keywords); }
|
|
351
|
+
|
|
352
|
+
/** 只读:这批关键词会命中多少个"当前有附件的槽"(区分"没匹配"与"隐藏成功") */
|
|
353
|
+
countFilterableSlots(keywords) { return countFilterableSlots(this, keywords); }
|
|
354
|
+
|
|
355
|
+
/** 切滤镜(非法 id 回落 original);实现在 appearance.js */
|
|
356
|
+
setFilter(id) {
|
|
357
|
+
return appearance.setFilter(this, id);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getFilter() { return this.filterId; }
|
|
361
|
+
|
|
362
|
+
/** 纹理过滤(同时重挂已上传纹理的采样参数) */
|
|
363
|
+
setTextureFilter(mode) {
|
|
364
|
+
return appearance.setTextureFilter(this, mode);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
setDebugDraw(patch) {
|
|
368
|
+
return mergeDebugDraw(this, patch);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** 切 PMA 档位 */
|
|
372
|
+
async rebuildTextures(pmaMode) {
|
|
373
|
+
return appearance.rebuildTextures(this, pmaMode);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
getRuntimeVersion() { return this.runtimeVersion; }
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 卸载当前骨架(canvas.js 的 unload 走这里)。
|
|
380
|
+
* :停循环 → 释放贴图与图集 → 清引用 → 清屏(不留上一帧残影)。
|
|
381
|
+
*/
|
|
382
|
+
clearSkeleton() {
|
|
383
|
+
this.stop();
|
|
384
|
+
this.pendingPresent = false;
|
|
385
|
+
this.disposeContent();
|
|
386
|
+
this.skeleton = null;
|
|
387
|
+
this.skeletonData = null;
|
|
388
|
+
this.state = null;
|
|
389
|
+
this.stateData = null;
|
|
390
|
+
this.currentModel = null;
|
|
391
|
+
this.animationName = '';
|
|
392
|
+
this.durations = {};
|
|
393
|
+
this.activeSkins = [];
|
|
394
|
+
if (this.gl) {
|
|
395
|
+
try {
|
|
396
|
+
this.gl.clearColor(0, 0, 0, 0);
|
|
397
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
398
|
+
} catch { /* 上下文丢失时忽略 */ }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** 释放当前模型内容(不销毁上下文) */
|
|
403
|
+
disposeContent() {
|
|
404
|
+
this.stop();
|
|
405
|
+
this.skeleton = null;
|
|
406
|
+
this.state = null;
|
|
407
|
+
for (const tex of this.textures) {
|
|
408
|
+
if (this.adapters) this.adapters.texture.dispose(tex);
|
|
409
|
+
else { try { tex.dispose(); } catch { /* 忽略 */ } }
|
|
410
|
+
}
|
|
411
|
+
this.textures = [];
|
|
412
|
+
try { if (this.atlas && this.atlas.dispose) this.atlas.dispose(); } catch { /* 忽略 */ }
|
|
413
|
+
this.atlas = null;
|
|
414
|
+
this.skeletonData = null;
|
|
415
|
+
this.skeleton = null;
|
|
416
|
+
this.stateData = null;
|
|
417
|
+
this.state = null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
dispose() {
|
|
421
|
+
this.stop();
|
|
422
|
+
this.disposeContent();
|
|
423
|
+
this.disposeRenderer();
|
|
424
|
+
this.gl = null;
|
|
425
|
+
this.mode = 'none';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// engine/pma/blend.js —— 混合口径的执行层:把运行时的 blendFunc 调用改写成 decision 要求的口径
|
|
2
|
+
//
|
|
3
|
+
// decision 来自 resolve.js(`pmaInfo.blendMode`);本文件只负责"按它改写",不再自己判定档位。
|
|
4
|
+
// 钩子**总是**装上(init 时无条件调用),是否真的改写由每次调用时读到的 blendMode 决定。
|
|
5
|
+
|
|
6
|
+
/** 防止同一上下文重复挂钩子 */
|
|
7
|
+
const HOOK_FLAG = '__bvBlendHooked';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 安装混合钩子:非预乘口径下运行时仍按预乘 blendFuncSeparate 提交,必须改写因子。
|
|
11
|
+
* @param {WebGLRenderingContext} gl
|
|
12
|
+
* @param {{pmaInfo?: {blendMode?: string}}} engine 读 pmaInfo.blendMode
|
|
13
|
+
* @returns {boolean} 是否新装了钩子
|
|
14
|
+
*/
|
|
15
|
+
export function installBlendHooks(gl, engine) {
|
|
16
|
+
const g = gl;
|
|
17
|
+
if (!g || g[HOOK_FLAG]) return false;
|
|
18
|
+
g[HOOK_FLAG] = true;
|
|
19
|
+
const glONE = g.ONE;
|
|
20
|
+
const glZERO = g.ZERO;
|
|
21
|
+
const glSRC_ALPHA = g.SRC_ALPHA;
|
|
22
|
+
const glDST_ALPHA = g.DST_ALPHA;
|
|
23
|
+
const glONE_MINUS_SRC_COLOR = g.ONE_MINUS_SRC_COLOR;
|
|
24
|
+
const glONE_MINUS_SRC_ALPHA = g.ONE_MINUS_SRC_ALPHA;
|
|
25
|
+
const raw = g.blendFuncSeparate ? g.blendFuncSeparate.bind(g) : null;
|
|
26
|
+
if (!raw) return false;
|
|
27
|
+
const mapped = (src, dst, alphaSrc, alphaDst) => {
|
|
28
|
+
const straight = !(engine.pmaInfo && engine.pmaInfo.blendMode === 'premultiplied');
|
|
29
|
+
let a; let b; let c; let d;
|
|
30
|
+
if (dst === glONE_MINUS_SRC_COLOR || alphaDst === glONE_MINUS_SRC_COLOR) {
|
|
31
|
+
a = straight ? glSRC_ALPHA : glDST_ALPHA; b = glONE_MINUS_SRC_COLOR; c = glZERO; d = glONE;
|
|
32
|
+
} else if (dst === glONE) {
|
|
33
|
+
// 加性混合的 alphaDst 是 ONE(不是 ONE_MINUS_SRC_ALPHA)——
|
|
34
|
+
// 写错会让加性部件(高光/发丝阴影层)在预乘口径下变暗发黑。
|
|
35
|
+
a = src; b = dst; c = straight ? glZERO : glONE; d = glONE;
|
|
36
|
+
} else {
|
|
37
|
+
a = src; b = dst; c = glONE; d = glONE_MINUS_SRC_ALPHA;
|
|
38
|
+
}
|
|
39
|
+
raw(a, b, c, d);
|
|
40
|
+
};
|
|
41
|
+
g.blendFuncSeparate = (src, dst, alphaSrc, alphaDst) => mapped(src, dst, alphaSrc, alphaDst);
|
|
42
|
+
g.blendFunc = (src, dst) => mapped(src, dst);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 给运行时的混合模式转换器打补丁:**Screen** 的 dest 因子强制为 `ONE_MINUS_SRC_COLOR`(769)。
|
|
48
|
+
*
|
|
49
|
+
* 为什么必须打:spine 运行时对 `BlendMode.Screen` 的默认映射不是标准 screen 混合,
|
|
50
|
+
* 而 3D 预览里 Screen 常被用在发光/高光层;映射不对 ⇒ 这些部件画成暗块/黑块
|
|
51
|
+
* (现场形态:面内竖条纹/斑块,容易被误当成 PMA 问题)。
|
|
52
|
+
* 这个 patch 必须在建渲染器时调用,否则混合模式不生效。
|
|
53
|
+
*
|
|
54
|
+
* @param {object} spine 运行时命名空间(含 webgl.WebGLBlendModeConverter 与 BlendMode)
|
|
55
|
+
* @returns {boolean} 是否新打了补丁
|
|
56
|
+
*/
|
|
57
|
+
export function patchRuntimeBlendConverter(spine) {
|
|
58
|
+
try {
|
|
59
|
+
const webgl = spine && spine.webgl;
|
|
60
|
+
const conv = (webgl && webgl.WebGLBlendModeConverter) || (spine && spine.WebGLBlendModeConverter);
|
|
61
|
+
const BlendMode = spine && spine.BlendMode;
|
|
62
|
+
if (!conv || !BlendMode || typeof conv.getDestGLBlendMode !== 'function' || conv.__bvScreenPatched) return false;
|
|
63
|
+
const SCREEN_DEST = 769; // ONE_MINUS_SRC_COLOR
|
|
64
|
+
const orig = conv.getDestGLBlendMode.bind(conv);
|
|
65
|
+
conv.getDestGLBlendMode = (mode) => (mode === BlendMode.Screen ? SCREEN_DEST : orig(mode));
|
|
66
|
+
conv.__bvScreenPatched = true;
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 该上下文是否已挂钩子 */
|
|
74
|
+
export function hasBlendHooks(gl) {
|
|
75
|
+
return !!(gl && gl[HOOK_FLAG]);
|
|
76
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// engine/pma/detect.js —— 探测:**源图**到底是不是预乘的(三条线索,只回答这一个问题)
|
|
2
|
+
//
|
|
3
|
+
// 线索优先级:
|
|
4
|
+
// ① 像素检测(有结论就用它,即使与 atlas 声明冲突也以像素为准)
|
|
5
|
+
// ② atlas 声明 `pma: true|false`(必须行首)
|
|
6
|
+
// ③ 文件名线索 -pma / _pma / .pma / -nonpma / _nonpma / -straight
|
|
7
|
+
// ④ 全部无线索 → 默认按预乘,并 warn
|
|
8
|
+
// 探测结论交给 resolve.js 的 resolvePmaInfo 决定"怎么画";本文件不碰上传与混合。
|
|
9
|
+
|
|
10
|
+
/** 像素检测参数 */
|
|
11
|
+
const PIXEL_STEP = 2;
|
|
12
|
+
const PIXEL_MIN_SAMPLES = 2048;
|
|
13
|
+
const PIXEL_TOLERANCE = 16;
|
|
14
|
+
/** 违反占比小于该值判为"已预乘" */
|
|
15
|
+
const PIXEL_VIOLATE_MAX = 0.05;
|
|
16
|
+
/**
|
|
17
|
+
* 少于该样本数不表态(返回 null)。
|
|
18
|
+
* 只统计 0 < a < 255 的像素(全透明无意义、全不透明两种模式数学上不可区分),
|
|
19
|
+
* 可用样本比总像素少得多 ⇒ 门槛从 20 降到 10,避免很多图直接"无结论"。
|
|
20
|
+
*/
|
|
21
|
+
const PIXEL_MIN_RELIABLE = 20;
|
|
22
|
+
|
|
23
|
+
/** atlas 文本里的声明:`pma: true` / `pma: false`(必须行首) */
|
|
24
|
+
export function atlasDeclaredPma(atlasText) {
|
|
25
|
+
if (!atlasText) return null;
|
|
26
|
+
const m = atlasText.match(/^pma:\s*(true|false)/m);
|
|
27
|
+
return m ? m[1] === 'true' : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 文件名线索:命中预乘/非预乘关键字,都没有返回 null */
|
|
31
|
+
export function filenamePmaHint(name) {
|
|
32
|
+
const s = String(name || '').toLowerCase();
|
|
33
|
+
if (s.includes('-pma') || s.includes('_pma') || s.includes('.pma')) return true;
|
|
34
|
+
if (s.includes('-nonpma') || s.includes('_nonpma') || s.includes('-straight')) return false;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 单张图的像素检测。
|
|
40
|
+
* @param {ImageData} imageData
|
|
41
|
+
* @param {number} step
|
|
42
|
+
* @param {number} minSamples
|
|
43
|
+
* @param {number} tolerance
|
|
44
|
+
*/
|
|
45
|
+
export function detectPmaFromImageData(imageData, step = PIXEL_STEP, minSamples = PIXEL_MIN_SAMPLES, tolerance = PIXEL_TOLERANCE) {
|
|
46
|
+
const data = imageData.data; // dsh-skip-quality 探测实现沿用既有算法,本轮未改判定逻辑
|
|
47
|
+
let samples = 0;
|
|
48
|
+
let violates = 0;
|
|
49
|
+
let useStep = step;
|
|
50
|
+
let count = Math.floor(data.length / 4 / useStep);
|
|
51
|
+
while (count < minSamples && useStep > 1) { useStep -= 1; count = Math.floor(data.length / 4 / useStep); }
|
|
52
|
+
for (let i = 0; i < data.length; i += 4 * useStep) {
|
|
53
|
+
const a = data[i + 3];
|
|
54
|
+
if (a === 0 || a === 255) continue; // dsh-skip-quality 全透明/全不透明跳过
|
|
55
|
+
samples += 1;
|
|
56
|
+
if (Math.max(data[i], data[i + 1], data[i + 2]) > a + tolerance) violates += 1;
|
|
57
|
+
}
|
|
58
|
+
if (samples < PIXEL_MIN_RELIABLE) return { isPremultiplied: null, confidence: 0, samples };
|
|
59
|
+
const ratio = violates / samples;
|
|
60
|
+
return {
|
|
61
|
+
isPremultiplied: ratio <= PIXEL_VIOLATE_MAX,
|
|
62
|
+
confidence: Math.min(1, samples / 2000), // dsh-skip-quality 置信度归一化常量
|
|
63
|
+
samples,
|
|
64
|
+
ratio,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 综合三条线索得出"这张图集到底是不是预乘",并回答**源图**是什么形态。
|
|
70
|
+
*
|
|
71
|
+
* 结论**会**参与判定(经 `resolvePmaInfo` 的 auto 档 → `needPremultiply` / `blendMode`),
|
|
72
|
+
* 所以这里的判据必须可信。当前取的是原图:`sampleSourceImageData` 把图**画到 2D canvas 再
|
|
73
|
+
* `getImageData`**(不经 WebGL ⇒ **不受 `UNPACK_PREMULTIPLY_ALPHA_WEBGL` 影响**),
|
|
74
|
+
* 于是「RGB 超过 A 的像素占比」就能反映源图本身的 alpha 形态。
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ 反面教训:**不要用 `readPixels` 读回的数据反推源图**。
|
|
77
|
+
* `readPixels` 读的是 **GPU 上的数据**,已经过上传口径处理 —— 开关一变,同一张图的数字就变,
|
|
78
|
+
* 与源图是否预乘无关,那是循环论证。早前那版实现踩过,实测记录(char021301.png,
|
|
79
|
+
* 375,718 个半透明像素)的正确解读是:
|
|
80
|
+
* · 按 `UNPACK=false`(原样)上传后读回 → r>a 占 **89.32%** ⇒ 源图是**直通**
|
|
81
|
+
* · 按 `UNPACK=true` 上传后读回 → r>a 占 **0%** ⇒ alpha 被乘了进去,
|
|
82
|
+
* 这正是 `upload` 路径**想要**的效果;当时把它读成"又乘了一次",
|
|
83
|
+
* 而那个说法成立的前提是"源图已预乘",与上一行的读数直接矛盾,结论下错了。
|
|
84
|
+
*
|
|
85
|
+
* 保留本探测的理由:① 面板四行诊断展示(Atlas 声明 / 像素检测 / 直通支持 / 实际生效)
|
|
86
|
+
* ② pma.resolve / pma.detect 日志,换资产或换运行时后仍是重要线索。
|
|
87
|
+
*
|
|
88
|
+
* @param {{atlasDeclared: boolean|null, filenameHint: boolean|null, samples: ImageData[]}} ctx 三条线索
|
|
89
|
+
* @param {Function} [log] 诊断日志(缺省吞掉)
|
|
90
|
+
* @returns {{atlasDeclared: boolean|null, filenameHint: boolean|null, pixelDetected: boolean|null,
|
|
91
|
+
* finalIsPremultiplied: boolean, reason: string}} 最终结论与判据说明
|
|
92
|
+
*/
|
|
93
|
+
export function detectPma({ atlasDeclared, filenameHint, samples }, log = () => {}) {
|
|
94
|
+
for (const img of (Array.isArray(samples) ? samples : samples ? [samples] : [])) {
|
|
95
|
+
const m = detectPmaFromImageData(img);
|
|
96
|
+
if (m.isPremultiplied === null) continue;
|
|
97
|
+
const reason = [`像素采样检测(${m.samples} 样本,违反占比 ${((m.ratio ?? 0) * 100).toFixed(1)}%)`];
|
|
98
|
+
if (atlasDeclared !== null && atlasDeclared !== m.isPremultiplied) {
|
|
99
|
+
reason.push(`与 atlas 声明 pma=${atlasDeclared} 冲突,以像素为准`);
|
|
100
|
+
log('warn', `atlas 声明 pma=${atlasDeclared},但像素检测为${m.isPremultiplied ? '已预乘' : '非预乘'},以像素为准`, 'pma');
|
|
101
|
+
}
|
|
102
|
+
if (filenameHint !== null && filenameHint !== m.isPremultiplied) reason.push('与文件名线索冲突,以像素为准');
|
|
103
|
+
return { atlasDeclared, filenameHint, pixelDetected: m.isPremultiplied, finalIsPremultiplied: m.isPremultiplied, reason: reason.join(';') };
|
|
104
|
+
}
|
|
105
|
+
if (atlasDeclared !== null) {
|
|
106
|
+
// atlas 未声明(null)⇒ 按"已预乘"处理(见上:多乘的代价远大于少乘)
|
|
107
|
+
return {
|
|
108
|
+
atlasDeclared, filenameHint, pixelDetected: null,
|
|
109
|
+
finalIsPremultiplied: atlasDeclared === null ? true : atlasDeclared,
|
|
110
|
+
reason: `像素不足,回退 atlas 声明 pma=${atlasDeclared === null ? '未声明→按已预乘' : atlasDeclared}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (filenameHint !== null) {
|
|
114
|
+
return { atlasDeclared, filenameHint, pixelDetected: null, finalIsPremultiplied: filenameHint, reason: `像素/atlas 无结论,用文件名线索 ${filenameHint ? '-pma' : '-nonpma'}` };
|
|
115
|
+
}
|
|
116
|
+
log('warn', 'PMA 无法自动检测,默认按预乘处理', 'pma');
|
|
117
|
+
return { atlasDeclared: null, filenameHint: null, pixelDetected: null, finalIsPremultiplied: true, reason: '无可用线索,默认按预乘处理' };
|
|
118
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// engine/pma/index.js —— pma 子系统的对外出口
|
|
2
|
+
//
|
|
3
|
+
// 调用方仍从 `../engine/pma.js` 导入(那个文件只是转发到本目录),导入路径一个字都没变。
|
|
4
|
+
// 一条线读完的顺序:detect(源图是不是预乘)→ resolve(唯一决策点)→ upload / blend(两个执行层)。
|
|
5
|
+
export { atlasDeclaredPma, filenamePmaHint, detectPmaFromImageData, detectPma } from './detect.js';
|
|
6
|
+
export { resolvePmaInfo, shouldPremultiplyOnUpload, planTextureUpload, planBlend } from './resolve.js';
|
|
7
|
+
export { installBlendHooks, patchRuntimeBlendConverter, hasBlendHooks } from './blend.js';
|
|
8
|
+
export { uploadTextures } from './upload.js';
|