This commit is contained in:
ShallowT1Dream
2026-09-10 17:29:24 +08:00
parent 3071ec4b8f
commit 80aa4f0436
20 changed files with 32618 additions and 0 deletions
@@ -0,0 +1,330 @@
using System;
using System.Runtime.CompilerServices;
using Coffee.UIEffectInternal;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Profiling;
using UnityEngine.UI;
using UnityEditor;
#if TIMELINE_ENABLE
using UnityEngine.Timeline;
#endif
[assembly: InternalsVisibleTo("UIEffect")]
[assembly: InternalsVisibleTo("Coffee.UIEffect.Editor")]
namespace Coffee.UIEffects
{
[ExecuteAlways]
[DisallowMultipleComponent]
public abstract class UIEffectBase : UIBehaviour, IMeshModifier, IMaterialModifier, ICanvasRaycastFilter
#if TIMELINE_ENABLE
, ITimeControl
#endif
{
private static readonly InternalObjectPool<UIEffectContext> s_ContextPool =
new InternalObjectPool<UIEffectContext>(() => new UIEffectContext(), x => true, x => x.Reset());
private Graphic _graphic;
private Material _material;
private UIEffectContext _context;
private Action _onBeforeCanvasRebuild;
private Action _onAfterCanvasRebuild;
private bool _canModifyMesh;
private Matrix4x4 _prevTransformHash;
private Canvas _canvas;
private bool _canvasCached = false;
private bool _isContextDirty;
public Material effectMaterial => _material;
public Graphic graphic => _graphic != null ? _graphic : _graphic = GetComponent<Graphic>();
public virtual uint effectId => (uint)GetHashCode();
public virtual float actualSamplingScale => 1;
public virtual bool canModifyShape => true;
protected Canvas canvas
{
get
{
if (_canvasCached) return _canvas;
_canvasCached = true;
_canvas = GetComponentInParent<Canvas>();
if (_canvas == null || !_canvas.isActiveAndEnabled)
{
_canvas = null;
_canvasCached = false;
}
return _canvas;
}
}
public virtual UIEffectContext context
{
get
{
if (_context == null)
{
_context = s_ContextPool.Rent();
UpdateContext(_context);
}
return _context;
}
}
protected Material GetCurrentMaterial()
{
if (!isActiveAndEnabled) return null;
var g = graphic;
if (g == null) return null;
var canvasRenderer = g.canvasRenderer;
if (canvasRenderer == null || canvasRenderer.materialCount == 0) return null;
return canvasRenderer.GetMaterial();
}
public virtual RectTransform transitionRoot => transform as RectTransform;
protected override void OnEnable()
{
_canModifyMesh = true;
_onBeforeCanvasRebuild ??= OnBeforeCanvasRebuild;
_onAfterCanvasRebuild ??= OnAfterCanvasRebuild;
UIExtraCallbacks.onBeforeCanvasRebuild += _onBeforeCanvasRebuild;
UIExtraCallbacks.onAfterCanvasRebuild += _onAfterCanvasRebuild;
UpdateContext(context);
SetMaterialDirty();
SetVerticesDirty();
}
protected override void OnDisable()
{
if (_onBeforeCanvasRebuild != null)
{
UIExtraCallbacks.onBeforeCanvasRebuild -= _onBeforeCanvasRebuild;
}
if (_onAfterCanvasRebuild != null)
{
UIExtraCallbacks.onAfterCanvasRebuild -= _onAfterCanvasRebuild;
}
MaterialRepository.Release(ref _material);
SetMaterialDirty();
SetVerticesDirty();
}
protected override void OnDestroy()
{
_onBeforeCanvasRebuild = null;
_graphic = null;
s_ContextPool.Return(ref _context);
}
private void OnBeforeCanvasRebuild()
{
// If the mesh is not modified in the previous frame, try to modify it if possible.
if (!_canModifyMesh && CanModifyMesh())
{
_canModifyMesh = true;
SetVerticesDirty();
}
}
private void OnAfterCanvasRebuild()
{
if (_material == null || graphic == null || canvas == null || context == null) return;
if (_isContextDirty)
{
_isContextDirty = false;
ApplyContextToMaterial(GetCurrentMaterial());
}
if (!context.useViewMatrix) return;
context.UpdateViewMatrix(GetCurrentMaterial(), transitionRoot, canvas.rootCanvas);
}
protected override void OnCanvasHierarchyChanged()
{
_canvasCached = false;
_canvas = null;
}
public void ModifyMesh(Mesh mesh)
{
}
public virtual void ModifyMesh(VertexHelper vh)
{
if (!isActiveAndEnabled || context == null) return;
_canModifyMesh = CanModifyMesh();
if (_canModifyMesh)
{
context.ModifyMesh(graphic, canvas, transitionRoot, vh, canModifyShape);
}
}
private bool CanModifyMesh()
{
if (graphic == null || !graphic.isActiveAndEnabled) return false;
if (!isActiveAndEnabled) return false;
// The transitionRoot is same as the transform => true.
var root = transitionRoot;
if (transform == root) return true;
// The transitionRoot is not visible => false.
var scale1 = root.lossyScale;
var scale2 = transform.lossyScale;
return !Mathf.Approximately(scale1.x * scale1.y * scale2.x * scale2.y, 0);
}
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
if (baseMaterial == null || !isActiveAndEnabled || context == null || !context.willModifyMaterial)
{
MaterialRepository.Release(ref _material);
return baseMaterial;
}
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
// 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
// root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
var configId = context.configId;
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
if (!MaterialRepository.Valid(hash, _material))
{
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial > Get or create material");
MaterialRepository.Get(hash, ref _material, x => new Material(x)
{
shader = UIEffectProjectSettings.shaderRegistry.FindOptionalShader(x.shader,
"(UIEffect)",
"Hidden/{0} (UIEffect)",
"Hidden/UI/Default (UIEffect)"),
hideFlags = HideFlags.HideAndDontSave
}, baseMaterial);
Profiler.EndSample();
}
_material.CopyPropertiesFromMaterial(baseMaterial);
ApplyContextToMaterial(_material);
Profiler.EndSample();
return _material;
}
#if UNITY_EDITOR
protected override void OnValidate()
{
UpdateContext(context);
SetVerticesDirty();
SetMaterialDirty();
}
protected override void Reset()
{
OnValidate();
}
internal virtual void SetEnablePreviewIfSelected(GameObject[] selection)
{
}
#endif
protected override void OnRectTransformDimensionsChange()
{
if (!isActiveAndEnabled) return;
SetVerticesDirty();
}
protected override void OnDidApplyAnimationProperties()
{
UpdateContext(context);
SetVerticesDirty();
SetMaterialDirty();
}
public virtual void SetVerticesDirty()
{
if (graphic != null)
{
graphic.SetVerticesDirty();
GraphicProxy.Find(graphic).SetVerticesDirty(graphic, enabled);
Misc.QueuePlayerLoopUpdate();
}
}
public virtual void SetMaterialDirty()
{
if (graphic != null)
{
graphic.SetMaterialDirty();
SetMaterialContextDirty();
Misc.QueuePlayerLoopUpdate();
}
}
public virtual void SetMaterialContextDirty()
{
_isContextDirty = true;
}
internal void ReleaseMaterial()
{
MaterialRepository.Release(ref _material);
}
internal abstract void UpdateContext(UIEffectContext c);
public virtual void ApplyContextToMaterial(Material material)
{
if (!isActiveAndEnabled || context == null || material == null) return;
_isContextDirty = false;
context.ApplyToMaterial(material, actualSamplingScale);
#if UNITY_EDITOR
Profiler.BeginSample("(Editor/UIE)[UIEffect] ApplyContextToMaterial Post Process");
if (!Application.isPlaying)
{
SetEnablePreviewIfSelected(Selection.gameObjects);
Misc.QueuePlayerLoopUpdate();
}
UIEffectProjectSettings.shaderRegistry.RegisterVariant(material, "UI > UIEffect");
Profiler.EndSample();
#endif
}
public abstract void SetRate(float rate, UIEffectTweener.CullingMask cullingMask);
public abstract bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera);
public void SetTime(double _)
{
}
public void OnControlTimeStart()
{
if (this != null)
{
enabled = true;
}
}
public void OnControlTimeStop()
{
if (this != null)
{
enabled = false;
}
}
}
}
@@ -0,0 +1,976 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Coffee.UIEffectInternal;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.Rendering;
using UnityEngine.UI;
namespace Coffee.UIEffects
{
public sealed class UIEffectContext
{
private static readonly UIEffectContext s_DefaultContext = new UIEffectContext();
private static readonly List<UIVertex> s_WorkingVertices = new List<UIVertex>(1024 * 8);
private static readonly int s_SrcBlend = Shader.PropertyToID("_SrcBlend");
private static readonly int s_DstBlend = Shader.PropertyToID("_DstBlend");
private static readonly int s_ToneIntensity = Shader.PropertyToID("_ToneIntensity");
private static readonly int s_ColorFilter = Shader.PropertyToID("_ColorFilter");
private static readonly int s_ColorValue = Shader.PropertyToID("_ColorValue");
private static readonly int s_ColorIntensity = Shader.PropertyToID("_ColorIntensity");
private static readonly int s_ColorGlow = Shader.PropertyToID("_ColorGlow");
private static readonly int s_SamplingIntensity = Shader.PropertyToID("_SamplingIntensity");
private static readonly int s_SamplingWidth = Shader.PropertyToID("_SamplingWidth");
private static readonly int s_SamplingScale = Shader.PropertyToID("_SamplingScale");
private static readonly int s_TransitionRate = Shader.PropertyToID("_TransitionRate");
private static readonly int s_TransitionReverse = Shader.PropertyToID("_TransitionReverse");
private static readonly int s_TransitionTex = Shader.PropertyToID("_TransitionTex");
private static readonly int s_TransitionTex_ST = Shader.PropertyToID("_TransitionTex_ST");
private static readonly int s_TransitionTex_Speed = Shader.PropertyToID("_TransitionTex_Speed");
private static readonly int s_TransitionWidth = Shader.PropertyToID("_TransitionWidth");
private static readonly int s_TransitionSoftness = Shader.PropertyToID("_TransitionSoftness");
private static readonly int s_TransitionRange = Shader.PropertyToID("_TransitionRange");
private static readonly int s_TransitionColorFilter = Shader.PropertyToID("_TransitionColorFilter");
private static readonly int s_TransitionColor = Shader.PropertyToID("_TransitionColor");
private static readonly int s_TransitionColorGlow = Shader.PropertyToID("_TransitionColorGlow");
private static readonly int s_TransitionPatternReverse = Shader.PropertyToID("_TransitionPatternReverse");
private static readonly int s_TransitionAutoPlaySpeed = Shader.PropertyToID("_TransitionAutoPlaySpeed");
private static readonly int s_TransitionGradientTex = Shader.PropertyToID("_TransitionGradientTex");
private static readonly int s_TargetColor = Shader.PropertyToID("_TargetColor");
private static readonly int s_TargetRange = Shader.PropertyToID("_TargetRange");
private static readonly int s_TargetSoftness = Shader.PropertyToID("_TargetSoftness");
private static readonly int s_ShadowColorFilter = Shader.PropertyToID("_ShadowColorFilter");
private static readonly int s_ShadowColor = Shader.PropertyToID("_ShadowColor");
private static readonly int s_ShadowBlurIntensity = Shader.PropertyToID("_ShadowBlurIntensity");
private static readonly int s_ShadowColorGlow = Shader.PropertyToID("_ShadowColorGlow");
private static readonly int s_EdgeWidth = Shader.PropertyToID("_EdgeWidth");
private static readonly int s_EdgeColorFilter = Shader.PropertyToID("_EdgeColorFilter");
private static readonly int s_EdgeColor = Shader.PropertyToID("_EdgeColor");
private static readonly int s_EdgeColorGlow = Shader.PropertyToID("_EdgeColorGlow");
private static readonly int s_EdgeShinyAutoPlaySpeed = Shader.PropertyToID("_EdgeShinyAutoPlaySpeed");
private static readonly int s_EdgeShinyRate = Shader.PropertyToID("_EdgeShinyRate");
private static readonly int s_EdgeShinyWidth = Shader.PropertyToID("_EdgeShinyWidth");
private static readonly int s_PatternArea = Shader.PropertyToID("_PatternArea");
private static readonly int s_DetailIntensity = Shader.PropertyToID("_DetailIntensity");
private static readonly int s_DetailThreshold = Shader.PropertyToID("_DetailThreshold");
private static readonly int s_DetailColor = Shader.PropertyToID("_DetailColor");
private static readonly int s_DetailTex = Shader.PropertyToID("_DetailTex");
private static readonly int s_DetailTex_ST = Shader.PropertyToID("_DetailTex_ST");
private static readonly int s_DetailTex_Speed = Shader.PropertyToID("_DetailTex_Speed");
private static readonly int s_GradationIntensity = Shader.PropertyToID("_GradationIntensity");
private static readonly int s_GradationColorFilter = Shader.PropertyToID("_GradationColorFilter");
private static readonly int s_GradationColor1 = Shader.PropertyToID("_GradationColor1");
private static readonly int s_GradationColor2 = Shader.PropertyToID("_GradationColor2");
private static readonly int s_GradationColor3 = Shader.PropertyToID("_GradationColor3");
private static readonly int s_GradationColor4 = Shader.PropertyToID("_GradationColor4");
private static readonly int s_GradationTex = Shader.PropertyToID("_GradationTex");
private static readonly int s_GradationTex_ST = Shader.PropertyToID("_GradationTex_ST");
private static readonly int s_GradationRadial = Shader.PropertyToID("_GradationRadial");
private static readonly int s_RootViewMatrix = Shader.PropertyToID("_RootViewMatrix");
private static readonly int s_GradViewMatrix = Shader.PropertyToID("_GradViewMatrix");
private static readonly int s_MirrorRootViewMatrix = Shader.PropertyToID("_MirrorRootViewMatrix");
private static readonly int s_MirrorGradViewMatrix = Shader.PropertyToID("_MirrorGradViewMatrix");
private static readonly int s_CanvasToWorldMatrix = Shader.PropertyToID("_CanvasToWorldMatrix");
private static readonly string[] s_ToneKeywords =
{
"",
"TONE_GRAYSCALE",
"TONE_SEPIA",
"TONE_NEGATIVE",
"TONE_RETRO",
"TONE_POSTERIZE"
};
private static readonly string[] s_ColorKeywords =
{
"",
"COLOR_FILTER"
};
private static readonly string[] s_SamplingKeywords =
{
"",
"SAMPLING_BLUR_FAST",
"SAMPLING_BLUR_MEDIUM",
"SAMPLING_BLUR_DETAIL",
"SAMPLING_PIXELATION",
"SAMPLING_RGB_SHIFT",
"SAMPLING_EDGE_LUMINANCE",
"SAMPLING_EDGE_ALPHA"
};
private static readonly string[] s_TransitionKeywords =
{
"",
"TRANSITION_FADE",
"TRANSITION_CUTOFF",
"TRANSITION_DISSOLVE",
"TRANSITION_SHINY",
"TRANSITION_MASK",
"TRANSITION_MELT",
"TRANSITION_BURN",
"TRANSITION_PATTERN",
"TRANSITION_BLAZE"
};
private static readonly string[] s_TargetKeywords =
{
"",
"TARGET_HUE",
"TARGET_LUMINANCE"
};
private static readonly string[] s_EdgeKeywords =
{
"",
"EDGE_PLAIN",
"EDGE_SHINY"
};
private static readonly string[] s_DetailKeywords =
{
"",
"DETAIL_MASKING",
"DETAIL_MULTIPLY",
"DETAIL_ADDITIVE",
"DETAIL_REPLACE",
"DETAIL_MULTIPLY_ADDITIVE",
"DETAIL_SUBTRACTIVE"
};
private static readonly string[] s_GradationKeywords =
{
"",
"GRADATION_GRADIENT",
"GRADATION_COLOR2",
"GRADATION_COLOR4"
};
private static readonly Vector2[][] s_ShadowVectors = new[]
{
Array.Empty<Vector2>(), // None
new[] { Vector2.one }, // Shadow
new[] { Vector2.one, Vector2.right, Vector2.up }, // Shadow3
new[] { Vector2.one, -Vector2.one, new Vector2(1, -1), new Vector2(-1, 1) }, // Outline
new[]
{
Vector2.one, -Vector2.one, new Vector2(1, -1), new Vector2(-1, 1),
Vector2.right, Vector2.up, Vector2.left, Vector2.down
} // Outline8
};
public static Func<UIVertex, Rect, UIVertex> onModifyVertex;
public ToneFilter m_ToneFilter;
public float m_ToneIntensity;
public ColorFilter m_ColorFilter;
public float m_ColorIntensity;
public Color m_Color;
public bool m_ColorGlow;
public SamplingFilter m_SamplingFilter;
public float m_SamplingIntensity;
public float m_SamplingWidth;
public TransitionFilter m_TransitionFilter;
public float m_TransitionRate;
public bool m_TransitionReverse;
public Texture m_TransitionTex;
public Vector2 m_TransitionTexScale;
public Vector2 m_TransitionTexOffset;
public Vector2 m_TransitionTexSpeed;
public float m_TransitionRotation;
public bool m_TransitionKeepAspectRatio;
public float m_TransitionWidth;
public float m_TransitionSoftness;
public MinMax01 m_TransitionRange;
public ColorFilter m_TransitionColorFilter;
public Color m_TransitionColor;
public bool m_TransitionColorGlow;
public bool m_TransitionPatternReverse;
public float m_TransitionAutoPlaySpeed;
public Gradient m_TransitionGradient;
public TargetMode m_TargetMode;
public Color m_TargetColor;
public float m_TargetRange;
public float m_TargetSoftness;
public BlendMode m_SrcBlendMode;
public BlendMode m_DstBlendMode;
public ShadowMode m_ShadowMode;
public Vector2 m_ShadowDistance;
public int m_ShadowIteration;
public float m_ShadowFade;
public float m_ShadowMirrorScale;
public float m_ShadowBlurIntensity;
public ColorFilter m_ShadowColorFilter;
public Color m_ShadowColor;
public bool m_ShadowColorGlow;
public EdgeMode m_EdgeMode;
public float m_EdgeShinyRate;
public float m_EdgeWidth;
public ColorFilter m_EdgeColorFilter;
public Color m_EdgeColor;
public bool m_EdgeColorGlow;
public float m_EdgeShinyWidth;
public float m_EdgeShinyAutoPlaySpeed;
public PatternArea m_PatternArea;
public GradationMode m_GradationMode;
public float m_GradationIntensity;
public GradationColorFilter m_GradationColorFilter;
public Color m_GradationColor1;
public Color m_GradationColor2;
public Color m_GradationColor3;
public Color m_GradationColor4;
public Gradient m_GradationGradient;
public float m_GradationOffset;
public float m_GradationScale;
public float m_GradationRotation;
public TextureWrapMode m_GradationWrapMode;
public bool m_GradationReverse;
public DetailFilter m_DetailFilter;
public float m_DetailIntensity;
public MinMax01 m_DetailThreshold;
public Color m_DetailColor;
public Texture m_DetailTex;
public Vector2 m_DetailTexScale;
public Vector2 m_DetailTexOffset;
public Vector2 m_DetailTexSpeed;
public Flip m_Flip;
public bool willModifyMaterial => m_ToneFilter != ToneFilter.None
|| m_ColorFilter != ColorFilter.None
|| m_SamplingFilter != SamplingFilter.None
|| m_TransitionFilter != TransitionFilter.None
|| m_SrcBlendMode != BlendMode.One
|| m_DstBlendMode != BlendMode.OneMinusSrcAlpha
|| m_ShadowMode != ShadowMode.None
|| m_EdgeMode != EdgeMode.None
|| m_DetailFilter != DetailFilter.None
|| m_GradationMode != GradationMode.None;
public bool willModifyVertex => willModifyMaterial;
public bool useViewMatrix => m_TransitionFilter != TransitionFilter.None
|| m_GradationMode != GradationMode.None
|| m_DetailFilter != DetailFilter.None
|| m_EdgeMode == EdgeMode.Shiny;
/// <summary>
/// 特效配置的稳定指纹。自动遍历所有序列化配置字段生成,因此插件新增属性时无需手动维护字段清单。
/// 相同配置会得到相同指纹,从而可以共享同一个材质并参与合批。
/// </summary>
public uint configId
{
get
{
unchecked
{
var h = (uint)2166136261;
var fields = s_ConfigFields;
for (var i = 0; i < fields.Length; i++)
{
var f = fields[i];
h = Mix(h, (uint)f.Name.GetHashCode());
h = Mix(h, HashValue(f.GetValue(this)));
}
// Avalanche 混淆(MurmurHash3 终混),让相近配置散列到不同指纹。
h ^= h >> 16;
h *= 0x85ebca6b;
h ^= h >> 13;
h *= 0xc2b2ae35;
h ^= h >> 16;
return h;
}
}
}
private static uint Mix(uint h, uint v)
{
h ^= v;
h *= 16777619;
return h;
}
/// <summary>
/// 对任意配置值做内容哈希。值类型会逐字段展开,嵌套结构体(Color、Vector2/3/4、MinMax01 等)
/// 无需单独写类型处理即可贡献全部数据。只有 UnityEngine.Object 和 Gradient 需要显式处理,
/// 因为它们的默认哈希基于引用而非内容。
/// </summary>
private static uint HashValue(object value)
{
if (value == null) return 0u;
if (value is UnityEngine.Object obj)
{
return obj ? (uint)obj.GetInstanceID() : 0u;
}
if (value is Gradient gradient)
{
return GradientHash(gradient);
}
var type = value.GetType();
if (type.IsPrimitive || type.IsEnum)
{
return unchecked((uint)value.GetHashCode());
}
if (type.IsValueType)
{
return HashFields(value, type);
}
return unchecked((uint)value.GetHashCode());
}
private static uint HashFields(object boxed, Type type)
{
unchecked
{
if (!s_FieldCache.TryGetValue(type, out var fields))
{
fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
s_FieldCache[type] = fields;
}
var h = (uint)2166136261;
for (var i = 0; i < fields.Length; i++)
{
h = Mix(h, HashValue(fields[i].GetValue(boxed)));
}
return h;
}
}
private static uint GradientHash(Gradient gradient)
{
unchecked
{
var h = (uint)2166136261;
var colorKeys = gradient.colorKeys;
for (var i = 0; i < colorKeys.Length; i++)
{
h = Mix(h, HashValue(colorKeys[i].color));
h = Mix(h, HashValue(colorKeys[i].time));
}
var alphaKeys = gradient.alphaKeys;
for (var i = 0; i < alphaKeys.Length; i++)
{
h = Mix(h, HashValue(alphaKeys[i].alpha));
h = Mix(h, HashValue(alphaKeys[i].time));
}
return h;
}
}
private static readonly FieldInfo[] s_ConfigFields = typeof(UIEffectContext)
.GetFields(BindingFlags.Public | BindingFlags.Instance);
private static readonly Dictionary<Type, FieldInfo[]> s_FieldCache =
new Dictionary<Type, FieldInfo[]>();
private Texture2D _gradationRampTex;
private Texture2D _transitionRampTex;
private bool _isGradientDirty = true;
private bool _isTransitionGradientDirty = true;
private static readonly Color[] s_Colors = new Color[256];
private static readonly InternalObjectPool<Texture2D> s_TexturePool = new InternalObjectPool<Texture2D>(
() =>
{
var texture = new Texture2D(s_Colors.Length, 1, TextureFormat.RGBAFloat, false, false)
{
name = "GradationRamp",
hideFlags = HideFlags.DontSave,
wrapMode = TextureWrapMode.Repeat,
anisoLevel = 0
};
return texture;
},
texture => texture,
_ => { });
private Texture2D gradationRampTex
{
get
{
if (m_GradationGradient == null) return null;
if (_gradationRampTex == null) _gradationRampTex = s_TexturePool.Rent();
if (!_isGradientDirty) return _gradationRampTex;
_isGradientDirty = false;
var w = s_Colors.Length;
for (var i = 0; i < w; i++)
{
var x = m_GradationReverse ? w - 1 - i : i;
var t = (float)i / (w - 1);
s_Colors[x] = ApplyColorSpace(m_GradationGradient.Evaluate(t));
}
_gradationRampTex.wrapMode = m_GradationWrapMode;
_gradationRampTex.filterMode = m_GradationGradient.mode == GradientMode.Blend
? FilterMode.Bilinear
: FilterMode.Point;
_gradationRampTex.SetPixels(s_Colors);
_gradationRampTex.Apply();
return _gradationRampTex;
}
}
private Texture2D transitionRampTex
{
get
{
if (m_TransitionGradient == null) return null;
if (_transitionRampTex == null) _transitionRampTex = s_TexturePool.Rent();
if (!_isTransitionGradientDirty) return _transitionRampTex;
_isTransitionGradientDirty = false;
var w = s_Colors.Length;
for (var i = 0; i < w; i++)
{
var t = (float)i / (w - 1);
s_Colors[i] = ApplyColorSpace(m_TransitionGradient.Evaluate(t));
}
s_Colors[w - 1].a = 0;
_transitionRampTex.wrapMode = TextureWrapMode.Clamp;
_transitionRampTex.filterMode = m_TransitionGradient.mode == GradientMode.Blend
? FilterMode.Bilinear
: FilterMode.Point;
_transitionRampTex.SetPixels(s_Colors);
_transitionRampTex.Apply();
return _transitionRampTex;
}
}
public void Reset()
{
_isGradientDirty = true;
_isTransitionGradientDirty = true;
s_TexturePool.Return(ref _gradationRampTex);
s_TexturePool.Return(ref _transitionRampTex);
CopyFrom(s_DefaultContext);
}
private void CopyFrom(UIEffectContext src)
{
var dst = this;
dst.m_ToneFilter = src.m_ToneFilter;
dst.m_ToneIntensity = src.m_ToneIntensity;
dst.m_ColorFilter = src.m_ColorFilter;
dst.m_Color = src.m_Color;
dst.m_ColorIntensity = src.m_ColorIntensity;
dst.m_ColorGlow = src.m_ColorGlow;
dst.m_SamplingFilter = src.m_SamplingFilter;
dst.m_SamplingIntensity = src.m_SamplingIntensity;
dst.m_SamplingWidth = src.m_SamplingWidth;
dst.m_TransitionFilter = src.m_TransitionFilter;
dst.m_TransitionRate = src.m_TransitionRate;
dst.m_TransitionReverse = src.m_TransitionReverse;
dst.m_TransitionTex = src.m_TransitionTex;
dst.m_TransitionTexScale = src.m_TransitionTexScale;
dst.m_TransitionTexOffset = src.m_TransitionTexOffset;
dst.m_TransitionTexSpeed = src.m_TransitionTexSpeed;
dst.m_TransitionKeepAspectRatio = src.m_TransitionKeepAspectRatio;
dst.m_TransitionRotation = src.m_TransitionRotation;
dst.m_TransitionWidth = src.m_TransitionWidth;
dst.m_TransitionSoftness = src.m_TransitionSoftness;
dst.m_TransitionRange = src.m_TransitionRange;
dst.m_TransitionColor = src.m_TransitionColor;
dst.m_TransitionColorFilter = src.m_TransitionColorFilter;
dst.m_TransitionColorGlow = src.m_TransitionColorGlow;
dst.m_TransitionPatternReverse = src.m_TransitionPatternReverse;
dst.m_TransitionAutoPlaySpeed = src.m_TransitionAutoPlaySpeed;
dst.m_TransitionGradient = src.m_TransitionGradient;
dst.m_TargetMode = src.m_TargetMode;
dst.m_TargetColor = src.m_TargetColor;
dst.m_TargetRange = src.m_TargetRange;
dst.m_TargetSoftness = src.m_TargetSoftness;
dst.m_SrcBlendMode = src.m_SrcBlendMode;
dst.m_DstBlendMode = src.m_DstBlendMode;
dst.m_ShadowMode = src.m_ShadowMode;
dst.m_ShadowDistance = src.m_ShadowDistance;
dst.m_ShadowIteration = src.m_ShadowIteration;
dst.m_ShadowFade = src.m_ShadowFade;
dst.m_ShadowMirrorScale = src.m_ShadowMirrorScale;
dst.m_ShadowBlurIntensity = src.m_ShadowBlurIntensity;
dst.m_ShadowColorFilter = src.m_ShadowColorFilter;
dst.m_ShadowColor = src.m_ShadowColor;
dst.m_ShadowColorGlow = src.m_ShadowColorGlow;
dst.m_EdgeMode = src.m_EdgeMode;
dst.m_EdgeShinyRate = src.m_EdgeShinyRate;
dst.m_EdgeWidth = src.m_EdgeWidth;
dst.m_EdgeColorFilter = src.m_EdgeColorFilter;
dst.m_EdgeColor = src.m_EdgeColor;
dst.m_EdgeColorGlow = src.m_EdgeColorGlow;
dst.m_EdgeShinyAutoPlaySpeed = src.m_EdgeShinyAutoPlaySpeed;
dst.m_EdgeShinyWidth = src.m_EdgeShinyWidth;
dst.m_PatternArea = src.m_PatternArea;
dst.m_GradationMode = src.m_GradationMode;
dst.m_GradationIntensity = src.m_GradationIntensity;
dst.m_GradationColorFilter = src.m_GradationColorFilter;
dst.m_GradationColor1 = src.m_GradationColor1;
dst.m_GradationColor2 = src.m_GradationColor2;
dst.m_GradationColor3 = src.m_GradationColor3;
dst.m_GradationColor4 = src.m_GradationColor4;
dst.m_GradationGradient = src.m_GradationGradient;
dst.m_GradationScale = src.m_GradationScale;
dst.m_GradationOffset = src.m_GradationOffset;
dst.m_GradationRotation = src.m_GradationRotation;
dst.m_GradationWrapMode = src.m_GradationWrapMode;
dst.m_GradationReverse = src.m_GradationReverse;
dst.m_DetailFilter = src.m_DetailFilter;
dst.m_DetailIntensity = src.m_DetailIntensity;
dst.m_DetailColor = src.m_DetailColor;
dst.m_DetailThreshold = src.m_DetailThreshold;
dst.m_DetailTex = src.m_DetailTex;
dst.m_DetailTexScale = src.m_DetailTexScale;
dst.m_DetailTexOffset = src.m_DetailTexOffset;
dst.m_DetailTexSpeed = src.m_DetailTexSpeed;
dst.m_Flip = src.m_Flip;
}
public void SetGradationDirty()
{
_isGradientDirty = true;
}
public void SetTransitionGradationDirty()
{
_isTransitionGradientDirty = true;
}
public void ApplyToMaterial(Material material, float actualSamplingScale = 1f)
{
if (material == null) return;
Profiler.BeginSample("(UIE)[UIEffect] ApplyToMaterial");
material.SetInt(s_SrcBlend, (int)m_SrcBlendMode);
material.SetInt(s_DstBlend, (int)m_DstBlendMode);
material.SetFloat(s_ToneIntensity, Mathf.Clamp01(m_ToneIntensity));
material.SetInt(s_ColorFilter, (int)m_ColorFilter);
material.SetColor(s_ColorValue, ApplyColorSpace(m_Color));
material.SetFloat(s_ColorIntensity, Mathf.Clamp01(m_ColorIntensity));
material.SetInt(s_ColorGlow, m_ColorGlow ? 1 : 0);
material.SetFloat(s_SamplingIntensity, Mathf.Clamp01(m_SamplingIntensity));
material.SetFloat(s_SamplingWidth, m_SamplingWidth);
material.SetFloat(s_SamplingScale, actualSamplingScale);
material.SetFloat(s_TransitionRate, Mathf.Clamp01(m_TransitionRate));
material.SetInt(s_TransitionReverse, m_TransitionReverse ? 1 : 0);
material.SetTexture(s_TransitionTex, m_TransitionFilter != 0 ? m_TransitionTex : null);
material.SetVector(s_TransitionTex_ST,
new Vector4(m_TransitionTexScale.x, m_TransitionTexScale.y,
m_TransitionTexOffset.x, m_TransitionTexOffset.y));
material.SetVector(s_TransitionTex_Speed, m_TransitionTexSpeed);
material.SetFloat(s_TransitionWidth, Mathf.Clamp01(m_TransitionWidth));
material.SetFloat(s_TransitionSoftness, Mathf.Clamp01(m_TransitionSoftness));
material.SetVector(s_TransitionRange, new Vector2(m_TransitionRange.min, m_TransitionRange.max));
material.SetInt(s_TransitionColorFilter, (int)m_TransitionColorFilter);
material.SetColor(s_TransitionColor, ApplyColorSpace(m_TransitionColor));
material.SetInt(s_TransitionColorGlow, m_TransitionColorGlow ? 1 : 0);
material.SetInt(s_TransitionPatternReverse, m_TransitionPatternReverse ? 1 : 0);
material.SetFloat(s_TransitionAutoPlaySpeed, m_TransitionAutoPlaySpeed);
material.SetTexture(s_TransitionGradientTex,
m_TransitionFilter == TransitionFilter.Blaze ? transitionRampTex : null);
material.SetColor(s_TargetColor, ApplyColorSpace(m_TargetColor));
material.SetFloat(s_TargetRange, Mathf.Clamp01(m_TargetRange));
material.SetFloat(s_TargetSoftness, Mathf.Clamp01(m_TargetSoftness));
switch (m_SamplingFilter)
{
case SamplingFilter.BlurFast:
case SamplingFilter.BlurMedium:
case SamplingFilter.BlurDetail:
material.SetFloat(s_ShadowBlurIntensity, Mathf.Clamp01(m_ShadowBlurIntensity));
break;
default:
material.SetFloat(s_ShadowBlurIntensity, Mathf.Clamp01(m_SamplingIntensity));
break;
}
material.SetInt(s_ShadowColorFilter, (int)m_ShadowColorFilter);
material.SetColor(s_ShadowColor, ApplyColorSpace(m_ShadowColor));
material.SetInt(s_ShadowColorGlow, m_ShadowColorGlow ? 1 : 0);
material.SetFloat(s_EdgeWidth, Mathf.Clamp01(m_EdgeWidth));
material.SetInt(s_EdgeColorFilter, (int)m_EdgeColorFilter);
material.SetColor(s_EdgeColor, ApplyColorSpace(m_EdgeColor));
material.SetInt(s_EdgeColorGlow, m_EdgeColorGlow ? 1 : 0);
material.SetFloat(s_EdgeShinyRate, Mathf.Clamp01(m_EdgeShinyRate));
material.SetFloat(s_EdgeShinyWidth, Mathf.Clamp01(m_EdgeShinyWidth));
material.SetFloat(s_EdgeShinyAutoPlaySpeed, m_EdgeShinyAutoPlaySpeed);
material.SetInt(s_PatternArea, m_EdgeMode != EdgeMode.None ? (int)m_PatternArea : 0);
material.SetTexture(s_GradationTex, m_GradationMode != 0 ? gradationRampTex : null);
material.SetVector(s_GradationTex_ST, GetGradationScaleAndOffset());
material.SetFloat(s_GradationIntensity, m_GradationIntensity);
material.SetInt(s_GradationColorFilter, (int)m_GradationColorFilter);
material.SetColor(s_GradationColor1, ApplyColorSpace(m_GradationColor1));
material.SetColor(s_GradationColor2, ApplyColorSpace(m_GradationColor2));
material.SetColor(s_GradationColor3, ApplyColorSpace(m_GradationColor3));
material.SetColor(s_GradationColor4, ApplyColorSpace(m_GradationColor4));
material.SetInt(s_GradationRadial, m_GradationMode == GradationMode.Radial
#pragma warning disable CS0612 // Type or member is obsolete
|| m_GradationMode == GradationMode.RadialDetail
#pragma warning restore CS0612
|| m_GradationMode == GradationMode.RadialGradient
? 1
: 0);
material.SetFloat(s_DetailIntensity, Mathf.Clamp01(m_DetailIntensity));
material.SetColor(s_DetailColor, ApplyColorSpace(m_DetailColor));
material.SetVector(s_DetailThreshold, new Vector2(m_DetailThreshold.min, m_DetailThreshold.max));
material.SetTexture(s_DetailTex, m_DetailFilter != 0 ? m_DetailTex : null);
material.SetVector(s_DetailTex_ST,
new Vector4(m_DetailTexScale.x, m_DetailTexScale.y,
m_DetailTexOffset.x, m_DetailTexOffset.y));
material.SetVector(s_DetailTex_Speed, m_DetailTexSpeed);
SetKeyword(material, s_ToneKeywords, (int)m_ToneFilter);
SetKeyword(material, s_ColorKeywords, m_ColorFilter != 0 || m_ShadowMode != 0 ? 1 : 0);
SetKeyword(material, s_SamplingKeywords, (int)m_SamplingFilter);
SetKeyword(material, s_TransitionKeywords, (int)m_TransitionFilter);
SetKeyword(material, s_EdgeKeywords, (int)m_EdgeMode);
SetKeyword(material, s_DetailKeywords, (int)m_DetailFilter);
SetKeyword(material, s_TargetKeywords, (int)m_TargetMode);
switch (m_GradationMode)
{
case GradationMode.None:
SetKeyword(material, s_GradationKeywords, 0);
break;
case GradationMode.HorizontalGradient:
case GradationMode.VerticalGradient:
case GradationMode.AngleGradient:
case GradationMode.RadialGradient:
SetKeyword(material, s_GradationKeywords, 1);
break;
case GradationMode.Horizontal:
case GradationMode.Vertical:
case GradationMode.DiagonalToRightBottom:
case GradationMode.DiagonalToLeftBottom:
case GradationMode.Angle:
case GradationMode.Radial:
#pragma warning disable CS0612 // Type or member is obsolete
case GradationMode.RadialDetail:
#pragma warning restore CS0612
SetKeyword(material, s_GradationKeywords, 2);
break;
case GradationMode.Diagonal:
SetKeyword(material, s_GradationKeywords, 3);
break;
}
Profiler.EndSample();
}
public void SetEnablePreview(bool enable, Material material)
{
if (material == null) return;
material.SetVector(s_TransitionTex_Speed, enable ? (Vector4)m_TransitionTexSpeed : Vector4.zero);
material.SetVector(s_DetailTex_Speed, enable ? (Vector4)m_DetailTexSpeed : Vector4.zero);
material.SetFloat(s_TransitionAutoPlaySpeed, enable ? m_TransitionAutoPlaySpeed : 0);
material.SetFloat(s_EdgeShinyAutoPlaySpeed, enable ? m_EdgeShinyAutoPlaySpeed : 0);
}
public void UpdateViewMatrix(Material material, RectTransform transitionRoot, Canvas canvas)
{
if (material == null) return;
var size = transitionRoot.rect.size;
var scale = new Vector3(1f / size.x, 1f / size.y, 1f);
if (m_TransitionKeepAspectRatio && !Mathf.Approximately(scale.x, scale.y))
{
scale.x = scale.y = Mathf.Min(scale.x, scale.y);
}
if (0 != (m_Flip & Flip.Effect))
{
scale.x = 0 != (m_Flip & Flip.Horizontal) ? -scale.x : scale.x;
scale.y = 0 != (m_Flip & Flip.Vertical) ? -scale.y : scale.y;
}
var pivot = new Vector3(0.5f, 0.5f);
var offset = (transitionRoot.pivot - (Vector2)pivot) * size;
var w2LMat = Matrix4x4.Translate(offset) * transitionRoot.worldToLocalMatrix;
var rootRotation = Quaternion.Euler(0, 0, m_TransitionRotation);
var rootScale = 1f / GetMultiplier(m_TransitionRotation);
material.SetMatrix(s_RootViewMatrix, Matrix4x4.TRS(pivot, rootRotation, scale * rootScale) * w2LMat);
var rotation = GetGradationRotation();
var gradRotation = Quaternion.Euler(0, 0, rotation);
var gradScale = 1f / GetMultiplier(rotation);
material.SetMatrix(s_GradViewMatrix, Matrix4x4.TRS(pivot, gradRotation, scale * gradScale) * w2LMat);
if (m_ShadowMode == ShadowMode.Mirror)
{
var mirrorInv = 1f / m_ShadowMirrorScale;
var mirrorOffset = new Vector3(0, size.y / 2 * (mirrorInv + 1) - m_ShadowDistance.y * mirrorInv, 0);
var mirrorScale = new Vector3(1, mirrorInv, 1);
var udScale = new Vector3(scale.x, -scale.y, scale.z);
var mirrorTranslation = Matrix4x4.TRS(mirrorOffset, Quaternion.identity, mirrorScale) * w2LMat;
material.SetMatrix(
s_MirrorRootViewMatrix,
Matrix4x4.TRS(pivot, rootRotation, udScale * rootScale) * mirrorTranslation
);
material.SetMatrix(
s_MirrorGradViewMatrix,
Matrix4x4.TRS(pivot, gradRotation, udScale * gradScale) * mirrorTranslation
);
}
var useIdentity =
canvas.renderMode == RenderMode.WorldSpace ||
(canvas.renderMode != RenderMode.ScreenSpaceOverlay && canvas.worldCamera);
material.SetMatrix(
s_CanvasToWorldMatrix,
useIdentity ? Matrix4x4.identity : canvas.transform.localToWorldMatrix
);
}
private Vector4 GetGradationScaleAndOffset()
{
var gScale = 1 / m_GradationScale;
switch (m_GradationMode)
{
case GradationMode.HorizontalGradient:
case GradationMode.VerticalGradient:
case GradationMode.AngleGradient:
return new Vector4(gScale, 1, -0.5f * (gScale + 1) - m_GradationOffset, 0);
case GradationMode.Radial:
#pragma warning disable CS0612 // Type or member is obsolete
case GradationMode.RadialDetail:
#pragma warning restore CS0612
{
return new Vector4(gScale, 1, m_GradationOffset, 0);
}
default:
return new Vector4(gScale, 1, m_GradationOffset * (gScale + 1) / 2 - gScale * 0.5f + 0.5f, 0);
}
}
private float GetGradationRotation()
{
switch (m_GradationMode)
{
case GradationMode.DiagonalToLeftBottom:
return 135;
case GradationMode.DiagonalToRightBottom:
return 45;
case GradationMode.Vertical:
return 90;
case GradationMode.VerticalGradient:
return -90;
case GradationMode.Angle:
case GradationMode.AngleGradient:
return m_GradationRotation;
default:
return 0;
}
}
private static float GetMultiplier(float deg)
{
var rad = Mathf.Deg2Rad * deg;
var sin = Mathf.Sin(rad);
var cos = Mathf.Cos(rad);
return Mathf.Max(Mathf.Abs(cos - sin), Mathf.Abs(cos + sin));
}
#if UNITY_2021_2_OR_NEWER
private static void SetKeyword(Material material, Span<string> keywords, int index)
#else
private static void SetKeyword(Material material, string[] keywords, int index)
#endif
{
var length = keywords.Length;
if (index < length && !string.IsNullOrEmpty(keywords[index]))
{
material.EnableKeyword(keywords[index]);
}
for (var i = 0; i < length; i++)
{
if (i == index) continue;
material.DisableKeyword(keywords[i]);
}
}
public void ModifyMesh(Graphic graphic, Canvas canvas, RectTransform transitionRoot, VertexHelper vh,
bool canModifyShape)
{
// Apply flip (without effect).
ApplyFlipWithoutEffect(vh, m_Flip);
var count = vh.currentIndexCount;
if (!willModifyVertex || count == 0) return;
var effectProxy = GraphicProxy.Find(graphic);
var isText = effectProxy.IsText(graphic);
effectProxy.OnPreModifyMesh(graphic, canvas);
var verts = s_WorkingVertices;
var expandSize = effectProxy.ModifyExpandSize(graphic, GetExpandSize(canModifyShape));
// Get the rectangle to calculate the normalized position.
vh.GetUIVertexStream(verts);
var bundleSize = isText ? 6 : count;
// Modify vertices for each bundled-quad.
for (var i = 0; i < count; i += bundleSize)
{
// min/max for bundled-quad
UIVertexUtil.GetBounds(verts, i, bundleSize, out var bounds, out var uvMask);
UIVertexUtil.Expand(verts, i, bundleSize, expandSize, bounds);
for (var j = 0; j < bundleSize; j++)
{
var vt = verts[i + j];
if (onModifyVertex != null)
{
vt = onModifyVertex(vt, uvMask);
}
else
{
vt.uv1 = new Vector4(uvMask.xMin, uvMask.yMin, uvMask.xMax, uvMask.yMax);
}
verts[i + j] = vt;
}
}
// Apply flip (with effect).
ApplyFlipWithEffect(verts, m_Flip);
// Apply shadow.
ApplyShadow(verts, transitionRoot, m_Flip);
vh.Clear();
vh.AddUIVertexTriangleStream(verts);
}
private void ApplyShadow(List<UIVertex> verts, RectTransform transitionRoot, Flip flip)
{
var distance = m_ShadowDistance;
if (distance == Vector2.zero) return;
if ((flip & Flip.Shadow) != 0)
{
distance.x = (flip & Flip.Horizontal) != 0 ? -distance.x : distance.x;
distance.y = (flip & Flip.Vertical) != 0 ? -distance.y : distance.y;
}
switch (m_ShadowMode)
{
case ShadowMode.Shadow:
case ShadowMode.Shadow3:
case ShadowMode.Outline:
case ShadowMode.Outline8:
ShadowUtil.DoShadow(verts, s_ShadowVectors[(int)m_ShadowMode], distance, m_ShadowIteration,
m_ShadowFade);
break;
case ShadowMode.Mirror:
ShadowUtil.DoMirror(verts, distance, m_ShadowMirrorScale, m_ShadowFade, transitionRoot);
break;
}
}
private static void ApplyFlipWithoutEffect(VertexHelper vh, Flip flip)
{
if ((flip & Flip.Effect) != 0) return;
var flipHorizontal = 0 != (flip & Flip.Horizontal);
var flipVertical = 0 != (flip & Flip.Vertical);
if (!flipHorizontal && !flipVertical) return;
UIVertexUtil.Flip(vh, flipHorizontal, flipVertical);
}
private static void ApplyFlipWithEffect(List<UIVertex> verts, Flip flip)
{
if ((flip & Flip.Effect) == 0) return;
var flipHorizontal = 0 != (flip & Flip.Horizontal);
var flipVertical = 0 != (flip & Flip.Vertical);
if (!flipHorizontal && !flipVertical) return;
UIVertexUtil.Flip(verts, flipHorizontal, flipVertical);
}
private Vector4 GetExpandSize(bool canModifyShape)
{
if (!canModifyShape) return Vector4.zero;
var expandSize = Vector4.zero;
switch (m_SamplingFilter)
{
case SamplingFilter.BlurFast:
expandSize += Vector4.one * 10;
break;
case SamplingFilter.BlurMedium:
expandSize += Vector4.one * 15;
break;
case SamplingFilter.BlurDetail:
expandSize += Vector4.one * 20;
break;
case SamplingFilter.RgbShift:
expandSize.x += 40;
expandSize.z += 40;
break;
}
switch (m_TransitionFilter)
{
case TransitionFilter.Melt:
expandSize.y += 40;
break;
case TransitionFilter.Burn:
expandSize.w += 40;
break;
}
return expandSize;
}
private static Color ApplyColorSpace(Color color)
{
return QualitySettings.activeColorSpace == ColorSpace.Linear ? color.linear : color;
}
}
}
@@ -0,0 +1,74 @@
# UIEffect 降 DC 改动总结
> 提交:`fa5f0cdb 降低DC的操作`(2026-08-26)
> 涉及目录:`unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/`
## 一、背景与目标
UI_Effect 插件原本每个 `UIEffect` 组件都会生成**独立的材质实例**(缓存 key 使用 `effectId`,即组件的 `GetInstanceID()`,每个实例唯一),导致:
- 相同配置的特效无法共享材质 → 无法参与动态合批 → **Draw Call 偏高**;
- 批量刷 UI 时材质创建开销大,伴随帧率波动。
目标:让**相同配置的 UIEffect 共享同一材质**,从而参与合批,降低 DC。
## 二、方案选型
| 方案 | 说明 | 结论 |
| --- | --- | --- |
| UIEffectReplica + Preset | 业务代码逐个改为副本组件,指向共享 preset | 需要改动大量业务文件、创建 preset 资产,侵入性大 |
| **configId 配置指纹(最终采用)** | 在插件内部按"配置内容哈希"共享材质 | **业务代码零改动**,同配置自动共享合批 |
## 三、具体改动
### 1. `UIEffectContext.cs`(新增约 121 行)
核心:新增 `configId` 属性——特效配置的"稳定指纹"。
- **反射字段缓存**:`s_ConfigFields` 为 `static readonly FieldInfo[]`,`GetFields` 只在类型加载时执行一次,运行时无反射类型发现开销,避免批量刷新时的 GC 尖峰(这是此前"帧率不稳"问题的根因与修复点)。
- **内容哈希 `HashValue`**:
- `null` → 0;
- `UnityEngine.Object` → `GetInstanceID()`(引用同一资产即同哈希);
- `Gradient` → `GradientHash` 按 colorKeys / alphaKeys 逐 key 做内容哈希(不同实例但内容相同的渐变得到相同指纹);
- 基元/枚举 → `GetHashCode()`;嵌套值类型(Color、Vector、MinMax01 等)→ `HashFields` 逐字段展开(`s_FieldCache` 按类型缓存)。
- **雪崩混淆**:FNV-1a 混合后追加 MurmurHash3 终混,让相近配置散列开,降低哈希碰撞概率。
### 2. `UIEffectBase.cs`(`GetModifiedMaterial`)
材质缓存 key 由 `effectId` 改为配置指纹:
```csharp
var configId = context.configId;
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
```
要点:
- `rootId` **仅在特效需要视图矩阵时**(transition / gradation / detail / shiny)才参与哈希,普通灰度(ToneFilter)等不受 root 影响,可共享范围更大;
- `Hash128(baseMaterial, configId, samplingScaleId, rootId)` 相同 → 命中 `MaterialRepository` 中同一材质实例 → 参与动态合批。
### 3. `ShaderVariantRegistry.cs`(Profiler 采样修复)
`RegisterVariant` / `GetVariant` 的**所有提前 return 分支**均补上 `Profiler.EndSample()`,保证 `BeginSample`/`EndSample` 严格配对,修复控制台的:
```
Non-matching Profiler.EndSample (Every EndSample call must have a preceding BeginSample call...)
```
## 四、业务侧影响
- **业务文件零改动**:`EquipBuffItem`、`MapBlockItem`、`PetItem`、`V_RechargeItem`、`FightLinkHeroItem`、`AvatarItem` 等继续正常使用 `UIEffect` 控制灰度,无需迁移到 `UIEffectReplica`。
- 同屏所有**同配置**灰度特效(如大量 `ToneFilter.Grayscale` 的图标)自动共享一个材质并合批。
## 五、效果
- N 个相同配置的 UIEffect:N 个材质实例 → **1 个共享材质**,可被 UGUI 动态合批;
- DC 显著下降(受益程度取决于面板内同配置特效数量)。
## 六、注意事项与局限
1. **配置不同的特效无法互相合批**——每种不同配置组合仍各占一个材质/DC,属预期行为;
2. `configId` 在画布重建时计算,字段遍历存在少量装箱开销,已通过静态缓存降到最低(普通 UI 不会每帧触发);
3. 新增 `UIEffectContext` 公共字段会自动纳入指纹(遍历 public instance 字段),**无需手动维护字段清单**;
4. 验证方式:打开 Frame Debugger / Profiler,确认同配置 UI 元素合并为同一批次;确认控制台不再出现 `Non-matching Profiler.EndSample`。
@@ -0,0 +1,156 @@
diff --git a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
index c3ceb2fd..210257d0 100644
--- a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
+++ b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
@@ -196,8 +196,11 @@ namespace Coffee.UIEffects
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
- var rootId = (uint)(transitionRoot != null ? transitionRoot.GetHashCode() : 0);
- var hash = new Hash128((uint)baseMaterial.GetHashCode(), effectId, samplingScaleId, rootId);
+ // 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
+ // root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
+ var configId = context.configId;
+ var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
+ var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
if (!MaterialRepository.Valid(hash, _material))
{
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial > Get or create material");
diff --git a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
index c315f4cb..48b6ae4b 100644
--- a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
+++ b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Reflection;
using Coffee.UIEffectInternal;
using UnityEngine;
using UnityEngine.Profiling;
@@ -263,6 +264,126 @@ namespace Coffee.UIEffects
|| m_DetailFilter != DetailFilter.None
|| m_EdgeMode == EdgeMode.Shiny;
+ /// <summary>
+ /// 特效配置的稳定指纹。自动遍历所有序列化配置字段生成,因此插件新增属性时无需手动维护字段清单。
+ /// 相同配置会得到相同指纹,从而可以共享同一个材质并参与合批。
+ /// </summary>
+ public uint configId
+ {
+ get
+ {
+ unchecked
+ {
+ var h = (uint)2166136261;
+ var fields = s_ConfigFields;
+ for (var i = 0; i < fields.Length; i++)
+ {
+ var f = fields[i];
+ h = Mix(h, (uint)f.Name.GetHashCode());
+ h = Mix(h, HashValue(f.GetValue(this)));
+ }
+
+ // Avalanche 混淆(MurmurHash3 终混),让相近配置散列到不同指纹。
+ h ^= h >> 16;
+ h *= 0x85ebca6b;
+ h ^= h >> 13;
+ h *= 0xc2b2ae35;
+ h ^= h >> 16;
+ return h;
+ }
+ }
+ }
+
+ private static uint Mix(uint h, uint v)
+ {
+ h ^= v;
+ h *= 16777619;
+ return h;
+ }
+
+ /// <summary>
+ /// 对任意配置值做内容哈希。值类型会逐字段展开,嵌套结构体(Color、Vector2/3/4、MinMax01 等)
+ /// 无需单独写类型处理即可贡献全部数据。只有 UnityEngine.Object 和 Gradient 需要显式处理,
+ /// 因为它们的默认哈希基于引用而非内容。
+ /// </summary>
+ private static uint HashValue(object value)
+ {
+ if (value == null) return 0u;
+
+ if (value is UnityEngine.Object obj)
+ {
+ return obj ? (uint)obj.GetInstanceID() : 0u;
+ }
+
+ if (value is Gradient gradient)
+ {
+ return GradientHash(gradient);
+ }
+
+ var type = value.GetType();
+ if (type.IsPrimitive || type.IsEnum)
+ {
+ return unchecked((uint)value.GetHashCode());
+ }
+
+ if (type.IsValueType)
+ {
+ return HashFields(value, type);
+ }
+
+ return unchecked((uint)value.GetHashCode());
+ }
+
+ private static uint HashFields(object boxed, Type type)
+ {
+ unchecked
+ {
+ if (!s_FieldCache.TryGetValue(type, out var fields))
+ {
+ fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ s_FieldCache[type] = fields;
+ }
+
+ var h = (uint)2166136261;
+ for (var i = 0; i < fields.Length; i++)
+ {
+ h = Mix(h, HashValue(fields[i].GetValue(boxed)));
+ }
+
+ return h;
+ }
+ }
+
+ private static uint GradientHash(Gradient gradient)
+ {
+ unchecked
+ {
+ var h = (uint)2166136261;
+
+ var colorKeys = gradient.colorKeys;
+ for (var i = 0; i < colorKeys.Length; i++)
+ {
+ h = Mix(h, HashValue(colorKeys[i].color));
+ h = Mix(h, HashValue(colorKeys[i].time));
+ }
+
+ var alphaKeys = gradient.alphaKeys;
+ for (var i = 0; i < alphaKeys.Length; i++)
+ {
+ h = Mix(h, HashValue(alphaKeys[i].alpha));
+ h = Mix(h, HashValue(alphaKeys[i].time));
+ }
+
+ return h;
+ }
+ }
+
+ private static readonly FieldInfo[] s_ConfigFields = typeof(UIEffectContext)
+ .GetFields(BindingFlags.Public | BindingFlags.Instance);
+
+ private static readonly Dictionary<Type, FieldInfo[]> s_FieldCache =
+ new Dictionary<Type, FieldInfo[]>();
+
private Texture2D _gradationRampTex;
private Texture2D _transitionRampTex;
private bool _isGradientDirty = true;
@@ -0,0 +1,286 @@
# 移植与验证手册
> 配套包:`DC优化归档/` | 验证过的环境:Unity 团结引擎(Tuanjie)+ UI Effect **5.11.3** + URP
> 改动量:2 个文件、126 行新增、2 行删除。业务代码 **0 改动**。
---
## 第一章 动手前的前提核对(3 项,缺一不可)
### 1.1 UIEffect 必须是"本地源码包",不能是 PackageCache 只读包
打开目标工程的 `Packages/manifest.json`,找 `com.coffee.ui-effect`:
```jsonc
// ✅ 可以改:file: 指向工程内源码目录
"com.coffee.ui-effect": "file:../my_libs/unityLib/UIEffect-main/Packages/src",
// ❌ 改了白改:版本号形式会被拉进 Library/PackageCache/(只读,Unity 重新解包会覆盖)
"com.coffee.ui-effect": "5.11.3"
```
如果是 ❌ 形式,先转成本地包:
1. 从 [github.com/mob-sakai/ParticleEffectForUGUI 同作者的 UIEffect 仓库](https://github.com/mob-sakai/UIEffect) 下载对应版本源码;
2. 放到工程内目录(如 `<项目>/unity/my_libs/unityLib/UIEffect-main/Packages/src`);
3. `manifest.json` 改成 `file:` 相对路径;
4. Unity 重新编译,确认 `Library/PackageCache/` 下的 `com.coffee.ui-effect@*` 不再被引用。
> T5 的实际路径:`d:\Project\T5\T5_game\unity\my_libs\unityLib\UIEffect-main\Packages\src`
> 需要改的两个文件都在它的 `Runtime/` 下。
### 1.2 版本核对:确认锚点代码存在
补丁靠"上下文行"定位,版本差异大就会 apply 失败。打开目标工程的两个文件,确认下面两段存在:
**`Runtime/UIEffectContext.cs`** —— 找到 `useViewMatrix` 属性(补丁插在它后面):
```csharp
public bool useViewMatrix => m_TransitionFilter != TransitionFilter.None
|| m_GradationMode != GradationMode.None
|| m_DetailFilter != DetailFilter.None
|| m_EdgeMode == EdgeMode.Shiny;
```
**`Runtime/UIEffectBase.cs`** —— 找到 `GetModifiedMaterial()` 里的这两行(补丁要替换它们):
```csharp
var rootId = (uint)(transitionRoot != null ? transitionRoot.GetHashCode() : 0);
var hash = new Hash128((uint)baseMaterial.GetHashCode(), effectId, samplingScaleId, rootId);
```
两段都在 → 走**方式 A**(git apply)。
`useViewMatrix` 不存在(更老的版本)→ 走**方式 C**(手工贴),并按 §3.3 的降级写法处理 `rootId`。
### 1.3 记录基线 DC 数字
改之前先量一次,否则改完无法证明有效:
- 打开目标面板(选 UIEffect 用得最多的那个,T5 用的是 `UI_LevelGiftPanel`);
- `Window → Analysis → Frame Debugger`,Enable;
- 记下左侧事件树里 `Canvas.BuildBatch` 下的 `Draw Mesh` 数量(或右下角 Batches 数字);
- 截图存档。
---
## 第二章 三种移植方式
### 方式 A:git apply(推荐,最快最安全)
补丁里的路径是 `a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/...`,
即**相对目标工程 git 根目录**。T5 的 git 根是 `<项目>/T5_game`(不是 `<项目>`),别站错目录。
```powershell
# 站到目标工程的 git 根目录
cd <目标工程 git 根目录>
# 先干跑,看能不能对上(不写文件)
git apply --check --3way "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\uieffect_configId.patch"
# 没问题再真打
git apply --3way "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\uieffect_configId.patch"
# 确认只动了这 2 个文件
git status --porcelain
```
**目标工程 UIEffect 放在别的路径**(比如 `Assets/Plugins/UIEffect/`)时,用 `--directory` 或 `-p` 调整剥层级,例如:
```powershell
# 补丁里 a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/X.cs
# 目标工程实际是 Assets/Plugins/UIEffect/Runtime/X.cs → 剥掉前 5 层再拼目标前缀
git apply -p5 --directory="Assets/Plugins/UIEffect" --3way "<补丁路径>"
```
`--check` 报错 `does not match index` → 目标文件已被改过或版本不同 → 转方式 B/C。
### 方式 B:直接覆盖两个 .cs(兜底,要求版本一致)
```powershell
$dst = "<目标工程>\unity\my_libs\unityLib\UIEffect-main\Packages\src\Runtime"
Copy-Item "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\UIEffectContext.cs" $dst -Force
Copy-Item "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\UIEffectBase.cs" $dst -Force
```
覆盖前**务必先备份原文件**(或确认在 git 里可回退):这两个文件是完整成品,覆盖会连带丢掉目标工程对它们的任何其它本地修改。
> `.meta` 文件不用管:这是"修改已存在的文件",不是"新增资源",guid 不变,prefab/场景引用不受影响。
### 方式 C:手工贴代码块(版本不同时的唯一选择)
三处改动,照着贴:
**C-1 `UIEffectContext.cs` 文件头加 using**
```csharp
using System;
using System.Collections.Generic;
using System.Reflection; // ← 新增这一行
using Coffee.UIEffectInternal;
using UnityEngine;
```
**C-2 `UIEffectContext.cs` 在 `useViewMatrix` 属性之后插入 121 行**
完整代码见本包 `01_UIEffect材质共享降DC/UIEffectContext.cs` 的 **267~385 行**(`configId` 属性 + `Mix` / `HashValue` / `HashFields` / `GradientHash` + `s_ConfigFields` / `s_FieldCache` 两个静态缓存)。
整块是纯新增,不改任何原有代码,粘到 `useViewMatrix` 与 `private Texture2D _gradationRampTex;` 之间即可。
**C-3 `UIEffectBase.cs` 的 `GetModifiedMaterial()` 换 key**(T5 里约 196~203 行)
```csharp
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
// 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
// root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
var configId = context.configId;
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
if (!MaterialRepository.Valid(hash, _material))
```
注意三个容易漏的细节:
- `baseMaterial.GetHashCode()` 也要一起改成 `baseMaterial.GetInstanceID()`(`GetHashCode` 对 Object 不是稳定 ID);
- 只改 `hash` 那一行、不改 `rootId`,会让 transition 类特效在不同 root 下错误共享材质(视觉错位);
- `effectId` 属性**保留不删**,它是 public API,`UIEffectReplica` 还在 override 它。
---
## 第三章 移植后必做的 5 项检查
### 3.1 编译通过 + 无警告
Unity Console 干净。报 `UIEffectContext does not contain a definition for 'useViewMatrix'` → 见 §3.3。
### 3.2 业务代码有没有绕过 context 直接改材质(**最重要**)
材质从"每实例独有"变成"同配置共享"后,**任何直接改 `effectMaterial` 的代码都会污染所有共享者**。
```powershell
# 在目标工程业务脚本目录执行,命中即需逐个确认
cd <目标工程>\unity\Assets
Select-String -Path (Get-ChildItem -Recurse -Filter *.cs).FullName `
-Pattern "effectMaterial|GetComponent<UIEffect>|UIEffect\[\]" | Select-Object Path, LineNumber, Line
```
判定规则:
- 只读(`var m = e.effectMaterial; if (m == null)`)→ 安全;
- 写入(`e.effectMaterial.SetColor/SetFloat/SetTexture/...`)→ **危险**,必须改成"改 UIEffect 组件的序列化字段(走 context)"或"给该实例单独指定不同配置";
- T5 实测:业务代码 0 命中,无需处理。
### 3.3 `useViewMatrix` 不存在的降级写法(老版本插件)
```csharp
var configId = context.configId;
// 老版本没有 useViewMatrix:保守起见让 root 始终参与,共享范围变小但结果一定正确
var rootId = transitionRoot != null ? (uint)transitionRoot.GetInstanceID() : 0u;
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
```
或者自己在 `UIEffectContext` 里补一个等价属性(判断条件见 §1.2 的代码块)。
### 3.4 UIEffectReplica / UIEffectPreset 兼容性
已核对源码(`UIEffectReplica.cs` 的 `context` override),补丁后两条路径行为不变:
| Replica 工作模式 | context 来源 | 补丁前共享依据 | 补丁后共享依据 | 结论 |
| --- | --- | --- | --- | --- |
| 跟随场景内的 target | `target.context`(同一对象) | `effectId = target.effectId` | 同一个 context → configId 必然相同 | 仍共享 |
| 跟随 preset(或 target 不在场景) | 自己的 `base.context` | `effectId = preset.GetHashCode()` | preset 配置相同 → configId 相同 | 仍共享 |
如果目标工程用了 Replica,改完抽查一处 Replica 的显示是否正常即可(T5 只有 1 个 prefab 涉及)。
### 3.5 IL2CPP / 高 stripping 下的一致性(出真机包前做)
`configId` 依赖反射遍历字段。字段本身被 Unity 序列化系统引用,正常不会被裁剪,但**换平台后建议验一次**:
```csharp
// 临时加在 UIEffectBase.GetModifiedMaterial 里,真机跑一次看日志
Debug.Log($"[UIE] configId={configId} hash={hash} name={name}");
```
同一配置的元素在真机上应打出**相同的 configId**,且与编辑器一致。验完删掉这行日志。
---
## 第四章 控制台报错处理
### 4.1 `Non-matching Profiler.EndSample`
```
Non-matching Profiler.EndSample (Every EndSample call must have a preceding BeginSample call...)
```
成因:`ShaderVariantRegistry.cs` 的 `RegisterVariant` / `GetVariant` 里有**提前 return 分支没配对 EndSample**。
处理:文件在 `Runtime/Internal/Utilities/ShaderVariantRegistry.cs`。检查每个 `Profiler.BeginSample(...)` 之后的**所有** `return` 路径,return 前补 `Profiler.EndSample();`。
> T5 的 5.11.3 版本该文件配对本身已正确(`RegisterVariant` 在 301 行 Begin,307/327/333 三处 End 覆盖全部分支;`GetVariant` 在 338 行 Begin,358/370 两处 End),**无需修改**,所以本包没有归档这个补丁。
> T11 当时是旧版本才需要补。
### 4.2 `NullReferenceException` 出现在 UIEffect 相关调用栈
九成是**删了 UIEffect 组件但业务代码还在 `GetComponent<UIEffect>()` 后用**。
血泪教训(T5 实测踩过):**删任何 UI 组件前,先 grep 业务代码有没有引用它**,确认无引用再删。
本补丁不删组件,正常不会触发;若你在做 DC 优化时顺手清理了"看起来没用的" UIEffect,请务必回查。
---
## 第五章 验证方法(怎么证明 DC 真的降了)
### 5.1 Frame Debugger 对比(最直接)
1. 打开同一个面板(跟 §1.3 基线同一个);
2. `Window → Analysis → Frame Debugger` → Enable;
3. 看 `Canvas.BuildBatch` 下的 `Draw Mesh` 数量,与基线对比;
4. **关键验证点**:在事件树里点几个原本各自独占一批的灰度图标,看右下角 **Material / Texture** 是否变成同一个(材质名相同即共享成功)。
预期:同配置特效密集的面板,批次数量级下降(T5 等级礼包面板 ~101 → 30~40)。
如果只降了一两个批次,说明该面板里 UIEffect 配置本来就各不相同(属正常,见 §7 原因 1)。
### 5.2 材质实例数对比(更硬的证据)
Play 模式下执行(临时挂个测试按钮或用 Console 执行):
```csharp
var all = Resources.FindObjectsOfTypeAll<Material>();
Debug.Log($"UIEffect 材质数: {all.Count(m => m.shader != null && m.shader.name.Contains("UIEffect"))}");
```
补丁前:数量 ≈ 场景内 UIEffect 组件数;补丁后:数量 ≈ **不同配置的种类数**。
### 5.3 视觉回归(必做)
逐个检查这几类特效显示是否正常——它们走的是 `rootId` 参与哈希的分支,是本次改动唯一有行为差异的地方:
- Transition(过渡/溶解)
- Gradation(渐变)
- Detail Filter(细节滤镜)
- Edge Mode = Shiny(发光边缘)
普通灰度(ToneFilter.Grayscale)不涉及 root,风险最低。
---
## 第六章 回滚
```powershell
cd <目标工程 git 根目录>
# 方式 A/B 打的补丁:一条命令还原这两个文件
git checkout -- "unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs" `
"unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs"
```
路径不同的按实际改。回滚后回 Unity 编译即可,不涉及资源/序列化数据,**无副作用**(材质缓存 key 是运行时的,不落盘)。
---
## 第七章 "改完了 DC 还是没降"的 7 个原因
按命中概率排序,逐条排:
1. **配置本来就各不相同**:一个面板里 10 个 UIEffect 用了 10 种参数组合 → 10 个材质,属预期。想再降只能统一美术配置(比如灰度就用同一套参数)。
2. **中间夹了不能合批的元素**:合批要求"同一 Canvas + 同材质 + 同纹理 + 层级相邻"。UIEffect 元素之间插了别的图(不同图集)→ 批次被打断,即使材质相同也合不了。
3. **Mask / RectMask2D 切割**:每个 Mask 内部独立成批,跨 Mask 永不合批。
4. **不同图集(SpriteAtlas)**:材质相同但纹理不同 → 不合批。Frame Debugger 里看纹理名 `sactx-...-atlas-<hash>`,hash 不同就是不同图集。
5. **Canvas 被拆成多个**(子物体挂了 `Canvas` 组件 / `SubCanvas`)→ 各自建批。
6. **TextMeshPro 与 Image 混排**:TMP 用字体图集,跟普通 Image 不同纹理,天然分批。
7. **补丁没生效**:改的是 `Library/PackageCache/` 下的副本(§1.1 没做对),或 Unity 没重新编译(改完必须等编译完成、必要时 `Assets → Reimport All`)。
排查工具与看图技巧见 `05_排查方法论/DC与帧率排查经验.md`。
---
## 第八章 升级插件时的注意事项
UIEffect 版本升级会**覆盖这两个文件**,补丁丢失。所以:
1. 补丁改动务必**提交进 git**,commit message 写明"UIEffect configId 降 DC 补丁"(T5 是 `c61b9012`,T11 是 `fa5f0cdb`);
2. 升级插件后重跑本手册第一章的锚点核对,再按方式 A/C 重新打;
3. 本包 `01_UIEffect材质共享降DC/` 里的两个成品 `.cs` 是 5.11.3 版本,**不要**用它覆盖更高版本的插件文件。