Search Unity

"Normal Extrusion" on sprites

Discussion in 'Shaders' started by Peritectoid, Jan 22, 2017.

  1. Peritectoid

    Peritectoid

    Joined:
    Oct 14, 2015
    Posts:
    5
    I am making a shader that distorts sprites, but I need to increase the area of the sprite because otherwise distorted parts go outside of the rendered area.
    I have tried using this code example provided by Unity (see "Normal Extrusion with Vertex Modifier").
    Code (csharp):
    1. float _Amount;
    2. void vert (inout appdata_full v)
    3. {
    4.   v.vertex.xyz += v.normal * _Amount;
    5. }
    Unfortunately sprites' normals seem to point in the z-direction, so the area is not increased but rather the sprite is moved forward (or backward, depending on the value of _Amount).

    I think if I knew the position of the sprites' center (within the vertex shader code) I could pull this off.

    Thank you in advance.
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    You are correct here ... and Unity's sprite system does not have a way for you to get the data you want / need for this. Even if you did, assuming your sprites are atlased, there's not a lot of room to expand to (maybe just 2 pixels).

    You're better off doing all your distortions to the sprite vertices if you can.
     
  3. Peritectoid

    Peritectoid

    Joined:
    Oct 14, 2015
    Posts:
    5
    I gave up on automatically finding the normals and decided to just let the user manually set the pivot position. Here is the shader I came up with:
    Code (Shaderlab):
    1.  
    2. Shader "Sprites/Diffuse-Distortion"
    3. {
    4.    Properties
    5.    {
    6.        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    7.        _Color ("Tint", Color) = (1,1,1,1)
    8.        _Cutoff ("Shadow alpha cutoff", Range(0,1)) = 0.5
    9.        //Distortion map; Only the red and green channel are read
    10.        _Distortion ("Distortion map (RGB)", 2D) = "white" {}
    11.        _SpeedX("Speed X", range(-1, 1))=0.1
    12.        _SpeedY("Speed Y", range(-1, 1))=0.1
    13.        //Scale of the distortion map
    14.        _Scale("Scale", range(0.0001, 10))=1
    15.        //How many pixels are in one unit in world space
    16.        _Pixel ("Pixel per unit", int)=16
    17.        //The maximum displacement in pixels
    18.        _DisplacementX("Displacement X", range(0, 128))=1
    19.        _DisplacementY("Displacement Y", range(0, 128))=1
    20.        //For sprites that are not centered
    21.        _PivotOffsetX ("Pivot offset X", Range(-1,1))=0
    22.        _PivotOffsetY ("Pivot offset Y", Range(-1,1))=0
    23.        //Optional parameters that are to be manipulated with external scripts to, for instance, offset the additional accrued distortion of moving objects
    24.        _PositionOffsetX ("Position offset X", float)=0
    25.        _PositionOffsetY ("Position offset Y", float)=0
    26.        //Interpolation of the pixels from the distortion map
    27.        [MaterialToggle] _Smooth("Smooth", Float) = 1
    28.    }
    29.          
    30.    SubShader
    31.    {
    32.        Tags
    33.        {
    34.            "Queue"="Transparent"
    35.            "IgnoreProjector"="True"
    36.            "RenderType"="Transparent"
    37.            "PreviewType"="Plane"
    38.            "CanUseSpriteAtlas"="True"
    39.            "DisableBatching"="True"
    40.        }
    41.        LOD 200
    42.        
    43.        Cull Off
    44.        Lighting On
    45.        ZWrite Off
    46.        Blend One OneMinusSrcAlpha
    47.            
    48.        CGPROGRAM
    49.        #pragma surface surf Ramp vertex:vert alphatest:_Cutoff
    50.      
    51.        sampler2D _MainTex;
    52.        sampler2D _Distortion;
    53.      
    54.        float _SpeedX;
    55.        float _SpeedY;
    56.        float _Scale;
    57.        float _DisplacementX;
    58.        float _DisplacementY;
    59.      
    60.        struct Input
    61.        {
    62.            float2 uv_MainTex;
    63.            float2 uv_Distortion;
    64.            float3 worldPos;
    65.        };
    66.  
    67.        half4 LightingRamp (SurfaceOutput s, half3 lightDir, half atten)
    68.        {
    69.            half4 c;
    70.            c.rgb = s.Albedo;
    71.            c.a = s.Alpha;
    72.            return c;
    73.        }
    74.  
    75.        float _Pixel;
    76.        float _PivotOffsetX;
    77.        float _PivotOffsetY;
    78.        void vert (inout appdata_full v)
    79.        {
    80.            const float2 displacement=float2(_DisplacementX,_DisplacementY);
    81.            float3 direction=sign(v.vertex).xyz;
    82.            if(direction.x==0 && _PivotOffsetX!=0)
    83.            {
    84.                direction.x=_PivotOffsetX;
    85.            }
    86.            if(direction.y==0 && _PivotOffsetY!=0)
    87.            {
    88.                direction.y=-_PivotOffsetY;
    89.            }
    90.            v.vertex.xy += direction*displacement.xy/_Pixel;
    91.        }
    92.  
    93.        float Repeat (float value, float ceiling)
    94.        {
    95.            while(value<0)
    96.            {
    97.                value+=ceiling;
    98.            }
    99.            while(value>=ceiling)
    100.            {
    101.                value-=ceiling;
    102.            }
    103.            return value;
    104.        }
    105.  
    106.        float2 Repeat (float2 value, float ceiling)
    107.        {
    108.            return float2(Repeat(value.x,ceiling),Repeat(value.y,ceiling));
    109.        }
    110.  
    111.        float2 Repeat (float2 value, float2 ceiling)
    112.        {
    113.            return float2(Repeat(value.x,ceiling.x),Repeat(value.y,ceiling.y));
    114.        }
    115.  
    116.        float4 Point(sampler2D tex, float2 uv, float4 texelSize)
    117.        {
    118.            float2 halfPixel=0.5*texelSize.xy;
    119.  
    120.            uv*=texelSize.zw;
    121.            uv=floor(uv)+0.5;
    122.            uv*=texelSize.xy;
    123.  
    124.            return tex2D(_Distortion, uv);
    125.        }
    126.  
    127.        float4 Bilinear(sampler2D tex, float2 uv, float4 texelSize)
    128.        {
    129.            float2 halfPixel=0.5*texelSize.xy;
    130.  
    131.            uv*=texelSize.zw;
    132.            float2 weight=uv;
    133.            uv=round(uv);
    134.            weight-=uv;
    135.            weight+=0.5;
    136.            uv*=texelSize.xy;
    137.  
    138.            float4 pixel1=tex2D(_Distortion, Repeat(uv+float2(-halfPixel.x,-halfPixel.y),1));
    139.            float4 pixel2=tex2D(_Distortion, Repeat(uv+float2(halfPixel.x,-halfPixel.y),1));
    140.            float4 pixel3=tex2D(_Distortion, Repeat(uv+float2(-halfPixel.x,halfPixel.y),1));
    141.            float4 pixel4=tex2D(_Distortion, Repeat(uv+float2(halfPixel.x,halfPixel.y),1));
    142.            float4 pixel12=lerp(pixel1,pixel2,weight.x);
    143.            float4 pixel34=lerp(pixel3,pixel4,weight.x);
    144.  
    145.            return lerp(pixel12,pixel34,weight.y);
    146.        }
    147.  
    148.        float4 BilinearAtBorders(sampler2D tex, float2 uv, float4 texelSize)
    149.        {
    150.            float2 halfPixel=0.5*texelSize.xy;
    151.  
    152.            if(uv.x<=halfPixel.x || uv.y<=halfPixel.y || uv.x>=1-halfPixel.x || uv.y>=1-halfPixel.y)
    153.            {
    154.                return Bilinear(tex,uv,texelSize);
    155.            }
    156.            return(tex2D(tex,uv));
    157.        }
    158.  
    159.        float4 _Color;
    160.        float _Alpha;
    161.  
    162.        float4 _MainTex_TexelSize;
    163.        float4 _Distortion_TexelSize;
    164.  
    165.  
    166.        float _PositionOffsetX;
    167.        float _PositionOffsetY;
    168.        float _Smooth;
    169.  
    170.        void surf (Input IN, inout SurfaceOutput o)
    171.        {
    172.            const float2 displacement=float2(_DisplacementX,_DisplacementY);
    173.            float2 distortionSeed=float2(0,0);
    174.            float2 finalScale=_Scale*_Pixel*_Distortion_TexelSize.xy;
    175.            distortionSeed.x+=(_Time.g*_SpeedX)-_PositionOffsetX*finalScale.x;
    176.            distortionSeed.y+=(_Time.g*_SpeedY)-_PositionOffsetY*finalScale.y;
    177.            distortionSeed+=IN.worldPos.xy*finalScale;
    178.  
    179.            distortionSeed=Repeat(distortionSeed,1);
    180.  
    181.            float2 offset;
    182.            if(_Smooth!=0)
    183.            {
    184.                offset=BilinearAtBorders(_Distortion,distortionSeed,_Distortion_TexelSize).rg;
    185.            }
    186.            else
    187.            {
    188.                offset=Point(_Distortion,distortionSeed,_Distortion_TexelSize).rg;
    189.            }
    190.  
    191.            offset-=0.5;
    192.            offset*=2*displacement*_MainTex_TexelSize.xy;
    193.  
    194.            float2 uv=IN.uv_MainTex;
    195.            uv-=0.5;
    196.            uv*=_MainTex_TexelSize.xy*(_MainTex_TexelSize.zw+displacement*2);
    197.            uv+=0.5;
    198.            uv+=offset;
    199.  
    200.            fixed4 c = tex2D (_MainTex, uv);
    201.            o.Albedo=c.rgb*_Color;
    202.            o.Alpha=c.a*_Color.a;
    203.            if(uv.x<0 || uv.y<0 || uv.x>1 || uv.y>1)
    204.            {
    205.                o.Alpha=0;
    206.            }
    207.        }
    208.        ENDCG
    209.    }
    210.    Fallback "Sprites/Diffuse"
    211. }
    Here's the distortion map I use:
    noise.png

    There's still a little problem that is bugging me; I had to implement the bilinear interpolation for the borders of the distortion map myself and my solution causes ugly, "infinitely small" artifacts around them.
    I have tried for a long time and I still don't know how to get rid of them.

    This error is not immediately apparent, but once you know it's there you can't help but notice.

    If somebody is interested in trying to fix this (which I would be very grateful for) insert the following code snippet to better visualize this error:
    Code (Shaderlab):
    1.         void surf (Input IN, inout SurfaceOutput o)
    2.         {
    3.            const float2 displacement=float2(_DisplacementX,_DisplacementY);
    4.            float2 distortionSeed=float2(0,0);
    5.            float2 finalScale=_Scale*_Pixel*_Distortion_TexelSize.xy;
    6.            distortionSeed.x+=(_Time.g*_SpeedX)-_PositionOffsetX*finalScale.x;
    7.            distortionSeed.y+=(_Time.g*_SpeedY)-_PositionOffsetY*finalScale.y;
    8.            distortionSeed+=IN.worldPos.xy*finalScale;
    9.  
    10.             distortionSeed=Repeat(distortionSeed,1);
    11.  
    12.             float2 offset;
    13.             if(_Smooth!=0)
    14.             {
    15.                 offset=BilinearAtBorders(_Distortion,distortionSeed,_Distortion_TexelSize).rg;
    16.             }
    17.             else
    18.             {
    19.                 offset=Point(_Distortion,distortionSeed,_Distortion_TexelSize).rg;
    20.             }
    21.  
    22.             float2 expose=offset;
    23.  
    24.             offset-=0.5;
    25.             offset*=2*displacement*_MainTex_TexelSize.xy;
    26.  
    27.             offset=0.5;
    28.  
    29.             float2 uv=IN.uv_MainTex;
    30.             uv-=0.5;
    31.             uv*=_MainTex_TexelSize.xy*(_MainTex_TexelSize.zw+displacement*2);
    32.             uv+=0.5;
    33.             uv+=offset;
    34.  
    35.             fixed4 c = tex2D (_MainTex, uv);
    36.             o.Albedo=c.rgb*_Color;
    37.             o.Alpha=c.a*_Color.a;
    38.             if(uv.x<0 || uv.y<0 || uv.x>1 || uv.y>1)
    39.             {
    40.                 o.Alpha=0;
    41.             }
    42.             o.Albedo=expose.x;
    43.         }
    Furthermore, use this (very tiny image) as the distortion map and enable the "Smooth" parameter in the shader.
    noiseExpose.png

    If anyone has any suggestions for any further improvements, again, I'd be grateful, and if you have any questions, feel free to ask them.
     
    Last edited: Feb 25, 2017
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
  5. Peritectoid

    Peritectoid

    Joined:
    Oct 14, 2015
    Posts:
    5
    Sorry if I sound stupid, I am a bit tired right now.
    I have played around with tex2Dgrad a bit now and I am not getting the results I expected; it got even worse on a few tries.
    What do I put into ddx() and ddy()? The world position of the fragment? All the documentation I can find on it is too vague for me to grasp.

    Lastly, would this solution remove my need to manually bilinearly interpolate the border values myself?

    Thanks in advance.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    On second thought, that might not be the solution, not exactly. I haven't run the shader itself to check, but basically I'm assuming the problem you're seeing is from mip mapping.

    To explain tex2Dgrad and ddx, basically behind the scenes this line of code:
    tex2D(_Tex, uv);
    Is actually doing the equivalent of:
    tex2Dgrad(_Tex, uv, ddx(uv), ddy(uv));

    The short explanation of what ddx and ddy are is "how much does this value change between screen pixels on the x or y axis". There's some finer details there, but that's just the short version. The important part is this is how the GPU knows what mip map level to use for a texture. Does the value change a lot, use a smaller mip. Barely changes at all, use the largest mip. When you're doing UV manipulation that value can change significantly between pixels, like from your "repeat" function (which, FYI, is what the fmod() function is for). The reason to use tex2Dgrad is to tell the GPU, "ignore the derivatives of the UV I'm passing in, and use these instead to choose the mip level".

    So my updated suggestion for you is either turn off mip maps on the texture, and / or use tex2Dlod and see if that fixes the issue. If it does you'll need to do a "bit" more work if you need those mipmaps.


    If you think about this long enough and think, "wait, there's an if statement there, Bilinear() doesn't get called every pixel, so those UVs don't get calculated for every pixel, so they shouldn't be changing!", you're wrong. Because tex2D needs to know those derivatives the Bilinear() function does get run for every pixel! This is one of the situations that cause people to say "don't use if statements, they're expensive!" It's not that they're expensive, it's that they don't always work like people expect. If you switch to tex2Dlod, or use tex2Dgrad and calculate the ddx and ddy from the unmodified UVs prior to the Bilinear() function call the GPU can actually skip the unneeded work entirely.
     
    Last edited: Feb 26, 2017