前段时间用Stencil实现了一套裁切,但是Stencil原理只能是裁或者不裁,这样是无法实现像NGUI这样的软裁切功能。之前的笔记在这里 https://www.xuanyusong.com/archives/4562
如下图所示,我们用Rect2DMask组件同时软裁切3D模型和Image。
裁切最容易出问题的就是,裁切区域发生变化,因为在Shader里需要根据裁切区域来计算,但实际项目中特别容易出问题的就是UI上滑动列表里裁切3D或者粒子特效,因为滑动列表可能是循环的,每个新出的都需要将裁切区域传入shader中。所以我分析了一遍UGUI的源码,按照UGUI的方式将区域传入shader中。
如下图所示,3D模型如果需要用UI摄像机看,那么需要调整3D模型的Scale,这里我们设置放大100倍(根据需求设置),另外将3D模型的Prefab以及支持被RectMask裁切的材质拖入。
接着就是脚本中要将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.
把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软裁切完全一样了。
- 本文固定链接: https://www.xuanyusong.com/archives/4650
- 转载请注明: 雨松MOMO于 雨松MOMO程序研究院 发表




点赞,收藏
最终结果增加了一个二阶曲线,柔和了点,不知道性能影响大不大
#ifdef UNITY_UI_CLIP_RECT
float outA = SoftUnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
color.a *= 2*outA*(1-outA)*_ClipSoftPow + outA*outA;
#endif
棒
好思路,而且非常实用的功能,感谢
一看16年的文章了,还是能学到东西,代码已经收录。雨松加油,希望有更多的文章分享。