diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageEnvMapDiag.cs b/Assets/Scripts/umamusume/Gallop/Live/StageEnvMapDiag.cs new file mode 100644 index 0000000..abb9505 --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/StageEnvMapDiag.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Gallop.Live +{ + /// + /// 舞台地板/高光面「过亮发白」的诊断。 + /// + /// 已从 bundle 查明的部分(ground truth,见 LIVE_AUDIT_2026-08-05.md): + /// + /// * live10149 上发白的 plane_000 / stage_object_001 / specular_002 三个物件, + /// 用的是**同一个** shader:Gallop/3D/Live/Stage/DefaultEnvMapNoAmbient。 + /// 它是舞台 shader 里唯一带环境反射通道的一族。 + /// * 该 shader 真实声明的属性只有 8 个: + /// _MainTex _MulColor0 _ColorPower _AddColor + /// _TripleMaskMap _EnvMap _EnvRate(Range 0..1, 默认 0.5) _EnvBias(Range 0..8, 默认 1) + /// * 三个材质的 _EnvRate 分别是 0.75 / 1.0 / 1.0,_EnvBias 是 2 / 2 / 5。 + /// * 材质里 _EnvMap 是**绑定了**的(指向别的 bundle,不是 null)。 + /// + /// 剩下的唯一未知项是运行时:那些跨 bundle 的贴图引用 UmaViewer 到底解析出来没有。 + /// 如果 _EnvMap 运行时是 null,Unity 会代入默认贴图(白),再乘上 _EnvRate=1.0 + /// 就是满强度的白色反射 —— 正好是「地板发白」的样子。 + /// + /// 这个类只负责把那个未知项打出来,不做任何修改。 + /// + /// 可证伪的预测:plane_000 的 _EnvRate 是 0.75,另外两个是 1.0, + /// 所以**如果**这个假设成立,plane_000 应该比另外两个略轻一些。 + /// 若三者一样白,说明白化另有来源,这条假设就该丢掉。 + /// + public static class StageEnvMapDiag + { + private const string kEnvShader = "Gallop/3D/Live/Stage/DefaultEnvMapNoAmbient"; + + public static void Dump(StageController stage) + { + if (stage == null) return; + + var lines = new List(); + int total = 0, nullEnv = 0; + + foreach (var r in stage.GetComponentsInChildren(true)) + { + foreach (var mat in r.sharedMaterials) + { + if (mat == null || mat.shader == null) continue; + if (mat.shader.name != kEnvShader) continue; + + total++; + // 必须问当前 shader;材质的 m_SavedProperties 里还留着一堆 URP Lit 的 + // 历史属性(_WorkflowMode/_Smoothness/_ClearCoatMask…),那个表不可信。 + Texture env = mat.HasProperty("_EnvMap") ? mat.GetTexture("_EnvMap") : null; + if (env == null) nullEnv++; + + lines.Add(string.Format( + " {0,-22} mat={1,-34} _EnvMap={2,-26} _EnvRate={3:F2} _EnvBias={4:F2}", + r.gameObject.name, + mat.name.Replace(" (Instance)", ""), + env == null ? "*** null(会代入白贴图)***" : env.name, + mat.HasProperty("_EnvRate") ? mat.GetFloat("_EnvRate") : -1f, + mat.HasProperty("_EnvBias") ? mat.GetFloat("_EnvBias") : -1f)); + break; + } + } + + if (total == 0) + { + Debug.Log($"[EnvMapDiag] 本舞台没有使用 {kEnvShader} 的渲染器"); + return; + } + + Debug.Log($"[EnvMapDiag] {kEnvShader}:{total} 个渲染器,其中 _EnvMap 为 null 的有 {nullEnv} 个\n" + + string.Join("\n", lines)); + + if (nullEnv > 0) + Debug.LogWarning($"[EnvMapDiag] {nullEnv}/{total} 个材质的 _EnvMap 运行时没解析出来 —— " + + "Unity 会代入白贴图,_EnvRate 越接近 1 就越白。地板发白的成因基本可以坐实。"); + else + Debug.Log("[EnvMapDiag] _EnvMap 全部解析成功 —— 发白不是贴图缺失造成的。" + + "按 F9 可在「原始 _EnvRate ↔ 0」之间来回切换,判断反射项是不是发白的来源。"); + + if (stage.GetComponent() == null) + stage.gameObject.AddComponent(); + } + } + + /// + /// 判决实验:按 F9 把 DefaultEnvMapNoAmbient 的 `_EnvRate` 在「原始值 ↔ 0」之间切换。 + /// + /// 为什么需要它:`_EnvRate` 0.75 与 1.00 只差 25%,如果反射项本来就超过 1.0 被钳到白, + /// 两者会一样白 —— 所以「plane_000 看起来没比别的轻」**不能**否定反射项假设, + /// 那个对比检验设计得太弱。把 `_EnvRate` 直接归零才是二元的: + /// + /// 发白消失 → 就是 env 反射项,问题转化为「反射强度该由谁压下来」 + /// 发白照旧 → env 路径无关,改查 Built-in shader 在 URP 下的翻译 + /// + /// 走 MaterialPropertyBlock,不动材质本身;再按一次即可还原。纯诊断,默认不改变任何东西。 + /// + public class StageEnvRateToggle : MonoBehaviour + { + public KeyCode key = KeyCode.F9; + + private const string kEnvShader = "Gallop/3D/Live/Stage/DefaultEnvMapNoAmbient"; + private bool _zeroed; + private MaterialPropertyBlock _block; + private readonly List _targets = new List(); + private bool _collected; + + private void Collect() + { + _collected = true; + foreach (var r in GetComponentsInChildren(true)) + { + foreach (var mat in r.sharedMaterials) + { + if (mat != null && mat.shader != null && mat.shader.name == kEnvShader) + { + _targets.Add(r); + break; + } + } + } + } + + private void Update() + { + if (!Input.GetKeyDown(key)) return; + if (!_collected) Collect(); + if (_targets.Count == 0) + { + Debug.Log("[EnvMapDiag] 本舞台没有带 env 反射的渲染器,F9 无事可做"); + return; + } + + _zeroed = !_zeroed; + _block ??= new MaterialPropertyBlock(); + + foreach (var r in _targets) + { + if (r == null) continue; + r.GetPropertyBlock(_block); + if (_zeroed) + { + _block.SetFloat("_EnvRate", 0f); + } + else + { + // 还原成材质自己的授权值(plane_000=0.75,另两个=1.0) + float authored = 0.5f; + foreach (var mat in r.sharedMaterials) + { + if (mat != null && mat.HasProperty("_EnvRate")) { authored = mat.GetFloat("_EnvRate"); break; } + } + _block.SetFloat("_EnvRate", authored); + } + r.SetPropertyBlock(_block); + } + + Debug.Log($"[EnvMapDiag] _EnvRate → {(_zeroed ? "0(反射关闭)" : "原始值(反射恢复)")}," + + $"作用于 {_targets.Count} 个渲染器。地板此刻变了吗?"); + } + } +} diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageEnvMapDiag.cs.meta b/Assets/Scripts/umamusume/Gallop/Live/StageEnvMapDiag.cs.meta new file mode 100644 index 0000000..8d83dcf --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/StageEnvMapDiag.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f47145aad184ad3a6ae81cb1b711182 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LIVE_AUDIT_2026-08-05.md b/LIVE_AUDIT_2026-08-05.md new file mode 100644 index 0000000..7748c3d --- /dev/null +++ b/LIVE_AUDIT_2026-08-05.md @@ -0,0 +1,535 @@ +# ✅ 轨道审计报告 — 2026-08-05 + +对 `LIVE_DEV_MAP.md` 中标记为 ✅「已完成」的轨道做实现审计。 +所有结论都用 `tools/` 从 bundle 里取了 ground truth,未验证的猜测不写进来。 + +结论:**4 条标 ✅ 的轨道实际上不工作或工作错误**,另有一组性能问题。 +它们全部属于同一个已知家族——数据解析正确,然后在下游被静默丢弃,无任何报错 +(和 commit `a3a7b83` 修的六条是同一形状)。 + +## 当前状态 + +| # | 问题 | 状态 | +|---|------|------| +| A1 | GlobalLight 被 BgColor1 每帧清空(59/59 首) | ✅ 已修 | +| A2 | BgColor2 丢弃组名(15 首 / 14346 keys) | ⚠️ 部分修复 —— 组名已接通、性能已修;**组名到底指向什么仍无 ground truth**,解析不到时退回原全舞台语义并打警告 | +| A3 | Spotlight3d 写 `_Color` 是空操作(31 首) | ✅ 已修 | +| A4 | Transform 只认 unit 名(12 首 / 681 keys) | ✅ 已修 | +| A5 | UVScrollLight 写 `_Color` 是空操作(**同一 bug 第三例**) | ✅ 已修。目标材质实测是 `LightAdd1_UV` / `StageLightAdd1_UVAlphaMask_TransmittedLightMask`,通道为 `_MulColor0`/`_MulColor1`/`_ColorPower`;顺带补上一直没用的 `mulColor1`,并改 MPB + 缓存(原先每帧遍历全舞台 + `r.materials`)| +| B1–B4 | 每帧 `r.materials` / 全场景遍历 / 每帧 new MPB | ✅ 随 A1–A3 一并修掉(Wash/Laser 的 `r.materials` 仍在,属 ⚠️ 轨道,未动) | +| C1 | GlobalFog 把高度雾当距离雾 | ❌ 未动,需先做语料统计定方案 | +| C2 | 初始化时同名子物件只有第一个被关,且没人再打开 | ✅ 已修(**前两稿的诊断都是错的**,见下文) | +| C4 | 舞台地板 / 高光面发白 | 🔍 已定位到 `DefaultEnvMapNoAmbient` 的 `_EnvMap`/`_EnvRate`;只剩「运行时贴图有没有解析出来」一个未知项,已加 `StageEnvMapDiag` 打印 | +| C3 | ParticleGroup 覆盖 Particle | ❌ 未动,优先级低 | + +**均未经实机验证** —— 环境里没有 Unity,也没有 C# 编译器,只做了括号配平和符号引用检查。 +下面每条的证据和改法都写在原处。 + +### 哪首歌能验哪条 + +本地目前只下载了 **1177** 的 live 数据。1177 里有 BgColor1(20 组) / BlinkLight / Object / +Effect / GlobalLight / GlobalFog / PostFilm,但**下面三条轨道在 1177 里是空的**, +改了也看不到任何效果: + +| 修复 | 1177 能验? | 需要的歌(按 keyframe 数排序) | +|------|------------|------------------------------| +| A1 GlobalLight | ✅ **能** | 1177 有 2 组 / 125+24 keys | +| A2 BgColor2 | ❌ 1177 无数据 | son1006 (4338) · son1009 (3446) · son1007 (3308) | +| A3 Spotlight3d | ❌ 1177 无数据 | son1081 (122) · son1048 (103) · son1051 (95) | +| A4 Transform | ❌ 1177 无数据 | son1028 (206) · son1009 (131) · son1027 (129) | + +A1 在 1177 上的判据见下方「A1」条目末尾。注意 1177 的 rim **颜色和宽度几乎不变** +(`rimColor` RGB 125 帧只有 8 个值且基本同色,`rimFeather`/`rimSpecRate` 全程恒定), +真正在动的是 `rimColor.a`(0.392→1.0)和 `lightDir`(125 帧 102 个不同值)。 +所以「看不出在动」是正常的,要盯**亮边在剪影上的位置**而不是颜色。 + +查某首歌带哪些轨道:`~/.venvs/umatools/bin/python3 -c "import csv; ..."` 读 +`out/scan_keys.csv` 对应列即可(该 CSV 是全 59 首的宽表,一行一首)。 + +--- + +## A. 功能性 Bug(标 ✅ 但实际不工作) + +### A1. GlobalLight (48) 被 BgColor1 每帧清空 —— 全语料 59/59 首受影响 + +**证据** + +``` +LiveTimelineControl.cs:364 AlterUpdate_GlobalLight(...) +LiveTimelineControl.cs:365 AlterUpdate_BgColor1(...) ← 紧接着跑 +``` + +两个 handler 都对**同一批** `container.Renderers` 做整块替换: + +```csharp +// Director.cs:347 OnGlobalLightUpdate +MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock(); +propertyBlock.SetFloat("_RimShadowRate", ...); // 共 13 个 rim 属性 +foreach (var renderer in container.Renderers) + renderer.SetPropertyBlock(propertyBlock); + +// Director.cs:413 OnBgColor1Update(角色分支) +MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock(); +propertyBlock.SetColor("_CharaColor", ...); // 只有 5 个属性 +foreach (var renderer in container.Renderers) + renderer.SetPropertyBlock(propertyBlock); // ← 整块替换,不是合并 +``` + +`Renderer.SetPropertyBlock()` 是**替换**语义,不是合并。BgColor1 在 GlobalLight +之后跑,于是每一帧 GlobalLight 刚写进去的 13 个 rim 属性立刻被抹掉,回落到材质默认值。 + +歌曲 1177 实测同时存在两者: + +``` +bgColor1 groups (20): ['BgBL', 'CharaColor', 'CharaColor', 'Shadow', ...] +globalLight groups: ['GlobalLight', 'GlobalLight'] +``` + +覆盖率:`globalLightDataLists` 9636 keys / 59 首,`bgColor1List` 43617 keys / 59 首。 +两条轨道在**每一首歌**里都同时有数据,所以这个 bug 是全语料生效的。 + +**方案** —— 改成读-改-写,并复用一个常驻 block(顺带干掉每帧 new): + +```csharp +private MaterialPropertyBlock _charaBlock; + +// 两个 handler 共用 +private void WriteCharaBlock(Renderer r, Action fill) +{ + _charaBlock ??= new MaterialPropertyBlock(); + r.GetPropertyBlock(_charaBlock); // 先取回渲染器上已有的块 + fill(_charaBlock); + r.SetPropertyBlock(_charaBlock); +} +``` + +`GetPropertyBlock` 会用渲染器当前的块内容覆盖填充传入的 block,所以两条轨道各写各的 +属性、互不清除。 + +--- + +### A2. BgColor2 (9) 完全忽略组名,10 个组互相覆盖 —— 15 首 / 14346 keys + +**证据** —— 歌曲 1006 的组名(4338 keys,语料内最多): + +``` +bgColor2List -> ['BgWashA','BgWashB','BgWashC','BgWashD','BgWashE', + 'BgWashF','BgWashG','BgWashH','BgWashI','BgWashJ'] +``` + +10 个独立的 wash 分区。但: + +```csharp +// LiveTimelineControl.cs:1879 —— TimelineNameHash 字段存在,dispatcher 从不填 +BgColor2UpdateInfo updateInfo = default; +updateInfo.color1 = ...; updateInfo.color2 = ...; updateInfo.value = ...; + +// Director.cs:664 —— handler 也从不读名字,直接刷整个舞台 +foreach (var r in _stageController.GetComponentsInChildren()) + foreach (var mat in r.materials) + if (mat.HasProperty("_AmbientColor")) + mat.SetColor("_AmbientColor", c); +``` + +后果是双重的:每帧 10 个组按顺序把**整个舞台**刷一遍,**只有最后一个 `BgWashJ` 留下来**, +而且它会把本该只属于 J 区的颜色涂到所有分区上。前 9 个组的 3900 多个 key 等于不存在。 + +这和 `a3a7b83` 修掉的 BgColor1 `validBgColorNames` 白名单是同一类错误——组名路由缺失。 + +**已做** + +1. `BgColor2UpdateInfo` 补了 `TimelineName`,dispatcher 从 `sheet.bgColor2List[i].name` 填。 + (组名一直是可读的 —— 它继承自 `ILiveTimelineGroupDataWithName` 基类,只是从没被传下去。) +2. handler 改走 `ResolveStageTargets(key, kStageBgColor2Props)`,通道 `_AmbientColor`, + 首帧解析一次并缓存,写入用 `MaterialPropertyBlock`。B1/B2 一并解决。 + +**没做,也不该猜:组名到底指向什么** + +全语料 15 首里出现过的组名只有这些: + +``` +BgWashA..BgWashO LaserA/B/C BgColor2 +``` + +已经排除了两种可能: + +- **不是 GameObject 名** —— 舞台层级里没有同名对象。 +- **不是 unit 名** —— 已下载的三个舞台(live10132 / 10149 / 10151)的 + `_stageObjectUnits` 分别是 `blinklight_washlight_wall_a_vertical_000_set`、 + `neonsign`、`stage000_shop` 这类具体名字,没有 `BgWash*`。 + +也查过 BlinkLight 的键里有没有回指 BgColor2 的索引字段 —— 没有 +(有 `CmnColorType0Array`/`CmnColorType1Array` 两个 int 数组,语义未知,可能相关但无证据)。 + +所以它和 BgColor1 的 `BgBL`/`FollowSpotColor`/`Shadow` 属于同一类「非物件名的组」, +映射未知。当前实现:**名字能解析到渲染器就只写那些;解析不到就退回原来的全舞台写入 +并打一次警告**。行为不会比改动前更差,映射查明后删掉 fallback 分支即可。 + +--- + +### A3. Spotlight3d (68) 的颜色写入是空操作 —— 31 首 / 1089 keys + +```csharp +// Director.cs:705 +mat.SetColor("_Color", keyData.color); // ← 没有任何舞台 shader 有 _Color +mat.SetFloat("_ColorPower", keyData.colorPower); // ← 这个是有的,会生效 +``` + +**证据** —— 枚举 `shader` bundle 里全部 133 个 `Gallop/3D/{Live,Bg,Stage}` shader, +暴露 `_Color` 的只有两个,都和聚光灯无关: + +``` +Gallop/3D/Bg/BgShadowOnly +Gallop/3D/Bg/RedAlphaGreenColorShadowFogUVScroll +``` + +`Gallop/3D/Live/Stage/*` 里**一个都没有**。灯柱用的是 `StageBeamLight` 系列: + +``` +Gallop/3D/Live/Stage/StageBeamLight ['_MulColor0','_MulColor1','_ColorPowerMultiply','_ColorPower'] +Gallop/3D/Live/Stage/StageBeamLightCutoff (同上) +Gallop/3D/Live/Stage/StageBeamLightFadeout (同上) +``` + +这就是 `a3a7b83` 里 BlinkLight 写 `_Color` 那个 bug 的同一份,只是当时没顺手扫到 +Spotlight3d。因为 `_ColorPower` **确实生效**,聚光灯现在的表现是「材质原色 × 关键帧亮度」—— +亮度会跟着音乐动,颜色永远不对。这正是那种「看着能用但不对」的路由问题。 + +**方案** —— 照搬 `OnBlinkLightUpdate` 已经写好的 shader 探测逻辑:按 +`_MulColor0` → `_MulColor1` → `_BlinkLightColor` 的顺序问 `Shader.GetPropertyType`, +写命中的那个,`colorPower` 单独进 `_ColorPower`(**不要折进颜色里**,遵守 CLAUDE.md 的规则)。 + +顺带确认过 key 结构是对的,不用动数据层:`assetName: "spotlight3d000"`、 +`characterIndex: -1`,C# 的 `LiveTimelineKeySpotlight3dData` 字段顺序与 TypeTree 完全一致。 + +--- + +### A4. Transform (31) 只认 unit 名,GameObject 名整条轨道静默失效 —— 12 首 / 681 keys + +```csharp +// StageController.cs:186 UpdateTransform +if (StageObjectUnitMap.TryGetValue(updateInfo.data.name, out StageObjectUnit objectUnit)) +{ ... } +// 没有 else —— 组名不是 unit 名就什么都不做,也不报错 +``` + +**证据** —— 歌曲 1028(transformList 语料最多,206 keys)第一个组名: + +``` +name: "light001_glow_001" +position: (-2.0, 0.5, 0.0) scale: (0.2, 0.2, 0.2) +``` + +这是 GameObject 命名风格,不是 `neonsign` 那种 unit 名。 + +这是 `a3a7b83` 修的 Object 轨道 bug 的**镜像**:那次是 handler 只查 `StageObjectMap`、 +漏了 unit 名;这次是只查 `StageObjectUnitMap`、漏了 GameObject 名。同一个函数对里, +`UpdateObject` 两种都查,`UpdateTransform` 只查一种。 + +同一处还有一个次要缺陷: + +- `StageObjectMap.TryGetValue(child.name, ...)`(:190)没有 `.Replace("(Clone)","")`, + 而 `UpdateObject` 有(:96)——map 里存的是剥掉 `(Clone)` 的名字。 + +> **更正**:初稿说这里「完全无视 `OffsetType`」,这条是错的。 +> `--tree transformList 1028` 实测 `LiveTimelineKeyTransformData` 只有 +> `position` / `rotate` / `scale` 三个字段,**没有 OffsetType**(和 Object 轨道不同), +> C# 类和 TypeTree 完全一致。所以绝对写入本来就是对的,不需要 Add 相对语义。 + +**方案** —— 先查 `StageObjectUnitMap`,miss 再查 `StageObjectMap`(剥 `(Clone)`), +TRS 抽成一个 `ApplyTransformTrs` helper 供两个分支共用。 + +--- + +## A5 补记:`_Color` 空写的系统性排查 + +同一个 bug 出现三次(BlinkLight → Spotlight3d → UVScrollLight)之后,做了一次全量对照, +以后加新的属性写入可以照跑一遍当回归检查: + +```bash +# 1. 抽出 Director 里写过的所有 shader 属性名 +grep -oE 'Set(Color|Float|Vector|Texture|Int)\("_[A-Za-z0-9_]+"' Director.cs \ + | sed 's/.*("//;s/"//' | sort -u > /tmp/props.txt + +# 2. 拿 499 个 shader 的属性表逐个核对(见本文件末尾的 MetaDb 用法) +# 重点不是「这个属性存不存在」,而是「它存在于**目标那一族** shader 上吗」 +``` + +2026-08-05 的结果(23 个属性名): + +| 属性 | 拥有它的 shader | 判断 | +|---|---|---| +| `_Color` | 152 个,**舞台/Bg 只有 2 个**(`BgShadowOnly`、`RedAlphaGreenColorShadowFogUVScroll`,都与灯光无关)| ❌ 就是这条暴露了 UVScrollLight | +| `_ProjectorColorPower` | 7 个,其中 6 个是舞台 | ✅ WashLight 的写入是对的 | +| rim / toon 那批(`_RimColor`…`_CharaColor`)| 100+ 个角色 shader,舞台 0 个 | ✅ 符合预期 | + +**教训**:「这个属性存在」不等于「目标 shader 上有这个属性」。 +三次都是栽在这一步 —— 属性名在别的 shader 家族里是真的,于是看起来很合理。 + +--- + +## B. 性能 / 一致性(不改变画面,但每帧都在烧) + +| # | 位置 | 问题 | +|---|------|------| +| B1 | `Director.cs` 668-669 / 702-703 / 719 / 365 | `Renderer.materials` 每帧调用。这个 getter **每次都实例化材质副本并新分配数组**,彻底破坏批处理。`a3a7b83` 已经为 BlinkLight 修过(改 MPB),BgColor2 / Spotlight3d / UVScrollLight / GlobalLight 四处漏网。BgColor2 最严重:最多 15 个组 × 全舞台渲染器 = 每帧上千次材质实例化 | +| B2 | 同上 + `OnParticleUpdate` / `OnParticleGroupUpdate` | `GetComponentsInChildren()` 每帧全场景遍历 + 分配。应照 `_bgColor1StageCache` 的做法首帧解析一次 | +| B3 | `Director.cs` 668 / 977 / 988 | 漏了 `true` 参数 → **跳过 inactive 对象**。而 `IsTimelineControlledLight` 命中的灯全部以 inactive 起步,UVScrollLight(:717) 和 Spotlight3d(:702) 传了 `true`,这几处没传。同一份代码里两种写法,是不一致而不是有意设计 | +| B4 | `Director.cs` 347 / 413 | 每个 locator 每帧 `new MaterialPropertyBlock()`。A1 的修法顺带解决 | + +--- + +## C. 语义存疑(有证据但需要更多 ground truth 才能定方案) + +### C1. GlobalFog (49):把高度雾当成距离雾在做 + +歌曲 1177 的 key 实测: + +```json +"isDistance": 0, "isHeight": 1, "height": 15.8, "heightDensity": 0.03, +"fogMode": 2, "expDensity": 0.02, "start": 0.0, "end": 300.0 +``` + +即「**不要**距离雾,要一层 y=15.8 的高度雾」。而 handler: + +```csharp +RenderSettings.fog = keyData.isDistance || keyData.isHeight || keyData.fogMode != 0; // → true +RenderSettings.fogMode = (FogMode)keyData.fogMode; // → Exponential,枚举值没问题 +RenderSettings.fogDensity = keyData.expDensity; // 距离雾密度 +``` + +于是数据说「没有距离雾」,画面上却出现了距离雾。`height` / `heightDensity` / +`startDistance` 三个字段完全没用上。 + +`(FogMode)2` = `Exponential` 是合法的,枚举转换本身**没有**越界问题——问题只在开关条件。 + +Unity 的 `RenderSettings` 没有高度雾,做不了 1:1。两条路: + +- **保守**:`RenderSettings.fog = keyData.isDistance != 0;`,高度雾留 TODO。 + 代价是 1177 这类只有高度雾的歌会完全没有雾——比现在错误的雾更接近真相,但视觉上更空。 +- **正确**:高度雾需要自写 URP RendererFeature,和 PostFilm 同一档工作量。 + +建议先跑一遍 41 首有 globalFog 的歌,统计 `isDistance` / `isHeight` 的分布再决定, +这个统计 `tools/` 一条命令就能出。 + +### C2. `StageObjectMap` 去重让重名灯的第 2..N 个永远关不掉 + +```csharp +// StageController.cs:60 +if (!StageObjectMap.ContainsKey(child.name)) // ← 用未剥 (Clone) 的名字判重 +{ + if (IsTimelineControlledLight(child.name)) + child.gameObject.SetActive(false); + var tmp_name = child.name.Replace("(Clone)", ""); + StageObjectMap.Add(tmp_name, child.gameObject); // ← 存剥掉的名字 +} +``` + +`SetActive(false)` 在判重的 `if` **里面**,所以同名对象只有第一个被关掉。 +另外判重用 `child.name`、插入用 `tmp_name`,两个键不一致。 + +> **这条我连着猜错了两次,实测数据两次都把方案推翻了。记在这里当反面教材。** +> +> **初稿的说法**:「一组 66 个同名 `wash_truss_a` 只有第一个被关,其余 65 个常亮」。 +> **错。** 那个 66 是观众 `mob_a000` 的数量(BgColor1 的目标),不是灯。 +> live10149 上重名的受控灯只有 4 个名字 / 10 个对象。 +> +> **第二稿的说法**:「要先给三个灯光 handler 加 name→List 支持,否则移动 SetActive +> 会让 65 盏灯全灭」。**也错。** 实测 1177 的 28 个 BlinkLight 组名**全部一一对应 +> 一个 GameObject**(27 命中 + 1 个 `wash_discoball` 无对应物件),组名指向的是**容器**, +> 真正的灯是子物件,`GetComponentsInChildren` 早就覆盖了。所以 name→List 完全没必要。 + +**真实的 bug,方向和前两稿都相反** + +重名的灯全是容器的**子孙**: + +``` +light001_wash_ground_a < swing_wash_ground_a_003 < wash_ground_a_000_l_003_set + < pfb_env_live10149_blinklight_wash_ground_a ← 轨道寻址的是这个 +``` + +容器名含 `blinklight` → 被关掉(正确,等 handler 打开)。 +子物件名含 `_wash_` → **只有第一个**被关掉,而 handler 只 `SetActive(true)` 容器, +那第一个子物件再没有人打开 —— **全程是黑的**。 + +live10149 上的受害者:`light001_wash_ground_a`(×4)、`swing_wash_ground_a`(×2)、 +`base_rotate_wash_ground_a`(×2)、`light000_wash_ground_a`(×2),各有 1 盏常黑,共 4 盏。 + +**已修** —— 只关「本分支里最上层」的那盏受控灯:祖先已经是受控灯的就跳过, +跟着祖先一起显隐。容器照常关,子物件保持 prefab 状态。判重键也统一成 `tmp_name`。 +纯增益,没有「某盏灯从此不亮」的倒退风险 —— 因为不再有子物件被单独关掉。 + +### C4. 舞台地板 / 高光面发白 —— 已定位到 shader,只剩一个运行时未知项 + +CLAUDE.md 长期记着「地板 `plane_000` / `stage_object_001` / `specular_002` 偏亮发白, +早于时间轴的改动就存在」。这次查清了它们的共同点。 + +**已确认(bundle ground truth)** + +三个物件用的是**同一个** shader,而且是舞台 shader 里唯一带环境反射的一族: + +``` +Gallop/3D/Live/Stage/DefaultEnvMapNoAmbient + 真实属性表(只有 8 个): + _MainTex _MulColor0 _ColorPower _AddColor + _TripleMaskMap _EnvMap _EnvRate(Range 0..1, 默认 0.5) _EnvBias(Range 0..8, 默认 1) +``` + +| 物件 | 材质 | `_EnvRate` | `_EnvBias` | +|---|---|---|---| +| `plane_000` | `mtl_env_live10149_default_env001` | **0.75** | 2.0 | +| `stage_object_001` | `mtl_env_live10149_default_env002` | **1.00** | 2.0 | +| `specular_002` | `mtl_env_live10149_glitter002` | **1.00** | 5.0 | + +三个材质的 `_EnvMap` 在数据里都是**绑定了**的(指向其它 bundle,`m_FileID` 1/2,不是 null)。 + +顺带印证了 CLAUDE.md 那条警告:这三个材质的 `m_SavedProperties` 里还留着一大堆 +URP Lit 的属性(`_WorkflowMode`、`_Smoothness`、`_ClearCoatMask`、`_BlendModePreserveSpecular`…), +而当前 shader 一个都没声明。**存档表不可信,必须问 shader。** + +**实机日志结果:贴图缺失假设被证伪** + +``` +[EnvMapDiag] DefaultEnvMapNoAmbient:3 个渲染器,其中 _EnvMap 为 null 的有 0 个 + plane_000 mat=mtl_env_live10149_default_env001 _EnvMap=tex_env_live10149_reflection000 _EnvRate=0.75 _EnvBias=2.00 + specular_002 mat=mtl_env_live10149_glitter002 _EnvMap=tex_env_live10149_reflection001 _EnvRate=1.00 _EnvBias=5.00 + stage_object_001 mat=mtl_env_live10149_default_env002 _EnvMap=tex_env_live10149_reflection000 _EnvRate=1.00 _EnvBias=2.00 +``` + +贴图全部解析成功,不是缺贴图。假设作废。 + +**已排除的三条** + +| 排除项 | 依据 | +|---|---| +| 光照太强 | 舞台 `Light` 组件 0 个,Live 代码不碰 `Light` | +| `_EnvMap` 缺失 | 上面的日志,0 个 null | +| BgColor1 的 `power` 没写进 `_ColorPower` | 三个组的 `power` **中位数都是 1.000**(范围 0.09–1.0,13 个不同值)。就算写了,绝大多数时间也是乘 1,**解释不了持续发白**。这条负面结果反过来支持了当初撤回该写入的决定 | + +**候选 A(证据较强):env 反射项不受颜色通道调制** + +这是一个干净的对照: + +- 发白的 3 个物件 = 全场仅有的 3 个用 `DefaultEnvMapNoAmbient` 的物件。 +- 它们的 `_MulColor0` 被 BgColor1 写成**接近黑**:1177 开场是 `(0.06, 0.06, 0.08)`, + 257 个关键帧 44 种颜色,确实在动。 +- 同样由 BgColor1 写同一个 `_MulColor0` 通道、但用 `DefaultNoAmbient`(无 env)的 + `mob_a000`(50 renderer) / `mob_b000`(57) / `truss_neonsign` 等**没有发白问题**。 + +同一个写入者、同一个通道,唯一变量是 env 路径 —— 颜色都压到近黑了还是亮, +说明亮的那一项**不经过 `_MulColor0`**。 + +~~可证伪预测:`plane_000` 的 `_EnvRate` 是 0.75、另两个是 1.0,所以 `plane_000` 应当 +明显比另外两个轻。如果三者一样白,候选 A 也要丢掉。~~ + +> **这个检验设计得不好,实测「看不出差异」并不能否定候选 A。** +> 0.75 与 1.00 只差 25%,如果反射项本来就超过 1.0 被钳到白,两者会一样白。 +> 也就是说这个对比无论结果如何都区分不了 —— 它不是一个有效的可证伪检验,是我把它说过头了。 +> +> 真正二元的检验是把 `_EnvRate` **直接归零**: +> 发白消失 → 就是 env 反射项;发白照旧 → env 路径无关。 +> 已实现为 `StageEnvRateToggle`(按 **F9** 在「原始值 ↔ 0」间来回切,走 MPB 不动材质, +> 随 `StageEnvMapDiag.Dump()` 自动挂上)。 + +**候选 B(可能相关,未证实):缺失的平面镜面反射系统** + +Environment (58) 轨道(1375 keys / 49 首,MAP 记为「Planar Reflection 完全没有」)在 1177 带着: + +``` +isValidMirror: 1 isBgMirror: 1 mirrorReflectionRate: 0.134 +``` + +反射强度 0.134,而材质里是 0.75–1.0。**但 `mirrorReflectionRate` → `_EnvRate` 没有任何证据**: +前者属于平面镜系统(第二摄像机 → RenderTexture),后者是静态 env 贴图的混合率, +是两套东西。Environment 的 key 里也没有任何直接对应 env map 的字段(其余全是 water/fov)。 +所以这条只当线索,不当结论。 + +**判决实验结果:候选 A、B 均被否定** + +F9 实测。日志确认开关确实生效(9 次切换 × 3 个渲染器,`[EnvMapDiag] _EnvRate → 0/原始值`), +**画面无变化**。所以 env 反射项不是发白的来源,候选 A 作废;候选 B 依附于同一表面的 +反射设想,一并降级。 + +**同时发现:地板本来就该是亮的** + +之前只看了第 0 帧的 `(0.06, 0.06, 0.08)` 就默认"数据要求地板是暗的",这是取样偏差。 +全曲 257 个关键帧的亮度分布(Rec.601 luma): + +| | plane_000 | +|---|---| +| 中位数 | **0.548** | +| 均值 | 0.505 | +| > 0.3 | 207 / 257(81%)| +| > 0.5 | 133 / 257(52%)| +| > 0.8 | 36 / 257(14%,最高 0.95)| + +近黑只出现在开场几帧。**全曲一多半时间地板本来就该是中等偏亮的** —— +所以"偏亮"这个主观印象里有多少是 bug、多少是原本的美术意图,目前无法区分。 + +**已排除的机制汇总(4 条)** + +1. 光照太强 —— 舞台 `Light` 组件 0 个 +2. `_EnvMap` 缺失 —— 日志实测 0 个 null +3. BgColor1 `power` → `_ColorPower` —— 中位数 1.000,写了也是乘 1 +4. env 反射项 —— `_EnvRate` 归零无变化 + +**下一个二元实验(未做)**:把 `_MulColor0` 强制设为纯黑。 +地板变黑 → BgColor1 的写入确实主导表面,亮度就是数据本身,"发白"是美术预期问题; +地板仍白 → 写入没有到达表面,回头查 MPB / renderer 解析 / shader 是否真的用这个通道。 + +### C3. ParticleGroup 覆盖 Particle(6 首 / 31+27 keys,优先级低) + +两个 handler 都按 `ps.gameObject.name == data.name` 匹配并写同一个 +`emission.rateOverTime`,ParticleGroup 在后(:376 vs :375),同名时覆盖 Particle。 +另外 `new MinMaxCurve(FlickerDarkRate, FlickerLightRate)` 隐含 dark < light, +数据里是否成立没验证过。数据量很小,可以最后处理。 + +--- + +## 建议顺序 + +按「影响面 × 修复成本」排: + +1. **A1** GlobalLight 被清空 —— 59/59 首,改动约 15 行,收益最大 +2. **A2** BgColor2 组名路由 —— 15 首 / 14346 keys,可复用 BgColor1 现成的解析缓存,顺带清掉 B1/B2 最重的一处 +3. **A3** Spotlight3d `_Color` —— 31 首,照抄 BlinkLight 的探测逻辑 +4. **C2** StageObjectMap 去重 —— 4 行,且会影响 A2/A3 的解析结果,建议和它们一起做 +5. **A4** Transform 解析 —— 12 首,需要先 dump 确认 OffsetType 是否存在 +6. **B1–B4** 性能 —— 多数在 1–4 做完时自然消掉,剩余单独扫一遍 +7. **C1 / C3** —— 需要先做语料统计才能定方案 + +前 4 项之后建议重跑 `LiveTimelineWorksheetDiag.Dump()` 对照歌曲 1177 与 1006, +确认组名解析命中率,再更新 `LIVE_DEV_MAP.md` 里这几行的 ✅/⚠️ 标记。 + +--- + +## 附:本次用到的验证命令 + +```bash +cd tools +P=~/.venvs/umatools/bin/python3 + +# 轨道覆盖率(keys / 首数) +$P -c "import csv;rows=list(csv.DictReader(open('out/scan_keys.csv'))); + v=[int(r['bgColor2List']) for r in rows];print(sum(v), sum(1 for x in v if x>0))" + +# 组名 —— 判断轨道是按 GameObject 名还是 unit 名 / 分区名路由 +$P -c "from uma_common import MetaDb +env=MetaDb().load('cutt/cutt_son1006/son1006_camera') +for o in env.objects: + if o.type.name=='MonoBehaviour': + t=o.read_typetree() + if t.get('bgColor2List'): print([g['name'] for g in t['bgColor2List']])" + +# shader 属性表 —— 确认某个属性名到底存不存在 +$P -c "from uma_common import MetaDb +for o in MetaDb().load('shader').objects: + if o.type.name!='Shader': continue + pf=o.read_typetree()['m_ParsedForm'] + if pf['m_Name'].startswith('Gallop/3D/Live/Stage'): + print(pf['m_Name'], [p['m_Name'] for p in pf['m_PropInfo']['m_Props']])" + +# 单个 key 的真实值 +$P dump_cutt_typetree.py --sample spotlight3dList 1081 +$P dump_cutt_typetree.py --tree transformList 1028 +``` diff --git a/LIVE_DEV_MAP.md b/LIVE_DEV_MAP.md index d4923a0..5993dd7 100644 --- a/LIVE_DEV_MAP.md +++ b/LIVE_DEV_MAP.md @@ -21,45 +21,65 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 --- +## 状态记号 + +2026-08-05 起区分「实现存在」和「实现被核查过」: + +| 记号 | 含义 | +|------|------| +| ✅ | 已实现,**且本次审计核查过**(读代码 + 用 `tools/` 对过 bundle 数据)| +| ✅? | 标着已实现,但**没人核查过**。本项目已经出现过 8 次「看着能用其实整条静默失效」,未核查 ≠ 正常 | +| ⚠️ | 已实现但不完整,缺的部分写在备注里 | +| ❌ | 未实现 | + +审计报告见 `LIVE_AUDIT_2026-08-05.md`。 + +--- + ## 轨道 → Unity API 对照 ### GameObject / Transform | 轨道 | 状态 | 操作 | |------|------|------| -| Object (33) | ✅ | `SetActive` | -| Transform (31) | ✅ | position / rotation / scale | +| Object (33) | ✅? | `SetActive` | +| Transform (31) | ✅ | position / rotation / scale。2026-08-05 修:原先只查 `StageObjectUnitMap`,组名是 GameObject 名(son1028 的 `light001_glow_001`)时整条轨道静默失效,12 首 / 681 keys 从未生效 | | WashLight (43) | ⚠️ | `SetActive`(颜色/投影未做) | | Laser (44) | ⚠️ | `SetActive` + transform(blink 未做) | -| BlinkLight (45) | ⚠️ | `SetActive` + 子 `Light`(color1Array/BlendMode 未做) | -| Spotlight3d (68) | ✅ | `SetActive` + `Light.color/intensity` | -| Effect (60) | ✅ | `Instantiate` prefab | +| BlinkLight (45) | ⚠️ | `SetActive` + 写 Renderer 的 `_BlinkLightColor`/`_ColorPower`(color1Array/BlendMode 未做) | +| Spotlight3d (68) | ✅ | `SetActive` + 写 Renderer 颜色属性(**不是** Unity `Light`)。2026-08-05 修:原先写 `_Color`,而 `Gallop/3D/Live/Stage/*` 下**没有任何 shader** 声明该属性(133 个全枚举过),颜色写入一直是空操作;灯柱是 `StageBeamLight` 系列 → `_MulColor0` + `_ColorPower`,改用 BlinkLight 的 shader 探测 | +| Effect (60) | ✅? | `Instantiate` prefab | + +> ⚠️ **舞台上没有 Unity `Light` 组件。** live10149 实测:10102 个 GameObject、 +> 898 MeshRenderer、602 SkinnedMeshRenderer、**Light 0 个**;整个 Live 代码也没有任何 +> 地方读写 `Light.color/intensity`。所有「灯」都是自发光几何体,靠时间轴写 shader 颜色属性。 +> 本表此前把 BlinkLight / Spotlight3d 写成「子 Light color/intensity」,是错的,已更正。 ### Camera | 轨道 | 状态 | |------|------| -| CameraPos (1) / LookAt (2) / Fov (3) / Roll (4) | ✅ | -| CameraSwitcher (11) | ✅ | -| MultiCameraPos (53) / LookAt (54) | ✅ | +| CameraPos (1) / LookAt (2) / Fov (3) / Roll (4) | ✅? | +| CameraSwitcher (11) | ✅? | +| MultiCameraPos (53) / LookAt (54) | ✅? | ### MaterialPropertyBlock → Shader | 轨道 | 状态 | 属性 | |------|------|------| | BgColor1 (8) 角色分支 | ⚠️ | `_CharaColor/_ToonDarkColor/_ToonBrightColor/_OutlineColor/_Saturation`;`vertexColorToonPower`/`outlineWidthPower`/`LightBlendMode`/`IsSilhouette` 已填入 UpdateInfo 但 handler 还没用 | | BgColor1 (8) 舞台分支 | ⚠️ | 2026-08-04 新增。遍历舞台层级按 Transform 名解析 Renderer,写 **`_MulColor0`**(已确认)。`_AmbientColor` 归 BgColor2,别抢 | -| GlobalLight (48) | ✅ | 角色 rim light 属性 | +| GlobalLight (48) | ✅ | 角色 rim light 属性。2026-08-05 修:`SetPropertyBlock` 是整块替换,BgColor1 角色分支在它之后跑,每帧把 13 个 rim 属性全抹掉 —— 全语料 59/59 首同时有这两条轨道,等于从来没生效过。两处都改成 `GetPropertyBlock` 读-改-写 | | UVScrollLight (46) | ⚠️ | `_MainTex` offset 累积(已修 bug,mulColor 等未用) | ### ParticleSystem | 轨道 | 状态 | |------|------| -| Particle (41) | ✅ `emission.rateOverTime` | -| ParticleGroup (42) | ✅ `FlickerLightRate` MinMaxCurve | +| Particle (41) | ✅? `emission.rateOverTime`;每帧全场景 GetComponentsInChildren 且漏 inactive | +| ParticleGroup (42) | ✅? `FlickerLightRate` MinMaxCurve;与 Particle 同名时会覆盖它 | ### RenderSettings | 轨道 | 状态 | |------|------| -| GlobalFog (49) | ✅ fogColor / fogMode / fogDensity / fogStart / fogEnd | -| BgColor2 (9) | ✅ 写含 `_AmbientColor` 属性的舞台材质,`Lerp(color1, white, value)` | +| GlobalFog (49) | ⚠️ 已核查:数据是**高度雾**(`isDistance=0,isHeight=1`),却当成距离雾在开;Unity RenderSettings 没有高度雾。见审计报告 C1 | +| BgColor2 (9) | ⚠️ 写 `_AmbientColor`,`Lerp(color1, color2, value)`。2026-08-05:组名已接通(原先整条被丢弃,最多 15 个组每帧各刷一遍全舞台、只有最后一个留得下来),性能已修。**但 `BgWashA..O`/`LaserA..C` 既不是 GameObject 名也不是 unit 名,映射未知** —— 解析不到时退回全舞台写入并打警告 | ### URP Volume / RendererFeature | 轨道 | 状态 | @@ -72,8 +92,8 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 ### 内部系统 | 轨道 | 状态 | |------|------| -| CharaMotionSequence (7) / LipSync (12) / FormationOffset (28) | ✅ | -| FacialFace/Eye/Mouth 等 (18–25) | ✅ | +| CharaMotionSequence (7) / LipSync (12) / FormationOffset (28) | ✅? | +| FacialFace/Eye/Mouth 等 (18–25) | ✅? | --- @@ -141,3 +161,4 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 | `Assets/Resources/RenderPipeline/UMAUniversalRenderPipelineAsset_Renderer.asset` | RendererFeature 注册位置 | | `LIVE_TRACKS.md` | 103 条轨道全览(ID/覆盖率/状态/Bug)| | `live-shader-todo.md` | 后处理轨道实现细节 | +| `LIVE_AUDIT_2026-08-05.md` | ✅ 轨道审计报告:A1–A4 已修的证据与改法,C1–C3 未修项及其阻塞原因 |