利用DirectX 11繪製的大致流程(天空盒示例)

本文的主要內容

剛開始學習DirectX 11時,看了諸多的資料包括源碼後,仍是一頭霧水,花費了大量的時間不斷地理解代碼到底是如何作用於我想要的結果的。本文旨在記錄和闡述各個代碼在整個程序中扮演的角色。並以天空盒的實現作爲了一個小小的例子。本人也是初學這部分內容,若有說的錯誤之處,還望指正。
源碼鏈接


一、準備知識

想要能上手Direct3D,必然不能是零基礎的,在開始學習之前,主要掌握或者瞭解以下知識:

  • 渲染流水線的大致步驟。
  • 空間幾何,線性代數。

掌握上述知識後,就可以開始看別人的代碼跟着學了。本文以天空盒的實現爲例,大致勾畫出一個完整的Direct3D程序應有的步驟。
至於天空盒,是利用立方體映射技術將一個貼着天空紋理的立方體映射到一個球面上,然後通過將天空盒的圓心位置和攝像機座標同時移動形成天空效果。

效果圖如下所示:
在這裏插入圖片描述


二、項目創建和Direct3D初始化

首先應當創建一個桌面應用或者空項目但子系統指定爲窗口(而不是控制檯應用),之後只需要少量的Win32代碼就可以創建出一個窗口,在處理消息的循環中,我們的程序會不斷更新。更新的具體方法可以在MsgProc函數中進行定義。

在這個部分,需要關注的變量和函數有如下:

  • MsgProc函數和MSG messages變量,毋庸置疑,這部分決定了我們的操作帶來什麼樣的效果。至於其中的更多細節,屬於Win32編程中的內容,可參考《 windows 程序設計 第五版》一書。
  • HWND MainWnd變量指的是該窗口的句柄,只有擁有該變量的值,我們才能夠捕獲窗口中的事件以及將場景幾何體渲染到該窗口。
LRESULT CALLBACK MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE prevInstance,
	_In_ LPSTR cmdLine, _In_ int showCmd)
{
	// 這些參數不使用
	UNREFERENCED_PARAMETER(prevInstance);
	UNREFERENCED_PARAMETER(cmdLine);
	UNREFERENCED_PARAMETER(showCmd);

	WNDCLASS wc;
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = MsgProc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = hInstance;
	wc.hIcon = LoadIcon(0, IDI_APPLICATION);
	wc.hCursor = LoadCursor(0, IDC_ARROW);
	wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
	wc.lpszMenuName = 0;
	wc.lpszClassName = L"D3DWndClassName";
	int width = 800, height = 600;

	if (!RegisterClass(&wc))
	{
		MessageBox(0, L"RegisterClass Failed.", 0, 0);
		return false;
	}

	RECT R = { 0, 0, width, height };
	AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);

	HWND MainWnd = CreateWindow(L"D3DWndClassName", L"SkyBox",
		WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, hInstance, 0);
	if (!MainWnd)
	{
		MessageBox(0, L"CreateWindow Failed.", 0, 0);
		return false;
	}

	ShowWindow(MainWnd, SW_SHOW);
	UpdateWindow(MainWnd);
	MSG messages = { 0 };
	
	/* 在這部分就可以創建並初始化我們的Direct3D程序了*/
	
	//這兒會不斷接受並處理消息
	while (messages.message != WM_QUIT)
	{
		if (PeekMessage(&messages, 0, 0, 0, PM_REMOVE))
		{ 
			TranslateMessage(&messages);
			DispatchMessage(&messages);
		}
		// 在這裏處理消息並更新我們的應用程序
	}

	return messages.wParam;
}

當窗口創建完畢,根據窗口的句柄即可以初始化Direct3D所需的資源,這部分的代碼相對變動較小,可以先抄了跑起來再說 之後再細細研究。

初始化Direct3D(D3D)所需的步驟大致如下:

  1. 定義想檢查的設備類型和特徵級別,這部分也就是檢查一下可用的設備(硬件設備,WARP設備,軟件驅動設備之類),以及所支持的DirectX類型(D3D11.0,D3D10.1,D3D10.0等)。

    // 驅動類型數組
    D3D_DRIVER_TYPE driverTypes[] =
    {
    	D3D_DRIVER_TYPE_HARDWARE,
    	D3D_DRIVER_TYPE_WARP,
    	D3D_DRIVER_TYPE_REFERENCE,
    	D3D_DRIVER_TYPE_SOFTWARE,
    };
    // 特性等級數組
    D3D_FEATURE_LEVEL featureLevels[] =
    {
    	D3D_FEATURE_LEVEL_11_1,
    	D3D_FEATURE_LEVEL_11_0,
    	D3D_FEATURE_LEVEL_10_1,
    	D3D_FEATURE_LEVEL_10_0,
    };
    
  2. 創建D3D設備,上下文和交換鏈,D3D設備(ID3D11Device)和上下文(ID3D11DeviceContext)是用於和硬件交互的接口,也是後續用到的最多的東西。交換鏈主要是用雙緩衝技術保證畫面穩定所用。D3D設備和D3D上下文的主要的功能分別爲,D3D設備一般用於分配資源(例如創建緩衝區等),而D3D上下文用於將資源綁定到圖形管線併產生渲染命令等。下段代碼是對這三個創建的示意程序。

    D3D_FEATURE_LEVEL featureLevel;
    ID3D11Device * pd3dDevice;						// D3D設備
    ID3D11DeviceContext * pd3dImmediateContext;		// D3D上下文
    IDXGISwapChain * pSwapChain;					// D3D交換鏈
    // 在hr中可以得知是否創建成功,這個返回值在之後也會反覆出現
    HRESULT hr = D3D11CreateDevice(0, D3D_DRIVER_TYPE_HARDWARE, 0, createDeviceFlags, 0, 0, 
    		D3D11_SDK_VERSION, &d3dDevice, &featureLevel, &d3dImmediateContext);
    // 先對交換鏈描述結構體進行填充,再調用函數來創建交換鏈
    DXGI_SWAP_CHAIN_DESC swapChainDesc;							// 交換鏈描述
    // 填充swapChainDesc
    CreateSwapChain(pd3dDevice, &swapChainDesc, &pSwapChain) 	// 創建交換鏈
    
  3. 創建渲染目標視圖,渲染目標視圖主要爲了在交換鏈的輔助緩存(在後面繪畫的緩衝區)中聯合渲染。

    ID3D11RenderTargetView * pBackBufferTarget;		// 渲染目標視圖
    ID3D11Texture2D * pBackBufferTexture;			// 貼圖
    HRESULT result;
    // 獲取輔助緩存的指針
    result = pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBufferTexture);
    // 創建渲染目標視圖
    result = pd3dDevice->CreateRenderTargetView(pBackBufferTexture, 0, &pBackBufferTarget);
    if (pBackBufferTexture)
    	pBackBufferTexture->Release();	// 釋放交換鏈的後向緩存指針
    d3dContext->OMSetRenderTargets(1, &pBackBufferTarget, 0);	// 
    
  4. 設置視口觀察區,也就是要在屏幕上渲染的區域,單人遊戲即爲D3D交換鏈的寬高即可。設置方法爲先填充D3D11_VIEWPORT結構體,再調用RSSetViewports函數即可。


三、創建幾何體數據

在初始化完上述Direct3D之後,就可以着手準備我們想實現的效果了,如果渲染流程有了大致瞭解的話,那麼就應該知道,我們現在只需要準備頂點數據設置渲染狀態即可,當這些做好之後,就可以調用Draw命令使GPU開始工作。

在該小節,就簡要介紹一下創建頂點數據部分。既然是要繪製天空球,那麼自然是應該創建一個球面,球面是由三角形面片來近似的,具體的創建方法也很簡單,大體思路是從球面上採樣取點,讓一個個三角形拼成大致的球即可。在這裏,主要是強調以下部分:

  • 創建一個幾何體,所需的只有兩個部分,頂點的數據以及索引數據,可以採用vector容器存儲。其中,頂點數據類型隨繪製物體不同包含的內容也不同,在繪製天空球時,僅僅只需要位置POSITION信息,若是場景中的物體且有光照紋理等,一般還需要NORMAL法線向量,TEXCOORD紋理座標等信息。
  • 採用索引來表示三角面片,並且圖元類型爲三角形列表SetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST)時,那麼應當採用逆時針的繪製方法,因爲我們是從圓的內部往外部看。
    準備好這些數據之後,就可以爲渲染做準備了。創建幾何體數據的步驟不一定非要在之前做,只需要在調用繪製命令之前,將所有東西準備好即可。創建球面的示意代碼如下所示:
struct VertexPos		// 頂點結構體
{
	VertexPos() = default;

	VertexPos(const VertexPos&) = default;
	VertexPos& operator=(const VertexPos&) = default;

	VertexPos(VertexPos&&) = default;
	VertexPos& operator=(VertexPos&&) = default;

	constexpr VertexPos(const DirectX::XMFLOAT3& _pos) : pos(_pos) {}

	DirectX::XMFLOAT3 pos;
	static const D3D11_INPUT_ELEMENT_DESC inputLayout[1];		// 這個部分在創建輸入佈局時所需
};

std::vector<VertexPos> vertexVector;		// 頂點數組
std::vector<UINT> indexVector;				// 索引數組

void CreateSphere(float radis, UINT levels, UINT slices)
{
	// 創建天空球的頂點數據和索引數據(逆時針繪製)
	UINT vertexCount = 2 + (levels - 1) * (slices + 1);
	UINT indexCount = 6 * (levels - 1) * slices;
	vertexVector.resize(vertexCount);
	indexVector.resize(indexCount);

	VertexPos vertexData;
	UINT vIndex = 0, iIndex = 0;

	float phi = 0.0f, theta = 0.0f;
	float per_phi = XM_PI / levels;
	float per_theta = XM_2PI / slices;
	float x, y, z;

	// 放入頂端點
	vertexData = {XMFLOAT3(0.0f, radius, 0.0f)};
	vertexVector[vIndex++] = vertexData;

	for (UINT i = 1; i < levels; ++i)
	{
		phi = per_phi * i;
		// 需要slices + 1個頂點是因爲 起點和終點需爲同一點,但紋理座標值不一致
		for (UINT j = 0; j <= slices; ++j)
		{
			theta = per_theta * j;
			x = radius * sinf(phi) * cosf(theta);
			y = radius * cosf(phi);
			z = radius * sinf(phi) * sinf(theta);
			// 計算出局部座標、法向量、Tangent向量和紋理座標
			vertexVector[vIndex++] = { XMFLOAT3(x, y, z) };
		}
	}

	// 放入底端點
	vertexData = { XMFLOAT3(0.0f, -radius, 0.0f) };
	vertexVector[vIndex++] = vertexData;

	// 放入索引
	if (levels > 1)
	{
		for (UINT j = 1; j <= slices; ++j)
		{
			indexVector[iIndex++] = 0;
			indexVector[iIndex++] = j;
			indexVector[iIndex++] = j % (slices + 1) + 1;
		}
	}

	for (UINT i = 1; i < levels - 1; ++i)
	{
		for (UINT j = 1; j <= slices; ++j)
		{
			indexVector[iIndex++] = (i - 1) * (slices + 1) + j;
			indexVector[iIndex++] = i * (slices + 1) + j % (slices + 1) + 1;
			indexVector[iIndex++] = (i - 1) * (slices + 1) + j % (slices + 1) + 1;

			indexVector[iIndex++] = i * (slices + 1) + j % (slices + 1) + 1;
			indexVector[iIndex++] = (i - 1) * (slices + 1) + j;
			indexVector[iIndex++] = i * (slices + 1) + j;
		}
	}

	if (levels > 1)
	{
		for (UINT j = 1; j <= slices; ++j)
		{
			indexVector[iIndex++] = (levels - 2) * (slices + 1) + j;
			indexVector[iIndex++] = (levels - 1) * (slices + 1) + 1;
			indexVector[iIndex++] = (levels - 2) * (slices + 1) + j % (slices + 1) + 1;
		}
	}
}

四、設置渲染狀態

這個部分的工作是最爲繁雜的,各種特效的實現也是依賴於此,由於本文更關注於捋清整個代碼工作流程,就一切從簡,只引入相機部分了(不然看個🔨)。就算偷懶,要做的事情仍然不少,總體而言有如下幾部分:

  • 加載和創建資源
  • 設置渲染流水線狀態
  • 綁定渲染管線並繪製
    接下來將對各個部分進行詳細描述。
1. 加載和創建資源

需要創建的資源有:

  • 頂點緩衝區
  • 索引緩衝區
  • 紋理數據
    頂點緩衝區,索引緩衝區,紋理數據和之前創建的幾何頂點數據是共同使用的,這三部份配合合適的渲染狀態(頂點佈局,着色器等)就可以繪製出令人滿意的效果。
ID3D11ShaderResourceView * pTextureCubeSRV;
// 頂點緩衝區創建
void CreateVertexBuffer(UINT byteWidth, const void *pInitialData, ID3D11Buffer ** pVB)
{
	// 頂點緩衝區描述
	D3D11_BUFFER_DESC vbd;
	ZeroMemory(&vbd, sizeof(vbd));
	vbd.Usage = D3D11_USAGE_IMMUTABLE;
	vbd.ByteWidth = byteWidth;
	vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vbd.CPUAccessFlags = 0;
	// 創建頂點緩衝區
	D3D11_SUBRESOURCE_DATA InitData;
	ZeroMemory(&InitData, sizeof(InitData));
	InitData.pSysMem = pInitialData;
	pd3dDevice->CreateBuffer(&vbd, &InitData, pVB);
}
// 索引緩衝區創建
void CreateIndexBuffer(UINT byteWidth, const void *pInitialData, ID3D11Buffer ** pIB)
{
	// 索引緩衝區描述
	D3D11_BUFFER_DESC ibd;
	ZeroMemory(&ibd, sizeof(ibd));
	ibd.Usage = D3D11_USAGE_IMMUTABLE;
	ibd.ByteWidth = byteWidth;	// 索引類型爲UINT
	ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
	ibd.CPUAccessFlags = 0;
	// 創建索引緩衝區
	D3D11_SUBRESOURCE_DATA InitData;
	ZeroMemory(&InitData, sizeof(InitData));
	InitData.pSysMem = pInitialData;
	pd3dDevice->CreateBuffer(&ibd, &InitData, pIB);
}
// 天空盒紋理加載
CreateDDSTextureFromFile(pd3dDevice, nullptr, L"Texture\\desertcube.dds", nullptr, &pTextureCubeSRV);
  • 深度模板狀態
    深度模板狀態是用來保證天空盒可以通過深度測試的。
void CreateDepthStencilState(ID3D11DepthStencilState **ppDSS)
{
	D3D11_DEPTH_STENCIL_DESC dsDesc;		// 填充該描述即可
	// 允許使用深度值一致的像素進行替換的深度/模板狀態
	// 該狀態用於繪製天空盒,因爲深度值爲1.0時默認無法通過深度測試
	dsDesc.DepthEnable = true;
	dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
	dsDesc.DepthFunc = D3D11_COMPARISON_LESS_EQUAL;

	dsDesc.StencilEnable = false;
	pd3dDevice->CreateDepthStencilState(&dsDesc, ppDSS);
}
  • 頂點着色器
  • 頂點佈局
  • 像素着色器
    着色器一般採用HLSL着色語言編寫,它最終會被顯卡驅動翻譯成GPU可以理解的語言。在以下的函數中,hlslFileName形參對應的文件中中存儲着HLSL着色器的代碼,它會被編譯成顯卡驅動能理解的中間語言,在這兒將它輸出至*.cso文件中,至於HLSL語言和C++如何對應互通,則是通過結構體和綁定的緩衝區來傳遞數據。
HRESULT CreateShaderFromFile(const WCHAR* csoFileNameInOut, const WCHAR* hlslFileName, 
	LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob** ppBlobOut)
{
	HRESULT hr = S_OK;
	// 尋找是否有已經編譯好的頂點着色器
	if (csoFileNameInOut && D3DReadFileToBlob(csoFileNameInOut, ppBlobOut) == S_OK)
	{
		return hr;
	}
	else
	{
		DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
		ID3DBlob* errorBlob = nullptr;
		hr = D3DCompileFromFile(hlslFileName, nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entryPoint, shaderModel,
			dwShaderFlags, 0, ppBlobOut, &errorBlob);
		if (FAILED(hr))
		{
			if (errorBlob != nullptr)
			{
				OutputDebugStringA(reinterpret_cast<const char*>(errorBlob->GetBufferPointer()));
			}
			SAFE_RELEASE(errorBlob);
			return hr;
		}
		// 若指定了輸出文件名,則將着色器二進制信息輸出
		if (csoFileNameInOut)
		{
			return D3DWriteBlobToFile(*ppBlobOut, csoFileNameInOut, FALSE);
		}
	}
	return hr;
}

// 創建頂點着色器
ID3DBlob * blob;
void CreateVertexShader(ID3D11VertexShader ** pVertexShader, const WCHAR* csoFileName, const WCHAR* hlslFileName)
{
	CreateShaderFromFile(csoFileName, hlslFileName, "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf());
	pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pVertexShader);
}
// 創建頂點佈局
void CreateInputLayout(ID3D11InputLayout ** pVertexLayout, const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, UINT NumElements)
{
	pd3dDevice->CreateInputLayout(pInputElementDescs, NumElements, blob->GetBufferPointer(), blob->GetBufferSize(), pVertexLayout);
}
// 創建像素着色器
void CreatePixelShader(ID3D11PixelShader ** pPixelShader, const WCHAR* csoFileName, const WCHAR* hlslFileName)
{
	CreateShaderFromFile(csoFileName, hlslFileName, "PS_3D", "ps_5_0", blob.ReleaseAndGetAddressOf());
	pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pPixelShader);
}
  • 常量緩衝區
    常量緩衝區是實現相機,光照,反射等效果所用到的重要部分。以相機爲例,我們得知了相機的視角矩陣和投影矩陣之後,那麼我們需要更新這兩個矩陣對應的緩衝區,並將新的數據傳進去,才能被着色器所用,繪製出新的畫面。着色器和常量緩衝區決定了各個頂點畫出來在什麼位置,是什麼樣子,它們是一起作用的。
    創建常量緩衝區的代碼如下所示:
// 創建緩衝區
void CreateConstBuffer(UINT byteWidth, ID3D11Buffer ** cb)
{
	// 常量緩衝區描述
	D3D11_BUFFER_DESC cbd;
	ZeroMemory(&cbd, sizeof(cbd));
	cbd.Usage = D3D11_USAGE_DYNAMIC;
	cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	cbd.ByteWidth = byteWidth;
	pd3dDevice->CreateBuffer(&cbd, nullptr, cb);
}
2. 設置渲染流水線狀態

這部分也就是把上述初始化以及創建的資源綁定到渲染管線上,我們只需要調用對應的方法即可。需要注意的是,如果在繪製天空球之前採用的不是這些渲染資源,那麼我們需要重新調用以下函數來設置渲染狀態(資源創建只需要調用一次)。

void SetRender(ID3D11DeviceContext * deviceContext)
{
	deviceContext->IASetInputLayout(m_pVertexPosLayout.Get());		// 設置輸入佈局
	deviceContext->VSSetShader(m_pSkyVS.Get(), nullptr, 0);			// 設置頂點着色器
	deviceContext->PSSetShader(m_pSkyPS.Get(), nullptr, 0);			// 設置像素着色器

	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);	// 設置圖元類型

	deviceContext->GSSetShader(nullptr, nullptr, 0);
	//deviceContext->RSSetState(RenderStates::RSNoCull.Get());

	//deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());

	deviceContext->OMSetDepthStencilState(pDSSLessEqual.Get(), 0);		// 設置深度/模板狀態
	deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}
3. 綁定渲染管線並繪製

在這個部分,也就是要繪製我們的模型了,可想而知,也就是切換頂點緩衝區,索引緩衝區,紋理資源等,因爲我們不可能就只畫我們的天空球,而這些也是組成我們天空球的部分。除此之外,在本文還需要更新常量緩衝區的內容,具體細節,在後面會更詳細敘述。以下代碼中還有攝像機作爲參數,因爲天空球需要隨攝像機移動也跟着移動,所需相機的位置也是需要知道的。

void Draw(ID3D11DeviceContext * deviceContext, const WZCamera * camera)
{
	UINT strides = sizeof(VertexPos);
	UINT offsets = 0;
	deviceContext->IASetVertexBuffers(0, 1, &pVertexBuffer, &strides, &offsets);	// 設置頂點緩衝區
	deviceContext->IASetIndexBuffer(pIndexBuffer, DXGI_FORMAT_R32_UINT, 0);			// 設置索引緩衝區
	// 先在結構體變量中更新值
	XMFLOAT3 pos = camera->GetPosition();
	varCBSky.worldViewProj = XMMatrixTranspose(XMMatrixTranslation(pos.x, pos.y, pos.z) * (camera->GetViewProjXM()));
	// 再更新至對應的常量緩衝區中
	D3D11_MAPPED_SUBRESOURCE mappedData;
	deviceContext->Map(pCBSkyBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);
	memcpy_s(mappedData.pData, sizeof(varCBSky), &varCBSky, sizeof(varCBSky));
	deviceContext->Unmap(pCBSkyBuffer.Get(), 0);

	// 將緩衝區綁定到渲染管線上
	deviceContext->VSSetConstantBuffers(0, 1, &pCBSkyBuffer);
	// 設置SRV
	deviceContext->PSSetShaderResources(0, 1, &pTextureCubeSRV);
	// 繪製
	deviceContext->DrawIndexed(indexCount, 0, 0);
}

至此,如果一切ok的話,在DrawIndexed方法調用之後,畫面就應該顯現在最初創建的窗口中了(上述代碼只是示意,並不完整)。


五、天空盒實現的更多細節

以上部分基本就是整個繪製的流程了,總結而言,就是先初始化設備環境,然後創建渲染所需資源,再然後就是綁定到渲染管線上,最後調用Draw方法即可繪製了。
想要實現一個完整的天空盒除了上述部分,還有其餘的一些操作輔助,包括鼠標鍵盤事件的捕獲和響應,攝像機類的實現以及立方體映射等。關於鼠標鍵盤以及攝像機類的實現,相關的資料教程很多,就不再贅述。在這小節中,主要敘述一下,我們用HLSL語言編寫的着色器到底是如何與C++代碼共同工作的(只因我自己曾糾結此處好久)。
當然,我所說的肯定不會是CPU和GPU如何通信之類,僅僅只是最淺層的配置而已,實踐起來也很簡單。

在本文天空盒繪製中,拋開對三角面片着色等不說,將繪製的天空變換投影到窗口上,這部分就需要頂點着色器來參與完成。至於代碼,就以下定義在Sky_VS.hlsl文件中的短短几行。

VertexPosHL Sky_VS(VertexPos vIn)
{
    VertexPosHL vOut;
    
	// 深度設置爲1,保證在無窮遠處
    float4 posH = mul(float4(vIn.PosL, 1.0f), g_WorldViewProj);
    vOut.PosH = posH.xyww;
    vOut.PosL = vIn.PosL;
    return vOut;
}

其中VertexPos就對應着我們最初的頂點數據類型,該結構體類型仍需要在HLSL代碼中進行定義。當我們調用SetVertexBuffers後,我們在頂點緩衝區中的頂點數據就會流入至上述着色器的形參VertexPos vIn中。
同理,VertexPosHL就代表着頂點着色器的輸出,像素着色器的輸入。
最後,觀察投影矩陣g_WorldViewProj是在另外一個結構體中定義的,這個結構體後的register(b0)表示它綁定到了0號端口,在我們的C++代碼中,我們只需要在調用SetVSConstantBuffers,也將同等語義對應的緩衝區綁定到0號端口即可。

// Sky.hlsli
cbuffer SkyBufferStruct : register(b0)
{
    matrix g_WorldViewProj;
}

struct VertexPos
{
    float3 PosL : POSITION;
};
// D3DSky.h
struct SkyBufferStruct
{
	DirectX::XMMATRIX worldViewProj;
};

struct VertexPos
{
	DirectX::XMFLOAT3 pos;
	static const D3D11_INPUT_ELEMENT_DESC inputLayout[1];
};

對於常量緩衝區而言,其所需要的操作都大致如下:

  • 創建緩衝區
  • 設置緩衝區
  • 利用變量更新數據
struct SkyBufferStruct
{
	DirectX::XMMATRIX worldViewProj;
};
ComPtr<ID3D11Buffer> pCBSkyBuffer;
SkyBufferStruct varCBSky;

d3dHelper->CreateConstBuffer(sizeof(SkyBufferStruct), pCBSkyBuffer.GetAddressOf());	// 創建緩衝區
d3dHelper->SetVSConstantBuffers(0, 1, pCBSkyBuffer.GetAddressOf());					// 綁定緩衝區到0號端口
// 修改varCBSky的值
d3dHelper->UpdateConstBuffer(varCBSky, pCBSkyBuffer.Get());							// 更新常量緩衝區

通過在程序中不斷更新常量緩衝區的值,我們的程序也就會展現出期望的效果。例如本文中隨着攝像頭移動,天空地面的全貌也能得以欣賞。

最後,該天空盒的代碼可在鏈接中進行下載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章