首页 >Unity3D频道 >【Unity3D研究院之游戏开发】 > Unity3D研究院之UGUI软裁切(一百零八)
2019
08-31

Unity3D研究院之UGUI软裁切(一百零八)

前段时间用Stencil实现了一套裁切,但是Stencil原理只能是裁或者不裁,这样是无法实现像NGUI这样的软裁切功能。之前的笔记在这里 https://www.xuanyusong.com/archives/4562

如下图所示,我们用Rect2DMask组件同时软裁切3D模型和Image。

QQ20190831-173603

裁切最容易出问题的就是,裁切区域发生变化,因为在Shader里需要根据裁切区域来计算,但实际项目中特别容易出问题的就是UI上滑动列表里裁切3D或者粒子特效,因为滑动列表可能是循环的,每个新出的都需要将裁切区域传入shader中。所以我分析了一遍UGUI的源码,按照UGUI的方式将区域传入shader中。

如下图所示,3D模型如果需要用UI摄像机看,那么需要调整3D模型的Scale,这里我们设置放大100倍(根据需求设置),另外将3D模型的Prefab以及支持被RectMask裁切的材质拖入。

QQ20190831-174214接着就是脚本中要将Rect2DMask的区域传入3D-Default了,完全通过UGUI的方式传入。

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(RectTransform))]
public class Clip : UIBehaviour, IClippable, ICanvasElement
{
    public Canvas m_Canvas;
    public GameObject m_Prefab;
    public Material m_Material;


    private RectMask2D m_ParentMask;
    private RectTransform m_RectTransform;
    private MeshRenderer m_MeshRenderer;

    public RectTransform rectTransform {
        get
        {
            if (m_RectTransform == null)
            {
                m_RectTransform = transform as RectTransform; ;
            }
            return m_RectTransform; }
    }


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

    public void RecalculateClipping()
    {
        UpdateClipParent();
    }


    public void SetClipRect(Rect value, bool validRect)
    {
        //在这里就可以将正确的Rect2DMask传入shader了
        if (m_MeshRenderer)
        {
            //缩放是屏幕高度的一半
            float scale = (m_Canvas.transform as RectTransform).sizeDelta.y / 2f;
            //将RectMask2D的区域传入
            m_MeshRenderer.sharedMaterial.SetVector("_ClipRect", new Vector4(value.xMin, value.yMin, value.xMax, value.yMax));
            //将Scale传入
            m_MeshRenderer.sharedMaterial.SetFloat("_Scale", scale);
        }
    }

    protected override void Awake()
    {
        base.Awake();
        m_MeshRenderer = Instantiate<GameObject>(m_Prefab, transform, false).GetComponent<MeshRenderer>();
        m_MeshRenderer.material = m_Material;
        //在这里通知 UGUI Rebuild
        //接着UGUI就会回调到SetClipRect方法中
        CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
    }

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

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


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


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

    protected override void OnCanvasHierarchyChanged()
    {
        base.OnCanvasHierarchyChanged();
        UpdateClipParent();
    }
    private void UpdateClipParent()
    {
        var newParent = MaskUtilities.GetRectMaskForClippable(this);

        if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive()))
        {
            m_ParentMask.RemoveClippable(this);
        }
        if (newParent != null && newParent.IsActive())
            newParent.AddClippable(this);

        m_ParentMask = newParent;
    }

    public void Rebuild(CanvasUpdate executing)
    {

    }

    public void LayoutComplete()
    {
    }

    public void GraphicUpdateComplete()
    {
    }
}

在来看看shader

Shader "3D-Default"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
        //软裁切XY轴的长度
        _ClipSoftX ("_ClipSoftX", Float) = 0
        _ClipSoftY ("_ClipSoftY", Float) = 0
        
	}
	SubShader
	{
		Tags { "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"
             
			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
			};

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

			sampler2D _MainTex;
			float4 _MainTex_ST;
		    float _Scale;
            
			v2f vert (appdata v)
			{
				v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldPosition = mul(unity_ObjectToWorld,v.vertex * _Scale);
                return o;
			}
			
            float4 _ClipRect;
            float _ClipSoftX;
            float _ClipSoftY;
            inline float SoftUnityGet2DClipping (in float2 position, in float4 clipRect)
            {
                float2 xy = (position.xy-clipRect.xy)/float2(_ClipSoftX,_ClipSoftY)*step(clipRect.xy, position.xy);
                float2 zw = (clipRect.zw-position.xy)/float2(_ClipSoftX,_ClipSoftY)*step(position.xy,clipRect.zw);
                float2 factor = clamp(0, zw, xy);
                return saturate(min(factor.x,factor.y));
            }
			
            fixed4 frag (v2f i) : SV_Target
			{
			   fixed4 color = tex2D(_MainTex, i.uv);
               //计算软裁切
               color.a *= SoftUnityGet2DClipping(i.worldPosition.xy, _ClipRect);
               return color;
                
			}
			ENDCG
		}
	}
}

 

然后就是UI了,如下图所示,将需要进行软裁切的UI绑定UI_Default材质,在下面可以设置软裁切的尺寸,这个例子中3D模型和UI的ClipSoft都设置的是15.

QQ20190831-175125

把UI的Shader拿出来,在shader中替换原来的UnityGet2DClipping方法即可,这代码和上面3D软裁切的算法完全一样。

    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15
        
        //--add---
        _ClipSoftX ("_ClipSoftX", Float) = 15
        _ClipSoftY ("_ClipSoftY", Float) = 15
        //--add---
        
        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }
            float _ClipSoftX;
            float _ClipSoftY;
            inline float SoftUnityGet2DClipping (in float2 position, in float4 clipRect)
            {
                float2 xy = (position.xy-clipRect.xy)/float2(_ClipSoftX,_ClipSoftY)*step(clipRect.xy, position.xy);
                float2 zw = (clipRect.zw-position.xy)/float2(_ClipSoftX,_ClipSoftY)*step(position.xy,clipRect.zw);
                float2 factor = clamp(0, zw, xy);
                return saturate(min(factor.x,factor.y));
            }
            
            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                //替换原来的
                //color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect)
                color.a *= SoftUnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif
                
                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif
                return color;
            }

大家需要注意的是Shader代码里的_ClipRect. UGUI会自动将正确的Rect2DMask区域传入_ClipRect,所以3D模型部分我也是参考UGUI的源码将Rect2DMask传入_ClipRect。具体大家可以参考UGUI的源码。

现在3D模型和UI使用的都是正交摄像机,其实也可以用透视摄像机。如下图所示,可以在创建一个3D摄像机通过层来看3D模型,当然也可以使用RT,这样就跟UI软裁切完全一样了。

QQ20190831-180144

 

最后编辑:
作者:雨松MOMO
专注移动互联网,Unity3D游戏开发
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!

Unity3D研究院之UGUI软裁切(一百零八)》有 5 条评论

  1. 星光白云说:

    点赞,收藏

  2. Lowy说:

    最终结果增加了一个二阶曲线,柔和了点,不知道性能影响大不大
    #ifdef UNITY_UI_CLIP_RECT
    float outA = SoftUnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
    color.a *= 2*outA*(1-outA)*_ClipSoftPow + outA*outA;
    #endif

  3. ww说:

    好思路,而且非常实用的功能,感谢

  4. 平子说:

    一看16年的文章了,还是能学到东西,代码已经收录。雨松加油,希望有更多的文章分享。

留下一个回复

你的email不会被公开。 必填项已用 * 标注。