Blending between alpha blended sprites

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:

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:

Shader "Sprite/Crossfade Alpha Blended" {
	Properties {
		_Fade ("Fade", Range(0, 1)) = 0
		_TexA ("Texture A", 2D) = "white" {}
		_TexB ("Texture B", 2D) = "white" {}
	}
	//Single pass for programmable pipelines
	SubShader {
		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
		ZWrite Off
		ColorMask RGB
		Blend One OneMinusSrcAlpha
		Pass {
			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#pragma fragmentoption ARB_fog_exp2
				#pragma fragmentoption ARB_precision_hint_fastest
				#include "UnityCG.cginc"
				
				struct appdata_tiny {
					float4 vertex : POSITION;
					float4 texcoord : TEXCOORD0;
					float4 texcoord1 : TEXCOORD1;
				};
				
				struct v2f { 
					float4 pos : SV_POSITION;
					float2 uv : TEXCOORD0;
					float2 uv2 : TEXCOORD1;
				};
				
				uniform float4	_TexA_ST,
								_TexB_ST;
				
				v2f vert (appdata_tiny v)
				{
					v2f o;
					o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
					o.uv = TRANSFORM_TEX(v.texcoord,_TexA);
					o.uv2 = TRANSFORM_TEX(v.texcoord1,_TexB);
					return o;
				}
				
				uniform float _Fade;
				uniform sampler2D	_TexA,
									_TexB;
				
				fixed4 frag (v2f i) : COLOR
				{
					half4	tA = tex2D(_TexA, i.uv),
							tB = tex2D(_TexB, i.uv2);
					fixed3 sum = lerp(tA.rgb * tA.a, tB.rgb * tB.a, _Fade);
					fixed alpha = lerp(tA.a, tB.a, _Fade);
					return fixed4(sum, alpha);
				}
			ENDCG
		}
	}
	// ---- Single texture cards
	SubShader {
		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
		ZWrite Off
		Pass {
			ColorMask A
			SetTexture [_TexA] {
				constantColor (0, 0, 0, [_Fade])
				combine texture * one - constant
			}
		}
		Pass {
			BindChannels {
				Bind "Vertex", vertex
				Bind "Texcoord1", texcoord0
			}
			ColorMask A
			Blend One One
			SetTexture [_TexB] {
				constantColor (0, 0, 0, [_Fade])
				combine texture * constant
			}
		}
		Pass {
			ColorMask RGB
			Blend Zero OneMinusDstAlpha
		}
		Pass {
			ColorMask RGB
			Blend SrcAlpha One
			SetTexture [_TexA] {
				constantColor ([_Fade], [_Fade], [_Fade], 0)
				combine texture * one - constant
			}
		}
		Pass {
			BindChannels {
				Bind "Vertex", vertex
				Bind "Texcoord1", texcoord0
			}
			ColorMask RGB
			Blend SrcAlpha One
			SetTexture [_TexB] {
				constantColor ([_Fade], [_Fade], [_Fade], 1)
				combine texture * constant
			}
		}
	}
}

I can get it down to three passes with two texture units:

	// ---- Dual texture cards
	SubShader {
		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
		ZWrite Off
		Pass {
			BindChannels {
				Bind "Vertex", vertex
				Bind "Texcoord", texcoord0
				Bind "Texcoord1", texcoord1
			}
			ColorMask RGB
			Blend Zero OneMinusSrcAlpha
			SetTexture [_TexA] {
				combine texture
			}
			SetTexture [_TexB] {
				constantColor (0, 0, 0, [_Fade])
				combine texture lerp(constant) previous
			}
		}
		Pass {
			ColorMask RGB
			Blend SrcAlpha One
			SetTexture [_TexA] {
				constantColor ([_Fade], [_Fade], [_Fade], 0)
				combine texture * one - constant
			}
		}
		Pass {
			BindChannels {
				Bind "Vertex", vertex
				Bind "Texcoord1", texcoord0
			}
			ColorMask RGB
			Blend SrcAlpha One
			SetTexture [_TexB] {
				constantColor ([_Fade], [_Fade], [_Fade], 1)
				combine texture * constant
			}
		}
	}

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:

SetTexture[_TexA] 
SetTexture[_TexB] {
	ConstantColor (0,0,0, [_Fade])
	Combine texture Lerp(constant) previous
}

(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:

fixed3 sum = lerp(tA.rgb * tA.a, tB.rgb * tB.a, _Fade);

Why isn’t

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?

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:

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:

fb0 = (0, 0, 0)
t1 = (1, 0, 1, 0)
t2 = (1, 1, 1, 1)
f = 0.5
fb1.rgb = fb0.rgb*lerp(t1.a, t2.a, f) + lerp(t1.rgb, t2.rgb, f)*lerp(t1.a, t2.a, f)
fb1.rgb = (0, 0, 0)*lerp(0, 1, 0.5) + lerp((1, 0, 1), (1, 1, 1), 0.5)*lerp(0, 1, 0.5)
fb1.rgb = (0, 0, 0)*0.5 + (1, 0.5, 1)*0.5
fb1.rgb = (0, 0, 0) + (0.5, 0.25, 0.5)
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:

fb1.rgb = fb0.rgb*lerp(t1.a, t2.a, f) + lerp(t1.rgb*t1.a, t2.rgb*t2.a, f)
fb1.rgb =(0, 0, 0)*lerp(0, 1, 0.5) + lerp((1, 0, 1)*0, (1, 1, 1)*1, 0.5)
fb1.rgb = (0, 0, 0)*0.5 + lerp((0, 0, 0), (1, 1, 1), 0.5)
fb1.rgb = (0, 0, 0) + (0.5, 0.5, 0.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.

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.

Pass {
	Blend SrcAlpha OneMinusSrcAlpha
	SetTexture[_TexA] {
		ConstantColor (0,0,0, [_Fade])
		Combine texture, texture * one - constant
	}
}
Pass {
	Blend SrcAlpha OneMinusSrcAlpha
	BindChannels {
		Bind "vertex", vertex
		Bind "texcoord1", texcoord
	}
	SetTexture[_TexB] {
		ConstantColor (0,0,0, [_Fade])
		Combine texture, texture * constant
	}
}

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. :stuck_out_tongue:

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.

SubShader {Pass {
	Blend One OneMinusSrcAlpha
	BindChannels {
		Bind "vertex", vertex
		Bind "texcoord", texcoord0
		Bind "texcoord1", texcoord1
		Bind "texcoord1", texcoord2
		Bind "texcoord", texcoord3
	}
	SetTexture[_TexA] {Combine texture * texture alpha}
	SetTexture[_TexB] {
		ConstantColor ([_Fade],[_Fade],[_Fade], [_Fade])
		Combine previous * one - constant, texture * constant
	}
	SetTexture[_TexB] {Combine texture * previous alpha + previous, previous}
	SetTexture[_TexA] {
		ConstantColor (0,0,0, [_Fade])
		Combine previous, texture * one - constant + previous
	}
}}

SubShader {
	Pass {
		Blend One OneMinusSrcAlpha
		BindChannels {
			Bind "vertex", vertex
			Bind "texcoord", texcoord0
			Bind "texcoord1", texcoord1
		}
		SetTexture[_TexA] {Combine texture * texture alpha, texture}
		SetTexture[_TexB] {
			ConstantColor ([_Fade],[_Fade],[_Fade], [_Fade])
			Combine previous * one - constant, texture Lerp(constant) previous
		}
	}
	Pass {
		Blend SrcAlpha One
		BindChannels {
			Bind "vertex", vertex
			Bind "texcoord1", texcoord
		}
		SetTexture[_TexB] {
			ConstantColor ([_Fade], [_Fade], [_Fade])
			Combine texture * constant, texture
		}
	}
}

SubShader {	
	Pass {
		ColorMask A
		SetTexture[_TexA] {
			ConstantColor (0,0,0, [_Fade])
			Combine texture * one - constant
		}
	}
	Pass {
		ColorMask A
		Blend One One
		BindChannels {
			Bind "vertex", vertex
			Bind "texcoord1", texcoord
		}
		SetTexture[_TexB] {
			ConstantColor (0,0,0, [_Fade])
			Combine texture * constant
		}
	}
	Pass {
		Blend SrcAlpha OneMinusDstAlpha
		SetTexture[_TexA] {
			ConstantColor ([_Fade],[_Fade],[_Fade])
			Combine texture * one - constant, texture
		}
	}
	Pass {
		Blend SrcAlpha One
		BindChannels {
			Bind "vertex", vertex
			Bind "texcoord1", texcoord
		}
		SetTexture[_TexB] {
			ConstantColor ([_Fade],[_Fade],[_Fade])
			Combine texture * constant, texture
		}
	}
}

582170–20725–$FixedFunctionStuff.shader (3.3 KB)

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.