运动模糊是个经常会用到的效果,常见的实现步骤是:
- 对深度纹理进行采样,取得当前片元的深度信息
- 根据深度信息建立当前片元的NDC空间的坐标curNDCPos
- 把curNDCPos乘以当前VP矩阵的逆矩阵(即View*Projection)-1,得到当前片元的世界空间坐标WorldPos
- 把WorldPos乘以上一帧的VP矩阵(即View*Projection),得到上一帧在裁切空间中的位置 lastClipPos
- 把lastClipPos除以其w分量,得到NDC空间位置lastNDCPos
- 用当前片元NDC空间位置 减去 上一帧NDC空间位置(即 curNDCPos-lastClipPos),得到速度的方向speed
- 沿speed方向进行多次采样,求出平均值作为当前片元的颜色
在Unity中实现运动模糊需要后处理的配合,在后处理代码中需要把 摄像机的depthTextureMode 设置为 DepthTextureMode.Depth(这样在shader中才能使用深度纹理),还要当前VP逆矩阵和上一帧的Vp矩阵传递给shader。
效果图:
C#代码:
using UnityEngine;
public class MotionBlur_CameraMove : MonoBehaviour
{
[Range(0, 0.5f)]
public float BlurSize;
private Material m_mat;
private const string ShaderName = "MJ/PostEffect/MotionBlur_CameraMove";
private Matrix4x4 m_curVP_Inverse; // 当前 VP矩阵的逆矩阵 //
private Matrix4x4 m_lastVP; // 上一帧的Vp矩阵 //
private Camera m_cam;
void Start()
{
Shader shader = Shader.Find(ShaderName);
if (shader == null)
{
enabled = false;
return;
}
m_mat = new Material(shader);
m_cam = Camera.main;
if (m_cam == null)
{
enabled = false;
return;
}
m_cam.depthTextureMode = DepthTextureMode.Depth;
}
void OnRenderImage(RenderTexture srcRT, RenderTexture dstRT)
{
if (m_mat == null || m_cam == null)
{
return;
}
Matrix4x4 curVP = m_cam.projectionMatrix*m_cam.worldToCameraMatrix;
m_curVP_Inverse = curVP.inverse;
m_mat.SetFloat("_BlurSize", BlurSize);
m_mat.SetMatrix("_CurVPInverse", m_curVP_Inverse);
m_mat.SetMatrix("_LastVP", m_lastVP);
Graphics.Blit(srcRT, dstRT, m_mat, 0);
m_lastVP = curVP;
}
}
Shader代码:
Shader "MJ/PostEffect/MotionBlur_CameraMove"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_BlurSize("Blur Size", Range(0, 10)) = 1
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" }
Cull Off
ZWrite Off
ZTest Always
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float2 uv_depth : TEXCOORD1;
};
sampler2D _MainTex;
float2 _MainTex_TexelSize;
float4 _MainTex_ST;
sampler2D _CameraDepthTexture;
uniform float _BlurSize;
uniform float4x4 _CurVPInverse;
uniform float4x4 _LastVP;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv_depth = TRANSFORM_TEX(v.uv, _MainTex);
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
{
o.uv_depth.y = 1-o.uv_depth.y;
}
#endif
return o;
}
float4 frag (v2f i) : SV_Target
{
float2 uv = i.uv;
float depth = tex2D(_CameraDepthTexture, i.uv_depth);
// float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
// depth = Linear01Depth(depth);
float4 curNDCPos = float4(uv.x*2-1, uv.y*2-1, depth*2-1, 1);
float4 worldPos = mul(_CurVPInverse, curNDCPos);
worldPos /= worldPos.w; // 为了确保世界空间坐标的w分量为1 //
// worldPos.w = 1;
float4 lastClipPos = mul(_LastVP, worldPos);
float4 lastNDCPos = lastClipPos/lastClipPos.w; // 一定要除以w分量, 转换到 NDC空间, 然后再做比较 //
float2 speed = (curNDCPos.xy - lastNDCPos.xy)*0.5; // 转到ndc空间做速度计算 //
float4 finalColor = float4(0,0,0,1);
for(int j=0; j<4; j++)
{
float2 tempUV = uv+j*speed*_BlurSize;
finalColor.rgb += tex2D(_MainTex, tempUV).rgb;
}
finalColor *= 0.25;
return finalColor;
}
ENDCG
}
}
Fallback Off
}
根据 [官网文档] (https://docs.unity3d.com/Manual/PostProcessingWritingEffects.html) 中的说明 建议写上一些几句:
Cull Off
ZWrite Off
ZTest Always
[图片上传失败...(image-bd99aa-1544771820108)]
由于后处理shader中使用了一张以上的纹理(_MainTex和_CameraDepthTexture),因此需要手动把uv的y坐标翻转下,以保持两张图uv的y坐标方向保持一致:
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
{
o.uv_depth.y = 1-o.uv_depth.y;
}
#endif
对深度纹理进行采样可以使用 unity自带的方法SAMPLE_DEPTH_TEXTURE 也可以直接对 _CameraDepthTexture 进行采样:
float depth = tex2D(_CameraDepthTexture, i.uv_depth);
或
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
两种方式都能获取到深度值,大部分平台上都可以用直接采样的方式获取深度值,但是一些平台需要做些特殊处理例如PSP2,因此使用 SAMPLE_DEPTH_TEXTURE 方式更安全,因为unity内部对各种宏进行了判断,能确保在不同的平台都能正确地得到深度值。
效果图:
最后感谢冯乐乐大神的书和博客。
package文件
提取码:vpud
参考链接:
: https://docs.unity3d.com/Manual/PostProcessingWritingEffects.html
: https://docs.unity3d.com/Manual/SL-PlatformDifferences.html