Introducción a OpenGL: un tutorial de representación de texto en 3D
Publicado: 2022-03-11Con la disponibilidad de herramientas como DirectX y OpenGL, hoy en día no es muy difícil escribir una aplicación de escritorio que represente elementos 3D. Sin embargo, como muchas tecnologías, a veces hay obstáculos que dificultan a los desarrolladores que intentan entrar en este nicho. Con el tiempo, la carrera entre DirectX y OpenGL ha hecho que estas tecnologías se vuelvan más accesibles para los desarrolladores, junto con una mejor documentación y un proceso más sencillo para convertirse en un desarrollador experto de DirectX u OpenGL.
DirectX, presentado y mantenido por Microsoft, es una tecnología específica de la plataforma Windows. Por otro lado, OpenGL es una API multiplataforma para el campo de gráficos 3D cuya especificación es mantenida por el Grupo Khronos.
En esta introducción a OpenGL, explicaré cómo escribir una aplicación muy simple para representar modelos de texto en 3D. Usaremos Qt/Qt Creator para implementar la interfaz de usuario, lo que facilitará la compilación y ejecución de esta aplicación en múltiples plataformas. El código fuente del prototipo creado para este artículo está disponible en GitHub.
El objetivo de esta sencilla aplicación es generar modelos 3D, guardarlos en un archivo con un formato simple y abrirlos y renderizarlos en la pantalla. El modelo 3D en la escena renderizada será giratorio y ampliable, para dar una mejor sensación de profundidad y dimensión.
requisitos previos
Antes de comenzar, necesitaremos preparar nuestro entorno de desarrollo con algunas herramientas útiles para este proyecto. Lo primero que necesitamos es el marco Qt y las utilidades relevantes, que se pueden descargar desde www.qt.io. También puede estar disponible a través del administrador de paquetes estándar de su sistema operativo; si ese es el caso, es posible que desee probarlo primero. Este artículo requiere cierta familiaridad con el marco Qt. Sin embargo, si no está familiarizado con el marco, no se desanime a seguirlo, ya que el prototipo se basa en algunas características bastante triviales del marco.
También puede usar Microsoft Visual Studio 2013 en Windows. En ese caso, asegúrese de estar utilizando el complemento Qt adecuado para Visual Studio.
En este punto, es posible que desee clonar el repositorio de GitHub y seguirlo mientras lee este artículo.
Descripción general de OpenGL
Comenzaremos creando un proyecto de aplicación Qt simple con un solo widget de documento. Dado que es un widget básico, compilarlo y ejecutarlo no producirá nada útil. Con Qt Designer, agregaremos un menú "Archivo" con cuatro elementos: "Nuevo...", "Abrir...", "Cerrar" y "Salir". Puede encontrar el código que vincula estos elementos de menú a sus acciones correspondientes en el repositorio.
Al hacer clic en "Nuevo ..." debería aparecer un cuadro de diálogo que se verá así:
Aquí, el usuario puede ingresar texto, elegir una fuente, ajustar la altura del modelo resultante y generar un modelo 3D. Hacer clic en "Crear" debería guardar el modelo y también debería abrirlo si el usuario elige la opción adecuada en la esquina inferior izquierda. Como puede ver, el objetivo aquí es convertir algún texto ingresado por el usuario en un modelo 3D y representarlo en la pantalla.
El proyecto tendrá una estructura simple y los componentes se dividirán en un puñado de archivos de encabezado y C++:
createcharmodeldlg.h/cpp
Los archivos contienen objetos derivados de QDialog. Esto implementa el widget de diálogo que permite al usuario escribir texto, seleccionar la fuente y elegir si desea guardar el resultado en un archivo y/o mostrarlo en 3D.
gl_widget.h/cpp
Contiene la implementación del objeto derivado de QOpenGLWidget. Este widget se utiliza para renderizar la escena 3D.
ventanaprincipal.h/cpp
Contiene la implementación del widget de la aplicación principal. Estos archivos se mantuvieron sin cambios desde que fueron creados por el asistente Qt Creator.
principal.cpp
Contiene la función principal (...), que crea el widget de la aplicación principal y lo muestra en la pantalla.
model2d_procesamiento.h/cpp
Contiene funcionalidad de creación de escena 2D.
modelo3d.h/cpp
Contiene estructuras que almacenan objetos de modelos 3D y permiten que las operaciones trabajen en ellos (guardar, cargar, etc.).
creador_modelo.h/cpp
Contiene la implementación de la clase que permite la creación de un objeto de modelo de escena 3D.
Implementación de OpenGL
Para abreviar, omitiremos los detalles obvios de implementar la interfaz de usuario con Qt Designer y el código que define los comportamientos de los elementos interactivos. Sin duda, hay algunos aspectos más interesantes de esta aplicación prototipo, que no solo son importantes sino también relevantes para la codificación y el renderizado de modelos 3D que queremos cubrir. Por ejemplo, el primer paso para convertir texto en un modelo 3D en este prototipo consiste en convertir el texto en una imagen monocromática 2D. Una vez que se genera esta imagen, es posible saber qué píxel de la imagen forma el texto y cuáles son solo espacio “vacío”. Hay algunas formas más simples de renderizar texto básico usando OpenGL, pero estamos tomando este enfoque para cubrir algunos detalles esenciales de renderizado 3D con OpenGL.
Para generar esta imagen, instanciamos un objeto QImage con el indicador QImage::Format_Mono. Dado que todo lo que necesitamos saber es qué píxeles son parte del texto y cuáles no, una imagen monocromática debería funcionar bien. Cuando el usuario ingresa algún texto, actualizamos sincrónicamente este objeto QImage. Según el tamaño de la fuente y el ancho de la imagen, hacemos todo lo posible para ajustar el texto dentro de la altura definida por el usuario.
A continuación, enumeramos todos los píxeles que forman parte del texto, en este caso, los píxeles negros. Cada píxel aquí se trata como unidades cuadradas separadas. En base a esto, podemos generar una lista de triángulos, calcular las coordenadas de sus vértices y almacenarlos en nuestro archivo de modelo 3D.
Ahora que tenemos nuestro propio formato de archivo de modelo 3D simple, podemos comenzar a enfocarnos en renderizarlo. Para el renderizado 3D basado en OpenGL, Qt proporciona un widget llamado QOpenGLWidget. Para usar este widget, se pueden anular tres funciones:
- initializeGl() - aquí es donde va el código de inicialización
- paintGl() - este método se llama cada vez que se vuelve a dibujar el widget
- resizeGl(int w, int h) - este método se llama con el ancho y alto del widget cada vez que se cambia el tamaño
Inicializaremos el widget estableciendo la configuración de sombreado adecuada en el método initializeGl.
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
La primera línea hace que el programa muestre solo los píxeles renderizados que están más cerca de nosotros, en lugar de los que están detrás de otros píxeles y fuera de la vista. La segunda línea especifica la técnica de sombreado plano. La tercera línea hace que el programa represente triángulos sin importar en qué dirección apunten sus normales.
Una vez inicializado, renderizamos el modelo en la pantalla cada vez que se llama a paintGl. Antes de anular el método paintGl, debemos preparar el búfer. Para hacer eso, primero creamos un identificador de búfer. Luego vinculamos el identificador a uno de los puntos de vinculación, copiamos los datos de origen en el búfer y finalmente le decimos al programa que desvincule el búfer:
// 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 del método paintGl predominante, usamos una matriz de vértices y una matriz de datos normales para dibujar los triángulos para cada cuadro:

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 mejorar el rendimiento, usamos Vertex Buffer Object (VBO) en nuestra aplicación prototipo. Esto nos permite almacenar datos en la memoria de video y usarlos directamente para renderizar. Un método alternativo a esto consiste en proporcionar los datos (coordenadas de vértice, normales y colores) del código de representación:
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();
Esto puede parecer una solución más simple; sin embargo, tiene serias implicaciones de rendimiento, ya que requiere que los datos viajen a través del bus de memoria de video, un proceso relativamente más lento. Después de implementar el método paintGl, debemos prestar atención a los sombreadores:
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"));
Con OpenGL, los sombreadores se implementan mediante un lenguaje conocido como GLSL. El lenguaje está diseñado para facilitar la manipulación de datos 3D antes de que se rendericen. Aquí, necesitaremos dos shaders: vertex shader y fragment shader. En vertex shader, transformaremos las coordenadas con la matriz de transformación para aplicar rotación y zoom, y calcular el color. En fragment shader, asignaremos color al fragmento. Estos programas de sombreado deben luego compilarse y vincularse con el contexto. OpenGL proporciona formas simples de unir los dos entornos para que los parámetros dentro del programa puedan ser accedidos o asignados desde el exterior:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
En el código del sombreador de vértices, calculamos la nueva posición del vértice aplicando la matriz de transformación en los vértices originales:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
Para calcular esta matriz de transformación, calculamos algunas matrices separadas: escala de pantalla, escena de traslación, escala, rotación y centro. Luego encontramos el producto de estas matrices para calcular la matriz de transformación final. Comience trasladando el centro del modelo al origen (0, 0, 0), que también es el centro de la pantalla. La rotación está determinada por la interacción del usuario con la escena utilizando algún dispositivo señalador. El usuario puede hacer clic en la escena y arrastrarla para rotarla. Cuando el usuario hace clic, almacenamos la posición del cursor, y después de un movimiento tenemos la segunda posición del cursor. Usando estas dos coordenadas, junto con el centro de la escena, formamos un triángulo. Siguiendo algunos cálculos simples, podemos determinar el ángulo de rotación y podemos actualizar nuestra matriz de rotación para reflejar este cambio. Para escalar, simplemente confiamos en la rueda del mouse para modificar el factor de escala de los ejes X e Y del widget de OpenGL. El modelo se traslada hacia atrás en 0,5 para mantenerlo detrás del plano desde el que se representa la escena. Finalmente, para mantener la relación de aspecto natural, necesitamos ajustar la disminución de la expansión del modelo a lo largo del lado más largo (a diferencia de la escena OpenGL, el widget donde se representa puede tener diferentes dimensiones físicas a lo largo de cualquiera de los ejes). Combinando todo esto, calculamos la matriz de transformación final de la siguiente manera:
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; }
Conclusión
En esta introducción al renderizado 3D de OpenGL, exploramos una de las tecnologías que permiten a ud utilizar nuestra tarjeta de video para renderizar un modelo 3D. Esto es mucho más eficiente que usar ciclos de CPU para el mismo propósito. Usamos una técnica de sombreado muy simple e hicimos que la escena fuera interactiva mediante el manejo de las entradas del usuario con el mouse. Evitamos usar el bus de memoria de video para pasar datos de un lado a otro entre la memoria de video y el programa. Aunque solo renderizamos una sola línea de texto en 3D, las escenas más complicadas se pueden renderizar de formas muy similares.
Para ser justos, este tutorial apenas ha arañado la superficie del modelado y renderizado 3D. Este es un tema amplio, y este tutorial de OpenGL no puede afirmar que esto es todo lo que necesita saber para poder crear juegos 3D o software de modelado. Sin embargo, el propósito de este artículo es darle un vistazo a este ámbito y mostrarle cuán fácilmente puede comenzar a usar OpenGL para crear aplicaciones 3D.