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,需要手動進行修改