1.2 HELLO 三角形

发布时间 2023-07-25 14:47:42作者: 跳岩

这一节,我觉得是相当有难度的。渲染一个三角形,就需要介绍GLSL语言,图形渲染管线(Graphics Pipeline)以及着色器(Shader),标准化设备坐标(NDC)等诸多概念。
图形渲染管线和坐标系统的变换当然很重要,但是我们现在还不需要懂,只要暂且弄懂这几件事就好了。

至少要知道这个吧:我们在干啥?

通过阅读这一节,你应该已经大概明白了,我们的任务就是把顶点数据(Vertex Data)输入进去,经过了好多道工序,这些数据最终被“画”在屏幕上。在本节,除了顶点着色器和片段着色器是我们自己定义的,其他“工序”都是自动进行的。

需要懂的三个名词:VAO,VBO,EBO

● 顶点数组对象:Vertex Array Object(VAO)
● 顶点缓冲对象:Vertex Buffer Object(VBO)
● 元素缓冲对象:Element Buffer Object(EBO)/ 索引缓冲对象:Index Buffer Object(IBO)
我们先从这三个东西的含义来讨论它们。VAO,VBO和EBO,指明了我们输入的顶点数据是“怎么样的”。一个顶点可能有很多属性,比如位置、颜色,法向量等等。现在我们假设我们要画一个矩形,矩形长这样:

因为任何在OpenGL中的物体都是三维的,所以我们再添加一维z=0。

VBO:数据具体是什么?

VBO存放了具体的数据,假如数据是float类型,那么VBO中存放的就是:

{ 0.0f, 0.0f, 0.0f, 2.0f, 0.0f, 0.0f, 2.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }
VAO:数据是什么意思?

VBO只是单纯的一堆计算机中的01数据罢了,VAO承担起了解释的责任。函数glVertexAttribPointer的含义可以看Learn OpenGL,不要期待我再复制粘贴到这里哦(虽然确实可以水字数)。

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

上面两行代码,规定了VBO的position属性。有人可能不服气了,你说position就是position啊?那我们提前瞅一眼vertex shader是怎么写的:

#version 330 core // 版本声明; 使用核心模式
layout (location = 0) in vec3 aPos; // 在顶点着色器中声明所有的输入顶点属性, 创建vec3输入变量aPos, 设定输入变量的位置值(location)

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

在这里,我们可以简单的认为,有个vec3类型的变量叫aPos,给了它一个location=0的值,而glVertexAttribPointer的第一个参数,就是去找location=0的变量,然后把对应的数据输入到这个变量里。
剩下参数的意思大致如下,(VAO清清嗓子开始说话:)这些数据都传到aPos里,数据的类型是float,一个顶点的属性包括3个float。对于输入的position属性,开头偏移量为0,也就是从第1个数据开始传,一口气传3个。

EBO:数据怎么用?

我们通过绘制两个三角形组成了一个矩形,但是现在顶点只有4个而不是6个对吧?EBO规定了按照怎样的方式绘制数据。EBO把绘制顶点的顺序放在缓冲里:

{ 0, 1, 3, 1, 2, 3 }

然后一顿操作猛如虎,在当前窗口上用6个索引对应的顶点绘制2个三角形

VAO,VBO,EBO绑定与解绑顺序

你可能对三者的绑定和解绑顺序感到有点迷糊(至少我当时有点),让我们回到这张图:

VAO不仅有指向VBO的指针, 也有指向EBO的指针。我们可以很容易想到,绑定顺序应该是这样:

一开始绑定了VAO,然后在VAO仍然被绑定的情况下,绑定VBO。VAO就指向了VBO。又在这样的情况下绑定了EBO,VAO又指向了EBO。最后,我们解除了VAO的绑定,但是VAO对VBO和EBO的俩指向并没有消失。下次再使用这个VAO,它仍然保持了对VBO和EBO的指向(这样就不用每次使用VAO都重新绑定一次相应的VBO和EBO了)。

但是,如果我们先解绑EBO和VBO,那么VAO对它们两个的指向也会消失。

比较官方的说法如下:

  • 当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。

  • 您可以在之后解除绑定VAO,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他VAO需要调用glBindVertexArray(),所以我们通常不会在没有直接必要的情况下解绑定VAO(或VBO)。

具体的绑定/解绑VAO的代码如下, VBO和EBO也类似。

glBindVertexArray(VAO_1); // 绑定VAO_1
glBindVertexArray(VAO_2); // 一行代码直接实现解绑VAO_1, 并绑定VAO_2
glBindVertexArray(0);     // 解绑VAO_2

在Render Loop里,也有绑定/解绑VAO的操作,我们一起来看下:

glBindVertexArray(0); // 首先,解绑
	while (!glfwWindowShouldClose(window))
    {
        // [...]
        // draw our first triangle
        glUseProgram(shaderProgram);
        // 在while循环开始之前, VAO解绑了。而在while循环中VAO一直是被绑着的,
        // 但是为了规范性还是在render loop每次循环都绑定了
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        //glDrawArrays(GL_TRIANGLES, 0, 6);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        // 为啥no need呢, 看我写的官方说法第二条()
        // glBindVertexArray(0); // no need to unbind it every time 
    }

下面是绑定/解绑的代码实现,可以稍微看一眼:

// 创建顺序: VAO->VBO->EBO
// VAO
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// VBO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// EBO
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 解绑顺序: VAO->VBO->EBO
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

GLSL语言

和C语言类似,真的不算难,看懂本节的vertex shader和fragment shader应该还是轻松的,但是不难不代表它不重要哦。后面几节着色器会写成文件形式,然后将着色器程序(shader program)的操作都封装到一个头文件中,会方便很多。

// vertex shader
#version 330 core // 版本声明; 使用核心模式
layout (location = 0) in vec3 aPos; // 在顶点着色器中声明所有的输入顶点属性, 创建vec3输入变量aPos, 设定输入变量的位置值(location)
void main()
{
  	gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
// fragment shader
#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

代码实现与结果分析

我的代码可能和练习中的参考答案不太一样,如果有需要可以查阅。但是前两节的代码因为我之前重装系统没保存所以有些找不到了(目移)。从Texture开始的代码应该是都在的(应该)

● (仅存的)练习1-3代码:https://www.luogu.com.cn/paste/h5rvfget

本节代码

完成了!记录下一个小插曲,写代码的时候,写成了

#inlcude <GLFW/glfw3.h>
#include <glad/glad.h>

然后开始报错0.0,查了一下发现第二行的代码必须写在最前面QAQ(不好好看教程是这样的)

左图:EBO+线框模式。啊,右图用了VAO+填充,怎么变这样了。哦懂了,就像教程前面说的那样,我的vertices没有相应的改变,改成这样就可以输出六个点,然后正常输出矩形。

在VAO还active的时候给EBO解绑,果然神魔都没有了

练习1-1: 添加更多顶点到数据中,使用glDrawArrays,尝试绘制两个彼此相连的三角形

我的小疑惑:在哪里设置线条的颜色啊。哦,我知道了,是在这里啊。大声的念出来,out的名字是啥?FragColor!!

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"
"}\0";

wwww于是又画了一个,放在右图了(什么嘛我的配色还是蛮有艺术细菌的)

练习1-2: 创建相同的两个三角形,但对它们的数据使用不同的VAO和VBO
练习1-3: 创建两个着色器程序,第二个程序使用一个不同的片段着色器,输出黄色;再次绘制这两个三角形,让其中一个输出为黄色

学到的知识:

// 同时生成两个可以这么写
glGenVertexArrays(2, VAOs);
glGenBuffers(2, VBOs);
// 同时清除两个可以这么写
glDeleteVertexArrays(2, VAOs);
glDeleteBuffers(2, VBOs);