Chapter12 屏幕后处理效果
一、屏幕后处理概述以及基本脚本系统 1.OnRenderImage 函数 —— 获取屏幕图像 2.Graphics.Blit 函数 —— 使用特定的Shader处理 3.在Unity中实现屏幕后处理的基本流程 4.屏幕后处理基类
二、调整亮度、饱和度和对比度 1.BrightnessSaturationAndContrast.cs 挂载在摄像机上 2.BrightnessSaturationAndContrastShader
三、边缘检测 1.卷积 2.EdgeDetection.cs 挂载在摄像机上 3.EdgeDetectionShader
四、高斯模糊 1.高斯滤波 2.GaussianBlur.cs 挂载在摄像机上 3.GaussianBlurShader
五、Bloom效果
六、运动模糊 1.MotionBlur.cs 2.MotionBlurShader
一、屏幕后处理概述以及基本脚本系统
概念:在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效
1.OnRenderImage 函数 —— 获取屏幕图像
OnRenderImage ( RenderTexture src, RenderTexture dest)
会把当前渲染得到的图像存储在第一个参数对应的原渲染纹理中 通过函数中一系列操作后 再把目标渲染纹理,即第二个参数对应的渲染纹理显示到屏幕上
2.Graphics.Blit 函数 —— 使用特定的Shader处理
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:源纹理(当前屏幕纹理或者上一步处理后得到的渲染纹理),会传递给Shader中的 _MainTex 纹理 dest:目标渲染纹理(如果值为null,就会直接渲染在屏幕上) mat:使用的材质,这个材质使用的Shader将会进行各种屏幕后处理 pass:默认值为-1,表示会依次调用Shader内所有Pass,反之就会调用给定索引的Pass
3.在Unity中实现屏幕后处理的基本流程
需要先在摄像中添加一个用于屏幕后处理的脚本 在此脚本中会实现 OnRenderImage 函数来获取当前屏幕图像 再调用Graphic.Blit 函数使用特定的Unity Shader来对图像进行处理(可以多次调用Blit) 再把返回的渲染纹理显示到屏幕上
4.屏幕后处理基类
PostEffectBase.cs 提供基础功能,包括资源检查、Shader 检查和材质创建等
protected void CheckResources ( ) {
bool isSupported = CheckSupport ( ) ;
if ( isSupported == false ) {
NotSupported ( ) ;
}
}
CheckResources() 检查各种资源和条件是否满足
protected bool CheckSupport ( ) {
if ( SystemInfo. supportsImageEffects == false || SystemInfo. supportsRenderTextures == false ) {
Debug. LogWarning ( "This platform does not support image effects or render textures." ) ;
return false ;
}
return true ;
}
protected void NotSupported ( ) {
enabled = false ;
}
CheckSupport() 检查平台是否支持图像效果和渲染纹理
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 ;
}
}
CheckShaderAndCreateMaterial(Shader shader, Material material) 指定一个Shader来创建一个用于处理渲染纹理的材质
二、调整亮度、饱和度和对比度
一个非常简单的屏幕特效——调整屏幕的亮度、饱和度和对比度
1.BrightnessSaturationAndContrast.cs 挂载在摄像机上
public class BrightnessSaturationAndContrast : PostEffectsBase
public Shader briSatConShader;
private Material briSatConMaterial;
public Material material
{
get
{
briSatConMaterial = CheckShaderAndCreateMaterial ( briSatConShader, briSatConMaterial) ;
return briSatConMaterial;
}
}
声明该效果需要的Shader —— briSatConShader,并据此创建相应的材质 —— briSatConMaterial material 的get函数调用了基类的 CheckShaderAndCreateMaterial 函数来得到对应的材质
[ 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 ;
private 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) ;
}
}
OnRenderImage 函数调用时,会检查材质是否可用,如果可用就把参数传递给材质,再调用Blit函数处理,反之,直接把原图像显示到图像上
2.BrightnessSaturationAndContrastShader
Properties {
_MainTex ( "Base (RGB)" , 2 D) = "white" { }
_Brightness ( "Brightness" , Float) = 1
_Saturation ( "Saturation" , Float) = 1
_Contrast ( "Contrast" , Float) = 1
}
SubShader
{
Pass{
ZTest Always Cull Off ZWrite Off
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;
}
定义顶点着色器,只需要进行必要的顶点转换 更重要的是把纹理坐标传递给片元着色器 使用了appdata_img 结构体作为顶点着色器的输入——只包含了图像处理时必须的顶点坐标和纹理坐标等变量
fixed4 frag ( v2f i) : SV_Target{
fixed4 renderTex = tex2D ( _MainTex, i. uv) ;
fixed3 finalColor = renderTex. rgb * _Brightness;
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) ;
}
首先对原屏幕图像(存储在_MainTex)的采样结果renderTex 再进行各个属性处理
三、边缘检测
1.卷积
使用一个卷积核(也称为边缘检测算子)对图像中的每个像素进行一系列计算,得到新的像素值 先将卷积核水平竖直翻转,再依次计算核之中每个元素和其覆盖的像素值的乘积,再求和,最后得到中心像素值 以上常用的卷积核都包含两个方向,分别用于水平方向和竖直方向上的边缘信息 我们需要对每个像素进行一次卷积计算,得到两个方向上的梯度值
G
x
G_{x}
G x 和
G
y
G_{y}
G y ,再计算得到整体的
G
x
2
+
G
y
2
\sqrt{G_{x}^2 + G_{y}^2}
G x 2 + G y 2
(出于性能考虑,有时候会用绝对值来代替
G
=
∣
G
x
∣
+
∣
G
y
∣
G = |G_{x}| + |G_{y}|
G = ∣ G x ∣ + ∣ G y ∣ ) 梯度值G大的越有可能是边缘点
2.EdgeDetection.cs 挂载在摄像机上
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null ;
public Material material
{
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial ( edgeDetectShader, edgeDetectMaterial) ;
return edgeDetectMaterial;
}
}
[ Range ( 0.0f , 1.0f ) ]
public float edgeOnly = 0.0f ;
public Color edgeColor = Color. black;
public Color backgroundColor = Color. white;
edgeOnly为0时,边缘会叠加在原渲染图像上;为1时不显示源渲染图像
private void OnRenderImage ( RenderTexture src, RenderTexture dest)
{
if ( material != null)
{
material. SetFloat ( "_EdgeOnly" , edgeOnly) ;
material. SetColor ( "_EdgeColor" , edgeColor) ;
material. SetColor ( "_BackgroundColor" , backgroundColor) ;
Graphics. Blit ( src, dest, material) ;
}
else
{
Graphics. Blit ( src, dest) ;
}
}
3.EdgeDetectionShader
Properties {
_MainTex ( "Base (RGB)" , 2 D) = "white" { }
_EdgeOnly ( "Edge Only" , Float) = 1.0
_EdgeColor ( "Edge Color" , Color) = ( 0 , 0 , 0 , 1 )
_BackgroundColor ( "Background Color" , Color) = ( 1 , 1 , 1 , 1 )
}
sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
_MainTex_TexelSize 可以提供访问_MainTex纹理对应的每个纹素的大小(比如一张512×512的图像大小就为 1/512 由于卷积需要对相邻区域内的像素进行采样,因此需要利用纹素大小来计算各个相邻区域的纹理坐标
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;
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;
}
在v2f中定义了9维数组,对应了sobel算子采样时需要的9个邻域纹理坐标 把计算采样纹理坐标的代码,从片元着色器移到顶点着色器中,可以减少运算,提高性能(不会影响结果)
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 } ;
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] ;
}
half edge = 1 - abs ( edgeX) - abs ( edgeY) ;
return edge;
}
Sobel函数利用Sobel算子对原图进行边缘检测 首先定义了水平方向和竖直方向使用的卷积核
G
x
G_{x}
G x 和
G
y
G_{y}
G y 再对9个像素进行依次采样,计算亮度值 再与卷积核
G
x
G_{x}
G x 和
G
y
G_{y}
G y 对应的权重相乘后,叠加到各自的梯度值上 最后用1减去水平方向和竖直方向的梯度值绝对值,得到edge,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) ;
}
四、高斯模糊
1.高斯滤波
同样使用卷积计算,其中每个元素都是基于下面的高斯方程计算
其中σ是标准方差(一般取为1) x和y分别对应了当前位置到卷积核中心的整数距离 要构建一个高斯核,只需要计算高斯核中各个位置的高斯值 为了保证变化后不会变暗,要将高斯核中的权重归一化(每个权重除以权重和) 当使用一个NxN的高斯核进行卷积滤波时,需要NxNxWxH(W和H为图像的宽和高)次纹理采样,当N大小不断增大时,采样次数会非常大。所以可以用两个一维的高斯核先后对图像进行滤波(见上图) ,采样次数只需要2xNxWxH(先后两个Pass,第一个Pass使用竖直方向的高斯核进行滤波。第二个使用水平方向的高斯核进行滤波)
2.GaussianBlur.cs 挂载在摄像机上
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越大,需要处理的像素数越少,也能提高模糊程度,但过大会让图像像素化
void OnRenderImage ( RenderTexture src, RenderTexture dest)
{
if ( material != null)
{
int rtW = src. width;
int rtH = src. height;
RenderTexture buffer = RenderTexture. GetTemporary ( rtW, rtH, 0 ) ;
Graphics. Blit ( src, buffer, material, 0 ) ;
Graphics. Blit ( buffer, dest, material, 1 ) ;
RenderTexture. ReleaseTemporary ( buffer) ;
}
else
{
Graphics. Blit ( src, dest) ;
}
}
第一版的OnRenderImage,利用了RenderTexture.GetTemporary(rtW, rtH, 0)分配了一块与屏幕图像大小相同的缓冲区 这是因为高斯模糊需要两个Pass,第一个Pass执行完毕后得到的模糊结果存储在buffer中 Graphics.Blit(src, buffer, material, 0) ,作为第二个Pass的输入 Graphics.Blit(buffer, dest, material, 1) 最后需要 RenderTexture.ReleaseTemporary(buffer) 来释放缓存
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;
Graphics. Blit ( src, buffer, material, 0 ) ;
Graphics. Blit ( buffer, dest, material, 1 ) ;
RenderTexture. ReleaseTemporary ( buffer) ;
}
else
{
Graphics. Blit ( src, dest) ;
}
}
第二版在第一版的基础上,利用缩放对图像进行降采样,减少需要处理的像素个数,提高性能 buffer.filterMode = FilterMode.Bilinear 将滤波模式设为双线性 过大的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) ;
}
}
3.GaussianBlurShader
Properties {
_MainTex ( "Base (RGB)" , 2D ) = "white" { }
_BlurSize ( "Blur Size" , Float) = 1.0
}
SubShader {
CGINCLUDE
ENDCG
}
使用CGINCLUDE来组织代码,类似于C++中头文件的功能,由于高斯模糊需要两个Pass,但他们使用的片元着色器代码是一样的,所以可以避免写两个一样的frag
struct v2f {
float4 pos : SV_POSITION;
half2 uv[ 5 ] : TEXCOORD0;
} ;
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;
}
一个5×5的高斯核可以分为两个大小为5的一维高斯核 o.uv[0] = uv;
: 将当前像素的纹理坐标存储到 o.uv[0] 中o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
: 计算当前像素 上方第一个像素的纹理坐标 ,并存储到 o.uv[1] 中。这里使用了 _MainTex_TexelSize.y 来获取纹理在垂直方向上的纹素大小,并与 _BlurSize 相乘来控制采样距离o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
: 计算当前像素下方第一个像素的纹理坐标,并存储到 o.uv[2] 中。o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
: 计算当前像素上方第二个像素的纹理坐标,并存储到 o.uv[3] 中。o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
: 计算当前像素下方第二个像素的纹理坐标,并存储到 o.uv[4] 中水平方向 只要把 _MainTex_TexelSize.y 改为 _MainTex_TexelSize.x 即可
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个数只需要记录3个数就好 fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
:从输入的纹理 _MainTex 中采样当前像素的颜色值,并乘以第一个权重值 weight[0],然后将结果存储到变量 sum 中。变量 sum 用于累加所有采样点的加权颜色值 sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
:从输入的纹理 _MainTex 中采样当前像素上方或下方第二个像素的颜色值,并乘以对应的权重值 weight[it],然后将结果累加到变量 sum 中sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
:输入的纹理 _MainTex 中采样当前像素上方或下方第一个像素的颜色值,并乘以对应的权重值 weight[it],然后将结果累加到变量 sum 中
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语义定义了他们的名字 —— 可以在其他Shader中直接通过名字来使用该Pass
五、Bloom效果
模拟真实摄像机的一种图像效果,让画面中较亮的区域“扩散”到周围区域中,造成一种朦胧的效果 实现原理:根据一个阈值提取出图像中较亮的部分,把他们存储在一张纹理中,再利用高斯模糊进行处理,再与原图像进行混合
1.Bloom.cs
[ 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 ;
private 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;
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) ;
}
}
根据前面的原理步骤,先进行高斯模糊,再与原图像混合 Graphics.Blit(src, buffer0, material, 0);
先提取较亮的区域(使用Shader中第一个Pass),存储在buffer0中后面进行与12.4一样的高斯迭代处理,模糊后较亮的区域会存储在buffer0中 再把buffer0传递给材质中_Bloom 纹理属性,并调用Graphics.Blit(src, dest, material, 3);
使用第四个Pass来进行最后的混合
2.BloomShader
Properties {
_MainTex ( "Base (RGB)" , 2D ) = "white" { }
_Bloom ( "Bloom (RGB)" , 2D ) = "black" { }
_LuminanceThreshold ( "Luminance Threshold" , Float) = 0.5
_BlurSize ( "Blur Size" , Float) = 1.0
}
_MainTex 对应了输入纹理 _Bloom 是高斯模糊后的高亮区域 _LuminanceThreshold 是用于提取高亮区域的阈值 _BlurSize 控制不同迭代之间高斯模糊的模糊区域范围
SubShader {
CGINCLUDE
ENDCG
}
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) ;
fixed val = clamp ( luminance ( c) - _LuminanceThreshold, 0.0 , 1.0 ) ;
return c * val;
}
这段代码用于实现Bloom 提取图像中较亮区域的功能 fixed4 c = tex2D(_MainTex, i.uv);
:对输入纹理进行采样,获取当前像素的颜色值用luminance(fixed4 color)
函数来计算像素亮度值 fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
:将亮度与阈值进行比较,并将结果截取到[0,1]片元着色器 fragExtractBright 返回值:较亮区域的像素颜色值
struct v2fBloom {
float4 pos : SV_POSITION;
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 {
return tex2D ( _MainTex, i. uv. xy) + tex2D ( _Bloom, i. uv. zw) ;
}
此段代码用于实现 Bloom 效果中 **混合亮部图像和原图像的功能 uv.xy 分量对应了_MainTex,即原图像纹理 uv.zw 分量对应了_Bloom,即模糊后的较亮区域的纹理坐标
# if UNITY_UV_STARTS_AT_TOP
if ( _MainTex_TexelSize. y < 0.0 )
o. uv. w = 1.0 - o. uv. w;
# endif
此段代码为平台差异化处理,根据不同平台调整纹理坐标的w分量
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
}
第一个用于提取亮度区域 第二个、第三个直接复用高斯模糊的 第四个用于混合
六、运动模糊
当拍摄对象或摄像机在曝光时间内发生移动时,就会产生模糊的效果 实现方法:
累积缓存 :将多张连续的图像混合在一起,得到模糊的效果。但需要记录多张图像,占用较多的内存和计算资源速度缓存 :速度缓存中存储了 各个像素当前的运动速度 ,根据运动速度计算模糊的方向和大小,可以得到更真实的运动模糊效果 本节中实现类似第一种方法,不需要渲染很多次场景,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中
1.MotionBlur.cs
[ Range ( 0.0f , 0.9f ) ]
public float blurAmount = 0.5f ;
private RenderTexture accumlationTexture;
private void OnDisable ( )
{
DestroyImmediate ( accumlationTexture) ;
}
blurAmount 值越大,运动拖尾效果越明显 private RenderTexture accumlationTexture;
:保存之前图像叠加的效果OnDisable函数:脚本不运行时,调用该函数,立即销毁清空
private void OnRenderImage ( RenderTexture src, RenderTexture dest)
{
if ( material != null )
{
if ( accumlationTexture == null || accumlationTexture. width != src. width || accumlationTexture. height != src. height)
{
DestroyImmediate ( accumlationTexture) ;
accumlationTexture = new RenderTexture ( src. width, src. height, 0 ) ;
accumlationTexture. hideFlags = HideFlags. HideAndDontSave;
Graphics. Blit ( src, accumlationTexture) ;
}
accumlationTexture. MarkRestoreExpected ( ) ;
material. SetFloat ( "_BlurAmount" , 1.0f - blurAmount) ;
Graphics. Blit ( src, accumlationTexture, material) ;
Graphics. Blit ( accumlationTexture, dest) ;
}
else
{
Graphics. Blit ( src, dest) ;
}
}
accumlationTexture.hideFlags = HideFlags.HideAndDontSave;
:表示这个变量不会显示在Hierarchy中,也不会保存到场景中accumlationTexture.MarkRestoreExpected();
:标记累加纹理,表明在渲染过程中使用它,并且不会对其进行清空和销毁Graphics.Blit(src, accumlationTexture, material);
:把当前屏幕图像src叠加到accumlationTexture中Graphics.Blit(accumlationTexture, dest);
:把结果显示在屏幕上
2.MotionBlurShader
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 fragRGB ( v2f i) : SV_Target {
return fixed4 ( tex2D ( _MainTex, i. uv) . rgb, _BlurAmount) ;
}
half4 fragA ( v2f i) : SV_Target {
return tex2D ( _MainTex, i. uv) ;
}
fragRGB 用于更新渲染纹理的RGB通道部分,fragA用于更新A通道部分 RGB 通道: 用于混合当前帧图像和累加纹理中的图像 ,创建模糊拖尾效果 A 通道: 用于存储透明度信息,例如物体的透明度或阴影 如果我们在混合 RGB 通道的同时也更新 A 通道,那么可能会导致透明度信息被错误地修改,例如透明物体变得不透明或阴影消失。fragA 直接使用 tex2D 函数采样 _MainTex 纹理的 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
}
两个Pass,一个渲染RGB,一个渲染A