NLDClient-yudde/ProjectNLD/Assets/Code/Scripts/UI/CommonUIShadow/CommonUIShadow.cs

648 lines
21 KiB
C#

using UnityEngine;
using UnityEngine.UI;
#if UNITY_2018_1_OR_NEWER
using TMPro;
#endif
namespace NLD.UI
{
public enum ShadowBlendMode
{
Normal,
Multiply,
Additive
}
[ExecuteAlways]
[DisallowMultipleComponent]
[AddComponentMenu("UI/Common UI Shadow")]
public class CommonUIShadow : MonoBehaviour, IMaterialModifier
{
private static readonly int ShadowColorID = Shader.PropertyToID("_ShadowColor");
private static readonly int ShadowOffsetID = Shader.PropertyToID("_ShadowOffset");
private static readonly int ShadowSkewID = Shader.PropertyToID("_ShadowSkew");
private static readonly int ShadowSoftnessID = Shader.PropertyToID("_ShadowSoftness");
private static readonly int ShadowBlurID = Shader.PropertyToID("_ShadowBlur");
private static readonly int ShadowBlurIntensityID = Shader.PropertyToID("_ShadowBlurIntensity");
[Header("Shadow Settings")]
[Tooltip("Size/softness of the shadow (for Image only, TMP shadow is always sharp)")]
[Range(0f, 20f)]
[SerializeField] private float _size = 0f;
[Tooltip("Direction to offset shadow (0-360 degrees)")]
[Range(0f, 360f)]
[SerializeField] private float _offsetAngle = 135f;
[Tooltip("Offset distance (no limit for TMP)")]
[SerializeField] private float _offsetDistance = 30f;
[Header("Skew")]
[Tooltip("Horizontal skew")]
[Range(-1f, 1f)]
[SerializeField] private float _skewX = 0f;
[Tooltip("Vertical skew")]
[Range(-1f, 1f)]
[SerializeField] private float _skewY = 0f;
[Header("Appearance")]
[SerializeField] private Color _color = new Color(0f, 0f, 0f, 0.5f);
[Tooltip("Blur intensity: 0 = sharp edge, 1 = fully blurred (controls gradient width in remaining SDF space)")]
[Range(0f, 1f)]
[SerializeField] private float _blurIntensity = 0f;
[Tooltip("Blur range: total size of the shadow effect (0-10 range)")]
[Min(0f)]
[SerializeField] private float _blurRange = 10f;
[Tooltip("Solid core: fraction of the blur range that is fully opaque (0 = all gradient, 1 = solid fills full range then gradient beyond)")]
[Range(0f, 1f)]
[SerializeField] private float _solidCore = 0f;
[SerializeField] private ShadowBlendMode _blendMode = ShadowBlendMode.Normal;
private enum ShadowMode { None, Shader, ChildObject }
private ShadowMode _mode = ShadowMode.None;
private static Shader _shadowShader;
private Material _modifiedMaterial;
private Graphic _graphic;
private GameObject _contentObject;
private RectTransform _contentRect;
private GameObject _shadowObject;
private RectTransform _shadowRect;
private RectTransform _selfRect;
#if UNITY_2018_1_OR_NEWER
private TextMeshProUGUI _contentTMP;
private TextMeshProUGUI _shadowTMP;
private TextMeshProUGUI _sourceTMP;
[SerializeField] private Color _originalColor = Color.white;
[SerializeField] private bool _colorInitialized = false;
private bool _isDirty = true;
private string _cachedText = null;
private TMP_FontAsset _cachedFont = null;
private float _cachedFontSize = 0f;
private Vector2 _cachedOffset = Vector2.zero;
private int _skipFrames = 0;
private const int SYNC_INTERVAL = 3;
#endif
#region Properties
public float Size
{
get => _size;
set { _size = Mathf.Max(0f, value); MarkDirty(); UpdateShadow(); }
}
public float OffsetAngle
{
get => _offsetAngle;
set { _offsetAngle = (value + 360f) % 360f; MarkDirty(); UpdateShadow(); }
}
public float OffsetDistance
{
get => _offsetDistance;
set { _offsetDistance = value; MarkDirty(); UpdateShadow(); }
}
public float SkewX
{
get => _skewX;
set { _skewX = Mathf.Clamp(value, -1f, 1f); MarkDirty(); UpdateShadow(); }
}
public float SkewY
{
get => _skewY;
set { _skewY = Mathf.Clamp(value, -1f, 1f); MarkDirty(); UpdateShadow(); }
}
public Color Color
{
get => _color;
set { _color = value; MarkDirty(); UpdateShadow(); }
}
public ShadowBlendMode BlendMode
{
get => _blendMode;
set { _blendMode = value; MarkDirty(); UpdateShadow(); }
}
public float BlurIntensity
{
get => _blurIntensity;
set { _blurIntensity = Mathf.Clamp01(value); MarkDirty(); UpdateShadow(); }
}
public float BlurRange
{
get => _blurRange;
set { _blurRange = Mathf.Max(0f, value); MarkDirty(); UpdateShadow(); }
}
public float SolidCore
{
get => _solidCore;
set { _solidCore = Mathf.Clamp01(value); MarkDirty(); UpdateShadow(); }
}
#endregion
private void MarkDirty()
{
#if UNITY_2018_1_OR_NEWER
_isDirty = true;
#endif
}
private void OnEnable()
{
_selfRect = GetComponent<RectTransform>();
DetectMode();
if (_mode == ShadowMode.Shader)
{
_graphic = GetComponent<Graphic>();
EnsureShaderLoaded();
if (_graphic != null)
_graphic.SetMaterialDirty();
}
else if (_mode == ShadowMode.ChildObject)
{
CreateShadowObject();
}
UpdateShadow();
}
private void OnDisable()
{
CleanupShaderMode();
CleanupChildMode();
}
private void OnDestroy()
{
CleanupShaderMode();
CleanupChildMode();
}
private void LateUpdate()
{
if (_mode == ShadowMode.ChildObject)
{
#if UNITY_2018_1_OR_NEWER
if (_sourceTMP != null)
{
bool contentChanged = _isDirty ||
_cachedText != _sourceTMP.text ||
_cachedFont != _sourceTMP.font ||
!Mathf.Approximately(_cachedFontSize, _sourceTMP.fontSize);
if (contentChanged)
{
SyncTMPContent();
_isDirty = false;
_skipFrames = 0;
}
else
{
_skipFrames++;
if (_skipFrames >= SYNC_INTERVAL)
{
SyncTMPContent();
_skipFrames = 0;
}
}
}
#endif
}
}
#if UNITY_EDITOR
private void OnValidate()
{
if (!isActiveAndEnabled) return;
UnityEditor.EditorApplication.delayCall += () =>
{
if (this == null) return;
DetectMode();
UpdateShadow();
};
}
#endif
private void DetectMode()
{
#if UNITY_2018_1_OR_NEWER
if (GetComponent<TextMeshProUGUI>() != null)
{
_mode = ShadowMode.ChildObject;
return;
}
#endif
if (GetComponent<Image>() != null || GetComponent<RawImage>() != null)
{
_mode = ShadowMode.Shader;
return;
}
_mode = ShadowMode.ChildObject;
}
private void UpdateShadow()
{
if (_mode == ShadowMode.Shader)
{
if (_graphic != null)
_graphic.SetMaterialDirty();
}
else if (_mode == ShadowMode.ChildObject)
{
UpdateChildTransforms();
UpdateChildColors();
}
}
#region Shader Mode
private static void EnsureShaderLoaded()
{
if (_shadowShader == null)
{
_shadowShader = Shader.Find("NLD/UI/CommonUIShadow");
}
}
public Material GetModifiedMaterial(Material baseMaterial)
{
if (!isActiveAndEnabled || _mode != ShadowMode.Shader || _shadowShader == null)
return baseMaterial;
if (_modifiedMaterial == null)
{
_modifiedMaterial = new Material(_shadowShader);
_modifiedMaterial.hideFlags = HideFlags.HideAndDontSave;
}
_modifiedMaterial.mainTexture = baseMaterial.mainTexture;
if (baseMaterial.HasProperty("_Color"))
_modifiedMaterial.SetColor("_Color", baseMaterial.GetColor("_Color"));
UpdateShaderProperties(_modifiedMaterial);
return _modifiedMaterial;
}
private void UpdateShaderProperties(Material mat)
{
float angleRad = _offsetAngle * Mathf.Deg2Rad;
Vector2 offset = new Vector2(
Mathf.Cos(angleRad) * _offsetDistance,
Mathf.Sin(angleRad) * _offsetDistance
);
Color shadowColor = GetProcessedColor();
mat.SetColor(ShadowColorID, shadowColor);
mat.SetVector(ShadowOffsetID, new Vector4(offset.x, offset.y, 0, 0));
mat.SetVector(ShadowSkewID, new Vector4(_skewX, _skewY, 0, 0));
mat.SetFloat(ShadowSoftnessID, _size * 0.1f);
mat.SetFloat(ShadowBlurID, _blurRange);
mat.SetFloat(ShadowBlurIntensityID, _blurIntensity);
}
private void CleanupShaderMode()
{
if (_modifiedMaterial != null)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmediate(_modifiedMaterial);
else
#endif
Destroy(_modifiedMaterial);
_modifiedMaterial = null;
}
if (_graphic != null)
_graphic.SetMaterialDirty();
}
#endregion
#region Child Object Mode
private void CreateShadowObject()
{
if (_shadowObject != null || _contentObject != null) return;
#if UNITY_2018_1_OR_NEWER
_sourceTMP = GetComponent<TextMeshProUGUI>();
if (_sourceTMP == null) return;
if (!_colorInitialized)
{
_originalColor = _sourceTMP.color;
_colorInitialized = true;
}
_sourceTMP.color = new Color(0, 0, 0, 0);
_isDirty = true;
_skipFrames = 0;
_shadowObject = new GameObject("_Shadow");
_shadowObject.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
_shadowRect = _shadowObject.AddComponent<RectTransform>();
_shadowObject.transform.SetParent(transform, false);
_shadowObject.transform.SetAsFirstSibling();
_shadowRect.anchorMin = Vector2.zero;
_shadowRect.anchorMax = Vector2.one;
_shadowRect.offsetMin = Vector2.zero;
_shadowRect.offsetMax = Vector2.zero;
_shadowRect.pivot = _selfRect.pivot;
_shadowTMP = _shadowObject.AddComponent<TextMeshProUGUI>();
_shadowTMP.raycastTarget = false;
_contentObject = new GameObject("_Content");
_contentObject.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
_contentRect = _contentObject.AddComponent<RectTransform>();
_contentObject.transform.SetParent(transform, false);
_contentObject.transform.SetAsLastSibling();
_contentRect.anchorMin = Vector2.zero;
_contentRect.anchorMax = Vector2.one;
_contentRect.offsetMin = Vector2.zero;
_contentRect.offsetMax = Vector2.zero;
_contentRect.pivot = _selfRect.pivot;
_contentTMP = _contentObject.AddComponent<TextMeshProUGUI>();
_contentTMP.raycastTarget = false;
SyncTMPContent();
UpdateChildColors();
UpdateChildTransforms();
#endif
}
private void CleanupChildMode()
{
#if UNITY_2018_1_OR_NEWER
if (_sourceTMP != null && _colorInitialized)
{
_sourceTMP.color = _originalColor;
}
#endif
if (_contentObject != null)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmediate(_contentObject);
else
#endif
Destroy(_contentObject);
_contentObject = null;
_contentRect = null;
#if UNITY_2018_1_OR_NEWER
_contentTMP = null;
#endif
}
if (_shadowObject != null)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmediate(_shadowObject);
else
#endif
Destroy(_shadowObject);
_shadowObject = null;
_shadowRect = null;
#if UNITY_2018_1_OR_NEWER
_shadowTMP = null;
_colorInitialized = false;
#endif
}
}
#if UNITY_2018_1_OR_NEWER
private void SyncTMPContent()
{
if (_sourceTMP == null || _shadowTMP == null || _contentTMP == null) return;
if (_sourceTMP.font == null) return;
if (!_colorInitialized)
{
_originalColor = _sourceTMP.color;
_colorInitialized = true;
_sourceTMP.color = new Color(0, 0, 0, 0);
}
_cachedText = _sourceTMP.text;
_cachedFont = _sourceTMP.font;
_cachedFontSize = _sourceTMP.fontSize;
SyncTMPProperties(_contentTMP);
SyncTMPProperties(_shadowTMP);
_contentTMP.color = _originalColor;
_shadowTMP.color = GetProcessedColor();
ApplyTMPShadowBlur();
ExpandShadowVerticesPerCharacter();
Vector2 currentOffset = CalculateOffset();
if (_cachedOffset != currentOffset)
{
UpdateChildTransforms();
_cachedOffset = currentOffset;
}
}
private Vector2 CalculateOffset()
{
float angleRad = _offsetAngle * Mathf.Deg2Rad;
return new Vector2(
Mathf.Cos(angleRad) * _offsetDistance,
Mathf.Sin(angleRad) * _offsetDistance
);
}
private void SyncTMPProperties(TextMeshProUGUI target)
{
if (target == null || _sourceTMP == null) return;
target.text = _sourceTMP.text;
target.font = _sourceTMP.font;
target.fontSize = _sourceTMP.fontSize;
target.fontStyle = _sourceTMP.fontStyle;
target.fontWeight = _sourceTMP.fontWeight;
target.alignment = _sourceTMP.alignment;
if (target.enableWordWrapping != _sourceTMP.enableWordWrapping)
target.enableWordWrapping = _sourceTMP.enableWordWrapping;
if (target.overflowMode != _sourceTMP.overflowMode)
target.overflowMode = _sourceTMP.overflowMode;
}
private void ApplyTMPShadowBlur()
{
if (_shadowTMP == null) return;
// totalExtent: total shadow size in SDF space (range 0-10 maps to 0-1)
float totalExtent = Mathf.Clamp01(_blurRange * 0.1f);
// solidCore controls what fraction of totalExtent is fully opaque:
// 0 = no solid core (all gradient) → 1 = solid fills full range then gradient beyond
float faceDilate = totalExtent * _solidCore;
// intensity controls gradient width in remaining SDF space (prevents inward blur at high faceDilate)
float outlineSoftness = _blurIntensity * (1f - faceDilate);
// Use fontMaterials (plural) to cover all sub-meshes including CJK fallback fonts
var materials = _shadowTMP.fontMaterials;
foreach (var mat in materials)
{
if (mat == null) continue;
mat.SetFloat(ShaderUtilities.ID_FaceDilate, faceDilate);
mat.SetFloat(ShaderUtilities.ID_OutlineSoftness, outlineSoftness);
}
}
private void ExpandShadowVerticesPerCharacter()
{
if (_shadowTMP == null || _blurRange <= 0f) return;
_shadowTMP.ForceMeshUpdate();
var textInfo = _shadowTMP.textInfo;
if (textInfo == null) return;
float scale = 1f + _blurRange * 0.02f;
for (int i = 0; i < textInfo.characterCount; i++)
{
var charInfo = textInfo.characterInfo[i];
if (!charInfo.isVisible) continue;
int meshIndex = charInfo.materialReferenceIndex;
int vi = charInfo.vertexIndex;
var meshInfo = textInfo.meshInfo[meshIndex];
// Character center (average of its 4 quad vertices)
Vector3 vCenter = (meshInfo.vertices[vi] + meshInfo.vertices[vi + 1] +
meshInfo.vertices[vi + 2] + meshInfo.vertices[vi + 3]) * 0.25f;
// UV center (average of its 4 quad UVs, used for SDF scaling)
Vector2 uvCenter = (meshInfo.uvs0[vi] + meshInfo.uvs0[vi + 1] +
meshInfo.uvs0[vi + 2] + meshInfo.uvs0[vi + 3]) * 0.25f;
for (int j = 0; j < 4; j++)
{
// Expand vertex position outward from this character's own center
meshInfo.vertices[vi + j] = vCenter + (meshInfo.vertices[vi + j] - vCenter) * scale;
// Contract UV inward so SDF boundary maps to the expanded position
meshInfo.uvs0[vi + j] = uvCenter + (meshInfo.uvs0[vi + j] - uvCenter) / scale;
}
}
// Push modified geometry back to TMP's mesh
for (int i = 0; i < textInfo.materialCount; i++)
{
var meshInfo = textInfo.meshInfo[i];
if (meshInfo.mesh == null) continue;
meshInfo.mesh.vertices = meshInfo.vertices;
meshInfo.mesh.uv = meshInfo.uvs0;
_shadowTMP.UpdateGeometry(meshInfo.mesh, i);
}
}
#endif
private void UpdateChildColors()
{
#if UNITY_2018_1_OR_NEWER
if (_contentTMP != null)
_contentTMP.color = _originalColor;
if (_shadowTMP != null)
{
_shadowTMP.color = GetProcessedColor();
ApplyTMPShadowBlur();
}
#endif
}
private void UpdateChildTransforms()
{
if (_shadowRect == null || _contentRect == null || _selfRect == null) return;
Vector2 offset = (_cachedOffset != Vector2.zero) ? _cachedOffset : CalculateOffset();
_contentRect.anchoredPosition = Vector2.zero;
_shadowRect.anchoredPosition = offset;
bool hasSkew = !Mathf.Approximately(_skewX, 0f) || !Mathf.Approximately(_skewY, 0f);
if (hasSkew)
{
float skewAngle = Mathf.Atan2(_skewX + _skewY, 2f) * Mathf.Rad2Deg;
_shadowRect.localRotation = Quaternion.Euler(0, 0, skewAngle * 0.5f);
float scaleX = 1f + Mathf.Abs(_skewY) * 0.15f;
float scaleY = 1f + Mathf.Abs(_skewX) * 0.15f;
_shadowRect.localScale = new Vector3(scaleX, scaleY, 1f);
}
else
{
_shadowRect.localRotation = Quaternion.identity;
_shadowRect.localScale = Vector3.one;
}
_contentRect.localRotation = Quaternion.identity;
_contentRect.localScale = Vector3.one;
}
private void CopyRectTransform(RectTransform source, RectTransform target)
{
target.anchorMin = source.anchorMin;
target.anchorMax = source.anchorMax;
target.pivot = source.pivot;
target.sizeDelta = source.sizeDelta;
}
#endregion
private Color GetProcessedColor()
{
Color shadowColor = _color;
switch (_blendMode)
{
case ShadowBlendMode.Multiply:
shadowColor = new Color(_color.r * 0.7f, _color.g * 0.7f, _color.b * 0.7f, _color.a);
break;
case ShadowBlendMode.Additive:
shadowColor = new Color(_color.r, _color.g, _color.b, _color.a * 0.6f);
break;
}
return shadowColor;
}
public void SetOffset(float x, float y)
{
_offsetDistance = Mathf.Sqrt(x * x + y * y);
_offsetAngle = Mathf.Atan2(y, x) * Mathf.Rad2Deg;
if (_offsetAngle < 0) _offsetAngle += 360f;
MarkDirty();
UpdateShadow();
}
}
}