QT+ OpenGL
本篇完整工程见gitee:QTOpenGL
对应点的tag,由turbolove提供技术支持,您可以关注博主或者私信博主。
什么是opengl
open graphics library 他是一个由Khronos组织制定并且维护的规范
opengl核心是一个c库,同时也支持多种语言的派生
核心模式(core-profile):
● 也叫可编程管线,提供了更多的灵活性,更高的效率,更重要的是可以深入的理解图形编程。
立即渲染模式(Immediate mode)
● 早期的OpenGL使用的模式(也就是固定渲染管线)
● OpenGL的大多数功能都被库隐藏起来,容易使用和理解,但是效率低下
● 开发者很少能控制OpenGL如何进行计算
● 因此从OpenGL3.2开始推出核心模式
状态机(state machine)
● OpenGL是一个聚到的状态机 - 描述如何操作的所有变量的大集合
● OpenGL的状态通常被称为上下文Context
● 状态设置函数(State-changing Function)
● 状态应用函数(State-using Function)
对象(Object)
● 一个对象是指一些选项的集合,代表OpenGL状态的一个子集
● 当前状态只有一份,如果每次显示不同的效果,都重新配置会很麻烦
● 我们需要使用一些小助理(对象),帮忙记录某些状态信息,以便复用
// 创建对象 GLunit objId = 0; glGenObject(1, &objId); // 绑定对象到上下文 glBindObject(GL_WINDOW_TARGET, objId); // 设置GL_WINDOW_TARGET对象的一些选项 glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800); glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600); // 将上下文的GL_WINDOW_TARGET对象设置成默认值 glBindObject(GL_WINDOW_TARGET, 0); // 一旦我们重新绑定这个对象到GL_WINDOW_TARGET位置,这些选项就会重新生效
QOpenGLWidget:不需要GLFW
QOpenGLWidget提供了三个便捷的虚拟函数,可以冲在,用来实现典型的opengl任务
● paintGL:渲染opengl场景,widget需要更新时候调用
● resizeGL:设置opengl视口,投影等,widget调整大小(或者首次显示)时候调用
● initializeGL:设置opengl资源和状态,第一次调用resizeGL()或者paintGL()之前调用一次
如果需要从paintGL()意外的位置触发重新绘制(典型示例是使用计时器设置场景动画),则应该调用widget的update()函数来安排更新。
调用paintGL()、resizeGL()、initializeGL()时,widget的opengl呈现上下文变为当前。如果需要从其他位置(例如在widget的构造函数或者自己的绘制函数中)调用openglAPI函数,则必须首先调用makeCurrent()。
QOpenGLFunction_X_X_Core:不需要GLAD
QOpenGLFunction_X_X_Core提供OpenGL x.x版本核心模式的所有功能,是对OpenGL函数的封装
turboopenglwidget.h
#ifndef QTOPENGL_TURBOOPENGLWIDGET_H #define QTOPENGL_TURBOOPENGLWIDGET_H #include <QOpenGLWidget> #include <QOpenGLFunctions_4_5_Core> class TurboOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core { Q_OBJECT public: explicit TurboOpenGLWidget(QWidget *parent = 0); protected: void initializeGL() override; void paintGL() override; void resizeGL(int w, int h) override; private: }; #endif //QTOPENGL_TURBOOPENGLWIDGET_H
turboopenglwidget.cpp
#include "turboopenglwidget.h" TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) { } void TurboOpenGLWidget::initializeGL() { initializeOpenGLFunctions(); QOpenGLWidget::initializeGL(); } void TurboOpenGLWidget::paintGL() { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); QOpenGLWidget::paintGL(); } void TurboOpenGLWidget::resizeGL(int w, int h) { QOpenGLWidget::resizeGL(w, h); }
你好,三角形
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f, };
标准化设备坐标:
订单着色器中处理过后,应该就是标准化设备坐标,x、y、z的值在-1.0到1.0的一小段空间(立方体)。落在范围外的坐标都会被裁减。
顶点输入
● 他会在GPU上创建内存,用于存储我们的顶点数据
○ 通过点缓冲对象(vertex buffer object , VBO)管理
● 顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
● 配置OpenGL如何解释这些内存
○ 通过顶点数组对象(vertex array object , VAO)管理
数组力的每一个项都对应一个属性的解析。
OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型(每个缓冲类型类似于前面说的子集,每个VBO是一个小助理)。
VAO并不保存实际数据,而是放顶点结构定义。
// 创建VAO和VBO对象,并且赋予ID unsigned int VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); // 绑定VAO和VBO对象 glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // 为当前绑定到target的缓冲区对象创建一个新的数据存储 // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告知显卡如何解析缓冲里面的属性值 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(0); // 释放VAO和VBO对象 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);
glBufferData
是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
● 第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。
● 第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。
● 第三个参数是我们希望发送的实际数据。
● 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
○ GL_STATIC_DRAW :数据不会或几乎不会改变。
○ GL_DYNAMIC_DRAW:数据会被改变很多。
○ GL_STREAM_DRAW :数据每次绘制时都会改变。
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。我们会在后面详细解释这个参数。
顶点着色器
#version 330 core layout (location = 0) in vec3 aPos; void main() { gl_Position = vec4(aPos, 1.0); }
// 创建顶点着色器 unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader ,1 ,&vertexShaderSource, NULL); glCompileShader(vertexShader);
glShaderSource
● 第一个参数是函数把要编译的着色器对象。
● 第二参数指定了传递的源码字符串数量,这里只有一个。
● 第三个参数是顶点着色器真正的源码,
● 第四个参数我们先设置为NULL。
片段着色器
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }
// 创建顶片段着色器 unsigned int fragShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragShader ,1 ,&fragShaderSource, NULL); glCompileShader(fragShader);
链接着色器
unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragShader);
本节代码
turboopenglwidget.cpp
#include "turboopenglwidget.h" float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f, }; const char *vertexShaderSource = "#version 330 core \n" "layout (location = 0) in vec3 aPos;\n" "\n" "void main()\n" "{\n" "\tgl_Position = vec4(aPos, 1.0);\n" "}"; const char *fragShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "} "; // 创建VAO和VBO对象,并且赋予ID unsigned int VBO, VAO; unsigned int shaderProgram; TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) { } void TurboOpenGLWidget::initializeGL() { initializeOpenGLFunctions(); glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); // 绑定VAO和VBO对象 glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // 为当前绑定到target的缓冲区对象创建一个新的数据存储 // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告知显卡如何解析缓冲里面的属性值 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(0); // 释放VAO和VBO对象 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // 创建顶点着色器 unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader ,1 ,&vertexShaderSource, NULL); glCompileShader(vertexShader); // 创建顶片段着色器 unsigned int fragShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragShader ,1 ,&fragShaderSource, NULL); glCompileShader(fragShader); shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragShader); } void TurboOpenGLWidget::paintGL() { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); } void TurboOpenGLWidget::resizeGL(int w, int h) { }
元素缓冲对象EBO
可以绘制两个三角形来组合成一个矩形,这会生成下面的顶点的集合:
float vertices[] = { // 第一个三角形 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, 0.5f, 0.0f, // 左上角 // 第二个三角形 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 };
值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引:
代码展示:
#include "turboopenglwidget.h" float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 }; unsigned int indices[] = { 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 }; const char *vertexShaderSource = "#version 330 core \n" "layout (location = 0) in vec3 aPos;\n" "\n" "void main()\n" "{\n" "\tgl_Position = vec4(aPos, 1.0);\n" "}"; const char *fragShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "} "; // 创建VAO和VBO对象,并且赋予ID unsigned int VBO, VAO; unsigned int EBO; unsigned int shaderProgram; TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) { } void TurboOpenGLWidget::initializeGL() { initializeOpenGLFunctions(); glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 绑定VAO和VBO对象 glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // 为当前绑定到target的缓冲区对象创建一个新的数据存储 // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告知显卡如何解析缓冲里面的属性值 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 释放VAO和VBO对象 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // 创建顶点着色器 unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader ,1 ,&vertexShaderSource, NULL); glCompileShader(vertexShader); // 创建顶片段着色器 unsigned int fragShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragShader ,1 ,&fragShaderSource, NULL); glCompileShader(fragShader); shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragShader); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } void TurboOpenGLWidget::paintGL() { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); // glDrawArrays(GL_TRIANGLES, 0, 6); // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); } void TurboOpenGLWidget::resizeGL(int w, int h) { }
QT交互
● 如果需要从paintGL()以外的位置触发重新绘制(典型示例是使用计时器设置场景动画), 则应该调用widget的update()函数来安排更新
● 调用paintGL()、resizeGL()和initializeGL()时,widget的OpenGL呈现上下文将变为当前。如果需要从其他位置(例如,在widget的构造函数或自己的绘制函数中)调用opengl API函数,则必须首先调用makeCurrent()。
代码示例:
turboopenglwidget.h
#ifndef QTOPENGL_TURBOOPENGLWIDGET_H #define QTOPENGL_TURBOOPENGLWIDGET_H #include <QOpenGLWidget> #include <QOpenGLShaderProgram> #include <QOpenGLFunctions_4_5_Core> class TurboOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core { Q_OBJECT public: enum Shape { None, Rect, Circle, Triangle, }; explicit TurboOpenGLWidget(QWidget *parent = 0); ~TurboOpenGLWidget() override; void drawShape(Shape shape); void setWireFrame(bool mode); protected: void initializeGL() override; void paintGL() override; void resizeGL(int w, int h) override; private: Shape shape_; QOpenGLShaderProgram shader_program_; }; #endif //QTOPENGL_TURBOOPENGLWIDGET_H
turboopenglwidget.cpp
#include <iostream> #include "turboopenglwidget.h" float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 }; unsigned int indices[] = { 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 }; // 创建VAO和VBO对象,并且赋予ID unsigned int VBO, VAO; unsigned int EBO; TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) { } TurboOpenGLWidget::~TurboOpenGLWidget() { makeCurrent(); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glDeleteVertexArrays(1, &VAO); doneCurrent(); } void TurboOpenGLWidget::initializeGL() { initializeOpenGLFunctions(); glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 绑定VAO和VBO对象 glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // 为当前绑定到target的缓冲区对象创建一个新的数据存储 // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告知显卡如何解析缓冲里面的属性值 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 释放VAO和VBO对象 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); bool success = false; shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/resources/shader.vert"); shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/resources/shader.frag"); success = shader_program_.link(); if(!success) { std::cout << "shader is failed" << std::endl; } // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } void TurboOpenGLWidget::paintGL() { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); shader_program_.bind(); glBindVertexArray(VAO); // glDrawArrays(GL_TRIANGLES, 0, 6); // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); switch(shape_) { case Rect: glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); break; } update(); } void TurboOpenGLWidget::resizeGL(int w, int h) { } void TurboOpenGLWidget::drawShape(TurboOpenGLWidget::Shape shape) { shape_ = shape; } void TurboOpenGLWidget::setWireFrame(bool mode) { makeCurrent(); if(mode) { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } else { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } }
mainwidow.h
#ifndef QTOPENGL_MAINWINDOW_H #define QTOPENGL_MAINWINDOW_H #include <QMainWindow> QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; protected slots: void drawRect(); void clearPic(); void lineModel(const bool &mode); private: Ui::MainWindow *ui; QToolBar *tool_bar_{nullptr}; }; #endif //QTOPENGL_MAINWINDOW_H
mainwidow.cpp
#include "mainwindow.h" #include "ui_MainWindow.h" #include <QToolBar> #include <QAction> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setCentralWidget(ui->openGLWidget); tool_bar_ = new QToolBar(this); auto *action = new QAction(tr("绘制矩形"), this); auto *action2 = new QAction(tr("清空图形"), this); auto *action3 = new QAction(tr("线框模式"), this); action3->setCheckable(true); connect(action, &QAction::triggered, this, &MainWindow::drawRect); connect(action2, &QAction::triggered, this, &MainWindow::clearPic); connect(action3, &QAction::triggered, this, &MainWindow::lineModel); tool_bar_->addAction(action); tool_bar_->addAction(action2); tool_bar_->addAction(action3); addToolBar(tool_bar_); } MainWindow::~MainWindow() { delete ui; } void MainWindow::drawRect() { ui->openGLWidget->drawShape(TurboOpenGLWidget::Rect); } void MainWindow::clearPic() { ui->openGLWidget->drawShape(TurboOpenGLWidget::None); } void MainWindow::lineModel(const bool &mode) { ui->openGLWidget->setWireFrame(mode); }
GLSL
OpenGL Shading Languaage
一个典型的shader程序结构:
#version version_number in type in_variable_name; in type in_variable_name; out type out_variable_name; uniform type uniform_name; int main() { // 处理输入并进行一些图形操作 ... // 输出处理过的结果到输出变量 out_variable_name = weird_stuff_we_processed; }
我们能声明的顶点数量是有限的,可以通过下面的代码获取:
int nrAttributes; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件可能会允许更多的顶点属性。
GLSL支持的类型
类型:
GLSL中包含C等其他语言大部分默认的基础数据类型
int float double uint bool
GLSL也有两种容器类型
类型 | 含义 |
vecn |
包含n 个float分量的默认向量 |
bvecn |
包含n 个bool分量的向量 |
ivecn |
包含n 个int分量的向量 |
uvecn |
包含n 个unsigned int分量的向量 |
dvecn |
包含n 个double分量的向量 |
向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:
vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy; vec2 vect = vec2(0.5, 0.7); vec4 result = vec4(vect, 0.0, 0.0); vec4 otherResult = vec4(result.xyz, 1.0);
输入输出
● 在发送方着色器声明一个输出
● 在接收方着色器声明一个类似的输入
● 当类型和名称都一致的时候OpenGL会把两个变量链接到一起(在链接程序对象时候完成)
顶点着色器
#version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 void main() { gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色 }
片段着色器
#version 330 core out vec4 FragColor; in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) void main() { FragColor = vertexColor; }
顶点着色器接收的是一种特殊形式的输入,否则就会效率低下
从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据(metadata)指定输入变量,这样我们才可以在CPU上配置顶点属性。例如: layout(location = 0)。 layout这个标识,使得我们能把它链接到顶点数据。
可以忽略layout( location = 0) 标识符,通过在OPenGL代码中使用glGetAttrribLocation查询属性位置(Location),或者是glBindAttribLocation属性位置值(Location),但是推荐在着色器中设置他们,这样会更容易理解而且节省你和(OpenGL)的工作量
#include <iostream> #include "turboopenglwidget.h" float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 }; unsigned int indices[] = { 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 }; // 创建VAO和VBO对象,并且赋予ID unsigned int VBO, VAO; unsigned int EBO; TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) { } TurboOpenGLWidget::~TurboOpenGLWidget() { makeCurrent(); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glDeleteVertexArrays(1, &VAO); doneCurrent(); } void TurboOpenGLWidget::initializeGL() { initializeOpenGLFunctions(); glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 绑定VAO和VBO对象 glBindVertexArray(VAO); shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/resources/shader.vert"); shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/resources/shader.frag"); bool success = false; success = shader_program_.link(); if(!success) { std::cout << "shader is failed" << std::endl; } glBindBuffer(GL_ARRAY_BUFFER, VBO); // 为当前绑定到target的缓冲区对象创建一个新的数据存储 // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告知显卡如何解析缓冲里面的属性值 int location = shader_program_.attributeLocation("aPos"); glVertexAttribPointer(location, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(location); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 释放VAO和VBO对象 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } void TurboOpenGLWidget::paintGL() { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); shader_program_.bind(); glBindVertexArray(VAO); // glDrawArrays(GL_TRIANGLES, 0, 6); // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); switch(shape_) { case Rect: glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); break; } update(); } void TurboOpenGLWidget::resizeGL(int w, int h) { } void TurboOpenGLWidget::drawShape(TurboOpenGLWidget::Shape shape) { shape_ = shape; } void TurboOpenGLWidget::setWireFrame(bool mode) { makeCurrent(); if(mode) { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } else { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } }
Uniform
另一种从CPU的应用,向GPU中的着色器发送数据的方式
uniform是全局的,可以被任意的着色器程序在任一阶段访问
#version 330 core out vec4 FragColor; uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 void main() { FragColor = ourColor; }
如果声明了一个uniform却没有用过,编译器会默认移除这个变,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,切记
这次我们不去给像素单独传递一个颜色,而是让他随着时间改变颜色。
OpenGL在其核心是一个C库,所以他不支持类型重载,在函数参数类型不同时候就要为其定义新的函数,glUniform是一个典型的例子。这个函数有特定的后缀,用来标识设定的uniform的类型。可能的后缀有:
后缀 | 含义 |
f |
函数需要一个float作为它的值 |
i |
函数需要一个int作为它的值 |
ui |
函数需要一个unsigned int作为它的值 |
3f |
函数需要3个float作为它的值 |
fv |
函数需要一个float向量/数组作为它的值 |
QT 为我们封装了这个函数,因此我们可以不用太过关注该函数的详细内容,但是你要是用原生的OpenGL的话,需要关注该函数。
纹理
当我们需要给图形赋予真实的颜色的时候,不大可能使用前面的方法为每一个顶点指定第一个颜色,通常我们会采用纹理贴图。
每个顶点关联一个纹理坐标(Texture Coordinate),之后在图形的其他片段上进行片段插值
我们只需要告诉OpenGL如何对纹理采样即可
顶点着色器
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor; layout (location = 1) in vec2 aTexCord; out vec3 ourColor; // 向片段着色器输出一个颜色 out vec2 texCord; // 向片段着色器输出一个颜色 void main() { gl_Position = vec4(aPos, 1.0); ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 texCord = aTexCord; }
片段着色器
#version 330 core out vec4 FragColor; in vec3 ourColor; in vec2 texCord; uniform sampler2D texture0; void main() { FragColor = texture(texture0, texCord); }
对应的显示代码:
#include <iostream> #include "turboopenglwidget.h" float vertices[] = { 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上角 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f // 左上角 }; unsigned int indices[] = { 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 }; // 创建VAO和VBO对象,并且赋予ID unsigned int VBO, VAO; unsigned int EBO; TurboOpenGLWidget::TurboOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) { connect(&timer, &QTimer::timeout, this, &TurboOpenGLWidget::timeout); // timer.start(100); } TurboOpenGLWidget::~TurboOpenGLWidget() { if(!isValid()) return; makeCurrent(); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glDeleteVertexArrays(1, &VAO); doneCurrent(); } void TurboOpenGLWidget::initializeGL() { initializeOpenGLFunctions(); glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 绑定VAO和VBO对象 glBindVertexArray(VAO); shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/resources/shader.vert"); shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/resources/shader.frag"); bool success = false; success = shader_program_.link(); if(!success) { std::cout << "shader is failed" << std::endl; } glBindBuffer(GL_ARRAY_BUFFER, VBO); // 为当前绑定到target的缓冲区对象创建一个新的数据存储 // 如果data不是NULL, 则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告知显卡如何解析缓冲里面的属性值 int location = shader_program_.attributeLocation("aPos"); glVertexAttribPointer(location, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void *)0); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(location); int location2 = shader_program_.attributeLocation("aColor"); glVertexAttribPointer(location2, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void *)(3*sizeof(float))); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(location2); int location3 = shader_program_.attributeLocation("aTexCord"); glVertexAttribPointer(location3, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void *)(6*sizeof(float))); // 开启VAO管理的第一个属性的值 glEnableVertexAttribArray(location3); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); texture_wall_ = new QOpenGLTexture(QImage(":/resources/wall.jpg")); // 释放VAO和VBO对象 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } void TurboOpenGLWidget::paintGL() { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); shader_program_.bind(); glBindVertexArray(VAO); // glDrawArrays(GL_TRIANGLES, 0, 6); // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); switch(shape_) { case Rect: texture_wall_->bind(); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); break; } update(); } void TurboOpenGLWidget::resizeGL(int w, int h) { } void TurboOpenGLWidget::drawShape(TurboOpenGLWidget::Shape shape) { shape_ = shape; } void TurboOpenGLWidget::setWireFrame(bool mode) { makeCurrent(); if(mode) { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } else { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } } #include <QTime> void TurboOpenGLWidget::timeout() { if(shape_) { return; } makeCurrent(); int time = QTime::currentTime().second(); float green = (sin(time) / 2.0f) + 0.5f; shader_program_.setUniformValue("ourColor", 0.0f, green, 0.0f, 1.0f); }
纹理单元
OpenGL保证至少16个纹理单元,也就是说你可以激活从GL_TEXTURE0到GL_TEXTURE15。他们都是按照顺序定义的, GL_TEXTURE0+8可以获得GL_TEXTURE8
以下是QT主要代码,在gitee项目中查看完整代码。
texture_wall_ = new QOpenGLTexture(QImage(":/resources/wall.jpg").mirrored()); texture_le_ = new QOpenGLTexture(QImage(":/resources/awesomeface.png").mirrored()); shader_program_.bind(); shader_program_.setUniformValue("textureWall", 0); shader_program_.setUniformValue("textureSmile", 1);
纹理环绕
环绕方式 | 描述 |
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
相关代码请到gitee查看,这里不复制
纹理过滤
纹理坐标不依赖于分辨率,OpenGL需要知道怎么将纹理像素映射到纹理坐标;
可以想象你打开一张图片,不断放大,会发现它是由无数像素点组成的,这个点就是纹理像素
● 纹理坐标的精度是无限的,可以是任意浮点值
● 纹理像素是有限的(图片分辨率)
● 一个像素需要一个颜色
● 所谓采样就是通过纹理坐标,问图片要纹理像素的颜色值
大图片贴小面片时:纹理的精度高,相邻纹理像素往往色差不打,无需融合,直接就近选取即可。
主要函数:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
相关代码请到gitee查看,这里不复制
多级渐远纹理
简单来说就是一系列的纹理图像,根据观察者与物体的距离,参考临界值,选择最适合物体的距离的那个纹理
OpenGL有一个glGenerateMipmaps函数,可以生产多级渐远纹理
过滤方式 | 描述 |
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
主要函数:
texture_small_->generateMipMaps(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
相关代码请到gitee查看,这里不复制