【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 AlwaysCull OffZwrite OffCGPROGRAM#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 AlwaysZWrite OffCull OffCGPROGRAM#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);// 先渲染到buffer0buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);Graphics.Blit(source, buffer0,BaseMaterial,0);// 再渲染到buffer1buffer1 = 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{CGINCLUDEsampler2D _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// verticalPass{NAME "GAUSSIAN_BLUR_VERTICAL"ZTest AlwaysZWrite OffCull OffCGPROGRAM#pragma fragment frag#pragma vertex vertv2f 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}// horizonPass{NAME "GAUSSIAN_BLUR_HORIZONTAL"ZTest AlwaysZWrite OffCull OffCGPROGRAM#pragma fragment frag#pragma vertex vertv2f 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和_Bloomhalf4 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;#endifreturn 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 fragGetBrightENDCG}// 高斯模糊UsePass "Custom/GaussianBlur_Copy/GAUSSIAN_BLUR_VERTICAL"UsePass "Custom/GaussianBlur_Copy/GAUSSIAN_BLUR_HORIZONTAL"// Bloom混合Pass{CGPROGRAM#pragma vertex vertBloom#pragma fragment fragBloomENDCG}}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;// 用于保存上一帧渲染结果的RTprivate RenderTexture m_accumulationTexture;//为了在开启后重新叠加图像(避免关闭脚本之前的画面错误叠加)需要摧毁RTprivate 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);}ENDCGZTest Always Cull Off ZWrite OffPass {Blend SrcAlpha OneMinusSrcAlphaColorMask RGBCGPROGRAM#pragma vertex vert  #pragma fragment fragRGB  ENDCG}// 处理透明度的Pass,看不出有什么影响Pass {   Blend One ZeroColorMask ACGPROGRAM  #pragma vertex vert  #pragma fragment fragAENDCG}}Fallback Off
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

上位机图像处理和嵌入式模块部署(树莓派4b的提高版)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 目前人工智能非常火&#xff0c;但是人工智能需要极高的算力和海量的数据&#xff0c;因此相关的关联公司非常吃香&#xff0c;nvidia就是提供算力…

【基本数据结构】链表

文章目录 前言链表简介头节点与尾节点特性 分类单向链表双向链表循环链表 单链表基本操作定义并初始化单链表读取节点插入节点删除节点修改节点 参考资料写在最后 前言 本系列专注更新基本数据结构&#xff0c;现有以下文章&#xff1a; 【算法与数据结构】数组. 【算法与数…

互联网搞钱大变天,这有几条活路

互联网搞钱大变天&#xff0c;这有几条活路 靠互联网营生的各位同胞&#xff0c;你们有没有想过这样一个问题&#xff1a;假如有一天你的自媒体账号全被封了&#xff0c;你手上的操作项目全都黄了&#xff0c;你会怎么办&#xff1f; 就封号这事在这几年相信大家都不会陌生&a…

【LLM第五篇】名词解释:prompt

1.是什么 提示工程&#xff08;Prompt Engineering&#xff09;是一门较新的学科&#xff0c;关注提示词开发和优化&#xff0c;帮助用户将大语言模型&#xff08;Large Language Model, LLM&#xff09;用于各场景和研究领域。 掌握了提示工程相关技能将有助于用户更好地了解…

深入了解 npm:Node.js 包管理工具详解

文章目录 一、npm 基本概念1.1 什么是 npm&#xff1f;1.2 package.json 文件 二、npm 常用命令2.1 初始化项目2.2 安装依赖2.2.1 安装单个包2.2.2 全局安装包2.2.3 安装开发依赖 2.3 移除依赖2.4 更新依赖2.5 查看已安装的包2.6 发布包 三、npm 高级用法3.1 使用 npm scripts3…

JETBRAINS IDES 分享一个2099通用试用码!CLion 2024 版 ,支持一键升级

文章目录 废话不多说上教程&#xff1a;&#xff08;动画教程 图文教程&#xff09;一、动画教程激活 与 升级&#xff08;至最新版本&#xff09; 二、图文教程 &#xff08;推荐&#xff09;Stage 1.下载安装 toolbox-app&#xff08;全家桶管理工具&#xff09;Stage 2 : 下…

nestJs中跨库查询

app.module.ts中配置 模块的module中 注意实体类在写的时候和数据库中的表名一样 service中使用一下

【JS】call和 apply函数的详解

JavaScript 中 call() 和 apply() 函数的详解 在JavaScript中&#xff0c;call()和apply()都是非常重要的方法&#xff0c;用于调用函数时指定函数体内的this的值&#xff0c;从而实现不同对象之间的方法共享。尽管它们的功能非常相似&#xff0c;但在实际使用中各有其优势和特…

生产环境磁盘变更方案

datanode磁盘扩容(1人天) 扩容磁盘 1.1 扩容前检查、新盘初始化 确定block副本统计正常,无丢块,无under-replicated 块 后台执行命令检查 hdfs dfs fsck / 无异常可进行以下步骤进行磁盘扩容 新盘初始化 ##格式化新盘,命令示例: mkfs.xfs /dev/sdc##挂载新盘,命令示…

【AI学习】聊两句昨夜OpenAI的GPT-4o

蹭个热点&#xff0c;聊两句昨夜的大事件——OpenAI发布GPT-4o&#xff0c;我看到和想到的一点东西。 首先是端到端方法&#xff0c;前面关于深度学习的文章&#xff0c;对端到端的重要性做了一些学习&#xff0c;对端到端这个概念有了一些理解。正如Richard Sutton在《苦涩的…

大数据Scala教程从入门到精通第六篇:Scala源文件编写和运行

一&#xff1a;Scala源文件编写和运行 1&#xff1a;源代码比较 public class HelloJava{public static void main(String[] args){System.out.println("hello scala")} } object HelloScala{//用于声明方法 入参是一个String类型的数组。返回值类型为空def main…

鸿蒙内核源码分析(gn应用篇) | gn语法及在鸿蒙的使用

gn是什么? gn 存在的意义是为了生成 ninja,如果熟悉前端开发,二者关系很像 Sass和CSS的关系. 为什么会有gn,说是有个叫even的谷歌负责构建系统的工程师在使用传统的makefile构建chrome时觉得太麻烦,不高效,所以设计了一套更简单,更高效新的构建工具gnninja,然后就被广泛的使用…

【docker】SpringBoot应用容器镜像日志挂载

启动镜像时候使用 -v 挂载 首先得在宿主机创建目录&#xff1a;/workspace/java/demo/logs mkdir -pv /workspace/java/demo/logs 启动镜像 docker run -p 8080:8080 -itd -v /workspace/java/demo/logs/:/logs/ 192.168.2.1:5000/demo:0.0.1-SNAPSHOT -v /workspace/ja…

【Shell脚本】Shell编程之数组

目录 一.数组 1.基本概念 2.定义数组的方法 2.1.方法一 2.2.方法二 2.3.方法三 2.4.方法四 2.5.查看数组长度 2.6.查看数组元素下标 3.数组分片 4.数组字符替换 4.1.临时替换 4.2.永久替换 5.数组删除 5.1.删除某个下标 5.2.删除整组 6.数组遍历和重新定义 7…

SpringBoot自动装配(二)

近日&#xff0c;余溺于先贤古哲之文无法自拔。虽未明其中真意&#xff0c;但总觉有理。遂抄录一篇以供诸君品鉴——公孙鞅曰&#xff1a;“臣闻之&#xff1a;‘疑行无名&#xff0c;疑事无功。’君亟定变法之虑&#xff0c;殆无顾天下之议之也。且夫有高人之行者&#xff0c;…

【备忘】Unable to stop the ctxapinject driver services

【问题】一台通过Autopilot部署的Windows11系统&#xff0c;在公司门户里看到 策略推送的Citrix workspace不成功&#xff0c;尝试手动安装&#xff0c;报错&#xff1a; 【解决】网上看了半天没找出个所以然来&#xff0c;尝试sc queryex ctxapinject 查到对应的进程ID是0&…

【JavaWeb】Day77.Spring——SpringBoot原理(一)

SpringBoot原理 Spring是目前世界上最流行的Java框架&#xff0c;它可以帮助我们更加快速、更加容易的来构建Java项目。而在Spring家族当中提供了很多优秀的框架&#xff0c;而所有的框架都是基于一个基础框架的SpringFramework(也就是Spring框架)。而如果我们直接基于Spring框…

MySQL企业级开发重点之事物和索引

事物 -- 解散学工部 delete from tb_dept where id 1;-- 删除部门下的员工 delete from tb_emp where dept_id 1; 介绍和操作 我们应该将两个语句写成一个语句 -- 开启事物 start transaction ;-- 解散学工部 delete from tb_dept where id 3;-- 删除部门下的员工 delete fr…

Ajax 学习

文章目录 1. 前置知识1.1 ajax 介绍1.2 XML 简介 2. AJAX 学习2.1 AJAX基础学习&#xff08;1&#xff09;AJAX的特点&#xff08;2&#xff09;AJAX 初体验&#xff08;3&#xff09;服务端响应json 数据 2.2 IE 缓存问题2.3 请求超时和网络异常2.4 手动取消请求2.5 重复请求2…

流畅的python-学习笔记_序列修改+散列+切片

vector第一版 reprlib.repr用于选取有限长度较长变量 vector第二版切片 注意切片还有indices属性&#xff0c;它可以入参一个序列长度&#xff0c;根据此序列长度&#xff0c;转化不规矩的start stop stride&#xff0c; vector第三版动态存取属性 obj.attra时&#xff0c;先…