1.简介
此代码是基于Qt+OpenGL实现的,但是大部分的代码是OpenGL,Qt封装了一些类,方便使用。
2.准备工作
QOpenGLWidget提供了三个便捷的虚函数,可以重写,用来重写实现典型的OpenGL任务。不需要GLFW。
- paintGL:渲染OpenGL场景。widget需要更新时调用。
- resizeGL:设置OpenGL视口、投影等,widget调整大小(或首次显示)时调用。
- initializeGL:设置OpenGL资源和状态。第一次调用resizeGL/paintGL之前调用一次。
①如果需要paintGL以外的位置重新绘制,应调用widget的update()函数重新绘制。
②调用paintGL、resizeGL、initializeGL时,widget的OpenGL呈现上下文将变为当前。如果需要从其他位置调用OpenGL API函数,则必须首先调用makeCurrent()。
QOpenGLFunctions_x_x_Core:不需要GLAD
QOpenGLFunctions_x_x_Core提供了OpenGL X.X版本核心模式的所有功能。是对OpenGL函数的封装。
//初始化OpenGL函数,将Qt里的函数指针指向显卡的函数
initializeOpenGLFunctions()
自定义Widget类继承上述两个类,重写以下的几个函数,实现OpenGL任务,代码如下。
#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
class MyOpenGLWidget : public QOpenGLWidget,public QOpenGLFunctions_3_3_Core
{
public:
MyOpenGLWidget(QWidget *parent = nullptr);
protected:
virtual void initializeGL();
virtual void paintGL();
virtual void resizeGL(int w, int h);
};
#endif // MYOPENGLWIDGET_H
ui界面上拖拽以下openglwidget。
然后将这个openglwidget提升为我们自定义的 MyOpenGLWidget 。
以上准备工作已经完成,只要写重写paintGL、resizeGL、initializeGL这个三个函数。
3.你好,三角形
先了解以下几点:
3.1图形渲染管线
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。 注意蓝色部分代表的是我们可以注入自定义的着色器的部分。
以下概括性地解释一下渲染管线的每个部分:
- 点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
- 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入,并所有的点装配成指定图元的形状。
- 几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
- 光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
- 片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
- 测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。
3.2标准化设备坐标(Normalized Device Coordinates, NDC)
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):
与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。
3.3链接顶点属性
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据会被解析为下面这样子:
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
- 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们:
- 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用
layout(location = 0)
定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0
。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。 - 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
- 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中
vec*
都是由浮点数值组成的)。 - 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个
float
之后,我们把步长设置为3 * sizeof(float)
。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 - 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。
最后应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。
代码实现。
#include "myopenglwidget.h"
//顶点坐标
GLfloat vertices[] = {
// Positions
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
//顶点着色器语言
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position,1.0);\n"
"}\n\0";
//片段着色器语言
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f,0.5f,0.2f,1.0f);\n"
"}\n\0";
GLuint VBO, VAO;//声明VAO、VBO
GLuint shaderProgram;//声明着色器程序
MyOpenGLWidget::MyOpenGLWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
}
void MyOpenGLWidget::initializeGL()
{
//初始化opengl的功能,必须调用
initializeOpenGLFunctions();
//创建顶点着色器
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//第二参数指定了传递的源码字符串数量
glCompileShader(vertexShader);
//创建片段着色器
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);//第二参数指定了传递的源码字符串数量
glCompileShader(fragmentShader);
//创建一个着色器程序
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
//使用完成后释放内存
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
//创建VAO VBO
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);//绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO);//顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//把顶点数据复制到缓冲的内存中GL_STATIC_DRAW :数据不会或几乎不会改变。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
//解绑VAO
glBindVertexArray(0);
}
void MyOpenGLWidget::paintGL()
{
//设置清空屏幕所用的颜色
glClearColor(0.2f,0.3f,0.3f,1.0f);
//清除颜色缓冲
glClear(GL_COLOR_BUFFER_BIT);
//激活这个着色器程序对象
glUseProgram(shaderProgram);
glBindVertexArray(VAO);//绑定VAO
glDrawArrays(GL_TRIANGLES,0,3);
glBindVertexArray(0); //解绑VAO
}
void MyOpenGLWidget::resizeGL(int w, int h)
{
}