把项目从「手写 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 处引用自包含。
10 KiB
10 KiB
规格:场景播放器(一档壁纸 = 一整页场景)
⚠ 本文写于 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 与文案。
落地记录(与规格的三处差异)
- 取景参考矩形改成"所有 part 变换后的并集包围盒",不是
ui矩形。 页面相机看向的是场景原点,而ui矩形的原点是它的左下角——直接拿它当可见区会把整个场景推向右上。 旧项目用的也是并集包围盒。ui只留作字段,不再参与取景。 flipY的实测结论:position[1]取反(默认)即可让 kv45 的场景正立;scaleY不取反(骨架本身在 spine-webgl 下渲染就是正的)。实测截图见tools/.cache/scene-player-shot.png。- glTF 网格平面的尺寸已补上(同日):抓取期解 bundle 里的
geometries表(position.array是扁平 xyz), 算顶点包围盒 →geometrySize/geometryCenter落进scene.json与 preset。实测这些网格都是中心为零的四边形; kv45 的w22_slg其实是geometry.type: 2(自带 config 尺寸),不受影响。
仍未做(按价值排序)
- modifier(
wiggle/BEZIER_PARTICLE粒子 /CSS3DObjectDOM 层)——真正的观感缺口: 粒子与摆动现在一律静态化。这是一个 feature 级工程(页面插件系统 + 贝塞尔粒子参数 + wiggle 数学), 值得单独立规格。 - 纯色平面(
kind: "solid")不画:它没有贴图,而 SceneRenderer 只提供drawTexture/drawSkeleton(传 undefined 会直接崩,已修)。数量进__sceneDebug.solids,验收断言它能被看见; 要真画就得走ShapeRenderer那条路。 - 页面 material 的混合模式:实测
blending只有1(Normal)与2(Additive,6 页共 1 处)。PolygonBatcher.setBlendMode(mode, pma)存在、映射也清楚(three.js 枚举 → spine BlendMode), 但收益只有一个节点,暂不做。 - 旋转合成(抓取期与运行时都只记录)。
- 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
单骨架(现状,不动)
{
"backgroundImage": "./images/ava.jpg",
"spineConfig": { "jsonUrl": "./effects/x.json", "atlasUrl": "./images/x.atlas",
"animation": "idle", "viewport": { "padLeft": "-25%" } }
}
场景(新)
{
"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/)
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 的变换后包围盒。
- 临时目录里放一份生成的 scene preset + staged 的
- 先证明它会红:故意把某个 part 的
atlasUrl指错 → 断言错误被报出来、且loaded < parts。 - 现有门必须全绿:
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 贴图 |
| 单骨架路径被误伤 | 新控制器独立文件;预设二选一由构建期报错保证;基线对拍门 |
七、待你定的三件事
- part 变换抄什么:直接抄
scene.json的世界变换(我建议;简单、且抓取期已按引擎语义算过), 还是抄 local + 树结构让运行时自己合成(更忠实,但运行时更复杂、且要复刻引擎语义)? backgroundImage从哪来:场景里没有"整页背景"这一件,但 WE 面板预览与单骨架路径都要它。 建议抓取期另挑一张全屏图当封面(或把场景第一张全屏 image part 复用为 backgroundImage)。- 验证样本:先用 staged 的 kv45 走临时夹具(不碰
wallpapers/),还是直接 promote 一页 (那就需要你给壁纸的name/title/description/ 音源清单)?