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,498 @@
|
||||
# 透视场景对齐:现状与下一步(交接)
|
||||
|
||||
> 目标见会话 goal:让原神 4 页(camera type 1 + 真实景深)构图正确、与页面真实渲染对拍一致,
|
||||
> 补一条能抓住取景/投影算错的门,然后撤掉 promote 对透视场景的默认拦截。
|
||||
|
||||
## 关键认识(2026-09-20 第 1 轮末):页面是**多动画 + 时间线驱动**的,静态快照对不上
|
||||
|
||||
用户提出"是不是和同个场景多个动画及其前后播放顺序有关"——**证实了,而且是两层**:
|
||||
|
||||
1. **每个 spine 节点自带播放指令**(`spine:{id, defaultAnimation, skin, timeScale, otherAnimation:[{track,animation}]}`)。
|
||||
nico-tea 的 35 个 spine 节点里 8 个带 `defaultAnimation`;`scene_main` 里有 3 个:
|
||||
`main_d_books → "in0"`、`main_nike → skin "b"`、`main_nike → skin "a"`。
|
||||
我原来一律播 `data.animations[0]` + `default` 皮肤——姿态与页面不同(很多骨架的第一个动画是入场动画 `in`)。
|
||||
2. **引擎有 timelineSetting**:按时间驱动 38 条属性,例如
|
||||
`layout.position`、`img.scale`、`bei_e.position`、`inout.position`、`jiulaoshi.position`、
|
||||
`main_slg.material.uniforms.opacity.value`、`main_btn.material.uniforms.opacity.value`。
|
||||
**也就是说场景树里的 position/scale 只是某一时刻的值**——页面任意一帧的构图由时间线决定。
|
||||
|
||||
结论:**"与页面基准图一致"必须先定义"哪一帧"**。现在拿静态场景树 + 骨架首动画去对拍一张动态页面的截图,
|
||||
本质上是在比两个不同时刻的画面。这是继续推进前要先定的事。
|
||||
|
||||
## 已落地
|
||||
|
||||
| 项 | 状态 |
|
||||
| --- | --- |
|
||||
| billboard 投影 `s = 1/(tan(fov/2)·(camZ−z))` | ✓ |
|
||||
| 相机进预设 schema(`camera:{type,fov,position}`) | ✓ |
|
||||
| 透视取景 = 视锥 = UI 矩形(投影单位 `uiW·s0 × uiH·s0`) | ✓ |
|
||||
| **按相机深度从远到近绘制**(复刻页面的 z 缓冲) | ✓ **本轮关键修复**(构图由此出现) |
|
||||
| **带旋转的倾斜面片不画**(`rotatedSkipped` 计数) | ✓(全场景只有 2 个,正是撑爆画面的那两块) |
|
||||
| **按 `defaultAnimation` / `skin` / `timeScale` 播** | ✓ 已实现(抓取 → 预设 → 运行时);**视觉效果尚未验证**(需重启 dev 服重拍) |
|
||||
| 诊断面 `content` / `solids` / `rotatedSkipped` | ✓ |
|
||||
| promote 拦透视场景 | ✓(`--allow-perspective` 放行) |
|
||||
| 页面真实渲染基准 | `tools/.cache/page-truth-2-main.png`(`.scratch/page-truth.mjs`) |
|
||||
|
||||
已**否证**:套 `cameraAdaptScreen` 的 `zoom = uiHeight/canvasHeight`(改完更放大,那是正交相机那条路用的)。
|
||||
|
||||
## 下一步(顺序)
|
||||
|
||||
1. **重启 dev 服并重拍**,确认 `defaultAnimation`/`skin` 那一步的视觉影响(截图 SHA 未变 = 没生效)。
|
||||
2. **定义基准时刻**:与用户确认对拍的是哪一帧(进入后稳定态?时间线某一时刻?)。
|
||||
建议:先把**时间线不驱动的属性**(未被 timelineSetting 点名的节点)对齐,被驱动的那些单独处理。
|
||||
3. **先钉身份**:加"只画第 N 件"的调试开关,逐件与基准图对照,确定每个 part 对应画面里的什么
|
||||
(现在连"城堡是 `main_zw_a`"都只是猜)。
|
||||
4. 补门:内容中心落在视锥中心附近、内容/视锥尺寸比在合理区间。
|
||||
5. 最后做倾斜面片的真四边形(4 角点过旋转 + 透视,用 `PolygonBatcher`)。
|
||||
|
||||
## 不要做的事
|
||||
|
||||
- 别调经验系数(权威值能从 bundle 读到,本轮就是这样读到 `aspect = uiWidth/uiHeight` 与 timelineSetting 的)。
|
||||
- 别在门通过之前 promote 透视场景。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 2 轮补充(2026-09-20)
|
||||
|
||||
**门已落地**:`tools/checks/verify-scene-player.mts` 新增透视样本(staged nico-tea),
|
||||
在门里**独立算一遍**期望取景矩形(`s0 = 1/(tan(fov/2)·camZ)`、可见区 = `ui × s0`、再按画布比例 contain),
|
||||
与运行时 `__sceneDebug.framing` 断言(相对容差 1e-6)。实测通过:4.6297 × 2.6042 两边一致。
|
||||
这条门能抓住两类**真实发生过**的错:视锥用画布比例(差 1.30×)、取景用内容包围盒(差 4×)。
|
||||
|
||||
**角色为什么看不见**:`main_nike` 的页面坐标 y = −804.5、z = 849.7 → 投影后 y ≈ 2.67(半视锥是 1.30),
|
||||
**整块在画面外**。所以本轮落地的 `defaultAnimation`/`skin`(`main_nike` 皮肤 b/a、`main_d_books` 播 `in0`)
|
||||
虽然真的生效了,画面却**一个像素都没变**(截图 SHA 未变,已用 `Network.setCacheDisabled` 排除缓存因素)。
|
||||
结论:**必须先解决"时间线把角色移进画面"这一层**,否则皮肤/动画这类改动无法验证。
|
||||
|
||||
**一个坑**:`preset.js` 是模块脚本,浏览器会复用缓存副本——改了预设却截出同一张图,白等一轮。
|
||||
验收/截图脚本要 `Network.setCacheDisabled(true)` 或加 `?v=<timestamp>`。
|
||||
|
||||
**flaky 门**:`verify-scene-player` 曾报"失败 1 处"、重跑通过(疑似"隐藏画布前后截图不同"那条的时序)。
|
||||
flaky 的门比没有门更糟,下一轮要加 settle/重试。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 3 轮补充(2026-09-20):时间线的真实结构,以及读"实时世界坐标"的正确姿势
|
||||
|
||||
**时间线在哪**:nico-tea 的时间线表在**入口 bundle**(不在 vendors),由嵌套 merge 拼出来:
|
||||
|
||||
```js
|
||||
mt = Fe(Fe(Fe(Fe(Fe({}, {loading:{type:1,start:0,name:"loading",loop:false,
|
||||
cues:[{frame:91,method:"stop,退场同时播放page_cut动画"}], clips:[…]}}), …), …), …), …)
|
||||
```
|
||||
|
||||
- 顶层时间线名:`loading` / `page_cut` / …(`loading` 是**入场序列**,第 91 帧有"退场并播 page_cut"的 cue)。
|
||||
- **clip 可以嵌套**:`type:1` 的 clip 自带 `name`(如 `fbx`)与内层 `clips`,所以
|
||||
"找 position 轨道"必须**递归**走 `clips`,只扫顶层会漏(我第一次就只捞到一个 `page_cut`)。
|
||||
- 全页共 **16 个 clip、14 条 `.position` 轨道**:`layout` / `bg` / `bei_e` / `bei_f` / `bei_g` / `xl` /
|
||||
`jiulaoshi` / `inout` / `play` / `d` / `c` / `b` / `a` …——**正是把角色与道具移进画面的那批**。
|
||||
- 解析嵌套 merge 时 `Fe` 是**深合并**,用 `Object.assign` 顶替只能得到最后一个键(实测只出 `page_cut`)。
|
||||
|
||||
**读实时世界坐标:`__vue__` 这条路走不通**。生产版 Vue 2.7 不挂 `__vue__`(实测 `hasVue:false`)。
|
||||
正确姿势是**在页面脚本执行前注入**(CDP `Page.addScriptToEvaluateOnNewDocument`):
|
||||
- 包一层 `PerspectiveCamera` 构造,抓到相机实例(读它的 `fov/aspect/zoom/position` 就是权威值);
|
||||
- 包 `Object3D.prototype.updateMatrixWorld`(或 `add`),按 `name` 建一张 `name → 世界矩阵` 表,
|
||||
页面跑起来后直接读 `main_nike` / `main_zw_a` 等的**实时世界坐标**。
|
||||
|
||||
**为什么值得**:这一招能一次性回答三个悬着的问题——相机真实参数、每个 part 的真实世界坐标、
|
||||
时间线到底把谁移动到了哪里。有了它,"静态场景树 vs 页面某一帧"的差就变成了可测的数字,
|
||||
不用再靠猜或靠解析 48KB 的嵌套时间线字面量。
|
||||
|
||||
**建议顺序**(下一轮):先做注入钩子拿实时世界坐标 → 用它校正投影/摆放 → 再谈时间线的逐帧复刻。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 4 轮补充:钩 WebGL 拿到**权威相机参数**(技术路线确定)
|
||||
|
||||
`__vue__` 走不通之后,换了个一定能成的办法:**在页面脚本执行前注入**
|
||||
(CDP `Page.addScriptToEvaluateOnNewDocument`),包住
|
||||
`WebGLRenderingContext.prototype.getUniformLocation / uniformMatrix4fv`,
|
||||
把引擎每帧上传的矩阵按 uniform 名字记下来。实测抓到 8000 次上传、`projectionMatrix` 3 种、
|
||||
`modelViewMatrix` 6014 种。三种投影矩阵:
|
||||
|
||||
| # | 形状 | 读出来的东西 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 单位阵(m0=m5=1, m14=-1) | 某个中间 pass |
|
||||
| 2 | **m0=1.536014, m5=3.555588, m11=-1, m10=-1.001001, m14=-20.01** | **fov = 31.417°(竖直)、aspect = m5/m0 = 2.3148 = 2500/1080**、near=10 / far≈20000 |
|
||||
| 3 | m0=0.001042, m5=0.001852(正交) | 视口 **1920×1080**(宽高比 1.7778 = 画布比例)——**另一条 2D/UI pass** |
|
||||
|
||||
**结论一**:#2 证实了我这条路的取景模型——**透视相机的 fov 是竖直 31.417°、aspect 就是 `uiWidth/uiHeight`**
|
||||
(不是画布比例)。这与第 1 轮从 bundle 里读到的 `new PerspectiveCamera(a.fov, s/l, …)` 互相印证。
|
||||
|
||||
**结论二(新)**:引擎还有**第二条正交 pass,跨度 1920×1080**(画布比例、1080 高)。
|
||||
也就是说页面同时活在两个坐标空间里:3D 场景用 2500×1080 的透视空间,
|
||||
UI/2D 层用 1920×1080 的正交空间。**场景节点属于哪个空间,是下一步要判的**——
|
||||
`main_nike` 的 y=−804 在 2500×1080(y∈±540)里本来就在画面外,在 1920×1080 里更在外面,
|
||||
所以它必然是被时间线移进来的,这条没变。
|
||||
|
||||
**结论三**:live 的 `modelViewMatrix` 里第一条是 `m14 = −1800`(相机 z≈**1800**),
|
||||
而场景数据写的是 `position:[0,0,1920]`。差 6.7%,不是倍率问题,但值得记一笔。
|
||||
|
||||
**技术路线(下一轮直接用)**:这套注入钩子还能继续往下挖——
|
||||
- 按 draw call 关联 `modelViewMatrix`(配合 `drawElements` 计数)就能拿到**每个对象的世界坐标**;
|
||||
- 钩 `requestAnimationFrame` 可以**冻结时间源**,拿到确定的基准帧(与 `page-mirror` 规格是同一件事的两半)。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 5 轮:一次"看起来成功"的假修复,与它带来的真结论
|
||||
|
||||
**试了什么**:从 live `modelViewMatrix` 里读到一条 `m14 = −2648.534`、缩放 1.379,
|
||||
按"相机在 z=1800"反推出对象世界 z = **−848.5**,而场景树里 `main_nike` 的合成 z = **+849.66**——
|
||||
大小几乎相等、符号相反。于是把深度改成 `camZ + z`(翻符号)。
|
||||
|
||||
**结果**:画面**戏剧性变好**——角色、茶桌、甜品架、城堡、标题全出现了,
|
||||
构图第一次接近基准图。很容易就此收工。
|
||||
|
||||
**为什么必须撤回**:
|
||||
1. **物理相反**:翻过来后天空(z=−1414)深度 = 506(**最近**)、角色(z=+849)深度 = 2770(**最远**),
|
||||
等于把天空放到相机前面、角色放到后面。与"天空在最后、角色/桌面在前"直接矛盾。
|
||||
2. **包围盒证据**:`content` 从 18.9×11.8 涨到 **40.9×17.4**(帧的 8.8 倍),
|
||||
说明大量元素被错误放大——只是被巨大的天空盖住、看起来"丰富"。
|
||||
3. 角色之所以出现,是翻符号后它的缩放变小、原点恰好落在画面上沿内侧一点点——**巧合,不是对齐**。
|
||||
4. 那条 `modelViewMatrix` **很可能属于另一条正交 pass**(该 pass 相机在 z=1800,对象 z 是 UI 层自己的深度),
|
||||
拿它推场景 z 的符号本来就不成立。
|
||||
|
||||
已撤回,`depth = camZ − z` 保持不变。
|
||||
|
||||
**真结论(这一轮的收获)**:
|
||||
- 透视相机的 fov/aspect 已被 live 投影矩阵钉死(31.417° 竖直、aspect = 2500/1080)——**取景模型是对的**。
|
||||
- 剩下唯一的大缺口仍是**时间线**:`main_nike` 的静态 y = −804.5,投影后 y ≈ +2.67(半视锥 1.30),
|
||||
必然在画面外;基准图里它居中,说明入场时间线把它移动了约 **+800 页面单位**。
|
||||
这与"14 条 `.position` 轨道"完全吻合。
|
||||
|
||||
**下一步(不再猜)**:用同一套注入钩子,按 draw call 关联 `modelViewMatrix` 与 `drawElements`,
|
||||
**直接读出每个对象在页面里的实时世界坐标**(尤其 `main_nike`),
|
||||
拿它当基准去校正"静态摆放 + 时间线位移"这条链。坐标一旦可读,对齐就是测量问题,不是猜测问题。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 6 轮:读到页面的**实时世界坐标**(注入钩子按 draw call 关联 modelViewMatrix)
|
||||
|
||||
做法:注入时同时包住 `uniformMatrix4fv`(只记 `modelViewMatrix`)与 `drawElements`/`drawArrays`,
|
||||
按"每个 draw 用最近一次上传的 MV"配对。相机无旋转,所以 MV 的平移列 = `对象世界坐标 − 相机坐标`。
|
||||
实测 17668 条日志、8015 次 draw、7087 个不同 MV。
|
||||
|
||||
**关键读数**(按出现次数排序,截取):
|
||||
|
||||
| 次数 | scale | 世界坐标(= MV 平移 + 相机坐标) |
|
||||
| --- | --- | --- |
|
||||
| 290 | 1.000 | (0, 0, **−1800**) ← 纯视图矩阵 ⇒ **相机 z = +1800** |
|
||||
| 82 | 22.320 | (0, −3.9, −2142.7) |
|
||||
| 82 | 1.000 | (0, −223.0, −1920.0) |
|
||||
| 68 | 1.379 | (0, 0.7, −2648.5) |
|
||||
| 动画中 | 1.301 | (−98.9, **−863.8 → −840.3 → −827.5 → … → −555.6**, −2498.7) |
|
||||
| 动画中 | 1.218 | (34.1, **−545.9 → … → −262.5**, −2338.4) |
|
||||
| 动画中 | 0.106→0.341 | (1.0, 42.0, −1965.1) ← 缩放从小长大(入场) |
|
||||
|
||||
**结论一:相机 z = 1800,不是场景数据里的 1920。** 那条 290 次的单位缩放 MV 就是纯视图矩阵,
|
||||
平移 = −相机坐标。所以 **`camera.position:[0,0,1920]` 不是 live 相机 z**(差 6.7%),以后一律用 1800。
|
||||
|
||||
**结论二:时间线正在把对象往上移——实测数据。** 两条轨道在世界坐标里连续变化:
|
||||
y 从 −863.8 一路升到 −555.6(同一 x/z、scale 不变),另一条从 −545.9 升到 −262.5。
|
||||
这就是入场动画,**上升方向 = +y**,速率约每帧几单位。上一轮推断的"时间线把角色移进画面"由此坐实。
|
||||
|
||||
**结论三:live 的 z 与我的合成 z 大小相等、符号相反。**
|
||||
例:live z = −848.5(= MV −2648.5 + 1800)↔ 我的 `main_nike` 合成 z = **+849.66**。
|
||||
上一轮我因为"天空会跑到相机前面"而否掉了翻符号——**但那条否证的前提(场景 z 就是 live z)现在被推翻了**:
|
||||
live 相机 z 是 1800、且 live z 全为负。所以符号问题要重新审,不能再用旧前提否它。
|
||||
|
||||
**下一步**:把每个 draw 的世界坐标与 `scene.json` 的 part 逐个**配对**(x/y/z + scale 三元组匹配),
|
||||
钉死"哪个 part 是哪个对象",然后:
|
||||
1. 用 live 的 z(负)与相机 z=1800 重算深度;
|
||||
2. 把时间线的**末态**(或某一确定时刻)也读出来(`requestAnimationFrame` 钩子冻结时间源),
|
||||
得到确定的基准帧与基准坐标。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 7 轮(关键):两条 pass 分开之后,一切对上了
|
||||
|
||||
**做法**:给每个 draw 记录**当时生效的投影矩阵**(是透视还是正交),把两条 pass 分开统计。
|
||||
|
||||
```
|
||||
透视 pass(3D 场景,7.5k draws 里的绝大多数)
|
||||
×218 scale=1.000 MV=( -4.0, -379.0, -1920.0) ↔ main_btn ( -4.00, -379.00, 0.00) ✓ 逐位
|
||||
× 39 scale=1.000 MV=(-212.1, -844.1, -764.5) ↔ main_down (-212.07,-844.15, 1155.45) ✓ 逐位
|
||||
×186 scale=22.300 MV=(0.0, -3.9, -2142.7)
|
||||
×149 scale=1.400 MV=(0.0, 0.7, -2648.5)
|
||||
× 84 scale=1.300 MV=(-98.9, -376.0, -2498.7) ← y 在动(入场)
|
||||
× 84 scale=1.200 MV=(34.1, -58.2, -2338.4) ← y 在动
|
||||
正交 pass(UI/2D)
|
||||
×372 scale=1.000 MV=(0.0, 0.0, -1800.0) ← **这条就是上一轮误判的来源**
|
||||
```
|
||||
|
||||
**结论一:透视相机 z = 1920**(与场景数据 `position:[0,0,1920]` 一致)。
|
||||
验证:`main_btn` 的世界 z=0 → MV z = 0 − 1920 = −1920 ✓;`main_down` 的 z=1155.45 → MV z = −764.5 ✓。
|
||||
|
||||
**结论二:我的世界变换合成是对的**——两个 part 的 x/y/z **逐位命中**。
|
||||
所以"静态摆放 + 透视投影 + 相机参数"这条链**已经正确**,不需要再动。
|
||||
|
||||
**结论三:第 5、6 轮两次误判的根源找到了**:把两条 pass 的相机混在一起看。
|
||||
正交 pass 的相机在 z=1800,我拿它的 `(0,0,−1800)` 当"透视相机 z=1800",
|
||||
于是推出"对象 z 符号相反"——**纯属跨 pass 误读**。
|
||||
|
||||
**结论四:唯一剩下的差异就是时间线。** 被动画驱动的是**容器节点**
|
||||
(`layout` / `inout` / `bei_e…g` / `xl` / `jiulaoshi` / `play` / `d` / `c` / `b` / `a`),
|
||||
叶子 part 跟着父容器走。所以:
|
||||
|
||||
**下一步(明确)**:把时间线**末态**的容器变换量出来(等入场动画跑完再读 MV,
|
||||
或用 `requestAnimationFrame` 冻结时间源取确定时刻),把它作为"容器附加位移/缩放"
|
||||
烘进预设——**摆放本身不用再推导,只要补这一个位移量**。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 8 轮:改为**读源码**(用户要求:出问题先看来源网站源码,别盲猜)
|
||||
|
||||
### 引擎的权威实现(vendors bundle 原文)
|
||||
|
||||
```js
|
||||
// 节点局部矩阵:只有 autoMatrix 为真才自动更新,否则算一次就冻结
|
||||
a.matrixAutoUpdate = void 0 !== e.autoMatrix && e.autoMatrix;
|
||||
a.matrixAutoUpdate || a.updateMatrix();
|
||||
|
||||
// 时间线:按路径取目标对象,**直接写**它的属性;属性在 matrixMatch 里则强制打开自动更新
|
||||
var v = parsePath(i, this.name), g = v.object, y = v.target, w = v.prop;
|
||||
g && (g.matrixAutoUpdate = g.matrixAutoUpdate || s.matrixMatch.includes(w));
|
||||
```
|
||||
|
||||
### 三条权威语义(替代此前的猜测)
|
||||
|
||||
1. **节点局部矩阵 = `compose(position, rotation, scale)`**(three.js `updateMatrix`),
|
||||
**默认冻结**;`autoMatrix:true` 或被时间线驱动 `position/scale/rotation` 的节点才逐帧重算。
|
||||
→ 所以我"只合成位移与缩放、忽略旋转"对**叶子**是安全的(两片倾斜面片是叶子),
|
||||
但对**有子节点的旋转节点**不成立。
|
||||
2. **时间线是覆盖(`target[prop] = 值`),不是叠加。**
|
||||
3. 因此"整场平移"= 某个**公共祖先容器**的 position 被时间线改写;该容器是所有相关 part 的祖先,
|
||||
所以对每个 part 表现为**同一个位移**——与第 8 轮实测的 `Δ=(212.1, 365.1, −1123.5)` 完全吻合。
|
||||
|
||||
### 本轮同时落地的实现
|
||||
|
||||
- `sceneConfig.timelineOffset`(页面单位)进 schema:types / globals / generate / 运行时都支持;
|
||||
运行时在投影前把它加到每个 part 的 position 上。
|
||||
- nico-tea 的 `preset.template.json` 里已写入实测值 `[212.1, 365.1, -1123.5]`。
|
||||
- 效果:`content` 包围盒从 18.9×11.8 收到 **7.58×3.62**(视锥 4.63×2.60),
|
||||
画面与基准图明显接近(角色、茶桌、甜品架、城堡、标题各就各位)。
|
||||
|
||||
### 还差的(下一步,仍按"读源码"办)
|
||||
|
||||
- 实测到**分组位移**不完全相同:`desk/nike` Δ=(138, 394, −1066)、`root/btn` Δ=(206, 70, 19)、
|
||||
`desk/xl` Δ=(−176, 271, −1123)——说明**不止一个容器**被时间线改写。
|
||||
下一步:把时间线里每条 `.position` 轨道的**末帧值**读出来(源码里 `data[].frames` 就是关键帧值,
|
||||
`getValue()` 返回 `target[prop]`),按容器分别落到 part 上,而不是一个全局值。
|
||||
- 读源码时优先看 `parsePath` / `matrixMatch` 的定义,确认 `position` 轨道的目标对象到底是哪个容器。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 9 轮:继续读源码——轨道的目标怎么解析,以及入场时间线的真实关键帧
|
||||
|
||||
### `parsePath`(vendors bundle 原文)
|
||||
|
||||
```js
|
||||
parsePath(e, t) {
|
||||
var n = t.split("."), r, i;
|
||||
if (e && e.isObject3D) i = r = e.getObjectByName(n[0]); // ← 轨道名第一段是**节点名**
|
||||
else r = e;
|
||||
if (!r) return null;
|
||||
for (var o = n.length - 1, a = 1; a < o; a++) if (!(r = r[n[a]])) return null;
|
||||
return { target: r, object: i, prop: n[o] }; // 写的是 target[prop]
|
||||
}
|
||||
```
|
||||
|
||||
**结论**:`layout.position` 这种轨道名 = `getObjectByName("layout")` 找到节点 → 写它的 `.position`。
|
||||
所以时间线**按名字**定位容器,与树路径无关。这解释了为什么我按 `path` 分组时
|
||||
`wiggle/bg`、`wiggle/desk` 等会呈现出各自不同的位移——**它们各自被不同的同名节点驱动**。
|
||||
|
||||
### 入场时间线(`loading`)自己写的值
|
||||
|
||||
```
|
||||
loading/fbx/layout.position 末帧 z = 539 (从 z=0.801 一路升上来)
|
||||
loading/fbx/bg.position 末帧 y = 0 (从 y=−502.6 一路升上来)
|
||||
loading/fbx/img.scale 末帧 = 1.023
|
||||
cues: [{frame:91, method:"stop,退场同时播放page_cut动画"}]
|
||||
```
|
||||
|
||||
即:入场时 `bg` 从 y=−502.6 升到 **0**、`layout` 的 z 从 0.8 升到 **539**。
|
||||
这两条正是"整场在动"的来源,而**稳定态就是末帧值**。
|
||||
|
||||
### 为什么之前算不出全部轨道
|
||||
|
||||
`timelineSetting` 是**多个变量**深合并起来的(`var rt,ot,mt=Fe(Fe(…{loading:…}…), …)`),
|
||||
我按 `mt=Fe(` 取到的那个表达式里只有一部分键(`page_cut` 等),其余时间线在**别的变量**里。
|
||||
下一步:把 `Fe(` 合并链的每个参数分别取出来解析(或直接找所有 `\w+=\{[a-z_]+:\{type:\d+,start:` 的赋值),
|
||||
拿到全部 14 条 `.position` 轨道的末帧值,按**节点名**落到对应容器上——
|
||||
这就是"读源码"版本的正确做法,不需要再拟合任何偏移量。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 10 轮:把时间线**按源码语义**接进摆放(取代拟合的偏移量)
|
||||
|
||||
### 拿到了全部 17 条轨道(末帧 = 稳定态)
|
||||
|
||||
```
|
||||
layout.position [0,0,539] bg.position [0,0,0]
|
||||
inout.position [0,0,-265] xl.position [736.665,52.619,-141.804]
|
||||
bei_e.position [0,0,0] bei_f.position [103.18,174.725,-35.011]
|
||||
bei_g.position [223.623,387.687,-179.149]
|
||||
a/b/c/d.position (−389…420, −396…−410, ±5) ← 桌子上的四件
|
||||
jiulaoshi/play [0,0,0] img.scale 1.023 btn_b/btn_c.scale 1
|
||||
```
|
||||
|
||||
### 实现(不再是补丁式的偏移)
|
||||
|
||||
- 抓取期:`_collect_timeline()` 按 `name:"X.position"` 锚定 clip → 取 `data[-1].frames[-1]`(末帧)。
|
||||
- 走场景树时:**节点名命中轨道就用末帧值覆盖**该节点的 position/scale(源码语义:`getObjectByName` + `target[prop] = 值`)。
|
||||
- 上一轮那个拟合出来的 `timelineOffset` **已从预设里删掉**,不留两套机制。
|
||||
|
||||
实测效果:`main_nike` 的合成位置从 `(−64.65, −804.50, 849.66)` 变成 `(47.42, −312.91, 146.71)`
|
||||
(时间线自己把它抬了 +492、拉近了 −703),画面上角色/书本/茶具/城堡/标题各就各位。
|
||||
|
||||
### 仍差一步(下一轮)
|
||||
|
||||
合成值 `(47.4, −312.9, 146.7)` 与 live 实测稳定值 `(147.4, −439.4, −273.8)` 还差 `(~100, ~126, ~420)`。两条线索:
|
||||
|
||||
1. `loading` 时间线在第 91 帧有 cue「stop,退场同时播放 page_cut」——
|
||||
**25 秒时页面正在跑的是 `page_cut`**,它的轨道在**另一个变量**里(我只取到了 `loading` 那一支)。
|
||||
把 `page_cut` 等其余时间线的轨道也取出来并**按先后覆盖**,才等于最终稳定态。
|
||||
2. **基准帧本身要定**:`tools/.cache/page-truth-2-main.png` 是"点两下、等 7 秒"拍的,
|
||||
当时登录弹窗还在、可能还没进 `scene_main` 的稳定态。整个目标以它为基准,
|
||||
所以"哪一帧"这个决定必须先落地(用户此前未答)。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 11 轮:基准帧确定下来了,`page_cut` 的交接仍是嫌疑
|
||||
|
||||
**不再等"哪一帧"这个决定,我自己取了确定的基准**:不点击、禁缓存、等 10/20/30 秒各拍一张,
|
||||
落 `tools/.cache/page-settled-{10,20,30}s.png`。三张一致 ⇒ 页面在 ~10 秒后就稳定了,
|
||||
所以 **`page-settled-30s.png` 就是稳定态基准**(比原来那张"点两下、7 秒、登录弹窗还在"的
|
||||
`page-truth-2-main.png` 干净)。
|
||||
|
||||
**关键观察(这次能对齐判断了)**:稳定态基准里的元素与我渲染的**是同一批**——
|
||||
居中偏右的尼可、她身后的城堡、左边的甜品架、带书本的茶桌、左上标题、右侧蓝色小精灵、右上三个圆按钮。
|
||||
也就是说 **`scene_main` 选对了**(不是"选错场景"),差的仍是**摆放**:基准里角色居中且大,
|
||||
我这边角色被顶到画面上沿、书本占据中央。
|
||||
|
||||
**因此剩下的嫌疑收窄成一条**:`loading` 时间线在第 91 帧 cue「stop,退场同时播放 page_cut」——
|
||||
我应用的 17 条轨道只包含 `loading` 那一支;`page_cut`(以及 merge 链里其它变量的时间线)
|
||||
的轨道没并进来,而 10 秒后页面跑的很可能正是后者。**按先后顺序覆盖**才是最终稳定态。
|
||||
|
||||
**注意**:`page-truth-2-main.png`(含登录弹窗与"进入活动"按钮)是同场景的**未稳定态**,
|
||||
以后一律用 `page-settled-30s.png` 作对拍基准。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 13 轮:与**来源代码的逻辑对照**(用户要求),并修掉最后一处取景差异
|
||||
|
||||
### 对照表(引擎源码 vs 我们的实现)
|
||||
|
||||
| 环节 | 引擎(源码原文/行为) | 我们的实现 | 判定 |
|
||||
| --- | --- | --- | --- |
|
||||
| 相机创建 | `new PerspectiveCamera(a.fov ‖ 32, uiWidth/uiHeight, near, far)`,position/rotation 来自数据 | 取 `camera.fov` + `camera.position[2]` 做 billboard 投影 | ✅ 一致(aspect 由 ui 决定,已用 live 投影矩阵验证) |
|
||||
| 画布适配 | `resizeUI`:`b = canvasAspect/uiAspect; b<1 ? uiW*=b : uiH/=b`,再 `cameraAdaptScreen` 只作用于 **scene_ui 相机** | **本轮改成复刻 resizeUI**(此前用 contain,多露 1.302×) | ✅ 已对齐 |
|
||||
| 节点矩阵 | `compose(position, rotation, scale)`;`matrixAutoUpdate = autoMatrix ?? false`,否则 `updateMatrix()` 一次(冻结) | 只合成位移+缩放;旋转不参与 | ⚠️ 已知差异(受影响的是两片倾斜面片,已跳过并计数) |
|
||||
| 父子链 | 矩阵相乘 | 位移受父 scale 缩放、缩放连乘 | ✅ 等价(无旋转时) |
|
||||
| 时间线取值 | 场景在 modifier 里声明 `playTimeline{sceneName, trackName, frame}`;`parsePath` 按**节点名**定位,`target[prop] = 帧值` | 按场景点名的**块**取轨道末帧,覆盖同名节点的 position/scale | ✅ 本轮修好(此前全局扫名字,拿错块) |
|
||||
| 时间线播放顺序 | 块内 `cues` 在第 N 帧"stop,退场同时播放下一条"(`loading`@91、`pv`@150、`page_cut`@20) | 只取单一块的末帧,未做逐段推进 | ⚠️ 近似(稳定态可取,过渡态不可) |
|
||||
| 绘制遮挡 | three.js z 缓冲(材质带 `depthTest/depthWrite`) | 按相机深度从远到近排序(画家算法) | ✅ 不透明情形等价 |
|
||||
| 混合模式 | 材质 `blending`(three.js 枚举) | 一律普通混合 | ⚠️ 未实现(全项目仅 1 处非 Normal) |
|
||||
| 第二条 pass | 正交 UI 层(1920×1080)另有一条渲染链 | 只做透视 3D pass | ⚠️ 未实现(UI 层不还原) |
|
||||
|
||||
### 本轮的取景修复
|
||||
|
||||
`resizeUI` 是引擎作者写的适配算法,直接照抄之后:
|
||||
`framing` 从 `4.6297 × 2.6042`(contain 的过露)变成 **`3.5556 × 2.0000` = 1920×1080 页面单位**,
|
||||
与第 4 轮量到的正交投影矩阵(1920×1080)逐位一致——两条独立证据互相印证。
|
||||
门里的期望值也改成同一算法(仍是独立实现),门通过。
|
||||
|
||||
### 仍差的一步
|
||||
|
||||
画面元素与排布已经对上基准(角色/书本/茶杯/城堡/标题/小精灵),但**整体仍偏下一段**
|
||||
(基准里标题在上方、我这边在中下)。下一轮优先验两件事:
|
||||
1. **基准帧的态**:匿名页面 10 秒后停在入场态(`loading`/`pv` 之前),而我们还原的是 `scene_main`;
|
||||
2. **逐段推进**:`pv` 在第 150 帧 cue 切 `page_cut`——若页面停在 `pv` 的**中段**,末帧值就不对。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第 14 轮:源码里一直写着「按第几帧」,我漏了它
|
||||
|
||||
### 漏掉的字段
|
||||
|
||||
```js
|
||||
{ id: "playTimeline", data: { sceneName: "scene_main", trackName1: "pv", frame1: 0, … } }
|
||||
```
|
||||
|
||||
`frame1: 0` 就是**该轨道要停在第几帧**。我一直取轨道**末帧**,等于假设它播到了结尾。
|
||||
|
||||
### 用算术验证(不需要浏览器)
|
||||
|
||||
```
|
||||
pv/fbx/layout.position 首帧 = [-0.487, 0.647, 2.136] 静态 = [-212.073, -358.588, 1141.952]
|
||||
delta = (+211.586, +359.235, −1139.816)
|
||||
main_nike 静态 (−64.65, −804.50, 849.66) + delta = (146.9, −445.3, −290.2)
|
||||
live 实测稳定值(第 6 轮钩 WebGL 量的) = (147.4, −439.4, −273.8)
|
||||
```
|
||||
|
||||
**逐位吻合**(x 差 0.5、y 差 6、z 差 16)。也就是说:`scene_main` 的稳定态 = `pv` 在**第 0 帧**的值,
|
||||
而我们此前的"末帧"取值本身就错了。残差那十几单位应当来自 part 自己那一层的轨道(`bei_f/bei_g` 等)。
|
||||
|
||||
### 落地
|
||||
|
||||
`_collect_timeline` 现在解析 `frameN` 并与 `trackNameN` 配对,取该帧的关键帧值(越界回退首/末帧)。
|
||||
抓取结果:`main_nike = (146.9, −445.3, −290.2)`,与 live 实测一致——**纯源码推导,无拟合量**。
|
||||
|
||||
### 仍然存在的差异(下一轮)
|
||||
|
||||
渲染出的元素与排布已与基准一致(角色/书本/茶杯/城堡/甜品架/标题/小精灵),
|
||||
但**整体仍比基准低约 250px**。候选原因(按可能性):
|
||||
1. **y 方向**:`flipY` 目前是"取反";若基准对应的是不取反 + 另一组偏移,会呈现为整体上/下移。
|
||||
2. **相机看向点**:我用 `camera.position` 与"看向 -z";若引擎实际用 `lookAt` 对准别的点,会整体平移。
|
||||
3. **基准帧的态**:匿名页面可能停在入场态(`loading` 那一支)而不是 `scene_main` 的 `pv@0`。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 达成(2026-09-21):对象级对拍定位到「y 方向」,改完构图与基准一致
|
||||
|
||||
### 最后一步的铁证(对象级对拍 = 页面当预言机)
|
||||
|
||||
用注入钩子拿页面自己的 `P × MV`,把每个对象投到**屏幕像素**(画布 1664×936 → 按 720/936 缩放对齐):
|
||||
|
||||
| 对象 | 页面屏幕坐标 | 我们的 | 差 |
|
||||
| --- | --- | --- | --- |
|
||||
| `main_btn`(世界 y=−379) | **(638, 612)** | (637, **107**) | x 差 1px,**y 完全镜像** |
|
||||
| `main_nike`(世界 y=−445) | **(706, 616)** | (725, **102**) | 同上 |
|
||||
|
||||
x 逐像素吻合说明"摆放 + 投影 + 相机 + 取景"**全对**;y 是镜像说明 `flipY` 那次取反是多余的——
|
||||
页面投影本身就把「世界 y 向上」映成「屏幕 y 向下」(`(1 − ndc.y)/2`)。
|
||||
默认改成不取反(需要时写 `flipY: true`)后,渲染与 `page-settled-30s.png` 的构图一致:
|
||||
角色居中持杯、城堡右后、标题左上、甜品架左、茶桌与书在底部、蓝色小精灵右。
|
||||
|
||||
### 收尾
|
||||
|
||||
- **promote 拦截已撤**(验收:不带任何标志 `promote` 退出码 0)。
|
||||
- 门全绿:`pnpm check`(6 分发自包含)、`verify-scene-player`(含透视取景门)、
|
||||
`check-downloader`、`verify`(7 页 101 骨架 85.5 MB)、`diff-project`(新增 nico-tea 档 + 属性差异,符合预期)。
|
||||
- 单骨架路径(xilian/kv37)**一行未动**,冻结基线不受影响。
|
||||
|
||||
### 留给以后的两条方法论(比这次的代码更值钱)
|
||||
|
||||
1. **遇到问题先读来源源码**——本目标的每一处真修复都来自源码(`getCamera`、`resizeUI`、`parsePath`、
|
||||
`matrixAutoUpdate`、`playTimeline{trackName,frameN}`),而两处"看起来成功"的假修复都来自猜。
|
||||
2. **用页面当预言机做对象级对拍**——`uniformMatrix4fv` + `drawElements` 钩子能给出
|
||||
每个对象的实时世界坐标与屏幕坐标;"整体偏了"这种模糊症状一旦变成带符号的像素差,
|
||||
结论就是唯一的(这次 x 差 1px / y 镜像,一步定案)。
|
||||
|
||||
### 仍未做的(不在本目标范围内,另立即可)
|
||||
|
||||
- 倾斜 3D 面片(全场景 2 个)的真四边形绘制;粒子/摆动/`CSS3DObject` 等 modifier;
|
||||
材料混合模式(全项目 1 处非 Normal);正交 UI 层的还原(站点外壳,按设计不做)。
|
||||
Reference in new issue
Block a user