Gaussian Blurring
Color filtering只是使用post-processing生成的衆多effects中的一種。另一種常用的technique是對渲染texture進行模糊(blur)處理。有多種方法可以實現模糊的效果,在本書上主要使用Gaussian blurring(高斯模糊),這種方法的名稱來自於高斯方程(也稱爲正態分佈),主要用於圖像模糊處理。圖像模糊處理是指,每一個pixel的顏色值通過採樣與該pixel相鄰的pixels計算得到。主要解決的問題是如何計算採樣pixels的加權平均值作爲最終的pixel顏色值。可以使用如下的高斯方程計算權重值:
雖然我們要使用上面的高斯方程對一個二維的texture進行模糊處理,但是該函數本身只表示一維的。高斯模糊是可以分離的,意味一個二維的模糊處理可以使用兩個獨立的一維運算。在實際操作中,就是對圖像先執行水平維度的模糊再進行垂直模糊處理。
繪製一個高斯模糊的effect可以分爲以下三個步驟:
1、把場景繪製到一個off-screen render target中。
2、對場景texture執行水平模糊,並保存到off-screen render target中。
3、對上一步經過水平模糊的texture再執行垂直模糊,並把最終的texture渲染到屏幕上。
A Gaussian Blurring Shader
列表18.8列出了GaussianBlur.fx effect的代碼。其中vertex inputs和vertex shader部分與ColorFilter.fx effect中的完全一樣,不同的是新增了SampleOffsets和SampleWeights數組。其中SampleOffsets數組存儲要採樣的與當前計算的pixel相鄰的pixels的位置值。而SampleWeights數組中存儲了每一個採樣的pixels的權重係數。列表18.8 The GaussianBlur.fx Effect
/************* Resources *************/
#define SAMPLE_COUNT 9
cbuffer CBufferPerFrame
{
float2 SampleOffsets[SAMPLE_COUNT];
float SampleWeights[SAMPLE_COUNT];
}
Texture2D ColorTexture;
SamplerState TrilinearSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
/************* Data Structures *************/
struct VS_INPUT
{
float4 ObjectPosition : POSITION;
float2 TextureCoordinate : TEXCOORD;
};
struct VS_OUTPUT
{
float4 Position : SV_Position;
float2 TextureCoordinate : TEXCOORD;
};
/************* Vertex Shader *************/
VS_OUTPUT vertex_shader(VS_INPUT IN)
{
VS_OUTPUT OUT = (VS_OUTPUT)0;
OUT.Position = IN.ObjectPosition;
OUT.TextureCoordinate = IN.TextureCoordinate;
return OUT;
}
/************* Pixel Shaders *************/
float4 blur_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = (float4)0;
for (int i = 0; i < SAMPLE_COUNT; i++)
{
color += ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate + SampleOffsets[i]) * SampleWeights[i];
}
return color;
}
float4 no_blur_pixel_shader(VS_OUTPUT IN) : SV_Target
{
return ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
}
/************* Techniques *************/
technique11 blur
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, blur_pixel_shader()));
}
}
technique11 no_blur
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, no_blur_pixel_shader()));
}
}
在這個effect中,採樣的數量被硬編碼爲9,但是你可以根據需要修改effec以支持其他的採樣數量值,或者包含多種technique,每一種對應一個通用的採樣數量傳遞給pixel shader。一般情況下,都是使用一個相鄰pixels組成的grid包圍要計算的pixel,因此採樣數量都使用一個奇數值。但是,需要知道的是採樣數量越多,pixel shader所要執行的運算量越大。
在pixel shader中,主要使用SampleOffsets數組的偏移值遍歷採樣相鄰的pixels。
A Gaussian Blurring Component
要在C++渲染引擎中集成blur shader,首先創建一個GaussianBlur類,該類的聲明代碼如列表18.9所示。列表18.9 Declaration of the GaussianBlur Class
#pragma once
#include "Common.h"
#include "DrawableGameComponent.h"
namespace Library
{
class Effect;
class GaussianBlurMaterial;
class FullScreenRenderTarget;
class FullScreenQuad;
class GaussianBlur : public DrawableGameComponent
{
RTTI_DECLARATIONS(GaussianBlur, DrawableGameComponent)
public:
GaussianBlur(Game& game, Camera& camera);
GaussianBlur(Game& game, Camera& camera, float blurAmount);
~GaussianBlur();
ID3D11ShaderResourceView* SceneTexture();
void SetSceneTexture(ID3D11ShaderResourceView& sceneTexture);
ID3D11ShaderResourceView* OutputTexture();
float BlurAmount() const;
void SetBlurAmount(float blurAmount);
virtual void Initialize() override;
virtual void Draw(const GameTime& gameTime) override;
void DrawToTexture(const GameTime& gameTime);
private:
GaussianBlur();
GaussianBlur(const GaussianBlur& rhs);
GaussianBlur& operator=(const GaussianBlur& rhs);
void InitializeSampleOffsets();
void InitializeSampleWeights();
float GetWeight(float x) const;
void UpdateGaussianMaterialWithHorizontalOffsets();
void UpdateGaussianMaterialWithVerticalOffsets();
void UpdateGaussianMaterialNoBlur();
static const float DefaultBlurAmount;
Effect* mEffect;
GaussianBlurMaterial* mMaterial;
ID3D11ShaderResourceView* mSceneTexture;
ID3D11ShaderResourceView* mOutputTexture;
FullScreenRenderTarget* mHorizontalBlurTarget;
FullScreenRenderTarget* mVerticalBlurTarget;
FullScreenQuad* mFullScreenQuad;
std::vector<XMFLOAT2> mHorizontalSampleOffsets;
std::vector<XMFLOAT2> mVerticalSampleOffsets;
std::vector<float> mSampleWeights;
float mBlurAmount;
};
}
在GaussianBlur類中包含了用於表示Gaussian blurring effect的成員變量,如對應的material,以及用於存儲輸入的scene texture(要進行模糊處理的texture圖像)。還有一個render target用於水平模糊操作,並把輸出的texture作爲垂直模糊的輸入texture。所有的渲染(包括渲染到render target或渲染到屏幕)操作都使用full-screen quad成員變量。
其中成員變量horizontal和vertical sample offsets,以及sample weights分別由函數InitializeSampleOffsets()和InitializeSampleWeights()進行初始化。GetWeight()函數使用高斯方程計算單個權重值。以“UpdateGaussianMaterial”開頭的三個函數是full-screen quad的回調函數,根據模糊處理過程的不同階段調用不同的函數。其中UpdateGaussianMaterialNoBlur()函數用於mBlurAmout爲0的情況。在這種情況下,shader中的no_blur technique會被應用到scene texture中,只是簡單地把未修改的texture渲染到屏幕上。
通過SetBlurAmount()函數可以在程序運行時修改blur數量值。前面已經講過,blur amount值表示高斯方程的σ係數,因此每次改變blur amount時,都需要調用InitializeSampleWeight()函數(用於重新計算採樣的權重)。列表18.10列出了初始化sample offsets和weights變量的函數代碼。
列表18.10 Initializing the Gaussian Blurring Sample Offsets and Weights
void GaussianBlur::InitializeSampleOffsets()
{
float horizontalPixelSize = 1.0f / mGame->ScreenWidth();
float verticalPixelSize = 1.0f / mGame->ScreenHeight();
UINT sampleCount = mMaterial->SampleOffsets().TypeDesc().Elements;
mHorizontalSampleOffsets.resize(sampleCount);
mVerticalSampleOffsets.resize(sampleCount);
mHorizontalSampleOffsets[0] = Vector2Helper::Zero;
mVerticalSampleOffsets[0] = Vector2Helper::Zero;
for (UINT i = 0; i < sampleCount / 2; i++)
{
float sampleOffset = i * 2 + 1.5f;
float horizontalOffset = horizontalPixelSize * sampleOffset;
float verticalOffset = verticalPixelSize * sampleOffset;
mHorizontalSampleOffsets[i * 2 + 1] = XMFLOAT2(horizontalOffset, 0.0f);
mHorizontalSampleOffsets[i * 2 + 2] = XMFLOAT2(-horizontalOffset, 0.0f);
mVerticalSampleOffsets[i * 2 + 1] = XMFLOAT2(0.0f, verticalOffset);
mVerticalSampleOffsets[i * 2 + 2] = XMFLOAT2(0.0f, -verticalOffset);
}
}
void GaussianBlur::InitializeSampleWeights()
{
UINT sampleCount = mMaterial->SampleOffsets().TypeDesc().Elements;
mSampleWeights.resize(sampleCount);
mSampleWeights[0] = GetWeight(0);
float totalWeight = mSampleWeights[0];
for (UINT i = 0; i < sampleCount / 2; i++)
{
float weight = GetWeight((float)i + 1);
mSampleWeights[i * 2 + 1] = weight;
mSampleWeights[i * 2 + 2] = weight;
totalWeight += weight * 2;
}
// Normalize the weights so that they sum to one
for (UINT i = 0; i < mSampleWeights.size(); i++)
{
mSampleWeights[i] /= totalWeight;
}
}
在InitializeSampleOffset()函數中,根據應用程序當前的分辨率大小,創建圍繞中心pixel(索引值爲0)的垂直和水平方向的偏移值數組。在InitializeSampleWeights()函數中把權重值賦值數組中對應索引指向的值,並對這些權重值進行規範化以確保權重總和爲1。如果不對權重值進行規範化,就會導致提高或降低最終圖像的亮度。
列表18.11列出了GaussianBlur::Draw()函數的代碼。
列表18.11 Drawing the Gaussian Blurring Component
void GaussianBlur::Draw(const GameTime& gameTime)
{
mOutputTexture = nullptr;
if (mBlurAmount > 0.0f)
{
// Horizontal blur
mHorizontalBlurTarget->Begin();
mGame->Direct3DDeviceContext()->ClearRenderTargetView(mHorizontalBlurTarget->RenderTargetView() , reinterpret_cast<const float*>(&ColorHelper::Purple));
mGame->Direct3DDeviceContext()->ClearDepthStencilView(mHorizontalBlurTarget->DepthStencilView(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
mFullScreenQuad->SetActiveTechnique("blur", "p0");
mFullScreenQuad->SetCustomUpdateMaterial(std::bind(&GaussianBlur::UpdateGaussianMaterialWithHorizontalOffsets, this));
mFullScreenQuad->Draw(gameTime);
mHorizontalBlurTarget->End();
// Vertical blur for the final image
mFullScreenQuad->SetCustomUpdateMaterial(std::bind(&GaussianBlur::UpdateGaussianMaterialWithVerticalOffsets, this));
mFullScreenQuad->Draw(gameTime);
}
else
{
mFullScreenQuad->SetActiveTechnique("no_blur", "p0");
mFullScreenQuad->SetCustomUpdateMaterial(std::bind(&GaussianBlur::UpdateGaussianMaterialNoBlur, this));
mFullScreenQuad->Draw(gameTime);
}
}
void GaussianBlur::UpdateGaussianMaterialWithHorizontalOffsets()
{
mMaterial->ColorTexture() << mSceneTexture;
mMaterial->SampleWeights() << mSampleWeights;
mMaterial->SampleOffsets() << mHorizontalSampleOffsets;
}
void GaussianBlur::UpdateGaussianMaterialWithVerticalOffsets()
{
mMaterial->ColorTexture() << mHorizontalBlurTarget->OutputTexture();
mMaterial->SampleWeights() << mSampleWeights;
mMaterial->SampleOffsets() << mVerticalSampleOffsets;
}
void GaussianBlur::UpdateGaussianMaterialNoBlur()
{
mMaterial->ColorTexture() << mSceneTexture;
}
在GaussianBlur::Draw()函數中,首先判斷變量mBlurAmount是否大於0。如果是就設置full-screen quad變量的technique爲blur technique以及p0 pass,並執行模糊處理過程。把水平模糊的render target綁定到管線的output-merger階段,並clear相關的render target view和depth-stencil view,然後調用回調函數UpdateGaussianMaterialWithHorizontalOffset()函數完成水平模糊的繪製。該回調函數主要用於把sample weights和horizontal sample offsets傳遞到scene texture中。繪製完成之後,恢復output-merger階段與back buffer的綁定。
接下來,調用回調函數UpdateGaussianMaterialWithVerticalOffsets()再次繪製四邊形。在該函數中,把水平模糊render target的輸出,sample weights以及vertical sample offsets傳遞到scene texture中。由於在output-merger階段綁定了back buffer,在swap chain完成交換之後就會在屏幕上演示繪製的結果。
要把GaussianBlur component集成到應用程序中(由Game派生),只需要使用以下的代碼初始化該component:
mGaussianBlur = new GaussianBlur(*this, *mCamera);
mGaussianBlur->SetSceneTexture(*(mRenderTarget->OutputTexture()));
mGaussianBlur->Initialize();
在該初始化代碼中,變量mRenderTarget用於渲染場景。以下是高斯模糊示例程序中GuassianBlurGame::Draw()函數的主要代碼。本書的配套網站上提供了完整的示例程序代碼。
mRenderTarget->Begin();
mDirect3DDeviceContext->ClearRenderTargetView(mRenderTarget->RenderTargetView() , reinterpret_cast<const float*>(&BackgroundColor));
mDirect3DDeviceContext->ClearDepthStencilView(mRenderTarget->DepthStencilView(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
Game::Draw(gameTime);
mRenderTarget->End();
mDirect3DDeviceContext->ClearRenderTargetView(mRenderTargetView, reinterpret_cast<const float*>(&BackgroundColor));
mDirect3DDeviceContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
mGaussianBlur->Draw(gameTime);
mRenderStateHelper->SaveAll();
mFpsComponent->Draw(gameTime);
圖18.5顯示了高斯模糊示例中blur amount值爲3.0時的輸出結果。