Search Unity

2D Outline Shader

Discussion in 'Shaders' started by P_e_t_a_c_h_e_k, Aug 4, 2021.

  1. P_e_t_a_c_h_e_k

    P_e_t_a_c_h_e_k

    Joined:
    Dec 26, 2017
    Posts:
    26
    Good evening) I make a 2d outline shader. Found answer to the post:

    https://forum.unity.com/threads/help-to-find-an-asset-solution.755273/#post-5246960

    there, script
    came up to me, but the outline resizes with the removal of the camera (I'm trying to make the size static (the size of the contour changes with the removal of the camera, but I do NOT need it )
    Will you help me please?(

    script:
    Code (CSharp):
    1. Shader "Unlit/Spine Outline"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.         _OutlineColor ("Outline Color", Color) = (1,1,1,1)
    7.         _OutlineWidth ("Outline Width", Range(0, 4)) = 1
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "Queue"="Transparent" "RenderType"="Transparent" }
    12.         LOD 100
    13.         ZWrite Off
    14.         Blend SrcAlpha OneMinusSrcAlpha
    15.         CGINCLUDE
    16.         #include "UnityCG.cginc"
    17.         sampler2D _MainTex;
    18.         float4 _MainTex_ST;
    19.         fixed4 _OutlineColor;
    20.         float _OutlineWidth;
    21.         struct v2fOutline
    22.         {
    23.             float4 pos : SV_POSITION;
    24.             float2 uv : TEXCOORD0;
    25.         };
    26.         v2fOutline vertOutline (appdata_base v, float2 offset)
    27.         {
    28.             v2fOutline o;
    29.             o.pos = UnityObjectToClipPos(v.vertex);
    30.             o.pos.xy += offset * 2 * o.pos.w * _OutlineWidth / _ScreenParams.xy;
    31.             o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    32.             return o;
    33.         }
    34.         fixed4 fragOutline (v2fOutline i) : SV_Target
    35.         {
    36.             fixed alpha = tex2D(_MainTex, i.uv).a;
    37.             fixed4 col = _OutlineColor;
    38.             col.a *= alpha;
    39.             return col;
    40.         }
    41.         ENDCG
    42.         Pass
    43.         {
    44.             CGPROGRAM
    45.             #pragma vertex vert
    46.             #pragma fragment fragOutline
    47.             v2fOutline vert (appdata_base v)
    48.             {
    49.                 return vertOutline(v, float2( 1, 1));
    50.             }
    51.             ENDCG
    52.         }
    53.         Pass
    54.         {
    55.             CGPROGRAM
    56.             #pragma vertex vert
    57.             #pragma fragment fragOutline
    58.             v2fOutline vert (appdata_base v)
    59.             {
    60.                 return vertOutline(v, float2(-1, 1));
    61.             }
    62.             ENDCG
    63.         }
    64.         Pass
    65.         {
    66.             CGPROGRAM
    67.             #pragma vertex vert
    68.             #pragma fragment fragOutline
    69.             v2fOutline vert (appdata_base v)
    70.             {
    71.                 return vertOutline(v, float2( 1,-1));
    72.             }
    73.             ENDCG
    74.         }
    75.         Pass
    76.         {
    77.             CGPROGRAM
    78.             #pragma vertex vert
    79.             #pragma fragment fragOutline
    80.             v2fOutline vert (appdata_base v)
    81.             {
    82.                 return vertOutline(v, float2(-1,-1));
    83.             }
    84.             ENDCG
    85.         }
    86.         Pass
    87.         {
    88.             CGPROGRAM
    89.             #pragma vertex vert
    90.             #pragma fragment frag
    91.             #include "UnityCG.cginc"
    92.             struct v2f
    93.             {
    94.                 float4 pos : SV_POSITION;
    95.                 float2 uv : TEXCOORD0;
    96.                 float4 color : TEXCOORD1;
    97.             };
    98.             v2f vert (appdata_full v)
    99.             {
    100.                 v2f o;
    101.                 o.pos = UnityObjectToClipPos(v.vertex);
    102.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    103.                 o.color = v.color;
    104.                 return o;
    105.             }
    106.             fixed4 frag (v2f i) : SV_Target
    107.             {
    108.                 return tex2D(_MainTex, i.uv) * i.color;
    109.             }
    110.             ENDCG
    111.         }
    112.     }
    113. }
     
    Last edited: Aug 4, 2021
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    The shader was designed such that
    _OutlineWidth
    was defined in screen pixels width. The
    _ScreenParams
    in the above shader holds the current render resolution. You can remove that
    / _ScreenParams.xy
    , but then the
    _OutlineWidth
    will be in "screen space" width. I'm not entirely sure what you mean by "removal of the camera" though. Are you using this on something you're manually rendering to a render texture?
     
  3. P_e_t_a_c_h_e_k

    P_e_t_a_c_h_e_k

    Joined:
    Dec 26, 2017
    Posts:
    26
    Let me explain again what I want to do:

    this shader adjusts the outline size depending on the distant camera, but I don't need that. I need a outline size of 3 (for example), that is, when the camera is farther away, it will be almost invisible (or it will be visible but quite a bit) will you help me please?


    if remove _ScreenParams.xy from the code, then the outline will appear only if the camera is very, very close. Gif:
     

    Attached Files:

  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    3 what. Three sprite texels? The shader has no idea how big a texel is in the vertex shader (where this code is running), so that's not actually possible to do programatically. You'd have to fudge the size to be whatever you needed. And it'll break if the sprite's scale changes because, again, the vertex shader has no idea how big a texel is.

    Because the
    _OutlineWidth
    is now in screen widths. If you originally had the value at "3" or something, you'll want to set it to something like 0.002 after you remove that value to get something similar. But it's technically still being done in screen space.

    For what you're looking to do you probably want something like this:
    Code (csharp):
    1. // add this line
    2. v.vertex.xy += offset * _OutlineWidth;
    3.  
    4. o.pos = UnityObjectToClipPos(v.vertex);
    5.  
    6. // comment this line out or delete it
    7. // o.pos.xy += offset * 2 * o.pos.w * _OutlineWidth / _ScreenParams.xy;
    If you're working on your sprites on a pixel sized canvas,
    _OutlineWidth
    will be fairly easy to understand as it'll just be canvas pixels. But again, if you scale the sprite to anything not 1:1 you'll have to figure out what the "correct"
    _OutlineWidth
    value is.
     
    P_e_t_a_c_h_e_k likes this.
  5. P_e_t_a_c_h_e_k

    P_e_t_a_c_h_e_k

    Joined:
    Dec 26, 2017
    Posts:
    26
    Thank you very much! You wrote in two minutes what I could not do in two days) And yes, shaders are an interesting topic, though little is clear)
     
  6. P_e_t_a_c_h_e_k

    P_e_t_a_c_h_e_k

    Joined:
    Dec 26, 2017
    Posts:
    26

    Once again I have to contact you ... I'm trying to improve this shader, as I found out that I could use the Flip function in SpriteRenderer, I had to write "Cull Off". Then I decided to get rid of the weird, narrow outline. It appears even when "_OutlineWidth" = 0, no matter how the script is written So:

    Code (CSharp):
    1.    v2fOutline vertOutline (appdata_base v, float2 offset)
    2.         {
    3.             v2fOutline o;
    4.             o.pos = UnityObjectToClipPos(v.vertex);
    5.             o.pos.xy += offset * 2 * o.pos.w * _OutlineWidth / _ScreenParams.xy;
    6.             o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    7.             return o;
    8.         }
    Or like this:
    Code (CSharp):
    1. v2fOutline vertOutline (appdata_base v, float2 offset)
    2.         {
    3.             v2fOutline o;
    4.             v.vertex.xy += offset * _OutlineWidth;
    5.             o.pos = UnityObjectToClipPos(v.vertex);
    6.             o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    7.             return o;
    8.         }
    I solved the given problem with a small outline (even with _OutlineWidth = 0) like this:
    Code (CSharp):
    1.  fixed4 fragOutline (v2fOutline i) : SV_Target
    2.         {
    3.             fixed alpha = tex2D(_MainTex, i.uv).a;
    4.          
    5.             fixed4 col = _OutlineColor;
    6.             col.a *= alpha;
    7.             // solution
    8.             if (_OutlineWidth == 0)
    9.             {
    10.                 col.rgba = fixed4(0, 0, 0, 0);
    11.             }
    12.             //
    13.             return col;
    14.         }
    This is a good decision?

    And the last thing. I needed to make the outline adjust to the screen size (as it was at the very beginning), but with some kind of border (that is, the outline size adjusts to the distance of the camera if it is not too far (some number)) ... That is, with a very strong distance from the camera, this would not happen:

    (the picture is not that far away but this is just an example. I will be glad for any help (especially with the last question, it worries me very much) For earlier thanks)
     

    Attached Files:

  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Its "fine", but not necessarily great. A better option would probably be to swap the sprite's material to one that doesn't use the outline shader instead.

    Otherwise I'd probably fix it in the vertex shader. Setting the alpha to zero does mean you don't see it, but you're still paying the full cost of it rendering at those pixels.

    Code (csharp):
    1. v.vertex.xy += offset * _OutlineWidth;
    2. o.pos = UnityObjectToClipPos(v.vertex);
    3. if (_OutlineWidth == 0.0)
    4.     o.pos = float4(0,0,0,0);
    Doing it that way will mean the meshes get shrunk down infinitely small and all fragment shader costs for the additional passes will disappear.
     
  8. P_e_t_a_c_h_e_k

    P_e_t_a_c_h_e_k

    Joined:
    Dec 26, 2017
    Posts:
    26
    The last lines in the post above refer to the shader in its original state (where the outline size adjusts to fit the screen). I will try to ask again How about putting a limit on the maximum size of the outline? (that is, if the camera is too far, for example, then the increase in the outline width stops) This worries me, and the last thing I want to improve. And yes, thank you so much for the answer.
     
  9. P_e_t_a_c_h_e_k

    P_e_t_a_c_h_e_k

    Joined:
    Dec 26, 2017
    Posts:
    26
    I still can't solve the problem ... Can anybody help me please?
     
    Last edited: Aug 5, 2021
  10. ActuallyAscyt

    ActuallyAscyt

    Joined:
    Sep 11, 2021
    Posts:
    9
    It works, but it's a little janky when the object is rotated. Does anyone have a fixed version?

    upload_2023-5-18_14-15-38.png
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    This specific technique can't handle sharp corners, unless the edges align perfectly with the world space axis. It's an unsolvable limitation of a multi-pass setup like this. If you want outlines on arbitrarily rotated objects, you will need to do the outline using a different technique.
     
  12. ActuallyAscyt

    ActuallyAscyt

    Joined:
    Sep 11, 2021
    Posts:
    9
    And another issue: If any part of the scale is negative, the entire thing doesn't render
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Cull Off
     
  14. ActuallyAscyt

    ActuallyAscyt

    Joined:
    Sep 11, 2021
    Posts:
    9
    What?