OpenGL之旅(3):绘制一个三角形

title: OpenGL(3)三角形
date: 2020-06-28 16:01
category: 图形学
tags: opengl
https://hashwaney.github.io/

1.概述

  • VAO: 顶点数组对象 Vertex Array Object
  • VBO: 顶点缓冲对象 Vertex Buffer Object
  • IBO(EBO): 索引缓冲对象 Element Buffer Object or Index Buffer Object

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D座标转换为适应屏幕的2D像素。

3D座标转为2D座标的处理过程是由OpenGL的图形渲染管线管理的;

上述意思:3D座标可以看成一堆的原始图形数据经过一个输送管道,在这个过程中经过座标变换,着色等等最终呈现在屏幕上。

  • 将3D座标转换为2D座标。

  • 把2D座标转换为实际的有颜色的像素。

流程:

顶点数据
------>
顶点着色器(1)
------>
图元装配(2)
------>
几何着色器(3)
------>
光栅化(4)
------>
片段着色器(5)
------>
测试与混合(6)

以三角形为例:

  1. 以数组的形式传递3个3D座标作为图形渲染管线的输入来表示一个三角形,这个数据叫做顶点数据(vertex data);那么顶点着色器
    是把3D座标转换为另一种3D座标(输入的3D座标不是标准的设备座标,即不是在OpenGl的可见区域)

  2. 利用顶点着色器输出的所有顶点作为输入,把所有的点装配成指定图元的形状

  3. 将图元装配阶段的输出传递给几何着色器,几何着色器把图元形式的一系列顶点的集合作为输入,通过产生新顶点构造新的图元来生成其他形状

  4. 几何着色器的输出被传入到光栅化阶段,将图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段,还要进行相应的裁切处理,裁切部分超出视图以外的所有像素。其实就用来生成片段,以便片段着色器对座标点进行着色

  5. 用来计算一个像素的最终颜色,其中还包括3D场景数据(光照、阴影、光的颜色),这些数据可以用来计算最终像素的颜色

  6. 在所有对应的颜色值确定之后,最终的对象将会被传到最后一个阶段,Alpha测试和混合测试,这个阶段检查片段对应的深度,用来判断这个像素是在其他物体的前面还是后面,决定是否应该丢弃,这个阶段也会检查alpha值(透明度)并对物体进行混合。

2.顶点输入

  • 绘制图形之前,需要给OpenGL输入一些顶点数据,由于OpenGL是一个3D图形库,因此OpenGL指定的所有的座标点都是3D座标(x,y,z)
    注意:OpenGL不是简单的把所有的3D座标变换为屏幕上的2D像素,OpenGL仅当3D座标在3个轴(x,y和z)上都为-1.0和1.0的范围内才会处理它。(归一化设备座标)渲染一个三角形,指定三个顶点,每个顶点有一个3D位置;
float vertices[]={
	0.5f,0.5f,0.0f, //(x,y,z)
	-0.5f,-0.5f,0.0f,
	-0.5f,0.5f,0.0f
}

z为0,由于渲染的是一个2D三角形,因此z座标设置为0,z座标可以理解为深度(depth),代表一个像素在空间中与你的距离。

  • 定义好了这样的顶点数据之后,将数据发送到图形渲染管线的第一个处理阶段:顶点着色器。

    • 在GPU上创建内存用于储存顶点数据
    • 配置OpenGL如何解释解释这些内存,指定如何将顶点着色器如何发送给显卡
    • 顶点着色器处理在内存中指定数量的顶点。
  • **顶点缓冲对象(VBO)**管理在GPU上创建的内存,在内存中存储大量的顶点。

    • 好处:一次性发送一大批数据到显卡上,原因是从CPU把数据发送到显卡相对较慢(针对每次发送一个顶点而言)

    • 是一个OpenGL对象,有一个独一无二的ID,使用glGenBuffers函数和一个缓冲ID生成一个VBO对象

unsigned int VBO;

glGenBuffers(1,&VBO);

顶点缓冲对象的缓冲类型为GL_ARRAY_BUFFER,OpenGL允许同时绑定多个缓冲,只要是不同的缓冲类型。使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER;

unsigned int VBO;

glBindBuffer(GL_ARRAY_BUFFER,VBO); // 绑定的是一个顶点缓冲对象类型为GL_ARRAY_BUFFER

glBindBuffer(GL_COPY_READ_BUFFER,VBO);

//2.生成一个VBO对象
glGenBuffers(1,&VBO);

为这个VBO对象绑定多种缓冲类型 VBO对象配置一些关于缓冲类型的信息 glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindBuffer(GL_COPY_READ_BUFFER, VBO);

以上两步只要配置了,那么使用任何在这种缓冲类型的缓冲调用都会用来配置到当前绑定的VBO上

其实就是只要是这两种或者还配置其他缓冲类型的数据,只要这些缓冲类型有数据,VBO对象都是使用这些数据。
最后调用glGenBuffers来生成一个VBO对象.

要想使用这些数据,就需要把用户输入的数据存储到对应的缓存类型的缓冲内存中,只需调用glBufferData函数,就可以把之前定义好的顶点数据复制到缓冲内存中

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

//glBufferData(GL_COPY_READ_BUFFER,sizeof(vetices),vetices,GL_STATIC_DRAW);?? 

上述参数解释:

  1. 目标缓冲类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。

  2. 指定传输数据的大小(以字节为单位),用sizeof计算出顶点数据大小。

  3. 发送的实际数据(当前来说就是顶点数组)

  4. 显卡如何管理给定的数据

渲染保持原样

GL_STATIC_DRAW:数据不会或几乎不会改变

数据频繁被改变,确保显卡把数据放在能够高速写入的内存部分

GL_DYNAMIC_DRAW:数据会改变很多

GL_STREAM_DRAW:数据每次绘制是都会改变。

综上:顶点数据存储在显卡的内存中,用VBO这个顶点缓冲对象管理。

3.顶点着色器

顶点着色器(Vertex Shader)是一种可编程的着色器的一种。

需要通过着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,最后使用。看以下示例:

#version 330 core //1

layout (layout=0) in vec3 aPos; //2

void main(){
	gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0); //3
}

第一行:版本 以及声明 使用的是核心模式

第二行in 关键字 表示的是输入顶点属性(Input Vertex Attribute),vec3 代表的是一个3D座标。

第三行:vec4:表示GLSL的一个向量数据类型为4分量的float类型,即(x,y,z,w)就是为了归一化处理,处理的原始3D座标归一化之后能够被OpenGL处理。

彩蛋

向量(Vector)表达的是任意空间中的位置和方向,上述的vec3和vec4.
gl_Position 内置的变量不能被写错和改变,唯一被OpenGL识别。通过gl_Position将位置数据传递给OpenGL。

4.编译着色器

编写一个顶点着色器源码,需要进行动态编译才能被OpenGL使用。

面向对象的思想:

  • 创建一个着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER); //每次调用肯定是长生不同的id,如何搞得,可能就根据时间戳。

传递的参数:GL_VERTEX_SHADER,

  • 将编写好的着色器源码与着色器对象进行绑定
const char* vertexShaderSource ;
int vertexShader;
glShaderSource(vertexShader,1,&vertexShaderSource,NULL);

vertexShaderSource :代表的是编写的顶点着色器的源码以字符串的形式提供(这个意味着各个平台通用)

  • 编译这个着色器对象
int vertexShader;

glCompileShader(vertexShader);

最后进行编译顶点着色器

  • 编译结果
int success;
char info[1024];
glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success);
if (!success) // 失败
{
	glGetShaderInfoLog(vertexShader,1024,NULL,info);
	std::cout<<" shader compile error "<<info<<std::endl;
}

5.片段着色器

为了计算第四步中的顶点所处位置的颜色,并且将这些颜色进行输出
片段着色器(Fragment Shader)用于渲染三角形;

计算机图形中颜色表示为4个元素的数组:红色,绿色,蓝色和aplha(透明度)分量RGBA,在OpenGL或者GLSL定义一个颜色的时候,颜色分量会在0.0-1.0之间。

#version 330 core // 1

out ver4 FragColor ; // 2

void mian(){
	FragColor = vec4(1.0f,0.5f,0.3f,1.0f);
}

第一步
如上分析可得;

第二步

out 关键字声明为输出变量,命名为FragColor,输出的是一个vec4的颜色组合。

片段着色器的创建与编译过程与上述的顶点着色器相似,此处不在做具体分析,给出示例代码

unsigned int fragShader;

const char* fragShaderSource;

fragShader = glCreateShader(GL_FRAGMENT_SHADER);

glShaderSource(fragShader,1,&fragShaderSource,NULL);

glCompileShader(fragShader);

着色器程序
上述两个着色器完成了编译,但是还没有链接在一起,所以发挥不了作用,因此将这两个着色器对象链接到一个渲染的着色器程序中。
好处:将着色器渲染功能封装到一个程序中,当有了渲染需求的时候就激活这个程序,已经被激活的着色器程序的着色器就会被调用,进行工作,你把着色器链接到程序中,就把每个着色器的输链接到下一个着色器的输入。(顶点输出—>需要进行着色—>片元着色器接收(输入)–>上色)

流程:

usigned int shaderProgram;

shaderProgram = glCreateProgram();
//0.
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragShader);

//1.
glLinkProgram(shaderProgram);

int success;
char info[1024];
glGetProgramiv(shaderProgram,GL_LINK_STATUS,&success);

if (!success)
{
	glGetProgramInfoLog(shaderProgram,1024,NULL,info);
	std::cout<<"link program error "<<info <<std::endl;
	shaderProgram=-1;
}
if (shaderProgram!-1)
{
	//2.使用着色器程序
	glUseProgram(shaderProgram);
}



//3. 将着色器对象链接到着色器程序,要删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragShader);

上述流程是将输入的顶点数据发送给
GPU,并且告诉GPU如何在顶点和片段着色器中处理它,但是OpenGL不知道如何解释内存中的顶点数据(输入的顶点数据),以及该如何将顶点数据链接到顶点着色器的属性上,看下面的6.链接顶点属性

6.链接顶点属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KWONZX0d-1593486438103)(https://i.loli.net/2020/06/30/iH7TycXs8d2RknY.png)]

  • 位置数据被存储为32位(4字节)比如说VerTex 1 这个顶点的X分量

  • 每个顶点位置包含3个这样的值,比如VerTex 1 的X,Y,Z

  • 在这3个值之间没有空隙(获取其他值),这几个值在数组中紧密排列

  • 数据中的第一个值在缓冲开始的位置

通过上述的图以及表述,可以得知OpenGL按照这种数据排列规律来解析并处理顶点数据,通过glVertexAttribPointer函数


//1. 解释顶点数据
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(viod*)0);
//2.将顶点数据链接到顶点着色器的属性
glEnableVertexAttribArray(0);

  • 第一个参数指定配置的顶点属性,由3.顶点着色器中的编写的glsl代码中 layout(location=0)定义了position顶点属性的位置location,可以将顶点属性的位置设置为0,那么数据传递到这个顶点属性中。

  • 第二个参数指定顶点属性的大小,顶点属性是一个vec3,由3个值组成,所以其大小是3。

  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中的vec* 都是由浮点数值组成)

  • 第四个参数指定是否需要数据标准化,设置GL_TRUE,所有数据都会被映射到0(对于有符号signed数据是-1)到1之间,这边不采用。

  • 第五个参数叫做步长(stride),告诉连续的顶点属性之间的间隔,由于下个组位置数据(即VerTex 2 是经历VerTex 1的三个float之后),因此步长为3*sizeof(float).

  • 第六个参数类型为void*,表示缓冲中起始位置的偏移量,由于位置数据在数组的开头,所以这里为0。

使用glEnableVertexAttribArray(0)启用(意味着可以链接)顶点属性,(默认是禁用的)。即顶点数据可以链接到顶点属性

绘制物体流程:


// 复制顶点数组到缓冲
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBindData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);

// 设置顶点属性
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0); // layout location(0) 往前面翻一翻

// 渲染一个物体需要使用着色器程序
glUseProgram(shaderProgram);

// 绘制物体
drawSomeOne();


顶点数组对象(Vertex Array Object,VAO)

用来管理一系列的顶点数据和存储顶点属性的对象,这些顶点数据保存在缓存对象中,并且由当前绑定的顶点数组对象进行管理。
顶点属性对应相关的顶点数据。

创建VAO

unsigned int VAO;

glGenVertexArrays(1,&VAO);

使用VAO,需要进行绑定,绑定之后,还需要绑定对应的VBO

//1. 绑定VAO
glBindVertexArray(VAO);
//2.把顶点数组复制到缓冲
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);

//3.设置顶点属性指针
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),
	(void*) 0);

//4.使用着色器程序进行绘制
glUseProgram(shaderProgram);

glfwSwapBuffers(window);



//5.解绑VAO VBO
glBindVertexArray(0);
glBindBuffer(0);

7.索引缓冲对象

索引缓冲对象(Element Buffer Object EBO,
Index Buffer Object IBO)

例子:假设我们绘制的是一个矩形而不是一个三角形,通过
绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)


float vertices[] ={

// 1、第一个三角形
	0.5f,0.5f,0.0f, // 右上角
	0.5f,-0.5f,0.0f,//右下角
	-0.5f,0.5f,0.0f,//左上角
//2、第二个三角形
	0.5f,-0.5f,0.0f,//右下角
	-0.5f,-0.5f,0.0f,//左下角
	-0.5f,0.5f,0.0f//左上角
}	     

由上述可知,对角线的右下角和左上角指定了两次,
一个矩阵只有4个而不是6个顶点,这样产生了50%的额外开销。

解决方案
只存储4个顶点,并且指定绘制的顺序即可,利用索引缓冲对象。

EBO和顶点缓冲一样,但是其专门存储索引,OpenGL调用这些顶点的
索引来决定该绘制哪个顶点,定义不重复的点,以及绘制出矩形所需的索引

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, // first triangle
	1,2,3 //  second triangle

}

//创建索引缓冲对象
unsigned int EBO;
glGenBuffers(1,&EBO);

//绑定EBO然后用glBufferData把索引复制到缓冲里,把这些函数调用
//放在绑定与解绑之间(TODO??),缓冲类型为GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,
	GL_STATIC_DRAW);


//注意上面传递的是GL_ELEMENT_ARRAY_BUFFER当作缓冲目标 而不是GL_ARRAY_BUFFER,因此在绘制过程中调用的是glDrawElements而不是
//glDrawArrays()
//1.绘制的图形
//2.绘制顶点的个数,共需要绘制6个顶点
//3.索引类型
//4.EBO偏移量
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章