Work with Dynamic Textures in Windows Phone 7


This chapter is taken from book "Programming Windows Phone 7" by Charles Petzold published by Microsoft press. http://www.charlespetzold.com/phone/index.html

The most common way for an XNA program to obtain a Texture2D object is by loading it as program content. It is also possible to create a Texture2D object entirely in code using this constructor:

Texture2D texture = new Texture2D(this.GraphicsDevice, width, height);

The width and height arguments are integers that indicate the desired size of the Texture2D in pixels; this size cannot be changed after the Texture2D is created. The total number of pixels in the bitmap is easily calculated as width * height. The result is a bitmap filled with zeros. So now the big question is: How do you get actual stuff onto the surface of this bitmap?

You have two ways:

  • Draw on the bitmap surface just as you draw on the video display.
  • Algorithmically manipulate the actual pixel bits that make up the bitmap.

You can use these two techniques separately, or in combination with each other. You can also begin with an existing image, and modify it using these techniques.

The Render Target

Strictly speaking, you actually can't use the first of the two techniques with a Texture2D object. You need to create an instance of a class that derives from Texture2D called RenderTarget2D:

RenderTarget2D renderTarget = new RenderTarget2D(this.GraphicsDevice, width, height);

As with any code that references the GraphicsDevice property of the Game class, you'll want to wait until the LoadContent method to create any Texture2D or RenderTarget2D objects your program needs. You'll usually be storing the objects in fields so you can display them later on in the Draw override.

If you're creating a RenderTarget2D that remains the same for the duration of the program, you'll generally perform this entire operation during the LoadContent override. If the RenderTarget2D needs to change, you can also draw on the bitmap during the Update override. Because RenderTarget2D derives from Texture2D you can display the RenderTarget2D on the screen during your Draw override just as you display any other Texture2D image.

Of course, you're not limited to one RenderTarget2D object. If you have a complex series of images that form some kind of animation, you can create a series of RenderTarget2D objects that you then display in sequence as a kind of movie.

Suppose you want to display something that looks like this:

11111111.gif

That's a bunch of text strings all saying "Windows Phone 7" rotated around a center point with colors that vary between cyan and yellow. Of course, you can have a loop in the Draw override that makes 32 calls to the DrawString method of SpriteBatch, but if you assemble those text strings on a single bitmap, you can reduce the Draw override to just a single call to the Draw method of SpriteBatch. Moreover, it becomes easier to treat this assemblage of text strings as a single entity, and then perhaps rotate it like a pinwheel.

That's the idea behind the PinwheelText program. The program's content includes the 14point Segoe UI Mono SpriteFont, but a SpriteFont object is not included among the program's fields, nor is the text itself:

public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch; 
        Vector2 screenCenter;
        RenderTarget2D renderTarget;
        Vector2 textureCenter;
        float rotationAngle;
        ...
    }

The LoadContent method is the most involved part of the program, but it only results in setting the screenCenter, renderTarget, and textureCenter fields. The segoe14 and textSize variables set early on in the method are normally saved as fields but here they're only required locally:

protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice); 
            // Get viewport info
            Viewport viewport = this.GraphicsDevice.Viewport;
            screenCenter = new Vector2(viewport.Width / 2, viewport.Height / 2);
            // Load font and get text size
            SpriteFont segoe14 = this.Content.Load<SpriteFont>("Segoe14");
            string text = " Windows Phone 7";
            Vector2 textSize = segoe14.MeasureString(text); 
            // Create RenderTarget2D
            renderTarget =
                new RenderTarget2D(this.GraphicsDevice, 2 * (int)textSize.X,
                                                        2 * (int)textSize.X); 
            // Find center
            textureCenter = new Vector2(renderTarget.Width / 2,
                                        renderTarget.Height / 2); 
            Vector2 textOrigin = new Vector2(0, textSize.Y / 2); 
            // Set the RenderTarget2D to the GraphicsDevice       
            this.GraphicsDevice.SetRenderTarget(renderTarget); 
            // Clear the RenderTarget2D and render the text
            this.GraphicsDevice.Clear(Color.Transparent);
            spriteBatch.Begin(); 
            for (float t = 0; t < 1; t += 1f / 32)
            {
                float angle = t * MathHelper.TwoPi;
                Color clr = Color.Lerp(Color.Cyan, Color.Yellow, t);
                spriteBatch.DrawString(segoe14, text, textureCenter, clr,
                                       angle, textOrigin, 1, SpriteEffects.None, 0);
            } 
            spriteBatch.End(); 
            // Restore the GraphicsDevice back to normal
            this.GraphicsDevice.SetRenderTarget(null);
        }

The RenderTarget2D is created with a width and height that is twice the width of the text string. The RenderTarget2D is set into the GraphicsDevice with a call to SetRenderTarget and then cleared to a transparent color with the Clear method. At this point a sequence of calls on the SpriteBatch object renders the text 32 times on the RenderTarget2D. The LoadContent call concludes by restoring the GraphicsDevice to the normal back buffer.

The Update method calculates a rotation angle for the resultant bitmap so it rotates 360° every eight seconds:

protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit(); 
            rotationAngle =
                (MathHelper.TwoPi * (float) gameTime.TotalGameTime.TotalSeconds / 8) %
                                                        MathHelper.TwoPi;
            base.Update(gameTime);
        } 

As promised, the Draw override can then treat that RenderTarget2D as a normal Texture2D in a single Draw call on the SpriteBatch. All 32 text strings seem to rotate in unison:

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Navy); 
            spriteBatch.Begin();
            spriteBatch.Draw(renderTarget, screenCenter, null, Color.White,
                             rotationAngle, textureCenter, 1, SpriteEffects.None, 0);
            spriteBatch.End(); 
            base.Draw(gameTime);
        }

Preserving Render Target Contents

The pixels in the Windows Phone 7 back buffer-and the video display itself-were only 16 bits wide. What is the color format of the bitmap created with RenderTarget2D?

By default, the RenderTarget2D is created with 32 bits per pixel-8 bits each for red, green, blue, and alpha-corresponding to the enumeration member SurfaceFormat.Color. I'll have more to say about this format before the end of this chapter, but this 32-bit color format is now commonly regarded as fairly standard.

You probably want to build up the random rectangles on a RenderTarget2D that's the size of the back buffer. The rectangles you successively plaster on this RenderTarget2D can be based on the same 1×1 white bitmap used in DragAndDraw.

These two bitmaps are stored as fields of the RandomRectangles program together with a Random object and the LoadContent method creates the two RenderTarget2D objects. The big one requires an extensive constructor, Update method determines some random coordinates and color values, sets the large RenderTarget2D object in the GraphicsDevice, and draws the tiny texture over the existing content with random Rectangle and Color valuesand the last Draw override simply displays that entire large RenderTarget2D on the display.

namespace RandomRectangles
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch; 
        Random rand = new Random();
        RenderTarget2D tinyTexture;
        RenderTarget2D renderTarget; 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content"
            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
        }
 
        protected override void Initialize()
        {
            base.Initialize();
        }
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice); 
            tinyTexture = new RenderTarget2D(this.GraphicsDevice, 1, 1);
            this.GraphicsDevice.SetRenderTarget(tinyTexture);
            this.GraphicsDevice.Clear(Color.White);
            this.GraphicsDevice.SetRenderTarget(null);
            renderTarget = new RenderTarget2D(
                        this.GraphicsDevice,
                        this.GraphicsDevice.PresentationParameters.BackBufferWidth,
                        this.GraphicsDevice.PresentationParameters.BackBufferHeight,
                        false,
                        this.GraphicsDevice.PresentationParameters.BackBufferFormat,
                        DepthFormat.None, 0, RenderTargetUsage.PreserveContents);
        } 
        protected override void UnloadContent()
        {
        } 
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit(); 
            int x1 = rand.Next(renderTarget.Width);
            int x2 = rand.Next(renderTarget.Width);
            int y1 = rand.Next(renderTarget.Height);
            int y2 = rand.Next(renderTarget.Height);
            int r = rand.Next(256);
            int g = rand.Next(256);
            int b = rand.Next(256);
            int a = rand.Next(256); 
            Rectangle rect = new Rectangle(Math.Min(x1, x2), Math.Min(y1, y2),
                                           Math.Abs(x2 - x1), Math.Abs(y2 - y1));
            Color clr = new Color(r, g, b, a); 
            this.GraphicsDevice.SetRenderTarget(renderTarget);
            spriteBatch.Begin();
            spriteBatch.Draw(tinyTexture, rect, clr);
            spriteBatch.End();
            this.GraphicsDevice.SetRenderTarget(null); 
            base.Update(gameTime);
        } 
        protected override void Draw(GameTime gameTime)
        {
            spriteBatch.Begin();
            spriteBatch.Draw(renderTarget, Vector2.Zero, Color.White);
            spriteBatch.End(); 
            base.Draw(gameTime);
        }
    }
}


After almost no time at all, the display looks something like this:

2222222.gif

Drawing Lines

For developers coming from more mainstream graphical programming environments, it is startling to realize that XNA has no way of rendering simple lines and curves in 2D.

Suppose you want to draw a red line between the points (x1, y1) and (x2, y2), and you want this line to have a 3-pixel thickness.

During the Draw override, draw this bitmap to the screen using a position of (x1, y1) with an origin of (0, 1). That origin is the point within the RenderTarget2D that is aligned with the position argument. This line is supposed to have a 3-pixel thickness so the vertical center of the bitmap should be aligned with (x1, y1). In this Draw call you'll also need to apply a rotation equal to the angle from (x1, y1) to (x2, y2), which can be calculated with Math.Atan2.

Actually, you don't need a bitmap the size of the line. You can use a much smaller bitmap and apply a scaling factor. Probably the easiest bitmap size for this purpose is 2 pixels wide and 3 pixels high. That allows you to set an origin of (0, 1) in the Draw call, which means the point (0, 1) in the bitmap remains fixed. A horizontal scaling factor then enlarges the bitmap for the line length, and a vertical scaling factor handles the line thickness.

I have such a class in a XNA library project called Petzold.Phone.Xna. I created this project in Visual Studio by selecting a project type of Windows Phone Game Library (4.0). Here's the complete class I call LineRenderer:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Petzold.Phone.Xna
{
    public class LineRenderer
    {
        RenderTarget2D lineTexture;
        public LineRenderer(GraphicsDevice graphicsDevice)
        {
            lineTexture = new RenderTarget2D(graphicsDevice, 2, 3);
            graphicsDevice.SetRenderTarget(lineTexture);
            graphicsDevice.Clear(Color.White);
            graphicsDevice.SetRenderTarget(null);
        }
        public void DrawLine(SpriteBatch spriteBatch,
        Vector2 point1, Vector2 point2,
        float thickness, Color color)
        {
            Vector2 difference = point2 - point1;
            float length = difference.Length();    
   
        float angle = (float)Math.Atan2(difference.Y, difference.X);
            spriteBatch.Draw(lineTexture, point1,null, color, angle,new Vector2(0, 1),
            new Vector2(length / 2, thickness / 3),
            SpriteEffects.None, 0);
        }
    }
}

The constructor creates the small white RenderTarget2D. The DrawLine method requires an argument of type SpriteBatch and calls the Draw method on that object. Notice the scaling factor, which is the 7th argument to that Draw call. The width of the RenderTarget2D is 2 pixels, so horizontal scaling is half the length of the line. The height of the bitmap is 3 pixels, so the vertical scaling factor is the line thickness divided by 3. I chose a height of 3 so the line always straddles the geometric point regardless how thick it is.

To use this class in one of your programs, you'll first need to build the library project. Then, in any regular XNA project, you can right-click the References section in the Solution Explorer and select Add Reference. In the Add Reference dialog select the Browse label. Navigate to the directory with Petzold.Phone.Xna.dll and select it.

In the code file you'll need a using directive:

using Petzold.Phone.Xna;

You'll probably create a LineRenderer object in the LoadContent override and then call DrawLine in the Draw override, passing to it the SpriteBatch object you're using to draw other 2D graphics.

All of this is demonstrated in the TapForPolygon project. The program begins by drawing a triangle including lines from the center to each vertex. Tap the screen and it becomes a square, than again for a pentagon, and so forth:


3333333333.gif

The Game1 class has fields for the LineRenderer as well as a couple helpful variables.

namespace
TapForPolygon
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch; 
        LineRenderer lineRenderer;
        Vector2 center;
        float radius;
        int vertexCount = 3; 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content"
            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333); 
            // Enable taps
            TouchPanel.EnabledGestures = GestureType.Tap;
        } 
        protected override void Initialize()
        {
           base.Initialize();
        } 
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice); 
            Viewport viewport= this.GraphicsDevice.Viewport;
            center = new Vector2(viewport.Width / 2, viewport.Height / 2);
            radius = Math.Min(center.X, center.Y) - 10;
 
            lineRenderer = new LineRenderer(this.GraphicsDevice);
        } 
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit(); 
            while (TouchPanel.IsGestureAvailable)
                if (TouchPanel.ReadGesture().GestureType == GestureType.Tap)
                    vertexCount++;           
            base.Update(gameTime);
        } 
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Navy); 
            spriteBatch.Begin(); 
            Vector2 saved = new Vector2(); 
            for (int vertex = 0; vertex <= vertexCount; vertex++)
            {
                double angle = vertex * 2 * Math.PI / vertexCount;
                float x = center.X + radius * (float)Math.Sin(angle);
                float y = center.Y - radius * (float)Math.Cos(angle);
                Vector2 point = new Vector2(x, y)
                if (vertex != 0)
                {
                    lineRenderer.DrawLine(spriteBatch, center, point, 3, Color.Red);
                    lineRenderer.DrawLine(spriteBatch, saved, point, 3, Color.Red);
                }
                saved = point;
            }
            spriteBatch.End(); 
            base.Draw(gameTime);
        }
    }
}

You don't have to use LineRenderer to draw lines on the video display. You can draw them on another RenderTarget2D objects. One possible application of the LineRenderer class used in this way is a "finger paint" program, where you draw free-form lines and curves with your finger. The next project is a very simple first stab at such a program. The lines you draw with your fingers are always red with a 25-pixel line thickness. Here are the fields and constructor (and please don't be too dismayed by the project name):

public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch; 
        RenderTarget2D renderTarget;
        LineRenderer vectorRenderer; 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content"
            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333); 
            // Enable gestures
            TouchPanel.EnabledGestures = GestureType.FreeDrag;
            ...
        }

Notice that only the FreeDrag gesture is enabled. Each gesture will result in another short line being drawn that is connected to the previous line.

The RenderTarget2D object named renderTarget is used as a type of "canvas" on which you can paint with your fingers. It is created in the LoadContent method to be as large as the back buffer, and with the same color format, and preserving content:

protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice); 
            renderTarget = new RenderTarget2D(
                        this.GraphicsDevice,
                        this.GraphicsDevice.PresentationParameters.BackBufferWidth,
                        this.GraphicsDevice.PresentationParameters.BackBufferHeight,
                        false,
                        this.GraphicsDevice.PresentationParameters.BackBufferFormat,
                        DepthFormat.None, 0, RenderTargetUsage.PreserveContents); 
            this.GraphicsDevice.SetRenderTarget(renderTarget);
            this.GraphicsDevice.Clear(Color.Navy);
            this.GraphicsDevice.SetRenderTarget(null); 
            vectorRenderer = new LineRenderer(this.GraphicsDevice);
        }

The LoadContent override also creates the LineRenderer object.

You'll recall that the FreeDrag gesture type is accompanied by a Position property that indicates the current location of the finger, and a Delta property, which is the difference between the current location of the finger and the previous location of the finger. That previous location can be calculated by subtracting Delta from Position, and those two points are used to draw a short line on the RenderTarget2D canvas:

protected
override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit(); 
            while (TouchPanel.IsGestureAvailable)
            {
                GestureSample gesture = TouchPanel.ReadGesture(); 
                if (gesture.GestureType == GestureType.FreeDrag &&
                    gesture.Delta != Vector2.Zero)
                {
                    this.GraphicsDevice.SetRenderTarget(renderTarget);
                    spriteBatch.Begin();
                    vectorRenderer.DrawLine(spriteBatch,
                                            gesture.Position,
                                            gesture.Position - gesture.Delta,
                                            25, Color.Red);   
                    spriteBatch.End();
                    this.GraphicsDevice.SetRenderTarget(null);
                }
            }
            base.Update(gameTime);
        }

The Draw override then merely needs to draw the canvas on the display:

protected override void Draw(GameTime gameTime)
        {
            spriteBatch.Begin();
            spriteBatch.Draw(renderTarget, Vector2.Zero, Color.White);
            spriteBatch.End(); 
            base.Draw(gameTime);
        }

When you try this out, you'll find that it works really well in that you can quickly move your finger around the screen and you can draw a squiggly line:

44444444.gif


Similar Articles