From 0ccf6b09f4a547173c70e2c954b044ce6ce9f145 Mon Sep 17 00:00:00 2001 From: ritenseki Date: Fri, 7 Aug 2026 14:10:19 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20record=20the=20triage=20rule=20?= =?UTF-8?q?=E2=80=94=20a=20stage=20element=20behaving=20wrong=20usually=20?= =?UTF-8?q?means=20a=20missing=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of this session's bugs had the same shape, and naming that shape is worth more than either fix: the wash lights whipping back and forth, and the billboards spinning. In both cases the data was fine — clip curves and serialized fields dumped clean and checked out — and what was wrong was behaviour we had inferred on behalf of a script that does not exist. So the default hypothesis for "this stage element moves when it shouldn't / doesn't move when it should / moves at the wrong rate" is now written down as: the original component is missing and something of ours is standing in for it. Not: the data was parsed wrong. That is justified by the ledger — 728 MonoBehaviour instances are still empty, AnimationObjectController alone accounting for 282, and every decision about what animates and when lived in those method bodies. Data is dumpable and has repeatedly proven correct; behaviour is the part we cannot retrieve. Two practical corollaries go with it: check what scripts an object carried and whether we have them ([StageScripts] plus the signature list) before suspecting the parser; and when standing in for a missing component, say in the source whose decisions you are making, and mark whether the rule came from bundle data or from watching reference footage. The latter is real evidence but not ground truth, and may not hold on another stage. The counter-example is recorded in the same breath so the rule does not become a reflex: the black light shafts were a data problem after all — every material carries a URP Lit property table whose opaque preset (_DstBlend = Zero) overrode the additive blend 14 of the 16 relevant shaders declare. That one was found by dumping, and the general lesson — a stale saved-property table can override render state, not just colour — now sits next to the existing warning about saved properties. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UzXXkLzVC8pNA3JjyUGMbq --- CLAUDE.md | 64 +++++++++++++++++++++++++++++++++++++++++++++---- LIVE_DEV_MAP.md | 10 +++++++- LIVE_TRACKS.md | 2 +- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9d90e69..3641a3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,6 +111,26 @@ pf = obj.read_typetree()["m_ParsedForm"] # m_Name, m_PropInfo.m_Props ⚠️ A material's `m_SavedProperties` in the bundle lists properties it no longer has — it retains entries from whatever shader it was authored against. `Material.HasProperty()` asks the *current shader*. Always enumerate the shader, never trust the bundle's saved-property table. +**渲染状态也从这张过期表里读,后果最严重。** Gallop 的发光类 shader 把混合写成 +`Blend [_SrcBlend] [_DstBlend]`,也就是**从材质属性取渲染状态**。live10149 的 +**26/26 个材质**存的都是 `_SrcBlend=One, _DstBlend=Zero`(不透明覆盖),且每一个的 float 表里 +都带着 `_WorkflowMode`/`_Surface`/`_ClearCoatMask`/`_Smoothness`/`_Blend` —— 那是 **URP Lit +的属性表**,整批盖上去的,One/Zero 正是 URP Lit 的不透明预设。 + +作者意图可查:全库 16 个声明这两个属性的 shader 里,**14 个默认 `One/One`(加法混合)** —— +`LightBlinkBlend`、`LightBlend`、`StageBeamLight{,Cutoff,Fadeout}`、 +`StageLightBlink{Cutoff,Fadeout}`、`StageProjectorBlend{,VertexAlpha,AnimVertexAlpha}`、 +`StageMonitorBlend*`、`MirrorBallProjector`、`BgUnlitStencilAlpha`,全是发光/投影那一批。 + +被压成不透明后,加法光柱「颜色为黑 = 什么都不加 = 不可见」变成了**一堵实心黑墙**。 +`StageController.RestoreAuthoredBlendState()` 在舞台初始化时把这两个属性写回 **shader 声明的 +默认值**(`Shader.GetPropertyDefaultFloatValue`),对本来就该不透明的 shader 是空操作,自限。 +改了哪些材质会逐条打进 `[StageBlend]` 日志。 + +> 教训比这个 bug 本身通用:**过期存档表不只影响颜色,还能改渲染状态**。 +> 看到「本该发光的东西是黑的/挡住后面的东西」,先查 `_SrcBlend`/`_DstBlend`, +> 再查颜色写对没有。 + Worked example — `mtl_env_live10149_projector001` (shader `Gallop/3D/MirrorAndShadow/MirrorBallProjector`, 22 real properties) saves 40+, including Standard-shader leftovers (`_Glossiness`, `_Metallic`, `_Parallax`, `_Mode`, `_UVSec`) **and** four plausible-looking ghosts the current shader never declares: `_IsLoopYRotation`, `_LoopYRotationSpeed`, `_MirrorBallProjectionIntensity`, `_MirrorBallRotateWS`. Two of those are older spellings of properties that *do* exist (`_MirrorBallIsLoopRotation`, `_MirrorBallLoopRotationSpeed`) — exactly the kind of near-miss that reads as a discovery. Note also that where a component field and a saved property disagree (`MirrorBallFallOffPower` = 1.0 on the component vs `_MirrorBallFalloffPower` = 2.0 in the material), the component wins at runtime: these controllers push their own fields into the material. **Track group names are not always GameObject names.** Four separate silent failures were traced to this in one session, so check the mapping before assuming a track is unimplemented: @@ -201,10 +221,46 @@ MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的** **舞台自带 Animation 的播放**(`StageAnimationPlayer`,顶替缺失的 `AnimationObjectController`): 283 个 `Animation` 组件全部 `playAutomatically = true` 但默认 clip 为空,Unity 因此什么都不播。 -只接管**唯一解**的情形(1 个 clip,live10149 上 4 个对象);3 个 / 5 个 clip 的状态机选择规则 -没有依据,只打日志不猜。播放不走 `Animation.Play()` —— 那用的是 Unity 自己的时钟,暂停和拖动 -进度条都会脱节;改为每帧按 `LiveTimelineControl.currentLiveTime` 设 `AnimationState.time` 再 -`Sample()`,与演出时间同源。 +播放不走 `Animation.Play()` —— 那用的是 Unity 自己的时钟,暂停和拖动进度条都会脱节; +改为每帧按 `LiveTimelineControl.currentLiveTime` 设 `AnimationState.time` 再 `Sample()`, +与演出时间同源。3 个 / 5 个 clip 的状态机选择规则没有依据,只打日志不猜。 + +⚠ **「只有一个 clip」≠「这个 clip 一直在播」。** 前者说明这个物件只有一个**状态**, +后者是 `AnimationObjectController` 每帧做的**决定**。最初的接管规则把两者混为一谈, +结果 `wash_ground_b/c/d` 的扫动 clip(`swing_*` 节点 90°→0°→90°、2.0 s 一个来回、12 盏同步) +被 free-run,一排地面 wash 灯疯狂摆头 —— 而原版 MV 里它们不扫、只闪。现已按 `_wash_` 跳过。 +镜面球保留(单调转到 −359°,自明的常转 idle)。 + +### 🔎 舞台上「行为不对」的第一嫌疑:缺组件,不是解析错 + +**这是分诊规则,不是某一次的结论。** 舞台 prefab 引用 11 个脚本类,至今仍有 **728 个 +MonoBehaviour 实例是空的**(基线 892,已补 `BillboardController` −164)。其中 +`AnimationObjectController` 一家就占 282 个实例 —— 舞台上所有「什么时候动、动多快、 +动哪一个」的决定原本都在它手里,而它整个不存在。 + +所以当某个舞台元素表现不对(该动的不动、不该动的在动、动的节奏不对), +**默认假设应该是「原版那个组件缺失,现在由我们猜的行为顶着」,而不是「数据解析错了」**。 +理由是可检验的:字段值、clip 曲线、shader 属性表这些**数据**层面的东西全都能 dump 出来核对, +而且一路核对下来**基本都是对的**;反复出错的是**行为**层面 —— 那正好是拿不到方法体的部分。 + +已经有两个同形状的例子,都是「物件选对了、行为猜错了」: + +| 现象 | 数据对不对 | 真实原因 | +|---|---|---| +| wash 灯疯狂摆头 | clip 曲线完全正确(实测 90°→0°→90°) | 缺 `AnimationObjectController`,我们替它决定了「一直播」 | +| 面片一直乱转 | 字段全部正确读到 | 缺原方法体,`_rotationType==0` 的语义是我们猜的 | + +推论一:动手前先查一眼**这个物件挂过什么脚本、那个脚本我们有没有** +(`[StageScripts]` 日志 + `CLAUDE.md` 的签名清单),比直接怀疑解析快得多。 + +推论二:**顶替缺失组件时,要把「我在替谁做决定」写清楚**,并且区分 +「从 bundle 数据推出来的」和「看参考视频观察到的」—— 后者是有效证据,但不是 ground truth, +换个舞台可能就不成立(`StageAnimationPlayer.IsSweepRig` 的注释就是按这个写的)。 + +反例也要记住:**并非所有舞台画面问题都是缺组件。** 光柱渲染成黑墙那次, +根因是材质被盖了 URP Lit 的属性表、`_DstBlend` 从 One 变成 Zero, +把加法混合压成了不透明 —— 那是**数据**问题,而且是 dump 得出来的(见下)。 +分诊规则的意思是「先查缺不缺组件」,不是「一律归咎于缺组件」。 ### Blocked diff --git a/LIVE_DEV_MAP.md b/LIVE_DEV_MAP.md index 243bfa2..39805a2 100644 --- a/LIVE_DEV_MAP.md +++ b/LIVE_DEV_MAP.md @@ -92,6 +92,14 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 ### 舞台自带脚本(原版 MonoBehaviour,按签名重建) +> **分诊规则:舞台元素「行为不对」时,第一嫌疑是缺组件,不是解析错。** +> 至今仍有 **728 个 MonoBehaviour 实例是空的**(基线 892),光 `AnimationObjectController` +> 就占 282 个 —— 舞台上「什么时候动、动多快」的决定原本都在这些缺失的脚本里。 +> 而数据层(字段值 / clip 曲线 / shader 属性表)是能 dump 出来核对的,一路核对下来基本都对。 +> 所以先查「这个物件挂过什么脚本、我们有没有」,再怀疑解析。详见 `CLAUDE.md` 同名小节。 +> ⚠ 但别一律归咎于缺组件:光柱变黑墙那次是**材质数据**问题(URP Lit 属性表把 `_DstBlend` +> 从 One 压成 Zero,加法混合变不透明),照样是 dump 出来的。 + `Assets/Scripts/umamusume.asmdef` 的程序集名与 bundle 里写的 `umamusume` 一致,所以按 **类名 + namespace + 程序集名** 建类就能让 Unity 把原版的序列化字段真正填进来。 清单和实例数见 `CLAUDE.md`「用原签名重建脚本」。 @@ -100,7 +108,7 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 |------|------| | `Gallop.MirrorReflection` | ✅ 已建并实现。第二摄像机 + 反射矩阵 + 斜投影 → RT → `_ReflectionTex`。**顺带解决了长期的「舞台地板发白」** —— `mirror_a` 的 `_ReflectionTex` 从没被赋值,Unity 代入白贴图 × 默认 `_ReflectionRate = 1.0` | | `Gallop.Live.BillboardController` | ⚠️ 已建,字段可读(`[StageScripts]` 丢失数 892→728)。朝向行为写过一版,实机是「一直转来转去而那东西不该转」,已默认关闭(`EnableRotation = false`)——`_rotationType` 语义和 up 轴取法都无依据。同批修掉一个确定 bug:目标摄像机被缓存,而生效摄像机随 CameraSwitcher 每帧变 | -| `Gallop.Live.AnimationObjectController` | ⚠️ 未建;其最直接的后果(283 个 `Animation` 一个都不播)已由 `StageAnimationPlayer` 顶上:只接管单 clip 的唯一解情形,按 `currentLiveTime` 采样而不是 `Animation.Play()`,暂停/拖动进度条都跟随 | +| `Gallop.Live.AnimationObjectController` | ⚠️ **未建,282 个实例,是缺口最大的一个**。其最直接的后果(283 个 `Animation` 一个都不播)由 `StageAnimationPlayer` 顶上:按 `currentLiveTime` 采样而不是 `Animation.Play()`,暂停/拖动进度条都跟随。但**「播哪个 clip」和「该不该播」都是它在决定**,我们只能顶前者——wash 灯扫动 clip 被 free-run 就是栽在后者(已按 `_wash_` 跳过) | | `Gallop.RenderPipeline.CustomLensFlare` (+`LensFlareData`) | ❌ 未建,但**数据齐全、语义不受 IL2CPP 阻塞**——`LensFlareData` 是 Unity legacy Flare 的逐字翻版,URP 侧有 `LensFlareComponentSRP` 对应。199+5 个物件,星芒在原版截图里很显眼。**下一个该做的就是它** | | `Gallop.RenderPipeline.MirrorBallProjector` | ❌ 字段已 dump,但 4 个 projector 全在原点、共用一个材质、没有任何指向灯球的引用字段——绑定关系只在方法体里。**卡住,不是没做** | | `Gallop.Live.ShaderParam.ShaderParamController` | ⚠️ 实测是空操作(两个向量都是 (1,1,1,1)),从待办里划掉 | diff --git a/LIVE_TRACKS.md b/LIVE_TRACKS.md index 0273200..df3ca89 100644 --- a/LIVE_TRACKS.md +++ b/LIVE_TRACKS.md @@ -216,7 +216,7 @@ originSessionId: 319ae839-0292-4b1e-80de-2f3c91b1f40e | 现象 | 状态 | |---|---| | 舞台地板长期偏亮发白 | ✅ 2026-08-06 解决。**白的不是文档里记的那三个物件** —— 是与 `plane_000` 同位同尺度、叠在一起的 `mirror_a`(同在 `pfb_env_live10149_main000` 下,局部变换全为单位)。它用 `Cygames/MirrorAndShadow/ReceiveMirror`,`_ReflectionRate` 默认 1.0 而 `_ReflectionTex` 从没赋值,Unity 对未绑定采样器代入白贴图 = 满强度白反射。实现 `Gallop.MirrorReflection` 接上反射 RT 后恢复正常。**教训见 `CLAUDE.md`「现象描述不是 ground truth」**:整轮排查都围着文档里那三个名字转,那个「`_EnvRate` 归零无变化」的实验切的是另一批物件 | -| 小灯泡渲染成黑块 | 用 `StageTransmittedLightMask` / `StageMirrorBallShine`,这两个 shader **没有任何颜色属性**,BlinkLight 点不亮。疑为 URP 移植问题,对应未实现的 95/96 TransmittedLight 轨道 | +| 小灯泡渲染成黑块 / 光柱变实心黑墙 | 🔧 2026-08-07 **找到根因并已修**(待实机确认):Gallop 发光类 shader 的混合状态写作 `Blend [_SrcBlend] [_DstBlend]`,从材质属性读;而 live10149 **26/26 个材质**都被盖上了 URP Lit 的属性表,`_DstBlend` 是 URP Lit 不透明预设的 Zero。全库 16 个声明该属性的 shader 里 **14 个作者默认是 One/One(加法)**,于是所有光柱/闪光/投影都被按不透明画 —— 加法下「颜色为黑=不可见」变成了「实心黑墙」。`StageController.RestoreAuthoredBlendState()` 已把这两个属性恢复成 shader 声明的默认值。**注**:`StageTransmittedLightMask`/`StageMirrorBallShine` 无颜色属性是另一回事,仍未解 | | LED 大屏黑屏 | MonitorControl (10) 未实现,见上 | ---