Search Unity

Dithering shader difficulties

Discussion in 'Shaders' started by spectracoder, Jun 30, 2022.

  1. spectracoder

    spectracoder

    Joined:
    Jul 14, 2021
    Posts:
    13
    I'm trying to make a fragment shader in HLSL that makes a dithering pattern when adjusting the transparency of the shader. This shader is added to a camera with a C# script. I copied a shader from the internet that kind of did what I wanted, and modified it to my needs.

    There are 3 problems I am facing, but I can't wrap my head around what is happening:
    1. The shader follows the movement of the camera. I would like the dithering pattern to stay in place.
    2. The scaling of the dithering pattern changes when I set Unity to Maximize On Play
    3. Changing the Transparency value in Play Mode causes some kind of weird ghosting effect. In Edit Mode it works as expected.
    I'm new to shaders, and I find some things pretty confusing. If I'm doing something very stupid in this code, please be kind :)


    Code (CSharp):
    1. Shader "TestShader"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Texture", 2D) = "white" {}    
    6.         _Transparency("Transparency", Range(0,1)) = 1.0
    7.         _PixelSize("Pixel size", Range(0,1)) = 0.25
    8.         _ColorToDither("Color to dither", Color) = (1,0,0,0)
    9.     }
    10.  
    11.     SubShader
    12.     {      
    13.         Cull Off ZWrite Off ZTest Always
    14.  
    15.         Tags
    16.         {
    17.             "RenderType" = "Opaque"
    18.         }
    19.  
    20.         Pass
    21.         {
    22.             CGPROGRAM
    23.             #include "UnityCG.cginc"
    24.             #pragma vertex vert
    25.             #pragma fragment frag
    26.  
    27.             sampler2D _MainTex;        
    28.             float _Transparency;
    29.             float _PixelSize;
    30.             float4 _ColorToDither;
    31.  
    32.  
    33.             struct appdata
    34.             {
    35.                 float4 position         : POSITION;
    36.                 float4 uv               : TEXCOORD0;
    37.             };
    38.  
    39.             struct v2f
    40.             {
    41.                 float4 position         : POSITION;
    42.                 float4 uv               : TEXCOORD1;
    43.                 float4 screenPosition   : TEXCOORD2;            
    44.             };
    45.  
    46.             v2f vert(appdata v, float2 screenPosition : TEXCOORD2)
    47.             {
    48.                 v2f OUT;
    49.                 OUT.position = UnityObjectToClipPos(v.position);
    50.                 OUT.uv = v.uv;          
    51.                 OUT.screenPosition = ComputeScreenPos(OUT.position);
    52.  
    53.                 return OUT;
    54.             }
    55.  
    56.             float4 frag(v2f i) : COLOR
    57.             {        
    58.                 float4 currentColor = tex2D(_MainTex, i.uv);
    59.  
    60.                 if (currentColor.r == _ColorToDither.r && currentColor.g == _ColorToDither.g && currentColor.b == _ColorToDither.b) //Checks if the current pixel is red, to only apply dithering to red pixels              
    61.                 {
    62.      
    63.                     float DITHER_THRESHOLDS[16] =
    64.                     {
    65.                         1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
    66.                         13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
    67.                         4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
    68.                         16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
    69.                     };
    70.  
    71.                     uint index = (uint(i.position.x * _PixelSize) % 4) * 4 + uint(i.position.y * _PixelSize) % 4;
    72.                     clip(_Transparency - DITHER_THRESHOLDS[index]);                
    73.                 }            
    74.  
    75.                 return currentColor;
    76.             }
    77.             ENDCG
    78.         }
    79.     }
    This is the C# script to add the shader to the camera:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [ExecuteInEditMode]
    6. public class ShaderHandler : MonoBehaviour
    7. {
    8.     public Material material;
    9.  
    10.     private void OnRenderImage(RenderTexture source, RenderTexture destination)
    11.     {
    12.         Graphics.Blit(source, destination, material);
    13.     }
    14. }
     
    Last edited: Jun 30, 2022
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    In place relative to what? You're dithering using the screen UVs currently, so technically the dithering isn't moving as it's always the same regardless of what's in view or where the camera moves. If you want it to stay stable with the world, you'd have to pick some other plane to project onto. That gets much more complicated depending on your setup. It's plausible if you're doing this for a 2D game, but basically impossible for 3D.

    It's using the render resolution. The dither pattern is 1:1 with the screen pixels. Again, if you want it to be relative to something else, like scaled with the world, or uniform screen height, you can do that, but it gets more complicated.

    When you call clip(), what is the result you expect? Should it be black? Or the original screen color? If you want one of those options, then that's what the shader should return rather than call clip().
     
  3. spectracoder

    spectracoder

    Joined:
    Jul 14, 2021
    Posts:
    13
    Thanks for your food for thought! It's for a 2D game, by the way.

    Sorry, I wasn't very clear on that! I was hoping I could add this shader to the camera as a sort of global effect, instead of having to add it to every object in the scene. I'm currently trying to rewrite the shader to add it to each individual object, to see if I can make that work. I am still having trouble letting the dithering pattern move with the object. So, as if the dithering pattern is on the object, instead of staying fixed on the background of the scene. Does this have to do with the dithering pattern being generated instead of using a texture?

    You were right, that was a pretty simple fix. This is how the shader looks right now:

    Code (CSharp):
    1. Shader "ObjectDitherShader"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Texture", 2D) = "white" {}
    6.         _Transparency("Transparency", Range(0,1)) = 1.0
    7.         _PixelSize("Pixel size", Range(0,1)) = 0.25
    8.         _ColorToDither("Color to dither", Color) = (1,0,0,0)
    9.     }
    10.  
    11.     SubShader
    12.     {
    13.         Tags
    14.         {
    15.             "Queue" = "Transparent"
    16.             "IgnoreProjector" = "True"
    17.             "RenderType" = "Transparent"
    18.             "PreviewType" = "Plane"
    19.             "CanUseSpriteAtlas" = "True"
    20.         }
    21.  
    22.         Cull Off
    23.         Lighting Off
    24.         ZWrite Off
    25.         Blend One OneMinusSrcAlpha
    26.  
    27.  
    28.  
    29.         Pass
    30.         {
    31.             CGPROGRAM
    32.             #include "UnityCG.cginc"
    33.             #pragma vertex vert
    34.             #pragma fragment frag
    35.  
    36.             sampler2D _MainTex;    
    37.             float _Transparency;
    38.             float _PixelSize;
    39.             float4 _ColorToDither;    
    40.  
    41.  
    42.             struct vertex
    43.             {
    44.                 float4 position         : POSITION;
    45.                 float4 uv               : TEXCOORD0;
    46.             };
    47.  
    48.             struct v2f
    49.             {
    50.                 float4 position         : SV_POSITION; //clip space position
    51.                 float4 uv               : TEXCOORD0;                    
    52.             };
    53.  
    54.  
    55.             v2f vert(vertex v)
    56.             {
    57.                 v2f OUT;
    58.                 OUT.position = UnityObjectToClipPos(v.position);
    59.                 OUT.uv = v.uv;      
    60.  
    61.                 return OUT;
    62.             }
    63.  
    64.             float4 frag(v2f i) : SV_Target
    65.             {    
    66.                 float4 currentColor = tex2D(_MainTex, i.uv);
    67.  
    68.                 if (currentColor.r == _ColorToDither.r && currentColor.g == _ColorToDither.g && currentColor.b == _ColorToDither.b) //Checks if the current pixel is red        
    69.                 {
    70.                     // Define a dither threshold matrix which can
    71.                     // be used to define how a 4x4 set of pixels
    72.                     // will be dithered
    73.            
    74.                     float DITHER_THRESHOLDS[16] =
    75.                     {
    76.                         1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
    77.                         13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
    78.                         4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
    79.                         16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
    80.                     };
    81.            
    82.                     uint index = uint(i.position.x * _PixelSize) % 4  + uint(i.position.y * _PixelSize) % 4 * 4;        
    83.  
    84.                     if (_Transparency - DITHER_THRESHOLDS[index] <= 0)
    85.                     {
    86.                         return float4(0.0f, 0.0f, 0.0f, 1.0f); //black
    87.                     }
    88.                 }
    89.          
    90.  
    91.                 return currentColor;
    92.             }
    93.             ENDCG
    94.         }
    95.     }
    96.  
     
    Last edited: Jul 2, 2022
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    If you want the dithering to stay “attached” to individual objects as they move, you will indeed need to apply the dithering shader on the objects themselves and not on the camera. The camera only knows the screen UVs and the color of the pixels that have been rendered previously. There’s no concept of individual objects at that point. Using a hard coded dither pattern or a texture is mostly irrelevant (though it might look nicer using a texture, depending on what you want it to look like).

    As for it sticking to objects vs the background, using the UVs of the objects should do that for you. Though if your objects aren’t at a consistent UV to pixel size it may cause some issues for you, and using the UV of 3D meshes can look very odd.

    Really, without a more specific explanation / visual example of what you’re trying to do I can’t really give you any good recommendations as how to do what you want can change significantly. Usually the end goal with dithering is you want a pattern that is either fairly large and “graphic” (like obvious circles, or stars, etc), or always perfectly aligned to the screen pixels. Having the effect be attached to the camera’s view, or to individual objects changes how you need to calculate the UVs for the dithering, as well as if it needs to be done on the individual objects or not. If it’s a 3D object and you want the pattern to face the camera, but not move with it, then you need to calculate the screen space UVs, and then offset that by the screen space position of the object’s pivot so the screen UVs are counter moved. If they’re sprites, which don’t have a usable pivot in the shader, you may need to rely on the sprite’s existing UVs, and either use the _MainTex_TexelSize or screen space derivatives of the UV to keep the scale consistent. Etc, etc, etc.
     
  5. spectracoder

    spectracoder

    Joined:
    Jul 14, 2021
    Posts:
    13
    I thought there could be the possibilty of moving the dither pattern a few pixels in the opposite direction of where the camera is going, using offset parameters passed in the shader by a C# script. But, no matter what I try, I can't seem to move the pattern. Even when I attach the shader to an object.

    This is how I want it to look, to simulate day and night in the game. The red pixels need to fade in and out using a dithering pattern based on the transparency value. The red pixels will eventually be white, but this makes it more clear how the effect should work.


    (Looks kind of crappy with all the compression, but I think you'll get the point)

     
    Last edited: Jul 4, 2022
  6. spectracoder

    spectracoder

    Joined:
    Jul 14, 2021
    Posts:
    13
    So, any ideas?
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Since you already have a texel size defined by your artwork, I'd use that for most things.

    Code (csharp):
    1. sampler2D _MainTex;
    2. float4 _MainTex_TexelSize; // add this line
    3.  
    4. // in the frag
    5. uint index = uint(i.uv.x * _MainTex_TexelSize.z + 0.5) % 4  + uint(i.uv.y * _MainTex_TexelSIze.w + 0.5) % 4 * 4;
    For the background, which presumably isn't using a big white sprite that's the "correct" resolution for the pixel art, you'll either need to use a sprite that is the correct resolution, or you'll need to do something like you were before where you set the "PixelSize" manually. But still use the
    i.uv
    instead of the
    i.position
    .
     
    spectracoder likes this.
  8. spectracoder

    spectracoder

    Joined:
    Jul 14, 2021
    Posts:
    13
    Wow, thank you so much! That's exactly what I needed! It now follows the sprites perfectly.
    Can you explain what the +0.5 is supposed to do? When I remove it, the dithering pattern is perfectly aligned with the artwork. So can I leave it that way, or does that have certain consequences?
     
    Last edited: Jul 12, 2022
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    If it works better w/o, use that!
     
  10. spectracoder

    spectracoder

    Joined:
    Jul 14, 2021
    Posts:
    13
    Thanks, then I will use it without!

    One last thing, then I will leave you alone! :)

    When I add the shader to sprites that are used in a tilemap, some weird clipping occurs.
    The clipping is gone when I uncheck the "Alpha is transparency" option in the sprite properties. It doesn't seem like unchecking it has any negative consequences though, but since it's on by default I would rather leave it on.
    This clipping doesn't happen with the Sprites-default shader. So, is there anything in the dithering shader that could be causing this?




    What it looks like with "Alpha is transparency" enabled:


    What it looks like with "Alpha is transparency" disabled:
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Alpha is Transparency clears anything in the RGB of the image where the alpha is completely transparent. But it clears it to the color of the closest visible texel. The reason for this is if you’re using bilinear or trilinear filtering on the texture, you don’t want the color that it interpolates to to be black or any other color but the one at the edge otherwise you can get weird color fringes.

    For your shader, you’re checking the color, and outputting a color, but not checking the alpha value. As you’re using point filtering, you don’t need to worry about it. But you could probably fix it by checking the alpha before doing the dithering code.
     
    spectracoder likes this.
  12. spectracoder

    spectracoder

    Joined:
    Jul 14, 2021
    Posts:
    13
    Yet again, thank you so much for your explanations and nudges in the right direction!

    For future reference here is the full shader:

    Code (CSharp):
    1. Shader "DitherShader"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Texture", 2D) = "white" {}
    6.         _Transparency("Transparency", Range(0,1)) = 1.0
    7.         _ColorToDither("Color to dither", Color) = (1,0,0,0)  
    8.     }
    9.  
    10.         SubShader
    11.         {
    12.             Tags
    13.             {
    14.                 "Queue" = "Transparent"
    15.                 "IgnoreProjector" = "True"
    16.                 "RenderType" = "Transparent"
    17.                 "PreviewType" = "Plane"
    18.                 "CanUseSpriteAtlas" = "True"
    19.             }
    20.  
    21.             Cull Off
    22.             Lighting Off
    23.             ZWrite Off
    24.             Blend One OneMinusSrcAlpha
    25.  
    26.  
    27.  
    28.             Pass
    29.             {
    30.                 CGPROGRAM
    31.                 #pragma vertex vert
    32.                 #pragma fragment frag              
    33.  
    34.                 #include "UnityCG.cginc"
    35.  
    36.                 sampler2D _MainTex;
    37.                 float4 _MainTex_TexelSize;
    38.                 float _Transparency;
    39.                 float _PixelSize;
    40.                 float4 _ColorToDither;
    41.                              
    42.  
    43.                 struct vertex
    44.                 {
    45.                     float4 position         : POSITION;
    46.                     float4 uv               : TEXCOORD0;
    47.                 };
    48.  
    49.                 struct v2f
    50.                 {
    51.                     float4 position         : SV_POSITION; //clip space position              
    52.                     float4 uv               : TEXCOORD0;
    53.  
    54.                 };
    55.  
    56.  
    57.                 v2f vert(vertex v)
    58.                 {
    59.                     v2f OUT;
    60.                     OUT.position = UnityObjectToClipPos(v.position);
    61.                     OUT.uv = v.uv;
    62.  
    63.  
    64.                     return OUT;
    65.                 }
    66.  
    67.                 float4 frag(v2f i) : SV_Target
    68.                 {
    69.  
    70.                     float4 currentColor = tex2D(_MainTex, i.uv);
    71.  
    72.                     float4 black = float4(0.0f, 0.0f, 0.0f, 1.0f);
    73.  
    74.                     if (currentColor.a == 0.0f) //Prevents clipping of transparent parts when "Alpha is Transparency" is turned on in the sprite properties.
    75.                     {
    76.                         discard;
    77.                     }
    78.                    
    79.  
    80.                     if (currentColor.r == _ColorToDither.r && currentColor.g == _ColorToDither.g && currentColor.b == _ColorToDither.b) //Checks if the current pixel is red              
    81.                     {                      
    82.  
    83.                         // Define a dither threshold matrix which can
    84.                         // be used to define how a 4x4 set of pixels
    85.                         // will be dithered
    86.                         float DITHER_THRESHOLDS[16] =
    87.                         {
    88.                             1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
    89.                             13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
    90.                             4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
    91.                             16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
    92.                         };
    93.  
    94.  
    95.                         uint index = uint(i.uv.x * _MainTex_TexelSize.z) % 4 + uint(i.uv.y * _MainTex_TexelSize.w) % 4 * 4;
    96.  
    97.  
    98.                         if (_Transparency - DITHER_THRESHOLDS[index] <= 0)
    99.                         {
    100.                             return black;
    101.                         }
    102.                     }
    103.  
    104.                     return currentColor;
    105.                 }
    106.                 ENDCG
    107.             }
    108.         }
    109. }