Unity特效在ScrollView中的裁剪
在项目中碰到裁剪UI这类问题,主要是使用ScrollView裁剪后对特效不起作用。下面就给大家介绍下unity特效在ScrollView中出现裁剪问题的解决办法,想知道的可以看一看。
问题的出现
从遇到这个问题的需求说起吧。
为了能够让玩家在可领奖的时候,有个更明显的提示和炫酷的效果,要在领奖按钮上加一个特效。一个特效拖上去,调整好大小,好像ok了。但是,这个奖励列表是可滑动的,一拖动问题就出现了:
UIPanel是和ScrollView结合使用的,可以裁剪UI,但对特效是不起作用的。
现在要解决的就是特效在ScrollView中的裁剪问题(准确说是在UIPanel中的裁剪,标题和内容说ScrollView是为了突出它的应用场景)。
以下是考虑和实践过的几个解决方案,分享给大家。
解决方案一:帧动画
按照特效的效果,做帧动画,这样播放帧动画的是UI元素就可以被正常裁剪了。
问题和缺点:
1. 帧动画跟原来的特效相比,效果打了折扣
解决方案二:添加摄像机
添加一个Camera,这个Canera只显示ScrollView的实际显示区域。
Camera中Culling Mask中可以设置其“摄像”的Layer层,我们先添加一个新的Layer层RewardEffect,并将新添加的Camera的显示层设置为RewardEffect。
Camera中的Viewport Rect是其“摄像”的区域,这里我们要调整为这个区域与ScrollView的显示区域吻合。
特效的Layer要设置为RewardEffect,特效不再会出现在UICamera中,而是由新的Camera负责显示。由于“摄像”区域的设置,特效在区域内的部分会正常显示,拖动超出“摄像”范围后,就看不到了,实现了裁剪的效果。
如何比较方便准确的设置摄像范围,有一个脚本UIViewport,将这个脚本挂在新加的摄像机上。在ScrollView的左上角和右下角位置各放一个Transform,设置到UIViewport。
问题和缺点:
1. 步骤较繁琐,开发和维护成本较高;
2. 新的摄像机下的特效,与原来在UI摄像机下相比,位置和大小有一些偏移,需要手动调整到吻合。如果按钮点击有缩小的效果的话,缩小时特效与按钮表现有些偏差;
3. 特效所属的Layer层级要高于UI的的Layer层,当这个界面弹出另一个界面(比如奖励领取界面),特效就显示在新界面之上了(可选择在新界面出现的时候监听暂时隐藏特效,但解决方案不够完美);
4. 会因为一些未知的原因会导致特效的Layer层从RewardEffect层变为UI,目前确定的一个会导致此问题出现的操作:使用UIGrid,调用UIGrid.RemoveAllChildren(),这个调用方法是有问题的,没有处理UIGrid中的List,目前看不会表现出其他问题,但会引起刚才说的那个问题。还有几次偶现的Layer层改变,未定位到原因。
解决方案三:使用RenderTexture
也可以通过代码来实现,这里选取了部分关键代码供参考:
1.创建相机。相机提前做好prefab,然后在使用的时候动态创建。
1 2 3 4 5 6 7 8 9 | GameObject camera = (GameObject)Resources.Load( @"UI/Prefabs/Camera/RoleCameraVariateInfo" , typeof (GameObject)); if (camera != null ) { GameObject go = GameObject.Instantiate(camera) as GameObject; if (go != null && go.camera != null ) { m_Camera = go.camera; } } |
2.加载特效文件,并设置特效及相机位置
1 2 3 4 5 6 7 8 9 10 11 | Object model = Resources.Load( "Effect/UI/Business/Busi_Effect001Test" ); if ( null == model) return ; m_Model = GameObject.Instantiate(model) as GameObject; if ( null == m_Model) return ; var t = m_Model.transform; t.localPosition = new Vector3(0f, m_nIndex * MODEL_OFFSET, 0f); t.localScale = modelScale > 0 ? new Vector3(modelScale, modelScale, modelScale) : new Vector3(1f, 1f, 1f); m_Camera.transform.localPosition = new Vector3(cameraX, cameraY + m_nIndex * MODEL_OFFSET, cameraZ); |
3.给相机设置targetTexture
1 2 3 4 5 6 7 8 9 | m_Camera.clearFlags = CameraClearFlags.Color; m_Camera.renderingPath = RenderingPath.DeferredLighting; RenderTexture rt = RenderTexture.GetTemporary(uiTexture.width, uiTexture.height, 0); if (rt != null ) { m_Camera.targetTexture = rt; m_Camera.mainTexture = rt; m_Camera.Render(); } |
问题和缺点:
- 当特效种类较多,不同应用场景多的时候,开发成本偏高;
- 粒子特效显示异常,贴图显示带有方块
解决方案四:通过Shader实现裁剪
将ScrollView的显示区域传给特效的Shader,超出显示区域部分将透明度设置为0,实现裁剪。这个方案实施分为两个部分:修改特效的Shader和显示区域传给Shader。
1.修改特效的Shader,下面是我们修改后的Shader,其中///////add clip的部分是为了实现裁剪添加的部分:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | Shader "Effect_Mid/ScrollClipAdditive" { Properties { ...... ///////add clip _MinX ( "Min X" , Float) = -10 _MaxX ( "Max X" , Float) = 10 _MinY ( "Min Y" , Float) = -10 _MaxY ( "Max Y" , Float) = 10 ///////add clip } SubShader { Tags { ...... } Pass { ...... // ZTest Off Fog { Color (0,0,0,0) } ...... ///////add clip float _MinX; float _MaxX; float _MinY; float _MaxY; ///////add clip ...... struct VS_OUTPUT { ...... ///////add clip float3 vpos : TEXCOORD2; ///////add clip }; VS_OUTPUT vert(VS_INPUT In) { ...... ///////add clip Out.vpos = mul(UNITY_MATRIX_MVP, In.position); ///////add clip ...... return Out; } float4 frag(VS_OUTPUT In) : COLOR { ...... ///////add clip color.a *= (In.vpos.x >= _MinX ); color.a *= (In.vpos.x <= _MaxX); color.a *= (In.vpos.y >= _MinY ); color.a *= (In.vpos.y <= _MaxY); ///////add clip return color; } ENDCG } } Fallback off CustomEditor "EffectMaterialEd" } 需要裁剪的特效属于少数,没必要所有特效的Shader都添加额外计算部分,建议复制原来的Shader来修改。 |
脚本代码如下,添加了比较详细的注释:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | /* * @file : EffectClipImpl.cs * @brief : 在scrollView中对特效进行裁剪 * @details : 该脚本使用在特效上,如果有多个特效放在一个共同的结点,可挂在这些特效的共同父节点上 * @author : jaredszhang * @date : 2016-01-06 */ using UnityEngine; using System.Collections; namespace NB { public class EffectClipImpl : MonoBehaviour { //要实现特效参见的UIPanel(UIPanel和ScrollView是结合使用的,真正管理裁剪区域的是UIPanel) public UIPanel m_RootPanel = null ; //默认值,写了一个很大的数值,保证不设置Panel时可以正常显示 float m_MinX = -10; float m_MinY = -10; float m_MaxX = 10; float m_MaxY = 10; void Start() { Reset(); } public void Reset() { if (m_RootPanel != null ) { //获取UI摄像机,为了获取panel相对于屏幕中心点的位置 GameObject UIRootCamera = GameObject.Find( "UI/UIRoot(Clone)/UICamera(Clone)" ); if (UIRootCamera != null ) { //获取panel相对于屏幕中心点的位置,准确说是获取ScrollView的实际显示区域相对于屏幕中心点的位置 Vector3 toCameraPos = UIRootCamera.transform.InverseTransformPoint(m_RootPanel.transform.position); //处理UIPanel的Center偏移 toCameraPos += new Vector3(m_RootPanel.baseClipRegion.x, m_RootPanel.baseClipRegion.y, 0); //去除滑动的偏移量 //添加特效的时候ScrollView可能已经滑动过了,这时候ScrollView的坐标位置发生了变化 //我们根据ScrollView的坐标位置计算显示区域的位置,要计算这部分滑动的量 toCameraPos += new Vector3(m_RootPanel.clipOffset.x, m_RootPanel.clipOffset.y, 0); //获取Panel实际显示区域的大小 Vector2 viewSize = m_RootPanel.GetViewSize(); //过渡部分处理,panel的过渡部分透明度会渐变消失 //我们项目使用的Softness基本都是4,比较小,这里处理为Softness的一半区域后消失 //虽然方案不完美,看上去还可以(有需要的话可以把Softness的值也传给Shader,做过渡消失的处理) Vector2 clipSoftness = m_RootPanel.clipSoftness; Vector2 clipSize = viewSize - clipSoftness * 0.5f; //获取窗口大小 Vector2 screenViewSize = m_RootPanel.GetWindowSize(); float width = screenViewSize.x; float height = screenViewSize.y; //计算要传送给Shder的四个值,最小X,最大X,最小Y,最大Y //这个值的大小是相对于屏幕常和宽的比例值 m_MinX = -(( float )(clipSize.x * 0.5f - toCameraPos.x)) / (width * 0.5f); m_MinY = -(( float )(clipSize.y * 0.5f - toCameraPos.y)) / (height * 0.5f); m_MaxX = (( float )(clipSize.x * 0.5f + toCameraPos.x)) / (width * 0.5f); m_MaxY = (( float )(clipSize.y * 0.5f + toCameraPos.y)) / (height * 0.5f); //传输范围数据 ExcuteChild(gameObject); } } } //传输范围数据 void ExcuteChild(GameObject go) { if (go.renderer != null ) { //material与sharedMaterial用法相同,但是效率有差别 //sharedMaterial是共享材质,修改的话内存只占用一份,如果使用material的话,每次属性修改都会new一份新的出来 //但在编辑器模式下,修改sharedMaterial会导致本地文件发生变化,为防止不必要的文件变动和提交,这里区分平台处理 #if UNITY_EDITOR Material[] list = go.renderer.materials; #else Material[] list = go.renderer.sharedMaterials; #endif for ( int i = 0; i < list.Length; i++) { list[i].SetFloat( "_MinX" , m_MinX); list[i].SetFloat( "_MinY" , m_MinY); list[i].SetFloat( "_MaxX" , m_MaxX); list[i].SetFloat( "_MaxY" , m_MaxY); } } //遍历处理子节点 int count = go.transform.childCount; for ( int i = 0; i < count; i++) { ExcuteChild(go.transform.GetChild(i).gameObject); } } } } |
问题和缺点:
1. 如果涉及Shader较多的话,开发和维护成本较高。
总结
第一个方案,效果打了折扣;
第二个方案,开发维护成本高,有一些不好避免的问题,不建议使用;
第三个方案,显示粒子特效异常,可以有选择的进行使用;
第四个方案,是个比较好的实现方案。如果有裁剪需求的特效使用的是通用的几个Shader,那么这个方案就更划算了。即使有几个特殊的Shader需要修改,成本也不算太高。
如果大家有裁剪特效的需求,希望以上内容可以帮助到你!
如果有更好的方案或者优化建议,欢迎交流!