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 画一个天蓝色的窗口。*

相关推荐
技术小甜甜1 天前
【Blender Texture】【游戏开发】高质感 Blender 4K 材质资源推荐合集 —— 提升场景真实感与美术表现力
blender·游戏开发·材质·texture
Thomas游戏开发2 天前
Unity3D TextMeshPro终极使用指南
前端·unity3d·游戏开发
Thomas游戏开发3 天前
Unity3D 逻辑代码性能优化策略
前端框架·unity3d·游戏开发
Thomas游戏开发4 天前
Unity3D HUD高性能优化方案
前端框架·unity3d·游戏开发
陈哥聊测试5 天前
游戏公司如何同时管好上百个游戏项目?
游戏·程序员·游戏开发
一名用户6 天前
unity随机生成未知符号教程
c#·unity3d·游戏开发
Be_Somebody10 天前
计算机图形学——Games101深度解析_第二章
游戏开发·计算机图形学·games101
GameTomato11 天前
【IOS】【OC】【应用内打印功能的实现】如何在APP内实现打印功能,连接本地打印机,把想要打印的界面打印成图片
macos·ios·objective-c·xcode·游戏开发·cocos2d
Be_Somebody11 天前
计算机图形学——Games101深度解析_第一章
游戏开发·计算机图形学·games101
飞起的猪22 天前
【虚幻引擎】UE5独立游戏开发全流程(商业级架构)
ue5·游戏引擎·游戏开发·虚幻·独立开发·游戏设计·引擎架构