Screen to World coordinates in Box2D

In this blog entry I'm going to talk about how to manage the coordinates system in your Box2D world. The problem arises because normally in games we want to use two sets of coordinates - one for the Box2D world and one for the screen output. Time and again I come across people using sub-optimal methods for managing the two sets of coordinates.

Mistake 1: Pixel coordinates

When people start out with Box2D normally they create a Box2D world with the same dimensions as the screen on which their world will be displayed i.e. pixel dimensions. If the game runs at 1000px x 800px then the Box2D world has dimensions 1000m x 800m. This is a simple and easy solution but can cause all sorts of problems.

Firstly, Box2D is optimised to run using real world dimensions i.e. objects ranging in size from 0.1m to 10m. Having 100m meter objects flying around will reduce performance and could lead to bugs and glitches.

Secondly, this method is horribly inflexible. What if you want to modify your game to run at 500px x 400px? The short answer is that you can't!

Mistake 2: SCREEN_TO_WORLD

The second method I see is to use a global parameter SCREEN_TO_WORLD which contains the ratio of Box2D units to screen pixels. This method allows you to run your Box2D world with sensible dimensions but still doesn't offer much flexibility.

What if you want to zoom in on a section of your world? What if you want to pan across your world like in Angry Birds? What if you want the coordinates from your world to go from -50 to +50 but your screen needs to go from 0 to +500? The answer to all these questions is simply "you can't!".

This method is also hacky! Your code ends up littered with references to SCREEN_TO_WORLD and WORLD_TO_SCREEN which isn't object orientated or flexible. It makes the code hard to read which in turn makes it much easier to make mistakes.

Solution: Camera class

So what's the solution? The answer is to use a Camera class to manage the conversion from screen to world. A camera class emulates the behavior of a camera moving over the Box2D world. The output of the camera is completely independent of what it's recording. The camera can pan, rotate and zoom without affecting the Box2D world in any way. You create your whole game using world coordinates and then at the end the camera converts everything to screen coordinates. Simple and clean.

I'm not going to go into the maths to work this out - it took me an evening of scribbling diagrams on scraps of paper to work it out but if you're interested then you can read more about Affine Transforms.

Below is my camera class:

  1. package library.physics;
  2.  
  3. import org.jbox2d.common.Vec2;
  4.  
  5. public class Camera {
  6. public final String TAG = this.getClass().getSimpleName();
  7.  
  8. public Vec2 rotationOrigin = new Vec2();
  9. public float rotation = 0;
  10.  
  11. public boolean updated = false;
  12.  
  13. public Vec2 scaleWorldToScreen = new Vec2();
  14. public Vec2 scaleScreenToWorld = new Vec2();
  15.  
  16. // Coordinate of bottom left hand corner of screen
  17. public Vec2 screenMin;
  18. // Coordinate of top right hand corner of screen
  19. public Vec2 screenMax;
  20.  
  21. // Coordinate of bottom left hand corner of world
  22. public Vec2 worldMin;
  23. // Coordinate of bottom top right corner of world
  24. public Vec2 worldMax;
  25.  
  26. /*
  27. * Screen max/min in world - i.e. if the world extends from 0 - 100 but you only want to display
  28. * from 50 - 100 then screenMinInWorld = (50, 50) screenMaxInWorld(100, 100)
  29. */
  30. public Vec2 screenMinInWorld;
  31. public Vec2 screenMaxInWorld;
  32.  
  33.  
  34. public Camera(Vec2 worldMin, Vec2 worldMax, Vec2 screenMin, Vec2 screenMax, Vec2 screenMinInWorld, Vec2 screenMaxInWorld) {
  35. setCamera(worldMin, worldMax, screenMin, screenMax, screenMinInWorld, screenMaxInWorld);
  36. }
  37. public Camera( Vec2 worldMax, Vec2 screenMax, Vec2 screenMinInWorld, Vec2 screenMaxInWorld) {
  38. setCamera(new Vec2(), worldMax, new Vec2(), screenMax, screenMinInWorld, screenMaxInWorld);
  39. }
  40. public Camera( Vec2 worldMax, Vec2 screenMax) {
  41. setCamera(new Vec2(), worldMax, new Vec2(), screenMax, new Vec2(), worldMax);
  42. }
  43.  
  44. public void setCamera(Vec2 worldMin, Vec2 worldMax, Vec2 screenMin, Vec2 screenMax, Vec2 screenMinInWorld, Vec2 screenMaxInWorld) {
  45.  
  46. scaleWorldToScreen.x = (screenMax.x-screenMin.x)/(screenMaxInWorld.x-screenMinInWorld.x);
  47. scaleWorldToScreen.y = (screenMax.y-screenMin.y)/(screenMaxInWorld.y-screenMinInWorld.y);
  48.  
  49. scaleScreenToWorld.x = 1/scaleWorldToScreen.x;
  50. scaleScreenToWorld.y = 1/scaleWorldToScreen.y;
  51.  
  52. this.screenMin = screenMin;
  53. this.screenMax = screenMax;
  54. this.worldMin = worldMin;
  55. this.worldMax = worldMax;
  56. this.screenMinInWorld = screenMinInWorld;
  57. this.screenMaxInWorld = screenMaxInWorld;
  58. }
  59.  
  60. public Vec2 worldToScreen(float x, float y) {
  61. return worldToScreen(new Vec2(x,y));
  62. }
  63. public Vec2 worldToScreen(Vec2 world) {
  64. Vec2 screen = new Vec2();
  65. screen.x = screenMin.x + scaleWorldToScreen.x * (world.x - worldMin.x - screenMinInWorld.x);
  66. screen.y = screenMin.y + scaleWorldToScreen.y * (world.y - worldMin.y - screenMinInWorld.y);
  67.  
  68. return screen;
  69. }
  70.  
  71. public Vec2 screenToWorld(float x, float y) {
  72. return screenToWorld(new Vec2(x,y));
  73. }
  74. public Vec2 screenToWorld(Vec2 screen) {
  75. Vec2 world = new Vec2();
  76. world.x = (screen.x - screenMin.x)*scaleScreenToWorld.x + worldMin.x + screenMinInWorld.x;
  77. world.y = (screen.y - screenMin.y)*scaleScreenToWorld.y + worldMin.y + screenMinInWorld.y;
  78. return world;
  79. }
  80.  
  81. public float worldWidth() {
  82. return (worldMax.x - worldMin.x);
  83. }
  84. public float worldHeight() {
  85. return (worldMax.y - worldMin.y);
  86. }
  87. public float screenWidth() {
  88. return (screenMax.x - screenMin.x);
  89. }
  90. public float screenHeight() {
  91. return (screenMax.y - screenMin.y);
  92. }
  93.  
  94. public String toString() {
  95. String s = "";
  96. s += screenMin.toString()+" -> "+screenToWorld(screenMin).toString()+"\n";
  97. s += screenMax.toString()+" -> "+screenToWorld(screenMax).toString();
  98. return s;
  99. }
  100.  
  101. // Used if the screen dimensions change i.e. if a user adjusts the
  102. // dimensions of the window
  103. public void reshape(float x1, float y1, float x2, float y2 ) {
  104. this.screenMin.set(x1, y1);
  105. this.screenMax.set(x2, y2);
  106.  
  107. setCamera(worldMin, worldMax, screenMin, screenMax, screenMinInWorld, screenMaxInWorld);
  108. }
  109.  
  110. // Pan by a vector
  111. public void pan (Vec2 pan) {
  112. screenMinInWorld = screenMinInWorld.add(pan);
  113. screenMaxInWorld = screenMaxInWorld.add(pan);
  114. updated = true;
  115. }
  116.  
  117. // Zoom by a vector
  118. public void zoom (Vec2 zoom) {
  119. screenMinInWorld = screenMinInWorld.add(zoom);
  120. screenMaxInWorld = screenMaxInWorld.sub(zoom);
  121. updated = true;
  122. }
  123.  
  124. // rotate by a certain angle
  125. public void rotate(float amount) {
  126. rotation += amount;
  127. updated = true;
  128. }
  129. }

So there we have it. Just define this class at the start of your project then use screenToWorld and worldToScreen to perform coordinate conversions.

Using the Camera class with OpenGL

If you are using OpenGL with an orthographic camera you can make your life even easier by including the following in your initialisation code:

  1. gl.glOrtho(camera.screenMinInWorld.x, camera.screenMaxInWorld.x, camera.screenMinInWorld.y, camera.screenMaxInWorld.y, -1.0, 1.0);

Then the following in your OpenGL display method:

  1. public void display(GL2 gl) {
  2.  
  3. // Move the camera position
  4. if(camera.updated == true) {
  5. gl.glMatrixMode(gl.GL_PROJECTION);
  6. gl.glLoadIdentity();
  7. gl.glOrtho(camera.screenMinInWorld.x, camera.screenMaxInWorld.x, camera.screenMinInWorld.y, camera.screenMaxInWorld.y, -1.0, 1.0);
  8.  
  9. gl.glTranslatef(camera.worldWidth()/2, camera.worldHeight()/2, 0);
  10. gl.glRotatef(-camera.rotation, 0, 0, 1);
  11. gl.glTranslatef(-camera.worldWidth()/2, -camera.worldHeight()/2, 0);
  12. }
  13. //...your display code...
  14. }

Finally if you want to support screen reshapes include the following in your OpenGL reshape method:

  1. public void reshape(GLAutoDrawable drawable, int arg1, int arg2, int arg3, int arg4) {
  2. camera.reshape(arg1, arg2, arg3, arg4);
  3. }
Tweet: 

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <c>, <cpp>, <drupal5>, <drupal6>, <java>, <javascript>, <php>, <python>, <ruby>. The supported tag styles are: <foo>, [foo].
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.