Automatically Generating Sprites for a Space Shooter

Published May 18, 2015
Advertisement
?

I'm writing yet another generic space shooter while learning WTL, DirectX, and modern graphics programming along the way. Today was spent writing a script to build SVG fragments into a texture atlas. Here's the result of today's work:



atlas_sample.png


Sample generated atlas generated from SVG fragments

SVG fragment for "box"


[subheading]The game[/subheading]
This game is a mix of shoot-em-up, adventure, with some RPG, resource management, and some puzzle elements. Here's the engine so far:




game.png


75625 unique untextured sprites, averaging 70 FPS

I had prototyped a similar game in immediate mode OpenGL many years ago. The most tedious thing I remember from that project was that I had to rerender every sprite by hand every time I tweaked the style. It wasn't unusual to have sprites with different stroke widths and inconsistent glows.

With this game, I wanted to avoid all that nonsense by generating assets automatically. This way, I could tweak and adjust the style and sizes, without having to visit each asset by hand and rerendering them.


[subheading]The Objectives[/subheading]
The objectives are simple:
1. Reusable SVG template. A skeleton SVG file which will be used to generate fully-formed SVG elements. This will include the style, and the filters. A script will apply SVG fragments and dimensions to the skeleton and feed this to the SVG rasterizer.
2. Automated SVG rasterization. A library which will transform an SVG into a PNG. A library that accepts a SVG as a string instead of a file is preferred, but not necessary.
3. Texture atlas. The resulting PNGs will be packed into a single texture, and will also output an accompanying layout file.


[subheading]The Script[/subheading]
Because these assets will be compiled and assembled on my development machine, I chose to write the script in nodejs, because of its vast selection of libraries.


[subheading]The libraries[/subheading]
From what I could find and scan, following libraries that do most of the heavy lifting are javascript wrappers that facilitate inter-process communication between nodejs and the actual binary. It's not really a concern, as almost all dependencies, except imagemagick, were installed from node's package manager. Here are the libraries I chose for this script:

1. svg-to-png. Uses chromimum to render SVG files to PNGs. Imagemagick can render SVGs as well, but I didn't learn this until the end of the day.
2. binpacking.
3. gm with imagemagick. Wrapper library to wrap image rendering operations. Used to generate the texture atlas.
4. underscore. Provides functional-like methods and the templating engine.


[subheading]The Style[/subheading]
Here's the first iteration of the style.



style.png


Player's ship sprite. Notice that the stroke and blur effect expands outside of the canvas and the geometry snaps nicely to the canvas edges.


[subheading]The Template[/subheading]
From here, I exported an optimized SVG from Inkscape and split out the template and guts into two separate files, while additionally factoring out the common path attributes to a stylesheet.
g.main path,g.main rect,g.main circle { stroke-linejoin: round; stroke-linecap: round; stroke-width: 1px; fill: none; stroke: #FFF;} <%= frag %> Initial SVG template. The <%= %> tags are evaluated dynamically.



SVG fragment for the player's sprite. The comment at the beginning signifies to the script the area this sprite's geometry occupies.

It took quite a few tries to get the filter and feGaussianBlur to behave correctly, especially with esoteric attributes on the filter tags. In retrospect, it may have been easier to keep the fragments as full Inkscape SVG documents and had the script extract the geometry and filter out attributes. This would also have elimiated the need for the size comment.


[subheading]The Script[/subheading]
And finally, this ugly beast of a script:


(function(){ // dependencies var fs = require('fs'), rasterizer = require('svg-to-png'), binpacking = require('binpacking'), gm = require('gm').subClass({imageMagick:true}), u = require('underscore'); // config var E = function(){}, defs_dir = 'defs', temp_dir = 'temp', ends_with = function(str) { return function(val) { return val.substr(val.length-str.length,val.length) == str; } }; // prepare svg template var build = u.template(''+fs.readFileSync('template.svgt')), outline_px = 4, outline_u = 1, blur_offset_u = 1.5; // compile defs into templates console.log('compiling...'); var compiled = []; u.filter(fs.readdirSync(defs_dir),ends_with('.svgfrag')) .forEach(function(name){ // load frag, find viewbox var frag = ''+fs.readFileSync(defs_dir+'/'+name); var box = (/^/.exec(frag) || []).slice(1,5); // parse viewbox: box=left, bottom, right, top if ( box.length != 4 ) { console.error('size meta malformed in '+name); return; } for ( var kdx in box ) { var val = parseFloat(box[kdx]); if ( isNaN(val) ) { console.error('size parameter #'+kdx+' invalid'); return; } box[kdx] = val; } // calculate viewbox and texture size var T = blur_offset_u + outline_u/2; box[0] -= T; box[1] -= T; box[2] += T; box[3] += T; var vw = box[2] - box[0], vh = box[3] - box[1], tw = Math.floor(vw*outline_px), th = Math.floor(vh*outline_px) // stuff frag into template var svg = build(tmp = { vx: box[0], vy: box[1], vw: vw, vh: vh, tw: tw, th: th, frag: frag, }); // record output var base_name = name.replace(/\..+$/, ''); var out_name = base_name+'.svg'; var out_svg = temp_dir+'/'+out_name; var out_png = temp_dir+'/'+base_name+'.png'; var cwd = process.cwd(); compiled.push({ name: base_name, svg: process.cwd()+'/'+out_svg, // ??? png: out_png, w: tw+2, h: th+2, }); // save to file fs.writeFileSync(out_svg, svg); }); // render compiled svgs to pngs console.log('rendering...'); try { process.chdir('temp'); // ??? rasterizer .convert(u.pluck(compiled,'svg'),'../..') // ??? .then(function(){ // sort and pack compiled svgs console.log('packing...'); var packer = new binpacking.GrowingPacker(); compiled.sort(function(a,b){ if ( a.w < b.w ) return 1; if ( a.w > b.w ) return -1; if ( a.h < b.h ) return 1; if ( a.h > b.h ) return -1; return 0; }); packer.fit(compiled); // render packed images to an atlas // and build map for atlas var img = gm(packer.root.w+1,packer.root.h+1,"#00000000"); var atlas = ''; for ( var cdx in compiled ) { var c = compiled[cdx]; var f = c.fit; if ( !f.used ) { console.error('unused: ', c); continue; } img .stroke('#FFFF00FF',1) .fill('#FFFFFF00') .drawRectangle(f.x,f.y,f.x+c.w,f.y+c.h) .draw('image Copy '+(f.x+1)+','+(f.y+1)+' 0,0 '+c.png); atlas += [c.name,f.x,f.y,f.w,f.h].join("\t"); atlas += "\n"; } img.write('atlas.png',function(e){ if ( e ) console.error(e); console.log('done'); }); fs.writeFileSync('atlas.txt', atlas); }); } finally { process.chdir('..'); }})();Svg-to-png was especially dumb, because for some reason it expects at least two levels of directories for input files and output directory, or something dumb like that. Had to do some chdir to trick it to output to the correct directory. Specifying inputs ['temp/a.svg','temp/b.svg'] and output 'temp' from working directory 'resources/svg' would place 'a.png' in 'temp/resources/svg/a.png'. Yeah, that was fun. No documentation on this behavior, either.


[subheading]Post-Mortem[/subheading]
Overall, the day was well spent. I got a script that works while learning about nodejs, imagemagick, and et. al. I can tweak and change the style at any time and rebuild the assets. Extracting the SVG fragments from Inkscape was a bit of a pain: Sometimes the coordinates of the optimized SVG were translated 10 units +y or 1000 units +y for no reason. Other than that, I'm happy with the results.


Also, what's a content pipeline?
And how do I IPBoard journal??
8 likes 1 comments

Comments

tnovelli

Cool. I've been down this road before, hahaha... I remember Inkscape SVG throwing a lot of curves, like "group"
elements with "transform=" attributes; maybe svg-to-png doesn't handle those. You might have better luck with librsvg, squirtle, shiva2d(?), etc. It's possible to render SVG with surprisingly little code, even in realtime.

Ultimately I created my own format and editor so I could have a decent layer system like CAD, or Synfig. And dynamically animated humanoid vector sprites...

May 18, 2015 11:03 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement
Advertisement