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来算
最终效果图: