LearnOpenGL 学习笔记
使用环境:mac10.12, xocde, cocos2d-x 3.13 ,即:opengl es。关于opengl 和 opengl es的区别,见:http://www.cnblogs.com/salam/archive/2016/01/08/5113572.html
要渲染一个三角形,需指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个GLfloat
数组。
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):
咱要渲染的是一个2D三角形,所以将它顶点的z坐标设置为0.0,这样子的话三角形每一点的深度都是一样的,看上去就像是2d了。深度
可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节
省资源。
定义这样的顶点数据以后,我们会把它输入给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数
据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。
顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个GPU内存(通常被称为显存),它会在GPU内存中储存大量顶点。使用这些缓冲对象的好处
是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶
点,这是个非常快的过程。
vbo的创建:
使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof
计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。
第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。
//三角形的vertex shader
attribute vec3 a_position; // attribute是从外部传进来的,每一个顶点都会有这两个属性,所以它也叫做vertex attribute(顶点属性)
void main()
{
gl_Position = vec4(a_position.x, a_position.y, a_position.z, 1);
}
vec4
类型的。在main
函数的最后,我们
将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以
把vec3
的数据作为vec4
构造器的参数,同时把w
分量设置为1.0f
(我们会在后面解释为什么)来完成这一任务。
在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x
、vec.y
、vec.z
和vec.w
来获取。注
意vec.w
分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视划分(Perspective Division)上。我们会在后面的
教程中更详细地讨论向量。
我们已经写了一个顶点着色器源码(储存在一个C的字符串中),但是为了能够让OpenGL使用它,我们必须在运行时动态编译它的源码。
在xcode- cocos2d-x 的环境下,只要在init中添加如下代码即可:
auto program = new GLProgram;
program->initWithFilenames("triangle.vsh", "triangle.fsh");
片段着色器全是关于计算你的像素最后的颜色输出。为了让事情更简单,我们的片段着色器将会一直输出固定色。
//三角形的fragment shader
void main()
{
gl_FragColor = vec4(1, 0.5, 0.5, 1);
}
auto program = new GLProgram;
program->initWithFilenames("triangle.vsh", "triangle.fsh");
program->link();
this->setGLProgram(program);
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪
一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
顶点缓冲数据会被解析为下面这样子:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
attribute vec3 a_position; 因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。
vec3
,它由3个值组成,所以大小是3。vecX
都是由浮点数值组成的)。GLfloat
之后,我们把步长设置为3 * sizeof(GLfloat)
。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。GLvoid*
,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调
用glVetexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVetexAttribPointer之前绑定的是先前定义的
VBO对象,顶点属性0
现在会链接到它的顶点数据。
已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属
性。代码会像是这样:
// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在
一个对象中,并且可以通过绑定这个对象来恢复状态呢?
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。
一个顶点数组对象会储存以下这些内容:
GLuint VAO;
glGenVertexArrays(1, &VAO);
VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看
起来像这样(伪代码):
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
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(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//4. 解绑VAO
glBindVertexArray(0);
[...]
// ..:: 绘制代(游戏循环中) :: ..
// 5. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);
//
// Triangle.cpp
// shaderTest
//
// Created by MacSBL on 2016/12/15.
//
//
#include "OpenGLTriangle.hpp"
bool OpenGLTriangle::init()
{
if (!Layer::init()) {
return false;
}
GLfloat vertices[] =
{
-0.5, -0.5, 0,
0.5, -0.5, 0,
0, 0.5, 0
};
//1、绑定vao
//顶点数组对象(Vertex Array Object)被绑定后,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 2. 把顶点数组复制到缓冲(vbo)中供OpenGL使用
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//把之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3* sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//解除绑定vao和vbo
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
auto program = new GLProgram;
program->initWithFilenames("triangle.vsh", "triangle.fsh");
program->link();
this->setGLProgram(program);
return true;
}
void OpenGLTriangle::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
{
Layer::visit(renderer, parentTransform, parentFlags);
_command.init(_globalZOrder);
_command.func = CC_CALLBACK_0(OpenGLTriangle::onDraw, this);
Director::getInstance()->getRenderer()->addCommand(&_command);
}
void OpenGLTriangle::onDraw()
{
//获取当前node 的shader
auto glprogram = getGLProgram();
//需要在init中指定shader才能在这use
glprogram->use();
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
}
要画一个四边形,我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合:
GLfloat 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, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
如果有包括上千个三角形的模型之后就太糟糕了,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺
序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。索引缓冲对象(Element Buffer Object,EBO,也叫
Index Buffer Object,IBO)的工作方式正是这样的。和顶点缓冲对象一样,EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的
索引来决定该绘制哪个顶点:
GLfloat 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 // 左上角
};
GLuint indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
用当前绑定的索引缓冲对象中的索引进行绘制:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 3. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 4. 解绑VAO(不是EBO!)
glBindVertexArray(0);
[...]
// ..:: 绘制代码(游戏循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
#include "OpenGLQuadrangle.hpp"
bool OpenGLQuadrangle::init()
{
if (!Layer::init()) {
return false;
}
GLfloat vertices[] = {
0.5, 0.5, 0, //右上
0.5, -0.5, 0, //右下
-0.5, -0.5, 0, //左下
-0.5, 0.5, 0 //左上
};
GLuint indices[] = {
0, 1, 3,
1, 2, 3
};
//1. 绑定顶点数组对象
//vao,
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
//vbo
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
//ebo
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//5.解绑VAO(不是EBO!)
glBindVertexArray(0);
//着色器程序
auto program = new GLProgram;
program->initWithFilenames("triangle.vsh", "triangle.fsh");
program->link();
this->setGLProgram(program);
return true;
}
void OpenGLQuadrangle::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
{
Layer::visit(renderer, parentTransform, parentFlags);
_command.init(_globalZOrder);
_command.func = CC_CALLBACK_0(OpenGLQuadrangle::onDraw, this);
Director::getInstance()->getRenderer()->addCommand(&_command);
}
void OpenGLQuadrangle::onDraw()
{
//获取当前node 的shader
auto glprogram = getGLProgram();
//需要在init中指定shader才能在这use
glprogram->use();
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}