Rendering With An Atlas Texture
Go to the
guide project to see the source code for a fully working example of this guide.Another way to render textures is by using an atlas texture. This guide is a little more in-depth and longer than the previous guides. Stick with it though, it will be worth it!!
Want more detail about what an atlas texture is? Refer to the links below:
An atlas texture is a large image with multiple sub-images packed into a single image. With this single image, you use another file of a particular format such as JSON or XML, that describes the location, size, and other kinds of attributes for each image in the atlas texture.
If you are new to JSON, click
for a great 10-minute video.When rendering textures the standard way, you render the entire texture. Though there is nothing wrong with rendering whole textures one at a time, there are pros and cons. In the end, it all depends on what you are trying to accomplish.
With an atlas texture, you can pack all of the sequences of animation into the atlas. This is usually loaded by a library that loads the atlas texture and the associated metadata. At this point, it is very easy to automate the process of switching between textures to run an animation. Velaptor comes built-in with loading atlas textures and their associated metadata.
Benefits:
- Reduces draw calls: Reduces the number of times Velaptor needs to send data to the GPU and reduces the number of draw calls. Instead of sending data and making draw calls for each texture, you only have to for a single atlas texture.
- Improved performance: Reduces draw calls to improve the performance of your game.
- Easier management: Easier to manage one large texture than many small ones, especially when it comes to loading and unloading textures and coordinating the draw calls between textures. This result is less code.
- Smoother Animations: Makes animations smoother by reducing the amount of time your game spends loading and unloading textures.
- Reduced memory usage: Reduces the amount of memory your games use since you only need to load one large texture instead of many smaller ones.
It all depends on your goals. A lot of games use a combination of whole texture and atlas texture rendering.
Various tools out there give you the ability to easily create atlas textures and metadata.
A couple of examples are:
Velaptor does not currently have a tool to build these atlas texture images with the associated metadata. Plans are in the works to add Texture Packer integration. One option you have is to create your own tool to load the Texture Packer JSON data for use. This is not complicated and can get you by until the Texture Packer integration is complete.
Do not overstress about whole vs. atlas texture rendering. This comes with practice and depends on your needs. Keep your needs into account as you develop your game and do not worry about performance until it starts to become an issue.
Expand me for more info!!
Let's consider a 2D space shooter game with lots of bullets and enemies. In this type of game, there may be a large number of sprites for the player, the enemies, and the projectiles. Loading and rendering all of these individually can quickly lead to performance issues due to the large number of draw calls required.
By using atlas textures, you can pack all of these sprites into a single texture, which can be loaded and rendered more efficiently. For example, an atlas texture can contain all of the different enemy sprites, along with their animations and various attack patterns. Similarly, you can create another atlas texture that contains all of the different bullet sprites, including the various types of projectiles and their associated effects.
By doing this, the game engine can render multiple sprites in a single draw call, significantly improving performance. Additionally, since all of the sprites are contained within a single atlas texture, there's no need to switch textures between draw calls, which reduces overhead and leads to smoother gameplay.
Another benefit of using atlas textures is that it can reduce the overall size of the game's asset files. This is because all of the sprites are packed into a single texture, reducing the amount of duplicated metadata and overhead that would be required for individual files.
Overall, using atlas textures in a game can improve performance, reduce file size, and lead to smoother gameplay.
Enough explanation!! Let's get our hands dirty and build something!!
For this tutorial, we are going to build a simple game that will show a blue animating flame. This flame will be rendered in the center of the window and will animate in a loop indefinitely. We will also add a very small amount of randomness to it as well to enhance the animation.
Step 1: Create Project
Create a basic Velaptor application.
For more info, refer to the Project Setup guide.
Step 2: Get atlas content
Download the atlas texture files below:
Step 3: Add content to project
Add the downloaded atlas texture and metadata JSON file to the *Content/Atlas/**/ directory in the project.
For more info, refer to the Adding Content guide.
Both files should be in the same directory and besides the extension, should have the same name. The two files you downloaded should be named atlas.png and atlas.json.
You can name the files whatever you want, but the file names, excluding the extensions, must match. When you load an atlas texture, you will specify the name of the atlas image and metadata and both files will be loaded at the same time.
If you have an image file with no metadata file or vice versa, Velaptor will throw an exception
letting you know that one, the other, or both are missing.
Step 4: Create class fields
Create the following class fields which will be used for holding content data, loading content, manage batching, and rendering content.
public class Game : Window
{
private readonly ITextureRenderer textureRenderer; // Renders textures
private readonly IBatcher batcher; // Batches the rendering of textures
private ILoader<IAtlasData>? atlasLoader; // Loads the atlas data
private AtlasSubTextureData[]? subTextureData; // Holds all of the atlas data
private IAtlasdata? atlasData; // The atlas data
}
Step 5: Basic setup
In the Game class, add the following code to the constructor to set up the window and instantiate the objects mentioned before.
public class Game : Window
{
...
public Game()
{
Title = "Atlas Textures";
Width = 500;
Height = 500;
this.atlasLoader = ContentLoaderFactory.CreateAtlasLoader(this.batcher);
this.textureRenderer = RendererFactory.CreateTextureRenderer();
this.batcher = rendererFactory.CreateBatcher();
}
}
Step 6: Load and unload the content
6.1: Load content
Next, let's create the OnLoad() override method and add code to the method to load the atlas data.
protected override void OnLoad()
{
// Loads the atlas.png and atlas.json file
this.atlasData = this.atlasLoader.Load("atlas");
this.subTextureData = this.atlasData.GetFrames("flame");
base.OnLoad();
}
The this.atlasData class field variable will contain the loaded texture and the metadata for the texture.
We can use this data to make decisions on animation, positioning and more.
All you have to provide is the name of the atlas. Though you can use the file extension, it is not required and there is no need to load the .png and .json files separately
Expand me for more info!!
Let's go over the JSON data that is contained in the metadata file.
$type: This is the type of object that is used by the JSON serializer to know what type of object to create.
There is no need to worry about this key. Do not change it and you will be fine.
Bounds: This is the x, y, width, and height of the sub-texture in the atlas. This is used to determine where the sub-texture is located in the atlas. These are what you will use during the rendering process to render the frame of the animation.
FrameIndex: This is the frame number of an animation. If the frame number is -1, then this is not part of an animation. This is what you will use to keep track of which frame of animation you want to render.
Name: This is the name of the sub-texture and is what you will use in determining which group of frames animation
you want to load and when calling the GetFrames() method. If you wanted to load the metadata for the ship
sub-texture, you would call GetFrames("ship") and you would only get an array with a single item.
{
"$type": "Velaptor.Graphics.AtlasSubTextureData, Velaptor",
"Bounds": "1, 846, 403, 948", // The x, y, width, and height of the sub-texture in the atlas
"FrameIndex": 8, // The frame number of an animation
"Name": "flame" // The name of the sub-texture
},
{
"$type": "Velaptor.Graphics.AtlasSubTextureData, Velaptor",
"Bounds": "428, 850, 361, 948",
"FrameIndex": 9,
"Name": "flame"
},
{
"$type": "Velaptor.Graphics.AtlasSubTextureData, Velaptor",
"Bounds": "0, 76, 75, 75",
"FrameIndex": -1, // -1 means that this is not part of an animation
"Name": "ship"
}
6.2: Unload content
Now we can unload the content when the game shuts down. This is to clean up resources that are no longer needed.
To do this, we can override the OnUnload() method. We can then call the Unload() method on the atlas loader
and send in the IAtlasData class field we created earlier. To do this, we need to add the Velaptor.ExtensionMethods namespace
to the top of the file.
Add the following using statement.
...
using Velaptor.ExtensionMethods; // Add this line here
...
public class Game : Window
{
...
}
Now we can override the OnUnload() method and unload the font.
protected override void OnUnload()
{
this.atlasLoader.Unload(this.atlasTexture);
base.OnUnload();
}
Step 7: Let's animate
Now that we have loaded the atlas data, let's add some code to the OnUpdate() method to animate the sub-textures.
7.1: Tracking time
For us to run an animation, we need to keep track of how much time has passed.
Add the following fields to the Game class.
public class Game : Window
{
...
private float elapsedMs; // The total milliseconds that have passed
private int currentFrame; // The current frame of the animation
private bool isFullSize; // Whether or not the flame has grown to full size
...
}
Expand me for more info!!
Think of a
. If you were to flip through the pages of a flip book, you would see the animation. The faster you flip through the pages, the faster the animation will appear. The slower you flip through the pages, the slower the animation will appear. The same concept applies to animations in games.The problem with software is that it runs each frame very fast. If we changed the frames of the animation as fast as the game would run, the animation would move too fast. We need to slow down the animation to the desired speed so that it runs smoothly.
How fast you want to run your animation is up to you. You can make it run as fast or as slow as you want.
7.2: Has enough time passed?
Add some code to the OnUpdate() method to check if enough time has passed to
change the frame of the animation.
protected override void OnUpdate(FrameTime frameTime)
{
this.elapsedMs += (float)frameTime.ElapsedTime.TotalMilliseconds;
// Move to the next frame every 124ms
if (this.elapsedMs >= 124)
{
// If the current frame is one of the frames after
// the flame has grown to full size.
if (this.currentFrame >= 8)
{
this.isFullSize = true;
}
// Get the starting frame index based on if the flame has
// grown to full size or not.
var startFrame = this.isFullSize ? 8 : 0;
// If the last frame has been reached, reset to the starting frame
this.currentFrame = this.currentFrame >= this.subTextureData.Length - 1
? startFrame
: this.currentFrame + 1;
// Reset the elapsed time so we can wait for another
// 124ms before moving to the next frame
this.elapsedMs = 0;
}
base.OnUpdate(frameTime);
}
This code will check if 124ms has passed. If it has, then it will move to the next frame of the animation. If the last frame has been reached, then it will reset to the first frame. Now we have our flip book!!
The startFrame is the frame where the animation starts, which will be either 0 or 8. If the flame has not grown to
full size, frames 0-7 will be used. If the flame has grown to full size, frames 8-15 will be used.
If you want to calculate how many milliseconds it takes for a particular frame rate, just take the value 1000 and divide it by the desired frame rate.
Formula: ms/sec ÷ fps = ms/frame.
For example, 1000 ÷ 60 = 16.66ms. This means if you want to run something at 60fps, you would need to to make something happen every 16ms to obtain that frame rate.
Expand me for more info!!
FPS stands for frames per second, which, as the name implies, is the number of frames that are rendered every second.
The game loop of a game consists of the two most important methods: OnUpdate() and OnRender().
The OnUpdate() and OnRender() methods are called once per frame. The speed (or frequency) at
which these methods are called is what determines the FPS of the game. The faster the methods run,
the higher the FPS.
To calculate the FPS, divide the number of frames rendered by the number of seconds that a game loop iteration has run. For example, if a game loop iteration runs for 1 second and 60 frames, then the FPS would be 60. If a game loop iteration runs for 1 second and 30 frames, then the FPS would be 30.
Since we can keep track of how much time has passed, we can calculate the FPS of the game. We can
take advantage of this time metric coming into the OnUpdate() and OnDraw() methods to have things
happen at a certain frequency. In this case, the speed at which we want our animation to run.
Step 8: Render animation
Now we can finally render the animation to the screen. Add the following code to the OnDraw() method.
protected override void OnDraw(FrameTime frameTime)
{
// Start the batch
this.batcher.Begin();
var pos = new Vector2(Width / 2f, Height / 2f);
// Render only the sub-texture in the atlas at the center of the window
this.textureRenderer.Render(this.atlasData,
"flame",
pos,
0f,
0.25f,
Color.White,
this.horizontalLayout,
this.currentFrame);
// End the batch to render the entire batch
this.batcher.End();
base.OnDraw(frameTime);
}
Here we are using the location data from the frame metadata to know where in the atlas the current frame or sub-texture is located. We then render only that sub-texture to the screen.
The srcRect and destRect variables are what tell Velaptor the section of the atlas to render.
Step 9: Run it
Run the game and see the results! You should see a small window with a blue flame animating in the center of the window as shown below.
The animation will be much smoother than what is shown here.

Step 10: Bonus!!
You do not have to do this step of course, but what is game development without a little bit of extra fun? Let's improve the randomness of the flame animation.
10.1: Add more fields
Add two more class fields to the Game class. The random field will be used to randomly choose between horizontal
or non-horizontal orientation. The horizontalLayout field will be the layout setting at the time of rendering.
public class Game: Window
{
...
private readonly Random random = new (); // Chooses random numbers
private RenderEffects horizontalLayout; // The horizontal orientation to render flame
...
}
10.2: Add flip behavior
Add the following code to randomly choose whether or not the flame should be flipped horizontally to the
bottom of the if block. This will choose a number between the values 0 and 1. If 0 is chosen,
the flame will be flipped horizontally. If 1 is chosen, the flame will not be flipped horizontally.
This is synonymous with flipping a coin.
protected override void OnUpdate(FrameTime frameTime)
{
this.elapsedMs += (float)frameTime.ElapsedTime.TotalMilliseconds;
// Move to the next frame every 124ms
if (this.elapsedMs >= 124)
{
...
// Randomly choose to have the flame flipped horizontally or not flipped at all
this.horizontalLayout = this.random.Next(0, 2) == 0
? RenderEffects.FlipHorizontally
: RenderEffects.None;
}
base.OnUpdate(frameTime);
}
10.3: Run it
Now run your game again and the flame will be a little bit more realistic to the randomness of the orientation of the flame!!
