FPE 2 to 3 Coordinate conversion

Topics: Developer Forum, Project Management Forum, User Forum
Aug 24, 2010 at 6:51 PM

I'm having major issues while trying to upgrade our project to FPE3. After getting the project to build and run again, which went fairly smooth, I bumped into huge problems with the coordinate system.

I read about the change to real world units, so I expected problems, but nothing I couldn't fix. 

These are some of the things I tried:

  • Moving entities closer together in the level
  • Changing the scale of the view matrix
  • Changing the scale of created fixtures vs the scale of the actual textures
  • Looking at the examples and changing my code accordingly

Somehow, none of this seems to work. The debug view and the code I use to draw textures at the position of the bodies don't match at all. The debug view shows entities at completely different positions than the graphics on the screen. I have no clue which one is correct, the actual view or the debug view - either projection could be wrong.

The largest issue is that I don't see a way to gradually move to the new coordinate system, and debug step by step. All entities and the debug view are converted immediately, which leaves me with an invalid representation of the game world, objects just flying around and a broken debug view.

I can either revert the whole thing back to the way it was before the upgrade and continue from the original situation, or continue messing around in the current code. Creating a new project from scratch and working from there is not an option.

Any advice on dealing with this breaking change is more than welcome. Thanks in advance for your help!

Aug 24, 2010 at 8:48 PM
Edited Aug 24, 2010 at 10:37 PM

This is how I do it, but it's not really that robust when add camera zooming. If you do zooming, then you need to scale screenSpaceScale by the amount of zoom. View is a Matrix initially set to Matrix.Identity, but could be your "camera view" (panning only unless you do the screenSpaceScale scaling):

 

		Vector2 ToWorldCoordinates(Vector2 pixels)
		{
			Vector3 worldPosition = graphics.GraphicsDevice.Viewport.Unproject(new Vector3(pixels, 0),
					Projection, View, Matrix.Identity);
			return new Vector2(worldPosition.X, worldPosition.Y);
		}
		Vector2 ToScreenCoordinates(Vector2 worldCoords)
		{
			var screenPositon = graphics.GraphicsDevice.Viewport.Project(new Vector3(worldCoords, 0),
					Projection, View, Matrix.Identity);
			return new Vector2(screenPositon.X, screenPositon.Y);
		}

		const float screenHeightInWorldCoordinates = 30;

		/// <summary>
		/// This is called when the game should draw itself.
		/// </summary>
		/// <param name="gameTime">Provides a snapshot of timing values.</param>
		protected override void Draw(GameTime gameTime)
		{
			GraphicsDevice.Clear(Color.CornflowerBlue);

			float aspect = (float)graphics.GraphicsDevice.Viewport.Width
			             / (float)graphics.GraphicsDevice.Viewport.Height;

			Projection = Matrix.CreateOrthographic(screenHeightInWorldCoordinates * aspect, screenHeightInWorldCoordinates, 0, 1);

			spriteBatch.Begin();

			float radiusInWorldCoordinates = .15f;
			float pixelsPerMeter = graphics.GraphicsDevice.Viewport.Height / screenHeightInWorldCoordinates;
			Vector2 origin = new Vector2(circleTex.Width / 2, circleTex.Height / 2);

			// 2 pixels are transparent on each side, so we subtract those from the width
			float textureTransparencyPadding = 4;
			
			foreach (var ball in world.BodyList)
			{
				Vector2 screenPos = ToScreenCoordinates(ball.Position);
				float widthInPixels = pixelsPerMeter * radiusInWorldCoordinates * 2;
				Vector2 scale = new Vector2(widthInPixels / (circleTex.Width - textureTransparencyPadding),
							    widthInPixels / (circleTex.Height - textureTransparencyPadding));

				spriteBatch.Draw(circleTex, screenPos, null, Color.Plum, 0, origin, scale, SpriteEffects.None, 0);
			}

			debugView.RenderDebugData(ref Projection, ref View);
			spriteBatch.End();
			base.Draw(gameTime);
		}
... etc..

Edit: OK, found the issue of everything drawing at half size. Was using radius instead of diameter. Heh.
Aug 24, 2010 at 9:45 PM

Also, according to Box2D's documentation, you'll want to stick with moving objects between .1 and 10 meters. Static objects can be larger. Moving objects smaller than .1 or larger than 10 meters will still work - the engine just isn't tuned for units outside that range, so accuracy/stability may suffer.

So, assuming all of your game assets are designed with pixel units instead of meters, just apply a scaling ratio to convert pixels to units. Say objects 40 pixels wide in the old engine were roughly 1 "meter", then objectSizeInMeters = objectSizeInPixels / 40;

Aug 24, 2010 at 11:15 PM
Edited Aug 24, 2010 at 11:19 PM

I use the camera class by RogueCommanderIX -> http://farseerphysics.codeplex.com/Thread/View.aspx?ThreadId=50483

I have this property which I use as my number of pixels in a meter

 

float m_PixelsMeter;
/// <summary>
/// Set the number of pixels in a meter. Used for drawing the debug view. Default: 100f
/// </summary>
public float PixelsAMeter
{
    get { return m_PixelsMeter; }
    set { m_PixelsMeter = value; }
}

 

This is my code for drawing the debug view.

 

public void Draw()
{
    if (DebugViewEnabled)
    {              

        if (UsingCamera)
        {                    
            m_View = Matrix.CreateTranslation(Engine.Camera2D.Position.X/-m_PixelsMeter, Engine.Camera2D.Position.Y /-m_PixelsMeter, 0);
            Vector2 size = (((new Vector2(Engine.Settings.Video.Width, Engine.Settings.Video.Height)) / (m_PixelsMeter*2)) / Engine.Camera2D.Zoom);
            m_Projection = Matrix.CreateOrthographicOffCenter(-size.X, size.X, size.Y, -size.Y, 0, 1);                    
        }
        else
        {
            float aspect = (float)Engine.Settings.Video.Width / (float)Engine.Settings.Video.Height;
            m_Projection = Matrix.CreateOrthographic(40 * aspect, 40, 0, 1);                    
            m_View = Matrix.Identity;                   
        }

        //DebugView.DrawDebugData();
        DebugView.RenderDebugData(ref m_Projection, ref m_View);
    }
}

 

I have these two functions to convert between my game world and the physics one.

 

/// <summary>
/// Converts the position from game world to the physics world
/// </summary>
/// <param name="position">Position in game world</param>
/// <returns></returns>
public Vector2 PositionToWorld(Vector2 position)
{
    return position / PixelsAMeter;
}
/// <summary>
/// Converts the position from physics world to game world
/// </summary>
/// <param name="position">Position in physics world</param>
/// <returns></returns>
public Vector2 PositionToGame(Vector2 position)
{
    return position * PixelsAMeter;
}

Other than having to change forces and density and changing sprite position a bit to line up with debug view it was not painful to switch over once I get the debug view and camera to work together. Using 100pixels works out really well since you just have to divide your pixels sizes by 100 to make them match the physics world. Example below. 

 

Vertices verts = new Vertices(); verts.Add(new Vector2(0f, 0f));
verts.Add(Engine.Physics.PositionToWorld(new Vector2(15f, 0f))); 
verts.Add(Engine.Physics.PositionToWorld(new Vector2(15f, 15f))); 
verts.Add(Engine.Physics.PositionToWorld(new Vector2(0f, 15f))); 

If you have anymore questions ask away I'll do what I can.

JRommann

www.rabiddesignstudios.com

Aug 25, 2010 at 6:39 PM

Ok, thanks a lot for the suggestions. I will give it another try!

Aug 25, 2010 at 8:56 PM

Ok, it's not perfect yet, but slowly improving :)

The biggest issue was that I had set a projection matrix in the spritebatch.begin call before drawing the entities, and at the same time applied an additional position correction to the actual spritebatch.draw call for the texture. So I tried to solve the problem twice, causing it to break.

I also had to manipulate the projection matrix for the DebugView - making it negative, transposing and scaling by some magic number. I'm still not sure what these numbers are based on, and how I can do this in a cleaner way. I'll have to do some more experimenting with all the values.

Anyway, it's a very good idea to take 100 pixels per meter. Thanks to both of you for the help...

I'll post the code when I've found a definitive and elegant solution (or I'll post follow-up questions if I stumble into related problems).

Aug 25, 2010 at 10:37 PM

Initially, I tried setting up a transformation matrix to send to SpriteBatch.Begin that would do the entire View, Projection, World, and ViewPort-projection transformation in one go (so each spriteBatch.Draw call could use world coordinates directly, the way the DebugView does using vertex primitives and BasicEffect), but I couldn't figure out how to turn what ViewPort.Project does into a matrix which I can pre-multiply in with the View/Projection/World matrices. 

Aug 26, 2010 at 3:41 PM

I posted a question on stackoverflow and got a good answer:

http://stackoverflow.com/questions/3570192/xna-viewport-projection-and-spritebatch

So, here is the updated code which uses a transformation matrix sent to spriteBatch.Begin, then handles everything else in world coordinates.

 

		const float baseScreenHeightInWorldCoordinates = 100;

		/// <summary>
		/// This is called when the game should draw itself.
		/// </summary>
		/// <param name="gameTime">Provides a snapshot of timing values.</param>
		protected override void Draw(GameTime gameTime)
		{
			GraphicsDevice.Clear(Color.CornflowerBlue);
			Viewport viewPort = GraphicsDevice.Viewport;

			//
			// Create View and Projection matrices
			//
			Projection = 
				Matrix.CreateOrthographic(baseScreenHeightInWorldCoordinates * viewPort.AspectRatio,
				                          baseScreenHeightInWorldCoordinates, 
							  0, 1);

			// move camera back and forth over time
			// this could be anything - pan, zoom, rotate, etc..
			View = Matrix.CreateTranslation((float)Math.Sin(gameTime.TotalGameTime.TotalSeconds), 0, 0);

			// Create a transform matrix to send to spriteBatch
			Matrix transform =
				  View
				* Matrix.CreateScale(viewPort.Height / baseScreenHeightInWorldCoordinates)
				* Matrix.CreateScale(1, -1, 1)
				* Matrix.CreateTranslation(viewPort.Width * .5f, viewPort.Height * .5f, 0f);

			spriteBatch.Begin(SpriteSortMode.Immediate, null, null, null, null, null, transform);

			Vector2 origin = new Vector2(circleTex.Width / 2, circleTex.Height / 2);

			// 2 pixels are transparent on each side, so we subtract those from the width
			float textureTransparencyPadding = 4;

			foreach (var circle in world.BodyList)
			{
				if (circle.FixtureList.Count > 0)
				{
					Fixture fixture = circle.FixtureList[0];
					if (fixture.ShapeType == ShapeType.Circle)
					{
						float width = fixture.Shape.Radius * 2;

						// note that, because we are sending spriteBatch a transformation matrix, it
						// assumes the texture coordinates are in world space, not screen space,
						// so we don't convert width to screenspace here
						Vector2 scale = new Vector2(width / (circleTex.Width - textureTransparencyPadding),
									    width / (circleTex.Height - textureTransparencyPadding));

						// Box2D's coordinate system is Y-axis reversed from XNA's (or at least spriteBatch's),
						// so we flip the texture here to avoid getting backface culled, and to draw it right
						// side up
						scale.Y = -scale.Y;
						spriteBatch.Draw(circleTex, circle.Position, null, Color.Plum, 0, origin,
							scale, SpriteEffects.None, 0);
					}
				}
			}

			spriteBatch.End();
		
			debugView.RenderDebugData(ref Projection, ref View);

			base.Draw(gameTime);
		}

Dec 4, 2010 at 7:04 AM

Late to this particular party, but thank you, JeroMiya, for mentioning the bit about flipping the Y-scale on the texture to prevent backface culling. I've been struggling and tearing my hair out trying to figure out why my matrix wasn't working all day, and if you hadn't mentioned that the y-axis flip in the matrix induces a backface cull, I doubt I'd have figured it out, because it rally just wasn't making sense at all. Forgot that 2D these days is all just flat 3D (also didn't realize that SpriteBatch culls backfaces!) :D