Skip to content

ImageAsset Guide

Peter Robinson edited this page Aug 5, 2022 · 5 revisions

Introduction

All assets are known engine types that allow instances to be created at runtime. The "ImageAsset", like all assets, is derived from a base type of "AssetBase". That means it includes all the fields from that type as well as adding its own fields specific to itself.

The ImageAsset provides a way to refer to a single image file and produce logical frames that represent areas within the image itself. It can represent a single frame or an unlimited number of frames. Each frame is referenced in one of two ways: either using a zero-based index (implicit mode) or by giving each frame a unique name (explicit mode).

As of version 4.0 ea3, the ImageAsset can also layer other images over the original image. Each additional layer can use a basic blending color and positioning. You can learn more below.

For an ImageAsset to be valid, it must contain at least a single frame i.e. frame #0.

TorqueScript Bindings

Exposed Fields

The ImageAsset type exposes the following fields in addition to those it inherits. Shown are the equivalent methods available that perform the same action in setting and getting the respective field:

  • ImageFile
    • setImageFile(fileName)
    • getImageFile()
  • CellCountX
    • setCellCountX(integer)
    • getCellCountX()
  • CellCountY
    • setCellCountY(integer)
    • getCellCountY()
  • CellWidth
    • setCellWidth(integer)
    • getCellWidth()
  • CellHeight
    • setCellHeight(integer)
    • getCellHeight()
  • CellStrideX
    • setCellStrideX(integer)
    • getCellStrideX()
  • CellStrideY
    • setCellStrideY(integer)
    • getCellStrideY()
  • CellOffsetX
    • setCellOffsetX(integer)
    • getCellOffsetX()
  • CellOffsetY
    • setCellOffsetY(integer)
    • getCellOffsetY()
  • CellRowOrder
    • setCellRowOrder(true/false)
    • getCellRowOrder()
  • FilterMode
    • setFilterMode(filterMode)
    • getFilterMode()
  • Force16bit
    • setForce16Bit(true/false)
    • getForce16Bit()
  • ExplicitMode
    • setExplicitMode(true/false)
    • getExplicitMode()
  • BlendColor - Note: only applies when using layers.
    • setBlendColor(red/green/blue/alpha)
    • getBlendColor()

In the list above, only "ImageFile" is mandatory, all others are completely optional.

Here's an example of the most basic form of an ImageAsset where only the mandatory "ImageFile" field is specified:

<ImageAsset
    AssetName="Gems"
    ImageFile="Gems.png"
/>

This would produce an asset known as "Gems" and produce a single texture from the image "Gems.png" which it expects to be located alongside where the XML ImageAsset file is. There is no need to make the asset name match the image name but it can make keeping the relevant files together on disk easier with an alphabetic file sort.

The following is a complete description of each of these fields.

ImageFile (string)

This is the only mandatory field for the type. It should refer to an image file that the engine can load. It is not essential to add the file extension here as the engine will attempt different extensions for you that it knows it can load (it'll try what you originally specified, then add a ".png" then a ".jpg").

If the image file could not be found or loaded then the asset is still generated however internally it is flagged as invalid. Additionally, you will see this as a warning in the console.

You can refer to sub-folders here but using a format like:

<ImageAsset
    AssetName="Gems"
    ImageFile="artwork/Gems.png"
/>

Do not use the "." to indicate the current folder as this is already implicitly assumed. You can however use ".." to indicate a parent folder but it is highly discouraged to store artwork in parent folders.

When a valid image file is found, this is used when the asset is eventually required. The image file is loaded and uploaded as a single texture. To do this, the image size must be supported by your graphics card as each graphics cards has a size limit for textures.

For the image to be uploaded as a texture however it must be a power-of-two in size but Torque 2D will internally re-size it if this is not the case so that it can be uploaded. Torque 2D will not modifie the image on the disk, it does this in-memory prior to uploading as a texture so the change is completely volatile. It also has to do this whenever the asset is loaded from disk and so whilst the process is very fast, it does waste a little time but more importantly is wasted memory on the graphics card. It also only adds extra space to the right and bottom of the image, it does not rescale or change the image in any way so this change is completely transparent but is done purely for the benefit of the graphics card.

If you turn-on the "metrics" on your SceneWindow i.e. something like 'SandboxScene.setDebugOn("metrics")' and look at the category "Textures", you'll see several entries that show you metrics like how many textures are resident on the graphics card, their total estimated size in bytes, how much main memory is used with images loaded into (ImageAsset releases its bitmaps so no need to worry about that) and finally how much waste there is on textures resident on the graphics card. Texture waste is the "space" that was created to resize the original image into a texture that was is a power-of-two in size. If your image was already a power-of-two in size then it will produce no waste so that's the optimal case.

Torque 2D does not perform any packing of textures like legacy Torque 2D. This was a major cause of performance and memory issues in that product, especially on lower-end devices. All those problems have been removed. Texture packing and ensuring that the images loaded are now in the hands of artists and other content pipeline products.

CellCountX & CellCountY (integer)

If you don't specify any "Cell" fields or don't use the "Explicit" mode described below then the ImageAsset will assume that the whole image is a single frame. This makes defining "full frame" images used for things like backgrounds extremely easy.

However, it's a very common practice to divide the image into a regular grid of "cells". All the "Cell" fields allow you to configure this grid of cells.

The "CellCountX" and "CellCountY" control how many of these cells there are in the horizontal and vertical axis respectively. These two fields multiplied together state how many frames are expected e.g a "CellCountX" of "5" and "CellCountY" of "10" would together produce fifty frames.

As with all the "Cell" fields described here, you must ensure that the frame areas existing within the dimensions of the image you specify. If any of the configuration results in cells positioned beyond the dimensions of the image you specify then the whole ImageAsset is created but flagged as invalid.

CellWidth & CellHeight (integer)

Controlling how many frames in the horizontal and vertical directions is the start however it's also vital that you configure the width and height of each cell. This is what "CellWidth" and "CellHeight" do.

So for example, if you have the following:

<ImageAsset
    AssetName="Gems"
    ImageFile="Gems.png"
    CellCountX="8"
    CellCountY="8"
    CellWidth="64"
    CellHeight="64" />

.. then it would be defining an asset named "Gems" with 8 x 8 frames (64) where each frame is a square 64x64 pixels.

You can see this asset in the codebase:

It's important to remember that whilst the overall image size needs to be a power-of-two for upload to your graphics card as a texture, cell width or cell height can be anything from a single pixel to as wide or tall as the image. Also, this example shows a 64x64 pixel cell size but the width and height don't have to be the same, you could have 1x50, 3x927, 321x33 etc.

The fields "CellCountX", "CellCountY", "CellWidth" and "CellHeight" are used in combination and none should be omitted if any of the others are used i.e. they must all be used together. If you only want a single frame then don't specify any of them and a single full-image frame is assumed in that case.

CellStrideX & CellStrideY (integer)

Typical cells are assumed to be next to each other i.e. the cell grid has no "spaces" in-between the cells. If this isn't the case then you can control how the ImageAsset steps or "strides" from cell to cell in both the horizontal and vertical.

By default, if you don't specify "CellStrideX" or "CellStrideY", the ImageAsset automatically sets them to be the "CellWidth" and "CellHeight" respectively.

This means that you can set them to be whatever you like. For instance, if each next cell in the horizontal has a single pixel space (gutter) in-between them then you want to set the "CellStrideX" to be the "CellWidth" plus one. The same would go for any configuration in the vertical using "CellStrideY".

Here's an example of that:

<ImageAsset
    AssetName="Gems"
    ImageFile="Gems.png"
    CellCountX="8"
    CellCountY="8"
    CellWidth="64"
    CellHeight="64"
    CellStrideX="70"
    CellStrideY="80"/>

In this example, each cell is 64x64 but the next cell in the horizontal as indicated by "CellStrideX" is "70" pixels along i.e. there's a 16 pixel gap in-between. In the vertical as indicated by the "CellStrideY", the next cell is "80" pixels down i.e. there's a "26" pixel gap in-between.

You are free to only specify the horizontal or vertical strides if you so choose.

CellOffsetX & CellOffsetY (integer)

As you have seen, when calculating the area for each cell, a cell width, height and horizontal/vertical strides are used. This however all assumes that the cells on the left of the image start at pixel 0 and likewise the cells on the top of the image start at pixel 0. These are denoted by "CellOffsetX" and "CellOffsetY" respectively.

If the cells start offset from the left of the image, you can specify this offset using "CellOffsetX". It must always be a positive integer and defaults to zero if you don't specify it. Likewise, if the cells start offset from the top of the image, you can specify this offset using "CellOffsetY".

CellRowOrder (bool)

This field controls how frame numbers are allocated to cells.

When using cells, the ImageAsset will generate frames from frame #0 onwards. The default for this generation is to look for cells horizontally i.e. cells order into rows. This means that the first frame will be at the top-left cell, the next frame will be the next cell horizontally (assuming there is a next cell horizontally) and will continue to find cells as specified using "CellCountX". When all the cells in that "row" have produced frames then the next row of cells will be processed according to "CellCountY".

In other words, frame numbers are allocated moving horizontally from left to right in "rows" moving down to the next "row" and continuing until all the cells have been processed.

This may not be how you wish to order your frames however. An alternate strategy is to not allocate frames in a "row" order but do so in a "column" order. By default "row order" is true but if you change this to "false" then "column order" is used where frame numbers are allocated vertically from top to bottom moving to the next "column" and continuing until all cells have been processed.

FilterMode (enum)

When the texture is generated on the graphics card, the texture is associated with a texture filtering mode. The two modes available are:

These are specified like this:

<ImageAsset
    AssetName="Logo"
    ImageFile="NiceLogo.png"
    FilterMode="Nearest"/>

... or like this ...

<ImageAsset
    AssetName="Logo"
    ImageFile="NiceLogo.png"
    FilterMode="Bilinear"/>

This allows you to control how the graphics card uses the texture on a per-ImageAsset basis. This is known as the "local filter" for that asset and you are free to configure each asset however you like.

You can however configure this using an engine-wide setting so you don't need to modify your assets. This may be useful if you are reusing assets and don't wish the "style" of filtering to be specified in the assets themselves.

You can specify the engine-wide filtering like this.

$pref::T2D::imageAssetGlobalFilterMode = "Nearest";
...
$pref::T2D::imageAssetGlobalFilterMode = "Bilinear";

To stop using an engine-wide filtering then set the above variable to be an empty string.

Force16bit (bool)

When the bitmap is uploaded to the graphics card, it is typically uploaded as a 24 or 32-bit image where each color channel (RGB) and an optional alpha channel is 8-bits wide however you can save graphics card memory by getting Torque 2D to upload the image as a 16-bit image. Depending on whether the bitmap contains an alpha channel or not, this typically means a red and blue with 5-bits and green with 6-bits or with an alpha channel, 4-bits for all color and alpha channels.

The obvious downside is greatly reduced texture quality.

Torque 2D needs to improve in this area and support more modern texture compression formats!

ExplicitMode (bool)

As you have seen, there are two ways to generate frames from an ImageAsset. The first is to not specify any frame dimensions, in which case the whole image is assumed to be a single frame. The second is to specify the layout and dimensions of "Cells" spanning the image.

There is however a third way known as "explicit" mode. It is explicit because it involves explicitly defining the positions of each and every cell within the image.

This is by far the most powerful and flexible method as it lends itself well to using images where frames have been packed tightly together into a complex arrangement. In this case, each frame needs to be specified.

By default, explicit mode is "false". Setting it to "true" however is only an indication of explicit mode, you still need to specify the frames themselves.

There are two ways to discuss how this is done and both are covered below.

BlendColor (red/green/blue/alpha)

Similar to sprites, the blend color is a string with four values between 0 and 1 separated by spaces. The blend color only applies to images that are using layers. If there's only one texture in the asset then you can simply set the blend color on the sprite that uses it. You can read more about layers in the appropriate section below.

Explicit Cell Mode

The following methods are available to configure the explicit cells:

  • clearExplicitCells()
  • addExplicitCell()
  • insertExplicitCell()
  • removeExplicitCell()
  • setExplicitCell()
  • getExplicitCellCount()
  • getExplicitCellWidth()
  • getExplicitCellHeight()

Without going into detail on each of these calls as the relevant information can be found in the script-binding referenced at the top of this document (also in the automatically produced reference documentation), an overview of this process can be shown.

Essentially, setting explicit mode on is the first thing to be done. When this is done, all the other "Cell" fields are ignored.

The ImageAsset has an internal list of frames. It's this internal list that you can modify. You can do this by simply adding, inserting, removing or clearing cell explicit cells like so:

// Create a bare asset.
%asset = new ImageAsset();

// Configure the basic asset details.
%asset.AssetName = "Stuff";
%asset.ImageFile = "things.png"
%asset.ExplicitMode = true;

// Configure the explicit cells i.e. frames.
%asset.addExplicitCell( 0, 0, 10, 20, "square" );
%asset.addExplicitCell( 10, 0, 30, 15, "circle" );
%asset.addExplicitCell( 40, 60, 23, 10, "triangle" );

// Save it as an asset definition to be used by the asset system.
TamlWrite( %asset, "stuff.asset.taml" );

The "addExplicitCell" call (as can be seen in the documentation) takes the following arguments:

  • cellOffsetX
  • cellOffsetY
  • cellWidth
  • cellHeight
  • regionName

These are the vital parameters of any cell. The "cellOffsetX" and "cellOffsetY" specify the position of the cell top-left relative to the image top-left i.e. the X/Y offset from the top-left of the image. The "cellWidth" and "cellHeight" obviously specify the width and height of that cell. The "regionName" defines the name of the frame, which can then be used when assigning an image frame to an object like a Sprite.

If no "regionName" is specified, it will automatically be assigned a frame number as its name. If all cells are unnamed, the order of these cells in the list is the order in which frame numbers will be allocated i.e the first unnamed cell will be frame#0, the second frame#1 etc. If you have a mix of named and unnamed cells, the unnamed cells are given a "frame number" name which corresponds to its position in the index list. It is highly recommended if you start naming cells, that all cells in the asset are given a name.

You can have as many cells as you wish, they can overlap, the same cell regions can be specified more than once without issue if it helps. In other words, you are free to do what you like.

You can use the "insertExplicitCell()" to insert a cell into an existing list and "removeExplicitCell()" to remove a cell from the list.

You must remember to set explicit mode to "true" prior to saving the asset otherwise the explicit cell details won't be saved.

TAML Format

As you saw in the previous section, you can set explicit cells using the API provided by the ImageAsset type itself. When you save the ImageAsset, it will save those explicit cells in its own custom format and that format is what is detailed here.

Obviously you are free to either programmatically or manually produce the appropriate TAML file to generate these explicit cells. Indeed, doing so programmatically would be a common way to produce an "export" of packed frames from other products.

This can be seen in practice using TexturePacker in the following video here.

You can also find the TexturePacker exported in the tool folder of the repository here.

Here's an example of the ImageAsset TAML XML format using the same example above generated using the ImageAsset API:

<ImageAsset
    AssetName="Stuff"
    ImageFile="things.png">
    <ImageAsset.Cells>
        <Cell Offset="0 0" Width="10" Height="20" RegionName="square"/>
        <Cell Offset="10 0" Width="30" Height="15" RegionName="circle"/>
        <Cell Offset="40 60" Width="23" Height="10" RegionName="triangle"/>
    </ImageAsset.Cells>
</ImageAsset>

As you can see, the ImageAsset uses a custom property of "Cells" (this is TAML custom formatting) which contains an unlimited number of "Cell" XML elements where each relates to an individual cell (frame).

The order that the "Cell" XML elements appear here are the order which the frames will be allocated i.e. the first one will be frame#0, the second frame#1 etc.

If these were generated via writing out an ImageAsset then the order here is the order of cells in the ImageAsset as discussed previously.

When reading in a TAML file, the "Offset" field can be specified as shown in the example above (a 2 value string). Alternatively, you can split the offset field into its X and Y component.

<Cell OffsetX="0" OffsetY="0" Width="10" Height="20" RegionName="square"/>

Image layers

Layers are a recently added feature that allow you to stack images into a single texture. This is similar in practice to creating a CompositeSprite with a custom layout, but there are a few key differences. First, the composite sprite will draw each image individually but the layered texture will draw all the images onto a single bitmap after which only a single draw command will be used to draw the texture. A composite sprite also can't be faded in and out but a sprite using a layered texture can. Composite sprites can also be made out of both still images and animation but a layered texture can only be a still image (which can then be used in an animation if you want).

So there are some key differences to be aware of, but the most important thing to know is that drawing the texture is expensive. Redrawing a layered texture is done in memory, by looping through all the bits and combining them, as opposed to doing it with the graphics card. This is fine for small images with few layers, but it can become time consuming for large images with lots of layers. So you should only be building textures when the game is loading - not during a level! Along those same lines, if you have several changes to make to the layers of a texture, you should postpone redrawing the image until you are done. The API is designed to allow you to do this.

Now let's look at a concrete example of a pirate flag taken from hit game Pirate Code (perhaps one of the greatest games of all time). The pirate flag is made of three layers: a base flag, a "pattern", and an "emblem". Each layer needs to have a blend color that can be applied to it to create different flags and each image has 16 frames that can be animated to make a flag that waves in the wind. Before layered textures, each layer would have to have its own animation and be placed over each other in a composite sprite. Then the animations would have to be carefully synced to give the illusion of a single flag. Here's what the three images would look like with a black background added.

Flag Layers

To configure our flag with a layered texture we'd start with a baseFlag texture created in the normal way from the first texture. We can then use any of the following calls to create and manage the layers.

  • addLayer(imageFile, position, blendColor, [doRedraw])
  • insertLayer(index, imageFile, position, blendColor, [doRedraw])
  • removeLayer(index, [doRedraw])
  • moveLayerForward(index, [doRedraw])
  • moveLayerBackward(index, [doRedraw])
  • moveLayerToFront(index, [doRedraw])
  • moveLayerToBack(index, [doRedraw])
  • setLayerImage(index, image, [doRedraw])
  • setLayerPosition(index, position, [doRedraw])
  • setLayerBlendColor(index, blendColor, [doRedraw])
  • getLayerImage(index)
  • getLayerPosition(index)
  • getLayerBlendColor(index)
  • forceRedraw()

The index starts with one, which is a little unusual in programming, but that's because the base image uses layer zero. Each call ends with an optional doRedraw value that defaults to true. If you pass in false, then the texture will not update. If you plan on to make multiple calls you should wait until the last call to redraw the texture. In the case of our example, we just need to call addLayer() twice to create the pattern and the emblem. Since all the images are white, we can use the blendColors to set the colors for our flag. Here's an example of what we end up with.

Pirate Flag Example

This image can now be used in animations or static sprites as if it were a single image on the disk. And should our pirates decide that blue is better than red, then we can change their flag with a single API call.

TAML Format

Similar to explicit cells, layers can be defined in TAML. Here's the TAML for our pirate flag.

<ImageAsset
    AssetName="pirateFlag"
    ImageFile="flagBase.png"
    BlendColor="0.2 0.2 0.2 1"
    CellWidth="200"
    CellCountX="4"
    CellStrideX="200"
    CellHeight="140"
    CellCountY="4"
    CellStrideY="140">
    <ImageAsset.ImageLayers>
        <ImageLayer
            ImageFile="flagJackPattern.png"
            Position="0 0"
            BlendColor="0.6 0 0 1" />
        <ImageLayer
            ImageFile="skullAndCrossbones.png"
            Position="0 0"
            BlendColor="0.95 0.95 0.95 1" />
    </ImageAsset.ImageLayers>
</ImageAsset>

The images are layered over the image from top to bottom. So the bottom most image will appear in the front, in this case, the skull and crossbones. The position is one part we haven't talked about much. It offsets the layer from the top left corner. Positive values for y move the layer down. Negative values for x and y are allowed.

Additional API

The ImageAsset exposes a few additional methods that can be useful under certain circumstances.

getFrameCount()

This method gets the count of frames that the ImageAsset defines. If the frame count is zero then the asset is invalid and something has gone wrong. There is no upper-limit to the number of frames allowed.

getFrameSize(frameIndex)

This method returns the dimensions of each frame specified by the frame number (index). Frame numbers are zero-based therefore they range from 0 to "getFrameCount()-1". You can use this range to iterate all the frames in an ImageAsset.

The frame size is in pixels and is returns as the frame width and height separated with a space like so:

for( %i = 0; %i < %asset.getFrameCount(); %i++ )
{
   echo( %asset.getFrameSize( %i ) );
}

Assuming the asset has only one frame with a width of 34 pixels and height of 78 pixels then it should output:

"34 78"

getImageWidth()

This method returns the width (in pixels) of the source bitmap. Note that this is the width of the image on disk and has nothing to do with any requirements for it to be a power-of-two in size.

getImageHeight()

This method returns the height (in pixels) of the source bitmap. Again, note that this is the height of the image on disk and has nothing to do with any requirements for it to be a power-of-two in size.

getImageSize()

This method returns both the width and height (in pixels) of the source bitmap. In other words, it's the source bitmap dimensions as returned individually by "getImageWidth()" & "getImageHeight()".

getIsImagePOT()

This method returns whether the original source bitmap was a valid power-of-two in size or not. You can calculate this yourself but this method performs it for you. Whilst this method has limited value, it would allow an editor to highlight to an end-user that the image being used in the ImageAsset is not optimal in size in relation to not being a power-of-two in dimension.

Clone this wiki locally