【笔记】跟着LearnOpenGL自说自话地学习OpenGL(三)

呼,又是搬砖忙碌的一天,回到家坐下来不禁思考了一下,学这玩意跟我搬砖的工作半毛钱关系都没有忙着干啥?

Em……可能是觉得好玩?

 

打开教程的"你好,三角形"章节,开头的那一段描述让我一阵头皮发麻,文字太长,图都不想截了,免得影响我笔记的篇幅,有兴趣的童鞋自己读一读吧。

大概提炼一下关键词:

图形渲染管线,大白话描述一下就是:举个栗子,大家高中都学过空间直角坐标系吧?真实的空间直角坐标系你画在纸上的这个过程。再具体点就是用数学方法描述的三维坐标(x,y,z),但是屏幕就像一张纸呀,显卡(注意这是显卡不是CPU了哦)就这样把一个三维的图形很多很多的坐标点变成屏幕上很多很多的像素点让你看上去画的跟三维的一样,另外别忘了我们看到的都是彩图,还可以给这些像素点涂上很真实的颜色,就这么个工具的样子。

着色器,就上面说的这个画图的过程,再细分一下步骤,其实这是两个人干的事情,一个人在画点,一个人在上色,画点的那个人叫顶点着色器,上色的那个人叫片段着色器。为了告诉OpenGL这个大家伙如何去把图形画准确并且画的更有色彩,OpenGL着色器语言OpenGL Shading Language, GLSL就成了我们的画笔来书写逻辑步骤。

其余的内容,什么顶点数组对象:Vertex Array Object,VAO,顶点缓冲对象:Vertex Buffer Object,VBO等等概念。后面拿代码来用大白话聊聊,现在写到这里聊这玩意,我自己还搞不懂,硬上这不耍流氓么?

 

接下来看一下,我们描述三角形常用坐标来描述,这里先搞一个数组这。

float vertices[] = {

    -0.5f, -0.5f, 0.0f,

     0.5f, -0.5f, 0.0f,

     0.0f,  0.5f, 0.0f

};

这里又有一些概念要理解了,怎么这么麻烦?

Z坐标为深度坐标,描述的是屏幕到想要模拟的虚拟空间距离,这个好理解

标准化设备坐标(Normalized Device Coordinates),我还是先贴官方描述吧

 

咋理解?

 

我觉着是这么个过程,首先我们的屏幕大小完全是不可控的,想想自己的手机屏幕和电脑屏幕,那么我在描述物体大小的时候定义的一系列坐标完全不可能适配所有屏幕。所以将坐标数据单位化成为标准化设备坐标,然后再真正的屏幕上再次按照一定比例进行坐标转换(glViewport函数,视口变换Viewport Transform),这样我们可以获得,更多尺寸的画面。是不是比较像拍照的过程?景物->底片->巨幅相片

不过实际上这也是对之前一篇笔记这个函数的理解:

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

接下来开始准备撸代码,首先又要接触第一个OpenGL对象,顶点缓冲对象(Vertex Buffer Objects, VBO),还来?

在OpenGL的教程中,接下来的代码部分LearnOpenGL给予了大量的详细解释,但是我觉得有个缺陷就是,知识讲解过于零散,并没有把整个代码给串起来,作为笔记,我还是按照自己的理解思路做一个宏观上的理解,分模块进行总结然后再深入细节理解

 

第一部分,着色器程序的使用

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

这里只是跟教材同步,写成了静态常量。实际上着色器程序完全可以做成两个独立的文件,从代码中剥离出来单独分析并在main函数中调用

 

main函数中,顶点着色器和片段着色器的使用

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;

}

 

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;

}

 

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::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;

}

glDeleteShader(vertexShader);

glDeleteShader(fragmentShader);

 

结合上面的代码流程,抛开代码的日志输出,总的流程需要理解并且记住,我的理解如图

 

 

 

这里面除了着色器程序内容是我们自己定义的内容以外,其余的部分都大量使用了OpenGL的API来遵从OpenGL的一般步骤来完成的。从这里开始我们要理解,在OpenGL初始化完了以后,他就像一个等着关键零件装完就开始跑的大机器,其中两个我们自己编写的shader执行了编译把字符串变成了码流,然后通过着色器程序塞给了OpenGL(附着并链接),那么OpenGL在拿到这一关键零件以后,它就已经拿住了,我们自己的shader就不需要了,这就是为啥最终还要销毁它们。

第二部分,顶点缓冲对象VBO和顶点缓冲数组VAO

这部分代码是这样的

unsigned int VBO, VAO;

glGenVertexArrays(1, &VAO);

glGenBuffers(1, &VBO);

glBindVertexArray(VAO);

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

拿出一张教材中的图,结合理解。

姑且简单的理解为VBO是更底层的对象,而创建VAO变成了我们和VBO交流的中间件,所以VAO和VBO是成对出现的,如果我需要两个VBO,那么就会变成这样

unsigned int VBO[2], VAO[2];

    glGenVertexArrays(2, VAO);

    glGenBuffers(2, VBO);

    glBindVertexArray(VAO[0]);

    glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);

    glBindVertexArray(VAO[1]);

    glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);

所以看起来我们任何的操作实际上都是基于OpenGL的二次开发,把特定的数据结构准备好往里面填再按照规则执行就行了,OpenGL已经帮我们做了很多基础的工作了

 

这里要明确一点的是,最终通过我们书写的代码部分直接操作的是VAO而不是VBO,别问为啥,问就是不会。如果需要理解,估计要把VAO的绑定过程打开看看是怎么回事

在大循环中的代码是这样的: 使用着色器程序,选中VAO,画图

glUseProgram(shaderProgram);

glBindVertexArray(VAO);

glDrawArrays(GL_TRIANGLES, 0, 3);  -- 这个函数应该就是基于VAO来绘制的,我试过删掉VAO结果就画不出来了

 

跑出来大概就是这样的,基于我上一次的代码进行扩充,输入还是可以接着用

最后有个自己曾经遇到的一个理解困难在这里记录一下:

那就是原文教材中多次提到的"绑定"

这里的绑定我之前一直没有理解,主要还是对于OpenGL是什么样的机制没有理解,就如同前面所说OpenGL就是一个初始化完就站在那里等待你的零件的机器,一个等候命令的机器人,所以这里的"绑定"对我的理解而言其实更像在魔兽游戏中用鼠标框了一群兵的选中(在英语原文中用的是bind, unbind则是解开; 松开,看自己怎么理解方便了),程序的执行总有一个先后过程,使用glBindVertexArray函数其实就是选中了一个VAO开始画,再选中一个VAO再画,只是计算机处理速度极快让我们看起来是同时的而已。

例如:

glUseProgram(shaderProgram);

glBindVertexArray(VAO[0]);

glDrawArrays(GL_TRIANGLES, 0, 3);

glBindVertexArray(VAO[1]);

glDrawArrays(GL_TRIANGLES, 0, 3);

这里仅是个人理解,方便对代码的理解,下一篇笔记中记录绘制两个三角形的过程试试,今天这篇就先到这吧。