‘Fast and Deep Deformation Approximations’ Implementation
Last updated
Last updated
3D animated characters in feature films use sophisticated rigs with complex deformations that can be computationally intensive. The authors of the ‘Fast and Deep Deformation Approximations’ paper, Bailey et al., propose a method for approximating such deformations using Neural Networks. I have created a short video outlining the proposed model; you can watch the video clicking here. This article is meant as support material to that video, so I encourage you to watch it first.
I have also implemented a prototype of that same model in Maya, and in this tutorial, I am going to show you how to implement it yourself. Here is what you will learn:
In the FDDA paper authors use one Neural Network per joint to correlate joint transformation to mesh deformation. So, the first thing you’ll need to do is create the dataset that allows you to train such a network.
I have created two samples scenes that you can download (in the resources for this article). One has a model deformed with clusters, which will be your base mesh, and the other has a more complex set of deformations, this is what you’ll try to approximate.
You can import both these models into a new scene (using namespaces) and use a script to extract the joint transform and the displacement between these two models in a CSV file. The full script is available in the resources for this article, but I’ll go over the important stuff here. The first thing we do is import the packages we’ll use, then we set some global parameters.
I think most of the code above is self-explanatory. One thing I’d like to comment is about how we are sampling the data. We’ll create random transforms for the joint. The variable ‘samples’ refer to how many of these random transforms, and corresponding deformations, we’ll create.
After that, we define a function to extract the displacement amongst two meshes. Note that we construct one big list with all displacements for all mesh vertices (1).
Finally, in the main execution, we get the meshes and joints and generate random transforms for which we’ll sample the inputs and outputs to our model. Both transforms and displacements are stored in NDArrays so they can be easily exported as CSVs.
There are two important things to note about the joint transformation. The first is that randQuatDim()is generating random values from -1 to 1 for every dimension in the quaternion we build. The second is that we need to get the transformation from the joints after we have set them because they have joint limitations turned on. Hence, the final transform we’ll be different than that random matrix we have created.
You can inspect the CSV files in Excel, Google Spreadsheets, or other tools of your choosing.
Now that you have the dataset it is time to train the network. In the resources for this post, you’ll see I have created a well-documented IPython notebook for you to run in Google Colaboratory. I’ll comment on the most critical aspects of that code here.
To train the network we’ll be using Keras, the same framework I have used in my first tutorial (here). These are all the packages you’ll need to load:
The last one (google.colab) is only relevant if you are using Google Colaboratory. If you are you might be wondering how to get your custom dataset up to that system. Here is what you’ll do:
You’ll be prompted to choose the files from your hard drive. The path to the files is stored as a key in a dict; this is how you retrieve the path and load the CSV as a Numpy NDArray:
In this prototype, I’m normalizing the dataset before proceeding with the training. This step is not mandatory, and I have not done it in previous tutorials for the sake of simplicity. But it is a common practice that you should get used to because it improves the accuracy of your model at no extra cost.
The idea behind feature normalization is that some features in your dataset might be huge scalar values that vary a lot, while others might be near constant, near zero values. Such different values will have a very different impact on the activation of your neurons and will bias the network. Therefore, it is good to rescale the data to avoid such effect.
Here I’m using a widespread scaling approach, I remove the feature’s mean and divide the remainder by its standard deviation. Notice these are not the mean and standard deviation of the whole dataset, but of all samples for each feature (i.e., every component of the transform matrix, and every dimension of every displacement vector).
I create one function to normalize and another to ‘denormalize’ features:
Note that in the ‘featNorm’ function we output not only the normalized features but the means and standard deviations. I do that because we’ll need this information to transform new data in the prediction phase, and also to ‘denormalize’ the network’s output. We apply the normalization and store the values using the following code:
Now that we have prepared the data, let’s train the model.
We represent the model using Keras sequential interface, much like we have done in the previous tutorial. The main difference is that here we are not training a classification model, but a regression model (see the video for further clarification). So, the activation function in the final layer is just a linear mapping of the activation values. Also, the loss function, the thing we are trying to minimize, is the ‘mean squared error’, that is, the squared distance between the predictions and the actual values.
We have used the number of neurons and the activation functions suggested by the authors in the paper. Although the model will work with other configurations.
We train the model and save the information in a history variable so that we can plot the learning graph afterward. We are reserving 30% of samples for testing (validation_split).
During training, you should see the error in the validation and training sets diminish continually. Here is the plot for my training:
Loss (mse) diminishes along the training
After the training has finished, you can save and download your model. Remember you also need to keep the normalization data to apply it to new data during prediction:
This final step is similar to what I have shown you in a previous tutorial. That is, we’ll be implementing a custom Python DG node to run our model live in Maya. But in this case, we are implementing a deformer, and Maya has a custom class for deformer nodes called MPxGeometryFilter. You should use it over MPxNode for convenience and performance. On the downside, this class is not available through OpenMaya 2, so you’ll have to stick to the old OpenMaya API. Here are the packages you’ll need to load:
This Python DG node has significantly more lines of code than the last example, so I took the liberty of hardcoding some things. If you don’t want to do that, make sure you check the previous tutorial.
Then we implement our normalization functions once again:
Note that here the ‘featNorm’ function does not generate the ‘mean’ and ‘std’ variables but instead receives them as input parameters.
As you have seen in the previous tutorial the first step in creating a custom Python DG node is setting up its attributes. In this case, since we are instantiating the MPxGeometryFilter class, some attributes are given, these are the input-output geometry, and the envelope. The envelope is a multiplier of the deformation effect.
We will add one other attribute, the matrix which we’ll use as input for the Neural Network. Declare it in the init function like this:
Note that in the last line we tell Maya to update the output geometry when the matrix changes the value.
In the MPxGeometryFilter nodes, you do not declare a ‘compute’ function but a ‘deform’ function. The deform function provides an iterator (geom_it) that can be used to iterate over all vertices. We start the deformation by setting up all the attributes we’ll need and check if the number of vertices in the mesh matches the number of output neurons in our network.
Then we get and cache the model’s prediction, so that if the joint’s transformation does not change we don’t re-evaluate it. Note that we normalize the network’s inputs (xfo) and denormalize its outputs (prediction/displacement).
Finally, we trigger the geometry iterator and update the position for every vertex. We have to get the correct x,y, and z values for every vertex and make them regular floats as the network’s outputs are Numpy.floats. Then we compose an MVector that we’ll add to the vertex position.
If you have set everything up properly, and if your ‘3DL_FDDA.py’ file is being loaded by Maya.env (if you don’t know how to do that look up the Python DG node tutorial) you can create your new deformer using maya.cmds.
Load that base model ‘linSkin.ma’ once again, select the mesh to be deformed and run the following code from the Maya Python script editor:
A deformer will be created and connected to the geometry, now plug Joint2’s matrix to the deformer and voila. You should see the magic.
If you have followed this tutorial up to here, congratulations, you have understood and implemented your first SIGGRAPH deep learning paper. ‘Fast and Deep Deformation Approximations’ provides an interesting solution to make character deformations faster and portable. This is a Python prototype implementation, so rest assured it won’t be fast, but all the pieces are there. The deformations look very much like the original and the model generalizes well (try to play around with the joint). While the paper is limited to movement from joint transformations I think you can see that it is not impossible to connect other things to the input and test how the model reacts.
Tell me what you think about this prototype. Were you able to run it properly? Can you think of similar applications that can be dealt with a model like this?