From 64087b534e830aabd0179cb76af91fc2cef03bd2 Mon Sep 17 00:00:00 2001 From: ritenseki Date: Wed, 5 Aug 2026 13:41:59 +0800 Subject: [PATCH] Live: fix five silently-failing track paths + one init bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All found by auditing the tracks LIVE_DEV_MAP marked as done. Every one of them parses its keyframes correctly and then drops the result downstream with no error, so the picture looks plausible and nothing ever surfaces the failure. Verified against the bundles with tools/ before changing anything. GlobalLight (48) — 9636 keys / 59 songs, never worked * Renderer.SetPropertyBlock() replaces the whole block rather than merging. AlterLateUpdate runs GlobalLight then BgColor1, and both allocated a fresh block for the same container.Renderers, so BgColor1 wiped all 13 rim properties every frame. Song 1177 carries GlobalLight x2 and CharaColor x2, and bgColor1/globalLight both cover 59/59 songs — this was never once live. * Both handlers now GetPropertyBlock -> modify -> SetPropertyBlock, sharing one persistent block. The stage-side BgColor1 branch and BlinkLight already did this; the two character-side handlers were the only exceptions. * _UseOriginalDirectionalLight / _OriginalDirectionalLightDir moved into the block too. They were written through renderer.materials, which instantiates a material copy and allocates an array on every frame, per renderer. BgColor2 (9) — 14346 keys / 15 songs, at most one group survived * The dispatcher never filled the group name and the handler never read one, so up to 15 groups (son1010 carries BgWashA..BgWashO) each repainted the entire stage every frame and only the last one in list order survived. * Name is now threaded through and resolved via ResolveStageTargets, writing _AmbientColor through a MaterialPropertyBlock, resolved once and cached. * The name -> renderer mapping is still unknown and is NOT guessed here. BgWashA..O / LaserA..C are neither GameObject names nor _stageObjectUnits unit names (the three downloaded stages use concrete names like neonsign), and BlinkLight carries no index back into BgColor2. Unresolved names fall back to the previous whole-stage write and log a warning, so behaviour is never worse than before; delete the fallback once the mapping is known. Spotlight3d (68) — 1089 keys / 31 songs UVScrollLight (46) * Both wrote _Color. Enumerating all 499 shaders shows no Gallop/3D/Live/Stage shader declares it — the only two stage/Bg shaders that do are BgShadowOnly and RedAlphaGreenColorShadowFogUVScroll, neither related. Spotlight beams are StageBeamLight*, and UVScroll targets LightAdd1_UV and StageLightAdd1_UVAlphaMask_TransmittedLightMask; all use _MulColor0 / _MulColor1 / _ColorPower. The colour write was a no-op while _ColorPower kept working, so the lights tracked the music in brightness but never in colour. * This is the same defect as the BlinkLight _Color bug fixed in a3a7b83, twice over. "The property exists" is not "the target shader has it" — all three slipped through on that distinction. * UVScrollLight also gains the previously unused mulColor1, and both now use MaterialPropertyBlock with cached lookups instead of walking every renderer on the stage and touching r.materials each frame. Transform (31) — 681 keys / 12 songs, never worked * UpdateTransform only consulted StageObjectUnitMap and had no else branch, so a group named after a GameObject silently did nothing. son1028's first group is light001_glow_001. This is the mirror image of the Object-track fix in a3a7b83, which was missing the unit-name path instead. * Also strips "(Clone)" on the child lookup, which UpdateObject already did. * TypeTree confirms LiveTimelineKeyTransformData has only position/rotate/ scale and no OffsetType, so absolute writes are correct here. StageController init — 4 permanently dark lights on live10149 * SetActive(false) sat inside the dedupe check, so among same-named objects only the first was disabled. Track groups address containers, and the lights are their children: light001_wash_ground_a (x4), swing_wash_ground_a (x2), base_rotate_wash_ground_a (x2) and light000_wash_ground_a (x2) all sit under pfb_env_live10149_blinklight_wash_ground_a. The handler re-enables only the container, so that one disabled child was never turned back on. * Now only the topmost timeline-controlled light in each branch is disabled; descendants keep their prefab state and follow the parent. Dedupe and insert also agree on the stripped name now. Only GlobalLight could be verified in the editor — song 1177 carries no bgColor2, spotlight3d or transform data. The rest is code review plus bundle ground truth. Co-Authored-By: Claude Opus 5 --- .../Gallop/Live/Cutt/LiveTimelineControl.cs | 2 + .../Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs | 8 + .../Scripts/umamusume/Gallop/Live/Director.cs | 290 +++++++++++++++--- .../umamusume/Gallop/Live/StageController.cs | 99 ++++-- 4 files changed, 332 insertions(+), 67 deletions(-) diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs index 34d6691..1bbbd70 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs @@ -1877,6 +1877,8 @@ namespace Gallop.Live.Cutt LiveTimelineKeyBgColor2Data d0 = curKey as LiveTimelineKeyBgColor2Data; LiveTimelineKeyBgColor2Data d1 = nextKey as LiveTimelineKeyBgColor2Data; BgColor2UpdateInfo updateInfo = default; + // 组名从 ILiveTimelineGroupDataWithName 基类继承而来,一直可读,只是从没被传下去。 + updateInfo.TimelineName = sheet.bgColor2List[i].name; if (d1 != null && d1.interpolateType != 0) { float t = CalculateInterpolationValue(d0, d1, currentFrame); diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs index 61cbe76..b54245f 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs @@ -53,6 +53,14 @@ namespace Gallop.Live.Cutt public Color color2; // 0x24 public float value; // 0x34 public int rndValueIdx; // 0x38 + + /// + /// 轨道组名(BgColor1UpdateInfo 同名字段的对应物)。 + /// 原先 dispatcher 从不填这个信息,handler 也不读,于是一首歌里最多 15 个组 + /// (son1010 实测 BgWashA..BgWashO)每帧各把整个舞台刷一遍,只有列表里最后一个 + /// 留得下来 —— 前面所有组的关键帧等于不存在。 + /// + public string TimelineName; } public delegate void BgColor2UpdateInfoDelegate(ref BgColor2UpdateInfo updateInfo); diff --git a/Assets/Scripts/umamusume/Gallop/Live/Director.cs b/Assets/Scripts/umamusume/Gallop/Live/Director.cs index 470a91a..f67be85 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Director.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Director.cs @@ -336,37 +336,57 @@ namespace Gallop.Live CharaContainerScript[position].FaceDrivenKeyTarget.AlterUpdateFacialNew(ref updateInfo_, liveTime_); } + /// + /// 角色渲染器共用的 MaterialPropertyBlock。 + /// + /// GlobalLight 和 BgColor1 的角色分支写的是同一批 container.Renderers,而 + /// Renderer.SetPropertyBlock() 是「整块替换」而不是合并。两者在同一帧里按 + /// AlterLateUpdate 的顺序先后跑(GlobalLight → BgColor1),所以各自 new 一个空 block + /// 再 Set 的话,后跑的 BgColor1 每帧都会把 GlobalLight 刚写进去的 13 个 rim 属性 + /// 抹回材质默认值 —— 全语料 59/59 首都同时带这两条轨道的数据(歌曲 1177 是 + /// GlobalLight×2 组 + CharaColor×2 组),于是 GlobalLight 实际上从来没生效过。 + /// + /// 正确做法是先 GetPropertyBlock 把渲染器上已有的块取回来,改完再写回去 —— + /// 舞台侧的 和 + /// 本来就是这么写的,这两个角色侧的 handler 是仅有的两处例外。 + /// 顺带干掉每个 locator 每帧一次的 new。 + /// + private MaterialPropertyBlock _charaBlock; + private void OnGlobalLightUpdate(ref GlobalLightUpdateInfo updateInfo) { var tmpPos = -(updateInfo.lightRotation * Vector3.forward).normalized; + _charaBlock ??= new MaterialPropertyBlock(); foreach (var locator in _liveTimelineControl.liveCharactorLocators) { if (locator == null || !updateInfo.flags.hasFlag(locator.liveCharaStandingPosition) || locator is not LiveTimelineCharaLocator charaLocator) continue; var container = charaLocator.UmaContainer; if (!container) continue; - MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock(); - propertyBlock.SetFloat("_RimShadowRate", updateInfo.globalRimShadowRate); - propertyBlock.SetColor("_RimColor", updateInfo.rimColor); - propertyBlock.SetFloat("_RimStep", updateInfo.rimStep); - propertyBlock.SetFloat("_RimFeather", updateInfo.rimFeather); - propertyBlock.SetFloat("_RimSpecRate", updateInfo.rimSpecRate); - propertyBlock.SetFloat("_RimHorizonOffset", updateInfo.RimHorizonOffset); - propertyBlock.SetFloat("_RimVerticalOffset", updateInfo.RimVerticalOffset); - propertyBlock.SetFloat("_RimHorizonOffset2", updateInfo.RimHorizonOffset2); - propertyBlock.SetFloat("_RimVerticalOffset2", updateInfo.RimVerticalOffset2); - propertyBlock.SetColor("_RimColor2", updateInfo.rimColor2); - propertyBlock.SetFloat("_RimStep2", updateInfo.rimStep2); - propertyBlock.SetFloat("_RimFeather2", updateInfo.rimFeather2); - propertyBlock.SetFloat("_RimSpecRate2", updateInfo.rimSpecRate2); - propertyBlock.SetFloat("_RimShadowRate2", updateInfo.globalRimShadowRate2); foreach (var renderer in container.Renderers) { - renderer.SetPropertyBlock(propertyBlock); - foreach (var mat in renderer.materials) - { - mat.SetFloat("_UseOriginalDirectionalLight", 1); - mat.SetVector("_OriginalDirectionalLightDir", tmpPos); - } + if (renderer == null) continue; + renderer.GetPropertyBlock(_charaBlock); + _charaBlock.SetFloat("_RimShadowRate", updateInfo.globalRimShadowRate); + _charaBlock.SetColor("_RimColor", updateInfo.rimColor); + _charaBlock.SetFloat("_RimStep", updateInfo.rimStep); + _charaBlock.SetFloat("_RimFeather", updateInfo.rimFeather); + _charaBlock.SetFloat("_RimSpecRate", updateInfo.rimSpecRate); + _charaBlock.SetFloat("_RimHorizonOffset", updateInfo.RimHorizonOffset); + _charaBlock.SetFloat("_RimVerticalOffset", updateInfo.RimVerticalOffset); + _charaBlock.SetFloat("_RimHorizonOffset2", updateInfo.RimHorizonOffset2); + _charaBlock.SetFloat("_RimVerticalOffset2", updateInfo.RimVerticalOffset2); + _charaBlock.SetColor("_RimColor2", updateInfo.rimColor2); + _charaBlock.SetFloat("_RimStep2", updateInfo.rimStep2); + _charaBlock.SetFloat("_RimFeather2", updateInfo.rimFeather2); + _charaBlock.SetFloat("_RimSpecRate2", updateInfo.rimSpecRate2); + _charaBlock.SetFloat("_RimShadowRate2", updateInfo.globalRimShadowRate2); + // 这两个原先走 renderer.materials,那个 getter 每次调用都会实例化材质副本 + // 并新分配数组(每帧、每渲染器)。两者在 bundle 的 Gallop/3D/Chara/* 上 + // 分别是 Float 和 Vector,MPB 能直接写;ToonEye/T 上的 [MaterialToggle] + // 只是编辑器 drawer,运行时 SetFloat 一样不会开关键字,所以行为不变。 + _charaBlock.SetFloat("_UseOriginalDirectionalLight", 1); + _charaBlock.SetVector("_OriginalDirectionalLightDir", tmpPos); + renderer.SetPropertyBlock(_charaBlock); } } } @@ -410,14 +430,20 @@ namespace Gallop.Live if (locator == null || (updateInfo.flags != 0 && !EFlags.hasFlag(locator.liveCharaStandingPosition)) || locator is not LiveTimelineCharaLocator charaLocator) continue; var container = charaLocator.UmaContainer; if (!container) continue; - MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock(); - propertyBlock.SetColor("_CharaColor", updateInfo.color); - propertyBlock.SetColor("_ToonDarkColor", updateInfo.toonDarkColor); - propertyBlock.SetColor("_ToonBrightColor", updateInfo.toonBrightColor); - propertyBlock.SetColor("_OutlineColor", updateInfo.outlineColor); - propertyBlock.SetFloat("_Saturation", updateInfo.Saturation); + // 必须 Get→改→Set,否则会把 GlobalLight 同一帧写进去的 rim 属性整块抹掉。 + // 详见 _charaBlock 的注释。 + _charaBlock ??= new MaterialPropertyBlock(); foreach (var renderer in container.Renderers) - renderer.SetPropertyBlock(propertyBlock); + { + if (renderer == null) continue; + renderer.GetPropertyBlock(_charaBlock); + _charaBlock.SetColor("_CharaColor", updateInfo.color); + _charaBlock.SetColor("_ToonDarkColor", updateInfo.toonDarkColor); + _charaBlock.SetColor("_ToonBrightColor", updateInfo.toonBrightColor); + _charaBlock.SetColor("_OutlineColor", updateInfo.outlineColor); + _charaBlock.SetFloat("_Saturation", updateInfo.Saturation); + renderer.SetPropertyBlock(_charaBlock); + } } } @@ -479,7 +505,7 @@ namespace Gallop.Live } /// 用 sharedMaterials 探测(不会实例化材质),只保留确实有候选属性的 Renderer。 - private List ResolveStageTargets(string timelineName) + private List ResolveStageTargets(string timelineName, string[] props = null) { var result = new List(); @@ -525,7 +551,7 @@ namespace Gallop.Live } if (byName.Count > 0) { - CollectTargets(byName, result); + CollectTargets(byName, result, props); if (result.Count > 0) return result; } @@ -544,7 +570,7 @@ namespace Gallop.Live } } } - CollectTargets(matched, result); + CollectTargets(matched, result, props); return result; } @@ -555,8 +581,10 @@ namespace Gallop.Live private int _bgColor1RendererCount; private readonly List _bgColor1MatInfo = new List(); - private static void CollectTargets(IEnumerable renderers, List result) + private static void CollectTargets(IEnumerable renderers, List result, + string[] props = null) { + props ??= kStageBgColor1Props; foreach (var r in renderers) { if (r == null) continue; @@ -564,7 +592,7 @@ namespace Gallop.Live { if (mat == null) continue; string hit = null; - foreach (string p in kStageBgColor1Props) + foreach (string p in props) { if (mat.HasProperty(p)) { hit = p; break; } } @@ -661,14 +689,81 @@ namespace Gallop.Live _activeCameraIndex = kTimelineCameraIndices[cameraIndex_]; } + // BgColor2 的通道。_MulColor0 归 BgColor1,两条轨道不能抢同一个属性。 + private static readonly string[] kStageBgColor2Props = { "_AmbientColor" }; + + private readonly Dictionary> _bgColor2StageCache = + new Dictionary>(); + private List _bgColor2AllTargets; + private MaterialPropertyBlock _bgColor2Block; + + /// + /// BgColor2。原实现有三个问题,这里修掉两个半: + /// + /// 1. **组名被完全丢弃**(已修)。dispatcher 现在把 TimelineName 传下来了。 + /// 2. **每帧 r.materials**(已修)。那个 getter 每次调用都会实例化材质副本并新分配数组, + /// 这里是「整个舞台的渲染器 × 每帧 × 最多 15 个组」,是全项目最重的一处。 + /// 改用 sharedMaterials 解析一次 + MaterialPropertyBlock 写入,和 BgColor1 + /// 舞台分支、BlinkLight 的做法统一。 + /// 3. **组名到底指向什么,仍然未知**(未修,缺 ground truth)。 + /// + /// 关于 3:全语料 15 首里出现过的组名只有 BgWashA..BgWashO、LaserA..LaserC、BgColor2。 + /// 已核实这些**既不是 GameObject 名,也不是 _stageObjectUnits 的 unit 名** + /// (已下载的 live10132/10149/10151 三个舞台的 unit 名分别是 + /// blinklight_washlight_wall_a_vertical_000_set / neonsign / stage000_shop 这类具体名字), + /// BlinkLight 的键里也没有回指 BgColor2 的索引字段。它和 BgColor1 的 + /// BgBL / FollowSpotColor / Shadow 属于同一类「非物件名的组」,映射关系尚未找到。 + /// + /// 所以这里**不猜**:名字能解析到渲染器就只写那些,解析不到就退回原来的全舞台写入 + /// 并打一次警告。行为上不会比改动前更差,映射一旦查明,只需删掉 fallback 分支。 + /// private void OnBgColor2Update(ref BgColor2UpdateInfo updateInfo) { if (_stageController == null) return; Color c = Color.Lerp(updateInfo.color1, updateInfo.color2, updateInfo.value); - foreach (var r in _stageController.GetComponentsInChildren()) - foreach (var mat in r.materials) - if (mat.HasProperty("_AmbientColor")) - mat.SetColor("_AmbientColor", c); + + string key = updateInfo.TimelineName ?? string.Empty; + if (!_bgColor2StageCache.TryGetValue(key, out var targets)) + { + targets = string.IsNullOrEmpty(key) + ? new List() + : ResolveStageTargets(key, kStageBgColor2Props); + _bgColor2StageCache[key] = targets; + if (targets.Count == 0) + Debug.LogWarning($"[BgColor2] 组名 '{key}' 解析不到任何渲染器,退回全舞台写入。" + + $"该组与其它未解析的组会互相覆盖 —— 映射关系待查,见 LIVE_TRACKS.md 「已知未解问题」"); + else + Debug.Log($"[BgColor2] 组名 '{key}' → {targets.Count} 个渲染器"); + } + + _bgColor2Block ??= new MaterialPropertyBlock(); + + // 名字解析成功:只写这一组。 + if (targets.Count > 0) + { + WriteAmbient(targets, c); + return; + } + + // Fallback:维持改动前的全舞台语义(缺映射依据,不改画面行为),但只解析一次。 + if (_bgColor2AllTargets == null) + { + _bgColor2AllTargets = new List(); + CollectTargets(_stageController.GetComponentsInChildren(true), + _bgColor2AllTargets, kStageBgColor2Props); + } + WriteAmbient(_bgColor2AllTargets, c); + } + + private void WriteAmbient(List targets, Color c) + { + foreach (var t in targets) + { + if (t.renderer == null) continue; + t.renderer.GetPropertyBlock(_bgColor2Block); + _bgColor2Block.SetColor(t.prop, c); + t.renderer.SetPropertyBlock(_bgColor2Block); + } } private void OnGlobalFogUpdate(LiveTimelineGlobalFogData fogData, LiveTimelineKeyGlobalFogData keyData) @@ -699,31 +794,132 @@ namespace Gallop.Live go.transform.eulerAngles = keyData.rotation; go.transform.localScale = keyData.scale; - foreach (var r in go.GetComponentsInChildren(true)) - foreach (var mat in r.materials) + // 之前写的是 _Color —— 枚举 shader bundle 里全部 133 个 Gallop/3D/{Live,Bg,Stage} + // shader 后确认,Gallop/3D/Live/Stage/* 下**没有任何一个** shader 声明 _Color + // (整个 bundle 里只有 Bg/BgShadowOnly 和 Bg/RedAlphaGreenColorShadowFogUVScroll 有, + // 都和灯柱无关)。灯柱用的是 StageBeamLight 系列,通道是 _MulColor0/_MulColor1 + + // _ColorPower。所以颜色写入一直是空操作,而 _ColorPower 是有效的 —— + // 表现为「材质原色 × 关键帧亮度」:亮度跟着音乐动,颜色永远不对。 + // 这和 BlinkLight 之前那个 _Color bug 是同一份,复用它的 shader 探测逻辑。 + if (!_spotlightRenderers.TryGetValue(go, out var renderers)) + { + renderers = go.GetComponentsInChildren(true); + _spotlightRenderers[go] = renderers; + } + + _spotlightBlock ??= new MaterialPropertyBlock(); + foreach (var r in renderers) + { + if (r == null) continue; + BlinkTarget t = ResolveBlinkTarget(r); + if (t.colorProp == null) continue; + + r.GetPropertyBlock(_spotlightBlock); + if (t.hasColorPower) { - mat.SetColor("_Color", keyData.color); - mat.SetFloat("_ColorPower", keyData.colorPower); + // 亮度是独立通道,不要折进颜色里,合成交给 shader。 + _spotlightBlock.SetColor(t.colorProp, keyData.color); + _spotlightBlock.SetFloat(kColorPowerProp, keyData.colorPower); } + else + { + _spotlightBlock.SetColor(t.colorProp, keyData.color * keyData.colorPower); + } + r.SetPropertyBlock(_spotlightBlock); + } + // TODO: localHeight / targetCameraType / targetCameraIndex 未实现。 } + private MaterialPropertyBlock _spotlightBlock; + private readonly Dictionary _spotlightRenderers = + new Dictionary(); + + /// 按材质名解析出来的 UVScroll 目标:渲染器 + 该材质原始的 _MainTex 缩放。 + private struct UVScrollTarget + { + public Renderer renderer; + public Vector2 baseScale; + } + + private readonly Dictionary> _uvScrollTargets = + new Dictionary>(); + private readonly HashSet _uvScrollLoggedShaders = new HashSet(); + private MaterialPropertyBlock _uvScrollBlock; + private void OnUVScrollLightUpdate(LiveTimelineUVScrollLightData data, LiveTimelineKeyUVScrollLightData keyData) { if (keyData == null || _stageController == null) return; + if (!_uvScrollAccum.ContainsKey(data.name)) _uvScrollAccum[data.name] = Vector2.zero; + // TODO: 这里用 Time.deltaTime 累积,拖动进度条/暂停后滚动相位会和时间轴对不上。 + // 改成按 currentLiveTime 积分才是确定性的,但「相位从歌曲起点算还是从关键帧起点算」 + // 没有依据,先不动 —— 换算错了比现在更难发现。 _uvScrollAccum[data.name] += new Vector2(keyData.scrollSpeedX, keyData.scrollSpeedY) * Time.deltaTime; Vector2 totalOffset = new Vector2(keyData.scrollOffsetX, keyData.scrollOffsetY) + _uvScrollAccum[data.name]; - foreach (var r in _stageController.GetComponentsInChildren(true)) + + // 首次按材质名解析并缓存。原实现每帧遍历整个舞台的 Renderer 并访问 r.materials, + // 那个 getter 每次都会实例化材质副本 + 新分配数组。 + if (!_uvScrollTargets.TryGetValue(data.name, out var targets)) { - foreach (var mat in r.materials) + targets = new List(); + foreach (var r in _stageController.GetComponentsInChildren(true)) { - if (mat.name.Replace(" (Instance)", "") != data.name) continue; - mat.SetTextureOffset("_MainTex", totalOffset); - mat.SetColor("_Color", keyData.mulColor0 * keyData.colorPower); - // TODO: mulColor1, ColorType0/1, CharacterIndex0/1, IsColorBlend0/1, loopType/loopCount + foreach (var mat in r.sharedMaterials) + { + if (mat == null) continue; + if (mat.name.Replace(" (Instance)", "") != data.name) continue; + targets.Add(new UVScrollTarget + { + renderer = r, + baseScale = mat.HasProperty("_MainTex") ? mat.GetTextureScale("_MainTex") : Vector2.one, + }); + if (!_uvScrollLoggedShaders.Add(data.name)) + break; + // 把实际命中的 shader 记一次,取代那个猜测性的兜底分支: + // 万一将来碰上没有 _ColorPower 的材质,这条日志能直接看出来。 + Debug.Log($"[UVScrollLight] '{data.name}' shader={mat.shader?.name} " + + $"_ColorPower={mat.HasProperty(kColorPowerProp)} _MulColor1={mat.HasProperty("_MulColor1")}"); + break; + } } + _uvScrollTargets[data.name] = targets; + Debug.Log($"[UVScrollLight] 材质 '{data.name}' → {targets.Count} 个渲染器"); } + if (targets.Count == 0) return; + + _uvScrollBlock ??= new MaterialPropertyBlock(); + + foreach (var t in targets) + { + if (t.renderer == null) continue; + t.renderer.GetPropertyBlock(_uvScrollBlock); + + // MPB 没有 SetTextureOffset,等价写法是 _MainTex_ST = (scaleX, scaleY, offsetX, offsetY)。 + _uvScrollBlock.SetVector("_MainTex_ST", + new Vector4(t.baseScale.x, t.baseScale.y, totalOffset.x, totalOffset.y)); + + // 之前写的是 _Color。实测这些材质用的是 + // Gallop/3D/Live/Stage/LightAdd1_UV + // Gallop/3D/Live/Stage/StageLightAdd1_UVAlphaMask_TransmittedLightMask + // 两者的属性都是 _MulColor0/_MulColor1/_ColorPower/_ColorPowerMultiply, + // **没有 _Color** —— 全部 499 个 shader 里带 _Color 的舞台 shader 只有 + // BgShadowOnly 和 RedAlphaGreenColorShadowFogUVScroll,都跟这里无关。 + // 所以颜色写入一直是空操作。这是 BlinkLight、Spotlight3d 之后的同一个 bug 第三例。 + // 字段名 mulColor0/mulColor1/colorPower 与属性名逐字对应,映射关系是硬的。 + // 亮度是独立通道,不折进颜色里,合成交给 shader。 + // 不做「没有 _ColorPower 就乘进颜色」的兜底:实测这条轨道的目标材质 + // (LightAdd1_UV / StageLightAdd1_UVAlphaMask_TransmittedLightMask)都有该属性, + // 兜底分支永远跑不到,却会在真跑到时悄悄产生另一种画面。 + // MPB 往 shader 没有的属性写入本身是无害空操作,不需要守卫。 + _uvScrollBlock.SetColor("_MulColor0", keyData.mulColor0); + _uvScrollBlock.SetColor("_MulColor1", keyData.mulColor1); + _uvScrollBlock.SetFloat(kColorPowerProp, keyData.colorPower); + + t.renderer.SetPropertyBlock(_uvScrollBlock); + } + // TODO: ColorType0/1、CharacterIndex0/1、IsColorBlend0/1、ColorBlendRate0/1、 + // AltCharaColor0/1、loopType/loopCount 未实现。 } private void OnChromaticAberrationUpdate(LiveTimelineChromaticAberrationData data, LiveTimelineKeyChromaticAberrationData keyData) diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageController.cs b/Assets/Scripts/umamusume/Gallop/Live/StageController.cs index 4de650b..9a1fe60 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/StageController.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/StageController.cs @@ -50,6 +50,20 @@ namespace Gallop.Live name.Contains("_wash_") || name.Contains("laser"); + /// + /// 祖先里是否已经有一盏受时间轴控制的灯。有的话本对象跟着祖先一起显隐, + /// 不该在初始化时被单独关掉(关了就再也没人打开)。 + /// + private bool HasTimelineControlledAncestor(Transform t) + { + for (Transform p = t.parent; p != null && p != transform; p = p.parent) + { + if (IsTimelineControlledLight(p.name.Replace("(Clone)", ""))) + return true; + } + return false; + } + public void InitializeStage() { foreach (GameObject stage_part in _stageObjects) @@ -57,11 +71,27 @@ namespace Gallop.Live var instance = Instantiate(stage_part, transform); foreach (var child in instance.GetComponentsInChildren(true)) { - if (!StageObjectMap.ContainsKey(child.name)) + // 判重和插入统一用剥掉 "(Clone)" 的名字。 + // 原先判重用 child.name(未剥)、插入用剥掉的,两个键不一致。 + var tmp_name = child.name.Replace("(Clone)", ""); + + // 只关「本分支里最上层」的那盏受控灯。 + // + // 轨道寻址的是**容器**:live10149 实测 28 个 BlinkLight 组名全部一一对应 + // 一个 GameObject(27 个命中、1 个 wash_discoball 没有对应物件),真正的灯是 + // 它的子物件,由 handler 的 GetComponentsInChildren 覆盖。 + // + // 原来 SetActive(false) 写在判重 if 里面,于是**同名子物件只有第一个被关**, + // 而 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 盏常黑。 + // + // 祖先已经是受控灯的,跟着祖先显隐即可,不该再单独关。 + if (IsTimelineControlledLight(tmp_name) && !HasTimelineControlledAncestor(child)) + child.gameObject.SetActive(false); + + if (!StageObjectMap.ContainsKey(tmp_name)) { - if (IsTimelineControlledLight(child.name)) - child.gameObject.SetActive(false); - var tmp_name = child.name.Replace("(Clone)", ""); StageObjectMap.Add(tmp_name, child.gameObject); StageParentMap.TryAdd(tmp_name, child.gameObject.transform.parent); } @@ -75,6 +105,9 @@ namespace Gallop.Live StageObjectUnitMap.Add(unit.UnitName, unit); } } + + // 地板发白的诊断,只打日志不改任何东西。见 StageEnvMapDiag 的说明。 + StageEnvMapDiag.Dump(this); } public void UpdateObject(ref ObjectUpdateInfo updateInfo) { @@ -177,32 +210,58 @@ namespace Gallop.Live : updateInfo.updateData.scale; } + /// + /// Transform 轨道。 + /// + /// 原实现只查 StageObjectUnitMap,组名不是 unit 名就直接什么都不做、也不报错 —— + /// 这是 UpdateObject 那个 bug 的**镜像**(那次是只查 StageObjectMap、漏了 unit 名)。 + /// 实测组名两种都有:son1028 的第一个组叫 light001_glow_001,是 GameObject 名, + /// 于是这 12 首歌 / 681 个关键帧整条轨道从来没生效过。两种都查。 + /// + /// 注:TypeTree 实测 LiveTimelineKeyTransformData 只有 position/rotate/scale, + /// **没有 OffsetType 字段**(和 Object 轨道不同),所以这里一律绝对写入是对的, + /// 不需要 ApplyObjectTrs 那套 Add 相对语义。 + /// public void UpdateTransform(ref TransformUpdateInfo updateInfo) { if (updateInfo.data == null) { return; } + if (StageObjectUnitMap.TryGetValue(updateInfo.data.name, out StageObjectUnit objectUnit)) { - foreach(var child in objectUnit.ChildObjects) + foreach (var child in objectUnit.ChildObjects) { - if (StageObjectMap.TryGetValue(child.name, out GameObject gameObject)) - { - if (updateInfo.data.enablePosition) - { - gameObject.transform.localPosition = updateInfo.updateData.position; - } - if (updateInfo.data.enableRotate) - { - gameObject.transform.localRotation = updateInfo.updateData.rotation; - } - if (updateInfo.data.enableScale) - { - gameObject.transform.localScale = updateInfo.updateData.scale; - } - } + if (child == null) continue; + // StageObjectMap 存的是剥掉 "(Clone)" 的名字,查的时候也得剥 + // —— UpdateObject 有这一步,这里原先漏了。 + if (!StageObjectMap.TryGetValue(child.name.Replace("(Clone)", ""), out var childGo)) + childGo = child; + ApplyTransformTrs(childGo.transform, ref updateInfo); } + return; + } + + if (StageObjectMap.TryGetValue(updateInfo.data.name, out GameObject gameObject)) + { + ApplyTransformTrs(gameObject.transform, ref updateInfo); + } + } + + private static void ApplyTransformTrs(Transform tr, ref TransformUpdateInfo updateInfo) + { + if (updateInfo.data.enablePosition) + { + tr.localPosition = updateInfo.updateData.position; + } + if (updateInfo.data.enableRotate) + { + tr.localRotation = updateInfo.updateData.rotation; + } + if (updateInfo.data.enableScale) + { + tr.localScale = updateInfo.updateData.scale; } } }