Las transiciones alpha de máscara consisten en aplicar una máscara alpha con un desplazamiento a una imagen.

Hacer este efecto con cualquier sistema de shaders es trivial. Por ejemplo, yo he hecho esta animación con after effects y pixel bender:

Aprovecho para mencionar el post sobre shaders que escribí hace tiempo: Charla sobre Shaders que impartí hace 3 años - DESCARGAR DEMO INTERACTIVA SOBRE LA CHARLA (8Mb)

<languageVersion : 1.0;>

kernel AlphaMask
<
    namespace : "soywiz";
    vendor : "soywiz";
    version : 1;
>
{
    input image4 src;
    input image4 mask;
    output pixel4 dst;
    parameter float offset
    <
        minValue : -1.0;
        maxValue : 1.0;
        defaultValue : 0.0;
        description : "offset";
    >;

    void evaluatePixel() {
        dst = sampleNearest(src, outCoord());
        dst.a = sampleNearest(mask, outCoord()).r + offset;
    }
}

Tras llevarme el chafón de que pese a que la GPU de los WP7 deben soportar DirectX9, no había soporte para efectos/shaders personalizados, este efecto tendría que conseguirlo con apaños antiguos.

Inicialmente pensaba que la GPU de la gráfica del WP7 estaría integrada con la memoria principal y que actualizar una textura desde la CPU no tendría por qué ser excesivamente costoso. Así que mis primeras pruebas fueron con el GetData, SetData del Texture2D.

Posibles optimizaciones:

  • Usar una estructura byte R,G,B,A para poder acceder al campo A escribiendo/leyendo de un byte en memoria
  • En vez de hacer la conversión una vez por pixel, hacer una paleta de 256 colores con el offset ya calculado, de forma que luego sea simplemente un acceso en el array
  • Evitar tanto el GetData contínuo de la imagen como el de la máscara y hacer únicamente un SetData por frame.

Pese a todas las optimizaciones, y pese a ir bien en el móvil, chupaba el 50% de la cpu. Y quedaría una cosa así (siendo lo más que logré optimizar a grandes rasgos) 256 sumas con clamping y 8004803 accesos a array:

Tras pensar un poco en cómo poder hacerlo en la GPU, tenía que encontrar una forma de escribir en el canal alpha y además hacerlo con una transformación de color. En flash es bastante fácil con el ColorTransform que permite hacer multiplicaciones y sumas por componente, pero aquí me costó bastante encontrar la forma. Al final lo conseguí, a base de ColorWriteChannels y blending aditivo y sustractivo.

Al ir a implementar esto descubrí dos cosas: no se puede escribir desde un canal de color a otro, y que el WP7 no soporta texturas con TextureFormat.Alpha8. Así que o bien cargaba una textura RGBA en PNG que tuviese ya el canal alpha adecuado, o bien cargaba una imagen RGBA y copiaba un canal de color al canal alpha con GetData y SetData (que es viable ya que el GetData y SetData se hace una única vez en vez de una vez por frame). Como es bastante más fácil trabajar con RGB de cara a crear máscaras, y por simplicidad, opté por copiar el canal a mano para empezar.

El método que utilicé es: teniendo una textura con el color y otra con la máscara en el canal alpha, renderizar la textura de color en un render target y luego aplicando un ColorWriteChannels.Alpha, escribir únicamente en el canal alpha. Copiando la máscara. Luego podemos “oscurecer” el canal alpha hasta dejarlo todo a 0 con un BlendFunction.ReverseSubtract en la primera parte de la animación, e “iluminar” el canal alpha en la segunda parte de la animación con un BlendFunction.Add.

Quedando el código algo así:

public class TransitionBlendGpu
{
	GraphicsDevice GraphicsDevice;
	SpriteBatch SpriteBatch;
	BlendState BlendAlpha1;
	BlendState BlendAlphaAdd;
	BlendState BlendAlphaSub;
	Texture2D DummyTexture;

	public TransitionBlendGpu(GraphicsDevice GraphicsDevice)
	{
		this.GraphicsDevice = GraphicsDevice;
		this.SpriteBatch = new SpriteBatch(GraphicsDevice);
		this.DummyTexture = new Texture2D(GraphicsDevice, 1, 1);
		this.DummyTexture.SetData(new Color[] { Color.White });

		BlendAlpha1 = new BlendState();
		BlendAlpha1.ColorWriteChannels = ColorWriteChannels.Alpha;
		BlendAlpha1.AlphaDestinationBlend = BlendAlpha1.ColorDestinationBlend = Blend.Zero;
		BlendAlpha1.AlphaSourceBlend = BlendAlpha1.ColorSourceBlend = Blend.One;
		BlendAlpha1.AlphaBlendFunction = BlendAlpha1.ColorBlendFunction = BlendFunction.Add;

		BlendAlphaSub = new BlendState();
		BlendAlphaSub.ColorWriteChannels = ColorWriteChannels.Alpha;
		BlendAlphaSub.AlphaDestinationBlend = BlendAlphaSub.ColorDestinationBlend = Blend.DestinationAlpha;
		BlendAlphaSub.AlphaSourceBlend = BlendAlphaSub.ColorSourceBlend = Blend.SourceAlpha;
		BlendAlphaSub.AlphaBlendFunction = BlendAlphaSub.ColorBlendFunction = BlendFunction.ReverseSubtract;

		BlendAlphaAdd = new BlendState();
		BlendAlphaAdd.ColorWriteChannels = ColorWriteChannels.Alpha;
		BlendAlphaAdd.AlphaDestinationBlend = BlendAlphaAdd.ColorDestinationBlend = Blend.DestinationAlpha;
		BlendAlphaAdd.AlphaSourceBlend = BlendAlphaAdd.ColorSourceBlend = Blend.SourceAlpha;
		BlendAlphaAdd.AlphaBlendFunction = BlendAlphaAdd.ColorBlendFunction = BlendFunction.Add;
	}


	public Texture2D GenerateAlpha(int Width, int Height)
	{
		var AlphaTexture = new Texture2D(GraphicsDevice, Width, Height);
		var WidthHeight = Width * Height;
		var Data = new RGBA[WidthHeight];
		for (int n = 0, y = 0; y < Height; y++)
		{
			for (int x = 0; x < Width; x++)
			{
				Data[n].A = (byte)(x * 255 / Width);
				n++;
			}
		}
		AlphaTexture.SetData(Data);
		return AlphaTexture;
	}

	public Texture2D GenerateAlphaFromRedChannel(Texture2D BaseImage)
	{
		return GenerateAlphaFromRedChannel(BaseImage, false);
	}

	public Texture2D GenerateAlphaFromRedChannel(Texture2D BaseImage, bool DisposeBaseImage)
	{
		var Width = BaseImage.Width;
		var Height = BaseImage.Height;
		var AlphaTexture = new Texture2D(GraphicsDevice, Width, Height);
		var WidthHeight = Width * Height;
		var Data = new RGBA[WidthHeight];
		BaseImage.GetData(Data);
		{
			for (int n = 0, y = 0; y < Height; y++)
			{
				for (int x = 0; x < Width; x++)
				{
					Data[n].A = Data[n].R;
					Data[n].R = 0;
					Data[n].G = 0;
					Data[n].B = 0;
					n++;
				}
			}
		}
		AlphaTexture.SetData(Data);
		if (DisposeBaseImage) BaseImage.Dispose();
		return AlphaTexture;
	}

	public void CombineAlpha(RenderTarget2D RenderTarget, Texture2D TextureColor, Texture2D TextureAlpha, float Step)
	{
		int Width = TextureColor.Width;
		int Height = TextureColor.Height;

		var GraphicsDevice = RenderTarget.GraphicsDevice;

		var OldRenderTargets = GraphicsDevice.GetRenderTargets();
		GraphicsDevice.SetRenderTarget(RenderTarget);
		{
			SpriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque);
			{
				SpriteBatch.Draw(TextureColor, new Vector2(0, 0), Color.White);
			}
			SpriteBatch.End();
		}
		{
			float AlphaSub = MathHelper.Clamp(1.0f - Step * 2.0f, 0.0f, 1.0f);
			float AlphaAdd = MathHelper.Clamp(Step * 2.0f - 1.0f, 0.0f, 1.0f);

			{
				SpriteBatch.Begin(SpriteSortMode.Immediate, BlendAlpha1);
				{
					SpriteBatch.Draw(
					 TextureAlpha,
					 Vector2.Zero,
					 Color.White
					);
				}
				SpriteBatch.End();
			}
			if (AlphaSub > 0)
			{
				SpriteBatch.Begin(SpriteSortMode.Immediate, BlendAlphaSub);
				{
					SpriteBatch.Draw(DummyTexture, new Rectangle(0, 0, Width, Height), new Color(AlphaSub, AlphaSub, AlphaSub, AlphaSub));
				}
				SpriteBatch.End();
			}
			if (AlphaAdd > 0)
			{
				SpriteBatch.Begin(SpriteSortMode.Immediate, BlendAlphaAdd);
				{
					SpriteBatch.Draw(DummyTexture, new Rectangle(0, 0, Width, Height), new Color(AlphaAdd, AlphaAdd, AlphaAdd, AlphaAdd));
				}
				SpriteBatch.End();
			}
		}
		GraphicsDevice.SetRenderTargets(OldRenderTargets);
	}
}