<三> 初探opengl, 画三角形

  环境搭建好,我们当然就是开始写代码,这里就得先了解opengl的一些工作流程。首先我们得了解三个单词:

    顶点数组对象(VAO) 

    顶点缓冲对象(VBO)

    索引缓冲对象(EBO)

   比较简单的概括下这节的工作流程。

    1.定义好三角形的三个顶点

    2.绑定VBO,把三角形数据传入进去

    3.做顶点的链接,规定属性

    4.编写最简单的顶点着色器和片段着色器

    5.编译着色器,并加载链接到项目里

    6.开启循环渲染模式

 

1.定义顶点

  首先我们定义要先定义好三角形的三个点,由于是传入给opengl使用,opengl内部是使用标准化设备坐标,也就是-1到1的坐标体系。因为opengl并不会也不想知道你设备显示多少尺寸啥的,它只要做好自己的计算,后面显示会通过转换为屏幕空间坐标来显示,这一步在视口变化的时候完成。

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

这里就有3个顶点,每3个一组,分别是x,y,z,这三个坐标应该来说大家都十分熟悉的吧。定义好后,我们需要把他传入到图形渲染管线的第一个处理阶段:顶点着色器。opengl通过VBO来管理这个内存,它会在gpu内存中存储打量顶点。使用它我们就可以把数据一次性传输到显卡里,而不用一点一点发,加快读取效率。

 

2.创建并绑定VBO数据

unsigned int VBO;
glGenBuffers(1, &VBO);
//创建并且声称一个vbo

opengl里的对象都是通过一个id来索引,这有点像当时看的一个叫Genius-X的游戏框架,此框架就是通过id来确定一个游戏对象实体,其他游戏组建通过此id依附到对象上。

此外,opengl操作对象都是需要我们先对其绑定到规定的缓冲对象类型上来进行操作,而不直接对我们索引目表进行操作,所以我们需要把顶点绑定到vbo时,需要这么写

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

把vbo绑定到GL_ARRAY_BUFFER缓冲类型上,opengl还有很多其他的对象类型,后面可能会慢慢说道。通过绑定后,我们对GL_ARRAY_BUFFER操作就可以了,然后glBufferData就是进行数据的传入,四个参数是缓冲对象,顶点数组大小,顶点数组,管理顶点方式。顶点管理方式有3种:

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

我们暂时不会改数据,所以用static就可以了。

 

  虽然这里已经达到了数据绑定的效果,但如果我们需要绘制多个物品的时候,我们需要重复多次重复顶点属性的设定,所以出现了VAO这个东西来做优化。VAO可以看起来是VBO数组,只是其还附加了其他参数和属性。使用它也很简单,和VBO类似。

unsigned int g_VAO;
glGenVertexArrays(1, &g_VAO);
glBindVertexArray(g_VAO);

也是先创建id,再绑定到顶点数组上。其后,我们照常做后面的VBO操作和顶点属性链接即可,vbo会自动存入到vao中,顶点属性链接只需要操作一次即可对所有vbo生效。

 

3.顶点属性链接

  传入了顶点数据后,我们要告知opengl怎么解析这些数据,其本身是不知道如何处理这个数组的

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer的参数一个一个解析

  第一个参数是指定我们在顶点着色器中使用的位置属性,一般默认为0,我们先传入0,这个属性会和顶点着色器里数据读取配合使用

  第二个参数是指定顶点属性的大小,我们由3个值组成

  第三个是顶点的数据类型,我们用了GL_FLOAT的浮点类型

  第四个是是否希望数据被标准化,如果我们设置为GL_TRUE就会被映射到-1 和 1之间,我们设为false,因为我们的数据已经标准化了

  第五个数据是步长,也就是读取下一个顶点需要跳多少内存,我们这里是3*sizeof(float)大小,因为我们顶点紧密排列的

  第六个是一个强制转换,表示数据在缓冲中位置偏移量,因为数据在数组开头,所以是0

而glEnableVertexAttribArray则是启用顶点属性,因为这个属性默认是禁止的,0跟上面说的第一个参数意义一样。

 

4.编写简单的顶点着色器

  当我们输入好了顶点,我们就开始编写渲染过程中可编程的两个部分,顶点着色器和片段着色器。在此之前我们先了解下图形渲染管线的几个阶段。

  首先就是我们的顶点,输入后,顶点着色器会确定顶点大概的位置,并进行一些基本处理。之后,图元装配会把 顶点装配成指定的土元形状,这里我用是需要三角形。 然后就是几何着色器,这个着色器还没了解是干嘛,只知道也是一个可编程的,大体就是用于生成其他形状。然后输出给光栅化阶段,这个阶段只是把图元映射成屏幕上对应的像素位,供片段着色器使用的片段。然后会进行一次裁剪,把视图看不到的区域给裁掉,提升执行效率。一个片段,就是渲染一个像素所需要的所有数据片段着色器目的就是计算一个像素的最终颜色,这里用于处理光照啊,阴影啊什么的。

  最后的数据会进行多种测试,这个在cocos2dx读书笔记里有提过,这里暂且不写了。(参考 这里

 

1.顶点着色器

  了解到这里,我们现在开始编写顶点着色器,他就是处理位置为主的一个编程块。

#version 330 core //设定使用的opengl版本,我们用3.3的核心模式
layout (location = 0) in vec3 aPos; //定位输入顶点的位置,这里和上面说的glVertexAttribPointer第一个参数对应,是一个三分量的对象

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); //原封不动传出去,第四个是齐次分量,用于3d使用,2d我们使用1.0
}

这是最简单的一个顶点着色器,把顶点原封不动传出去,我们可以做各种变换达到我们想要的效果,例如位移,缩放。这段代码可以开一个txt文本来写,写好后放到项目工程一个新目录里,例如shader目录即可,这里我改名为ver.vs,而不用放入源码目录里,否则项目编译时候会报错,我就是踩了这个坑。

 

 写好后,我们需要编译这个着色器。

// 编译顶点着色器
unsigned int vertexShader;
string vertexShaderSource = readFile("ver.vs"); //读取着色器代码字符串
const char* verCode = vertexShaderSource.c_str();
vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建顶点着色器,创建片段则用GL_FRAGMENT_SHADER
glShaderSource(vertexShader, 1, &verCode, NULL); //绑定着色器源码,1代表只有一段源码,可传入多段,第三个就是源码地址
glCompileShader(vertexShader); //编译源码

如果想知道是否编译成功,可以用glCompileShader来查看

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

glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); //infoLog里就有对应的错误日志

 

2.片段着色器

  编写片段着色器

#version 330 core
out vec4 FragColor; //输出的片段变量

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); //设定了某个颜色
} 
//绑定片段着色器
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

 

3.着色器程序

  写完2个着色器后,我们需要把他连接合并在一起,供给系统使用。

unsigned int g_ShaderID;
g_ShaderID = glCreateProgram(); //创建着色器程序
glAttachShader(g_ShaderID, vertexShader); //绑定顶点着色器
glAttachShader(g_ShaderID, fragmentShader); //绑定片段着色器
glLinkProgram(g_ShaderID); //链接程序
//链接后我们可以删除2个着色器了
glDeleteShader(vertexShader); 
glDeleteShader(fragmentShader);
//之后我们可以通过glUseProgram(g_ShaderID)来使用这个程序

 

开启循环渲染模式

  到这里,我们离渲染出三角形还差一步,就是调用画图函数

glUseProgram(g_ShaderID); //使用这个着色器程序
glDrawArrays(GL_TRIANGLES, 0, 3); //画图

glDrawArrays第一个参数是我们需要画的图形,我们设定为三角形,我估计这里是给图形装配使用的。第二个参数是顶点数组起始索引,这里填0,第三个是绘制多少个顶点,这里我们是3个,此时一运行,三角形出来了。

我们尝试注释glUseProgram,可以发现图形变白,可见他有默认的着色器在运行。

 

EBO的使用

  当我们想要画2个三角形的时候,我们可能需要传入6个点,但如果顶点有重复的时候,就会出现内存的浪费,而3d模型中就是通过很多微小的三角形连接而成,所以出现了EBO。简单来说,ebo就是对顶点数据进行一个索引

float vertices[] = {
    0.5f, 0.5f, 0,
    0.5f, -0.5f, 0,
    -0.5f, -0.5f, 0,
    -0.5f, 0.5f, 0.0f
};
unsigned int indices[] = {
    0, 1, 3,
    1, 2, 3
};

可以看到我们2个三角形,是用vertices的0,1,3三个点构成,1,2,3构成另一个三角形。

glGenBuffers(1, &g_EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

可以看出和vbo加载差不多,只是他绑定到GL_ELEMENT_ARRAY_BUFFER缓冲区。

然后我们通过替换画图函数,即可实现画图

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glDrawElements就是使用索引来画图,第一个参数指定图形,第二个是需要顶点个数,第三个是索引数据类型,第四个是ebo数组中的起始偏移量,现在就大概过了一遍简单的渲染过程。

 

posted on 2018-07-05 18:49  usp10  阅读(964)  评论(0编辑  收藏  举报

导航