Carregar recursos no jogo em DirectX
A maioria dos jogos, em algum momento, carrega recursos e ativos (como sombreadores, texturas, malhas predefinidas ou outros dados gráficos) do armazenamento local ou de algum outro fluxo de dados. Aqui, orientamos você em uma exibição de alto nível do que você deve considerar ao carregar esses arquivos para uso no seu jogo da Universal Windows Platform (UWP) em C/C++ e DirectX.
Por exemplo, as malhas para objetos poligonais no jogo podem ter sido criadas com outra ferramenta e exportadas para um formato específico. O mesmo vale para texturas, e mais ainda: embora um bitmap plano e descompactado possa ser comumente escrito pela maioria das ferramentas e compreendido pela maioria das APIs gráficas, ele pode ser extremamente ineficiente para uso no jogo. Aqui, orientamos você nas etapas básicas para carregar três tipos diferentes de recursos gráficos para uso com o Direct3D: malhas (modelos), texturas (bitmaps) e objetos de sombreador compilados.
O que você precisa saber
Tecnologias
- Biblioteca de padrões paralelos (ppltasks.h)
Pré-requisitos
- Compreender o Windows Runtime básico
- Compreender tarefas assíncronas
- Compreender os conceitos básicos de programação de gráficos 3D.
Este exemplo também inclui três arquivos de código para carregamento e gerenciamento de recursos. Você encontrará os objetos de código definidos nesses arquivos ao longo deste tópico.
- BasicLoader.h/.cpp
- BasicReaderWriter.h/.cpp
- DDSTextureLoader.h/.cpp
O código completo para esses exemplos pode ser encontrado nos links a seguir.
Tópico | Descrição |
---|---|
Código completo para uma classe e métodos que convertem e carregam objetos de malha de gráficos na memória. |
|
Código completo para uma classe e métodos para ler e gravar arquivos de dados binários em geral. Usado pela classe BasicLoader. |
|
Código completo para uma classe e método que carrega uma textura DDS da memória. |
Instruções
Carregamento assíncrono
O carregamento assíncrono é manipulado usando o modelo de tarefa da PPL (Biblioteca de Padrões Paralelos). Uma tarefa contém uma chamada de método seguida por um lambda que processa os resultados da chamada assíncrona depois que ela é concluída e, geralmente, segue o formato de:
task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });
.
As tarefas podem ser encadeadas usando a sintaxe .then(), de modo que, quando uma operação é concluída, outra operação assíncrona que depende dos resultados da operação anterior pode ser executada. Dessa forma, você pode carregar, converter e gerenciar ativos complexos em threads separados de uma forma que pareça quase invisível para o jogador.
Para obter mais detalhes, leia Programação assíncrona em C++.
Agora, vamos examinar a estrutura básica para declarar e criar um método de carregamento de arquivo assíncrono, 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;
});
}
Nesse código, quando seu código chama o método ReadDataAsync definido acima, uma tarefa é criada para ler um buffer do sistema de arquivos. Depois de concluída, uma tarefa encadeada transmite os bytes desse buffer para uma matriz usando o tipo estático 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.
});
Veja a chamada que você faz para ReadDataAsync. Quando ela é concluída, o código recebe uma matriz de bytes lidos do arquivo fornecido. Como o próprio ReadDataAsync é definido como uma tarefa, você pode usar um lambda para executar uma operação específica quando a matriz de bytes é retornada, como passar esses dados de byte para uma função DirectX que possa usá-los.
Se seu jogo for suficientemente simples, carregue os recursos com um método como este quando o usuário iniciar o jogo. Você pode fazer isso antes de iniciar o loop do jogo principal de algum ponto na sequência de chamadas da implementação IFrameworkView::Run. Além disso, você chama os métodos de carregamento de recursos de forma assíncrona para que o jogo possa iniciar mais rápido e para que o jogador não precise esperar até a conclusão do carregamento antes de se envolver em interações iniciais.
No entanto, você não quer iniciar o jogo corretamente até que todo o carregamento assíncrono tenha sido concluído! Crie algum método para sinalizar quando o carregamento está concluído, como um campo específico, e use os lambdas dos métodos de carregamento para definir esse sinal quando terminar. Verifique a variável antes de iniciar os componentes que usam esses recursos carregados.
Veja um exemplo usando os métodos assíncronos definidos em BasicLoader.cpp para carregar sombreadores, uma malha e uma textura quando o jogo é iniciado. Observe que isso define um campo específico no objeto do jogo, m_loadingComplete, quando todos os métodos de carregamento terminam.
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.
}
Observe que as tarefas foram agregadas usando o operador && de modo que o lambda que define o sinalizador de carregamento concluído seja acionado somente quando todas as tarefas forem concluídas. Observe que, se você tiver vários sinalizadores, terá a possibilidade de condições de corrida. Por exemplo, se o lambda definir dois sinalizadores sequencialmente para o mesmo valor, outro thread só poderá ver o primeiro sinalizador definido se examiná-los antes que o segundo sinalizador seja definido.
Você viu como carregar arquivos de recursos de forma assíncrona. Os carregamentos de arquivos síncronos são muito mais simples e você pode encontrar exemplos em Código completo para BasicReaderWriter e Código completo para BasicLoader.
É claro que diferentes tipos de recursos e ativos muitas vezes exigem processamento ou conversão adicional antes de estarem prontos para serem usados no pipeline de gráficos. Vamos dar uma olhada em três tipos específicos de recursos: malhas, texturas e sombreadores.
Carregando malhas
Malhas são dados de vértice, gerados processualmente por código dentro do jogo ou exportados para um arquivo de outro aplicativo (como 3DStudio MAX ou Alias WaveFront) ou ferramenta. Essas malhas representam os modelos no jogo, de simples primitivas, como cubos e esferas, a carros, casas e personagens. Elas geralmente também contêm dados de cores e animação, dependendo do formato. Vamos nos concentrar em malhas que contêm apenas dados de vértice.
Para carregar uma malha corretamente, você deve conhecer o formato dos dados no arquivo para a malha. Nosso tipo simples BasicReaderWriter acima simplesmente lê os dados como um fluxo de bytes. Ele não sabe que os dados de bytes representam uma malha, muito menos um formato de malha específico exportado por outro aplicativo! Você deve executar a conversão à medida que traz os dados de malha para a memória.
(Você deve sempre tentar empacotar dados de ativos em um formato que seja o mais próximo possível da representação interna. Isso reduzirá a utilização de recursos e economizará tempo.)
Vamos obter os dados de bytes do arquivo da malha. O formato do exemplo presume que o arquivo é um formato específico de exemplo com sufixo .vbo. (Além disso, esse formato não é o mesmo que o formato VBO do OpenGL.) Cada vértice por si só mapeia para o tipo BasicVertex, que é uma struct definida no código para a ferramenta de conversor obj2vbo. O layout dos dados de vértice no arquivo .vbo tem esta aparência:
- Os primeiros 32 bits (4 bytes) do fluxo de dados contêm o número de vértices (numVertices) na malha, representado como um valor uint32.
- Os próximos 32 bits (4 bytes) do fluxo de dados contêm o número de índices na malha (numIndices), representado como um valor uint32.
- Depois disso, os bits seguintes (numVertices * sizeof(BasicVertex)) contêm os dados de vértice.
- Os últimos bits (numIndices * 16) de dados contêm os dados de índice, representados como uma sequência de valores uint16.
O ponto é o seguinte: conheça o layout em nível de bit dos dados de malha que você carregou. Além disso, seja consistente com endian-ness. Todas as plataformas Windows 8 são little-endian.
No exemplo, você chama um método, CreateMesh do método LoadMeshAsync para executar essa interpretação em nível de 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
);
});
}
O CreateMesh interpreta os dados de bytes carregados do arquivo e cria um buffer de vértice e um buffer de índice para a malha passando as listas de vértice e índice, respectivamente, para ID3D11Device::CreateBuffer e especificando D3D11_BIND_VERTEX_BUFFER ou D3D11_BIND_INDEX_BUFFER. Veja o código usado no 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;
}
}
Normalmente, você cria um par de buffer de vértice/índice para cada malha usada no seu jogo. Você decide onde e quando carregar as malhas. Se você tiver muitas malhas, talvez queira carregar apenas algumas do disco em pontos específicos do jogo, como durante estados de carregamento específicos e predefinidos. Para malhas grandes, como dados de terreno, você pode transmitir os vértices de um cache, mas esse é um procedimento mais complexo e não está no escopo deste tópico.
Além disso, conheça o formato de dados de vértice! Há muitas, muitas maneiras de representar dados de vértice nas ferramentas usadas para criar modelos. Há também muitas maneiras diferentes de representar o layout de entrada dos dados de vértice para o Direct3D, como listas de triângulos e faixas. Para obter mais informações sobre dados de vértice, leia Introdução a buffers no Direct3D 11 e Primitivas.
A seguir, vamos examinar o carregamento de texturas.
Carregar texturas
O recurso mais comum em um jogo, e aquele que abrange a maioria dos arquivos em disco e na memória, são as texturas. Como as malhas, as texturas podem vir em uma variedade de formatos e você as converte em um formato que o Direct3D pode usar quando você as carrega. As texturas também vêm em uma grande variedade de tipos e são usadas para criar diferentes efeitos. Os níveis da PIM (Proteção de Informações da Microsoft) para texturas podem ser usados para melhorar a aparência e o desempenho de objetos distantes; mapas de sujeira e luz são usados para efeitos de camadas e detalhes sobre uma textura base; e mapas normais são usados em cálculos de iluminação por pixel. Em um jogo moderno, uma cena típica pode ter milhares de texturas individuais e o seu código deve gerenciar efetivamente todas elas!
Como as malhas, também há uma série de formatos específicos que são usados para tornar o uso de memória eficiente. Como as texturas podem facilmente consumir uma grande parte da memória da GPU (e do sistema), elas geralmente são compactadas de alguma forma. Você não precisa usar compactação nas texturas do jogo e pode usar os algoritmos de compactação/descompactação desejados, desde que forneça aos sombreadores do Direct3D dados em um formato que ele possa entender (como um bitmap Texture2D).
O Direct3D fornece suporte para os algoritmos de compactação de textura DXT, embora cada formato DXT possa não ser compatível com o hardware de gráficos do jogador. Os arquivos DDS contêm texturas DXT (e outros formatos de compactação de textura) e têm o sufixo .dds.
O arquivo DDS é um arquivo binário que contém as seguintes informações:
Um DWORD (número mágico) contendo o valor de código de quatro caracteres 'DDS' (0x20534444).
Uma descrição dos dados no arquivo.
Os dados são descritos com uma descrição de cabeçalho usando DDS_HEADER. O formato de pixel é definido usando DDS_PIXELFORMAT. Observe que as estruturas DDS_HEADER e DDS_PIXELFORMAT substituem as estruturas preteridas DDSURFACEDESC2, DDSCAPS2 e DDPIXELFORMAT do DirectDraw 7. DDS_HEADER é o equivalente binário de DDSURFACEDESC2 e DDSCAPS2. DDS_PIXELFORMAT é o equivalente binário do DDPIXELFORMAT.
DWORD dwMagic; DDS_HEADER header;
Se o valor de dwFlags em DDS_PIXELFORMAT estiver definido como DDPF_FOURCC e dwFourCC estiver definido como “DX10”, uma estrutura adicional DDS_HEADER_DXT10 estará presente para acomodar matrizes de textura ou formatos DXGI que não podem ser expressos como um formato de pixel RGB, como formatos de ponto flutuante, formatos sRGB etc. Quando a estrutura DDS_HEADER_DXT10 estiver presente, toda a descrição dos dados terá a aparência a seguir.
DWORD dwMagic; DDS_HEADER header; DDS_HEADER_DXT10 header10;
Um ponteiro para uma matriz de bytes que contém os principais dados de superfície.
BYTE bdata[]
Um ponteiro para uma matriz de bytes que contém as superfícies restantes, como níveis mipmap, faces em um mapa de cubo, profundidades em uma textura de volume. Siga estes links para obter mais informações sobre o layout de arquivos DDS para: uma textura, um mapa de cubo ou uma textura de volume.
BYTE bdata2[]
Muitas ferramentas exportam para o formato DDS. Se você não tiver uma ferramenta para exportar sua textura para esse formato, considere criar uma. Para obter mais detalhes sobre o formato DDS e como trabalhar com ele no código, leia Guia de programação para DDS. No nosso exemplo, vamos usar DDS.
Assim como acontece com outros tipos de recursos, você lê os dados de um arquivo como um fluxo de bytes. Quando a tarefa de carregamento é concluída, a chamada de lambda executa código (o método CreateTexture) para processar o fluxo de bytes em um formato que o Direct3D pode usar.
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
);
});
}
No trecho anterior, o lambda verifica se o nome do arquivo tem a extensão “dds”. Se isso acontecer, você presume que é uma textura DDS. Caso contrário, use as APIs do WIC (Windows Imaging Component) para descobrir o formato e decodificar os dados como um bitmap. De qualquer forma, o resultado é um bitmap Texture2D (ou um erro).
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();
}
}
Quando esse código é concluído, você tem um Texture2D na memória, carregado de um arquivo de imagem. Tal como acontece com as malhas, você provavelmente tem muitos deles no jogo e nas cenas. Considere criar caches para texturas acessadas regularmente por cena ou por nível, em vez de carregar todas quando o jogo ou o nível começar.
(O método CreateDDSTextureFromMemory chamado no exemplo acima pode ser explorado na íntegra em Código completo para DDSTextureLoader.)
Além disso, texturas individuais ou “peles” de textura podem ser mapeadas para polígonos ou superfícies de malha específicas. Esses dados de mapeamento geralmente são exportados pela ferramenta que um artista ou designer usou para criar o modelo e as texturas. Não deixe de capturar essas informações também quando carregar os dados exportados, pois você os usará para mapear as texturas corretas para as superfícies correspondentes quando executar o sombreamento de fragmentos.
Carregando sombreadores
Os sombreadores são arquivos HLSL (High Level Shader Language) compilados que são carregados na memória e invocados em estágios específicos do pipeline de gráficos. Os sombreadores mais comuns e essenciais são os sombreadores de vértice e pixel, que processam os vértices individuais da malha e os pixels nos visores da cena, respectivamente. O código HLSL é executado para transformar a geometria, aplicar efeitos de iluminação e texturas e executar o pós-processamento na cena renderizada.
Um jogo Direct3D pode ter vários sombreadores diferentes, cada um compilado em um arquivo CSO (Compiled Shader Object, .cso) separado. Normalmente, você não tem tantos que precise carregar dinamicamente e, na maioria dos casos, você pode simplesmente carregá-los quando o jogo está começando ou em cada nível (como um sombreador para efeitos de chuva).
O código na classe BasicLoader fornece várias sobrecargas para diferentes sombreadores, incluindo sombreadores de vértice, geometria, pixel e envoltória. O código abaixo aborda sombreadores de pixel como exemplo. (Você pode revisar o código completo em Código completo para 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);
});
}
Neste exemplo, você usa a instância BasicReaderWriter (m_basicReaderWriter) para ler o arquivo .cso fornecido como um fluxo de bytes. Quando essa tarefa é concluída, o lambda chama ID3D11Device::CreatePixelShader com os dados de byte carregados do arquivo. O retorno de chamada deve definir algum sinalizador indicando que o carregamento foi bem-sucedido e o código deve verificar este sinalizador antes de executar o sombreador.
Os sombreadores de vértice são um pouco mais complexos. Para um sombreador de vértice, você também carrega um layout de entrada separado que define os dados de vértice. O código a seguir pode ser usado para carregar de forma assíncrona um sombreador de vértice com um layout de entrada de vértice personalizado. Verifique se as informações de vértice que você carrega das malhas podem ser representadas corretamente por este layout de entrada!
Vamos criar o layout de entrada antes de carregar o sombreador de vértice.
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);
}
}
Neste layout específico, cada vértice tem os seguintes dados processados pelo sombreador de vértice:
- Uma posição de coordenada 3D (x, y, z) no espaço de coordenadas do modelo, representada como um trio de valores de ponto flutuante de 32 bits.
- Um vetor normal para o vértice, também representado como três valores de ponto flutuante de 32 bits.
- Um valor de coordenada de textura 2D transformado (u, v) , representado como um par de valores flutuantes de 32 bits.
Esses elementos de entrada por vértice são chamados de semântica HLSL e são um conjunto de registros definidos usados para transmitir dados de e para o objeto do sombreador compilado. Seu pipeline executa o sombreador de vértice uma vez para cada vértice na malha que você carregou. A semântica define a entrada (e a saída) do sombreador de vértice à medida que ele é executado e fornece esses dados para os cálculos por vértice no código HLSL do sombreador.
Agora, carregue o objeto de sombreador de vértice.
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);
}
});
}
Nesse código, depois de ler os dados de byte do arquivo CSO do sombreador de vértice, você cria o sombreador de vértice ao chamar ID3D11Device::CreateVertexShader. Depois disso, você cria o layout de entrada para o sombreador no mesmo lambda.
Outros tipos de sombreador, como sombreadores de envoltória e geometria, também podem exigir configuração específica. O código completo para vários métodos de carregamento de sombreador é fornecido no Código completo para BasicLoader e no exemplo de carregamento de recursos do Direct3D.
Comentários
Neste ponto, você deve entender e poder criar ou modificar métodos para carregar de forma assíncrona recursos e ativos comuns de jogos, como malhas, texturas e sombreadores compilados.
Tópicos relacionados
- Amostra de carregamento de recursos do Direct3D
- Código completo para BasicLoader
- Código completo para BasicReaderWriter
- Código completo para DDSTextureLoader