New in Qt 5.10: Texture Based Animations in Qt 3D Or how to blow things up
Many new features were added to Qt 3D in the 5.10 release. One of them is the support for sprite sheets, contributed by KDAB, as provided by QSpriteGrid and QSpriteSheet and their respective QML items.
One way of animating things is to switch between many different versions of the same object at different points in time, like the flip books we all enjoyed as kids. If you flip fast enough, you get the illusion of animation.
In the context of OpenGL and Qt 3D, images are simply textures and are very commonly used to add details to 3d models. The naive approach to animating texture would be to use lots of them. However, switching textures has a very high cost so traditionally modellers will use texture atlases, where all the images are arranged into a single texture. This does complicate modelling slightly as the original texture coordinates need to be modified to point to the portion of the atlas that now contains the relevant image.
In effect, sprite sheets are simplified atlases that take care of this for you. They are commonly used in 2d or 2.5d applications to animate effects or characters.
Building Sprite Sheets
Simple sprite sheets are just regular grids of images.
However, as with general atlases, all individual images need not be the same size or be arranged in a grid. In that case, individual sprites need to be specified by their bounding rectangle within the texture.
There’s a number of applications that can be used to create them. Blender can output steps in animations to a sprite sheet. TexturePacker can also be used to assemble pre-existing images.
Texture Transforms
In the simplest case, a sprite will be mapped on to planar surfaces, a simple rectangle. When applying textures to a surface, you of course need to provide texture coordinates, mapping points to pixels in the texture. In the case of the PlanarMesh, precomputed texture coordinates will map to textures such as it covers the entirety of the surface.
However, if the source texture is a sprite sheet, then the texture coordinates do not work any more as we want to cover the specific sprite, not the entire sheet.
One thing you do NOT want to do is replace the texture coordinates every time you want to change sprite.
In effect, texture coordinates need to be:
- scaled to cover the range of a sprite cell
- offset to specify the right origin
This transformation can easily be encoded in a 3×3 matrix which is applied to texture coordinates in the vertex shader. In order to support this, QTextureLoader has been extended with a textureTransform parameter which is passed to the shader as a uniform.
Putting it all together
A sprite sheet is conceptually very simple in Qt 3D. It has:
- a list of “areas”, one for each sprite
- a current sprite index in that list
- an input texture to pick sprites from
- a 3×3 texture transform matrix which is updated every time the current index changes
So simple animations can be achieved by changing the current sprite index and binding the texture transform to the matching property in the QTextureLoader instance.
Sprite Grids
QSpriteGrid is used when sprites are arranged in regular grid.
Entity {
PlaneMesh {
id: mesh
}
TextureMaterial {
id: material
texture: TextureLoader {
id: textureLoader
source: "spritegrid.png"
mirrored: false
}
textureTransform: spriteGrid.textureTransform
}
SpriteGrid {
id: spriteGrid
rows: 2; columns: 6
texture: textureLoader
}
components: [ mesh, material ]
}
Images in the grid are assumed to be arranged in row major order. So currentIndex must remain between 0 and rows * columns. The texture property points to the image containing the sprites. The current index, number of rows and columns and the actual size of the texture are all used to compute the texture transform.
Sprite Sheets
QSpriteSheet is used when the sprites are not all the same size and/or are not organised in a grid. You then need specify the extent of each sprite.
Entity {
PlaneMesh {
id: mesh
}
TextureMaterial {
id: material
texture: TextureLoader {
id: textureLoader
source: "spritegrid.png"
mirrored: false
}
textureTransform: spriteGrid.textureTransform
}
SpriteSheet {
id: spriteSheet
texture: textureLoader
SpriteItem { x: 0; y: 0; width: 250; height: 172 }
SpriteItem { x: 276; y: 0; width: 250; height: 172 }
SpriteItem { x: 550; y: 0; width: 250; height: 172 }
//...
}
components: [ mesh, material ]
}
The currentIndex must remain between 0 and the number to QSpriteItem children.
Example
In this example, we use a sprite grid which contains frames of an explosion. A timer is used to change the current index. While the animation runs, the object is faded out.
Looking straight on, you see a rather nice effect. The illusion becomes clear when you look at it sideways and see the plane on which the texture is mapped.
Notes:
- In the general 3d case, it is common to combine sprite sheets with billboards in order to keep the plane aligned with the screen. This can normally easily be done in the shaders.
- QSpriteSheet doesn’t currently support rotated sprites.
- QTextureMaterial does not currently support alpha blending, so transparent portions of the textures will appear black. This can worked around by building a custom material and will be fixed in 5.11.
- One of the problems with putting all the images in one texture is that you get quickly limited by the maximum texture size. For bigger images, a more modern approach may be to use QTexture2DArray. However, in this case all images need to be the same size. But the only limit then would be the maximum texture size and the amount of texture memory available…
Happy to see some info on this. The API docs are still a bit spartan so I couldn’t figure out how to use it when I ran across it. Would also love to see these examples fleshed out with C++ example code for these kinds of posts! I sometimes find myself scratching my head trying to figure out how to translate QML to C++ because I am not as familiar with QML.
The example I built for this is indeed in QML, mostly because I’m using QtQuick to drive the animation. Time permitting I’d like to write a C++ version which would use Qt3D’s own animation engine to do the work…
Nice write-up. One nitpick:
“So currentIndex must remain between 0 and rows * columns.”
I think you mean rows * columns – 1 right? Or maybe write it as “in range [0, rows * columns)”.
Also, the way it’s written (“So…”) you make it sound like it’s because they are in row major, but it would hold even if it’s column major order, so I suggest dropping to “So”.
yes, you are correct about the range
What is the difference between SpriteGrid and AnimatedSprite qml types? They look very similar. Does SpriteGrid provide significant performance benefit?
While they may appear to do the same thing, they are completely different beasts. AnimatedSprite is to use with QtQuick, SpriteGrid can only be used in Qt 3D scenes.
Thanks, this is really cool! I’m wondering: Is there a way to create dynamic textures from a QImage? I tried to use a TextureLoader with a source pointing to a custom QQuickImageProvider, but nothing appears on screen.