Caricare risorse nel gioco DirectX
La maggior parte dei giochi, a un certo punto, carica risorse e asset (ad esempio shader, trame, mesh predefinite o altri dati grafici) dalla risorsa di archiviazione locale o da un altro flusso di dati. In questa sezione viene illustrata una panoramica generale di ciò che è necessario prendere in considerazione durante il caricamento di questi file da usare nel gioco di piattaforma UWP (Universal Windows Platform) UWP (DirectX C/C++).
Ad esempio, le mesh per gli oggetti poligonali nel gioco potrebbero essere state create con un altro strumento ed esportate in un formato specifico. Lo stesso vale per le trame e di più: mentre una bitmap piatta e non compressa può essere scritta comunemente dalla maggior parte degli strumenti e compresa dalla maggior parte delle API grafiche, può essere estremamente inefficiente per l'uso nel tuo gioco. In questo articolo vengono illustrati i passaggi di base per caricare tre diversi tipi di risorse grafiche da usare con Direct3D: mesh (modelli), trame (bitmap) e oggetti shader compilati.
Informazioni importanti
Tecnologie
- Libreria di modelli paralleli (ppltasks.h)
Prerequisiti
- Informazioni su Windows Runtime di base
- Comprendere le attività asincrone
- Comprendere i concetti di base della programmazione grafica 3D.
Questo esempio include anche tre file di codice per il caricamento e la gestione delle risorse. In questo argomento verranno visualizzati gli oggetti di codice definiti in questi file.
- BasicLoader.h/.cpp
- BasicReaderWriter.h/.cpp
- DDSTextureLoader.h/.cpp
Il codice completo per questi esempi è disponibile nei collegamenti seguenti.
Argomento | Descrizione |
---|---|
Codice completo per una classe e metodi che converte e caricano oggetti mesh grafici in memoria. |
|
Codice completo per una classe e metodi per la lettura e la scrittura di file di dati binari in generale. Utilizzato dalla classe BasicLoader. |
|
Codice completo per una classe e un metodo che carica una texture DDS dalla memoria. |
Istruzioni
Caricamento asincrono
Il caricamento asincrono viene gestito usando il modello di attività dalla libreria PPL (Parallel Patterns Library). Un'attività contiene una chiamata al metodo seguita da un'espressione lambda che elabora i risultati della chiamata asincrona dopo il completamento e in genere segue il formato di:
task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });
.
Le attività possono essere concatenate usando la sintassi .then(), in modo che, al termine di un'operazione, sia possibile eseguire un'altra operazione asincrona che dipende dai risultati dell'operazione precedente. In questo modo, è possibile caricare, convertire e gestire asset complessi in thread separati in modo che appare quasi invisibile al giocatore.
Per altre informazioni, vedere Programmazione asincrona in C++.
Si esaminerà ora la struttura di base per dichiarare e creare un metodo di caricamento di file asincrono, 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;
});
}
In questo codice, quando il codice chiama il metodo ReadDataAsync definito in precedenza, viene creata un'attività per leggere un buffer dal file system. Al termine, un'attività concatenato accetta il buffer e trasmette i byte da tale buffer in una matrice usando il tipo staticc 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.
});
Ecco la chiamata eseguita a ReadDataAsync. Al termine, il codice riceve una matrice di byte letti dal file fornito. Poiché ReadDataAsync stesso è definito come attività, è possibile usare un'espressione lambda per eseguire un'operazione specifica quando viene restituita la matrice di byte, ad esempio passando i dati di byte a una funzione DirectX che può usarla.
Se il gioco è sufficientemente semplice, caricare le risorse con un metodo simile al seguente quando l'utente avvia il gioco. È possibile eseguire questa operazione prima di avviare il ciclo principale del gioco da un punto nella sequenza di chiamata dell'implementazione di IFrameworkView::Run . Anche in questo caso, chiami i metodi di caricamento delle risorse in modo asincrono in modo che il gioco possa iniziare più rapidamente e quindi il giocatore non deve attendere il completamento del caricamento prima di interagire con le prime interazioni.
Tuttavia, non vuoi avviare il gioco correttamente fino al completamento di tutto il caricamento asincrono! Creare un metodo per segnalare quando il caricamento è completo, ad esempio un campo specifico, e usare le espressioni lambda nei metodi di caricamento per impostare tale segnale al termine. Controllare la variabile prima di avviare tutti i componenti che usano le risorse caricate.
Ecco un esempio che usa i metodi asincroni definiti in BasicLoader.cpp per caricare shader, una mesh e una trama all'avvio del gioco. Si noti che imposta un campo specifico sull'oggetto gioco, m_loadingComplete, al termine di tutti i metodi di caricamento.
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.
}
Si noti che le attività sono state aggregate usando l'operatore && in modo che l'espressione lambda che imposta il flag di completamento del caricamento venga attivata solo al completamento di tutte le attività. Si noti che se si dispone di più bandiere, si ha la possibilità di race condition. Ad esempio, se l'espressione lambda imposta due flag in sequenza sullo stesso valore, un altro thread può visualizzare solo il primo flag impostato se li esamina prima che venga impostato il secondo flag.
Si è visto come caricare i file di risorse in modo asincrono. I caricamenti sincroni dei file sono molto più semplici ed è possibile trovare esempi in Codice completo per BasicReaderWriter e codice completo per BasicLoader.
Naturalmente, diversi tipi di risorse e asset spesso richiedono ulteriore elaborazione o conversione prima che siano pronti per essere usati nella pipeline grafica. Verranno ora esaminati tre tipi specifici di risorse: mesh, trame e shader.
Caricamento delle mesh
Le mesh sono dati sui vertici, generati proceduralmente dal codice all'interno del gioco o esportati in un file da un'altra app (ad esempio 3DStudio MAX o Alias WaveFront) o da uno strumento. Queste mesh rappresentano i modelli del gioco, da primitive semplici come cubi e sfere a automobili e case e personaggi. Spesso contengono dati di colore e animazione, a seconda del formato. Ci concentreremo sulle mesh che contengono solo i dati dei vertici.
Per caricare correttamente una mesh, è necessario conoscere il formato dei dati nel file per la mesh. Il semplice tipo BasicReaderWriter precedente legge semplicemente i dati in come flusso di byte; non sa che i dati dei byte rappresentano una mesh, molto meno un formato mesh specifico come esportato da un'altra applicazione. È necessario eseguire la conversione man mano che si inseriscono i dati mesh in memoria.
È sempre consigliabile provare a creare un pacchetto dei dati degli asset in un formato più vicino possibile alla rappresentazione interna. In questo modo si riduce l'utilizzo delle risorse e si risparmia tempo.
È possibile ottenere i dati dei byte dal file della mesh. Il formato nell'esempio presuppone che il file sia un suffisso di formato specifico del campione con estensione vbo. Anche in questo caso, questo formato non è uguale al formato VBO di OpenGL. Ogni vertice stesso esegue il mapping al tipo BasicVertex , ovvero uno struct definito nel codice per lo strumento di conversione obj2vbo. Il layout dei dati dei vertici nel file vbo è simile al seguente:
- I primi 32 bit (4 byte) del flusso di dati contengono il numero di vertici (numVertice) nella mesh, rappresentati come valore uint32.
- I 32 bit successivi (4 byte) del flusso di dati contengono il numero di indici nella mesh (numIndices), rappresentati come valore uint32.
- Successivamente, i bit (numVertices * sizeof(BasicVertex)) successivi contengono i dati dei vertici.
- Gli ultimi bit (numIndices * 16) di dati contengono i dati dell'indice, rappresentati come sequenza di valori uint16.
Il punto è questo: conoscere il layout a livello di bit dei dati mesh caricati. Inoltre, assicurarsi di essere coerenti con endian-ness. Tutte le piattaforme Windows 8 sono little-endian.
Nell'esempio si chiama un metodo CreateMesh dal metodo LoadMeshAsync per eseguire questa interpretazione a livello di bit.
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 interpreta i dati dei byte caricati dal file e crea un buffer dei vertici e un buffer di indice per la mesh passando rispettivamente i vertici e gli elenchi di indici a ID3D11Device::CreateBuffer e specificando D3D11_BIND_VERTEX_BUFFER o D3D11_BIND_INDEX_BUFFER. Ecco il codice usato in 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;
}
}
In genere si crea una coppia di vertex/index buffer per ogni mesh usata nel gioco. Dove e quando si caricano le mesh è possibile. Se hai molte mesh, potresti voler caricare solo alcuni dal disco in punti specifici del gioco, ad esempio durante specifici stati di caricamento predefiniti. Per mesh di grandi dimensioni, ad esempio i dati del terreno, è possibile trasmettere i vertici da una cache, ma si tratta di una procedura più complessa e non nell'ambito di questo argomento.
Anche in questo caso, conoscere il formato dei dati dei vertici. Esistono molti modi per rappresentare i dati dei vertici tra gli strumenti usati per creare modelli. Esistono anche molti modi diversi per rappresentare il layout di input dei dati dei vertici in Direct3D, ad esempio elenchi di triangoli e strisce. Per altre informazioni sui dati dei vertici, vedere Introduzione ai buffer in Direct3D 11 e primitive.
Si esaminerà ora il caricamento delle trame.
Caricamento delle trame
L'asset più comune in un gioco, e quello che comprende la maggior parte dei file su disco e in memoria, sono trame. Come le mesh, le trame possono venire in un'ampia gamma di formati e convertirli in un formato che Direct3D può usare quando li carichi. Le trame sono disponibili anche in un'ampia gamma di tipi e vengono usate per creare effetti diversi. I livelli MIP per le trame possono essere usati per migliorare l'aspetto e le prestazioni degli oggetti distanza; mappe sporcizia e luce vengono usate per gli effetti di livello e dettaglio su una trama di base; e le mappe normali vengono usate nei calcoli di illuminazione per pixel. In un gioco moderno, una scena tipica può potenzialmente avere migliaia di singole trame e il codice deve gestirli tutti in modo efficace!
Analogamente alle mesh, esistono diversi formati specifici usati per rendere efficiente l'utilizzo della memoria. Poiché le trame possono usare facilmente una grande parte della memoria GPU (e di sistema), spesso vengono compresse in qualche modo. Non è necessario usare la compressione sulle trame del gioco ed è possibile usare qualsiasi algoritmo di compressione/decompressione desiderato purché fornisca agli shader Direct3D i dati in un formato comprensibile (ad esempio una bitmap Texture2D).
Direct3D fornisce il supporto per gli algoritmi di compressione delle trame DXT, anche se ogni formato DXT potrebbe non essere supportato nell'hardware grafico del lettore. I file DDS contengono trame DXT (e altri formati di compressione delle trame) e sono suffisso con .dds.
Il file DDS è un file binario che contiene le seguenti informazioni:
DWORD (numero magico) contenente il valore di codice di quattro caratteri 'DDS ' (0x20534444).
Descrizione dei dati contenuti nel file.
I dati vengono descritti con una descrizione dell'intestazione utilizzando DDS_HEADER; il formato pixel viene definito usando DDS_PIXELFORMAT. Si noti che le strutture DDS_HEADER e DDS_PIXELFORMAT sostituiscono le strutture deprecate DDSURFACEDESC2, DDSC piattaforma di strumenti analitici 2 e DDPIXELFORMAT DirectDraw 7. DDS_HEADER è l'equivalente binario di DDSURFACEDESC2 e DDSC piattaforma di strumenti analitici 2. DDS_PIXELFORMAT è l'equivalente binario di DDPIXELFORMAT.
DWORD dwMagic; DDS_HEADER header;
Se il valore di dwFlags in DDS_PIXELFORMAT è impostato su DDPF_FOURCC e dwFourCC è impostato su "DX10" una struttura aggiuntiva DDS_HEADER_DXT10 sarà presente per contenere matrici di trame o formati DXGI che non possono essere espressi come formato pixel RGB, ad esempio formati a virgola mobile, formati sRGB e così via. Quando la struttura DDS_HEADER_DXT10 è presente, l'intera descrizione dei dati avrà un aspetto simile al seguente.
DWORD dwMagic; DDS_HEADER header; DDS_HEADER_DXT10 header10;
Puntatore a una matrice di byte che contiene i dati della superficie principale.
BYTE bdata[]
Puntatore a una matrice di byte che contiene le superfici rimanenti, ad esempio; livelli mipmap, visi in una mappa del cubo, profondità in una trama del volume. Seguire questi collegamenti per avere altre informazioni sul layout del file DDS per: una trama, una mappa del cubo o una trama del volume.
BYTE bdata2[]
Molti strumenti esportano nel formato DDS. Se non si dispone di uno strumento per esportare la trama in questo formato, è consigliabile crearne una. Per altre informazioni sul formato DDS e su come usarlo nel codice, vedere Guida alla programmazione per DDS. Nell'esempio verrà usato DDS.
Come con altri tipi di risorse, si leggono i dati da un file come flusso di byte. Al termine dell'attività di caricamento, la chiamata lambda esegue il codice (metodo CreateTexture ) per elaborare il flusso di byte in un formato utilizzabile da 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
);
});
}
Nel frammento di codice precedente, l'espressione lambda verifica se il nome file ha un'estensione "dds". In caso affermativo, si presuppone che si tratti di una trama DDS. In caso contrario, usare le API Windows Imaging Component (WIC) per individuare il formato e decodificare i dati come bitmap. In entrambi i casi, il risultato è una bitmap Texture2D (o un errore).
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();
}
}
Al termine di questo codice, si dispone di un texture2D in memoria, caricato da un file di immagine. Come per le mesh, probabilmente hai un sacco di loro nel tuo gioco e in qualsiasi scena specifica. È consigliabile creare cache per trame a cui si accede regolarmente per scena o per livello, anziché caricarle tutte all'avvio del gioco o del livello.
(L'oggetto Il metodo CreateDDSTextureFromMemory chiamato nell'esempio precedente può essere esaminato completamente in Codice completo per DDSTextureLoader.
Inoltre, le singole trame o "skin" possono essere mappate a poligoni o superfici di mesh specifiche. Questi dati di mapping vengono in genere esportati dallo strumento un artista o un progettista usato per creare il modello e le trame. Assicurarsi di acquisire queste informazioni anche quando si caricano i dati esportati, perché verranno usate per eseguire il mapping delle trame corrette alle superfici corrispondenti quando si esegue l'ombreggiatura del frammento.
Caricamento di shader
Gli shader vengono compilati file HLSL (High Level Shader Language) caricati in memoria e richiamati in fasi specifiche della pipeline grafica. Gli shader più comuni ed essenziali sono i vertex shader e pixel shader, che elaborano rispettivamente i singoli vertici della mesh e i pixel nei viewport della scena. Il codice HLSL viene eseguito per trasformare la geometria, applicare effetti di illuminazione e trame ed eseguire post-elaborazione sulla scena sottoposta a rendering.
Un gioco Direct3D può avere diversi shader, ognuno dei quali compilato in un file CSO separato (Compiled Shader Object, .cso). Normalmente, non hai così tanti che devi caricarli dinamicamente, e nella maggior parte dei casi, puoi semplicemente caricarli all'avvio del gioco o a livello (ad esempio uno shader per gli effetti della pioggia).
Il codice nella classe BasicLoader fornisce diversi overload per diversi shader, tra cui vertex, geometry, pixel e hull shader. Il codice seguente illustra gli shader pixel come esempio. (È possibile esaminare il codice completo in Codice completo per 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);
});
}
In questo esempio si usa l'istanza BasicReaderWriter (m_basicReaderWriter) per leggere il file dell'oggetto shader compilato fornito (con estensione cso) come flusso di byte. Al termine dell'attività, l'espressione lambda chiama ID3D11Device::CreatePixelShader con i dati dei byte caricati dal file. Il callback deve impostare un flag che indica che il caricamento è riuscito e il codice deve controllare questo flag prima di eseguire lo shader.
I vertex shader sono leggermente più complessi. Per un vertex shader, si carica anche un layout di input separato che definisce i dati dei vertici. Il codice seguente può essere usato per caricare in modo asincrono un vertex shader insieme a un layout di input dei vertici personalizzato. Assicurarsi che le informazioni sui vertici caricate dalle mesh possano essere rappresentate correttamente da questo layout di input.
Creare il layout di input prima di caricare il vertex shader.
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);
}
}
In questo particolare layout, ogni vertice ha i dati seguenti elaborati dal vertex shader:
- Posizione delle coordinate 3D (x, y, z) nello spazio delle coordinate del modello, rappresentato come un trio di valori a virgola mobile a 32 bit.
- Vettore normale per il vertice, rappresentato anche come tre valori a virgola mobile a 32 bit.
- Valore della coordinata di trama 2D trasformato (u, v), rappresentato come coppia di valori mobili a 32 bit.
Questi elementi di input per vertice sono denominati semantica HLSL e sono un set di registri definiti usati per passare dati da e verso l'oggetto shader compilato. La pipeline esegue il vertex shader una volta per ogni vertice nella mesh caricata. La semantica definisce l'input (e l'output da) il vertex shader durante l'esecuzione e fornisce questi dati per i calcoli per vertice nel codice HLSL del shader.
Caricare ora l'oggetto vertex shader.
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);
}
});
}
In questo codice, dopo aver letto i dati dei byte per il file CSO del vertex shader, si crea il vertex shader chiamando ID3D11Device::CreateVertexShader. Successivamente, si crea il layout di input per lo shader nella stessa espressione lambda.
Anche altri tipi di shader, ad esempio hull shader e geometry shader, possono richiedere una configurazione specifica. Il codice completo per un'ampia gamma di metodi di caricamento dello shader è disponibile in Codice completo per BasicLoader e nell'esempio di caricamento delle risorse Direct3D.
Osservazioni:
A questo punto, è necessario comprendere e essere in grado di creare o modificare metodi per caricare in modo asincrono risorse e asset di gioco comuni, ad esempio mesh, trame e shader compilati.
Argomenti correlati
- Esempio di caricamento delle risorse Direct3D
- Codice completo per BasicLoader
- Codice completo per BasicReaderWriter
- Codice completo per DDSTextureLoader