前段时间用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的方式传入。
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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
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
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 |
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软裁切的算法完全一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 } |
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 |
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年的文章了,还是能学到东西,代码已经收录。雨松加油,希望有更多的文章分享。