本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:
- 书本中句子照抄 + 个人批注
- 项目源码
- 一堆新手会犯的错误
- 潜在的太监断更,有始无终
总之适用于同样开始学习Shader的同学们进行有取舍的参考。
屏幕后处理效果(screen post-processing effects) 是游戏中实现屏幕特效的常见方法。Unity使用渲染纹理和脚本实现屏幕后处理效果。
建立一个基本的屏幕后处理脚本系统
屏幕后处理的原理是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效。
想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而Unity为我们提供了这一接口——OnRenderImage函数 。它的函数声明如下:
Monobehaviour.OnRenderImage(RenderTexture src,RenderTexture dest)
简单易懂的函数,当我们再脚本中声明此函数后,Unity会把当前渲染得到的图像存储在src源渲染纹理中,通过函数中的一系列操作后(该函数内操作是我们自定义的),再把目标渲染纹理存储在dest渲染纹理中,dest最终会被显示到屏幕上。
通常我们使用Graphics.Blit函数 来完成对渲染纹理的处理,它有三种函数声明:
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对应了源纹理,这个参数通常是当前屏幕的渲染纹理或是上一步处理后得到的渲染纹理(RenderTexture是Texture的子类)。dest是目标纹理,会直接渲染到屏幕上,mat是我们使用的材质,这个材质使用的UnityShader会对src画面进行后处理。而Src纹理将会被传递进Shader的_MainTex属性 中,也就是说我们直接在Shader中对_MainTex进行处理即可。参数Pass的默认值为-1,表示将会依次调用Shader内的所有Pass ,否则,只会调用给定索引的Pass。
通常情况下,OnRenderImage函数会在所有的不透明和透明的Pass执行完毕后被调用,以便对场景中的所有游戏对象产生影响。但有时,我们希望在不透明的Pass(即渲染队列小于等于2500的Pass)执行完毕后立即调用OnRenderImage(也就是仅仅对不透明物体进行处理),可以用onRenderImage函数前添加ImageEffectOpaque属性来实现这样的目的。
实现后处理的效果通常如下:
- 在摄像机中添加一个用于屏幕后处理的脚本,在这个脚本中,我们会实现OnRenderImage函数来获取当前屏幕的渲染纹理,然后再调用Graphic.Blit函数使用特定的UnityShader来对当前图像Texture进行处理,再把返回的渲染纹理显示到屏幕上
- 对于一些复杂的屏幕特效,我们可能需要多次调用Blit函数来进行多步骤处理。
在进行屏幕后处理之前,我们需要检查一系列条件是否满足,例如当前平台是否支持渲染纹理和屏幕特效,是否支持当前使用的UnityShader等。为此,我们创建了一个用于屏幕后处理效果的基类,在实现各种屏幕特效时,我们只需要继承自该基类,再实现派生类中不同的操作即可:
Shader:
Shader "Custom/BrightnessSaturationAndContrast_Copy"
{
Properties
{
_MainTex("BaseTexture",2D) = "white"{}
_Brightness("Brightness",Float) = 1
_Saturation("Saturation",Float) = 1
_Contrast("Contrast",Float) = 1
}
SubShader
{
Pass
{
// 该语句是屏幕后处理的标配
// 因为屏幕画面应当是最前方的,因此深度测试应当总是通过
// 关闭背面剔除
// 关闭深度写入以防止它覆盖其他物体渲染
ZTest Always
Cull Off
Zwrite Off
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float _Brightness;
float _Saturation;
float _Contrast;
struct v2f
{
float4 pos : SV_POSITION;
float2 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;
// 在线性颜色空间下的RGB 转为灰度值的心理学公式
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);
// 对比度颜色值
fixed3 avgColor = fixed3(0.5,0.5,0.5);
finalColor = lerp(avgColor,finalColor,_Contrast);
return fixed4(finalColor,renderTex.a);
}
ENDCG
}
}
Fallback Off
}
C#后处理脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
public class CustomBrightnessSaturationAndContrast : CustomPostEffectsBase
{
public Shader BriSatConShader;
private Material m_briSatConMaterial;
public Material BaseMaterial
{
get
{
m_briSatConMaterial = CheckShaderAndCreateMaterial(BriSatConShader, m_briSatConMaterial);
return m_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;
override protected void OnRenderImage (RenderTexture source, RenderTexture destination)
{
if (BaseMaterial != null)
{
BaseMaterial.SetFloat("_Brightness",brightness);
BaseMaterial.SetFloat("_Saturation",saturation);
BaseMaterial.SetFloat("_Contrast",contrast);
Graphics.Blit(source,destination,BaseMaterial);
}
else
{
Graphics.Blit(source,destination);
}
}
}
通过对后处理脚本的变量修改,我们就可以轻松实现材质面板的变量修改,对画面进行后处理
边缘检测
边缘检测的原理是使用边缘检测算子对图像进行卷积(convolution) 操作,至于什么是卷积请看我以往写的一篇文章卷积网络前序——卷积背后的数学原理
什么是卷积
简单的来说,使用卷积我们可用以某个像素为中心的周围像素进行权值计算,并讲处理后的值重新赋值给中心像素。其中这个由权值构成的矩阵被称为卷积核(kenel) 。
通过卷积操作,我们可以对图像进行一系列的处理,如图像模糊,边缘检测,颜色均值等等操作。
常见的边缘检测算子
让我引用卷积网络前序——卷积背后的数学原理中的一段话来描述为什么边缘检测算子可以实现边缘检测:
接下来我们用灰度图来表示要处理的图像,因为RGB需要三维向量,灰度可以用单个值表示。在灰度图中,白色代表1,黑色代表0,因此我们来看上面这个例子,我们可以把矩阵分为三列,第一列为正数权值,第二列为0,第三列为负数权值,是第一列的相反数。
因此我们来看上图这个例子,明显结果是 1 ∗ ( − 0.25 ) + 1 ∗ ( − 0.5 ) + 1 ∗ ( − 0.25 ) = − 1 1*(-0.25)+1*(-0.5)+1*(-0.25)=-1 1∗(−0.25)+1∗(−0.5)+1∗(−0.25)=−1,我们最后的结果用蓝色表示正数,红色表示负数。因此最后的卷积结果是一个代表-1的红色格子。
由于这个卷积的矩阵中间列的值是0,因此中间列不影响计算,它的作用实际上是找出左列和右列灰度值不一样的区块,因此实际上功能相当于对图像中所有竖向方向上产生了颜色变化的色块边界进行描边。
这就是边缘检测算子的基本原理,本质上来说它的数学原理是基于梯度的,所谓边缘指的其实就是左右(或上下)颜色变化较大的像素点,如果将颜色值用函数的数值来表示的话,那么就如下图:
显然颜色值相差大的部分对应的函数值变化很快,而用于描述函数值变化快慢的标准就是函数在该点上的切线角度——也就是函数的梯度,因此对于二维函数(x,y)——即为描述颜色值同时在xy轴上的函数,若对其直接求x轴的偏导,得到的梯度就代表着颜色值在横向上的变换,同理对y轴求偏导代表了在纵向上的变换。
上式其实对应的就是横向的像素值
[
f
(
x
,
y
)
,
f
(
x
+
1
,
y
)
]
[f(x,y),f(x+1,y)]
[f(x,y),f(x+1,y)]乘以了矩阵
[
−
1
0
0
1
]
\begin{bmatrix}-1 & 0\\ 0 & 1\end{bmatrix}
[−1001]。旋转180度后对应的卷积核就是
[
1
0
0
−
1
]
\begin{bmatrix}1 & 0\\ 0 & -1\end{bmatrix}
[100−1]
了解了原理后,我们来看看常见的边缘检测算子:
这几种常见的边缘检测算子包含了两个方向的卷积核,分别用于检测水平和垂直方向上的边缘信息。我们可以单用其中一个卷积核来检测水平或垂直方向上的梯度变换,也可以两个合用并计算开方后的均值(以横向和纵向两个方位的梯度均值来判定边缘)。
实现
后处理脚本:
public class CustomEdgeDetection : CustomPostEffectsBase
{
public Shader EdgeDectecShader;
private Material m_edgeDectecShader;
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;
public Color edgeColor = Color.black;
public Color backgroundColor = Color.white;
public Material BaseMaterial
{
get
{
m_edgeDectecShader = CheckShaderAndCreateMaterial(EdgeDectecShader, m_edgeDectecShader);
return m_edgeDectecShader;
}
}
override protected void OnRenderImage (RenderTexture source, RenderTexture destination)
{
if (BaseMaterial != null)
{
BaseMaterial.SetFloat("_EdgeOnly", edgesOnly);
BaseMaterial.SetColor("_EdgeColor", edgeColor);
BaseMaterial.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(source,destination,BaseMaterial);
}
else
{
Graphics.Blit(source,destination);
}
}
}
Shader
Shader "Custom/EdgeDetectionCopy"
{
Properties
{
_MainTex ("MainTex", 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
ZWrite Off
Cull Off
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
#include "UnityCG.cginc"
sampler2D _MainTex;
// 小坑,变量名定义需要使用XXX_TexelSize来访问对应纹理的纹素
uniform half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f
{
float4 pos : SV_POSITION;
// 该数组用于采样卷积用的像素
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;
}
// 在线性颜色空间下的RGB 转为灰度值的心理学公式
fixed luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
// 应用Sobel卷积
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};
half texColor;
half GradientX = 0;
half GradientY = 0;
for (int index = 0;index<9;index++)
{
texColor = luminance(tex2D(_MainTex,i.uv[index]));
GradientX += texColor * Gx[index];
GradientY += texColor * Gy[index];
}
half edge = 1-abs(GradientX)-abs(GradientY);
return edge;
}
fixed4 frag(v2f i):SV_Target
{
// 获取边缘(Sobel返回结果越<1则越边缘)
half edge = Sobel(i);
//对卷积中心根据卷积值来lerp颜色,edge值越小越接近_EdgeColor,反之越接近原色
fixed4 edgeColorMixRigionColor = lerp(_EdgeColor,tex2D(_MainTex,i.uv[4]),edge);
fixed4 edgeColorMixCustomBGColor = lerp(_EdgeColor,_BackgroundColor,edge);
return lerp(edgeColorMixRigionColor,edgeColorMixCustomBGColor,_EdgeOnly);
}
ENDCG
}
}
FallBack Off
}
最后的结果可以看到,卷积后的图像边缘被我们用黑色进行了描边
高斯模糊
高斯模糊的效果我们在之前的文章中也介绍过了,就是用高斯核(一个符合高斯分布的卷积核)进行计算。
(若将卷积核转化为高度图,可以看到高斯分布)
其中每个元素的计算基于下面的高斯方程:
高斯核具有两个特性:
- 距离卷积中心越近的像素影响更大(符合高斯分布)
- 高斯核越大,则模糊程度越大
假设有一张W*H像素的图像,我们要使用一个N*N的高斯核进行卷积,那么就要经过N*N*W*H次计算。高斯核阶数越大计算越复杂。
不过高斯分布本身存在一个性质,就是 G ( x , y ) = G ( x ) ∗ G ( y ) G(x,y)=G(x) * G(y) G(x,y)=G(x)∗G(y)(不信可以代入之前的公式验算)。之前我在学习机器学习的那节课的时候曾记得两个高斯分布相乘的结果也可以表示为一个高斯分布。
因此该二维的高斯核可以拆分为两个一维的高斯核,甚至两个高斯核是对称的,甚至整个高斯核还是以中心为对称的,我们只需要一个数组就能存储了。
因此,相比于直接应用高斯核计算,我们可以分为两步:先进行横向的一维高斯核计算,再进行纵向的一维高斯核计算。
实现
C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomGaussianBlur : CustomPostEffectsBase
{
public Shader GaussianBlurShader;
private Material m_gaussianBlurMaterial;
public Material BaseMaterial
{
get
{
m_gaussianBlurMaterial = CheckShaderAndCreateMaterial(GaussianBlurShader, m_gaussianBlurMaterial);
return m_gaussianBlurMaterial;
}
}
[Range(0.2f, 3.0f)]
public float BlurSize = 0.6f;
private RenderTexture buffer0;
private RenderTexture buffer1;
override protected void OnRenderImage (RenderTexture source, RenderTexture destination)
{
if (BaseMaterial != null)
{
int rtW = source.width;
int rtH = source.height;
BaseMaterial.SetFloat("_BlurSize", BlurSize);
// 先渲染到buffer0
buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(source, buffer0,BaseMaterial,0);
// 再渲染到buffer1
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, BaseMaterial, 1);
// 最后渲染到屏幕
Graphics.Blit(buffer1, destination);
RenderTexture.ReleaseTemporary(buffer0);
RenderTexture.ReleaseTemporary(buffer1);
} else {
Graphics.Blit(source, destination);
}
}
}
Shader:
Shader "Custom/GaussianBlur_Copy"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
CGINCLUDE
sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;
float _BlurSize;
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0;
};
fixed4 CaculateGaussionKenel(v2f i)
{
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 texColor;
fixed3 finalColor = 0;
for (int index = 0;index <5;index++)
{
texColor = tex2D(_MainTex, i.uv[index]);
finalColor += texColor.rgb * weight[abs(index-2)];
}
return fixed4(finalColor,1.0);
}
fixed4 frag(v2f i):SV_Target
{
fixed4 Blur = CaculateGaussionKenel(i);
return Blur;
}
ENDCG
// vertical
Pass
{
NAME "GAUSSIAN_BLUR_VERTICAL"
ZTest Always
ZWrite Off
Cull Off
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(0, -2) * _BlurSize;
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1) * _BlurSize;
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(0, 0) * _BlurSize;
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(0, 1) * _BlurSize;
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 2) * _BlurSize;
return o;
}
ENDCG
}
// horizon
Pass
{
NAME "GAUSSIAN_BLUR_HORIZONTAL"
ZTest Always
ZWrite Off
Cull Off
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-2, 0) * _BlurSize;
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(-1, 0) * _BlurSize;
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(0, 0) * _BlurSize;
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(1, 0) * _BlurSize;
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(2, 0) * _BlurSize;
return o;
}
ENDCG
}
}
FallBack Off
}
注意,在渲染两个Pass的时候,我们不可以直接调用两次Graphics.Blit(source, dest,BaseMaterial,0);Graphics.Blit(source, dest,BaseMaterial,1);
因为source是那一帧时屏幕截取的RenderTexture,而Dest是目标帧的画面,如果按刚才这样写,那么就会出现第二次渲染把第一次渲染结果覆盖的情况。
正确的操作是:先进行一次渲染,然后把上一次的Destination RenderTexture作为下一次渲染的Source。以此类推将最后一次渲染的Dest作为输出结果。
上述代码都是我自己编写的,实际上书中的代码要更加全面,我这里就暂不贴出,一方面书中的代码提供了下采样系数,可以手动降采样图像的分辨率,减少计算量。另一方面又提供了高斯模糊的迭代代码,对图像模糊效果进行多次迭代,并使用了buffer0和buffer1两个变量来保存上一次Blit的dest和下一次Blit的src,为渲染交替存储RenderTexture。(此外提出一点,不要把变量定义放在每帧调用的代码中,像该例的buffer0和buffer1应当作为全局变量定义。)
Bloom效果
Bloom特效简单描述就是让画面中较亮的区域扩散到周围的区域中,造成一种朦胧的效果。
根据我们本节的学习,你可以想到要如何实现这种效果吗?如何使得周围较亮的区域拓展到该区域边缘的像素,就需要为这部分像素应用一种卷积,这种卷积的效果应当使得该像素的颜色值与周边较亮区域的像素进行混合,并且实现模糊。
因此,实现思路就是:
- 先提取出图像中较亮部分的像素,并将它们存储在一张RenderTexture当中,对这些像素进行高斯模糊处理
- 最后将该部分RenderTexture与原图像进行混合,得到最终的效果。
上代码,其实很简单,最关键的找到亮度区域的步骤,实际上是将整张图像变暗,并与原图进行像素叠加,如此一来,阈值以下的颜色值归为0,阈值以上的被归为亮部。
CS:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomBloom : CustomPostEffectsBase
{
public Shader BloomShader;
private Material m_bloomMaterial;
public Material BaseMaterial
{
get
{
m_bloomMaterial = CheckShaderAndCreateMaterial(BloomShader, m_bloomMaterial);
return m_bloomMaterial;
}
}
private RenderTexture buffer0;
private RenderTexture buffer1;
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
// 判断照明区域的亮度阈值
[Range(0.0f, 4.0f)]
public float luminanceThreshold = 0.6f;
override protected void OnRenderImage (RenderTexture source, RenderTexture destination)
{
if (BaseMaterial != null)
{
BaseMaterial.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = source.width;
int rtH = source.height;
buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
#region Pass0 : 提取较亮区域
// buffer0此时存储亮区
Graphics.Blit(source, buffer0, BaseMaterial, 0);
#endregion
#region Pass1 : 垂直模糊
// buffer1此时存储亮区垂直模糊效果
BaseMaterial.SetFloat("_BlurSize", 1.0f + 1 * blurSpread);
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, BaseMaterial, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
#endregion
#region Pass2 : 水平模糊
// buffer1此时存储亮区垂直 + 水平模糊效果
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, BaseMaterial, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
#endregion
#region Pass3 : 混合两张图像
// buffer0为处理后画面,并用pass3和原画面混合
BaseMaterial.SetTexture ("_Bloom", buffer0);
Graphics.Blit(source,destination,BaseMaterial,3);
RenderTexture.ReleaseTemporary(buffer0);
#endregion
}
else
{
Graphics.Blit(source,destination);
}
}
}
Shader:
Shader "Custom/Bloom_Copy"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
// 由于需要混合两张RenderTexture,因此设置两个2DTex
_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;
half4 _Bloom_TexelSize;
float _LuminanceThreshold;
float _BlurSize;
struct v2f
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vertGetBright(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
// 在线性颜色空间下的RGB 转为灰度值的心理学公式
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
fixed4 fragGetBright(v2f i):SV_Target
{
fixed4 c = tex2D(_MainTex, i.uv);
// 获取较亮区域的方法竟然是使得整张图变暗
// 减去阈值获取暗度图像,阈值以上视为亮部
fixed val = clamp(c - _LuminanceThreshold, 0.0, 1.0);
return c * val;
}
struct v2fBloom {
float4 pos : SV_POSITION;
// 存储了两张uv,_MainTex和_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;
#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
{
// 将暗度图像和原图相加,暗色部分接近0,叠加后颜色变化小,亮色部分接近1,叠加后颜色变化大
// 因此可以实现亮部突出的效果
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
ENDCG
// 获取较亮区域
Pass
{
CGPROGRAM
#pragma vertex vertGetBright
#pragma fragment fragGetBright
ENDCG
}
// 高斯模糊
UsePass "Custom/GaussianBlur_Copy/GAUSSIAN_BLUR_VERTICAL"
UsePass "Custom/GaussianBlur_Copy/GAUSSIAN_BLUR_HORIZONTAL"
// Bloom混合
Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
Fallback Off
}
同样建议看书中的代码,比我写的更规范。
运动模糊
当物体在摄像头内运动的时候,会产生运动模糊的视觉效果,运动模糊的效果可以让物体运动看起来更加丝滑。最暴力的方法,其原理是为一个物体渲染多张连续的图像然后进行混合,这意味着需要在同一帧内渲染多次场景。
另一种方法是使用速度缓存,在该缓存中存储各个像素当前的运动速度,然后用该值来决定模糊的方向和大小,显然这种方法更好
书中使用了第一种方法,不过不是在一帧内渲染多个场景,而是在后处理脚本中保存了之前的渲染结果,并不断将当前的渲染图象叠加到之前的渲染图象中。
C#:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomMotionBlur : CustomPostEffectsBase
{
public Shader MotionBlurShader;
private Material m_motionBlurMaterial = null;
public Material BaseMaterial {
get {
m_motionBlurMaterial = CheckShaderAndCreateMaterial(MotionBlurShader, m_motionBlurMaterial);
return m_motionBlurMaterial;
}
}
[Range(0.0f, 0.9f)]
public float BlurAmount = 0.5f;
// 用于保存上一帧渲染结果的RT
private RenderTexture m_accumulationTexture;
//为了在开启后重新叠加图像(避免关闭脚本之前的画面错误叠加)需要摧毁RT
private void OnDisable()
{
DestroyImmediate(m_accumulationTexture);
}
// 检查保存渲染结果的RT是否可用(为空且尺寸与画面帧相符)
bool IsAccumulationTextureAvailable(RenderTexture source)
{
return (m_accumulationTexture == null
|| m_accumulationTexture.width != source.width
|| m_accumulationTexture.height != source.height);
}
override protected void OnRenderImage (RenderTexture source, RenderTexture destination)
{
if (BaseMaterial != null)
{
if (IsAccumulationTextureAvailable(source))
{
// 若不可用则重建RT并渲染
DestroyImmediate(m_accumulationTexture);
m_accumulationTexture =new RenderTexture(source.width, source.height, 0);
m_accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
Graphics.Blit(source, m_accumulationTexture);
}
//通常在对目标RT再次渲染时,需要先清除之前渲染的内容(例如ReleaseTemporary或者DiscardContents)
//调用下面函数后则不会对未清除内容的RT再渲染时报错(新版本已弃用该函数)
m_accumulationTexture.MarkRestoreExpected();
BaseMaterial.SetFloat("_BlurAmount", 1.0f - BlurAmount);
// 将当前帧画面和之前累加的画面进行shader处理
Graphics.Blit (source, m_accumulationTexture, BaseMaterial);
// 最后渲染到目标帧
Graphics.Blit (m_accumulationTexture, destination);
}
else
{
Graphics.Blit(source,destination);
}
}
}
Shader:
Shader "Custom/MotionBlur_Copy"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BlurAmount ("BlurAmount", Range(0,1)) = 0.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
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;
}
// 只需把传入的图像的颜色值直接渲染叠加到当前帧即可
// 第二帧会叠加第一帧的颜色值,第三帧会叠加第二帧的,而第二帧中包含第一帧
//假设第一帧叠加到第二帧后透明底0.9,则叠加到第三帧后为0.81,以此类推直到接近0为止第一帧就完全不显示了
// 因此每次叠加就像递归一样,_BlurAmount越大,运动模糊效果越明显(当然不能为1,否则直接覆盖了)
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);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
// 处理透明度的Pass,看不出有什么影响
Pass {
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
Fallback Off
}