docs+diag: audit report, stage env-map diagnostic, verified/unverified marks

LIVE_AUDIT_2026-08-05.md records the audit behind the previous commit: the
evidence for each fix, the tool invocations that produced it, and — deliberately
— the three conclusions I got wrong before the data corrected them, since each
was a plausible-sounding guess:

  * "66 same-named wash lights stay lit" — that 66 is the mob_a000 crowd, not
    lights. live10149 has 4 duplicated light names, 10 objects.
  * "the light handlers need name -> List support first" — every one of song
    1177's 28 BlinkLight group names maps to exactly one GameObject; the groups
    address containers and GetComponentsInChildren already covered the children.
  * "plane_000 should look lighter than the other two" — _EnvRate 0.75 vs 1.00
    cannot be distinguished once the term saturates to white, so that comparison
    could never have decided anything either way.

Also records which song exercises which fix, since only 1177 is downloaded
locally and it carries no bgColor2 / spotlight3d / transform data.

Stage floor blow-out (plane_000 / stage_object_001 / specular_002): four
mechanisms now ruled out rather than one cause found. Not lighting — live10149
has 10102 GameObjects and zero Light components, and the Live code never touches
Light at all. Not a missing reflection texture — StageEnvMapDiag reports 0 of 3
null. Not the unwritten _ColorPower — BgColor1's power field has a median of
1.000 on all three groups, which retroactively supports reverting that write.
Not the env reflection term — F9 zeroes _EnvRate on all three renderers, and the
log confirms it fired nine times with no visible change. Also corrects a sampling
error of mine: frame 0 is near-black, but across all 257 keys the floor's median
luminance is 0.548 and 81% of keys exceed 0.3, so it is meant to be bright for
most of the song. Next binary test is forcing _MulColor0 to black.

StageEnvMapDiag logs the env-map bindings once at stage init and attaches an
opt-in F9 toggle that flips _EnvRate between its authored value and 0 through a
property block. Diagnostic only; inert unless the key is pressed.

LIVE_DEV_MAP.md now separates "implemented" from "checked". Ten rows drop to
"OK?" because nobody has verified them — this session found four tracks that
were marked done and had never once produced output, so an unchecked mark is
not evidence of health. GlobalFog drops to partial: its data is height fog
(isDistance=0, isHeight=1) but the handler enables Unity distance fog, and
RenderSettings has no height fog. The claim that BlinkLight and Spotlight3d
drive child Light components was wrong and is corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ritenseki
2026-08-05 13:42:47 +08:00
parent 64087b534e
commit 652c306099
4 changed files with 744 additions and 15 deletions
@@ -0,0 +1,162 @@
using System.Collections.Generic;
using UnityEngine;
namespace Gallop.Live
{
/// <summary>
/// 舞台地板/高光面「过亮发白」的诊断。
///
/// 已从 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 应该比另外两个略轻一些。
/// 若三者一样白,说明白化另有来源,这条假设就该丢掉。
/// </summary>
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<string>();
int total = 0, nullEnv = 0;
foreach (var r in stage.GetComponentsInChildren<Renderer>(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<StageEnvRateToggle>() == null)
stage.gameObject.AddComponent<StageEnvRateToggle>();
}
}
/// <summary>
/// 判决实验:按 F9 把 <c>DefaultEnvMapNoAmbient</c> 的 `_EnvRate` 在「原始值 ↔ 0」之间切换。
///
/// 为什么需要它:`_EnvRate` 0.75 与 1.00 只差 25%,如果反射项本来就超过 1.0 被钳到白,
/// 两者会一样白 —— 所以「plane_000 看起来没比别的轻」**不能**否定反射项假设,
/// 那个对比检验设计得太弱。把 `_EnvRate` 直接归零才是二元的:
///
/// 发白消失 → 就是 env 反射项,问题转化为「反射强度该由谁压下来」
/// 发白照旧 → env 路径无关,改查 Built-in shader 在 URP 下的翻译
///
/// 走 MaterialPropertyBlock,不动材质本身;再按一次即可还原。纯诊断,默认不改变任何东西。
/// </summary>
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<Renderer> _targets = new List<Renderer>();
private bool _collected;
private void Collect()
{
_collected = true;
foreach (var r in GetComponentsInChildren<Renderer>(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} 个渲染器。地板此刻变了吗?");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2f47145aad184ad3a6ae81cb1b711182
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+535
View File
@@ -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<MaterialPropertyBlock> 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<Renderer>())
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<Renderer/ParticleSystem>()` 每帧全场景遍历 + 分配。应照 `_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
```
+36 -15
View File
@@ -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 未修项及其阻塞原因 |