UGUI的ScrollRect可以正确的裁剪包括Image,Text等UGUI自身的组件,但是不能正确裁剪ParticleSystem粒子,这给客户端和美术人员造成了很大麻烦,今天我们就通过修改shader的方式,解决裁剪问题
环境搭建
- 创建新的场景,添加ScrollView,并将子对象Viewport的Mask替换成Rect Mask 2D(重要)
- 创建UI摄像机,修改Projection为Orthographic,Culling Mask仅选择UI
- 修改Canvas的RenderMode为Screen-Space Camera,并绑定2中创建的摄像机
- 在ScrollView的content下添加一个ParticleSystem
- 运行
可以看到此时,粒子从ScrollView的边缘穿透出去
修改Unity原生particle shader
- 下载Unity对应版本shader代码(如果链接失效,可以直接去Unity官网找下载),我使用的是unity5.5.1版本
- 打开并定位到Particle Add.shader文件,复制一份命名为UIParticleAdditiveClip.shader保存到Asset文件夹中
- 修改UIParticleAdditiveClip.shader中的shader名称为“Custom/UI/Particle_Additive_Clip”
- 创建一个Material命名为pt,并使用3中创建的shader,选择任意一张图片作为粒子的图片,用pt替换ParticleSystem的默认材质
这时运行程序,还看不到任何效果,因为我们的shader中缺少裁剪区域,也就是说,我们需要告诉GPU,哪个区域中的粒子需要显示,区域外的粒子不需要显示,为此,我们在shader中添加变量
float4 _ClipRect;
该变量用来保存2D裁剪框的左下角和右上角2个点,共4个float值。为了简化计算,我们设定这个变量中保存的是world space下的坐标,并且限定摄像机的平行是椎体是朝向Z轴正方向的
接下来修改我们需要在vertex shader中计算粒子的世界坐标,由于我们需要的仅仅是world space中顶点的xy坐标,所以我将这两个坐标保存到了uv坐标的zw中,代码如下:
... // UnityUI.cginc中包含了UnityGet2DClipping的实现 #include "UnityUI.cginc" ... struct v2f { ... // 将texcoord扩展为float4,会使shader代码编译错误,这时只要将错误的位置改为texcoord.xy即可 float4 texcoord : TEXCOORD0; ... }; ... v2f vert(appdata_t v) { ... // 原有uv坐标保存到texcoord.xy中,world space中的xy坐标保存到o.texcoord.zw中 o.texcoord.xy = TRANSFORM_TEX(v.texcoord.xy,_MainTex).xy; o.texcoord.zw = mul(unity_ObjectToWorld, v.vertex).xy; ... return o; } // C#代码需要传入的裁剪区域变量, 这里我们增加一个变量(_UseClipRect)用来标记是否需要裁剪 float4 _ClipRect; float _UseClipRect; ... fixed4 frag(v2f i) : SV_Target { ... // fragment shader非常简单,我们只需要在最后,对fragment进行裁剪即可, // UnityGet2DClipping这个函数实现了判断2D空间中的一点是否在一个矩形区域中 // lerp函数用来标记是否需要进行裁剪,当_UseClipRect值为1时表示裁剪 float c = UnityGet2DClipping(i.texcoord.zw, _ClipRect); col.a = lerp(col.a, c * col.a, _UseClipRect); return col; }
编写C#代码,计算裁剪区域
[RequireComponent(typeof(ParticleSystem))] public class ParticleMask : MonoBehaviour { public RectMask2D mask; public Material mt; private void Awake() { mt = GetComponent<ParticleSystem>().GetComponent<Renderer>().material; mask = GetComponentInParent<RectMask2D>(); // ScrollView位置变化时重新计算裁剪区域 GetComponentInParent<ScrollRect>().onValueChanged.AddListener((e) => { setClip(); }); setClip(); } void setClip() { Vector3[] wc = new Vector3[4]; mask.GetComponent<RectTransform>().GetWorldCorners(wc); // 计算world space中的点坐标 var clipRect = new Vector4(wc[0].x, wc[0].y, wc[2].x, wc[2].y);// 选取左下角和右上角 mt.SetVector("_ClipRect", clipRect); // 设置裁剪区域 mt.SetFloat("_UseClipRect", 1.0f); // 开启裁剪 } }
完成代码后,将这个代码绑定到ParticleSystem上即可
最终效果
可以看到粒子被正确裁剪了
后记
- 一般情况下,美术制作的粒子特效会包含多个ParticleSystem,这时就需要获取所有ParticleSystem的materil分别设置裁剪区域和裁剪开关
- 这种裁剪方法有一定局限性,需要修改UI的原生shader,并限定美术的shader使用,一般情况下,仅适用于界面上的小特效,比如选中,技能高亮等
- Unity中右键创建的ScrollView的Mask不是Rect Mask 2D,需要手动进行修改