最近完整的学习了learnopengl(http://www.learnopengl.com/),觉得非常有启发,从而又想起了这个长草许久的专题,正好趁这段时间,从本篇起完成这个专题,需要说明的是,从本系列的第五篇起将使用cocos2d-x3.10版本对应的代码,最早的三篇采用的是相对比较老的代码,但是我重新阅读后发现对理解有没有什么影响,所以暂时我不会升级之前的代码,但是为了保持和最新代码的一致性,从本篇起我们会采用最新的代码。
cocos2d-x的底层调用的是openGL ES的api,因此所有的绘制命令追根到底都是调用openGLES,本系列的教程会“刨根问底”的从cocos2d-x的功能讲解到底层代码的调用,从而达到“从cocos2d-x学习openGL”的目的。
几乎所有openGL教程都会从渲染过程调用着色器的渲染顺序讲起,由于我们这个系列是从openGL,所以,我们的顺序略有不同,我们会从cocos2d-x的应用层讲起,然后讲到调用openGL的代码,将这个过程讲清楚后,我们会在下一节从openGL的层面介绍渲染顺序。
下面的代码会被添加到我们的场景的上,从而我们的屏幕会显示一条直线:
auto drawNode = DrawNode::create(); drawNode->drawLine(Vec2(100,100),Vec2(200,200),Color4F::BLUE); this->addChild(drawNode);细节我想应该不用详细解释了,我们创建了一个DrawNode节点,然后让他绘制了一个从(100,100)到(200,200)的蓝色直线,显示的结果如图所示:
那么这个直线到底是如何绘制出来的呢?首先,我们先复习一下VAO和VBO的概念,VBO即顶点缓冲对象,他可以发送顶点数据到GPU上,VAO是顶点数组对象,可以绑定VBO,这样就不用每次都重新配置VBO了,任何对VBO数据层面的修改都可以直接映射,类似数据的指针和组织形式,但是并不是所有设备都支持VAO,所以,我们cocos2d-x通过一个函数来判断当前设备的openGL是否支持VAO:
Configuration::getInstance()->supportsShareableVAO()
这个函数返回布尔型的返回值,如果是true说明设备支持VAO,我们首先看 DrawNode类的init函数,我们从相对简单的VBO部分说起
//生成VBO glGenBuffers(1,&_vbo); //绑定VBO glBindBuffer(GL_ARRAY_BUFFER,_vbo); //传入数据 glBufferData(GL_ARRAY_BUFFER,sizeof(V2F_C4B_T2F)* _bufferCapacity,_buffer,GL_STREAM_DRAW); //直线部分 glGenBuffers(1,&_vboGLLine); glBindBuffer(GL_ARRAY_BUFFER,_vboGLLine); glBufferData(GL_ARRAY_BUFFER,sizeof(V2F_C4B_T2F)*_bufferCapacityGLLine,_bufferGLLine,GL_STREAM_DRAW); //点部分 glGenBuffers(1,&_vboGLPoint); glBindBuffer(GL_ARRAY_BUFFER,_vboGLPoint); glBufferData(GL_ARRAY_BUFFER,sizeof(V2F_C4B_T2F)*_bufferCapacityGLPoint,_bufferGLPoint,GL_STREAM_DRAW); //解绑 glBindBuffer(GL_ARRAY_BUFFER,0);
glGenBuffers返回n(第一个参数)个当前未使用的缓存对象名称,并保存到第二个参数的地址中。这里返回的名称只用于分配其他缓存对象,它们在绑定之后只会记录一个可用的状态。
然后是调用glBindBuffer绑定缓冲,glBindBuffer完成三项工作,1)如果是第一次绑定,且它是一个非零的无符号整型,那么将创建一个与该名称相应得新缓存对象。2)如果绑定到一个已经创建的缓存对象,那么它将成为当前被激活的缓存对象。3)如果绑定的buffer值为0,那么openGL将不再对当前target应用任何缓存对象。要记住,openGL是个状态机,所有的设置都是设置当前的状态,如果进行下一次设置,那么当前的状态将会继续保留,就像一个工作台,我们把需要处理的零件放在工作台上才能处理,为了防止零件被做额外的处理,在处理完成后我们要把工作台清理干净,这里“glBindBuffer(GL_ARRAY_BUFFER,_vbo);”就是把零件放在加工台上,而“glBindBuffer(GL_ARRAY_BUFFER,0);”就是将零件台清理干净。
最后就是调用glBufferData装入数据,它主要有两个任务:分配顶点数据所需的存储空间,然后将数据从应用程序的数组中拷贝到openGL服务端的内存中。这个函数有五个参数,它们的意义如下:
第一个参数:顶点属性数据类型,顶点数据是GL_ARRAY_BUFFER,索引数据是GL_ELEMENT_ARRAY_BUFFER,纹理缓冲数据GL_TEXTURE
第二个参数:数据大小
第三个参数:要么是个客户端内存指针,以便初始化缓存对象,要么是NULL。如果传入的指针非空,那么数据将从客户端拷贝到服务器端,如果传入的指针为空,那么将保留未初始化数据。
第四个参数:分配数据的读取和写入方式,包括GL_STATIC_DRAW(几乎不会改变),GL_DYNAMIC_DRAW(数据易变),GL_STEAM_DRAW(每次都会被改变)等
需要说明的是,这里把数据分为vbo,线vbo和点vbo,因为这里绘制的基本图形不是由线组成的,就是用点组成的(是的,画圆的时候要设置segments,其实就是设置直线的片段数)。
下面是VAO,VAO也分为vao,线vao和点vao,这里节约空间,只介绍vao一个:
//VAO的处理 glGenVertexArrays(1,&_vao); GL::bindVAO(_vao); //设置VBO数据 glGenBuffers(1,&_vbo); glBindBuffer(GL_ARRAY_BUFFER,_vbo); glBufferData(GL_ARRAY_BUFFER,GL_STREAM_DRAW); //绑定VAO //顶点 glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION); glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION,2,GL_FLOAT,GL_FALSE,sizeof(V2F_C4B_T2F),(GLvoid *)offsetof(V2F_C4B_T2F,vertices)); //颜色 glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_COLOR); glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR,4,GL_UNSIGNED_BYTE,GL_TRUE,colors)); //贴图 glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_TEX_COORD); glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD,texCoords)); //解绑 GL::bindVAO(0); glBindBuffer(GL_ARRAY_BUFFER,0);整个过程和VBO类似,首先调用 glGenVertexArrays 生成一个VAO,然后调用 GL :: bindVAO ( _vao );绑定VAO,最后调用GL::bindVAO(0);解绑
中间是VAO和VBO的数据连接的过程
如图就是VBO和VAO的关系,VAO索引到VBO数据上,像个指针,索引着VBO上的数据。
接下来调用 glEnableVertexAttribArray和 glVertexAttribPointer连接顶点属性,glEnableVertexAttribArray设置是否启用与index(参数)相关联的顶点数据,默认是关闭的。glVertexAttribPointer是一个非常灵活的命令,我们手动指定我们的数据中的那部分对应相应的着色器上的那部分,参数的意义如下第一个参数:着色器位置索引,在 GLProgram中我们定义了一个枚举,索引了所有的属性,这个参数和glEnableVertexAttribArray一致
第二个参数:每个顶点的元素数目,第三个参数:数据类型
第四个参数:是否对参数进行标准化,标准化即把参数转换成(-1,1)的范围,注意这项如果设为TRUE会进行额外的转化计算,会造成额外的开销,这时需要注意的
第五个参数:步长,两段相同数据之间的距离,第六个参数:偏移量,为什么要设计偏移量呢,因为数据未必是连续的,有可能是位置和颜色混在一起,就如同cocos2d-x中的一样,我们来看V2F_C4B_T2F结构体的定义:
struct V2F_C4B_T2F { /// vertices (2F) Vec2 vertices; /// colors (4B) Color4B colors; /// tex coords (2F) Tex2F texCoords; };也就是首先是二维位置,然后是颜色和贴图,所以,偏移量就是我们需要的数据的起始位置,注意,我们2d图形只有x,y坐标,所以就是Vec2,DrawNode3D就是三维的。
为什么在init中采用VAO的代码量反倒多了呢,因为VAO一劳永逸的把数据的组织方式都设置好了,后面不再修改,VBO则要在绘制的每一帧都设置一遍这些内容,代码如下:
if (_dirty) { glBindBuffer(GL_ARRAY_BUFFER,_vbo); glBufferData(GL_ARRAY_BUFFER,sizeof(V2F_C4B_T2F)*_bufferCapacity,GL_STREAM_DRAW); _dirty = false; } if (Configuration::getInstance()->supportsShareableVAO()) { GL::bindVAO(_vao); } else { GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX); glBindBuffer(GL_ARRAY_BUFFER,_vbo); // vertex glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION,vertices)); // color glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR,colors)); // texcood glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD,texCoords)); } glDrawArrays(GL_LINES,_bufferCount); glBindBuffer(GL_ARRAY_BUFFER,0); if (Configuration::getInstance()->supportsShareableVAO()) { GL::bindVAO(0); }
这里只有一个新函数,就是glDrawArrays,它使用当前绑定的顶点数组元素(VAO)来建立一系列几何图元,第一个参数是绘制图形,第二个参数是起始位置,第三个参数是元素个数
以上就是OpenGL的VAO和VBO的使用,以及绘制流程,下一篇将介绍着色器
能力不足,水平有限,如有错误,多谢指出。