ScrollView裁剪特效 - 裁剪区域的方式(推荐)

特效shader添加裁剪区域

使用裁剪区域进行裁剪, 粒子的shader得支持裁剪区域,需要在vert函数中加上下面的代码:

#ifdef UNITY_UI_CLIP_RECT
c.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect); //xy在裁剪矩形范围内就返回1, 否则返回0
clip(c.a - 0.001);
#endif

上面的代码参考自UI-Default.shader

 

效果

 

下面的脚本用于把裁剪区域设置到Renderer的Material中,该脚本挂在Content或粒子上都行。

注意:Unity2018挂在Viewport-RectMask2D上也行,但在Unity2019中挂在Viewport-RectMask2D就不行了,应该是RectMask2D的实现有改动。

该代码是在MaskableGraphic的基础上精简下了,核心点:

1) 在裁剪回调函数SetClipRect中将裁剪区域设置到Renderer的Material中
2) 在事件函数(OnEnable, OnDisable, OnTransformParentChanged, OnCanvasHierarchyChanged)中把自己添加到RectMask2D的裁剪列表中
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class Clip3D : UIBehaviour, IClippable
{

    [NonSerialized]
    private RectTransform m_RectTransform;

    [NonSerialized]
    private RectMask2D m_ParentMask;

    private List<Renderer> m_rendererList = new List<Renderer>();
    private List<Material> m_matList = new List<Material>();

    public RectTransform rectTransform
    {
        get
        {
            // The RectTransform is a required component that must not be destroyed. Based on this assumption, a
            // null-reference check is sufficient.
            if (ReferenceEquals(m_RectTransform, null))
            {
                m_RectTransform = GetComponent<RectTransform>();
            }
            return m_RectTransform;
        }
    }

    public void SetClipSoftness(Vector2 clipSoftness)
    {
        //not implement
    }

    public void Cull(Rect clipRect, bool validRect)
    {
        //not implement
    }

    public virtual void SetClipRect(Rect clipRect, bool validRect)
    {
        Debug.Log($"({clipRect.xMin}, {clipRect.yMin}), ({clipRect.xMax}, {clipRect.yMax})"); //rootCanvas坐标空间下的, 我们这边要用世界坐标空间下的

        GetComponentsInChildren<Renderer>(m_rendererList);
        if (m_rendererList.Count > 0)
        {
            if (validRect)
            {
                var worldCorners = new Vector3[4]; //lb, lt, rt, rb
                m_ParentMask.rectTransform.GetWorldCorners(worldCorners);
                var worldClipRect = new Vector4(worldCorners[0].x, worldCorners[0].y, worldCorners[2].x, worldCorners[2].y);

                foreach (var render in m_rendererList)
                {
                    render.GetMaterials(m_matList);
                    if (m_matList.Count > 0)
                    {
                        foreach (var mat in m_matList)
                        {
                            if (null != mat && mat.HasProperty("_ClipRect"))
                            {
                                mat.SetVector("_ClipRect", worldClipRect);
                                mat.EnableKeyword("UNITY_UI_CLIP_RECT");
                            }
                        }
                        m_matList.Clear();
                    }
                }
            }
            else
            {
                foreach (var render in m_rendererList)
                {
                    render.GetMaterials(m_matList);
                    if (m_matList.Count > 0)
                    {
                        foreach (var mat in m_matList)
                        {
                            if (null != mat)
                            {
                                mat.DisableKeyword("UNITY_UI_CLIP_RECT");
                            }
                        }
                        m_matList.Clear();
                    }
                }
            }

            m_rendererList.Clear();
        }
    }

    protected override void OnEnable()
    {
        base.OnEnable();
        UpdateClipParent();
    }

    protected override void OnDisable()
    {
        base.OnDisable();
        UpdateClipParent();
    }

#if UNITY_EDITOR
    protected override void OnValidate()
    {
        base.OnValidate();
        UpdateClipParent();
    }
#endif

    protected override void OnTransformParentChanged()
    {
        base.OnTransformParentChanged();

        if (!isActiveAndEnabled)
            return;

        UpdateClipParent();
    }

    protected override void OnCanvasHierarchyChanged()
    {
        base.OnCanvasHierarchyChanged();

        if (!isActiveAndEnabled)
            return;

        UpdateClipParent();
    }

    private void UpdateClipParent()
    {
        var newParent = IsActive() ? MaskUtilities.GetRectMaskForClippable(this) : null;

        // if the new parent is different OR is now inactive
        if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive()))
        {
            m_ParentMask.RemoveClippable(this);
        }

        // don't re-add it if the newparent is inactive
        if (newParent != null && newParent.IsActive())
            newParent.AddClippable(this);

        m_ParentMask = newParent;
    }

    /// <summary>
    /// See IClippable.RecalculateClipping
    /// </summary>
    public virtual void RecalculateClipping()
    {
        UpdateClipParent();
    }

}

 

粒子用到的支持裁剪的shader,这边仅仅为了演示随意写了一个(只求能看见粒子就行)

UnlitParticle.shader, UnlitParticle.mat

Shader "My/UnlitParticle"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ClipRect ("Clip Rect", Vector) = (0, 0, 0, 0)
    }
    SubShader
    {
        Tags 
        {
            "RenderType"="Opaque" 
        }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile __ UNITY_UI_CLIP_RECT

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 worldPosition : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _ClipRect;

            v2f vert (appdata v)
            {
                v2f o;
                o.worldPosition = v.vertex;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, i.uv);

                #ifdef UNITY_UI_CLIP_RECT
                c.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect);
                clip(c.a - 0.001);
                #endif

                return c;
            }
            ENDCG
        }
    }
}

粒子的材质设置为UnlitParticle.mat,_ClipRect放在Properties中仅仅是为了在运行时看下裁剪区域点值(调试用)

 

Canvas不能用Overlay模式,否则粒子就看不到,因为Overlay模式,粒子不是直接在相机视口范围内

这边用Camera模式

 

UI相机得用正交相机,否则粒子会因为透视的原因(近大远小),而导致裁剪区域视觉上不对

 

这边没处理到的问题

1) 粒子的层级问题,上面的粒子实际是白色的,但我们看到的是灰色的,因为粒子出现在了ScrollView背景的后面,这个涉及到ugui和3d物体的层级关系控制了,不在这边的处理范围,这边只处理裁剪

2) Renderer材质生成副本的问题,可以考虑用MaterialBlockProperty修改材质属性

 

关于step函数

step(a, b)函数可以用于代替if (a>=b)的判断,它的逻辑:

if (a>=b) 
    return 0;
else 
    return 1;

 

UnityGet2DClipping函数

CGIncludes/UnityUI.cginc

inline float UnityGet2DClipping (in float2 position, in float4 clipRect)
{
    float2 inside = step(clipRect.xy, position.xy) * step(position.xy, clipRect.zw);
    return inside.x * inside.y;
}

 

参考

【Shader案例】实现UGUI裁剪功能_两水先木示的博客-CSDN博客_ugui scroll 裁剪3d模型 shader

Unity游戏开发-UI中裁剪特效_shitangdejiaozi的博客-CSDN博客

posted @ 2023-01-20 00:01  yanghui01  阅读(525)  评论(0编辑  收藏  举报