OpenGL简介(3D文本渲染教程)

本文概述

  • 先决条件
  • OpenGL概述
  • OpenGL示例
  • 总结
借助DirectX和OpenGL等工具的可用性, 如今编写渲染3D元素的桌面应用程序并不困难。但是, 像许多技术一样, 有时会遇到障碍, 使开发人员难以进入这个利基市场。随着时间的流逝, DirectX和OpenGL之间的竞争已使这些技术变得更易于开发人员使用, 同时还提供了更好的文档和更容易的成为熟练的DirectX或OpenGL开发人员的过程。
由Microsoft引入和维护的DirectX是Windows平台特有的技术。另一方面, OpenGL是用于3D图形领域的跨平台API, 其规范由Khronos Group维护。
OpenGL简介(3D文本渲染教程)

文章图片
在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 Designer, 我们将添加一个” 文件” 菜单, 其中包含四个项目:” 新建… ” , “ 打开… ” , “ 关闭” 和” 退出” 。你可以在存储库中找到将这些菜单项绑定到其相应操作的代码。
单击” 新建…” 应弹出一个对话框, 如下所示:
OpenGL简介(3D文本渲染教程)

文章图片
在这里, 用户可以输入一些文本, 选择字体, 调整生成的模型高度, 并生成3D模型。单击” 创建” 应保存模型, 如果用户从左下角选择适当的选项, 则也应将其打开。如你所知, 此处的目标是将一些用户输入的文本转换为3D模型, 并将其呈现在显示器上。
【OpenGL简介(3D文本渲染教程)】该项目将具有一个简单的结构, 并且这些组件将分解为几个C ++和头文件:
OpenGL简介(3D文本渲染教程)

文章图片
createcharmodeldlg.h / cpp 文件包含QDialog派生的对象。这实现了对话框窗口小部件, 该对话框窗口小部件允许用户键入文本, 选择字体以及选择是否将结果保存到文件中和/或以3D显示。
gl_widget.h / cpp 包含QOpenGLWidget派生对象的实现。此小部件用于渲染3D场景。
mainwindow.h / cpp 包含主应用程序小部件的实现。这些文件是由Qt Creator向导创建的, 因此保持不变。
main.cpp 包含main(…)函数, 该函数创建主应用程序小部件并在屏幕上显示。
model2d_processing.h / cpp 包含2D场景创建功能。
model3d.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)-每次调整窗口小部件的宽度和高度时都会调用此方法
OpenGL简介(3D文本渲染教程)

文章图片
我们将通过在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应用程序。

    推荐阅读