Unity特效在ScrollView中的裁剪

发表于2017-03-06
评论0 1.01w浏览

 在项目中碰到裁剪UI这类问题,主要是使用ScrollView裁剪后对特效不起作用。下面就给大家介绍下unity特效在ScrollView中出现裁剪问题的解决办法,想知道的可以看一看。

 

问题的出现

从遇到这个问题的需求说起吧。

为了能够让玩家在可领奖的时候,有个更明显的提示和炫酷的效果,要在领奖按钮上加一个特效。一个特效拖上去,调整好大小,好像ok了。但是,这个奖励列表是可滑动的,一拖动问题就出现了:

UIPanel是和ScrollView结合使用的,可以裁剪UI,但对特效是不起作用的。

现在要解决的就是特效在ScrollView中的裁剪问题(准确说是在UIPanel中的裁剪,标题和内容说ScrollView是为了突出它的应用场景)。

以下是考虑和实践过的几个解决方案,分享给大家。

解决方案一:帧动画

按照特效的效果,做帧动画,这样播放帧动画的是UI元素就可以被正常裁剪了。

问题和缺点:

1.            帧动画跟原来的特效相比,效果打了折扣

解决方案二:添加摄像机

添加一个Camera,这个Canera只显示ScrollView的实际显示区域。

CameraCulling Mask中可以设置其摄像Layer层,我们先添加一个新的LayerRewardEffect,并将新添加的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();
}

问题和缺点:

  1. 当特效种类较多,不同应用场景多的时候,开发成本偏高;
  2. 粒子特效显示异常,贴图显示带有方块

    

解决方案四:通过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来修改。
2.将显示区域传给Shader,为了方便使用,写了一个脚本,使用时将该脚本挂在特效上或多个特效的父节点上,然后设置UIPanel给脚本。

 

脚本代码如下,添加了比较详细的注释:

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需要修改,成本也不算太高。

如果大家有裁剪特效的需求,希望以上内容可以帮助到你!

如果有更好的方案或者优化建议,欢迎交流!

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引