屏幕后处理效果
屏幕后处理效果(screen post - processing effects)是游戏中实现屏幕特效的常见方法。
建立一个基本的后处理脚本系统
屏幕后处理,顾名思义,通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效。使用这种技术,可以为游戏画面添加更多艺术效果,例如景深、运动模糊等。
OnRenderImage函数
想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而Unity为我们提供了这样一个方便的接口OnRenderImage函数。它的函数声明如下:
MonoBehaviour.OnRenderImage(RenderTexture src,RenderTexture dest)
Graphics.Blit函数
当我们再脚本中声明此函数后,Unity会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理中,通过函数中的一系列操作后,再把目标渲染纹理,即第二个参数对应的渲染纹理显示到屏幕上。在OnRenderImage函数中,我们通常是利用Graphics.Blit函数来完成对渲染纹理的处理。它有3种函数声明:
public static void Blit(Texture src,RenderTexture dest);
public static void Blit(Texture src,RenderTexture dest,Material mat,int pass = -1);
public static void Blit(Texture src,Material mat,int pass = -1);
参数
- 参数src对应了源纹理,在屏幕后处理技术中,这个参数通常就是当前屏幕的渲染纹理或是上一步处理后得到的渲染纹理。
- 参数dest是目标渲染纹理,如果它的值为null就会直接将结果显示在屏幕上。
- 参数mat是我们使用的材质,这个材质使用的Unity Shader将会进行各种屏幕后处理操作,而src纹理将会被传递给Shader中名为_MainTex的纹理属性。
- 参数pass的默认值为-1,表示将会依次调用Shader内的所有Pass。否则,只会调用给定索引的Pass。
ImageEffectOpaque属性
在默认的情况下,OnRenderImage 函数会在所有的不透明和透明的Pass执行完毕后被调用,以便对场景中所有的游戏对象都产生影响。但有时,我们希望在不透明的Pass(即渲染队列小于等于2500 的Pass,内置的Background、Geometry 和 AlphaTest渲染队列均在此范围内)执行完毕后立即调用OnRenderImage 函数,从而不对透明物体产生任何影响。此时,我们可以在OnRenderImage 函数前添加ImageEffectOpaque 属性来实现这样的目的。
实现过程
因此,要在Unity 中实现屏幕后处理效果,过程通常如下:
- 在进行屏幕后处理之前,我们需要检查一系列条件是否满足(例如当前平台是否支持渲染纹理和屏幕特效,是否支持当前使用的Unity Shader等),为此,我们创建了一个用于屏幕后处理效果的基类,在实现各种屏幕特效时,我们只需要继承自该基类,再实现派生类中不同的操作即可。
- 我们需要再摄像机中添加一个用于屏幕后处理的脚本。在这个脚本中,我们会实现OnRenderImage函数来获取当前屏幕的渲染纹理。
- 然后,再调用Graphics.Blit 函数使用特定的Unity Shader 来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上。对于一些复杂的屏幕特效,我们可能需要多次调用Graphics.Blit 函数来对上一步的输出结果进行下一步处理。
在该节中,我们实现了一个屏后处理基类,用于检查条件是否满足等,后续章节的屏后处理脚本需要继承该基类,详细代码见下文。
调整屏幕的亮度、饱和度和对比度
在上面,我们了解了实现屏幕后处理特效的技术原理。我们现在先来实现一个非常简单的屏幕特效——调整屏幕的亮度、饱和度和对比度。我们的效果图如下所示
实现
- 新建一个脚本,名为BrightnessSaturationAndContrast.cs。添加到摄像机上。
- 修改BrightnessSaturationAndContrast.cs,声明材质与Shader属性,使用CheckShaderAndCreateMaterial创建一个材质。声明亮度、饱和度、对比度属性用于写入Shader中。在OnRenderImage方法中检测材质并写入Shader参数
- 新建一个Unity Shader。在片元着色器中,通过源颜色乘以亮度系数可以调整亮度;然后通过将源颜色乘以对应的系数可以得到亮度值(饱和度为0的颜色),通过插值该亮度值与源颜色即可调账饱和度;创建一个颜色为(0.5,0.5,0.5)的参数可以得到一个对比度为0的颜色,通过插值源颜色与该值可以调节对比度。
- 在编辑器下将Shader拖入前面新建的脚本组件中,即可。
具体代码实现见下文。
边缘检测
边缘检测是描边效果的一种实现方法。原理是利用一些边缘检测算子对图像进行卷积操作。
什么是卷积
在图像处理中,卷积操作指的就是使用一个卷积核对一张图像中的每个像素进行一系列操作。卷积核通常是一个四方形网格结构,该区域内每个网格都有一个权重值。当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,如下图所示,翻转核之后再一次计算核中的每个元素和覆盖的图像像素值的乘积并求和,得到的结果就是该位置的新像素值。
这样的计算过程虽然简单,但可以实现很多常见的图像处理效果,例如图像模糊、边缘检测等。例如,如果我们想要对图像进行均值模糊,可以使用一个3×3的卷积核,核内每个元素的值均为1/9。
常见的边缘检测算子
卷积操作的神奇之处在于选择的卷积核。那么用于边缘检测的卷积核(也被称为边缘检测算)应该长什么样的呢?在回答这个问题之前,我们可以首先回想一下边到底是如何形成的。
如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,我们就会认为它们之间应该有一条边界。这种相邻像素之间的差值可以用梯度来表示,可以想象得到,边缘处的梯度绝对值会比较大。基于这样的理解,有几种不同的边缘检测算子被先后提出来。
3种常见的边缘检测算子如上图所示,它们都包含了两个方向的卷积核,分别用于检测水平方向和竖直方向上的边缘信息。在进行边缘检测时,我们需要对每个像素分别进行一次卷积计算,得到两个方向上的梯度值G(x)和G(y),而整体的梯度可按下面的公式计算而得:
由于上述操作包含了开根号操作,处于性能的考虑,我们有时会使用绝对值操作来代替开根号的操作:
当得到梯度G后,我们就可以据此来判断哪些像素对应了边缘(梯度值越大,越有可能是边缘点)。
实现
- 新建一个脚本,名为EdgeDetectiont.cs添加到摄像机上。该脚本与上一节创建的脚本类似,只是对Shader传入的属性进行了修改(边缘线强度、描边颜色、背景颜色等)
- 创建一个Unity Shader,并对其做以下一些修改
- 定义uniform half4 _MainTex_TexelSize;变量,用于访问主纹理对应的每个纹素值大小。
- 在结构体中定义一个维数为9的纹理数组,对应了Sobel算子需要的9个邻域纹理坐标
- 写一个Sobel函数用于计算像素的梯度值,在该函数中做了以下事情
- 定义卷积核Gx、Gy。
- 依次对9个邻域像素进行采样,并计算他们的亮度值(即饱和度为0的图片,需要使用黑白的图片才好计算边缘)
- 使用亮度值于卷积核中对应的权重相乘,叠加到各自的梯度值上
- 最后使用1减去水平和垂直方向的梯度值的绝对值,得到梯度值edge,该值越小,表明该值越有可能是一个边缘点。
- 在片元着色器中调用Sobel函数计算梯度值,然后使用该值分别计算原图情况下的颜色和纯色下的颜色
- 使用插值得到最终的像素值
具体代码实现见下文
高斯模糊
模糊的实现由很多方法,例如均值模糊和中值模糊。
- 均值模糊同样使用了卷积操作,它使用的卷积核中的各个元素值都相等,且相加等于1,也就是说,卷积后得到的像素值是其领域内各个像素值的平均值。
- 中值模糊则是选择领域内对所有像素排序后的中值替换掉原颜色。
一个更高级的模糊方法是高斯模糊。我们可以得到类似下图的效果。
[图片上传失败...(image-4bdae8-1608604102453)]
20201124182859320.png)
高斯滤波
高斯模糊同样利用了卷积计算,它使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:
其中σ 是标准方差(一般取值为1),x和y分别对应了当前位置到卷积核中心的整数距离。
要构建一个高斯核,我们只需要计算高斯核中各个位置对应的高斯值。为了保证滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化,即让每个权重除以所有权重的和,这样可以保证所有权重的和为1。因此,高斯函数中e的前面的系数实际不会对结果又任何影响。下图显示了一个标准方差为1的5×5大小的高斯核。
高斯方程很好地模拟了邻域每个像素对当前处理像素的影响程度——距离越近,影响越大。高斯核的维数越高,模糊程度越大。使用一个N×N的高斯核对图像进行卷积滤波,就需要N×N×W×H(W和H分别是图像的宽和高)次纹理采样。当N的大小不断增加时,采样次数会变得非常巨大。
简化高斯核
幸运的是,我们可以把这个二维高斯函数拆分成两个一维函数。也就是说,我们可以使用两个一维的高斯核(上图中的右图)先后对图像进行滤波,它们得到的结果和直接使用二维高斯核是一样的,但采样次数只需要2×N×W×H.我们可以进一步观察到,两个一维高斯核中包含了很多重复的权重,对弈一个大小为5的一维高斯核,我们实际只需要记录3个权重即可(0.4026, 0.2442, 0.0545)。
实现
我们将会使用上述5×5的高斯核对原图像进行高斯模糊。我们将先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水平方向的一维高斯核对图像进行滤波,得到最红的目标图像。在实现中,我们还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度。
- 新建一个脚本,名为GaussianBlur.cs。添加到摄像机上。
- 修改该脚本定义三个属性
- iterations(模糊迭代次数——更多的次数意味着更多的模糊)
- blurSpread(模糊范围——值越大意味着模糊越多,模糊程度越高,但过大值会造成虚影)
- downSample(缩放系数——值越大,需要处理的像素数越少,模糊程度越高,但过大值会使图像像素化)
- 在OnRenderImage函数中我们进行以下操作
- 使用参数RenderTexture的大小除以缩放系数,得到降采样纹理大小
- 根据上一步得到的纹理大小创建缓冲区,且设置纹理过滤模式为FilterMode.Bilinear(求平均值)
- 创建循环,循环次数为模糊迭代次数。设置Shader的模糊大小参数为模糊范围,在循环中调用Shader中一个pass渲染垂直通道,调用第二个pass渲染水平通道
- 释放缓冲区
- 创建Shader,定义俩个属性,一个使主纹理,另一个是模糊大小。
- 定义两个顶点着色器代码块,分别获取横向纵向的四个相邻纹理坐标
- 定义一个片元着色器代码块,定义一个高斯权重数组(0.4026, 0.2442, 0.0545),然后使用该权重组分别对不同的相邻坐标进行纹理采样并叠加。
- 定义两个Pass,分别采用横向和纵向顶点着色器代码块,然后使用共用的片元着色器代码块。
- 最后将Shader拖入到相机上GaussianBlur组件的Shader参数中。
具体代码实现见下文。
Bloom效果
Bloom特效是游戏中常见的一种屏幕效果。这种特效可以模拟真实摄像机的一种图像效果,它让画面中较亮的区域“扩散”到周围的区域中,造成一种朦胧的效果。下图给出了这样一种Bloom的效果。
原理
Bloom的实现原理非常简单:我们首先根据一个阈值提取出图像中较亮的区域,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终的效果。
注意:正常情况下,图像的亮度值不会超过1,如果我们开启了HDR,硬件会允许我们把颜色值存储到一个更高的精度范围的缓冲区中。
开启HDR,需要我们在摄像机的组件上勾选Allow HDR开关
实现
- 在摄像机上添加新建脚本Bloom.cs
- Bloom脚本内容与高斯模糊采用的脚本类似,不过有以下几点不同
- 增加了一个新的参数luminanceThreshold,用来控制提取较亮区域时使用的阈值。
- 没有直接对src进行降采样,而是使用Shader中的第一个Pass提取纹理中的较亮区域,然后进行降采样
- 调用Shader中两个模糊Pass对较亮区域进行模糊
- 将模糊后的较亮区域纹理写入到Shader中,并调用Pass中的第四个Pass和原纹理进行合并。
- 新建一个UnityShader,主要做以下几点修改
- 定义一个用于提取较亮区域的Pass,该Pass在片元着色器中通过获取纹理的亮度值,然后用亮度值减去亮度阈值,将结果限制到0-1之间后乘以原像素值得到较亮区域的纹理
- 定义两个Pass,使用UsePass函数调用高斯模糊Shader中的两个模糊Pass
- 定义最后的混合Pass,需要注意该Pass的输入结构体使用的纹理坐标使用的是half4类型的,它包含了原纹理的坐标和Bloom的坐标。
具体实现代码见下文。
运动模糊
运动模糊的实现方法有很多种。
-
一种实现方法是利用一块累积缓存来混合多张连续的图像。当物体快速移动产生多张图像后,我们取它们之间的平均值作为最后的运动模糊图像。
然而,这种暴力的方法对性能的消耗很大,因为想要获取多张帧图像往往意味着我们需要在同一帧里渲染多次场景。
另一种应用广泛的方法是创建和使用速度缓存,这个缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小。
我们使用类似上述第一种方法的实现来模拟运动模糊的效果。我们不需要再一帧中把场景渲染多次,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法与原始的利用累计缓存的方法相比性能更好,但模糊效果可能会略有影响。
实现
- 在摄像机上添加新建脚本MotionBlur.cs
- 在MotionBlur脚本中,我们声明了一个accumulationTexture叠加纹理变量用于叠加之前的渲染结果。
- 在OnRenderImage函数中,我们先对叠加进行条件检测(是否为空,是否与当前屏幕分辨率相等)。如果不满足条件,就重新新建一个叠加纹理,并将它的hideFlags设置为HideFlags.HideAndDontSave(不会显示在Hierarchy中,且不会销毁)。
- 调用accumulationTexture.MarkRestoreExpected();来表明我们需要一个渲染纹理的恢复操作。恢复操作发生在渲染到纹理,而该纹理有没有被提前清空或销毁的情况下(如果不声明Unity会报错)。
- 新建一个UnityShader,并拖动赋值到相机的脚本组件中。
- 在该Shader中,我们实现了两个Pass,一个用于渲染纹理的RGB通道,另一个用于更新A通道。(之所以要分开RGB通道与A通道,是因为在更新RGB时,我们需要设置它的A通道来混合图像,但又不希望A通道的值写入渲染纹理中)
具体代码实现见下文。
实现代码
屏后处理基类
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]// 使其能在编辑器下运行
[RequireComponent(typeof(Camera))]// 绑定脚本所需前置组件
public class PostEffectsBase : MonoBehaviour
{
// 开始时调用
protected void CheckResources()
{
bool isSupported = CheckSupport();
if (isSupported == false)
{
NotSupported();
}
}
// 调用CheckResources来检查对该平台的支持
protected bool CheckSupport()
{
if (SystemInfo.supportsImageEffects == false)
{
Debug.LogWarning("此平台不支持图像效果。");
return false;
}
return true;
}
// 当平台不支持此效果时调用
protected void NotSupported()
{
enabled = false;
}
protected void Start()
{
CheckResources();
}
/// <summary>
/// 当需要创建此效果所使用的材质时调用
/// </summary>
/// <param name="shader">需要使用的Shader</param>
/// <param name="material">用于后期处理的材质</param>
/// <returns></returns>
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
{
if (shader == null)
{
return null;
}
if (shader.isSupported && material && material.shader == shader)
return material;
if (!shader.isSupported)
{
return null;
}
else
{
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
}
调整亮度、饱和度、对比度
using UnityEngine;
using System.Collections;
public class BrightnessSaturationAndContrast : PostEffectsBase// 继承屏后处理基类
{
/// <summary>使用的Shader</summary>
public Shader briSatConShader;
/// <summary>创建的材质</summary>
private Material briSatConMaterial;
public Material material
{
get
{
// CheckShaderAndCreateMaterial:检查着色器并创建材质
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}
// 定义亮度参数
[Range(0.0f, 3.0f)]
public float brightness = 1.0f;
// 定义饱和度参数
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;
// 定义对比度
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;
// 渲染图像
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
// 检测材质是否为空
if (material != null)
{
// 将参数传递给材质
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Brightness Saturation And Contrast"
{
Properties
{
// 基础纹理
_MainTex ("Base (RGB)", 2D) = "white" { }
// 亮度
_Brightness ("Brightness", Float) = 1
// 饱和度
_Saturation ("Saturation", Float) = 1
// 对比度
_Contrast ("Contrast", Float) = 1
}
SubShader
{
Pass
{
// 开启深度测试 关闭剔除 关闭深度写入
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;
struct v2f
{
float4 pos: SV_POSITION;
half2 uv: TEXCOORD0;
};
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i): SV_Target
{
// 纹理采样
fixed4 renderTex = tex2D(_MainTex, i.uv);
// 调整亮度 = 原颜色 * 亮度值
fixed3 finalColor = renderTex.rgb * _Brightness;
// 调整饱和度
// 亮度值(饱和度为0的颜色) = 每个颜色分量 * 特定系数
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
// 插值亮度值和原图
finalColor = lerp(luminanceColor, finalColor, _Saturation);
// 调整对比度
// 对比度为0的颜色
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
Fallback Off
}
边缘检测
using UnityEngine;
using System.Collections;
public class EdgeDetection : PostEffectsBase// 继承屏后处理基类
{
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material
{
get
{
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
/// <summary>边缘线强度</summary>
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;
// 描边颜色
public Color edgeColor = Color.black;
// 背景颜色
public Color backgroundColor = Color.white;
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Edge Detection"
{
Properties
{
// 基础纹理
_MainTex ("Base (RGB)", 2D) = "white" { }
// 边缘线强度
_EdgeOnly ("Edge Only", Float) = 1.0
// 描边颜色
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
// 背景颜色
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment fragSobel
sampler2D _MainTex;
// 用于访问_MainTex纹理对应的每个纹素大小
uniform half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f
{
float4 pos: SV_POSITION;
// 定义了维数为9的纹理数组,对应了Sobel算子采样时需要的9个领域纹理坐标。
half2 uv[9]: TEXCOORD0;
};
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 获取当前纹理坐标周围的9个纹理坐标
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
// 计算亮度值
fixed luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
// 算子计算
half Sobel(v2f i)
{
// 定义垂直于水平方向的卷积核
const half Gx[9] = {
- 1, 0, 1,
- 2, 0, 2,
- 1, 0, 1
};
const half Gy[9] = {
- 1, -2, -1,
0, 0, 0,
1, 2, 1
};
// 对周围的9个
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it ++)
{
// 对像素进行采样后计算亮度值
texColor = luminance(tex2D(_MainTex, i.uv[it]));
// 乘以卷积核种对应的权重
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}
// 使用1减去梯度值的绝对值,结果越小越可能是边缘点
half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
fixed4 fragSobel(v2f i): SV_Target
{
// 通过算子计算当前像素的梯度值
half edge = Sobel(i);
// 背景为原图下的颜色值
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
// 背景为纯色下的颜色值
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
// 插值原图于纯色背景
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
}
}
FallBack Off
}
高斯模糊
using UnityEngine;
using System.Collections;
public class GaussianBlur : PostEffectsBase
{
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;
/// 第一版:只应用模糊
// 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);
// }
// }
/// 第二版:缩放渲染纹理
// 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);
// }
// }
/// 第三版:使用迭代来获得更大的模糊
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
int rtW = src.width / downSample;
int rtH = src.height / downSample;
// 根据屏幕大小创建缓冲区buffer0
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 纹理的过滤模式 FilterMode.Bilinear:对纹理样本求平均值,纹理将变得模糊
buffer0.filterMode = FilterMode.Bilinear;
// 将source存储到buffer0中
Graphics.Blit(src, buffer0);
for (int i = 0; i < iterations; i++)
{
// 设置Shader的模糊强度参数
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
// 第一次创建buffer1
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 渲染垂直通道
// 使用Shader中的第一个Pass对buffer0进行滤波,将结果存储到buffer1
Graphics.Blit(buffer0, buffer1, material, 0);
// 释放buffer0,然后将buffer1中数据重新保存到buffer0中
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
// 第二次创建buffer1
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 渲染水平通道
// 使用Shader中的第二个Pass对buffer0进行滤波,将结果存储到buffer1
Graphics.Blit(buffer0, buffer1, material, 1);
// 释放buffer0,然后将buffer1中数据重新保存到buffer0中
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
// 将buffer0存储到destination中
Graphics.Blit(buffer0, dest);
// 释放缓冲区
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" { }
// 模糊大小
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f
{
float4 pos: SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
// 纵向渲染Pass的顶点着色器代码块
v2f vertBlurVertical(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(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;
}
// 横向渲染Pass的顶点着色器代码块
v2f vertBlurHorizontal(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(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;
}
// 片元着色器代码块(共用)
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);
}
ENDCG
ZTest Always Cull Off ZWrite Off
// 声明Pass1
Pass
{
// 为Pass取名
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
// 指定顶点着色器代码块
#pragma vertex vertBlurVertical
// 指定片元着色器代码块
#pragma fragment fragBlur
ENDCG
}
// 声明Pass2
Pass
{
// 为Pass取名
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
// 指定顶点着色器代码块
#pragma vertex vertBlurHorizontal
// 指定片元着色器代码块
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
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;
}
}
// 模糊迭代次数——更多的次数意味着更多的模糊
[Range(0, 4)]
public int iterations = 3;
// 模糊范围——值越大意味着模糊越多
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
// 缩放系数——值越大,需要处理的像素数越少
[Range(1, 8)]
public int downSample = 2;
// 亮度阈——值越小,Bloom效果越强
[Range(0.0f, 4.0f)]
public float luminanceThreshold = 0.6f;
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = src.width/downSample;
int rtH = src.height/downSample;
// 根据屏幕大小创建缓冲区buffer0
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
// 使用Shader中的第一个Pass提取图像中的较亮区域——泛光纹理。
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);
// 调用Shader中的第四个Pass进行混合
Graphics.Blit (src, dest, material, 3);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, 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 = UnityObjectToClipPos(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);
// 提取像素亮度,减去泛光强度后将结果限制在0-1之间 clamp:将值限制于参数中
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
// 与原像素值相乘
return c * val;
}
struct v2fBloom
{
float4 pos: SV_POSITION;
// 用于存储两个坐标(原图纹理坐标与_Bloom的纹理坐标)
half4 uv: TEXCOORD0;
};
// 泛光的顶点着色器
v2fBloom vertBloom(appdata_img v)
{
v2fBloom o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
// 判断当前平台是否是DirectX类型的平台
#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
{
// 采样原图与_Bloom然后进行叠加
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
ENDCG
ZTest Always Cull Off ZWrite Off
// 用于提取亮部的Pass
Pass
{
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
// 调用高斯模糊垂直渲染Pass
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
// 调用高斯模糊横向渲染Pass
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
// 用于混合亮部分与原图部分的Pass
Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
运动模糊
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;
// 用于保存之前图像叠加的结果
private RenderTexture accumulationTexture;
void OnDisable()
{
// 销毁纹理,为了使下次开始运动模糊时重新叠加图像
DestroyImmediate(accumulationTexture);
}
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);
// 设置该叠加纹理不会显示在层级视图中,且不会被销毁
// HideFlags.HideAndDontSave:该 GameObject 不显示在层级视图中、不保存到场景、也不会被 Resources.UnloadUnusedAssets 卸载。
accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
Graphics.Blit(src, accumulationTexture);
}
// 我们在没有清晰/丢弃的情况下通过帧累积运动
// 根据设计,所以关闭Unity的任何性能警告
accumulationTexture.MarkRestoreExpected();
// 写入模糊次数
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
// 将纹理传递给Shder,并将Shder输出的结果叠加到accumulationTexture中
Graphics.Blit(src, accumulationTexture, material);
// 将结果输出到屏幕
Graphics.Blit(accumulationTexture, dest);
}
else
{
Graphics.Blit(src, 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 = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
// 用于渲染纹理RGB通道的片元着色器
fixed4 fragRGB(v2f i): SV_Target
{
// 渲染纹理的A通道值为模糊次数
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
// 用于渲染纹理A通道的片元着色器
half4 fragA(v2f i): SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass
{
// 混合模式为透明度混合
Blend SrcAlpha OneMinusSrcAlpha
// 通道遮罩 只输出RGB
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
Pass
{
// 混合模式
Blend One Zero
// 通道遮罩 只输出A
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
FallBack Off
}