Search Unity

Free script: Soft mask for vertical scrollRect

Discussion in 'UGUI & TextMesh Pro' started by bartofzo, Feb 14, 2018.

  1. bartofzo

    bartofzo

    Joined:
    Mar 16, 2017
    Posts:
    151
    Hi Everybody,

    Thought I'd share this script I made, because I couldn't find any free solutions that worked for what I was trying to do. Took me some experimenting (for instance, finding out Unity's UI Mask messes with the material of graphic components).

    What it does? It allows for soft clipping edges of the viewport. Currently it only works vertically but feel free to expand on this if you need it to work horizontally too.

    It's tuned to my use case, so it has to be set up at runtime (because I'm filling the scrollRect). Also, it only works at runtime because it uses screen coordinates in the shader to determine the alpha value.
    Another cool feature is that it subscibes to the scroll event of the rect, and adjusts the top edge so that when there is no scrolling, no top edge is visible.

    There will probably be better ways to do this, but it works for me, and since it took me quite some time to figure it out, maybe it is of use of somebody here!

    Usage example:

    Code (CSharp):
    1.  VerticalSoftMaskScrollRect.Create(scrollRect, 20);
    VerticalSoftMaskScrollRect.cs
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.UI;
    3.  
    4. /// <summary>
    5. /// Soft clipping scroll viewport.
    6. /// By Bart van de Sande (Nonline)
    7. ///
    8. /// NOTE: Only works vertically for now
    9. ///
    10. /// NOTE: if the scroll viewport has a Mask component it will be removed and replaced by RectMask2D
    11. /// Mask component messes with material. But we do need a mask to prevent raycasting on invisible portions of the content
    12. ///
    13. /// </summary>
    14. public class VerticalSoftMaskScrollRect : MonoBehaviour {
    15.  
    16.     /// <summary>
    17.     /// Creates soft clip for a scrollrect, automatically subscribes to scroll event
    18.     /// to disable top clipping when scrolling is at zero
    19.     /// </summary>
    20.     public static VerticalSoftMaskScrollRect Create(ScrollRect scrollRect, float edgeSize = 20)
    21.     {
    22.         // check if a regular mask is used, if so, replace with RectMask2D
    23.         if (scrollRect.viewport.GetComponent<Mask>() != null)
    24.         {
    25.             Destroy(scrollRect.viewport.GetComponent<Mask>());
    26.             Image vpImage = scrollRect.viewport.GetComponent<Image>();
    27.             vpImage.color = Color.clear;
    28.             vpImage.sprite = null;
    29.         }
    30.  
    31.         // apply rectMask2D
    32.         if (scrollRect.viewport.GetComponent<RectMask2D>() == null)
    33.             scrollRect.viewport.gameObject.AddComponent<RectMask2D>();
    34.  
    35.  
    36.         VerticalSoftMaskScrollRect scp = Create(scrollRect.content.gameObject, scrollRect.viewport, edgeSize);
    37.  
    38.  
    39.         // check if scroll event listener is already present
    40.         if (scp.subscribedScrollRect == null)
    41.         {
    42.             scp.subscribedScrollRect = scrollRect;
    43.             scrollRect.onValueChanged.AddListener(scp.onScrollChange);
    44.         }
    45.  
    46.         // set correct initial clipping, argument doesn't matter
    47.         scp.onScrollChange(Vector2.zero);
    48.  
    49.         return scp;
    50.     }
    51.  
    52.  
    53.     /// <summary>
    54.     /// Use this to give a gameObject soft clipping edges
    55.     /// </summary>
    56.     public static VerticalSoftMaskScrollRect Create(GameObject contentParent, RectTransform viewportRT, float edgeSize = 20)
    57.     {
    58.  
    59.         VerticalSoftMaskScrollRect scp = contentParent.gameObject.GetComponent<VerticalSoftMaskScrollRect>();
    60.         if (scp == null)
    61.             scp = contentParent.gameObject.AddComponent<VerticalSoftMaskScrollRect>();
    62.  
    63.  
    64.         scp.init(viewportRT, edgeSize);
    65.         return scp;  
    66.     }
    67.  
    68.  
    69.     private ScrollRect subscribedScrollRect;
    70.     private float defaultTopEdgeSize;
    71.     private Material mat;
    72.     private RectTransform ClipRT;
    73.     private float _topPixels = 20;
    74.     private float _bottomPixels = 20;
    75.  
    76.     /// <summary>
    77.     /// When scrolling adjusts the clipping of the top
    78.     /// </summary>
    79.     protected void onScrollChange(Vector2 scroll)
    80.     {
    81.         _topPixels = Mathf.Min(defaultTopEdgeSize, Mathf.Max(0, subscribedScrollRect.content.anchoredPosition.y));
    82.         setShaderValues();
    83.     }
    84.  
    85.     /// <summary>
    86.     /// Size of soft clip
    87.     /// </summary>
    88.     public float TopEdgePixels
    89.     {
    90.         get
    91.         {
    92.             return _topPixels;
    93.         }
    94.         set
    95.         {
    96.             _topPixels = defaultTopEdgeSize = value;
    97.             setShaderValues();
    98.         }
    99.     }
    100.  
    101.     /// <summary>
    102.     /// Size of soft clip
    103.     /// </summary>
    104.     public float BottomEdgePixels
    105.     {
    106.         get
    107.         {
    108.             return _bottomPixels;
    109.         }
    110.         set
    111.         {
    112.             _bottomPixels = value;
    113.             setShaderValues();
    114.         }
    115.     }
    116.  
    117.     /// <summary>
    118.     /// Code taken from:
    119.     /// https://answers.unity.com/questions/1013011/convert-recttransform-rect-to-screen-space.html
    120.     /// @ Tobias-Pott
    121.     /// </summary>
    122.     private static Rect RectTransformToScreenSpace(RectTransform transform)
    123.     {
    124.         Vector2 size = Vector2.Scale(transform.rect.size, transform.lossyScale);
    125.         Rect rect = new Rect(transform.position.x, Screen.height - transform.position.y, size.x, size.y);
    126.         rect.x -= (transform.pivot.x * size.x);
    127.         rect.y -= ((1.0f - transform.pivot.y) * size.y);
    128.         return rect;
    129.     }
    130.  
    131.     /// <summary>
    132.     /// Initializes the material and applies it to all children
    133.     /// </summary>
    134.     private void init(RectTransform clipRT, float edgeSize)
    135.     {
    136.         this.ClipRT = clipRT;
    137.         this.mat = new Material(Shader.Find("UI/VerticalSoftMaskScrollRectShader"));
    138.  
    139.         _topPixels = _bottomPixels = defaultTopEdgeSize = edgeSize;
    140.  
    141.         setMaterialOnChildren(this.gameObject.transform);
    142.         setShaderValues();
    143.     }
    144.  
    145.     /// <summary>
    146.     /// Recursive function that sets the material on all children of the content
    147.     /// </summary>
    148.     private void setMaterialOnChildren(Transform T)
    149.     {
    150.         // only add to children without children
    151.         if (T.childCount == 0)
    152.         {
    153.             Graphic graphic = T.GetComponent<Graphic>();
    154.             if (graphic != null)
    155.                 graphic.material = mat;
    156.            
    157.             Text text = T.GetComponent<Text>();
    158.             if (text != null)
    159.                 text.material = mat;
    160.  
    161.             return;
    162.         }
    163.         else
    164.         {
    165.             // find children in this transform
    166.             foreach (Transform child in T)
    167.                 setMaterialOnChildren(child);
    168.         }
    169.     }
    170.  
    171.     /// <summary>
    172.     /// (Re)applies values to material
    173.     /// </summary>
    174.     public void setShaderValues()
    175.     {
    176.         Rect clipRect = RectTransformToScreenSpace(ClipRT);
    177.  
    178.         float yRangeTop = _topPixels / (float)Screen.height;
    179.         float yRangeBottom = _bottomPixels / (float)Screen.height;
    180.  
    181.         float yMin = (clipRect.yMin / (float)Screen.height) + yRangeTop;
    182.         float yMax = (clipRect.yMax / (float)Screen.height) - yRangeBottom;
    183.  
    184.         mat.SetFloat("_yMax", yMax);
    185.         mat.SetFloat("_yMin", yMin);
    186.         mat.SetFloat("_yRangeTop", yRangeTop);
    187.         mat.SetFloat("_yRangeBottom", yRangeBottom);
    188.     }
    189.  
    190.     void OnRectTransformDimensionsChange()
    191.     {
    192.         setShaderValues();  
    193.     }
    194. }
    195.  
    VerticalSoftMaskScrollRectShader.shader
    Code (CSharp):
    1. // By Bart van de Sande (Nonline)
    2. // replaces default UI material on children elements of a scrollrect
    3.  
    4. Shader "UI/VerticalSoftMaskScrollRectShader"
    5. {
    6.     Properties
    7.     {
    8.         [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    9.         _Color ("Tint", Color) = (1,1,1,1)
    10.    
    11.         _StencilComp ("Stencil Comparison", Float) = 8
    12.         _Stencil ("Stencil ID", Float) = 0
    13.         _StencilOp ("Stencil Operation", Float) = 0
    14.         _StencilWriteMask ("Stencil Write Mask", Float) = 255
    15.         _StencilReadMask ("Stencil Read Mask", Float) = 255
    16.         _ColorMask ("Color Mask", Float) = 15
    17.  
    18.         _yMin("yMin",Float) = 0
    19.         _yMax("yMax",Float) = 0
    20.         _yRangeTop("yRangeTop",Float) = 0
    21.         _yRangeBottom("yRangeBottom",Float) = 0
    22.  
    23.     }
    24.     SubShader
    25.     {
    26.         Tags
    27.         {
    28.             "Queue"="Transparent"
    29.             "IgnoreProjector"="True"
    30.             "RenderType"="Transparent"
    31.             "PreviewType"="Plane"
    32.             "CanUseSpriteAtlas"="True"
    33.         }
    34.    
    35.         Stencil
    36.         {
    37.             Ref [_Stencil]
    38.             Comp [_StencilComp]
    39.             Pass [_StencilOp]
    40.             ReadMask [_StencilReadMask]
    41.             WriteMask [_StencilWriteMask]
    42.         }
    43.         Cull Off
    44.         Lighting Off
    45.         ZWrite Off
    46.         ZTest [unity_GUIZTestMode]
    47.         Blend SrcAlpha OneMinusSrcAlpha
    48.         ColorMask [_ColorMask]
    49.         Pass
    50.         {
    51.         CGPROGRAM
    52.             #pragma vertex vert
    53.             #pragma fragment frag
    54.             #include "UnityCG.cginc"
    55.             #include "UnityUI.cginc"
    56.        
    57.             struct appdata_t
    58.             {
    59.                 float4 vertex   : POSITION;
    60.                 float4 color    : COLOR;
    61.                 float2 texcoord : TEXCOORD0;
    62.             };
    63.             struct v2f
    64.             {
    65.                 float4 vertex   : SV_POSITION;
    66.                 fixed4 color    : COLOR;
    67.                 half2 texcoord  : TEXCOORD0;
    68.                 float4 worldPosition : TEXCOORD1;
    69.                 float2 screenPos : TEXCOORD2;
    70.             };
    71.        
    72.             fixed4 _Color;
    73.             fixed4 _TextureSampleAdd;
    74.  
    75.             // Not used?
    76.             //bool _UseClipRect;
    77.             // float4 _ClipRect;
    78.             //bool _UseAlphaClip;
    79.             v2f vert(appdata_t IN)
    80.             {
    81.                 v2f OUT;
    82.  
    83.                 OUT.worldPosition = IN.vertex;
    84.                 OUT.vertex =  UnityObjectToClipPos(OUT.worldPosition);
    85.  
    86.                 OUT.screenPos = ComputeScreenPos(OUT.vertex);
    87.                 OUT.screenPos.y = 1-OUT.screenPos.y;
    88.  
    89.                 OUT.texcoord = IN.texcoord;
    90.            
    91.                 #ifdef UNITY_HALF_TEXEL_OFFSET
    92.                 OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
    93.                 #endif
    94.  
    95.                 OUT.color = IN.color * _Color;
    96.                 return OUT;
    97.             }
    98.             sampler2D _MainTex;
    99.             float _yRangeTop;
    100.             float _yRangeBottom;
    101.             float _yMin;
    102.             float _yMax;
    103.             fixed4 frag(v2f IN) : SV_Target
    104.             {
    105.                 half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
    106.  
    107.                 // Code below commented out because not necessary?
    108.  
    109.                 //if (_UseClipRect)
    110.                 //    color *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
    111.          
    112.                 //if (_UseAlphaClip)
    113.                 //    clip (color.a - 0.001);
    114.  
    115.  
    116.                 float clipMin = min((IN.screenPos.y - _yMin) / _yRangeTop, 1);
    117.                 float clipMax = min((_yMax - IN.screenPos.y) / _yRangeBottom, 1);
    118.                 float clipAlpha = min(clipMin,clipMax);
    119.  
    120.                  color.a *= saturate(clipAlpha);
    121.  
    122.                 return color;
    123.             }
    124.         ENDCG
    125.         }
    126.     }
    127. }
    128.  
     

    Attached Files:

    CrazyPanda likes this.
  2. SweatyChair

    SweatyChair

    Joined:
    Feb 15, 2016
    Posts:
    140
    Look nice!

    Will there be a landscape version? Or even better, having an selection of UnityEngine.UI.Navigation in editor.
     
  3. dsavickij

    dsavickij

    Joined:
    May 17, 2019
    Posts:
    12
    Correct me if i'm wrong but this method only works on unity built in text (not TMP) and sprites?
    in cases where TMP is used this method is useless