本系列文章由zhmxy555(毛星云)编写,转载请注明出处。
作者:毛星云 邮箱: happylifemxy@163.com
本篇文章将讲解如何通过我们在之前的文章里面已掌握的DirectX 11的知识,来一步一步创建一个基于Direct3D11的Blank Windows Demo,而我们在这节里面完成的这个demo,将在后面的文章里面作为一个模板,用于演示之后的各种新奇DirectX11的功能。
首先呢,为了代码的重用性着想,我们会写一个 DirectX11DemoBase,并借鉴在笔记二十六中的Win32风格的Blank Win32 Window Demo中的代码,然后通过派生的方式,以及重载一些必要的虚函数,进行整合,来创建我们的demo。
一、 关于代码书写风格的讨论
首先,我们提出一个问题,采用自问自答的形式来讨论:
在这个demo的设计过程中,我们是采用C语言式的全局变量与全局函数的搭档模式来完成,还是采用C++式的面向对象风格的类Class来编写呢?
答案是后者,采用面向对象的思想来完成。至于这个问题的解释,答案就仁者见仁智者见智了。
浅墨之前看过一本C++界的名著《C++沉思录》,作者在文章的开篇举了一个例子,然后通过例子对比出来的效果,折射出了对C与C++的一个中肯的评价,是这样的一段话:
“C++鼓励采用类来表示类似于输出流的事物,而类就提供了一个理想的位置来存放状态信息。而C语言倾向于不存储状态信息,除非事先已经规划妥当。因此C程序员趋向于假设有这样一个“环境”:存在一个位置集合,他们可以在其中找到系统的当前状态。如果只有一个环境和一个系统,这样考虑毫无问题,但是,系统在不断增长的过程中往往需要引入某些独一无二的东西,并且创建更多这类东西。”
对这段话的解释,浅墨还是用自己的话来叙述吧:
通常我们采用一般的变量作为传递数据的容器,但是随着程序的复杂会导致数据量的加大,有太多的数据需要被传递,而且这些数据基本上都是需要传递到近乎是每一个函数当中的,这样我们就要创建很多的全局变量作为“容器”,如此下去我们的设计的程序只会越来越臃肿,越来越乱。别怕别怕,有绝招呢——我们可以创建一个类或者结构体来收容这些对象,使之显得不是那么乱,取而代之的是井井有条。
绕了这么大一圈子,一言以蔽之,就是运用全局变量是不太好的编程习惯,我们应当少用甚至不用,转而使用“类”来完成这些任务。
二、 Dx11DemoBase类的设计
作为目前来说,我们要求本节的demo做到以下几点功能:
▲初始化D3D
▲释放在启动过程中创建的Direct3D对象
▲为我们的D3D对象存储成员变量
▲提供一个装载demo的具体内容的方式
▲提供一个卸载demo的具体内容的方式
▲能够显示demo每帧的更新的具体内容
▲demo渲染内容的具体代码
由我们上面的清单来看,创建一个公共的初始化和卸载函数,用于装载和卸载内容功能的虚函数,以及渲染和更新游戏循环步骤的虚函数的基类是很有必要的。通过将这些函数设为虚函数,由基类派生出来的demo类能够实现他们自定义的逻辑和行为。
根据上面的这些叙述,我们可以写出下面的这段为Dx11DemoBase量身打造的代码:
代码段一 Dx11DemoBases类的头文件
#ifndef _DEMO_BASE_H_
#define _DEMO_BASE_H_
#include<d3d11.h>
#include<d3dx11.h>
#include<DxErr.h>
class Dx11DemoBase
{
public:
Dx11DemoBase();
virtual ~Dx11DemoBase();
bool Initialize( HINSTANCE hInstance, HWND hwnd );
void Shutdown( );
virtual bool LoadContent( );
virtual void UnloadContent( );
virtual void Update( float dt ) = 0;
virtual void Render( ) = 0;
protected:
HINSTANCE hInstance_;
HWND hwnd_;
D3D_DRIVER_TYPE driverType_;
D3D_FEATURE_LEVEL featureLevel_;
ID3D11Device* d3dDevice_;
ID3D11DeviceContext* d3dContext_;
IDXGISwapChain* swapChain_;
ID3D11RenderTargetView* backBufferTarget_;
};
#endif
上面这段代码中我们可以看到最精简的D3D对象,以protected类成员的形式存在于类之中。在类体外初始化变量是比较好的编程习惯,而且效率比让先调用拷贝构造函数,再调用默认构造函数要高得多。
Dx11DemoBase类构造函数,析构函数,装载内容,卸载内容,shutdown函数定义如下:
代码段二 一些Dx11DemoBase 组成代码
#include"Dx11DemoBase.h"
Dx11DemoBase::Dx11DemoBase( ) : driverType_( D3D_DRIVER_TYPE_NULL),
featureLevel_( D3D_FEATURE_LEVEL_11_0 ), d3dDevice_( 0 ),d3dContext_( 0 ),
swapChain_( 0 ), backBufferTarget_( 0 )
{
}
void Dx11DemoBase::UnloadContent( )
{
//可以在此处进行重载,加入代码实现相关功能
void Dx11DemoBase::Shutdown( )
{
UnloadContent( );
if( backBufferTarget_ ) backBufferTarget_->Release( );
if( swapChain_ ) swapChain_->Release( );
if( d3dContext_ ) d3dContext_->Release( );
if( d3dDevice_ ) d3dDevice_->Release( );
d3dDevice_ = 0;
d3dContext_ = 0;
swapChain_ = 0;
backBufferTarget_ = 0;
}
Dx11DemoBase类中的最后一个函数是Initialize函数。Initialize函数执行我们在这章中讲到的D3D初始化工作。这个函数开始声明我们的硬件,WARP或者软件的驱动类型,和我们的D3D11.0,10.1或者10.0的特征等级。代码的设定即尝试在D3D 11中创建一个硬件设备。如果创建失败,我们会尝试其他的驱动类型和特征等级直到我们找到一个合适的类型。这也意味着如果我们采用D3D10硬件我们可以也可以在硬件中运行这个demo,因为我们可以选择10.1或者10.0的特征等级。
下一步便是创建交换链的描述,以及使用这些信息来试着找到支持的设备类型和特征等级。如果成功的搜索到了我们需要的这些信息。接下来就是行云流水地创建渲染目标,创建视口,以及调用LoadContent方法加载特定的内容了。需要指出的是,LoadContent方法最好留着最后进行调用,以免出现不必要的错误。
下面便是DirectX11初始化的全过程:
代码段三 Dx11DemoBase类的初始化函数
bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd )
{
hInstance_ =hInstance;
hwnd_ = hwnd;
RECT dimensions;
GetClientRect( hwnd,&dimensions );
unsigned int width =dimensions.right - dimensions.left;
unsigned int height =dimensions.bottom - dimensions.top;
D3D_DRIVER_TYPEdriverTypes[] =
{
D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP,
D3D_DRIVER_TYPE_REFERENCE, D3D_DRIVER_TYPE_SOFTWARE
};
unsigned inttotalDriverTypes = ARRAYSIZE( driverTypes );
D3D_FEATURE_LEVELfeatureLevels[] =
{
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0
};
unsigned inttotalFeatureLevels = ARRAYSIZE( featureLevels );
DXGI_SWAP_CHAIN_DESCswapChainDesc;
ZeroMemory(&swapChainDesc, sizeof( swapChainDesc ) );
swapChainDesc.BufferCount = 1;
swapChainDesc.BufferDesc.Width = width;
swapChainDesc.BufferDesc.Height = height;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.OutputWindow = hwnd;
swapChainDesc.Windowed= true;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality= 0;
unsigned intcreationFlags = 0;
#ifdef _DEBUG
creationFlags |=D3D11_CREATE_DEVICE_DEBUG;
#endif
HRESULT result;
unsigned int driver =0;
for( driver = 0;driver < totalDriverTypes; ++driver )
{
result =D3D11CreateDeviceAndSwapChain( 0, driverTypes[driver], 0, creationFlags,
featureLevels, totalFeatureLevels,
D3D11_SDK_VERSION, &swapChainDesc, &swapChain_,
&d3dDevice_, &featureLevel_, &d3dContext_ );
if( SUCCEEDED(result ) )
{
driverType_ =driverTypes[driver];
break;
}
}
if( FAILED( result ) )
{
DXTRACE_MSG("创建 Direct3D 设备失败!" );
return false;
}
ID3D11Texture2D*backBufferTexture;
result =swapChain_->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID*)&backBufferTexture );
if( FAILED( result ) )
{
DXTRACE_MSG("获取交换链后台缓存失败!" );
return false;
}
result =d3dDevice_->CreateRenderTargetView( backBufferTexture, 0,&backBufferTarget_ );
if( backBufferTexture)
backBufferTexture->Release( );
if( FAILED( result ) )
{
DXTRACE_MSG("创建渲染目标视图失败!" );
return false;
}
d3dContext_->OMSetRenderTargets( 1, &backBufferTarget_, 0 );
D3D11_VIEWPORTviewport;
viewport.Width =static_cast<float>(width);
viewport.Height =static_cast<float>(height);
viewport.MinDepth =0.0f;
viewport.MaxDepth =1.0f;
viewport.TopLeftX =0.0f;
viewport.TopLeftY =0.0f;
d3dContext_->RSSetViewports( 1, &viewport );
return LoadContent( );
}
bool Dx11DemoBase::LoadContent( )
{
//可以进行重载来丰富相关功能
return true;
}
void Dx11DemoBase::UnloadContent( )
{
//可以进行重载来丰富相关功能
}
void Dx11DemoBase::Shutdown( )
{
UnloadContent( );
if( backBufferTarget_) backBufferTarget_->Release( );
if( swapChain_ )swapChain_->Release( );
if( d3dContext_ )d3dContext_->Release( );
if( d3dDevice_ )d3dDevice_->Release( );
backBufferTarget_ = 0;
swapChain_ = 0;
d3dContext_ = 0;
d3dDevice_ = 0;
}
三、BlankDx11Demo类的设计
万事具备,只欠东风。
下面我们便从上面写的Dx11DemoBase类里派生出一个叫BlankDx11Demo的类。
以下就是 BlankDx11Demo类头文件的代码:
代码段四 BlankDx11Demo 类的头文件
#ifndef _BLANK_DEMO_H_
#define _BLANK_DEMO_H_
#include"Dx11DemoBase.h"
class BlankDx11Demo : public Dx11DemoBase
{
public:
BlankDx11Demo( );
virtual ~BlankDx11Demo( );
bool LoadContent( );
void UnloadContent( );
void Update( float dt );
void Render( );
};
#endif
这段代码中可以看到。叫做Update的函数中取了一个叫做dt的变量,后面将更详细地剖析这个变量,目前我们按这样理解就好了:在游戏程序中我们经常需要进行实时的游戏逻辑更新,而dt用于代表最后一帧的时间到当前时间的时间差,这个时间差记录我们用dt记录了下来,便于我们的基于时间的更新操作。
由于这个只是一个骨架式的空DirectXDemo,以尽量精简易懂作为此Demo的宗旨,以便于大家更容易地理解一个DirectX 11 Demo的筋骨脉络,所以在这里只是只进行了一个清屏的操作,且所有的函数重载都是空的。Render函数中我们也就调用了两个Direct3D的函数:ClearRenderTargetView函数用于清除屏幕上指定的颜色,Present函数用于显示新渲染的场景。
代码段五BlankDx11Demo类的源文件
#include"BlankDx11Demo.h"
BlankDx11Demo::BlankDx11Demo( )
{
}
BlankDx11Demo::~BlankDx11Demo( )
{
}
bool BlankDx11Demo::LoadContent( )
{
return true;
}
void BlankDx11Demo::UnloadContent( )
{
}
void BlankDx11Demo::Update( float dt )
{
}
void BlankDx11Demo::Render( )
{
if( d3dContext_ == 0 )
return;
float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };
d3dContext_->ClearRenderTargetView( backBufferTarget_,clearColor );
swapChain_->Present( 0, 0 );
}
四、 赋予程序生命——wWinMain函数的书写
之前我们创建的这些类都只是一个躯壳,并没有生命,而现在我们会将今天我们创建的这个主角赋予生命。最后一步就是在工程中修改并添加我们在笔记二十六中提出的Blank Win32 Window demo中的wWinMain函数以及余下的功能函数,使我们今天设计出的这个demo浑然一体。以下就是最后需要的源码:
代码段六 main.cpp的完整源代码
#include<Windows.h>
#include<memory>
#include"BlankDx11demo.h"
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAMwParam, LPARAM lParam );
int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE prevInstance,LPWSTR cmdLine, int cmdShow )
{
UNREFERENCED_PARAMETER( prevInstance );
UNREFERENCED_PARAMETER( cmdLine );
WNDCLASSEX wndClass ={ 0 };
wndClass.cbSize =sizeof( WNDCLASSEX ) ;
wndClass.style =CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc =WndProc;
wndClass.hInstance =hInstance;
wndClass.hCursor =LoadCursor( NULL, IDC_ARROW );
wndClass.hbrBackground= ( HBRUSH )( COLOR_WINDOW + 1 );
wndClass.lpszMenuName= NULL;
wndClass.lpszClassName= "DX11BookWindowClass";
if( !RegisterClassEx(&wndClass ) )
return -1;
RECT rc = { 0, 0, 640,480 };
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE );
HWND hwnd =CreateWindowA( "DX11BookWindowClass", "Blank Direct3D 11 Window演示程序", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,
NULL, NULL, hInstance, NULL );
if( !hwnd )
return -1;
ShowWindow( hwnd,cmdShow );
std::auto_ptr<Dx11DemoBase>demo( new BlankDemo( ) ); //使用智能指针
// Demo初始化工作
bool result =demo.Initialize( hInstance, hwnd );
if( result == false )
return -1;
MSG msg = { 0 };
while( msg.message !=WM_QUIT )
{
if( PeekMessage(&msg, 0, 0, 0, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
// 更新以及绘制图形
demo.Update( 0.0f);
demo.Render( );
}
// Demo开始卸载
demo.Shutdown( );
returnstatic_cast<int>( msg.wParam );
}
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAMwParam, LPARAM lParam )
{
PAINTSTRUCTpaintStruct;
HDC hDC;
switch( message )
{
case WM_PAINT:
hDC =BeginPaint( hwnd, &paintStruct );
EndPaint( hwnd,&paintStruct );
break;
case WM_DESTROY:
PostQuitMessage( 0 );
break;
default:
returnDefWindowProc( hwnd, message, wParam, lParam );
}
return 0;
}
笔记二十六里的demo的基础上,我们在wWinMain函数中加了7行代码。首先我们运用了C++中的标准智能指针auto_ptr<>。
auto_ptr<>智能指针会在指向的内容结束或者此指针的作用域指向其他的智能指针时自动释放内存。这个作用域可以是,一个if语句,一个内循环,或者在一对大括号里面随意摆放来创建一个新的作用域。
这样做的好处是非常舒服的——我们并不需要手动删除分配的数据,而且使用auto_ptr<>是非常安全环保的。即使出现了异常或者bug,应用程序停止运行了,auto_ptr<>在堆栈展开过程中依然会释放其数据。这样的话,即使运用程序崩溃了,依然会做到没有内存的泄露。若我们手动删除这个指针,且执行没有停止的话,就会留下泄露的内存。采用类似auto_ptr<>的内存对象有很多好处。
不是很熟悉这些内容的朋友,最好是阅读一些智能指针和其他新潮的C++编程语言的书籍进行了解和提高,掌握最新标准的C++(C++0x)。
在wWinMain函数中的最后一件事情是要注意,我们正在返回MSG对象的wParam成员,来返回应用程序的退出代码。由于wWinMain函数返回一个整型,我们把整个对象用C++标准运算符static_cast<>进行强制类型转换,转换为整型。
Blank Direct3D Window的截图可以在下面看到。采用深蓝色来清屏。
本节的知识就介绍到这里。
本篇文章配套的源代码请点击这里下载: 【Visual C++】Note_Code_28