Estrutura de renderização I: Introdução à renderização
Observação
Este tópico faz parte da série de tutoriais Criar um jogo simples da Plataforma Universal do Windows (UWP) com DirectX. O tópico nesse link define o contexto da série.
Até agora, abordamos como estruturar um jogo UWP (Plataforma Universal do Windows) e como definir um computador de estado para lidar com o fluxo do jogo. Agora é hora de aprender como desenvolver a estrutura de renderização. Vejamos como o jogo de exemplo renderiza a cena do jogo usando o Direct3D 11.
O Direct3D 11 contém um conjunto de APIs que fornecem acesso aos recursos avançados de hardware gráfico de alto desempenho que podem ser usados para criar gráficos 3D para aplicativos com uso intensivo de gráficos, como jogos.
Renderizar gráficos de jogos na tela significa basicamente renderizar uma sequência de quadros na tela. Em cada quadro, você precisa renderizar objetos visíveis na cena, com base na exibição.
Para renderizar um quadro, você precisa passar as informações de cena necessárias para o hardware para que possam ser exibidas na tela. Se você quiser que algo seja exibido na tela, você precisa começar a renderizar assim que o jogo começar a ser executado.
Objetivos
Para configurar uma estrutura de renderização básica para exibir a saída gráfica de um jogo UWP DirectX. Você pode dividir isso livremente nessas três etapas.
- Estabeleça uma conexão com a interface gráfica.
- Crie os recursos necessários para desenhar os gráficos.
- Exiba os gráficos renderizando o quadro.
Este tópico explica como os gráficos são renderizados, abrangendo as etapas 1 e 3.
Estrutura de renderização II: a renderização do jogo abrange a etapa 2: como configurar a estrutura de renderização e como os dados são preparados antes que a renderização possa acontecer.
Introdução
É uma boa ideia se familiarizar com os conceitos básicos de gráficos e renderização. Se você não estiver familiarizado com o Direct3D e a renderização, consulte Termos e conceitos para obter uma breve descrição dos gráficos e termos de renderização usados neste tópico.
Para este jogo, a classe GameRenderer representa o renderizador para este jogo de exemplo. Ele é responsável por criar e manter todos os objetos Direct3D 11 e Direct2D usados para gerar os visuais do jogo. Ele também mantém uma referência ao objeto Simple3DGame usado para recuperar a lista de objetos a serem renderizados, bem como o status do jogo para o heads-up display (HUD).
Nesta parte do tutorial, vamos nos concentrar na renderização de objetos 3D no jogo.
Estabelecer uma conexão com a interface gráfica
Para obter informações sobre como acessar o hardware para renderização, consulte o tópico Definir a estrutura do aplicativo UWP do jogo.
O método App::Initialize
A função std::make_shared , conforme mostrado abaixo, é usada para criar um shared_ptr para DX::D eviceResources, que também fornece acesso ao dispositivo.
No Direct3D 11, um dispositivo é usado para alocar e destruir objetos, renderizar primitivos e se comunicar com a placa gráfica por meio do driver gráfico.
void Initialize(CoreApplicationView const& applicationView)
{
...
// At this point we have access to the device.
// We can create the device-dependent resources.
m_deviceResources = std::make_shared<DX::DeviceResources>();
}
Exiba os gráficos renderizando o quadro
A cena do jogo precisa ser renderizada quando o jogo é iniciado. As instruções para renderização começam no método GameMain::Run , conforme mostrado abaixo.
O fluxo simples é este.
- Atualizar
- Render
- Presente
Método GameMain::Run
void GameMain::Run()
{
while (!m_windowClosed)
{
if (m_visible) // if the window is visible
{
switch (m_updateState)
{
...
default:
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
Update();
m_renderer->Render();
m_deviceResources->Present();
m_renderNeeded = false;
}
}
else
{
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
}
}
m_game->OnSuspending(); // Exiting due to window close, so save state.
}
Atualizar
Consulte o tópico Gerenciamento de fluxo de jogo para obter mais informações sobre como os estados do jogo são atualizados no método GameMain::Update.
Renderizar
A renderização é implementada chamando o método GameRenderer::Render de GameMain::Run.
Se a renderização estéreo estiver habilitada, haverá duas passagens de renderização — uma para o olho esquerdo e outra para o direito. Em cada passagem de renderização, associamos o destino de renderização e a exibição de estêncil de profundidade ao dispositivo. Também limpamos a exibição do estêncil de profundidade posteriormente.
Observação
A renderização estéreo pode ser obtida usando outros métodos, como estéreo de passagem única, usando instanciação de vértice ou sombreadores de geometria. O método two-rendering-passes é uma maneira mais lenta, mas mais conveniente, de obter renderização estéreo.
Depois que o jogo estiver em execução e os recursos forem carregados, atualizaremos a matriz de projeção, uma vez por passagem de renderização. Os objetos são ligeiramente diferentes de cada visualização. Em seguida, configuramos o pipeline de renderização de gráficos.
Observação
Consulte Criar e carregar recursos gráficos do DirectX para obter mais informações sobre como os recursos são carregados.
Neste jogo de exemplo, o renderizador foi projetado para usar um layout de vértice padrão em todos os objetos. Isso simplifica o design do sombreador e permite mudanças fáceis entre os sombreadores, independentemente da geometria dos objetos.
Método GameRenderer::Render
Definimos o contexto do Direct3D para usar um layout de vértice de entrada. Os objetos de layout de entrada descrevem como os dados do buffer de vértice são transmitidos para o pipeline de renderização.
Em seguida, definimos o contexto do Direct3D para usar os buffers constantes definidos anteriormente, que são usados pelo estágio de pipeline do sombreador de vértice e pelo estágio do pipeline do sombreador de pixel.
Observação
Consulte Estrutura de renderização II: renderização de jogos para obter mais informações sobre a definição dos buffers constantes.
Como o mesmo layout de entrada e conjunto de buffers constantes é usado para todos os sombreadores que estão no pipeline, ele é configurado uma vez por quadro.
void GameRenderer::Render()
{
bool stereoEnabled{ m_deviceResources->GetStereoState() };
auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };
int renderingPasses = 1;
if (stereoEnabled)
{
renderingPasses = 2;
}
for (int i = 0; i < renderingPasses; i++)
{
// Iterate through the number of rendering passes to be completed.
// 2 rendering passes if stereo is enabled.
if (i > 0)
{
// Doing the Right Eye View.
ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetViewRight() };
// Resets render targets to the screen.
// OMSetRenderTargets binds 2 things to the device.
// 1. Binds one render target atomically to the device.
// 2. Binds the depth-stencil view, as returned by the GetDepthStencilView method, to the device.
// For more info, see
// https://video2.skills-academy.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-omsetrendertargets
d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());
// Clears the depth stencil view.
// A depth stencil view contains the format and buffer to hold depth and stencil info.
// For more info about depth stencil view, go to:
// https://video2.skills-academy.com/windows/uwp/graphics-concepts/depth-stencil-view--dsv-
// A depth buffer is used to store depth information to control which areas of
// polygons are rendered rather than hidden from view. To learn more about a depth buffer,
// go to: https://video2.skills-academy.com/windows/uwp/graphics-concepts/depth-buffers
// A stencil buffer is used to mask pixels in an image, to produce special effects.
// The mask determines whether a pixel is drawn or not,
// by setting the bit to a 1 or 0. To learn more about a stencil buffer,
// go to: https://video2.skills-academy.com/windows/uwp/graphics-concepts/stencil-buffers
d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);
// Direct2D -- discussed later
d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmapRight());
}
else
{
// Doing the Mono or Left Eye View.
// As compared to the right eye:
// m_deviceResources->GetBackBufferRenderTargetView instead of GetBackBufferRenderTargetViewRight
ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetView() };
// Same as the Right Eye View.
d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());
d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);
// d2d -- Discussed later under Adding UI
d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmap());
}
const float clearColor[4] = { 0.5f, 0.5f, 0.8f, 1.0f };
// Only need to clear the background when not rendering the full 3D scene since
// the 3D world is a fully enclosed box and the dynamics prevents the camera from
// moving outside this space.
if (i > 0)
{
// Doing the Right Eye View.
d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetViewRight(), clearColor);
}
else
{
// Doing the Mono or Left Eye View.
d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetView(), clearColor);
}
// Render the scene objects
if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
{
// This section is only used after the game state has been initialized and all device
// resources needed for the game have been created and associated with the game objects.
if (stereoEnabled)
{
// When doing stereo, it is necessary to update the projection matrix once per rendering pass.
auto orientation = m_deviceResources->GetOrientationTransform3D();
ConstantBufferChangeOnResize changesOnResize;
// Apply either a left or right eye projection, which is an offset from the middle
XMStoreFloat4x4(
&changesOnResize.projection,
XMMatrixMultiply(
XMMatrixTranspose(
i == 0 ?
m_game->GameCamera().LeftEyeProjection() :
m_game->GameCamera().RightEyeProjection()
),
XMMatrixTranspose(XMLoadFloat4x4(&orientation))
)
);
d3dContext->UpdateSubresource(
m_constantBufferChangeOnResize.get(),
0,
nullptr,
&changesOnResize,
0,
0
);
}
// Update variables that change once per frame.
ConstantBufferChangesEveryFrame constantBufferChangesEveryFrameValue;
XMStoreFloat4x4(
&constantBufferChangesEveryFrameValue.view,
XMMatrixTranspose(m_game->GameCamera().View())
);
d3dContext->UpdateSubresource(
m_constantBufferChangesEveryFrame.get(),
0,
nullptr,
&constantBufferChangesEveryFrameValue,
0,
0
);
// Set up the graphics pipeline. This sample uses the same InputLayout and set of
// constant buffers for all shaders, so they only need to be set once per frame.
// For more info about the graphics or rendering pipeline, see
// https://video2.skills-academy.com/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline
// IASetInputLayout binds an input-layout object to the input-assembler (IA) stage.
// Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
// Set up the Direct3D context to use this vertex layout. For more info, see
// https://video2.skills-academy.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetinputlayout
d3dContext->IASetInputLayout(m_vertexLayout.get());
// VSSetConstantBuffers sets the constant buffers used by the vertex shader pipeline stage.
// Set up the Direct3D context to use these constant buffers. For more info, see
// https://video2.skills-academy.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-vssetconstantbuffers
ID3D11Buffer* constantBufferNeverChanges{ m_constantBufferNeverChanges.get() };
d3dContext->VSSetConstantBuffers(0, 1, &constantBufferNeverChanges);
ID3D11Buffer* constantBufferChangeOnResize{ m_constantBufferChangeOnResize.get() };
d3dContext->VSSetConstantBuffers(1, 1, &constantBufferChangeOnResize);
ID3D11Buffer* constantBufferChangesEveryFrame{ m_constantBufferChangesEveryFrame.get() };
d3dContext->VSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
ID3D11Buffer* constantBufferChangesEveryPrim{ m_constantBufferChangesEveryPrim.get() };
d3dContext->VSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);
// Sets the constant buffers used by the pixel shader pipeline stage.
// For more info, see
// https://video2.skills-academy.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-pssetconstantbuffers
d3dContext->PSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
d3dContext->PSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);
ID3D11SamplerState* samplerLinear{ m_samplerLinear.get() };
d3dContext->PSSetSamplers(0, 1, &samplerLinear);
for (auto&& object : m_game->RenderObjects())
{
// The 3D object render method handles the rendering.
// For more info, see Primitive rendering below.
object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
}
}
// Start of 2D rendering
...
}
}
Renderização primitiva
Ao renderizar a cena, você percorre todos os objetos que precisam ser renderizados. As etapas abaixo são repetidas para cada objeto (primitivo).
- Atualize o buffer constante (m_constantBufferChangesEveryPrim) com a matriz de transformação do mundo do modelo e as informações do material.
- O m_constantBufferChangesEveryPrim contém parâmetros para cada objeto. Inclui a matriz de transformação objeto-mundo, bem como propriedades do material, como cor e expoente especular para cálculos de iluminação.
- Defina o contexto do Direct3D para usar o layout de vértice de entrada para que os dados do objeto de malha sejam transmitidos para o estágio IA-assembler (input-assembler) do pipeline de renderização.
- Defina o contexto do Direct3D para usar um buffer de índice no estágio IA. Forneça as informações primitivas: tipo, ordem dos dados.
- Envie uma chamada de desenho para desenhar o primitivo indexado e não instanciado. O método GameObject::Render atualiza o buffer constante primitivo com os dados específicos de um determinado primitivo. Isso resulta em uma chamada DrawIndexed no contexto para desenhar a geometria de cada primitivo. Especificamente, essa chamada de desenho enfileira comandos e dados para a GPU (unidade de processamento gráfico), conforme parametrizado pelos dados de buffer constante. Cada chamada de desenho executa o sombreador de vértice uma vez por vértice e, em seguida, o sombreador de pixel uma vez para cada pixel de cada triângulo no primitivo. As texturas fazem parte do estado que o sombreador de pixel usa para fazer a renderização.
Aqui estão os motivos para usar vários buffers constantes.
- O jogo usa vários buffers constantes, mas só precisa atualizar esses buffers uma vez por primitivo. Como mencionado anteriormente, os buffers constantes são como entradas para os sombreadores executados para cada primitivo. Alguns dados são estáticos (m_constantBufferNeverChanges); alguns dados são constantes no quadro (m_constantBufferChangesEveryFrame), como a posição da câmera; e alguns dados são específicos do primitivo, como sua cor e texturas (m_constantBufferChangesEveryPrim).
- O renderizador do jogo separa essas entradas em diferentes buffers constantes para otimizar a largura de banda de memória que a CPU e a GPU usam. Essa abordagem também ajuda a minimizar a quantidade de dados que a GPU precisa acompanhar. A GPU tem uma grande fila de comandos e, cada vez que o jogo chama Draw, esse comando é enfileirado junto com os dados associados a ele. Quando o jogo atualiza o buffer constante primitivo e emite o próximo comando Draw , o driver gráfico adiciona esse próximo comando e os dados associados à fila. Se o jogo desenhar 100 primitivos, ele poderá ter 100 cópias dos dados de buffer constantes na fila. Para minimizar a quantidade de dados que o jogo está enviando para a GPU, o jogo usa um buffer constante primitivo separado que contém apenas as atualizações de cada primitivo.
Método GameObject::Render
void GameObject::Render(
_In_ ID3D11DeviceContext* context,
_In_ ID3D11Buffer* primitiveConstantBuffer
)
{
if (!m_active || (m_mesh == nullptr) || (m_normalMaterial == nullptr))
{
return;
}
ConstantBufferChangesEveryPrim constantBuffer;
// Put the model matrix info into a constant buffer, in world matrix.
XMStoreFloat4x4(
&constantBuffer.worldMatrix,
XMMatrixTranspose(ModelMatrix())
);
// Check to see which material to use on the object.
// If a collision (a hit) is detected, GameObject::Render checks the current context, which
// indicates whether the target has been hit by an ammo sphere. If the target has been hit,
// this method applies a hit material, which reverses the colors of the rings of the target to
// indicate a successful hit to the player. Otherwise, it applies the default material
// with the same method. In both cases, it sets the material by calling Material::RenderSetup,
// which sets the appropriate constants into the constant buffer. Then, it calls
// ID3D11DeviceContext::PSSetShaderResources to set the corresponding texture resource for the
// pixel shader, and ID3D11DeviceContext::VSSetShader and ID3D11DeviceContext::PSSetShader
// to set the vertex shader and pixel shader objects themselves, respectively.
if (m_hit && m_hitMaterial != nullptr)
{
m_hitMaterial->RenderSetup(context, &constantBuffer);
}
else
{
m_normalMaterial->RenderSetup(context, &constantBuffer);
}
// Update the primitive constant buffer with the object model's info.
context->UpdateSubresource(primitiveConstantBuffer, 0, nullptr, &constantBuffer, 0, 0);
// Render the mesh.
// See MeshObject::Render method below.
m_mesh->Render(context);
}
Método MeshObject::Render
void MeshObject::Render(_In_ ID3D11DeviceContext* context)
{
// PNTVertex is a struct. stride provides us the size required for all the mesh data
// struct PNTVertex
//{
// DirectX::XMFLOAT3 position;
// DirectX::XMFLOAT3 normal;
// DirectX::XMFLOAT2 textureCoordinate;
//};
uint32_t stride{ sizeof(PNTVertex) };
uint32_t offset{ 0 };
// Similar to the main render loop.
// Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
ID3D11Buffer* vertexBuffer{ m_vertexBuffer.get() };
context->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
// IASetIndexBuffer binds an index buffer to the input-assembler stage.
// For more info, see
// https://video2.skills-academy.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetindexbuffer.
context->IASetIndexBuffer(m_indexBuffer.get(), DXGI_FORMAT_R16_UINT, 0);
// Binds information about the primitive type, and data order that describes input data for the input assembler stage.
// For more info, see
// https://video2.skills-academy.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetprimitivetopology.
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// Draw indexed, non-instanced primitives. A draw API submits work to the rendering pipeline.
// For more info, see
// https://video2.skills-academy.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-drawindexed.
context->DrawIndexed(m_indexCount, 0, 0);
}
Método DeviceResources::P resent
Chamamos o método DeviceResources::P resent para exibir o conteúdo que colocamos nos buffers.
Usamos o termo cadeia de troca para uma coleção de buffers que são usados para exibir quadros para o usuário. Cada vez que um aplicativo apresenta um novo quadro para exibição, o primeiro buffer na cadeia de troca substitui o buffer exibido. Esse processo é chamado de troca ou inversão. Para obter mais informações, consulte Cadeias de troca.
- O método Present da interface IDXGISwapChain1 instrui o DXGI a bloquear até que a sincronização vertical (VSync) ocorra, colocando o aplicativo em suspensão até o próximo VSync. Isso garante que você não desperdice nenhum ciclo renderizando quadros que nunca serão exibidos na tela.
- O método DiscardView da interface ID3D11DeviceContext3 descarta o conteúdo do destino de renderização. Esta é uma operação válida somente quando o conteúdo existente for totalmente substituído. Se forem usados retângulos sujos ou de rolagem, essa chamada deverá ser removida.
- Usando o mesmo método DiscardView, descarte o conteúdo do estêncil de profundidade.
- O método HandleDeviceLost é usado para gerenciar o cenário do dispositivo que está sendo removido. Se o dispositivo foi removido por uma desconexão ou uma atualização de driver, você deverá recriar todos os recursos do dispositivo. Para obter mais informações, consulte Manipular cenários removidos do dispositivo no Direct3D 11.
Dica
Para obter uma taxa de quadros suave, você deve garantir que a quantidade de trabalho para renderizar um quadro se ajuste ao tempo entre VSyncs.
// Present the contents of the swap chain to the screen.
void DX::DeviceResources::Present()
{
// The first argument instructs DXGI to block until VSync, putting the application
// to sleep until the next VSync. This ensures we don't waste any cycles rendering
// frames that will never be displayed to the screen.
HRESULT hr = m_swapChain->Present(1, 0);
// Discard the contents of the render target.
// This is a valid operation only when the existing contents will be entirely
// overwritten. If dirty or scroll rects are used, this call should be removed.
m_d3dContext->DiscardView(m_d3dRenderTargetView.get());
// Discard the contents of the depth stencil.
m_d3dContext->DiscardView(m_d3dDepthStencilView.get());
// If the device was removed either by a disconnection or a driver upgrade, we
// must recreate all device resources.
if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
{
HandleDeviceLost();
}
else
{
winrt::check_hresult(hr);
}
}
Próximas etapas
Este tópico explicou como os gráficos são renderizados na exibição e fornece uma breve descrição de alguns dos termos de renderização usados (abaixo). Saiba mais sobre a renderização no tópico Estrutura de renderização II: renderização de jogos e saiba como preparar os dados necessários antes da renderização.
Termos e conceitos
Cena de jogo simples
Uma cena de jogo simples é composta de alguns objetos com várias fontes de luz.
A forma de um objeto é definida por um conjunto de coordenadas X, Y, Z no espaço. O local real de renderização no mundo do jogo pode ser determinado aplicando uma matriz de transformação às coordenadas posicionais X, Y, Z. Ele também pode ter um conjunto de coordenadas de textura - U e V - que especificam como um material é aplicado ao objeto. Isso define as propriedades da superfície do objeto e permite ver se um objeto tem uma superfície áspera (como uma bola de tênis) ou uma superfície lisa e brilhante (como uma bola de boliche).
As informações de cena e objeto são usadas pela estrutura de renderização para recriar a cena quadro a quadro, tornando-a viva no monitor de exibição.
Pipeline de renderização
O pipeline de renderização é o processo pelo qual as informações da cena 3D são convertidas em uma imagem exibida na tela. No Direct3D 11, esse pipeline é programável. Você pode adaptar os estágios para dar suporte às suas necessidades de renderização. Os estágios que apresentam núcleos de sombreador comuns são programáveis usando a linguagem de programação HLSL. Também é conhecido como pipeline de renderização de gráficos ou simplesmente pipeline.
Para ajudá-lo a criar esse pipeline, você precisa estar familiarizado com esses detalhes.
- HLSL. Recomendamos o uso do HLSL Shader Model 5.1 e superior para jogos UWP DirectX.
- Shaders.
- Sombreadores de vértice e sombreadores de pixel.
- Estágios de sombreador.
- Vários formatos de arquivo de sombreador.
Para obter mais informações, consulte Entender o pipeline de renderização do Direct3D 11 e o pipeline de gráficos.
HLSL
HLSL é a linguagem de sombreador de alto nível para DirectX. Usando o HLSL, você pode criar sombreadores programáveis semelhantes a C para o pipeline do Direct3D. Para obter mais informações, consulte HLSL.
Sombreadores
Um sombreador pode ser considerado como um conjunto de instruções que determinam como a superfície de um objeto aparece quando renderizada. Aqueles que são programados usando HLSL são conhecidos como sombreadores HLSL. Os arquivos de código-fonte para sombreadores [HLSL])(#hlsl) têm a extensão de .hlsl
arquivo. Esses sombreadores podem ser compilados em tempo de build ou em tempo de execução e definidos em tempo de execução no estágio de pipeline apropriado. Um objeto de sombreador compilado tem uma extensão de .cso
arquivo.
Os sombreadores do Direct3D 9 podem ser projetados usando o modelo de sombreador 1, o modelo de sombreador 2 e o modelo de sombreador 3; Os sombreadores do Direct3D 10 podem ser projetados somente no modelo de sombreador 4. Os sombreadores do Direct3D 11 podem ser projetados no modelo de sombreador 5. O Direct3D 11.3 e o Direct3D 12 podem ser projetados no modelo de sombreador 5.1 e o Direct3D 12 também pode ser projetado no modelo de sombreador 6.
Sombreadores de vértice e sombreadores de pixel
Os dados entram no pipeline de gráficos como um fluxo de primitivos e são processados por vários sombreadores, como os sombreadores de vértice e os sombreadores de pixel.
Os sombreadores de vértice processam vértices, normalmente executando operações como transformações, skinning e iluminação. Os sombreadores de pixel permitem técnicas avançadas de sombreamento, como iluminação por pixel e pós-processamento. Ele combina variáveis constantes, dados de textura, valores interpolados por vértice e outros dados para produzir saídas por pixel.
Estágios de sombreamento
Uma sequência desses vários sombreadores definidos para processar esse fluxo de primitivos é conhecida como estágios de sombreador em um pipeline de renderização. Os estágios reais dependem da versão do Direct3D, mas geralmente incluem os estágios de vértice, geometria e pixel. Há também outros estágios, como os sombreadores de casco e domínio para mosaico e o sombreador de computação. Todos esses estágios são completamente programáveis usando HLSL. Para obter mais informações, consulte Pipeline de gráficos.
Vários formatos de arquivo de sombreamento
Aqui estão as extensões de arquivo de código do sombreador.
- Um arquivo com a extensão contém o
.hlsl
código-fonte [HLSL])(#hlsl). - Um arquivo com a
.cso
extensão contém um objeto de sombreador compilado. - Um arquivo com a extensão é um arquivo de cabeçalho, mas em um contexto de código de sombreador, esse arquivo de cabeçalho define uma matriz de bytes que contém dados de
.h
sombreador. - Um arquivo com a
.hlsli
extensão contém o formato dos buffers constantes. No jogo de exemplo, o arquivo é Shaders>ConstantBuffers.hlsli.
Observação
Você insere um sombreador carregando um .cso
arquivo em tempo de execução ou adicionando um .h
arquivo em seu código executável. Mas você não usaria os dois para o mesmo shader.
Compreensão mais profunda do DirectX
O Direct3D 11 é um conjunto de APIs que pode nos ajudar a criar gráficos para aplicativos com uso intensivo de gráficos, como jogos, em que queremos ter uma boa placa gráfica para processar computação intensiva. Esta seção explica brevemente os conceitos de programação gráfica do Direct3D 11: recurso, sub-recurso, dispositivo e contexto do dispositivo.
Recurso
Você pode pensar em recursos (também conhecidos como recursos de dispositivo) como informações sobre como renderizar um objeto, como textura, posição ou cor. Os recursos fornecem dados para o pipeline e definem o que é renderizado durante a cena. Os recursos podem ser carregados da mídia do jogo ou criados dinamicamente em tempo de execução.
Um recurso é, na verdade, uma área na memória que pode ser acessada pelo pipeline do Direct3D. Para que o pipeline acesse a memória com eficiência, os dados fornecidos ao pipeline (como geometria de entrada, recursos de sombreador e texturas) devem ser armazenados em um recurso. Existem dois tipos de recursos dos quais todos os recursos de Direct3D são derivados: um buffer ou uma textura. Até 128 recursos podem estar ativos para cada estágio do pipeline. Para saber mais, confira Recursos.
Sub-recurso
O termo sub-recurso refere-se a um subconjunto de um recurso. O Direct3D pode fazer referência a um recurso inteiro ou pode fazer referência a subconjuntos de um recurso. Para obter mais informações, consulte Sub-recurso.
Estêncil de profundidade
Um recurso de estêncil de profundidade contém o formato e o buffer para armazenar informações de profundidade e estêncil. Ele é criado usando um recurso de textura. Para obter mais informações sobre como criar um recurso de estêncil de profundidade, consulte Configurando a funcionalidade de estêncil de profundidade. Acessamos o recurso de estêncil de profundidade por meio da exibição de estêncil de profundidade implementada usando a interface ID3D11DepthStencilView .
As informações de profundidade nos dizem quais áreas dos polígonos estão atrás de outras, para que possamos determinar quais estão ocultas. As informações do estêncil nos dizem quais pixels estão mascarados. Ele pode ser usado para produzir efeitos especiais, pois determina se um pixel é desenhado ou não; define o bit como 1 ou 0.
Para obter mais informações, consulte Exibição de estêncil de profundidade, buffer de profundidade e buffer de estêncil.
Destino de renderização
Um destino de renderização é um recurso no qual podemos gravar no final de uma passagem de renderização. Normalmente, ele é criado usando o método ID3D11Device::CreateRenderTargetView usando o buffer traseiro da cadeia de troca (que também é um recurso) como o parâmetro de entrada.
Cada destino de renderização também deve ter uma exibição de estêncil de profundidade correspondente, pois quando usamos OMSetRenderTargets para definir o destino de renderização antes de usá-lo, ele também requer uma exibição de estêncil de profundidade. Acessamos o recurso de destino de renderização por meio da exibição de destino de renderização implementada usando a interface ID3D11RenderTargetView .
Dispositivo
Você pode imaginar um dispositivo como uma forma de alocar e destruir objetos, renderizar primitivos e se comunicar com a placa gráfica por meio do driver gráfico.
Para obter uma explicação mais precisa, um dispositivo Direct3D é o componente de renderização do Direct3D. Um dispositivo encapsula e armazena o estado de renderização, executa transformações e operações de iluminação e rasteriza uma imagem para uma superfície. Para obter mais informações, consulte Dispositivos
Um dispositivo é representado pela interface ID3D11Device . Em outras palavras, a interface ID3D11Device representa um adaptador de exibição virtual e é usada para criar recursos que pertencem a um dispositivo.
Existem diferentes versões do ID3D11Device. ID3D11Device5 é a versão mais recente e adiciona novos métodos aos do ID3D11Device4. Para obter mais informações sobre como o Direct3D se comunica com o hardware subjacente, consulte Arquitetura WDDM (Modelo de Driver de Dispositivo) do Windows.
Cada aplicativo deve ter pelo menos um dispositivo; A maioria dos aplicativos cria apenas um. Crie um dispositivo para um dos drivers de hardware instalados em seu computador chamando D3D11CreateDevice ou D3D11CreateDeviceAndSwapChain e especificando o tipo de driver com o sinalizador D3D_DRIVER_TYPE . Cada dispositivo pode usar um ou mais contextos de dispositivo, dependendo da funcionalidade desejada. Para obter mais informações, consulte a função D3D11CreateDevice .
Contexto de dispositivo
Um contexto de dispositivo é usado para definir o estado do pipeline e gerar comandos de renderização usando os recursos pertencentes a um dispositivo.
O Direct3D 11 implementa dois tipos de contextos de dispositivo, um para renderização imediata e outro para renderização adiada; ambos os contextos são representados com uma interface ID3D11DeviceContext .
As interfaces ID3D11DeviceContext têm versões diferentes; ID3D11DeviceContext4 adiciona novos métodos aos de ID3D11DeviceContext3.
ID3D11DeviceContext4 é introduzido na Atualização do Windows 10 para Criadores e é a versão mais recente da interface ID3D11DeviceContext . Os aplicativos direcionados ao Windows 10 Creators Update e posteriores devem usar essa interface em vez de versões anteriores. Para obter mais informações, consulte ID3D11DeviceContext4.
DX::D eviceResources
A classe DX::D eviceResources está nos arquivos DeviceResources.cpp.h/ e controla todos os recursos do dispositivo DirectX.
Buffer
Um recurso de buffer é uma coleção de dados totalmente tipados agrupados em elementos. Você pode usar buffers para armazenar uma ampla variedade de dados, incluindo vetores de posição, vetores normais, coordenadas de textura em um buffer de vértice, índices em um buffer de índice ou estado do dispositivo. Os elementos de buffer podem incluir valores de dados compactados (como valores de superfície R8G8B8A8 ), inteiros únicos de 8 bits ou quatro valores de ponto flutuante de 32 bits.
Há três tipos de buffers disponíveis: buffer de vértice, buffer de índice e buffer constante.
Buffer de vértice
Contém os dados de vértice usados para definir sua geometria. Os dados de vértice incluem coordenadas de posição, dados de cor, dados de coordenadas de textura, dados normais e assim por diante.
Buffer de índice
Contém deslocamentos inteiros em buffers de vértice e são usados para renderizar primitivos com mais eficiência. Um buffer de índice contém um conjunto sequencial de índices de 16 bits ou 32 bits; Cada índice é usado para identificar um vértice em um buffer de vértice.
Buffer constante ou buffer constante de sombreador
Permite que você forneça dados de sombreador com eficiência para o pipeline. Você pode usar buffers constantes como entradas para os sombreadores que são executados para cada primitivo e armazenar resultados do estágio de saída de fluxo do pipeline de renderização. Conceitualmente, um buffer constante se parece com um buffer de vértice de elemento único.
Desenho e implementação de buffers
Você pode criar buffers com base no tipo de dados, por exemplo, como em nosso jogo de exemplo, um buffer é criado para dados estáticos, outro para dados constantes no quadro e outro para dados específicos de um primitivo.
Todos os tipos de buffer são encapsulados pela interface ID3D11Buffer e você pode criar um recurso de buffer chamando ID3D11Device::CreateBuffer. Mas um buffer deve ser associado ao pipeline antes que ele possa ser acessado. Os buffers podem ser associados a vários estágios de pipeline simultaneamente para leitura. Um buffer também pode ser associado a um único estágio de pipeline para gravação; no entanto, o mesmo buffer não pode ser associado para leitura e gravação simultaneamente.
Você pode associar buffers dessas maneiras.
- Para o estágio do assembler de entrada chamando métodos ID3D11DeviceContext , como ID3D11DeviceContext::IASetVertexBuffers e ID3D11DeviceContext::IASetIndexBuffer.
- Para o estágio de saída de fluxo chamando ID3D11DeviceContext::SOSetTargets.
- Para o estágio do sombreador chamando métodos de sombreador, como ID3D11DeviceContext::VSSetConstantBuffers.
Para obter mais informações, consulte Introdução aos buffers no Direct3D 11.
DXGI
O Microsoft DirectX Graphics Infrastructure (DXGI) é um subsistema que encapsula algumas das tarefas de baixo nível necessárias para o Direct3D. Deve-se tomar cuidado especial ao usar o DXGI em um aplicativo multithread para garantir que não ocorram deadlocks. Para obter mais informações, consulte Multithreading e DXGI
Nível de recurso
O nível de recurso é um conceito introduzido no Direct3D 11 para lidar com a diversidade de placas de vídeo em computadores novos e existentes. Um nível de recurso é um conjunto bem definido de funcionalidade de unidade de processamento gráfico (GPU).
Cada placa de vídeo implementa um certo nível de funcionalidade do DirectX, dependendo das GPUs instaladas. Em versões anteriores do Microsoft Direct3D, você podia descobrir a versão do Direct3D que a placa de vídeo implementava e, em seguida, programar seu aplicativo de acordo.
Com o nível de recurso, ao criar um dispositivo, você pode tentar criar um dispositivo para o nível de recurso que deseja solicitar. Se a criação do dispositivo funcionar, esse nível de recurso existe, caso contrário, o hardware não oferece suporte a esse nível de recurso. Você pode tentar recriar um dispositivo em um nível de recurso inferior ou pode optar por sair do aplicativo. Por exemplo, o nível de recurso 12_0 requer Direct3D 11.3 ou Direct3D 12 e modelo de sombreador 5.1. Para obter mais informações, consulte Níveis de recursos do Direct3D: visão geral de cada nível de recurso.
Usando níveis de recursos, você pode desenvolver um aplicativo para Direct3D 9, Microsoft Direct3D 10 ou Direct3D 11 e, em seguida, executá-lo em hardware 9, 10 ou 11 (com algumas exceções). Para obter mais informações, consulte Níveis de recursos do Direct3D.
Renderização estéreo
A renderização estéreo é usada para aumentar a ilusão de profundidade. Ele usa duas imagens, uma do olho esquerdo e outra do olho direito para exibir uma cena na tela de exibição.
Matematicamente, aplicamos uma matriz de projeção estéreo, que é um ligeiro deslocamento horizontal para a direita e para a esquerda, da matriz de projeção mono regular para conseguir isso.
Fizemos duas passagens de renderização para obter renderização estéreo neste jogo de exemplo.
- Vincule ao destino de renderização correto, aplique a projeção correta e desenhe o objeto primitivo.
- Vincule ao destino de renderização esquerdo, aplique a projeção à esquerda e desenhe o objeto primitivo.
Câmera e espaço de coordenadas
O jogo tem o código para atualizar o mundo em seu próprio sistema de coordenadas (às vezes chamado de espaço do mundo ou espaço da cena). Todos os objetos, incluindo a câmera, são posicionados e orientados neste espaço. Para obter mais informações, consulte Sistemas de coordenadas.
Um sombreador de vértice faz o trabalho pesado de conversão das coordenadas do modelo em coordenadas do dispositivo com o algoritmo a seguir (em que V é um vetor e M é uma matriz).
V(device) = V(model) x M(model-to-world) x M(world-to-view) x M(view-to-device)
M(model-to-world)
é uma matriz de transformação para coordenadas de modelo para coordenadas mundiais, também conhecida como matriz de transformação mundial. Isso é fornecido pelo primitivo.M(world-to-view)
é uma matriz de transformação para coordenadas mundiais para coordenadas de exibição, também conhecida como matriz de transformação de exibição.- Isso é fornecido pela matriz de visualização da câmera. Ele é definido pela posição da câmera junto com os vetores de aparência (o vetor de visão que aponta diretamente para a cena da câmera e o vetor de pesquisa que é perpendicular a ela).
- No jogo de exemplo, m_viewMatrix é a matriz de transformação de exibição e é calculada usando Camera::SetViewParams.
M(view-to-device)
é uma matriz de transformação para coordenadas de exibição para coordenadas do dispositivo, também conhecida como matriz de transformação de projeção.- Isso é fornecido pela projeção da câmera. Ele fornece informações sobre quanto desse espaço é realmente visível na cena final. O campo de visão (FoV), a taxa de proporção e os planos de recorte definem a matriz de transformação de projeção.
- No jogo de exemplo, m_projectionMatrix define a transformação para as coordenadas de projeção, calculadas usando Camera::SetProjParams (para projeção estéreo, você usa duas matrizes de projeção, uma para a exibição de cada olho).
O código do sombreador é VertexShader.hlsl
carregado com esses vetores e matrizes dos buffers constantes e executa essa transformação para cada vértice.
Transformação de coordenadas
O Direct3D usa três transformações para alterar as coordenadas do modelo 3D em coordenadas de pixel (espaço de tela). Essas transformações são transformação do mundo, transformação da visão e transformação da projeção. Para obter mais informações, consulte Visão geral da transformação.
Matriz de transformação do mundo
Uma transformação de mundo altera as coordenadas do espaço do modelo, onde os vértices são definidos em relação à origem local de um modelo, para o espaço do mundo, onde os vértices são definidos em relação a uma origem comum a todos os objetos em uma cena. Em essência, a transformação do mundo coloca um modelo no mundo; daí seu nome. Para obter mais informações, consulte Transformação de mundo.
Exibir matriz de transformação
A transformação da vista localiza o espectador no espaço do mundo, transformando vértices em espaço de câmera. No espaço da câmera, a câmera, ou espectador, está na origem, olhando na direção z positiva. Para obter mais informações, acesse Exibir transformação.
Matriz de transformação de projeção
A transformação de projeção converte o tronco de visualização em uma forma paralelepípeda. Um tronco de visualização é um volume 3D em uma cena posicionada em relação à câmera da janela de visualização. Uma viewport é um retângulo 2D no qual uma cena 3D é projetada. Para obter mais informações, consulte Viewports e recorte
Como a extremidade próxima do tronco de visualização é menor que a extremidade oposta, isso tem o efeito de expandir os objetos que estão próximos à câmera; É assim que a perspectiva é aplicada à cena. Portanto, os objetos que estão mais próximos do jogador parecem maiores; objetos que estão mais distantes parecem menores.
Matematicamente, a transformação de projeção é uma matriz que normalmente é uma escala e uma projeção em perspectiva. Funciona como a lente de uma câmera. Para obter mais informações, consulte Transformação de projeção.
Estado do amostrador
O estado do amostrador determina como os dados de textura são amostrados usando modos de endereçamento de textura, filtragem e nível de detalhe. A amostragem é feita sempre que um pixel de textura (ou texel) é lido de uma textura.
Uma textura contém uma matriz de texels. A posição de cada texel é indicada por (u,v)
, onde u
é a largura e v
é a altura, e é mapeada entre 0 e 1 com base na largura e altura da textura. As coordenadas de textura resultantes são usadas para endereçar um texel ao amostrar uma textura.
Quando as coordenadas de textura estão abaixo de 0 ou acima de 1, o modo de endereço de textura define como a coordenada de textura aborda um local de texel. Por exemplo, ao usar TextureAddressMode.Clamp, qualquer coordenada fora do intervalo de 0 a 1 é fixada a um valor máximo de 1 e um valor mínimo de 0 antes da amostragem.
Se a textura for muito grande ou muito pequena para o polígono, a textura será filtrada para caber no espaço. Um filtro de ampliação amplia uma textura, um filtro de minificação reduz a textura para caber em uma área menor. A ampliação de textura repete o texel de exemplo para um ou mais endereços, o que produz uma imagem mais desfocada. A minificação de textura é mais complicada porque requer a combinação de mais de um valor de texel em um único valor. Isso pode causar aliasing ou bordas irregulares, dependendo dos dados de textura. A abordagem mais popular para minificação é usar um mipmap. Um mipmap é uma textura de vários níveis. O tamanho de cada nível é um poder de 2 menor que o nível anterior até uma textura 1x1. Quando a minificação é usada, um jogo escolhe o nível de mipmap mais próximo do tamanho necessário no momento da renderização.
A classe BasicLoader
BasicLoader é uma classe de carregador simples que fornece suporte para carregar sombreadores, texturas e malhas de arquivos no disco. Ele fornece métodos síncronos e assíncronos. Neste jogo de exemplo, os BasicLoader.h/.cpp
arquivos são encontrados na pasta Utilitários .
Para obter mais informações, consulte Carregador Básico.