Sunday, December 8, 2013

Motion Blur in Unity Part 3: Rendering Motion Vectors

Today we're going to be rendering the motion vector buffer used for motion blur. To do this, we'll set up a special camera to render the scene with a replacement shader.

Image Effect Script

Before we continue, let's create an image effect skeleton.

using UnityEngine;
using System.Collections;

public class MotionBlurEffect : MonoBehaviour
{
 // the shader used to render motion vectors
 public Shader MotionVectorShader;
 
 // the shader used to render motion blur
 // will do so in two passes:
 // first pass: render motion blur
 // second pass: blend half-resolution blurred image with full resolution screen buffer
 public Shader MotionBlurShader;
 
 private Material motionBlurMaterial;
 
 void Awake()
 {
  motionBlurMaterial = new Material( MotionBlurShader );
 }
 
 void OnRenderImage( RenderTexture src, RenderTexture dest )
 {
  Graphics.Blit( src, dest );
 }
}
This sets up the material we will use during Blit, and implements OnRenderImage (at the moment it does nothing, simply blits the source texture directly to the destination).
Next, we'll set up the camera to render motion vectors.

private Camera motionVectorCamera;
 
void Awake()
{
 motionBlurMaterial = new Material( MotionBlurShader );
 
 // set up the motion vector camera
 motionVectorCamera = new GameObject("MotionVectorCamera").AddComponent<Camera>();
 motionVectorCamera.transform.parent = transform;
 motionVectorCamera.transform.localPosition = Vector3.zero;
 motionVectorCamera.transform.localRotation = Quaternion.identity;
        motionVectorCamera.backgroundColor = new Color( 0f, 0f, 0f, 0f );
        motionVectorCamera.renderingPath = RenderingPath.Forward;
// use motion vector replacement shader // we only want to render objects with matching 'RenderType' shader tag motionVectorCamera.SetReplacementShader( MotionVectorShader, "RenderType" ); // disable camera // we'll manually render it when needed motionVectorCamera.enabled = false; }
Let's also add a LayerMask field to this script. This LayerMask will affect which objects are to be motion blurred.

// affects which layers are to be motion blurred (default is everything)
public LayerMask MotionBlurLayers = ~0;

// ...

void Awake()
{
 motionBlurMaterial = new Material( MotionBlurShader );
 
 // set up the motion vector camera
 motionVectorCamera = new GameObject("MotionVectorCamera").AddComponent<Camera>();
 motionVectorCamera.transform.parent = transform;
 motionVectorCamera.transform.localPosition = Vector3.zero;
 motionVectorCamera.transform.localRotation = Quaternion.identity;
 motionVectorCamera.backgroundColor = new Color( 0f, 0f, 0f, 0f );
        motionVectorCamera.renderingPath = RenderingPath.Forward;
 motionVectorCamera.cullingMask = MotionBlurLayers;
 
 // use motion vector replacement shader
 // we only want to render objects with matching 'RenderType' shader tag
 motionVectorCamera.SetReplacementShader( MotionVectorShader, "RenderType" );
 
 // disable camera
 // we'll manually render it when needed
 motionVectorCamera.enabled = false;
}
And finally, let's render this camera to a half-resolution render texture in preparation for motion blur.

void OnRenderImage( RenderTexture src, RenderTexture dest )
{
 // make sure motion vector camera matches current camera
 motionVectorCamera.fieldOfView = camera.fieldOfView;
 motionVectorCamera.nearClipPlane = camera.nearClipPlane;
 motionVectorCamera.farClipPlane = camera.farClipPlane;
 
 // get temporary render texture for motion vectors
 RenderTexture motionVectors = RenderTexture.GetTemporary( src.width / 2, src.height / 2, 0, RenderTextureFormat.ARGBFloat );
 
 // render motion vector camera
 motionVectorCamera.targetTexture = motionVectors;
 motionVectorCamera.Render();
 
 Graphics.Blit( src, dest );
 
 // release temporary render textures
 RenderTexture.ReleaseTemporary( motionVectors );
}
Now that we have much of the framework in place to render our motion blur, let's get started on the actual motion vector shader.

Motion Vector Shader

We'll start with a simple vertex/fragment skeleton for our shader.


Shader "Custom/RenderMotionVector" {
 Properties {
 }
 SubShader {
  Tags { "RenderType"="Opaque" }
  LOD 200
  
  Pass {
  CGPROGRAM
  #pragma vertex vert
  #pragma fragment frag
  
  #include "UnityCG.cginc"

  struct v2f {
   float4 position : SV_POSITION;
  };
  
  v2f vert( appdata_full v )
  {
   v2f o;
   
   o.position = mul( UNITY_MATRIX_MVP, v.vertex);
   
   return o;
  }
  
  float4 frag( v2f i ) : COLOR
  {
   return float4( 1,1,1,1 );
  }
  
  ENDCG
  }
 }
}
This does hardly anything yet, simply outputting White.
Let's add to our v2f structure the current and last positions of the vertex. We'll calculate the last position via the matrix our helper script is passing to the shader.

Shader "Hidden/RenderMotionVector" {
 Properties {
 }
 SubShader {
  Tags { "RenderType"="Opaque" }
  LOD 200
  
  Pass {
  CGPROGRAM
  #pragma vertex vert
  #pragma fragment frag
  
  #include "UnityCG.cginc"
  
  // the model*view matrix of the last frame
  float4x4 _Previous_MV;

  struct v2f {
   float4 position : SV_POSITION;
   float4 curPos : TEXCOORD0;
   float4 lastPos : TEXCOORD1;
  };
  
  v2f vert( appdata_full v )
  {
   v2f o;
   
   o.position = mul( UNITY_MATRIX_MVP, v.vertex);
   o.curPos = o.position;
   o.lastPos = mul( UNITY_MATRIX_P, mul( _Previous_MV, v.vertex ) );
   
   return o;
  }
  
  float4 frag( v2f i ) : COLOR
  {
   return float4( 1,1,1,1 );
  }
  
  ENDCG
  }
 }
}
At first glance, it might seem odd that we're duplicating the 'position' field. This is done because we cannot read from the SV_POSITION semantic. We can, however, read from the TEXCOORD semantic, so we duplicate the position and pass it as TEXCOORD0 (the previous position is passed as TEXCOORD1).
Next, per pixel we will calculate the difference between curPos and lastPos and output this.

float4 frag( v2f i ) : COLOR
{
 float2 a = (i.curPos.xy / i.curPos.w) * 0.5 + 0.5;
 float2 b = (i.lastPos.xy / i.lastPos.w) * 0.5 + 0.5;
 float2 oVelocity = a - b;
 return float4( oVelocity.x, -oVelocity.y, 0, 1 );
}
This is the basis of our motion vector shader. However, remember that our SetReplacementShader call specified the "RenderType" tag. Since this subshader was set to "Opaque", our shader will only replace objects with a shader set to "Opaque". In order to replace other types, you must write more than one subshader for these types. Many of these are simply minor variations. Here is the full shader code with four variations: Opaque, TransparentCutout, TreeBark, and TreeLeaf

Shader "Hidden/RenderMotionVector" {
 Properties {
 }
 SubShader {
  Tags { "RenderType"="Opaque" }
  LOD 200
  
  Pass {
  CGPROGRAM
  #pragma vertex vert
  #pragma fragment frag
  
  #include "UnityCG.cginc"
  
  // the model*view matrix of the last frame
  float4x4 _Previous_MV;

  struct v2f {
   float4 position : SV_POSITION;
   float4 curPos : TEXCOORD0;
   float4 lastPos : TEXCOORD1;
  };
  
  v2f vert( appdata_full v )
  {
   v2f o;

   o.position = mul( UNITY_MATRIX_MVP, v.vertex);
   o.curPos = o.position;
   o.lastPos = mul( UNITY_MATRIX_P, mul( _Previous_MV, v.vertex));
   
   return o;
  }
  
  float4 frag( v2f i ) : COLOR
  {
   float2 a = (i.curPos.xy / i.curPos.w) * 0.5 + 0.5;
   float2 b = (i.lastPos.xy / i.lastPos.w) * 0.5 + 0.5;
   float2 oVelocity = a - b;
   return float4( oVelocity.x, -oVelocity.y, 0, 1 );
  }
  
  ENDCG
  }
 }
 SubShader {
  Tags { "RenderType"="TreeBark" }
  LOD 200
  
  Pass {
  CGPROGRAM
  #pragma vertex vert
  #pragma fragment frag
  
  #include "UnityCG.cginc"
  #include "TerrainEngine.cginc"
  
  // the model*view matrix of the last frame
  float4x4 _Previous_MV;

  struct v2f {
   float4 position : SV_POSITION;
   float4 curPos : TEXCOORD0;
   float4 lastPos : TEXCOORD1;
  };
  
  v2f vert( appdata_full v )
  {
   v2f o;

   v.vertex.xyz *= _Scale.xyz;
   v.vertex = AnimateVertex( v.vertex, v.normal, float4( v.color.xyz, v.texcoord1.xy));
   v.vertex = Squash( v.vertex );

   o.position = mul( UNITY_MATRIX_MVP, v.vertex);
   o.curPos = o.position;
   o.lastPos = mul( UNITY_MATRIX_P, mul( _Previous_MV, v.vertex));
   
   return o;
  }
  
  float4 frag( v2f i ) : COLOR
  {
   float2 a = (i.curPos.xy / i.curPos.w) * 0.5 + 0.5;
   float2 b = (i.lastPos.xy / i.lastPos.w) * 0.5 + 0.5;
   float2 oVelocity = a - b;
   return float4( oVelocity.x, -oVelocity.y, 0, 1 );
  }
  
  ENDCG
  }
 } 
 SubShader {
  Tags { "RenderType"="TransparentCutout" }
  LOD 200
  
  Pass {
  ZWrite Off
  AlphaTest Greater [_Cutoff]
  CGPROGRAM
  #pragma vertex vert
  #pragma fragment frag
  
  #include "UnityCG.cginc"

  sampler2D _MainTex;
  
  // the model*view matrix of the last frame
  float4x4 _Previous_MV;

  struct v2f {
   float4 position : SV_POSITION;
   float4 curPos : TEXCOORD0;
   float4 lastPos : TEXCOORD1;
   float4 tex : TEXCOORD2;
  };
  
  v2f vert( appdata_base v )
  {
   v2f o;
   
   o.position = mul( UNITY_MATRIX_MVP, v.vertex);
   o.curPos = o.position;
   o.lastPos = mul( UNITY_MATRIX_P, mul( _Previous_MV, v.vertex));
   o.tex = v.texcoord;
   
   return o;
  }
  
  float4 frag( v2f i ) : COLOR
  {
   float2 a = (i.curPos.xy / i.curPos.w) * 0.5 + 0.5;
   float2 b = (i.lastPos.xy / i.lastPos.w) * 0.5 + 0.5;
   float2 oVelocity = a - b;
   return float4( oVelocity.x, -oVelocity.y, 0, tex2D( _MainTex, i.tex.xy ).a );
  }
  
  ENDCG
  }
 }
 SubShader {
  Tags { "RenderType"="TreeLeaf" }
  LOD 200
  
  Pass {
  ZWrite Off
  AlphaTest Greater [_Cutoff]
  CGPROGRAM
  #pragma vertex vert
  #pragma fragment frag
  
  #include "UnityCG.cginc"
  #include "TerrainEngine.cginc"

  sampler2D _MainTex;
  
  // the model*view matrix of the last frame
  float4x4 _Previous_MV;

  struct v2f {
   float4 position : SV_POSITION;
   float4 curPos : TEXCOORD0;
   float4 lastPos : TEXCOORD1;
   float4 tex : TEXCOORD2;
  };
  
  v2f vert( appdata_full v )
  {
   v2f o;
   
   ExpandBillboard (UNITY_MATRIX_IT_MV, v.vertex, v.normal, v.tangent);
   v.vertex.xyz *= _Scale.xyz;
   v.vertex = AnimateVertex( v.vertex, v.normal, float4( v.color.xyz, v.texcoord1.xy));
   v.vertex = Squash( v.vertex );

   o.position = mul( UNITY_MATRIX_MVP, v.vertex);
   o.curPos = o.position;
   o.lastPos = mul( UNITY_MATRIX_P, mul( _Previous_MV, v.vertex));
   o.tex = v.texcoord;
   
   return o;
  }
  
  float4 frag( v2f i ) : COLOR
  {
   float2 a = (i.curPos.xy / i.curPos.w) * 0.5 + 0.5;
   float2 b = (i.lastPos.xy / i.lastPos.w) * 0.5 + 0.5;
   float2 oVelocity = a - b;
   return float4( oVelocity.x, -oVelocity.y, 0, tex2D( _MainTex, i.tex.xy ).a );
  }
  
  ENDCG
  }
 }
}
This should support most, if not all, cases.
If you don't really know what's going on in the Tree variations, don't worry. I'm not sure I understand, either :). The vertex shader code was copied over from the builtin tree shaders.

We're done with our replacement shader. As a final step, simply drag the shader onto the Motion Vector Shader field of the image effect.
In the next post, I'll cover creating the two-pass motion blur shader, and hooking it up to our image effect script.

1 comment:

  1. Hello Masaaki,
    I can't find your next post covering the two-pass motion blur shader?
    I'd love to get this working!
    Thanks

    ReplyDelete