Com a disponibilidade de ferramentas como DirectX e OpenGL, escrever um aplicativo desktop que renderize elementos 3D não é muito difícil hoje em dia. No entanto, como muitas tecnologias, às vezes existem obstáculos que dificultam para os desenvolvedores que tentam entrar nesse nicho. Com o tempo, a corrida entre DirectX e OpenGL fez com que essas tecnologias se tornassem mais acessíveis aos desenvolvedores, junto com uma documentação melhor e um processo mais fácil de se tornar um especialista em DirectX ou Desenvolvedor OpenGL .
DirectX, introduzido e mantido pela Microsoft, é uma tecnologia específica para o Plataforma Windows . Por outro lado, OpenGL é uma API de plataforma cruzada para a área de gráficos 3D, cuja especificação é mantida pelo Khronos Group.
Nesta introdução ao OpenGL, explicarei como escrever um aplicativo muito simples para renderizar modelos de texto 3D. Estaremos usando o Qt / Qt Creator para implementar a IU, tornando mais fácil compilar e executar este aplicativo em várias plataformas. O código-fonte do protótipo construído para este artigo é disponível no GitHub .
O objetivo deste aplicativo simples é gerar modelos 3D, salvá-los em um arquivo de formato simples e abri-los e renderizá-los na tela. O modelo 3D na cena renderizada será giratório e com zoom, para dar uma melhor sensação de profundidade e dimensão.
exemplos de problemas de aprendizado de máquina
Antes de começar, precisaremos preparar nosso ambiente de desenvolvimento com algumas ferramentas úteis para este projeto. A primeira coisa que precisamos é o framework Qt e utilitários relevantes, que podem ser baixados de www.qt.io . Ele também pode estar disponível por meio do gerenciador de pacotes padrão do seu sistema operacional; se for esse o caso, você pode tentar primeiro. Este artigo requer alguma familiaridade com o framework Qt. No entanto, se você não estiver familiarizado com a estrutura, não se sinta desencorajado em acompanhá-la, pois o protótipo depende de alguns recursos bastante triviais da estrutura.
Você também pode usar Microsoft Visual Studio 2013 no Windows. Nesse caso, certifique-se de que está usando o apropriado Suplemento Qt para Visual Studio .
Neste ponto, você pode querer clonar o repositório do GitHub e acompanhe enquanto você lê este artigo.
Começaremos criando um projeto de aplicativo Qt simples com um widget de documento único. Por ser um widget básico, compilá-lo e executá-lo não produzirá nada de útil. Com o Qt designer, adicionaremos um menu “Arquivo” com quatro itens: “Novo…”, “Abrir…”, “Fechar” e “Sair”. Você pode encontrar o código que liga esses itens de menu às suas ações correspondentes no repositório .
Clicar em “Novo…” deve abrir uma caixa de diálogo parecida com esta:
Aqui, o usuário pode inserir algum texto, escolher uma fonte, ajustar a altura do modelo resultante e gerar um modelo 3D. Clicar em “Criar” deve salvar o modelo, e também deve abri-lo se o usuário escolher a opção apropriada no canto inferior esquerdo. Como você pode ver, o objetivo aqui é converter algum texto inserido pelo usuário em um modelo 3D e renderizá-lo na tela.
O projeto terá uma estrutura simples e os componentes serão divididos em um punhado de arquivos C ++ e de cabeçalho:
como codificar com c ++
Os arquivos contêm objeto derivado de QDialog. Isso implementa o widget de diálogo que permite ao usuário digitar texto, selecionar a fonte e escolher se deseja salvar o resultado em um arquivo e / ou exibi-lo em 3D.
Contém a implementação do objeto derivado de QOpenGLWidget. Este widget é usado para renderizar a cena 3D.
Contém a implementação do widget do aplicativo principal. Esses arquivos não foram alterados desde que foram criados pelo assistente Qt Creator.
Contém a função principal (…), que cria o widget principal do aplicativo e o mostra na tela.
Contém funcionalidade de criação de cena 2D.
Contém estruturas que armazenam objetos de modelo 3D e permitem operações para trabalhar neles (salvar, carregar etc.).
Contém implementação de classe que permite a criação de objeto de modelo de cena 3D.
Para resumir, pularemos os detalhes óbvios de implementação da interface do usuário com o Qt Designer e o código que define os comportamentos dos elementos interativos. Certamente, existem alguns aspectos mais interessantes deste aplicativo de protótipo, que não são apenas importantes, mas também relevantes para a codificação e renderização do modelo 3D que queremos cobrir. Por exemplo, a primeira etapa da conversão de texto em um modelo 3D neste protótipo envolve a conversão do texto em uma imagem monocromática 2D. Uma vez gerada essa imagem, é possível saber qual pixel da imagem forma o texto e quais são apenas espaços “vazios”. Existem algumas maneiras mais simples de renderizar texto básico usando OpenGL, mas estamos usando essa abordagem para cobrir alguns detalhes essenciais da renderização 3D com OpenGL.
Para gerar esta imagem , instanciamos um objeto QImage com o sinalizador QImage :: Format_Mono. Como tudo o que precisamos saber é quais pixels fazem parte do texto e quais não, uma imagem monocromática deve funcionar bem. Quando o usuário insere algum texto, atualizamos de forma síncrona este objeto QImage. Com base no tamanho da fonte e largura da imagem, tentamos o nosso melhor para ajustar o texto dentro da altura definida pelo usuário.
A seguir, enumeramos todos os pixels que fazem parte do texto - neste caso, os pixels pretos. Cada pixel aqui é tratado como unidades quadradas separadas. Com base nisso, podemos gerar uma lista de triângulos, computando as coordenadas de seus vértices, e armazená-los em nosso arquivo de modelo 3D.
Agora que temos nosso próprio formato de arquivo de modelo 3D simples, podemos começar a nos concentrar em renderizá-lo. Para renderização 3D baseada em OpenGL, Qt fornece um widget chamado QOpenGLWidget. Para usar este widget, três funções podem ser substituídas:
Inicializaremos o widget definindo a configuração de sombreador apropriada no método initializeGl.
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
A primeira linha faz com que o programa mostre apenas os pixels renderizados que estão mais próximos de nós, ao invés dos que estão atrás de outros pixels e fora de vista. A segunda linha especifica a técnica de sombreamento plano. A terceira linha faz com que o programa renderize triângulos independentemente da direção para a qual seus normais apontem.
Uma vez inicializado, renderizamos o modelo no display toda vez que paintGl é chamado. Antes de substituir o método paintGl, devemos preparar o buffer. Para fazer isso, primeiro criamos um identificador de buffer. Em seguida, vinculamos o identificador a um dos pontos de vinculação, copiamos os dados de origem no buffer e, finalmente, dizemos ao programa para desvincular o buffer:
// Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we’ll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we’ve finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);
Dentro do método paintGl de substituição, usamos uma matriz de vértices e uma matriz de dados normais para desenhar os triângulos para cada quadro:
QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);
Para melhorar o desempenho, usamos Vertex Buffer Object (VBO) em nosso aplicativo de protótipo. Isso nos permite armazenar dados na memória de vídeo e usá-los diretamente para renderização. Um método alternativo para isso envolve fornecer os dados (coordenadas do vértice, normais e cores) do código de renderização:
tipos de riscos cambiais
glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();
Isso pode parecer uma solução mais simples; no entanto, tem sérias implicações de desempenho, pois exige que os dados percorram o barramento de memória de vídeo - um processo relativamente mais lento. Depois de implementar o método paintGl, devemos prestar atenção aos shaders:
m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( '#version 400
' '
' 'layout (location = 0) in vec3 coordVertexes;
' 'layout (location = 1) in vec3 coordNormals;
' 'flat out float lightIntensity;
' '
' 'uniform mat4 matrixVertex;
' 'uniform mat4 matrixNormal;
' '
' 'void main()
' '{
' ' gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
' ' lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);
' '}')); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( '#version 400
' '
' 'flat in float lightIntensity;
' '
' 'layout (location = 0) out vec4 FragColor;
' 'uniform vec3 fragmentColor;
' '
' 'void main()
' '{
' ' FragColor = vec4(fragmentColor * lightIntensity, 1.0);
' '}')); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8('coordVertexes')); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8('coordNormals')); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8('matrixVertex')); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8('matrixNormal')); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8('fragmentColor'));
Com OpenGL, os sombreadores são implementados usando uma linguagem conhecida como GLSL . A linguagem foi projetada para facilitar a manipulação de dados 3D antes de serem renderizados. Aqui, precisaremos de dois shaders: vertex shader e fragment shader. No sombreador de vértice, vamos transformar as coordenadas com a matriz de transformação para aplicar rotação e zoom, e calcular a cor. No shader de fragmento, atribuiremos cor ao fragmento. Esses programas de sombreador devem ser compilados e vinculados ao contexto. OpenGL fornece maneiras simples de conectar os dois ambientes para que os parâmetros dentro do programa possam ser acessados ou atribuídos de fora:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
No código de sombreador de vértice, calculamos a posição do novo vértice aplicando a matriz de transformação nos vértices originais:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
Para calcular essa matriz de transformação, calculamos algumas matrizes separadas: escala da tela, translação da cena, escala, rotação e centro. Em seguida, encontramos o produto dessas matrizes para calcular a matriz de transformação final. Comece transladando o centro do modelo para a origem (0, 0, 0), que é o centro da tela também. A rotação é determinada pela interação do usuário com a cena usando algum dispositivo apontador. O usuário pode clicar na cena e arrastar para girar. Quando o usuário clica, armazenamos a posição do cursor e, após um movimento, temos a segunda posição do cursor. Usando essas duas coordenadas, junto com o centro da cena, formamos um triângulo. Seguindo alguns cálculos simples, podemos determinar o ângulo de rotação e podemos atualizar nossa matriz de rotação para refletir essa mudança. Para dimensionamento, simplesmente contamos com a roda do mouse para modificar o fator de dimensionamento dos eixos X e Y do widget OpenGL. O modelo é transladado de volta em 0,5 para mantê-lo atrás do plano a partir do qual a cena é renderizada. Finalmente, para manter a proporção natural, precisamos ajustar a diminuição da expansão do modelo ao longo do lado mais longo (ao contrário da cena OpenGL, o widget onde é renderizado pode ter diferentes dimensões físicas ao longo de qualquer um dos eixos). Combinando tudo isso, calculamos a matriz de transformação final da seguinte forma:
void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast(qMin(width(), height())); float scaleScreenVert = static_cast(dimMin / static_cast(height())); float scaleScreenHorz = static_cast(dimMin / static_cast(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }
Nesta introdução à renderização 3D OpenGL, exploramos uma das tecnologias que permitem ao ud utilizar nossa placa de vídeo para renderizar um modelo 3D. Isso é muito mais eficiente do que usar ciclos de CPU para a mesma finalidade. Usamos uma técnica de sombreamento muito simples e tornamos a cena interativa por meio do manuseio de entradas do usuário com o mouse. Evitamos usar o barramento de memória de vídeo para passar dados e para trás entre a memória de vídeo e o programa. Embora tenhamos apenas renderizado uma única linha de texto em 3D, cenas mais complicadas podem ser renderizadas de maneiras muito semelhantes.
Para ser justo, este tutorial mal arranhou a superfície da modelagem e renderização 3D. Este é um tópico vasto e este tutorial OpenGL não pode afirmar que isso é tudo que você precisa saber para ser capaz de construir jogos 3D ou softwares de modelagem. No entanto, o objetivo deste artigo é dar uma espiada nesse reino e mostrar como é fácil começar a usar o OpenGL para criar aplicativos 3D.