1. 程式人生 > >Unity Shader-Command Buffer的使用(景深與描邊效果重置版)

Unity Shader-Command Buffer的使用(景深與描邊效果重置版)

簡介

Command Buffer是Unity5新增的一個灰常灰常強大的功能。先祭出官方介紹文件。我們在渲染的時候,給OpenGL或者DX的就是一系列的指令,比如glDrawElement,glClear等等,這些東西目前是引擎去呼叫的,而Unity也為我們封裝了更高一級的API,也就是CommandBuffer,可以讓我們更加方便靈活地實現一些效果。CommandBuffer最主要的功能是可以預定義一些列的渲染指令,然後將這些指令在我們想要的時機進行執行。本篇文章簡單介紹一下CommandBuffer的使用,首先實現一個簡單的攝像機效果,然後通過Command Buffer重置一下之前實現過的兩個效果:
景深
描邊效果

CommandBuffer的基本用法

我們先來看一個最簡單的例子,直接在一張RT上畫個人,其實類似於攝影機效果,我們用當前的相機看見正常要看見的物件,然後在一張幕布(簡單的來說,就是一個。。額,面片)再渲染一次這個人物(也可以直接渲染到UI上)。
//Command Buffer測試
//by: puppet_master
//2017.5.26

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CommandBufferTest : MonoBehaviour {

    private CommandBuffer commandBuffer = null;
    private RenderTexture renderTexture = null;
    private Renderer targetRenderer = null;
    public GameObject targetObject = null;
    public Material replaceMaterial = null;

	void OnEnable()
    {
        targetRenderer = targetObject.GetComponentInChildren<Renderer>();
        //申請RT
        renderTexture = RenderTexture.GetTemporary(512, 512, 16, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, 4);
        commandBuffer = new CommandBuffer();
        //設定Command Buffer渲染目標為申請的RT
        commandBuffer.SetRenderTarget(renderTexture);
        //初始顏色設定為灰色
        commandBuffer.ClearRenderTarget(true, true, Color.gray);
        //繪製目標物件,如果沒有替換材質,就用自己的材質
        Material mat = replaceMaterial == null ? targetRenderer.sharedMaterial : replaceMaterial;
        commandBuffer.DrawRenderer(targetRenderer, mat);
        //然後接受物體的材質使用這張RT作為主紋理
        this.GetComponent<Renderer>().sharedMaterial.mainTexture = renderTexture;
        //直接加入相機的CommandBuffer事件佇列中
        Camera.main.AddCommandBuffer(CameraEvent.AfterForwardOpaque, commandBuffer);
    }

    void OnDisable()
    {
        //移除事件,清理資源
        Camera.main.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, commandBuffer);
        commandBuffer.Clear();
        renderTexture.Release();
    }

    //也可以在OnPreRender中直接通過Graphics執行Command Buffer,不過OnPreRender和OnPostRender只在掛在相機的指令碼上才有作用!!!
    //void OnPreRender()
    //{
    //    //在正式渲染前執行Command Buffer
    //    Graphics.ExecuteCommandBuffer(commandBuffer);
    //}
}
然後,我們可以把這個指令碼掛在一個物件上,將要渲染的目標拖入就可以了。我們測試一下:
Command Buffer在渲染目標的時候,是支援我們使用自定義材質的,我們可以換一個材質,如果我們做了個攝像機,還不帶美顏功能的話,肯定是要得差評的,所以,我們給渲染的物件換一個自定義的材質,比如邊緣光效果,直接將調整好的邊緣光材質球賦給Replace Material即可:

通過Command Buffer對RT進行後處理

如果感覺只是換一個材質球不夠過癮的話,我們就再發掘一下Command Buffer更深層次的功能吧!下面放大招啦!又是後處理,不過我們後處理的物件改了一下,不是基於螢幕,而是基於Command Buffer輸出的那張Render Texture。比如我們稍微修改一下上面的程式碼,增加一個最簡單的
螢幕較色
後處理(其實就是我比較懶罷了>_<)
//Command Buffer測試
//by: puppet_master
//2017.5.26

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CommandBufferTest : PostEffectBase {

    private CommandBuffer commandBuffer = null;
    private RenderTexture renderTexture = null;
    private Renderer targetRenderer = null;
    public GameObject targetObject = null;
    public Material replaceMaterial = null;

    [Range(0.0f, 3.0f)]
    public float brightness = 1.0f;//亮度
    [Range(0.0f, 3.0f)]
    public float contrast = 1.0f;  //對比度
    [Range(0.0f, 3.0f)]
    public float saturation = 1.0f;//飽和度

    void OnEnable()
    {
        targetRenderer = targetObject.GetComponentInChildren<Renderer>();
        //申請RT
        renderTexture = RenderTexture.GetTemporary(512, 512, 16, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, 4);
        commandBuffer = new CommandBuffer();
        //設定Command Buffer渲染目標為申請的RT
        commandBuffer.SetRenderTarget(renderTexture);
        //初始顏色設定為灰色
        commandBuffer.ClearRenderTarget(true, true, Color.gray);
        //繪製目標物件,如果沒有替換材質,就用自己的材質
        Material mat = replaceMaterial == null ? targetRenderer.sharedMaterial : replaceMaterial;
        commandBuffer.DrawRenderer(targetRenderer, mat);
        //然後接受物體的材質使用這張RT作為主紋理
        this.GetComponent<Renderer>().sharedMaterial.mainTexture = renderTexture;
        if (_Material)
        {
            //這是個比較危險的寫法,一張RT即作為輸入又作為輸出,在某些顯示卡上可能不支援,如果不像我這麼懶的話...還是額外申請一張RT
            commandBuffer.Blit(renderTexture, renderTexture, _Material);
        }
        //直接加入相機的CommandBuffer事件佇列中
        Camera.main.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, commandBuffer);
    }

    void OnDisable()
    {
        //移除事件,清理資源
        Camera.main.RemoveCommandBuffer(CameraEvent.BeforeForwardOpaque, commandBuffer);
        commandBuffer.Clear();
        renderTexture.Release();
    }

    //為方便調整,放在update裡面了
    void Update()
    {
        _Material.SetFloat("_Brightness", brightness);
        _Material.SetFloat("_Saturation", saturation);
        _Material.SetFloat("_Contrast", contrast);
    }
}
好了,我們美顏相機第二版本就完成了,增加了後處理之後的效果,調整了一下飽和度,亮度,對比度之後的效果如下: 動態效果: 通過上面的幾個例子,我們大概瞭解了Command Buffer能幹的事情。首先,Command Buffer可以讓我們定義一連串的渲染指令,我們可以直接在需要的時候通過Graphics.ExecuteCommandBuffer執行也可以通過Camera.AddCommandBuffer函式根據不同的CameraEvent來控制CommandBuffer執行的時間。其次,Command Buffer的效果大致等同於新建一個和主相機引數大致相同的攝像機,並且可以附加一些額外的渲染效果支援,比如替換材質渲染,對渲染的RT進行後處理等等。 不過這裡我也遇到了一個問題,當使用Command Buffer渲染時,物件的shader使用diffuse等自定義shader的話,Command Buffer渲染出來的結果有時會不對,通過Frame Debuger看的時候發現Command Buffer渲染的流程是Deffer Path的,而不是我們正常的Forward Path,而工程設定以及相機設定都為Forward Path。將shader換為自己寫的就木有這個問題。不知道是我哪裡姿勢不對還是Unity的bug。。。

“假”景深效果

所謂假景深,叫做背景虛化更加貼切一些。其實所有的渲染效果都是假的,都是是忽悠我們的眼睛,就比誰忽悠得又好又省。正常的景深效果,一般是基於深度計算,根據焦距將模糊圖與原圖插值控制焦點距離清晰,原理焦點距離模糊。but,這個效果雖然很好,但是需要深度資訊,如果是deffer或者開了實時陰影,深度自然就有了,也就無所謂了,但是一般手遊的基本這兩種都是開不得的,那麼開了深度,就會DC翻倍,再加上模糊至少十次全屏取樣(降取樣也許會好些)。個人感覺景深應該最昂貴的後處理效果之一了。而背景虛化的效果,簡單來說就是隻讓人物(或者我們需要突出的部分)清晰,其他所有部分全都模糊。這種效果不需要深度圖,可以在DC上省下很多消耗,而且有時候這種效果會比真正的景深更好,但還是需要看需求。我們如果實現這種效果,最簡單的並且容易想到的就是新建一個相機,然後將需要突出的部分放在另一個相機渲染,在場景相機上增加一個模糊的後處理,這樣,突出部分渲染後的相機疊加到場景相機的結果上,就可以只讓場景模糊而人物不模糊了。不過在有了Command Buffer之後,就不需要這麼麻煩了,我們可以直接通過Command Buffer控制模型的渲染時機,首先將正常的Renderer元件disable,然後強行通過Command Buffer讓其在後處理之後渲染:
//在後處理之後渲染
//by: puppet_master
//2017.6.5

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]

public class RenderAfterPostEffect : MonoBehaviour
{
    private CommandBuffer commandBuffer = null;
    private Renderer targetRenderer = null;

    void OnEnable()
    {
        targetRenderer = this.GetComponentInChildren<Renderer>();
        if (targetRenderer)
        {
            commandBuffer = new CommandBuffer();
            commandBuffer.DrawRenderer(targetRenderer, targetRenderer.sharedMaterial);
            //直接加入相機的CommandBuffer事件佇列中,
            Camera.main.AddCommandBuffer(CameraEvent.AfterImageEffects, commandBuffer);
            targetRenderer.enabled = false;
        }
    }

    void OnDisable()
    {
        if (targetRenderer)
        {
            //移除事件,清理資源
            Camera.main.RemoveCommandBuffer(CameraEvent.AfterImageEffects, commandBuffer);
            commandBuffer.Clear();
            targetRenderer.enabled = true;
        }
    }
}
上面的指令碼只是控制修改模型渲染時機的,然後,我們可以在主相機上掛一個高斯模糊的後處理,調整引數後就可以得到背景虛化的效果了: 當然,不僅僅是模糊效果,掛了該指令碼的物件會被所有後處理所“拋棄”,我們也可以放一些其他的後處理效果,比如漩渦扭曲
Command Buffer的這個功能是個人感覺最有用的一個功能,可以進一步地控制某個物件的渲染序列,讓我們更加方便地實現一些效果。

描邊效果Command Buffer實現

描邊效果(後處理版本)首先將要描邊的物件用描邊色渲染到一張RT上,然後將RT進行模糊操作,使物件外擴,再用外擴的RT減去原始RT就得到了輪廓部分,最後再將輪廓部分與原圖混合就得到了最終的描邊效果。描邊效果將物件渲染到RT上之前通過額外建立一個攝像機實現的,有了Command Buffer,我們就可以直接在原始相機上進行這個操作,大大簡化了程式的複雜度。關於描邊效果的具體原理,可以參考本人之前的文章,這裡就不多說了,直接上程式碼。補充說明:PostEffectBase類是所有本人之前做的後處理效果的基類。 C#指令碼部分:
/********************************************************************
 FileName: OutlinePostEffectCmdBuffer.cs
 Description: 後處理描邊效果CommandBuffer版本
 Created: 2017/06/07
 by puppet_master
*********************************************************************/
using UnityEngine;
using System.Collections;
using UnityEngine.Rendering;

public class OutlinePostEffectCmdBuffer : PostEffectBase
{
    private RenderTexture renderTexture = null;
    private CommandBuffer commandBuffer = null;
    private Material outlineMaterial = null;
    //描邊prepass shader(渲染純色貼圖的shader)
    public Shader outlineShader = null;
    //取樣率
    public float samplerScale = 1;
    //降取樣
    public int downSample = 1;
    //迭代次數
    public int iteration = 2;
    //描邊顏色
    public Color outLineColor = Color.green;
    //描邊強度
    [Range(0.0f, 10.0f)]
    public float outLineStrength = 3.0f;
    //目標物件
    public GameObject targetObject = null;


    void OnEnable()
    {
        if (outlineShader == null)
            return;
        if (outlineMaterial == null)
            outlineMaterial = new Material(outlineShader);
        Renderer[] renderers = targetObject.GetComponentsInChildren<Renderer>();
        if (renderTexture == null)
            renderTexture = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 0);
        //建立描邊prepass的command buffer
        commandBuffer = new CommandBuffer();
        commandBuffer.SetRenderTarget(renderTexture);
        commandBuffer.ClearRenderTarget(true, true, Color.black);
        foreach (Renderer r in renderers)
            commandBuffer.DrawRenderer(r, outlineMaterial);
    }

    void OnDisable()
    {
        if (renderTexture)
        {
            RenderTexture.ReleaseTemporary(renderTexture);
            renderTexture = null;
        }
        if (outlineMaterial)
        {
            DestroyImmediate(outlineMaterial);
            outlineMaterial = null;
        }
        if (commandBuffer != null)
        {
            commandBuffer.Release();
            commandBuffer = null;
        }
           
    }

    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (_Material && renderTexture && outlineMaterial && commandBuffer != null)
        {
            //通過Command Buffer可以設定自定義材質的顏色
            outlineMaterial.SetColor("_OutlineCol", outLineColor);
            //直接通過Graphic執行Command Buffer
            Graphics.ExecuteCommandBuffer(commandBuffer);

            //對RT進行Blur處理
            RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);
            RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);

            //高斯模糊,兩次模糊,橫向縱向,使用pass0進行高斯模糊
            _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
            Graphics.Blit(renderTexture, temp1, _Material, 0);
            _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
            Graphics.Blit(temp1, temp2, _Material, 0);

            //如果有疊加再進行迭代模糊處理
            for(int i = 0; i < iteration; i++)
            {
                _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
                Graphics.Blit(temp2, temp1, _Material, 0);
                _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
                Graphics.Blit(temp1, temp2, _Material, 0);
            }

            //用模糊圖和原始圖計算出輪廓圖
            _Material.SetTexture("_BlurTex", temp2);
            Graphics.Blit(renderTexture, temp1, _Material, 1);

            //輪廓圖和場景圖疊加
            _Material.SetTexture("_BlurTex", temp1);
            _Material.SetFloat("_OutlineStrength", outLineStrength);
            Graphics.Blit(source, destination, _Material, 2);

            RenderTexture.ReleaseTemporary(temp1);
            RenderTexture.ReleaseTemporary(temp2);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
    }


}
描邊後處理部分shader:
//後處理描邊Shader
//by:puppet_master
//2017.6.7

Shader "Custom/OutLinePostEffect" {

	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_BlurTex("Blur", 2D) = "white"{}
	}

	CGINCLUDE
	#include "UnityCG.cginc"
	
	//用於剔除中心留下輪廓
	struct v2f_cull
	{
		float4 pos : SV_POSITION;
		float2 uv : TEXCOORD0;
	};

	//用於模糊
	struct v2f_blur
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float4 uv01 : TEXCOORD1;
		float4 uv23 : TEXCOORD2;
		float4 uv45 : TEXCOORD3;
	};

	//用於最後疊加
	struct v2f_add
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 uv1 : TEXCOORD1;
	};

	sampler2D _MainTex;
	float4 _MainTex_TexelSize;
	sampler2D _BlurTex;
	float4 _BlurTex_TexelSize;
	float4 _offsets;
	float _OutlineStrength;

	//Blur圖和原圖進行相減獲得輪廓
	v2f_cull vert_cull(appdata_img v)
	{
		v2f_cull o;
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		o.uv = v.texcoord.xy;
		//dx中紋理從左上角為初始座標,需要反向
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_cull(v2f_cull i) : SV_Target
	{
		fixed4 colorMain = tex2D(_MainTex, i.uv);
		fixed4 colorBlur = tex2D(_BlurTex, i.uv);
		//最後的顏色是_BlurTex - _MainTex,周圍0-0=0,黑色;邊框部分為描邊顏色-0=描邊顏色;中間部分為描邊顏色-描邊顏色=0。最終輸出只有邊框
		//return fixed4((colorBlur - colorMain).rgb, 1);
		return colorBlur - colorMain;
	}

	//高斯模糊 vert shader(之前的文章有詳細註釋,此處也可以用BoxBlur,更省一點)
	v2f_blur vert_blur(appdata_img v)
	{
		v2f_blur o;
		_offsets *= _MainTex_TexelSize.xyxy;
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		o.uv = v.texcoord.xy;

		o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);
		o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;
		o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;

		return o;
	}

	//高斯模糊 pixel shader
	fixed4 frag_blur(v2f_blur i) : SV_Target
	{
		fixed4 color = fixed4(0,0,0,0);
		color += 0.40 * tex2D(_MainTex, i.uv);
		color += 0.15 * tex2D(_MainTex, i.uv01.xy);
		color += 0.15 * tex2D(_MainTex, i.uv01.zw);
		color += 0.10 * tex2D(_MainTex, i.uv23.xy);
		color += 0.10 * tex2D(_MainTex, i.uv23.zw);
		color += 0.05 * tex2D(_MainTex, i.uv45.xy);
		color += 0.05 * tex2D(_MainTex, i.uv45.zw);
		return color;
	}

	//最終疊加 vertex shader
	v2f_add vert_add(appdata_img v)
	{
		v2f_add o;
		//mvp矩陣變換
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		//uv座標傳遞
		o.uv.xy = v.texcoord.xy;
		o.uv1.xy = o.uv.xy;
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_add(v2f_add i) : SV_Target
	{
		//取原始場景圖片進行取樣
		fixed4 ori = tex2D(_MainTex, i.uv1);
		//取得到的輪廓圖片進行取樣
		fixed4 blur = tex2D(_BlurTex, i.uv);
		fixed4 final = ori + blur * _OutlineStrength;
		return final;
	}

		ENDCG

	SubShader
	{
		//pass 0: 高斯模糊
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_blur
			#pragma fragment frag_blur
			ENDCG
		}
		
		//pass 1: 剔除中心部分 
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_cull
			#pragma fragment frag_cull
			ENDCG
		}


		//pass 2: 最終疊加
		Pass
		{

			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_add
			#pragma fragment frag_add
			ENDCG
		}

	}
}
描邊PrePass部分shader:
//描邊Shader
//by:puppet_master
//2017.6.7

Shader "ApcShader/OutlinePrePass"
{
	//子著色器	
	SubShader
	{
		//描邊使用兩個Pass,第一個pass沿法線擠出一點,只輸出描邊的顏色
		Pass
		{	
			CGPROGRAM
			#include "UnityCG.cginc"
			fixed4 _OutlineCol;
			
			struct v2f
			{
				float4 pos : SV_POSITION;
			};
			
			v2f vert(appdata_full v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				//這個Pass直接輸出描邊顏色
				return _OutlineCol;
			}
			
			//使用vert函式和frag函式
			#pragma vertex vert
			#pragma fragment frag
			ENDCG
		}
	}
}
將描邊prepass和後處理shader賦給指令碼,並將需要描邊的物件賦給targetObject,就可以看到描邊效果了: 與之前的描邊效果一致,但是程式碼更簡潔。並且描邊顏色可以直接在指令碼中設定。順便來一發好玩的效果,調整Sampler Scale以及OutLineStrength: