OpenGL简介
OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。
这个接口由近350个不同的函数调用组成,用来绘制从简单的图形比特到复杂的三维景象。而另一种程序接口系统是仅用于Microsoft Windows上的Direct3D。OpenGL常用于CAD、虚拟现实、科学可视化程序和电子游戏开发。
OpenGL的高效实现(利用了图形加速硬件)存在于Windows,部分UNIX平台和Mac OS。这些实现一般由显示设备厂商提供,而且非常依赖于该厂商提供的硬件。开放源代码库Mesa是一个纯基于软件的图形API,它的代码兼容于OpenGL。但是,由于许可证的原因,它只声称是一个“非常相似”的API。
Qt中的OpenGL
通过继承QOpenGLWidget和QOpenGLExtraFunctions,重载void initializeGL(),void paintGL()还有void resizeGL(int w, int h)三个函数进行绘图
创建一个QWidget
继承QOpenGLWidget和QOpenGLExtraFunctions
#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
class OpenGLWidget : public QOpenGLWidget, public QOpenGLExtraFunctions
重载initializeGL(),paintGL()和resizeGL(int w, int h)
protected:
virtual void initializeGL() override;
virtual void resizeGL(int x, int y) override;
virtual void paintGL() override;
在initializeGL()函数中做一些基本的初始化
OpenGL本身的API只提供了“函数定义”,所以所有的实现实际上是操作系统或者其它库的工作。
初始化OpenGL函数的目的,就是加载这些OpenGL的实现。
initializeOpenGLFunctions();
设置一些OpenGL的特性,例如深度测试。
深度测试是指,“近处的物体会遮挡远处的物体”这种在现实中最为基础的法则。
glEnable(GL_DEPTH_TEST);
设置一下刷新时的背景颜色,四个参数分别为R,G,B,A,的值,取值范围[0, 1]。
glClearColor(0, 0.5, 0.7, 1);
创建缓存
OpenGL的世界中,想要绘制3D图像,3个点,就可以确认一个三角形。将顶点中的x,y,z每个值,一个一个的放到缓存中。
OpenGL中存在两个概念:
VAO指的是顶点列表对象,VBO指的是顶点缓存对象。
VAO可以帮助我们在绘制多个3D物品时,将各自物品的绘制状态给隔离。即:每个物品都可以有自己的顶点缓存,shader,以及其它的各种各样的状态。VAO会帮你把这些状态保存下来,下一次执行的时候,你就不需要重复的设置这些状态了。简单来说就是:一次设置,到处使用。
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
QOpenGLVertexArrayObject m_vao;
QOpenGLBuffer m_vbo;
m_vao.create();
m_vbo.create();
Shader
需要使用Shader控制渲染流程。在Qt中,提供了QOpenGLShaderProgram类帮助用户来使用shader。
in 用在函数的参数中,表示这个参数是输入的,在函数中改变这个值,并不会影响对调用的函数产生副作用。(相当于C语言的传值),这个是函数参数默认的修饰符
out 用在函数的参数中,表示该参数是输出参数,值是会改变的。
#include <QOpenGLShaderProgram>
QOpenGLShaderProgram *m_program;
m_program = new QOpenGLShaderProgram();
///gl_Position vec4 输出属性-变换后的顶点的位置,用于后面的固定的裁剪等操作。
/// 所有的顶点着色器都必须写这个值。
/// 顶点着色器(Vertex Shader)
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, u8R"(
#version 330 core
in vec3 vPos;
in vec2 vTexture;
out vec2 oTexture;
void main()
{
gl_Position = vec4(vPos, 1.0);
oTexture = vTexture;
}
)");
///sampler2D 二维纹理句柄
/// uniform 一致变量。这个值在编译时期是未知的是由着色器外部初始化的。
///只能在全局范围进行声明。
///gl_FragColor vec4 输出的颜色用于随后的像素操作
/// Fragment Shader(片段着色器)
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, u8R"(
#version 330 core
in vec2 oTexture;
uniform sampler2D uTexture;
void main()
{
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
//gl_FragColor = texture(uTexture, oTexture);
}
)");
顶点缓存
一个一维数组,存储了顶点信息
float _vertex[] = {
-0.5, -0.5, 0.0,
-0.5, 0.5, 0.0,
0.5, 0.5, 0.0,
0.5, -0.5, 0.0,
};
可以将其看作四个点,按照x,y,z,x,y,z,x,y,z这个顺序排列。
使用vPos这个输入变量的名字来告诉OpenGL,我们的顶点缓存中,是按照vPos这个变量的类型,即vec3的标准来保存顶点信息的。
m_program->bind();
// 绑定顶点坐标信息, 从0 * sizeof(float)字节开始读取3个float, 因为一个顶点有3个float数据, 所以下一个数据需要偏移3 * sizeof(float)个字节
m_program->setAttributeBuffer("vPos", GL_FLOAT, 0, 3, 3 * sizeof(float));
m_program->enableAttributeArray("vPos");
释放VAO和Shader
m_program->release();
m_vao.release();
完整的初始化代码
void OpenGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glEnable(GL_DEPTH_TEST);
glClearColor(0, 0.5, 0.7, 1);
m_vao.create();
m_vbo.create();
m_program = new QOpenGLShaderProgram();
///gl_Position vec4 输出属性-变换后的顶点的位置,用于后面的固定的裁剪等操作。
/// 所有的顶点着色器都必须写这个值。
/// 顶点着色器(Vertex Shader)
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, u8R"(
#version 330 core
in vec3 vPos;
in vec2 vTexture;
out vec2 oTexture;
void main()
{
gl_Position = vec4(vPos, 1.0);
oTexture = vTexture;
}
)");
///sampler2D 二维纹理句柄
/// uniform 一致变量。这个值在编译时期是未知的是由着色器外部初始化的。
///只能在全局范围进行声明。
///gl_FragColor vec4 输出的颜色用于随后的像素操作
/// Fragment Shader(片段着色器)
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, u8R"(
#version 330 core
in vec2 oTexture;
uniform sampler2D uTexture;
void main()
{
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
//gl_FragColor = texture(uTexture, oTexture);
}
)");
m_program->link();
float _vertex1[] = {
-0.5, -0.5, 0.0,
-0.5, 0.5, 0.0,
0.5, 0.5, 0.0,
0.5, -0.5, 0.0,
};
// 顶点缓存中前三个是顶点坐标, 后两个是纹理坐标, 一个顶点由5个float值组成
float _vertex0[] = {
// 顶点 纹理
-1, 1, 0, 0, 1, // 左上
-1, -1, 0, 0, 0, // 左下
1, -1, 0, 1, 0, // 右下
1, 1, 0, 1, 1, // 右上
};
m_vao.bind();
m_vbo.bind();
//m_vbo.allocate(_vertex, 9 * sizeof(float));
m_vbo.allocate(_vertex1, sizeof(_vertex1));
m_program->bind();
// 绑定顶点坐标信息, 从0 * sizeof(float)字节开始读取3个float, 因为一个顶点有5个float数据, 所以下一个数据需要偏移3 * sizeof(float)个字节
m_program->setAttributeBuffer("vPos", GL_FLOAT, 0, 3, 3 * sizeof(float));
m_program->enableAttributeArray("vPos");
m_program->release();
m_vao.release();
}
绘制图形
void OpenGLWidget::paintGL()
{
m_vao.bind();
m_program->bind();
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
//GL_TRIANGLE_FAN绘制各三角形形成一个扇形序列,以v0为起始点,(v0,v1,v2)、(v0,v2,v3)、(v0,v3,v4)。
m_program->release();
m_vao.release();
}
一般情况下有三种绘制一系列三角形的方式,分别是
- GL_TRIANGLES
- GL_TRIANGLE_STRIP
- GL_TRIANGLE_FAN
GL_TRIANGLES是以每三个顶点绘制一个三角形。第一个三角形使用顶点v0,v1,v2,第二个使用v3,v4,v5,以此类推。如果顶点的个数n不是3的倍数,那么最后的1个或者2个顶点会被忽略。
GL_TRIANGLE_STRIP则稍微有点复杂。
其规律是:
构建当前三角形的顶点的连接顺序依赖于要和前面已经出现过的2个顶点组成三角形的当前顶点的序号的奇偶性(如果从0开始):
- 如果当前顶点是奇数: 组成三角形的顶点排列顺序:T = [n-1、n-2、n].
- 如果当前顶点是偶数: 组成三角形的顶点排列顺序:T = [n-2、n-1、n].
以上图为例,第一个三角形,顶点v2序号是2,是偶数,则顶点排列顺序是v0,v1,v2。第二个三角形,顶点v3序号是3,是奇数,则顶点排列顺序是v2,v1,v3,第三个三角形,顶点v4序号是4,是偶数,则顶点排列顺序是v2,v3,v4,以此类推。
注意:顶点个数n至少要大于3,否则不能绘制任何三角形。
GL_TRIANGLE_FAN绘制各三角形形成一个扇形序列,以v0为起始点,(v0,v1,v2)、(v0,v2,v3)、(v0,v3,v4)。
运行结果
参考学习
在Qt中使用OpenGL(一)
在Qt中使用OpenGL(二)
理解GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN绘制三角形序列的三种方式