在 DirectX 遊戲中載入資源
大多數遊戲在某些時候會本機儲存體或其他資料流載入資源和資產 (例如著色器、紋理、預先定義網格或其他圖形資料)。 我們在此將逐步引導您全面瞭解,載入這些檔案以在 DirectX C/C++ 通用 Windows 平台 (UWP) 遊戲中使用時必須考慮的事項。
例如,遊戲中多邊形物件的網格可能是使用其他工具建立的,並匯出為特定格式。 對於紋理也是如此,而且更甚者是:雖然大多數工具通常都可以編寫平面的、未壓縮的點陣圖,並且大多數圖形 API 都可以理解,但在遊戲中的使用效率可能非常低。 我們在此引導您完成載入三種不同類型的圖形資源以用於 Direct3D 的基本步驟:網格 (模型)、紋理 (點陣圖) 和已編譯的著色器物件。
您需要瞭解的資訊
技術
- 並行模式程式庫 (ppltasks.h)
必要條件
- 瞭解基本 Windows 執行階段
- 了解非同步工作
- 瞭解 3D 圖形程式設計的基本概念。
此範例也包含三個用於資源載入和管理的程式碼檔案。 您將會在本主題中遇到這些檔案中定義的程式碼物件。
- BasicLoader.h/.cpp
- BasicReaderWriter.h/.cpp
- DDSTextureLoader.h/.cpp
您可以在下列連結中找到這些範例的完整程式碼。
主題 | 說明 |
---|---|
將圖形網格物件轉換並載入到記憶體中的類別和方法的完整程式碼。 |
|
完成類的程式碼以及讀取和寫入一般二進位制資料檔案的方法。 由 BasicLoader class 使用。 |
|
從記憶體載入 DDS 紋理的類別和方法的完整程式碼。 |
指示
非同步載入
使用並行模式程式庫 (PPL) 中的工作範本來處理非同步載入。 工作包含一個方法呼叫,後面跟著一個 lambda,用於在非同步呼叫完成後處理結果,通常遵循以下格式:
task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });
.
可以使用 .then() 語法將任務鏈結在一起,如此當一個操作完成時,可以執行另一個依賴前一操作結果的非同步操作。 如此一來,您就可以在個別執行緒上載入、轉換和管理複雜資產,讓玩家幾乎看不見。
如需詳細資訊,請參閱 C++ 中的非同步程式設計。。
現在,讓我們來看看宣告和建立非同步檔案載入方法 ReadDataAsync 的基本結構。
#include <ppltasks.h>
// ...
concurrency::task<Platform::Array<byte>^> ReadDataAsync(
_In_ Platform::String^ filename);
// ...
using concurrency;
task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
_In_ Platform::String^ filename
)
{
return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
{
return FileIO::ReadBufferAsync(file);
}).then([=](IBuffer^ buffer)
{
auto fileData = ref new Platform::Array<byte>(buffer->Length);
DataReader::FromBuffer(buffer)->ReadBytes(fileData);
return fileData;
});
}
在此程式碼中,當您的程式碼呼叫上面定義的 ReadDataAsync 方法時,將建立一個工作來從檔案系統讀取緩衝區。 完成之後,鏈結工作會取得緩衝區,並使用靜態 DataReader 類型,從該緩衝區將位元組串流至陣列。
m_basicReaderWriter = ref new BasicReaderWriter();
// ...
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
{
// Perform some operation with the data when the async load completes.
});
這是您對 ReadDataAsync 進行的呼叫。 完成時,您的程式碼會收到從提供的檔案讀取的位元組陣列。 由於 ReadDataAsync 本身被定義為工作,因此您可以在傳回位元組陣列時使用 lambda 執行特定操作,例如將該位元組資料傳遞給可以使用它的 DirectX 函式。
如果您的遊戲相當簡單,請在使用者啟動遊戲時,使用類似的方法載入您的資源。 您可以在從 IFrameworkView::Run 實作的呼叫序列中的某個點開始主遊戲迴圈之前執行此操作。 同樣地,您會以非同步方式呼叫您的資源載入方法,讓遊戲可以更快速啟動,因此玩家不必等到載入完成,再進行早期互動。
但是,在所有非同步載入完成之前,您不想正確啟動遊戲! 建立一些在載入完成時發出訊號的方法 (例如特定欄位),並在載入方法上使用 lambda 來在完成時設定該訊號。 在啟動使用這些已載入資源的任何元件之前,請先檢查變數。
以下是使用 BasicLoader.cpp 中定義的非同步方法,在遊戲啟動時載入著色器、網格和紋理的範例。 請注意,當所有載入方法完成時,它會在遊戲物件 m_loadingComplete 上設定一個特定欄位。
void ResourceLoading::CreateDeviceResources()
{
// DirectXBase is a common sample class that implements a basic view provider.
DirectXBase::CreateDeviceResources();
// ...
// This flag will keep track of whether or not all application
// resources have been loaded. Until all resources are loaded,
// only the sample overlay will be drawn on the screen.
m_loadingComplete = false;
// Create a BasicLoader, and use it to asynchronously load all
// application resources. When an output value becomes non-null,
// this indicates that the asynchronous operation has completed.
BasicLoader^ loader = ref new BasicLoader(m_d3dDevice.Get());
auto loadVertexShaderTask = loader->LoadShaderAsync(
"SimpleVertexShader.cso",
nullptr,
0,
&m_vertexShader,
&m_inputLayout
);
auto loadPixelShaderTask = loader->LoadShaderAsync(
"SimplePixelShader.cso",
&m_pixelShader
);
auto loadTextureTask = loader->LoadTextureAsync(
"reftexture.dds",
nullptr,
&m_textureSRV
);
auto loadMeshTask = loader->LoadMeshAsync(
"refmesh.vbo",
&m_vertexBuffer,
&m_indexBuffer,
nullptr,
&m_indexCount
);
// The && operator can be used to create a single task that represents
// a group of multiple tasks. The new task's completed handler will only
// be called once all associated tasks have completed. In this case, the
// new task represents a task to load various assets from the package.
(loadVertexShaderTask && loadPixelShaderTask && loadTextureTask && loadMeshTask).then([=]()
{
m_loadingComplete = true;
});
// Create constant buffers and other graphics device-specific resources here.
}
請注意,工作已使用 && 運算子進行彙總,以便僅當所有工作完成時才會觸發設定載入完成旗標的 lambda。 請注意,如果您有多個旗標,則可能會出現比賽條件。 例如,如果 lambda 依序將兩個旗標設為相同的值,則另一個執行緒如果在設定第二個旗標之前檢查它們,則可能只能看到第一個旗標設定。
您已瞭解如何以非同步方式載入資源檔。 同步檔案載入要簡單得多,您可以在 BasicReaderWriter 的完整程式碼 和 BasicLoader 的完整程式碼中找到它們的範例。
當然,不同的資源和資產類型通常需要額外的處理或轉換,才能準備好在圖形管線中使用。 讓我們看看三種特定的資源類型:網格、紋理和著色器。
載入網格
網格是頂點資料,可以由遊戲中的程式碼按照程序產生,也可以從其他應用程式 (例如 3DStudio MAX 或 Alias WaveFront) 或工具匯出到檔案。 這些網格代表遊戲中的模型,從簡單的基本類型,如立方體和球體到汽車和房屋和人物。 它們通常也會包含色彩和動畫資料,視其格式而定。 我們將著重於只包含頂點資料的網格。
若要正確載入網格,您必須知道網格檔案中的資料格式。 我們上述簡單的 BasicReaderWriter 類型只是將資料當作位元組串流讀取;它不知道位元組資料代表一個網格,更不用說由另一個應用程式匯出的特定網格格式了! 您必須在將網格資料存入記憶體時執行轉換。
(您應該一律嘗試以盡可能接近內部表示法的格式封裝資產資料。這樣做將減少資源利用率並節省時間。)
讓我們從網格的檔案取得位元組資料。 範例中的格式假定該檔案是範例特定的格式,以 .vbo 結尾。 (同樣地,此格式與 OpenGL 的 VBO 格式不同。) 每個頂點本身都會對應到 BasicVertex 類型,該類型是 obj2vbo 轉換器工具程式碼中定義的結構。 .vbo 檔案中頂點資料的配置如下所示:
- 資料串流的前 32 位元 (4 個位元組) 包含網格中的頂點數 (numVertices),表示為 uint32 值。
- 資料流的下一個 32 位元 (4 個位元組) 包含網格中的索引數量 (numIndices),表示為 uint32 值。
- 之後,後續的 (numVertices * sizeof(BasicVertex)) 位元包含頂點資料。
- 最後一個 (numIndices * 16) 位元資料包含索引資料,表示為 uint16 值序列。
重點是:瞭解您已載入之網格資料的位元層級配置。 此外,請確定您與 endian-ness 一致。 所有 Windows 8 平台都是由小到大。
在範例中,您從 LoadMeshAsync 方法來執行此位元層級解釋。
task<void> BasicLoader::LoadMeshAsync(
_In_ Platform::String^ filename,
_Out_ ID3D11Buffer** vertexBuffer,
_Out_ ID3D11Buffer** indexBuffer,
_Out_opt_ uint32* vertexCount,
_Out_opt_ uint32* indexCount
)
{
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ meshData)
{
CreateMesh(
meshData->Data,
vertexBuffer,
indexBuffer,
vertexCount,
indexCount,
filename
);
});
}
CreateMesh 能解釋從檔案載入的位元組資料,並透過將頂點和索引清單分別傳遞到 ID3D11Device::CreateBuffer 並指定 D3D11_BIND_VERTEX_BUFFER 或 D3D11_BIND_INDEX_BUFFER 來為網格建立頂點緩衝區和索引緩衝區。 這是 BasicLoader 中使用的程式碼:
void BasicLoader::CreateMesh(
_In_ byte* meshData,
_Out_ ID3D11Buffer** vertexBuffer,
_Out_ ID3D11Buffer** indexBuffer,
_Out_opt_ uint32* vertexCount,
_Out_opt_ uint32* indexCount,
_In_opt_ Platform::String^ debugName
)
{
// The first 4 bytes of the BasicMesh format define the number of vertices in the mesh.
uint32 numVertices = *reinterpret_cast<uint32*>(meshData);
// The following 4 bytes define the number of indices in the mesh.
uint32 numIndices = *reinterpret_cast<uint32*>(meshData + sizeof(uint32));
// The next segment of the BasicMesh format contains the vertices of the mesh.
BasicVertex* vertices = reinterpret_cast<BasicVertex*>(meshData + sizeof(uint32) * 2);
// The last segment of the BasicMesh format contains the indices of the mesh.
uint16* indices = reinterpret_cast<uint16*>(meshData + sizeof(uint32) * 2 + sizeof(BasicVertex) * numVertices);
// Create the vertex and index buffers with the mesh data.
D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
vertexBufferData.pSysMem = vertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;
CD3D11_BUFFER_DESC vertexBufferDesc(numVertices * sizeof(BasicVertex), D3D11_BIND_VERTEX_BUFFER);
m_d3dDevice->CreateBuffer(
&vertexBufferDesc,
&vertexBufferData,
vertexBuffer
);
D3D11_SUBRESOURCE_DATA indexBufferData = {0};
indexBufferData.pSysMem = indices;
indexBufferData.SysMemPitch = 0;
indexBufferData.SysMemSlicePitch = 0;
CD3D11_BUFFER_DESC indexBufferDesc(numIndices * sizeof(uint16), D3D11_BIND_INDEX_BUFFER);
m_d3dDevice->CreateBuffer(
&indexBufferDesc,
&indexBufferData,
indexBuffer
);
if (vertexCount != nullptr)
{
*vertexCount = numVertices;
}
if (indexCount != nullptr)
{
*indexCount = numIndices;
}
}
您通常會為遊戲中使用的每一個網格建立頂點/索引緩衝區配對。 載入網格的位置和時機會由您決定。 如果您有很多網格,您可能只想在遊戲中的特定點從磁碟載入一些網格,例如在特定的預先定義載入狀態期間。 對於大型網格,例如地形資料,您可以從快取串流頂點,但這是更複雜的程序,而不屬於本主題的範圍。
同樣地,請知道您的頂點資料格式! 有許多方式可以代表用來建立模型的工具的頂點資料。 還有許多不同的方式來表示 Direct3D 的頂點資料的輸入配置,例如三角形清單和條帶。 有關頂點資料的更多資訊,請閱讀 Direct3D 11 和基本類型中的緩衝區簡介。
接下來,讓我們來看看載入紋理。
載入紋理
遊戲中最常見的資產,以及包含磁碟和記憶體中大部分檔案的資產,都是紋理。 就像網格一樣,紋理可以採用各種格式,而且您可以將它們轉換成 Direct3D 載入時可以使用的格式。 紋理也採用各種不同的類型,可用來建立不同的效果。 紋理的 MIP 層級可用於改善遠處物件的外觀和性能;污垢和光照貼圖用於在基礎紋理上分層效果和細節; 法線貼圖用於每像素光照計算。 在現代遊戲中,一般場景可能會有數千個個別紋理,您的程式碼必須有效地管理所有紋理!
與網格一樣,有許多特定格式可用於提高記憶體使用效率。 由於紋理很容易消耗大部分 GPU (和系統) 記憶體,因此它們通常會以某種方式進行壓縮。 您不需要對遊戲的紋理使用壓縮,並且可以使用任何您想要的壓縮/解壓縮演算法,只要您向 Direct3D 著色器提供它可以理解的格式的資料 (例如 Texture2D 點陣圖)。
Direct3D 提供 DXT 紋理壓縮演算法的支援,不過播放器圖形硬體中可能不支援每個 DXT 格式。 DDS 檔案也包含 DXT 紋理 (以及其他紋理壓縮格式),且以 .dds 結尾。
DDS 檔案是包含下列資訊的二進位檔:
包含四個字元代碼值「DDS」(0x20534444) 的 DWORD (魔術數字)。
檔案中資料的描述。
使用 DDS_HEADER 對資料進行標題描述;像素格式使用 DDS_PIXELFORMAT 定義。 請注意,DDS_HEADER 和 DDS_PIXELFORMAT 結構取代了已被取代的 DDSURFACEDESC2、DDSCAPS2 和 DDPIXELFORMAT DirectDraw 7 結構。 DDS_HEADER 是 DDSURFACEDESC2 和 DDSCAPS2 的二進位等效項。 DDS_PIXELFORMAT 是 DDPIXELFORMAT 的二進位等效項。
DWORD dwMagic; DDS_HEADER header;
如果 DDS_PIXELFORMAT 中 dwFlags 的值設定為 DDPF_FOURCC 且 dwFourCC 設定為「DX10」,將存在額外的 DDS_HEADER_DXT10 結構,以容納無法表示為 RGB 像素格式的紋理陣列或 DXGI 格式,例如浮點格式、sRGB 格式等。當存在 DDS_HEADER_DXT10 結構時,整個資料描述將如下所示。
DWORD dwMagic; DDS_HEADER header; DDS_HEADER_DXT10 header10;
指向包含主表面資料的位元組陣列的指標。
BYTE bdata[]
指向包含剩餘表面的位元組陣列的指標,例如;mipmap 層級、立方體貼圖中的面、體積紋理中的深度。 請點擊以下連結以取得更多有關紋理、立方體貼圖或體積紋理的 DDS 檔案配置的資訊。
BYTE bdata2[]
許多工具會匯出至 DDS 格式。 如果您沒有工具可將紋理匯出至此格式,請考慮建立一個。 有關 DDS 格式以及如何在程式碼中使用它的更多詳細資訊,請閱讀 DDS 程式設計指南。 在我們的範例中,我們將使用 DDS。
與其他資源類型一樣,您可以從檔案中以位元組串流的形式讀取資料。 載入工作完成後,lambda 呼叫將執行程式碼 (CreateTexture 方法) 以將位元組串流處理為 Direct3D 可以使用的格式。
task<void> BasicLoader::LoadTextureAsync(
_In_ Platform::String^ filename,
_Out_opt_ ID3D11Texture2D** texture,
_Out_opt_ ID3D11ShaderResourceView** textureView
)
{
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ textureData)
{
CreateTexture(
GetExtension(filename) == "dds",
textureData->Data,
textureData->Length,
texture,
textureView,
filename
);
});
}
在上一個代碼段中,Lambda 會檢查檔名是否具有「dds」的副檔名。 如果有,您假設它是 DDS 紋理。 如果沒有,那麼可以使用 Windows Imaging Component (WIC) API 來探索格式並將資料解碼為點陣圖。 無論哪種方式,結果都是一個 Texture2D 點陣圖 (或一個錯誤)。
void BasicLoader::CreateTexture(
_In_ bool decodeAsDDS,
_In_reads_bytes_(dataSize) byte* data,
_In_ uint32 dataSize,
_Out_opt_ ID3D11Texture2D** texture,
_Out_opt_ ID3D11ShaderResourceView** textureView,
_In_opt_ Platform::String^ debugName
)
{
ComPtr<ID3D11ShaderResourceView> shaderResourceView;
ComPtr<ID3D11Texture2D> texture2D;
if (decodeAsDDS)
{
ComPtr<ID3D11Resource> resource;
if (textureView == nullptr)
{
CreateDDSTextureFromMemory(
m_d3dDevice.Get(),
data,
dataSize,
&resource,
nullptr
);
}
else
{
CreateDDSTextureFromMemory(
m_d3dDevice.Get(),
data,
dataSize,
&resource,
&shaderResourceView
);
}
resource.As(&texture2D);
}
else
{
if (m_wicFactory.Get() == nullptr)
{
// A WIC factory object is required in order to load texture
// assets stored in non-DDS formats. If BasicLoader was not
// initialized with one, create one as needed.
CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_wicFactory));
}
ComPtr<IWICStream> stream;
m_wicFactory->CreateStream(&stream);
stream->InitializeFromMemory(
data,
dataSize);
ComPtr<IWICBitmapDecoder> bitmapDecoder;
m_wicFactory->CreateDecoderFromStream(
stream.Get(),
nullptr,
WICDecodeMetadataCacheOnDemand,
&bitmapDecoder);
ComPtr<IWICBitmapFrameDecode> bitmapFrame;
bitmapDecoder->GetFrame(0, &bitmapFrame);
ComPtr<IWICFormatConverter> formatConverter;
m_wicFactory->CreateFormatConverter(&formatConverter);
formatConverter->Initialize(
bitmapFrame.Get(),
GUID_WICPixelFormat32bppPBGRA,
WICBitmapDitherTypeNone,
nullptr,
0.0,
WICBitmapPaletteTypeCustom);
uint32 width;
uint32 height;
bitmapFrame->GetSize(&width, &height);
std::unique_ptr<byte[]> bitmapPixels(new byte[width * height * 4]);
formatConverter->CopyPixels(
nullptr,
width * 4,
width * height * 4,
bitmapPixels.get());
D3D11_SUBRESOURCE_DATA initialData;
ZeroMemory(&initialData, sizeof(initialData));
initialData.pSysMem = bitmapPixels.get();
initialData.SysMemPitch = width * 4;
initialData.SysMemSlicePitch = 0;
CD3D11_TEXTURE2D_DESC textureDesc(
DXGI_FORMAT_B8G8R8A8_UNORM,
width,
height,
1,
1
);
m_d3dDevice->CreateTexture2D(
&textureDesc,
&initialData,
&texture2D);
if (textureView != nullptr)
{
CD3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc(
texture2D.Get(),
D3D11_SRV_DIMENSION_TEXTURE2D
);
m_d3dDevice->CreateShaderResourceView(
texture2D.Get(),
&shaderResourceViewDesc,
&shaderResourceView);
}
}
if (texture != nullptr)
{
*texture = texture2D.Detach();
}
if (textureView != nullptr)
{
*textureView = shaderResourceView.Detach();
}
}
當此程式碼完成時,記憶體中就會有一個從影像檔載入的 Texture2D。 與網格一樣,您的遊戲和任何指定場景中可能會有很多網格。 考慮為每個場景或每個關卡定期存取的紋理建立快取,而不是在遊戲或關卡開始時載入它們。
(上面範例中呼叫的 CreateDDSTextureFromMemory 方法可以在 Complete code for DDSTextureLoader 的完整程式碼中完整探索。)
此外,個別紋理或紋理「皮膚」可能會對應至特定的網格多邊形或表面。 此對應資料通常由藝術家或設計師用於建立模型和紋理的工具匯出。 確保在載入會出的資料時也擷取此資訊,因為在執行片段著色時,您將使用它將正確的紋理對應到相應的表面。
載入著色器
著色器是編譯後的高階著色器語言 (HLSL) 檔案,這些文件載入到記憶體中並在圖形管道的特定階段叫用。 最常見和最重要的著色器是頂點著色器和像素著色器,它們分別處理網格的各個頂點和場景視窗中的像素。 執行 HLSL 程式碼以變換幾何、套用光照效果和紋理,並對轉譯的場景執行後處理。
一款 Direct3D 遊戲可以有許多不同的著色器,每個著色器都編譯成單獨的 CSO (編譯著色器物件、.cso) 檔案。 通常,您不會有太多而需要動態載入它們,並且在大多數情況下,您可以簡單地在遊戲開始時載入它們,或者在每個關卡的基礎上載入它們 (例如用於下雨效果的著色器)。
BasicLoader 類別中的程式碼為不同的著色器提供了許多多載,包括頂點、幾何、像素和輪廓著色器。 下列程式碼涵蓋像素著色器做為範例。 (您可以在 BasicLoader 的完整程式碼中查看完整的程式碼。)
concurrency::task<void> LoadShaderAsync(
_In_ Platform::String^ filename,
_Out_ ID3D11PixelShader** shader
);
// ...
task<void> BasicLoader::LoadShaderAsync(
_In_ Platform::String^ filename,
_Out_ ID3D11PixelShader** shader
)
{
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
{
m_d3dDevice->CreatePixelShader(
bytecode->Data,
bytecode->Length,
nullptr,
shader);
});
}
在此範例中,您使用 BasicReaderWriter 執行個體 (m_basicReaderWriter) 將提供的已編譯著色器物件 (.cso) 檔案做為位元組串流讀入。 工作完成後,lambda 會使用從檔案載入的位元組資料來呼叫 ID3D11Device::CreatePixelShader。 您的回呼必須設定一些旗標來指出載入成功,並且您的程式碼必須在執行著色器之前檢查此旗標。
頂點著色器比較複雜。 針對頂點著色器,您也會載入定義頂點資料的個別輸入配置。 下列程式碼可用來以非同步方式載入頂點著色器,以及自訂頂點輸入配置。 確保您從網格載入的頂點資訊可以透過此輸入配置正確表示!
讓我們先建立輸入配置,再載入頂點著色器。
void BasicLoader::CreateInputLayout(
_In_reads_bytes_(bytecodeSize) byte* bytecode,
_In_ uint32 bytecodeSize,
_In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC* layoutDesc,
_In_ uint32 layoutDescNumElements,
_Out_ ID3D11InputLayout** layout
)
{
if (layoutDesc == nullptr)
{
// If no input layout is specified, use the BasicVertex layout.
const D3D11_INPUT_ELEMENT_DESC basicVertexLayoutDesc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
m_d3dDevice->CreateInputLayout(
basicVertexLayoutDesc,
ARRAYSIZE(basicVertexLayoutDesc),
bytecode,
bytecodeSize,
layout);
}
else
{
m_d3dDevice->CreateInputLayout(
layoutDesc,
layoutDescNumElements,
bytecode,
bytecodeSize,
layout);
}
}
在此特定配置中,每個頂點都有頂點著色器處理的下列資料:
- 模型座標空間中的 3D 座標位置 (x,y,z),表示為三個 32 位元浮點值。
- 頂點的一般向量,也以三個 32 位元浮點值表示。
- 已轉換的 2D 紋理座標值 (u,v),以一對 32 位元浮點值表示。
這些每個頂點輸入元素稱為 HLSL semantics 語意,它們是一組定義的暫存器,用於將資料傳入和傳出已編譯的著色器物件。 您的管線會針對您所載入網格中的每個頂點執行一次頂點著色器。 語意定義頂點著色器執行時的輸入 (和輸出),並為著色器的 HLSL 程式碼中的每頂點計算提供此資料。
現在,載入頂點著色器物件。
concurrency::task<void> LoadShaderAsync(
_In_ Platform::String^ filename,
_In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
_In_ uint32 layoutDescNumElements,
_Out_ ID3D11VertexShader** shader,
_Out_opt_ ID3D11InputLayout** layout
);
// ...
task<void> BasicLoader::LoadShaderAsync(
_In_ Platform::String^ filename,
_In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
_In_ uint32 layoutDescNumElements,
_Out_ ID3D11VertexShader** shader,
_Out_opt_ ID3D11InputLayout** layout
)
{
// This method assumes that the lifetime of input arguments may be shorter
// than the duration of this task. In order to ensure accurate results, a
// copy of all arguments passed by pointer must be made. The method then
// ensures that the lifetime of the copied data exceeds that of the task.
// Create copies of the layoutDesc array as well as the SemanticName strings,
// both of which are pointers to data whose lifetimes may be shorter than that
// of this method's task.
shared_ptr<vector<D3D11_INPUT_ELEMENT_DESC>> layoutDescCopy;
shared_ptr<vector<string>> layoutDescSemanticNamesCopy;
if (layoutDesc != nullptr)
{
layoutDescCopy.reset(
new vector<D3D11_INPUT_ELEMENT_DESC>(
layoutDesc,
layoutDesc + layoutDescNumElements
)
);
layoutDescSemanticNamesCopy.reset(
new vector<string>(layoutDescNumElements)
);
for (uint32 i = 0; i < layoutDescNumElements; i++)
{
layoutDescSemanticNamesCopy->at(i).assign(layoutDesc[i].SemanticName);
}
}
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
{
m_d3dDevice->CreateVertexShader(
bytecode->Data,
bytecode->Length,
nullptr,
shader);
if (layout != nullptr)
{
if (layoutDesc != nullptr)
{
// Reassign the SemanticName elements of the layoutDesc array copy to point
// to the corresponding copied strings. Performing the assignment inside the
// lambda body ensures that the lambda will take a reference to the shared_ptr
// that holds the data. This will guarantee that the data is still valid when
// CreateInputLayout is called.
for (uint32 i = 0; i < layoutDescNumElements; i++)
{
layoutDescCopy->at(i).SemanticName = layoutDescSemanticNamesCopy->at(i).c_str();
}
}
CreateInputLayout(
bytecode->Data,
bytecode->Length,
layoutDesc == nullptr ? nullptr : layoutDescCopy->data(),
layoutDescNumElements,
layout);
}
});
}
在此程式碼中,讀入頂點著色器的 CSO 檔案的位元組資料後,即可透過呼叫 ID3D11Device::CreateVertexShader 建立頂點著色器。 之後,您會在相同的 Lambda 中建立著色器的輸入配置。
其他著色器類型 (例如輪廓著色器和幾何著色器) 也可能需要特定的設定。 BasicLoader 的完整程式碼和 Direct3D 資源載入範例中提供了各種著色器載入方法的完整程式碼。
備註
此時,您應該瞭解並能夠建立或修改方法,以非同步方式載入常見的遊戲資源和資產,例如網格、紋理和已編譯的著色器。
相關主題