现代3D图形编程学习-你好,三角形(译)

你好,三角形

传统的入门教程在介绍编程语言的时候,一般从“Hello,World!”的程序开始。这样的程序拥有最简单的可以直接输出“Hello, World!”的代码。这是一种熟悉编译系统以及代码执行的很好的一种方式。ios

使用opengl来写实际的文本的具备必定难度的。在第一章中,咱们采用将三角形绘制到屏幕上还取代文本的输出。c++

本章例子中的代码

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
#include <sstream>
#include <GL/glew.h>
#include <GL/glut.h>
#include <GL/gl.h>

GLuint positionBufferObject;
GLuint theProgram;
const float vertexPositions[] = {
    0.75f, 0.75f, 0.0f, 1.0f,
    0.75f, -0.75f, 0.0f, 1.0f,
    -0.75f, -0.75f, 0.0f, 1.0f,
};

void InitializeVertexBuffer()
{
    glGenBuffers(1, &positionBufferObject);

    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

GLuint CreateShader(GLenum eShaderType, const std::string &strShaderFilename)
{
    GLuint shader = glCreateShader(eShaderType);
    std::ifstream ifile(strShaderFilename.c_str());
    std::stringstream buffer;
    buffer << ifile.rdbuf();
    std::string contents(buffer.str());
    const char* pContents = contents.c_str();

    glShaderSource(shader, 1, &pContents, NULL);

    glCompileShader(shader);
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetShaderInfoLog(shader, infoLogLength, NULL, strInfoLog);
        fprintf(stderr, "Compile failure in %d shader: \n%s\n", eShaderType, strInfoLog);
        delete[] strInfoLog;
    }
    return shader;
}

GLuint CreateProgram(const std::vector<GLuint>& vecShader)
{
    GLuint program = glCreateProgram();
    for (size_t i=0; i<vecShader.size(); ++i)
    {
        glAttachShader(program, vecShader[i]);
    }

    glLinkProgram(program);

    GLint status;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
        printf("Log length: %d\n", infoLogLength);
        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetProgramInfoLog(program, infoLogLength, NULL, strInfoLog);
        fprintf(stderr, "Linker failure: %s\n", strInfoLog);
        delete[] strInfoLog;
    }

    for (size_t i=0; i<vecShader.size(); i++)
    {
        glDetachShader(program, vecShader[i]);
    }
    return program;
}


void InitializeProgram()
{
    std::vector<GLuint> shaderList;
    shaderList.push_back(CreateShader(GL_VERTEX_SHADER, "./triangle.vert"));
    shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, "./triangle.frag"));

    theProgram = CreateProgram(shaderList);
    std::for_each(shaderList.begin(), shaderList.end(), glDeleteShader);
}

void init()
{
    InitializeProgram();
    InitializeVertexBuffer();
}

void reshape(int w, int h)
{
    glViewport(0, 0, (GLsizei)w, (GLsizei)h);
}

void display()
{
    glClearColor(0.0f, 0.0f, 0.0f,0.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(theProgram);
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,0,0);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glDisableVertexAttribArray(0);
    glUseProgram(0);
    glutSwapBuffers();
}


int main(int argc, char** argv)
{
    glutInit(&argc, argv);

    int width = 500;
    int height = 500;

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH);
    glutInitWindowSize(width, height);
    glutInitWindowPosition(300,200);
    int window = glutCreateWindow(argv[0]);

    GLenum err = glewInit();
    if (err != GLEW_OK)
    {
        std::cout << "glewInit failed: " << glewGetErrorString(err) << std::endl;
    }

    init();
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    /* glutKeyboardFunc(keyboard); */
    glutMainLoop();
    return 0;
}

display函数详解

display函数从表面上来看是很是简单的。可是,它的功能是很是复杂的,而且和init中的初始化过程有关系。在阅读过程当中对于暂时不理解的地方不用纠结,略过继续看便可。编程

// Example 1.1. The display Function
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);

glUseProgram(theProgram);

glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);

glDrawArrays(GL_TRIANGLES, 0, 3);

glDisableVertexAttribArray(0);
glUseProgram(0);

glutSwapBuffers();

前两行用来清空屏幕。glClearColor是其中一个状态设置函数,它用来设置清空屏幕的时候使用的颜色。此处将清空颜色设置成了黑色。glClear并无设置opengl的状态,它用来触发屏幕清空操做。其中GL_COLOR_BUFFER_BIT参数表示此次清空操做会影响到颜色缓存区,将颜色缓存区中的颜色设置成上述设定的黑色。数组

接下来的一行代码是用来设置当前的纹理程序,这个纹理程序会被接下去的渲染命令用到。在后面的内容中咱们会详细阐述纹理程序是怎么工做的。缓存

再接下去的三行指令都使用来设置opengl上下文中的状态的。这些命令设置了将被渲染的三角形的坐标。他们告诉opengl三角形坐标在内存中的位置。详细内容后续会深刻介绍。编程语言

glDrawArrays从命名上能够发现端倪,这是一个渲染相关的函数。根据当前设定的上下文中的状态,来绘制图形,参数中GL_TRIANGLES说明当前绘制的是三角形。函数

再接下去的两行是简单的清理工做,将渲染完后原先设置的一些参数清空。oop

最后一行,glutSwapBuffers,是FreeGLUT的命令,并非opengl的命令。咱们在程序中将opengl的帧缓存设定成了双缓存。这意味着当前显示图像的缓存和当前用于渲染的缓存是两个独立的缓存。所以,咱们全部的渲染过程只有在渲染玩以后才会呈现给用户。也就是说,用户不会看到渲染通常的图像。glutSwapBuffers是用来将渲染获得的图像呈现给用户。布局

跟着数据走

基础简介的章节中,咱们介绍了opengl管线的功能。此次咱们将会依据代码再次对管线的内容进行介绍。这里将会更深次的理解到opengl渲染数据相关的细节。性能

顶点传输

光栅化管线的第一个步骤是将顶点转换到裁剪空间。在opengl执行转换以前,必须接受到顶点的列表。所以该管线更初始的步骤是将三角形的顶点数据传输给opengl。

下面是将要被传输的顶点:

const float vertexPositions[] = {
    0.75f, 0.75f, 0.0f, 1.0f,
    0.75f, -0.75f, 0.0f, 1.0f,
    -0.75f, -0.75f, 0.0f, 1.0f,
}

每一行都表示一个顶点的四维坐标。这些顶点是四维的缘由是裁剪空间中的顶点是四维的。这些顶点已是裁剪坐标系中的点了。如今咱们想要让opengl根据这些点(三角形的三个定点)来渲染三角形。

尽管咱们已经有了数据,可是opengl并不能直接使用他们。opengl对于能够读取的内存区域是有必定限制的。你能够本身给顶点分配内存空间,可是opengl并不能直接看到这些内存。所以,咱们须要作的第一步事情就是分配一个opengl可见的内存空间,而后将咱们的数据填充到里面。这个建立的对象叫作“缓存对象(buffer object)”。

缓存的对象是一个线性的存储空间,受opengl管理和分配。这个内存空间中的内容是受用户控制的,可是用户只有间接的访问能力。能够将这些缓存对象想象成GPU的内存。GPU能够很快的从这个内存中读取内容,所以将数据存储到这里面,是有性能优点的。

本章中的缓存对象是在初始话的时候建立的。以下:

void InitializeVertexBuffer()
{
    glGenBuffers(1, &positionBufferObject);

    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

第一行用来建立缓存对象,并将句柄存储到全局变量positionBufferObject中。这个对象被建立出来了,可是尚未被分配任何的内存空间。

glBindBuffer函数将新建立的缓存对象绑定到GL_ARRAY_BUFFER。正如在前面简介中介绍的,opengl中的对象一般必须被绑定到上下文,这样opengl在渲染的时候才会用到这些对象。

glBufferData函数进行了两个操做,内存分配和内容拷贝。它为当前绑定到GL_ARRAY_BUFFER对象分配内存。经过sizeof(vertexPositions)获取分配内存的大小,而后将vertexPosition对应大小的内存内容拷贝到glBufferData为对象分配的内存中。简单的来说,能够理解为咱们已经拥有了内存中存储的特定大小的数组,而后须要在GPU中分配相应大小的内存空间,最后,将内存拷贝到GPU中的内存中。

glBufferData第四个参数的含义将会在后面的内容中给出解释。

第二个绑定操做是起到清理做用。将上下文中GL_ARRAY_BUFFER绑定到0,也就是说先前绑定的对象将会被解绑。这个操做从严格意义上来说并非必须的。由于,后续的绑定操做,会对当前的对象解绑。可是,进行解绑操做仍是一个很好的习惯,除非你对渲染过程有个严格的控制。

经过上述的步骤,咱们实现了顶点数据到GPU内存的传输。可是,咱们仅仅分配了必定大小的内存,而后用二进制进行填充,opengl并不知道内存中数据的格式。所以,咱们须要告诉opengl怎么处理这些存储的数据。

在渲染的代码里咱们作了以下操做:

glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);

第一个函数咱们以前已经说明过了,就是让opengl上下文使用buffer对象。第二个函数,glEnableVertexAttribArray将在下一节进行介绍,若是没有这个函数那么第三个函数就不重要了。

第三个函数是最为关键的函数。glVertexAttribPointer这个函数里面虽而后Pointer单词,可是它并非用来处理指针的。而是用来处理缓存对象的。

当渲染发生的时候,opengl会将缓存对象中存储的数据取出来。接着,咱们须要告诉opengl的是,这些顶点的数据存储的格式是怎么样的。也就是说,咱们须要告诉opengl怎么去识别buffer中存储的数据。

在上面的例子中,咱们的数据格式是这样的:

  • 咱们的位置数据是以32位浮点型数据(c/c++中的float)
  • 顶点的每个位置都有4个数字组成
  • 每一个顶点数据之间没有空余空间。是在数组中紧密排列的。
  • 数组中的第一个值是缓存对象中的起点位置。

glVertexAttribPointer将上述四个信息都告诉了opengl。函数的第三个参数是数据中每一个值的基本类型,在这个例子中是GL_FLOAT,表示32位浮点型。第二个参数表示了有多少个基本类型的数据能够组成一个小的单元,这里是4个。第五个参数用来表示每组数值之间的间隔。在这个例子中,每组数值之间没有间隔,所以这个值是0。第六个参数表示缓存对象的起始的字节偏移量,0表示在缓存对象的起端。

第四个参数会在接下去的章节中介绍。第一个参数会在下面一节中介绍。

这里有一点看上去被忽略的事情是,数据来自于哪一个缓存对象。这一点是隐含的。glVertexAttribPointer老是绑定到以前绑定到GL_ARRAY_BUFFER缓存对象上。所以,函数中没有说明缓存对象的句柄,直接使用了先前绑定的句柄。

这个函数在接下去的章节中会给出详细介绍。

如今opengl知道从哪里获取顶点数据,因而可使用下面的函数进行渲染。

glDrawArrays(GL_TRIANGLES, 0, 3);

这个函数表面上看起来很是简单,可是它干了不少活。第二个和第三个参数分别表示从顶点数据中读取的初始位置以及个数。0,3就表示被处理的顶点为0,1,2。

第一个参数用来告诉opengl,从顶点中读取的三个数值是用来绘制三角形的。

顶点处理和着色器

如今咱们已经告诉opengl顶点数据有哪些了,渲染管线的下一个步骤是顶点的处理。这是本章中介绍的两个可编程阶段中的一个,这涉及到着色器的使用。

着色器是运行在gpu上的一个程序。在渲染管线中存在几个可能的着色器阶段,而且没一个都有本身的输入和输出。着色器的目的是对输入进行必定的处理,得到想要的输出。

每一个着色器都是在一系列的输入下执行的。这里须要强调的是任何阶段的着色器之间是相互独立的。在单独执行着色器的时候不会出现串扰。着色器对于输入和输出是有严格的定义的。着色器在执行完成以后没有对全部的输出赋值的行为在大多数状况下是不合法的。

顶点着色器正如名称所示,是对一系列顶点进行操做的。严格的来说,每个顶点着色器的调用都是针对一个顶点的。这些着色器必须输出用户定义的输出和顶点在裁剪坐标系下对应的位置。这个裁剪坐标系的计算逻辑彻底由顶点着色器控制。

opengl中的着色器由opengl着色器语言编写的(GLSL)。这个语言看上去很想C语言,可是和C仍是有很大区别的。相对于C语言而言,GLSL有不少的限制,如不能有递归的逻辑等。下面咱们给出了顶点着色器的示例:

#version 330

layout(location = 0) in vec4 position;
void main()
{
    gl_Position = position;
}

上面的代码看上去是很是简单的。第一行表示了这个着色器使用的GLSL的版本是3.30。全部的GLSL着色器都须要声明版本号。

接下去的一行定义了顶点着色器的输入。输入变量的名字为position,类型是vec4:一个四维的浮点型向量。这一句话也说明了变量的布局位置是0,这个在下面会给出更多的解释。

和C语言相似的是,着色器的执行也都是从main函数的调用开始的。这个着色器很是简单,仅仅实现了将输入position的值拷贝给gl_Position的变量。这个变量没有在着色器中定义;由于它是一个在每一个着色器中都事先定义好了的标准变量。若是在GLSL代码中发现一个变量是以gl_开始的,那么这个变量必定是内置的变量。在本身对变量生命定义的时候,是不容许使用gl_做为开头的。

gl_Position的定义相似以下:

out vec4 gl_Position;

顶点着色器至少须要作的事情是产生顶点对应的裁剪坐标系下的位置。gl_Position正是顶点在裁剪坐标系中的位置。所以,此处输入的顶点已是裁剪坐标系下的点了,这个着色器就直接将输入的值复制到输出便可。

顶点属性。着色器有输入输出。能够将这想象成函数的入参和返回值。若是一个着色器是一个函数,那么它被调用的时候会有一系列的入参,而且指望获得一系列的输出。

着色器的输入和输出都分别有本身的来源和去处。所以,入参position必须在一开始附上来自某个地方的数值。那么这些数值从什么地方来的呢?装载这些数值的地方叫作顶点属性。

你也许会发现“vertex attribute”这两个词好像在哪里出现过。不错,就在glEnableVertexAttribArrayglVertexAttribPointer两个函数中带有这两个词。

这说明了数据是怎么在opengl管线中传输的。当渲染开始的时候,缓存对象中的顶点数据的读取和设定由函数glVertexAttribPointer函数完成。这个函数描述了数据从什么地方来的。经过glVertexAttribPointer函数将数据和顶点着色器中的变量名字联系在一块儿的具体实现是比较复杂的。

顶点着色器中的每一个输入参数都有一个对应的位置,叫作index属性。正如上文中提到的,入参的定义以下:

layout(location = 0) in vec4 position;

布局的位置(lcoation)就是变量position的index属性。该属性值必须大于等于0,它的取值范围是和硬件相关的。

在代码里,当指向一个属性的时候,都是经过指向index属性实现的。函数glEnableVertexAttribArrayglDisableVertexAttribArrayglVertexAttribPointer的第一个参数都是index属性。在着色器代码里,咱们将position变量的index属性设置为0。所以在调用glEnableVertexAttribArray(0)函数的时候,就是使得position的index属性有效。

下面给除了数据如何传输到顶点着色器的示意图:

没有调用glEnableVertexAttribArray,仅仅调用glVertexAttribPointer到那个index是没有意义的。enable函数并不必定要放在glVertexAttribPointer函数调用以前,可是在渲染命令调用前,enable函数必需要被调用。若是这个属性没被enable,那么渲染的时候将不会用到这个属性。

光栅化

到目前已经完成了将3个顶点传递给opengl,并经过顶点着色器将其转换成了裁剪坐标系中的3个位置。接下来,顶点的位置经过将xyz三个份量,分别除以w份量获得标准化设备坐标系中点。在咱们的例子中,3个顶点的w份量是1,所以获得的位置已是标准化设备坐标系中的位置了。

在这以后,顶点位置还须要转换成窗口坐标系中。这个转换过程被成为视图变换。在opengl中对应的函数为glviewport。本章中,窗口大小的变化都会触发该函数的执行。所以本章中reshape函数的实现为:

void reshape(int w, int h)
{
    glViewport(0, 0, (GLsizei)w, (GLsizei)h);
}

这个告诉opengl窗口的哪一个区域是能够用于渲染的。在这个例子中,咱们将整个窗口的可视区域做为渲染区域。若是不调用这个函数,窗口大小的变化就不会对渲染产生影响。同时须要注意的是,上述的代码并无让渲染保持原来的比例。窗口的收缩和延展会一样影响到三角形的收缩和延展。

glviewport函数调用时,对应的原点位于窗口的左下方。一旦位于窗口坐标系中,opengl能够将三角形的三个顶点经过扫描转换的过程,将三角形转换成一系列的片断。为了完成这样的操做,opengl必须肯定这些点表述的是什么。

opengl能够将一系列的点以不一样的方式表述。让opengl将点理解成三角形的命令是

glDrawArrays(GL_TRIANGLES, 0, 3);

枚举GL_TRIANGLES告诉opengl一系列输入点中的每三个点用来构建一个三角形。因为咱们仅仅传入了3个点,咱们只获得了一个三角形。

Figure 1.2. Data Flow to Rasterizer

若是咱们传入的是6个点,那么咱们就可以获得2个三角形。

片断处理

片断着色器是用来计算片断的输出颜色。片断着色器的输入包括片断在窗口空间中的xyz坐标位置。输入中也能够包含一些用户定义的数据。

这里咱们使用的片断着色器相似以下:

Example 1.5. Fragment Shader

#version 330

out vec4 outputColor;
void main()
{
    outputColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
}

第一行与顶点着色器相同,都是用来表示着色器使用的GLSL语言的版本。第二行代码表示的是片断着色器的输出。输出参数的类型是vec4。

main函数中说明了这个片断着色器的输出是一个份量都为1的四维向量。这样建立获得的是一个白色的三角形。第四个维度表示的是透明度的意思。因为全部的片断着色器输出的点都是对应到窗口坐标系中的,所以这里不须要在对其进行赋值等操做。

Note

在顶点着色器一节中,咱们使用了layout (location=#)的表达式的目的是用来提供顶点着色器输入和顶点index属性值之间的关联。这个为了将顶点数组和顶点着色器中的输入关联起来。所以,你接下来会对片断着色器的输出和屏幕中的点之间的关联产生必定的疑惑。
opengl可以本身进行处理。由于,对于片断着色器输出的位置是惟一肯定的,如当前图像要被渲染到屏幕。正由于这样,若是你定义一个片断着色器的输出,那么这个输出值会自动的输出到目标位置的图像中。对于不一样的片断着色器的结果输出到不一样的图像上是可能的,这样会增长一些复杂度,他的实现也会和输入的处理方式相似。但这里不会对此进行更详细的说明。

生成着色器

这里咱们将会说明,着色器的相关代码如何传递给opengl,并让它识别的。

着色器语言是用类c语言编写的。所以,opengl对着色器语言的处理方式也相似与对c语言的处理方式。在c语言中,单独的c文件被编译成目标文件,而后,一个或多个目标文件经过连接的方式,生成单个项目,或是动态,静态库。opengl的处理方式很是相似。

一个着色器语言首先被编译成着色器目标对象,而后一个或多个着色器目标对象经过连接的方式生成项目对象。

opengl项目对象包含了用于渲染操做的全部着色器。在本章的示例中,咱们有顶点着色器和片断着色器;他们二者经过连接的方式共同组成了项目对象。具体实现的方式相似以下:

void InitializeProgram()
{
    std::vector<GLuint> shaderList;

    shaderList.push_back(CreateShader(GL_VERTEX_SHADER, strVertexShader));
    shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, strFragmengShader));

    theProgram = CreateProgram(shaderList);

    std::for_each(shaderList.begin(), shaderList.end(), glDeleteShader);
}

第一个语句建立的对象用于存储将要被连接的着色器目标。接下去的两条语句将两条着色器代码进行了编译。最后经过CreateProgram将两个着色器目标进行连接,生成项目对象。CreateShader代码以下:

GLuint CreateShader(GLenum eShaderType, const std::string& strShader)
{
    GLuint shader = glCreateShader(eShaderType);
    const char *pStrShader = strShader.c_str();
    glShaderSource(shader, 1, &pStrShader, NULL);

    glCompileShader(shader);

    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);

        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetShaderInfoLog(shader, infoLogLength, NULL, strInfoLog);
        
        const char *strShaderType = NULL;
        switch(eShaderType)
        {
        case GL_VERTEX_SHADER: strShaderType = "vertex"; break;
        case GL_GEOMETRY_SHADER: strShaderType = "geometry"; break;
        case GL_FRAGMENT_SHADER: strShaderType = "fragment"; break;

        }
        fprintf(stderr, "Compile failure in %s shader:\n%s\n", strShaderType, strInfoLog);
        delete[] strInfoLog;
    }
    return shader;
}

首先告诉opengl,须要建立什么类型的对象,由glCreateShader完成。这个函数能够建立特定类型的纹理对象,如顶点或是片断纹理,所以该函数的入参是纹理对象的类型。因为不一样版本的着色器语言会有不一样的格式规则,以及对应的预先定义的变量和常量,所以在着色器语言的字符串中首先必需要有说明对应的版本。这样编译器才会知道采用怎样的方式对其进行编译。

Note

着色器和程序对象都是opengl中的对象。可是,他们的建立方式等和其余的opengl对象有显著的区别。好比,对于缓存对象,它的建立是经过相似与“glGenBuffer”的形式建立的。除此以外还有其余不少的不一样之处。

上述代码中的下一个阶段是经过glShaderSource将等待编译的着色器语句导入到建立的着色器对象中。该函数的第一个参数是着色器对象,第二个参数是有多少个字符串会被载入到着色器对象。将多个字符串载入到一个纹理对象中,相似于c语言中,将头文件合并到c文件中。第三个参数是将要被载入的字符串数组,最后一个参数是字符串长度的数组,这里传入NULL,表示咱们传入的字符串都是以null结尾的。一般状况下,最后一个参数传入NULL就能够了,除非你须要在字符串中使用null字符,才须要传入字符串的长度。

一旦这些字符串在对象中时,他们就能够经过glCompileShader对着色器进行编译。

当编译之后,须要查看编译是否成功。这里,咱们使用了glGetShaderiv来获取GL_COMPILE_STATUS。若是返回的是GL_FALSE,那么编译就是失败了;不然编译就是成功了。

当编译失败的时候,咱们会将错误打印出来。它将消息输出到stderr,解释编译失败的缘由。这个错误就类比于c编译器报的错误。

当建立的着色器对象都编译成功后,咱们将其传入到CreateProgram函数中。

GLuint CreateProgram(const std::vector<GLuint> &shaderList)
{
    GLuint program = glCreateProgram();

    for (size_t iLoop=0; iLoop<shaderList.size(); ++iLoop)
        glAttachShader(program, shaderList[iLoop]);

    glLinkProgram(program);

    GLint status;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
        
        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetProgramInfoLog(program, infoLogLength, NULL, strInfoLog);
        fprintf(stderr, "Linker failure: %s\n", strInfoLog);
        delete[] strInfoLog;
    }

    for (size_t iLoop=0; iLoop<shaderList.size(); ++iLoop)
        glDetachShader(program, shaderList[iLoop]);

    return program;
}

这个函数是至关简单的。首先用glCreateProgram建立空的项目对象。这个函数没有入参。项目对象是全部着色器集合而成的。其次,将全部的建立的着色器经过glAttachShader对象挂载到项目对象中。在挂载的时候,并不须要指出各个着色器对象的类型。

当全部的着色器对象都挂载到项目对象后,经过glLinkProgram将代码进行连接。与先前的处理方式相似。咱们必须经过调用glGetProgramiv获取对象连接是否成功。若是连接失败,则输出错误日志,不然返回建立了的项目对象。

Note

在上述着色器对象中,顶点着色器中的输入position的属性位置是着色器自动赋值的。其它的属性须要经过layout(location=#)的方式赋值。若是,你忘了对属性的index进行赋值,opengl会自动进行赋值。所以,你可能并不知道一个属性的index。若是你须要查询属性的位置,你能够经过glGetAttribLocation获取。

当整个项目成功连接以后,你能够经过glDeatchShader将挂载的着色器对象从项目对喜好那个中删除。项目对象的状态和功能并不会收到影响。这样作的做用是告诉opengl这些着色器对象对于项目对象而言没有做用了。

在项目对象连接以后,还须要将其余的着色器对象完全删除,使用的函数是glDeleteShader

项目对象的使用。在opengl开始渲染的时候,须要告诉opengl执行哪一个项目对象,这是须要调用glUseProgram函数。在本章的例子中,咱们在display函数中调用了两次该函数。第一次传入的参数是theProgram,第二次入参是0,告诉opengl没有项目对象用于渲染了。

清理工做

本章的例子中建立了不少opengl资源。它分配了缓存对象(占用显存资源)。建立了两个着色器对象,和一个项目对象,这些对象都是存储在opengl的内存中。可是,咱们到目前为止仅仅删除了着色器对象。

在这个例子中,并不须要进行额外的清理工做。由于当opengl工做的窗口被关闭的时候,这些资源会被opengl释放。

可是,一般意义上而言,在关掉opengl以前,将这些对象进行释放,是一个比较好的习惯。若是,你将对象放在c++对象中,那么在析够函数中能够将这些资源进行释放。

回顾

  • 缓存对象是opengl分配的现行内存空间,用来存储顶点数值
  • glsl语言编写的着色器被编译成着色器对象,而后经过连接产生项目对象,项目对象将在渲染过程当中执行。
  • glDrawArrays函数用来绘制三角形,使用当前绑定到buffer对象中的点进行渲染。

opengl函数

glClearColor,glClear:设置清空屏幕的颜色,当glClear调用GL_COLOR_BUFFER_BIT的时候,使用设定的颜色进行清屏。

glGenBuffers,glBindBuffer,glBufferData:这些函数用来建立和操做缓存对象。glGenBuffers建立一个或多个缓存对象,glBindBuffer将建立的对象绑定到上下文特定的位置,glBufferData用来分配内存,并将特定的数据存入到分配的内存中。

glEnableVertexAttribArray,glDisableVertexAttribArray,glVertexAttribPointer:这些函数用来控制顶点属性数组。glEnableVertexAttribArray用来激活属性index,glDisableVertexAttribArray用来disable激活的属性index,glVertexAttribPointer用来定义顶点数据格式,和具体位置,告诉opengl如何解析缓存对想中的定点数据。

glDrawArrays:用来执行渲染,使用当前激活的顶点属性,以及当前的项目对象。这个函数致使一系列的定点按照必定的顺序进行绘制。

glViewPort:这个函数定义了当前的视口变换。定义了窗口的显示区域。

glCreateShader, glShaderSource, glCompileShader, glDeleteShader:这些函数是用来建立纹理项目对象。glCreateShader用来建立特定类型的空的纹理对象。glShaderSource将特定的纹理语言字符串设定到特定纹理对象;屡次调用该函数会将原先设定的纹理内容覆盖。glCompileShader用来对刚设定的纹理对象进行编译。glDeleteShader将原先的纹理对象删除。

glCreateProgram, glAttachShader, glLinkProgram, glDetachShader:这些函数用来处理项目对象。glCreateProgram建立一个空的项目对象。glAttachShader用来将纹理对象绑定到项目对象上。屡次调用该函数能够将多个纹理对象绑定到项目对象。glLinkProgram将全部绑定的纹理对象进行连接操做。glDetachShader用来将项目对象中绑定的纹理对象删除。

glUseProgram:这个函数输入的项目对象设为当前使用的项目。全部发生的渲染操做都是对当前设定的项目所绑定的纹理对象进行操做。若是这个函数的入参设为0,就表示当前没有项目被绑定。

glGetAttribLocation:这个函数使用来获取特定名字的属性的index。

相关文章
相关标签/搜索