顶点缓冲区和着色器
哔哩哔哩 2023-08-17 23:33:24

顶点缓冲区

使用OpenGL画一个三角形,我们需要创建一个vertex buffer(顶点缓冲区)和一个shader(着色器),vertex buffer本质上就是一个内存缓冲区,简单来说,就是一块用来存字节的内存,它是OpenGL中的内存缓冲区,存在于显卡上,在我们的VRAM中(VideoRAM,显存的一种形式,允许CPU、GPU同时访问);着色器基本上就是一个在GPU上运行的程序,意思是它就是我们写的一堆代码,被传递给显卡,然后像别的程序一样进行编译,链接,然后运行,与其他程序的不同之处只在于它运行在GPU上,而不是像C++程序一样运行在CPU上。


(资料图)

画一个三角形的基本思想是这样的,首先我们用C++写的东西是在CPU上运行的,我们定义一些数据来表示三角形,把这些数据放到显卡的VRAM中,然后CPU发出DrawCall指令,这是一个绘制命令,它告诉显卡去读取显存中的这些数据,并且把它画在屏幕上,如何读取、解释这些数据以及如何把它画到屏幕上也是我们需要告诉显卡的,这就是着色器的作用。

我们要做的就是把之前这些顶点数据放进一个缓冲区,传到OpenGL的VRAM,然后发出一个DrawCall指令,所以我们先定义一个缓冲区:

创建了缓冲区后,我们要使用这个缓冲区,就要先绑定它:

下一步就是指定数据,先创建一个数组来储存顶点数据,这里可以简单地定义数组([]中空着),也可以像这里一样更具体地定义,glBufferData()函数的第三个参数需要的指针就指向positons数组

这样我们就准备好了一个缓冲区,现在只要发出一个DrawCall指令,由两种方式:一个是glDrawArrays()函数,这是一个没有索引缓冲区时可以用的方法,现在我们还没有索引缓冲区,所以就用这个方法。另一个是glDrawElements()函数,这个是有索引缓冲区时使用的函数,

现在你可能注意到我们告诉了OpenGL要画一个三角形,从0开始,渲染3个点,但并没有指明要画的是哪个缓冲区里的数据,这是因为OpenGL是一个状态机,当我们创建了缓冲区buffer后,用glBindBuffer(GL_ARRAY_BUFFER, buffer)函数绑定了这个缓冲区,之后调用DrawCall指令,画的就是绑定的这个缓冲区,如果在绑定之前调用DrawCall指令或者绑定的是其他缓冲区,就会出现问题。

着色器

目前简单的来说,OpenGL为我们的显卡提供了我们想要画的三角形的数据,现在我们要用到一个着色器,它会在GPU上执行,去读取这些顶点数据,然后在屏幕上显示。着色器读取的数据是一大堆浮点数,这些浮点数用来表示每个顶点的位置,还包括坐标、纹理、法线等,但我们需要告诉OpenGL它们是如何布局的以及这些内存到底是什么东西,我们把这些布局方式定义在CPU里。

我们在这里需要了解vertex attributes(顶点属性),顶点不只是位置,它是一个在几何上的点,它可以包含位置、纹理、坐标、法线、颜色等属性。

调用glVertexAttribPointer()前,需要先调用glEnableVertexAttribArray(),就像绑定缓存一样,这样一来,这两行代码就能告诉OpenGL我们的缓存数据的布局,此时不带任何着色器去运行,也能看到一个白色的三角形了,因为如果我们没有提供自己的着色器的话,GPU驱动会向我们提供默认的着色器。

我们想要给GPU编程,利用GPU的性能在屏幕上画出图形,但这并不意味着我们需要在GPU上做所有的事,有些东西在CPU上是更快的,某些情况可能只是发送结果数据给GPU,但过程是在CPU上运行的,但不可否认,有大量和图形有关的事情,GPU运行起来是更快的,这就是着色器派上用场的地方。现在如果我们着手写一个自己的着色器,我们要考虑到,即使只是画一个简单的三角形,我们仍然希望能告诉GPU如何去画这个三角形,比如顶点位置在哪,这个三角形的颜色应该是什么,应该怎么画等等,特别是当我们在一个更加复杂的3D场景时,光照是一个很典型的需要被考虑的例子,我们需要告诉GPU,我们发给它的这些数据要做什么,这就是着色器的本质。

对于大多数图形编程来说,要注意两种类型的着色器,Vertex Shader(顶点着色器)和fragment Shader(片段着色器),片段着色器在有些地方也叫作Pixel Shader(像素着色器),这两种着色器是目前为止最流行的两种,可能90%的时间都会用到,其它还有很多不同类型的着色器,我们暂时不去考虑那些。

现在我们讨论一下顶点着色器和片段着色器,我们粗略地说明图像渲染管线是如何工作的,我们在CPU上写了一大堆数据,把数据发给GPU,再绑定一些状态,就可以发出一个调用,最后就进入了着色阶段,确切地说,GPU此时开始处理调用并在屏幕上绘制一些东西,先是顶点着色器,再到片段着色器,然后就能够在屏幕上看到图形,这里顶点着色器做的就是获取每一个我们想要渲染的顶点,在这个例子里,我们有3个顶点,顶点着色器会调用3次,每个顶点各1次,目的就是告诉OpenGL我们想要那些顶点显示在显示器的哪里,或者说在窗口的哪里,总之顶点着色器指定了所有我们写在缓冲里面的顶点的位置,同时顶点着色器会带着每个顶点的属性,所有的这些属性我们可以在顶点着色器里访问到,glVertexAttribPointer()函数的第一个参数index与我们定义在顶点着色器里面的index是相符合的,我们可以通过这个格式访问顶点属性,在当前的例子,我们的顶点只有位置属性;下一个阶段就是片段着色器,片段着色器会为每个像素运行一次光栅化,我们的窗口实际上是由像素组成的,比如1920✖1080的显示屏,就可以简单的理解为屏幕上有1920✖1080个像素,画一个三角形实际上就是去充满一部分像素,这就是光栅化做的事情,片段着色器的基本目的就是决定像素的颜色应该是什么;现在我们发现,顶点着色器调用3次,片段着色器可能调用成百上千次,这取决于这个三角形在我们的屏幕上占用了多大空间,当涉及到性能优化时,片段着色器里面的东西往往花销更大,因为片段着色器是要给每个像素运行一次的,一个很好的例子,就是在光照下进行计算时,每个像素都会有一个颜色值,这个值由光照、环境、纹理、材质等所有东西共同决定的,而每个像素都需要由片段着色器去一个个计算清楚,这就是片段着色器需做的事情。有了顶点着色器和片段着色器,我们可以做出90%的图形程序,目前我们在游戏中看到的所有东西,都可能用这两个着色器完成了80%到90%,很多游戏引擎,会根据游戏中发生的事情,根据我们的设置,动态的生成着色器,这在游戏引擎中是十分常见的。最后需要提一下的是,着色器在OpenGL中的工作也是基于机器的状态,比如,我们实现了某个着色器,当我们想要使用这个着色器时,我们也会发送数据到这个着色器,就像我们在一个顶点缓冲调用前从CPU给GPU发送顶点数据一样。