第十八章 Post-Processing

第十八章 Post-Processing

Post-processing是指在場景渲染之後,使用一些圖形技術對場景進行處理。比如,把整個場景轉換爲grayscale(灰度)樣式或使場景中明亮的區域發光。本章將編寫一些post-processing effects,並集成到C++渲染引擎框架中。

Render Targets

到目前爲止,所有的示例程序都是直接把場景渲染到back buffer,這是一個2D texture用於在渲染完成之後把圖像顯示到屏幕上。但是在post-processing應用程序中,首先把場景渲染到一箇中間層的texture buffer中,然後把post-processing effect應用到該texture。最後使用一個全屏的四邊形(由兩個三角形組成,包含整個屏幕區域)渲染最終要顯示到屏幕上的圖像。
下面的步驟概括了post-processing的處理過程:
1、將一個off-screen render target(離屏渲染目標)綁定到管線的output-merger階段。
2、在off-scrent render target上繪製場景。
3、恢復back buffer,並作爲綁定到output-merger階段的render target。
4、使用off-scrent render target的texture作爲一種post-processing effect的輸入buffer,繪製一個全屏的四邊形。
爲了更容易的執行這些步驟,首先創建一個FullScreenRenderTarget類,該類的聲明代碼如列表18.1所示。

列表18.1 Declaration of the FullScreenRenderTarget Class

#pragma once

#include "Common.h"

namespace Library
{
    class Game;

    class FullScreenRenderTarget
    {
    public:
        FullScreenRenderTarget(Game& game);
        ~FullScreenRenderTarget();

        ID3D11ShaderResourceView* OutputTexture() const;
        ID3D11RenderTargetView* RenderTargetView() const;
        ID3D11DepthStencilView* DepthStencilView() const;

        void Begin();
        void End();

    private:
        FullScreenRenderTarget();
        FullScreenRenderTarget(const FullScreenRenderTarget& rhs);
        FullScreenRenderTarget& operator=(const FullScreenRenderTarget& rhs);

        Game* mGame;
        ID3D11RenderTargetView* mRenderTargetView;
        ID3D11DepthStencilView* mDepthStencilView;
        ID3D11ShaderResourceView* mOutputTexture;
    };
}


FullScreenRenderTarget類中的成員變量看起來非常熟悉,在Game類中已經包含了同樣的數據類型ID3D11RenderTargetView和ID3D11DepthStencilView。這些類型的變量是用於把一個render target和depth-stencil buffer綁定到管線的output-merger階段。與Game類不同的是,在FullScrentRenderTarget類中還包含有一個ID3D11ShaderResourceView類型的成員變量,表示render target底層的2D texture buffer。這種類型的輸出texture可以作爲post-processing shaders的輸入數據。FullScreenRenderTarget::Begin()和FullScreenRenderTarget::End()函數分別用於把render target綁定以output-merger階段和恢復back buffer。列表18.2列出了FullScrennRenderTarget類的實現代碼。

列表18.2 Implementation of the FullScreenRenderTarget Class

#include "FullScreenRenderTarget.h"
#include "Game.h"
#include "GameException.h"

namespace Library
{
    FullScreenRenderTarget::FullScreenRenderTarget(Game& game)
        : mGame(&game), mRenderTargetView(nullptr), mDepthStencilView(nullptr), mOutputTexture(nullptr)
    {
        D3D11_TEXTURE2D_DESC fullScreenTextureDesc;
        ZeroMemory(&fullScreenTextureDesc, sizeof(fullScreenTextureDesc));
        fullScreenTextureDesc.Width = game.ScreenWidth();
        fullScreenTextureDesc.Height = game.ScreenHeight();
        fullScreenTextureDesc.MipLevels = 1;
        fullScreenTextureDesc.ArraySize = 1;
        fullScreenTextureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
        fullScreenTextureDesc.SampleDesc.Count = 1;
        fullScreenTextureDesc.SampleDesc.Quality = 0;
        fullScreenTextureDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
        fullScreenTextureDesc.Usage = D3D11_USAGE_DEFAULT;

        HRESULT hr;
        ID3D11Texture2D* fullScreenTexture = nullptr;
        if (FAILED(hr = game.Direct3DDevice()->CreateTexture2D(&fullScreenTextureDesc, nullptr, &fullScreenTexture)))
        {
            throw GameException("IDXGIDevice::CreateTexture2D() failed.", hr);
        }

        if (FAILED(hr = game.Direct3DDevice()->CreateShaderResourceView(fullScreenTexture, nullptr, &mOutputTexture)))
        {
            throw GameException("IDXGIDevice::CreateShaderResourceView() failed.", hr);
        }

        if (FAILED(hr = game.Direct3DDevice()->CreateRenderTargetView(fullScreenTexture, nullptr, &mRenderTargetView)))
        {
            ReleaseObject(fullScreenTexture);
            throw GameException("IDXGIDevice::CreateRenderTargetView() failed.", hr);
        }

        ReleaseObject(fullScreenTexture);

        D3D11_TEXTURE2D_DESC depthStencilDesc;
        ZeroMemory(&depthStencilDesc, sizeof(depthStencilDesc));
        depthStencilDesc.Width = game.ScreenWidth();
        depthStencilDesc.Height = game.ScreenHeight();
        depthStencilDesc.MipLevels = 1;
        depthStencilDesc.ArraySize = 1;
        depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
        depthStencilDesc.SampleDesc.Count = 1;
        depthStencilDesc.SampleDesc.Quality = 0;
        depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
        depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;            

        ID3D11Texture2D* depthStencilBuffer = nullptr;
        if (FAILED(hr = game.Direct3DDevice()->CreateTexture2D(&depthStencilDesc, nullptr, &depthStencilBuffer)))
        {
            throw GameException("IDXGIDevice::CreateTexture2D() failed.", hr);
        }

        if (FAILED(hr = game.Direct3DDevice()->CreateDepthStencilView(depthStencilBuffer, nullptr, &mDepthStencilView)))
        {
            ReleaseObject(depthStencilBuffer);
            throw GameException("IDXGIDevice::CreateDepthStencilView() failed.", hr);
        }

        ReleaseObject(depthStencilBuffer);
    }

    FullScreenRenderTarget::~FullScreenRenderTarget()
    {
        ReleaseObject(mOutputTexture);
        ReleaseObject(mDepthStencilView);
        ReleaseObject(mRenderTargetView);
    }

    ID3D11ShaderResourceView* FullScreenRenderTarget::OutputTexture() const
    {
        return mOutputTexture;
    }

    ID3D11RenderTargetView* FullScreenRenderTarget::RenderTargetView() const
    {
        return mRenderTargetView;
    }

    ID3D11DepthStencilView* FullScreenRenderTarget::DepthStencilView() const
    {
        return mDepthStencilView;
    }

    void FullScreenRenderTarget::Begin()
    {	
        mGame->Direct3DDeviceContext()->OMSetRenderTargets(1, &mRenderTargetView, mDepthStencilView);
    }

    void FullScreenRenderTarget::End()
    {
        mGame->ResetRenderTargets();
    }
}


在FullScreenRenderTarget類的構造函數中,包含了該類的大部分實現代碼。在該構造函數中,首先構建一個D3D11_TEXTURE2D_DESC結構體類型的實例,並用於創建render target的底層texture buffer。在Game類的初始化過程中這些步驟並不是顯式定義的,因爲在構建swap chain時就會隱含的創建back buffer。其中在D3D11_TEXTURE2D_DESC結構體實例化時指定了兩個bind flags:D3D11_BIND_RENDER_TARGET和D3D_BIND_SHADER_RESOURCE。這兩個flags值表示該texture可以同時作爲一個render target和一個shader的輸入數據。
創建完ID3D11Texture2D類型的texture之後,就可以使用該texture創建shader resource view和render target view。並對外提供了訪問這兩個views變量的公有函數接口;並且在這兩個變量初始化之後就可以釋放對texture的引用了。對於depth-stencil view變量的初始化使用類似的操作步驟。
在FullScrentRenderTarget::Begin()函數中,直接調用ID3D11DeviceContext::OMSetRenderTargets()函數把render target view和depth-stencil view綁定到管線的output-merger階段。而FullScreenRenderTarget::End()函數中則是調用Game::ResetRenderTargets()函數,該函數同樣是調用OMSetRenderTargets()函數重置綁定爲Game類的中render target view和detph-stencil view變量。
對於FullScreenRenderTarget類的調用方式如下:
mRenderTarget->Begin();
// 1. Clear mRenderTarget->RenderTargetView()
// 2. Clear mRenderTarget->DepthStencilView()
// 3. Draw Objects

mRenderTarget->End();


這種方法首先把objects渲染到FullScreenRenderTarget實例下的2D texture,並且可以通過調用FullScreenRenderTarget::OutputTexture()函數訪問該texture變量。最後調用FullScreenRenderTarget::End()函數,就可以把所有的渲染數據寫到back buffer中。

A Full-Screen Quad Component

現在我們可以把場景渲染到一個off-screen texture中,接下來需要把一種effect應用到該texture並在屏幕上顯示應用的輸出結果。在這樣一個系統結構中需要封裝渲染部分的代碼,否則在程序之間將會出現重複多餘的代碼,但是又要使渲染代碼足夠靈活,以及支持任意的post-processing effects。列表18.3列出了FullScreenQuad類的聲明代碼。

列表18.3 Declaration of the FullScreenQuad Class

#pragma once

#include <functional>
#include "DrawableGameComponent.h"

namespace Library
{
	class Effect;
	class Material;
	class Pass;

	class FullScreenQuad : public DrawableGameComponent
	{
		RTTI_DECLARATIONS(FullScreenQuad, DrawableGameComponent)

	public:
		FullScreenQuad(Game& game);
		FullScreenQuad(Game& game, Material& material);
		~FullScreenQuad();

		Material* GetMaterial();
		void SetMaterial(Material& material, const std::string& techniqueName, const std::string& passName);
		void SetActiveTechnique(const std::string& techniqueName, const std::string& passName);
		void SetCustomUpdateMaterial(std::function<void()> callback);

		virtual void Initialize() override;
		virtual void Draw(const GameTime& gameTime) override;

	private:
		FullScreenQuad();
		FullScreenQuad(const FullScreenQuad& rhs);
		FullScreenQuad& operator=(const FullScreenQuad& rhs);

		Material* mMaterial;
		Pass* mPass;
		ID3D11InputLayout* mInputLayout;

		ID3D11Buffer* mVertexBuffer;
		ID3D11Buffer* mIndexBuffer;
		UINT mIndexCount;
		std::function<void()> mCustomUpdateMaterial;
	};
}


在FullScreenQuad類中,成員變量mMaterial指向一個用於繪製四邊形的material對象,並可以通過調用SetMaterial函數對該變量進行賦值。這樣就可以在同一個FullScreenQuad實例對象的生存期內使用不同的materials。成員變量mPass和mInputLayout中存儲了Draw()函數中使用的相關數據。另外兩個非常熟悉的變量vertex和index buffers存儲了該四邊形對象的vertices和indices。在FullScreenQuad類中使用了一種新的數據類型std::function<T>,如果你之前沒有見過的話,這是一種通用的函數封裝語法,用於存儲並調用函數,bind表達式,lambda表達式(回調以及關閉)。在這裏是爲了支持FullScreenQuad的調用者更新material shader中的變量。之所以這樣做是因爲在FullScreenQuad類中並不知道用於渲染四邊形的是哪種material(因此也就無法知道material使用的shader輸入變量)。FullScreenQuad類要完成的全部操作就是渲染一個四邊形;而更新material變量的操作由該類對象的調用者完成。
列表18.4中列出了FullScreenQuad類的實現代碼。

列表18.4 Implementation of the FullScreenQuad Class

#include "FullScreenQuad.h"
#include "Game.h"
#include "GameException.h"
#include "Material.h"
#include "VertexDeclarations.h"

namespace Library
{
	RTTI_DEFINITIONS(FullScreenQuad)

		FullScreenQuad::FullScreenQuad(Game& game)
		: DrawableGameComponent(game),
		mMaterial(nullptr), mPass(nullptr), mInputLayout(nullptr),
		mVertexBuffer(nullptr), mIndexBuffer(nullptr), mIndexCount(0), mCustomUpdateMaterial(nullptr)
	{
	}

	FullScreenQuad::FullScreenQuad(Game& game, Material& material)
		: DrawableGameComponent(game),
		mMaterial(&material), mPass(nullptr), mInputLayout(nullptr),
		mVertexBuffer(nullptr), mIndexBuffer(nullptr), mIndexCount(0), mCustomUpdateMaterial(nullptr)
	{
	}

	FullScreenQuad::~FullScreenQuad()
	{
		ReleaseObject(mIndexBuffer);
		ReleaseObject(mVertexBuffer);
	}

	Material* FullScreenQuad::GetMaterial()
	{
		return mMaterial;
	}

	void FullScreenQuad::SetMaterial(Material& material, const std::string& techniqueName, const std::string& passName)
	{
		mMaterial = &material;
		SetActiveTechnique(techniqueName, passName);
	}

	void FullScreenQuad::SetActiveTechnique(const std::string& techniqueName, const std::string& passName)
	{
		Technique* technique = mMaterial->GetEffect()->TechniquesByName().at(techniqueName);
		assert(technique != nullptr);

		mPass = technique->PassesByName().at(passName);
		assert(mPass != nullptr);
		mInputLayout = mMaterial->InputLayouts().at(mPass);
	}

	void FullScreenQuad::SetCustomUpdateMaterial(std::function<void()> callback)
	{
		mCustomUpdateMaterial = callback;
	}

	void FullScreenQuad::Initialize()
	{
		VertexPositionTexture vertices[] =
		{
			VertexPositionTexture(XMFLOAT4(-1.0f, -1.0f, 0.0f, 1.0f), XMFLOAT2(0.0f, 1.0f)),
			VertexPositionTexture(XMFLOAT4(-1.0f, 1.0f, 0.0f, 1.0f), XMFLOAT2(0.0f, 0.0f)),
			VertexPositionTexture(XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f), XMFLOAT2(1.0f, 0.0f)),
			VertexPositionTexture(XMFLOAT4(1.0f, -1.0f, 0.0f, 1.0f), XMFLOAT2(1.0f, 1.0f)),
		};

		D3D11_BUFFER_DESC vertexBufferDesc;
		ZeroMemory(&vertexBufferDesc, sizeof(vertexBufferDesc));
		vertexBufferDesc.ByteWidth = sizeof(VertexPositionTexture)* ARRAYSIZE(vertices);
		vertexBufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
		vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

		D3D11_SUBRESOURCE_DATA vertexSubResourceData;
		ZeroMemory(&vertexSubResourceData, sizeof(vertexSubResourceData));
		vertexSubResourceData.pSysMem = vertices;
		if (FAILED(mGame->Direct3DDevice()->CreateBuffer(&vertexBufferDesc, &vertexSubResourceData, &mVertexBuffer)))
		{
			throw GameException("ID3D11Device::CreateBuffer() failed.");
		}

		UINT indices[] =
		{
			0, 1, 2,
			0, 2, 3
		};

		mIndexCount = ARRAYSIZE(indices);

		D3D11_BUFFER_DESC indexBufferDesc;
		ZeroMemory(&indexBufferDesc, sizeof(indexBufferDesc));
		indexBufferDesc.ByteWidth = sizeof(UINT)* mIndexCount;
		indexBufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
		indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;

		D3D11_SUBRESOURCE_DATA indexSubResourceData;
		ZeroMemory(&indexSubResourceData, sizeof(indexSubResourceData));
		indexSubResourceData.pSysMem = indices;
		if (FAILED(mGame->Direct3DDevice()->CreateBuffer(&indexBufferDesc, &indexSubResourceData, &mIndexBuffer)))
		{
			throw GameException("ID3D11Device::CreateBuffer() failed.");
		}
	}

	void FullScreenQuad::Draw(const GameTime& gameTime)
	{
		assert(mPass != nullptr);
		assert(mInputLayout != nullptr);

		ID3D11DeviceContext* direct3DDeviceContext = mGame->Direct3DDeviceContext();
		direct3DDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
		direct3DDeviceContext->IASetInputLayout(mInputLayout);

		UINT stride = sizeof(VertexPositionTexture);
		UINT offset = 0;
		direct3DDeviceContext->IASetVertexBuffers(0, 1, &mVertexBuffer, &stride, &offset);
		direct3DDeviceContext->IASetIndexBuffer(mIndexBuffer, DXGI_FORMAT_R32_UINT, 0);

		if (mCustomUpdateMaterial != nullptr)
		{
			mCustomUpdateMaterial();
		}

		mPass->Apply(0, direct3DDeviceContext);

		direct3DDeviceContext->DrawIndexed(mIndexCount, 0, 0);
	}
}


首先分析FullScreenQuad::Initialize()函數,該函數的實現與之前示例中的代碼基本相同,主要不同的地方是4個vertices所處的空間位置。在screen space中,(-1, -1)表示屏幕的左上角,(1, 1)表示右下角。因爲vertices的座標位置已經是處於screen space中,所以在vertext shader中不需要對這些座標執行變換。
接下來,分析FullScreen::Draw()函數。除了執行std::function類型的mCustomUpdateMaterial()語句之外,在Draw()函數沒有特別需要說明的地方。其中std::function<T>類是一個函數對象(或函數),並提供了公有的operator()函數(重載()運算符)。使用這種方式,看起來就像是調用了一個命名爲mCustomUpdateMaterial的函數;實際上,只是調用了std::function<T>::operation()運算符重載函數,真正的目的是用於回調函數或lambda表達式。
最後,需要注意的是四邊形的vertices由一個position和texture coordinates表示。這種表示方式限制了在FullScreenQuad類中能夠使用的material種類,但是該依然具有多種應用。只需要對該類做一點點擴展,就可以根據具體的material動態的創建vertex buffer。
發佈了5 篇原創文章 · 獲贊 27 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章