DX12 快速教程(1) —— 做窗口

DirectX 12 是微软 2014 年开发并长期维护的一套 3D 图形 API 接口 ,相比前任 DirectX 11 ,最大的特点是 大幅度提升多线程异步图形渲染 的性能,开放大量的底层细节,将资源管理和底层验证统统交由开发者,让开发者自行处理,来提升图形渲染的性能。用 DirectX 12 开发的游戏有:《无主之地3》、《全境封锁2》、《幽灵行动:断点》、《世界战争:风云变幻》。

渲染 (Render) 就是 用软件从模型生成图像的过程。

《全境封锁2》
*## 安装必要组件
要开始 DirectX 12 编程,首先需要检查是否安装了必要组件。

  • 安装"使用C++的桌面开发"
  • 单个组件勾选:"HLSL工具" 和 "用于DirectX的图形调试器和GPU探测器"
  • 检查可选组件是否安装了 "图形工具"
    "设置" -> "应用" -> "应用和功能" -> "可选功能" -> 下拉找到"图形工具"

如果没装图形工具,后文开启 DX12 调试层时会报错 "找不到相应的DLL" 强行退出,请留意!
如果没有找到,点击上面的 "添加功能",搜索"图形工具"并点击安装


新建空项目,链接头文件和DLL

  • 打开 VS2022,新建空项目
  • 解决方案为 "DX12",项目名称为 "001-InitWindow",位置选桌面,然后按"创建"
  • 右键项目 -> "链接器" -> "系统" -> "子系统" -> 选择"窗口" -> 按"确定"

  • 右键项目新建源文件,命名为 "main.cpp"
  • 在 main.cpp 上写上以下代码:
cpp 复制代码
#include<Windows.h> // Windows 窗口编程核心头文件
#include<d3d12.h> // DX12 核心头文件
#include<dxgi1_6.h> // DXGI 头文件,用于管理与 DX12 相关联的其他必要设备,如 DXGI 工厂和 交换链

#include<wrl.h> // COM 组件模板库,方便写 DX12 和 DXGI 相关的接口

#pragma comment(lib,"d3d12.lib") // 链接 DX12 核心 DLL
#pragma comment(lib,"dxgi.lib") // 链接 DXGI DLL
#pragma comment(lib,"dxguid.lib") // 链接 DXGI 必要的设备 GUID

using namespace Microsoft;
using namespace Microsoft::WRL; // 使用 wrl.h 里面的命名空间

快速做一个窗口

1.WinMain 函数:窗口程序的入口点

顾名思义,我们写控制台(黑窗)程序的时候,都是先从 int main() 开始写的,原因是因为 main函数是控制台程序的入口点,是程序开始执行的地方。
像下面这种 GUI 应用(白窗)程序,它的主函数则是:

cpp 复制代码
int WINAPI WinMain(HINSTANCE hins, HINSTANCE hPrev, LPSTR cmdLine, int cmdShow);

第一个参数 hins 表示当前应用程序的实例句柄
第二个参数 hPrev 表示保留句柄,不用管
第三个参数 cmdLine 表示命令行参数,不用管
第四个参数 cmdShow 表示窗口开始时的状态,不用管
每个窗口程序开始时,操作系统都会分配一个实例句柄 hinstance (也就是第一个参数),来标识这个程序是唯一的进程。程序可以用这个 hinstance 来使用操作系统的各种设备,包括下文我们要提到的窗口类。
DirectX 12的接口是超级超级多的。为了方便后面写 3D 程序调用接口,我们把所有的函数都写在一个叫 DX12Engine 的类,主函数调用时直接 DX12Engine::Run() 就行了:

cpp 复制代码
// DX12 引擎
class DX12Engine
{
private:

int WindowWidth = 640; // 窗口宽度
int WindowHeight = 480; // 窗口高度
HWND m_hwnd; // 窗口句柄

public:

// 初始化窗口
void InitWindow(HINSTANCE hins)
{

}

// 渲染循环
void RenderLoop()
{

}

// 回调函数
static LRESULT CALLBACK CallBackFunc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{

}

// 运行窗口
static void Run(HINSTANCE hins)
{

}
};


// 主函数
int WINAPI WinMain(HINSTANCE hins, HINSTANCE hPrev, LPSTR cmdLine, int cmdShow)
{
DX12Engine::Run(hins);
}

2.初始化窗口:InitWindow(HINSTANCE hins)

首先,我们要注册窗口类,窗口类 (Window Class,WndClass) 是一系列窗口属性的集合:

cpp 复制代码
WNDCLASS wc = {}; // 用于记录窗口类信息的结构体
wc.hInstance = hins; // 窗口类需要一个应用程序的实例句柄 hinstance
wc.lpfnWndProc = CallBackFunc; // 窗口类需要一个回调函数,用于处理窗口产生的消息
wc.lpszClassName = L"DX12 Game"; // 窗口类的名称

RegisterClass(&wc); // 注册窗口类,将窗口类录入到操作系统中

WNDCLASS 窗口类结构体
hInstance 代表应用程序的实例句柄。该值就是 WinMain 函数 的 hInstance 参数。
lpfnWndProc 指定窗口类需要绑定的回调函数,处理各种窗口消息
lpszClassname 是一个字符串,用来标识一个窗口类。窗口类的名称在进程内必须唯一,不可以重名,否则会注册失败
然后我们用上文的窗口类,创建一个窗口:

cpp 复制代码
// 使用上文的窗口类创建窗口
m_hwnd = CreateWindow(wc.lpszClassName, L"DX12画窗口", WS_SYSMENU | WS_OVERLAPPED,
10, 10, WindowWidth, WindowHeight,
NULL, NULL, hins, NULL);

// 因为指定了窗口大小不可变的 WS_SYSMENU 和 WS_OVERLAPPED,应用不会自动显示窗口,需要使用 ShowWindow 强制显示窗口
ShowWindow(m_hwnd, SW_SHOW);

CreateWindow 用于创建一个 Windows 窗口
第一个参数 lpClassName 已经注册的窗口类
第二个参数 lpWindowName 窗口标题名
第三个参数 dwStyle 窗口风格,指定窗口长什么样。要指定多个风格可以用 | 连接; WS_SYSMENU 表示使用系统菜单, WS_OVERLAPPED 表示一个带标题栏的普通窗口样式
第四个参数 x 窗口左上角 x 坐标
第五个参数 y 窗口左上角 y 坐标
第六个参数 nWidth 窗口宽度
第七个参数 nHeight 窗口高度
第八个参数 nhWndParent 父窗口句柄,NULL表示这是一个独立的窗口,没有父窗口
第九个参数 hMenu 窗口菜单,NULL表示没有菜单
第十个参数 hInstance 进程实例句柄,这里要填上文 WinMain 的 hinstance ,因为主窗口需要绑定相应进程的 消息队列 ,将产生的窗口消息放入消息队列,再由回调函数逐个获取处理
第十一个参数 lpParam 创建窗口附带的附加参数,NULL表示没有
返回值是一个 HWND 窗口句柄,表示一个窗口
这样我们就成功创建了一个白窗口,运行发现窗口只会闪一下,这是因为我们还需要处理窗口类绑定的回调函数:
wc.lpfnWndProc = CallBackFunc; // 窗口类需要一个回调函数,用于处理窗口产生的消息

3.窗口回调:CallBackFunc()

CallBack 回调函数,顾名思义就是指一个函数作为参数传递给另一个函数,并且在特定事件发生或条件满足时被调用执行的函数。


我们每一次在窗口的操作,都会产生窗口消息 (Message) ,操作系统通过窗口消息告诉你发生了什么事件,例如鼠标移动,键盘响应,程序退出,窗口最大最小化 等等
产生消息 -> 获取消息 -> 处理消息 这一过程称为窗口过程 (Window Procedure) ,操作系统会不停地通过回调函数 来处理各种窗口消息,保证窗口程序的正常运行。所以回调函数也被称为窗口过程函数 (Window Procedure Func,WndProc)

回调函数接收窗口产生的消息,并将处理后的消息发送给窗口
你只需要定义一个回调函数 CallbackFunc,告诉操作系统接收到消息时该怎么处理,然后把函数指针绑定到窗口类的 lpfnWndProc 就行

cpp 复制代码
// 回调函数
static LRESULT CALLBACK CallBackFunc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
// 用 switch 将第二个参数分流,每个 case 分别对应一个窗口消息
switch (msg)
{
case WM_DESTROY: // 窗口被销毁 (当按下右上角 X 关闭窗口时)
PostQuitMessage(0); // 向操作系统发出退出请求 (WM_QUIT),结束消息循环
break;

// 如果接收到其他消息,直接默认返回整个窗口
default: return DefWindowProc(hwnd, msg, wParam, lParam);
}

return 0; // 注意这里!
}

CallBackFunc 处理窗口消息的回调函数,由操作系统调用
LRESULT 一个整形变量,应用程序在执行完窗口过程函数后通过该值将结果返回给 Windows。这个值包含了应用程序对具体消息的处理结果,不同的消息该值可能不同。
CALLBACK 函数调用约定。窗口过程函数本质上是一个回调函数,调用者是操作系统。一个典型的窗口过程函数内部是一个巨大的选择/分支语句 (switch),根据不同的消息类型执行不同的代码逻辑。
hwnd 窗口句柄,指向回调函数绑定的窗口
msg 窗口消息,一次回调只传递一种窗口消息
wParam 附加参数1
lParam 附加参数2
操作系统通过 wParamlParam 传递额外的附加信息,例如鼠标位置,鼠标、键盘按键状态,窗口控件消息等等

4.渲染循环:RenderLoop()

那么操作系统调用回调函数时,如何获取窗口消息呢?
每个进程 (应用程序) 会维护一个 消息队列 (Message Queue) ,窗口产生消息时,会优先将消息放入消息队列中。我们可以在主函数使用 GetMessage() 每次从队首取出一个消息,然后派发给操作系统:

cpp 复制代码
MSG msg = {}; // 消息结构体
while (GetMessage(&msg, NULL, 0, 0) > 0) // 如果程序没有收到退出消息,就一直向消息队列获取消息
{
TranslateMessage(&msg); // 翻译消息,将虚拟按键值转换为对应的 ASCII 码 (后文会讲)
DispatchMessage(&msg); // 派发消息,通知操作系统调用回调函数处理消息
}

GetMessage() 从消息队列中获取一条消息
第一个参数 lpMsg 返回从队列中摘下来的消息
第二个参数 hWnd 要取的是哪个窗口的消息,NULL表示获取该进程 (应用程序) 下所有窗口产生的消息
第三个,第四个参数用于消息过滤,0 (NULL) 表示不过滤,我们用不到它,填0即可
返回值是一个整数 (正整数、0 或 -1),正整数表示成功获取到消息的 ID;0 表示接收到了退出消息 WM_QUIT;-1 表示发生了错误,例如 hwnd 和 lpmsg 都无效的时候

5.运行程序:Run()

万事俱备,只欠东风!我们在 WinMain() 上直接调用静态方法 DX12Engine::Run() 就可以运行程序了:

cpp 复制代码
// 运行窗口
static void DX12Engine::Run(HINSTANCE hins)
{
DX12Engine engine;
engine.InitWindow(hins);
engine.RenderLoop();
}

// 主函数
int WINAPI WinMain(HINSTANCE hins, HINSTANCE hPrev, LPSTR cmdLine, int cmdShow)
{
DX12Engine::Run(hins);
}

PeekMessage() 实现高效的渲染循环

事实上,我们做 3D 游戏是 不用 GetMessage() 来获取窗口消息 的。

原因:GetMessage 函数在消息队列为空时,会阻塞当前线程,使线程停止运行、强制等待,直到队列有消息为止。
阻塞 对于渲染可是一个不好的东西,因为现在的 3D 渲染都是多线程渲染,渲染同时进行的情况下,主函数 (主线程) 还可以处理窗口消息、内存和磁盘文件读写、数据计算等其他事情,阻塞相当于浪费CPU资源,还会使 FPS 帧率大幅降低,造成画面卡顿。
PeekMessage() 能完美解决 GetMessage() 的问题。
PeekMessage 会查看消息队列中有没有消息。不管有没有,获取结果以后,立即返回(不阻塞)。

cpp 复制代码
bool isExit = false; // 是否退出
MSG msg = {}; // 消息结构体

while (isExit != true)
{
// 查看消息队列是否有消息,如果有就获取。 PM_REMOVE 表示获取完消息,就立刻将该消息从消息队列中移除
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
// 如果程序没有收到退出消息,就向操作系统发出派发消息的命令
if (msg.message != WM_QUIT)
{
TranslateMessage(&msg); // 翻译消息,将虚拟按键值转换为对应的 ASCII 码 (后文会讲)
DispatchMessage(&msg); // 派发消息,通知操作系统调用回调函数处理消息
}
else
{
isExit = true; // 收到退出消息,就退出消息循环
}
}
}

第一节全代码

cpp 复制代码
// (1) InitWindow:做一个窗口

#include<Windows.h> // Windows 窗口编程核心头文件
#include<d3d12.h> // DX12 核心头文件
#include<dxgi1_6.h> // DXGI 头文件,用于管理与 DX12 相关联的其他必要设备,如 DXGI 工厂和 交换链

#include<wrl.h> // COM 组件模板库,方便写 DX12 和 DXGI 相关的接口

#pragma comment(lib,"d3d12.lib") // 链接 DX12 核心 DLL
#pragma comment(lib,"dxgi.lib") // 链接 DXGI DLL
#pragma comment(lib,"dxguid.lib") // 链接 DXGI 必要的设备 GUID

using namespace Microsoft;
using namespace Microsoft::WRL; // 使用 wrl.h 里面的命名空间


// DX12 引擎
class DX12Engine
{
private:

int WindowWidth = 640; // 窗口宽度
int WindowHeight = 480; // 窗口高度
HWND m_hwnd; // 窗口句柄

public:

// 初始化窗口
void InitWindow(HINSTANCE hins)
{
WNDCLASS wc = {}; // 用于记录窗口类信息的结构体
wc.hInstance = hins; // 窗口类需要一个应用程序的实例句柄 hinstance
wc.lpfnWndProc = CallBackFunc; // 窗口类需要一个回调函数,用于处理窗口产生的消息
wc.lpszClassName = L"DX12 Game"; // 窗口类的名称

RegisterClass(&wc); // 注册窗口类,将窗口类录入到操作系统中

// 使用上文的窗口类创建窗口
m_hwnd = CreateWindow(wc.lpszClassName, L"DX12画窗口", WS_SYSMENU | WS_OVERLAPPED,
10, 10, WindowWidth, WindowHeight,
NULL, NULL, hins, NULL);

// 因为指定了窗口大小不可变的 WS_SYSMENU 和 WS_OVERLAPPED,应用不会自动显示窗口,需要使用 ShowWindow 强制显示窗口
ShowWindow(m_hwnd, SW_SHOW);
}

// 渲染循环
void RenderLoop()
{
bool isExit = false; // 是否退出
MSG msg = {}; // 消息结构体

while (isExit != true)
{
// 查看消息队列是否有消息,如果有就获取。 PM_REMOVE 表示获取完消息,就立刻将该消息从消息队列中移除
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
// 如果程序没有收到退出消息,就向操作系统发出派发消息的命令
if (msg.message != WM_QUIT)
{
TranslateMessage(&msg); // 翻译消息,将虚拟按键值转换为对应的 ASCII 码 (后文会讲)
DispatchMessage(&msg); // 派发消息,通知操作系统调用回调函数处理消息
}
else
{
isExit = true; // 收到退出消息,就退出消息循环
}
}
}
}

// 回调函数
static LRESULT CALLBACK CallBackFunc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
// 用 switch 将第二个参数分流,每个 case 分别对应一个窗口消息
switch (msg)
{
case WM_DESTROY: // 窗口被销毁 (当按下右上角 X 关闭窗口时)
PostQuitMessage(0); // 向操作系统发出退出请求 (WM_QUIT),结束消息循环
break;

// 如果接收到其他消息,直接默认返回整个窗口
default: return DefWindowProc(hwnd, msg, wParam, lParam);
}

return 0; // 注意这里!
}

// 运行窗口
static void Run(HINSTANCE hins)
{
DX12Engine engine;
engine.InitWindow(hins);
engine.RenderLoop();
}
};


// 主函数
int WINAPI WinMain(HINSTANCE hins, HINSTANCE hPrev, LPSTR cmdLine, int cmdShow)
{
DX12Engine::Run(hins);
}

下一节,我们将正式开始接触 DirectX 12,学会用 DirectX 12 画一个天蓝色的窗口。*

相关推荐
北冥没有鱼啊4 天前
UE 像素和线框盒子 材质
c++·ue5·游戏开发·虚幻·材质
大飞pkz5 天前
【Unity】使用XLua进行热修复
unity·c#·游戏引擎·lua·游戏开发·xlua·lua热修复
工藤新一¹6 天前
C++/SDL 进阶游戏开发 —— 双人塔防(代号:村庄保卫战 19)
开发语言·c++·游戏引擎·游戏开发·sdl
大飞pkz9 天前
【Unity】使用XML进行数据读存的简单例子
xml·unity·c#·游戏引擎·游戏开发·数据读写
大飞pkz9 天前
【Unity】如何解决UI中的Button无法绑定带参数方法的问题
ui·unity·游戏引擎·游戏开发·开发记录·button绑定
大飞pkz9 天前
【Unity】使用LitJson保存和读取数据的例子
unity·游戏引擎·游戏开发·数据保存和读取·游戏中的数据处理·类似jsonunility
大模型铲屎官10 天前
Unity C# 与 Shader 交互入门:脚本动态控制材质与视觉效果 (含 MaterialPropertyBlock 详解)(Day 38)
c语言·unity·c#·交互·游戏开发·材质·shader
Thomas游戏开发11 天前
Unity3D Timeline扩展与自定义事件处理
前端框架·unity3d·游戏开发
龙智DevSecOps解决方案12 天前
CI/CD解决方案TeamCity在游戏开发中的应用价值与优势分析
ci/cd·游戏开发·jetbrains·持续集成·teamcity
工藤新一¹12 天前
C++/SDL 进阶游戏开发 —— 双人塔防(代号:村庄保卫战 14)
开发语言·c++·游戏引擎·游戏开发·sdl·实践项目