屏幕后处理定义:就是渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效。
屏幕后处理实现过程:首先需要在摄像机上添加一个用于屏幕后处理的脚本。在这个脚本中,我们会实现MonoBehaviour.OnRenderImage函数来获取当前屏幕的渲染纹理。然后再调用Graphics.Blit函数使用特定的Unity Shader对当前的图像进行处理,再把返回的渲染纹理显示到屏幕上。
明亮度&饱和度&对比度屏幕特效:就是摄像机挂载的屏幕后处理脚本中通过控制明亮度,饱和度,对比度系数,传递给材质中的Shader进行处理,Shader代码如下:
Shader "Custom/BrightnessSaturationAndContast" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Pass {
// 开启深度测试,关闭深度写入和图元裁剪
ZTest Always ZWrite Off Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 定义材质传递的参数属性
sampler2D _MainTex; // 渲染完场景后得到的屏幕图像,必须使用该属性名,由Graphics.Blit函数内部传递
half _Brightness; // 明亮度
half _Saturation; // 饱和度
half _Contrast; // 对比度
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vert(appdata_img i) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
o.uv = i.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
}
卷积核:通常是一个四方形的网格结构,并且每一个方格具有一定的权重值。
卷积操作:就是将图像中每一个像素对应卷积核的中心点,然后计算卷积核与图像覆盖像素的乘积并累加起来得到当前处理像素的新像素值。
边缘检测屏幕特效:选择合适的卷积核来对图像中的所有像素进行卷积操作,参考代码如下:
Shader "Custom/EdgeDetection" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} // Graphics.Blit传递过来的源屏幕图像
}
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 定义材质传递过来的属性变量
sampler2D _MainTex;
half4 _MainTex_TexelSize; // 纹素大小
fixed _EdgeOnly; // 边缘线强度,当为1时表示只显示边缘线,不显示原图像
fixed4 _EdgeColor; // 边缘颜色
fixed4 _BackgroundColor; // 背景颜色
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0; // 包含当前像素以及邻近素在内的9个像素在纹理中的采样坐标
};
v2f vert(appdata_img i) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
half2 uv = i.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;
}
// 设置指定的像素为0饱和度
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
// 卷积操作函数
half Sobel(v2f i) {
// 定义Sobel卷积核
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;
}
fixed4 frag(v2f i) : SV_Target {
// 使用Sobel卷积核进行卷积操作
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
}
高斯核定义:高斯核是一个正方形的滤波核,核中元素可以使用高斯方程来获取。其中高斯方程的定义如下:
所以临近像素与当前处理像素越近,影响程度越大。
高斯模糊屏幕特效:就是使用高斯核来对图像的每一个像素进行卷积操作。5x5维度的高斯模糊参考代码如下:
Shader "Custom/GaussianBlur" {
Properties {
_MainTex("Base (RGB)", 2D) = "white" {} // 场景渲染完毕后得到的屏幕对象
}
SubShader {
// 定义头文件,该文件的内容可以在包含该文件的地方进行使用
CGINCLUDE
#include "UnityCG.cginc"
// 定义材质传递的属性变量
sampler2D _MainTex;
half4 _MainTex_TexelSize; // 当前纹素大小
float _BlurSize; // 模糊范围大小
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0; // 当前像素和临近四个像素的纹理采样坐标
};
// 顶点着色器中使用高斯核垂直分量进行计算采样操作
v2f vertBlurVertical(appdata_img i) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
half2 uv = i.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;
}
// 顶点着色器使用高斯核水平分量进行计算采样操作
v2f vertBlurHorizontal(appdata_img i) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
half2 uv = i.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};
// 当前像素点和临近4个点(水平或者垂直)进行卷积操作
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
// 第一个Pass主要进行高斯核垂直方向的处理
Pass {
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
// 第二个Pass主要进行高斯核水平方向的处理
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
运动模糊屏幕特效:通过将当前渲染纹理与之前渲染纹理之间进行混合,并按照模糊因子进行相乘来得到运动中模糊的效果。参考实现代码如下:
Shader "Custom/Motion Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
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;
}
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 {
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
FallBack Off
}
Bloom屏幕特效:首先根据阈值提取屏幕图像中较亮的区域,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其与源屏幕图像进行混合,得到最终效果。参考代码如下:
Shader "Custom/Bloom" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} // 场景渲染完毕后得到的屏幕对象
}
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;
}
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);
}
ENDCG
ZTest Always Cull Off ZWrite Off
// 第一个Pass根据阈值提取屏幕图像中较亮的区域,把它们存储在一张渲染纹理中
Pass {
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
// 第二个Pass对渲染纹理进行高斯核垂直分量的模糊处理
UsePass "Custom/GAUSSIAN_BLUR_VERTICAL"
// 第三个Pass对渲染纹理进行高斯核水平分量的模糊处理
UsePass "Custom/GAUSSIAN_BLUR_HORIZONTAL"
// 第四个Pass对源渲染纹理和高斯模糊以及抽离明亮区域后得到的渲染纹理进行混合处理
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
注意:
1.屏幕后处理能不能正常使用通常是有一些限制条件的,如:当前环境是否支持渲染纹理,纹理图像是否支持特效处理,指定的Shader是否被支持等。
2.Graphics.Blit函数来指定Shader处理MonoBehaviour.OnRenderImage中接收到的场景渲染结束后对应的屏幕图像时,需要在Shader的Properties中加入_MainTex (“Base (RGB)”, 2D) = “white” {}来接收该屏幕图像。
3.NxN大小的高斯核进行采样时,对应的采样次数是NxNxWxH,其中W是图像的宽度,H是图像的高度,为了降低采样次数,我们可以将高斯核进行横向和纵向的拆分,此时的采样次数为:2xNxWxH。
4.屏幕后处理中,标准的渲染标签为:ZTest Always Cull Off ZWrite Off。
5.在Pass渲染流程中使用Name "Pass的名称"来对Pass赋予一个名字,在其他的Shader中可以通过UsePass "Pass的名称"来进行访问。
6.CGINCLUDE … ENDCG语义块进行定义头文件,文件内部内容可以在包含该语义块的文件的任何地方使用。