对于opengl的学习来说,绘制一个三角形是学习一种计算机语言时的一个hello world级的入门程序,个人觉得相比主流语言的helloworld,openGL的入门确实是有一些劝退,虽然说有不错的教程,但简明与全面不可兼得,很容易面对教程中一大堆概念和术语而摸不到头脑,本文试图用“相对”简单和直观的方式让人成功的绘制出第一个三角形。对于使用QT的同学,可以直接从文末的链接下载完整代码,自己修改其中的参数观察变化,这样理解起来更快。希望能让使用QT并且想学习openGL的人踏出第一步,而不是被画一个三角形拒之门外。

文章目录

  • 前言
  • 环境
  • 论绘制一个三角形都需要什么
    • 比较重要的组成部分
      • 各部分关系图
      • 一种可行的绘制流程
  • 编写代码
    • 关于QT使用openGL
    • 按步骤来
      • 创建openGL程序
      • 编写顶点着色器程序和片段着色器并添加到openGL程序中去
      • 创建并绑定VAO
      • 创建并绑定VBO
      • 将数据存入VBO
      • 告诉openGL该如何分配和使用数据
      • 绘制
    • 完整的MyWidget.cpp代码
    • 绘制最终效果
  • 完整项目代码

前言

本人也是初学openGL,而且代码部分会出现QT封装类和openGL原生函数混用的情况(QT的一些封装对比原生确实方便一些,但是缺点是文档不够亲民,示例代码少,有时会不知该如何使用),对于并不使用QT的同学来说,可以仅参考各部分组成关系绘制流程,我觉得应该还是有助于初期对openGL的理解的。

隔了一段时间回头来修改了一下这篇文章,想让他更加详细亲民。不过仔细想了想,感觉学习openGL最好的入门方法就是找一个在你的环境下可以编译运行的绘制三角形或正方形的openGL代码,结合着各种教程,修改其中参数,观察变化来理解其中的意义。
虽然只是绘制一个三角形,但是需要相对较多的准备知识,而且直到最后你能将这些知识组合起来并正确组织代码之前,你无法得到任何的反馈,因为这个过程几乎已经没法再分解了,你没办法先画出一个点,再画出一条线,进而画出一个三角形,因为openGL的绘制过程并不是这种逻辑,如果你只是对着文字教程一点一点编写代码,可能很久都没法得到正确的结果,也不知道是哪里出错了,如此既会浪费时间,又会有挫败感。所以如果你对这个入门感到头痛,那么就先去找一份适合自己的可用代码吧。

本文不会介绍的很全面,只是希望能够通过此文让同学们跨过opengl的门槛,更加轻松的去理解和学习其他人的教程,这里也给出两个教程链接:

一个比较全面系统的教程: LearnOpenGL CN

和我一样使用QT的同学在学习上方教程时如果想知道QT做了哪些封装以及如何使用相应的类时,可以参考这里:基于QT的openGL学习,这一系列的主要问题是基本就是示例代码,而没有解释。不过入了门之后直接看代码可能反而比文字描述直观,也是不错的参考。

环境

Windows7
QT 5.10.1 (MSVC2017_x64)

论绘制一个三角形都需要什么

对于openGL来说,绘制一个三角形需要我们通过计算机语言向其提供 顶点颜色 的信息。

比较重要的组成部分

关于顶点和颜色信息的存储:openGL使用简称为 VAO(Vertex Array Object,顶点数组对象) 的对象来存储这些信息。
向VAO传递信息的过程中,我们会使用简称为 VBO(Vertex Buffer Object,顶点缓冲对象) 的对象。

对VAO中存储的信息进行处理:openGL使用一个 程序(program) 对象来决定使用VAO中信息的方式并进行最终的绘制。程序包含所谓的 “着色器”(shader) ,你可以把着色器理解成是由openGL语言(基本独立于你所使用的语言)编写而成的程序

一个能够绘制图形的openGL程序至少包含顶点着色器(Vertex Shader)和片段着色器(Fragment Shader),其中顶点着色器会决定绘制顶点的位置,而片段着色器用来决定这些位置的颜色

上述的组成部分如果按照一定的流程全部正确设置完毕,我们就可以绘制出我们的第一个三角形了。

各部分关系图

一种可行的绘制流程

当你熟悉了openGL的基础知识之后,你会知道绘制流程的顺序并不是固定的,只需满足一些必要条件即可,但是现在知道这一点就可以了,然后暂且认为流程就是如下固定的,否则容易头晕。

编写代码

了解了上述知识后,现在要做的就是学习如何通过代码实现上述的步骤来绘制了,到现在为止可以说第一步只迈出了小半,因为openGL并没有特别符合直觉和方便使用的函数接口,像如下

#include <openGL>
void main()
{setVertex(xxxx);setColor(xxxx);paint();
}

这样的使用方法并不存在,必须结合前述知识去学习openGL存储和处理数据的方法。

关于QT使用openGL

编写一个自定义类,继承QOpenGLWidgetQOpenGLFunctions即可,我们的绘制便会在这个Widget内进行。
头文件MyWidget.h示例如下:

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>class MyWidget : public QOpenGLWidget,protected QOpenGLFunctions
{Q_OBJECTpublic:MyWidget(QWidget* parent=0);~MyWidget();protected:void initializeGL() override;void paintGL() override;void resizeGL(int width, int height) override;private:QOpenGLShaderProgram* program;QOpenGLVertexArrayObject m_vao;QOpenGLBuffer m_vbo;int m_attr;int m_color;};

继承后的类会包含initializeGL(),paintGL(),resizeGL()这三个需要重写的函数。

顾名思义,initializeGL用来初始化各项openGL相关的部件,设置openGL程序,存储数据等操作的代码通常在此函数内进行编写,而不是在类的构造函数中。

paintGL用来编写绘制相关操作的代码。

resizeGL用来在Widget尺寸发生改变时修改设置以得到预期的效果。

注意在此三个函数之外的自定义函数中调用openGL的函数功能时,通常需要先调用makeCurrent() 函数来获得上下文(context)。

按步骤来

创建openGL程序

QT创建和绑定openGL程序比较简单直观

QOpenGLShaderProgram* program = new QOpenGLShaderProgram;
program->bind();

但是这仅仅是创建了一个空的程序,如前述,想要绘制图形,openGL程序中至少包含顶点着色器程序和片段着色器程序,于是我们接着往下来。

编写顶点着色器程序和片段着色器并添加到openGL程序中去

着色器程序的名称和后缀没有固定要求,方便区分功能用途即可,你甚至可以直接在QT代码中用字符串的形式编写并传入着色器程序,不过我觉得额外编写文件好修改一些,这里便介绍从其他文件添加着色器程序的方法。

顶点着色器程序 triangle.vert:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;out vec3 ourColor;void main()
{ourColor = aColor;gl_Position =vec4(aPos, 1.0);
}

第一行#version 330 core为openGL版本声明,你可能会看到不含这一行的openGL程序代码,变量的定义方法可能会与此不同

layout 和 location是什么现在可以不管,事实上没有这两个也没关系。文章后面会有简单解释

in 代表此变量会由我们的程序传入

vec3 代表3维向量的数据类型

aPos和aColor和ourColor是我们自己定义的变量名

out 代表此变量会由此着色器传出给片段着色器 (其实着色器并不只有两种,他们会按一定的顺序将数据传递下去,由于我们只使用了两种着色器程序,此处直接理解为传给片段着色器即可) 供其使用。

gl_Position 是顶点着色器的内建变量,是一个4维向量,前3维代表顶点的空间位置,第4维目前我们一律设置为1.0即可。后续我们将通过自己的程序,借助顶点着色器中定义的aPos变量将顶点的空间坐标传递进来。

openGL的绘图空间为一个长宽高均为2的立方体:

只有在这个空间范围内的顶点才能够被绘制出来。

片段着色器程序triangle.frag:

#version 330 core
in vec3 ourColor;
void main()
{gl_FragColor = vec4(ourColor,1.0);
}

in代表此变量由其他着色器程序传入,在本文中会接收由顶点着色器传入的ourColor变量,注意想要正确的传出传入变量,需要保证不同着色器中的变量名称和类型一致,而且传递方向不要搞反。
gl_FrageColor为内建变量,用来表示颜色,也是一个4维变量(4个分量分别代表RGBA:红,绿,蓝,α值),α值现在统一设置为1.0即可。

接下来就可以将以上两个程序添加到program中了

//向program中添加顶点着色器if(!program->addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){qDebug()<< (program->log());return;}//向program中添加片段着色器if(!program->addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){qDebug()<< (program->log());return;}if(!program->link()){qDebug()<< (program->log());return;}

创建并绑定VAO

QT中VAO的创建和绑定也比较简单

 QOpenGLVertexArrayObject m_vao;m_vao.create();m_vao.bind();

创建并绑定VBO

基本同上

 QOpenGLBuffer m_vbo;m_vbo.create();m_vbo.bind();

将数据存入VBO

注意我们并不会直接对VAO进行操作,VBO可以看做是某一项数据,而VAO则是这些数据的集合,可以理解为在program从VBO中取数据时会将这些数据自动存入VAO,所以我们也需要在绑定VBO之前绑定一个VAO。
我们所需要的数据可以在代码中使用一个静态数组创建

static GLfloat vertices[] = {//我们所准备的需要提供给openGL的顶点数据// 位置              // 颜色0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下-0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
};

这里应该也算是个难理解的地方,这里数组中每一行代表一个顶点数据,而其中每一行的前三个数代表空间坐标,后三个数代表颜色分量。
然而这种区分是我们自行规定的,对于VAO和VBO来说,目前他们就只是二进制数据而已,openGL本身并没有规定各项数据必须以何种形式组织起来,我们将通过一些方式来告诉openGL如何来分配和使用这些数据。(这里可以在成功画出三角形后再回头来理解)
将这些数据存入VBO只需一行代码

m_vbo.allocate(vertices, sizeof(vertices));

这个语句也在一定程度上表示vbo只关心数据的大小,你准备了一个数组,VBO把这个数组中的内容一股脑复制进来,数据存储就算完成了。这里跟memcpy函数有一定的相似性,他只管拷贝数据,至于数据是什么意义,则需要程序员来控制。

告诉openGL该如何分配和使用数据

现在搬来我们之前写好的顶点着色器程序

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;out vec3 ourColor;void main()
{ourColor = aColor;gl_Position =vec4(aPos, 1.0);
}

我们需要告诉顶点着色器程序,我们希望aPos这个变量中持有顶点的空间坐标信息,这是一个三维向量,所以我们让他持有静态数组中每一行的前三个数据,这可以通过以下代码来完成

 int m_attr=program->attributeLocation("aPos");program->setAttributeBuffer(m_attr,GL_FLOAT, 0, 3,6*sizeof(GLfloat));program->enableAttributeArray(m_attr);

上方这种方式更加贴近原生openGL的方法,在QT中,你也可以使用下方这种更易于理解的方式(至少你暂时不用思考上面方法中那个整形变量是做什么的)

    program->setAttributeBuffer("aPos",GL_FLOAT, 0, 3,6*sizeof(GLfloat));program->enableAttributeArray("aPos");

在这里解释一下这个整形变量m_attr。openGL会把变量名和一个整型数字一一对应起来,原生openGL的函数需要一个传出参数,用来告诉你变量名"aPos"应该对应哪个整型值。假设你现在需要一个变量"aPos",openGL通过这个函数告诉你,在我的内部用数字"1"代表这个变量,那么在这之后,当你想要使用"aPos"时,你需要告诉openGL,我现在要操作"1"所代表的变量了。

还记得前面写的layout (location=0)吗?这里其实就是相当于手动分配了变量和数字的对应关系。
比如layout (location = 0) in vec3 aPos;
就代表我们想让数字"0"来代表"aPos"这个变量,这个属于程序员与openGL的约定,当使用数字"0"时,双方都明白所指的是什么。当你想使用aPos时,你告诉openGL我要使用"aPos"和我要使用"0"意思是一样的。区别在于对于前者,openGL先要查找aPos所对应的数字,所以事先手动分配数字并直接使用数字可以节省一步从而一定程度上提高性能。

setAttributeBuffer这个函数中的参数还是需要好好解释一下的,而且主要是后三个参数


GL_FLOAT是用来告诉着色器VAO中的数据类型。
举个相似的例子来说明着色器是如何看待这些数据的。
假设我们内存中有16进制数据(0x)123456789ABC,这数据本身目前并没有意义
那么我们用get(short,0,2,3sizeof(short))可以取出[12,34],[78,9A](16进制)两组数据
get(short,0,2,2
sizeof(short))可以取出[12,34],[56,78],[9A,BC]
get(short,2,2,2sizeof(short))可以取出[56,78],[9A,BC]
get(int,0,1,1
sizeof(int))则可以取出[1234],[5678],[9ABC]三组数据

可以看到不同的参数会赋予数据不同的意义,而对于openGL的setAttributeBuffer函数来说,你要做的就是通过这个函数及其参数来确保openGL可以按照你的要求读出正确的数据。
通过上述语句,openGL就知道了名为"aPos"的变量表示三组顶点数据:(0.5f,-0.5f,0.0f),(-0.5f,-0.5f,0.0f)以及(0.0f,0.5f,0.0f)。

如果你理解了我们如何通过上述代码告诉openGL在当前VBO的数据中取每行的前三个数据给aPos,那么每行后三个数据如何传给aColor也就应该清楚了

 int m_color=program->attributeLocation("aColor");program->setAttributeBuffer(m_color,GL_FLOAT,3*sizeof(GLfloat),3,6*sizeof(GLfloat));program->enableAttributeArray(m_color);

注:其实你也可以通过两个数组,两个VBO分别来传递信息,两种方式难理解的点不同,我觉得都是入门需要掌握的,但是这里先只给出这一种方式吧。

这里还是应该多看几遍,力求理解着色器程序是如何取到数据的,如果觉得讲得不清楚可以留言,我会尝试说得再细致一些。

至此,我们的openGL程序设置已经完成,顶点位置和颜色数据也都存到了VAO中,接下来,终于可以在崩溃前进行激动人心的绘制了……

绘制

上述编码基本都是在initializeGL()函数中编写的,绘制通常在paintGL()函数中完成,绘制时需要设置完整的program(决定如何确定顶点和颜色)和VAO(其中存储了绘制过程中所需要的数据)
因此在绘制函数之前(一般为glDrawXXXX)确保我们绑定了正确的openGL程序和VAO。
于是在paintGL()函数中,我们可以编写如下代码:

 program->bind();//绑定绘制所要使用的openGL程序m_vao.bind();//绑定包含openGL程序所需信息的VAOglDrawArrays(GL_TRIANGLES, 0, 3);//绘制

通过这个程序解释glDrawArrays各项参数的意义既麻烦又不好理解,但是还是适当说明下。

GL_TRIANGLES 表示我们要绘制的是三角形(然而并不是说绘制矩形就有GL_RECTANGLE可以用…)可以认为这表示一种排列顶点的方式。

0 表示从第0个顶点开始绘制

3 表示我们一共要绘制3个顶点

至此,三角形的绘制可以说已经结束了。后面会给出完整项目代码的链接,当你成功绘制出三角形,并更进一步的可以参照其他教程画出正方形,正方体等等图形时,修改几次其中的参数即可让你有个直观的认识。

完整的MyWidget.cpp代码

#include "mywidget.h"
#include <QDebug>static GLfloat vertices[] = {//我们所准备的需要提供给openGL的顶点数据// 位置              // 颜色0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下-0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
};MyWidget::MyWidget(QWidget* parent):QOpenGLWidget(parent)
{}MyWidget::~MyWidget()
//建议在析构函数中手动销毁openGL相关的对象,
//文档中特意提到QT的回收机制难以保证回收所有openGL使用的资源
//不销毁的话在关闭程序时可能会出现异常
{makeCurrent();m_vao.destroy();m_vbo.destroy();doneCurrent();
}void MyWidget::initializeGL()
{initializeOpenGLFunctions();// 创建并绑定着色器程序program = new QOpenGLShaderProgram;program->bind();//向program中添加顶点着色器if(!program->addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){qDebug()<< (program->log());return;}//向program中添加片段着色器if(!program->addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){qDebug()<< (program->log());return;}if(!program->link()){qDebug()<< (program->log());return;}//创建并绑定VAOm_vao.create();m_vao.bind();//创建并绑定VBOm_vbo.create();m_vbo.bind();m_vbo.allocate(vertices, sizeof(vertices));//向VBO传递我们准备好的数据(本文件起始部分的静态数组)//向顶点着色器传递其中定义为"aPos"的变量所需的数据m_attr=program->attributeLocation("aPos");program->setAttributeBuffer(m_attr,GL_FLOAT, 0, 3,6*sizeof(GLfloat));program->enableAttributeArray(m_attr);//向顶点着色器传递其中定义为"aColor"的变量所需的数据m_color=program->attributeLocation("aColor");program->setAttributeBuffer(m_color,GL_FLOAT,3*sizeof(GLfloat),3,6*sizeof(GLfloat));program->enableAttributeArray(m_color);program->release();//解绑程序}void MyWidget::paintGL()
{//glClearColor(0.2f, 0.3f, 0.3f, 1.0f);//glClear(GL_COLOR_BUFFER_BIT);program->bind();//绑定绘制所要使用的openGL程序m_vao.bind();//绑定包含openGL程序所需信息的VAOglDrawArrays(GL_TRIANGLES, 0, 3);//绘制m_vao.release();//解绑VAOprogram->release();//解绑程序//update();//调用update()函数会执行paintGL,现在绘制一个静态的三角形可以不使用//也可以用定时器连接update()函数来控制帧率,直接在paintGL函数中调用update()大概是60帧
}void MyWidget::resizeGL(int width, int height)
{}

绘制最终效果

如果你一切顺利,你就可以得到自己通过openGL绘制的第一个三角形啦,效果如下:

完整项目代码

完整的QT项目已上传至github

希望同学们都能够顺利地迈出学习openGL的第一步!如果觉得本文中有写的不清楚或者是错误的地方,欢迎留言指出。

QT使用openGL绘制一个三角形相关推荐

  1. OpenGL绘制一个三角形

    OpenGL绘制一个三角形 OpenGL绘制一个三角形简介 源代码剖析 主要源代码 OpenGL绘制一个三角形简介 在本课中,我们仍然不转换坐标,因此顶点仅在正方形内可见.从 Z 的反轴看,正方形如下 ...

  2. OpenGL绘制一个三角形的实例

    OpenGL绘制一个橘黄色的三角形 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <glad/glad.h> #include &l ...

  3. OpenGL学习笔记(八):进一步理解VAO、VBO和SHADER,并使用VAO、VBO和SHADER绘制一个三角形

    原博主博客地址:http://blog.csdn.net/qq21497936 本文章博客地址:http://blog.csdn.net/qq21497936/article/details/7888 ...

  4. OpenGL(十八)——Qt OpenGL绘制一个3D世界

    OpenGL(十八)--Qt OpenGL绘制一个3D世界 一.说明 本篇介绍构建一个3D的世界. 二.简介 加载3D世界,并在其中漫游: 在这一课中,你将学会如何加载3D世界,并在3D世界中漫游. ...

  5. Android使用NDK OpenGL ES3.0绘制一个三角形

    Android使用NDK  OpenGL ES3.0绘制一个三角形 [尊重原创,转载请注明出处]https://blog.csdn.net/guyuealian/article/details/820 ...

  6. OpenGL学习脚印: 绘制一个三角形

    写在前面 接着上一节内容,开发环境搭建好后,我们当然想立即编写3D应用程序了.不过我们还需要些耐心,因为OpenGL是一套底层的API,因而我们要掌握的基本知识稍微多一点,在开始绘制3D图形之前,本节 ...

  7. NDK OpenGL ES 3.0 开发(一):绘制一个三角形

    该原创文章首发于微信公众号:字节流动 什么是 OpenGLES OpenGLES 全称 OpenGL for Embedded Systems ,是三维图形应用程序接口 OpenGL 的子集,本质上是 ...

  8. 【OpenGL ES】入门及绘制一个三角形

    本文首发于个人博客:Lam's Blog - [OpenGL ES]入门及绘制一个三角形,文章由MarkDown语法编写,可能不同平台渲染效果不一,如果有存在排版错误图片无法显示等问题,烦请移至个人博 ...

  9. OpenGL绘制Triangle三角形

    OpenGL绘制Triangle三角形 前期知识准备 顶点输入 顶点着色器 编译着色器 片段着色器 着色器程序 链接顶点属性 顶点数组对象 我们一直期待的三角形 索引缓冲对象 前期知识准备 在Open ...

最新文章

  1. 书单 | 计算机视觉的修炼秘笈
  2. 【干货】Kaggle 数据挖掘比赛经验分享(mark 专业的数据建模过程)
  3. 几个故事告诉你,火热的区块链究竟是什么?
  4. python语言特性-------python2.7教程学习【廖雪峰版】(一)
  5. ES6——generator与yield
  6. 已重置默认应用设置html,win10已重置应用默认设置一直弹怎么办_win10已重置应用默认设置老是弹出解决方法...
  7. (原)Lazarus 异构平台下多层架构思路、DataSet转换核心代码
  8. android 1141错误,Android Studio中报错的问题
  9. 【深度优先搜索】LeetCode77:组合
  10. 【visio】visio软件安装
  11. 家里的钱都花哪儿了?做份支出记账表看看
  12. 一年级计算机算文具吗,一年级老师说,用这样文具的孩子,课堂上都没有认真听讲...
  13. 代码整洁之道——如何写出整洁高效的代码
  14. vlan的基本指令_思科VLAN的基本配置命令
  15. 一系列国内外顶尖互联网公司的技术博客,晋升程序员必备!
  16. [GXYCTF2019]BabySQli
  17. BZOJ P1856 字符串
  18. oracle weblogic开启,菜鸟经验:oracle与weblogic自动启动与停止
  19. the7主题footer.php,WordPress主题制作(七):制作基础模板Index.php
  20. 《软件工具》公式识别神器 – Mathpix,科研人员的福利

热门文章

  1. 实现一个英文词典的功能
  2. colored manual page
  3. 围棋学习18k到7k
  4. HPA控制器介绍以及实战案例
  5. 计算机无法投影,如果无法连接计算机和投影仪怎么办
  6. android之网络状态判断(wifi和移动网络数据)
  7. 51单片机驱动DS18B20温度传感器测量温度
  8. 【RPC】分布式一致性与一致性协议
  9. 管理-管理:管理百科
  10. Blos查看计算机硬盘,如何在bios查看硬盘