Creating levels in Box2D part 1 - Parsing Inkscape SVG files in Java

Welcome to the first in a series of guides on how to create Box2D levels using by loading SVG images from Inkscape.

In this series I'm going to cover the whole process: loading the load the SVG document into the java enviroment, to parsing the SVG data using the open source library Batik to extract object, generating Benzier curves different levels of accuracy, triangulating your shapes using a constrained Delaunay triangulation and finally creating the Box2D objects and adding them to your Box2D world.

Firstly, what's the motivation for doing this? Designing levels for games manually is time consuming. Firstly you need to design the level then work out the coordinates for each point. Because Box2D can only work with convex objects with a maximum of 8 sides you need to decompose your level into convex objects.

The motivation is simple, it's much easier to design your levels in a paint program and then load them into Box2D than to manually enter the coordinates. You can buy software to do this for you but it's much better to know how to do it yourself than using a black box solution.

One word of warning. To follow this guide you will need to be proficient using Java and understand the basics of XML and the Document Object model.

Part 1: Loading your SVG document into Java

For this tutorial I'll be using Inkscape which is available to download for free here. The first step in the process is loading your SVG document into Java after that it will be possible to extract the path and shape data. You could load the file as a string but then you would have to write code to parse it to extract the path data. A better solution is to use pre-existing functionality to load the file as an XML document. After that you will be able to traverse the DOM to extract the relevant tags.

I tried using the XML loading functionality included in Batik but it was very slow - usually taking 3 - 4 seconds to load a simple SVG file. In the end I found it was better to use the standard Java XML parsing functionality. Below is the function I used:

  1. public Document loadXmlFile (String path) {
  2. //get the factory
  3. DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  4.  
  5. try {
  6.  
  7. //Using factory get an instance of document builder
  8. DocumentBuilder db = dbf.newDocumentBuilder();
  9.  
  10. //parse using builder to get DOM representation of the XML file
  11. Document dom = db.parse(path);
  12.  
  13. return dom;
  14.  
  15. }catch(ParserConfigurationException pce) {
  16. pce.printStackTrace();
  17. }catch(SAXException se) {
  18. se.printStackTrace();
  19. }catch(IOException ioe) {
  20. ioe.printStackTrace();
  21. }
  22. return null;
  23. }

The code above loads the resource from the specified URI (if the svg file is in your "src" file then you just need to put "src/[filename].svg" otherwise a path in the form "file:///Users/bensmiley45/Documents/workspace...." to the location of the file on your computer). The code returns the document object model representation of the file.

Parsing the Inkscape path information

Now we have the XML loaded into an object we need to think about parsing the SVG data to extract the information we're interested in. For a basic implementation we need four pieces of information:
- Global translation: When you move a path in Inkscape sometimes a global transformation takes place. This means that all object coordinates are translated by a certain amount. To render the paths in their correct positions we need to parse this global translation.
- Path point data: We need to extract the path point coordinates
- Path translation: Each path can also be translated individually
- Canvas dimensions: Internally Inkscape uses a coordinate system with the origin in the top right. This means that x increases left to right and y increases top to bottom. Box2D however uses a coordinate system with the origin in the bottom left hand corner. To convert the Inkscape coordinates to the Box2D coordinates we need to know the height of the Inkscape canvas.

The strategy I used was to create one class responsible for loading the SVG file, extracting and parsing the necessary data, applying the coordinate transform and generating the paths as Cubic Splines. I'll cover more of this in later sections but the basic structure of this class is as follows:

  1. public class Inkscape {
  2. // Used for logging
  3. public final String TAG = this.getClass().getSimpleName();
  4.  
  5. // Affine transform - used to apply coordinate transformations
  6. public AffineTransformation at = new AffineTransformation();
  7.  
  8. // An array of spline objects to store the paths we parse
  9. public ArrayList<Spline> paths = new ArrayList<Spline>();
  10.  
  11. private String URI;
  12.  
  13. // The class take the URI of the SVG file as it's constructor argument
  14. public Inkscape(String path) {
  15. URI = path;
  16.  
  17. // This class handles the loading of external data from files.
  18. ResourceManager res = new ResourceManager();
  19.  
  20. // We use the loadXmlFile method to load out SVG as detailed above
  21. Document dom = res.loadXmlFile(path);
  22.  
  23. // The base element is the outermost tag of the SVG i.e. the <SVG> tag
  24. Element baseElement = dom.getDocumentElement();
  25.  
  26. // Now in turn the pieces of data we need are extracted:
  27.  
  28. // parse attributes from the SVG tag - to get the canvas dimensions
  29. parseSVG(baseElement);
  30.  
  31. // Parse global transforms - get the global translation
  32. parseGlobalTransform(baseElement);
  33.  
  34. // Parse paths - get the individual path data
  35. parsePaths(baseElement);
  36. }
  37.  
  38. public void parseSVG (Element baseElement) {
  39.  
  40. // The canvas dimensions are just stored as floats. We use the getAttribute method to get the width and height as strings. Next we cast these
  41. //strings as floats
  42. float width = new Float(baseElement.getAttribute("width"));
  43. float height = new Float(baseElement.getAttribute("height"));
  44.  
  45. // Apply coordinate transformation to move the origin to the bottom left hand corner
  46. at.scale(1,-1);
  47. at.translate(0, height);
  48. }
  49.  
  50.  
  51. public void parseGlobalTransform(Element baseElement) {
  52. // TODO: this is covered in the next section of the guide
  53. }
  54.  
  55. public AffineTransformation parseGlobalTransform(String transform) {
  56. // TODO: this is covered in the next section of the guide
  57. }
  58.  
  59. public void parsePaths(Element baseElement) {
  60. // TODO: This is covered in the next section of the guide
  61. }
  62. }
  63.  

Affine Transform

A this point I think it's worth describing why I'm using an Affine Transformation object to store transformations. An Affine Transform is just a technical name for a a linear transformation which maps one 2D region to another 2D region. A linear transform is a transform is a transform where if you drew a straight line through three points in the original image you would be able to draw a straight line through the same three points in the transformed image.

The line might be at a different angle, shorter or longer but it would still be straight and still go through the three points. The advantage of using an Affine Transformation object is that it does a lot of the hard work for us. All transformations can be stored in one object and we can add simply add global transformations from the canvas to the local transformations for out line.

  1. at.scale(1,-1);
  2. at.translate(0, height);

This code uses the canvas height to move the origin from the top left corner of the screen to the bottom left corner of the screen. First the y-coordinate is inverted then the origin is moved down by the height of the canvas.

There we have it. We have successfully loaded an SVG file and started to extract the information we need. In the next section I'll describe how we can use the open source library Batik parse the transform and path data.

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.