Tutorial DirectX (Parte I): Desenhando o primeiro triângulo
Índice
- Introdução
- API DirectX
- A história do DirectX
- Direct3D
- Device
- Device Context
- Swap Chain
- Input Layout
- Topologia Primitiva
- HLSL
- Pipeline Gráfica
- Gráficos 3D
- Sequência de ações em MQL
- Prática
- Visão geral da classe
- Matriz de vértices
- Inicialização
- Criando uma Tela
- Inicialização do DirectX
- Exibição da imagem
- Liberando os recursos
- Sombreadores
- OnStart
- Conclusão
- Referências e Links
Introdução
Um rito de iniciação, ou rito de inicialização, é o que vem à mente quando você entende que precisa escrever tanto código e preencher enormes estruturas em C++, para desenhar até mesmo um triângulo primitivo usando o DirectX. Sem falar em coisas mais complexas, como texturas, matrizes de transformação e sombras. Felizmente, a MetaQuotes cuidou disso: eles esconderam toda a rotina, deixando apenas as funções mais necessárias. No entanto, há um efeito colateral disso: qualquer pessoa que não esteja familiarizada com o DirectX não poderá ver a imagem inteira e entender o por que e como tudo está acontecendo. Ainda há muito código a ser escrito em MQL.
Sem entender o que está por detrás, o DirectX é desconcertante: "Por que ele é tão difícil e confuso, não pode ser simplificado?" E esta é apenas a primeira etapa. Além disso, você estuda a linguagem de sombreamento HLSL e as peculiaridades da programação da placa de vídeo. Para evitar todas essas confusões, eu sugiro considerar a estrutura interna do DirectX, embora sem entrar em detalhes. Em seguida, nós escreveremos um pequeno script em MQL que exibe um triângulo na tela.
API DirectX
A história do DirectX
O DirectX é um conjunto de APIs (Application Programming Interface) para trabalhar com multimídia e vídeo nas plataformas da Microsoft. Ele foi desenvolvido principalmente para criar jogos, mas com o tempo os desenvolvedores começaram a usá-lo em softwares de engenharia e matemática. O DirectX permite trabalhar com gráficos, som, entradas e redes sem a necessidade de acessar funções de baixo nível. A API surgiu como uma alternativa ao OpenGL, que é multiplataforma. Ao criar o Windows 95, a Microsoft implementou mudanças substanciais, que dificultaram o ambiente de desenvolvimento de aplicativos e jogos e, portanto, poderiam afetar a popularidade desse sistema operacional. O DirectX foi criado como uma solução para fazer com que mais programadores desenvolvessem jogos para o Windows. O desenvolvimento do DirectX foi iniciado por Craig Eisler, Alex St. John e Eric Engstrom.
- Setembro de 1995. Primeira versão. Era uma versão bastante primitiva, que funcionava como um complemento para a API do Windows. Ela não chamou muita atenção. Os desenvolvedores estavam usando principalmente o DOS, que se comparado com o novo sistema operacional, ele tinha maiores requisitos de sistema. Além disso, o OpenGL já existia naquela época. Não ficou claro se a Microsoft continuaria a oferecer suporte ao DirectX.
- Junho de 1996. O lançamento da versão 2.
- Setembro de 1996. Versão 3.
- Agosto de 1997. Em vez da quarta versão, a Microsoft lançou a versão 5. Ficou mais fácil escrever códigos com esta versão e os programadores começaram a prestar mais atenção nela.
- Agosto de 1998. Versão 6. O trabalho foi mais simplificado.
- Setembro de 1999. Versão 7. Tornou-se possível criar buffers de vértice na memória de vídeo, sendo uma grande vantagem sobre o OpenGL.
- Novembro de 2000. Versão 8. Momento crucial. Antes deste momento, o DirectX estava tentando recuperar o atraso, mas a versão oito ultrapassou a indústria. A Microsoft começou a cooperar com os fabricantes de placas gráficas. Apareceram os vértices e sombreadores de pixel. O desenvolvimento exigia apenas um computador pessoal, ao contrário do OpenGL que exigia uma estação de trabalho.
- Dezembro de 2002. Versão 9. O DirectX tornou-se o padrão da indústria. Apareceu a linguagem sombreadora HLSL. Provavelmente esta foi a versão mais duradoura do DirectX. Como o soquete 775...
- Novembro de 2006. Versão 10. Ao contrário da versão 9, esta tinha uma ligação com o sistema operacional Vista, que não era popular. Esses fatores tiveram um efeito negativo no sucesso da versão 10. A Microsoft adicionou um sombreador de geometria.
- Outubro de 2009. Versão 11. Adição do mosaico, sombreador de computação, aprimorado o trabalho com os processadores multi-núcleo.
- Julho de 2015. Versão 12. API de baixo nível. A versão proporcionou uma compatibilidade ainda melhor com os processadores multi-núcleo, a possibilidade de combinar os recursos de várias placas de vídeo de diferentes fornecedores, ray tracing.
Direct3D
O Direct3D é um dos diversos componentes da API DirectX; ele é responsável pelos gráficos e é um intermediário entre as aplicações e o driver da placa gráfica. O Direct3D é baseado em COM (Component Object Model). COM é um padrão de interface binária de aplicação (ABI) introduzido pela Microsoft em 1993. Ele é usado para criar objetos em comunicação entre processos (IPC) em várias linguagens de programação. O COM surgiu como uma solução com o objetivo de fornecer uma maneira independente da linguagem para implementar objetos que pudessem ser usados fora de seu ambiente de criação. COM permite que objetos sejam reutilizados sem o conhecimento de sua implementação interna, pois fornecem interfaces bem definidas e separadas da implementação. Os objetos COM são responsáveis por sua própria criação e destruição usando contagem de referência.
Interfaces
Device
Tudo no Direct3D começa com um Device. Ele é usado para criar recursos (buffers, textura, sombreadores, objetos de estado) e a enumeração de recursos dos adaptadores gráficos. O Device é um adaptador virtual localizado no sistema do usuário. O adaptador pode ser uma placa de vídeo real ou uma emulação de software. Os dispositivos de hardware são usados com mais frequência porque fornecem o mais alto desempenho. O dispositivo fornece uma interface unificada para todos esses adaptadores e os usa para a renderização gráfica de uma ou mais saídas.
Device
Device Context
O Device Context (contexto do dispositivo) é responsável por qualquer coisa relacionada à renderização. Isso inclui a configuração do pipeline e a criação de comandos para a renderização. O Device Context apareceu na décima primeira versão do DirectX — antes, a renderização era implementada pelo Device. Existem dois tipos de contexto: Immediate Context (Contexto Imediato) e Deferred Context (Contexto Diferido).
O contexto imediato fornece acesso aos dados na placa de vídeo e a capacidade de executar imediatamente uma lista de comandos no dispositivo. Cada Device tem apenas um Immediate Context. Apenas uma thread pode acessá-lo por vez. A sincronização deve ser usada para permitir o acesso a várias threads.
O Deferred Context adiciona comandos à lista de comandos para serem executados posteriormente no Immediate Context. Assim, todos os comandos eventualmente passam pelo Immediate Context. O Deferred Context envolve alguma sobrecarga, portanto, os benefícios de usá-lo são visíveis apenas na paralelização de tarefas com uso intensivo de recursos. Você pode criar vários contextos adiados e acessar cada um de uma thread separada. Mas para acessar o mesmo Deferred Context de várias threads, você precisa de sincronização, assim como no Immediate Context.
Swap Chain
O Swap Chain (cadeia de troca) é projetado para criar um ou mais buffers de retorno. Esses buffers armazenam imagens renderizadas até que elas sejam exibidas na tela. Os buffers frontais e traseiros funcionam da seguinte forma. O buffer frontal é o que você está vendo na tela. O buffer traseiro é a imagem que está sendo renderizada. Em seguida, os buffers são trocados: o frontal se torna traseiro e o de trás vai para a frente. E todo o processo é repetido diversas vezes. Assim, nós sempre vemos a imagem, enquanto a próxima está sendo renderizada "nos bastidores".
Swap Chain
Device, Device Context e Swap Chain são os principais componentes necessários para a renderização de uma imagem.
Input Layout
O Input Layout informa ao pipeline sobre a estrutura do buffer de vértice. Nós só precisamos das coordenadas para os nossos propósitos, e é por isso que nós podemos simplesmente passar o array de vértices do tipo float4, sem usar uma estrutura especial. float4 é uma estrutura que consiste em quatro variáveis float.
struct float4 { float x; float y; float z; float w; };
Por exemplo, considere uma estrutura de vértices mais complexa que consiste numa coordenada e duas cores:
struct Vertex
{
float4 Pos;
float4 Color0;
float4 Color1;
};
O Input Layout em MQL para esta estrutura ficará assim:
DXVertexLayout layout[3] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT}, {"COLOR", 0, DX_FORMAT_R32G32B32A32_FLOAT}, {"COLOR", 1, DX_FORMAT_R32G32B32A32_FLOAT}};
Cada elemento do array 'layout' descreve o elemento correspondente da estrutura Vertex.
- O primeiro elemento da estrutura DXVertexLayout é o nome semântico. Ele é usado para mapear os elementos da estrutura Vertex com os elementos da estrutura no sombreador de vértices. "POSITION" significa que o valor é responsável pelas coordenadas, "COLOR" é usado para a cor.
- O segundo elemento é o índice semântico. Se precisarmos passar vários parâmetros do mesmo tipo, por exemplo, dois valores de cor, o primeiro é passado com índice 0 e o segundo com índice 1.
- O último elemento descreve o tipo que o valor é representado na estrutura Vertex. DX_FORMAT_R32G32B32A32_FLOAT significa literalmente que é uma cor RGBA representada por um valor de ponto flutuante de 32 bits para cada componente. Isso pode ser confuso. Esse tipo pode ser usado para passar coordenadas — ele fornece informações sobre quatro valores de ponto flutuante de 32 bits, assim como float4 na estrutura Vertex.
Topologia Primitiva
O buffer de vértices armazena informações sobre os pontos, mas nós não sabemos como eles estão localizados em relação uns aos outros na primitiva. É para isso que a Topologia Primitiva é usada. PointList significa que o buffer armazena pontos individuais. Line Strip representa o buffer como pontos conectados formando uma polilinha. Cada dois pontos na Line List descrevem uma única linha. Triangle Strip e Triangle List definem a ordem dos pontos para os triângulos, semelhantes às linhas.
Topologia
HLSL
High Level Shading Language (HLSL) é uma linguagem semelhante a C para escrever sombreadores. Sombreadores, por sua vez, são programas projetados para rodar numa placa gráfica. A programação em todas as linguagens GPGPU é muito semelhante e possui um recurso específico relacionado ao design das placas gráficas. Se você tiver experiência com OpenCL, Cuda ou OpenGL, entenderá o HLSL muito rapidamente. Mas se você criou apenas programas para CPUs, pode ser difícil mudar para um novo paradigma. Muitas vezes, os métodos de otimização tradicionalmente usados para o processador não funcionarão. Como exemplo, seria correto que o processador usasse a instrução 'if' para evitar cálculos desnecessários ou para selecionar o algoritmo ideal. Mas na GPU, pelo contrário, isso pode aumentar o tempo de execução do programa. Para tirar o máximo, você pode ter que contar o número de registros envolvidos. Os três principais princípios de alto desempenho na programação de placas gráficas são: paralelismo, taxa de transferência e ocupação.
Pipeline Gráfica
O pipeline foi projetado para converter uma cena 3D numa representação de exibição 2D. O pipeline é um reflexo da estrutura interna da placa de vídeo. O diagrama abaixo mostra como o fluxo de dados flui da entrada do pipeline para a sua saída em todos os estágios. O oval mostra os estágios que são programados usando a linguagem HLSL — sombreadores, e o retângulo mostra os estágios fixos. Alguns deles são opcionais e podem ser facilmente ignorados.
Pipeline Gráfica
-
O estágio do Input Assembler (Montador de Entrada) recebe os dados dos buffers do vértice e índice e os prepara para o sombreador de vértice.
- O estágio Vertex Shader (Sombreador de Vértice) realiza as operações com os vértices. Estágio programável. Obrigatório no pipeline.
- O estágio Hull Shader é responsável pelo nível de tesselação. Estágio programável. Opcional.
- O estágio Tessellator cria primitivas menores. Estágio fixo. Opcional.
- O estágio Domain Shader calcula os valores finais dos vértices após a tesselação. Estágio programável. Opcional.
- O estágio Geometry Shader (Sombreador Geometry) aplica várias transformações nas primitivas (pontos, linhas, triângulos). Estágio programável. Opcional.
- O estágio Stream Output (Saída de Fluxo) transfere os dados para a memória da GPU a partir da qual eles podem ser enviados de volta ao pipeline. Estágio fixo. Opcional.
- O estágio Rasterizer (Rasterizador) corta tudo o que não cai no escopo, prepara os dados para o Sombreador de Pixel. Estágio fixo.
-
O estágio Pixel Shader (Sombreador de Pixel) executa as operações de pixel. Estágio programável. Obrigatório no pipeline.
- O estágio Output Merger (Fusão de Saída) forma a imagem final. Estágio fixo.
Outro sombreador que vale a pena mencionar é o Compute Shader (DirectCompute), que é um pipeline separado. Este sombreador é projetado para cálculos de uso geral, semelhante ao OpenCL e Cuda. Estágio programável. Opcional.
A implementação do DirectX da MetaQuotes não inclui o DirectCompute e o estágio de tesselação. Assim, nós temos apenas três sombreadores: vértice, geometria e pixel.
Gráficos 3D
Primitivas
A renderização de primitivas é o objetivo principal da API gráfica. As placas de vídeo modernas são adaptadas para a renderização rápida de um grande número de triângulos. Na verdade, no atual estágio de desenvolvimento da computação gráfica, a maneira mais eficaz de desenhar objetos 3D é criar uma superfície a partir de polígonos. Uma superfície pode ser descrita especificando apenas três pontos. O software de modelagem 3D geralmente usa retângulos, mas a placa gráfica ainda forçará os polígonos em triângulos.
Malha de triângulos
Vértices
Três vértices devem ser especificados para a renderização de um triângulo no Direct3D. Pode parecer que um vértice é a posição de um ponto no espaço, mas no Direct3D é algo mais que isso. Além da posição do vértice, nós podemos passar as cores, coordenadas de textura e normais. Geralmente, as transformações de matrizes são geralmente usadas para normalizar as coordenadas. Para não complicar neste momento, leve em consideração o fato de que na etapa de rasterização as coordenadas dos vértices ao longo dos eixos X e Y devem estar dentro de [-1; 1]; ao longo de Z de 0 a 1.
Cor
A cor na computação gráfica tem três componentes: vermelho, verde e azul. Isso está relacionado às características estruturais do olho humano. Os pixels de exibição também consistem em três subpixels dessas cores. MQL tem a função ColorToARGB para converter as cores da web para o formato ARGB, que armazena a transparência além da cor. A cor pode ser normalizada quando os componentes estiverem no intervalo [0;1] e não normalizada: por exemplo, componentes para uma cor de 32 bits terão valores de 0 a 255 (2^8-1). A maioria dos monitores modernos funcionam com cores de 32 bits.
Sequência de ações em MQL
Para exibir uma imagem usando o DirectX em MQL, você precisa fazer o seguinte:
- Criar uma "Etiqueta Gráfica" ou um objeto Bitmap usando a ObjectCreate.
- Criar um recurso gráfico dinâmico usando a ResourceCreate.
- Vincular um recurso ao objeto usando a ObjectSetString com o parâmetro OBJPROP_BMPFILE.
- Criar um arquivo para os sombreadores (ou salvar os sombreadores numa variável de string).
- Escrever sombreadores de vértice e pixel em HLSL.
- Conectar o arquivo do sombreador usando o #resource "NomeArquivo.hlsl" as string variable_name;
- Descrever o formato dos vértices numa matriz do tipo DXVertexLayout
- Criar um contexto — DXContextCreate.
- Criar o sombreador de vértice — DXShaderCreate com o parâmetro DX_SHADER_VERTEX.
- Criar o sombreador de pixel — DXShaderCreate com o parâmetro DX_SHADER_PIXEL.
- Criar o buffer de vértice — DXBufferCriar com o parâmetro DX_BUFFER_VERTEX.
- Se necessário, criar um buffer de índice — DXBufferCriar com o parâmetro DX_BUFFER_INDEX.
- Passar o formato do vértice — DXShaderSetLayout.
- Definir a topologia das primitivas — DXPrimiveTopologySet.
- Vincular os sombreadores de vértice e pixel — DXShaderSet.
- Vincular o buffer de vértice (e índice, se houver) — DXBufferSet.
- Limpar o buffer de profundidade — DXContextClearDepth.
- Se necessário, limpar o buffer de cores — DXContextClearColors
- Enviar um comando de renderização — DX Draw (ou DXDrawIndexado se um buffer de índice for especificado)
- Passar o resultado para o recurso gráfico — DXContextGetColors
- Atualizar o recurso gráfico — ResourceCreate
- Não se esqueça de atualizar o gráfico — ChartRedraw
- Limpar após o uso — DX Release
- Excluir o recurso gráfico —ResourceFree
- Excluir o objeto gráfico —ObjectDelete
Você ainda está comigo? Na verdade, tudo é mais fácil do que parece. Você vai ver mais adiante.
Prática
Visão geral da classe
Todo o processo de utilização do DirectX pode ser dividido em várias etapas: criação de uma tela, inicialização do dispositivo, escrita do sombreadores de vértice e pixel, exibição da imagem resultante na tela, liberação dos recursos. A classe ficará assim:
class DXTutorial { private: int m_width; int m_height; uint m_image[]; string m_canvas; string m_resource; int m_dx_context; int m_dx_vertex_shader; int m_dx_pixel_shader; int m_dx_buffer; bool InitCanvas(); bool InitDevice(float4 &vertex[]); void Deinit(); public: void DXTutorial() { m_dx_context = 0; m_dx_vertex_shader = 0; m_dx_pixel_shader = 0; m_dx_buffer = 0; } void ~DXTutorial() { Deinit(); } bool Init(float4 &vertex[], int width, int height); bool Draw(); };
Membros privados:
- m_width and m_height — largura e altura da tela. Esses membros são usados ao criar o objeto "Etiqueta Gráfica", recurso gráfico dinâmico e o contexto gráfico. Seus valores são definidos durante a inicialização, mas também é possível definir seus valores manualmente.
- m_image — uma matriz usada ao criar um recurso gráfico. O resultado da operação do DirectX é passado para ele.
- m_canvas — o nome do objeto gráfico, m_resource — o nome do recurso gráfico. Usado durante a inicialização e desinicialização.
- m_dx_context — o mais importante, o identificador de contexto gráfico. Participa de todas as operações do DirectX. Ele é inicializado quando o contexto gráfico é criado.
- m_dx_vertex_shader — manipulador do sombreador de vértice. Ele é usado ao definir a marcação do vértice, vincular ao contexto gráfico e desinicialização. Inicializado na compilação do sombreador de vértice.
- m_dx_pixel_shader — manipulador do sombreador de pixel. Ele é usado ao vincular ao contexto gráfico e na desinicialização. Inicializado na compilação do sombreador de pixel.
- m_dx_buffer — identificador de buffer do vértice. Ele é usado ao vincular ao contexto gráfico e na desinicialização. Ele é inicializado quando o buffer de vértice é criado.
Métodos de inicialização e desinicialização:
- InitCanvas() — cria uma tela para exibir a imagem. O objeto "Etiqueta Gráfica" e o recurso gráfico dinâmico são usados. O fundo é preenchido com preto. Retorna o andamento da operação.
- InitDevice() — inicializa o DirectX. Cria um contexto gráfico, sombreadores de vértice e pixel e um buffer de vértice. Define o tipo de primitivas e marca os vértices. Recebe um array de vértices como entrada. Retorna o andamento da operação.
- Deinit() — libera os recursos usados. Exclui o contexto gráfico, sombreadores de vértice e pixel, buffer de vértice, objeto "Etiqueta Gráfica" e o recurso gráfico dinâmico.
Membros públicos:
- DXTutorial() — construtor. Define os identificadores do DirectX como 0.
- ~DXTutorial() — destrutor. Chama o método Deinit().
- Init() — preparação para o trabalho. Recebe uma matriz de vértices e uma altura e largura opcionais como entrada. Valida os dados recebidos, chama a InitCanvas() e InitDevice(). Retorna o andamento da operação.
- Draw() — exibe a imagem na tela. Limpa os buffers de cor e profundidade, envia a imagem para um recurso gráfico. Retorna o andamento da operação.
Matriz de vértices
Como os vértices contêm apenas informações sobre as coordenadas, por simplicidade nós usaremos uma estrutura contendo 4 variáveis flutuantes. X, Y, Z são as coordenadas no espaço tridimensional. W é uma constante auxiliar, deve ser igual a 1, usada para as operações com matrizes.
struct float4 { float x; float y; float z; float w; };
Um triângulo precisa de 3 vértices, então nós usamos uma matriz de tamanho igual a 3.
float4 vertex[3] = {{-0.5f, -0.5f, 0.0f, 1.0f}, {0.0f, 0.5f, 0.0f, 1.0f}, {0.5f, -0.5f, 0.0f, 1.0f}};
Inicialização
Passamos a matriz de vértices e o tamanho da tela para o objeto. Verificamos os dados de entrada. Se a largura ou altura passada for menor que um, o parâmetro será definido como 500 pixels. O tamanho da matriz de vértices deve ser 3. Em seguida, cada vértice é verificado num loop. As coordenadas X e Y devem estar no intervalo [-1;1], Z deve ser igual a 0 - se for forçosamente redefinido para este valor. W deve ser 1, também se redefine forçadamente. Chamamos as funções de inicialização de tela e o DirectX.
bool DXTutorial::Init(float4 &vertex[], int width = 500, int height = 500) { if(width <= 0) { m_width = 500; Print("Warning: width changed to 500"); } else { m_width = width; } if(height <= 0) { m_height = 500; Print("Warning: height changed to 500"); } else { m_height = height; } if(ArraySize(vertex) != 3) { Print("Error: 3 vertex are needed for a triangle"); return(false); } for(int i = 0; i < 3; i++) { if(vertex[i].w != 1) { vertex[i].w = 1.0f; Print("Warning: vertex.w changed to 1"); } if(vertex[i].z != 0) { vertex[i].z = 0.0f; Print("Warning: vertex.z changed to 0"); } if(fabs(vertex[i].x) > 1 || fabs(vertex[i].y) > 1) { Print("Error: vertex coordinates must be in the range [-1;1]"); return(false); } } ResetLastError(); if(!InitCanvas()) { return(false); } if(!InitDevice(vertex)) { return(false); } return(true); }
Criando uma Tela
A função InitCanvas() cria um objeto do tipo "Etiqueta Gráfica" cujas coordenadas são definidas em pixels. Em seguida, um recurso gráfico dinâmico é vinculado a esse objeto, no qual a imagem do DirectX será enviada.
bool DXTutorial::InitCanvas() { m_canvas = "DXTutorialCanvas"; m_resource = "::DXTutorialResource"; int area = m_width * m_height; if(!ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0)) { Print("Error: failed to create an object to draw"); return(false); } if(!ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)) { Print("Warning: failed to move the object horizontally"); } if(!ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)) { Print("Warning: failed to move the object vertically"); } if(ArrayResize(m_image, area) != area) { Print("Error: failed to resize the array for the graphical resource"); return(false); } if(ArrayInitialize(m_image, ColorToARGB(clrBlack)) != area) { Print("Warning: failed to initialize array for graphical resource"); } if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Error: failed to create a resource to draw"); return(false); } if(!ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)) { Print("Error: failed to bind resource to object"); return(false); } return(true); }
Vamos considerar o código com mais detalhes.
m_canvas = "DXTutorialCanvas";
Especificamos o nome do recurso gráfico "DXTutorialCanvas".
m_resource = "::DXTutorialResource";
Especificamos o nome do recurso gráfico dinâmico "::DXTutorialResource".
int area = m_width * m_height;
O método precisará do produto da largura e da altura várias vezes, então nós calculamos com antecedência e salvamos o resultado.
ObjectCreate(0, m_canvas, OBJ_BITMAP_LABEL, 0, 0, 0)
Criamos um objeto do tipo "Etiqueta Gráfica" chamado "DXTutorialCanvas".
ObjectSetInteger(0, m_canvas, OBJPROP_XDISTANCE, 100)
Movemos o objeto 100 pixels para a direita a partir do canto superior esquerdo do gráfico.
ObjectSetInteger(0, m_canvas, OBJPROP_YDISTANCE, 100)
Movemos o objeto 100 pixels para baixo a partir do canto superior esquerdo do gráfico.
ArrayResize(m_image, area)
Redimensionamos a matriz para desenhar.
ArrayInitialize(m_image, ColorToARGB(clrBlack))
Preenchemos a matriz com a cor preta. As cores na matriz devem ser armazenadas no formato ARGB. Por conveniência, usamos a função padrão ColorToARGB para converter a cor para o formato necessário.
ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)
Criamos um recurso gráfico dinâmico chamado "::DXTutorialResource", com a largura de m_width e com a altura de m_height. Indicamos o uso de uma cor com transparência através da COLOR_FORMAT_ARGB_NORMALIZE. Usamos a matriz m_imagem como fonte de dados.
ObjectSetString(0, m_canvas, OBJPROP_BMPFILE, m_resource)
Associamos o objeto e o recurso. Anteriormente, nós não especificamos o tamanho do objeto, pois ele se ajustará automaticamente ao tamanho do recurso.
Inicialização do DirectX
Vamos para a parte mais interessante.
bool DXTutorial::InitDevice(float4 &vertex[]) { DXVertexLayout layout[1] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT }}; string shader_error = ""; m_dx_context = DXContextCreate(m_width, m_height); if(m_dx_context == INVALID_HANDLE) { Print("Error: failed to create graphics context: ", GetLastError()); return(false); } m_dx_vertex_shader = DXShaderCreate(m_dx_context, DX_SHADER_VERTEX, shader, "VShader", shader_error); if(m_dx_vertex_shader == INVALID_HANDLE) { Print("Error: failed to create vertex shader: ", GetLastError()); Print("Shader compilation error: ", shader_error); return(false); } m_dx_pixel_shader = DXShaderCreate(m_dx_context, DX_SHADER_PIXEL, shader, "PShader", shader_error); if(m_dx_pixel_shader == INVALID_HANDLE) { Print("Error: failed to create pixel shader: ", GetLastError()); Print("Shader compilation error: ", shader_error); return(false); } m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex); if(m_dx_buffer == INVALID_HANDLE) { Print("Error: failed to create vertex buffer: ", GetLastError()); return(false); } if(!DXShaderSetLayout(m_dx_vertex_shader, layout)) { Print("Error: failed to set vertex layout: ", GetLastError()); return(false); } if(!DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST)) { Print("Error: failed to set primitive type: ", GetLastError()); return(false); } if(!DXShaderSet(m_dx_context, m_dx_vertex_shader)) { Print("Error, failed to set vertex shader: ", GetLastError()); return(false); } if(!DXShaderSet(m_dx_context, m_dx_pixel_shader)) { Print("Error: failed to set pixel shader: ", GetLastError()); return(false); } if(!DXBufferSet(m_dx_context, m_dx_buffer)) { Print("Error: failed to set buffer to render: ", GetLastError()); return(false); } return(true); }
Vamos analisar o código.
DXVertexLayout layout[1] = {{"POSITION", 0, DX_FORMAT_R32G32B32A32_FLOAT }};
Esta linha descreve o formato dos vértices. Essas informações são necessárias para que a placa gráfica manipule corretamente a matriz de entrada dos vértices. Neste caso o tamanho do array é igual a 1, pois os vértices armazenam apenas as informações de posição. Mas se nós adicionarmos as informações sobre a cor do vértice, nós precisaremos de outra célula do array. "POSITION" significa que a informação está relacionada com as coordenadas. 0 é um índice semântico. Se nós precisarmos passar duas coordenadas diferentes num vértice, nós podemos usar o índice 0 para o primeiro e 1 para o segundo. DX_FORMAT_R32G32B32A32_FLOAT - formato de representação das informações. Nesse caso, quatro números de ponto flutuante de 32 bits.
string shader_error = "";
Esta variável armazenará os erros de compilação do sombreador.
m_dx_context = DXContextCreate(m_width, m_height);
Criamos um contexto gráfico com a largura de m_width e a altura de m_height. Lembre-se do manipulador.
m_dx_vertex_shader = DXShaderCreate(m_dx_context, DX_SHADER_VERTEX, shader, "VShader", shader_error);
Criamos o sombreador de vértice e salvamos o identificador. DX_SHADER_VERTEX indica o tipo de sombreador - vértice. A String shader armazena o código-fonte dos sombreadores de vértice e pixel, mas é recomendável armazená-los em arquivos separados e incluí-los como recursos. "VShader" é o nome do ponto de entrada (função 'main' em programas normais). Se ocorrer um erro de compilação do sombreador, informações adicionais serão gravadas no shader_error. Por exemplo, se você especificar o ponto de entrada "VSha", a variável conterá o seguinte texto: "error X3501: 'VSha': entrypoint not found".
m_dx_pixel_shader = DXShaderCreate(m_dx_context, DX_SHADER_PIXEL, shader, "PShader", shader_error);
O mesmo diz respeito ao sombreador de pixel: specify here the appropriate type and entry point.
m_dx_buffer = DXBufferCreate(m_dx_context, DX_BUFFER_VERTEX, vertex);
Criamos um buffer e salvamos o identificador. Indicamos que ele é um buffer de vértice. Passamos um array de vértices.
DXShaderSetLayout(m_dx_vertex_shader, layout)
Passamos informações sobre o layout dos vértices.
DXPrimiveTopologySet(m_dx_context, DX_PRIMITIVE_TOPOLOGY_TRIANGLELIST)
Definimos a "lista de triângulos" como o tipo de primitivas.
DXShaderSet(m_dx_context, m_dx_vertex_shader)
Passamos informações sobre o sombreador de vértice.
DXShaderSet(m_dx_context, m_dx_pixel_shader)
Passamos informações sobre o sombreador de pixel.
DXBufferSet(m_dx_context, m_dx_buffer)
Passamos informações sobre o buffer.
Exibição da imagem
O DirectX envia a imagem para uma matriz. Um recurso gráfico é criado com base nessa matriz.
bool DXTutorial::Draw() { DXVector dx_color{1.0f, 0.0f, 0.0f, 0.5f}; if(!DXContextClearColors(m_dx_context, dx_color)) { Print("Error: failed to clear the color buffer: ", GetLastError()); return(false); } if(!DXContextClearDepth(m_dx_context)) { Print("Error: failed to clear the depth buffer: ", GetLastError()); return(false); } if(!DXDraw(m_dx_context)) { Print("Error: failed to draw vertices of the vertex buffer: ", GetLastError()); return(false); } if(!DXContextGetColors(m_dx_context, m_image)) { Print("Error: unable to get image from the graphics context: ", GetLastError()); return(false); } if(!ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Error: failed to create a resource to draw"); return(false); } return(true); }
Vamos analisar o método com mais detalhes.
DXVector dx_color{1.0f, 0.0f, 0.0f, 0.5f};
A variável dx_color do tipo DXVector é criada. A cor vermelha com meia transparência é atribuída a ele. Formato RGBA com valores float de 0 a 1.
DXContextClearColors(m_dx_context, dx_color)
Preenchemos o buffer com a cor dx_color.
DXContextClearDepth(m_dx_context)
Limpamos o buffer de profundidade.
DXDraw(m_dx_context)
Enviamos uma tarefa de renderização para o DirectX.
DXContextGetColors(m_dx_context, m_image)
Retornamos o resultado para a matriz m_image.
ResourceCreate(m_resource, m_image, m_width, m_height, 0, 0, m_width, COLOR_FORMAT_ARGB_NORMALIZE)
Atualizamos o recurso gráfico dinâmico.
Liberando os recursos
O DirectX requer que os recursos sejam liberados manualmente. Além disso, é necessário excluir o objeto gráfico e o recurso. Verificamos se nós precisamos liberar os recursos e depois chamamos o DXRelease. O recurso gráfico dinâmico é excluído por meio da ResourceFree. O objeto gráfico é liberado através da ObjectDelete.
void DXTutorial::Deinit() { if(m_dx_pixel_shader > 0 && !DXRelease(m_dx_pixel_shader)) { Print("Error: failed to release the pixel shader handle: ", GetLastError()); } if(m_dx_vertex_shader > 0 && !DXRelease(m_dx_vertex_shader)) { Print("Error: failed to release the vertex shader handle: ", GetLastError()); } if(m_dx_buffer > 0 && !DXRelease(m_dx_buffer)) { Print("Error: failed to release the vertex buffer handle: ", GetLastError()); } if(m_dx_context > 0 && !DXRelease(m_dx_context)) { Print("Error: failed to release the graphics context handle: ", GetLastError()); } if(!ResourceFree(m_resource)) { Print("Error: failed to delete the graphics resource"); } if(!ObjectDelete(0, m_canvas)) { Print("Error: failed to delete graphical object"); } }
Sombreadores
Sombreamentos serão armazenados na string shader. Mas com grandes volumes, é melhor colocá-los em arquivos externos separados e conectá-los como recursos.
string shader = "float4 VShader( float4 Pos : POSITION ) : SV_POSITION \r\n" " { \r\n" " return Pos; \r\n" " } \r\n" " \r\n" "float4 PShader( float4 Pos : SV_POSITION ) : SV_TARGET \r\n" " { \r\n" " return float4( 0.0f, 1.0f, 0.0f, 1.0f ); \r\n" " } \r\n";
Um shader é um programa para uma placa gráfica. No DirectX ele é escrito na linguagem HLSL do tipo C. O float4 no sombreador é um tipo de dado embutido, ao contrário de nossa estrutura. O VShader é um sombreador de vértice neste caso, enquanto o PShader é um sombreador de pixel. POSITION - semântica indicando que os dados de entrada são as coordenadas; o significado é o mesmo que em DXVertexLayout. SV_POSITION - também semântico, mas é usado para o valor de saída. O prefixo SV_ indica que este é um valor do sistema. SV_TARGET - semântica, indica que o valor será escrito na textura ou no buffer de pixel. Então o que está acontecendo aqui. As coordenadas são inseridas no sombreador de vértice, que as passa inalteradas para a saída. O sombreador de pixel (do estágio de rasterização) recebe os valores interpolados, para os quais a cor é definida como verde.
OnStart
Uma instância da classe DXTutorial é criada na função. A função Init é chamada, para a qual o array de vértices é passado. Então a função Draw é chamada. Depois disso, a execução do script termina.
void OnStart() { float4 vertex[3] = {{-0.5f, -0.5f, 0.0f, 1.0f}, {0.0f, 0.5f, 0.0f, 1.0f}, {0.5f, -0.5f, 0.0f, 1.0f}}; DXTutorial dx; if(!dx.Init(vertex)) return; ChartRedraw(); Sleep(1000); if(!dx.Draw()) return; ChartRedraw(); Sleep(1000); }
Conclusão
Neste artigo, nós consideramos a história do DirectX. Nós tentamos entender o que ele é e a sua finalidade. Nós também consideramos a estrutura interna da API. Nós vimos o pipeline que converte os vértices em pixels em placas gráficas modernas. Além disso, o artigo fornece uma lista de ações necessárias para trabalhar com o DirectX e um pequeno exemplo em MQL. Finalmente, nós renderizamos nosso primeiro triângulo! Parabéns! Mas há muitas outras coisas novas e interessantes para aprender para uma operação completa com o DirectX. Isso inclui a transferência de outros dados além dos vértices, a linguagem de programação do sombreador HLSL, várias transformações usando matrizes, texturas, normais e vários efeitos especiais.
Referências e Links
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/10425
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
O artigo ficou muito bom e muito bem escrito. Mas gostaria de deixar como dica para futuros artigos, você disponibilizar em anexo o código fonte usado ne explicação, para quem deseja ver a coisa funcionando de fato, assim o interesse no artigo será ainda maior ... LEMBRE-SE : Muitos estão começando a aprender MQL5, e não vão conseguir de fato acompanhar a explicação a ponto de fazer um código funcional, o que acabara por desestimular a pessoa a aprender a programar em MQL5 ... principalmente que tem muito pouca experiência em programação. Fica ai a dica ... 😁👍