Box2D SVG Parser with Curve Support
See this tutorial for instructions on creating Box2D levels in InkScape and importing them into Flash. It also explains floors and ceilings. Additionally, you can check out the SVG Parser thread off the Box2D forums to get updates that may not be posted here.
Functionality
The new parser will supports all declarations of curves. Additionally, it requires you to provide a new argument called resolution. Resolution is the number of segments each curve is split into.
If you pass a value less than 1 for resolution, the parser will use the formula (Line Width * 10) to determine resolution. So if you set the Line Stroke Width to 2 in InkScape, the curve will be split into 20 segments. Basically, thicker lines make smoother curves.
The green curves were drawn from left-to-right, so they are floors and check collision on their upper edge. The red curves are drawn right-to-left and check collision on their lower edge. Notice that none of the curves cover an angle of more than 180 degrees; you may end up with unexpected results if you do go over this limit.
Caution
You will need to use continuous collision checking on objects that are going to roll along curves at high speeds. The setBullet flag will help with this, but setting the whole world to continuous collision is needed for good looking rolls. Additionally, objects will roll with jerky motion if you set the line resolution too high.
The Parser in Action
To use the curve parser, import b2SVG.as and call parseSVG.
- parseSVG(SVG, b2World, Number, Number)
- SVG variable (see this tutorial)
- World – Your b2world object
- Ratio – Scale of pixels:units in your Box2D world
- Resolution – If > one, parses all curves with the passed argument. If < one, uses (Line-Width * 10) to determine curve resolution.
The Code
b2SVG.as
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | /** * @author John Nesky <http://www.johnnesky.com> * Bezier curves added by Quest Yarbrough <http://www.ezqueststudios.com> * partially based on some code by Helen Triolo <http://flash-creations.com/notes/sample_svgtoflash.php> * * This code is available under the LGPL license. <http://www.gnu.org/licenses/lgpl-3.0.txt> */ package { import Box2D.Dynamics.b2World; import Box2D.Common.Math.b2Vec2; import Box2D.Collision.Shapes.b2EdgeChainDef; import Box2D.Collision.Shapes.b2ShapeDef; import Box2D.Collision.Shapes.b2Shape; import b2Bezier; public class b2SVG { public static function parseSVG(svg: XML, world: b2World, RATIO:Number, useDefaultCurveResolution:Number): Array { var ns: Namespace = svg.namespace(""); var useCurveThickness:Boolean = false; var resolution:Number; if (useDefaultCurveResolution > 0) { resolution = useDefaultCurveResolution; } else { useCurveThickness = true; } var chainDef: b2EdgeChainDef = new b2EdgeChainDef(); chainDef.friction = 0.5; chainDef.restitution = 0.0; for each (var path: XML in svg..ns::path) { if (useCurveThickness) resolution = Math.round(returnStrokeWidth(path.@style) * 10); // the entire path: var d: String = path.@d; // Inkscape, Illustrator, and other programs use slightly different // formats for the path, because the SVG specs are very flexible. This // puts some strain on the people who write path parsers (me!), but // Helen Triolo's example illustrates an easy way to convert the path // to a consistent format and then break it up into an array: // replace whitespace with commas: var letter: String; for each (letter in [" ","\f","\n","\r","\t"]) d = d.split(letter).join(","); // surround letters with commas: for each (letter in ["M","m","Z","z","L","l","H","h","V","v","C","c","S","s","Q","q","T","t","A","a"]) d = d.split(letter).join("," + letter + ","); // insert commas before negative symbols: d = d.split("-").join(",-"); // now get all tokens separated by commas: var args: Array = d.split(","); // remove empty strings: args = args.filter(filterEmptyString); var currentPosition: b2Vec2 = new b2Vec2(0,0); var control1: b2Vec2 = null; var control2: b2Vec2 = null; var control3: b2Vec2 = null; var prevControl: b2Vec2 = null; var prevCommand: String = null; var relative: Boolean = false; var curve: Array; var shapes: Array = new Array(); chainDef.vertices.length = 0; var i: int = 0; while (true) { if (i == args.length) { // cleanup: If a path is incomplete, finish it without closing the loop // and add it to the Box2D world. if (chainDef.vertices.length >= 2) { chainDef.vertexCount = chainDef.vertices.length; chainDef.isALoop = false; shapes.push(world.GetGroundBody().CreateShape(chainDef)); } break; } var command: String = args[i]; i++; switch (command) { case "Z": case "z": // closepath: If a path is incomplete, finish it, close the loop, // and add it to the Box2D world. if (chainDef.vertices.length >= 3) { chainDef.vertices.pop(); // the last vertex of the loop is redundant. chainDef.vertexCount = chainDef.vertices.length; chainDef.isALoop = true; shapes.push(world.GetGroundBody().CreateShape(chainDef)); } chainDef.vertices.length = 0; chainDef.vertices.push(currentPosition); break; case "M": case "m": // moveto: If a path is incomplete, finish it without closing the loop // and add it to the Box2D world. // Start a new path. if (chainDef.vertices.length >= 2) { chainDef.vertexCount = chainDef.vertices.length; chainDef.isALoop = false; shapes.push(world.GetGroundBody().CreateShape(chainDef)); } relative = (command == "m"); if (relative) { currentPosition = new b2Vec2(currentPosition.x + args[i] / RATIO, currentPosition.y + args[i+1] / RATIO); } else { currentPosition = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO); } i += 2; chainDef.vertices.length = 0; chainDef.vertices.push(currentPosition); // According to the SVG spec, a moveto command can be implicitly followed // by lineto coordinates. So there is no "break" here. case "L": case "l": // lineto: a series of straight lines. Keep parsing until you hit a non-number. if (command == "l") relative = true; else if (command == "L") relative = false; while (!isNaN(parseFloat(args[i]))) { if (relative) { currentPosition = new b2Vec2(currentPosition.x + args[i] / RATIO, currentPosition.y + args[i+1] / RATIO); } else { currentPosition = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO); } i += 2; chainDef.vertices.push(currentPosition); } break; case "H": case "h": // horizontal lineto: a series of horizontal lines. // keep parsing until you hit a non-number. // Box2D works much better if adjacent parallel lines // are merged into one, so I'll go ahead and merge them. relative = (command == "h"); do { if (relative) { currentPosition = new b2Vec2(currentPosition.x + args[i] / RATIO, currentPosition.y); } else { currentPosition = new b2Vec2(args[i] / RATIO, currentPosition.y); } i++; } while (!isNaN(parseFloat(args[i]))); chainDef.vertices.push(currentPosition); break; case "V": case "v": // vertical lineto: a series of vertical lines. // keep parsing until you hit a non-number. // Box2D works much better if adjacent parallel lines // are merged into one, so I'll go ahead and merge them. relative = (command == "v"); do { if (relative) { currentPosition = new b2Vec2(currentPosition.x, currentPosition.y + args[i] / RATIO); } else { currentPosition = new b2Vec2(currentPosition.x, args[i] / RATIO); } i++; } while (!isNaN(parseFloat(args[i]))); chainDef.vertices.push(currentPosition); break; case "C": case "c": // curveto relative = (command == "c"); do { if (relative) { control1 = new b2Vec2(currentPosition.x + args[i] / RATIO, currentPosition.y + args[i+1] / RATIO); control2 = new b2Vec2(currentPosition.x + args[i+2] / RATIO, currentPosition.y + args[i+3] / RATIO); control3 = new b2Vec2(currentPosition.x + args[i+4] / RATIO, currentPosition.y + args[i+5] / RATIO); } else { control1 = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO); control2 = new b2Vec2(args[i+2] / RATIO, args[i+3] / RATIO); control3 = new b2Vec2(args[i+4] / RATIO, args[i+5] / RATIO); } i += 6; curve = b2Bezier.parseCurve([currentPosition, control1, control2, control3], resolution); curve.shift(); // the first point is redundant, "currentPosition" is already added. chainDef.vertices = chainDef.vertices.concat(curve); currentPosition = control3; } while (!isNaN(parseFloat(args[i]))); prevControl = control2; break; case "S": case "s": // shorthand curveto relative = (command == "s"); do { if (prevCommand == "C" || prevCommand == "c" || prevCommand == "S" || prevCommand == "s") { control1 = new b2Vec2(currentPosition.x*2 - prevControl.x, currentPosition.y*2 - prevControl.y); } else { control1 = new b2Vec2(currentPosition.x, currentPosition.y); } if (relative) { control2 = new b2Vec2(currentPosition.x + args[i] / RATIO, currentPosition.y + args[i+1] / RATIO); control3 = new b2Vec2(currentPosition.x + args[i+2] / RATIO, currentPosition.y + args[i+3] / RATIO); } else { control2 = new b2Vec2(args[i] / RATIO, args[i+1] / RATIO); control3 = new b2Vec2(args[i+2] / RATIO, args[i+3] / RATIO); } i += 4; curve = b2Bezier.parseCurve([currentPosition, control1, control2, control3], resolution); curve.shift(); // the first point is redundant, "currentPosition" is already added. chainDef.vertices = chainDef.vertices.concat(curve); currentPosition = control3; prevControl = control2; prevCommand = command; } while (!isNaN(parseFloat(args[i]))); break; case "Q": case "q": case "T": case "t": case "A": case "a": throw new Error("TODO: Unimplemented path command: " + command); break; } prevCommand = command; } } return shapes; } private static function filterEmptyString(item: *, index: int, array: Array): Boolean { return item != ""; } public static function returnStrokeWidth(pathS:String):Number { var bar:Array; bar = (pathS.replace("px", "")).split(";"); //get rid of px in line-width and split return Number(bar[3].slice(13, bar[3].length)); } } } |
b2Bezier.as
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | package { import Box2D.Common.Math.b2Vec2; /** * ...This class accepts an Array of four b2vec2s (corresponding to the SVG format of Bezier curves) and outputs an array of b2vec2s * The returned array contains all of the points that should be declared in an edgeChain version of the curve * This is a Flash port of the C++ version at http://www.box2d.org/forum/viewtopic.php?p=9865#p9865 * The math for transforming a Bezien curve is found at http://www.niksula.cs.hut.fi/~hkankaan/Homepages/bezierfast.html * Thanks to Shaktool off the Box2D forums for the help * * Quest Yarbrough/Ezion <www.ezqueststudios.com> */ public class b2Bezier { //Resolution is the number of lines to segment the curve into //cPoints must be structured like below. //cPoints[0] = starting point //cPoints[1] = starting point control point //cPoints[2] = end point control point //cPoints[3] = end point public static function parseCurve(cPoints:Array, resolution:Number) { if (resolution == 0) { return null; } var f:b2Vec2, fd:b2Vec2, fdd:b2Vec2, fddd:b2Vec2, fdd_per_2:b2Vec2, fddd_per_2:b2Vec2, fddd_per_6:b2Vec2; f = new b2Vec2(); fd = new b2Vec2(); fdd = new b2Vec2(); fddd = new b2Vec2(); fdd_per_2 = new b2Vec2(); fddd_per_2 = new b2Vec2(); fddd_per_6 = new b2Vec2(); var t:Number = 1.0 / resolution; var t2:Number = t * t; //I've tried to optimize the amount of //multiplications here, but these are exactly //the same formulas that were derived earlier //for f(0), f'(0)*t etc. f.x = cPoints[0].x; f.y = cPoints[0].y; fd.x = 3.0 * t * (cPoints[1].x - cPoints[0].x); fd.y = 3.0 * t * (cPoints[1].y - cPoints[0].y); fdd_per_2.x = 3.0 * t2 * (cPoints[0].x - 2.0 * cPoints[1].x + cPoints[2].x); fdd_per_2.y = 3.0 * t2 * (cPoints[0].y - 2.0 * cPoints[1].y + cPoints[2].y); fddd_per_2.x = 3.0 * t2 * t * (3.0 * (cPoints[1].x - cPoints[2].x) + cPoints[3].x - cPoints[0].x); fddd_per_2.y = 3.0 * t2 * t * (3.0 * (cPoints[1].y - cPoints[2].y) + cPoints[3].y - cPoints[0].y); fddd.x = fddd_per_2.x + fddd_per_2.x; fddd.y = fddd_per_2.y + fddd_per_2.y; fdd.x = fdd_per_2.x + fdd_per_2.x; fdd.y = fdd_per_2.y + fdd_per_2.y; fddd_per_6.x = (1.0 / 3) * fddd_per_2.x; fddd_per_6.y = (1.0 / 3) * fddd_per_2.y; var ret:Array = new Array(resolution); if (!ret) { return null; } for (var loop = 0; loop < resolution - 1; loop += 1) { ret[loop] = new b2Vec2(); ret[loop].x = f.x; ret[loop].y = f.y; f.x = f.x + fd.x + fdd_per_2.x + fddd_per_6.x; f.y = f.y + fd.y + fdd_per_2.y + fddd_per_6.y; fd.x = fd.x + fdd.x + fddd_per_2.x; fd.y = fd.y + fdd.y + fddd_per_2.y; fdd.x = fdd.x + fddd.x; fdd.y = fdd.y + fddd.y; fdd_per_2.x = fdd_per_2.x + fddd_per_2.x; fdd_per_2.y = fdd_per_2.y + fddd_per_2.y; } ret[ret.length - 1] = new b2Vec2(cPoints[3].x, cPoints[3].y); //last vertice not added by C++ version return ret; } } } |
These classes may be included in the Box2D engine at some point, but until then they are available here. If anyone comes up with errors, drop a response in the comments and I’ll see what I can do.
->
Thanks to John Nesky for helping write the curve parser, and writing/restructuring the entire parser.
Related posts:



August 24th, 2009 at 4:02 PM
Nice, but when can I find the b2EdgeChainDef class?
August 24th, 2009 at 5:51 PM
Check the Box2DFlash SVN Repository:
http://www.ezqueststudios.com/blog/creating-levels-box2d/
Getting an SVN client working and getting the latest copy might take a little time, but there’s a few updates there you cannot get in the public release.
October 15th, 2009 at 4:11 PM
i’m getting some strange problem when i’m trying to compile the code…
1119: Access of possibly undefined property friction through a reference with static type Box2D.Collision.Shapes:b2EdgeChainDef.
this is at line 43: ‘chainDef.friction = 0.5′
i downloaded the latest SVN repository, but i saw that in the b2EdgeChainDef.as that ‘extends b2ShapeDef’ was commented out.
i uncommented this but still get errors. am i doing something wrong?
October 15th, 2009 at 5:55 PM
Hm, based on your comment I assume you’ve already tried v2.0.1 on the Box2D frontpage along with the SVN.
The current SVN was updated to be 3-4x slower (revision 52) and was changed around a little bit, which might be the cause of the errors.
I’ve uploaded the copy of Box2D I’m using at the link below:
http://www.ezqueststudios.com/source/Box2DAS3.zip
Give that a shot and tell me if it works.
October 15th, 2009 at 8:25 PM
hey. thanks for the speedy help!
i’m still just trying to compile the parser, but it’s still giving me the same error. is it my file structure that could be causing the problem?
i’m just baffled as to why an instance of b2EdgeChainDef isn’t inheriting values from b2ShapeDef
October 16th, 2009 at 4:27 PM
Hm, are you having problems compiling Box2D as a whole or only the parser? I assume you probably have your SWF properties set to reference the Box2D folder? (In Flash, File -> Publish Settings -> “ActionScript 3.0 Settings” -> Source Path)
The error sounds like it isn’t finding the Box2D files, which is odd, but my only suggestion for now is to make sure the SWF knows the correct filepaths and that the b2SVG class isn’t in a lone package or something odd that would interfere with it finding the files.
I’ll let you know if I think of more possible solutions, but you might want to ask the guys down at the Box2D forums if nothing is working.
October 16th, 2009 at 9:31 PM
holy crap! thanks so much! it worked! it was a problem with my source path.