第一章 OpenGL概述

标签(空格分隔): OpenGL


1.1 什么是OpenGL

OpenGLwikipedia是一种应用程序编程接口(API),它是一种能够对图形硬件设备特性进行訪问的软件库。

一个用来渲染图像的OpenGL程序须要运行的主要操作例如以下:
1. 从OpenGL的几何图元中设置数据,用于构建形状。


2. 使用不同的着色器(shader)对输入的图元数据运行计算操作。推断它们的位置、颜色。以及其它渲染属性。
3. 将输入图元的数学描写叙述转化为与屏幕位置相应的像素片元(fragment)。这一步也称为光栅化(rasterization)。
4. 最后,针对光栅化过程产生的每一个片元,运行片元着色器(fragment shader),从而决定这个片元的终于颜色和位置。
5. 假设有必要,还须要对每一个片元运行一些额外的操作,比如推断片元相应的对象是否可见,或者将片元的颜色与当前屏幕位置的颜色进行融合。

何为光栅化(rasterization)?
将输入图元的数学描写叙述转换为与屏幕位置相应的像素片元。称为光栅化。

1.2 初识OpenGL程序

一些图形学名词:
1. 几何图元,包括点、线、三角形以及Patch。


2. 渲染(render),表示计算机从模型创建终于图像的过程。
3. 着色器(shader),专为图形处理单元(GPU)编译的一种小型程序。
4. 四种不同的着色阶段(shander stage),当中最经常使用的包括顶点着色器(vertex shader)以及片元着色器。前者用于处理顶点数据,后者用于处理光栅化后的片元数据。

全部OpenGL程序都须要用到这两类着色器。
5. 帧缓存(framebuffer)。像素(pixel),是显示器上最小的可见单元。计算机系统将全部的像素保存到帧缓存当中,后者是有图形硬件设备管理的一块独立内存区域,能够直接映射到终于的显示设备上。

例1.1 第一个OpenGL程序triangles.cpp
代码(坚果云)
在VS 2013上实现。当中包括了红书官网所提供的源文件里的头文件,以及当中的库文件。运行结果例如以下:
[OpenGL红宝书]第一章 OpenGL概述_数据

1.3 OpenGL语法

本书所使用的GLUT(OpenGL Utility Toolkit)版本号为Freeglut。是原始GLUT库的一个新变种。

1.4 OpenGL渲染管线

rendering pipeline,它是一系列数据处理过程,而且将应用程序的数据转换到终于渲染的图像。

下图为OpenGL4.3版本号的管线。包括:
顶点数据,顶点着色器,细分着色器(细分控制着色器。细分计算着色器),几何着色器。图元设置,剪切。光栅化。片元着色器
[OpenGL红宝书]第一章 OpenGL概述_数组_02

1.4.1 准备向OpenGL传输数据

OpenGL须要将全部的数据都保存到缓存对象(buffer object)中。


我们能够使用多种方式创建这种数据缓存,最经常使用的是glBufferData()wiki

Buffer Objects are OpenGL Objects that store an array of unformatted memory allocated by the OpenGL context (aka: the GPU). These can be used to store vertex data, pixel data retrieved from images or the framebuffer, and a variety of other things.
–from wiki

1.4.2 将传输数据到OpenGL

当将缓存初始化完毕后,通过调用OpenGL的一个绘制命令来请求渲染几何图元。

glDrawArrays()wiki就是一个经常使用的绘制命令。

OpenGL的绘制通常就是将顶点传输数据到OpenGL服务端。

1.4.3 顶点着色

对于绘制命令传输的每一个顶点,OpenGL都会调用一个顶点着色器来处理顶点相关的数据。


仅仅是将数据复制并传递到下一个着色阶段,叫做传递着色器(pass-through shader)。


通常来说。一个复杂的应用程序可能包括很多顶点着色器,但在同一时刻仅仅能有一个顶点着色器起作用。

1.4.4 细分着色

顶点着色器处理每一个顶点的关联数据之后。假设同一时候激活了细分着色器,那么它将进一步处理这些数据。

(第9章介绍)
细分着色器阶段会用到两个着色器来分别管理Patch数据并产生终于的形状。

1.4.5 几何着色

第10章介绍。

1.4.6 图元装配

图元装配将顶点及相关的集合图元之间组织起来。准备下一步剪切和光栅化操作。

1.4.7 剪切

顶点可能落在视口(viewport)之外,此时与顶点相关的图元会做出修改,以保证相关的像素不会在视口外绘制。剪切(clipping)由OpenGL自己主动完毕。

1.4.8 光栅化

将更新后的图元(primitive)传递到光栅化单元。生成相应的片元(fragment)。我们将一个片元是为一个“候选的像素”。

也就是能够放置在帧缓存(framebuffer)中的像素,可是它也可能被终于剔除,不再更新相应的像素位置。之后两个阶段将会运行片元的处理。

1.4.9 片元着色

最后一个能够通过编程控制屏幕上显示颜色的阶段。在Fragment Shader阶段中,我们使用着色器计算片元的终于颜色和它的深度值。

顶点着色器与片元着色器之间的差别:
顶点着色(包括细分和几何着色)决定了一个图元应该位于屏幕的什么位置,而片元着色使用这些信息来决定某个片元的颜色应该是什么。

1.4.10 逐片元的操作

在这个阶段会使用深度測试(depth test,或者通常也称为z-bufffering)和模板測试(stencil test)的方式来决定一个片元是否是可见的。

1.5 第一个程序:深入分析

1.5.1 进入main()函数

int main (int argc, char ** argv)
{
    glutInit (&argc, argv);
    glutInitDisplayMode (GLUT_RGBA);
    glutInitWindowSize (512, 512);
    glutInitContextVersion (4, 3);                  
    glutInitContextProfile (GLUT_CORE_PROFILE); 
    glutCreateWindow (argv[0]);
    glewExperimental = GL_TRUE;     

    if (glewInit ()) {
        cerr << "Unable to initialize GLEW ... exiting..." << endl;
        exit (EXIT_FAILURE);
    }

    init ();

    glutDisplayFunc (display);

    glutMainLoop ();

前面的6行使用GLUT(OpenGL Utility Toolkit)初始化和打开了一个渲染用的窗体:
1. glutInit()负责初始化GLUT库,负责设置其它GLUT例程所必须的数据结构。


2. glutInitDisplayMode()设置了程序所使用的窗体的类型。在这个样例中仅仅设置了窗体使用的RGBA颜色空间。
3. glutInitWindowSize()设置所需的窗体大小。
4. glutInitContextVersion()、glutInitContextProfile设置所需的OpenGL环境(context)的类型。这个样例中使用OpenGL 4.3版本号的核心模式(core profile)来创建环境。这个模式确保使用的仅仅是OpenGL的最新特性。否则也能够使用兼容模式,这样自OpenGL 1.0以来的全部特性都能够在程序中使用。
5. glutCreateWindow(),假设当前的系统环境能够满足glutInitDisplayMode()的显示模式要求,这里就会创建一个窗体(此时会调用计算机窗体系统的接口)。仅仅有GLUT创建了一个窗体之后(当中包括创建创建OpenGL环境的过程),我们才干够使用OpenGL相关的函数。


接下来会调用glewInit()函数,属于还有一个辅助库GLEW(OpenGL Extention Wrangler)。

GLEW能够简化获取函数地址的过程,而且包括了能够跨平台使用的其它一些OpenGL编程方法。

到这,完毕了使用OpenGL之前的全部设置工作。

之后init()函数初始化OpenGL相关的全部数据。

在之后完毕渲染工作。

  1. glutDisplayFunc(),它设置了一个显示回调(diplay callback),即GLUT在每次更新窗体内容的时候回自己主动调用该例程。
  2. glutMainLoop(),这是一个无限运行的循环,它会负责一直处理窗体和操作系统的用户输入等操作。(注意:不会运行在glutMainLoop()之后的全部命令。)

1.5.2 OpenGL的初始化过程

void init (void)
{
    glGenVertexArrays (NumVAOs, VAOs);
    glBindVertexArray (VAOs[Triangles]);
    GLfloat vertices[NumVertices][2] = {
        { -0.90, -0.90 },       // Triangle 1
        { 0.85, -0.90 },
        { -0.90, 0.85 },
        { 0.90, -0.85 },        // Triangle 2,
        { 0.90,  0.90 },
        { -0.85, 0.90 }
    };

    glGenBuffers (NumBuffers, Buffers);
    glBindBuffer (GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);
    glBufferData (GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
    ShaderInfo shaders[] = {                            
        { GL_VERTEX_SHADER, "triangles.vert" },
        { GL_FRAGMENT_SHADER, "triangles.frag" },
        { GL_NONE, NULL }
    };

    GLuint program = LoadShaders (shaders); 
    glUseProgram (program);
    glVertexAttribPointer (vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET (0));
    glEnableVertexAttribArray (vPosition);
}

初始化顶点数组对象

  • glGenVertexArrays(NumVAOs, VAOs)分配顶点数组对象(vertext array object)。OpenGL会因此分配一部分(NumVAOs个)顶点数组对象的名称供我们使用,保存到数组VAOs中。

void glGenVertexArrays(GLsizei n​, GLuint *arrays​);
n
Specifies the number of vertex array object names to generate.
arrays
Specifies an array in which the generated vertex array object names are stored.
The names returned in arrays​ are marked as used, for the purposes of glGenVertexArrays only, but they acquire state and type only when they are first bound.

非常多OpenGL命令都是glGen*的形式,它们负责分配不同类型的OpenGL对象的名称。这里的名称相似C语言中的指针变量。必须分配内存而且用名称引用它之后,名称才有意义。

在OpenGL中。这个分配的机制叫做绑定对象(bind an object)。这通过一系列glBind*形式的OpenGL函数集合去实现。

  • glBindVertexArray (VAOs[Triangles])创建一个顶点数组对象,并与其名称(VAOs[Triangles])关联起来。

    void glBindVertexArray(GLuint array​);
    array
    Specifies the name of the vertex array to bind.

  • 当我们第一次绑定对象时(比如,第一次用指定的对象名作为參数调用glBind*()),OpenGL内部会分配这个对象所需的内存而且将它作为当前对象。即后继的操作都会作用于这个被绑定的对象。比如,这里的顶点数组对象就会被后面运行的代码所改变。
    有两种情况我们须要绑定一个对象:

    1. 创建对象并初始化它所相应的数据时。
    2. 每次我们准备使用这个对象。而它并非当前绑定的对象时。

  • (未使用)glDeleteVertexArrays()。当我们完毕对顶点数组对象的操作之后。能够调用此函数将它(们)释放。

    void glDeleteVertexArrays(GLsizei n​, const GLuint *arrays​);
    n
    Specifies the number of vertex array objects to be deleted.
    arrays
    Specifies the address of an array containing the n​ names of the objects to be deleted.

  • (未使用)glIsVertexArray(),检查某个名称是否已经关联到一个顶点数组对象了。

    GLboolean glIsVertexArray(GLuint array​);
    array
    Specifies a value that may be the name of a vertex array object.

分配顶点缓存对象

顶点数组对象(VAO)负责保存一系列顶点的数据。

这些数据保存到缓存对象(Buffer Object)中,而且由当前绑定的顶点数组对象管理。

官方解释:A Vertex Array Object (VAO) is an OpenGL Object that stores all of the state needed to supply vertex data (with one minor exception noted below). It stores the format of the vertex data as well as the Buffer Objects providing the vertex data arrays.

  • glGenBuffers (NumBuffers, Buffers) 返回NumBuffers个当前未使用的缓存对象名称到数组Buffers中。(名称不一定是连续的整型数据)

    void glGenBuffers(GLsizei n​, GLuint * buffers​);
    n
    Specifies the number of buffer object names to be generated.
    buffers
    Specifies an array in which the generated buffer object names are stored.
    No buffer objects are associated with the returned buffer object names until they are first bound by calling glBindBuffer​.

  • glBindBuffer (GL_ARRAY_BUFFER, Buffers[ArrayBuffer])在分配缓存的名称之后。就能够调用glBindBuffer()来绑定它们了。由于OpenGL中有非常多种不同类型的缓存对象,因此绑定一个缓存时,须要指定它所相应的类型。此例中由于是将顶点数据保存到缓存中(故为顶点缓存对象VBO),因此使用GL_ARRAY_BUFFER类型。

    void glBindBuffer(GLenum target​, GLuint buffer​);
    target
    Specifies the target buffer object. The symbolic constant must be GL_ARRAY_BUFFER, GL_ATOMIC_COUNTER_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_DRAW_INDIRECT_BUFFER, GL_DISPATCH_INDIRECT_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_QUERY_BUFFER, GL_SHADER_STORAGE_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER.
    buffer
    Specifies the name of a buffer object.
    When a buffer object is bound to a target, the previous binding for that target is automatically broken.

  • (未使用)glDeleteBuffers(),全部的缓存对象都能够使用glDeleteBuffers()直接释放。

    void glDeleteBuffers(GLsizei n​, const GLuint * buffers​);
    n
    Specifies the number of buffer objects to be deleted.
    buffers
    Specifies an array of buffer objects to be deleted.

  • glIsBuffer(),使用此函数来推断一个整数值是否为缓存对象的名称。

    GLboolean glIsBuffer(GLuint buffer);
    buffer
    Specifies a value that may be the name of a buffer object.
    A name returned by glGenBuffers​, but not yet associated with a buffer object by calling glBindBuffer​, is not the name of a buffer object.

将数据加载缓存对象

  • glBufferData (GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW),初始化顶点缓存对象之后(VBO)。我们须要把顶点传输数据到缓存对象中。它主要有两个任务:分配顶点数据所需的存储空间(内存中)。然后将数据从应用程序的数组中复制到OpenGL服务端的内存中。
    void glBufferData(GLenum target​, GLsizeiptr size​, const GLvoid * data​, GLenum usage​);
    target
    Specifies the target buffer object. The symbolic constant must be GL_ARRAY_BUFFER, GL_ATOMIC_COUNTER_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_DRAW_INDIRECT_BUFFER, GL_DISPATCH_INDIRECT_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_QUERY_BUFFER, GL_SHADER_STORAGE_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER.
    size
    Specifies the size in bytes of the buffer object’s new data store.
    data
    Specifies a pointer to data that will be copied into the data store for initialization, or NULL if no data is to be copied.
    usage
    Specifies the expected usage pattern of the data store. The symbolic constant must be GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, or GL_DYNAMIC_COPY.

初始化顶点与片元着色器

  • 对于每一个OpenGL程序,当它所使用的OpenGL版本号高于或等于3.1时,就须要指定至少两个着色器:顶点着色器和片元着色器。这个样例中,我们使用LoadShaders()来实现这个要求。
    对于OpenGL程序猿而言,着色器就是使用OpenGL着色语言(GLSL,OpenGL Shading Language)编写的一个小型函数。

例1.2 triangles.cpp相应的顶点着色器:triangles.vert

#version 430 core
layout (location = 0) in vec4 vPosition;
void
main ()
{
        gl_Position = vPosition;
}

其实这就是我们所说的传递着色器(pass-through shader)。它仅仅负责将输入数据复制到输出数据中。
第一行

 #version 430 core

指定了OpenGL着色语言的版本号。

每一个着色器的第一行都应该设置#version,否则就会假设使用“110”版本号。
下一步,分配了一个着色器变量。着色器变量是着色器与外部世界的联系所在。着色器并不知道自己的数据从哪里来。它仅仅是在每次运行时直接获取数据相应的输入变量。而我们必须自己完毕着色管线的装配。然后才干够将应用程序中的数据与不同的OpenGL着色阶段相互关联。

layout (location = 0) in vec4 vPosition;
  • vPosition 是变量的名称。

    这个变量保存的是顶点的位置信息。(字符“v”作为这个顶点属性名称的前缀)

  • vec4,是vPosition的类型。在这里它是一个GLSL的四维浮点数向量。、
  • in。它指定了数据进入着色器的流向。
  • layout(location = 0)。叫做布局限定符(layout qualifier)。目的是为变量提供元数据(meta data)。这里设置vPosition的位置属性location为0。这个设置与init()函数的最后两行会共同起作用。

最后。在着色器的main()函数中实现它的主体部分。对于这个着色器而言。它所实现的就是将输入的顶点位置(存在vPosition中)复制到顶点着色器的指定输出位置gl_Position

例1.3 triangles.cpp相应的片元着色器:triangles.frag

@version 430 core
out vec4 fColor
void
main()
{
    fColor = vec4(0.0, 0.0, 1.0, 1.0);
}
  • 变量名为fColor,使用了out限定符,在这里着色器将会把fColor相应的数值输出,而这也就是片元所相应的颜色值。

    (因此这里前缀“f”)

  • OpenGL使用了RGBA颜色空间,A即aplha值(透明度)。1.0为不透明。

init()中的最后两个函数指定了顶点着色器的变量与我们存储在缓存对象中的数据的关系。这也就是我们所说的着色管线的装配过程,即将程序与着色器之间,以及不同着色阶段之间的数据通道连接起来。

  • glVertexAttribPointer (vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET (0));
    为了输入顶点着色器所需的数据。也就是OpenGL将要处理的全部顶点数据,须要在着色器中声明一个in变量,然后使用glVertexAttribPointer()将它关联到一个顶点属性数组。

    glVertexAttribPointer()是一个非常灵活的命令,仅仅要内存中的数据是规范组织的(保存在一个连续的数组中,不使用其它基于节点的容器,如链表)。我们就能够使用glVertexArrtibPointer()告诉OpenGL直接从内存中获取数据。

    void glVertexAttribPointer(GLuint index​, GLint size​, GLenum type​, GLboolean normalized​, GLsizei stride​, const GLvoid * pointer​);
    void glVertexAttribIPointer(GLuint index​, GLint size​, GLenum type​, GLsizei stride​, const GLvoid * pointer​);
    void glVertexAttribLPointer(GLuint index​, GLint size​, GLenum type​, GLsizei stride​, const GLvoid * pointer​);
    index
    Specifies the index of the generic vertex attribute to be modified.
    size
    Specifies the number of components per generic vertex attribute. Must be 1, 2, 3, 4. Additionally, the symbolic constant GL_BGRA is accepted by glVertexAttribPointer. The initial value is 4.
    type
    Specifies the data type of each component in the array. The different functions take different values.
    glVertexAttribPointer and glVertexAttribIPointer both take: GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, and GL_UNSIGNED_INT
    glVertexAttribPointer also can take: GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE, GL_FIXED, GL_INT_2_10_10_10_REV, GL_UNSIGNED_INT_2_10_10_10_REV, and GL_UNSIGNED_INT_10F_11F_11F_REV.
    glVertexAttribLPointer takes only GL_DOUBLE.
    The initial value is GL_FLOAT.
    normalized
    For glVertexAttribPointer, specifies whether fixed-point data values should be normalized (GL_TRUE) or converted directly as fixed-point values (GL_FALSE) when they are accessed.
    stride
    Specifies the byte offset between consecutive generic vertex attributes. If stride​ is 0, the generic vertex attributes are understood to be tightly packed in the array. The initial value is 0.
    pointer
    Specifies a offset of the first component of the first generic vertex attribute in the array in the data store of the buffer currently bound to the GL_ARRAY_BUFFER target. The initial value is 0.

  • glEnableVertexAttribArray (vPosition);
    我们通过glEnableVertexAttribArray()来启用顶点属性数组。

    glVertexAttribPointer()初始化的属性数组指针索引传入这个函数。

    void glEnableVertexAttribArray(GLuint index​);
    void glDisableVertexAttribArray(GLuint index​);
    index
    Specifies the index of the generic vertex attribute to be enabled or disabled.

1.5.3 第一次使用OpenGL进行渲染

void display (void)
{
    glClear (GL_COLOR_BUFFER_BIT);
    glBindVertexArray (VAOs[Triangles]);
    glDrawArrays (GL_TRIANGLES, 0, NumVertices);
    glFlush ();
}


  • glClear(GL_COLOR_BUFFER_BIT),清除帧缓存的数据。

void glClear(GLbitfield mask​);
mask
Bitwise OR of masks that indicate the buffers to be cleared. The three masks are GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, and GL_STENCIL_BUFFER_BIT.

(未使用)glClearColor(GLfloat red​, GLfloat green​, GLfloat blue​, GLfloat alpha​),设置当前使用的清除颜色值,用于RGBA模式下对颜色缓存的清除工作。

清除颜色本身也是OpenGL状态机制的一个样例,它的数值会一直保留在当前OpenGL环境当中。
OpenGL有一个庞大的状态量列表。

当创建一个新的OpenGL环境时。全部的状态量都会被初始化为默认值。由于OpenGL会保留全部更改的状态值,所以我们能够降低设置状态数值的次数。
故而。在设置清除颜色为白色时,能够在display()函数中调用glClearColor(1, 1, 1, 1)。也能够在init()函数中调用glClearColor(1, 1, 1, 1),后者效率更高,由于能够避免冗余的状态切换。

glBindVertexArray (VAOs[Triangles]),选择作为顶点数据使用的顶点数组。我们能够使用这个函数来切换程序中保存的多个顶点数据对象集合。

glDrawArrays (GL_TRIANGLES, 0, NumVertices),使用当前绑定的顶点数组元素来建立一系列的几何图元。

实现顶点数据向OpenGL管线的传输。

glDrawArrays: render primitives from array data
void glDrawArrays(GLenum mode​, GLint first​, GLsizei count​);
mode
Specifies what kind of primitives to render. Symbolic constants GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_LINE_STRIP_ADJACENCY, GL_LINES_ADJACENCY, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY and GL_PATCHES are accepted.
first
Specifies the starting index in the enabled arrays.
count
Specifies the number of indices to be rendered.

glFlush (),强制全部进行中的OpenGL命令马上完毕并传输到OpenGL服务端处理。

glFlush: force execution of GL commands in finite time
void glFlush(void​);

(未使用)glFinish(),会等待全部当前的OpenGL操作完毕。

而glFlush()则仅仅是强制全部运行中的命令送入OpenGL服务端而已。不会等待全部的命令完毕。因此当我们须要了解OpenGL是在是么时候完毕操作的时候使用glFinish()。

glFinish: block until all GL execution is complete
void glFinish(void​);
glFinish does not return until the effects of all previously called GL commands are complete. Such effects include all changes to GL state, all changes to connection state, and all changes to the frame buffer contents.

(未使用)glEnable(),glDisable(),启用或禁用OpenGL的操作模式。

glEnable: enable or disable server-side GL capabilities
void glEnable(GLenum cap​);
void glDisable(GLenum cap​);
cap
Specifies a symbolic constant indicating a GL capability.

(未使用)glIsEnabled(),返回是否启用指定模式。


glIsEnabled, glIsEnabledi: test whether a capability is enabled
GLboolean glIsEnabled(GLenum cap​);
GLboolean glIsEnabledi(GLenum cap​, GLuint index​);
cap
Specifies a symbolic constant indicating a GL capability.
index
Specifies the index of the capability.


相关链接:
- OpenGL WIKI
- 关于VAO && VBO官方
- 关于VAO && VBO博客
- GLUT API
- FreeGLUT API
- GLEW 官网