feat: TS 构建管线、Spine 抓取器与 wallpapers/ 唯一真相来源
把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。
- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
(.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)
同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)
pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
This commit is contained in:
1 parent
b8eee05d78
commit
3f11426964
297 files changed
+216627
-1926
No files matched your search
@@ -0,0 +1,161 @@
|
||||
# 规格:场景播放器(一档壁纸 = 一整页场景)
|
||||
|
||||
> ⚠ 本文写于 ADR 0008 之前:下面的 preset 示例里 `./images/…`、`./effects/<名>/…` 是**当时的布局**,
|
||||
> 现在读作 `./scene/…`、`./spines/<名>/…`(字段与规则没变,只是目录名换了)。
|
||||
|
||||
> 状态:**已落地**(2026-09-20)。抓取侧 6 页 74.15 MB 已落 staging,`verify` 绿。
|
||||
> 门:`node tools/checks/verify-scene-player.mts`(夹具绿 + atlas 指错必红 + 还原重新变绿)。
|
||||
> 本文只覆盖播放器与预设 schema,不含 promote 与文案。
|
||||
|
||||
## 落地记录(与规格的三处差异)
|
||||
|
||||
1. **取景参考矩形改成"所有 part 变换后的并集包围盒"**,不是 `ui` 矩形。
|
||||
页面相机看向的是**场景原点**,而 `ui` 矩形的原点是它的左下角——直接拿它当可见区会把整个场景推向右上。
|
||||
旧项目用的也是并集包围盒。`ui` 只留作字段,不再参与取景。
|
||||
2. **`flipY` 的实测结论**:`position[1]` 取反(默认)即可让 kv45 的场景正立;
|
||||
`scaleY` **不取反**(骨架本身在 spine-webgl 下渲染就是正的)。实测截图见
|
||||
`tools/.cache/scene-player-shot.png`。
|
||||
3. **glTF 网格平面的尺寸已补上**(同日):抓取期解 bundle 里的 `geometries` 表(`position.array` 是扁平 xyz),
|
||||
算顶点包围盒 → `geometrySize` / `geometryCenter` 落进 `scene.json` 与 preset。实测这些网格都是中心为零的四边形;
|
||||
kv45 的 `w22_slg` 其实是 `geometry.type: 2`(自带 config 尺寸),不受影响。
|
||||
|
||||
## 仍未做(按价值排序)
|
||||
|
||||
1. **modifier(`wiggle` / `BEZIER_PARTICLE` 粒子 / `CSS3DObject` DOM 层)**——真正的观感缺口:
|
||||
粒子与摆动现在一律静态化。这是一个 feature 级工程(页面插件系统 + 贝塞尔粒子参数 + wiggle 数学),
|
||||
值得单独立规格。
|
||||
2. **纯色平面(`kind: "solid"`)不画**:它没有贴图,而 SceneRenderer 只提供 `drawTexture`/`drawSkeleton`
|
||||
(传 undefined 会直接崩,已修)。数量进 `__sceneDebug.solids`,验收断言它能被看见;
|
||||
要真画就得走 `ShapeRenderer` 那条路。
|
||||
3. **页面 material 的混合模式**:实测 `blending` 只有 `1`(Normal)与 `2`(Additive,6 页共 1 处)。
|
||||
`PolygonBatcher.setBlendMode(mode, pma)` 存在、映射也清楚(three.js 枚举 → spine BlendMode),
|
||||
但收益只有一个节点,**暂不做**。
|
||||
4. 旋转合成(抓取期与运行时都只记录)。
|
||||
5. promote 进 `wallpapers/`(需要壁纸文案与音源清单)。
|
||||
|
||||
## 一、目标
|
||||
|
||||
让一档壁纸能播放"整页场景":**N 具骨架 + M 块贴图平面**,按抓取期定下的世界变换与绘制层级合成。
|
||||
单骨架路径(`spineConfig`,xilian / kv37)**一行不动**——它被 `tools/shots/baseline-pre-refactor/`
|
||||
的冻结基线钉着,`verify-visual-equivalence.mts` 靠它判等。
|
||||
|
||||
非目标(本轮):
|
||||
|
||||
- 不换 vendored 包(`src/vendor/spine-player.js` 是 spine-ts 4.2 线)、不引三方依赖;
|
||||
- 不做几何平面的动画(`BEZIER_PARTICLE` 粒子等先静态化);
|
||||
- 不做 promote(等本规格定案;验证走临时夹具,不碰 `wallpapers/`);
|
||||
- 不做旋转合成(与抓取期一致:`rotation` 只记录不参与,`stats.rotatedNodes` 报数量)。
|
||||
|
||||
## 二、数据契约(抓取侧已产出,别再改形状)
|
||||
|
||||
`tools/downloader/_out/<游戏>/<页面>/`:
|
||||
|
||||
| 文件 | 内容 |
|
||||
| --- | --- |
|
||||
| `scene.json` | `{version, id, ui, camera, parts[], stats}`;part = `{kind, id, order, renderOrder, position[3], scale[3], localPosition?, localScale?, rotation?, geometryType?, modifiers?, runtime?}` |
|
||||
| `spine/<id>/<id>.json` | 骨架(`skeleton.images` 已归一化为空串) |
|
||||
| `spine/<id>/<id>.atlas` | 第一行 = 贴图页名,与落盘文件名逐字一致 |
|
||||
| `spine/<id>/meta.json` | `spine` 版本 / `animations` / `skins` / `pages` / `originalImages` |
|
||||
| `scene/<id>.<ext>` | `kind: "image"` 的平面贴的图 |
|
||||
| `page.json` | 来源 URL、入口脚本、场景清单、警告 |
|
||||
|
||||
- `kind: "solid"`(`USE_TEXTURE == 0` 的纯色平面)**没有资源**,只按几何尺寸画一块颜色。
|
||||
- `"runtime": true` 的 image part 是**由骨架渲染进贴图缓冲**的平面(`cacheContainer`),
|
||||
运行时不下载也不画(它本来就是骨架的中间结果)。
|
||||
- 世界变换语义:`world_position = parent_position + parent_scale ⊙ local_position`、
|
||||
`world_scale = parent_scale ⊙ local_scale`;`order` = 树序遍历序(绘制层级)。
|
||||
|
||||
## 三、预设 schema
|
||||
|
||||
**单骨架(现状,不动)**
|
||||
|
||||
```json
|
||||
{
|
||||
"backgroundImage": "./images/ava.jpg",
|
||||
"spineConfig": { "jsonUrl": "./effects/x.json", "atlasUrl": "./images/x.atlas",
|
||||
"animation": "idle", "viewport": { "padLeft": "-25%" } }
|
||||
}
|
||||
```
|
||||
|
||||
**场景(新)**
|
||||
|
||||
```json
|
||||
{
|
||||
"backgroundImage": "./images/cover.jpg",
|
||||
"sceneConfig": {
|
||||
"ui": [2500, 1080],
|
||||
"parts": [
|
||||
{ "kind": "image", "id": "main_sky_jpg", "image": "./images/main_sky_jpg.jpg",
|
||||
"order": 0, "renderOrder": 0, "position": [0, 0, 0], "scale": [1, 1, 1] },
|
||||
{ "kind": "spine", "id": "main_nike",
|
||||
"jsonUrl": "./effects/main_nike/main_nike.json",
|
||||
"atlasUrl": "./images/main_nike/main_nike.atlas",
|
||||
"animation": "眨眼",
|
||||
"order": 3, "renderOrder": 0, "position": [-120.5, 480.25, 0], "scale": [1.09, 1.09, 1] }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
约定:
|
||||
|
||||
- `spineConfig` 与 `sceneConfig` **二选一**;同时出现时**构建期直接报错**(不做静默优先级,歧义会烂在产物里)。
|
||||
- part 的摆放与层级**由构建从 `scene.json` 抄进 `preset.js`**(构建期烘焙),运行时只读 `preset.js`:
|
||||
分发自包含、`check:dist` 已经会校验 preset.js 引用的文件都在,且避免运行时多一次 sidecar 探测。
|
||||
`scene.json` 留在抓取器 `_out/` 作留档与重跑凭据。
|
||||
- 每个 part 的路径都用现有 `asset()`(`import.meta.url` 推导)解析;缺文件由构建 fail-fast + `check:dist` 兜住。
|
||||
- `animation` 缺省 = 该骨架的第一个动画(清单在 `spine/<id>/meta.json` 的 `animations` 里,抓取期已记)。
|
||||
- `Preset` 接口新增可选 `sceneConfig`;`generatePresetModule` 新增一个分支(与 `spineConfig` 对称)。
|
||||
|
||||
## 四、运行时
|
||||
|
||||
新增 `src/runtime/scene-controller.ts`,**不改** `spine-controller.ts`:
|
||||
|
||||
- 画布与渲染器:用 vendored 包已导出的 `ManagedWebGLRenderingContext` + `SceneRenderer`
|
||||
自建(已核实这些名字都在 UMD 导出表里:`SkeletonJson` / `TextureAtlas` /
|
||||
`AtlasAttachmentLoader` / `AnimationState` / `AssetManager` / `SceneRenderer` / `SpineCanvas` /
|
||||
`ResizeMode` / `OrthoCamera` / `ManagedWebGLRenderingContext`)。
|
||||
**不经过 `spine.SpinePlayer`**——`config.draw` 只在宿主骨架画完之后调用(vendored 包 15128 行),
|
||||
画不出"位于宿主下面的层",而 nico-tea 的 `scene_main` 第一件就是贴图平面。
|
||||
- 加载:每个 part 一个 `AssetManager`(或 `SkeletonJson` + `TextureAtlas` + `AtlasAttachmentLoader`)。
|
||||
贴图直通 alpha、`premultipliedAlpha=false`(沿用现有渲染约定)。
|
||||
- 每帧:逐 spine part `AnimationState.update(delta)` → `apply(skeleton)` →
|
||||
`updateWorldTransform(Physics.update)`;然后 `renderer.begin()` → 按 `order` 依次
|
||||
`drawTexture(...)`(image/solid)/ `drawSkeleton(skeleton, pma, ..., transform)` → `renderer.end()`。
|
||||
- fps 门控:WE 不替壁纸限流,沿用现有"包一层排帧函数"的做法(自持循环后更简单)。
|
||||
- 取景:`ui` 矩形(如 2500×1080)按等比 contain 映射到画布,再套现有背景比例链
|
||||
(`viewport-fitter.frameForAspect`)——与单骨架的构图行为保持一致,`framing: "author"` 仍然生效。
|
||||
- y 轴:页面是 y-up(three.js),spine 是 y-down;抓取期 `scene.json` 的坐标保持页面原样,
|
||||
方向在渲染时统一处理(先按"整体 y 取反"实现,实测后定)。
|
||||
- 就绪与失败:`window.__sceneDebug = { parts, loaded, errors, framing }` 供验收断言;
|
||||
加载失败**显式记录**,不静默降级。
|
||||
|
||||
## 五、验证(不碰 `wallpapers/`)
|
||||
|
||||
1. `tools/checks/verify-scene-player.mts`(新):
|
||||
- 临时目录里放一份**生成的** scene preset + staged 的 `kv45`(2.8 MB、4 骨架 + 1 平面,最小样本)
|
||||
的资产拷贝 + 构建出的 `dist/scripts/spine-player.js`;
|
||||
- 起一个临时静态服务(复用 `tools/serve.mjs` 的思路),用现有 CDP 管线(`tools/cdp.mjs`)打开;
|
||||
- 断言:`__sceneDebug.parts` 全部 loaded、`errors` 为空、0 未捕获异常、画布非空、
|
||||
取景矩形包含 parts 的变换后包围盒。
|
||||
2. **先证明它会红**:故意把某个 part 的 `atlasUrl` 指错 → 断言错误被报出来、且 `loaded < parts`。
|
||||
3. 现有门必须全绿:`pnpm check`(含 `check:dist` 自包含)与 `verify-visual-equivalence`
|
||||
(单骨架基线,**不许动**)。
|
||||
|
||||
## 六、风险
|
||||
|
||||
| 风险 | 处置 |
|
||||
| --- | --- |
|
||||
| y 轴方向 / 页面相机(type 1 透视 fov、type 2 正交)不一致 | 先用正交近似;拿 nico-tea(camera type 1)实测,必要时把相机参数也抄进 preset |
|
||||
| 页面 material 的混合模式(`【相加】`/`【滤色】` 等) | 先支持 NORMAL / ADD,其余记进 `page.json` 的 warnings,不假装还原 |
|
||||
| 粒子与 modifier(`BEZIER_PARTICLE` / `wiggle` / `CSS3DObject`) | 本轮静态化,只画 diffuse 贴图 |
|
||||
| 单骨架路径被误伤 | 新控制器独立文件;预设二选一由构建期报错保证;基线对拍门 |
|
||||
|
||||
## 七、待你定的三件事
|
||||
|
||||
1. **part 变换抄什么**:直接抄 `scene.json` 的世界变换(我建议;简单、且抓取期已按引擎语义算过),
|
||||
还是抄 local + 树结构让运行时自己合成(更忠实,但运行时更复杂、且要复刻引擎语义)?
|
||||
2. **`backgroundImage` 从哪来**:场景里没有"整页背景"这一件,但 WE 面板预览与单骨架路径都要它。
|
||||
建议抓取期另挑一张全屏图当封面(或把场景第一张全屏 image part 复用为 backgroundImage)。
|
||||
3. **验证样本**:先用 staged 的 kv45 走临时夹具(不碰 `wallpapers/`),还是直接 promote 一页
|
||||
(那就需要你给壁纸的 `name` / `title` / `description` / 音源清单)?
|
||||
Reference in new issue
Block a user