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

Discussion in 'Tutorials' started by kupo15, May 7, 2018.

  1. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    769
    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!

    [​IMG]

    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
    [​IMG]

    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:
    [​IMG]

    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
    [​IMG]

    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
    [​IMG]

    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: Jan 1, 2019
    Timf, GMWolf, NeZvers and 4 others like this.
  2. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    769
    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)
    [​IMG]
    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.
    [​IMG]

    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"
    [​IMG]

    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:

    [​IMG]

    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: May 8, 2018
  3. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    769
    Last Section, more in depth example and future things to explore coming soon


    If anything I mention in the memory background section is inaccurate, please let me know so I can change it! Hope you like the Guide so far! :)
     
  4. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    769
    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!
     
    JackTurbo likes this.
  5. The Reverend

    The Reverend Member

    Joined:
    Sep 8, 2016
    Posts:
    552
    Very interesting read - thank you for this tutorial :)
     
    kupo15 likes this.
  6. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    769
    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
     
  7. Jack McRip

    Jack McRip Member

    Joined:
    Dec 25, 2018
    Posts:
    12
    Thank you so much
    :)
     
    kupo15 likes this.
  8. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    769
    You're Welcome :)
     
  9. clee2005

    clee2005 Member

    Joined:
    Jun 25, 2016
    Posts:
    198
    @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 likes this.
  10. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    769
    You're welcome! Glad you find it useful :)
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice