Search Unity

Blending between alpha blended sprites

Discussion in 'Shaders' started by Daniel_Brauer, May 19, 2011.

  1. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    Alpha blending is great, but sometimes you need to smoothly transition from one alpha blended sprite to another. The best formula I've come up with for interpolating between two textures with alpha and then blending them against the background is this:

    Code (csharp):
    1. fb1.rgb = fb0.rgb*lerp(t1.a, t2.a, f) + lerp(t1.rgb*t1.a, t2.rgb*t2.a, f)
    This is another of those situations where pre-multiplied alpha would really help, but we're currently using traditional textures with explicit alpha.

    Through some messy use of the frame buffer's alpha channel, I managed to cram this function into a five pass (I know) fixed function shader that only requires one texture unit.

    The idea is that five passes is better than pink on horrible old hardware, but I was wondering if any of you fixed function gurus could do better:

    Code (csharp):
    1. Shader "Sprite/Crossfade Alpha Blended" {
    2.     Properties {
    3.         _Fade ("Fade", Range(0, 1)) = 0
    4.         _TexA ("Texture A", 2D) = "white" {}
    5.         _TexB ("Texture B", 2D) = "white" {}
    6.     }
    7.     //Single pass for programmable pipelines
    8.     SubShader {
    9.         Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
    10.         ZWrite Off
    11.         ColorMask RGB
    12.         Blend One OneMinusSrcAlpha
    13.         Pass {
    14.             CGPROGRAM
    15.                 #pragma vertex vert
    16.                 #pragma fragment frag
    17.                 #pragma fragmentoption ARB_fog_exp2
    18.                 #pragma fragmentoption ARB_precision_hint_fastest
    19.                 #include "UnityCG.cginc"
    20.                
    21.                 struct appdata_tiny {
    22.                     float4 vertex : POSITION;
    23.                     float4 texcoord : TEXCOORD0;
    24.                     float4 texcoord1 : TEXCOORD1;
    25.                 };
    26.                
    27.                 struct v2f {
    28.                     float4 pos : SV_POSITION;
    29.                     float2 uv : TEXCOORD0;
    30.                     float2 uv2 : TEXCOORD1;
    31.                 };
    32.                
    33.                 uniform float4  _TexA_ST,
    34.                                 _TexB_ST;
    35.                
    36.                 v2f vert (appdata_tiny v)
    37.                 {
    38.                     v2f o;
    39.                     o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
    40.                     o.uv = TRANSFORM_TEX(v.texcoord,_TexA);
    41.                     o.uv2 = TRANSFORM_TEX(v.texcoord1,_TexB);
    42.                     return o;
    43.                 }
    44.                
    45.                 uniform float _Fade;
    46.                 uniform sampler2D   _TexA,
    47.                                     _TexB;
    48.                
    49.                 fixed4 frag (v2f i) : COLOR
    50.                 {
    51.                     half4   tA = tex2D(_TexA, i.uv),
    52.                             tB = tex2D(_TexB, i.uv2);
    53.                     fixed3 sum = lerp(tA.rgb * tA.a, tB.rgb * tB.a, _Fade);
    54.                     fixed alpha = lerp(tA.a, tB.a, _Fade);
    55.                     return fixed4(sum, alpha);
    56.                 }
    57.             ENDCG
    58.         }
    59.     }
    60.     // ---- Single texture cards
    61.     SubShader {
    62.         Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
    63.         ZWrite Off
    64.         Pass {
    65.             ColorMask A
    66.             SetTexture [_TexA] {
    67.                 constantColor (0, 0, 0, [_Fade])
    68.                 combine texture * one - constant
    69.             }
    70.         }
    71.         Pass {
    72.             BindChannels {
    73.                 Bind "Vertex", vertex
    74.                 Bind "Texcoord1", texcoord0
    75.             }
    76.             ColorMask A
    77.             Blend One One
    78.             SetTexture [_TexB] {
    79.                 constantColor (0, 0, 0, [_Fade])
    80.                 combine texture * constant
    81.             }
    82.         }
    83.         Pass {
    84.             ColorMask RGB
    85.             Blend Zero OneMinusDstAlpha
    86.         }
    87.         Pass {
    88.             ColorMask RGB
    89.             Blend SrcAlpha One
    90.             SetTexture [_TexA] {
    91.                 constantColor ([_Fade], [_Fade], [_Fade], 0)
    92.                 combine texture * one - constant
    93.             }
    94.         }
    95.         Pass {
    96.             BindChannels {
    97.                 Bind "Vertex", vertex
    98.                 Bind "Texcoord1", texcoord0
    99.             }
    100.             ColorMask RGB
    101.             Blend SrcAlpha One
    102.             SetTexture [_TexB] {
    103.                 constantColor ([_Fade], [_Fade], [_Fade], 1)
    104.                 combine texture * constant
    105.             }
    106.         }
    107.     }
    108. }
    109.  
     
  2. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    I can get it down to three passes with two texture units:
    Code (csharp):
    1.     // ---- Dual texture cards
    2.     SubShader {
    3.         Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
    4.         ZWrite Off
    5.         Pass {
    6.             BindChannels {
    7.                 Bind "Vertex", vertex
    8.                 Bind "Texcoord", texcoord0
    9.                 Bind "Texcoord1", texcoord1
    10.             }
    11.             ColorMask RGB
    12.             Blend Zero OneMinusSrcAlpha
    13.             SetTexture [_TexA] {
    14.                 combine texture
    15.             }
    16.             SetTexture [_TexB] {
    17.                 constantColor (0, 0, 0, [_Fade])
    18.                 combine texture lerp(constant) previous
    19.             }
    20.         }
    21.         Pass {
    22.             ColorMask RGB
    23.             Blend SrcAlpha One
    24.             SetTexture [_TexA] {
    25.                 constantColor ([_Fade], [_Fade], [_Fade], 0)
    26.                 combine texture * one - constant
    27.             }
    28.         }
    29.         Pass {
    30.             BindChannels {
    31.                 Bind "Vertex", vertex
    32.                 Bind "Texcoord1", texcoord0
    33.             }
    34.             ColorMask RGB
    35.             Blend SrcAlpha One
    36.             SetTexture [_TexB] {
    37.                 constantColor ([_Fade], [_Fade], [_Fade], 1)
    38.                 combine texture * constant
    39.             }
    40.         }
    41.     }
     
  3. Jessy

    Jessy

    Joined:
    Jun 7, 2007
    Posts:
    7,325
    Well, regardless of if this is what you want, I'd recommend this thing you already wrote for old hardware, just because it's one pass:
    Code (csharp):
    1. SetTexture[_TexA]
    2. SetTexture[_TexB] {
    3.     ConstantColor (0,0,0, [_Fade])
    4.     Combine texture Lerp(constant) previous
    5. }
    (Remember, the calculation is for all four channels, not just RGB.)

    But I'll try harder to match what you want, if you can explain to me your rationale for:
    Code (csharp):
    1. fixed3 sum = lerp(tA.rgb * tA.a, tB.rgb * tB.a, _Fade);
    Why isn't
    Code (csharp):
    1. lerp(tA.rgb, tB.rgb, _Fade)
    good enough? Is it just yielding a more pleasing curve? Are you looking to reduce transparency when you're in the middle of an interpolation?
     
  4. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    A linear interpolation between the two textures is not quite the same as crossfading. Here's the math for a lerp as you suggest, but expanded so it can easily be compared to my original equation:
    Code (csharp):
    1. fb1.rgb = fb0.rgb*lerp(t1.a, t2.a, f) + lerp(t1.rgb, t2.rgb, f)*lerp(t1.a, t2.a, f)
    The problem with this approach is that it does not treat the two textures' alpha channels separately. This becomes an issue when crossfading between transparent and non-transparent pixels. For instance, transparent magenta and solid white on a black background:
    Code (csharp):
    1. fb0 = (0, 0, 0)
    2. t1 = (1, 0, 1, 0)
    3. t2 = (1, 1, 1, 1)
    4. f = 0.5
    5. fb1.rgb = fb0.rgb*lerp(t1.a, t2.a, f) + lerp(t1.rgb, t2.rgb, f)*lerp(t1.a, t2.a, f)
    6. fb1.rgb = (0, 0, 0)*lerp(0, 1, 0.5) + lerp((1, 0, 1), (1, 1, 1), 0.5)*lerp(0, 1, 0.5)
    7. fb1.rgb = (0, 0, 0)*0.5 + (1, 0.5, 1)*0.5
    8. fb1.rgb = (0, 0, 0) + (0.5, 0.25, 0.5)
    9. fb1.rgb = (0.5, 0.25, 0.5)
    Transparent magenta shouldn't find its way into the result, but this method does the alpha blending after the colours have already been mixed. Contrast with the original blending function:
    Code (csharp):
    1. fb1.rgb = fb0.rgb*lerp(t1.a, t2.a, f) + lerp(t1.rgb*t1.a, t2.rgb*t2.a, f)
    2. fb1.rgb =(0, 0, 0)*lerp(0, 1, 0.5) + lerp((1, 0, 1)*0, (1, 1, 1)*1, 0.5)
    3. fb1.rgb = (0, 0, 0)*0.5 + lerp((0, 0, 0), (1, 1, 1), 0.5)
    4. fb1.rgb = (0, 0, 0) + (0.5, 0.5, 0.5)
    5. fb1.rgb = (0.5, 0.5, 0.5)
    The single texture unit solution is the one I would like to optimize, because those machines need all the help they can get.
     
  5. Jessy

    Jessy

    Joined:
    Jun 7, 2007
    Posts:
    7,325
    Right. Sorry, I missed the Blend One OneMinusSrcAlpha in your Cg shader. Has this become a problem for you in practice? I can't think of a way to do it in one pass on obsolete hardware, so I'll stick with what I recommended in my other post, and offer the following code, for single-texture cards.

    It all depends on the artwork, whether it works or not. You haven't said why you can't just have black as a bakground color, although you said "premultiplying", which that is, would help. In Cg, your shader should look better, (if you're using compression, anyway), because premultiplied blending is never going to look as good as GPU-computed alpha blending, unless the textures are uncompressed. But on older hardware, I'd go for the fast-and-not-quite-right route, instead of trying to get perfect results. I might use Solidify on the images, and then fill in with black, wherever the alpha is zero, which should yield good results even without your Cg.

    Code (csharp):
    1. Pass {
    2.     Blend SrcAlpha OneMinusSrcAlpha
    3.     SetTexture[_TexA] {
    4.         ConstantColor (0,0,0, [_Fade])
    5.         Combine texture, texture * one - constant
    6.     }
    7. }
    8. Pass {
    9.     Blend SrcAlpha OneMinusSrcAlpha
    10.     BindChannels {
    11.         Bind "vertex", vertex
    12.         Bind "texcoord1", texcoord
    13.     }
    14.     SetTexture[_TexB] {
    15.         ConstantColor (0,0,0, [_Fade])
    16.         Combine texture, texture * constant
    17.     }
    18. }
    With all of this stuff, there may be optimizations to be made on scalar vs. vector hardware. You seem to be writing for vector, but I'm used to iOS/POWERVR, where it's scalar, so you may want to make small revisions depending on your target. I've thought of a couple ways to incorporate the distinct blending modes for DstAlpha if one were to go that route, so keep it in mind if you want to. As it is, I've got it in 1, 2, and 4 passes. I have doubts about everything working right on the actual cards you're targeting, though. Something always likes to go haywire with fixed function stuff, in my limited experience, but they at least all look the same on my computer. :p

    I also never pay attention to the alpha that's getting written to the screen in the end, so let me know if that's a problem, and you want some help with it.
    Code (csharp):
    1. SubShader {Pass {
    2.     Blend One OneMinusSrcAlpha
    3.     BindChannels {
    4.         Bind "vertex", vertex
    5.         Bind "texcoord", texcoord0
    6.         Bind "texcoord1", texcoord1
    7.         Bind "texcoord1", texcoord2
    8.         Bind "texcoord", texcoord3
    9.     }
    10.     SetTexture[_TexA] {Combine texture * texture alpha}
    11.     SetTexture[_TexB] {
    12.         ConstantColor ([_Fade],[_Fade],[_Fade], [_Fade])
    13.         Combine previous * one - constant, texture * constant
    14.     }
    15.     SetTexture[_TexB] {Combine texture * previous alpha + previous, previous}
    16.     SetTexture[_TexA] {
    17.         ConstantColor (0,0,0, [_Fade])
    18.         Combine previous, texture * one - constant + previous
    19.     }
    20. }}
    21.  
    22. SubShader {
    23.     Pass {
    24.         Blend One OneMinusSrcAlpha
    25.         BindChannels {
    26.             Bind "vertex", vertex
    27.             Bind "texcoord", texcoord0
    28.             Bind "texcoord1", texcoord1
    29.         }
    30.         SetTexture[_TexA] {Combine texture * texture alpha, texture}
    31.         SetTexture[_TexB] {
    32.             ConstantColor ([_Fade],[_Fade],[_Fade], [_Fade])
    33.             Combine previous * one - constant, texture Lerp(constant) previous
    34.         }
    35.     }
    36.     Pass {
    37.         Blend SrcAlpha One
    38.         BindChannels {
    39.             Bind "vertex", vertex
    40.             Bind "texcoord1", texcoord
    41.         }
    42.         SetTexture[_TexB] {
    43.             ConstantColor ([_Fade], [_Fade], [_Fade])
    44.             Combine texture * constant, texture
    45.         }
    46.     }
    47. }
    48.  
    49. SubShader {
    50.     Pass {
    51.         ColorMask A
    52.         SetTexture[_TexA] {
    53.             ConstantColor (0,0,0, [_Fade])
    54.             Combine texture * one - constant
    55.         }
    56.     }
    57.     Pass {
    58.         ColorMask A
    59.         Blend One One
    60.         BindChannels {
    61.             Bind "vertex", vertex
    62.             Bind "texcoord1", texcoord
    63.         }
    64.         SetTexture[_TexB] {
    65.             ConstantColor (0,0,0, [_Fade])
    66.             Combine texture * constant
    67.         }
    68.     }
    69.     Pass {
    70.         Blend SrcAlpha OneMinusDstAlpha
    71.         SetTexture[_TexA] {
    72.             ConstantColor ([_Fade],[_Fade],[_Fade])
    73.             Combine texture * one - constant, texture
    74.         }
    75.     }
    76.     Pass {
    77.         Blend SrcAlpha One
    78.         BindChannels {
    79.             Bind "vertex", vertex
    80.             Bind "texcoord1", texcoord
    81.         }
    82.         SetTexture[_TexB] {
    83.             ConstantColor ([_Fade],[_Fade],[_Fade])
    84.             Combine texture * constant, texture
    85.         }
    86.     }
    87. }
     

    Attached Files:

    Last edited: May 20, 2011
  6. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    These are great! Rolling the destination multiply into the first additive stage is a good idea.

    The only problem I had is that my computer (iMac with Radeon 2600 HD) doesn't support the four combiner version. I don't see why: I know my driver exposes four texture units, and I don't see anything wrong with the shader.

    This shader is a fix for an existing game, so we don't have the opportunity to change all the assets. Future games will all use pre-multiplied alpha, which will solve this and a number of other problems.