Jump to content

Animated Textures


Josh

2,122 views

 Share

One of my goals in Ultra Engine is to avoid "black box" file formats and leave all game assets in common file formats that can be easily read in a variety of programs. For this reason, I put a lot of effort into the Pixmap class and the DDS load and save capabilities.

In Ultra Engine animated textures can be stored in a volume texture. To play the animation, the W component of the UVW texcoord is scrolled. The fragment shader will sample the volume texture between the nearest two slices on the Z axis of the texture, resulting in a smooth transition between frames using linear interpolation. There's no need to constantly swap a lot of textures in a material, as all animation frames are packed away in a single DDS file.

The code below shows how multiple animation frames can be loaded and saved into a 3D texture:

int framecount = 128;
std::vector<shared_ptr<Pixmap> > pixmaps(framecount);
for (int n = 0; n < framecount; ++n)
{
    pixmaps[n] = LoadPixmap("https://raw.githubusercontent.com/Leadwerks/Documentation/master/Assets/Materials/Animations/water1_" + String(n) + ".png");
}
SaveTexture("water1.dds", TEXTURE_3D, pixmaps, framecount);

Here is the animation playing in the engine:

The resulting DDS file is 32 MB for a 256x256x128 RGBA texture:

water1.zip

You can open this DDS file in Visual Studio and view it. Note that the properties indicate this is the first slice of 128, verifying that our texture does contain the animation data:

Untitled.thumb.png.210ccfcb29cbfc13bdd706398b88230c.png

Adding Mipmaps

The DDS format supports mipmaps in volume textures. A volume mipmap is just a lower-resolution image of the original, with all dimensions half the size of the previous frame, with a minimum dimension of 1. They are stored in the DDS file in descending order. The code below is a little complicated, but it will reliably compute mipmaps for any volume texture. Note the code is creating another STL vector called "mipchain" where all slices of all mipmaps are stored in order:

auto plg = LoadPlugin("Plugins/FITextureLoader.dll");

int framecount = 128;
std::vector<shared_ptr<Pixmap> > pixmaps(framecount);
for (int n = 0; n < framecount; ++n)
{
    pixmaps[n] = LoadPixmap("https://raw.githubusercontent.com/Leadwerks/Documentation/master/Assets/Materials/Animations/water1_" + String(n) + ".png");
}
    
//Build mipmaps
iVec3 size = iVec3(pixmaps[0]->size.x, pixmaps[0]->size.y, pixmaps.size());
auto mipchain = pixmaps;
while (true)
{
    auto osize = size;
    size.x = Max(1, size.x / 2);
    size.y = Max(1, size.y / 2);
    size.z = Max(1, size.z / 2);
    for (int n = 0; n < size.z; ++n)
    {
        auto a = pixmaps[n * 2 + 0];
        auto b = pixmaps[n * 2 + 1];
        auto mipmap = CreatePixmap(osize.x, osize.x, pixmaps[0]->format);
        for (int x = 0; x < pixmaps[0]->size.x; ++x)
        {
            for (int y = 0; y < pixmaps[0]->size.y; ++y)
            {
                int rgba0 = a->ReadPixel(x, y);
                int rgba1 = b->ReadPixel(x, y);
                int rgba = RGBA((Red(rgba0)+Red(rgba1))/2, (Green(rgba0) + Green(rgba1)) / 2, (Blue(rgba0) + Blue(rgba1)) / 2, (Alpha(rgba0) + Alpha(rgba1)) / 2);
                mipmap->WritePixel(x, y, rgba);
            }
        }
        mipmap = mipmap->Resize(size.x, size.y);
        pixmaps[n] = mipmap;
        mipchain.push_back(mipmap);
    }
    if (size == iVec3(1, 1, 1)) break;
}
 
SaveTexture("water1.dds", TEXTURE_3D, mipchain, framecount);

The resulting DDS file is a little bigger (36.5 MB) because it includes the mipmaps.

water1_mipmaps.zip

We can open this DDS file in Visual Studio and verify that the mipmaps are present and look correct:

mips.thumb.png.3e475f9b298c94fe395a4307a195a952.png

Texture Compression

Volume textures can be stored in compressed texture formats. This is particularly useful for volume textures, since they are so big. Compressing all the mipmaps in a texture before saving can be easily done by replacing the last line of code in the previous example with the code below. We're going to use BC5 compression because this is a normal map.

//Compress all images
for (int n = 0; n < mipchain.size(); ++n)
{
	mipchain[n] = mipchain[n]->Convert(TEXTURE_BC5);
}
SaveTexture("water1.dds", TEXTURE_3D, mipchain, framecount);

The resulting DDS file is just 9.14 MB, about 25% the size of our uncompressed DDS file.

water1_bc5.zip

When we open this file in Visual Studio, we can verify the texture format is BC5 and the blue channel has been removed. (Only the red and green channels are required for normal maps, as the Z component can be reconstructed in the fragment shader): Other types of textures may use a different compression format.

comp.thumb.png.2988de7a6467bf6f71721142ec872225.png

This method can be used to make animated water, fire, lava, explosions and other effects packed away into a single DDS file that can be easily read in a variety of programs.

  • Like 4
 Share

1 Comment


Recommended Comments

I found the cross-fading is not good for animations that use an alpha channel, but it’s great for solid textures. So I would not use this with 2D sprites unless the texture was not using a linear filter, so the frame change would be instantaneous.

Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...