Showing posts with label optimisation. Show all posts
Showing posts with label optimisation. Show all posts

Wednesday, 10 March 2021

Finding a suitable toolchain for animating Blightbound’s 2D characters

For Blightbound our technical ambitions for the 2D animation system were quite lofty: we wanted high quality animation, screen-filling bosses, crisp character art, high framerate and swappable gear. This required a complete rework of our animation pipeline, since the sprite sheets we used in Awesomenauts and Swords & Soldiers 2 allow for none of those requirements, except for high quality animations. In today’s blogpost I’d like to explain the problems we faced, and what combination of tools we chose to solve them. This is the first half of a two-parter: next week I’ll discuss our skin editor and its implications on animation.


Blightbound features huge screen-filling bosses that need to animate smoothly without taking up insane amounts of memory.

Traditionally 2D animation in games is done using sprite sheets. A sprite sheet is simply a big texture with all the frames for all a character’s animations in it. Sprite sheets aren’t limited to just character animation: they can also store other things, like special effects or plants. The frames might be in a grid, or spaced more efficiently like we did for Awesomenauts. In any case, it’s just a series of tightly packed images.

Sprite sheets are great because they allow for complete freedom. The animator can draw anything they like and it will just work. Extreme squash-and-stretch? Perspective changes? Morphs? Do as you please, to the game it’s all just images!

However, sprite sheets come with a few huge downsides. First is size. If you have a big character and want it to be crisp and high-resolution, then each frame is going to take up a lot of space. Having lots of large images is going to cost too much memory. In Awesomenauts this was indeed a problem for some of the characters. For Clunk to fit in one 4096*4096 sprite sheet (the maximum we chose for memory and compatibility reasons), we had to either lower the resolution a bit, or reduce the framerate. And that’s not even that big of a character! The high memory usage of sprite sheets means that screen-filling bosses either can’t be done, or need to have very few frames, or can’t be crisp.


In Awesomenauts, red Clunk occupies an entire 4k sprite sheet texture for all his animation frames. Clunk fit only after slightly downsizing him.

The second major downside of sprite sheets is that since it’s just images, it’s very hard to do anything with them except simply showing them. For Awesomenauts we’ve often talked about letting players customise characters by changing for example their hat or other parts, but simple sprite sheets make that incredibly hard. The engine would need to know where the hat is exactly, but the sprite sheet doesn’t contain that information. Nor does it tell us whether there are different perspectives of the hat in different frames, or whether anything is ever in front of or behind the hat.

I can think of some workarounds for this problem, but it’s all so cumbersome that’s it’s not practical to actually do. This is the main reason why Awesomenauts only contains skins that swap the entire look of a character, and no swappable hats or clothing: those would be completely new sprite sheets for each combination.


Since every skin change in Awesomenauts requires an entire new sprite sheet we decided to make full reskins instead of allowing the user to customise individual parts (like hats or weapons). These are the four looks for Ted McPain.

So, sprite sheets can’t give us four of our requirements (screen-filling bosses, crisp character art, high animation framerate and swappable items). What can we do instead? The obvious direction for a solution is to split a character in parts, and let those parts move relative to each other. Or even to go for full skeletal animation.

If you have a background in 3D animation, you might think “well skeletal animation of course, it’s awesome!” And in 3D it is indeed, but in 2D it’s a lot more limited. Because we’re restricted to the 2D plane, there’s a lot of animation that can’t be done with a standard skeleton in 2D. Rotating the head of the character away from the camera is super easy in 3D, but impossible with 2D skeletal animation. Swinging a sword vertically is easy, but horizontally is very hard because that requires 3D perspective. Wanting to circumvent these limitations, we opted for a combination of 2D skeletal animation and traditional frame-to-frame animation on parts where needed.

A common tool for doing part-based 2D animation is Spine. Spine is used by a lot of games and we expected this to be our best option. We tried Spine and it was indeed quick and easy to use, as well as easy to integrate into our own engine. However, it turned out to be a bit too simple: Spine doesn’t allow swapping a part in the middle of an animation. This is needed if you want to mix skeletal animation with frame-to-frame animation, for example by switching to a different drawing of a head or hand. The workaround was to layer several skeletons on top of each other, but that was too clunky to work with. We reached out to Spine at the time, explaining this issue, and they confirmed this was the only way. Our evaluation was three years ago and we haven't tried Spine since, but according to this post by them their options for combining frame-to-frame with skeletal animation seem to have improved recently.


Spine by Esoteric Software is a 2D character animation tool for games for creating animations using parts, skeletons and deformations (screenshot taken from one of Spine's tutorial videos).

We also tried some other tools, including Blender and 3D Studio MAX, but those turned out to be too focussed on 3D to create a good workflow for 2D animation for us. Especially the nice skeletal systems they have became quite impractical when used for 2D animation.

So instead, we wanted to stick with After Effects. After Effects is mostly known as a tool for editing video and doing special effects and such, but it’s actually also just a really good general animation tool. Our artists happily used it for character animation in our previous games Awesomenauts and Swords & Soldiers 2. The basic idea is this: each part in an animation is a layer, and can be animated, linked and deformed freely. By turning layers on and off, you can do frame-to-frame animation on parts (like a hand opening and closing) or on the character as a whole.

I imagine After Effects would be a bad fit for full hand-drawn frame-to-frame animation, like in Cuphead, since After Effects doesn’t even allow drawing on layers directly (we draw all our parts in Photoshop). However, for our games it’s an excellent tool because we don’t want to redraw the entire character every frame anyway.

There was something missing though: After Effects is a great animation tool, but it’s not really tailored to 2D character animation. Good thing we already knew about the Duik plug-in. Duik adds the rigging features that are common in 3D animation to After Effects. Combined with the strong tools After Effects already has, it’s a really effective 2D animation tool for games.


Duik is an After Effects plugin that adds tons of 2D animation features (screenshot from this tutorial video by Motion Tutorials).

I previously mentioned we wanted to mix skeletal animation with frame-to-frame animation. That part is actually really simple with Duik. The artist just links several drawings of a part to a bone and switches which is visible. This way we can change facial expressions during animations, as well as do things like open and close hands, pivot or distort a torso and switch facial expressions.


Thumbnails of most animation parts needed for one character in Blightbound. The various hands, faces and chests are for frame-to-frame animation on parts of the body.

Happy with After Effects+Duik as our animation tool, another important thing was missing: After Effects files can’t be played back in real-time engines. So I set about the task of writing an exporter from After Effects that exports all the hierarchy and animation data. I also implemented the in-game side of playing that back in real-time.

At it’s core, this is a surprisingly simple task. Each part in an After Effects animation is a layer and we can just export all the layers with their animation frames and play them back. Where I went wrong though, is that I wanted to emulate Duik’s systems in-game. I thought Duik only did basic two-bone inverse kinematics and implemented that in the engine. This worked with simple animations, but for complete characters the animations were completely broken in-game. When I looked deeper I learned that Duik actually has a ton of different animation systems and approaches. Mimicking all of those in-game was totally undoable.


Duik has so many features that only supporting parenting and two-bone IK produced completely wrong results in-game, as can be seen in this 'beautiful' monstrosity. This was intended to be a normal walk animation, early during development of Blightbound.

Once I realised this, I went for an easy alternative: animation baking. I export the orientation, scale and position of each part at 30 frames per second, simply storing the values for each frame (unless they’re not changing). This is a lot more data, but compared to the size of textures it’s really negligible. I did keep the hierarchy of the bones, so parts don’t start to subtly 'float' compared to each other.


Our custom file format is simply a text file with a long list of all parts and the information needed to position and animate them correctly, including parenting and keyframes.

I previously said we wanted high framerate. The game actually interpolates between these frames, so while they’re exported at 30fps, they’ll also run smoothly at 60fps, 144fps, or any other framerate. That’s also great for when slowing an enemy: in Awesomenauts, if you slow an enemy, you can see the low framerate of their slowed down walk animation. In Blightbound, movement remains perfectly smooth.

A note there is that whenever something needs to jump from one position to another instead of smoothly interpolating there, the artist needs to use a hold key. Our tool exports those and applies them correctly during playback. Overlooking this caused some bugs here and there, because artists aren’t used to needing to use hold keys when two frames are directly next to each other. In-game however there can be frames in between, so that case still needs a hold key.


A few animations that Ronimo animator Tim Scheel made for the Blightbound character Karrogh.

After Effects is a huge tool with tons of features, so it’s impossible to support them all in-game. Our exporter only supports what we actually need, and nothing else. This means our artists need to avoid using some After Effects features that they might want to use. In most cases this is fine, but one particular feature that we ended up not supporting due to a lack of time, is mesh deformations (also known as puppet pins). Being able to deform a part would have helped a lot, but we didn’t find the time to implement a good emulation of After Effects’ deformation features in-game. Quite a pity, and I wonder whether our artists have ever wished they had chosen Spine after all, since Spine supports deformations in-game out-of-the-box.

Our artists did apply a simple workaround for the lack of deformations. By doing them in Photoshop and saving them as different frames for a part, they work fine. Technically the engine then thinks it’s frame-to-frame animation, but it’s actually deformed instead of redrawn in Photoshop. This approach wasn’t used much because it requires a lot of handwork, especially when reusing animations on several characters, but it does get the job done when needed.



The chest deformation in this animation from Blightbound was made by deforming the chest part in Photoshop and then exporting it as separate parts.

From a memory perspective the result of our toolchain is pretty impressive. Whereas a single Awesomenauts character can take up 16mb of texture space (excluding skins and the blue team), all Blightbound characters and enemies together use only 65mb of texture space, spread out over 4,600 character parts. On top of that, Blightbound animations are much higher resolution and higher framerate and we can swap parts and weapons dynamically.

What we have so far is strong animation tools (After Effects + Duik) and a way to play animations back in-game. This is enough to make a playable game, but we wanted considerably more: easy reuse of animations between characters, equippable gear, attaching special effects to bodyparts and efficiently handling thousands of character parts. In next week’s blogpost I'll dive into how we achieved those goals using our own skin editor.

Special thanks to Ronimo artists Koen Gabriels and Gijs Hermans for providing feedback on this article.

Sunday, 1 March 2020

How we made particles twice as fast through cache optimisation

Cache optimisation is one of the most hardcore, low-level topics in game programming. It's also a topic that I haven't been very successful with myself: I tried optimising for cache efficiency a few times but never managed to achieve a measurable improvement in those specific cases. That's why I was quite intrigued when our colleague Jeroen D. Stout (famous as the developer of the experimental indie game Dinner Date) managed to make our particles twice as fast. What's the trick? Jeroen was kind enough to tell me and today I'll share his approach. Since it's such a hardcore topic, I'll start by giving a general overview of what cache optimisation is in the first place.



Let's have a look at computer memory, or RAM. Computers these days have gigabytes of very fast memory that's used to store whatever the game or program needs to use on the fly. But is that memory truly so fast? Physically, it's a separate part of the computer. Whenever the processor needs to get something from memory, it needs to be transported to the CPU. That only takes a tiny bit of time, but a modern processor can perform billions of operations per second. At those kinds of speeds, a tiny bit of time is actually a lot of operations that the processor could have done during that time. Instead, it's waiting for something to come out of memory.

This is where cache comes in. Cache is a small amount of even faster memory that's directly on the processor and is thus a lot faster to access than normal memory. So whenever something is already in cache, the processor doesn't have to wait for it (or at least not as long) and can keep working. According to this article cache can be up to 27 times faster than main memory.



As programmers we generally don't have direct control over what's in cache: the processor tries to utilise the cache as efficiently as possible by it's own volition. However, depending on how we access memory, we can make it a lot easier for the processor to use cache efficiently. This is where cache optimisation comes in.

The goal of cache optimisation is to structure our code in such a way that we use cache as much as possible and need to wait for memory as little as possible. Whenever the processor needs to get something from memory and it's not already available in cache, that's called a cache miss. Our goal is to avoid cache misses as much as possible.

To be able to avoid cache misses, we need to know a little bit about how cache works. I'm no specialist in this field, but we only need a small bit of knowledge to already understand how a lot of cache optimisations work. These are the basic ideas:
  • Data is transferred from memory to cache in blocks. So whenever you're using something, the memory directly around that is likely also already available in cache.
  • Cache is often not cleared immediately. Things you've just used have a good chance of still being available in cache.
  • The processor tries to predict what you'll use next and get that into cache ahead of time. The more predictable your memory access, the better cache will work.

These three concepts lead to a general rule for writing code that's efficient for cache: memory coherence. If you write your code in such a way that it doesn't bounce all over memory all the time, then it will likely have better performance.

Now that the basics are clear, let's have a look at the specific optimisation that Jeroen did that halved the CPU usage of our particles.



To avoid dumping huge amounts of code here, I'm going to work with a highly simplified particle system. Please ignore any other inefficiencies: I'm focusing purely on the cache misses here. Let's have a look at a very naive approach to implementing a particle system:

struct Particle
{
   Vector3D position;
};

class ParticleSystem
{
   std::vector<Particle*> particles;

   void update()
   {
      for (int i = 0; i < particles.size(); ++i)
      {
         particles[i]->position += speed;
         if (shouldDestroy(particles[i]))
         {
            delete particles[i];
            particles.erase(particles.begin() + i);
            --i;
         }
      }
      while (shouldAddParticle())
      {
         particles.push_back(new Particle{});
      }
   }
};

This code is simple enough, but if we look at it from a cache perspective, it's highly inefficient. The problem is that every time we create a new particle with new, it's placed in a random free spot somewhere in memory. That means that all the particles will be spread out over memory. The line particles[i]->position += speed; is now very slow, because it will result in a cache miss most of the time. The time the processor spends waiting for the particle's position to be read from memory is much greater than the time that simple addition takes.



I knew that this would give performance problems, so I immediately built the particles in a more efficient way. If we know the maximum number of particles a specific particle system can contain, then we can reserve a block of memory for that on startup and use that instead of calling new all the time.

This results in a bit more complex code, since we now need to manage that block of memory and create objects inside it using placement new. In C++, placement new allows us to call a constructor the normal way, but use a block of memory that we already have. This is what the resulting code can look like:

struct Particle
{
   Vector3D position;
};

class ParticleSystem
{
   unsigned char* particleMemory;
   std::vector<Particle*> particles;
   int numUsedParticles;

   ParticleSystem():
      numUsedParticles(0)
   {
      particleMemory = new unsigned char[maxCount * sizeof(Particle)];
      for (unsigned int i = 0; i < maxCount; ++i)
      {
         particles.push_back(reinterpret_cast<Particle*>(
            particleMemory + i * sizeof(Particle)));
      }
   }

   ~ParticleSystem()
   {
      delete [] particleMemory;
   }

   void update()
   {
      for (int i = 0; i < numUsedParticles; ++i)
      {
         particles[i]->position += speed;
         if (shouldDestroy(particles[i]))
         {
            //Call the destructor without releasing memory
            particles[i]->~Particle();

            // Swap to place the dead particle in the last position
            if (i < numUsedParticles - 1)
               swap(particles[i], particles[numUsedParticles - 1]);

            --numUsedParticles;
            --i;
         }
      }
      while (shouldAddParticle() && bufferNotFull())
      {
         //Placement new: constructor without requesting memory
         new(particles[numUsedParticles]) Particle{};
         ++numUsedParticles;
      }
   }
};

Now all the particles are close to each other in memory and there should be much fewer cache misses when iterating over them. Since I built it this way right away, I'm not sure how much performance I actually won, but I assumed it would be pretty efficient this way.

Nope.

Still lots of cache misses.

In comes Jeroen D. Stout, cache optimiser extraordinaire. He wasn't impressed by my attempts at cache optimisation, since he saw in the profiler that the line particles[i]->position += speed; was still taking a disproportionate amount of time, indicating cache misses there.

It took me a while to realise why this is, but the problem is in the swap-line. Whenever a particle is removed, it's swapped with the last one to avoid moving all particles after it one forward. The result however is that as particles are removed, the order in which we go through the particles becomes very random very quickly.



The particles in our example here are really small: just a single Vector3D, so 12 bytes per particle. This means that even if we go through the particles in random order, we might occasionally still be staying in the same cache line. But a real particle in the Secret New Game (tm) that we're developing at Ronimo has much more data, like speed, scale, orientation, rotation speed, vibration, colour, and more. A real particle in our engine is around 130 bytes. Now imagine a particle system with 100 particles in it. That's a block of memory of 13,000 bytes. Hopping through that in random order is much more problematic for cache!

Thus, Jeroen set out to make it so that updating particles goes through memory linearly, not jumping around in memory at all anymore.

Jeroen's idea was to not have any pointers to the particles anymore. Instead we have that big block of memory with all the particles in it, and we store the indices of the first and last currently active particles in it. If a particle dies, we update the range of living particles and mark the particle as dead. If the particle happens to be somewhere in the middle of the list of living particles then we simply leave it there and skip it while updating.

Whenever we create a new particle, we just use the next bit of memory and increment the index of the last living particle. A tricky part here is what to do if we've reached the end of the buffer. For this we consider the memory to be a ring buffer: once we reach the end, we continue at the start, where there's room since those particles will have died by now. (Or, if there's no room there, then we don't create a particle since we don't have room for it.)

The code for this isn't that complicated, but it's a bit too much to post here. Instead, I'm just going to post this scheme:



Compared to my previous version, Jeroen managed to make our particles twice as fast with this approach. That's a pretty spectacular improvement and shows just how important cache optimisation can be to performance!

Interestingly, my first hunch here is that this optimisation is a bad idea since we need to touch all the dead particles in between the living ones to know that they're dead. Worst case, that's a lot of dead particles. In fact, when looking at computational complexity, the worst case has gone from being linear in the number of currently existing particles, to being linear in the number of particles that has ever existed. When I studied computer science at university there was a lot of emphasis on Big O Notation and complexity analysis, so to me this is a big deal.

However, as was mentioned above, accessing cache can be as much as 27 times faster than accessing main memory. Depending on how many dead particles are in the middle, touching all those dead particles can take a lot less time than getting all those cache misses.

In practice, the lifetimes of particles don't vary all that much. For example, realistic particles might randomly live between 1.2 seconds and 1.6 seconds. That's a relatively small variation. In other words: if there are dead particles in between, then the ones before them will also die soon. Thus the number of dead particles that we need to process doesn't become all that big.

The big lesson this has taught me personally, is that cache optimisation can be more important for performance than Big O Notation, despite that my teachers at university talked about Big O all the time and rarely about cache optimisation.

EDIT: Several commenters on Twitter and Reddit suggested an even simpler and faster solution: we can swap the dead particles towards the end of the list. This removes the overhead of needing to skip the dead particles and also removes the memory that's temporarily lost because dead particles are in the middle. It also simplifies the code as we don't need to handle that ring buffer anymore.

For further reading, I would like to again recommend this article, as it explains the principles behind cache lines very well and shows some clever tricks for optimising to reduce cache misses.

As a final note, I would like to also stress the unimportance of cache optimisations. Computers these days are super fast, so for the vast majority of code, it's fine if it has very poor cache behaviour. In fact, if you're making a small indie game, then it's probably fine to ignore cache misses altogether: the computer is likely fast enough to run your game anyway, or with only simpler optimisations. As your game becomes bigger and more complex, optimisations become more important. Cache optimisations generally make code more complex and less readable, which is something we should avoid. So, use cache optimisations only where they're actually needed.

Have you ever successfully pulled off any cache optimisations? Are there any clever tricks that you used? Please share in the comments!

Saturday, 5 December 2015

Texture formats for 2D games, part 4: Overview

In my last three blogposts I've discussed the details of various texture formats. Now that we've seen them, it's time for an overview. Below you'll find a big table that lists the properties of a lot of different formats and shows their results. I'll also discuss some final considerations on performance.

The important thing here is to realize that there is no perfect solution: every choice has its own drawbacks, so even with the same art style you might end up with different texture formats depending on the platform. These are the main arguments to consider:
  • Quality
  • Size in video memory
  • Size on disk
  • Download size
  • Loading time
  • Framerate
  • Development time

Most of these are pretty obvious and I've already discussed quality and size in video memory extensively in my previous posts. The reason I'm listing them here is that it's important to keep in mind that you can mix and match formats for different goals.

There are also more complex things that might matter to specific games, but if things like memory bandwidth are an issue then you are probably making an extremely complex game and hopefully know more about it than I do anyway.

Let me give an example of mixing formats to achieve a specific combination of desired properties. For the original version of Swords & Soldiers 1 on the Nintendo Wii we wanted to minimize size on disk and size in video memory. In video memory we used palette textures, but on disk we also ZIPped those. ZIP cannot be used by the videocard but it makes textures a lot smaller. The downside of this is that unZIPping costs additional loading time when starting the game. However, I didn't care much about loading times because the Wii was quite fast compared to the amount of disk space available for downloadable games, so loading times were going to be fine anyway.

Here's the big overview this blogpost is all about. It compares DXT, ETC, PVRTC, palettes and several more basic formats. Creating these was a lot of work so you'd better click them for a good look! ;)

Click for full resolution

Click for full resolution

If there are any mistakes in the descriptions please let me know in the comments so that I may correct them!

An interesting note here is that Android devices differ so much that texture format support varies from phone to phone. Apparently the solution is to either only use ETC1 and uncompressed formats (since those are supported on all Android devices), or to create different versions of the game using different texture formats per type of phone.

It's important to realise that not everything is always what it seems. The benefit of saving your exact texture format on disk (through DDS or TGA instead of something like JPEG) is that you can load the texture right into video memory. You might think this means fast loading times, but this is not always the case: if reading from disk is slow (for example when reading from a DVD) then this might turn out to be a bottleneck. If your processor is fast and your disk is slow then using ZIP or JPEG might actually give you faster loading times, despite the additional processing needed. Note that in the case of Swords & Soldiers 1 this was not the case: the Wii had a small but very fast hard disk.

One final consideration I'd like to mention is framerate. In most cases texture formats don't really influence framerate: the performance bottlenecks are usually fillrate, polygon count, post processing or something on the CPU. As long as all the textures being used all fit into video memory simultaneously they won't influence performance much. As soon as you grow out of video memory however, you're in trouble. On console your game might crash or show broken graphics due to this. On PC you're less likely to see anything so obvious: PC drivers can start swapping textures between video memory and normal RAM. This is devastating to performance but you might not notice this if you're working on a fast development PC. Be sure to keep a close watch on your texture budgets, even if everything seems to still work fine on your own PC!

Texture formats can also influence fillrate. When reading the texture it might get cache misses, slowing down the GPU. I won't go into too much detail here, but the thing to keep in mind is that making your textures much higher resolution than how they're shown in-game might reduce the framerate. If you zoom in and out a lot then mipmapping is a good solution to solve this. It also helps a lot to use a texture format with few bits per pixel, like DXT1.

Some hardware (including some of the current gen consoles) also support tiled textures. Normally a “tiling texture” refers to a texture that can be repeated, but that shouldn't be confused with tiled texture formats: tiled textures in this case refer to a certain memory layout of the texture that reduces cache misses. I won't go into detail on that here either, but if you're running into performance problems due to slow texture reads it might be worthwhile to check whether your specific platform supports tiled texture formats.

Each game and platform has its own requirements and I hope this series of posts has helped you understand the various options for handling textures. Finally, for those who don't have time to seriously look into this and just want some quick and dirty advice: if you're on PC or console DXT1 is usually the best option, or DXT5 if you need alpha.

See also:
-Texture formats for 2D games, part 1
-Texture formats for 2D games, part 2: Palettes
-Texture formats for 2D games, part 3: DXT and PVRTC


PS. We're looking for interns for the coming period! We have openings in the fields of C++ coding and 2D art! Check here for more info: internships at Ronimo. Note that a basic understanding of the Dutch language is a requirement.

Saturday, 21 November 2015

Texture formats for 2D games, part 3: DXT and PVRTC

DXT is the most used texture format in games and it is what we use most in Swords & Soldiers II and Awesomenauts. DXT is easy to use but provides some weird compression artefacts, so a good understanding of how it works is very useful for both artists and graphics programmers. Since DXT is not supported on iOS I will also shortly discuss the quite similar PVRTC format. This is part 3 in my short series on texture formats for 2D games. Here are part 1 and part 2.

Note that DXT is often also referred to as DDS because DXT textures are usually saved as .DDS files. DXT is also known as S3 Texture Compression.

There are several versions of DXT. The most versatile type is DXT5, which stores an alpha channel that can be used for transparency. DXT5 uses only 8 bits per pixel, making it 75% smaller than full 32bit RGBA. If no alpha is needed or only on/off alpha, DXT1 can be used, which is even smaller: 4 bits per pixel, making it 87.5% smaller than RGBA. That's quite a spectacular win, so where's the catch?

The downside is that DXT can bring serious quality loss due to how it compresses textures. The quality impact of this compression heavily depends on the type of art: some textures will look really bad but in most cases it will look fine as long as you don't zoom in on the compression. Zooming in that far is not common in 2D games so DXT is very usable despite the quality loss.



The most basic type of DXT is DXT1. Here's how it works:



This means that if a single 4x4 block contains few different colours, there won't be any significant compression artefacts. Having more colours inside the block is also fine if those colours are near the others. For example, if the block stores red and yellow then it can also store orange well since that is in between. Heavy artefacts occur if the block contains more really different colours. For example, if a 4x4 block contains red, green and black, then one of those will be lost and replaced by one of the others (whichever is nearest to reduce the artefacts).



This sounds absolutely horrible, but the fun part is that we are only talking about tiny 4x4 blocks here. In practice few blocks contain many different colours, and even if they do the next block will be able to choose different colours, so artefacts usually remain small. In practice in 2D games DXT artefacts will mostly appear in spots where three coloured areas touch each other, and around thin outlines. Have a good look at the image below to get an idea of which situations result in serious artefacts and which are fine.



As you can see above, if your art contains lots of thin outlines it will be damaged severely by DXT1. However, fixing this by going from DXT1 to 24bit RGB makes your textures 6 times bigger. Auch! Luckily there is an awesome compromise: if you store your DXT texture at a slightly higher resolution it will look a lot better and still be smaller than RGB. Upscaling makes the outlines (or other small details) thicker, greatly reducing the artefacts. If you make your texture 41% wider and higher it becomes twice as big, which is still 3 times smaller than RGB. As a huge bonus you also get sharper textures, so this probably looks way better than the lower resolution RGB texture would have. We've used this trick in Awesomenauts on all the characters: they're stored a bit higher than required for a 1920x1080 screen to reduce artefacts.





The format I've explained so far is DXT1, while DXT5 is what you'll want to use most. Colour is stored in the exact same way in DXT1 and DXT5. The difference is that DXT1 only has 1 bit alpha, while DXT5 can also store partially transparent pixels. DXT5 stores alpha in the same way as colour, so it stores two alpha values per block. Per pixel it stores an interpolation value for the alpha. So in total DXT5 stores two colours and two alpha values per block, and for each pixel one interpolation value for the colour and one for the alpha. This makes DXT5 exactly twice as big as DXT1 and results in high quality alpha/transparency.

There's also DXT3, but in practice I've so far never found it useful. DXT3 does not store two alpha values to interpolate between: instead it just uses 4 bits per pixel to directly store alpha per pixel. Since it's only 4 bits this means DXT3's smallest alpha step is 16. In other words: DXT3 is really bad at doing subtle alpha gradients, but really good at doing strong alpha contrasts (like object outlines). Since DXT5 is already good enough at doing strong alpha contrasts I've so far never used DXT3 in an actual game.

DXT1 can also store alpha, but only 1 bit alpha, so only on/off. A clever yet simple trick is used for this. Remember that each 4x4 block stores two colours. If the first colour is brighter than the second colour, then a block can store four different colours: the two main colours and two interpolated colours. This is how I explained it above. If however the first colour is darker than the second colour, then the block can store only three different colours. The fourth colour is used for fully transparent pixels. This results in slightly less colour quality around edges in the texture. Unfortunately it's only 1 bit alpha, so any anti-aliasing on object edges is lost.



There is another type of quality loss happening with DXT compression that I haven't mentioned yet. Colours are stored with 16 bits instead of the full 24 bits. This means that on top of the compression artefacts already mentioned we also get less colour depth. For most textures this is not a problem, but for subtle gradients it is. For this reason in Awesomenauts and Swords & Soldiers II we don't store pure gradients using DXT. We use simple 24 bit TGA instead. This is much bigger, but gradients are usually very thin, so storing a bunch of 256x1 textures with less compression is no problem.



Since DXT is used by so many games there are plenty of tools and plugins to work with it. For example, Nvidia has released a free Photoshop plugin for handling DDS files. When using this for 2D games, be sure to turn off mipmaps to save some more texture space (unless you actually need those mipmaps of course). Image browsing tool XnView supports DXT right away and I've heard there's even a plugin for Windows to see thumbnails of DXT files directly in Windows Explorer.

A fun aside here is that we've tried writing our own DXT compression tool to achieve DXT with less visual artefacts specifically for 2D games. We indeed managed to slightly reduce compression artefacts, but at the cost of more flickering in animations, so we ended up not using our own DXT compression tool. Maybe I'll get around to writing about that some day, let me know if you're specifically interested in this.

An important note on DXT is that iOS doesn't support the DXT format. Instead it has PVRTC. I haven't made any iOS games myself (Proun+ and Swords & Soldiers 1 were ported to iOS by respectively Engine Software and Two Tribes) so I don't personally have any experience with PVRTC). The rough concepts are pretty similar though. The big difference is that PVRTC doesn't have the little 4x4 block artefacts that DXT produces. Instead, PVRTC's compression artefacts come in the form of a slight blurriness.

One trick to improve quality with PVRTC is the same as with DXT: saving at a slightly higher resolution than what you really need will massively reduce the compression artefacts. Aaron Oostdijk from Gamistry (creators of the mobile hits Gold Diggers and Munch Time) mentions it also helps a lot to first smear the RGB colours our across the transparent areas. “A tool called Solidify B does this very nicely. And sometimes a light dither after that will improve results even more.” Note that I didn't use those tricks when compressing the image below.



On iOS you get the additional requirements that textures need to be square (same width and height) and power-of-two (16, 32, 64, 128, 256, 512, etc.). In many cases these two extra requirements demand a huge waste of texture space, unless you work around them through clever usage of texture atlases. On PC and console these requirements have been abolished around a decade ago, making efficient texture usage much easier there.

DXT results in serious compression artefacts but it provides such powerful compression that it is by far the best option for the vast majority of games. Next week I'll wrap up this series on texture formats by giving a big overview of all the formats discussed. See you then!

See also:
-Texture formats for 2D games, part 1
-Texture formats for 2D games, part 2: Palettes
-Texture formats for 2D games, part 4: Overview

Friday, 13 November 2015

Texture formats for 2D games, part 2: Palettes

After last week's introduction to texture formats we are going to look at palette textures today. Palette textures are an interesting and underused option for 2D games. Using them is quite a hassle since normal videocards don't support them, but for certain art styles they offer high quality and small texture size.

Palette formats are similar to GIF files. The idea is that we have a palette, a list of all the colours in a specific texture. In the actual texture we don't store the colour of each pixel, but rather a reference to the palette. If the texture doesn't have a whole lot of colours we can get away with using only 256 different colours. References to these can be stored in 8 bits per pixel, so this format is 75% smaller than uncompressed RGBA.



Palette textures are particularly useful for cartoony styles with flat shading, since those have fewer colours per texture. If your textures have more than 256 colours, you can reduce the number of colours. Depending on the art style this loss in quality might be acceptable. For a style with lots of colours and gradients, it probably isn't.

Most textures in Swords & Soldiers 1 on Nintendo Wii were stored using palettes. Some textures even had few enough colours that we could get away with only 16 different colours in the entire texture. This means we needed only 4 bits per pixel. That's 87.5% smaller than uncompressed RGBA! This would bring a 4096x4096 texture down from 64mb to 8mb. Of course such heavy compression is only suitable for some art styles and some textures.





The big caveat is that most videocards don't support palette textures directly. Older videocards like the one of the Nintendo Wii support it, but none of the modern Nvidia, AMD or Intel cards do. Swords & Soldiers 1 was originally a Wii game so we could easily use palette textures there.

This doesn't mean that using palette textures is impossible, just that it's more involved. It's quite easy to write a pixel shader that uses the colour from a greyscale texture as an index for a lookup in a 1D palette texture.

The problem with doing this yourself is that you can't use texture filtering any more. Interpolating palette indices results in random colours. To implement standard bilinear filtering you need to sample and interpolate the colours yourself in the pixel shader. Including the lookups in the palette this means 8 texture reads per pixel. If you happen to also need trilinear filtering for mipmapping this increases to 16 texture reads per pixel. Luckily not all 2D games need mipmapping, but still: 8 texture reads per pixel is significant and costs performance. However, the fun part is that many 2D games have plenty of performance to waste. If this is the case for your game and you have the time to write a good pipeline for them, then palette textures might be an excellent choice.



An interesting side effect of reducing the number of colours for a palette texture is that it makes ZIPping the texture much more effective. With less colours in the texture there is less variation so it can be compressed more effectively. This only helps for download size and for the size of the texture on disk: videocards can't handle ZIPped textures so the texture needs to be unzipped before it goes into video memory.

We used this extensively in Swords & Soldiers 1 to reduce download size. Since the Nintendo Wii had an extremely small hard disk keeping the install size of the game small was really important. For this reason we reduced the number of colours even in cases where it didn't make the texture itself smaller. For example, the size in video memory is the same for a texture with 200 colours or 256 colours. However, the texture with only 200 colours can be ZIPped further. Often there is no visible quality loss when reducing the number of colours a bit further, so we did this with a lot of textures on the Wii version of Swords & Soldiers 1.

When saving an image for the web in the GIF format it's common to turn on dithering. Dithering makes a palette texture alternate between two different colours in a noisy pattern to achieve what looks like smoother gradients with fewer colours. This works quite well for the web, but is totally unsuitable for games. Dithering makes the art flicker like crazy when an object moves around, so in games it can only be used on objects that are rendered pixel perfect, like the backdrops in old adventure games.



A huge benefit of palette textures is that team colours and colour variations can be done extremely efficiently. By simply swapping the palette you can change all the colours. This costs hardly any memory or additional performance.

In Awesomenauts we don't use palette textures so we have to store each character twice: once for the red team and once for the blue team. With palette textures this wouldn't have been necessary. To add to the fun this would also have allowed us to let the player customise his team colour (not that we would want that in a team-based game in which the visuals are all about readability). Lots of other fun tricks can be achieved through clever usage of palette swapping.





Since palette textures aren't supported by videocards by default they are quite impractical to use. Their limitation of a maximum of 256 different colours per texture is also very limiting. However, with certain art styles this technique will give you powerful, lossless compression and easy colour swapping, unique benefits that no other texture format can provide.

See you next week for a look into the master: DXT texture compression!

See also:
-Texture formats for 2D games, part 1
-Texture formats for 2D games, part 3: DXT and PVRTC
-Texture formats for 2D games, part 4: Overview

Friday, 6 November 2015

Texture formats for 2D games, part 1

When making a large scale 2D game like Swords & Soldiers II or Awesomenauts using the right texture formats can make a big difference for visual quality, video memory, loading times and download size. What format works best varies depending on art style and platform and a good understanding of the options helps a lot in making the right choices.

This is a huge topic so I've split it into four posts. Today I'll cover the basics. The other three posts will discuss the more obscure palette textures, DXT and finally a comparison table of all the discussed techniques. This short series will get a bit technical at points but I've tried to make it as readable for artists as possible: knowing how your art will be compressed is important for game artists.

While the texture formats available for 2D and 3D games are the same, 2D games have different requirements, so it is interesting to approach this topic from a 2D standpoint. Also, 2D games usually don't have things like normal maps, roughness maps and reflection maps and mostly don't need high dynamic range (HDR) textures, so I won't be discussing the requirements of such special kinds of textures.

The first thing to realise is that transparency is extremely important in 2D games. While in a 3D game the shape of an object is generally determined by its polygons, the shape in a 2D game usually comes from transparency in the texture. In many 2D games the art is put on the screen by just having a simple rectangle with an image with transparency on it. Transparency is generally stored in the alpha channel, so the quality of the alpha channel is an important factor in 2D games.



The most basic and highest quality texture format is a simple RGBA32 with 8 bits per colour channel. In total each pixel then uses 4 bytes, or 3 bytes if there is no alpha channel. These can be stored in various file formats, like TGA and BMP, but in the end in the videocard it's all the same. At Ronimo we use TGA for these. This image format is lossless: whatever your original image was, you get exactly that in the game. Note that compressed TGA isn't supported by videocards, so when using TGA you will need to use its uncompressed 32 bit or 24 bit format (depending on whether you have alpha).



The big downside of this format is that it's huge. A 1024x1024 texture with alpha is already 4mb this way. Awesomenauts characters have a texture of nearly 4096x4096, containing all their animation frames. With simple RGBA, this would amount to a whopping 64mb of texture space for just a single character texture! This results in a simple conclusion: we need something smaller for the vast majority of our art. Even when you have several gigabytes of texture space available, full RGBA32 will fill it up really quickly. At Ronimo we only use TGA for images that are small and would lose too much quality when compressed. In practice this means we only use TGA for gradients.



One approach to reducing texture size is to use vector art and render that directly. This means you hardly need to use any textures at all since most colour and shape are defined by polygons instead of by a texture. It also means that artists need to use special vector tools like Illustrator and Flash. This is extremely limiting: more painterly styles are practically impossible this way. It also uses way more performance because in-game animation isn't simply swapping an image frame any more. Rendering vector art in real-time is a relatively complex and performance-heavy task.

Since vector tools are so limiting and since rendering vectors is a lot of work to program and much slower than textures, we don't use vector art in-game at all at Ronimo. We sometimes use vector tools when specific images are most easily made that way, but by the time they get to the game we always save the result as simple textures. Depending on your art style vectors might be a great option though!



A common suggestion from artists when discussing texture formats is to use JPG or PNG. These formats are excellent at achieving small image size with little visible loss. However, they aren't suitable for real-time games: videocards don't support these texture formats at all. That means that a texture that is stored on disk as JPG or PNG needs to be converted to something else to go into video memory, often growing the texture to the size of an uncompressed TGA. An important goal in choosing a texture format is to reduce the usage of video memory (usually to be able to store even more art there). Since JPG and PNG cannot be stored in video memory, they don't help there at all.



This doesn't mean these formats are entirely useless. In a game where the size on disk is most important you can use JPG for all the textures and then simply put them in the much larger RGB24 format in video memory. A game that famously did something like this is Rage (one of my favourite games of the past years). Rage needs to constantly stream in new textures, so the speed of reading from disk was a problem. To solve this they stored the files in a JPG-like format on disk, loaded that into memory, and then converted it in real-time to DXT to be used by the game. Such hardcore streaming tech is generally not necessary for 2D games though.

Videocards can only handle a very limited set of texture types. The three main approaches to reducing texture size without simply reducing their resolution are to use less bit-depth, to use palettes or to use the DXT format. Note that specific types of hardware might offer additional formats, so it can really pay off to look into the details of whatever platform you are developing for. Palettes and DXT will be separate blog posts, so let's look into reducing bit-depth now.

Using less bit-depth is the simplest approach to texture compression. For example, you can use a 16 bit format. Instead of using 8 bits per channel for RGB, you might use 5 bits for red, 6 for green and 5 for blue. Such an R5G6B5 format is 33% smaller than uncompressed 24bit RGB. It also means less colour depth, so especially gradients will look visibly worse. The difference is quite small though so for most textures this loss in quality is hardly noticeable and totally acceptable. The reason green is 6 bits while red and blue are 5 bits is that 16 doesn't divide by 3 equally. Human eyes are better at seeing in the green spectrum, so the extra bit is put in the channel where it matters most.



If you need alpha there's often something like R5G5B5A1 available, which also uses only 16 bits per pixel, but at slightly greater compression costs. This specific format has only 1 bit for alpha, so pixels are either fully transparent or fully opaque. This not only means that partially transparent pixels aren't possible, but also that anti-aliasing on the image's edges is entirely lost. In 2D games anti-aliasing is usually part of the original art and stored in the texture. Losing this can be pretty bad for image quality, so I generally wouldn't advice using formats with only 1 bit for alpha. There are also other combinations, like R4G4B4A4, giving better alpha but significantly worse colours. If you hate your own game you can even use R2G3B3.



Videocards support many different combinations of bitrates per channel so it's useful to have a look and figure out what works for your purpose. Specifically interesting is L8A8: this is a greyscale texture (L for “luminance”) with high quality alpha. This is 50% smaller than full R8G8B8A8. If the original image is already greyscale this format doesn't result in any quality loss. If you happen to be making a game with many monochrome textures, this might be a relevant option. If you don't need alpha you can half the size again by using L8.

In our own games none of the formats discussed so far are used extensively. We use a little bit of R8G8B8A8 through TGA files for gradients, and L8A8 for fonts. The vast majority of our textures is DXT5, which I will discuss in part 3 of this short series. Next week I will first look into the format we used for Swords & Soldiers 1 on Nintendo Wii: palette textures. This format is often overlooked and usually quite a hassle to use, but for certain art styles it delivers an excellent combination of small textures with little quality loss, as I'll show next week.

See also:
-Texture formats for 2D games, part 2: Palettes
-Texture formats for 2D games, part 3: DXT and PVRTC
-Texture formats for 2D games, part 4: Overview

Saturday, 4 January 2014

Bitcrunching numbers for bandwidth optimisation

An important part of making a complex multiplayer game like Awesomenauts is optimising bandwidth usage. Without optimisations, just sending position updates for one character to one other player already costs 240 bytes per second (30 updates per second at 4 bytes each for both X and Y). And this is just a fraction of what we need to send: Awesomenauts can have up to 35 characters running around the game world, and we need to synchronise much more than just their positions. As you can see, bandwidth usage quickly spirals out of control, so we need to be smarter than just naively sending all the data all the time.

In the many months we spent on bandwidth optimisation, we employed lots of different techniques. The one I would like to talk about today can massively decrease bandwidth by crunching numbers on the level of individual bits. This technique not only worked great for the multiplayer implementation of Awesomenauts, but I am now also using this extensively for reducing the size of our replays.

Let's have a look at the core idea here. Storing one unsigned int normally costs 32 bits. In this we can store a value in the range of 0 to over 4 billion. However, we rarely need a range as big as that. For example, the amount of Solar you collect during an Awesomenauts match rarely goes over 5,000. In a really long match we might go past that, so let's bet on the safe side and say we want to store Solar values up to 10,000. This could be stored in 14 bits, since 2^14 = 16,384. That is less than half of the normal 32 bits used for a value like this!

Using too many bits is not a problem in memory since modern computers have plenty of that. But when sending the Solar over the network, we would like to use only those 14 bits we actually need. However, normally we can only handle data on a byte level, not on a bit level, so the best we can easily do is to store this in two bytes (16 bits).

What we need here to solve this is a bitstream, something that we can use to group together a lot of values on a bit-level. This way we can just grab a block of memory and push in all the values we want to send over the network. The idea is quite straightforward, so let's jump right in and have a look at how one could use that:

//Class for pushing bits into a block of memory
class BitInStream
{
public:
    BitInStream(char* data);
    void addUnsignedInt(unsigned int value, unsigned int numBits);
    void addBool(bool value);
};

//Class for reading bits from a block of memory
class BitOutStream
{
public:
    BitOutStream(const char* data);
    unsigned int getUnsignedInt(unsigned int numBits) const;
    bool getBool() const;
};

//Example of using these
void main()
{
    //The bits are stored in this
    char data[4];
 
    //Example of writing some values
    BitInStream bitInStream(data);
    bitInStream.addUnsignedInt(solar, 14);
    bitInStream.addUnsignedInt(itemsBought, 6);
    bitInStream.addBool(isAlive);
    bitInStream.addUnsignedInt(damageDone, 10);
 
    //Read back values
    BitOutStream bitOutStream(data);
    solar       = bitOutStream.getUnsignedInt(14);
    itemsBought = bitOutStream.getUnsignedInt(6);
    isAlive     = bitOutStream.getBool();
    damageDone  = bitOutStream.getUnsignedInt(10);
}

The details of actually implementing the bitstreams itself is too much for this blogpost, so I will leave that as a fun exercise for the reader. Hint: you can use bitwise operators like |, ‹‹, &.

When looking at this code there are a number of observations to make. First is that I am actually using only 31 bits while storing them in 4 bytes. In other words: I am wasting 1 bit here! This is something that almost always happens when using bits: at some point they need to turn into bytes, since that is what network packets are measured in, and we usually waste a couple of bits there. This loss can be decreased by using larger bitstreams with many more values in them.

A much more important realisation here is how easy it is to break this code. If I read back the solar with the wrong number of bits, then not only will the solar be complete garbage, but also all values after it. The same happens if I accidentally read back values in the wrong order, or forget to read one. The compiler will not warn you of any of these things and debugging on a bit-level is difficult, so this produces extremely stubborn bugs! Another easy mistake is to use too few bytes for the data, resulting in buffer overflows and thus memory corruption, or to use too many bytes, resulting in wasting bandwidth. In short: use bitstreams only when you really need them and be extremely careful around them!

So far we have only stored the easy kinds of data: bools and unsigned ints translate very easily to bits. However, many things in a game are floats. When using an unsigned int, taking just its first 10 bits results in a valid number, just with a smaller range. But with a float taking those same 10 bits results in nonsense. How can we send floats with arbitrary numbers of bits?

The trick here is rather simple. A float has a number of properties that we usually don't need. Floats have very high precision near 0, but when storing a position, we want the same precision everywhere. Floats also have an extremely large range, but for that same position we already know the size of the world and we don't need to be able to store values outside that range.



Knowing these things we can store positions (and most other floats) in a different way. Levels in Awesomenauts happen to all be in the range -10.0 to 30.0. Knowing this we can just store the x-position as an unsigned int with an arbitrary number of bits. If we want to store a position in 8 bits, then the lowest value (0) means position -10.0, and the highest value (255) means position 30.0. Values in between are just interpolated. This is a simple and very effective trick. Depending on how much precision we need, we can use more or less bits.



When synching over the network we actually don't need all that much precision, since the receiver is not using the position for a physics simulation. He mostly just uses it for visualisation and checking whether a bullet hits. So in Awesomenauts we use 13 bits for synching the x-position and another 13 bits for the y-position, which is a lot less than the normal 32 bits each would need as a float!

The same idea can be applied to many other things that are also floats and for which we know the range. Some things even need extremely little precision. For example, the health of other players is only visualised in their healthbar, so we only need to synch as many bits of that as are needed to make the healthbar look correct. Since healthbars are usually in the order of 100 pixels wide, we can store health in 7 bits, even if the actual range of the health might be 0.0 to 2000.0. With 7 bits our for that range would be only 15.625, but more isn't needed because the difference is not visible in the healthbar anyway. The simulation is not influenced at all by this, since the owner of the character still stores the full precision float for the health.



Using tricks like these we managed to get the bandwidth usage of Awesomenauts down to around 16 kilobyte per second. This is the total upload needed at the player who manages bots and turrets. The five other players in a match don't manage those bots and turrets, so they use far less bandwidth still. For comparison: our very first network implementation in the Awesomenauts prototype we had four years ago used around 50x (!) more bandwidth. Bit-crunching numbers as explained in this blogpost is just one of the many tricks we used to decrease bandwidth, but it was definitely one of the strongest!

Saturday, 23 February 2013

Optimising special effects in Awesomenauts

The latest Awesomenauts patch increased the framerate a lot for players with older videocards, especially during fierce battles. We managed to optimise our special effects without making them look noticeably different. Today I would like to explain how we did that!

Before I dive into the details, let me first give some background. In a 2D game like Awesomenauts, most objects are just a square with a texture on it. The texture contains an image, and you only see that image, not the entire square. However, the videocard renders the entire square. So from a performance perspective, it doesn't matter how much of the texture is actually visible. The entire square is rendered and the every pixel uses performance!



Our artists know this, so they try to crop the image to have as few transparent pixels as possible (without changing the actual looks of the end result, of course).

Since objects in Awesomenauts contain so many (partially) transparent pixels, we cannot easily detect whether an object is hidden entirely behind another object or not. So we always render every object that is on the screen, from back to front, regardless of whether an object is actually visible or not. All objects are rendered on top of each other until the image is complete.



This introduces a big performance issue: massive overdraw. Overdraw is when a single pixel needs to be drawn several times each frame. On 1920x1080, the screen contains over 2 million pixels. If we draw every pixel 10 times, that is already 20 million pixels per frame. Now do that 60 times per second, and we are rendering a whopping 1.2 billion pixels per second. That's a mindbogglingly large number of pixels per frame!

The fun thing, however, is that modern videocards are totally okay with that. However, for slightly older ones, it might become a problem to pull this off fluently...

In a previous blogpost I explained how I abused my depth of field blur as an excuse to render the backgrounds in Awesomenauts on a much lower resolution. This greatly reduces the number of pixels that need to be rendered every frame, but it only helps for the background, since the rest of the scene needs to be sharp.

Since the background are optimised this way already, the biggest remaining overdraw in Awesomenauts happens during special effects, mostly those of nauts' special skills. This is because they have a lot of overlapping particles for smoke, fire, dust and debris. Having several of those special skills on screen at the same time decreased the framerate on older and mobile videocards too much. This is extra problematic because during fierce battles, high framerate is needed most!



Once I figured out that overdraw was the cause of the framedrops we were seeing on older videocards, the solution seemed simple: decrease the number of particles on screen. The difficulty, however, is that it is difficult for artists to optimise things without being able to see what they need to optimise. Having a ton of large particles for thin smoke looks very subtle, but eats performance like crazy. And if an effect needs to be optimised, what part of that effect is the problem exactly?

So I made a small addition to our engine to visualise overdraw. Internally we have a shortcut now to enable this, and when this is pressed, the screen shows how often each pixel is rendered. White means a pixel is rendered 85 times (!), while black means it is rendered 0 times. Technically, this is a very simple thing to make: all objects are simply rendered on a very dark additive blend mode. So each rendered object adds a little bit of brightness.

This is not just informative, but also looks pretty awesome! Have a look at this video of going through the menus and then some gameplay with Skølldir to see how weird and cool this looks:



As you can see, the character itself is not every expensive to render, since it is only one square. The problem comes from the tons of particles.

Using the overdraw visualisation, our artists (in this case mostly Koen, Ralph and Gijs) were able to quickly identify which parts of which special effect needed to be changed. These changes are usually pretty simple: 40 smoke particles that are 10% opaque, look nearly exactly the same as 20 smoke particles that are 20% opaque. So in most cases the trick was to drastically decrease the number of particles and then just modify some colours to compensate for that.

The video above shows what the overdraw looks like after these optimisations were already done, so here are some comparison shots that show how massively the overdraw was decreased while hardly changing the looks of these effects:





We tested the results on our Mac (even fast, expensive Macs usually have mobile videocards, which are not very fast and far from ideal for gaming) and indeed the game runs a lot smoother now. Several users reported that while they could previously only run Awesomenauts smoothly on Normal graphics quality, they can now run on High!

These improvements were released on Steam in patch 1.14 two weeks ago, so I hope players with older videocards are enjoying the smoother framerate they are getting!