Chapter12 屏幕后处理效果——Shader入门精要学习笔记

news2024/9/21 0:49:17

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.Bloom.cs
    • 2.BloomShader
  • 六、运动模糊
    • 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;
	}

	// Called when the platform doesn't support this effect
	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
  • 继承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)", 2D) = "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);

    //Brightness
    fixed3 finalColor = renderTex.rgb * _Brightness;
    //Saturation
    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);
    //Contrast
    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} Gx G y G_{y} Gy,再计算得到整体的 G x 2 + G y 2 \sqrt{G_{x}^2 + G_{y}^2} Gx2+Gy2 (出于性能考虑,有时候会用绝对值来代替 G = ∣ G x ∣ + ∣ G y ∣ G = |G_{x}| + |G_{y}| G=Gx+Gy
  • 梯度值G大的越有可能是边缘点

2.EdgeDetection.cs 挂载在摄像机上

 public Shader edgeDetectShader;
 private Material edgeDetectMaterial = null;
 public Material material
 {
     get {
         edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
         return edgeDetectMaterial;
     }
 }
  • 声明了所需要的Shader,并据此创建相应的材质
[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)", 2D) = "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} Gx G y G_{y} Gy
  • 再对9个像素进行依次采样,计算亮度值
  • 再与卷积核 G x G_{x} Gx G y G_{y} Gy对应的权重相乘后,叠加到各自的梯度值上
  • 最后用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;
     }
 }

 //Blur Interations
 [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越大,需要处理的像素数越少,也能提高模糊程度,但过大会让图像像素化
    /// 1st edition: just apply blur
 void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            int rtW = src.width;
            int rtH = src.height;
            RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);

            // Render the vertical pass
            Graphics.Blit(src, buffer, material, 0);
            // Render the horizontal pass
            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) 来释放缓存
/// 2nd edition: scale the render texture
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;

		// Render the vertical pass
		Graphics.Blit(src, buffer, material, 0);
		// Render the horizontal pass
		Graphics.Blit(buffer, dest, material, 1);

		RenderTexture.ReleaseTemporary(buffer);
	}
	else
	{
		Graphics.Blit(src, dest);
	}
}
  • 第二版在第一版的基础上,利用缩放对图像进行降采样,减少需要处理的像素个数,提高性能
  • buffer.filterMode = FilterMode.Bilinear 将滤波模式设为双线性
  • 过大的downSample会造成图像像素化
/// 3rd edition: use iterations for larger blur
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);

            // Render the vertical pass
            Graphics.Blit(buffer0, buffer1, material, 0);

            RenderTexture.ReleaseTemporary(buffer0);
            buffer0 = buffer1;
            buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

            // Render the horizontal pass
            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);
             //Vertical Pass
             Graphics.Blit(buffer0, buffer1, material, 1);
             RenderTexture.ReleaseTemporary(buffer0);
             buffer0 = buffer1;
             buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

             //Horizontal Pass
             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
		}
  • 用 CGINCLUDE 组织代码,提高复用性
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
    在这里插入图片描述

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

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

相关文章

Postman安装使用教程(详解)

目录 一、Postman是什么 二、安装系统要求 三、下载Postman 四、注册和登录Postman 五、创建工作空间 六、创建请求 一、Postman是什么 在安装之前&#xff0c;让我们先来简单了解一下Postman。Postman是一个流行的API开发工具&#xff0c;它提供了友好的用户界面用于发送…

简单实用的企业舆情安全解决方案

前言&#xff1a;企业舆情安全重要吗&#xff1f;其实很重要&#xff0c;尤其面对负面新闻&#xff0c;主动处理和应对&#xff0c;可以掌握主动权&#xff0c;避免股价下跌等&#xff0c;那么如何做使用简单实用的企业舆情解决方案呢&#xff1f; 背景 好了&#xff0c;提取词…

python CMD命令行传参实现:argparse、click、fire

1、argparse 设置传入和默认参数&#xff0c;也可以通过–help参考具体设置参数 bool值 参考&#xff1a; https://docs.python.org/zh-cn/3/howto/argparse.html https://www.bilibili.com/video/BV1nb41157Zc expected one argumrnt 报错&#xff0c;传入坐标类型字符串…

MYSQL 四、mysql进阶 9(数据库的设计规范)

一、为什么需要数据库设计 二、范 式 2.1 范式简介 在关系型数据库中&#xff0c;关于数据表设计的基本原则、规则就称为范式。 可以理解为&#xff0c;一张数据表的设计结 构需要满足的某种设计标准的 级别 。要想设计一个结构合理的关系型数据库&#xff0c;必须满足一定的…

couldn‘t read native报错!Typora中使用Pandoc导出Word失败的解决方法

couldn‘t read native报错&#xff01;Typora中使用Pandoc导出Word失败的解决方法 一、问题描述 在Typora中使用Pandoc将markdown文件导出为word文件时&#xff0c;发生如下图所示错误: 在网上找了资料以后&#xff0c;发现是因为md文件里面有表格&#xff0c;如果把表格删掉…

【深度学习】PyTorch框架(4):初始网络、残差网络 和密集连接网络

1、引言 在本篇文章中&#xff0c;我们将深入探讨并实现一些现代卷积神经网络&#xff08;CNN&#xff09;架构的变体。近年来&#xff0c;学界提出了众多新颖的网络架构。其中一些最具影响力&#xff0c;并且至今仍然具有重要地位的架构包括&#xff1a;GoogleNet/Inception架…

linux搭建mysql主从复制(一主一从)

目录 0、环境部署 1、主服务器配置 1.1 修改mysql配置文件 1.2 重启mysql 1.3 为从服务器授权 1.4 查看二进制日志坐标 2、从服务器配置 2.1 修改mysql配置文件 2.2 重启mysql 2.3 配置主从同步 2.4 开启主从复制 3、验证主从复制 3.1 主服务器上创建test…

Stable Diffusion【美女写实模型】:亚洲女性写实大模型,皮肤细腻光滑!

今天介绍一款专注于亚洲女性写实的SDXL模型&#xff1a;XXMix_9realisticSDXL。该模型绘图质量相当出色&#xff1a;面部在真实感基础上增加了一些轻度的美颜效果&#xff1b;以及增强的光影特效方面效果&#xff1b;只需要简单提示语就可以画出典型的亚洲女孩风格高质量图像。…

通过vue3 + TypeScript + uniapp + uni-ui 实现下拉刷新和加载更多的功能

效果图: 核心代码: <script lang="ts" setup>import { ref, reactive } from vue;import api from @/request/api.jsimport empty from @/component/empty.vueimport { onLoad,onShow, onPullDownRefresh, onReachBottom } from @dcloudio/uni-applet form …

Gradio技术入门(一)

Gradio是一个开源的Python库&#xff0c;旨在让创建机器学习模型的应用界面变得简单快捷。 官网&#xff1a;格罗特 (gradio.app) 一、基本概述 1&#xff0c;技术概述 1. 定义与用途 Gradio通过Python生成一套HTML页面&#xff0c;其中编写好了大部分的组件&#xff0c;主…

《大数据基础》相关知识点及考点,例题

1.6大数据计算模式 1、MapReduce可以并行执行大规模数据处理任务&#xff0c;用于大规模数据集&#xff08;大于1TB&#xff09;的并行运算。MapReduce 极大地方便了分布式编程工作&#xff0c;它将复杂的、运行于大规模集群上的并行计算过程高度地抽象为两个函数一一Map和Redu…

数据库系统概论:数据库完整性

引言 数据库是现代信息系统的心脏&#xff0c;数据的准确性和一致性对于业务流程至关重要。数据库完整性是确保数据质量的基石&#xff0c;它涵盖了数据的正确性、相容性和一致性&#xff0c;是数据安全与业务连续性的保障。 数据库完整性是指数据的精确性、可靠性和逻辑一致…

Gitee使用教程2-克隆仓库(下载项目)并推送更新项目

一、下载 Gitee 仓库 1、点击克隆-复制代码 2、打开Git Bash 并输入复制的代码 下载好后&#xff0c;找不到文件在哪的可以输入 pwd 找到仓库路径 二、推送更新 Gitee 项目 1、打开 Git Bash 用 cd 命令进入你的仓库&#xff08;我的仓库名为book&#xff09; 2、添加文件到 …

【Unity】升级至API34,编译报错Java Runtime版本问题

文章目录 一、背景二、问题描述三、原因和解决方法 一、背景 1、Unity 2021.3.33f1 2、Firebase 11.7.0 3、Max Unity 6.5.2 3、升级至API-34 二、问题描述 错误信息 Could not load custom lint check jar file C:\Users\xxx.gradle\caches\transforms-2\files-2.1\b27e2aac8…

pnpm build打包时占内溢出

这两天在打包H5网页的时候&#xff0c;失败&#xff0c;总是提示下方错误&#xff0c;试了多种方法下方的亲测有效 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 严重错误&#xff1a;堆限制附近标记压缩无效分…

【字幕】字幕特效入门

前言 最近两周调研了一下字幕特效的底层程序逻辑&#xff0c;因为工作内容的原因&#xff0c;就分享几个自己找的链接具体细节就不分享了&#xff0c;CSDN也是我的个人笔记&#xff0c;只记录一些简单的内容用于后续自己方便查询&#xff0c;顺便帮助一下正在苦苦查阅资料入门…

.net core appsettings.json 配置 http 无法访问

1、在appsettings.json中配置"urls": "http://0.0.0.0:8188" 2、但是网页无法打开 3、解决办法&#xff0c;在Program.cs增加下列语句 app.UseAntiforgery();

数据库系统概论:数据库系统模式

数据库系统在我们的数字世界中扮演着至关重要的角色&#xff0c;无论是个人设备还是企业级应用&#xff0c;数据的有效管理和访问都是必不可少的。而数据库系统的模式结构是确保数据一致性和可访问性的关键组成部分。 数据库系统模式 基本概念 型和值 数据模型中有 型(type…

数据结构之细说链表

1.1顺序表的问题以及思考 经过上一篇顺序表的学习&#xff0c;我们知道顺序表还是有很多缺点 顺序表的缺点&#xff1a; 1.中间/头部的插入删除&#xff0c;实际复杂度为O(N) 2.增容需要申请新空间&#xff0c;拷贝数据&#xff0c;释放旧空间。会有不小的消耗 3.扩容一般…

【ECharts】使用 ECharts 处理不同时间节点的数据系列展示

使用 ECharts 处理不同时间节点的数据系列展示 在数据可视化中&#xff0c;我们经常遇到这样的问题&#xff1a;不同数据系列的数据点在时间轴上并不对齐。这种情况下&#xff0c;如果直接在 ECharts 中展示&#xff0c;图表可能会出现混乱或不准确。本文将通过一个示例代码&a…