Search Unity

Color swap shader breaks draw call batching [Solved]

Discussion in 'Shaders' started by ChrisJohnson, Dec 7, 2018.

  1. ChrisJohnson

    ChrisJohnson

    Joined:
    Feb 20, 2013
    Posts:
    64
    Hello everyone,

    Currently I'm working on a color swap shader to out swap colors from a sprite using the red channel of the sprite. Right now the shader is working fine except for one problem. It breaks draw call batching of the sprites.

    I'm wondering if there is a way to modify the shader so it won't break draw call batching?




    Here is the code for the shader, and the C# script that I'm using.



    Code (CSharp):
    1. Shader "Sprites/ColorSwap"
    2. {
    3.     Properties
    4.     {
    5.         [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    6.         [PerRendererData] _SwapTex("Color Data", 2D) = "transparent" {}
    7.         _Color ("Tint", Color) = (1,1,1,1)
    8.         [MaterialToggle] PixelSnap ("Pixel snap", Float) = 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.         Pass
    28.         {
    29.         CGPROGRAM
    30.             #pragma vertex vert
    31.             #pragma fragment frag
    32.             #pragma multi_compile _ PIXELSNAP_ON
    33.             #pragma multi_compile_instancing
    34.             #include "UnityCG.cginc"
    35.            
    36.             struct appdata_t
    37.             {
    38.                 float4 vertex   : POSITION;
    39.                 float4 color    : COLOR;
    40.                 float2 texcoord : TEXCOORD0;
    41.             };
    42.  
    43.             struct v2f
    44.             {
    45.                 float4 vertex   : SV_POSITION;
    46.                 fixed4 color    : COLOR;
    47.                 half2 texcoord  : TEXCOORD0;
    48.             };
    49.            
    50.             fixed4 _Color;
    51.  
    52.             v2f vert(appdata_t IN)
    53.             {
    54.                 v2f OUT;
    55.                 OUT.vertex = UnityObjectToClipPos(IN.vertex);
    56.                 OUT.texcoord = IN.texcoord;
    57.                 OUT.color = IN.color * _Color;
    58.                 #ifdef PIXELSNAP_ON
    59.                 OUT.vertex = UnityPixelSnap (OUT.vertex);
    60.                 #endif
    61.  
    62.                 return OUT;
    63.             }
    64.  
    65.             sampler2D _MainTex;
    66.             sampler2D _AlphaTex;
    67.             float _AlphaSplitEnabled;
    68.  
    69.             sampler2D _SwapTex;
    70.  
    71.             fixed4 SampleSpriteTexture (float2 uv)
    72.             {
    73.                 fixed4 color = tex2D (_MainTex, uv);
    74.  
    75.                 if (_AlphaSplitEnabled)
    76.                     color.a = tex2D (_AlphaTex, uv).r;
    77.  
    78.                 return color;
    79.             }
    80.  
    81.             fixed4 frag(v2f IN) : SV_Target
    82.             {
    83.                 fixed4 c = SampleSpriteTexture (IN.texcoord);
    84.                 fixed4 swapCol = tex2D(_SwapTex, float2(c.x, 0));
    85.                 fixed4 final = lerp(c, swapCol, swapCol.a) * IN.color;
    86.                 final.a = c.a;
    87.                 final.rgb *= c.a;
    88.  
    89.                 return final;
    90.             }
    91.         ENDCG
    92.         }
    93.     }
    94. }



    Code (CSharp):
    1. public class ColorSwapSprite : MonoBehaviour
    2. {
    3.     public SpriteRenderer spriteRenderer;
    4.     public Color32[] colors;
    5.  
    6.     public Texture2D colorSwapTexture;
    7.     private byte[] swapIndex = { 255, 223, 191, 159, 127, 95 };
    8.  
    9.     void Start()
    10.     {
    11.         SetSwapTexture();
    12.         SetColors(colors);
    13.     }
    14.  
    15.     public void SetColors(Color32[] colors)
    16.     {
    17.         for (int i = 0; i < colors.Length; i++)
    18.             colorSwapTexture.SetPixel(swapIndex[i], 0, colors[i]);
    19.  
    20.         colorSwapTexture.Apply();
    21.     }
    22.  
    23.     public void SetSwapTexture()
    24.     {
    25.         MaterialPropertyBlock properties = new MaterialPropertyBlock();
    26.         spriteRenderer.GetPropertyBlock(properties);
    27.         properties.SetTexture("_SwapTex", colorSwapTexture);
    28.         spriteRenderer.SetPropertyBlock(properties);
    29.     }
    30. }
    Thanks alot,
    Chris Johnson
     

    Attached Files:

    JesseSTG likes this.
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Yes and no.

    No change to the shader will prevent what you're doing from breaking batching, since it's not a problem stemming from the shader. You're applying a material property block to the sprite, that alone breaks batching. Any modification to the material at all prevents batching. Batching requires all sprites / meshes to be using the exact same material as is.

    Instancing does allow for modifications of the material via material property blocks, if the shader is setup for it, and only numerical values, not textures. And Unity doesn't support sprite instancing out of the box, only batching, so that's moot.


    So why did I say "yes" above"? Because you need to make two significant changes to both the script and the shader side of things for this to work.

    Change 1: Use an atlas for your swap texture. Use one giant texture that holds all of the possible palettes you want and set that texture on the shared material, or potentially as a global texture so you're not modifying the material constantly.

    Change 2: Select which palette to use from the swap texture atlas by storing the "index" in the vertex color. For sprites, setting the color on the renderer changes the vertex colors of the sprite's mesh.

    Unfortunately, baring some possible gymnastics with custom sprite meshes, the vertex color is the only things you can really control on sprite renderers from script. I'd recommend using the sprite's alpha value. This of course does mean you can no longer use the sprite's alpha to fade the sprite out. If you don't plan on coloring the sprite with the sprite renderer's color, you can use one of the other channels, red, green, or blue. Just be sure to stop using the vertex color in the shader, and make sure you're using gamma color space for your project or the vertex color's RGB values will need some additional handling to correct for sRGB when using linear color space. The alpha does not have this problem, which is why I recommended using it.

    For simplicity, I'd set the atlas to be 256 pixels high all of the time. Vertex colors are stored as Color32 values, so each component only has 256 steps (0 - 255), so unless you start using multiple color channels that's as many as you can index anyway. Then in the shader, take the vertex alpha and do this:

    fixed4 = tex2D(_SwapAtlasTex, float2(c.x, IN.color.a * (255.0/256.0) + (0.5/255.0)));

    That odd multiply and add ensures that the 0.0 to 1.0 range of IN.color.a that the shader sees gets remapped to the pixel centers of the texture.


    Also, one last thing to be mindful of. An alpha of 0 will use the swap palette at the bottom of the texture.
     
    dyupa, ChrisJohnson and SugoiDev like this.
  3. ChrisJohnson

    ChrisJohnson

    Joined:
    Feb 20, 2013
    Posts:
    64

    Thanks alot bgolus,

    I tried your method out and it looks like it is working now! I wasn't sure if I was going to be able to get it working, but now this looks like it should work for my purposes.




    Also here is the updated code for anyone who might end up running into the same type of problem.

    Code (CSharp):
    1. Shader "Sprites/ColorSwap"
    2. {
    3.     Properties
    4.     {
    5.         [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    6.         _SwapTex("Color Data", 2D) = "transparent" {}
    7.         _Color ("Tint", Color) = (1,1,1,1)
    8.         [MaterialToggle] PixelSnap ("Pixel snap", Float) = 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.         Pass
    28.         {
    29.         CGPROGRAM
    30.             #pragma vertex vert
    31.             #pragma fragment frag
    32.             #pragma multi_compile _ PIXELSNAP_ON
    33.             #pragma multi_compile_instancing
    34.             #include "UnityCG.cginc"
    35.          
    36.             struct appdata_t
    37.             {
    38.                 float4 vertex   : POSITION;
    39.                 float4 color    : COLOR;
    40.                 float2 texcoord : TEXCOORD0;
    41.             };
    42.  
    43.             struct v2f
    44.             {
    45.                 float4 vertex   : SV_POSITION;
    46.                 fixed4 color    : COLOR;
    47.                 half2 texcoord  : TEXCOORD0;
    48.             };
    49.          
    50.             fixed4 _Color;
    51.  
    52.             v2f vert(appdata_t IN)
    53.             {
    54.                 v2f OUT;
    55.                 OUT.vertex = UnityObjectToClipPos(IN.vertex);
    56.                 OUT.texcoord = IN.texcoord;
    57.                 OUT.color = IN.color * _Color;
    58.                 #ifdef PIXELSNAP_ON
    59.                 OUT.vertex = UnityPixelSnap (OUT.vertex);
    60.                 #endif
    61.  
    62.                 return OUT;
    63.             }
    64.  
    65.             sampler2D _MainTex;
    66.             sampler2D _AlphaTex;
    67.             float _AlphaSplitEnabled;
    68.  
    69.             sampler2D _SwapTex;
    70.  
    71.             fixed4 SampleSpriteTexture (float2 uv)
    72.             {
    73.                 fixed4 color = tex2D (_MainTex, uv);
    74.  
    75.                 if (_AlphaSplitEnabled)
    76.                     color.a = tex2D (_AlphaTex, uv).r;
    77.  
    78.                 return color;
    79.             }
    80.  
    81.             fixed4 frag(v2f IN) : SV_Target
    82.             {
    83.                 fixed4 c = SampleSpriteTexture (IN.texcoord);
    84.                 fixed4 swapCol = tex2D(_SwapTex, float2(c.x, IN.color.a * (255.0 / 256.0) + (0.5 / 255.0)));
    85.                 fixed4 final = lerp(c, swapCol, swapCol.a) * IN.color;
    86.                 final.a = c.a;
    87.                 final.rgb *= c.a;
    88.  
    89.                 return final;
    90.             }
    91.         ENDCG
    92.         }
    93.     }
    94. }


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class ColorSwapSprite : MonoBehaviour
    4. {
    5.     public SpriteRenderer spriteRenderer;
    6.     public Texture2D colorSwapTexture;
    7.     public byte swapTextureIndex;
    8.  
    9.     public Color32[] colors;
    10.  
    11.     private byte[] swapIndex = { 255, 223, 191, 159, 127, 95, 175 };
    12.  
    13.     void Start()
    14.     {
    15.         SetSwapTexture();
    16.         SetColors(colors);
    17.     }
    18.  
    19.     public void SetColors(Color32[] colors)
    20.     {
    21.         for (int i = 0; i < colors.Length; i++)
    22.             colorSwapTexture.SetPixel(swapIndex[i], swapTextureIndex, colors[i]);
    23.  
    24.         Color32 color = spriteRenderer.color;
    25.         color.a = swapTextureIndex;
    26.         spriteRenderer.color = color;
    27.  
    28.         colorSwapTexture.Apply();
    29.     }
    30.  
    31.     public void SetSwapTexture()
    32.     {
    33.         MaterialPropertyBlock properties = new MaterialPropertyBlock();
    34.         spriteRenderer.GetPropertyBlock(properties);
    35.         properties.SetTexture("_SwapTex", colorSwapTexture);
    36.         spriteRenderer.SetPropertyBlock(properties);
    37.     }
    38. }
     

    Attached Files:

  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Be careful here, you're multiplying the vertex color by the material's tint color property in the vertex shader. If you modify the alpha of the tint color, it'll mess up your swap index.

    If you don't plan on using the tint color property, I'd remove it entirely. Otherwise apply the color in the fragment shader instead.
     
  5. ChrisJohnson

    ChrisJohnson

    Joined:
    Feb 20, 2013
    Posts:
    64



    Cool I’ll definitely make that change. I also decided that I want to try to index the _SwapTex on both the x and y coordinates. Now I’m trying to use IN.color.b for the x coordinate, and IN.color.a for the y coordinate.


    Kind of like:

    Code (CSharp):
    1.  
    2. float4 i = IN.color;
    3. i.b = GammaToLinearSpace(i.b) / 256.0;
    4. i.a *= (255.0 / 256.0) + (0.5 / 255.0);
    5.  
    6. fixed4 swapCol = tex2D(_SwapTex, float2(c.x + i.b, i.a));

    But now I’m having a problem with the sRGB stuff that you were talking about in your first post. I tried a few different ways of converting the IN.color.b from sRGB to linear, but nothing I try seems to work.

    I tried both GammaToLinearSpace(i.b) and pow(i.b, 2.2)
     
    Last edited: Dec 8, 2018
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Why? Just to try it, or is there some specific effect you’re going for that requires more than 256 options?

    Not saying don’t do this, just wondering what you have in mind.

    Is your project using linear color space? It’s unusual for a 2D project to be using anything but gamma space, and if you’re using gamma space then this isn’t an issue. If for some reason you were using linear color space, unless you’re using real time lighting, I’d suggest switching to gamma color space. But I doubt you actually are using linear space as your sprite textures would have also needed this sRGB conversion to map property to the swap texture palettes, but they didn’t.

    If for some reason you were to use linear color space, the function you’d be wanting to use is LinearToGammaSpace(), as the shader is doing it’s math in linear space and you want the color values in the original gamma space used to define them. However for vertex colors, you’ve already lost if you’re trying to undo the initial gamma to linear conversion. Vertex colors are Color32 values and only have 256 steps of precision in each component. When you convert from, gamma to linear space and store those values as a Color32, you loose a lot of of data from quantization. Values 0/255 through 12/255 in gamma space are all represented as 0/255 in linear space. Gamma 13/255 is just barely linear 1/255. AFAIK Unity’s sprite renderers internally use a Color and not a Color32, so you should be able to apply the linear to gamma conversion in c# to counter the conversion back.

    I might also be wrong and Unity’s sprites don’t do any correction. I don’t ever use sprites, so really I’m just guessing here.

    And, again, this isn’t needed anyway since you likely should be in gamma color space for your project anyway.



    So, that was a lot of text to say “I don’t think the gamma space thing is the problem”.

    The bit of math I gave you for the alpha channel explicitly works out to index the middle of a pixel on a texture that’s 256 pixels tall. If the texture you’re using is not 256 pixels wide then you’re going to get unwanted results. It also depends on exactly what you want to accomplish. Like if you have your textures be only 8 pixels wide, do you want to index those pixels by using the color values 0/255 through 7/255, or 0/255 through 255/255? Do you want the values to use bilinear blending and be able to wrap, or do you only want the exact pixel color values?
     
  7. ChrisJohnson

    ChrisJohnson

    Joined:
    Feb 20, 2013
    Posts:
    64

    Yes, I’m using linear color space because I’m using 2D sprite lights that are rendered to a texture, then to the screen. If I use gamma color space it will wreck the look of the lights.





    Also the reason that I want to index the swap texture in 2d is so I can create color profiles that many different sprites can use.

    The game that I’m going to be using this for has various different procedurally generated sprites for creatures/trees and other types of stuff. So I want to be able to create one sprite for a creature that can use different color profiles.


    An example might be:

    • An ice wolf would use a blue color pallet
    • A fire wolf would use an orange color pallet
    • A lightning wolf would use a yellow color pallet

    but then also

    • An ice bear would use the same blue color pallet
    • A fire bear would use the same orange color pallet
    • A lightning bear would use the same yellow color pallet

    I am using a 256x256 texture so I think that part should be fine,
    but I did notice the loss of precision issue that you were talking about as well.
     
    Last edited: Dec 8, 2018
    bgolus likes this.
  8. ChrisJohnson

    ChrisJohnson

    Joined:
    Feb 20, 2013
    Posts:
    64
    Yeah, I did have that problem with the swap pallets, but there is a setting to turn off sRGB (Color Texture) in the texture import settings.
     
    bgolus likes this.
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I get that, for the first part at least, the texture row is the palette selected by the alpha. I don't understand why you want to include an x offset as well. Wouldn't that just be a constant per sprite, and if so could be built into the sprite's texture? The sprite texture, assuming you've set them to be uncompressed, have the same 0-255 range that the alpha does. Really you should be rescaling the sprite's red channel the same way you are the alpha to index specific pixels in the row if you're going to use a 256x256 swap texture.
     
    ChrisJohnson likes this.
  10. ChrisJohnson

    ChrisJohnson

    Joined:
    Feb 20, 2013
    Posts:
    64
    Yeah, that’s the way that I am probably going to end up doing it.

    An example would be:
    • Characters could have Red values 0-6
    • Enemies could have Red values 7-11
    • Trees could have Red values 12-15
    • Weapons could have Red values 16-19

    This way I can still use up most the space in the swap texture by using the alpha for the y axis, and the red values of the textures for the x axis.


    I just thought that being able to change both the x, and y values from the shader would give me more flexibility in how I could index the color profiles. But I think that your probably right that it is not worth the effort, and maybe just overkill.

    I’ll probably mess around with it a little more just to see if I can get it working. But just using the alpha and red values will probably work just as well anyways.
     
    Last edited: Dec 9, 2018
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Try:

    Color offsetColor = new Color32(0, xOffset, 0, swapIndex);
    sprite.color = offsetColor.gamma;

    That should apply the linear to gamma curve to the color value, which should negate the gamma to linear conversion Unity applies, resulting in the values you get in the shader letting you properly extract 1/255 again.
     
    ChrisJohnson likes this.
  12. ChrisJohnson

    ChrisJohnson

    Joined:
    Feb 20, 2013
    Posts:
    64
    Cool, that might of fixed it. I also had another bug in the fragment shader that was messing up the result to. But I'm going to do some more testing to make sure it is fully working.

    Edited:
    I did a little bit more testing, and now it looks like it is indexing the swap texture correctly on both the x and y axis's. Thanks for all the help bgolus! For a while I didn't think I would be able to get it working.
     
    Last edited: Dec 9, 2018
  13. darkwingfart

    darkwingfart

    Joined:
    Oct 13, 2018
    Posts:
    78
    I'm trying to apply this principle to a 3d model with a skinned mesh renderer.
    The idea is to create a base grayscale texture, and have a list of colors that map to those different grayscale sections and set the display texture colors.
    So...
    I'd have a palette. The palette maps to the grayscale locations and sets the display texture.
     
  14. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The discussion above works well for 2D content, but not great for 3D. More specifically it works well with "pixelated" point filtered textures, of which many 2D games use. If you're going for a retro PS1 look, then maybe the above may work well for you. Otherwise you're probably going to be using bilinear or better filtered textures and the above approach no longer works.

    You'll instead need to use a mask texture with a single RGBA color channel per color area you want to modify. See this article on the common naive (though totally serviceable) and "proper" way to do this:
    https://bgolus.medium.com/the-team-color-problem-b70ec69d109f

    The alternative would be to point sample the textures and do all the bilinear filtering manually in the shader. I do not suggest this option.