【图形学与游戏编程】开发笔记-基础篇2:DX11初始化

(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn.net/pancy12138)

这篇文章应该属于开始编程的第一节了,这一节我们将要讲解如何使用C++初始化directx,以及一些新的调试技巧。因为windows程序框架相关的内容在入门篇的时候讲过了,而且也非常简单,如当初所说,大部分都是些傀儡代码,没什么难处。大家很容易就能看懂。所以这里不会再继续讲解这些东西,以图形学相关的知识为重点吐舌头

首先,我们需要知道directx11是面向对象的,这一点和openGL有本质上的区别。在dx11里面,其所有API函数几乎都被封装在了两个类里面,一个是d3ddevice(也就是d3d设备),一个是d3dcontex(也就是渲染描述表)。这两个类一个负责管理资源,一个负责管理绘制。这与当年的dx9是不同的,dx9当初把所有的API函数都封装在了d3ddevice里面,并没有把渲染部分的API拆成一个单独的类。因此dx11的架构可以说是更为细致和方便了一些。那么我们程序的书写方式也就很明显了,既然所有的API函数都存放在这两个类里面,那么我们自然应该首先注册和初始化这两个类了。那么注册这两个类需要神马东西呢。很显然,至少我们得需要一些窗口的信息,不然的话d3d是无法知道该在哪个窗口上做绘制工作的。那么也就是说在注册这两个类之前我们必须要先把窗口给注册了。那么下面我截取一部分winmain函数的代码来大致说明一下directx的两个重要的类(d3d设备以及d3d描述表)在winmain函数里的注册方式:

wndclass.lpfnWndProc = WndProc;                                   //确定窗口的回调函数,当窗口获得windows的回调消息时用于处理消息的函数。
	wndclass.cbClsExtra = 0;                                         //为窗口类末尾分配额外的字节。
	wndclass.cbWndExtra = 0;                                         //为窗口类的实例末尾额外分配的字节。
	wndclass.hInstance = hInstance;                                 //创建该窗口类的窗口的句柄。
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);          //窗口类的图标句柄。
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);              //窗口类的光标句柄。
	wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);     //窗口类的背景画刷句柄。
	wndclass.lpszMenuName = NULL;                                      //窗口类的菜单。
	wndclass.lpszClassName = TEXT("pancystar_engine");                                 //窗口类的名称。

	if (!RegisterClass(&wndclass))                                      //注册窗口类。
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"),
			TEXT("pancystar_engine"), MB_ICONERROR);
		return E_FAIL;
	}
	RECT R = { 0, 0, window_width, window_hight };
	AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
	int width = R.right - R.left;
	int height = R.bottom - R.top;

	hwnd = CreateWindow(TEXT("pancystar_engine"), // window class name创建窗口所用的窗口类的名字。
		TEXT("pancystar_engine"), // window caption所要创建的窗口的标题。
		WS_OVERLAPPEDWINDOW,        // window style所要创建的窗口的类型(这里使用的是一个拥有标准窗口形状的类型,包括了标题,系统菜单,最大化最小化等)。
		CW_USEDEFAULT,              // initial x position窗口的初始位置水平坐标。
		CW_USEDEFAULT,              // initial y position窗口的初始位置垂直坐标。
		width,               // initial x size窗口的水平位置大小。
		height,               // initial y size窗口的垂直位置大小。
		NULL,                       // parent window handle其父窗口的句柄。
		NULL,                       // window menu handle其菜单的句柄。
		hInstance,                  // program instance handle窗口程序的实例句柄。
		NULL);                     // creation parameters创建窗口的指针
	if (hwnd == NULL) 
	{
		return E_FAIL;
	}
	ShowWindow(hwnd, SW_SHOW);   // 将窗口显示到桌面上。
	UpdateWindow(hwnd);           // 刷新一遍窗口(直接刷新,不向windows消息循环队列做请示)。
	ZeroMemory(&msg, sizeof(msg));
	d3d_pancy_1 *d3d11_test = new d3d_pancy_1(hwnd, viewport_width, viewport_height, hInstance);
	if (d3d11_test->init_create() == S_OK)
	{
		while (msg.message != WM_QUIT)
		{
			if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
			{
				TranslateMessage(&msg);//消息转换
				DispatchMessage(&msg);//消息传递给窗口过程函数
				d3d11_test->update();
				d3d11_test->display();
			}
			else
			{
				d3d11_test->update();
				d3d11_test->display();
			}
		}
		d3d11_test->release();
	}

上面d3d11->initcreate()函数就是我们用来注册d3d设备,描述表以及在他们两个类管理下的其他资源的函数。那么现在我们来看看如何才能初始化这两个重要的类以及在他们管理下的诸多资源,当然,因为这是第一个最简单的程序,因此不会有太多的资源被注册,大家只需要关心一些比较重要的资源注册情况。首先看那两个重要的类的注册方法,其实所谓的注册两大核心类,只需要一个函数D3D11CreateDevice()就可以了:

HRESULT WINAPI D3D11CreateDevice(
    _In_opt_ IDXGIAdapter* pAdapter,
    D3D_DRIVER_TYPE DriverType,
    HMODULE Software,
    UINT Flags,
    _In_reads_opt_( FeatureLevels ) CONST D3D_FEATURE_LEVEL* pFeatureLevels,
    UINT FeatureLevels,
    UINT SDKVersion,
    _Out_opt_ ID3D11Device** ppDevice,
    _Out_opt_ D3D_FEATURE_LEVEL* pFeatureLevel,
    _Out_opt_ ID3D11DeviceContext** ppImmediateContext );
上面就是注册函数的定义部分,第一个参数是你的显卡标识,填上NULL就是默认显卡啦,那有人就问了,啥是默认显卡呢,大部分人的显卡肯定有分集显和独显,一般驱动也会带一个调配节能/高性能的软件,一般来说这个驱动软件工作在神马性能上,你的默认显卡就算是哪个性能的显卡了。第二个参数是创建的驱动类型,也就是渲染的时候究竟用CPU(软件模拟)还是GPU(硬件模拟)神马的。这个一般来说只是对于那些老掉牙的机器才会出现显卡驱动不支持的情况,对于现在大部分机器来说,这块不用想了,直接填硬件模拟就行了,千万别用CPU去做渲染,到时候游戏帧率之慢能吓死你大笑。不过对于有些显卡你在直接指定更高级显卡的时候填D3D_DRIVER_TYPE_HARDWARE会出现不识别的情况,这个时候填D3D_DRIVER_TYPE_UN KNOWN也是可以让他做硬件模拟的,不过除非它提示你不识别,否则就不要这么开了。第三个参数是标识软件模拟参数的,如果前面都已经指定了要用硬件模拟的话,这里直接赋值为NULL就可以了。第四个参数是创建格式,也就是debug或者release格式,后者的渲染速度要快于前者,但是会丧失一些调试信息,具体对我们的影响就是vs的那个图形调试器会变得很慢很慢。所以大家在写程序调程序的时候最好选debug格式,然后发布的时候挑一个NULL就可以了。后面紧接着的三个参数是指定directx版本的,这里直接填上缺省的参数NULL NULL和D3D11_SDK_VERSION就可以了。然后最后面的参数就是我们需要注册的d3d设备和d3d描述表,当然还有一个featurelevel用于检验是不是真的创建了dx11版本的设备和描述表。

OK这样大家就了解这两个最重要的类是怎么创建出来的了。下面贴出整个创建的函数,来讲解创建完两大基础类之后我们需要做的事情。

bool d3d_pancy_basic::init(HWND hwnd_need, UINT width_need, UINT hight_need)
{
	UINT create_flag = 0;
	bool if_use_HIGHCARD = true;
	//debug格式下选择返回调试信息
#if defined(DEBUG) || defined(_DEBUG)
	create_flag = D3D11_CREATE_DEVICE_DEBUG;
	if_use_HIGHCARD = false;
#endif
	//~~~~~~~~~~~~~~~~~创建d3d设备以及d3d设备描述表
	HRESULT hr;
	if (if_use_HIGHCARD == true)
	{
		std::vector<IDXGIAdapter1*> vAdapters;
		IDXGIFactory1* factory;
		CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory);
		IDXGIAdapter1 * pAdapter = 0;
		DXGI_ADAPTER_DESC1 pancy_star;
		UINT i = 0;
		HRESULT check_hardweare;
		while (factory->EnumAdapters1(i, &pAdapter) != DXGI_ERROR_NOT_FOUND)
		{
			vAdapters.push_back(pAdapter);
			++i;
		}
		vAdapters[1]->GetDesc1(&pancy_star);
		hr = D3D11CreateDevice(vAdapters[1], D3D_DRIVER_TYPE_UNKNOWN, NULL, create_flag, 0, 0, D3D11_SDK_VERSION, &device_pancy, &leave_need, &contex_pancy);
		int a = 0;
	}
	else
	{
		hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, create_flag, 0, 0, D3D11_SDK_VERSION, &device_pancy, &leave_need, &contex_pancy);
	}
	if (FAILED(hr))
	{
		MessageBox(hwnd_need, L"d3d设备创建失败", L"提示", MB_OK);
		return false;
	}
	if (leave_need != D3D_FEATURE_LEVEL_11_0)
	{
		MessageBox(hwnd_need, L"显卡不支持d3d11", L"提示", MB_OK);
		return false;
	}
	//return true;
	//~~~~~~~~~~~~~~~~~~检查是否支持四倍抗锯齿
	device_pancy->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, &check_4x_msaa);
	if (create_flag == D3D11_CREATE_DEVICE_DEBUG)
	{
		assert(check_4x_msaa > 0);
	}
	//~~~~~~~~~~~~~~~~~~设置交换链的缓冲区格式信息
	DXGI_SWAP_CHAIN_DESC swapchain_format;//定义缓冲区结构体
	swapchain_format.BufferDesc.Width = width_need;
	swapchain_format.BufferDesc.Height = hight_need;
	swapchain_format.BufferDesc.RefreshRate.Numerator = 60;
	swapchain_format.BufferDesc.RefreshRate.Denominator = 1;
	swapchain_format.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	swapchain_format.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
	swapchain_format.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
	//设置缓冲区的抗锯齿信息
	if (check_4x_msaa > 0)
	{
		swapchain_format.SampleDesc.Count = 4;
		swapchain_format.SampleDesc.Quality = check_4x_msaa - 1;
	}
	else
	{
		swapchain_format.SampleDesc.Count = 1;
		swapchain_format.SampleDesc.Quality = 0;
	}
	swapchain_format.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;//渲染格式为渲染到缓冲区
	swapchain_format.BufferCount = 1;                              //仅使用一个缓冲区作为后台缓存
	swapchain_format.OutputWindow = hwnd_need;                     //输出的窗口句柄
	swapchain_format.Windowed = true;                              //窗口模式
	swapchain_format.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;        //让渲染驱动选择最高效的方法
	swapchain_format.Flags = 0;                                    //是否全屏调整
	//~~~~~~~~~~~~~~~~~~~~~~~创建交换链
	IDXGIDevice *pDxgiDevice(NULL);
	HRESULT hr1 = device_pancy->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&pDxgiDevice));
	IDXGIAdapter *pDxgiAdapter(NULL);
	hr1 = pDxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&pDxgiAdapter));
	IDXGIFactory *pDxgiFactory(NULL);
	hr1 = pDxgiAdapter->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&pDxgiFactory));
	hr1 = pDxgiFactory->CreateSwapChain(device_pancy, &swapchain_format, &swapchain);
	//释放接口  
	pDxgiFactory->Release();
	pDxgiAdapter->Release();
	pDxgiDevice->Release();
	change_size();
	return true;
}
当两个基本的类被创建完毕之后,接下来我们要做的就是创建交换链和缓冲区了。那么神马是交换链,神马又是缓冲区呢。下面我先讲交换链。首先,交换链是一个比较著名的游戏算法“双缓冲抗闪屏”。这个算法不仅仅是在3D游戏中会用到,以往的2D游戏也会经常的用到。如果大家以前写过一些2D的游戏的话肯定已经接触过这个算法了。那么这个算法究竟有什么用呢,我们可以看下面的图片来讲解:

这就是一个 模仿flappy bird的2D游戏,现在我们来分析,在每一帧,我们应该怎么给屏幕上绘制这些东西。这个时候有些人肯定会说了,这个还不简单,先给屏幕上贴上背景,然后再一根根的把柱子画上去,最后再把精灵贴上去不就齐活了........当然,这种做法可以达到目的,但是会出现一个问题,当帧率不是很高的时候,贴柱子,帖背景,贴精灵这些事情中间是有间隔的,那到最后观众就会感觉整个屏幕上一大堆东西在闪来闪去的。这就是所谓的“闪屏”。那么怎么解决这个问题呢? 很简单,我们先把要贴的东西不直接贴到显示屏上,先一个个的贴到一个后台内存里面,最后把这个内存直接一整张的贴到到屏幕上就可以了。这种算法就叫做双缓存,也就是说我们准备一张后台缓冲区,然后所有的绘制操作都绘制在它上面,最后再一股脑的把它的数据交换到显示屏上。当然,在3D游戏中,我们也是把每个模型的效果都先投影到这个屏幕上,然后再交换给前台。这也就是交换链的作用,通过不停地把前后台数据作交换来防止闪屏的出现。上面的代码里面就是设置交换链缓冲区的代码,每一个参数的作用我都写在后面的注释里了,大家可以看一看,其实主要就是告诉d3d后台缓冲区的大小啊,格式啊这些信息,大部分参数都不是很重要直接copy就好了。这里给大家需要详细介绍的是抗锯齿,神马是抗锯齿呢,我们知道,无论是3D模型,还是投影之后的2D矢量图,都是不能直接在屏幕上显示的,因此就需要一些算法来进行矢量->光栅图的转换,这一转换就会出现一些边缘的锯齿现象。那么如何才能把这些锯齿的边缘消除呢,当然最简单的办法是提升分辨率。但是很明显我们电脑的分辨率一般不怎么变。这里一般都采用多重采样抗锯齿,也就是对于每一个像素,转换的时候看看他周围像素的光栅化情况来决定这个像素是保持全部的颜色,还是只保持一部分颜色。具体的算法我就不详细说了,很多图形学课本上会讲到,而且以后大家学到ssao或者shadow map的时候也会用到相关的算法,到时候我们再详谈。那么究竟光栅化的时候参考周围的几个像素就成了一个问题,多了影响效率,少了影响效果。那么这里设置的抗锯齿倍数就是根据机器的性能来决定了。现在大部分的电脑都是支持4倍抗锯齿的,至于8倍或者16倍估计有些电脑就不支持了。不过抗锯齿属于一种效果不明显但是消耗特别大的算法,在玩游戏的时候一般都是能不开尽量不开的。这里作为参考,大家可以在程序里面根据自己的喜好来设置这个参数。

当交换链完成之后,我们就只剩下最后一个步骤了,设置缓冲区以及视口了。下面最后一部分的代码:

	//~~~~~~~~~~~~~~~~~~~~~~~创建视图资源
	ID3D11Texture2D *backBuffer = NULL;
	//获取后缓冲区地址 
	HRESULT hr;
	hr = swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backBuffer));
	//创建视图
	if (FAILED(hr))
	{
		MessageBox(wind_hwnd, L"change size error", L"tip", MB_OK);
		return false;
	}
	hr = device_pancy->CreateRenderTargetView(backBuffer, 0, &m_renderTargetView);
	if (FAILED(hr))
	{
		MessageBox(wind_hwnd, L"change size error", L"tip", MB_OK);
		return false;
	}
	//释放后缓冲区引用  
	backBuffer->Release();
	//~~~~~~~~~~~~~~~~~~~~~~~创建深度及模板缓冲区
	D3D11_TEXTURE2D_DESC dsDesc;
	dsDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
	dsDesc.Width = wind_width;
	dsDesc.Height = wind_hight;
	dsDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
	dsDesc.MipLevels = 1;
	dsDesc.ArraySize = 1;
	dsDesc.CPUAccessFlags = 0;
	dsDesc.MiscFlags = 0;
	dsDesc.Usage = D3D11_USAGE_DEFAULT;
	if (check_4x_msaa > 0)
	{
		dsDesc.SampleDesc.Count = 4;
		dsDesc.SampleDesc.Quality = check_4x_msaa - 1;
	}
	else
	{
		dsDesc.SampleDesc.Count = 1;
		dsDesc.SampleDesc.Quality = 0;
	}
	ID3D11Texture2D* depthStencilBuffer;
	device_pancy->CreateTexture2D(&dsDesc, 0, &depthStencilBuffer);
	device_pancy->CreateDepthStencilView(depthStencilBuffer, 0, &depthStencilView);
	depthStencilBuffer->Release();
	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~绑定视图信息到渲染管线
	contex_pancy->OMSetRenderTargets(1, &m_renderTargetView, depthStencilView);
	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~设置视口变换信息
	viewPort.Width = static_cast<FLOAT>(wind_width);
	viewPort.Height = static_cast<FLOAT>(wind_hight);
	viewPort.MaxDepth = 1.0f;
	viewPort.MinDepth = 0.0f;
	viewPort.TopLeftX = 0.0f;
	viewPort.TopLeftY = 0.0f;
	contex_pancy->RSSetViewports(1, &viewPort);
	return true;

先说缓冲区,其实之前我们设置交换链的时候设置的后台缓冲区也算是一种缓冲区,所谓缓冲区其实可以看做就是一个跟屏幕大小一样的二维数组。而后台缓冲区的作用就是在每帧存储并积累每个光栅信息,最后将存储的信息交换到显示屏幕上。而在这里需要我们设置的缓冲区就一个,就是深度+模板缓冲区。这时候大家估计又要问了,我去你这到底是一个缓冲区还是两个缓冲区啊,说是一个咋后面变俩了。当然,这是一个缓冲区,但是被拆开来做两个用处了,每个像素数据的前24位用来做深度缓冲区,后面8位作为模板缓冲区。前者用于做深度测试,后者用于做模板测试。我们先说什么是深度测试,我们都知道投影的时候,如果前一个物体在投影方向上遮住了后面一个物体,那么最终出现在投影屏幕上的只应该是前面的物体,如何表达这种遮挡关系呢,这里我们就在投影物体到屏幕上的时候顺便记录下屏幕上这一点的至今为止最近的深度,然后根据深度来判断这个物体是否应该被投影下来。而深度缓冲区就是做这个记录的工作的,这样我们才能正确的表达物体之间的深度关系。而模板缓冲区是为用户预留的一个缓冲区,和深度缓冲区不同,我们依靠这个缓冲区可以随意根据我们的想法来决定什么物体应该被投影到屏幕上,一般来说这个缓冲区很少用到,比较著名的用法就是shadow valume阴影体算法。
最后就是介绍视口,其实这个概念比较简单,我们的投影算法是把三维世界整个投影到一个x∈[-1,1],y∈[-1,1]的一张虚拟图片上,之后我们要把这个虚拟图片的矢量信息光栅化就需要知道整个游戏窗口的大小,也就是我们一开始创建的窗口的宽高。只要设置了这个宽高硬件就能进行矢量->光栅坐标的转换了,非常的容易。

那么,当上面这些步骤完成的时候,其实整个基本的directx就算是注册完毕了。接下来就是在winmain函数的消息循环里面不断地绘制,更新,并且刷新屏幕像素就好了。winmain部分的刷新代码在第一段代码里面就已经给出了,可以看到无论是否有消息,我们都强制屏幕进行一次绘制,也就是说我们不会去等待windows消息的回调来决定神马时候绘制。一般来说,我们设计的游戏分成update和display两个部分,前一部分负责更新游戏的信息,后一部分根据更新的信息进行绘制。这一节我们只是讲解如何注册并初始化d3d11,所以我们神马都不需要绘制,只要把屏幕每一帧都清成红色就可以了。下面是display里面的代码:非常简单,每次绘制之前,把我们之前注册的两个缓冲区先清空了(后台缓冲,深度/模板缓冲)。然后在下面就可以进行我们的渲染工作,最后调用交换链把缓冲区的信息交换到屏幕就可以了。

void d3d_pancy_1::display()
{
	//初始化
	XMVECTORF32 color = { 1.0f,0.0f,0.0f,1.0f };
	contex_pancy->ClearRenderTargetView(m_renderTargetView, reinterpret_cast<float*>(&color));
	contex_pancy->ClearDepthStencilView(depthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.f, 0);
	//交换到屏幕
	HRESULT hr = swapchain->Present(0, 0);
	int a = 0;
}
非常简单,每次绘制之前,把我们之前注册的两个缓冲区先清空了(后台缓冲,深度/模板缓冲)。然后在下面就可以进行我们的渲染工作,最后调用交换链把缓冲区的信息交换到屏幕就可以了。



最后的效果就是这个样子,只要清空出一个红色的屏幕出来就算是成功啦。

上述就是整个d3d初始化的方式,而openGL的话初始化的方法跟directx基本上差别不是很大。比较大的区别呢是OpenGl因为不是windows自带的API,所以不能根据简单的根据窗口句柄来找到windows的渲染设备,这就得先获取windows的设备描述表HDC,然后封装成自己的描述表RC才能成功注册,因为之前说过教程以directx为主,所以在这里就不继续细讲OpenGL了,但是我会把我写的OpenGL初始化的代码给大家用于参考和学习。下面是本次教程的代码地址:

dx11及OpenGL4.0初始化

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值