Adicionando conteúdo visual ao exemplo do Marble Maze
Este documento descreve como o jogo Marble Maze usa o Direct3D e Direct2D no ambiente de aplicativo da Plataforma Universal do Windows (UWP) para que você possa aprender os padrões e adaptá-los ao trabalhar com seu próprio conteúdo de jogo. Para saber como os componentes do jogo visual se encaixam na estrutura geral do aplicativo do Marble Maze, consulte Estrutura do aplicativo Marble Maze.
Seguimos estas etapas básicas conforme desenvolvemos os aspectos visuais do Marble Maze:
- Crie uma estrutura básica que inicialize os ambientes Direct3D e Direct2D.
- Use programas de edição de imagens e modelos para projetar os ativos 2D e 3D que aparecem no jogo.
- Certifique-se de que os ativos 2D e 3D sejam carregados e exibidos corretamente no jogo.
- Integre sombreadores de pixel e de vértice que melhoram a qualidade visual dos ativos do jogo.
- Integre a lógica do jogo, como animação e entrada do usuário.
Também nos concentramos primeiro em adicionar ativos 3D e depois em ativos 2D. Por exemplo, nos concentramos na lógica principal do jogo antes de adicionar o sistema de menu e o temporizador.
Também precisávamos iterar algumas dessas etapas várias vezes durante o processo de desenvolvimento. Por exemplo, à medida que fizemos alterações nos modelos de malha e da bolinha, também tivemos que alterar parte do código do sombreador que suporta esses modelos.
Observação
O código de exemplo que corresponde a este documento é encontrado no exemplo de jogo DirectX Marble Maze.
Aqui estão alguns dos principais pontos que este documento aborda quando você trabalha com DirectX e conteúdo de jogo visual, ou seja, quando você inicializa as bibliotecas de elementos gráficos DirectX, carrega recursos de cena e atualiza e renderiza a cena:
- A adição de conteúdo de jogos normalmente envolve muitas etapas. Essas etapas também geralmente exigem iteração. Os desenvolvedores de jogos geralmente se concentram primeiro em adicionar conteúdo de jogos 3D e, em seguida, em adicionar conteúdo 2D.
- Alcance mais clientes e proporcione a todos eles uma ótima experiência, oferecendo suporte à maior variedade possível de hardware gráfico.
- Separe os formatos de tempo de design e tempo de execução de forma limpa. Estruture seus ativos de tempo de design para maximizar a flexibilidade e permitir iterações rápidas no conteúdo. Formate e compacte seus ativos para carregar e renderizar da forma mais eficiente possível em tempo de execução.
- Você cria os dispositivos Direct3D e Direct2D em um aplicativo UWP da mesma forma que faz em um aplicativo de área de trabalho clássico do Windows. Uma diferença importante é como a cadeia de troca está associada à janela de saída.
- Ao projetar seu jogo, certifique-se de que o formato de malha escolhido ofereça suporte aos seus principais cenários. Por exemplo, se o jogo exigir colisão, certifique-se de que você pode obter dados de colisão de suas malhas.
- Separe a lógica do jogo da lógica de renderização atualizando primeiro todos os objetos de cena antes de renderizá-los.
- Normalmente, você desenha seus objetos de cena 3D e, em seguida, quaisquer objetos 2D que aparecem na frente da cena.
- Sincronize o desenho com o espaço em branco vertical para garantir que seu jogo não gaste tempo desenhando quadros que nunca serão realmente mostrados na tela. Um vazio vertical é o tempo entre quando um quadro termina de desenhar para o monitor e o próximo quadro começa.
Introdução aos elementos gráficos DirectX
Quando planejamos o jogo Marble Maze Universal da Windows Platform (UWP), escolhemos C++ e Direct3D 11.1 porque eles são excelentes opções para criar jogos 3D que exigem controle máximo sobre renderização e alto desempenho. O DirectX 11.1 oferece suporte ao hardware do DirectX 9 ao DirectX 11 e, portanto, pode ajudar você a alcançar mais clientes com mais eficiência, pois você não precisa reescrever o código para cada uma das versões anteriores do DirectX.
O Marble Maze usa o Direct3D 11.1 para renderizar os ativos do jogo 3D, ou seja, a bolinha e o labirinto. O Marble Maze também usa Direct2D, DirectWrite e Windows Imaging Component (WIC) para desenhar os ativos do jogo 2D, como os menus e o temporizador.
O desenvolvimento de jogos requer planejamento. Se você é novo em elementos gráficos DirectX, recomendamos que leia DirectX: Introdução para se familiarizar com os conceitos básicos da criação de um jogo UWP DirectX. Ao ler este documento e trabalhar com o código-fonte do Marble Maze, você pode consultar os seguintes recursos para obter informações mais detalhadas sobre elementos gráficos DirectX:
- Elementos gráficos do Direct3D 11: descreve o Direct3D 11, uma poderosa API de gráficos 3D acelerada por hardware para renderizar geometria 3D na plataforma Windows.
- Direct2D: descreve o Direct2D, uma API de gráficos 2D acelerada por hardware que fornece renderização de alto desempenho e alta qualidade para geometria, bitmaps e texto 2D.
- DirectWrite: descreve o DirectWrite, que oferece suporte à renderização de texto de alta qualidade.
- Windows Imaging Component: descreve o WIC, uma plataforma extensível que fornece API de baixo nível para imagens digitais.
Níveis de recursos
O Direct3D 11 introduz um paradigma chamado níveis de recurso. Um nível de recurso é um conjunto bem definido de funcionalidades de GPU. Use os níveis de recurso para direcionar seu jogo para ser executado em versões anteriores do hardware Direct3D. O Marble Maze suporta o nível de recursos 9.1 porque não requer recursos avançados dos níveis superiores. Recomendamos que você ofereça suporte à maior variedade de hardware possível e dimensione o conteúdo do jogo para que seus clientes que têm computadores de baixo ou alto custo tenham uma ótima experiência. Para obter mais informações sobre níveis de recurso, consulte Direct3D 11 em hardware de nível inferior.
Inicializando Direct3D e Direct2D
Um dispositivo representa o adaptador de vídeo. Você cria os dispositivos Direct3D e Direct2D em um aplicativo UWP da mesma forma que faz em um aplicativo de área de trabalho clássico do Windows. A principal diferença é como você conecta a cadeia de troca Direct3D ao sistema de janelas.
A classe DeviceResources é uma base para gerenciar Direct3D e Direct2D. Essa classe lida com infraestrutura geral, não com ativos específicos do jogo. O Marble Maze define a classe MarbleMazeMain para manipular ativos específicos do jogo, que tem uma referência a um objeto DeviceResources para dar acesso ao Direct3D e Direct2D.
Durante a inicialização, o construtor DeviceResources cria recursos independentes de dispositivo e os dispositivos Direct3D e Direct2D.
// Initialize the Direct3D resources required to run.
DX::DeviceResources::DeviceResources() :
m_screenViewport(),
m_d3dFeatureLevel(D3D_FEATURE_LEVEL_9_1),
m_d3dRenderTargetSize(),
m_outputSize(),
m_logicalSize(),
m_nativeOrientation(DisplayOrientations::None),
m_currentOrientation(DisplayOrientations::None),
m_dpi(-1.0f),
m_deviceNotify(nullptr)
{
CreateDeviceIndependentResources();
CreateDeviceResources();
}
A classe DeviceResources separa essa funcionalidade para que ela possa responder mais facilmente quando o ambiente for alterado. Por exemplo, ele chama o método CreateWindowSizeDependentResources quando o tamanho da janela muda.
Inicializando as fábricas Direct2D, DirectWrite e WIC
O método DeviceResources::CreateDeviceIndependentResources cria as fábricas para Direct2D, DirectWrite e WIC. Em gráficos DirectX, as fábricas são os pontos de partida para a criação de recursos gráficos. O Marble Maze especifica D2D1_FACTORY_TYPE_SINGLE_THREADED porque executa todo o desenho no thread principal.
// These are the resources required independent of hardware.
void DX::DeviceResources::CreateDeviceIndependentResources()
{
// Initialize Direct2D resources.
D2D1_FACTORY_OPTIONS options;
ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS));
#if defined(_DEBUG)
// If the project is in a debug build, enable Direct2D debugging via SDK Layers.
options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif
// Initialize the Direct2D Factory.
DX::ThrowIfFailed(
D2D1CreateFactory(
D2D1_FACTORY_TYPE_SINGLE_THREADED,
__uuidof(ID2D1Factory2),
&options,
&m_d2dFactory
)
);
// Initialize the DirectWrite Factory.
DX::ThrowIfFailed(
DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof(IDWriteFactory2),
&m_dwriteFactory
)
);
// Initialize the Windows Imaging Component (WIC) Factory.
DX::ThrowIfFailed(
CoCreateInstance(
CLSID_WICImagingFactory2,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_wicFactory)
)
);
}
Criando os dispositivos Direct3D e Direct2D
O método DeviceResources::CreateDeviceResources chama D3D11CreateDevice para criar o objeto de dispositivo que representa o adaptador de vídeo Direct3D. Como o Marble Maze oferece suporte ao nível de recurso 9.1 e superior, o método DeviceResources::CreateDeviceResources especifica os níveis 9.1 a 11.1 na matriz featureLevels. O Direct3D percorre a lista em ordem e fornece ao aplicativo o primeiro nível de recurso disponível. Portanto, as entradas de matriz D3D_FEATURE_LEVEL são listadas da mais alta para a mais baixa para que o aplicativo obtenha o nível de recurso mais alto disponível. O método DeviceResources::CreateDeviceResources obtém o dispositivo Direct3D 11.1 consultando o dispositivo Direct3D 11 retornado de D3D11CreateDevice.
// This flag adds support for surfaces with a different color channel ordering
// than the API default. It is required for compatibility with Direct2D.
UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(_DEBUG)
if (DX::SdkLayersAvailable())
{
// If the project is in a debug build, enable debugging via SDK Layers
// with this flag.
creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
}
#endif
// This array defines the set of DirectX hardware feature levels this app will support.
// Note the ordering should be preserved.
// Don't forget to declare your application's minimum required feature level in its
// description. All applications are assumed to support 9.1 unless otherwise stated.
D3D_FEATURE_LEVEL featureLevels[] =
{
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1
};
// Create the Direct3D 11 API device object and a corresponding context.
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;
HRESULT hr = D3D11CreateDevice(
nullptr, // Specify nullptr to use the default adapter.
D3D_DRIVER_TYPE_HARDWARE, // Create a device using the hardware graphics driver.
0, // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
creationFlags, // Set debug and Direct2D compatibility flags.
featureLevels, // List of feature levels this app can support.
ARRAYSIZE(featureLevels), // Size of the list above.
D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for UWP apps.
&device, // Returns the Direct3D device created.
&m_d3dFeatureLevel, // Returns feature level of device created.
&context // Returns the device immediate context.
);
if (FAILED(hr))
{
// If the initialization fails, fall back to the WARP device.
// For more information on WARP, see:
// https://go.microsoft.com/fwlink/?LinkId=286690
DX::ThrowIfFailed(
D3D11CreateDevice(
nullptr,
D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device.
0,
creationFlags,
featureLevels,
ARRAYSIZE(featureLevels),
D3D11_SDK_VERSION,
&device,
&m_d3dFeatureLevel,
&context
)
);
}
// Store pointers to the Direct3D 11.1 API device and immediate context.
DX::ThrowIfFailed(
device.As(&m_d3dDevice)
);
DX::ThrowIfFailed(
context.As(&m_d3dContext)
);
O método DeviceResources::CreateDeviceResources cria o dispositivo Direct2D. O Direct2D usa o Microsoft DirectX Graphics Infrastructure (DXGI) para interoperar com o Direct3D. O DXGI permite que superfícies de memória de vídeo sejam compartilhadas entre tempos de execução gráficos. O Marble Maze usa o dispositivo DXGI subjacente do dispositivo Direct3D para criar o dispositivo Direct2D da fábrica do Direct2D.
// Create the Direct2D device object and a corresponding context.
ComPtr<IDXGIDevice3> dxgiDevice;
DX::ThrowIfFailed(
m_d3dDevice.As(&dxgiDevice)
);
DX::ThrowIfFailed(
m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice)
);
DX::ThrowIfFailed(
m_d2dDevice->CreateDeviceContext(
D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
&m_d2dContext
)
);
Para obter mais informações sobre DXGI e interoperabilidade entre Direct2D e Direct3D, consulte Visão geral do DXGI e Visão geral da interoperabilidade do Direct2D e do Direct3D.
Associando o Direct3D ao modo de exibição
O método DeviceResources::CreateWindowSizeDependentResources cria os recursos gráficos que dependem de um determinado tamanho de janela, como a cadeia de troca e os destinos de renderização Direct3D e Direct2D. Uma maneira importante de um aplicativo UWP DirectX diferir de um aplicativo de área de trabalho é como a cadeia de troca é associada à janela de saída. Uma cadeia de troca é responsável por exibir o buffer para o qual o dispositivo é renderizado no monitor. A estrutura do aplicativo Marble Maze descreve como o sistema de janelas para um aplicativo UWP difere de um aplicativo de desktop. Como um aplicativo UWP não funciona com objetos HWND o Marble Maze deve usar o método IDXGIFactory2::CreateSwapChainForCoreWindow para associar a saída do dispositivo à exibição. O exemplo a seguir mostra a parte do método DeviceResources::CreateWindowSizeDependentResources que cria a cadeia de troca.
// Obtain the final swap chain for this window from the DXGI factory.
DX::ThrowIfFailed(
dxgiFactory->CreateSwapChainForCoreWindow(
m_d3dDevice.Get(),
reinterpret_cast<IUnknown*>(m_window.Get()),
&swapChainDesc,
nullptr,
&m_swapChain
)
);
Para minimizar o consumo de energia, o que é importante fazer em dispositivos alimentados por bateria, como laptops e tablets, o método DeviceResources::CreateWindowSizeDependentResources chama o método IDXGIDevice1::SetMaximumFrameLatency para garantir que o jogo seja renderizado somente após o espaço em branco vertical. A sincronização com o espaço em branco vertical é descrita com mais detalhes na seção Apresentando a cena neste documento.
// Ensure that DXGI does not queue more than one frame at a time. This both
// reduces latency and ensures that the application will only render after each
// VSync, minimizing power consumption.
DX::ThrowIfFailed(
dxgiDevice->SetMaximumFrameLatency(1)
);
O método DeviceResources::CreateWindowSizeDependentResources inicializa recursos gráficos de uma maneira que funciona para a maioria dos jogos.
Observação
O termo exibição tem um significado diferente no Windows Runtime do que no Direct3D. No Windows Runtime, um modo de exibição refere-se à coleção de configurações da interface do usuário para um aplicativo, incluindo a área de exibição e os comportamentos de entrada, além do thread que ele usa para processamento. Você especifica a configuração e as definições necessárias ao criar um modo de exibição. O processo de configuração da exibição do aplicativo é descrito em estrutura do aplicativo Marble Maze. No Direct3D, o termo modo de exibição tem vários significados. Uma exibição de recurso define os subrecursos que um recurso pode acessar. Por exemplo, quando um objeto de textura é associado a uma exibição de recurso de sombreador, esse sombreador pode acessar a textura posteriormente. Uma vantagem de uma exibição de recursos é que você pode interpretar dados de maneiras diferentes em diferentes estágios no pipeline de renderização. Para obter mais informações sobre exibições de recursos, consulte Exibições de recursos. Quando usado no contexto de uma matriz de transformação de exibição ou transformação de exibição, a exibição refere-se à localização e orientação da câmera. Uma transformação de exibição realoca objetos no mundo em torno da posição e orientação da câmera. Para obter mais informações sobre transformações de exibição, consulte Transformação de exibição (Direct3D 9). Como o Marble Maze usa modos de exibição de recursos e matriz é descrito com mais detalhes neste tópico.
Carregando recursos de cena
O Marble Maze usa a classe BasicLoader, que é declarada em BasicLoader.h, para carregar texturas e sombreadores. O Marble Maze usa a classe SDKMesh para carregar as malhas 3D para o labirinto e a bolinha.
Para garantir um aplicativo responsivo, o Marble Maze carrega recursos de cena de forma assíncrona ou em segundo plano. À medida que os ativos são carregados em segundo plano, seu jogo pode responder a eventos de janela. Esse processo é explicado com mais detalhes em Carregando ativos de jogo em segundo plano neste guia.
Carregando a sobreposição 2D e a interface do usuário
No Marble Maze, a sobreposição é a imagem que aparece na parte superior da tela. A sobreposição sempre aparece na frente da cena. No Marble Maze, a sobreposição contém o logotipo do Windows e a cadeia de caracteres de texto exemplo de jogo DirectX Marble Maze. O gerenciamento da sobreposição é realizado pela classe SampleOverlay que é definida em SampleOverlay.h. Embora usemos a sobreposição como parte dos exemplos do Direct3D, você pode adaptar esse código para exibir qualquer imagem que apareça na frente da cena.
Um aspecto importante da sobreposição é que, como seu conteúdo não é alterado, a classe SampleOverlay desenha, ou armazena em cache, seu conteúdo em um objeto ID2D1Bitmap1 durante a inicialização. No momento do desenho, a classe SampleOverlay só precisa desenhar o bitmap na tela. Dessa forma, rotinas caras, como desenho de texto, não precisam ser realizadas para cada quadro.
A interface do usuário (UI) consiste em componentes 2D, como menus e exibições de alerta (HUDs), que aparecem na frente da cena. O Marble Maze define os seguintes elementos da interface do usuário:
- Itens de menu que permitem ao usuário iniciar o jogo ou visualizar pontuações altas.
- Um cronômetro que faz a contagem regressiva por três segundos antes do início do jogo.
- Um temporizador que rastreia o tempo de reprodução decorrido.
- Uma tabela que lista os tempos de chegada mais rápidos.
- Texto que diz pausado quando o jogo é pausado.
O Marble Maze define elementos de interface do usuário específicos do jogo em UserInterface.h. O Marble Maze define a classe ElementBase como um tipo base para todos os elementos da interface do usuário. A classe ElementBase define atributos como tamanho, posição, alinhamento e visibilidade de um elemento da interface do usuário. Ele também controla como os elementos são atualizados e renderizados.
class ElementBase
{
public:
virtual void Initialize() { }
virtual void Update(float timeTotal, float timeDelta) { }
virtual void Render() { }
void SetAlignment(AlignType horizontal, AlignType vertical);
virtual void SetContainer(const D2D1_RECT_F& container);
void SetVisible(bool visible);
D2D1_RECT_F GetBounds();
bool IsVisible() const { return m_visible; }
protected:
ElementBase();
virtual void CalculateSize() { }
Alignment m_alignment;
D2D1_RECT_F m_container;
D2D1_SIZE_F m_size;
bool m_visible;
};
Ao fornecer uma classe base comum para elementos da interface do usuário, a classe UserInterface, que gerencia a interface do usuário, precisa conter apenas uma coleção de objetos ElementBase, o que simplifica o gerenciamento da interface do usuário e fornece um gerenciador de interface do usuário que é reutilizável. O Marble Maze define tipos que derivam de ElementBase que implementam comportamentos específicos do jogo. Por exemplo, HighScoreTable define o comportamento da tabela de pontuação alta. Para obter mais informações sobre esses tipos, consulte o código-fonte.
Observação
Como o XAML permite que você crie interfaces de usuário complexas com mais facilidade, como as encontradas em jogos de simulação e estratégia, considere se deseja usar XAML para definir sua interface do usuário. Para obter informações sobre como desenvolver uma interface de usuário em XAML em um jogo UWP DirectX, consulte Estender o exemplo de jogo, que se refere ao exemplo de jogo de tiro 3D do DirectX.
Carregando sombreadores
O Marble Maze usa o método BasicLoader::LoadShader para carregar um sombreador de um arquivo.
Os sombreadores são a unidade fundamental da programação de GPU em jogos de hoje. Quase todo o processamento gráfico 3D é conduzido por meio de sombreadores, seja a transformação do modelo e a iluminação da cena, ou o processamento de geometria mais complexo, desde a capa de caracteres até a tesselação. Para obter mais informações sobre o modelo de programação de sombreador, consulte HLSL.
O Marble Maze usa sombreadores de vértice e pixel. Um sombreador de vértice sempre opera em um vértice de entrada e produz um vértice como saída. Um sombreador de pixel usa valores numéricos, dados de textura, valores interpolados por vértice e outros dados para produzir uma cor de pixel como saída. Como um sombreador transforma um elemento de cada vez, o hardware gráfico que fornece vários pipelines de sombreador pode processar conjuntos de elementos em paralelo. O número de pipelines paralelos disponíveis para a GPU pode ser muito maior do que o número disponível para a CPU. Portanto, até mesmo sombreadores básicos podem melhorar muito a taxa de transferência.
O método MarbleMazeMain::LoadDeferredResources carrega um sombreador de vértice e um sombreador de pixel depois de carregar a sobreposição. As versões em tempo de design desses sombreadores são definidas em BasicVertexShader.hlsl e BasicPixelShader.hlsl, respectivamente. O Marble Maze aplica esses sombreadores à bola e ao labirinto durante a fase de renderização.
O projeto Marble Maze inclui versões .hlsl (o formato de tempo de design) e .cso (o formato de tempo de execução) dos arquivos de sombreador. Em tempo de compilação, o Visual Studio usa o fxc.exe effect-compiler para compilar seu arquivo de origem .hlsl em um sombreador binário .cso. Para obter mais informações sobre a ferramenta de compilador de efeitos, consulte Ferramenta de compilador de efeitos.
O sombreador de vértice usa o modelo fornecido, a visualização e as matrizes de projeção para transformar a geometria de entrada. Os dados de posição da geometria de entrada são transformados e emitidos duas vezes: uma vez no espaço da tela, que é necessário para a renderização, e novamente no espaço geral para permitir que o sombreador de pixel execute cálculos de iluminação. O vetor normal da superfície é transformado no espaço mundial, que também é usado pelo sombreador de pixel para iluminação. As coordenadas de textura são passadas inalteradas para o sombreador de pixel.
sPSInput main(sVSInput input)
{
sPSInput output;
float4 temp = float4(input.pos, 1.0f);
temp = mul(temp, model);
output.worldPos = temp.xyz / temp.w;
temp = mul(temp, view);
temp = mul(temp, projection);
output.pos = temp;
output.tex = input.tex;
output.norm = mul(float4(input.norm, 0.0f), model).xyz;
return output;
}
O sombreador de pixel recebe a saída do sombreador de vértice como entrada. Este sombreador realiza cálculos de iluminação para imitar um destaque de bordas suaves que paira sobre o labirinto e está alinhado com a posição da bolinha. A iluminação é mais forte para superfícies que apontam diretamente para a luz. O componente difuso se afunde a zero à medida que a superfície normal se torna perpendicular à luz, e o termo ambiente diminui à medida que os pontos normais se afastam da luz. Pontos mais próximos da bolinha (e, portanto, mais próximos do centro de destaque) são iluminados com mais força. No entanto, a iluminação é modulada para pontos abaixo da bolinha para simular uma sombra suave. Em um ambiente real, um objeto como a bolinha branca refletiria difusamente o destaque em outros objetos da cena. Isso é aproximado para as superfícies que estão em vista da metade brilhante da bolinha. Os fatores de iluminação adicionais estão em ângulo relativo e distância da bolinha. A cor de pixel resultante é uma composição da textura amostrada com o resultado dos cálculos de iluminação.
float4 main(sPSInput input) : SV_TARGET
{
float3 lightDirection = float3(0, 0, -1);
float3 ambientColor = float3(0.43, 0.31, 0.24);
float3 lightColor = 1 - ambientColor;
float spotRadius = 50;
// Basic ambient (Ka) and diffuse (Kd) lighting from above.
float3 N = normalize(input.norm);
float NdotL = dot(N, lightDirection);
float Ka = saturate(NdotL + 1);
float Kd = saturate(NdotL);
// Spotlight.
float3 vec = input.worldPos - marblePosition;
float dist2D = sqrt(dot(vec.xy, vec.xy));
Kd = Kd * saturate(spotRadius / dist2D);
// Shadowing from ball.
if (input.worldPos.z > marblePosition.z)
Kd = Kd * saturate(dist2D / (marbleRadius * 1.5));
// Diffuse reflection of light off ball.
float dist3D = sqrt(dot(vec, vec));
float3 V = normalize(vec);
Kd += saturate(dot(-V, N)) * saturate(dot(V, lightDirection))
* saturate(marbleRadius / dist3D);
// Final composite.
float4 diffuseTexture = Texture.Sample(Sampler, input.tex);
float3 color = diffuseTexture.rgb * ((ambientColor * Ka) + (lightColor * Kd));
return float4(color * lightStrength, diffuseTexture.a);
}
Aviso
O sombreador de pixel compilado contém 32 instruções aritméticas e 1 instrução de textura. Esse sombreador deve ter um bom desempenho em computadores desktop ou tablets de alto desempenho. No entanto, alguns computadores podem não ser capazes de processar esse sombreador e ainda fornecer uma taxa de quadros interativa. Considere o hardware típico do seu público-alvo e projete seus shaders para atender aos recursos desse hardware.
O método MarbleMazeMain::LoadDeferredResources usa o método BasicLoader::LoadShader para carregar os sombreadores. O exemplo a seguir carrega o sombreador de vértice. O formato de tempo de execução para esse sombreador é BasicVertexShader.cso. A variável de membro m_vertexShader é um objeto ID3D11VertexShader.
BasicLoader^ loader = ref new BasicLoader(m_deviceResources->GetD3DDevice());
D3D11_INPUT_ELEMENT_DESC layoutDesc [] =
{
{ "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 },
{ "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
m_vertexStride = 44; // must set this to match the size of layoutDesc above
Platform::String^ vertexShaderName = L"BasicVertexShader.cso";
loader->LoadShader(
vertexShaderName,
layoutDesc,
ARRAYSIZE(layoutDesc),
&m_vertexShader,
&m_inputLayout
);
A variável de membro m_inputLayout é um objeto ID3D11InputLayout. O objeto de layout de entrada encapsula o estado de entrada do estágio do montador de entrada (IA). Um trabalho do estágio IA é tornar os sombreadores mais eficientes usando valores gerados pelo sistema, também conhecidos como semântica, para processar apenas os primitivos ou vértices que ainda não foram processados.
Use o método ID3D11Device::CreateInputLayout para criar um layout de entrada a partir de uma matriz de descrições de elementos de entrada. A matriz contém um ou mais elementos de entrada; cada elemento de entrada descreve um elemento de dados de vértice de um buffer de vértice. O conjunto inteiro de descrições de elementos de entrada descreve todos os elementos de dados de vértice de todos os buffers de vértice que serão vinculados ao estágio IA.
layoutDesc no trecho de código acima mostra a descrição do layout que o Marble Maze usa. A descrição do layout descreve um buffer de vértice que contém quatro elementos de dados de vértice. As partes importantes de cada entrada na matriz são o nome semântico, o formato de dados e o deslocamento de bytes. Por exemplo, o elemento POSITION especifica a posição do vértice no espaço do objeto. Ele começa no deslocamento de bytes 0 e contém três componentes de ponto flutuante (para um total de 12 bytes). O elemento NORMAL especifica o vetor normal. Ele começa no deslocamento de bytes 12 porque aparece diretamente após POSITION no layout, que requer 12 bytes. O elemento NORMAL contém um inteiro não assinado de quatro componentes e 32 bits.
Compare o layout de entrada com a estrutura sVSInput definida pelo sombreador de vértice, conforme mostrado no exemplo a seguir. A estrutura sVSInput define os elementos POSITION, NORMAL e TEXCOORD0. O tempo de execução do DirectX mapeia cada elemento no layout para a estrutura de entrada definida pelo sombreador.
struct sVSInput
{
float3 pos : POSITION;
float3 norm : NORMAL;
float2 tex : TEXCOORD0;
};
struct sPSInput
{
float4 pos : SV_POSITION;
float3 norm : NORMAL;
float2 tex : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
sPSInput main(sVSInput input)
{
sPSInput output;
float4 temp = float4(input.pos, 1.0f);
temp = mul(temp, model);
output.worldPos = temp.xyz / temp.w;
temp = mul(temp, view);
temp = mul(temp, projection);
output.pos = temp;
output.tex = input.tex;
output.norm = mul(float4(input.norm, 0.0f), model).xyz;
return output;
}
O documento Semântica descreve cada uma das semânticas disponíveis com mais detalhes.
Observação
Em um layout, você pode especificar componentes adicionais que não são usados para permitir que vários sombreadores compartilhem o mesmo layout. Por exemplo, o elemento TANGENT não é usado pelo sombreador. Você pode usar o elemento TANGENT se quiser experimentar técnicas como o mapeamento normal. Usando o mapeamento normal, também conhecido como mapeamento de colisões, você pode criar o efeito de solavancos nas superfícies dos objetos. Para obter mais informações sobre mapeamento de colisão, consulte Mapeamento de colisão (Direct3D 9).
Para obter mais informações sobre o estágio de montagem de entrada, consulte Estágio de montagem de entrada e Introdução ao estágio de montagem de entrada.
O processo de usar os sombreadores de vértice e pixel para renderizar a cena é descrito na seção Renderizando a cena mais adiante neste documento.
Criando o buffer constante
O buffer Direct3D agrupa uma coleção de dados. Um buffer constante é um tipo de buffer que você pode usar para passar dados para sombreadores. O Marble Maze usa um buffer constante para manter a exibição de modelo (ou geral) e as matrizes de projeção para o objeto de cena ativo.
O exemplo a seguir mostra como o método MarbleMazeMain::LoadDeferredResources cria um buffer constante que posteriormente manterá dados de matriz. O exemplo cria uma estrutura D3D11_BUFFER_DESC que usa o sinalizador D3D11_BIND_CONSTANT_BUFFER para especificar o uso como um buffer constante. Este exemplo passa essa estrutura para o método ID3D11Device::CreateBuffer. A variável m_constantBuffer é um objeto ID3D11Buffer.
// Create the constant buffer for updating model and camera data.
D3D11_BUFFER_DESC constantBufferDesc = {0};
// Multiple of 16 bytes
constantBufferDesc.ByteWidth = ((sizeof(ConstantBuffer) + 15) / 16) * 16;
constantBufferDesc.Usage = D3D11_USAGE_DEFAULT;
constantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags = 0;
constantBufferDesc.MiscFlags = 0;
// This will not be used as a structured buffer, so this parameter is ignored.
constantBufferDesc.StructureByteStride = 0;
DX::ThrowIfFailed(
m_deviceResources->GetD3DDevice()->CreateBuffer(
&constantBufferDesc,
nullptr, // leave the buffer uninitialized
&m_constantBuffer
)
);
O método MarbleMazeMain::Update atualiza posteriormente ConstantBuffer objetos, um para o labirinto e outro para a bolinha. O método MarbleMazeMain::Render vincula cada objeto ConstantBuffer ao buffer constante antes que cada objeto seja renderizado. O exemplo a seguir mostra a estrutura ConstantBuffer, que está em MarbleMazeMain.h.
// Describes the constant buffer that draws the meshes.
struct ConstantBuffer
{
XMFLOAT4X4 model;
XMFLOAT4X4 view;
XMFLOAT4X4 projection;
XMFLOAT3 marblePosition;
float marbleRadius;
float lightStrength;
};
Para entender melhor como os buffers constantes são mapeados para o código do sombreador, compare a estrutura ConstantBuffer em MarbleMazeMain.h com o buffer constante ConstantBuffer definido pelo sombreador de vértice em BasicVertexShader.hlsl:
cbuffer ConstantBuffer : register(b0)
{
matrix model;
matrix view;
matrix projection;
float3 marblePosition;
float marbleRadius;
float lightStrength;
};
O layout da estrutura ConstantBuffer corresponde ao objeto cbuffer. A variável cbuffer especifica o registro b0, o que significa que os dados do buffer constante são armazenados no registro 0. O método MarbleMazeMain::Render especifica o registro 0 quando ele ativa o buffer constante. Esse processo é descrito com mais detalhes mais adiante neste documento.
Para obter mais informações sobre buffers constantes, consulte Introdução aos buffers no Direct3D 11. Para obter mais informações sobre a palavra-chave do registro, consulte registro.
Carregando malhas
O Marble Maze usa o SDK-Mesh como o formato de tempo de execução porque esse formato fornece uma maneira básica de carregar dados de malha para aplicativos de exemplo. Para uso em produção, você deve usar um formato de malha que atenda aos requisitos específicos do seu jogo.
O método MarbleMazeMain::LoadDeferredResources carrega dados de malha depois de carregar os sombreadores de vértice e pixel. Uma malha é uma coleção de dados de vértice que geralmente inclui informações como posições, dados normais, cores, materiais e coordenadas de textura. As malhas são normalmente criadas em software de criação 3D e mantidas em arquivos separados do código do aplicativo. A bolinha e o labirinto são dois exemplos de malhas que o jogo usa.
O Marble Maze usa a classe SDKMesh para gerenciar malhas. Essa classe é declarada em SDKMesh.h. SDKMesh fornece métodos para carregar, renderizar e destruir dados de malha.
Importante
O Marble Maze usa o formato SDK-Mesh e fornece a classe SDKMesh apenas para ilustração. Embora o formato SDK-Mesh seja útil para o aprendizado e para a criação de protótipos, é um formato muito básico que pode não atender aos requisitos da maioria dos desenvolvimentos de jogos. Recomendamos que você use um formato de malha que atenda aos requisitos específicos do seu jogo.
O exemplo a seguir mostra como o método MarbleMazeMain::LoadDeferredResources usa o método SDKMesh::Create para carregar dados de malha para o labirinto e para a bolinha.
// Load the meshes.
DX::ThrowIfFailed(
m_mazeMesh.Create(
m_deviceResources->GetD3DDevice(),
L"Media\\Models\\maze1.sdkmesh",
false
)
);
DX::ThrowIfFailed(
m_marbleMesh.Create(
m_deviceResources->GetD3DDevice(),
L"Media\\Models\\marble2.sdkmesh",
false
)
);
Carregando dados de colisão
Embora esta seção não se concentre em como o Marble Maze implementa a simulação física entre a bolinha e o labirinto, observe que a geometria de malha para o sistema de física é lida quando as malhas são carregadas.
// Extract mesh geometry for the physics system.
DX::ThrowIfFailed(
ExtractTrianglesFromMesh(
m_mazeMesh,
"Mesh_walls",
m_collision.m_wallTriList
)
);
DX::ThrowIfFailed(
ExtractTrianglesFromMesh(
m_mazeMesh,
"Mesh_Floor",
m_collision.m_groundTriList
)
);
DX::ThrowIfFailed(
ExtractTrianglesFromMesh(
m_mazeMesh,
"Mesh_floorSides",
m_collision.m_floorTriList
)
);
m_physics.SetCollision(&m_collision);
float radius = m_marbleMesh.GetMeshBoundingBoxExtents(0).x / 2;
m_physics.SetRadius(radius);
A maneira como você carrega dados de colisão depende em grande parte do formato de tempo de execução que você usa. Para obter mais informações sobre como o Marble Maze carrega a geometria de colisão de um arquivo SDK-Mesh, consulte o método MarbleMazeMain::ExtractTrianglesFromMesh no código-fonte.
Atualizando o estado do jogo
O Marble Maze separa a lógica do jogo da lógica de renderização, primeiro atualizando todos os objetos de cena antes de renderizá-los.
Estrutura do aplicativo Marble Maze descreve o loop principal do jogo. A atualização da cena, que faz parte do loop do jogo, acontece depois que os eventos e a entrada do Windows são processados e antes que a cena seja renderizada. O método MarbleMazeMain::Update manipula a atualização da interface do usuário e do jogo.
Atualizando a interface do usuário
O método MarbleMazeMain::Update chama o método UserInterface::Update para atualizar o estado da interface do usuário.
UserInterface::GetInstance().Update(
static_cast<float>(m_timer.GetTotalSeconds()),
static_cast<float>(m_timer.GetElapsedSeconds()));
O método UserInterface::Update atualiza cada elemento na coleção de interface do usuário.
void UserInterface::Update(float timeTotal, float timeDelta)
{
for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
{
(*iter)->Update(timeTotal, timeDelta);
}
}
As classes que derivam de ElementBase (definidas em UserInterface.h) implementam o método Update para executar comportamentos específicos. Por exemplo, o método StopwatchTimer::Update atualiza o tempo decorrido pela quantidade fornecida e atualiza o texto exibido posteriormente.
void StopwatchTimer::Update(float timeTotal, float timeDelta)
{
if (m_active)
{
m_elapsedTime += timeDelta;
WCHAR buffer[16];
GetFormattedTime(buffer);
SetText(buffer);
}
TextElement::Update(timeTotal, timeDelta);
}
Atualizando a cena
O método MarbleMazeMain::Update atualiza o jogo com base no estado atual da máquina de estado (o GameState, armazenado em m_gameState). Quando o jogo está no estado ativo (GameState::InGameActive), o Marble Maze atualiza a câmera para seguir a bolinha, atualiza a parte da matriz de exibição dos buffers constantes e atualiza a simulação de física.
O exemplo a seguir mostra como o método MarbleMazeMain::Update atualiza a posição da câmera. O Marble Maze usa a variável m_resetCamera para sinalizar que a câmera deve ser redefinida para estar localizada diretamente acima da bolinha. A câmera é redefinida quando o jogo começa ou a bolinha cai pelo labirinto. Quando o menu principal ou a tela de exibição de pontuação alta está ativa, a câmera é definida em um local constante. Caso contrário, o Marble Maze usa o parâmetro timeDelta para interpolar a posição da câmera entre suas posições atual e de destino. A posição alvo é ligeiramente acima e à frente da bolinha. O uso do tempo de quadro decorrido permite que a câmera siga gradualmente, ou persiga a bolinha.
static float eyeDistance = 200.0f;
static XMFLOAT3A eyePosition = XMFLOAT3A(0, 0, 0);
// Gradually move the camera above the marble.
XMFLOAT3A targetEyePosition;
XMStoreFloat3A(
&targetEyePosition,
XMLoadFloat3A(&marblePosition) - (XMLoadFloat3A(&g) * eyeDistance));
if (m_resetCamera)
{
eyePosition = targetEyePosition;
m_resetCamera = false;
}
else
{
XMStoreFloat3A(
&eyePosition,
XMLoadFloat3A(&eyePosition)
+ ((XMLoadFloat3A(&targetEyePosition) - XMLoadFloat3A(&eyePosition))
* min(1, static_cast<float>(m_timer.GetElapsedSeconds()) * 8)
)
);
}
// Look at the marble.
if ((m_gameState == GameState::MainMenu) || (m_gameState == GameState::HighScoreDisplay))
{
// Override camera position for menus.
XMStoreFloat3A(
&eyePosition,
XMLoadFloat3A(&marblePosition) + XMVectorSet(75.0f, -150.0f, -75.0f, 0.0f));
m_camera->SetViewParameters(
eyePosition,
marblePosition,
XMFLOAT3(0.0f, 0.0f, -1.0f));
}
else
{
m_camera->SetViewParameters(eyePosition, marblePosition, XMFLOAT3(0.0f, 1.0f, 0.0f));
}
O exemplo a seguir mostra como o método MarbleMazeMain::Update atualiza os buffers constantes para a bolinha e o labirinto. O modelo do labirinto, ou matriz de mundo, permanece sempre a matriz de identidade. Com exceção da diagonal principal, cujos elementos são todos uns, a matriz de identidade é uma matriz quadrada composta por zeros. A matriz do modelo da bolinha é baseada em sua matriz de posição vezes sua matriz de rotação.
// Update the model matrices based on the simulation.
XMStoreFloat4x4(&m_mazeConstantBufferData.model, XMMatrixIdentity());
XMStoreFloat4x4(
&m_marbleConstantBufferData.model,
XMMatrixTranspose(
XMMatrixMultiply(
marbleRotationMatrix,
XMMatrixTranslationFromVector(XMLoadFloat3A(&marblePosition))
)
)
);
// Update the view matrix based on the camera.
XMFLOAT4X4 view;
m_camera->GetViewMatrix(&view);
m_mazeConstantBufferData.view = view;
m_marbleConstantBufferData.view = view;
Para obter informações sobre como o método MarbleMazeMain::Update lê a entrada do usuário e simula o movimento da bolinha, consulte Adicionando entrada e interatividade ao exemplo do Marble Maze.
Renderizando a cena
Quando uma cena é renderizada, essas etapas geralmente são incluídas.
- Defina o buffer de estêncil de profundidade de destino de renderização atual.
- Limpe as exibições de renderização e estêncil.
- Prepare os sombreadores de vértice e pixel para desenho.
- Renderize os objetos 3D na cena.
- Renderize qualquer objeto 2D que você deseja que apareça na frente da cena.
- Apresente a imagem renderizada ao monitor.
O método MarbleMazeMain::Render vincula as exibições de estêncil de profundidade e destino de renderização, limpa essas exibições, desenha a cena e desenha a sobreposição.
Preparando os destinos de renderização
Antes de renderizar sua cena, você deve definir o buffer de estêncil de profundidade de destino de renderização atual. Se não for garantido que sua cena atraia todos os pixels da tela, limpe também as visualizações de renderização e estêncil. O Marble Maze limpa as exibições de renderização e estêncil em cada quadro para garantir que não haja artefatos visíveis do quadro anterior.
O exemplo a seguir mostra como o método MarbleMazeMain::Render chama o método ID3D11DeviceContext::OMSetRenderTargets para definir o destino de renderização e o buffer de estêncil de profundidade como os atuais.
auto context = m_deviceResources->GetD3DDeviceContext();
// Reset the viewport to target the whole screen.
auto viewport = m_deviceResources->GetScreenViewport();
context->RSSetViewports(1, &viewport);
// Reset render targets to the screen.
ID3D11RenderTargetView *const targets[1] =
{ m_deviceResources->GetBackBufferRenderTargetView() };
context->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());
// Clear the back buffer and depth stencil view.
context->ClearRenderTargetView(
m_deviceResources->GetBackBufferRenderTargetView(),
DirectX::Colors::Black);
context->ClearDepthStencilView(
m_deviceResources->GetDepthStencilView(),
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
1.0f,
0);
As interfaces ID3D11RenderTargetView e ID3D11DepthStencilView oferecem suporte ao mecanismo de exibição de textura fornecido pelo Direct3D 10 e posterior. Para obter mais informações sobre modos de exibição de textura, consulte Modos de exibição de textura (Direct3D 10). O método OMSetRenderTargets prepara o estágio de fusão de saída do pipeline Direct3D. Para obter mais informações sobre o estágio de fusão de resultados, consulte Estágio de fusão de resultados.
Preparando os sombreadores de vértice e pixel
Antes de renderizar os objetos de cena, execute as seguintes etapas para preparar os sombreadores de vértice e pixel para desenho:
- Defina o layout de entrada do sombreador como o layout atual.
- Defina os sombreadores de vértice e pixel como os sombreadores atuais.
- Atualize todos os buffers constantes com dados que você precisa passar para os sombreadores.
Importante
O Marble Maze usa um par de sombreadores de vértice e pixel para todos os objetos 3D. Se o jogo usar mais de um par de sombreadores, você deverá executar essas etapas sempre que desenhar objetos que usam sombreadores diferentes. Para reduzir a sobrecarga associada à alteração do estado do sombreador, recomendamos que você agrupe as chamadas de renderização para todos os objetos que usam os mesmos sombreadores.
A seção Carregando sombreadores neste documento descreve como o layout de entrada é criado quando o sombreador de vértice é criado. O exemplo a seguir mostra como o método MarbleMazeMain::Render usa o método ID3D11DeviceContext::IASetInputLayout para definir esse layout como o layout atual.
m_deviceResources->GetD3DDeviceContext()->IASetInputLayout(m_inputLayout.Get());
O exemplo a seguir mostra como o método MarbleMazeMain::Render usa os métodos ID3D11DeviceContext::VSSetShader e ID3D11DeviceContext::P SSetShader para definir os sombreadores de vértice e pixel como os sombreadores atuais, respectivamente.
// Set the vertex shader stage state.
m_deviceResources->GetD3DDeviceContext()->VSSetShader(
m_vertexShader.Get(), // use this vertex shader
nullptr, // don't use shader linkage
0); // don't use shader linkage
m_deviceResources->GetD3DDeviceContext()->PSSetShader(
m_pixelShader.Get(), // use this pixel shader
nullptr, // don't use shader linkage
0); // don't use shader linkage
m_deviceResources->GetD3DDeviceContext()->PSSetSamplers(
0, // starting at the first sampler slot
1, // set one sampler binding
m_sampler.GetAddressOf()); // to use this sampler
Depois que MarbleMazeMain::Render define os sombreadores e seu layout de entrada, ele usa o método ID3D11DeviceContext::UpdateSubresource para atualizar o buffer constante com as matrizes de modelo, exibição e projeção para o labirinto. O método UpdateSubresource copia os dados da matriz da memória da CPU para a memória da GPU. Lembre-se de que os componentes de modelo e exibição da estrutura ConstantBuffer são atualizados no método MarbleMazeMain::Update. O método MarbleMazeMain::Render chama os métodos ID3D11DeviceContext::VSSetConstantBuffers e ID3D11DeviceContext::PSSetConstantBuffers para definir esse buffer constante como o atual.
// Update the constant buffer with the new data.
m_deviceResources->GetD3DDeviceContext()->UpdateSubresource(
m_constantBuffer.Get(),
0,
nullptr,
&m_mazeConstantBufferData,
0,
0);
m_deviceResources->GetD3DDeviceContext()->VSSetConstantBuffers(
0, // starting at the first constant buffer slot
1, // set one constant buffer binding
m_constantBuffer.GetAddressOf()); // to use this buffer
m_deviceResources->GetD3DDeviceContext()->PSSetConstantBuffers(
0, // starting at the first constant buffer slot
1, // set one constant buffer binding
m_constantBuffer.GetAddressOf()); // to use this buffer
O método MarbleMazeMain::Render executa etapas semelhantes para preparar a bolinha a ser renderizada.
Renderizando o labirinto e a bolinha
Depois de ativar os sombreadores atuais, você pode desenhar seus objetos de cena. O método MarbleMazeMain::Render chama o método SDKMesh::Render para renderizar a malha do labirinto.
m_mazeMesh.Render(
m_deviceResources->GetD3DDeviceContext(),
0,
INVALID_SAMPLER_SLOT,
INVALID_SAMPLER_SLOT);
O método MarbleMazeMain::Render executa etapas semelhantes para renderizar a bolinha.
Como mencionado anteriormente neste documento, a classe SDKMesh é fornecida para fins de demonstração, mas não a recomendamos para uso em um jogo com qualidade de produção. No entanto, observe que o método SDKMesh::RenderMesh, que é chamado por SDKMesh::Render, usa os métodos ID3D11DeviceContext::IASetVertexBuffers e ID3D11DeviceContext::IASetIndexBuffer para definir os buffers de vértice e índice atuais que definem a malha e o método ID3D11DeviceContext::D rawIndexed para desenhar os buffers. Para obter mais informações sobre como trabalhar com buffers de vértice e índice, consulte Introdução aos buffers no Direct3D 11.
Desenhando a interface do usuário e a sobreposição
Depois de desenhar objetos de cena 3D, o Marble Maze desenha os elementos da interface do usuário 2D que aparecem na frente da cena.
O método MarbleMazeMain::Render termina desenhando a interface do usuário e a sobreposição.
// Draw the user interface and the overlay.
UserInterface::GetInstance().Render(m_deviceResources->GetOrientationTransform2D());
m_deviceResources->GetD3DDeviceContext()->BeginEventInt(L"Render Overlay", 0);
m_sampleOverlay->Render();
m_deviceResources->GetD3DDeviceContext()->EndEvent();
O método UserInterface::Render usa um objeto ID2D1DeviceContext para desenhar os elementos da interface do usuário. Esse método define o estado de desenho, desenha todos os elementos ativos da interface do usuário e restaura o estado de desenho anterior.
void UserInterface::Render(D2D1::Matrix3x2F orientation2D)
{
m_d2dContext->SaveDrawingState(m_stateBlock.Get());
m_d2dContext->BeginDraw();
m_d2dContext->SetTransform(orientation2D);
m_d2dContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
{
if ((*iter)->IsVisible())
(*iter)->Render();
}
// We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device
// is lost. It will be handled during the next call to Present.
HRESULT hr = m_d2dContext->EndDraw();
if (hr != D2DERR_RECREATE_TARGET)
{
DX::ThrowIfFailed(hr);
}
m_d2dContext->RestoreDrawingState(m_stateBlock.Get());
}
O método SampleOverlay::Render usa uma técnica semelhante para desenhar o bitmap de sobreposição.
Apresentando a cena
Depois de desenhar todos os objetos de cena 2D e 3D, o Marble Maze apresenta a imagem renderizada ao monitor. Ele sincroniza o desenho com o espaço em branco vertical para garantir que o tempo não seja gasto desenhando quadros que nunca serão realmente mostrados na tela. O Marble Maze também lida com mudanças de dispositivo quando apresenta a cena.
Depois que o método MarbleMazeMain::Render retorna, o loop do jogo chama o método DX::D eviceResources::P resent para enviar a imagem renderizada para o monitor ou exibição. O método DX::D eviceResources::P resent chama IDXGISwapChain::P resent para executar a operação atual, conforme mostrado no exemplo a seguir:
// 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);
Neste exemplo, m_swapChain é um objeto IDXGISwapChain1 A inicialização desse objeto é descrita na seção Inicializando Direct3D e Direct2D neste documento.
O primeiro parâmetro para IDXGISwapChain::Present, SyncInterval, especifica o número de espaços em branco verticais a serem aguardados antes de apresentar o quadro. O Marble Maze especifica 1 para que ele aguarde até o próximo espaço em branco vertical.
O método IDXGISwapChain::Present retorna um código de erro que indica que o dispositivo foi removido ou falhou. Nesse caso, o Marble Maze reinicializa o dispositivo.
// 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)
{
HandleDeviceLost();
}
else
{
DX::ThrowIfFailed(hr);
}
Próximas etapas
Leia Adicionando entrada e interatividade ao exemplo do Marble Maze para obter informações sobre algumas das principais práticas a serem consideradas ao trabalhar com dispositivos de entrada. Este documento discute como o Marble Maze oferece suporte a toque, acelerômetro, controladores de jogos e entrada de mouse.
Tópicos relacionados
- Adicionando entrada e interatividade à amostra do Marble Maze
- Estrutura de aplicação do Marble Maze
- Como desenvolver o Marble Maze, um jogo UWP em C++ no DirectX