OpenGL 简介:3D 文本渲染教程
已发表: 2022-03-11借助 DirectX 和 OpenGL 等工具的可用性,如今编写呈现 3D 元素的桌面应用程序并不是很困难。 但是,与许多技术一样,有时会遇到一些障碍,使开发人员难以进入这一领域。 随着时间的推移,DirectX 和 OpenGL 之间的竞争使开发人员更容易使用这些技术,同时提供更好的文档和更容易成为熟练的 DirectX 或 OpenGL 开发人员的过程。
DirectX 由 Microsoft 引入和维护,是一种特定于 Windows 平台的技术。 另一方面,OpenGL 是 3D 图形领域的跨平台 API,其规范由 Khronos Group 维护。
在这篇关于 OpenGL 的介绍中,我将解释如何编写一个非常简单的应用程序来渲染 3D 文本模型。 我们将使用 Qt/Qt Creator 来实现 UI,以便在多个平台上轻松编译和运行此应用程序。 为本文构建的原型的源代码可在 GitHub 上找到。
这个简单应用程序的目标是生成 3D 模型,将它们保存到具有简单格式的文件中,然后在屏幕上打开和渲染它们。 渲染场景中的 3D 模型将是可旋转和可缩放的,以提供更好的深度和维度感。
先决条件
在开始之前,我们需要为这个项目准备一些有用的工具来准备我们的开发环境。 我们首先需要的是 Qt 框架和相关实用程序,可以从 www.qt.io 下载。 它也可以通过操作系统的标准包管理器获得; 如果是这种情况,您可能想先尝试一下。 本文需要对 Qt 框架有一定的了解。 但是,如果您不熟悉该框架,请不要气馁,因为原型依赖于框架的一些相当琐碎的功能。
您还可以在 Windows 上使用 Microsoft Visual Studio 2013。 在这种情况下,请确保您使用的是适用于 Visual Studio 的适当 Qt 插件。
此时,您可能希望从 GitHub 克隆存储库并在阅读本文时关注它。
OpenGL 概述
我们将从创建一个带有单个文档小部件的简单 Qt 应用程序项目开始。 由于它是一个简单的小部件,编译和运行它不会产生任何有用的东西。 使用 Qt 设计器,我们将添加一个包含四个项目的“文件”菜单:“新建...”、“打开...”、“关闭”和“退出”。 您可以在存储库中找到将这些菜单项绑定到其相应操作的代码。
单击“新建...”应该会弹出一个对话框,如下所示:
在这里,用户可以输入一些文本,选择一种字体,调整生成的模型高度,并生成一个 3D 模型。 单击“创建”应该保存模型,如果用户从左下角选择适当的选项,也应该打开它。 如您所知,这里的目标是将一些用户输入的文本转换为 3D 模型并将其呈现在显示器上。
该项目将具有简单的结构,并且组件将被分解为少量 C++ 和头文件:
创建charmodeldlg.h/cpp
文件包含 QDialog 派生对象。 这实现了允许用户键入文本、选择字体并选择是否将结果保存到文件和/或以 3D 显示它的对话框小部件。
gl_widget.h/cpp
包含 QOpenGLWidget 派生对象的实现。 此小部件用于渲染 3D 场景。
主窗口.h/cpp
包含主应用程序小部件的实现。 这些文件自 Qt Creator 向导创建以来保持不变。
主文件
包含 main(...) 函数,它创建主应用程序小部件并将其显示在屏幕上。
model2d_processing.h/cpp
包含创建 2D 场景的功能。
模型3d.h/cpp
包含存储 3D 模型对象并允许对其进行操作(保存、加载等)的结构。
model_creator.h/cpp
包含允许创建 3D 场景模型对象的类的实现。
OpenGL 实现
为简洁起见,我们将跳过使用 Qt Designer 实现用户界面的明显细节,以及定义交互元素行为的代码。 这个原型应用程序肯定有一些更有趣的方面,这些方面不仅重要,而且与我们想要介绍的 3D 模型编码和渲染相关。 例如,在此原型中将文本转换为 3D 模型的第一步是将文本转换为 2D 单色图像。 一旦生成了这个图像,就可以知道图像的哪个像素构成了文本,而哪些只是“空白”空间。 使用 OpenGL 渲染基本文本有一些更简单的方法,但我们采用这种方法是为了涵盖使用 OpenGL 进行 3D 渲染的一些基本细节。
为了生成这个图像,我们用 QImage::Format_Mono 标志实例化一个 QImage 对象。 由于我们只需要知道哪些像素是文本的一部分,哪些不是,因此单色图像应该可以正常工作。 当用户输入一些文本时,我们同步更新这个 QImage 对象。 根据字体大小和图像宽度,我们尽力使文本适合用户定义的高度。
接下来,我们枚举作为文本一部分的所有像素 - 在本例中为黑色像素。 这里的每个像素都被视为单独的方形单元。 基于此,我们可以生成一个三角形列表,计算它们的顶点坐标,并将它们存储在我们的 3D 模型文件中。
现在我们有了自己的简单 3D 模型文件格式,我们可以开始专注于渲染它了。 对于基于 OpenGL 的 3D 渲染,Qt 提供了一个名为 QOpenGLWidget 的小部件。 要使用这个小部件,可以覆盖三个函数:
- initializeGl() - 这是初始化代码的地方
- paintGl() - 每次重绘小部件时都会调用此方法
- resizeGl(int w, int h) - 每次调整大小时都会使用小部件的宽度和高度调用此方法
我们将通过在 initializeGl 方法中设置适当的着色器配置来初始化小部件。
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
第一行让程序只显示那些离我们更近的渲染像素,而不是那些在其他像素后面且看不见的像素。 第二行指定平面着色技术。 第三行让程序渲染三角形,不管它们的法线指向哪个方向。
初始化后,每次调用paintGl 时,我们都会在显示器上渲染模型。 在我们重写paintGl 方法之前,我们必须准备缓冲区。 为此,我们首先创建一个缓冲区句柄。 然后我们将句柄绑定到绑定点之一,将源数据复制到缓冲区中,最后我们告诉程序取消绑定缓冲区:
// 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);
在覆盖的paintGl方法中,我们使用一个顶点数组和一个法线数据数组来为每一帧绘制三角形:
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);
为了提高性能,我们在原型应用程序中使用了顶点缓冲区对象 (VBO)。 这让我们可以将数据存储在视频内存中并直接用于渲染。 另一种方法是从渲染代码中提供数据(顶点坐标、法线和颜色):

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();
这似乎是一个更简单的解决方案; 但是,它具有严重的性能影响,因为这需要数据通过视频内存总线传输 - 一个相对较慢的过程。 实现paintGl方法后,我们必须注意着色器:
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"));
在 OpenGL 中,着色器是使用称为 GLSL 的语言实现的。 该语言旨在使 3D 数据在渲染之前易于操作。 在这里,我们需要两个着色器:顶点着色器和片段着色器。 在顶点着色器中,我们将使用变换矩阵变换坐标以应用旋转和缩放,并计算颜色。 在片段着色器中,我们将为片段分配颜色。 然后必须编译这些着色器程序并将其与上下文链接。 OpenGL 提供了桥接两种环境的简单方法,以便可以从外部访问或分配程序内部的参数:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
在顶点着色器代码中,我们通过对原始顶点应用变换矩阵来计算新的顶点位置:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
为了计算这个变换矩阵,我们计算了几个单独的矩阵:屏幕缩放、平移场景、缩放、旋转和居中。 然后我们找到这些矩阵的乘积,以计算最终的变换矩阵。 首先将模型中心平移到原点 (0, 0, 0),这也是屏幕的中心。 旋转由用户使用一些指点设备与场景的交互来确定。 用户可以单击场景并拖动以进行旋转。 当用户点击时,我们存储光标位置,移动后我们有第二个光标位置。 使用这两个坐标以及场景中心,我们形成了一个三角形。 通过一些简单的计算,我们可以确定旋转角度,我们可以更新我们的旋转矩阵以反映这种变化。 对于缩放,我们只需依靠鼠标滚轮来修改 OpenGL 小部件的 X 和 Y 轴的缩放因子。 模型向后平移 0.5 以使其保持在渲染场景的平面后面。 最后,为了保持自然的纵横比,我们需要调整模型扩展沿较长边的减小(与 OpenGL 场景不同,渲染它的小部件可能沿任一轴具有不同的物理尺寸)。 结合所有这些,我们计算最终的变换矩阵如下:
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; }
结论
在这篇关于 OpenGL 3D 渲染的介绍中,我们探索了一种允许 ud 使用我们的视频卡来渲染 3D 模型的技术。 这比将 CPU 周期用于相同目的要高效得多。 我们使用了一种非常简单的着色技术,并通过处理来自鼠标的用户输入使场景具有交互性。 我们避免使用显存总线在显存和程序之间来回传递数据。 即使我们只是在 3D 中渲染了一行文本,也可以用非常相似的方式渲染更复杂的场景。
公平地说,本教程几乎没有触及 3D 建模和渲染的表面。 这是一个庞大的话题,本 OpenGL 教程不能声称这是您构建 3D 游戏或建模软件所需的全部知识。 但是,本文的目的是让您了解这个领域,并展示您可以如何轻松地开始使用 OpenGL 构建 3D 应用程序。