zoukankan      html  css  js  c++  java
  • OpenGL学习笔记《二》绘制简单的图形

      在开始绘图之前,简单的了解一下opengl的绘图流程。在opengl里面,所有的事物都是处于3D空间中,而我们的屏幕及像素是以2D表现的,所以就需要将3D转换为2D。opengl内部管理这个流程的叫做渲染管线,主要分为两个部分:3D坐标转换为2D坐标,2D坐标转换为带颜色的像素数据。细分来看主要分为6个流程/步骤,每个流程相互独立,以流程数据的输入、输出作为数据传递方式。每个流程/步骤,由一小段程序/代码组成,所以又可以称之为shader/着色器程序。而gpu对于这些片段程序的执行效率非常高,并且由于gpu的核心数多,可以并发执行的量也非常大。

      这六个步骤如下所示:

       上图中蓝色标识的步骤,就是一般我们做shader编程的步骤。不过目前我们主要是做第一步(Vertex Shader顶点着色器)和第五步(Fragment Shader片段着色器)这两个步骤的编程。顶点着色器处理输入的顶点数据,转换好坐标,片段着色器处理进过光栅化后的像素数据,生成最终显示的颜色信息。

      我们要在屏幕上绘制出简单的图形,那么就必须要提供顶点着色器和片段着色器,才能最终显示出样式、颜色正常的图形。

    1、简单的着色器程序

      首先是Vertex Shader,顶点着色器程序代码

    const char* vertexShaderSource = "#version 330 core
    "
            "layout (location = 0) in vec3 aPos;
    "
            "void main()
    {"
            "    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    "
            "}";

      代码中指定了opengl的版本,然后指定了 in vec3类型的输入变量,这个是我们输入的顶点数据,注意到这里我们额外用到了layout(location = 0),这个在后面会提到。然后就是着色器程序的核心main函数了,在这里我们给opengl的內建变量gl_Position赋值,即我们传过来的顶点位置属性值。

      其次是Fragment Shader,片段着色器程序代码

    const char* fragmentShaderSource = "#version 330 core
    "
            "out vec4 FragColor;
    "
            "void main()
    {"
            "    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
    "
            "}";

      同样的需要指定opengl的版本,然后声明一个out vec4类型的变量,这个表示是着色器的输出数据,也就是片段着色器处理好的像素颜色值的数据,没有这个返回,我们的绘图出来的将会是空白或者黑的。

      然后我们需要根据程序代码编译链接成着色器程序

       // build and compile shader
        // --------------------------------
        // create vertex shader
        unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
        // attach shader source
        glShaderSource(vertexShader, 1, &vSource, nullptr);
        // compile shader
        glCompileShader(vertexShader);
        // check error
        int success;
        char infoLog[512];
        glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
            std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED
    " << infoLog << std::endl;
        }
        // create fragment shader
        unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fSource, nullptr);
        glCompileShader(fragmentShader);
        glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
            std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED
    " << infoLog << std::endl;
        }
        // build and link shaderprogram
        // --------------------------------
        // create shader program
        unsigned int shaderProgram = glCreateProgram();
        // attach shader and link program
        glAttachShader(shaderProgram, vertexShader);
        glAttachShader(shaderProgram, fragmentShader);
        glLinkProgram(shaderProgram);
        // check error
        glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
        if (!success) {
            glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
            std::cout << "ERROR::PROGRAM::ATTACH_LINK_FAILED
    " << infoLog << std::endl;
        }
        // delete shader 
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);

      创建和编译着色器的流程都差不多:

      glCreateShader根据参数类型,返回我们需要创建的着色器类型;

      glShaderSource将我们提供的着色器代码绑定到着色器上;

      glCompileShader编译我们的着色器;

      glGetShaderiv可以检查我们的编译结果;

       编译好着色器之后,我们需要创建着色器程序:

      glCreateProgram创建着色器程序,返回一个unsigned int类型的索引ID,在后面我们需要用到;

      glAttachShader将我们编译好的着色器附加到着色器程序上;

      glLinkProgram链接好着色器程序;

      glGetProgramiv可以检查我们的链接结果。

      最后,我们需要删除编译的着色器,通过glDeleteShader方法。

      我们最终需要用到的是glCreateProgram方法返回的着色器程序的索引ID。

    2、提供顶点数据

      上面的流程图中可以看到,第一步就是对输入的顶点数据进行处理,所以我们需要提供顶点数据。顶点数据包含顶点的位置、颜色、纹理坐标等多种类型的数据,在这里我们先简单的提供位置数据,颜色的话直接在片段着色器中固定一种颜色。

      我们可以一次提供一个顶点数据,但是这个方式效率太低。以前的opengl也有一个Display list 的概念,一次打包提供一组数据,这样效率高,但是由于数据是直接存储在了GPU端,数据一旦提供了就无法再调整了。后来又提供了VBO(vertex buffer object)的概念,也是一次收集/打包好一组数据,但相较于Display List,数据是收集在CPU端,每次渲染会再传递一次。所以这种方式相较于一次提供一个顶点数据,效率更高,但是相较于Display List方式,效率稍微低一点,不过灵活性提高了。

      所以我们首先可以声明一个顶点数据数组,然后绑定到VBO对象上:

    float vertices[] = {
            -1.0f, -0.5f, 0.0f, // left
            0.0f, -0.5f, 0.0f, // right
            -0.5f, 0.5f, 0.0f // top
        };
    unsigned int VBO;
    glGenBuffers(1, &VBO);
    ... loop
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glUseProgram(shaderProgram); 
    ...draw
    ...loop

      glBindBuffer 和 glBufferData 两个方法,及将顶点数据绑定VBO及赋值VBO对象的方法。然后GPU需要知道如何使用这些数据,glVertexAttribPointer方法负责让GPU知道如何使用这些数据。

      glVertexAttribPointer的参数1对应我们在顶点着色器中提到的(layout = 0)的属性,参数2表示这个属性值用到的数据数量(我们这里用到的是位置属性,每个属性由三个浮点数据构成),参数3表示数据类型(我们这里用到的是浮点型),参数4表示是否需要对数据类型进行转换,参数5表示两两数据属性间的间隔(因为我们这里每个属性值之间是紧挨着的,所以间隔就是一个属性的数据量),参数6表示属性数据的起始索引(在这里我们填0,在后面涉及到配置多个顶点属性的时候,这个地方的值就会有变化);

      glEnableVertexAttribArray 方法的参数,指定激活我们配置的哪个属性,在上面我提到了配置的是(layout = 0) 的属性,所以这里填0;

      在上面的代码中我们可以看到,在渲染的循环中,我们一直要调用绑定VBO、赋值VBO、设置数据使用方法等接口,有点复杂。这个时候就需要引入另外一个概念vertex array object(VAO),来简化我们的操作。VAO概念是用来记录我们在绑定、赋值VBO对象,设置数据使用方法的,在引入VAO之后,我们的渲染流程可以调整为:    unsigned int VBO, VAO;    glGenVertexArrays(1, &VAO);

        glGenBuffers(1, &VBO);
        // bind the vertex array object
        glBindVertexArray(VAO);
        // bind the vertex buffer object
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        // fill data uage:GL_STATIC_DRAW, GL_DYNAMIC_DRAW, GL_STREAM_DRAW
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        // tell opengl how it should interpret the vertex data
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
       ...loop
       glUseProgram(shaderProgram);
       glBindVertexArray(VAO);
       ...draw
       ...loop
       glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

      可以看到,我们的渲染流程变得简单了,数据的初始化和配置,放到了渲染流程之外,在渲染的时候,只需要绑定VAO对象,就可以直接执行绘图操作了。当然在循环退出之后,我们也要释放掉VAO和VBO对象。

    3、绘制第一个三角形

      在上面我们提供了一个包含3个顶点位置的顶点数据,创建好了着色器程序,同时也引入了VAO概念,简化了我们的渲染流程。opengl提供了简单的绘图方法供我们使用,在这里我们需要绘制的是三角形,我们可以用如下的代码来绘图:

      glDrawArrays(GL_TRIANGLES, 0, 3);

      我们画的是三角形,所以我们第一个参数类型填的就是三角形;第二个参数指定顶点数据在数组中的起始位置,在这里我们填0;第三个参数表示要绘制的点数量,我们这里有三个顶点,我们传3就可以了。编译执行我们的项目,可以得到一个简单的三角形:

     4、绘制一个矩形

      在上面我们绘制了一个三角形,现在我们要绘制一个矩形,该如何操作?

      我们知道一个矩形可以通过画两个三角形实现,所以我们可以改一下我们的顶点数据数组,提供两个三角形的顶点数据

    float vertices[] = {
        // first triangle
         0.5f,  0.5f, 0.0f,  // top right
         0.5f, -0.5f, 0.0f,  // bottom right
        -0.5f,  0.5f, 0.0f,  // top left 
        // second triangle
         0.5f, -0.5f, 0.0f,  // bottom right
        -0.5f, -0.5f, 0.0f,  // bottom left
        -0.5f,  0.5f, 0.0f   // top left
    }; 

      然后修改我们的绘图函数,将顶点数量改为6,我们就可以得到一个矩形了。

      但是再看一下数组的数据,发现其中其实是有重复的,比如第一个三角形的右下角和第二个三角形的右下角。如果要绘制的矩形数量较少影响不大,如果矩形数量多了,那么我们就会有好多重复的数据,将会占用额外的内存,同时也造成项目数据复杂化了。所以此时我们需要引入另外一个概念element buffer object(EBO)对象,来简化我们的操作,在中文中这个对象也翻译为顶点索引对象。顾名思义,这个是对顶点进行索引编号,告诉GPU该如何使用我们提供的顶点数据。

      

       // indice data
        unsigned int indices[] = {  // note that we start from 0!
            0, 1, 3,   // first triangle
            1, 2, 3    // second triangle
        };
       unsigned int VAO, VBO, EBO;
        // gen
        glGenBuffers(1, &EBO);
        // bind VAO
        // bind VBO
        // bind EBO
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
        // set pointer
        ... render loop
        ... render loop
        glDeleteVertexArrays(1, &VAO);
        glDeleteBuffers(1, &VBO);
        glDeleteBuffers(1, &EBO);

      简化一下流程。我们需要提供一份索引列表,对应的就是顶点数据中的顶点索引。然后绑定并赋值EBO对象,其余的操作跟之前类似,编译执行,我们就得到了一个简单的矩形。

      在这里我们需要注意的是,在最后删除VAO、VBO、EBO的时候,一定不能在删除VAO对象之前先删除EBO,因为在VAO对象内部其实维护了一份EBO对象的数据,如果先删除了EBO,会导致删除出现异常。

      以上就是利用opoengl提供的接口,绘制的简单图形。

      对应的代码在这里

  • 相关阅读:
    HDU
    POJ
    快速幂运算
    RocketMQ集群
    RocketMQ角色介绍
    RocketMQ初探
    MySQL 串行隔离级别小记
    分布式事务笔记
    MySQL分库分表篇
    MySQL主从篇
  • 原文地址:https://www.cnblogs.com/zhong-dev/p/11598177.html
Copyright © 2011-2022 走看看