Game Components Affine and Non-Affine Transforms in Windows Phone 7

To give your fingers a target to touch and drag, the programs display translucent disks at the Texture2D corners. It would be nice to code these draggable translucent disks so they’re usable by multiple programs.


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

Game Components

To give your fingers a target to touch and drag, the programs display translucent disks at the Texture2D corners. It would be nice to code these draggable translucent disks so they're usable by multiple programs. In a traditional graphics programming environment, we might think of something like this as a control but in XNA it's called a game component.

Components help modularize your XNA programs. Components can derive from theGameComponent class but often they derive fromDrawableGameComponent so they can display something on the screen in addition to (and on top of) what goes out in the Draw method of your Game class.

To add a new component class to your project, right-click the project name, select Add and then New Item, and then pick Game Component from the list. You'll need to change the base class to DrawableGameComponent and override the Draw method if you want the component to participate in drawing.

A game generally instantiates the components that it needs either in the game's constructor or during the Initialize method. The components officially become part of the game when they are added to the Components collection defined by the Game class.

As with Game, a DrawableGameComponent derivative generally overrides the Initialize, LoadContent,Update, and Draw methods. When the Initialize override of the Game derivative calls the Initialize method in the base class, the Initialize methods in all the components are called. Likewise, when the LoadComponent,Update, and Draw overrides in the Game derivative call the method in the base class, the LoadComponent,Update, and Draw methods in all the components are called.

As you know, the Update override normally handles touch input. In my experience that attempting to access touch input in a game component is somewhat problematic. It seems as if the game itself and the components end up competing for input.

To fix this, I decided that my Game derivative would be solely responsible for calling TouchPanel.GetState, but the game would then give the components the opportunity to

process this touch input. To accommodate this concept, I created this interface for GameComponent and DrawableGameComponent derivatives:

using Microsoft.Xna.Framework.Input.Touch; 
namespace Petzold.Phone.Xna
{
    public interface IProcessTouch
    {
        bool ProcessTouch(TouchLocation touch);
    }
}

When a game component implements this interface, the game calls the game component's ProcessTouch method for every TouchLocation object. If the game component needs to use that TouchLocation, it returns true from ProcessTouch, and the game then probably ignores that TouchLocation.

The first component I'll show you is called Dragger, and it is part of the Petzold.Phone.Xna library. Dragger derives from DrawableGameComponent and implements the IProcessTouch interface:

public class Dragger : DrawableGameComponent, IProcessTouch
    {
        SpriteBatch spriteBatch;
        int? touchId; 
        public event EventHandler PositionChanged; 
        public Dragger(Game game)
            : base(game)
        {
        }
        public Texture2D Texture { set; get; }
        public Vector2 Origin { set; get; }
        public Vector2 Position { set; get; }
        ....
    }

A program making use of Dragger could define a custom Texture2D for the component and set it through this public Texture property, at which time it would probably also set the Origin property. However, Dragger defines a default Texture property for itself during its LoadContent method:

protected override void  LoadContent()
        {
            spriteBatch = new SpriteBatch(this.GraphicsDevice); 
            // Create default texture
            int radius = 48;
            Texture2D texture = new Texture2D(this.GraphicsDevice, 2 * radius, 2 * radius);
            uint[] pixels = new uint[texture.Width * texture.Height]; 
            for (int y = 0; y < texture.Height; y++)
                for (int x = 0; x < texture.Width; x++)
                {
                    Color clr = Color.Transparent; 
                    if ((x - radius) * (x - radius) +
                        (y - radius) * (y - radius) <
                        radius * radius)
                    {
                        clr = new Color(0, 128, 128, 128);
                    }
                    pixels[y * texture.Width + x] = clr.PackedValue;
                }
            texture.SetData<uint>(pixels); 
            Texture = texture;
            Origin = new Vector2(radius, radius); 
               base.LoadContent();
        }

The Dragger class implements the IProcessTouch interface so it has a ProcessTouch method that is called from the Game derivative for each TouchLocation object. The ProcessTouch method is interested in finger presses that occur over the component itself. If that is the case, it retains the ID and basically owns that finger until it lifts from the screen. For every movement of that finger, Dragger fires a PositionChanged event.

public bool ProcessTouch(TouchLocation touch)
        {
            if (Texture == null)
                return false
            bool touchHandled = false;               
            switch (touch.State)
            {
                case TouchLocationState.Pressed:
                    if ((touch.Position.X > Position.X - Origin.X) &&
                        (touch.Position.X < Position.X - Origin.X + Texture.Width) &&
                        (touch.Position.Y > Position.Y - Origin.Y) &&
                        (touch.Position.Y < Position.Y - Origin.Y + Texture.Height))
                    {
                        touchId = touch.Id;
                        touchHandled = true;
                    }
                    break
                case TouchLocationState.Moved:
                    if (touchId.HasValue && touchId.Value == touch.Id)
                    {
                        TouchLocation previousTouch;
                        touch.TryGetPreviousLocation(out previousTouch);
                        Position += touch.Position - previousTouch.Position; 
                        // Fire the event!
                        if (PositionChanged != null)
                            PositionChanged(this, EventArgs.Empty); 
                        touchHandled = true;
                    }
                    break
                case TouchLocationState.Released:
                    if (touchId.HasValue && touchId.Value == touch.Id)
                    {
                        touchId = null;
                        touchHandled = true;
                    }
                    break;
            }
            return touchHandled;
        }

The Draw override just draws the Texture2D at the new position:

public override void Draw(GameTime gameTime)
        {
            if (Texture != null)
            {
                spriteBatch.Begin();
                spriteBatch.Draw(Texture, Position, null, Color.White,
                                 0, Origin, 1, SpriteEffects.None, 0);
                spriteBatch.End();
            }
            base.Draw(gameTime);
        }

Affine and Non-Affine Transforms

Sometimes it's convenient to derive a transform that maps a particular set of points to a particular destination. For example, here's a program that incorporates three instances of the Dragger component I just described, and lets you drag three corners of the Texture2D
to arbitrary locations on the screen:

111.gif

This program uses an affine transform, which means that rectangles are always mapped to parallelograms. The fourth corner isn't draggable because it's always determined by the other three:

222.gif

You can't choose just any three points. Everything goes kaflooey if you attempt to make an interior angle greater than 180 degree.

A static class named MatrixHelper in the Petzold.Phone.Xna library has a method named ComputeAffineTransform that creates a Matrix object based on these formulas:

static Matrix ComputeAffineTransform(Vector2 ptUL, Vector2 ptUR, Vector2 ptLL)
        {
            return new Matrix()
            {
                M11 = (ptUR.X - ptUL.X),
                M12 = (ptUR.Y - ptUL.Y),
                M21 = (ptLL.X - ptUL.X),
                M22 = (ptLL.Y - ptUL.Y),
                M33 = 1,
                M41 = ptUL.X,
                M42 = ptUL.Y,
                M44 = 1
            };
        }

This method isn't public because it's not very useful by itself. It's not very useful because the formulas are based on transforming an image that is one-pixel wide and one-pixel tall. Notice, however, that the code sets M33 and M44 to 1. This doesn't happen automatically and it is essential for the matrix to work right.

To compute a Matrix for an affine transform that applies to an object of a particular size, this public method is much more useful:

public static Matrix ComputeMatrix(Vector2 size, Vector2 ptUL, Vector2 ptUR, Vector2 ptLL)
        {
            // Scale transform
            Matrix S = Matrix.CreateScale(1 / size.X, 1 / size.Y, 1); 
            // Affine transform
            Matrix A = ComputeAffineTransform(ptUL, ptUR, ptLL); 
            // Product of two transforms
            return S * A;
        }

The first transform scales the object down to a 1*1 size before applying the computed affine transform. The AffineTransform project is responsible for the two screen shots shown above. It creates three instances of the Dragger component in its Initialize override, sets a handler for the PositionChanged event, and adds the component to the Components collection:

public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch; 
        Texture2D texture;
        Matrix matrix = Matrix.Identity;
        Dragger draggerUL, draggerUR, draggerLL; 
        ...
        protected override void Initialize()
        {
            draggerUL = new Dragger(this);
            draggerUL.PositionChanged += OnDraggerPositionChanged;
            this.Components.Add(draggerUL); 
            draggerUR = new Dragger(this);
            draggerUR.PositionChanged += OnDraggerPositionChanged;
            this.Components.Add(draggerUR); 
            draggerLL = new Dragger(this);
            draggerLL.PositionChanged += OnDraggerPositionChanged;
            this.Components.Add(draggerLL); 
            base.Initialize();
        } 
        ...
    }

Don't forget to add the components to the Components collection of the Game class!

The LoadContent override is responsible for loading the image that will be transformed and initializing the Position properties of the three Dragger components at the three corners of the image:

protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice); 
            Viewport viewport = this.GraphicsDevice.Viewport;
            texture = this.Content.Load<Texture2D>("PetzoldTattoo"); 
            draggerUL.Position = new Vector2((viewport.Width - texture.Width) / 2,
                                             (viewport.Height - texture.Height) / 2); 
            draggerUR.Position = draggerUL.Position + new Vector2(texture.Width, 0);
            draggerLL.Position = draggerUL.Position + new Vector2(0, texture.Height); 
            OnDraggerPositionChanged(null, EventArgs.Empty);
        }

Dragger only fires its PositionChanged event when the component is actually dragged by the user, so the LoadContent method concludes by simulating a PositionChanged event, which calculates an initial Matrix based on the size of the Texture2D and the initial positions of the Dragger components:

void OnDraggerPositionChanged(object sender, EventArgs args)
        {
            matrix = MatrixHelper.ComputeMatrix(new Vector2(texture.Width, texture.Height),
                                                draggerUL.Position,
                                                draggerUR.Position,
                                                draggerLL.Position);
        }

The program doesn't need to handle any touch input of its own, but Dragger implements the IProcessTouch interface, so the program funnels touch input to the Dragger components. These Dragger components respond by possibly moving themselves and setting new Position properties, which will cause PositionChanged events to be fired.

protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit(); 
            TouchCollection touches = TouchPanel.GetState(); 
            foreach (TouchLocation touch in touches)
            {
                bool touchHandled = false
                foreach (GameComponent component in this.Components)
                {
                    if (component is IProcessTouch &&
                        (component as IProcessTouch).ProcessTouch(touch))
                    {
                        touchHandled = true;
                        break;
                    }
                } 
                if (touchHandled == true)
                    continue;
            } 
            base.Update(gameTime);
        }

It is possible for the program to dispense with setting handlers for the PositionChanged event of the Dragger components and instead poll the Position properties during each Update call and recalculate a Matrix from those values. However, recalculating a Matrix only when one of the Position properties actually changes is much more efficient.

The Draw override uses that Matrix to display the texture:

protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue); 
            spriteBatch.Begin(SpriteSortMode.Immediate, null, null, null, null, null, matrix);
            spriteBatch.Draw(texture, Vector2.Zero, Color.White);
            spriteBatch.End() 
            base.Draw(gameTime);
        }

As you experiment with AffineTransform, you'll want to avoid making the interior angles at any corner greater than 180 degree. (In other words, keep it convex.) Affine transforms can express familiar operations like translation, scaling, rotation, and skew, but they never transform a square into anything more exotic than a parallelogram.

Non-affine transforms are much more common in 3D than 2D. In 3D, non-affine transforms are necessary to implement perspective effects. A long straight desert highway in a 3D world must seem to get narrower as it recedes into the distance, just like in the real world. Although we know that the sides of the road remains parallel, visually they seem to converge at infinity. This tapering effect is characteristic of non-affine transforms.

Although non-affine transforms are essential for 3D graphics programming, I wasn't even sure if SpriteBatch supported two-dimensional non-affine transforms until I tried them, and I was pleased to discover that XNA says "No problem!" What this means is that you can use non-affine transforms in 2D programming to simulate perspective effects.

A non-affine transform in 2D can transform a square into a simple convex quadrilateral-a four-sided figure where the sides meet only at the corners, and interior angles at any corner are less than 180 degree. Here's one example:

333.gif

This one makes me look really smart:

444.gif

This program is called NonAffineTransform and it's just like AffineTransform except it has a fourth Dragger component and it calls a somewhat more sophisticated method in the MatrixHelper class in Petzold.Phone.Xna. You can move the little disks around with a fair amount of freedom; as long as you're not trying to form a concave quadrilateral, you'll get an image stretched to fit.

The math of NonAffineTransform has been incorporated into a second static MatrixHelper.ComputeMatrix method in the Petzold.Phone.Xna library:

public
static Matrix ComputeMatrix(Vector2 size, Vector2 ptUL, Vector2 ptUR,
                                                         Vector2 ptLL, Vector2 ptLR)
        {
            // Scale transform
            Matrix S = Matrix.CreateScale(1 / size.X, 1 / size.Y, 1); 
            // Affine transform
            Matrix A = ComputeAffineTransform(ptUL, ptUR, ptLL);
 
            // Non-Affine transform
            Matrix B = new Matrix();
            float den = A.M11 * A.M22 - A.M12 * A.M21;
            float a = (A.M22 * ptLR.X - A.M21 * ptLR.Y +
                       A.M21 * A.M42 - A.M22 * A.M41) / den; 
            float b = (A.M11 * ptLR.Y - A.M12 * ptLR.X +
                       A.M12 * A.M41 - A.M11 * A.M42) / den; 
            B.M11 = a / (a + b - 1);
            B.M22 = b / (a + b - 1);
            B.M33 = 1;
            B.M14 = B.M11 - 1;
            B.M24 = B.M22 - 1;
            B.M44 = 1; 
            // Product of three transforms
            return S * B * A;
        }

I won't show you the NonAffineTransform program here because it's pretty much the same as the AffineTransform program but with a fourth Dragger component whose Position property is passed to the second ComputeMatrix method.

The big difference with the new program is that non-affine transforms are much more fun!