Stage: restore the authored blend state that a URP Lit property table overwrote

Chasing "why does this ground wash light keep swinging as a black wedge" landed on something
much wider than one lamp.

Gallop's light shaders take their blend state from material properties — LightBlinkBlend and
friends declare `Blend [_SrcBlend] [_DstBlend]`. Every one of live10149's 26 materials stores
_SrcBlend=One, _DstBlend=Zero, i.e. opaque overwrite. That is not authoring intent: all 26 also
carry _WorkflowMode, _Surface, _ClearCoatMask, _Smoothness and _Blend in their float tables,
which is URP Lit's property set, and One/Zero is URP Lit's opaque preset. The table was stamped
over the whole batch.

The authored values are recoverable, because the shaders declare their own defaults. Of the 16
shaders in the bundle that expose these two properties, 14 default to One/One — additive — and
they are exactly the emissive family: LightBlinkBlend, LightBlend, StageBeamLight and its
Cutoff/Fadeout variants, StageLightBlinkCutoff/Fadeout, the three StageProjectorBlend variants,
StageMonitorBlend*, MirrorBallProjector, BgUnlitStencilAlpha.

Forced opaque, an additive beam whose _BlinkLightColor is black stops being "adds nothing, so
invisible" and becomes a solid black wall — which is what the swinging wash beams look like on
screen, and very likely also what LIVE_TRACKS.md has been recording as "small bulbs render as
black blocks".

So on stage init, every material whose shader declares _SrcBlend/_DstBlend gets those reset to
the shader's declared default. The correction is self-limiting: for a shader that really is
opaque the default is already One/Zero and nothing changes. Only properties the current shader
actually declares are touched — the material's saved table is the thing we do not trust here,
so it is not consulted for what to write.

Every change is logged with material, shader and before/after, so the next run says exactly what
was corrected rather than leaving it to inference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzXXkLzVC8pNA3JjyUGMbq
This commit is contained in:
ritenseki
2026-08-07 13:57:41 +08:00
parent 3b4485506a
commit f75e6fdd1f
@@ -2,6 +2,7 @@ using Gallop.Live.Cutt;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace Gallop.Live
@@ -106,6 +107,9 @@ namespace Gallop.Live
}
}
// 光柱/闪光类 shader 的混合状态被 bundle 材质里的 URP Lit 属性表压成了不透明,先修回来。
RestoreAuthoredBlendState();
// 舞台自带 Animation 的播放(原版由缺失的 AnimationObjectController 负责)。
StageAnimationPlayer.Setup(this);
@@ -113,6 +117,85 @@ namespace Gallop.Live
StageMissingScriptCensus.Dump(this);
}
/// <summary>
/// 把材质的 `_SrcBlend`/`_DstBlend` 恢复成 **shader 自己声明的默认值**。
///
/// 为什么必须做:Gallop 的光柱类 shader 把混合状态写成 `Blend [_SrcBlend] [_DstBlend]`,
/// 也就是从材质属性里读。而 live10149 的 **26/26 个材质**存的都是
/// `_SrcBlend=One, _DstBlend=Zero`(= 不透明覆盖),并且每一个的 float 表里都齐刷刷带着
/// `_WorkflowMode`/`_Surface`/`_ClearCoatMask`/`_Smoothness`/`_Blend` —— 那是 **URP Lit 的
/// 属性表**,整批盖上去的,One/Zero 正是 URP Lit 的不透明预设,不是作者意图。
///
/// 作者意图是可查的:全库 499 个 shader 里有 16 个声明了这两个属性,其中 **14 个的
/// 声明默认值是 `One/One`(加法混合)** —— LightBlinkBlend、LightBlend、StageBeamLight
/// 三兄弟、StageLightBlink{Cutoff,Fadeout}、StageProjectorBlend 三兄弟、
/// MirrorBallProjector 等,全是发光/投影的那一批。
///
/// 后果:加法混合下 `_BlinkLightColor` 为黑 = 什么都不加 = 光柱不可见;被压成不透明后,
/// 同一个黑色就变成一堵实心黑墙。ground wash light 扫动时那几片「转来转去的黑楔子」
/// 就是这么来的,`LIVE_TRACKS.md` 里「小灯泡渲染成黑块」多半也是同一根因。
///
/// 这个修法是自限的:对本来就该不透明的 shader,声明默认值就是 One/Zero,写回去等于没动。
/// ⚠ 只改 shader **确实声明了**的属性 —— 材质存档表不可信,shader 才是准。
/// </summary>
private void RestoreAuthoredBlendState()
{
var done = new HashSet<Material>();
var changed = new List<string>();
foreach (var r in GetComponentsInChildren<Renderer>(true))
{
foreach (var mat in r.sharedMaterials)
{
if (mat == null || mat.shader == null || !done.Add(mat)) continue;
string before = null, after = null;
foreach (string prop in kBlendProps)
{
if (!TryGetShaderDefaultFloat(mat.shader, prop, out float authored)) continue;
float current = mat.GetFloat(prop);
if (Mathf.Approximately(current, authored)) continue;
before = $"{before}{prop}={current} ";
after = $"{after}{prop}={authored} ";
mat.SetFloat(prop, authored);
}
if (after != null)
changed.Add($"{mat.name}[{mat.shader.name}] {before}→ {after}");
}
}
if (changed.Count == 0)
{
Debug.Log($"[StageBlend] {done.Count} 个材质的混合状态与 shader 声明一致,无需修正");
return;
}
Debug.Log($"[StageBlend] {changed.Count}/{done.Count} 个材质的 _SrcBlend/_DstBlend 与 shader 声明不符," +
$"已恢复成 shader 的默认值:\n " + string.Join("\n ", changed));
}
private static readonly string[] kBlendProps = { "_SrcBlend", "_DstBlend" };
/// <summary>shader 声明的默认值。材质没有这个属性、或类型不是 Float/Range 时返回 false。</summary>
private static bool TryGetShaderDefaultFloat(Shader shader, string propName, out float value)
{
value = 0f;
int count = shader.GetPropertyCount();
for (int i = 0; i < count; i++)
{
if (shader.GetPropertyName(i) != propName) continue;
var type = shader.GetPropertyType(i);
if (type != ShaderPropertyType.Float && type != ShaderPropertyType.Range) return false;
value = shader.GetPropertyDefaultFloatValue(i);
return true;
}
return false;
}
public void UpdateObject(ref ObjectUpdateInfo updateInfo) {
if (updateInfo.data == null)