本文概述
借助DirectX和OpenGL等工具的可用性, 如今编写渲染3D元素的桌面应用程序并不困难。但是, 像许多技术一样, 有时会遇到障碍, 使开发人员难以进入这个利基市场。随着时间的流逝, DirectX和OpenGL之间的竞争已使这些技术变得更易于开发人员使用, 同时还提供了更好的文档和更容易的成为熟练的DirectX或OpenGL开发人员的过程。
由Microsoft引入和维护的DirectX是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 Designer, 我们将添加一个”文件”菜单, 其中包含四个项目:”新建…”, “打开…”, “关闭”和”退出”。你可以在存储库中找到将这些菜单项绑定到其相应操作的代码。
单击”新建…”应弹出一个对话框, 如下所示:
在这里, 用户可以输入一些文本, 选择字体, 调整生成的模型高度, 并生成3D模型。单击”创建”应保存模型, 如果用户从左下角选择适当的选项, 则也应将其打开。如你所知, 此处的目标是将一些用户输入的文本转换为3D模型, 并将其呈现在显示器上。
该项目将具有一个简单的结构, 并且这些组件将分解为几个C ++和头文件:
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)-每次调整窗口小部件的宽度和高度时都会调用此方法
我们将通过在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应用程序。
评论前必须登录!
注册