Graphics Using TexturePacker: A guide to proper external loading and texture management

kupo15

Member
GM Version: Studio 2
Target Platform: Windows
Download: (N/A) code examples below
Links:
Relevant GMC discussion:
https://forum.yoyogames.com/index.php?threads/gms-2-memory-management-inquiry-suggestion.12365/
https://forum.yoyogames.com/index.php?threads/memory-use.36369/
https://forum.yoyogames.com/index.p...ment-101-tricks-and-clarity-sprite_add.35850/

TexturePacker Website

Summary:
This is a tutorial about creating and decoding JSON data structures. This particular example uses texture pages created with a commercial software package called TexturePacker. But the techniques are applicable to other JSON data applications.

In this tutorial I will be demonstrating and showing you how to integrate the program TexturePacker by CodeandWeb into your graphics workflow. Doing so will allow you to take control over your texture pages to optimize texture memory instead of having GM do it for you. This provides you with the freedom to proper memory management natively without dlls or extensions which results in allowing you to create bigger games and use more texture memory at once than you otherwise would be able to.

Who is this system designed for?
This method is not necessary for everyone and every project. Its useful mainly for those who have really large projects and large resolution games where memory tends to be a source of concern. GM does a fine job on its own for smaller games and unless you desire this amount of control and extra work, you are better off using GM's built in IDE capabilities

Brief Introduction to memory
(skip to the tutorial if you know about this)
If anything in the spoiler is inaccurate, please inform me so I can modify it!
The memory "wall" in brief
One of the main problems we have to solve especially for the large projects is running out of memory. Currently in GM, that memory limit is 2GB (but you really only have ~1.7GB) meaning that the game will not run if you use more than this amount. This is because GM1.4 creates a 32bit application when you compile your game, I'm not sure if GM2.0 compiles a 32bit game.

The issue with adding sprites to the IDE
You might be wondering why is this needed? Doesn't GM already pack sprites onto texture pages efficiently to reduce memory usage and don't we have memory management with texture_flushing? Yes this is true however its still rather limited. Currently texture management within the IDE is only for VRAM which is memory loaded into a dedicated graphics card.

This does nothing to address the core concern that simply having resources in the IDE uses up precious system memory and chips away at that 2GB limit even if you aren't using those resources in the game. This means that even if you have an empty room in your game and no graphics loaded, you can still run out of memory simply by having a lot of graphics in the IDE.

This is due to all resources in the IDE being bundled into the same WAD file which is all put into memory. If in the future GM allows the use of multiple WAD files you can swap in/out of memory, this would make this system less useful because one of the goals of this tutorial is to mimic multiple WAD files. This brings us to...

External Files
But hey?! I can simply throw everything as an external file and never have to worry about getting close to that 2GB limit? Not so fast. If you never need to have a large amount of texture memory loaded at one time, then sure the memory limit won't be an issue. You still might run into performance issues associated with simple sprite_add functions which put each resource on its own texture page and requires swaps between each loaded resource.

However, if you are making a game with a large world, high res graphics and super fluid animations the simple sprite_add won't work. This will put each resource on its own texture page which most likely will waste space by requiring a bigger tex page than needed. Essentially you don't have the benefits of smart texture atlas to fit everything efficiently the way GM does with sprites inside the IDE

The Goal
The goal is to manually perform the same texture packing method GM does with sprites inside the IDE and apply that to external loading via sprite_add. We do this by:

1. Creating our own texture pages in TexturePacker
2. Add that texture page into the IDE
3. Draw_sprite_general only the sprites in that page we want to display in our game

This is what GM does behind the scenes. You think draw_sprite(spr_walk,image_index,x,y) simply displays an isolated sprite strip? Think again. When you ask to draw spr_walk, subimage0, GM finds which messy looking texture page that subimage is on at its location and draw_part() from that texture page only where that frame is located. Draw_sprite secretly is draw_part under the hood.

What is TexturePacker?
Here's a basic overview of the tool and the concept behind what we are going to do. The next section shows how to integrate Texturepacker into a workflow that GM can use!



TexturePacker is an external program dedicated to packing and creating texture pages, just like GM does automatically except better. There is a free trial but its relatively inexpensive to purchase a full license. Its better for these reasons:

1. It gives you more control over the size of texture pages you want created
2. You can easily scale the tex page itself without needing to individually scale every sprite and even has HD scaling
3. Allows for more packing options such as polygon fitting and rotation!
4. Can preview your pages before compiling to see how they fit and how much memory will be used allowing for a greater deal of control and memory management

Does it really pack things better? Check out this simple comparison


The left was generated by GM with a max Tex Size of 4096x4096. Even on its simplest settings, Texture packer packed it in a smaller texture size than GM was able to do resulting in halving the memory usage! If external images weren't placed onto a power of 2 texture page, you could have texturepacker output a texture page size tailored exactly to what is needed for even more memory savings. If only!

However, Texturepack has an additional memory saving feature GM doesn't have that you can take advantage of:


Allow rotation enables individual sprites to be rotated 90 degrees which allows Texture pages to be packed even tighter. GM doesn't do this which results in wasted space by forcing all sprites to be packed in their original rotation instead potentially rotating them to fit optimally.
Guide
this section is only going to show how to extract the data from the files. How to apply it will be in the next section.

Outputting from TexturePacker
When you export from TexturePacker, it spits out two files: A texture page(s) and a data file associated with that texture page. TexturePacker has two file formats for export that is useful for GM


Json Array and Json Hash. There is only a small difference in the formats, the array decodes into a ds_list where all the sprites can easily be looped through and the hash decodes into an all ds_map json format. I've found that the Jason Array is the best method to work with simply because the list makes it easy to loop through but you might find the Hash could work better for what you need.

The first step is to take the exported files created from TexturePacker and through them in the included files. You can simply sprite_add the texture pages from the included files folder to load them into memory. All the magic now happens with decoding that json file and creating a drawing system to display the correct sprites when you want to

Json decoding
Credits
Before I begin I want to recognize the members that helped me out with a previous thread of mine dealing with JSON files here https://forum.yoyogames.com/index.php?threads/json-decode-help-solved.35997/
Also a huge shoutout to @chamaeleon @Tsa05 because their code snippets in that thread together gave me the information needed to put all the pieces together to create this system I'm going to be talking about in this tutorial.

When decoding the json, you can't simply json_decode that file as it will not work. Instead you have to create a string and line by line, read from the json file and add it to the string. Then decode that entire string

Code:
var filename = [included file path];
var data = "";
var json = file_text_open_read(filename);

while(!file_text_eof(json))
    {
    data += file_text_read_string(json);
    file_text_readln(json);
    }
file_text_close(json);

var resultMap = json_decode(data); // decode the json
resultMap now holds all the json information as nested maps/lists depending on the type of json you are using. The first parsing script is for the Json Array Format.

Each Json file has two sections in it, one for the frames or data for each sprite and one section for meta data of the texture page. The metadata section can be largely ignored unless you need it but I included it anyway. Here is a section of the json Array


Here is the script to pull all the information shown. You can delete from the script the information you don't need but I included everything

Json Array
The purpose of an Array Json is it makes it easy to loop through all the sprite sections because of the format and extract the data for each that you want.
Code:
var framesList = resultMap[? "frames"]; // load all the frame section information
var metaList = resultMap[? "meta"]; // load all meta data

// loop through all sprites
for(var i=0;i<ds_list_size(framesList);i++)
    {
    var frameROOT = framesList[| i]; // get ALL sprite data

    // extract individual data
    var spritename = frameROOT[? "filename"];
    var rotated = frameROOT[? "rotated"];
    var trimmed = frameROOT[? "trimmed"];

    // get Sprite Source Size data
    var sssMap = frameROOT[? "spriteSourceSize"]; // decode the map
    var sss_x = sssMap[? "x"];
    var sss_y = sssMap[? "y"];
    var sss_w = sssMap[? "w"];
    var sss_h = sssMap[? "h"];
 
    // get source size data
    var ssMap = frameROOT[? "sourceSize"]; // decode the map
    var ss_w = ssMap[? "w"];
    var ss_h = ssMap[? "h"];
 
    // get Frame data
    var frameMap = frameROOT[? "frame"]; // decode the map
    var frame_x = frameMap[? "x"];
    var frame_y = frameMap[? "y"];
    var frame_w = frameMap[? "w"];
    var frame_h = frameMap[? "h"];
 
    // get pivot data
    var pivotMap = frameROOT[? "pivot"]; // decode the map
    var pivot_x = pivotMap[? "x"];
    var pivot_y = pivotMap[? "y"];
    }
 
// Get Metadata
var version = metaList[? "version"]; // get version
var format = metaList[? "format"]; // get format
var scale = metaList[? "scale"]; // get scale
var image = metaList[? "image"]; // get imagename

// Get texture size
var sizeMap = metaList[? "size"];
var size_w = sizeMap[? "w"];
var size_h = sizeMap[? "h"];
Json Hash
This format is better suited if you only want to extract a particular image data that you know ahead of time instead of pulling all the data at once. I don't find it that useful currently
Code:
var filename = "sprite_0"; // the name of the sprite you are looking for

var framesList = resultMap[? "frames"]; // load all the frame section information
var metaList = resultMap[? "meta"]; // load all meta data

// Get Targeted Image Data
var spritename = filename;
var frameROOT = framesList[? string(spritename)]; // decode the sprite data

// extract individual data
var rotated = frameROOT[? "rotated"];

// get Sprite Source Size data
var sssMap = frameROOT[? "spriteSourceSize"]; // decode the map
var sss_x = sssMap[? "x"];
var sss_y = sssMap[? "y"];
var sss_w = sssMap[? "w"];
var sss_h = sssMap[? "h"];
 
// get source size data
var ssMap = frameROOT[? "sourceSize"]; // decode the map
var ss_w = ssMap[? "w"];
var ss_h = ssMap[? "h"];
 
// get Frame data
var frameMap = frameROOT[? "frame"]; // decode the map
var frame_x = frameMap[? "x"];
var frame_y = frameMap[? "y"];
var frame_w = frameMap[? "w"];
var frame_h = frameMap[? "h"];
 
// get pivot data
var pivotMap = frameROOT[? "pivot"]; // decode the map
var pivot_x = pivotMap[? "x"];
var pivot_y = pivotMap[? "y"];
 
// Get Metadata
var scale = metaList[? "scale"]; // get scale
var image = metaList[? "image"]; // get imagename

// Get texture size
var sizeMap = metaList[? "size"];
var size_w = sizeMap[? "w"];
var size_h = sizeMap[? "h"];

And don't forget to destroy the resultMap ds_map to avoid a memory leak. This will also destroy all the nested data structures as well
Code:
ds_map_destroy(resultMap);
Stay tuned for the next section where I will go over an example of how to use the data you have to draw a single image. I'll get more in detail with what the sections in the json file are for.
 
Last edited:

kupo15

Member
Simple Application

In this section we will add to the code snippets from before and come up with a way to apply the data we got from the json to drawing a sprite. This will be for sprites with an origin set to the top left (0,0) to make things easy.

I'll be using the Json Array file type and the only data we will need from it are:
-sprite name
-rotated (true/false)
-Frame Data Section (x,y,w,h)
The next hurdle we have to overcome is figuring out how to know what sprites we are pulling the data from so we can use it. Remember, even if you name your sprites in order they can be in any order on any texture page because its trying to pack things the best. The method I came up with is creating a separate ds_map to hold the different variables because ds_maps are designed to be unordered, thus easy to search.
These maps are all going to have the key be the sprite name and a value corresponding to the data that map holds.
Code:
// create the ds_maps
tex_data = ds_map_create();
rotate_data = ds_map_create();
left_data = ds_map_create();
top_data = ds_map_create();
width_data = ds_map_create();
height_data = ds_map_create();
Also we need to create a new system to load both the JSON files and the texture files we added to the included files.


Here is the complete script for loading these files and parsing them. I also turned the previous snippets in the post above into scripts
Code:
/// scr_load_external_files()

// loop through all the texture pages
var tex_num = -1;
while true
    {
    tex_num ++;
    var filename = "texture_export-"+string(tex_num)+".png"; // path of tex files in included files
    var json_filename = "data_export-"+string(tex_num)+".json"; // path of json data files in included files
 
    if file_exists(filename)
        {
        tex_page[tex_num] = sprite_add(filename,0,false,false,0,0); // add the texpage
        var resultMap = load_json(json_filename); // load the texture page's json file
        parse_json_array(resultMap,tex_page[tex_num]); // extract all sprites on the texpage
        }
    else
    break;
    }
The following are the scripts inside the above snippet
Code:
/// load_json()
/// @param filename

var filename = string(argument0);
var json = file_text_open_read(filename);
var data = "";

while(!file_text_eof(json))
    {
    data += file_text_read_string(json);
    file_text_readln(json);
    }
file_text_close(json);

return json_decode(data); // decode the json
Here is the json array parsing code simplified only to use the values we need with the additional code to add them to the map
Code:
/// parse_json_array(resultMap,texpage_id);

var framesList = argument0[? "frames"]; // load all the frame section information
var tex_page = argument1; // texpage id from sprite_add

// loop through all sprites
for(var i=0;i<ds_list_size(framesList);i++)
    {
    var frameROOT = framesList[| i]; // get ALL sprite data

    // extract individual data
    var filename = frameROOT[? "filename"];
    var rotated = frameROOT[? "rotated"];

    // get Sprite Source Size data
 
    // get source size data
 
    // get Frame data
    var frameMap = frameROOT[? "frame"]; // decode the map
    var frame_x = frameMap[? "x"];
    var frame_y = frameMap[? "y"];
    var frame_w = frameMap[? "w"];
    var frame_h = frameMap[? "h"];
 
    // get pivot data
 
    // add to map
    ds_map_add(tex_data,filename,tex_page);
    ds_map_add(rotate_data,filename,rotated);
    ds_map_add(left_data,filename,frame_x);
    ds_map_add(top_data,filename,frame_y);
    ds_map_add(width_data,filename,frame_w);
    ds_map_add(height_data,filename,frame_h);
    }
As you can see, as we are looping through all the sprites on a tex page, we are adding that data to the new maps we created using the sprite_name as the key.

The last step is to pick a sprite we want to draw, pull the data from the maps and draw it. We are going to pick this particular sprite as an example. The sprite is named "sprite1_0"


Code:
/// draw_script

var sprite = tex_data[? "sprite1_0"]; // tex page id
var left = left_data[? "sprite1_0"]; // left coor on the tex page
var top = top_data[? "sprite1_0"]; // top coor on the tex page
var width = width_data[? "sprite1_0"]; // original width of sprite
var height = height_data[? "sprite1_0"]; // original height of sprite
var rotated = rotated_data[? "sprite1_0"]; // if rotated
var xx = 500; // the x coor of where to draw on the screen
var yy = 500; // the y coor of where to draw on the screen

if rotated
   {
   yy +=  height; // offset the y coor by the original height
   // swap the width and height
   var old_width = width;
   width = height;
   height = old_width;
   }
 
draw_sprite_general(sprite,0,left,top,width,height,xx,yy,1,1,90*rotated,c_white,c_white,c_white,c_white,1);
Drawing is pretty straightforward using accessors to pull the needed data from the different ds_maps we created....EXCEPT rotated sprites are a bit tricky and require and additional step. Take a look:



The faded sprite is what is on the texture page which shows the width is smaller than the height. However, the JSON data gives us the original, unrotated dimensions which has the width longer than the height. Since we are drawing part of the tex page which has the sprite rotated, we have to swap the width and height so we are drawing the sprite correctly.

We also set the rotation value to 90*rotated so that if the sprite isn't rotated, that value is 0 and thus not rotated. If it is rotate, that value is 90 and thus rotated 90 deg. As you can see above if the red dot represents the top left corner and the position on the screen we want to draw it to, after rotating that point becomes the bottom left corner of the rotated sprite. Therefore we need to add the original height of the sprite to the y coor we want the sprite displayed at. This is why I do this step before swapping the dimensions.

Lastly when you are done with these textures and want to delete them from memory, simply loop through the created array assigned with sprite_add and clear (or destroy) the ds_maps we created
Code:
// delete menu background textures
for(var i=0;i<array_length_1d(tex_page);i++)
sprite_delete(tex_page[i]);

// clear texture maps
ds_map_clear(tex_data);
ds_map_clear(rotate_data);
ds_map_clear(left_data);
ds_map_clear(top_data);
ds_map_clear(width_data);
ds_map_clear(height_data);


Hopefully that wasn't too difficult to follow. If you have a better method for being able to sort the JSON data instead of creating multiple ds_maps, please let me know! The next section will deal with drawing a sprite with the origin not at 0,0
 
Last edited:

kupo15

Member
Polygon Export
1664755524833.png

This is another way TexturePacker is able to pack and create texture pages by using . Instead of packing each sprite cleanly into its rectangular borders, it outlines each sprite via many triangle polygons. This allows the ability to have other sprites "invade" the empty space of other sprites to pack the pages even tighter. This will create "garbage" overlapping sprite pixels within the rectangular space of the sprite you want to draw, but we have a way to draw to only the parts we want using primatives.
1664755732250.png

Polygon Struct
This exports a few new pieces of data within the json file in order to draw each individual triangle
1664755953100.png
  • Triangles: a group of 3 that represents the triangle indices that are used to create a triangle in the following vertice arrays
  • Vertices: The x and y position within the untrimmed texture page the polygon points are located (not needed)
  • VerticesUV: The x and y position within the trimmed texture page the polygon points are located
For example:
triangles[0] = [24,25,26


  • frame: the coordinates, width and height of the sprite inside the spritesheet.
  • trimmed: whether the empty pixels were removed or not.
  • spriteSourceSize: the coordinates and size of the trimmed sprite inside the original image.
  • sourceSize: the size of the original image.
 
Last edited:

kupo15

Member
Kupo, when you sort out how youre gonna approach this issue fancy doing a write up of it here? Id be curious to hear how you eventually tackle the issue.
That day has come! :)

Any and all feedback is welcomed! The first post primarily deals with the basics of importing the json file from TP plus lots of front end information about what we are trying to accomplish. The second post is the meat of the guide showing an application of it!

Section 3 is yet to come!
 

kupo15

Member
Thanks, glad you liked it! :) I should also note this can be done internally as well and from my tests it seems that might be the best method. Simply load those texture pages in as sprites instead of included files
 

clee2005

Member
@kupo15 thanks so much for writing this all down. It's a fantastic way to get around the sprite_add limitation (1 image per texture page). I'm looking at using Rubber along with TexturePacker CLI and then - gods willing, should have a pretty good setup.
 

kupo15

Member
@kupo15 thanks so much for writing this all down. It's a fantastic way to get around the sprite_add limitation (1 image per texture page). I'm looking at using Rubber along with TexturePacker CLI and then - gods willing, should have a pretty good setup.
You're welcome! Glad you find it useful :)
 

Khao

Member
I've been looking for something like this for a while. My game is aiming towards a full HD presentation, and though my characters are relatively small, they're animated by hand. Each with several different actions with lots of individual frames.

With every character needing their own 4096x4096 page, I was pretty worried I would be hitting the memory limits on the way to my planned roster of 8 characters. Especially once I consider audio, stage backgrounds and menu graphics. And even more so when I consider the possibility of expanding the roster, if those 8 characters prove to be feasible enough.

Welp, this not only solves all of my worries. It means I might be able to have character sprites at an even higher resolution to ensure no detail is lost when the camera zooms in, which is something I've been worried about.

So thank you so much for this. If I manage to implement this correctly, it basically means I get to stop worrying about texture limits!

And seeing you working on a game that's way more ambitious than mine graphically speaking just fills me with hope. Hope you're successful!
 

kupo15

Member
Thanks so much! Just saw this. This is for the game in your sig? It looks pretty neat!

Yeah it seems like you won't have to worry about memory issues if your characters each hold one 4096x page as one of my characters hold at least 8, and there are two on the screen at once...not included a few pages for the stage! So far I'm only using this for my stage backgrounds, eventually I'll get around to transitioning the characters to this but even as is I don't need this system for the characters which is quite amazing.

It seems like you don't come close to needing a system as complex as what I'm doing which makes things simpler for you.

So thank you so much for this. If I manage to implement this correctly, it basically means I get to stop worrying about texture limits!
Pretty much...but technically its not limitless still. This system is designed to squeeze even more memory saving out of a texture system so you can maximize your memory usage better. There still are two hard memory limits regardless unfortunately. One is VRAM which this system optimizes. I haven't tried out of an irrational fear but I doubt I could load 3 characters at once...maybe I could get 4 actually. But definitely not all my characters at once even with this system haha

The other is the RAM which is less likely to hit the limit. Simply having sprites in the IDE uses up RAM even if those sprites aren't being used in game. The thing is those sprites are compressed so they are compact and save lots of space. Though theoretically you could have so so much high res sprites that eventually you'll run out of space. But that doesn't seem very likely something to be concerned about. Most likely, you'll run out of VRAM well before you run out of RAM.

And seeing you working on a game that's way more ambitious than mine graphically speaking just fills me with hope. Hope you're successful!
Thanks, appreciate it! :)
 

Khao

Member
It seems like you don't come close to needing a system as complex as what I'm doing which makes things simpler for you.
Yeah, I'm starting to investigate a little bit more. If I did my research right, in theory, my characters would be using around 512 mb in total. Adding the stages to that (which would generally fit on 2048x pages considering they're mostly single-screen backgrounds with just a few extra small objects), and the generic elements (effects, gui, general objects) my entire game actually ends up using slightly less than 1gb ram which is nothing to worry about. Doubly so, considering that my game is currently using way less ram than I'd expect from my research anyway, so the end result might be even lower than I anticipate.

Still. Using this system is very tempting in that I could literally just duplicate the size of my characters and not run into any issues. This would give me smoother zoom-ins, which are used pretty often, and would also make the game look amazing in 4k. Without external loading, though, I'm assuming this is not even an option due to the massive memory requirements. I might be missing something though, I'm very surprised you don't even need this systems for characters!

Still, I'll be trying it out to see if it works out for me. If it's doable and doesn't complicate development too much, I'll go for it and use larger characters. Otherwise, welp, the game is still gonna work well enough, and if I want to make smoother zoom-ins, I can still increase the size of just the animations that trigger a zoom more often. I'll be posting again if it works!

Also, yes, it's the game in my sig. Thanks!
 
Last edited:

rIKmAN

Member
Most likely, you'll run out of VRAM well before you run out of RAM.
The GMS2 IDE is 64bit, but it still only compiles 32bit binaries for Windows so you will be bound by the same 32bit limits found in GMS 1.4.
UWP compiles 64bit binaries, but the regular Windows Desktop export is still (sadly) 32bit only.

The problem isn't "running out" of RAM in the traditional sense, it's that a 32bit process can't access more than ~2GB by default - no matter how much RAM the user has installed.
 

kupo15

Member
Still. Using this system is very tempting in that I could literally just duplicate the size of my characters and not run into any issues. This would give me smoother zoom-ins, which are used pretty often, and would also make the game look amazing in 4k. Without external loading, though, I'm assuming this is not even an option due to the massive memory requirements. I might be missing something though, I'm very surprised you don't even need this systems for characters!
Yeah I'm very surprised about my characters too! And considering that your characters are 512mb TOTAL is good for you. If you only plan on having 2 loaded at one time then you are using even less memory. At least this system is here to give you flexibility to do what you want to do. I know that feeling is huge as the major reason I pioneered this system was to give myself that comfort and push the limits to allow us to really push the game.

I will have to say though, if you plan on stitching together your backgrounds by the way of splicing segments to remove extra blank space (essentially not splitting up whole sections) you will need this system. Not for memory but for rendering. Because you are having a zooming camera, splicing a background will result in seam lines where you put them back together. Using TP allows you to extend the borders of your sprites automatically so that way it covers up the seams.

The GMS2 IDE is 64bit, but it still only compiles 32bit binaries for Windows so you will be bound by the same 32bit limits found in GMS 1.4.
UWP compiles 64bit binaries, but the regular Windows Desktop export is still (sadly) 32bit only.

The problem isn't "running out" of RAM in the traditional sense, it's that a 32bit process can't access more than ~2GB by default - no matter how much RAM the user has installed.
Yep that's exactly right. I should have been more specific :)
 

otterZ

Member
Excellent tutorial kupo15. This is a great option as I've already hit the 2GB limit in my game. This helps a lot. Thank you.
 
Top