《Unity Shader入门精要》笔记:高级篇(1)

  3.2 游戏引擎技术
  • 本篇博客主要为个人学习所编写读书笔记,不用于任何商业用途,以及不允许任何人以任何形式进行转载。
  • 本篇博客会补充一些扩展内容(例如其他博客链接)。
  • 本篇博客还会提供一些边读边做的效果截图。文章内所有数学公式都由Latex在线编辑器生成。
  • 本篇博客主要提供一个“glance”,知识点的总结。如有需要请到书店购买正版。
  • 博客提及所有官方文档基于2022.2版本,博客会更新一些书中的旧的知识点到2022.2版本。
  • 如有不对之处欢迎指正。
  • 我创建了一个游戏制作交流群:637959304 进群密码:(CSGO的拆包密码)欢迎各位大佬一起学习交流,不限于任何平台(U3D、UE、COCO2dx、GamesMaker等),以及欢迎编程,美术,音乐等游戏相关的任何人员一起进群学习交流。

屏幕后处理效果

  • 屏幕后处理效果(screen post-processing effects):在渲染完整个屏幕图像后,再对这个图像进行一系列操作。使用该技术可以为游戏画面添加更多的艺术效果,例如景深(Depth of Field)、运动模糊(Motion Blur)等。
  • OnRenderImage,抓取屏幕函数:MonoBehavior.OnRenderImage(RenderTexture src,RenderTexture dest) 。Unity会把当前渲染得到的图像存储在src中,然后通过函数的操作把目标渲染纹理dest显示到屏幕上。
  • Graphics.Blit:利用该函数完成对渲染纹理的处理
//三种函数声明,dest=null时会直接将结果显示在屏幕上。mat是材质,pass是调用指定编号的pass,pass=-1就会一次调用所有Pass
Blit(Texture src,RenderTexture dest);
Blit(Texture src,RenderTexture dest,Material mat,int pass = -1);
Blit(Texture src,Material mat,int pass = -1);

调整亮度、饱和度、对比度

  • 调整屏幕的亮度、饱和度、对比度:该部分需要编写一些C#代码来进行综合完成。
//C#脚本代码,该脚本直接挂载在摄像机下
using UnityEngine;
using System.Collections;

public class BrightnessSaturationAndContrast : PostEffectsBase {

	public Shader briSatConShader;
	private Material briSatConMaterial;
	public Material material {  
		get {
			briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
			return 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;

	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);
		}
	}
}
//该Shader直接传入public Shader briSatConShader;
Shader "Example/Brightness Saturation And Contrast" {
	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
			
			CGPROGRAM  
			#pragma vertex vert  
			#pragma fragment frag  
			  
			#include "UnityCG.cginc"  
			  
			sampler2D _MainTex;  
			half _Brightness;
			half _Saturation;
			half _Contrast;
			  
			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 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);  
			}  
			  
			ENDCG
		}  
	}
	
	Fallback Off
}

image 7 1024x969 - 《Unity Shader入门精要》笔记:高级篇(1)
场景窗口
image 8 - 《Unity Shader入门精要》笔记:高级篇(1)
游戏窗口

边缘检测

  • 边缘检测:利用边缘检测算子对图像进行卷积(convolution)操作。(俗称:开卷)如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,那么就可以判定他们之间有一条边界。
  • 什么是卷积:传送门,这个讲的很不错,还有很多例子和应用。
  • 边缘检测算子:Sobel、Prewitt、Roberts。传送门
Shader "Example/Shader09"
{
   	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_EdgesOnly("Edges Only",Float) = 1.0
        _EdgesColor("Edges Color",Color) = (0,0,0,1)
        _BackgroundColor("Background Color",Color)=(1,1,1,1)
	}
	SubShader {
		Pass {  
			ZTest Always Cull Off ZWrite Off
			
			CGPROGRAM  
			#pragma vertex vert  
			#pragma fragment frag  
			  
			#include "UnityCG.cginc"  
			  
			sampler2D _MainTex;  
			half4 _MainTex_TexelSize;
			fixed4 _EdgesColor;
            fixed4 _BackgroundColor;
            float _EdgesOnly;
			  
			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;
                const half Gx[9] = {-1,0,1,-1,0,1,-1,0,1};
                const half Gy[9] = {-1,-1,-1,0,0,0,1,1,1};
                for(int j = 0 ; j < 9 ; j++)
				    o.uv[j] = uv + _MainTex_TexelSize.xy * half2(Gx[j],Gy[j]);
						 
				return o;
			}

			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,-2,-1,0,0,0,1,2,1};
                const half Gy[9] = {-1,0,1,-2,0,2,-1,0,1};
                half edgeX = 0;
                half edgeY = 0;
                half texColor = 0;
                for(int j = 0 ; j < 9 ; j++)
                {
                   	texColor = luminance(tex2D(_MainTex, i.uv[j]));
                    edgeX += texColor * Gx[j];
                    edgeY += texColor * Gy[j];
                }

                return 1 - abs(edgeX) - abs(edgeY);
            }
		
			fixed4 frag(v2f i) : SV_Target 
            {
				half edge = Sobel(i);
				
                fixed4 withEdgeColor = lerp(_EdgesColor,tex2D(_MainTex,i.uv[4]),edge);
                fixed4 onlyEdgeColor = lerp(_EdgesColor,_BackgroundColor,edge);

				return lerp(withEdgeColor,onlyEdgeColor,_EdgesOnly);  
			}  
			  
			ENDCG
		}  
	}
	
	Fallback Off
}
image 9 1024x573 - 《Unity Shader入门精要》笔记:高级篇(1)

高斯模糊

  • 高斯模糊:利用高斯核进行卷积计算,每个元素的计算基于如下公式。σ是标准方差(一般取1),x和y分别对应了当前位置到卷积核中心的整数距离。为了确保滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化,让每个权重除以所有权重的和。
image 10 - 《Unity Shader入门精要》笔记:高级篇(1)
//在OnRenderImage需要进行两次Graphics.Blit,即需要用到两个Pass来实现高斯模糊
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);
		}
	}
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BlurSize ("Blur Size", Float) = 1.0
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		sampler2D _MainTex;  
		half4 _MainTex_TexelSize;
		float _BlurSize;
		  
		struct v2f {
			float4 pos : SV_POSITION;
//第一个坐标存储当前采样纹理坐标,剩下四个坐标是高斯模糊中要用到的对领域采样的纹理坐标
			half2 uv[5]: TEXCOORD0;
		};
//片元着色器计算消耗性能会比顶点高,所以顶点着色器中计算,一次Pass进行竖直方向的计算,一次Pass进行水平方向的计算
		v2f vertBlurVertical(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);
			
			half2 uv = v.texcoord;
//这里的*1.0,*2.0应该是进行纹理偏移
			o.uv[0] = uv;
			o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
			o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
			o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
			o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
					 
			return o;
		}
		
		v2f vertBlurHorizontal(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);
			
			half2 uv = v.texcoord;
			
			o.uv[0] = uv;
			o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
			o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
			o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
			o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
					 
			return o;
		}
//滤波函数
		fixed4 fragBlur(v2f i) : SV_Target {
//5X5的高斯核化简成一个1x5和5x1的一维向量,v = {0.0545,0.2442,0.4026,0.2442,0.0545} h^T={0.0545,0.2442,0.4026,0.2442,0.0545}所以可以直接化简成如下的weight矩阵,只需要三个变量即可
			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);
		}
		    
		ENDCG
		
		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
		}
	} 
	FallBack "Diffuse"
}

image 11 1024x582 - 《Unity Shader入门精要》笔记:高级篇(1)
  • Bloom扩散效果:可以将较亮的区域扩散到周围的去榆中,造成一种朦胧的效果。
  • 原理:根据设定的阈值提取较亮的区域,然后存储到一个纹理渲染中,然后进行高斯模糊处理,模拟光线扩散效果,最后再和原图像进行混合
image 12 1024x586 - 《Unity Shader入门精要》笔记:高级篇(1)

运动模糊

  • 实现方法:1、利用一块积累缓存来混合多张连续的图像,然后取平均值输出。但消耗性能过大。
    2、利用速度缓存存储各个像素当前的运动速度,然后利用这些值来决定模糊的方向和大小。
    (在书中的下一章节会进行基于速度映射图的运动模糊,故这里就不再深究代码方面的内容)

LEAVE A COMMENT