The goal in this tutorial is to make a sprite sheet generator tool with SFML.
In this tutorial we will make a tool to generate sprite sheets.
What is a sprite sheet ?
Multiple images merged into one such as the one above.
As most of you already know sprite sheets are useful in games to reduce gpu memory consumption and loading time of images by using a big POT texture.
There are already nice tools made to do generate sprite sheets such as TexturePacker but if you want more control over the generation you will probably need to modify the source code.
In this tutorial I will use C++ for the generator and the images data will be generated in XML. The goal of this article is to obtain a spritesheet and a xml file from these images.
<t f="sheet.png">
<i n="Agahnim'sShadow.png" x="0" y="0" w="144" h="102"/>
<i n="AnglerFish.png" x="144" y="0" w="168" h="144"/>
<i n="Angler_Key.png" x="0" y="102" w="17" h="10" r="90"/>
<i n="Armosknight_LA.png" x="17" y="102" w="96" h="96"/>
<i n="Blade_TrapLA.png" x="312" y="0" w="48" h="48"/>
<i n="BonePutter.png" x="360" y="0" w="81" h="57"/>
<i n="BowWow.png" x="312" y="57" w="123" h="123"/>
<i n="DeathEye.png" x="113" y="144" w="198" h="156" r="90"/>
<i n="GiantGhini.png" x="0" y="198" w="93" h="90"/>
<i n="HardhatbeetleOracle.png" x="0" y="288" w="48" h="48"/>
<i n="Hippo_Model.png" x="435" y="57" w="70" h="106"/>
<i n="LA_Piece_of_Power.png" x="113" y="102" w="24" h="24"/>
<i n="LikeLikeLA.png" x="441" y="0" w="45" h="48" r="90"/>
<i n="MegaThwomp.png" x="311" y="180" w="96" h="93"/>
<i n="MegaThwompcracked.png" x="407" y="180" w="96" h="93"/>
<i n="Mr.Write.png" x="311" y="273" w="96" h="96"/>
<i n="RedHinox.png" x="407" y="273" w="96" h="96"/>
<i n="Richard.png" x="48" y="288" w="48" h="45" r="90"/>
<i n="ShroudedStalfosLA.png" x="96" y="300" w="54" h="87"/>
<i n="SparkLA.png" x="48" y="333" w="48" h="48"/>
<i n="SpikedBeetleLA.png" x="0" y="336" w="48" h="45"/>
<i n="SwordMoblin.png" x="0" y="381" w="69" h="54" r="90"/>
<i n="TalTal.png" x="150" y="300" w="160" h="128"/>
<i n="Walrus.png" x="310" y="369" w="90" h="96" r="90"/>
<i n="Warp_Point_LA.png" x="400" y="369" w="96" h="96"/>
<i n="Wind_Fish's_Egg.png" x="69" y="387" w="58" h="50" r="90"/>
<i n="Wind_Fish.png" x="127" y="428" w="130" h="82"/>
<i n="WitchLA.png" x="0" y="437" w="90" h="48" r="90"/>
<i n="ZolGreenOracle.png" x="90" y="437" w="36" h="33"/>
</t>
Things to download before staring to code the generator :
Now you can create a SFML project : official tutorials
For rapidXML you simply need to include the folder to the include path since there are only headers (manual).
The key to a good sprite sheet generator is the algorithm used to pack the textures.
We want to find the optimal packing of a set of n images but there is one small thing :
texture packing problem also known as Bin packing problem is NP-Hard. This means we do not have only one best algorithm but multiple algorithm and heuristics which can generate the best solution for the given set.
You can try to implement your own algorithm if you want but the chances are it will be inefficient. So we need to find a bunch of algorithm to solve the bin packing problem.
In this tutorial we will use the library of Jukka Jylänki released under public domain : RectangleBinPack
I will use this version in this tutorial : RectangleBinPack.7z
If you want to understand the algorithm before using them, you can read the articles written by the author of the library :
Now that we have everything we need time to make the generator. In this tutorial I will use only one algorithm int the file MaxRectsBinPack.h and MaxRectsBinPack.cpp with multiple heuristics to make the code simpler to understand.
To sum up the things we need to do are:
I should mention that the algorithm for the packing can rotate the rectangles so we will need to put that in the xml file as well.
You can try to do that on your own and play around a little bit. But if you do not know SFML or rapidXML you can read the main below.
Let’s took a look at the main to understand how to put all the pieces together.
std::vector<sf::Texture*> imgTex; // images textures
std::vector<std::string> imgTexID; // name of the images
std::vector<Image> images; // xml data of the images
std::string filename = "sheet"; // filename of the sprite sheet
sf::Vector2i size(512, 512); // size of the sprite sheet
sf::RenderTexture rend; // texture to render the sprite sheet
rend.create(size.x, size.y);
rbp::MaxRectsBinPack pack(size.x, size.y); //pack of image
// list all filenames in the folder images
std::vector<std::string> listAll = getListFiles("images\\");
// load all the images
for (auto& img : listAll) {
sf::Texture *texP = new sf::Texture();
texP->loadFromFile("images/" + img);
imgTex.push_back(texP);
imgTexID.push_back(img.substr(0, listAll.size() - 4));
}
float rotation = 0;
// choose the best heuristic
const rbp::MaxRectsBinPack::FreeRectChoiceHeuristic best1 = chooseBestHeuristic(&imgTex, size.x, size.y);
for (size_t i = 0; i < imgTex.size(); i++) {
// insert the image into the pack
rbp::Rect packedRect = pack.Insert(imgTex[i]->getSize().x, imgTex[i]->getSize().y, best1);
if (packedRect.height <= 0) {
std::cout << "Error: The pack is full\n";
}
sf::Sprite spr(*imgTex[i]); // sprite to draw on the rendertexture
// if the image is rotated
if (imgTex[i]->getSize().x == packedRect.height && packedRect.width != packedRect.height) {
rotation = 90; // set the rotation for the xml data
// rotate the sprite to draw
size_t oldHeight = spr.getTextureRect().height;
spr.setPosition((float) packedRect.x, (float) packedRect.y);
spr.rotate(rotation);
spr.setPosition(spr.getPosition().x + oldHeight, spr.getPosition().y);
}
else { // if there is no rotation
rotation = 0;
spr.setPosition((float) packedRect.x, (float) packedRect.y);
}
rend.draw(spr); // draw the sprite on the sprite sheet
// save data of the image for the xml file
images.push_back(Image(filename, imgTexID[i], packedRect.x, packedRect.y, packedRect.width,
packedRect.height, (size_t)rotation));
}
rend.display(); // render the texture properly
// free the memory of the images
for (auto& tex : imgTex) {
delete(tex);
}
// save the sprite sheet
sf::Texture tex = rend.getTexture();
sf::Image img = tex.copyToImage(); // need to create an image to save a file
img.saveToFile("sheets/" + filename + ".png");
// generate the xml document
std::string xml = getXMLSheet(images, filename + ".png");
std::cout << xml; // display the xml file in the console
// save the xml document
std::ofstream xml_file;
xml_file.open("sheets/" + filename + ".xml");
xml_file << xml;
xml_file.close();
// see the occupancy of the packing
std::cout << "pack1 : " << pack.Occupancy() << "%\n";
// SFML code the create a window and diplay the sprite sheet
sf::RenderWindow window(sf::VideoMode(size.x, size.y), "Sprite sheets generator");
sf::Sprite spr(tex);
while (window.isOpen()) {
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed)
window.close();
}
window.clear(sf::Color::White);
window.draw(spr);
window.display();
}
As you can see the main is pretty short. But if you try to compile that directly you will get errors like :
1>main.cpp(102): error C3861: ‘getListFiles’ : identifier not found
1>main.cpp(115): error C3861: ‘chooseBestHeuristic’ : identifier not found
1>main.cpp(160): error C3861: ‘getXMLSheet’ : identifier not found
Which is normal because these functions are not implemented yet.
The method getListFiles return the filename of every files in a folder, this method is system dependent so the code I will give you will only work on windows. You can get another function to do the same thing on stackoverflow.
The best method if you have a big project is to use boost.
std::vector<std::string> getListFiles(std::string dirName) {
DIR *dir;
std::vector<std::string> list;
struct dirent *ent;
std::string filename = "c:\\Users\\Nicolas\\Google Drive\\project\\SpriteSheetsGenerator\\
SpriteSheetsGenerator\\" + dirName;
if ((dir = opendir(filename.c_str())) != NULL) {
// print all the files and directories within directory
while ((ent = readdir(dir)) != NULL) {
std::string str = ent->d_name;
if (str != "." && str != "..")
list.push_back(str);
}
closedir(dir);
}
return list;
}
If you want to use this function you need to change the path of the filename and include the file dirent.h
The next function chooseBestHeuristic try every heuristics for the algorithm and pick the best one by comparing occupancy.
rbp::MaxRectsBinPack::FreeRectChoiceHeuristic chooseBestHeuristic(std::vector<sf::Texture*> *rects,
size_t texWidth, size_t texHeight) {
rbp::MaxRectsBinPack pack;
std::vector<rbp::MaxRectsBinPack::FreeRectChoiceHeuristic> listHeuristics;
listHeuristics.push_back(rbp::MaxRectsBinPack::RectBestAreaFit);
listHeuristics.push_back(rbp::MaxRectsBinPack::RectBestLongSideFit);
listHeuristics.push_back(rbp::MaxRectsBinPack::RectBestShortSideFit);
listHeuristics.push_back(rbp::MaxRectsBinPack::RectBottomLeftRule);
listHeuristics.push_back(rbp::MaxRectsBinPack::RectContactPointRule);
rbp::MaxRectsBinPack::FreeRectChoiceHeuristic res;
float max = 0;
for (auto& heu : listHeuristics) {
pack.Init(texWidth, texHeight);
for (size_t j = 0; j < rects->size(); j++) {
pack.Insert(rects->at(j)->getSize().x, rects->at(j)->getSize().y, heu);
}
if (pack.Occupancy() > max) {
max = pack.Occupancy();
res = heu;
}
}
return res;
}
The next functions getXMLSheet generate the xml document from the data.
std::string getXMLSheet(std::vector<Image> images, std::string name) {
rapidxml::xml_document<> doc;
rapidxml::xml_node<>* root = doc.allocate_node(rapidxml::node_element, "t");
root->append_attribute(doc.allocate_attribute("f", doc.allocate_string(name.c_str())));
doc.append_node(root);
for (auto& i : images) {
rapidxml::xml_node<>* child = doc.allocate_node(rapidxml::node_element, "i");
child->append_attribute(doc.allocate_attribute("n", doc.allocate_string(i.getName().c_str())));
child->append_attribute(doc.allocate_attribute("x", doc.allocate_string(toStr(i.getTx()))));
child->append_attribute(doc.allocate_attribute("y", doc.allocate_string(toStr(i.getTy()))));
child->append_attribute(doc.allocate_attribute("w", doc.allocate_string(toStr(i.getTw()))));
child->append_attribute(doc.allocate_attribute("h", doc.allocate_string(toStr(i.getTh()))));
if (i.getR() != 0) {
child->append_attribute(doc.allocate_attribute("r",
doc.allocate_string(toStr(i.getR()))));
}
root->append_node(child);
}
std::string xml_as_string;
print(std::back_inserter(xml_as_string), doc, 0);
return xml_as_string;
}
In this function we need to convert a size_t to a string.
const char* toStr(size_t value) {
std::string str = std::to_string(value);
return str.c_str();
}
Everything is implemented maybe you will need to add this line in the MaxRectsBinPack.cpp to have min and max:
#include <algorithm>
If you have some problems with the compilation ou can have a look at the vs2015 project (with the libraries) of the sprite sheet generator: SpriteSheetsGenerator.7z
However do not forget to change the include path and the lib path of the project.