using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Serialization; using UnityEngine.UI; [assembly: InternalsVisibleTo("Coffee.UIParticle.Editor")] namespace Coffee.UIExtensions { /// /// Render maskable and sortable particle effect ,without Camera, RenderTexture or Canvas. /// [ExecuteInEditMode] [RequireComponent(typeof(RectTransform))] [RequireComponent(typeof(CanvasRenderer))] public class UIParticle : MaskableGraphic #if UNITY_EDITOR , ISerializationCallbackReceiver #endif { [HideInInspector] [SerializeField] internal bool m_IsTrail = false; [Tooltip("Ignore canvas scaler")] [SerializeField] [FormerlySerializedAs("m_IgnoreParent")] bool m_IgnoreCanvasScaler = true; [Tooltip("Particle effect scale")] [SerializeField] float m_Scale = 100; [Tooltip("Animatable material properties. If you want to change the material properties of the ParticleSystem in Animation, enable it.")] [SerializeField] internal AnimatableProperty[] m_AnimatableProperties = new AnimatableProperty[0]; [Tooltip("Particles")] [SerializeField] private List m_Particles = new List(); private bool _shouldBeRemoved; private DrivenRectTransformTracker _tracker; private Mesh _bakedMesh; private readonly List _modifiedMaterials = new List(); private readonly List _maskMaterials = new List(); private long _activeMeshIndices; private Vector3 _cachedPosition; private static readonly List s_TempMaterials = new List(2); private static MaterialPropertyBlock s_Mpb; /// /// Should this graphic be considered a target for raycasting? /// public override bool raycastTarget { get { return false; } set { } } public bool ignoreCanvasScaler { get { return m_IgnoreCanvasScaler; } set { // if (m_IgnoreCanvasScaler == value) return; m_IgnoreCanvasScaler = value; _tracker.Clear(); if (isActiveAndEnabled && m_IgnoreCanvasScaler) _tracker.Add(this, rectTransform, DrivenTransformProperties.Scale); } } /// /// Particle effect scale. /// public float scale { get { return m_Scale; } set { m_Scale = Mathf.Max(0.001f, value); } } internal Mesh bakedMesh { get { return _bakedMesh; } } public List particles { get { return m_Particles; } } public IEnumerable materials { get { return _modifiedMaterials; } } internal long activeMeshIndices { get { return _activeMeshIndices; } set { if (_activeMeshIndices == value) return; _activeMeshIndices = value; UpdateMaterial(); } } internal Vector3 cachedPosition { get { return _cachedPosition; } set { _cachedPosition = value; } } public void Play() { particles.Exec(p => p.Play()); } public void Pause() { particles.Exec(p => p.Pause()); } public void Stop() { particles.Exec(p => p.Stop()); } public void RefreshParticles() { GetComponentsInChildren(particles); foreach (var ps in particles) { var tsa = ps.textureSheetAnimation; if (tsa.mode == ParticleSystemAnimationMode.Sprites && tsa.uvChannelMask == (UVChannelFlags) 0) tsa.uvChannelMask = UVChannelFlags.UV0; } particles.Exec(p => p.GetComponent().enabled = !enabled); particles.SortForRendering(transform); SetMaterialDirty(); } protected override void UpdateMaterial() { // Clear mask materials. for (var i = 0; i < _maskMaterials.Count; i++) { StencilMaterial.Remove(_maskMaterials[i]); _maskMaterials[i] = null; } _maskMaterials.Clear(); // Clear modified materials. for (var i = 0; i < _modifiedMaterials.Count; i++) { DestroyImmediate(_modifiedMaterials[i]); _modifiedMaterials[i] = null; } _modifiedMaterials.Clear(); // Recalculate stencil value. if (m_ShouldRecalculateStencil) { var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform); m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0; m_ShouldRecalculateStencil = false; } // No mesh to render. if (activeMeshIndices == 0 || !isActiveAndEnabled || particles.Count == 0) { _activeMeshIndices = 0; canvasRenderer.Clear(); return; } // var materialCount = Mathf.Max(8, activeMeshIndices.BitCount()); canvasRenderer.materialCount = materialCount; var j = 0; for (var i = 0; i < particles.Count; i++) { if (materialCount <= j) break; var ps = particles[i]; if (!ps) continue; var r = ps.GetComponent(); r.GetSharedMaterials(s_TempMaterials); // Main var bit = (long) 1 << (i * 2); if (0 < (activeMeshIndices & bit) && 0 < s_TempMaterials.Count) { var mat = GetModifiedMaterial(s_TempMaterials[0], ps.GetTextureForSprite()); canvasRenderer.SetMaterial(mat, j); UpdateMaterialProperties(r, j); j++; } // Trails if (materialCount <= j) break; bit <<= 1; if (0 < (activeMeshIndices & bit) && 1 < s_TempMaterials.Count) { var mat = GetModifiedMaterial(s_TempMaterials[1], null); canvasRenderer.SetMaterial(mat, j++); } } } private Material GetModifiedMaterial(Material baseMaterial, Texture2D texture) { if (0 < m_StencilValue) { baseMaterial = StencilMaterial.Add(baseMaterial, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0); _maskMaterials.Add(baseMaterial); } if (texture == null && m_AnimatableProperties.Length == 0) return baseMaterial; baseMaterial = new Material(baseMaterial); _modifiedMaterials.Add(baseMaterial); if (texture) baseMaterial.mainTexture = texture; return baseMaterial; } internal void UpdateMaterialProperties(Renderer r, int index) { if (m_AnimatableProperties.Length == 0 || canvasRenderer.materialCount <= index) return; r.GetPropertyBlock(s_Mpb ?? (s_Mpb = new MaterialPropertyBlock())); if (s_Mpb.isEmpty) return; // #41: Copy the value from MaterialPropertyBlock to CanvasRenderer var mat = canvasRenderer.GetMaterial(index); if (!mat) return; foreach (var ap in m_AnimatableProperties) { ap.UpdateMaterialProperties(mat, s_Mpb); } s_Mpb.Clear(); } /// /// This function is called when the object becomes enabled and active. /// protected override void OnEnable() { _cachedPosition = transform.localPosition; _activeMeshIndices = 0; UIParticleUpdater.Register(this); particles.Exec(p => p.GetComponent().enabled = false); if (isActiveAndEnabled && m_IgnoreCanvasScaler) { _tracker.Add(this, rectTransform, DrivenTransformProperties.Scale); } // Create objects. _bakedMesh = MeshPool.Rent(); base.OnEnable(); InitializeIfNeeded(); } /// /// This function is called when the behaviour becomes disabled. /// protected override void OnDisable() { UIParticleUpdater.Unregister(this); if (!_shouldBeRemoved) particles.Exec(p => p.GetComponent().enabled = true); _tracker.Clear(); // Destroy object. MeshPool.Return(_bakedMesh); _bakedMesh = null; base.OnDisable(); } /// /// Call to update the geometry of the Graphic onto the CanvasRenderer. /// protected override void UpdateGeometry() { } /// /// Callback for when properties have been changed by animation. /// protected override void OnDidApplyAnimationProperties() { } private void InitializeIfNeeded() { if (enabled && m_IsTrail) { UnityEngine.Debug.LogWarningFormat(this, "[UIParticle] The UIParticle component should be removed: {0}\nReason: UIParticle for trails is no longer needed.", name); gameObject.hideFlags = HideFlags.None; _shouldBeRemoved = true; enabled = false; return; } else if (enabled && transform.parent && transform.parent.GetComponentInParent()) { UnityEngine.Debug.LogWarningFormat(this, "[UIParticle] The UIParticle component should be removed: {0}\nReason: The parent UIParticle exists.", name); gameObject.hideFlags = HideFlags.None; _shouldBeRemoved = true; enabled = false; return; } if (!this || 0 < particles.Count) return; m_IgnoreCanvasScaler = true; // refresh. #if UNITY_EDITOR if (!Application.isPlaying) UnityEditor.EditorApplication.delayCall += () => { if (this) RefreshParticles(); }; else #endif RefreshParticles(); } #if UNITY_EDITOR void ISerializationCallbackReceiver.OnBeforeSerialize() { InitializeIfNeeded(); } void ISerializationCallbackReceiver.OnAfterDeserialize() { UnityEditor.EditorApplication.delayCall += () => { if (this) InitializeIfNeeded(); }; } #endif } }