Intro
本系列笔记不只是通过DirectX 3D实现一些简单的功能,而是通过对DirectX 3D接口的封装,一步步完善一个简单的图形库LibDX
。旨在可以通过该库快速的创建一个图形渲染程序。
本文介绍了如何搭建DirectX开发环境,并使用DirectX 3D绘制了一个简单窗口。
环境搭建
DirectX是Microsoft开的一套功能丰富的底层API,功能包括2D/3D图形加速支持、各种输入设备控制、声音和音乐输出的混音和采样、多玩家网络游戏控制等等。而Direct 3D是其一个十分核心的子集,负责图形的绘制及渲染。
使用DirectX开发需要DirectX SDK
环境,首先下载DirectX SDK
并安装。
我安装在 C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)
以下遇到与DirectX SDK安装路径相关的配置请按照本地安装路径进行相应修改
开发工具为Visual Studio 2015
。
创建工程
在学习DirectX 3D(以下简称D3DX)过程中,我们会使用D3DX一步步完善一个简单的图形库LibDX
。LibDX
中对D3DX的底层接口进行了封装,用于快速的创建图形渲染程序。
使用VS2015创建一个新的解决方案,并在其中创建两个VC++空项目LibDX
、Lesson01-createWindow
配置解决方案
设置Lesson01-createWindow
依赖于LibDX
,并将前者设为启动项目
配置LibDX
常规/配置类型
=> 静态库(.lib)
VC++目录/包含目录
添加
- C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Include
配置Lesson01-createWindow
VC++目录/包含目录
添加
- $(SolutionDir)
- C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Include
VC++目录/库目录
添加
- $(OutDir)
- C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Lib
链接器/输入/附加依赖项
添加
- LibDX.lib
- d3d9.lib
- d3dx9d.lib
- winmm.lib
绘制窗口
在LibDX
中创建以下文件
日志
log.h中定义了Message和Console两个方法,用于输出提示信息,调用方法与printf相同。这两个函数的区别在于前者通过弹窗提示,会使程序阻塞,主要用于程序报错;后者在控制台输出,不会阻塞程序,主要用于调试(win32程序不能直接在VS中输出信息,需要单独创建一个控制台进行输出)。
log.h1
2
3
4
5
6
7
8
namespace dx {
// 通过弹窗提示
void Message(const char *fmt, ...);
// 打印在控制台提示
void Console(const char *fmt, ...);
}
log.cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using namespace dx;
void dx::Message(const char *fmt, ...) {
char buf[4096];
va_list args;
va_start(args, fmt);
int n = vsnprintf(buf, 4096, fmt, args);
buf[n] = 0;
va_end(args);
// 调用Win32的MessageBox弹窗提示
::MessageBoxA(0, buf, 0, 0);
}
void dx::Console(const char*fmt, ...) {
char buf[4096];
va_list args;
va_start(args, fmt);
int n = vsnprintf(buf, 4096, fmt, args);
buf[n] = 0;
va_end(args);
// 打开控制台,输出调试信息
AllocConsole();
FILE *stream = NULL;
freopen_s(&stream, "CONOUT$", "w", stdout);
printf("%s\n", buf);
}
DxWindow类
DxWindows类对整个窗口的生命周期进行了封装,主要成员函数如下
函数 | 功能 |
---|---|
DxWindow(…) | 构造方法,传入窗口参数 |
bool Initialize() | 初始化窗口资源,创建窗口 |
void Release() | 销毁窗口资源 |
void Run() | 进入窗口事件循环 |
void Present() | 使用Direct3D在窗口中绘制 |
此外,在类的外面定义了一个WndProc()函数,作为窗口过程的回调函数,处理各种窗口事件。
dxwindow.h1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace dx {
class DxWindow {
public:
DxWindow(HINSTANCE instance, std::string tiltle,
int width, int height, bool windowed);
bool Initialize();
void Release();
void Run();
protected:
void Present();
HWND m_hwnd; // 窗口句柄
HINSTANCE m_hInstance; // 实例句柄
LPDIRECT3D9 m_pD3D9; // Direct3D接口对象
LPDIRECT3DDEVICE9 m_pDevice;// Direct3D设备对象
std::string m_strTitle; // 窗口标题
int m_iWidth; // 窗口宽
int m_iHeight; // 窗口长
int m_bWindowed; // 是否窗口化显示
};
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
}
构造函数
构造函数没有什么可说的,主要就是对DxWindow的成员变量进行初始化,各成员变量的含义可见上面头文件中的注释。1
2
3
4
5
6
7
8
9
10
11DxWindow::DxWindow(HINSTANCE instance, std::string title, int width, int height, bool windowed)
: m_hwnd(NULL),
m_pD3D9(NULL),
m_pDevice(NULL),
m_hInstance(instance),
m_strTitle(title),
m_iWidth(width),
m_iHeight(height),
m_bWindowed(windowed)
{
}
初始化
初始化窗口的函数比较长,过程主要分为以下几步
- 填充Win32窗口参数
- 注册、创建、显示Win32窗口
- 初始化D3DX接口指针
- 读取显卡信息
- 填充D3DX绘制参数
- 创建D3DX设备指针
我们不关注Win32窗口的相关函数,接下来主要介绍和D3DX有关的过程。
1 | bool DxWindow::Initialize() { |
读取设备信息
GetDeviceCaps用于获得设备的一些特定信息,函数的原型如下,其中Adapter指的是显卡设备,一般使用D3DADAPTER_DEFAULT,而D3DDEVTYPE是一个枚举类型,指定了D3DX的渲染方式,常用的选项有D3DDEVTYPE_HAL(硬件渲染)、D3DDEVTYPE_REF(软件渲染),其中软件渲染比硬件渲染慢,但可以模拟一些本机硬件不支持的特性。1
2
3
4
5HRESULT GetDeviceCaps(
[in] UINT Adapter,
[in] D3DDEVTYPE DeviceType,
[out] D3DCAPS9 *pCaps
);
这里主要使用该函数判断硬件是否支持硬件顶点处理,并据此设置变量vp,该变量在之后创建D3DX设备指针中会用到。
填充绘制参数
填充绘制参数主要就是填充D3DPRESENT_PARAMETER结构体,该结构体的成员变量及解释如下
变量名 | 解释 | |
---|---|---|
BackBufferWidth | 窗口宽度,窗口模式下可为0,全屏模式通过EnumAdapterModes获得 | |
BackBufferHeight | 窗口高度,同上 | |
BackBufferFormat | 缓存格式,可通过GetDisplayMode 获得,这里D3DFMT_A8R8G8B8表示alpha,blue,green,red通道各8位 | |
BackBufferCount | 后台缓存数量,使用双缓冲技术一边绘制一边显示 | |
MultiSampleType | 多采样类型,必须设置为D3DMULTISAMPLE_NONE除非SwapEffect设置为D3DSWAPEFFECT_DISCARD | |
MultiSampleQuality | 多采样质量 | |
SwapEffect | 指定缓冲区的交换方式,有Copy和Flip两种方式,而Discard是由程序自动选择 | |
hDeviceWindow | 窗口句柄 | |
Windowed | 是否窗口化 | |
EnableAutoDepthStencil | 是否应用深度缓存 | |
AutoDepthStencilFormat | 深度缓存格式 | |
Flags | 绘制参数 | ; |
FullScreen_RefreshRateInHz | FPS,窗口模式必须为0,全屏模式通过EnumAdapterModes获得 | |
PresentationInterval | 交换链中后台缓存切换到前台缓存的最大速率,详见D3DPRESENT.aspx) |
创建设备指针
最后一步就是通过CreateDevice创建设备指针,创建成功后,就可以通过该指针进行绘制了。CreateDevice的函数原型如下。其中BehaviorFlags使用的值就是前面读取设备信息创建的变量vp,其他几个参数很容易理解或已经介绍过,这里就不再赘述。1
2
3
4
5
6
7
8HRESULT CreateDevice(
[in] UINT Adapter,
[in] D3DDEVTYPE DeviceType,
[in] HWND hFocusWindow,
[in] DWORD BehaviorFlags,
[in, out] D3DPRESENT_PARAMETERS *pPresentationParameters,
[out, retval] IDirect3DDevice9 **ppReturnedDeviceInterface
);
消息循环
消息循环是Windows编程中比较基础的一个概念,在循环中不断查看是否有事件发生,如果有事件发生就对事件进行处理,否则调用绘制函数进行绘制。
消息循环在收到WM_QUIT消息后退出。1
2
3
4
5
6
7
8
9
10
11
12
13
14void DxWindow::Run() {
MSG msg;
ZeroMemory(&msg, sizeof(msg));
while (msg.message != WM_QUIT) {
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
// 分发事件
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// 调用绘制函数,对窗口进行渲染
Present();
}
}
}
事件响应
事件的回调函数在创建窗口时指定,当前只对用户退出窗口的行为做出了响应。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16LRESULT CALLBACK dx::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_KEYDOWN:
if (wParam == VK_ESCAPE) {
PostQuitMessage(0);
}
break;
default:
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
渲染
在成员函数Present中使用D3DX设备接口对窗口进行渲染。1
2
3
4
5
6
7
8
9void DxWindow::Present() {
if (m_pDevice == NULL) {
return;
}
m_pDevice->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL, D3DCOLOR_XRGB(0, 0, 0xFF), 1.0f, 0L);
m_pDevice->BeginScene();
m_pDevice->EndScene();
m_pDevice->Present(0, 0, 0, 0);
}
首先调用Clear清空后台缓存,Clear的函数原型如下。其中Count指的是第二个参数pRects数组的长度,如果pRects是NULL的话Count必须为0;pRects是需要Clear的区域数组,为NULL时表示清空整个屏幕区域;Flags表示清空的目标,可以包括D3DCLEAR_TARGET(颜色缓存)、D3DCLEAR_STENCIL(模板缓存)、D3DCLEAR_ZBUFFER(深度缓存),而接下来三个参数则分别指定Clear的颜色、深度与模板值。这里我们将颜色设置为蓝色。1
2
3
4
5
6
7
8HRESULT Clear(
[in] DWORD Count,
[in] const D3DRECT *pRects,
[in] DWORD Flags,
[in] D3DCOLOR Color,
[in] float Z,
[in] DWORD Stencil
);
接下来调用Present进行渲染,其函数原型如下。其中除了第三个参外都涉及到交换链,通常指定为NULL即可;第三个参数指的是窗口句柄,设定为NULL时会使用填充绘制参数时设定的窗口句柄。1
2
3
4
5
6HRESULT Present(
[in] const RECT *pSourceRect,
[in] const RECT *pDestRect,
[in] HWND hDestWindowOverride,
[in] const RGNDATA *pDirtyRegion
);
销毁
释放在初始化过程中申请的资源,包括Win32窗口、D3DX接口指针,D3DX设备指针等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void DxWindow::Release() {
if (m_pDevice) {
m_pDevice->Release();
m_pDevice = NULL;
}
if (m_pD3D9) {
m_pD3D9->Release();
m_pD3D9 = NULL;
}
if (m_hwnd) {
DestroyWindow(m_hwnd);
m_hwnd = NULL;
}
UnregisterClass(m_strTitle.c_str(), m_hInstance);
}
运行
在项目Lesson01-createWindow
中新建main.cpp文件,并添加以下内容,在WinMain函数中,我们创建了一个DxWindow类,并依次调用了Initialize、Run、Release这几个方法,即完成了一个窗口的初始化到渲染、再到释放的整个过程。
点击运行,即可看到本文开头的运行效果。
1 |
|