前言
就普遍性而言,粒子系统常用在一闪而过的效果,来达到一种烘托氛围、增强玩家体验的目的,比如胜利撒个花,胜利过关放个烟花,等等。但这次就遇到了不普遍的:它出现在了列表中,并且会随着列表一起滑动,当超过Scroll Rect的Viewport范围时,它并不会像列表中其他UI元素一样,自觉消失于视野之中,而是以一种让人意想不到的方式,继续发着光、闪着亮。
1. 原因 首先需要知道的是,Scroll Rect中能够自动裁剪超过视窗的元素,是通过挂在Viewport上的Mask组件实现的,与之类似的还有RectMask,等等。原理很好理解,通过Mask组件来限定一个范围,超过这个范围的便不绘制,利用GPU的Stencil Buffer对Canvas Renderer绘制的UI进行裁剪。
而粒子系统和这些UI元素不在同一个渲染管线,用的是ParticleSystemRenderer,Mask的裁剪影响不到它,所以在超出Mask的范围后,粒子依然会渲染。
2. 如何裁剪 这里只是说一种方式,当然还有其他方式,有想法可以多研究研究。
这里是参考Mask的裁剪方式,给粒子的渲染也加一个区域限制,区域内的渲染,超出区域范围的放弃渲染,以达到一个裁剪的效果。
看过Shader的都知道,Pass里主要就是2个函数,一个vert函数用来计算顶点坐标,还有一个frag函数用来计算顶点的颜色,思路就是,给Shader增加一个区域参数,在得到顶点坐标后,判断是否在目标区域内,如果在其中,则照常绘制渲染,若不在,则放弃绘制,将颜色设置为全透明即可。
这里的目标区域,用2个坐标表示,即Mask的左下角和右上角的坐标,当然也可以用左上角和右下角的坐标,效果一样,最终都会得到4个数:
1 2 3 4 _MinX, _MinY, _MaxX, _MaxY,
这4个是Shader的属性,需要在程序运行后,把具体的值传递进去。
3. 实现Shader 我们这里特效大致有这么几类:发光、烟花、彩带,拿一个常用的Shader举例:Additive。在Inspector中很容易就可以找到Shader的源码,复制一份出来,记得要改一下顶部Shader的路径,然后就可以着手修改了。
3.1 增加属性 这一步是为了可以在代码中传递限定区域的坐标值
1 2 3 4 5 6 7 Properties { _MinX ("Min X", Float) = 0 _MaxX ("Max X", Float) = 0 _MinY ("Min Y", Float) = 0 _MaxY ("Max Y", Float) = 0 }
3.2 增加变量 增加同名变量,以便于可以在frag函数中使用
1 2 3 4 5 6 7 Pass { float _MinX; float _MaxX; float _MinY; float _MaxY; }
3.3 裁剪 为了便于计算,坐标空间需要统一,最省事的就是都是用世界坐标,代码中传递过来的就是转换之后的坐标,这样的即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 v2f vert(appdata_t v) { v2f o; // 省略了原有代码 o.pos = mul(unity_ObjectToWorld, v.vertex); return o; } fixed4 farg(v2f i) : SV_Target { // 省略了原有代码 float clipX1 = step(_MinX, i.pos.x); float clipX2 = step(i.pos.x, _MaxX); float clipY1 = step(_MinY, i.pos.y); float clipY2 = step(i.pos.y, _MaxY); float rectClip = clipX1 * clipX2 * clipY1 * clipY2; col.a *= rectClip; return col; }
其中,float c = step(a, b)是Shader内置的API函数,当b大于a时,返回0,剩下返回1,对4个值分布判断,只有都在区域内时,最后的rectClip才为1,否则为0,最后返回的顶点颜色也就成了全透明,视觉上就是被裁剪了。Shader中少用if,多用自带的api,由于GPU的计算方式不同于CPU,if这种是个高消耗的计算操作。
4. 实现MonoBehavior 脚本中做的事也很简单,先找到Mask获取限定区域的坐标,然后再找到Material,把坐标传递过去即可。由于项目有别,结构也是多种多样,获取这两个对象的方式自然不可同一而论,这里说一种最直接的方式:直接赋值,没有更直接的了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using UnityEngine;using UnityEngine.UI;public class ClipParticleSystem : MonoBehaviour { public Mask mask; public Material[] materials; private void LateUpdate ( ) { Clip(); } private void Clip ( ) { var corner = new Vector3[4 ]; var rect = (RectTransform) mask.transform; rect.GetWorldCorners(corner); foreach (var mat in materials) { mat.SetFloat("_MinX" , corner[0 ].x); mat.SetFloat("_MinY" , corner[0 ].y); mat.SetFloat("_MaxX" , corner[2 ].x); mat.SetFloat("_MaxY" , corner[2 ].y); } } }
其中,Mask是Scorll Rect视窗的Mask,Material则是需要裁剪的粒子效果中所有的材质。当然,这样写很粗躁,主要是为了说明方式主干,其中还有不少可以优化的地方,比如:
缓存Mask的RectTransform,不必每一帧都获取
重复使用Vector3数组,不必每次都要new一个
获取材质属性的id,通过id传递值
判断坐标变化,若无变化则跳过这一帧
至于,获取Mask和Material的方式,就要因地制宜了,也可以通过GetComponent在代码中获取,因为节点结构的不同,这个就视情况而定,各显神通就好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 using System;using UnityEngine;using UnityEngine.UI;public class ClipParticleSystem : MonoBehaviour { public Mask mask; public Material[] materials; private RectTransform _rect; private Vector3[] _cor; private Vector4 _lastCor; private static readonly int MinXId = Shader.PropertyToID("_MinX" ); private static readonly int MinYId = Shader.PropertyToID("_MinY" ); private static readonly int MaxXId = Shader.PropertyToID("_MaxX" ); private static readonly int MaxYId = Shader.PropertyToID("_MaxY" ); private void Start ( ) { _rect = mask.transform.GetComponent<RectTransform>(); _cor = new Vector3[4 ]; } private void LateUpdate ( ) { Clip(); } private void Clip ( ) { var rect = (RectTransform)mask.transform; rect.GetWorldCorners(_cor); if (!Mathf.Approximately(_lastCor.x, _cor[0 ].x) || !Mathf.Approximately(_lastCor.y, _cor[0 ].y) || !Mathf.Approximately(_lastCor.z, _cor[2 ].x) || !Mathf.Approximately(_lastCor.w, _cor[2 ].y)) { _lastCor.x = _cor[0 ].x; _lastCor.y = _cor[0 ].y; _lastCor.z = _cor[2 ].x; _lastCor.w = _cor[2 ].y; foreach (var mat in materials) { mat.SetFloat(MinXId, _cor[0 ].x); mat.SetFloat(MinYId, _cor[0 ].y); mat.SetFloat(MaxXId, _cor[2 ].x); mat.SetFloat(MaxYId, _cor[2 ].y); } } } }