UnityShader入门精要——运动模糊

运动模糊是真实世界中的摄像机的一种效果。如果在摄像机曝光时,拍摄场景发生了变化,就会产生模糊的画面。

运动模糊的实现有多种方法。一种实现方法是利用一块累积缓存(accumulation buffer )来混合多张连续的图像。当物体快速移动产生多张图像后,我们取它们之间的平均值作为最后的运动模糊图像。然而,这种暴力的方法对性能的消耗很大,因为想要获取多张帧图像往往意味着我们需要在同一帧里渲染多次场景。另一种应用广泛的方法是创建和使用速度缓存(velocity buffer),这个缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小。

在本节中,我们将使用类似上述第一种方法的实现来模拟运动模糊的效果。我们不需要在一帧中把场景渲染多次,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法与原始的利用累计缓存的方法相比性能更好,但模糊效果可能会略有影响。

后处理代码:

using UnityEngine;
using System.Collections;

public class MotionBlur : PostEffectsBase {

	public Shader motionBlurShader;
	private Material motionBlurMaterial = null;

	public Material material {  
		get {
			motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
			return motionBlurMaterial;
		}  
	}

	[Range(0.0f, 0.9f)]
	public float blurAmount = 0.5f;    //运动模糊参数,值越大,拖尾效果越明显
	
	private RenderTexture accumulationTexture;    //保存之前图像叠加的结果

	void OnDisable() {
		DestroyImmediate(accumulationTexture);
	}

	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			// Create the accumulation texture
			if (accumulationTexture == null || accumulationTexture.width != src.width || accumulationTexture.height != src.height) {
				DestroyImmediate(accumulationTexture);
				accumulationTexture = new RenderTexture(src.width, src.height, 0);
				accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
				Graphics.Blit(src, accumulationTexture);
			}

			// We are accumulating motion over frames without clear/discard
			// by design, so silence any performance warnings from Unity
			accumulationTexture.MarkRestoreExpected();

			material.SetFloat("_BlurAmount", 1.0f - blurAmount);

			Graphics.Blit (src, accumulationTexture, material);
			Graphics.Blit (accumulationTexture, dest);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

在上面的代码里,我们在该脚本不运行时,即调用OnDisable函数时,立即销毁accumulationTexture。这是因为,我们希望在下一次开始应用运动模糊时重新叠加图像。

在确认材质可用后,我们首先判断用于混合图像的accumulationTexture是否满足条件。我们不仅判断它是否为空,还判断它是否与当前的屏幕分辨率相等,如果不满足,就说明我们需要重新创建一个适合于当前分辨率的accumulationTexture变量。创建完毕后,由于我们会自己控制该变量的销毁,因此可以把它的hideFlags设置为HideFlags.HideAndDontSave,这意味着这个变量不会显示在Hierarchy中,也不会保存到场景中。然后,我们使用当前的帧图像初始化accumulationTexture (使用Graphics.Blit(src, accumulationTexture)代码)。

当得到了有效的accumulationTexture 变量后,我们调用了accumulationTexture.MarkRestoreExpected函数来表明我们需要进行一个渲染纹理的恢复操作。恢复操作发生在渲染到纹理而该纹理又没有被提前清空或销毁的情况下在本例中,我们每次调用OnRenderlmage时都需要把当前的帧图像和accumulationTexture 中的图像混合,accumulationTexture纹理不需要提前清空,因为它保存了我们之前的混合结果。然后,我们将参数传递给材质,并调用Graphics.Blit (src, accumulationTexture, materal)把当前的屏幕图像src叠加到accumulationTexture中。最后使用Graphics.Blit (accumulationTexture, dest)把结果显示到屏幕上。

RenderTexture.MarkRestoreExpected

表示预期将进行 RenderTexture 恢复操作。

在移动图形仿真模式下,当执行 RenderTexture“恢复”操作时,Unity 会发出警告。如果在不先进行清除或丢弃的情况下渲染到纹理,就会执行恢复操作。对于许多移动 GPU 和多 GPU 系统来说,这是一项代价高昂的操作,应该予以避免。

但是,如果渲染效果要求必须进行 RenderTexture 恢复,则您可以调用该函数来指示 Unity 恢复操作是预期行为,不要发出警告。

Shader代码:

Shader "Unity Shaders Book/Chapter 12/Motion Blur" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BlurAmount ("Blur Amount", Float) = 1.0
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		sampler2D _MainTex;
		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;
		}
		
        // 更新渲染纹理的RGB通道部分
		fixed4 fragRGB (v2f i) : SV_Target {
			return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
		}

		// 更新渲染纹理的A通道部分
		half4 fragA (v2f i) : SV_Target {
			return tex2D(_MainTex, i.uv);
		}
		
		ENDCG
		
		ZTest Always Cull Off ZWrite Off
		
        // 更新渲染纹理的RGB通道部分
		Pass {
			Blend SrcAlpha OneMinusSrcAlpha
			ColorMask RGB
			
			CGPROGRAM
			
			#pragma vertex vert  
			#pragma fragment fragRGB  
			
			ENDCG
		}
		
        // 更新渲染纹理的A通道部分
		Pass {   
			Blend One Zero
			ColorMask A
			   	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment fragA
			  
			ENDCG
		}
	}
 	FallBack Off
}

RGB通道版本的Shader对当前图像进行采样,并将其A通道的值设为_BlurAmount, 以便在后面混合时可以使用它的透明通道进行混合。A通道版本的代码就更简单了,直接返回采样结果。实际上,这个版本只是为了维护渲染纹理的透明通道值,不让其受到混合时使用的透明度值的影响。

之所以要把A通道和RGB通道分开,是因为在更新RGB时我们需要设置它的A通道来混合图像,但又不希望A通道的值写入渲染纹理中。
 

猜你喜欢

转载自blog.csdn.net/weixin_51327051/article/details/123048558