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时的输出结果。