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

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

原博主博客地址:http://blog.csdn.net/qq21497936

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

 

《OpenGL学习笔记》系列博客目录地址:http://blog.csdn.net/qq21497936/article/category/7315532

目录

前话

专业名词

渲染管线

VAO顶点输入

顶点着色器

编译着色器

片段着色器

着色器程序

链接顶点属性

顶点数组对象

绘制三角形

Demo

程序源码


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

 

前话

       前面分别介绍了OpenGL+GLAD+GLFW组成与平台无关的OpenGL工程,根据不同的开发平台和系统平台,选择自己去配置开发环境,有需要读者可参考笔记五六七。

       本章节开始,我们将继续前行,不论你是VS还是Qt(其他环境读者可自己搭建),我们都将使用一套同样的代码继续学习。

笔者使用Qt5.9.3(安装配置环境比VS简单一些,笔者三台电脑,比较方便互相迁移)。

       本章节内容较多,建议先大致阅读一遍本文章最后的源码,不懂不要紧,然后再回到此继续往后阅读。

 

 

专业名词

 

VAO(vertex Array Object):顶点数组对象

VBO(vertex Array Object):顶点缓冲对象

 

渲染管线

       关于渲染管线的介绍、理解和实现过程,可参考之前的博客笔记《OpenGL学习笔记(二):OpenGL语法、渲染管线以及具体实现过程详解》:

 

http://blog.csdn.net/qq21497936/article/details/78671396

 

 

VAO顶点输入

 

       开始绘图之前,先给OpenGL输入一些顶点数据,OpenGL是一个3D图形库,所以再OpenGL中指定的所又坐标都是3D坐标(x、y和z)。注意,OpenGL仅当3D坐标再3个轴(x、y和z)上都为-1.0到1.0的范围时,才处理它,即标准化设备坐标 (标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上)范围内的坐标才最终呈现的屏幕上。

 

       先画一个等边三角形,所有输入3个顶点,每个顶点都有1个3D位置:

图片.png

 

定义一个float数组

float vertices[] = {
  -0.5f, 0.0f, 0.0f,
  0.5f, 0.0f,0.0f,
  0.0f, 0.866f, 0.0f,
}

       补充:Z轴也理解为深度(Depth),通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。

       定义这样的顶点数据以后,我们把它输入发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们再内存中指定数量的顶点。

       通过VBO管理这个内存,它会在GPU内存(即显存)中存储大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。        顶点缓冲对象有一个唯一的ID,我们使用glGenBuffers()函数和一个缓冲ID生成一个VBO对象

unsigned int VBO;
glGenBuffers(1, &VBO);

       OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们绑定多个缓冲,只要它们是不同的缓冲类型。可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:

 

glBindBuffer(GL_ARRAY_BUFFER, VBO);

 

       从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

       glBufferData(目标缓冲器类型,传输数据的字节大小,发送的实际数据,显卡如何管理给定的数据);

       第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:

 

  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

 

       三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。

       现在已经把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。下面会创建一个顶点和片段着色器来真正处理这些数据,接下来需要创建顶点着色器和片段着色器。

 

顶点着色器

       顶点着色器(Vertex Shader)是几个可编程着色器中的一个。如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。下面写一个GLSL顶点着色器的源代码:

 

#version 330 core
Layout (location = 0) in ver3 aPos;
void main()
{
  gl_Position = ver4(aPos.x, aPos.y, aPos.z, 1.0);
}

 

       GLSL看起来很像C语言。每个着色器都起始于一个版本声明。

       下一步,使用in关键字,再顶点着色器中声明所有的输入顶点属性(Input Vertex Arribut)。现在只需要位置(Position)数据,所以只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。由于每一个顶点都有一个3D坐标,于是创建一个vec3输入变量aPos,同时也通过layout (location=0)设定了输入变量的位置值(Location)。

 

       向量:在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取,注意vev.w是用在所谓透视除法(PerspectiveDivision)上。

 

       为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数的最后,我们将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f来完成这一步。

       当前这个顶点着色器是最简单的顶点着色器了,因为对输入数据什么都没有处理就把它传到着色器的输出了。在真实的程序里输入数据通常都不是标准化设备坐标,所以我们首先必须先把它们转换至OpenGL的可视区域内。

 

 

编译着色器

       一个顶点着色器源码(储存在一个C的字符串中),但是为了能够让OpenGL使用它,我们必须在运行时动态编译它的源码。

 

       步骤一:创建一个着色器对象,注意还是用ID来引用。所以存储这个着色器为unsigned int,然后用glCreateShader创建这个着色器:

 

Unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

 

       把需要创建的着色器类型以参数形式提供给glCreateShader。创建的是顶点着色器,所以传递的参数是GL_VERTEX_SHADER。

 

       步骤二:把着色器源码附加到着色器对象上,然后编译它:

 

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertextShader);

 

       glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。

       步骤二附加(可选择):检测glCompileShader()后编译是否成功了,如果没成功的话,需要获得错误原因,检测编译时错误可以通过以下代码来实现:

 

int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

 

       首先我们定义一个整型变量来表示是否成功编译,还定义了一个储存错误消息(如果有的话)的容器。然后我们用glGetShaderiv()检查是否编译成功。如果编译失败,我们会用glGetShaderInfoLog()获取错误消息,然后打印它。

 

if(!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog << std::endl;
}

 

       如果编译的时候没有检测到任何错误,顶点着色器就被编译成功了。

 

片段着色器

 

       片段着色器(FragmentShader)是本程序第二个也是最后一个我们打算渲染三角形的着色器。片段着色器所做的是计算像素最后的颜色输出。

       步骤一:创建一个着色器对象。

#version 330 core
out vec4 FragColor;
void main()
{
  // RGBA         r   g    b   a
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); 
}

       拓展:在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。这三种颜色分量的不同调配可以生成超过1600万种不同的颜色!

       步骤二:编译着色器对象,编译片段着色器与顶点着色器类似,只不过着色器类型不一样,使用的常量为GL_FRAGMENT_SHADER。

 

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

 

       两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。

 

 

着色器程序

       着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们连接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。

 

       创建一个程序对象:

 

unsigned int shaderPrograme;
shaderPrograme = glCreateProgram();

 

       glCreateProgram()函数创建一个程序,并返回新创建程序对象的ID引用。现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:

glAttachShader(shaderProgram, vertexShader); 
glAttachShader(shaderProgram, fragmentShader); 
glLinkProgram(shaderProgram);

       就像着色器的编译一样,我们也可以检测链接着色器程序是否失败,并获取相应的日志。与上面不同,我们不会调用glGetShaderiv和glGetShaderInfoLog,现在我们使用:

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    std::cout<<"ERROR::GET::PROGRAMINFO_FAILED\n"<<infoLog << std::endl;
}

       得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:

glUseProgram(shaderProgram);

       在glUserProgram()函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。

       注意,在把着色器对象链接到程序对象以后,记得删除着色器对象,释放内存,因为我们不再需要它们了:

 

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

 

       现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。

 


相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
存储 异构计算
Opengl ES之VBO和VAO
Opengl ES连载系列
85 0
|
小程序 知识图谱 异构计算
|
存储 API
学习OpenGL ES之绘制三角形
OpenGL入门经典案例,绘制一个三角形
学习OpenGL ES之绘制三角形
|
Kotlin
嵌入式实践教程--OpenGL3.0 Shader使用
嵌入式实践教程--OpenGL3.0 Shader使用
|
异构计算
学习OpenGL ES之VBO&VAO
学习OpenGL ES之VBO&VAO
|
缓存 索引
OpenGL学习笔记(十三):将纹理贴图应用到四边形上,对VAO/VBO/EBO/纹理/着色器的使用方式进行总结
OpenGL学习笔记(十三):将纹理贴图应用到四边形上,对VAO/VBO/EBO/纹理/着色器的使用方式进行总结
OpenGL学习笔记(十三):将纹理贴图应用到四边形上,对VAO/VBO/EBO/纹理/着色器的使用方式进行总结
|
存储 缓存 C++
OpenGL学习笔记(十二):纹理的使用
OpenGL学习笔记(十二):纹理的使用
OpenGL学习笔记(十二):纹理的使用
|
编译器 C语言 C++
OpenGL学习笔记(十一):封装自己的着色器类
OpenGL学习笔记(十一):封装自己的着色器类
OpenGL学习笔记(十一):封装自己的着色器类
|
小程序 编译器 C语言
OpenGL学习笔记(十):深入学习和理解着色器
OpenGL学习笔记(十):深入学习和理解着色器
OpenGL学习笔记(十):深入学习和理解着色器
|
5月前
|
XML 小程序 Java
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
109 0