648 lines
21 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|