美文网首页
OpenGL从入门到放弃 #03 Create a Triang

OpenGL从入门到放弃 #03 Create a Triang

作者: MisakiMel | 来源:发表于2019-07-23 19:31 被阅读0次

      在这节中,我们打算使用在上节创建的窗口里渲染一个三角形出来,虽然说只是一个三角形,但里面已经涉及到了很多图形编程的知识和OpenGL函数的调用,所以可能要花很多笔墨去讲述这个三角形究竟是怎么出来。
      在OpenGL中,大部分的工作都是把3D空间的坐标(coordinates)转为适应屏幕的2D像素,而这个过程的是由OpenGL图形渲染管线(Graphics Pipeline)实现的。图形渲染管线可以被划分为两个部分:第一个部分就是将3D坐标转为2D坐标;第二部分就是将2D坐标转为实际有颜色的像素。

      2D坐标与像素是有所区别的。2D坐标准确表示一个点的在2D空间的位置,而像素只是2D坐标的近似值(approximation),因为屏幕分辨率的关系,所以像素并不会准确反映2D坐标值。

      图形渲染管线能够分为几个阶段(several step),且每个阶段的输出都是下个阶段的输入。所有的这些阶段都是高内聚低耦合的(highly specialized),即每个阶段都有特定的功能,而且这些阶段都能够并行执行(be executed in parallel)。因为图形渲染管线的并行执行特性,许多的显卡都会包含成千上万个小处理单元(small processing cores),在图形渲染管线每个阶段GPU都会利用小处理单元运行很多的小项目来处理用户的数据,而这些小项目就叫着色器(shader)。对,说了这么多其实就是为了引出着色器这个概念。
      OpenGL允许开发者去配置(configurable)部分shaders以得到属于他们的shaders,这些shaders能代替已存在的默认的shaders。此举能让我们更细致(fine-grained)地去控制图形渲染管线的特定部分。因为shaders是运行在GPU上的,所以节省了很多CPU的时间。而对于shaders本身,是使用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的。
      以下是图形渲染管线所有阶段的一个抽象表示(an abstract representation),但其实这个图只是列出了几个比较重要的阶段,是并不完整的。
      要注意是的,蓝色的阶段代表这个阶段我们可以写入自己的shaders。


      可以看到图形渲染管线包含了很多个阶段,且每个阶段处理的使用都不一样,但最终结果都是为了把进来的顶点数据(Vertex Date)变成被完全渲染的像素。接下来将简述每个阶段作用。
      我们会通过使用3个能形成一个三角形的3D坐标构成一个数组去作为图形渲染管线的输入,这个输入的数组成为顶点数据。一个顶点是每一个3D坐标的数据的基本集合(basically a collection)。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据。
      首先CPU会往GPU丢顶点数据和材质球的配置,材质球包含shaders的代码和材质球的设置。这些都会在图形渲染管线的阶段里所用到。
      图形渲染管线的第一个部分就是顶点着色器(Vertex Shader),它仅接收一个简单的顶点。它的主要目的就是把3D坐标转为不同的3D坐标(这里不理解,),且它允许我们去对顶点坐标的属性(attributes)做一些基本的处理。
      第二个阶段是图元装配(primitive assembly)阶段,这个阶段接收顶点着色器输出的所有顶点(如果在程式码中选择了GL_POINTS那就是一个顶点),这个阶段会把这些点组成一个原始的形状(primitive shape),这里叫图元。在图示中,它形成了一个三角形作为图元。通俗地讲就是通过传递进来的信息,图元装配阶段去决定这些点谁跟谁会连成线,谁跟谁会连成面亦或是谁会单独成一个点。
      第三个阶段是几何着色器(geometry shader),这个几何着色器有能力通过生成新的顶点(emitting new vertices)去构造新的图元(form new primitive)。在图示中它生成了另外一个三角形。通俗地讲就是这个着色器能偷偷补一些点进去,当然这要代码去实现,使得原本比较粗糙的图元变得精细一点。
      第四个阶段是光栅化(rasterization stage),它会把几何着色器输出的图元在最终的屏幕上映射(map)为相应的像素(corresponding pixels),而且会生成供片段着色器使用的片段。在片段着色器运行之前,会先执行(perform)片段的剪辑(clip),剪辑会丢弃所有超出你视角范围的片段,以此提升性能(performance)。
      第五个阶段就是片段着色器(fragment shader),它的主要目的就是计算每一个像素的最终颜色,而这经常是所有OpenGL高级(advanced)效果产生的地方。片段着色器经常包含3D场景的数据(例如灯光、阴影等等),这主要用来计算最终像素的颜色。
      在全部像素对应的颜色值都被确定时,在经历最后一个阶段,称为alpha测试和混合(blending)测试阶段。这个阶段检查每个片段相应的深度值(depth value)且使用这个深度值来判断这个片段是在其他物体的前面还是后面(in front or behind other objects),以此决定是否丢弃这个片段。这个阶段也会检查alpha值(alpha值就是一个物体的不透明度(opacity))并对物体进行混合。所以即使一个像素的颜色在片段着色器被计算好了,但最终它的颜色也有可能会在渲染中发生很大的变化(entirely different)。

      从上述就可以了解到,GPU会接收来CPU的顶点数据,然后在接收到绘制指令之后,会把数据喂给shaders后开始一连续的处理最终呈现出漂亮的像素。那么我们需要考虑的只是shaders的定义吗?不,远不止这些。
      第一个要考虑的就是:GPU接收顶点数据,那肯定需要一个缓冲区来存放这些数据,上面也有提到,图形渲染管线具有并行处理的特性,那么肯定能同时处理很多数据,那自然是需要一个很大缓冲区(显存)。而且仅仅是存放是不够的,因为一个模型会对应一大堆顶点数据,而CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据,所以亟需一个管理者来管理这一次性进来的这么多数据,这个管理者就叫顶点缓冲对象(Vertex Buffer Objects, VBO)。通常一个VBO对应一个模型的顶点数据,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
      概括地讲,VBO就是用来管理显存,可大量接收顶点数据且做好被顶点着色器访问的准备。
      所以在考虑shaers之前,我们就要做好被shaders接收的准备,所以要先输入顶点数据创建VBO对象

    输入顶点数据

      首先OpenGL对输入的坐标是有要求的,并不是说你给它什么它都要,但是真实情况是进来的顶点数据一般都不是OpenGL想要的。那么OpenGL想要怎样的坐标?OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。这种坐标叫做标准化设备坐标(Normalized Device Coordinates (NDC))

    Normalized Device Coordinates (NDC)

    Once your vertex coordinates have been processed in the vertex shader, they should be in normalized device coordinates which is a small space where the x, y and z values vary from -1.0 to 1.0. Any coordinates that fall outside this range will be discarded/clipped and won't be visible on your screen. Below you can see the triangle we specified within normalized device coordinates (ignoring the z axis):
    Unlike usual screen coordinates the positive y-axis points in the up-direction and the (0,0) coordinates are at the center of the graph, instead of top-left. Eventually you want all the (transformed) coordinates to end up in this coordinate space, otherwise they won't be visible.

    Your NDC coordinates will then be transformed to screen-space coordinates via the viewport transform using the data you provided with glViewport. The resulting screen-space coordinates are then transformed to fragments as inputs to your fragment shader.

      一般顶点数据OpenGL是不想要的,那就要通过顶点着色器(vertex shader)处理,转换为标准化设备坐标。这个标准化设备坐标轴跟我们书上学的xy坐标轴的方向是一致的,y轴向上,x轴向右,原点在中央,但与屏幕的坐标轴不同(Unlike usual screen coordinates)。由于标准化设备坐标的xyz轴的大小范围都是-1到,所以如果有超出坐标系的顶点,这些顶点会被丢弃掉(discarded/clipped)且不能在你的屏幕上所被看到。你的标准化设备坐标会通过glViewport()提供的数据转变为屏幕空间坐标(screen-space coordinates)。这个屏幕坐标会转变为片段传递给片段着色器。
      概括地讲,顶点数据从VBO进入到顶点着色器后变为标准化设备坐标,紧接着转换为屏幕空间坐标,最后转换为片段输出给片段着色器。其实从VBO进入到顶点着色器是需要自己配置的,不过这是后话了。
      由于我们是初学的关系,并不能实现顶点着色器的实际功能,所以打算在输入顶点数据时,直接输入标准化设备坐标,然后在顶点着色器里不再处理数据,直接原封不动的输出。
      我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组:

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

    创建VBO对象

      一个VBO对象一般都要经历创建→绑定→绑定顶点数据。
      首先就是创建:

        //在上节代码调用glViewport()之后
        unsigned int VBO;
        glGenBuffers(1, &VBO);
    

    glGenBuffers(GLsizei n, GLunit *buffers):这个函数会产生VBO对象,且能根据你给第一个参数的数字创建相应个数的VBO对象,因为OpenGL要求每个对象要有独一无二的ID,所以需要传给它一个地址来记录对象独一无二的ID。由于我们这里只需要1个VBO对象,所以不用传数组,只传一个uint类型变量就好。
      那么如果我想操作某个VBO,那么就要绑定它,说明接下来我的操作都是只对应这个被绑定的VBO:

        glBindBuffer(GL_ARRAY_BUFFER, VBO);
    

    glBindBuffer(GLenum target, GLunit buffer):OpenGL有很多缓冲对象类型,所以我们需要指明我们要绑定的是顶点缓冲对象,其枚举值是GL_ARRAY_BUFFER。第二个参数就是你要绑定的VBO的ID。
      例如我们之后需要做的是绑定顶点数据这个操作,虽然没指定把顶点数据绑到哪个VBO,但是由于我们已经在上面做了绑定的操作,所以OpenGL是知道绑定到哪个VBO的:

        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    

    glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage):前面有提到OpenGL有很多缓冲对象类型GL_ARRAY_BUFFER,所以这里指定是顶点缓冲对象,它就知道接下来的操作是对那个被绑定的VBO做处理。第二个参数是指定传输数据的大小,这里用一个sizeof()就能算出来。第三个参数就是要发送的数据的地址。第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
    GL_STATIC_DRAW :数据不会或几乎不会改变。
    GL_DYNAMIC_DRAW:数据会被改变很多。
    GL_STREAM_DRAW :数据每次绘制时都会改变。
      因为我们要渲染的三角形的数据不会改变,所以使用GL_STATIC_DRAW形式就好。
      至此,我们已经把顶点数据灌进显卡的内存里面去了,是时候考虑shaders部分了。

      在图形渲染管线的众多阶段中,我们至少需要定义一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器),所以本节会着手创建这两个着色器,但本节只会配置非常简单的着色器,详细的讨论我打算留在下一节。
      另外,在开始创建我们的着色器之前,我想先讨论一下一个着色器究竟是如何创建的。在前面有提到着色器是要用着色器语言GLSL(OpenGL Shading Language)编写的,它虽然与C语言类似,但是要想OpenGL认出你写的代码是为了创建一个着色器,那就要交给OpenGL它本人来编译(是不能给你IDE的去编译的,你的IDE并不会认为你写的是一个着色器)。在OpenGL中,是有专门的函数去编译你写的代码的(运行时编译),那么如何把你的代码灌进一个函数让它去编译呢,OpenGL的做法是接收一个里面保存了代码的字符串指针。所以在下面你能看到这一做法,我在这里已作出解释。

    顶点着色器(Vertex Shader)

      我觉得首先就要开门见山,把源码摆上来,然后在逐一解释它们的含义最好:

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

    #version 330 core:每个着色器都起始于一个版本声明,在3.3以后OpenGL的版本与GLSL的版本是匹配的,因为我们在初始化中用的OpenGL版本就是3.3,所以这里就是330且明确表示用的是核心(core)模式。
    in vec3 aPos;in关键字是用来声明输入的顶点属性(Input Vertex Attribute),由于我们这里只有一种顶点属性就是position,所以只有一条声明式。另外我们需要输入变量来保存我们的顶点属性,这里是position,是一个3D坐标,所以就创建一个vec3类型的输入变量aPos(这里的vec3跟Unity的vector3一样都是向量数据类型)。概括地讲,就是要声明所有要用到的顶点属性,并用相应类型的变量接收。
    layout (location = 0):这里我引用一篇文章中对layout关键字的解释:

      另外,这里使用了layout关键字(通常是layout(layoutAttrib1=XXX, layoutAttrib2=XXX, ...)这样的形式)。这个关键字用于一个具体变量前,用于显式标明该变量的一些布局属性,这里就是显式设定了该attribute变量的位置值(location),

      我的理解是,一旦我们的顶点着色器要去访问VBO,那么该从哪里开始去获得它所需要的指定属性的数据(这里是position),layout (location = 0)就是告知顶点着色器:你去数据最开始的地方(0)获取数据并存在aPos里面。
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);:这一行是设置顶点着色器的输出,gl_Position可以是个输入变量也可以是个输出变量,在main函数的最后,我们将gl_Position设置的值就会成为该顶点着色器的输出,输出给下一着色器。而且能看出来它是个vec4类型的变量,这4个分量中每个分量值都代表空间中的一个坐标,它们可以通过vec.xvec.yvec.zvec.w来获取。前3个参数我们直接把接收到的位置数据(aPos)原封不动的喂给它们,至于第4个参数我现在还不理解,可能要日后才能作出解释。
      跟前面说的一样,我们对顶点着色器的要求就仅限于原封不动的输出数据。

    编译着色器

      由于前面有提到着色器怎么创建的缘故,所以这里不再作过多原理上的解释,只讨论函数的使用。而且我把上述的源码喂给了一个const char*类型的指针变量,命名为:vertexShaderSource。
      与创建VBO对象类似,我们也要用相应的函数创建一个shader对象,并用一个unit类型变量去记录这个对象的ID,由于我们创建的顶点着色器,传递的参数就是GL_VERTEX_SHADER

        unsigned int vertexShader;
        vertexShader = glCreateShader(GL_VERTEX_SHADER);
    

      接下里自然就是把我们写的源码附加到这个shader对象上,然后再去编译它:

        glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
        glCompileShader(vertexShader);
    

    glCompileShader(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length):第一个参数表明要把源码附加到哪个shader对象上;第二个参数表明源码的个数,由于我们只有一个字符串源码,而不是字符串数组,所以数量为1;第三个参数表明字符串的位置;第四个参数用不上,设置为NULL。
    glCompileShader(GLuint shader):编译指定shader对象。
      至此一个顶点着色器就被创建好了,接下来可以考虑片段着色器了。

    片段着色器(Fragment Shader)

      由于有了先前创建顶点着色器的经验,现在创建片段着色器只需解释GLSL部分相关知识,剩余的部分因为与创建顶点着色器是一致的,所以只给出代码,不再赘述。
      先来看看片段着色器的源码:

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

    out vec4 FragColor:片段着色器只需要一个输出变量,表示最终输出的颜色。颜色要用4分量向量即vec4表示,因为:

      计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。

      为了简单起见,这个片段着色器将不会做计算像素的颜色输出,而是一直输出橘色,对应的RGB值如源码所示。我把这段源码写进了一个命名为fragmentShaderSource的字符串变量里。但我这里有一个疑惑,片段着色器不用输入的吗?我带着这个疑问去网上搜寻了资料,还好在这里找到我要的答案:

      gl_Position是vertex shader内建的输出变量,传递给fragment shader,必须设置。这里将Position直接传递给fragment shader(片元着色器)。

      是gl_Position直接传递,片段着色器自动接收,不用额外再给它定义接收输入的操作。
      编译部分与顶点着色器类似,但唯一不同的就是创建的着色器类型不一样,这里是GL_FRAGMENT_SHADER

        unsigned int fragmentShader;
        fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
        glCompileShader(fragmentShader);
    

      至此两个必须着色器已经准备完毕,但别着急啊,现在还不能用,OpenGL规定,要把所有自己定义的着色器合并并且链接(Link)形成一个着色器程序对象(Shader Program Object),这个对象还要被激活才算是真正投入使用。

    着色器程序对象(Shader Program Object)

      把各个着色器合并链接成一个着色器程序对象要有以下步骤:
    1.创建着色器程序对象
    2.把写好的着色器按顺序附加(attach)到着色器程序对象上
    3.然后把它们串(链接)起来
    4.激活着色器程序对象
    5.删除着色器

    1.创建

      与创建其他OpenGL对象一样,只不过用到函数不一样,这里用的是glCreateProgram()

        unsigned int shaderProgram;
        shaderProgram = glCreateProgram();
    
    2.附加和链接
        glAttachShader(shaderProgram, vertexShader);
        glAttachShader(shaderProgram, fragmentShader);
        glLinkProgram(shaderProgram);
    

    glAttachShader(GLuint program, Gluint shader):把指定shader附加到指定program上。
    glLinkProgram(GLunit program):把指定着色器程序里面的着色器给链接起来。

    3.激活
        glUseProgram(shaderProgram);
    

    glUseProgram(GLunit program):激活指定program。激活以后,每个着色器调用和渲染调用都会使用这个程序对象。

    4.删除

      在把着色器附加到着色器程序对象之后,我们的对象已经能实现着色器的相关功能了,那么这些单独存在的着色器就已经失去了作用了,我们不再需要它们了,记得删除:

        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
    

      现在我们已经把顶点数据发送到了GPU(VBO),而且还告知GPU怎么处理这么顶点,但是实际上还有一步需要处理。在把顶点送到顶点着色器之前,还要告知OpenGL如何解析这么顶点。因为顶点数据是一堆充满各种顶点属性的数组,属性包含了有顶点坐标、法向量、UV值等等,我们在做的就是告知OpenGL在顶点数据里,哪些数据是对应顶点着色器的某个顶点属性。

    链接顶点属性

      例如我们在顶点输入时给出的顶点数据应该被解析为下面的样子:


      为了解释清楚,我们可以先来回忆一下,在之前讨论顶点着色器时,是不是定义了一个顶点属性?那一行源码是:layout (location = 0) in vec3 aPos;。aPos就是一个顶点属性的变量(我称这个顶点属性叫坐标),它一次接受3个值。我们要做的就是告知OpenGL如何在顶点数据中了连续筛选出3个值并存储成一个个坐标(在迭代中)。
      为此我们应该告知进来的顶点数据要像上图一样解析:告知从数据的开始位置直接读取数据;告知每3个值应为一个顶点属性的大小,且应该每次连续读取3个值(没有空隙);在完成一次坐标的读取后,告知下一次读取位置在哪,由于我们是只放了坐标,没有其他顶点属性,所以下一次的位置就是上一次读取完之后。
      现在来看看如何把文字转为代码实现,我们要用glVertexAttribPointer函数去告知OpenGL该如何解析顶点数据,为了让顶点着色器能够在准确的位置读取准确地读取到信息,这个函数需要的参数可以说是很多了。要花较多的笔墨去解释它们:
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    

      它的Signature我就不列出来了,太长了,直接对应上图解释参数的含义吧:

    • 0 第一个参数指定我们配置的顶点属性是哪一个,它与定义顶点属性时的location对应,因为我们定义的顶点属性坐标的location = 0,所以这里也是0,把数据传递到aPos这个顶点属性中。
    • 3 第二个参数指定顶点属性的大小,由于我们定义顶点属性时是个vec3,它由3个值组成,所以是3.
    • GL_FLOAT 第三个参数指定顶点属性的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
    • GL_FALSE 第四个参数是决定数据是否被标准化(Normalize),标准化坐标在上面我们也有提到。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE,因为我们输入的顶点数据就是标准化坐标。
    • 3 * sizeof(float) 第五个参数叫做步长(Stride),这个参数告知连续的顶点属性组之间的间隔,由于下个组位置数据在3个float之后,所以设置为3 * sizeof(float)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,
    • (void*)0 最后一个参数是void*类型。它表示位置数据在缓冲中起始位置的偏移量(Offset)。但是为什么要进行这么奇怪的强制类型转换?这个会在以后详细解释。

    Each vertex attribute takes its data from memory managed by a VBO and which VBO it takes its data from (you can have multiple VBOs) is determined by the VBO currently bound to GL_ARRAY_BUFFER when calling glVertexAttribPointer. Since the previously defined VBO is still bound before calling glVertexAttribPointer vertex attribute 0 is now associated with its vertex data.

      每一个顶点属性从VBO中获取它的数据,但是它获取的是哪一个VBO中的数据呢?这一步在glBindBuffer(GL_ARRAY_BUFFER, VBO);绑定VBO时就已经决定。
      现在从VBO到顶点着色器的桥梁已然架好,需要的只是一声令下,这座桥就会马上通车,把顶点数据源源不断地、有序地从桥的一头运送到另一头。那么充当这个发令员的就是glEnableVertexAttribArray()函数,它的作用是以顶点属性位置值(location = 0)作为参数,启用顶点属性,因为顶点属性默认是禁用的。

        glEnableVertexAttribArray(0);
    

      现在,我们已经离终点很近了,坚持住。为了捋顺思路,我们将整个过程复述一遍:我们使用VBO数据缓冲对象将从CPU送过来的顶点数据储存在缓冲中,然后建立顶点着色器和片段着色器,把两个着色器链接成一个着色器程序对象,并告知OpenGL该如何解析在VBO里的顶点数据。


    // 0. 复制顶点数组到缓冲中供OpenGL使用
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 1. 设置顶点属性指针
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
    // 2. 当我们渲染一个物体时要使用着色器程序
        glUseProgram(shaderProgram);
    // 3. 绘制物体
        someOpenGLFunctionThatDrawsOurTriangle();
    

      至此我们就能看到一个橘色的三角形出现视窗中了。但是先别急着看结果,先看看目前代码仍存在的一点问题:每当我们绘制一个物体时都要重复这几行代码:绑定VBO,链接顶点属性,激活顶点属性,使用着色器程序,绘制物体。试想如果我要绘制很多个物体,且每个物体都有很多个顶点属性,那么上述的过程将会使代码变得非常冗杂,那么有没有方法,能把这些状态配置存储在一个对象中呢?

    顶点数组对象(Vertex Array Object, VAO)

      我们先来看看VAO究竟能干嘛,于我愚见,VAO有点类似于C#的委托机制,相似的地方在于,它能帮你自动调用你想调用的函数,但我觉得比委托更强大,因为它还帮你记住了调用函数所要用到的参数,且各个函数参数类型不一,不过要这么方便的使用VAO的前提是:在创建VAO时先调用一次对应的函数。
      一般一个VAO对应一个模型的状态配置,在VAO看来所谓的状态配置是指:

    • glEnableVertexAttribArrayglDisableVertexAttribArray的调用。
    • 通过glVertexAttribPointer设置的顶点属性配置。
    • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
      VAO2示范了多个顶点属性时是怎么记录的

      能看到一个VAO最多能存储16个顶点属性。
      创建一个VAO的操作与创建VBO类似:

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

      要使用VAO,就要绑定VAO,在绑定了VAO之后,(紧接着)我们应该在VAO面前教它如何绑定和配置对应的VBO和链接属性,在VAO过目不忘的能力下,也不会辜负你的期待,日后要是想绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。

    // ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
    // 1. 绑定VAO
    glBindVertexArray(VAO);
    // 2. 把顶点数组复制到缓冲中供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 3. 设置顶点属性指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    
    ...
    
    // ..:: 绘制代码(渲染循环中) :: ..
    // 4. 绘制物体
    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    someOpenGLFunctionThatDrawsOurTriangle();
    

      一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。

    Triangle

      千呼万唤始出来!我们的三角形快要跃然于屏幕上了。灰常之激动是不是?别急,还有一点手尾需要解决。要想绘制我们想要的物体,OpenGL给我们提供了glDrawArrays()函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。

    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    

    glDrawArrays(GLenum mode, GLint first, GLsizei count):这个函数会使用当前激活的着色器、配置好的顶点属性和VBO顶点数据来绘制图元。第一个参数指定绘制图元的类型,由于我们是打算绘制一个三角形,所以是GL_TRIANGLES;第二个参数指定了顶点数组的起始索引;第三个参数指定了我们打算绘制多少个顶点,这里是3。
      现在尝试编译代码,如果编译通过了,就能看到三角形啦!


      需要注意的是,在把源码灌进字符串的时候,记得加上换行符,不然编译会失败,导致只显示一个黑色的三角形。
    const char * vertexShaderSource =
    "#version 330 core                                  \n"
    "layout (location = 0) in vec3 aPos;                \n"      
    "void main()                                        \n"      
    "{gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);} \n";    
    
    const char * fragmentShaderSource =
    "   #version 330 core                           \n   "
    "   out vec4 FragColor;                         \n   "
    "   void main()                                 \n   "
    "   {FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);} \n   ";
    

    相关文章

      网友评论

          本文标题:OpenGL从入门到放弃 #03 Create a Triang

          本文链接:https://www.haomeiwen.com/subject/akajlctx.html