Introdução ao OpenGL: um tutorial de renderização de texto 3D
Publicados: 2022-03-11Com a disponibilidade de ferramentas como DirectX e OpenGL, escrever um aplicativo de 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 a entrada de desenvolvedores nesse nicho. Com o tempo, a corrida entre DirectX e OpenGL fez com que essas tecnologias se tornassem mais acessíveis aos desenvolvedores, juntamente com uma melhor documentação e um processo mais fácil de se tornar um desenvolvedor qualificado de DirectX ou OpenGL.
DirectX, introduzido e mantido pela Microsoft, é uma tecnologia específica para a plataforma Windows. Por outro lado, OpenGL é uma API multiplataforma 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. Usaremos o Qt/Qt Creator para implementar a interface do usuário, facilitando a compilação e execução deste aplicativo em várias plataformas. O código fonte do protótipo construído para este artigo está disponível no GitHub.
O objetivo deste simples aplicativo é gerar modelos 3D, salvá-los em um arquivo com 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 noção de profundidade e dimensão.
Pré-requisitos
Antes de começar, precisaremos preparar nosso ambiente de desenvolvimento com algumas ferramentas úteis para este projeto. A primeira coisa que precisamos é do framework Qt e utilitários relevantes, que podem ser baixados em www.qt.io. Ele também pode estar disponível através do gerenciador de pacotes padrão do seu sistema operacional; se for esse o caso, você pode querer tentar com ele primeiro. Este artigo requer alguma familiaridade com o framework Qt. No entanto, se você não estiver familiarizado com o framework, não se sinta desencorajado a seguir adiante, pois o protótipo depende de alguns recursos bastante triviais do framework.
Você também pode usar o Microsoft Visual Studio 2013 no Windows. Nesse caso, verifique se você está usando o suplemento Qt apropriado para o Visual Studio.
Neste ponto, você pode querer clonar o repositório do GitHub e segui-lo enquanto lê este artigo.
Visão geral do OpenGL
Começaremos criando um projeto de aplicativo Qt simples com um widget de documento único. Como é um widget básico, compilá-lo e executá-lo não produzirá nada útil. Com o Qt designer, adicionaremos um menu “Arquivo” com quatro itens: “Novo…”, “Abrir…”, “Fechar” e “Sair”. Você pode encontrar o código que vincula esses itens de menu às 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:
createcharmodeldlg.h/cpp
Os arquivos contêm objetos derivados do QDialog. Isso implementa o widget de diálogo que permite ao usuário digitar texto, selecionar fonte e escolher se deseja salvar o resultado em um arquivo e/ou exibi-lo em 3D.
gl_widget.h/cpp
Contém a implementação do objeto derivado de QOpenGLWidget. Este widget é usado para renderizar a cena 3D.
mainwindow.h/cpp
Contém a implementação do widget do aplicativo principal. Esses arquivos foram deixados inalterados desde que foram criados pelo assistente do Qt Creator.
main.cpp
Contém a função main(…), que cria o widget principal do aplicativo e o mostra na tela.
model2d_processing.h/cpp
Contém funcionalidade de criação de cena 2D.
model3d.h/cpp
Contém estruturas que armazenam objetos de modelo 3D e permitem que as operações trabalhem neles (salvar, carregar etc.).
model_creator.h/cpp
Contém a implementação da classe que permite a criação do objeto modelo de cena 3D.
Implementação OpenGL
Por brevidade, pularemos os detalhes óbvios da 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 protótipo, que não são apenas importantes, mas também relevantes para a codificação e renderização de modelos 3D que queremos abordar. 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 adotando essa abordagem para cobrir alguns detalhes básicos 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 são, uma imagem monocromática deve funcionar bem. Quando o usuário insere algum texto, atualizamos de forma síncrona esse objeto QImage. Com base no tamanho da fonte e na largura da imagem, fazemos o possível para ajustar o texto à altura definida pelo usuário.
Em seguida, 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, o Qt fornece um widget chamado QOpenGLWidget. Para usar este widget, três funções podem ser substituídas:
- initializeGl() - é aqui que o código de inicialização vai
- paintGl() - este método é chamado toda vez que o widget é redesenhado
- resizeGl(int w, int h) - este método é chamado com a largura e altura do widget toda vez que ele é redimensionado
Vamos inicializar o widget definindo a configuração apropriada do shader 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, em vez 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 o programa renderizar triângulos independentemente da direção para a qual suas normais apontam.
Uma vez inicializado, renderizamos o modelo na tela 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, informamos 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 de substituição paintGl, 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 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 de vértice, normais e cores) do código de renderização:
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 requer que os dados passem pelo 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\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); 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 o OpenGL, os shaders 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 sombreadores: sombreador de vértice e sombreador de fragmento. No vertex shader, vamos transformar as coordenadas com a matriz de transformação para aplicar rotação e zoom e para calcular a cor. No fragment shader, atribuiremos cor ao fragmento. Esses programas de sombreamento devem ser compilados e vinculados ao contexto. O 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 do vertex shader, calculamos a nova posição do 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 de tela, translação de cena, escala, rotação e centro. Em seguida, encontramos o produto dessas matrizes para calcular a matriz de transformação final. Comece traduzindo o centro do modelo para a origem (0, 0, 0), que também é o centro da tela. 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 é convertido 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 (diferentemente da cena OpenGL, o widget onde ele é renderizado pode ter dimensões físicas diferentes ao longo 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<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(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<float>(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; }
Conclusão
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 através do manuseio de entradas do usuário a partir do mouse. Evitamos usar o barramento de memória de vídeo para passar dados entre a memória de vídeo e o programa. Mesmo que tenhamos 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 o que você precisa saber para poder construir jogos 3D ou softwares de modelagem. No entanto, o objetivo deste artigo é dar uma olhada nesse domínio e mostrar como é fácil começar a usar o OpenGL para criar aplicativos 3D.