UGUI上显示特效插件ParticleEffectForUGUI

1. 前言

ui上显示特效常常涉及到层级的问题,常见处理办法是通过控制Canvas和特效Renderer的Sorting Layer以及Order in Layer来控制渲染顺序,这种实现的弊端是不好实现遮罩功能,需要手动把裁剪区域传递给自定义的Shader来实现裁剪,对于使用模板测试的Mask,由于Mask在子节点的UI元素渲染完后,会自动清除模板值,导致渲染特效时无法做模板测试;

鉴于此,有大佬实现了一种用CanvasRenderer来渲染特效的方案,github传送门

由于使用CanvasRenderer来渲染特效,ParticleEffectForUGUI插件具有如下特点:

        1. 和其他ui元素一样按gameObject位置排序;

        2. 支持Mask和RectMask2D;

        3. 显示相同的特效时,可共享mesh;

2. 安装ParticleEffectForUGUI插件:

3. 使用UIParticle组件来渲染特效:

将要显示的特效拖进UIParticle下:

UIParticle组件识别不到拖进了特效,手动刷新一下:

调整Scale控制特效显示,注意若要支持Mask和RectMask2D,渲染特效的Shader得支持模板测试和矩形裁剪,可参阅插件UIAdditive shader或者unity的UI/Default shader;

可看到特效显示在ui中间:

4. 实现原理:

UIParticleUpdater类,驱动渲染刷新:

#if UNITY_EDITOR
        [InitializeOnLoadMethod]
#else
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#endif
        private static void InitializeOnLoad()
        {
            //先于ugui中CanvasUpdateRegistry注册,回调应该先执行
            Canvas.willRenderCanvases -= Refresh;
            Canvas.willRenderCanvases += Refresh;
        }

        private static void Refresh()
        {
            // Do not allow it to be called in the same frame.
            if (s_FrameCount == Time.frameCount) return;
            s_FrameCount = Time.frameCount;

            // Simulate -> Primary
            for (var i = 0; i < s_ActiveParticles.Count; i++)
            {
                var uip = s_ActiveParticles[i];
                if (!uip || !uip.canvas || !uip.isPrimary || !s_UpdatedGroupIds.Add(uip.groupId)) continue;

                uip.UpdateTransformScale();
                uip.UpdateRenderers();
            }

            // Simulate -> Others
            for (var i = 0; i < s_ActiveParticles.Count; i++)
            {
                var uip = s_ActiveParticles[i];
                if (!uip || !uip.canvas) continue;

                uip.UpdateTransformScale();     //Primary也会再次执行,感觉不严谨

                if (!uip.useMeshSharing)
                {
                    uip.UpdateRenderers();
                }
                else if (s_UpdatedGroupIds.Add(uip.groupId))
                {
                    uip.UpdateRenderers();
                }
            }

            s_UpdatedGroupIds.Clear();

            // Attract
            for (var i = 0; i < s_ActiveAttractors.Count; i++)
            {
                s_ActiveAttractors[i].Attract();
            }
        }

核心组件UIParticle:

//父节点global Scale
public Vector3 parentScale { get; private set; }

//实际为 1 / Canvas local Scale
public Vector3 canvasScale { get; private set; }

protected override void OnEnable()
{
    _isScaleStored = false;
    ResetGroupId();

    //组件注册
    UIParticleUpdater.Register(this);

    //maskable变化时,触发材质球刷新
    RegisterDirtyMaterialCallback(UpdateRendererMaterial);

    if (0 < particles.Count)
    {
        RefreshParticles(particles);
    }
    else
    {
        RefreshParticles();
    }

    base.OnEnable();
}

/// <summary>
/// This function is called when the behaviour becomes disabled.
/// </summary>
protected override void OnDisable()
{
    _tracker.Clear();
    if (autoScalingMode == AutoScalingMode.Transform && _isScaleStored)
    {
        transform.localScale = _storedScale;
    }

    _isScaleStored = false;
    UIParticleUpdater.Unregister(this);
    _renderers.ForEach(r => r.Reset());
    UnregisterDirtyMaterialCallback(UpdateRendererMaterial);

    base.OnDisable();
}

/// <summary>
/// Refresh UIParticle.
/// Collect ParticleSystems under the GameObject and refresh the UIParticle.
/// </summary>
public void RefreshParticles()
{
    RefreshParticles(gameObject);
}

/// <summary>
/// Refresh UIParticle.
/// Collect ParticleSystems under the GameObject and refresh the UIParticle.
/// </summary>
private void RefreshParticles(GameObject root)
{
    if (!root) return;

    //获取子节点中属于自己的所有ParticleSystem
    root.GetComponentsInChildren(true, particles);
    for (var i = particles.Count - 1; 0 <= i; i--)
    {
        var ps = particles[i];
        if (!ps || ps.GetComponentInParent<UIParticle>(true) != this)
        {
            particles.RemoveAt(i);
        }
    }

    for (var i = 0; i < particles.Count; i++)
    {
        var ps = particles[i];
        var tsa = ps.textureSheetAnimation;
        if (tsa.mode == ParticleSystemAnimationMode.Sprites && tsa.uvChannelMask == 0)
        {
            tsa.uvChannelMask = UVChannelFlags.UV0;
        }
    }

    RefreshParticles(particles);
}

/// <summary>
/// Refresh UIParticle using a list of ParticleSystems.
/// </summary>
public void RefreshParticles(List<ParticleSystem> particleSystems)
{
    // Collect children UIParticleRenderer components.
    // #246: Nullptr exceptions when using nested UIParticle components in hierarchy
    _renderers.Clear();
    var childCount = transform.childCount;
    for (var i = 0; i < childCount; i++)
    {
        var child = transform.GetChild(i);
        if (child.TryGetComponent(out UIParticleRenderer uiParticleRenderer))
        {
            _renderers.Add(uiParticleRenderer);
        }
    }

    // Reset the UIParticleRenderer components.
    for (var i = 0; i < _renderers.Count; i++)
    {
        _renderers[i].Reset(i);
    }

    //每个ParticleSystem对应一个UIParticleRenderer,trail还需要额外一个UIParticleRenderer
    // Set the ParticleSystem to the UIParticleRenderer. If the trail is enabled, set it additionally.
    var j = 0;
    for (var i = 0; i < particleSystems.Count; i++)
    {
        var ps = particleSystems[i];
        if (!ps) continue;
        GetRenderer(j++).Set(this, ps, false);

        // If the trail is enabled, set it additionally.
        if (ps.trails.enabled)
        {
            GetRenderer(j++).Set(this, ps, true);
        }
    }
}

internal void UpdateTransformScale()
{
    _tracker.Clear();
    //抵消canvas缩放
    canvasScale = canvas.rootCanvas.transform.localScale.Inverse();
    parentScale = transform.parent.lossyScale;
    if (autoScalingMode != AutoScalingMode.Transform)
    {
        if (_isScaleStored)
        {
            transform.localScale = _storedScale;
        }

        _isScaleStored = false;
        return;
    }

    //AutoScalingMode.Transform 的流程
    var currentScale = transform.localScale;
    if (!_isScaleStored)
    {
        _storedScale = currentScale.IsVisible() ? currentScale : Vector3.one;
        _isScaleStored = true;
    }

    _tracker.Add(this, rectTransform, DrivenTransformProperties.Scale);
    var newScale = parentScale.Inverse();
    if (currentScale != newScale)
    {
        //场景中特效Scale为1,放进ui里面得抵消掉ui缩放
        transform.localScale = newScale;
    }
}

internal void UpdateRenderers()
{
    if (!isActiveAndEnabled) return;

    for (var i = 0; i < _renderers.Count; i++)
    {
        var r = _renderers[i];
        if (r) continue;
        //有UIParticleRenderer不存在时,重新刷新下列表
        RefreshParticles(particles);
        break;
    }

    var bakeCamera = GetBakeCamera();
    for (var i = 0; i < _renderers.Count; i++)
    {
        var r = _renderers[i];
        if (!r) continue;

        //获取特效mesh,同步到ui渲染
        r.UpdateMesh(bakeCamera);
    }
}

最后看各个特效的ui渲染组件UIParticleRenderer:

        /// <summary>
        /// Perform material modification in this function.
        /// </summary>
        public override Material GetModifiedMaterial(Material baseMaterial)
        {
            _currentMaterialForRendering = null;

            if (!IsActive() || !_parent)
            {
                ModifiedMaterial.Remove(_modifiedMaterial);
                _modifiedMaterial = null;
                return baseMaterial;
            }

            var modifiedMaterial = base.GetModifiedMaterial(baseMaterial);

            var texture = mainTexture;
            if (texture == null && _parent.m_AnimatableProperties.Length == 0)
            {
                ModifiedMaterial.Remove(_modifiedMaterial);
                _modifiedMaterial = null;
                return modifiedMaterial;
            }

            var id = _parent.m_AnimatableProperties.Length == 0 ? 0 : GetInstanceID();
#if UNITY_EDITOR
            var props = EditorJsonUtility.ToJson(modifiedMaterial).GetHashCode();
#else
            var props = 0;
#endif
            //有变化的属性时,需要同步渲染参数到材质球
            modifiedMaterial = ModifiedMaterial.Add(modifiedMaterial, texture, id, props);
            ModifiedMaterial.Remove(_modifiedMaterial);
            _modifiedMaterial = modifiedMaterial;

            return modifiedMaterial;
        }

        public void Set(UIParticle parent, ParticleSystem ps, bool isTrail)
        {
            _parent = parent;
            maskable = parent.maskable;

            gameObject.layer = parent.gameObject.layer;

            _particleSystem = ps;
            _preWarm = _particleSystem.main.prewarm;

#if UNITY_EDITOR
            if (Application.isPlaying)
#endif
            {
                if (_particleSystem.isPlaying || _preWarm)
                {
                    _particleSystem.Clear();
                    _particleSystem.Pause();
                }
            }

            ps.TryGetComponent(out _renderer);
            _renderer.enabled = false;

            //_emitter = emitter;
            _isTrail = isTrail;

            _renderer.GetSharedMaterials(s_Materials);
            material = s_Materials[isTrail ? 1 : 0];
            s_Materials.Clear();

            // Support sprite.
            var tsa = ps.textureSheetAnimation;
            if (tsa.mode == ParticleSystemAnimationMode.Sprites && tsa.uvChannelMask == 0)
            {
                tsa.uvChannelMask = UVChannelFlags.UV0;
            }

            _prevScale = GetWorldScale();
            _prevPsPos = _particleSystem.transform.position;
            _prevScreenSize = new Vector2Int(Screen.width, Screen.height);
            _prevCanvasScale = canvas ? canvas.scaleFactor : 1f;
            _delay = true;

            canvasRenderer.SetTexture(null);

            enabled = true;
        }

        public void UpdateMesh(Camera bakeCamera)
        {
            // No particle to render: Clear mesh.
            if (
                !isActiveAndEnabled || !_particleSystem || !_parent
                || !canvasRenderer || !canvas || !bakeCamera
                || _parent.meshSharing == UIParticle.MeshSharing.Replica
                || !transform.lossyScale.GetScaled(_parent.scale3DForCalc).IsVisible() // Scale is not visible.
                || (!_particleSystem.IsAlive() && !_particleSystem.isPlaying) // No particle.
                || (_isTrail && !_particleSystem.trails.enabled) // Trail, but it is not enabled.
#if UNITY_2018_3_OR_NEWER
                || canvasRenderer.GetInheritedAlpha() <
                0.01f // #102: Do not bake particle system to mesh when the alpha is zero.
#endif
            )
            {
                //不需要渲染特效时,清掉mesh
                Profiler.BeginSample("[UIParticleRenderer] Clear Mesh");
                workerMesh.Clear();
                canvasRenderer.SetMesh(workerMesh);
                _lastBounds = new Bounds();
                Profiler.EndSample();

                return;
            }

            var main = _particleSystem.main;
            var scale = GetWorldScale();
            var psPos = _particleSystem.transform.position;

            // Simulate particles.
            Profiler.BeginSample("[UIParticle] Bake Mesh > Simulate Particles");
            if (!_isTrail && _parent.canSimulate)
            {
#if UNITY_EDITOR
                if (!Application.isPlaying)
                {
                    SimulateForEditor(psPos - _prevPsPos, scale);
                }
                else
#endif
                {
                    ResolveResolutionChange(psPos, scale);
                    Simulate(scale, _parent.isPaused || _delay);

                    if (_delay && !_parent.isPaused)
                    {
                        Simulate(scale, _parent.isPaused);
                    }

                    // When the ParticleSystem simulation is complete, stop it.
                    if (!main.loop
                        && main.duration <= _particleSystem.time
                        && (_particleSystem.IsAlive() || _particleSystem.particleCount == 0)
                       )
                    {
                        _particleSystem.Stop(false);
                    }
                }

                _prevScale = scale;
                _prevPsPos = psPos;
                _delay = false;
            }

            Profiler.EndSample();

            // Bake mesh.
            Profiler.BeginSample("[UIParticleRenderer] Bake Mesh");
            s_CombineInstances[0].mesh.Clear(false);

            // Assertion failed on expression: 'ps->array_size()' #278
            var extends = s_CombineInstances[0].mesh.bounds.extents.x;
            if (!float.IsNaN(extends) && !float.IsInfinity(extends) && 0 < extends)
            {
                //mesh清掉了,但是bounds不会自动清掉
                s_CombineInstances[0].mesh.RecalculateBounds();
            }

            if (_isTrail && _parent.canSimulate && 0 < _particleSystem.particleCount)
            {
#if PS_BAKE_API_V2
                _renderer.BakeTrailsMesh(s_CombineInstances[0].mesh, bakeCamera,
                    ParticleSystemBakeMeshOptions.BakeRotationAndScale);
#else
                _renderer.BakeTrailsMesh(s_CombineInstances[0].mesh, bakeCamera, true);
#endif
            }
            else if (!_isTrail && _renderer.CanBakeMesh())
            {
                _particleSystem.ValidateShape();
#if PS_BAKE_API_V2
                _renderer.BakeMesh(s_CombineInstances[0].mesh, bakeCamera,
                    ParticleSystemBakeMeshOptions.BakeRotationAndScale);    //不包含世界位置,为局部的
#else
                _renderer.BakeMesh(s_CombineInstances[0].mesh, bakeCamera, true);
#endif
            }

            // Too many vertices to render.
            if (65535 <= s_CombineInstances[0].mesh.vertexCount)
            {
                Debug.LogErrorFormat(this,
                    "Too many vertices to render. index={0}, isTrail={1}, vertexCount={2}(>=65535)",
                    _index,
                    _isTrail,
                    s_CombineInstances[0].mesh.vertexCount
                );
                s_CombineInstances[0].mesh.Clear(false);
            }

            Profiler.EndSample();

            // Combine mesh to transform. ([ParticleSystem local ->] world -> renderer local)
            Profiler.BeginSample("[UIParticleRenderer] Combine Mesh");
            if (_parent.canSimulate)
            {
                if (_parent.positionMode == UIParticle.PositionMode.Absolute)
                {
                    s_CombineInstances[0].transform =
                        canvasRenderer.transform.worldToLocalMatrix
                        * GetWorldMatrix(psPos, scale);     // 世界到局部 * 特效局部到世界
                }
                else
                {
                    var diff = _particleSystem.transform.position - _parent.transform.position;
                    s_CombineInstances[0].transform =
                        canvasRenderer.transform.worldToLocalMatrix
                        * Matrix4x4.Translate(diff.GetScaled(scale - Vector3.one))
                        * GetWorldMatrix(psPos, scale);
                }

                workerMesh.CombineMeshes(s_CombineInstances, true, true);

                workerMesh.RecalculateBounds();
                var bounds = workerMesh.bounds;
                //去掉z轴值
                var center = bounds.center;
                center.z = 0;
                bounds.center = center;
                var extents = bounds.extents;
                extents.z = 0;
                bounds.extents = extents;
                workerMesh.bounds = bounds;
                _lastBounds = bounds;

                // Convert linear color to gamma color.
                if (QualitySettings.activeColorSpace == ColorSpace.Linear)
                {
                    //ui渲染默认是Gamma空间?
                    Profiler.BeginSample("[UIParticleRenderer] Convert Linear to Gamma");
                    workerMesh.GetColors(s_Colors);
                    var count_c = s_Colors.Count;
                    for (var i = 0; i < count_c; i++)
                    {
                        var c = s_Colors[i];
                        c.r = c.r.LinearToGamma();
                        c.g = c.g.LinearToGamma();
                        c.b = c.b.LinearToGamma();
                        s_Colors[i] = c;
                    }

                    workerMesh.SetColors(s_Colors);
                    Profiler.EndSample();
                }

                GetComponents(typeof(IMeshModifier), s_Components);
                for (var i = 0; i < s_Components.Count; i++)
                {
#pragma warning disable CS0618 // Type or member is obsolete
                    ((IMeshModifier)s_Components[i]).ModifyMesh(workerMesh);
#pragma warning restore CS0618 // Type or member is obsolete
                }

                s_Components.Clear();
            }

            Profiler.EndSample();


            // Get grouped renderers.
            s_Renderers.Clear();
            if (_parent.useMeshSharing)
            {
                //获取同一组的Renderers,渲染相同特效时,每个特效上都得有个独立的UIParticle, 这样_index才对应
                UIParticleUpdater.GetGroupedRenderers(_parent.groupId, _index, s_Renderers);
            }

            // Set mesh to the CanvasRenderer.
            Profiler.BeginSample("[UIParticleRenderer] Set Mesh");
            for (var i = 0; i < s_Renderers.Count; i++)
            {
                if (s_Renderers[i] == this) continue;
                //共享mesh
                s_Renderers[i].canvasRenderer.SetMesh(workerMesh);
                s_Renderers[i]._lastBounds = _lastBounds;
            }

            if (!_parent.canRender)
            {
                workerMesh.Clear();
            }

            //ui渲染的mesh
            canvasRenderer.SetMesh(workerMesh);
            Profiler.EndSample();

            // Update animatable material properties.
            Profiler.BeginSample("[UIParticleRenderer] Update Animatable Material Properties");

#if UNITY_EDITOR
            if (_modifiedMaterial != material)
            {
                //编辑器显示用了的材质球
                _renderer.GetSharedMaterials(s_Materials);
                material = s_Materials[_isTrail ? 1 : 0];
                s_Materials.Clear();
                SetMaterialDirty();
            }
#endif
            //将粒子渲染器的材质球属性同步到当前使用的材质球
            UpdateMaterialProperties();
            if (_parent.useMeshSharing)
            {
                if (!_currentMaterialForRendering)
                {
                    _currentMaterialForRendering = materialForRendering;
                }

                //将渲染使用的材质球同步到同组的其他特效渲染器
                for (var i = 0; i < s_Renderers.Count; i++)
                {
                    if (s_Renderers[i] == this) continue;

                    s_Renderers[i].canvasRenderer.materialCount = 1;
                    s_Renderers[i].canvasRenderer.SetMaterial(_currentMaterialForRendering, 0);
                }
            }

            Profiler.EndSample();

            s_Renderers.Clear();
        }

        private void Simulate(Vector3 scale, bool paused)
        {
            var main = _particleSystem.main;
            var deltaTime = paused
                ? 0
                : main.useUnscaledTime
                    ? Time.unscaledDeltaTime
                    : Time.deltaTime;

            // Pre-warm:
            if (0 < deltaTime && _preWarm)
            {
                deltaTime += main.duration;
                _preWarm = false;
            }

            // get world position.
            var isLocalSpace = _particleSystem.IsLocalSpace();
            var psTransform = _particleSystem.transform;
            var originLocalPosition = psTransform.localPosition;
            var originLocalRotation = psTransform.localRotation;
            var originWorldPosition = psTransform.position;
            var originWorldRotation = psTransform.rotation;
            var emission = _particleSystem.emission;
            var rateOverDistance = emission.enabled
                                   && 0 < emission.rateOverDistance.constant
                                   && 0 < emission.rateOverDistanceMultiplier;
            if (rateOverDistance && !paused && _isPrevStored)
            {
                // (For rate-over-distance emission,) Move to previous scaled position, simulate (delta = 0).
                var prevScaledPos = isLocalSpace
                    ? _prevPsPos
                    : _prevPsPos.GetScaled(_prevScale.Inverse());
                psTransform.SetPositionAndRotation(prevScaledPos, originWorldRotation);
                _particleSystem.Simulate(0, false, false, false);
            }

            // Move to scaled position, simulate, revert to origin position.
            var scaledPos = isLocalSpace
                ? originWorldPosition
                : originWorldPosition.GetScaled(scale.Inverse());
            psTransform.SetPositionAndRotation(scaledPos, originWorldRotation);
            //驱动特效
            _particleSystem.Simulate(deltaTime, false, false, false);
            psTransform.localPosition = originLocalPosition;
            psTransform.localRotation = originLocalRotation;
        }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值