一、屏幕后处理概述以及基本脚本系统
概念:在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效
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