DirectX11编程1 游戏循环框架的搭建

环境:VS2017  语言:C++

 

总起:

本次的博文主要参考红龙书(《Introduction to 3D Game Programming with Directx 11》)和X_Jun96大佬将DirectX SDK移植到最新的Windows SDK的博文:https://blog.csdn.net/x_jun96/article/details/80293670

 

之前的Windows编程系列其实就是DirectX(以下简称Dx)编程,当时简单研究后就没有下文了,3D游戏编程大师技巧这本书也是一直忘记拿出来看,最近趁着工作稍微清闲点,打算研究一下底层的东西。

 

首先说一下红龙书,他的第四章节相当于《Windows游戏编程大师技巧》的所有内容还要多(除了没有实现音乐),所以刚入门的同学还是推荐《Windows游戏编程大师技巧》或跟其内容相似的最新DirectX编程,毕竟《Windows游戏编程大师技巧》实在是太老了,使用的还是Dx7。

 

接着说一下红龙书上的示例,虽说是Dx11,但他当时使用的是独立的DirectX SDK库,就是一个叫做June 2010的SDK库,但后来微软将Dx库合并到了Windows库中了,所以现在编写Dx程序,打开VS2017,安装Windows环境,就能直接编写Dx程序。

 

然后说一下为什么选择Dx11。现在已经有Dx12了,但Dx12不支持现在占有率还非常高的Win7系统,而现在公司内部基本用的都是Win7,所以考虑到兼容性和受众率还是选择了Dx11,当然学完之后想学Dx12甚至是OpenGL会很轻松,毕竟大部分思路是共同的(今天要说的这套框架,其实跟Dx7的也没啥太大区别,多了些东西而已)。

 

接下来进入正题,先附上工程链接:https://github.com/anguangzhihen/Dx11

1.程序最好运行在Win32上(后来我这边的项目都在x64上跑了,没有说明的话x64都是可以运行的);

2.如果Common下的脚本没有找到,请到工程/属性中添加包含目录;

3.所有的练习都在其中,全局搜索“练习”关键字就能找到,想要运行打开注释即可。

 

龙书示例:https://github.com/jjuiddong/Introduction-to-3D-Game-Programming-With-DirectX11

 

Window SDK示例:https://github.com/walbourn/directx-sdk-samples

 

我这边会按照流程梳理一遍整体的思路,然后重点说一下关键的代码。

 

框架的流程和思路:

D3DApp是游戏循环的框架类,InitDirect3DApp作为它的实现对象完成了将整个屏幕显示成蓝色的任务。

 

让我先来看看InitDirect3DApp的代码:

// 最简单的Dx框架类实现
class InitDirect3DApp : public D3DApp
{
public:
	InitDirect3DApp(HINSTANCE hInstance);
	~InitDirect3DApp();

	bool Init();
	void OnResize();
	void UpdateScene(float dt);
	void DrawScene();
};

// 程序入口
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPWSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	// 开启实时内存检测
#if defined(DEBUG) | defined(_DEBUG)
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif

	// 创建游戏运行对象
	InitDirect3DApp theApp(hInstance);
	if (!theApp.Init())
		return 0;
	return theApp.Run();
}

InitDirect3DApp::InitDirect3DApp(HINSTANCE hInstance) : D3DApp(hInstance)
{
}

InitDirect3DApp::~InitDirect3DApp()
{
}

// 初始化
bool InitDirect3DApp::Init()
{
	if (!D3DApp::Init())
		return false;
	return true;
}

// 在玩家改变屏幕分辨率时调用
void InitDirect3DApp::OnResize()
{
	D3DApp::OnResize();
}

// 每帧都会调用一次,用于更新当前的逻辑,dt为上一帧到当前帧的时间,类似于Unity中的Update
void InitDirect3DApp::UpdateScene(float dt)
{
}

// 渲染当前场景,类似Unity生命周期中的Scene rendering阶段(OnWillRenderObject、OnRenderImage等方法)
void InitDirect3DApp::DrawScene()
{
	assert(md3dImmediateContext);
	assert(mSwapChain);

	// 蓝色,Colors类好像被移除了
	static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f };	

	// 清空当前的渲染对象,并所有像素点默认填上蓝色
	md3dImmediateContext->ClearRenderTargetView(mRenderTargetView, reinterpret_cast<const float*>(&blue));
	// 清除深度和模版buffer,深度默认为最远1(范围0到1),深度和模版是以像素点为最小单位的,所以如果一个800*600的画面的话,深度和模版值也会有800*600个
	md3dImmediateContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	// 切换显示和后备缓冲
	mSwapChain->Present(0, 0);
}

 

wWinMain是整个游戏的入口,创建了InitDirect3App对象后,调用Init初始化,然后调用Run进入游戏循环。

 

其中Init调用了父类的Init方法: 

bool D3DApp::Init()
{
	if (!InitMainWindow())
		return false;
	if (!InitDirect3D())
		return false;
	return true;
}

 

初始化了一个窗口后,初始化了Direct对象。

 

Run方法也是父类的:

int D3DApp::Run()
{
	MSG msg = { 0 };
	mTimer.Reset();	// 时间类重置

	// 进入主循环
	while (msg.message != WM_QUIT)
	{
		// 如果收到窗口消息则处理
		if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
		{
			// 否则运行游戏逻辑
			mTimer.Tick();

			if (!mAppPaused)
			{
				CalculateFrameStats();	// 更新当前的帧率状态
				UpdateScene(mTimer.DeltaTime());	// 更新场景内容逻辑
				DrawScene();	// 更新场景的渲染
			}
			else
			{
				// 暂停
				Sleep(100);
			}
		}
	}

	// 游戏结束
	return (int)msg.wParam;
}

 

这个循环是不是似曾相识?进入一个while循环,收到消息就处理,不然就处理游戏内逻辑。

 

整理下来就是这么简单:初始化窗口和DirectX,接着进入游戏循环,处理由子类实现的UpdateScene和DrawScene。

 

DirectX的初始化:

窗口初始化和时间类没什么好说的,重点说一下Dx的初始化,这个操作涵盖了两个方法:InitDirect3D和OnResize。其中OnResize在窗口发生变化时也会调用。

 

让我们一步步来看。

 

创建设备和设备环境

UINT createDeviceFlags = 0;

// DEBUG模式
#if defined(DEBUG) || defined(_DEBUG)
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif 

// 创建设备和设备环境
D3D_FEATURE_LEVEL featureLevel;
HRESULT hr = D3D11CreateDevice(
	0,	// 使用默认显卡
	md3dDriverType,	// 一般使用D3D_DRIVER_TYPE_HARDWARE作为参数,不然买来的显卡只能吃灰了
	0,	// 不使用软件驱动
	createDeviceFlags,	// Debug下,我们要求Dx输出一些log信息
	0, 0,	// 默认等级的特性,默认就是11
	D3D11_SDK_VERSION,	// 我们使用Dx11的SDK
	&md3dDevice,
	&featureLevel,
&md3dImmediateContext);

 

重点说一下第四个Flags参数,我们这边在Debug模式下填入了D3D11_CREATE_DEVICE_DEBUG,但是在Win10下调试可能会产生错误,这时就需要打开Win10的可选功能——Graphics Tools。

 

Flags还可以添加一个D3D11_CREATE_DEVICE_SINGLETHREADED,表明Dx不会被多线程调用,比如在该模式下ID3D11Device::CreateDeferredContext的调用会出错。

 

创建缓冲切换链

// 检查设备是否支持多重采样
HR(md3dDevice->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, &m4xMsaaQuality));
assert(m4xMsaaQuality > 0);

// 缓冲切换链需要完成DXGI_SWAP_CHAIN_DESC结构
DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth;	// 缓冲的宽
sd.BufferDesc.Height = mClientHeight;	// 缓冲的高
sd.BufferDesc.RefreshRate.Numerator = 60;	// 显示刷新率
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;	// 使用最经典的32色格式,大部分情况下都使用该格式,可能有些情况下会舍弃Alpha通道
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;	// 显示扫描线模式,似乎是指定图片显示排列是从前往后还是从后往前,这边是未指定
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;	// 缩放模式

// 使用多重采样MSAA
if (mEnable4xMsaa)
{
	sd.SampleDesc.Count = 4;
	sd.SampleDesc.Quality = m4xMsaaQuality - 1;
}
else
{
	// 不使用
	sd.SampleDesc.Count = 1;
	sd.SampleDesc.Quality = 0;
}

sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;	// 固定,因为我们想要使用后备缓冲
sd.BufferCount = 1;	// 指定一个后备缓冲
sd.OutputWindow = mhMainWnd;	// 输出窗口
sd.Windowed = true;	// 是否窗口化
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;	// 固定,显卡会运行更有效率的方法
sd.Flags = 0;	// 如果指定为DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,在切换为全屏时,会选择最适配的桌面,否则在哪个桌面切换到哪个

// 为了精确获取到SwapChain,必须使用当前设备类查询相关的Factory
IDXGIDevice* dxgiDevice = 0;
HR(md3dDevice->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice));

IDXGIAdapter* dxgiAdapter = 0;
HR(dxgiDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&dxgiAdapter));

IDXGIFactory* dxgiFactory = 0;
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory), (void**)&dxgiFactory));
HR(dxgiFactory->CreateSwapChain(md3dDevice, &sd, &mSwapChain));	// 根据设备和切换链描述创建切换链

ReleaseCOM(dxgiDevice);
ReleaseCOM(dxgiAdapter);
ReleaseCOM(dxgiFactory);

 

多重采样MSAA是在一些硬朗的连接处进行一定的模糊处理,使过渡更为平滑,本次的程序关闭了该技术的使用。

 

创建缓冲时,使用到了DXGI(DirectX Graphics Infrastructure),该API与Dx是分离的,主要用于处理切换链、枚举硬件设备、切换全屏窗口模式,因为是独立的,所以不用Dx使用其他图形API也可调用其方法。

 

创建显示缓存

HR(mSwapChain->ResizeBuffers(1, mClientWidth, mClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
ID3D11Texture2D* backBuffer;
HR(mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backBuffer)));	// 创建一个后备缓存
HR(md3dDevice->CreateRenderTargetView(backBuffer, 0, &mRenderTargetView));	// 创建渲染对象
ReleaseCOM(backBuffer);

GetBuffer的第一个参数指定了当前获取后备缓冲的index,因为只有一个所以只能获取index为0的buffer。

 

CreateRenderTargetView的第二个参数可以指定当前的数据类型,但我们传入的backBuffer自带了类型数据(即一级的mipmap),所以可以为空。

 

创建深度/模板图

// 定义深度/模板图,深度图用于确定遮挡关系,模板图一般用于模板测试表明像素点是否能显示
D3D11_TEXTURE2D_DESC depthStencilDesc;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.MipLevels = 1;	// 固定不生成mipmap
depthStencilDesc.ArraySize = 1;	// 固定只生成一张图
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;	// 深度24位,模板8位

// 重置多重采样
if (mEnable4xMsaa)
{
	depthStencilDesc.SampleDesc.Count = 4;
	depthStencilDesc.SampleDesc.Quality = m4xMsaaQuality - 1;
}
else
{
	depthStencilDesc.SampleDesc.Count = 1;
	depthStencilDesc.SampleDesc.Quality = 0;
}
depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;	// 指定深度/模板图只能由GPU读取写入
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;	// 指定是深度/模板图
depthStencilDesc.CPUAccessFlags = 0;	// 不给予CPU访问权限
depthStencilDesc.MiscFlags = 0;

// 创建深度/模板图
HR(md3dDevice->CreateTexture2D(&depthStencilDesc, 0, &mDepthStencilBuffer));	
HR(md3dDevice->CreateDepthStencilView(mDepthStencilBuffer, 0, &mDepthStencilView));

// 将渲染对象和深度/模板图放到渲染流水线中
md3dImmediateContext->OMSetRenderTargets(1, &mRenderTargetView, mDepthStencilView);

 

D3D11_TEXTURE2D_DESC.Usage可指定的参数:

1.D3D11_USAGE_DEFAULT,GPU可读可写,CPU不可读不可写

2.D3D11_USAGE_IMMUTABLE,GPU只读,除了创建时CPU和GPU都不可写;

3.D3D11_USAGE_DYNAMIC,CPU可写;

4.D3D11_USAGE_STAGING,CPU可读。

 

后两种情况都会在CPU内存和GPU内存间进行图片传输,所以应该避免。

 

D3D11_TEXTURE2D_DESC.BindFlags可指定的参数:

1.D3D11_BIND_DEPTH_STENCIL,深度模板图;

2.D3D11_BIND_RENDER_TARGET,渲染管线中的渲染对象;

3.D3D11_BIND_SHADER_RESOURCE,渲染Shader中的源文件。

 

D3D11_TEXTURE2D_DESC.CPUAccessFlags可指定的参数:

1.空(0),CPU没有访问权限;

2.D3D11_CPU_ACCESS_WRITE,Usage为CPU可写时指定;

3.D3D11_CPU_ACCESS_READ,Usage为CPU可读时指定。

 

设置视窗

// 设置视窗
mScreenViewport.TopLeftX = 0;	// 视窗距窗口的X距离
mScreenViewport.TopLeftY = 0;	// 视窗距窗口的Y距离
mScreenViewport.Width = static_cast<float>(mClientWidth);	// 宽
mScreenViewport.Height = static_cast<float>(mClientHeight);	// 高
mScreenViewport.MinDepth = 0.0f;	// 最小深度
mScreenViewport.MaxDepth = 1.0f;	// 最大深度

md3dImmediateContext->RSSetViewports(1, &mScreenViewport);	// 重置视窗

 

视窗的设置一般按以上参数就可以了,除非两种情况:1.双人同屏,需要两个视窗;2.想要使用Windows的组件(按钮之类),给其留出一些空余空间。第二种情况已经非常少见了,一般是电子游戏早期可能这么做多一些。

 

总结:

窗口初始化、消息处理和时间类本文中没有提到,有需要的可以看一下工程。

 

我这边说一下我研究Dx的流程:

1.将书的一个章节通读一遍;

2.根据龙书的官方示例和Window SDK的官方示例,尽量把程序复写出来;

3.参照X_Jun96大佬的文章把一些过时的方法替换掉;

4.整理整个章节,提取重点部分,写成博文。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值