opengl 画三角形

LearnOpenGL 学习笔记

使用环境:mac10.12, xocde, cocos2d-x 3.13 ,即:opengl es。关于opengl 和 opengl es的区别,见:http://www.cnblogs.com/salam/archive/2016/01/08/5113572.html

opengl 画三角形_第1张图片

顶点输入

要渲染一个三角形,需指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个GLfloat数组。

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

这些坐标点都是“ 标准化设备坐标(Normalized Device Coordinates, NDC)。标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任

何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):

opengl 画三角形_第2张图片


咱要渲染的是一个2D三角形,所以将它顶点的z坐标设置为0.0,这样子的话三角形每一点的深度都是一样的,看上去就像是2d了。深度

可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节

省资源。


定义这样的顶点数据以后,我们会把它输入给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数

据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。


顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个GPU内存(通常被称为显存),它会在GPU内存中储存大量顶点。使用这些缓冲对象的好处

是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶

点,这是个非常快的过程。


vbo的创建

使用glGenBuffers函数和一个缓冲ID生成一个VBO对象

GLuint VBO;
glGenBuffers(1, &VBO); 

顶点缓冲对象的缓冲类型是 GL_ARRAY_BUFFER 。我们使用glBindBuffer 函数把新创建的缓冲绑定到 GL_ARRAY_BUFFER 目标上:

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 :数据每次绘制时都会改变。

三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是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);
}

为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的 gl_Position 变量,它在幕后是 vec4 类型的。在main 函数的最后,我们

gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以

vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。

在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.xvec.yvec.zvec.w来获取。注

vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视划分(Perspective Division)上。我们会在后面的

教程中更详细地讨论向量。




编译着色器

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

在xcode- cocos2d-x 的环境下,只要在init中添加如下代码即可:

    auto program = new GLProgram;
    program->initWithFilenames("triangle.vsh", "triangle.fsh");

triangle.vsh就是上边刚定义的那个vertex shader。triangle.fsh是下边定义的fragment shader。


片段着色器(Fragment Shader)

片段着色器全是关于计算你的像素最后的颜色输出。为了让事情更简单,我们的片段着色器将会一直输出固定色。

//三角形的fragment shader

void main()
{
    gl_FragColor = vec4(1, 0.5, 0.5, 1);
}

这里弄好后,上边的 glprogram  着色器程序需要链接这2个着色器:

    auto program = new GLProgram;  
    program->initWithFilenames("triangle.vsh", "triangle.fsh");  
    program->link();  
    this->setGLProgram(program); 


链接顶点属性

顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪

一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。

顶点缓冲数据会被解析为下面这样子:

opengl 画三角形_第3张图片

  • 位置数据被储存为32-bit(4字节)浮点值。
  • 每个位置包含3个这样的值。
  • 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列。
  • 数据中第一个值在缓冲开始的位置。
代码:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);

  • 第一个参数指定我们要配置的顶点属性。这里把数据传到vertex shader中第一个位置上(从0开始)的那个属性,也就是

    attribute vec3 a_position; 因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0

  • 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vecX都是由浮点数值组成的)。
  • 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
  • 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个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();

每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕

见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在

一个对象中,并且可以通过绑定这个对象来恢复状态呢?


顶点数组对象

顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。

一个顶点数组对象会储存以下这些内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
创建一个VAO:

GLuint VAO;
glGenVertexArrays(1, &VAO); 

要想使用VAO,要做的只是使用glBindVertexArray 绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑

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);

然后用glBufferData 把索引复制到缓冲里,这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 

最后一件要做的事是用glDrawElements 来替换glDrawArrays 函数,来指明我们从索引缓冲渲染。使用glDrawElements 时,我们会使

用当前绑定的索引缓冲对象中的索引进行绘制:

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);
}




你可能感兴趣的:(shader,opengl,图形基础)