unity Shader之——屏幕后处理效果(原理以及实现一个简单的屏幕后处理效果)

一、建立一个基本的屏幕后处理脚本系统

屏幕后处理,通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列的操作,实现各种屏幕特效。可以为游戏画面添加更多的艺术效果,例如景深、运动模糊等。

因此想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而unity为我们提供了这样一个方便的接口——OnRenderImage函数。它的函数声明如下:

MonoBehaviour.OnRenderImage(RenderTexture src,RenderTexture dest)

当我们在脚本中声明此函数后,会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理中,通过函数中的一系列操作后,再把目标渲染纹理,即第二个参数对应的渲染纹理显示到屏幕上。在OnRenderImage函数中,我们通常是利用Graphics.Blit函数来完成对渲染纹理的处理。

它有3种函数声明:

  • 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对应了源纹理,在屏幕后处理技术中,这个参数通常就是当前屏幕的渲染纹理或上一步处理后得到的渲染纹理。参数dest是目标渲染纹理,如果它的值为null就会直接将结果显示在屏幕上。参数mat是我们使用的材质,这个材质使用的unity Shader将会进行各种屏幕后处理操作,而src纹理将会被传递给Shader中名为_MainTex的纹理属性。参数pass的默认值为-1,表示将会依次调用shader内的所有pass,否则只会调用给定索引的pass。

在默认情况下,OnRenderImage函数会在所有的不透明和透明的pass执行完毕后被调用,以便对场景中所有游戏对象都产生影响。但有时希望在不透明的pass执行完毕后立即调用OnRenderImage函数,从而不对透明物体产生任何影响。此时我们可以在OnRenderImage函数前添加ImageEffectOpaque属性来实现这样的目的。

总的来说,要在unity中实现屏幕后处理效果,过程通常如下:首先需要在摄像机中添加一个用于屏幕后处理的脚本,在这个脚本中,我们会实现OnRenderImage函数来获取当前屏幕的渲染纹理。然后再调用Graphics.Blit函数使用特定的Unity Shader来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上。对于一些复杂的屏幕特效,我们可能需要多次调用Graphics.Blit函数来对上一步的输出结果进行下一步处理。

但是在进行屏幕后处理之前,我们需要检查一系列条件是否满足,例如当前平台是否支持渲染纹理和屏幕特效,是否支持当前使用的Unity Shader等。为此我们创建了一个用于屏幕后处理效果的基类,在实现各种屏幕特效时,我们只需要继承该基类,再实现派生类中不同的操作即可。

(1)首先所有屏幕后处理效果都需要绑定在某个摄像机上,并且在编辑器状态下也可以执行该脚本来查看效果:

[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {

(2)为了提前检查各种资源和条件是否满足,我们在Start函数中调用CheckResources函数:

// Called when start
	protected void CheckResources() {
		bool isSupported = CheckSupport();
		
		if (isSupported == false) {
			NotSupported();
		}
	}
 
	// Called in CheckResources to check support on this platform
	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;
	}
	
	protected void Start() {
		CheckResources();
	}

一些屏幕特效可能需要更多的设置。例如设置一些默认值等,可以重载Start、ChenckResources或CheckSupport函数。

(3)由于每个屏幕后处理效果通常都需要指定一个shader来创建一个用于处理渲染纹理的材质,因此基类中也提供了这样的方法:

// Called when need to create the material used by this effect
	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的可用性,检查通过后就返回一个使用了该shader的材质,否则返回null。

二、调整屏幕的亮度、饱和度和对比度

上节我们了解了实现屏幕后处理特效的技术原理,本节来实现一个简单的屏幕特效————调整屏幕的亮度、饱和度和对比度。首先编写BrightSaturationAndContrast.cs脚本。

1. 首先继承基类:

public class BrightnessSaturationAndContrast : PostEffectsBase {

2. 声明该效果需要的shader,并据此创建相应的材质:

public Shader briSatConShader;
	private Material briSatConMaterial;
	public Material material {  
		get {
			briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
			return briSatConMaterial;
		}  
	}

上述代码中,briSatConShader是我们制定的Shader,对应了后面要实现的一个shader,briSatConMaterial是创建的材质,我们提供了名为material的材质来访问它,get函数调用了基类的函数来得到对应的材质。

3. 脚本中提供了调整亮度、饱和度和对比度的参数:

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

利用Unity提供的Range属性为每个参数提供了合适的变化区间。

4. 最后定义OnRenderImage函数来进行真正的特效处理:

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函数被调用时,它会检查材质是否可用,如果可用就把参数传递给材质,在调用Graphics.Blit进行处理,否则直接把原图显示到屏幕上,不做任何处理。

下面来实现shader的部分。

1. 首先声明本例使用的各个属性:

 
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_Brightness ("Brightness", Float) = 1
		_Saturation("Saturation", Float) = 1
		_Contrast("Contrast", Float) = 1
	}

在第一节中提到Graphics.Blit(src, dest, material);将把第一个参数传递给shader名为_MainTex的属性。因此我们必须声明一个名为_MainTex的纹理属性,除此之外,还声明了用于调整亮度、饱和度和对比度的属性,这些值将由脚本传递而得。实际上可以省略Properties中的属性声明,这些仅仅是为了显示在材质面板中,但对于屏幕特效来说,它们使用的材质都是临时创建的,我们也不需要在材质面板上调整参数,而是直接从脚本传递给unity Shader。

2. 定义用于屏幕后处理的Pass:

SubShader {
		Pass {  
			ZTest Always Cull Off ZWrite Off

屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片,为了防止它对其他物体产生影响,我们需要设置相关的渲染状态。在这里关闭了深度写入,是为了防止它“挡住”再其后面被渲染的物体。例如当前的OnRenderImage函数在所有不透明的Pass执行完毕后立即被调用,不关闭深度写入就会影响后面透明物体的Pass的渲染,这些状态设置可以认为是用于屏幕后处理的Shader的“标配”。

3. 为了访问代码中访问各个属性,我们需要在CG代码块中声明对应的变量:

            sampler2D _MainTex;  
			half _Brightness;
			half _Saturation;
			half _Contrast;

4. 定义顶点着色器,屏幕特效使用的顶点着色器代码通常都比较简单,我们只需要进行必需的顶点变换,更重要的是把正确的纹理坐标传递给片元着色器,以便对屏幕图像进行正确的采样。

struct v2f {
				float4 pos : SV_POSITION;
				half2 uv: TEXCOORD0;
			};
			  
			v2f vert(appdata_img v) {
				v2f o;
				
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				
				o.uv = v.texcoord;
						 
				return o;
			}

5. 接着实现了用于调整亮度、饱和度和对比度的片元着色器:

fixed4 frag(v2f i) : SV_Target {
				fixed4 renderTex = tex2D(_MainTex, i.uv);  
				  
				// Apply brightness
				fixed3 finalColor = renderTex.rgb * _Brightness;
				
				// Apply 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);
				
				// Apply contrast
				fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
				finalColor = lerp(avgColor, finalColor, _Contrast);
				
				return fixed4(finalColor, renderTex.a);  
			}  

首先得到原屏幕图像(存储在_MainTex中)的采样结果renderTex。然后利用_Brightness属性来调整亮度。只需要把原颜色乘以亮度系数_Brightness即可。然后计算像素对应的亮度值,这是通过对每个颜色分量乘以一个特定的系统再相加得到的。我们使用该亮度值创建了一个饱和度为0的颜色值,并使用_Saturation属性在其和上一步得到的颜色之间进行插值,从而得到希望的饱和度颜色。对比度的处理类似,首先创建一个对比度为0的颜色值(各分量均为0.5),在使用_Constrast属性和在其和上一步得到的颜色之间进行插值,从而得到最终的处理结果。

6. 最后我们关闭该Unity Shader的Fallback:

Fallback Off

最后手动把shader拖拽到脚本的参数上,把脚本拖拽到摄像机上。一个简单的屏幕后处理效果完成了。

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/100665580

最新文章