Chapter 21 - Indirect drawing (animated models) and compute shaders
In this chapter we will add support for animated models when using indirect drawing. In order to do so, we will introduce a new topic, compute shaders. We will use compute shaders to transform model vertices from the binding pose to their final position (according to current animation). Once we have done this, we can use regular shaders to render them, there will be no need to distinguish between animated and non animated models while rendering. In addition to that, we will be able decouple animation transformations from the rendering process. By doing so, we will be able to update animation models in a different rate than the render rate (we do not need to transform animated vertices in each frame if they have not changed).
You can find the complete source code for this chapter here.
Concepts
Prior to explaining the code, let's explain the concepts behind indirect drawing for animated models. The approach we will follow will be more or less the same as the one used in the previous chapter. We will have a global buffer which will contain vertices data. The main difference is that we will use first a compute shader to transform vertices from the binding pose to the final one. In addition to that,we will not use multiple instances for a model. The reason for that is.,, even if we have several entities that share the same animated model, they can be in different animation state (the animation may have started after, have a lower update rate or even the specific selected animation of the model may be different). Therefore we will need, inside the global buffer that will contain the animated vertices, a single chunk of data per entity.
We will still need to keep binding data, we will create another global buffer for that for all the meshes of the scene. In this case we do not need to have separate chunks per entity, just one per mesh. The compute shader will access that binding poses data buffer, will process that for each of the entities and will store the results into another global buffer with a structure similar to the one used for static models.
Model loading
We need to update the Model class since we will not store bone matrices data any more in this class. Instead, that information will be stored in a common buffer. Therefore the inner class AnimatedFrame cannot be a record any longer (records are immutable).
The fact that we pass from a record to a regular inner class, changing the way we access Model class attributes requires a slight modification in the ModelLoader class:
The animVaoId will store the VAO which will define the data which will contain the transformed animation vertices, that is, the data after it has been processed by the compute shader (remember one chunk per mesh and entity). The data itself will be stored in a buffer, whose handle will be stored in destAnimationBuffer. We need to access that buffer in the compute shader which doe snot understand VAOs, just buffers. We will need also to store bone matrices and indices and weights into two buffers represented by bonesMatricesBuffer and bonesIndicesWeightsBuffer respectively. In the cleanup method we must not forget to clean the new VAO. We also need to add getters for the new attributes.
We can now implement the loadAnimatedModels which starts like this:
We will see later on how the following methods are defined but, by now:
loadBindingPoses: Stores binding pose information for all the meshes associated to animated model.
loadBonesMatricesBuffer : Stores the bone matrices for each animation of the animated models.
loadBonesIndicesWeights: Stores the bones indices and weights information of the animated models.
The code is very similar to the loadStaticModels, we start by creating a VAO for animated models, and then iterate over the meshes of the models. We will use a single buffer to hold all the data, so we just iterate over those elements to get the final buffer size. Please note that the first loop is a little bit different than the static version. We need to iterate over the entities associated to a model, and for each of them we calculate the size of all the associated meshes.
The loadBindingPoses iterates over all the animated models, getting the total size to accommodate all the associated meshes. With that size, a buffer is created and populated using the populateMeshBuffer which was already present in the chapter before. Therefore, we store binding pose vertices for all the meshes of the animated models into a single buffer. We will access this buffer in the compute shader, so you can see that we use the GL_SHADER_STORAGE_BUFFER flag when binding.
The loadBonesMatricesBuffer method is defined like this:
We start iterating over the animation data for each of the models, getting the associated transformation matrices (for all the bones) for each of the animated frames in order to calculate the buffer that will hold all that information. Once we have the size, we create the buffer and start populating that (in the second loop) with those matrices. As in the previous buffer we will access this buffer in the compute shader, therefore we need to use the GL_SHADER_STORAGE_BUFFER flag.
The loadBonesIndicesWeights method is defined like this:
As in the previous methods, we will store the weights and bone indices information into a single buffer, so we need to first calculate its size and later on populate it. As in the previous buffer we will access this buffers in the compute shader, therefore we need to use the GL_SHADER_STORAGE_BUFFER flag.
Compute shaders
It is turn now to implement animation transformations through compute shaders. As it has been said before, a shader is like any other shader but it does not compose any restrictions on its inputs and its outputs. We will use them to transform data, they will have access to the global buffers that hold information about binding poses and animation transformation matrices and it will dump the result into another buffer. The shader code for animations (anim.comp) is defined like this:
As you can see the code is very similar to the one used in previous chapters for animation (unrolling the loops). You wil notice that we need to apply an offset for each mesh, since the data is now stored in a common buffer. In order to support push constants in the compute shader. The input / output data is defined as a set of buffers:
srcVector: this buffer will contain vertices information (positions, normals, etc.).
weightsVector: this buffer will contain the weights for the current animation state for a specific mesh and entity.
bonesMatrices: the same but with bones matrices information.
dstVector: this buffer will hold the result of applying animation transformations.
The interesting thing is how we compute that offset. The gl_GlobalInvocationID variable will contain the index of work item currently being execute din the compute shader. In our case, we will create as many work items as "chunks" we will have in the global buffer. A chunk models a vertex data, that its its position, normals, texture coordinates etc. Therefore, por vertices data each time the work item is increased, we need to move forward in the buffer 14 positions (14 floats: 3 for positions,. 3 for normals, 3 for bitangents, 3 for tangent and 2 for texture coordinates). The same applies for weights buffers which holds data for weights (4 floats) and bone indices (4 floats) associated to each vertex. We use also the vertex offset to move long the binding poses buffer and the destination buffer along with the drawParameters data which point to th ebase offset for each mesh and entity.
We will use this shader in a new class named AnimationRender which is defined like this:
As you can see, the definition is quite simple, when creating the shader we need to set up the GL_COMPUTE_SHADER to indicate that this is the compute shader. The uniforms that wew ill use will contain the offset in binding pose buffer, weights and matrices buffer and destination buffer. In the render method we just iterate over the models and get the mesh draw data for each entity to dispatch a call to the compute shader by invoking the glDispatchCompute. The key, again tos the groupSize variable. As you can see we need to invoke the shader as many times as vertices chunks there are in the mesh.
Other changes
We need to update the SceneRender class to render the entities associated to animated models. The changes are shown below:
The code to render animated models is quite similar as the one used for static entities. The differences is that we are not grouping entities that share the same model, we need to record draw instructions for each of the entities and associated meshes.
We need also to update the ShadowRender class to render animated models:
In the Render class we just need to instantiate the AnimationRender class, and use it in the render loop and the cleanup method. In the render loop we will invoke the AnimationRender class render method at the very beginning, so animation transformations are applied prior to render the scene.
Finally, in the Main class we will create two animated entities which will have a different animation update rate to check that we correctly separate per entity information: