H1
关于OpenGL
OpenGL(Open Graphics Library,译名:开放图形库)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近350个不同的函数调用组成,用来从简单的图形到绘制复杂的三维景象。OpenGL的能够利用图形加速硬件来高效实现图形绘制。目前OpenGL的版本是2017年7月31日发布4.6,不同版本的API函数差异较大,此后的相关例子都以版本4.6作为基础。
H1
在Qt中使用OpenGL
Qt是一个跨平台的C++应用程序开发框架,目前广泛应用于GUI程序和非GUI程序的开发。在Qt中,OpenGL主要封装在QOpenGLWidget类中,它是QWidget的一个子类。QWidget就是一个窗口类,实现了窗口的基本功能。OpenGL不仅语言无关,而且平台无关。规范只字未提获得和管理OpenGL上下文相关的内容,而是将这些作为细节交给底层的窗口系统。出于同样的原因,OpenGL纯粹专注于渲染,而不提供输入、音频以及窗口相关的API。所以,要实现OpenGL的功能必须跟一个具体的平台和开发工具结合起来,这种结合最重要的就是管理OpenGL上下文(context)。简单地说,OpenGL上下文就是要跟具体平台的窗口渲染机制结合起来,使得OpenGL的结果能够渲染到窗口中。Qt中的QOpenGLWidget类实现了管理OpenGL的上下文,使用者不需要关注上下文如何实现的,只需关注OpenGL如何渲染。
H2
重载QOpenGLWidget实现OpenGL
在Qt中,通过重载QOpenGLWidget来实现OpenGL的图形渲染,下面是一个简单例子:
#ifndef QKGLWIDGET_H
#define QKGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QScopedPointer>
QT_FORWARD_DECLARE_CLASS(QOpenGLShaderProgram)
class QkGLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
Q_OBJECT
public:
explicit QkGLWidget(QWidget *parent = nullptr);
~QkGLWidget();
protected:
void initializeGL() override;
void paintGL() override;
private:
Q_DISABLE_COPY(QkGLWidget)
};
H2
OpenGL坐标系
在介绍OpenGL渲染函数之前,先解一下OpenGL的坐标系。
世界坐标系以屏幕中心为原点(0,0,0),当你面对屏幕时,右边是X正轴,上方是Y轴正轴,屏幕指向你的方向为Z轴负轴。窗口范围是从左至右是(-1,1),即屏幕左下角坐标为(-1,-1, 1),右上角坐标为(1,1, -1)。
H2
OpenGL基本数据类型
typedef unsigned int GLenum;
typedef unsigned char GLboolean;
typedef unsigned int GLbitfield;
typedef signed char GLbyte;
typedef short GLshort;
typedef int GLint;
typedef int GLsizei;
typedef unsigned char GLubyte;
typedef unsigned short GLushort;
typedef unsigned int GLuint;
typedef float GLfloat;
typedef float GLclampf;
typedef double GLdouble;
typedef double GLclampd;
typedef void GLvoid;
H2
在Qt中使用OpenGL渲染
在子类重载函数 paintGL() 中实现OpenGL的渲染,窗口的任何变化,如:移动、缩放、遮盖等操作都会触发系统发调用paintGL。
void QkGLWidget::paintGL()
{
// 将窗口的位平面区域设置为先前由glClearColor选择的值。
glClear(GL_COLOR_BUFFER_BIT);
// 渲染图元
glDrawArrays(GL_LINES, 0, 2);
}
我们看看该函数的定义:
void glDrawArrays( GLenum mode, GLint first, GLsizei count);
从上面的代码我们看不出渲染的数据在哪里,只知道有三个参数中GL_LINES表示画线,0和2也看不出跟坐标有关的数据。在OpenGL中,一般需要将顶点数据都保存到缓存对象(buffer object)中,它是OpenGL维护的一块内存区域。
H2
顶点缓存对象VBO - Vertex Buffer Object
顶点(Vertex)是OpenGL的基本图元数据,也可以说是所有图形最基础的数据。其中顶点缓存对象 (VBO) 是一种 OpenGL 功能,它提供将顶点数据(位置、法向量、颜色等)上传到视频设备以进行非即时模式渲染的方法。VBO 比立即模式渲染提供了显着的性能提升,主要是因为数据驻留在视频设备内存中而不是系统内存中,因此它可以直接由视频设备渲染。下面简单的示例代码展示了创建VBO并将顶点数据并写入内存。
void QkGLWidget::initializeGL()
{
// 初始化OpenGL函数
initializeOpenGLFunctions();
// 指定glClear用来清除颜色缓冲区的颜色(设置背景色)
RGBA_TYPE bg_color = GL_COLOR_BLACK;
glClearColor(RGB_TO_VERTEX(bg_color));
float points[] = {
0.0f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
};
// 生成缓冲对象并将缓冲对象名称存储到vbo
glGenBuffers(
1, // 指定要生成的缓冲区对象名称的数量。
&m_vbo // 存储生成的缓冲区对象名称
);
// 指定VBO的种类并激活VBO
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
// 上传顶点数据到视频设备
glBufferData(
GL_ARRAY_BUFFER,
6 * sizeof(float),
points,
GL_STATIC_DRAW // 指定数据存储的预期使用模式
);
QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
// VAO(Vertex Array Object)是指顶点数组对象,主要用于管理 VBO 或 EBO ,
// 减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,
// 高效地实现在顶点数组配置之间切换。
// 创建并绑定 VAO
f_extra->glGenVertexArrays(1, &m_vao);
f_extra->glBindVertexArray(m_vao);
// 设置着色器属性位置对应的数值
glVertexAttribPointer(
vPosition, // 这就是顶点着色器中输入变量的location值。
3, // 每个顶点的元素数目
GL_FLOAT, // 指定数组中每个组件的数据类型
GL_FALSE,
0, // 指定连续通用顶点属性之间的字节偏移量。 如果步幅为 0,则通用顶点属性被理解为紧密打包在数组中。 初始值为 0。
0 // 当数据指针为NULL时,说明该数据来源于顶点缓存对象VBO
);
// 启用VAO
glEnableVertexAttribArray(vPosition);
}
H1
OpenGL渲染管线
即使在缓存中有了顶点数据,这时渲染到窗口仍然无法看见,这是因为还没有使用着色器。在渲染之前,还必须了解渲染管线(绘图流水线)(Graphics pipeline),它是计算机图形系统将三维模型渲染到二维屏幕上的过程。简单地说,在计算机即将显示电子游戏或三维动画内的三维模型时,绘图流水线就是把该模型转换成屏幕画面的过程。了争取更多的优化机会,绘图流水线大多时候是用硬件实现的。
H2
GLSL
GLSL - OpenGL Shading Language 也称作 GLslang,是一个以C语言为基础的高阶着色语言。它是由 OpenGL ARB 所建立,提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言。
H2
着色器
着色器是使用GLSL语言来实现的小程序,OpenGL内部包含了所有的编译器工具,可以直接从着色器代码创建GPU所需的编译代码并执行。GLSL必须在运行时编译,这意味着每次启动程序时,所有的着色器将重新编译。在OpenGL中,会用到六种不同的着色阶段。其中最常用的包括顶点着色器(vertex shader)和片元着色器(fragment shader),前者用于处理顶点数据,后者用于处理光栅化后的片元数据。
最终生成的图像包含了屏幕上绘制的所有像素点。像素(pixel)是显示器上最小的可见单元。计算机将所有的像素保存到帧缓存(framebuffer)当中,后者由图形硬件设备管理的一块独立内存区域,可以直接映射到最终的显示设备上。
因此,简单来说,着色器建立了应用程序和图形设备之间的通信通道,给着色器提供数据,实现着色运算。
H2
动态编译GLSL
// 创建 vertext shader 对象
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
// 将shader中的源代码设置为string指定的字符串数组中的源代码。
// OpenGL 在调用 glShaderSource 时复制着色器源代码字符串,因此应用程序可以在函数返回后立即释放其源代码字符串副本。
glShaderSource(
vs, // glCreateShader创建的对象
1, // 指定字符串数组的个数。
&vertex_shader, // GLSL代码
NULL // 指定字符串数组的长度,如果 length 为 NULL,则获取字符串以null结尾的字符串。
);
// 编译着色器对象
glCompileShader(vs);
// 创建 fragment shader并编译
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &fragment_shader, NULL);
glCompileShader(fs);
// glCreateProgram 创建一个空的程序对象并返回一个可以引用它的非零值。
// 它可以附加着色器的对象, 这提供了一种机制来指定将链接以创建程序的着色器对象。
// 通过使用 glAttachShader来关联着色器对象,并使用 glLinkProgram 链接程序对象,在程序对象中创建一个或多个可执行文件。
// 当调用 glUseProgram 时,这些可执行文件成为当前状态的一部分。
// 程序可以通过调用 glDeleteProgram 来删除。
m_shader_program = glCreateProgram();
glAttachShader(m_shader_program, fs);
glAttachShader(m_shader_program, vs);
// 将将顶点属性索引与顶点着色器中的中的变量输入关联起来。
glBindAttribLocation(m_shader_program, vPosition, "in_Position");
glLinkProgram(m_shader_program);
glUseProgram(m_shader_program);
H1
渲染过程
综上所述,一个用来渲染图像的OpenGL程序需要执行的主要操作如下:
- 从OpenGL的几何图元中设置数据,用于构建形状。
- 使用不同的着色器(shader)对输入的图元数据执行计算操作,判断它们的位置、颜色以及其他渲染属性。
- 将输入图元的数学描述转换为与屏幕位置对应的像素片元(fragment)。这一步也称为光栅化(rasterization)。
- 最后、针对光栅化过程产生每个片元,执行片元着色器(fragment shader),从而决定这个片元的最终颜色和位置。
- 如有必要,还需要针对每个片元执行一个额外的操作,例如判断片元对应的对象是否可见,或者将片元的颜色与当前的颜色进行融合。
OpenGL是使用客户端-服务器端的形式实现的,我们编写的应用程序可以看作客户端,而计算机图形硬件厂商所提供的OpenGL实现可以看作服务器(客户端和服务器端并不是通过TCP/IP来通讯的)。在大多数现代的实现方案中,硬件图形加速器被用来完成大部分的OpenGL指令,它往往被构建在计算机的核心处理器中,或者作为一个独立的电路板插入到计算机主板上。