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