Saturday, December 7, 2013

Motion Blur in Unity Part 2: The Model*View Matrix

The way our motion vectors are typically calculated is that, instead of just projecting a vertex with the model*view*projection matrix, we project it twice - once with the MVP matrix of the current frame, and once with the matrix of the last frame. Then, we calculate the displacement between them.
Unity, however, does not provide the model*view matrix of the last frame. So, we'll need to calculate it ourselves.
On the surface, this sounds easy. You can easily calculate the projection matrix of any camera in Unity, and a model matrix is just basically a local-to-world matrix, right? Yes. Usually. But in Unity, it's not so simple.
In Unity, how the model matrix is calculated depends on a wide array of factors. Whether it's a skinned or non-skinned mesh, whether it's uniform or non-uniformly scaled, whether it's static, whether it's statically batched, etc. The "local-to-world matrix" concept quickly breaks down.
I should note before we continue, that the following cases were discovered through personal trial and error.

Model Matrix Cases

So when is a model matrix just a local-to-world matrix? As it turns out, there is one case when this is true - when the model is not skinned, isn't statically batched, and is uniformly scaled.
In all other cases, it will not be.
So first, what happens if the model is not uniformly scaled? In this case, our model matrix considers the scale of the object to just be (1,1,1).
Also note that we need to recursively multiply scale by each parent.
With this in mind, let's build a function to calculate scale for our matrix.

Vector3 calculateScale()
{
 Vector3 scale = Vector3.one;
 
 // the model is uniformly scaled, so we'll use localScale in the model matrix
 if( transform.localScale.x == Vector3.one * transform.localScale.x )
 {
  scale = transform.localScale;
 }
 
 // recursively multiply scale by each parent up the chain
 Transform parent = transform.parent;
 while( parent != null )
 {
  scale = new Vector3( scale.x * parent.localScale.x, scale.y * parent.localScale.y, scale.z * parent.localScale.z );
  parent = parent.parent;
 }
 return scale;
}
This function starts with a scale value which depends on whether the transform is uniformly or non-uniformly scaled. Then, it recursively multiplies by the scale of each parent.
Let's also start putting together the function to calculate our model matrix.

Matrix4x4 calculateModelMatrix()
{
 return Matrix4x4.TRS( transform.position, transform.rotation, calculateScale() );
}
This now handles non-static, non-skinned meshes.
Now, what about static meshes?
Well, unfortunately here's where we get a bit project-specific. If your project has static batching disabled, then you don't have to worry about this. However, if your project does have static batching enabled, for static meshes Unity actually pre-transforms the geometry and simply uses identity as the model matrix.
I'm going to assume your project has static batching enabled (it is enabled by default in Unity). If it doesn't, you can skip this portion.

Matrix4x4 calculateModelMatrix()
{
 if( renderer.isPartOfStaticBatch )
 {
  return Matrix4x4.identity;
 }
 return Matrix4x4.TRS( transform.position, transform.rotation, calculateScale() );
}
Note that isPartOfStaticBatch does not seem to work as expected. That is, I found it reports true even if static batching is disabled. I do not know whether this is a bug or not.
So now our code handles regular and static meshes. But what about skinned meshes?
This one took me a while to figure out, but eventually I did. Skinned meshes for one act a bit like non-uniformly scaled meshes for one, and for another they do not use the transform the skinned mesh renderer component is attached to. Instead, they use the root bone transform.

Matrix4x4 calculateModelMatrix()
{
 if( renderer is SkinnedMeshRenderer )
 {
  Transform rootBone = ((SkinnedMeshRenderer)renderer).rootBone;
  return Matrix4x4.TRS( rootBone.position, rootBone.rotation, Vector3.one );
 }
 if( renderer.isPartOfStaticBatch )
 {
  return Matrix4x4.identity;
 }
 return Matrix4x4.TRS( transform.position, transform.rotation, calculateScale() );
}
And now, we've finally got an accurate model view matrix for all cases.
So, now that we can calculate the model matrix, let's create a helper script.

The Helper Script

Our script will keep track of the current and last model*view matrices, and will pass the matrix of the last frame to our material. Note that even if our shader does not have the matrix as a property, it will still store the matrix. We'll use this to our advantage when we go to render it with a replacement shader, which will be able to make use of this stored value.
We'll use OnWillRenderObject to do this. Later on, our image effect will create a secondary camera to render the motion vector. We'll call it "MotionVectorCamera" so that our matrix helper script can detect which camera is rendering it (we won't update the matrix if it isn't being rendered by our motion vector camera).

using UnityEngine;
using System.Collections;

public class MotionBlurMatrixHelper : MonoBehaviour
{
 private Matrix4x4 lastModelView = Matrix4x4.identity;
 
 void OnWillRenderObject()
 {
  if( Camera.current.name != "MotionVectorCamera" ) return;
  
  foreach (Material mat in renderer.materials)
  {
   mat.SetMatrix("_Previous_MV", lastModelView);
  }
  
  Matrix4x4 view = Camera.current.worldToCameraMatrix;
  Matrix4x4 model = calculateModelMatrix();
  
  lastModelView = view * model;
 }
 
 Matrix4x4 calculateModelMatrix()
 {
  if( renderer is SkinnedMeshRenderer )
  {
   Transform rootBone = ((SkinnedMeshRenderer)renderer).rootBone;
   return Matrix4x4.TRS( rootBone.position, rootBone.rotation, Vector3.one );
  }
  if( renderer.isPartOfStaticBatch )
  {
   return Matrix4x4.identity;
  }
  return Matrix4x4.TRS( transform.position, transform.rotation, calculateScale() );
 }
 
 Vector3 calculateScale()
 {
  Vector3 scale = Vector3.one;
  
  // the model is uniformly scaled, so we'll use localScale in the model matrix
  if( transform.localScale.x == Vector3.one * transform.localScale.x )
  {
   scale = transform.localScale;
  }
  
  // recursively multiply scale by each parent up the chain
  Transform parent = transform.parent;
  while( parent != null )
  {
   scale = new Vector3( scale.x * parent.localScale.x, scale.y * parent.localScale.y, scale.z * parent.localScale.z );
   parent = parent.parent;
  }
  return scale;
 }
}

Now that our shader has access to the model*view of the previous frame, we can get started on the motion vector shader. In the next post, I will cover creating the replacement shader and using it in an image effect script.

1 comment:

  1. I enjoyed your post very much. Thank you!
    BTW, there is a typo in your CalculateScale function, it should be
    transform.localScale == Vector3.one * transform.localScale.x

    ReplyDelete