[learnOpenGL学习笔记_3] 你好,三角形

简介: [learnOpenGL学习笔记_3] 你好,三角形

learnOpenGL网站是学习openGL非常有用的网站,作为一个小白,为了方便后续回顾同时给大家提供借鉴,在此记录学习的过程。
本文参考:你好,三角形


0. 预备知识

三角形是怎样画到屏幕上的?

想要显示三角形,需要准备什么素材?

在开始写代码之前,我们应该首先了解上面两个问题。

0.1 图形渲染管线

下面两段的意思总结起来就是:图形渲染管线将3D坐标通过一步一步的处理变为屏幕上显示的2D像素,而一步一步的处理是通过各种着色器实现的。如果不想读下面的官方理论,可以直接看图,图中表示了图形渲染管线处理顶点的过程。

OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。

2D坐标和像素也是不同的,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制。

图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。


上图是图形渲染管线的各个阶段,蓝色背景部分表示我们可以注入自定义的着色器部分。下面对每个着色器阶段进行简要介绍(基础理论,建议不要跳过,了解一下渲染过程):

  • 顶点数据
    首先,以数组形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个3D坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置和一些颜色值组成的吧。
  • 顶点着色器
  • 图元装配
    将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定图元的形状;本节例子中是一个三角形。

为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去指定这些数据所表示的渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL。这是其中的几个:GL_POINTSGL_TRIANGLESGL_LINE_STRIP

  • 几何着色器
    几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
  • 光栅化
    把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。

  • 片段着色器
    片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
  • 测试与混合
    Alpha测试混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。


下面,开始写代码。

1. 顶点输入

开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示,更多说明请跳到这里)。

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

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

2. 顶点缓冲对象(VBO)

2.1 概念

顶点缓冲对象VBO是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标,顶点法向量,顶点颜色数据等。在渲染时,可以直接从VBO中取出顶点的各类属性数据,由于VBO在显存而不是在内存中,不需要从CPU传输数据,处理效率更高。

所以可以理解为VBO就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个VBO,每个VBO在OpenGL中有它的唯一标识ID,这个ID对应着具体的VBO的显存地址,通过这个ID可以对特定的VBO内的数据进行存取操作。

2.2 创建及配置

1. 创建一个VBO对象:

unsigned int VBO;
glGenBuffers(1, &VBO);

2. 绑定一个VBO对象:

OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上

glBindBuffer(GL_ARRAY_BUFFER, VBO);  

从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。

3. 将顶点数据复制到缓冲内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
  • glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

3. 顶点数组对象(VAO)

3.1 概念

VBO保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息,当数据量很大时,重复这样的动作变得非常麻烦。VAO可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个VAO对象就可以了。

VAO是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的VBO对象的引用。

VAO本身并没有存储顶点的相关属性数据,这些信息是存储在VBO中的,VAO相当于是对很多个VBO的引用,把一些VBO组合在一起作为一个对象统一管理。

OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。

3.2 创建及配置

1. 创建一个VAO对象:

unsigned int VAO;
glGenVertexArrays(1, &VAO);

2. 绑定一个VAO对象:

// bind the Vertex Array Object first
glBindVertexArray(VAO);

执行VAO绑定之后其后的所有VBO配置都是这个VAO对象的一部分,可以说VBO是对顶点属性信息的绑定,VAO是对很多个VBO的绑定。

4. 链接顶点属性

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
  • 使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上):
  • 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
  • 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
  • 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE
  • 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔。
  • 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。

每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。

  • 使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。

想要最终画出三角形,除了以上对顶点数据的处理外,还需要构造相应的着色器。 ⬇️⬇️⬇️

5. 构造着色器

5.1 顶点着色器

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

5.2 片段着色器

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

5.3 编译着色器

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

- 编译顶点着色器

  1. 创建一个着色器对象
//创建顶点着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
  1. 将着色器源码附加到着色器对象上
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
  1. 编译
glCompileShader(vertexShader);
  1. 检测编译是否成功
int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

- 编译片段着色器

  1. 创建一个着色器对象
//创建片段着色器对象
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
  1. 将着色器源码附加到着色器对象上
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
  1. 编译
glCompileShader(fragmentShader);
  1. 检测编译是否成功
int  success;
char infoLog[512];
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
    glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}

5.4 链接着色器

unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

检测链接是否错误:

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    ...
}

在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

激活链接程序:

glUseProgram(shaderProgram);

6. 画三角形

要想绘制我们想要的物体,OpenGL给我们提供了glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
  • glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。第二个参数指定了顶点数组的起始索引,我们这里填0。最后一个参数指定我们打算绘制多少个顶点,这里是3。

最终效果:

7. 总结

绘制三角形,需要做两大块内容:顶点数据和着色器。

本文前4节是对顶点数据的处理:输入顶点数据,创建并绑定VAO,创建并绑定VBO,链接顶点属性。

本文第5节是对着色器的描述:顶点着色器,片段着色器,以及将着色器链接到程序中

第6节将顶点数据和着色器合并起来画出了三角形。

8. 一些名词

8.1 标准化设备坐标(Normalized Device Coordinates, NDC)

一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):

与通常的屏幕坐标不同,y轴正方向为向上,(0,0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。

你的标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。

9. 全部源码

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";
const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\n\0";
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
void draw(GLFWwindow *window);
int main()
{
    glfwInit(); //初始化GLFW
    
    //配置GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  //告知系统我们使用的opengl版本是3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //同样明确告诉GLFW我们使用的是核心模式(Core-profile)
    
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); //MAC环境下需加这一句才能使以上配置生效
    
    GLFWwindow *window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    
    //初始化glad
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    
    //设置视口
    glViewport(0, 0, 800, 600);
    
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    
    //----------> 1. 输入三角形顶点
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f
    };
    
    //---------> 2. 创建VBO并绑定
    unsigned int VBO;
    glGenBuffers(1, &VBO);
    //绑定一个VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //将顶点数据复制到缓冲内存中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    //---------> 3. 创建VAO并绑定
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    // bind the Vertex Array Object first
    glBindVertexArray(VAO);
    //---------> 4. 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    
    //---------> 5. 创建并编译顶点着色器
    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    //检测编译是否成功
    int  success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if(!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    
    //--------> 6. 创建并编译片段着色器
    //创建片段着色器对象
    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    
    //--------> 7. 链接着色器
    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if(!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::LINK_FAILED\n" << infoLog << std::endl;
    }
    
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    
    //循环渲染
    while(!glfwWindowShouldClose(window))
    {
        //检测输入事件
        processInput(window);
        
        //渲染指令
        draw(window);
        
        //------> 8. 画三角形
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    
    //正确释放/删除之前的分配的所有资源
    glfwTerminate();
    
    return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}
void draw(GLFWwindow *window)
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
}
相关文章
|
计算机视觉
OpenCV-计算轮廓周长cv::arcLength
OpenCV-计算轮廓周长cv::arcLength
137 0
|
6月前
|
存储 索引
[learnOpenGL学习笔记_4] 你好,三角形 2
[learnOpenGL学习笔记_4] 你好,三角形 2
46 0
|
6月前
|
BI C++
[learnOpenGL学习笔记_7] 纹理
[learnOpenGL学习笔记_7] 纹理
69 0
|
6月前
|
小程序 编译器 C语言
[learnOpenGL学习笔记_5] 着色器
[learnOpenGL学习笔记_5] 着色器
63 0
|
6月前
|
存储
[learnOpenGL学习笔记_6] 着色器 - 我们自己的着色器类
[learnOpenGL学习笔记_6] 着色器 - 我们自己的着色器类
73 0
|
存储 编解码 算法
OpenCV-霍夫圆变换cv::HoughCircles
OpenCV-霍夫圆变换cv::HoughCircles
130 0
|
计算机视觉
OpenCV-绘制箭头cv::arrowedLine
OpenCV-绘制箭头cv::arrowedLine
136 0
|
搜索推荐 Java C++
OpenglEs之三角形绘制
Opengl ES连载系列
76 0
Delphi绘图功能[3] —— 使用Polyline、Polygon绘制多边形、System.Sin、System.Cos函数画圆 — 绘制五环图标
Delphi绘图功能[3] —— 使用Polyline、Polygon绘制多边形、System.Sin、System.Cos函数画圆 — 绘制五环图标
288 0
Delphi绘图功能[3] —— 使用Polyline、Polygon绘制多边形、System.Sin、System.Cos函数画圆 — 绘制五环图标
|
缓存
学习OpenGL ES之绘制一个正方体
学习OpenGL ES之绘制一个正方体
学习OpenGL ES之绘制一个正方体