Seven is the number.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

565 lines
16 KiB

3 years ago
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.EventSystems;
  5. using UnityEngine.Serialization;
  6. using UnityEngine.UI;
  7. namespace LeTai.TrueShadow
  8. {
  9. [RequireComponent(typeof(Graphic))]
  10. // Doesn't seem to cause problem any more. Hmm
  11. // [DisallowMultipleComponent]
  12. [HelpURL("https://leloctai.com/trueshadow/docs/articles/customize.html")]
  13. [ExecuteAlways]
  14. public partial class TrueShadow : UIBehaviour, IMeshModifier, ICanvasElement
  15. {
  16. static readonly Color DEFAULT_COLOR = new Color(0, 0, 0, .3f);
  17. [Tooltip("Size of the shadow")]
  18. [SerializeField] float size = 32;
  19. [Tooltip("Spread of the shadow")]
  20. [SpreadSlider]
  21. [SerializeField] float spread = 0;
  22. [Tooltip("Direction to offset the shadow toward")]
  23. [Knob]
  24. [SerializeField] float offsetAngle = 90;
  25. [Tooltip("How far to offset the shadow")]
  26. [SerializeField] float offsetDistance = 8;
  27. [SerializeField] Vector2 offset = Vector2.zero;
  28. [Tooltip("Tint the shadow")]
  29. [SerializeField] Color color = DEFAULT_COLOR;
  30. [Tooltip("Inset shadow")]
  31. [InsetToggle]
  32. [SerializeField] bool inset = false;
  33. [Tooltip("Blend mode of the shadow")]
  34. [SerializeField] BlendMode blendMode;
  35. [FormerlySerializedAs("multiplyCasterAlpha")]
  36. [Tooltip("Allow shadow to cross-fade with caster")]
  37. [SerializeField] bool useCasterAlpha = true;
  38. [Tooltip("Ignore the shadow caster's color, so you can choose specific color for your shadow")]
  39. [SerializeField] bool ignoreCasterColor = false;
  40. [Tooltip(
  41. "How to obtain the color of the area outside of the source image. Automatically set based on Blend Mode. You should only change this setting if you are using some very custom UI that require it")]
  42. [SerializeField] ColorBleedMode colorBleedMode;
  43. [Tooltip("Position the shadow GameObject as previous sibling of the UI element")]
  44. [SerializeField] bool shadowAsSibling;
  45. [Tooltip("Cut the source image from the shadow to avoid shadow showing behind semi-transparent UI")]
  46. [SerializeField] bool cutout;
  47. #pragma warning disable 0649
  48. [Tooltip(
  49. "Bake the shadow to a sprite to reduce CPU and GPU usage at runtime, at the cost of storage, memory and flexibility")]
  50. [SerializeField] bool baked;
  51. #pragma warning restore 0649
  52. [SerializeField] bool modifiedFromInspector = false;
  53. public float Size
  54. {
  55. get => size;
  56. set
  57. {
  58. var newSize = Mathf.Max(0, value);
  59. if (modifiedFromInspector || !Mathf.Approximately(size, newSize))
  60. {
  61. modifiedFromInspector = false;
  62. SetLayoutDirty();
  63. SetTextureDirty();
  64. }
  65. size = newSize;
  66. if (Inset && OffsetDistance > Size)
  67. {
  68. OffsetDistance = Size;
  69. }
  70. }
  71. }
  72. public float Spread
  73. {
  74. get => spread;
  75. set
  76. {
  77. var newSpread = Mathf.Clamp01(value);
  78. if (modifiedFromInspector || !Mathf.Approximately(spread, newSpread))
  79. {
  80. modifiedFromInspector = false;
  81. SetLayoutDirty();
  82. SetTextureDirty();
  83. }
  84. spread = newSpread;
  85. }
  86. }
  87. public float OffsetAngle
  88. {
  89. get => offsetAngle;
  90. set
  91. {
  92. var newValue = (value + 360f) % 360f;
  93. if (modifiedFromInspector || !Mathf.Approximately(offsetAngle, newValue))
  94. {
  95. modifiedFromInspector = false;
  96. SetLayoutDirty();
  97. if (Cutout)
  98. SetTextureDirty();
  99. }
  100. offsetAngle = newValue;
  101. offset = Math.AngleDistanceVector(offsetAngle, offset.magnitude, Vector2.right);
  102. }
  103. }
  104. public float OffsetDistance
  105. {
  106. get => offsetDistance;
  107. set
  108. {
  109. // Limit offset distance for now.
  110. // In order to properly render larger offset, imprint have to be rendered twice.
  111. // TODO: Implement if no one complain about perf
  112. var newValue = value;
  113. if (Inset)
  114. newValue = Mathf.Clamp(newValue, 0, Size);
  115. else
  116. newValue = Mathf.Max(0, newValue);
  117. if (modifiedFromInspector || !Mathf.Approximately(offsetDistance, newValue))
  118. {
  119. modifiedFromInspector = false;
  120. SetLayoutDirty();
  121. if (Cutout)
  122. SetTextureDirty();
  123. }
  124. offsetDistance = newValue;
  125. offset = offset.sqrMagnitude < 1e-6f
  126. ? Math.AngleDistanceVector(offsetAngle, offsetDistance, Vector2.right)
  127. : offset.normalized * offsetDistance;
  128. }
  129. }
  130. public Color Color
  131. {
  132. get => color;
  133. set
  134. {
  135. if (modifiedFromInspector || value != color)
  136. {
  137. modifiedFromInspector = false;
  138. SetLayoutDirty();
  139. }
  140. color = value;
  141. }
  142. }
  143. /// <summary>
  144. /// Allow shadow to cross-fade with caster
  145. /// </summary>
  146. public bool UseCasterAlpha
  147. {
  148. get => useCasterAlpha;
  149. set
  150. {
  151. if (modifiedFromInspector || value != useCasterAlpha)
  152. {
  153. modifiedFromInspector = false;
  154. SetLayoutDirty();
  155. }
  156. useCasterAlpha = value;
  157. }
  158. }
  159. /// <summary>
  160. /// Ignore the shadow caster's color, so you can choose specific color for your shadow.
  161. /// When false, <see cref="Color"/> is multiplied with caster's color.
  162. /// </summary>
  163. public bool IgnoreCasterColor
  164. {
  165. get => ignoreCasterColor;
  166. set
  167. {
  168. if (modifiedFromInspector || value != ignoreCasterColor)
  169. {
  170. modifiedFromInspector = false;
  171. SetTextureDirty();
  172. }
  173. ignoreCasterColor = value;
  174. }
  175. }
  176. public bool Inset
  177. {
  178. get => inset;
  179. set
  180. {
  181. if (modifiedFromInspector || value != inset)
  182. {
  183. modifiedFromInspector = false;
  184. SetTextureDirty();
  185. }
  186. inset = value;
  187. if (Inset && OffsetDistance > Size)
  188. {
  189. OffsetDistance = Size;
  190. }
  191. }
  192. }
  193. public BlendMode BlendMode
  194. {
  195. get => blendMode;
  196. set
  197. {
  198. // Work around for Unity bug causing references loss
  199. if (!Graphic || !CanvasRenderer)
  200. return;
  201. blendMode = value;
  202. shadowRenderer.UpdateMaterial();
  203. switch (blendMode)
  204. {
  205. case BlendMode.Normal:
  206. case BlendMode.Additive:
  207. case BlendMode.Screen:
  208. case BlendMode.Multiply:
  209. ColorBleedMode = ColorBleedMode.Black;
  210. break;
  211. default:
  212. ColorBleedMode = ColorBleedMode.Black;
  213. break;
  214. }
  215. }
  216. }
  217. /// <summary>
  218. /// How to obtain the color of the area outside of the source image. Automatically set based on Blend Mode. You should only change this setting if you are using some very custom UI that require it.
  219. ///
  220. /// <seealso cref="ClearColor"/>
  221. /// </summary>
  222. public ColorBleedMode ColorBleedMode
  223. {
  224. get => colorBleedMode;
  225. set
  226. {
  227. if (modifiedFromInspector || colorBleedMode != value)
  228. {
  229. modifiedFromInspector = false;
  230. colorBleedMode = value;
  231. SetTextureDirty();
  232. }
  233. }
  234. }
  235. /// <summary>
  236. /// The area where the alpha channel = 0 can be either 0, or the color of the edge of the texture, depend on how the texture was authored.
  237. /// Normally this is not visible, but when blurred, the alpha in these area will become greater than 0
  238. /// Depend on the blendmode, different color for this clear area may be desired.
  239. ///
  240. /// You can provide custom clear color by implementing <see cref="PluginInterfaces.ITrueShadowCasterClearColorProvider"/>, and set this to Plugin
  241. /// </summary>
  242. /// <exception cref="ArgumentOutOfRangeException"></exception>
  243. public Color ClearColor
  244. {
  245. get
  246. {
  247. switch (colorBleedMode)
  248. {
  249. case ColorBleedMode.ImageColor:
  250. return Graphic.color.WithA(0);
  251. case ColorBleedMode.ShadowColor:
  252. return Color.WithA(0);
  253. case ColorBleedMode.Black:
  254. return Color.clear;
  255. case ColorBleedMode.White:
  256. return new Color(1, 1, 1, 0);
  257. case ColorBleedMode.Plugin:
  258. return casterClearColorProvider?.GetTrueShadowCasterClearColor() ?? Color.clear;
  259. default:
  260. throw new ArgumentOutOfRangeException();
  261. }
  262. }
  263. }
  264. /// <summary>
  265. /// Can't be implemented due to <see href="https://issuetracker.unity3d.com/issues/prefab-instances-sibling-index-is-not-updated-when-a-lower-index-sibling-is-deleted">Unity's bug 1280465</see>. Do not use!
  266. /// </summary>
  267. [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
  268. public bool ShadowAsSibling
  269. {
  270. get => shadowAsSibling;
  271. set
  272. {
  273. shadowAsSibling = value;
  274. ShadowRenderer.ClearMaskMaterialCache();
  275. if (shadowAsSibling)
  276. {
  277. ShadowSorter.Instance.Register(this);
  278. }
  279. else
  280. {
  281. ShadowSorter.Instance.UnRegister(this);
  282. if (shadowRenderer) // defensive. undo & prefab make state weird sometime
  283. {
  284. var rendererTransform = shadowRenderer.transform;
  285. rendererTransform.SetParent(transform, true);
  286. rendererTransform.SetSiblingIndex(0);
  287. }
  288. }
  289. }
  290. }
  291. /// <summary>
  292. /// Always true due to <see cref="ShadowAsSibling"/>. Do not use!
  293. /// </summary>
  294. [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
  295. public bool Cutout
  296. {
  297. get => !shadowAsSibling || cutout;
  298. set => cutout = value;
  299. }
  300. [SerializeField] List<Sprite> bakedShadows;
  301. public Vector2 Offset => offset;
  302. internal ShadowRenderer shadowRenderer;
  303. internal Mesh SpriteMesh { get; set; }
  304. internal Graphic Graphic { get; set; }
  305. internal CanvasRenderer CanvasRenderer { get; set; }
  306. internal RectTransform RectTransform { get; private set; }
  307. internal Texture Content
  308. {
  309. get
  310. {
  311. switch (Graphic)
  312. {
  313. case Image image:
  314. var sprite = image.overrideSprite;
  315. return sprite ? sprite.texture : null;
  316. case RawImage rawImage: return rawImage.texture;
  317. case Text text: return text.mainTexture;
  318. default: return null;
  319. }
  320. }
  321. }
  322. internal int TextureRevision { get; private set; }
  323. #if LETAI_TRUESHADOW_DEBUG
  324. public bool alwaysRender;
  325. #endif
  326. ShadowContainer shadowContainer;
  327. internal ShadowContainer ShadowContainer
  328. {
  329. get => shadowContainer;
  330. private set => shadowContainer = value;
  331. }
  332. bool textureDirty;
  333. bool layoutDirty;
  334. internal bool hierachyDirty;
  335. protected override void Awake()
  336. {
  337. if (ShadowAsSibling)
  338. ShadowSorter.Instance.Register(this);
  339. }
  340. protected override void OnEnable()
  341. {
  342. base.OnEnable();
  343. RectTransform = GetComponent<RectTransform>();
  344. Graphic = GetComponent<Graphic>();
  345. CanvasRenderer = GetComponent<CanvasRenderer>();
  346. if (!SpriteMesh) SpriteMesh = new Mesh();
  347. InitializePlugins();
  348. if (bakedShadows == null)
  349. bakedShadows = new List<Sprite>(4);
  350. InitInvalidator();
  351. ShadowRenderer.Initialize(this, ref shadowRenderer);
  352. Canvas.willRenderCanvases += OnWillRenderCanvas;
  353. if (Graphic)
  354. Graphic.SetVerticesDirty();
  355. #if UNITY_EDITOR
  356. UnityEditor.Undo.undoRedoPerformed += ApplySerializedData;
  357. #endif
  358. #if UNITY_EDITOR
  359. if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
  360. UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
  361. #endif
  362. }
  363. public void ApplySerializedData()
  364. {
  365. // Changes from prefab apply does not seem to call certain setters. Call manually
  366. Size = size;
  367. Spread = spread;
  368. OffsetAngle = offsetAngle;
  369. OffsetDistance = offsetDistance;
  370. BlendMode = blendMode;
  371. ShadowAsSibling = shadowAsSibling;
  372. SetHierachyDirty();
  373. SetLayoutDirty();
  374. SetTextureDirty();
  375. if (shadowRenderer) shadowRenderer.SetMaterialDirty();
  376. }
  377. protected override void OnDisable()
  378. {
  379. Canvas.willRenderCanvases -= OnWillRenderCanvas;
  380. TerminateInvalidator();
  381. if (shadowRenderer) shadowRenderer.gameObject.SetActive(false);
  382. #if UNITY_EDITOR
  383. UnityEditor.Undo.undoRedoPerformed -= ApplySerializedData;
  384. #endif
  385. }
  386. protected override void OnDestroy()
  387. {
  388. ShadowSorter.Instance.UnRegister(this);
  389. if (shadowRenderer) shadowRenderer.Dispose();
  390. ShadowFactory.Instance.ReleaseContainer(shadowContainer);
  391. }
  392. bool ShouldPerformWorks()
  393. {
  394. bool areCanvasRenderersCulled = CanvasRenderer && CanvasRenderer.cull &&
  395. shadowRenderer.CanvasRenderer && shadowRenderer.CanvasRenderer.cull;
  396. return isActiveAndEnabled && !areCanvasRenderersCulled;
  397. }
  398. void LateUpdate()
  399. {
  400. if (!ShouldPerformWorks())
  401. return;
  402. CheckTransformDirtied();
  403. }
  404. public void Rebuild(CanvasUpdate executing)
  405. {
  406. // Debug.Assert(true, "This should not be called in child mode");
  407. if (!ShouldPerformWorks()) return;
  408. if (executing == CanvasUpdate.PostLayout)
  409. {
  410. if (layoutDirty)
  411. {
  412. shadowRenderer.ReLayout();
  413. layoutDirty = false;
  414. }
  415. }
  416. }
  417. void OnWillRenderCanvas()
  418. {
  419. if (!isActiveAndEnabled) return;
  420. #if LETAI_TRUESHADOW_DEBUG
  421. if (alwaysRender) textureDirty = true;
  422. #endif
  423. if (!ShouldPerformWorks()) return;
  424. if (textureDirty && Graphic && Graphic.canvas)
  425. {
  426. ShadowFactory.Instance.Get(new ShadowSettingSnapshot(this), ref shadowContainer);
  427. shadowRenderer.SetTexture(shadowContainer?.Texture);
  428. textureDirty = false;
  429. }
  430. if (!shadowAsSibling)
  431. {
  432. if (shadowRenderer.transform.parent != transform)
  433. shadowRenderer.transform.SetParent(RectTransform, true);
  434. if (shadowRenderer.transform.GetSiblingIndex() != 0)
  435. shadowRenderer.transform.SetSiblingIndex(0);
  436. UnSetHierachyDirty();
  437. if (layoutDirty)
  438. {
  439. shadowRenderer.ReLayout();
  440. layoutDirty = false;
  441. }
  442. }
  443. }
  444. public void LayoutComplete() { }
  445. public void GraphicUpdateComplete() { }
  446. public void SetTextureDirty()
  447. {
  448. textureDirty = true;
  449. unchecked
  450. {
  451. TextureRevision++;
  452. }
  453. }
  454. public void SetLayoutDirty()
  455. {
  456. layoutDirty = true;
  457. }
  458. public void SetHierachyDirty()
  459. {
  460. hierachyDirty = true;
  461. }
  462. internal void UnSetHierachyDirty()
  463. {
  464. hierachyDirty = false;
  465. }
  466. }
  467. }