【UnityShader入门精要学习笔记】第十二章 屏幕后处理效果

在这里插入图片描述
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:

  • 书本中句子照抄 + 个人批注
  • 项目源码
  • 一堆新手会犯的错误
  • 潜在的太监断更,有始无终

我的GitHub仓库

总之适用于同样开始学习Shader的同学们进行有取舍的参考。


文章目录

  • 建立一个基本的屏幕后处理脚本系统
  • 边缘检测
    • 什么是卷积
    • 常见的边缘检测算子
    • 实现
  • 高斯模糊
    • 实现
  • Bloom效果
  • 运动模糊


屏幕后处理效果(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;

                // 自定义的饱和度值,当饱和度>1,权值越大的越明显,若<1则权值越小越明显
                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} [1001]

了解了原理后,我们来看看常见的边缘检测算子:

在这里插入图片描述
这几种常见的边缘检测算子包含了两个方向的卷积核,分别用于检测水平和垂直方向上的边缘信息。我们可以单用其中一个卷积核来检测水平或垂直方向上的梯度变换,也可以两个合用并计算开方后的均值(以横向和纵向两个方位的梯度均值来判定边缘)。

在这里插入图片描述

实现

后处理脚本:

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;
    		}

		    // 计算对比度
			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
}

在这里插入图片描述

最后的结果可以看到,卷积后的图像边缘被我们用黑色进行了描边

高斯模糊

高斯模糊的效果我们在之前的文章中也介绍过了,就是用高斯核(一个符合高斯分布的卷积核)进行计算。
在这里插入图片描述

(若将卷积核转化为高度图,可以看到高斯分布)

其中每个元素的计算基于下面的高斯方程:

在这里插入图片描述
在这里插入图片描述

高斯核具有两个特性:

  • 距离卷积中心越近的像素影响更大(符合高斯分布)
  • 高斯核越大,则模糊程度越大

假设有一张WH像素的图像,我们要使用一个NN的高斯核进行卷积,那么就要经过NNW*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;
        }

        // 计算对比度,此处因为物体主体部分是红绿,因此希望提高红绿采样的对比度
        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
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/626436.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

元宇宙,可能是未来经济新趋势,但不是文明的跃升

为什么说元宇宙是未来的一个经济趋势&#xff0c;甚至是一个即将来临的风口&#xff0c;说到元宇宙&#xff0c;相信大家都听说过&#xff0c;但是真正了解的人还是占少部分的。其实概率很容易理解&#xff0c;我们可以把他看成是一个虚拟世界&#xff0c;一个平行空间&#xf…

菲律宾签证照片尺寸要求,用手机生成

菲律宾签证照片尺寸要求如下图所示&#xff0c;可以用手机在微信搜索随时照小程序&#xff0c;快速生成哦。

做私域,该如何从公域向私域引流?

说到私域运营&#xff0c;很多人首先就会想到&#xff1a;私域流量。企业做私域&#xff0c;流量从哪来&#xff1f;该怎样去引流&#xff1f;又该如何保障私域流量的质量等一系列问题&#xff0c;都需要企业一一解决。所以&#xff0c;今天&#xff0c;我们就来探讨一下&#…

产品经理资料包干货

1.《产品汪》免费电子书 2016年我面试了差不多有200多位产品求职者&#xff0c;其中不乏之前做厨师编剧这些岗位的人。在这个过程中我意识到大众或许对产品经理这个岗位存在一些认知和理解上的误差&#xff0c;于是我就想着写一本产品经理相关的书。 关于本书的更多信息可查看…

dvwa靶场 Content Security Policy (CSP) Bypass(CSP绕过)全难度教程(附代码分析)

前置知识 Content Security Policy&#xff08;内容安全策略&#xff09;&#xff0c;用于定义脚本和其他资源从何处加载或者执行&#xff0c;总结的来说就时白名单。会一定程度的缓解xss脚本问题&#xff0c;也可以自己设定规则&#xff0c;管理网站允许加载的内容。 CSP 以…

简单记录下:Navicat 导出表结构至 Excel

首先我们需要通过sql语句查询出相关的表结构的结构 SELECT COLUMN_NAME AS 字段名称,COLUMN_TYPE AS 字段类型,IF(IS_NULLABLENO,否,是) AS 是否必填,COLUMN_COMMENT AS 注释FROM INFORMATION_SCHEMA.COLUMNSWHERE table_schema bs-gdsAND table_name sys_menu;查询的结构如下…

Linux下Code_Aster源码编译安装及使用

目录 软件介绍 基本依赖 其它依赖 一、源码下载 二、解压缩 三、编译安装 四、算例运行 软件介绍 Code_aster为法国电力集团&#xff08;EDF&#xff09;自1989年起开始研发的通用结构和热力耦合有限元仿真软件。Code_aster可用于力学、热学和声学等物理现象的仿真分析&…

LQ杯当时的WP

RC4 32位程序用IDA打开看看 进行反汇编 RC4提示&#xff0c;就是一个加密 在sub_401005函数中找到输出的变量&#xff0c;并且立下断点 动调 Packet 字符串搜索flag 看到是给192.168.11.128发送了cat flag的命令 看到它回传 Base64加密了 解一下密码就可以 CC 密码这…

Windows snmp++获取本地主机信息

编译snmp的包 调用snmp.lib实现信息获取_哔哩哔哩_bilibili 代码&#xff1a; #include <iostream> #include <libsnmp.h> #include <vector> #include <fstream> #include <string> #include "snmp_pp/snmp_pp.h" //#define _NO_L…

Go微服务: Gin框架搭建网关, 接入熔断器,链路追踪以及服务端接入限流和链路追踪

概述 本文使用最简单和快速的方式基于Gin框架搭建一个微服务的网关调用微服务的场景网关作为客户端基于RPC调用某一服务端的服务并接入熔断和限流以及链路追踪具体场景&#xff1a;通过网关API查询购物车里的数据在最后&#xff0c;会贴上网关和购物车服务的代码仓库 服务端搭…

端口号查询进程PID

情况1&#xff1a;由于 idea 突然闪退&#xff0c;导致正在 debug 的 Java 进程没结束掉&#xff0c;端口还在占用&#xff0c;重新 debug 不了&#xff0c;所以需要到任务管理器把进程结束掉 但问题是如果当任务管理器进程同时有多个 Java 进程在运行&#xff08;而且名字一样…

CSS表格特殊样式

列组样式 使用colgroup与col标签配合可以定义列祖样式&#xff1a;例 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><style>table,tr,th,td{border: 1px solid #000;}table{border-collapse: coll…

java导出excel动态加载多sheet多复杂表头

java导出excel动态加载多sheet多复杂表头 实体实现类sheet方法业务工具方法实现效果 实体 import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.ToString; import lombok.experimental.Accessors;import java.io.Serializable; import ja…

IPSSL证书:为特定IP地址通信数据保驾护航

IPSSL证书&#xff0c;顾名思义&#xff0c;是专为特定IP地址设计的SSL证书。它不仅继承了传统SSL证书验证网站身份、加密数据传输的基本功能&#xff0c;还特别针对通过固定IP地址进行通信的场景提供了强化的安全保障。在IP地址直接绑定SSL证书的模式下&#xff0c;它能够确保…

tomcat--目录结构和文件组成

目录结构 目录说明bin服务启动&#xff0c;停止等相关程序和文件conf配置文件lib库目录logs日志记录webapps应用程序&#xff0c;应用部署目录workjsp编译后的结果文件&#xff0c;建议提前预热访问 /usr/local/apache-tomcat-8.5.100/work/Catalina/localhost/ROOT/org/apac…

MyBatis-Plus核心功能详解:条件构造器、自定义SQL与Service接口

在Java的Web开发中&#xff0c;MyBatis-Plus作为MyBatis的增强工具&#xff0c;提供了许多实用的功能&#xff0c;极大地简化了数据库操作的开发过程。下面&#xff0c;我们将详细探讨MyBatis-Plus的三大核心功能&#xff1a;条件构造器、自定义SQL以及Service接口。 一、条件…

【Vue】更换vue-element-admin左侧 logo

准备&#xff1a;目标svg格式的 logo&#xff0c;并将目标logo命名为 vuejs-fill.svg替换路径&#xff1a;/icons 文件夹下&#xff0c;覆盖掉原本的 vuejs-fill.svg 原因&#xff1a;配置项的logo设置的是 vuejs-fill

摊还分析

一、摊还分析 概念&#xff1a;是求数据结构中一个操作序列执行所有操作的平均时间&#xff0c;与平均复杂度不同&#xff0c;它不涉及输入概率&#xff0c;能够保证在最坏情况下操作的平均性能。 适用场景&#xff1a;用含 n 个操作的序列&#xff08;o1&#xff0c;o2&#x…

【系统架构师】-选择题(十四)数据库基础

1、某企业开发信息管理系统平台进行 E-R 图设计&#xff0c;人力部门定义的是员工实体具有属性&#xff1a;员工号、姓名、性别、出生日期、联系方式和部门,培训部门定义的培训师实体具有属性:培训师号&#xff0c;姓名和职称&#xff0c;其中职称{初级培训师&#xff0c;中级培…