Sgt. Conker We are "absolutely fine"

26Apr/100

Article : Battlestar Galactica Text Effects in XNA

by UberGeekGames

If you’ve watched the excellent sci-fi series Battlestar Galactica (the 2004+ series, not the original!), you may have noticed that during scene transitions, they use a cool effect that draws text in strips. For example, when the story jumps from a Cylon Baseship back to Galactica, it might cut to an outside view of BSG and have text in the lower left corner of the screen noting the time and place. The text fades in strips, which I thought was a pretty cool effect. I decided to replicate that in XNA.
First, let’s look at the end result:

You can see how the text is fading in strips. What isn’t really clear from the image is that the lines are moving horizontally, instead of only vertically like a Movie Maker effect. The program uses a multi step process to achieve this:

  • The message is drawn to a rendertarget, and the texture is kept as the Destination texture.
  • A brush texture is created by drawing a vignette texture to a rendertarget, based on the speed and size variables.
  • An alpha map rendertarget is iteratively drawn on with the brush texture. Since the brush is made up of a vignette, it softens the fade in effect. A pure white texture would look much harsher.
  • The destination texture is drawn with a custom shader that takes the alpha map texture as a parameter. The alpha of the destination texture is multiplied by the color value of the alpha map texture, thus fading the text in.
  • The end result is an effect that looks much cooler than the standard “pop in text” or “fade entire text in”!

The example contains two classes in BSGText.cs:

  • BSGText
  • BSGTextManager

The latter is a manager class that makes it easier to use multiple BSGText objects at once, and the former contains the actual nuts and bolts that make the effect work. The Manager is very simple to use and is demonstrated in the example project, and I won’t cover it here as it’s fairly boring. Now, let’s go over the cool parts that make the effect work:

//create new rendertargets for the effect
            currentAlpha = new RenderTarget2D(parentManager.graphicsDevice, (int)size.X, (int)size.Y, 0, SurfaceFormat.Color);
            RenderTarget2D destination = new RenderTarget2D(parentManager.graphicsDevice, (int)size.X, (int)size.Y, 0, SurfaceFormat.Color);

//-----------------------------------------------------------------------
//| First, we want to create the destination texture.
//| The destination texture is simply the text drawn to a rendertarget,
//| and is how the text will look once it's fully transitioned in.
//-----------------------------------------------------------------------
//set the rendertarget
parentManager.graphicsDevice.SetRenderTarget(0, destination);

//clear it with a transparent color
parentManager.graphicsDevice.Clear(Color.TransparentBlack);

//begin spritebatch
parentManager.spriteBatch.Begin();

//draw the message
parentManager.spriteBatch.DrawString(parentManager.font, message, Vector2.Zero, Color.White);

//end spritebatch
parentManager.spriteBatch.End();

//set rendertarget back to the backbuffer
parentManager.graphicsDevice.SetRenderTarget(0, null);
//grab the destination texture. We don't need the destination rendertarget anymore now.
destinationTex = destination.GetTexture();

//---------------------------------------------------------------------------------
//| Now, we need to setup a blank alpha map. The alpha map is used to iteratively
//| add alpha to the destination image, thus fading it in in strips.
//---------------------------------------------------------------------------------

parentManager.graphicsDevice.SetRenderTarget(0, currentAlpha);

//all blank for right now
parentManager.graphicsDevice.Clear(Color.TransparentBlack);

parentManager.graphicsDevice.SetRenderTarget(0, null);

//=======================================================================
//| setup a new "brush" rendertarget.
//| The brush is used to iteritively add alpha to the alpha map, so the
//| text gets drawn in strips.
//=======================================================================

//setup the size based on the percentage variables
RenderTarget2D rt = new RenderTarget2D(parentManager.graphicsDevice,
(int)(destinationTex.Width * xSizePercent),
(int)(destinationTex.Height * ySizePercent),
0, SurfaceFormat.Color);

//draw the vignette texture to the rendertarget. We could use a solid color, but again,
//it wouldn't be as smooth as it is with a soft-edged vignette texture.
parentManager.graphicsDevice.SetRenderTarget(0, rt);
parentManager.graphicsDevice.Clear(Color.TransparentBlack);
parentManager.spriteBatch.Begin(SpriteBlendMode.Additive);
parentManager.spriteBatch.Draw(parentManager.vignetteTex, new Rectangle(0, 0, rt.Width, rt.Height), Color.White);
parentManager.spriteBatch.End();
parentManager.graphicsDevice.SetRenderTarget(0, null);
This is the setup code for the effect. It’s heavily documented so it should be pretty easy to follow – basically, we’re drawing the message to a rendertarget so we have the “destination” texture, creating a brush texture by drawing a vignette texture to another rendertarget, and setting up an alpha map rendertarget that we’ll use to iteritively draw the brush to.
Let’s look at the update code next:
//there are 3 strips
for (int i = 0; i < 3; i++)
{
    //the middle strip goes faster to match the effect.
    int spd = i == 1 ? speed * 2 : speed;

    //draw and move the strip
    for (int x = 0; x < spd; x++)
    {
        currentPos[i].X += brush.Width * .5f;
        UpdateText(currentPos[i]);

        //if we've passed the edge of the texture, loop back and increment the Y axis
        if (currentPos[i].X >= (destinationTex.Width + brush.Width))
        {
            //stop looping if we've reached the end of our assigned secion + plus some buffer
            if (currentPos[i].Y < (brush.Height * 2) + ((i + 1) * (destinationTex.Height / 3)))
            {
                currentPos[i].X = -brush.Width;
                currentPos[i].Y += brush.Height * .5f;
            }
            else
            isDone = true;
        }
    }
}
…
private void UpdateText(Vector2 currentPos)
{
    //grab the last result
    Texture2D prev = currentAlpha.GetTexture();

    parentManager.graphicsDevice.SetRenderTarget(0, currentAlpha);
    //clear the rendertarget
    parentManager.graphicsDevice.Clear(Color.TransparentBlack);
    parentManager.spriteBatch.Begin(SpriteBlendMode.Additive);
    //draw the last result
    parentManager.spriteBatch.Draw(prev, Vector2.Zero, Color.White);
    //and draw the new brush over it
    parentManager.spriteBatch.Draw(brush, currentPos, Color.White);
    //done!
    parentManager.spriteBatch.End();
    parentManager.graphicsDevice.SetRenderTarget(0, null);
}

We have three different positions that are kept track of in an array. These are used to draw the brush texture, and have multiple “strips” adding alpha to the alpha map at different positions. This will be clearer when you watch the output of the finished program – it looks like there are three distinct lines that are adding slices of the text in. An important note is that the middle slice is slightly faster than the outer two, hence the line that initializes the spd variable. (note: If you haven’t used or seen conditional operators before that line might look strange – it’s essentially C# shorthand for an if statement. Check out the MSDN page (http://msdn.microsoft.com/en-us/library/ty67wk28%28v=VS.100%29.aspx) for a more technical description.)
Next, here’s the draw code. There’s not much to look at – the real magic happens in the shader, which we’ll examine afterwards.

//set the shader's alpha texture to our current alpha texture
parentManager.effect.Parameters["alpha"].SetValue(currentAlpha.GetTexture());

spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.None);

//start the custom BSG text effect shader
parentManager.effect.Begin();
parentManager.effect.CurrentTechnique.Passes[0].Begin();

//draw the destination texture
spriteBatch.Draw(destinationTex, position, Color.White);

parentManager.effect.CurrentTechnique.Passes[0].End();
parentManager.effect.End();
spriteBatch.End();
And finally, here’s the shader that ties everything together:
BSGTextEffect.fx
//the texture that's being drawn with the shader
sampler TextureSampler : register(s0);

//the alpha texture map
texture2D alpha;
sampler2D alphaSampler = sampler_state
{
	Texture		=	<alpha>;
	MinFilter	=	Linear;
	MagFilter	=	Linear;
	MipFilter	=	Linear;
	AddressU	=	WRAP;
	AddressV	=	WRAP;
};

float4 PixelShader(float2 texCoord : TEXCOORD0) : COLOR0
{
    //Look up the original image color.
    float4 c = tex2D(TextureSampler, texCoord);

    //Look up the alpha value at the alpha texture.
    float4 alpha = tex2D(alphaSampler, texCoord);

    //Despite the name we don't actually use alpha :)
    //(mainly so you can more easily see it for yourself if you enable the debug text)
    //instead we use a grayscale gradient texture, so any color component will work for us.
    c.a *= alpha.r;

    return c;
}

technique BSGText
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShader();
    }
}

Nothing too complicated going on here either! The shader looks up the color of the pixel in the alpha map at the same position as the current pixel, and multiplies the alpha by it’s color. Note how the alpha map really isn’t an alpha map as we’re using it’s red color channel, but we could easily change it to use alpha if we wanted. But by using grayscale instead, it’s slightly easier to see the output for debugging, and works just the same.
So there were are. A fullly featured Battlestar Galactica text effect, using nothing more than a few rendertargets and a tiny HLSL shader! Commander Adama would be proud.

Download the sample code here

About Sgt. Conker

The Sergeant!
Comments (0) Trackbacks (1)

Leave a comment


*