UGUI - 解决粒子特效无法被遮罩遮住问题

今天UX要我给滚动列表上的item加上粒子特效,想着没问题啊。直接把特效挂在item上,但没有考虑到particle system的canvas order问题,导致出现了例子特效出现在窗口上方,特效并不能被mask遮盖掉的问题。

额外做了个简单的demo,scrollview做窗口

方案一:用图片遮盖特效

一开始想到用最简单最naive的方法是在窗口上下加个带canvas的图片, 通过调高它的层级把特效遮住,有点打补丁的意思。但!如果在多分辨率的屏幕适配的情况下,这个方法及其有可能被发现有问题,还是要谨慎使用哈

在底下加了个白色的图片,可以看出特效已经被遮盖住了

方案二:修改shader

不管窗口大小如何变都可以完美的遮住超出来的粒子特效,我们可以通过改shader的方式解决:shader传scrollview的四个顶点的世界坐标,shader判断特效在框内的话则显示,反之则隐藏。

Step 1: 我的粒子特效挂的是unity内置的shader particle.addtive.shader, 先从unity官网上下载其源代码,注意用旧版本的比较好改,新版本的简化了很多,路径是builtin_shaders/DefaultResourcesExtra/Particle Add.shader. 把shader复制出一份并重命名Addtive.shader,修改的部分在注释上标出:

// Additive Particle shader that can be hidden if it is not in the canvas
Shader "Particle/Additive" {
Properties {
    _TintColor ("Tint Color", Color) = (0.5,0.5,0.5,0.5)
    _MainTex ("Particle Texture", 2D) = "white" {}
    _InvFade ("Soft Particles Factor", Range(0.01,3.0)) = 1.0
    // Record the value of the border
    _Area ("Area", Vector) = (0,0,1,1)
    // 1 means clip on, 0 means clip off
    _IsClip ("IsClip", Int) = 0
}

Category {
    Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
    Blend SrcAlpha One
    ColorMask RGB
    Cull Off Lighting Off ZWrite Off

    SubShader {
        Pass {

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile_particles
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            fixed4 _TintColor;

            float4 _Area;
            int _IsClip;

            struct appdata_t {
                float4 vertex : POSITION;
                fixed4 color : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float4 vertex : SV_POSITION;
                fixed4 color : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                #ifdef SOFTPARTICLES_ON
                float4 projPos : TEXCOORD2;
                #endif
                float2 worldPos : TEXCOORD3;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            float4 _MainTex_ST;

            v2f vert (appdata_t v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.vertex = UnityObjectToClipPos(v.vertex);
                #ifdef SOFTPARTICLES_ON
                o.projPos = ComputeScreenPos (o.vertex);
                COMPUTE_EYEDEPTH(o.projPos.z);
                #endif
                o.color = v.color;
                o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xy;
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
            float _InvFade;

            fixed4 frag (v2f i) : SV_Target
            {
                #ifdef SOFTPARTICLES_ON
                float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
                float partZ = i.projPos.z;
                float fade = saturate (_InvFade * (sceneZ-partZ));
                i.color.a *= fade;
                #endif

                fixed4 col = 2.0f * i.color * _TintColor * tex2D(_MainTex, i.texcoord);
                col.a = saturate(col.a); // alpha should not have double-brightness applied to it, but we can't fix that legacy behavior without breaking everyone's effects, so instead clamp the output to get sensible HDR behavior (case 967476)

                UNITY_APPLY_FOG_COLOR(i.fogCoord, col, fixed4(0,0,0,0)); // fog towards black due to our blend mode

                // Check whether the vertex coordinates are in the clipping frame
                bool inArea = i.worldPos.x >= _Area.x && i.worldPos.x <= _Area.z && i.worldPos.y >= _Area.y && i.worldPos.y <= _Area.w;
                // If its position is in the clipping frame or the effect of cliping is off, return the original effect, otherwise it will be hidden
                return (!inArea && _IsClip == 1) ? fixed4(0, 0, 0, 0) : col;
            }
            ENDCG
        }
    }
}
}

Step2: 先将将UI中的粒子特效的shader都改成修改过后的Addtive.shader,再写一个用来计算裁剪框的四个顶点的世界坐标并传到shader里的脚本VFXClip.cs,把其挂到相对应的特效上:

using System.Collections.Generic;
using UnityEngine;

namespace Demo
{
    public class VFXClip : MonoBehaviour
    {
        private RectTransform rectTrans; // Mask transform
        private List<Material> materialList = new List<Material>();
        private Transform canvas;
        private float halfWidth;
        private float halfHeight;
        private float canvasScale;

        void Awake()
        {
            var mask  = this.transform.GetComponentInParent<UnityEngine.UI.Mask>();
            if (mask != null)
            {
                this.rectTrans = mask.gameObject.GetComponent<RectTransform>();
            }
        }
        void Start()
        {
            if (this.rectTrans == null)
            {
                return;
            }

            this.canvas = this.transform.GetComponentInParent<Canvas>().transform;

            var renders = this.transform.GetComponentsInChildren<ParticleSystemRenderer>();
            for (int i = 0, j = renders.Length; i < j; i++)
            {
                var render = renders[i];
                var mat = render.material;
                this.materialList.Add(mat);
            }

            this.canvasScale = this.canvas.localScale.x;
            this.halfWidth = this.rectTrans.rect.width * 0.5f * this.canvasScale;
            this.halfHeight = this.rectTrans.rect.height * 0.5f * this.canvasScale;

            Vector4 area = this.CalculateArea(this.rectTrans.position);
            for (int i = 0, len = this.materialList.Count; i < len; i++)
            {
                this.materialList[i].SetInt("_IsClip", 1);
                this.materialList[i].SetVector("_Area", area);
            }
        }

        private Vector4 CalculateArea(Vector3 position)
        {
            return new Vector4()
            {
                x = position.x - this.halfWidth,
                y = position.y - this.halfHeight,
                z = position.x + this.halfWidth,
                w = position.y + this.halfHeight
            };
        }
    }
}

N.B. 要注意计算四个顶点的时候,viewport的pivot值会影响到中心点的位置, CalcaulateArea这个函数根据我scrollview所用的pivot来算

最终效果图:

 

posted @ 2021-09-23 17:39  cancantrbl  阅读(2916)  评论(0编辑  收藏  举报