Color Filtering
现在我们使用FullScreenRenderTarget和FullScreenQuad类编写一个color filter演示程序。一个color filter就是以某种方法修改输出的颜色值。如果你曾经佩戴过彩色的眼镜,你就已经见过color filtering的效果。在这个示例程序中,我们将会编写多个color filter shaders,包括grayscale,inverse,sepia以及一个通用的color filtering系统用于生成任意的color effects。A Grayscale Filter
首先创建一个ColorFilter.fx文件,该shader的代码如列表18.5所示。列表18.5 A Grayscale Shader
static const float3 GrayScaleIntensity = { 0.299f, 0.587f, 0.114f };
static const float3x3 SepiaFilter = { 0.393f, 0.349f, 0.272f,
0.769f, 0.686f, 0.534f,
0.189f, 0.168f, 0.131f };
/************* Resources *************/
cbuffer CBufferPerObject
{
float4x4 ColorFilter = { 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1 };
}
Texture2D ColorTexture;
SamplerState TrilinearSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
/************* Data Structures *************/
struct VS_INPUT
{
float4 Position : 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.Position;
OUT.TextureCoordinate = IN.TextureCoordinate;
return OUT;
}
/************* Pixel Shaders *************/
float4 grayscale_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
float intensity = dot(color.rgb, GrayScaleIntensity);
return float4(intensity.rrr, color.a);
}
float4 inverse_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
return float4(1 - color.rgb, color.a);
}
float4 sepia_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
return float4(mul(color.rgb, SepiaFilter), color.a);
}
float4 genericfilter_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
return float4(mul(color, ColorFilter).rgb, color.a);
}
/************* Techniques *************/
technique11 grayscale_filter
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, grayscale_pixel_shader()));
}
}
technique11 inverse_filter
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, inverse_pixel_shader()));
}
}
technique11 sepia_filter
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, sepia_pixel_shader()));
}
}
technique11 generic_filter
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, genericfilter_pixel_shader()));
}
}
这是一种grayscale filter;通过查找color的强度值把把采样的RGB颜色转换成grayscale样式。一种最容易想到的方法是通过计算RGB三个通道的平均值得到一个color的强度值:
Intensity = (R + G + B) / 3
但是人的眼睛对红,绿,蓝三种颜色的感知是不同的。具体地说就是,我们对绿色更敏感,其次是红色,最后才蓝色。因此,一种更精确的表示grayscale强度的方法是计算三个color通道值的加权平均值,该计算公式如下:
Intensity = 0.299 * R + 0.587 * G + 0.114 * B
在grayscale pixel shader中通过一个简单的dot product运算就可以计算出grayscale强度值。
另外,需要注意的是在vertex shader中,传递vertex坐标值的时候不需要对坐标进行变换,因为这些坐标值已经指定为sceen space中。
A Color Filter Demo
现在我们创建一个派生自Game类的新类ColorFilteringGame,用于在前面几章所编写的示例中添加color filter渲染。与之前一样,在ColorFilteringGame类中需要初始化mouse和keyboard,camera,skybox,reference grid,frame-rate component,以及point light demo component。还包括SpriteBatch和SpriteFont类型的成员变量用于在屏幕上渲染文字。在成员列表最后,需要添加以下的成员变量:FullScreenRenderTarget* mRenderTarget;
FullScreenQuad* mFullScreenQuad;
Effect* mColorFilterEffect;
ColorFilterMaterial* mColorFilterMaterial;
其中包括新的FullScreenRenderTarget和FullScreenQuad类型的成员变量。此外还有一个mColorFilterEffect存储编译好的color filter shader,并用于初始化mColorFilterMaterial变量。ColorFilterMaterial类与ColorFilter.fx shader的输入数据匹配。
然后在ColorFilteringGame::Initialize()函数中使用以下的代码初始化这些成员变量:
void ColorFilteringGame::Initialize()
{
if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
{
throw GameException("DirectInput8Create() failed");
}
mKeyboard = new Keyboard(*this, mDirectInput);
mComponents.push_back(mKeyboard);
mServices.AddService(Keyboard::TypeIdClass(), mKeyboard);
mMouse = new Mouse(*this, mDirectInput);
mComponents.push_back(mMouse);
mServices.AddService(Mouse::TypeIdClass(), mMouse);
mCamera = new FirstPersonCamera(*this);
mComponents.push_back(mCamera);
mServices.AddService(Camera::TypeIdClass(), mCamera);
mFpsComponent = new FpsComponent(*this);
mFpsComponent->Initialize();
mSkybox = new Skybox(*this, *mCamera, L"Content\\Textures\\Maskonaive2_1024.dds", 500.0f);
mComponents.push_back(mSkybox);
mGrid = new Grid(*this, *mCamera);
mComponents.push_back(mGrid);
RasterizerStates::Initialize(mDirect3DDevice);
SamplerStates::BorderColor = ColorHelper::Black;
SamplerStates::Initialize(mDirect3DDevice);
mPointLightDemo = new PointLightDemo(*this, *mCamera);
mComponents.push_back(mPointLightDemo);
mRenderStateHelper = new RenderStateHelper(*this);
mRenderTarget = new FullScreenRenderTarget(*this);
SetCurrentDirectory(Utility::ExecutableDirectory().c_str());
mColorFilterEffect = new Effect(*this);
mColorFilterEffect->LoadCompiledEffect(L"Content\\Effects\\ColorFilter.cso");
mColorFilterMaterial = new ColorFilterMaterial();
mColorFilterMaterial->Initialize(*mColorFilterEffect);
mFullScreenQuad = new FullScreenQuad(*this, *mColorFilterMaterial);
mFullScreenQuad->Initialize();
mFullScreenQuad->SetActiveTechnique(ColorFilterTechniqueNames[mActiveColorFilter], "p0");
mFullScreenQuad->SetCustomUpdateMaterial(std::bind(&ColorFilteringGame::UpdateColorFilterMaterial, this));
mSpriteBatch = new SpriteBatch(mDirect3DDeviceContext);
mSpriteFont = new SpriteFont(mDirect3DDevice, L"Content\\Fonts\\Arial_14_Regular.spritefont");
Game::Initialize();
mCamera->SetPosition(0.0f, 0.0f, 25.0f);
}
在该函数中,使用color filtering material以及grayscale_filter technique中的pass p0初始化full-screen quad变量。并指定ColorFilteringGame::UpdateColorFilterMaterial()函数作为full-screen quad的custom material回调函数,该函数实现代码如下:
void ColorFilteringGame::UpdateColorFilterMaterial()
{
XMMATRIX colorFilter = XMLoadFloat4x4(&mGenericColorFilter);
mColorFilterMaterial->ColorTexture() << mRenderTarget->OutputTexture();
}
每一次绘制full-screen quad时都会调用一次该函数,并把render target的输出数据传递给shader变量ColorTexture。最后要分析的是ColorFilteringGame::Draw()函数 ,该函数的实现代码如下:
void ColorFilteringGame::Draw(const GameTime &gameTime)
{
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);
mFullScreenQuad->Draw(gameTime);
mRenderStateHelper->SaveAll();
mFpsComponent->Draw(gameTime);
mSpriteBatch->Begin();
std::wostringstream helpLabel;
helpLabel << L"Ambient Intensity (+PgUp/-PgDn): " << mPointLightDemo->GetAmbientColor().a << "\n";
helpLabel << L"Point Light Intensity (+Home/-End): " << mPointLightDemo->GetPointLight().Color().a << "\n";
helpLabel << L"Specular Power (+Insert/-Delete): " << mPointLightDemo->GetSpecularPower() << "\n";
helpLabel << L"Move Point Light (8/2, 4/6, 3/9)\n";
helpLabel << std::setprecision(2) << L"Active Filter (Space Bar): " << ColorFilterDisplayNames[mActiveColorFilter].c_str();
if (mActiveColorFilter == ColorFilterGeneric)
{
helpLabel << L"\nBrightness (+Comma/-Period): " << mGenericColorFilter._11 << "\n";
}
mSpriteFont->DrawString(mSpriteBatch, helpLabel.str().c_str(), mTextPosition);
mSpriteBatch->End();
mRenderStateHelper->RestoreAll();
HRESULT hr = mSwapChain->Present(0, 0);
if (FAILED(hr))
{
throw GameException("IDXGISwapChain::Present() failed.", hr);
}
}
在Draw()函数中使用了本章开始部分所讨论的post-processing步骤。首先,调用mRenderTarget->Begin()函数把off-screen render target绑定到管线的output-merger阶段。然后绘制场景,先通过调用ClearRenderTargetView()和ClearDepthStencilView()函数clear off-screen的render target以及depth-stencil views,再调用mRenderTarget->End()函数恢复back buffer作为与output-merger阶段绑定的render target。最后,把full-screen quad渲染到back buffer中。由于在full-screen quad已经使用了ColorFilter.fx shader中的grayscale_filter technique,因此在最终的back buffer中就是应用了该effect的渲染结果。
图18.1中显示了在一个包含有point light,reference grid和skybox的场景中使用grayscale filter的输出结果。与之前的point light示例程序一样,在ColorFilter示例中也可以通过数字键盘与场景中的point light进行交互。其中post-processing effect与渲染到off-screen render target中的数据是完全独立的。
图18.1 Output of the grayscale post-processing effect. (Skybox texture by Emil Persson. Earth texture from Reto Stöckli, NASA Earth Observatory.)
A Color Inverse Filter
只需要在ColorFilter.fx shader中做一点点修改就可以增加更多的color filters。例如,使用下面的pixel shader和technique就可以实现一种color inverse filter:float4 inverse_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
return float4(1 - color.rgb, color.a);
}
technique11 inverse_filter
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, inverse_pixel_shader()));
}
}
在这种filter中只是把每一个颜色的RGB通道值进行反转,最终产生的输出结果如图18.2所示。
图18.2 Output of the color inverse post-processing effect. (Skybox texture by Emil Persson. Earth texture from Reto Stöckli, NASA Earth Observatory.)
A Sepia Filter
现在我们开始创建一种filter用于近似模拟古老的照片。在那个年代,黑白照片实际上是红褐色(reddish-brown)的阴影,称为sepia色调。与grayscale effect一样,通过计算RGB各个通道的加权平均值得到一个pixel的强度值,可以创建一种sepia shader。但是在sepia effect中,对于每一个color channel计算一个不同的强度值。计算公式为:
通过分别执行三次dot product运算操作就可以实现。但是一种更简洁的方法是构建一个表示sepia相关系数的3×3矩阵,并执行一次简单的矩阵乘法运算(矩阵乘法实际上就是一系列dot product操作)。列表18.6列出了用于sepia post-processing effect的pixel shader和technique代码。可以在ColorFilter.fx文件中直接添加这些代码,而不用再创建一个完全独立的effect文件。
列表18.6 A Sepia Shader
static const float3x3 SepiaFilter = { 0.393f, 0.349f, 0.272f,
0.769f, 0.686f, 0.534f,
0.189f, 0.168f, 0.131f };
float4 sepia_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
return float4(mul(color.rgb, SepiaFilter), color.a);
}
technique11 sepia_filter
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, sepia_pixel_shader()));
}
}
Sepia shader的输出结果与图18.3所示。
图18.3 Output of the sepia post-processing effect. (Skybox texture by Emil Persson. Earth texture from Reto Stöckli, NASA Earth Observatory.)
A Generic Color Filter
在sepia shader的基础上做一点点扩展,就可以支持在应用程序运行时动态指定color filter矩阵值。使用这种方法,可以只使用一种technique表示上述所有的color filters。为了使用generic-filter尽可能通用,其中使用了一个4×4的color filter矩阵。列表18.7中列出了generic-filter的pixel shader和technique代码。列表18.7 A Generic Color Filter Shader
cbuffer CBufferPerObject
{
float4x4 ColorFilter = { 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1 };
}
float4 genericfilter_pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 color = ColorTexture.Sample(TrilinearSampler, IN.TextureCoordinate);
return float4(mul(color, ColorFilter).rgb, color.a);
}
technique11 generic_filter
{
pass p0
{
SetVertexShader(CompileShader(vs_5_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, genericfilter_pixel_shader()));
}
}
在使用generic-filter shader的情况下,通过以下的矩阵可以分别表示grayscale,inverse以及sepia effects:
在本书的配套网站上提供了ColorFilteringGame类的代码,通过使用一个单位缩放矩阵模拟brightness(color filter矩阵对角线上的值在0与1之间变动)的应用程序演示了generic color filter。在该示例中,可以使用键盘上的逗号和句号按键分别增加或减少亮度。另外,还可以通过按下空格键盘在各种color filters之间进行切换。要实现这种功能,需要在ColorFilteringGame::UpdateColorFilterMaterial()回调函数中实时更新传递给shader变量ColoFilter的值。函数的实现如下所示:
void ColorFilteringGame::UpdateColorFilterMaterial()
{
XMMATRIX colorFilter = XMLoadFloat4x4(&mGenericColorFilter);
mColorFilterMaterial->ColorTexture() << mRenderTarget->OutputTexture();
mColorFilterMaterial->ColorFilter() << colorFilter;
}
图18.4显示了使用generic color filter模拟一种full-screen brightness的输出结果。
图18.4 Output of the generic color filter shader simulating brightness. (Skybox texture by Emil Persson. Earth texture from Reto Stöckli, NASA Earth Observatory.)