最近在熟悉 URP 自定义后处理流程时,遇到一个奇怪的 ⌈BUG⌋:自定义的效果无法被完全关闭,或者说在关闭之后,场景显示为非预期的画面。自定义的 Volume 代码如下:

public class SingleColor : VolumeComponent, IPostProcessComponent
{
    public FloatParameter intensity = new FloatParameter(1f);
    public ColorParameter color = new ColorParameter(Color.black);

    public bool IsActive() => intensity.value > 0;

    public bool IsTileCompatible() => false;
}

Inspector 上的设置为:

而此时整个 Scene 视图都显示为黑色。

从 FrameDebug 中可以发现相关的 Pass 还在运行,程序断点也表明 Pass 的 Execute 方法正在运行,并且 SingleColorIsActive() 始终返回 true

public override void Execute(...)
{
    var stack = VolumeManager.instance.stack;
    var singleColor = stack.GetComponent<SingleColor>();
    if (singleColor.IsActive())
    {
        // Blit
    }
}

由于场景的表现与 SingleColorParameter 的初始值一致,因此猜测当在 Inspector 面板上禁用 VolumeComponent 时会恢复其初始值。比较坑的是,官方文档并没有相关的说明,搜索引擎也找不到相关的内容,只能靠经验和猜测去定位问题。最终定位问题的代码如下:

public class SingleColor : VolumeComponent, IPostProcessComponent
{
    public CustomFloatParameter intensity = new CustomFloatParameter(0f);
    public ColorParameter color = new ColorParameter(Color.black);

    public bool IsActive() => intensity.value > 0;

    public bool IsTileCompatible() => false;

    [Serializable]
    public sealed class CustomFloatParameter : VolumeParameter<float>
    {
        public CustomFloatParameter(float value, bool overrideState = false) : base(value, overrideState) { }

        // 猜测会调用 SetValue 来修改值
        public override void SetValue(VolumeParameter parameter)
        {
            // 这里直接使用 Debug.LogError 无法在控制台看到堆栈信息,因此需要手动打印调用栈
            System.Diagnostics.StackTrace st = new System.Diagnostics.StackTrace();
            System.Diagnostics.StackFrame[] sfs = st.GetFrames();
            for (int u = 0; u < sfs.Length; ++u)
            {
                System.Reflection.MethodBase mb = sfs[u].GetMethod();
                Debug.LogError($"{mb.DeclaringType.FullName}.{mb.Name}");
            }
        }
    }
}

最后使用 ILSpy 去查看相关 dll 的反编译代码。几经折腾后发现,当 Inspector 上的 Volume 被禁用时,UIElementsUtility 会派发相关事件,最终 VolumeManager 会使用 ReplaceData 方法用默认值去设置 SingleColor 中所有 Parameter 的值

因此,如果要在 IsActive 中使用 Parameter,那么 Parameter 的初始值最好与期望的禁用状态的值相同。

public class SingleColor : VolumeComponent, IPostProcessComponent
{
    // 将 1f 改为了 0f
    public FloatParameter intensity = new FloatParameter(0f);

    public bool IsActive() => intensity.value > 0;
}