个性化阅读
专注于IT技术分析

OpenGL简介:3D文本渲染教程

本文概述

借助DirectX和OpenGL等工具的可用性, 如今编写渲染3D元素的桌面应用程序并不困难。但是, 像许多技术一样, 有时会遇到障碍, 使开发人员难以进入这个利基市场。随着时间的流逝, DirectX和OpenGL之间的竞争已使这些技术变得更易于开发人员使用, 同时还提供了更好的文档和更容易的成为熟练的DirectX或OpenGL开发人员的过程。

由Microsoft引入和维护的DirectX是Windows平台特有的技术。另一方面, OpenGL是用于3D图形领域的跨平台API, 其规范由Khronos Group维护。

opengl简介

在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模型, 并将其呈现在显示器上。

该项目将具有一个简单的结构, 并且这些组件将分解为几个C ++和头文件:

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)-每次调整窗口小部件的宽度和高度时都会调用此方法
3dmodel文件格式

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

赞(0)
未经允许不得转载:srcmini » OpenGL简介:3D文本渲染教程

评论 抢沙发

评论前必须登录!