12.4 高斯模糊
高斯滤波
高斯模糊同样利用了卷积计算,它使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:
其中,σ是标准方差(一般取值为1),x和y分别对应了当前位置到卷积核中心的整数距离。要构建一个高斯核,我们只需要计算告诉核内各个位置对应的高斯值。为了保证滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化,即让每个权重除以所有权重的和,这样可以保证所有权重的和为1。因此,高斯函数中e前面的系数实际不会对结果有任何影响。下图显示了一个标准方差为1的5×5大小的高斯核。
高斯方程很好地模拟了领域每个像素对当前处理像素的影响程度——距离越近,影响越大。高斯核的维数越高,模糊程度越大。使用一个N×N的高斯核对图像进行卷积滤波,就需要N×N×W×H(W和H分别是图像的宽和高)次纹理采样。当N的大小不断增加时。采样次数会变得非常巨大。于是我们可以把这个二维高斯函数拆分成两个一维函数。也就是说,我们可以使用两个一维的高斯核先后对图像进行滤波,这样的采样次数只需要2×N×W×H。进一步观察到,两个一维高斯核中包含了很多重复的权重。对于一个大小为5的一维高斯核,我们实际只需要记录3个权重值。
我们先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水平方向的一维高斯核对图像进行滤波,得到最终的目标图像。在实现中,我们还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。
实现流程
(1)新建场景(Scene_12_4)。
(2)把Sakural.jpg拖拽到场景中(纹理类型为Sprite)。
(3)新建一个脚本(GaussianBlur.cs),拖拽到摄像机上。
(4)新建Unity SHader(Chapter12-GaussianBlur)
using UnityEngine;
using System.Collections;
//依旧继承12.1节中创建的基类:
public class GaussianBlur : PostEffectsBase {
//指定的Shader,将会实现的Chapter12-GaussianBlur
public Shader gaussianBlurShader;
private Material gaussianBlurMaterial = null;
public Material material {
get {
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
// 模糊迭代——更大的数字意味着更多的模糊
[Range(0, 4)]
public int iterations = 3;
// 每个迭代的模糊扩展——更大的值意味着更多的模糊
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
[Range(1, 8)]
public int downSample = 2;
//blurSpread和downSample都是出于性能的考虑。在高斯核维数不变的情况下,_BlurSize越大,模糊程度
//越高,但采样数却不会受到影响。但过大的_BlurSize值会造成虚影,这并不是我们希望的。而downSample越大,
//需要处理的像素越少,同时也能进一步提高模糊程度,但过大的downSample可能会使图像像素化。
/// 第1版:应用模糊
// void OnRenderImage(RenderTexture src, RenderTexture dest) {
// if (material != null) {
// int rtW = src.width;
// int rtH = src.height;
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//
// // Render the vertical pass
// Graphics.Blit(src, buffer, material, 0);
// // Render the horizontal pass
// Graphics.Blit(buffer, dest, material, 1);
//
// RenderTexture.ReleaseTemporary(buffer);
// } else {
// Graphics.Blit(src, dest);
// }
// }
///与上两节的实现不同,我们这里利用RenderTexture.GetTemporary函数分配了一块与屏幕图像大小
///相同的缓冲区。这是因为,高斯模糊需要调用两个Pass,我们需要使用一块中间缓存来存储第一个Pass执行
///完毕后得到的模糊结果。如代码所示,我们首先调用Graphics.Blit(src, buffer, material, 0);,使用
///Shader中的第一个Pass(即使用竖直方向的一维高斯核进行滤波)对src进行处理,并将结果存储在了
///buffer中。然后再调用Graphics.Blit(buffer, dest, material, 1),使用Shader中的第二个Pass(即
///使用水平方向的一维高斯核进行滤波)对buffer进行处理,返回最终的屏幕图像。最后,我们还需要
///调用RenderTexture.ReleaseTemporary来释放之前分配的缓存。
/// 第二版:缩放渲染纹理
// void OnRenderImage (RenderTexture src, RenderTexture dest) {
// if (material != null) {
// int rtW = src.width/downSample;
// int rtH = src.height/downSample;
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
// buffer.filterMode = FilterMode.Bilinear;
//
// // Render the vertical pass
// Graphics.Blit(src, buffer, material, 0);
// // Render the horizontal pass
// Graphics.Blit(buffer, dest, material, 1);
//
// RenderTexture.ReleaseTemporary(buffer);
// } else {
// Graphics.Blit(src, dest);
// }
// }
///将利用缩放对图像进行降采样,从而减少需要处理的像素个数,提高性能。
///与第一个版本代码不同,我们在声明缓冲区的大小时,使用了小于原屏幕分辨率的尺寸,并将该临时
///渲染纹理的滤波模式设置为双线性。这样,在调用第一个Pass时,我们需要处理的像素个数就是原来
///的几分之一。对图像进行降采样不仅可以减少需要处理的像素个数,提高性能,而且适当的降采样往往还可以
///得到更好的模糊效果。尽管downSample值越大,性能越好,但过大的downSample可能会造成图像像素化。
/// 第三版:为更大的模糊使用迭代
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0);
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 呈现垂直传递
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 呈现水平通过
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
///上面的代码显示了如何利用两个临时缓存在跌代之间进行交替的过程。在跌代开始前,我们首先定义了第一个缓存
///buffer0,并把src中的图像缩放后存储到buffer0中。在跌代过程中,我们又定义了第二个缓存buffer1.在
///执行第一个Pass时,输入是buffer0,输出是buffer1,完毕后首先把buffer0释放,再把结果值buffer1
///存储到buffer0中,重新分配buffer1,然后再调用第二个Pass,重复上述过程。迭代完成后,buffer0将存储最终的
///图像,我们再利用 Graphics.Blit(buffer0,dest)把结果显示到屏幕上,并释放缓存。
}
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
Properties {
//_MainTex对应了输入的渲染纹理。
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
//在本节中,我们使用CGINCLUDE来组织代码。
//这些代码不需要包含在任何Pass语义快中,在使用时,我们只需要在Pass中直接指定需要使用的顶点
//着色器和片元着色器函数名即可。CGINCLUDE类似于C++中头文件的功能。由于高斯迷糊需要定义两个Pass,
//但他们使用的片元着色器代码是完全相同的,使用CGINCLUDE可以避免我们编写两个完全一样的frag函数
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
//由于要得到相邻像素的纹理坐标,我们这里再一次使用了Unity提供的_MainTex_TexelSize变量,以计算
//相邻像素的纹理坐标偏移量。
//分别定义两个Pass使用的顶点着色器。下面是竖直方向的顶点着色器代码:
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
//本节中我们利用5×5大小的高斯核对原图像进行高斯模糊,而由12.4.1节可知,一个5×5的二维高斯核可以
//拆分成两个大小为5的一维高斯核,因此我们只需要计算5个纹理坐标即可。为此,我们在v2f结构体中定义了
//一个5维的纹理坐标数组。数组的第一个坐标存储了当前的采样纹理,而剩余的四个坐标则是高斯模糊中
//对领域采样时使用的纹理坐标。我们还和属性_BlurSize相乘来控制采样距离。在高斯核维数不变的情况下,
//_BlurSize越大,模糊程度越高,但采样数却不会受到影响。但过大的_BlurSize值会造成虚影,这是我们要注意的。通过把
//计算采样纹理坐标的代码从片元着色器中转移到顶点着色器中,可以减少运算,提高性能。由于从顶点
//着色器到片元着色器的插值是线性的,因此这样的转移并不会影响纹理坐标的计算结果。
v2f vertBlurHorizontal(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
//水平方向的顶点着色器和上面的代码类似,只是在计算4个纹理坐标时使用了水平方向的
//纹素大小进行纹理偏移。
//定义两个Pass共用的片元着色器
fixed4 fragBlur(v2f i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
//一个5×5的二维高斯核可以拆分成两个大小为5的一维高斯核,并且由于它的对称性,我们
//只需要记录3个高斯权重,也就是代码中的weight变量。我们首先声明了各个领域像素对应的权重
//weight,然后将结果值sum初始化为当前的像素值乘以它的权重值。根据对称性,我们进行了
//两次迭代,每次迭代包含了两次纹理采样,并把像素值和权重相乘后的结果叠加到sum中。最后,函数返回滤波结果sum
ENDCG
//然后,定义高斯模糊使用的两个Pass
ZTest Always Cull Off ZWrite Off
Pass {
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
//注意,我们仍然首先设置了渲染状态。和之前实现不同的是,我们为两个Pass使用NAME语义(3.3.3)
//定义了他们的名字。这是因为,高斯模糊是非常常见的图像处理操作,很对屏幕特效都是奖励在它的
//基础上的,例如Bloom效果(12.5)。为Pass定义名字,可以在其他Shader中直接通过他们的
//名字来使用该Pass,而不需要再重复编写代码。
}
FallBack "Diffuse"
}
12.5 Bloom效果
这种特效可以模拟真实摄像机的一种图像效果,它让画面中较亮的区域“扩散”到周围的区域中,造成一种朦胧的效果。
Bloom的实现原理非常简单:我们首先根据一个阀值提取出图像中的较亮区域,把他们存储在一张渲染纹理中,再利用高斯模式对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终的效果。
(1)新建场景(Scene_12_5)。
(2)Sakura1.jpg拖拽到场景中。
(3)新建一个脚本(Bloom.cs)。将该脚本拖拽到摄像机上。
(4)新建一个Unity Shader(Chapter12-Bloom)。
using UnityEngine;
using System.Collections;
public class Bloom : PostEffectsBase {
public Shader bloomShader;
private Material bloomMaterial = null;
public Material material {
get {
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
//bloomShader是我们指定的Shader,对应了后面将会实现的Chapter12-Bloom。
//由于Bloom效果是建立在高斯模糊的基础上的,因此脚本中提供的参数和12.4节中的几乎完全一样,
//我们只增加了一个新的参数luminanceThreshold来控制提取较亮区域时使用的阀值大小。
// 模糊迭代——更大的数字意味着更多的模糊。
[Range(0, 4)]
public int iterations = 3;
// 每个迭代的模糊扩展——更大的值意味着更多的模糊
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
[Range(1, 8)]
public int downSample = 2;
[Range(0.0f, 4.0f)]
public float luminanceThreshold = 0.6f;
//尽管在绝大多数情况下,图像的亮度值不会超过1。但如果我们开启了HDR(18.4.3),硬件会允许我们把
//颜色值存储在一个更高精度的缓冲中,此时像素的亮度值可能会超过1。因此,在这里我们把
//luminanceThreshold的值规定在【0,4】范围内。
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0, material, 0);
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 呈现垂直传递
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 呈现水平传递
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
material.SetTexture ("_Bloom", buffer0);
Graphics.Blit (src, dest, material, 3);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
//代码和高斯模糊使用的基本相同,但进行了一些修改。Bloom效果需要3个步骤:首先,提供图像中国
//较亮的区域,因此我们没有像12.4节那样直接对src进行降采样,而是通过调用Graphics.Blit(src, buffer0, material, 0);;
//来使用Shader中的第一个Pass提供图像中的较亮区域,提取得到的较亮区域将存储在buffer0中。然后,我们进行
//和12.4节中完全一样的高斯模糊迭代处理,这些Pass对应了Shader的第二个和第三个Pass。模糊后的较亮区域仅会
//存储在buffer0中,此时,我们在把buffer0传递给材质中的_Bloom纹理属性,并调用Graphics.Blit(src, dest, material, 3)
//使用Shader中的第四个Pass来进行最后的混合,将结果存储在目标渲染纹理dest中。最后,释放临时缓存。
}
Shader "Unity Shaders Book/Chapter 12/Bloom" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
//高斯模糊后的较亮区域
_Bloom ("Bloom (RGB)", 2D) = "black" {}
//用于提取较亮区域使用的阀值
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5
//用于控制不同迭代之间高斯模糊的模糊区域范围
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
//首先定义提取较亮区域需要使用的顶点着色器和片元着色器
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vertExtractBright(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
return o;
}
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
fixed4 fragExtractBright(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
return c * val;
}
//顶点着色器和之前的实现完全相同。在片元着色器中,我们将采样得到的亮度值减去阀值_LuminanceThreshold
//,并把结果截取0~1范围内。然后,我们把该值和原像素值相乘,得到提取后的亮部区域。
//然后,我们定义了混合亮部图像和原图像时使用的顶点着色器和片元着色器:
struct v2fBloom {
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
v2fBloom vertBloom(appdata_img v) {
v2fBloom o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
fixed4 fragBloom(v2fBloom i) : SV_Target {
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
//这里使用的顶点着色器与之前的有所不同,我们定义了两个纹理坐标,并存储在同一个类型为half4的
//变量uv中。它的xy分量对应了_MainTex,即原图像的纹理坐标。而它的zw分量是_Bloom,即模糊后的较亮区域和纹理
//坐标。我们需要对这个纹理坐标进行平台差异化处理(5.6.1)。
//片元着色器的代码就很简单了。我们只需要把两张纹理的采样结果相加混合即可。
ENDCG
//接着,我们定义了Bloom效果需要四个Pass
ZTest Always Cull Off ZWrite Off
Pass {
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
//其中,第二个和第三个Pass我们直接使用了12.4节高斯模糊中定于的两个Pass,这是通过UsePass语义
//指明他们的Pass名实现的。需要主席的是,由于Unity内部会把所有的Pass的Name转换的成大写字母表示,因此在使用
//UsePass命令时沃我们必须使用大写形式的名字。
}
FallBack Off
}
12.6 运动模糊
运动模糊真实世界中的摄像机的一种效果。如果在摄像机曝光时,拍摄场景发生了变化,就会产生模糊的画面。但在计算机产生的图像中,不存在曝光这一物理现象。
运动模糊的实现方法有很多种。一种实现方法是利用一块累积缓存(accumulation buffer)来混合多张连续的图像。当物体快速移动产生多张图像后,我们取它们之间的平均值作为最后的运动模糊图像。然而,这种暴力的方法对性能的消耗很大,因为想要获取多张帧图像往往意味着我们需要在同一帧里渲染多次场景。另一种应用广泛的方法是创建和使用速度缓存(velocity buffer),这个缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小。
我们使用类似上述第一种方法的实现来模拟运动模糊的效果。我们不需要再一帧中把场景渲染多次,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法与原始的利用累计缓存的方法相比性能更好,但模糊效果可能会略有影响。
为此,我们需要做如下准备工作。
1)新建一个场景,去掉天空盒子
2)我们需要搭建一个测试运动模糊的场景。我们构建了一个包含3面墙的房间,并放置了4个立方体。并让立方体不停运动。
3)在摄像机上新建脚本MotionBlur.cs
4)新建一个Unity Shader。
using UnityEngine;
using System.Collections;
public class MotionBlur : PostEffectsBase {
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material {
get {
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
//定于运动模糊在混合图像时使用的模糊参数
[Range(0.0f, 0.9f)]
public float blurAmount = 0.5f;
//blurAmount值越大,运动拖尾的效果就越明显,为了防止拖尾效果完全替代当前帧的渲染结果,我们把它的值截取在0.0~0.9范围内。
//定义一个RenderTexture类型的变量,保存之前图像叠加的结果:
private RenderTexture accumulationTexture;
void OnDisable() {
DestroyImmediate(accumulationTexture);
}
//在上面的代码里,我们在该脚本不运行时,即调用OnDisable函数时,立即销毁accumulationTexture。
//这是因为,我们希望在下一次开始应用运动模糊时重新叠加图像。
//定于运动模糊使用的OnRenderImage函数。
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
// 创建纹理积累
if (accumulationTexture == null || accumulationTexture.width != src.width || accumulationTexture.height != src.height) {
DestroyImmediate(accumulationTexture);
accumulationTexture = new RenderTexture(src.width, src.height, 0);
accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
Graphics.Blit(src, accumulationTexture);
}
//在确认材质可用后,我们首先判断用于混合图像的accumulationTexture是否满足条件。我们不仅
//判断他是否为空,还判断它是否与当前的屏幕分辨率相等,如果不满足,就说明我们需要重新创建一个
//适合于当前分辨率的accumulationTexture变量。创建完毕后,由于我们会自己控制该变量的销毁,
//因此可以把它的hideFlash设置为HideFlash.HideAndDontSave,这意味着这个变量不会显示在Hierarchy中,
//也不会保存到场景中。然后,我们使用当前的帧图像初始化accumulationTexture(Graphics.Blit(src,accumulationTexture))
// 我们在没有清除/丢弃的帧上积累运动
// 通过设计,使任何来自Unity的性能警告都保持沉默
accumulationTexture.MarkRestoreExpected();
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
Graphics.Blit (src, accumulationTexture, material);
Graphics.Blit (accumulationTexture, dest);
} else {
Graphics.Blit(src, dest);
}
//当得到了有效的accumulationTexture变量后,我们调用了accumulationTexture.MarkRestoreExpected()
//函数来表明我们需要进行一个渲染纹理的回复操作。恢复操作(restore operation)发生在渲染到
//纹理而该纹理又没有被提前清空或销毁的情况下。在本例中,我们每次调用OnRenderImage时都需要
//把当前的帧图像和accumulationTexture中的图像混合,accumulationTexture纹理不需要提前清空,因为
//它保存了我们之前的混合结果。然后,我们将参数传递给材质,并调用Graphics.Blit(src, accumulationTexture, material)
//把当前的屏幕图像src叠加到accumulationTexture中。最后使用Graphics.Blit (accumulationTexture, dest);把结果显示到屏幕上。
}
}
Shader "Unity Shaders Book/Chapter 12/Motion Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
//混合图像时使用的混合系数。
_BlurAmount ("Blur Amount", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed _BlurAmount;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
return o;
}
//定义两个片元着色器,一个用于更新渲染纹理的RGB通道部分,第一个用于更新渲染纹理的a通道部分
fixed4 fragRGB (v2f i) : SV_Target {
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
half4 fragA (v2f i) : SV_Target {
return tex2D(_MainTex, i.uv);
}
//RGB通道版本的Shader对当前图像进行采样,并将其A通道的值设为_BlurAmount,以便在后面混合时
//可以使用它的透明通道进行混合。A通道版本的到代码就更简单了,直接返回采样结果。实际上,这个版本
//只是为了维护渲染纹理的透明通道值,不让其受到混合时使用的透明度值的影响。
ENDCG
//然后,我们定义了运动模糊所需的Pass。在本例中我们需要两个Pass,一个用于更新渲染纹理的
//RGB通道,第一个用于更新A通道。之所以要把A通道和RGB通道分开,是因为在更新RGB是我么n。
//需要设置它的A通道来混合图像,但又不希望A通道的值写入渲染纹理中。
ZTest Always Cull Off ZWrite Off
Pass {
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
Pass {
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
FallBack Off
}
12.7 扩展阅读
https://docs.unity3d.com/Manual/comp-ImageEffects.html
《GPU Gems》系列(https://developer.nvidia.com/gpugems/GPUGems/gpugems_pref01.html)中,介绍了许多基于图像处理的渲染技术。例如27章就就介绍了一种景深效果的实现方法。