DirectX 3D学习笔记01——环境搭建与窗口创建

Intro

本系列笔记不只是通过DirectX 3D实现一些简单的功能,而是通过对DirectX 3D接口的封装,一步步完善一个简单的图形库LibDX。旨在可以通过该库快速的创建一个图形渲染程序。

本文介绍了如何搭建DirectX开发环境,并使用DirectX 3D绘制了一个简单窗口。

环境搭建

DirectX是Microsoft开的一套功能丰富的底层API,功能包括2D/3D图形加速支持、各种输入设备控制、声音和音乐输出的混音和采样、多玩家网络游戏控制等等。而Direct 3D是其一个十分核心的子集,负责图形的绘制及渲染。

使用DirectX开发需要DirectX SDK环境,首先下载DirectX SDK并安装。

DirectX SDK下载地址

我安装在 C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)
以下遇到与DirectX SDK安装路径相关的配置请按照本地安装路径进行相应修改

开发工具为Visual Studio 2015

创建工程

在学习DirectX 3D(以下简称D3DX)过程中,我们会使用D3DX一步步完善一个简单的图形库LibDXLibDX中对D3DX的底层接口进行了封装,用于快速的创建图形渲染程序。

使用VS2015创建一个新的解决方案,并在其中创建两个VC++空项目LibDXLesson01-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.h

1
2
3
4
5
6
7
8
#pragma once

namespace dx {
// 通过弹窗提示
void Message(const char *fmt, ...);
// 打印在控制台提示
void Console(const char *fmt, ...);
}

log.cpp

1
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
#include <stdarg.h>
#include <stdio.h>
#include <Windows.h>

#include "log.h"

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.h

1
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
#pragma once
#include <d3dx9.h>
#include <d3d.h>
#include <string>

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
11
DxWindow::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)
{
}

初始化

初始化窗口的函数比较长,过程主要分为以下几步

  1. 填充Win32窗口参数
  2. 注册、创建、显示Win32窗口
  3. 初始化D3DX接口指针
  4. 读取显卡信息
  5. 填充D3DX绘制参数
  6. 创建D3DX设备指针

我们不关注Win32窗口的相关函数,接下来主要介绍和D3DX有关的过程。

1
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
bool DxWindow::Initialize() {
// 初始化窗口
WNDCLASSEX wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = dx::WndProc;
wc.cbSize = sizeof(WNDCLASSEX);
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = m_hInstance;
wc.hIcon = LoadIcon(m_hInstance, IDI_APPLICATION);
wc.hIconSm = LoadIcon(m_hInstance, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = m_strTitle.c_str();

// 注册窗口
if (!RegisterClassEx(&wc)) {
Message("RegisterClassEx Failed!");
return false;
}

// 创建窗口
m_hwnd = CreateWindowEx(NULL, m_strTitle.c_str(), m_strTitle.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0, 0, m_iWidth, m_iHeight, 0, 0, m_hInstance, 0);
if (!m_hwnd) {
Message("CreateWindowEx Failed!");
Release();
return false;
}

// 显示并更新窗口
ShowWindow(m_hwnd, SW_SHOW);
UpdateWindow(m_hwnd);

// 初始化Dierte3D指针
if (NULL == (m_pD3D9 = Direct3DCreate9(D3D_SDK_VERSION))) {
Message("Direct3DCreate9 Failed!");
Release();
return false;
}

// 判断显卡是否支持硬件定点处理
D3DCAPS9 caps;
DWORD vp = 0;
if (FAILED(m_pD3D9->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps))) {
Message("GetDeviceCaps Failed!");
Release();
return false;
}
if (caps.VertexProcessingCaps) {
vp = D3DCREATE_HARDWARE_VERTEXPROCESSING;
} else {
vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
}

// 填充DXD3的绘制参数
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.BackBufferWidth = m_iWidth;
d3dpp.BackBufferHeight = m_iHeight;
d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8;
d3dpp.BackBufferCount = 1;
d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE;
d3dpp.MultiSampleQuality = 0;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = m_hwnd;
d3dpp.Windowed = m_bWindowed;
d3dpp.EnableAutoDepthStencil = true;
d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8;
d3dpp.Flags = 0;
d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

// 创建Dierect3D设备对象
if (FAILED(m_pD3D9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_hwnd,
vp, &d3dpp, &m_pDevice))) {
Message("CreateDevice Failed!");
Release();
return false;
}
return true;
}

读取设备信息

GetDeviceCaps用于获得设备的一些特定信息,函数的原型如下,其中Adapter指的是显卡设备,一般使用D3DADAPTER_DEFAULT,而D3DDEVTYPE是一个枚举类型,指定了D3DX的渲染方式,常用的选项有D3DDEVTYPE_HAL(硬件渲染)、D3DDEVTYPE_REF(软件渲染),其中软件渲染比硬件渲染慢,但可以模拟一些本机硬件不支持的特性。

1
2
3
4
5
HRESULT 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
8
HRESULT 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
14
void 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
16
LRESULT 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
9
void 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
8
HRESULT 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
6
HRESULT 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
15
void 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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <Windows.h>
#include <stdio.h>
#include <LibDX/dxwindow.h>

using namespace dx;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdline, int showCmd) {
DxWindow *dw = new DxWindow(hInstance, "Lesson 01-Window", 800, 600, true);
if (dw->Initialize()) {
dw->Run();
dw->Release();
}
return 0;
}
文章目录
  1. 1. Intro
  2. 2. 环境搭建
  3. 3. 创建工程
    1. 3.1. 配置解决方案
    2. 3.2. 配置LibDX
    3. 3.3. 配置Lesson01-createWindow
  4. 4. 绘制窗口
    1. 4.1. 日志
    2. 4.2. DxWindow类
    3. 4.3. 构造函数
    4. 4.4. 初始化
    5. 4.5. 读取设备信息
    6. 4.6. 填充绘制参数
    7. 4.7. 创建设备指针
    8. 4.8. 消息循环
    9. 4.9. 事件响应
    10. 4.10. 渲染
    11. 4.11. 销毁
  5. 5. 运行