本次利用速度映射图方法实现运动模糊。速度映射图中存储了每个像素的速度,然后使用这个速度来决定模糊的方向和大小。
速度缓冲生成的方法:
(1)把场景中所有物体的速度渲染到一张纹理中。但这种方法的缺点在于需要修改场景中所有物体的Shader代码,使其添加计算速度的代码并输出到一个渲染纹理中。
(2)利用深度纹理在片元着色器中为每个像素计算其在世界空间下的位置,这是通过使用当前的视角*投影矩阵的逆矩阵对NDC下的顶点坐标进行变换得到的。当得到世界空间中的顶点坐标后,我们使用前一帧的视角*投影矩阵对其进行变换,得到该位置在前一帧中的NDC坐标。然后,我们计算前一帧和当前帧的位置差,生成该像素的速度。这种方法的优点是可以在一个屏幕后处理步骤中完成整个效果的模拟,但缺点是需要在片元着色器中进行两次矩阵乘法的操作,对性能有所影响。
本次利用第二种方法实现运动模糊。
后处理代码:
using UnityEngine;
using System.Collections;
public class MotionBlurWithDepthTexture : PostEffectsBase {
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material {
get {
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
//由于本节需要得到摄像机的视角和投影矩阵,我们需要定义一个Camera类型的变量
//以获取该脚本所在的摄像机组件:
private Camera myCamera;
public Camera camera {
get {
if (myCamera == null) {
myCamera = GetComponent();
}
return myCamera;
}
}
[Range(0.0f, 1.0f)]
public float blurSize = 0.5f; //运动模糊时图像使用的大小
private Matrix4x4 previousViewProjectionMatrix; //保存上一帧摄像机的视角*投影矩阵
void OnEnable() {
camera.depthTextureMode |= DepthTextureMode.Depth; //获取摄像机的深度纹理
previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
}
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_BlurSize", blurSize);
material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
previousViewProjectionMatrix = currentViewProjectionMatrix;
Graphics.Blit (src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
OnRenderImage函数:我们首先需要计算和传递运动模糊使用的各个属性。本例需要使用两个变换矩阵——前一帧的视角 *投影矩阵以及当前帧的视角*投影矩阵的逆矩阵。因此,我们通过调用camera.worldToCameraMatrix和camera.projectionMatrix来分别得到当前摄像机的视角矩阵和投影矩阵。对它们相乘后取逆,得到当前帧的视角*投影矩阵的逆矩阵,并传递给材质。然后,我们把取逆前的结果存储在previousViewProjectionMatrix变量中,以便在下一帧时传递给材质的_PreviousViewProjectionMatrix 属性。
Shader代码:
Shader "Unity Shaders Book/Chapter 13/Motion Blur With Depth Texture" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture; //深度纹理
float4x4 _CurrentViewProjectionInverseMatrix; //当前视角*投影矩阵的逆矩阵
float4x4 _PreviousViewProjectionMatrix; //上一帧视角*投影矩阵
half _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
//平台差异化处理
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
return o;
}
fixed4 frag(v2f i) : SV_Target {
// 对深度纹理进行采样,得到深度值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
// 构建像素的NDC坐标H,同理得到NDC的xy分量,范围均为[-1,1]
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
// Transform by the view-projection inverse.
float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
// Divide by w to get the world position.
float4 worldPos = D / D.w;
// Current viewport position
float4 currentPos = H;
// 前一帧在NDC下的坐标
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
// Convert to nonhomogeneous points [-1,1] by dividing by w.
previousPos /= previousPos.w;
// 前一帧和当前帧在屏幕空间下的位置差,得到当前像素的速度
float2 velocity = (currentPos.xy - previousPos.xy)/2.0f;
float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);
uv += velocity * _BlurSize;
for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
float4 currentColor = tex2D(_MainTex, uv);
c += currentColor;
}
c /= 3;
return fixed4(c.rgb, 1.0);
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
_MainTex对应了输入的渲染纹理,_BlurSize 是模糊图像时使用的参数。我们注意到,虽然在脚本里设置了材质的PreviousViewProjectionMatrix 和CurrentViewProjectionInverseMatrix 属性,但并没有在Properties块中声明它们。这是因为Unity没有提供矩阵类型的属性,但我们仍然可以在CG代码块中定义这些矩阵,并从脚本中设置它们。
由于在本例中,我们需要同时处理多张渲染纹理,因此在DirectX这样的平台.上,我们需要.处理平台差异导致的图像翻转问题。在上面的代码中,我们对深度纹理的采样坐标进行了平台差异化处理,以便在类似DirectX的平台上,在开启了抗锯齿的情况下仍然可以得到正确的结果。
片元着色器:我们首先需要利用深度纹理和当前帧的视角*投影矩阵的逆矩阵来求得该像素在世界空间下的坐标。过程开始于对深度纹理的采样,我们使用内置的SAMPLE DEPTH _TEXTURE宏和纹理坐标对深度纹理进行采样,得到了深度值d。
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
d是由NDC下的坐标映射而来的。我们想要构建像素的NDC坐标H,就需要把这个深度值重新映射回NDC。这个映射很简单,只需要使用原映射的反函数即可,即d*2-1。同样,NDC的xy分量可以由像素的纹理坐标映射而来(NDC下的xyz分量范围均为[-1, 1])。
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
当得到NDC下的坐标H后,我们就可以使用当前帧的视角*投影矩阵的逆矩阵对其进行变换,并把结果值除以它的w分量来得到世界空间下的坐标表示worldPos。
float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
float4 worldPos = D / D.w;
一旦得到了世界空间下的坐标,我们就可以使用前- -帧的视角*投影矩阵对它进行变换,得到前一帧在NDC下的坐标previousPos。
float4 currentPos = H;
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
previousPos /= previousPos.w;
然后,我们计算前一-帧和当 前帧在屏幕空间下的位置差,得到该像素的速度velocity。
float2 velocity = (currentPos.xy - previousPos.xy)/2.0f;
当得到该像素的速度后,我们就可以使用该速度值对它的邻域像素进行采样,相加后取平均值得到一一个模糊的效果。采样时我们还使用了_ BlurSize 来控制采样距离。
本节实现的运动模糊适用于场景静止、摄像机快速运动的情况,这是因为我们在计算时只考虑了摄像机的运动。因此,如果读者把本节中的代码应用到---个物体快速运动而摄像机静止的场景,会发现不会产生任何运动模糊效果。如果我们想要对快速移动的物体产生运动模糊的效果,就需要生成更加精确的速度映射图。读者可以在Unity自带的ImageEffect包中找到更多的运动模糊的实现方法。