OpenGL ES

OpenGL3.0教程 第五课:纹理立方体


OpenGL3.0教程

原文链接:http://www.opengl-tutorial.org/beginners-tutorials/tutorial-5-a-textured-cube/

原译文链接: http://www.opengl-tutorial.org/zh-hans/beginners-tutorials-zh/tutorial-5-a-textured-cube-zh/

本课学习如下几点:

  • 什么是UV坐标
  • 怎样自行加载纹理
  • 怎样在OpenGL中使用纹理
  • 什么是滤波?什么是mipmap?怎样使用?
  • 怎样利用GLFW更加有效地加载纹理?
  • 什么是alpha通道?

关于UV坐标

给一个模型贴纹理时,需要通过某种方式告诉OpenGL用哪一块图像来填充三角形。这是借助UV坐标来实现的。

每个顶点除了位置坐标外还有两个浮点数坐标:U和V。这两个坐标用于获取纹理,如下图所示:

注意纹理是怎样在三角形上扭曲的。

自行加载.BMP图片

了解BMP文件格式并不重要:很多库可以帮你做这个。但BMP格式极为简单,可以帮助你理解那些库的工作原理。所以,我们从头开始写一个BMP文件加载器,以便你理解其工作原理,不过(在实际工程中)千万别再用这个实验品

如下是加载函数的声明:

GLuint loadBMP_custom(const char * imagepath);

使用方式如下:

GLuint image = loadBMP_custom("./my_texture.bmp");

接下来看看如何读取BMP文件。

首先需要一些数据。读取文件时将设置这些变量。

// Data read from the header of the BMP file
unsigned char header[54]; // Each BMP file begins by a 54-bytes header
unsigned int dataPos;     // Position in the file where the actual data begins
unsigned int width, height;
unsigned int imageSize;   // = width*height*3
// Actual RGB data
unsigned char * data;

现在正式开始打开文件。

// Open the file
FILE * file = fopen(imagepath,"rb");
if (!file)                              {printf("Image could not be openedn"); return 0;}

文件一开始是54字节长的文件头,用于标识“这是不是一个BMP文件”、图像大小、像素位等等。来读取文件头吧:

if ( fread(header, 1, 54, file)!=54 ){ // If not 54 bytes read : problem
    printf("Not a correct BMP filen");
    return false;
}

文件头总是以“BM”开头。实际上,如果用十六进制编辑器打开BMP文件,你会看到如下情形:

因此,得检查一下头两个字节是否确为‘B’和‘M’:

if ( header[0]!='B' || header[1]!='M' ){
    printf("Not a correct BMP filen");
    return 0;
}

现在可以读取文件中图像大小、数据位置等信息了:

// Read ints from the byte array
dataPos    = *(int*)&(header[0x0A]);
imageSize  = *(int*)&(header[0x22]);
width      = *(int*)&(header[0x12]);
height     = *(int*)&(header[0x16]);

如果这些信息缺失得手动补齐:

// Some BMP files are misformatted, guess missing information
if (imageSize==0)    imageSize=width*height*3; // 3 : one byte for each Red, Green and Blue component
if (dataPos==0)      dataPos=54; // The BMP header is done that way

现在我们知道了图像的大小,可以为之分配一些内存,把图像读进去:

// Create a buffer
data = new unsigned char [imageSize];

// Read the actual data from the file into the buffer
fread(data,1,imageSize,file);

//Everything is in memory now, the file can be closed
fclose(file);    

到了真正的OpenGL部分了。创建纹理和创建顶点缓冲器差不多:创建一个纹理、绑定、填充、配置。

在glTexImage2D函数中,GL_RGB表示颜色由三个分量构成,GL_BGR则说明在内存中颜色值是如何存储的。实际上,BMP存储的并不是RGB,而是BGR,因此得把这个告诉OpenGL。

// Create one OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);

// "Bind" the newly created texture : all future texture functions will modify this texture
glBindTexture(GL_TEXTURE_2D, textureID);

// Give the image to OpenGL
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, data);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

稍后再解释最后两行代码。同时,得在C++代码中使用刚写好的函数加载一个纹理:

GLuint Texture = loadBMP_custom("uvtemplate.bmp");

另外十分重要的一点: 使用2次幂(power-of-two)的纹理!

  • 优质纹理: 128128, 256256, 10241024, 2*2…
  • 劣质纹理: 127128, 35, …
  • 勉强可以但很怪异的纹理: 128*256

在OpenGL中使用纹理

先来看看片断着色器。大部分代码一目了然:

#version 330 core   
// Interpolated values from the vertex shaders
in vec2 UV;

// Ouput data
out vec3 color;

// Values that stay constant for the whole mesh.
uniform sampler2D myTextureSampler;

void main(){

    // Output color = color of the texture at the specified UV
    color = texture( myTextureSampler, UV ).rgb;
}

注意三个点:

  • 片断着色器需要UV坐标。看似合情合理。
  • 同时也需要一个“Sampler2D”来获知要加载哪一个纹理(同一个着色器中可以访问多个纹理)
  • 最后一点,用texture()访问纹理,该方法返回一个(R,G,B,A)的vec4变量。马上就会了解到分量A。

顶点着色器也很简单,只需把UV坐标传给片断着色器:

#version 330 core
// Input vertex data, different for all executions of this shader.
layout(location = 0) in vec3 vertexPosition_modelspace;
layout(location = 1) in vec2 vertexUV;

// Output data ; will be interpolated for each fragment.
out vec2 UV;

// Values that stay constant for the whole mesh.
uniform mat4 MVP;

void main(){

    // Output position of the vertex, in clip space : MVP * position
    gl_Position =  MVP * vec4(vertexPosition_modelspace,1);

    // UV of the vertex. No special space for this one.
    UV = vertexUV;
}

还记得第四课中的“layout(location = 1) in vec2 vertexUV” 吗?我们得在这儿把相同的事情再做一遍,但这次的缓冲器中放的不是(R,G,B)三元组,而是(U,V)数对。

// Two UV coordinatesfor each vertex. They were created with Blender. You'll learn shortly how to do this yourself.
static const GLfloat g_uv_buffer_data[] = {
    0.000059f, 1.0f-0.000004f,
    0.000103f, 1.0f-0.336048f,
    0.335973f, 1.0f-0.335903f,
    1.000023f, 1.0f-0.000013f,
    0.667979f, 1.0f-0.335851f,
    0.999958f, 1.0f-0.336064f,
    0.667979f, 1.0f-0.335851f,
    0.336024f, 1.0f-0.671877f,
    0.667969f, 1.0f-0.671889f,
    1.000023f, 1.0f-0.000013f,
    0.668104f, 1.0f-0.000013f,
    0.667979f, 1.0f-0.335851f,
    0.000059f, 1.0f-0.000004f,
    0.335973f, 1.0f-0.335903f,
    0.336098f, 1.0f-0.000071f,
    0.667979f, 1.0f-0.335851f,
    0.335973f, 1.0f-0.335903f,
    0.336024f, 1.0f-0.671877f,
    1.000004f, 1.0f-0.671847f,
    0.999958f, 1.0f-0.336064f,
    0.667979f, 1.0f-0.335851f,
    0.668104f, 1.0f-0.000013f,
    0.335973f, 1.0f-0.335903f,
    0.667979f, 1.0f-0.335851f,
    0.335973f, 1.0f-0.335903f,
    0.668104f, 1.0f-0.000013f,
    0.336098f, 1.0f-0.000071f,
    0.000103f, 1.0f-0.336048f,
    0.000004f, 1.0f-0.671870f,
    0.336024f, 1.0f-0.671877f,
    0.000103f, 1.0f-0.336048f,
    0.336024f, 1.0f-0.671877f,
    0.335973f, 1.0f-0.335903f,
    0.667969f, 1.0f-0.671889f,
    1.000004f, 1.0f-0.671847f,
    0.667979f, 1.0f-0.335851f
};

上述UV坐标对应于下面的模型:

其余的就很清楚了。创建一个缓冲器、绑定、填充、配置,与往常一样绘制顶点缓冲器对象。要注意把glVertexAttribPointer的第二个参数(大小)3改成2。

结果如下:

放大后:

什么是滤波和mipmap?怎样使用?

正如在上面截图中看到的,纹理质量不是很好。这是因为在loadBMP_custom函数中,有两行这样写道:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

这意味着在片断着色器中,texture()将直接提取位于(U,V)坐标的纹素(texel)。

有几种方法可以改善这一状况。

线性滤波(Linear filtering)

若采用线性滤波。texture()会查看周围的纹素,然后根据UV坐标距离各纹素中心的距离来混合颜色。这就避免了前面看到的锯齿状边缘。

线性滤波可以显著改善纹理质量,应用的也很多。但若想获得更高质量的纹理,可以采用各向异性滤波,不过速度上有些慢。

各向异性滤波(Anisotropic filtering)

这种方法逼近了真正片断中的纹素区块。例如下图中稍稍旋转了的纹理,各向异性滤波将沿蓝色矩形框的主方向,作一定数量的采样(即所谓的“各向异性层级”),计算出其内的颜色。

Mipmaps

线性滤波和各向异性滤波都存在一个共同的问题。那就是如果从远处观察纹理,只对4个纹素作混合显得不够。实际上,如果3D模型位于很远的地方,屏幕上只看得见一个片断(像素),那计算平均值得出最终颜色值时,图像所有的纹素都应该考虑在内。很显然,这样做没有考虑性能问题。相反,人们引入了mipmap这一概念:

  • 一开始,把图像缩小到原来的1/2,接着一次做下去,直到图像只有1×1大小(应该是图像所有纹素的平均值)
  • 绘制模型时,根据纹素大小选择合适的mipmap。
  • 可以选用nearest、linear、anisotropic等任意一种滤波方式来对mipmap采样。
  • 要想效果更好,可以对两个mipmap采样然后混合,得出结果。

好在这个比较简单,OpenGL都帮我们做好了,只需一个简单的调用:

// When MAGnifying the image (no bigger mipmap available), use LINEAR filtering
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// When MINifying the image, use a LINEAR blend of two mipmaps, each filtered LINEARLY too
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// Generate mipmaps, by the way.
glGenerateMipmap(GL_TEXTURE_2D);

怎样利用GLFW加载纹理?

我们的loadBMP_custom函数很棒,因为这是我们自己写的!不过用专门的库更好。GLFW就可以加载纹理(仅限TGA文件):

GLuint loadTGA_glfw(const char * imagepath){

    // Create one OpenGL texture
    GLuint textureID;
    glGenTextures(1, &textureID);

    // "Bind" the newly created texture : all future texture functions will modify this texture
    glBindTexture(GL_TEXTURE_2D, textureID);

    // Read the file, call glTexImage2D with the right parameters
    glfwLoadTexture2D(imagepath, 0);

    // Nice trilinear filtering.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);

    // Return the ID of the texture we just created
    return textureID;
}

压缩纹理

学到这儿,你可能会想怎样加载JPEG文件而不是TGA文件呢?

简单的说:别这么干。还有更好的选择。

创建压缩纹理

  • 下载The Compressonator,一款ATI工具
  • 用它加载一个二次幂纹理
  • 将其压缩成DXT1、DXT3或DXT5格式(这些格式之间的差别请参考Wikipedia):

  • 生成mipmap,这样就不用在运行时生成mipmap了。
  • 导出为.DDS文件。

至此,图像已压缩为可被GPU直接使用的格式。在着色中随时调用texture()均可以实时解压。这一过程看似很慢,但由于它节省了很多内存空间,传输的数据量就少了。传输内存数据开销很大;纹理解压缩却几乎不耗时(有专门的硬件负责此事)。一般情况下,才用压缩纹理可使性能提升20%。

使用压缩纹理

来看看怎样加载压缩纹理。这和加载BMP的代码很相似,只不过文件头的结构不一样:

GLuint loadDDS(const char * imagepath){

    unsigned char header[124];

    FILE *fp;

    /* try to open the file */
    fp = fopen(imagepath, "rb");
    if (fp == NULL)
        return 0;

    /* verify the type of file */
    char filecode[4];
    fread(filecode, 1, 4, fp);
    if (strncmp(filecode, "DDS ", 4) != 0) {
        fclose(fp);
        return 0;
    }

    /* get the surface desc */
    fread(&header, 124, 1, fp); 

    unsigned int height      = *(unsigned int*)&(header[8 ]);
    unsigned int width         = *(unsigned int*)&(header[12]);
    unsigned int linearSize     = *(unsigned int*)&(header[16]);
    unsigned int mipMapCount = *(unsigned int*)&(header[24]);
    unsigned int fourCC      = *(unsigned int*)&(header[80]);

文件头之后是真正的数据:紧接着是mipmap层级。可以一次性批量地读取:

    unsigned char * buffer;
    unsigned int bufsize;
    /* how big is it going to be including all mipmaps? */
    bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;
    buffer = (unsigned char*)malloc(bufsize * sizeof(unsigned char));
    fread(buffer, 1, bufsize, fp);
    /* close the file pointer */
    fclose(fp);

这里要处理三种格式:DXT1、DXT3和DXT5。我们得把“fourCC”标识转换成OpenGL能识别的值。

    unsigned int components  = (fourCC == FOURCC_DXT1) ? 3 : 4;
    unsigned int format;
    switch(fourCC)
    {
    case FOURCC_DXT1:
        format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
        break;
    case FOURCC_DXT3:
        format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
        break;
    case FOURCC_DXT5:
        format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
        break;
    default:
        free(buffer);
        return 0;
    }

像往常一样创建纹理:

    // Create one OpenGL texture
    GLuint textureID;
    glGenTextures(1, &textureID);

    // "Bind" the newly created texture : all future texture functions will modify this texture
    glBindTexture(GL_TEXTURE_2D, textureID);

现在只需逐个填充mipmap:

    unsigned int blockSize = (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16;
    unsigned int offset = 0;

    /* load the mipmaps */
    for (unsigned int level = 0; level < mipMapCount && (width || height); ++level)
    {
        unsigned int size = ((width+3)/4)*((height+3)/4)*blockSize;
        glCompressedTexImage2D(GL_TEXTURE_2D, level, format, width, height, 
            0, size, buffer + offset);

        offset += size;
        width  /= 2;
        height /= 2;
    }
    free(buffer); 

    return textureID;

反转UV坐标

DXT压缩源自DirectX。和OpenGL相比,DirectX中的V纹理坐标是反过来的。所以使用压缩纹理时,得用(coord.v, 1.0-coord.v)来获取正确的纹素。这步操作何时做都可以:可以在导出脚本中做,可以在加载器中做,也可以在着色器中做……

总结

刚刚学习的是创建、加载以及在OpenGL中使用纹理。

总的来说,压缩纹理体积小、加载迅速、使用便捷,应该只用压缩纹理;主要的缺点是得用The Compressonator来转换图像格式。

练习

  • 源代码中实现了DDS加载器,但没有做纹理坐标的改动(译者注:指文中讲述的反转 UV坐标)。在适当的位置添加该功能,以使正方体正确显示。
  • 试试各种DDS格式。所得结果有何不同?压缩率呢?
  • 试试在The Compressonator不生成mipmap。结果如何?请给出3种方案解决这一问题。

参考文献

OpenGL3.0教程 第四课:彩色立方体


OpenGL3.0教程

原文链接:http://www.opengl-tutorial.org/beginners-tutorials/tutorial-4-a-colored-cube/

原译文链接: http://www.opengl-tutorial.org/zh-hans/beginners-tutorials-zh/tutorial-4-a-colored-cube-zh/

欢迎来到第四课!你将学到:

  • 画立方体,代替单调的三角形
  • 加上绚丽的色彩
  • 学习深度缓存(Z-Buffer)

画立方体

立方体有六个方形表面,而OpenGL只支持画三角形,因此需要画12个三角形,每面两个。我们用定义三角形顶点的方式来定义这些顶点。

// Our vertices. Tree consecutive floats give a 3D vertex; Three consecutive vertices give a triangle.
// A cube has 6 faces with 2 triangles each, so this makes 6*2=12 triangles, and 12*3 vertices
static const GLfloat g_vertex_buffer_data[] = {
-1.0f,-1.0f,-1.0f, // triangle 1 : begin
-1.0f,-1.0f, 1.0f,
-1.0f, 1.0f, 1.0f, // triangle 1 : end
1.0f, 1.0f,-1.0f, // triangle 2 : begin
-1.0f,-1.0f,-1.0f,
-1.0f, 1.0f,-1.0f, // triangle 2 : end
1.0f,-1.0f, 1.0f,
-1.0f,-1.0f,-1.0f,
1.0f,-1.0f,-1.0f,
1.0f, 1.0f,-1.0f,
1.0f,-1.0f,-1.0f,
-1.0f,-1.0f,-1.0f,
-1.0f,-1.0f,-1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, 1.0f,-1.0f,
1.0f,-1.0f, 1.0f,
-1.0f,-1.0f, 1.0f,
-1.0f,-1.0f,-1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f,-1.0f, 1.0f,
1.0f,-1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f,-1.0f,-1.0f,
1.0f, 1.0f,-1.0f,
1.0f,-1.0f,-1.0f,
1.0f, 1.0f, 1.0f,
1.0f,-1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f,-1.0f,
-1.0f, 1.0f,-1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f,-1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f,-1.0f, 1.0f
};

OpenGL的缓冲区由一些标准的函数(glGenBuffers, glBindBuffer, glBufferData, glVertexAttribPointer)来创建、绑定、填充和配置;这些可参阅第二课。绘制的函数调用也没变,只需改绘制的点的个数:

// Draw the triangle !
glDrawArrays(GL_TRIANGLES, 0, 12*3); // 12*3 indices starting at 0 -> 12 triangles -> 6 squares

这段代码,有几点要解释:

  • 现在为止,三维模型都是固定的:要改就要改源码,重新编译,然后祈望不会错。我们将在第七课中学习如何加载动态模型。
  • 实际上,每个顶点至少被写了三次(在以上代码中搜索“-1.0f,-1.0f,-1.0f”看看)。这是可怕的内存浪费。我们将在第九课中学习怎样优化。

现在,你有了画一个白色立方体的所有必备条件。让着色器运行起来,至少试试吧:)

添加颜色 Adding colors

颜色,从概念上说,像极了位置:它就是数据。OpenGL中,它们都是“属性”。事实上,之前已在glEnableVertexAttribArray()和glVertexAttribPointer()用过属性设置了。现在我们加上颜色属性,代码很相似的。

首先,声明颜色:每个顶点一个RGB(红绿蓝)三元组。这里用随机的方式生成的,所以结果可能看起来不那么好;但你可以调整得更好,例如:把顶点的位置作为颜色值。

// One color for each vertex. They were generated randomly.
static const GLfloat g_color_buffer_data[] = {
0.583f, 0.771f, 0.014f,
0.609f, 0.115f, 0.436f,
0.327f, 0.483f, 0.844f,
0.822f, 0.569f, 0.201f,
0.435f, 0.602f, 0.223f,
0.310f, 0.747f, 0.185f,
0.597f, 0.770f, 0.761f,
0.559f, 0.436f, 0.730f,
0.359f, 0.583f, 0.152f,
0.483f, 0.596f, 0.789f,
0.559f, 0.861f, 0.639f,
0.195f, 0.548f, 0.859f,
0.014f, 0.184f, 0.576f,
0.771f, 0.328f, 0.970f,
0.406f, 0.615f, 0.116f,
0.676f, 0.977f, 0.133f,
0.971f, 0.572f, 0.833f,
0.140f, 0.616f, 0.489f,
0.997f, 0.513f, 0.064f,
0.945f, 0.719f, 0.592f,
0.543f, 0.021f, 0.978f,
0.279f, 0.317f, 0.505f,
0.167f, 0.620f, 0.077f,
0.347f, 0.857f, 0.137f,
0.055f, 0.953f, 0.042f,
0.714f, 0.505f, 0.345f,
0.783f, 0.290f, 0.734f,
0.722f, 0.645f, 0.174f,
0.302f, 0.455f, 0.848f,
0.225f, 0.587f, 0.040f,
0.517f, 0.713f, 0.338f,
0.053f, 0.959f, 0.120f,
0.393f, 0.621f, 0.362f,
0.673f, 0.211f, 0.457f,
0.820f, 0.883f, 0.371f,
0.982f, 0.099f, 0.879f
};

缓冲区的创建、绑定和填充方法和之前一样:

GLuint colorbuffer;
glGenBuffers(1, &colorbuffer);
glBindBuffer(GL_ARRAY_BUFFER, colorbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_color_buffer_data), g_color_buffer_data, GL_STATIC_DRAW);

配置也一样:

// 2nd attribute buffer : colors
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, colorbuffer);
glVertexAttribPointer(
1, // attribute. No particular reason for 1, but must match the layout in the shader.
3, // size
GL_FLOAT, // type
GL_FALSE, // normalized?
0, // stride
(void*)0 // array buffer offset
);

现在,顶点着色器中,我们已能访问这个额外的缓冲区:

// Notice that the "1" here equals the "1" in glVertexAttribPointer
layout(location = 1) in vec3 vertexColor;

本例将不会在顶点着色器里做花哨的玩意,只是简单地过渡到片断着色器:

// Output data ; will be interpolated for each fragment.
out vec3 fragmentColor;

void main(){

[...]

// The color of each vertex will be interpolated
// to produce the color of each fragment
fragmentColor = vertexColor;
}

片断着色器中,要再次声明片断颜色:

// Interpolated values from the vertex shaders
in vec3 fragmentColor;

…然后把它的值赋给输出颜色:

// Output color = color specified in the vertex shader,
// interpolated between all 3 surrounding vertices
color = fragmentColor;

于是得到:

额,好丑。为了搞清楚,我们先看看各画一个看起来“远”和“近”的三角形,会发生什么:

似乎挺好。现在画“远”的三角形:

它遮住了“近”三角形!它本应该画在“近”三角形后面的!我们的立方体就有这个问题:一些理应被遮挡的面,因为绘制时间晚,实际可见。我们将用深度缓存(Z-Buffer)算法解决它。

便签1
: 如果你没发现问题,把相机放到(4,3,-3)试试

便签2
: 如果“类似于位置,颜色是一种属性”,那为什么颜色要声明 vec3 fragmentColor,而位置不需要?实际上,位置有点特殊:它是唯一必须赋初值的(否则OpenGL不知道在哪画三角形)。所以在顶点着色器里, gl_Position是内置变量。

深度缓存(Z-Buffer)The Z-Buffer

该问题的解决方案是:在缓冲区中存储每个片断的深度(即“Z”值);而每次画片断时,先确保当前片断确实比先前画的片断更近。

你可以自己实现,但让硬件自己去做更简单:

// Enable depth test
glEnable(GL_DEPTH_TEST);
// Accept fragment if it closer to the camera than the former one
glDepthFunc(GL_LESS);

这就解决之前所有问题了。

练习

  • 在不同的位置画立方体和三角形。你需要生成两个MVP矩阵,在主循环中做两次绘制调用,但只需一个着色器。

  • 自己生成颜色值。一些提示:随机生成,使每次运行颜色都不同;依据顶点的位置;将前二者结合;或其他的创新想法。若你不了解C,参考以下语法:

static GLfloat g_color_buffer_data[12*3*3];
for (int v = 0; v < 12*3 ; v++){
g_color_buffer_data[3*v+0] = your red color here;
g_color_buffer_data[3*v+1] = your green color here;
g_color_buffer_data[3*v+2] = your blue color here;
}
  • 完成上面习题后,试令颜色在每帧都改变。你需要在每一帧都调用glBufferData。请确保已先绑定(glBindBuffer)了合适的缓冲区!

OpenGL3.0教程 第三课: 矩阵


OpenGL3.0教程

原文链接:http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/

原译文链接: http://www.opengl-tutorial.org/zh-hans/beginners-tutorials-zh/tutorial-3-matrices-zh/

引擎完全没有推动飞船。飞船静止在原处,而引擎推动了环绕着飞船的宇宙。

《飞出个未来》(一部美国科幻动画片)

这一课是所有课程中最重要的。请至少看八遍。

齐次坐标(Homogeneous coordinates)

目前为止,我们仍然把三维顶点视为三元组(x, y, z)。现在引入一个新的分量w,得到向量(x, y, z, w)。

请先记住以下两点(稍后我们会给出解释):

若w==1,则向量(x, y, z, 1)为空间中的点。

若w==0,则向量(x, y, z, 0)为方向。

(事实上,要永远记着。)

这有什么不同呢?对于旋转,二者没什么不同。当你旋转点和方向时,结果是一样的。但对于平移(将点沿着某个方向移动),情况就不同了。『平移一个方向』是毫无意义的。

齐次坐标使我们能用同一个公式对点和方向作运算。

变换矩阵(Transformation matrices)

矩阵简介

简而言之,矩阵就是一个行、列数固定的,纵横排列的数表。比如,一个2×3矩阵看起来像这样:

2X3

三维图形学中我们只用到4×4矩阵,它能对顶点(x, y, z, w)作变换。这一变换是用矩阵左乘顶点来实现的:

矩阵x顶点(记住顺序!!矩阵左乘顶点,顶点用列向量表示)= 变换后的顶点

MatrixXVect

这看上去复杂,实则不然。左手指着a,右手指着x,得到ax。 左手移向右边一个数b,右手移向下一个数y,得到by。依次类推,得到cz、dw。最后求和ax + by + cz + dw,就得到了新的x!每一行都这么算下去,就得到了新的(x, y, z, w)向量。

这种重复无聊的计算就让计算机代劳吧。

用C++,GLM表示:

glm::mat4 myMatrix;
glm::vec4 myVector;
// fill myMatrix and myVector somehow
glm::vec4 transformedVector = myMatrix * myVector; // Again, in this order ! this is important.

用GLSL表示:

mat4 myMatrix;
vec4 myVector;
// fill myMatrix and myVector somehow
vec4 transformedVector = myMatrix * myVector; // Yeah, it's pretty much the same than GLM

(还没把这些复制到你的代码里跑跑吗?赶紧试试!)

平移矩阵(Translation matrices)

平移矩阵是最简单易懂的变换矩阵。平移矩阵是这样的:

translationMatrix

其中,X、Y、Z是点的位移增量。

例如,若想把向量(10, 10, 10, 1)沿X轴方向平移10个单位,可得:

translationExamplePosition1

(算算看!一定要动手算算!!)

这样就得到了齐次向量(20, 10, 10, 1)!记住,末尾的1表示这是一个点,而不是方向。经过变换计算后,点仍然是点,很合理。

下面来看看,对一个代表Z轴负方向的向量,作上述平移变换会得到什么结果:

translationExampleDirection1

即还是原来的(0, 0, -1, 0)方向,这也很合理,正好印证了前面的结论:“平移一个方向是毫无意义的”。

那怎么用代码表示平移变换呢?

用C++,GLM表示:


include  // after 

glm::mat4 myMatrix = glm::translate(10,0,0);
glm::vec4 myVector(10,10,10,0);
glm::vec4 transformedVector = myMatrix * myVector; // guess the result

用GLSL表示:呃,实际中我们几乎不用GLSL做。大多数情况下在C++代码中用glm::translate()算出矩阵,然后把它传给GLSL。在GLSL中只做一次乘法:

vec4 transformedVector = myMatrix * myVector;

单位矩阵(Identity matrix)

单位矩阵很特殊,它什么也不做。我提到它是因为,知道它和知道A*1.0=A一样重要。

identityExample

用C++表示:

glm::mat4 myIdentityMatrix = glm::mat4(1.0);

缩放矩阵(Scaling matrices)

缩放矩阵也很简单:

scalingMatrix

例如把一个向量(点或方向皆可)沿各方向放大2倍:

scalingExample

w还是没变。你也许会问:“缩放一个向量”有什么用?嗯,大多数情况下是没什么用,所以一般不会去做;但在某些罕见情况下它就有用了。(顺便说一下,单位矩阵只是缩放矩阵的一个特例,其(X, Y, Z) = (1, 1, 1)。单位矩阵同时也是旋转矩阵的一个特例,其(X, Y, Z)=(0, 0, 0))。

用C++表示:

// Use #include 

旋转矩阵(Rotation matrices)

旋转矩阵比较复杂。这里略过细节,因为日常应用中,你并不需要知道矩阵的内部构造。
想了解更多,请看矩阵和四元组常见问题(这个资源很热门,应该有中文版吧)。

用C++表示:

// Use #include 

复合变换

前面已经学习了如何旋转、平移和缩放向量。要是能将它们组合起来就更好了。只需把这些矩阵相乘即可,例如:

TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * OriginalVector;

!!!千万注意!!!这行代码最先执行缩放,接着旋转,最后才是平移。这就是矩阵乘法的工作方式。

变换的顺序不同,得出的结果也不同。体验一下:
- 向前一步(小心别磕着爱机)然后左转;
- 左转,然后向前一步

实际上,上述顺序正是你在变换游戏人物或者其他物体时所需的:先缩放;再调整方向;最后平移。例如,假设有个船的模型(为简化,略去旋转):

错误做法:
- 按(10, 0, 0)平移船体。船体中心目前距离原点10个单位。

  • 将船体放大2倍。以原点为参照,每个坐标都变成原来的2倍,就出问题了。……最后你是得到一艘放大的船,但
    其中心位于2*10=20。这可不是你想要的结果。

正确做法:
- 将船体放大2倍,得到一艘中心位于原点的大船。

  • 平移船体。船大小不变,移动距离也正确。

矩阵-矩阵乘法和矩阵-向量乘法类似,所以这里也会省略一些细节,不清楚的请移步“矩阵和四元数常见问题”。现在,就让计算机来算:

用C++,GLM表示:

glm::mat4 myModelMatrix = myTranslationMatrix * myRotationMatrix * myScaleMatrix;
glm::vec4 myTransformedVector = myModelMatrix * myOriginalVector;

用GLSL表示:

mat4 transform = mat2 * mat1;
vec4 out_vec = transform * in_vec;

模型(Model)、视图(View)和投影(Projection)矩阵

在接下来的课程中,我们假定已知绘制Blender经典三维模型:小猴Suzanne的方法。

利用模型、视图和投影矩阵,可以将变换过程清晰地分解为三个阶段。这个方法你可以不用(我们在前两课就没用),但最好要用。我们即将看到,它们把整个流程划分得很清楚,故被广为使用。

模型矩阵

这个三维模型,和我们心爱的红色三角形一样,是由一组顶点定义的。顶点的XYZ坐标是相对于物体中心定义的:也就是说,若某顶点位于(0, 0, 0),它就在物体的中心。

model

也许玩家需要用键鼠控制这个模型,所以我们希望能够移动它。这简单,只需学会:缩放旋转平移就行了。在每一帧中,用算出的这个矩阵,去乘(在GLSL中乘,不是C++中!)所有的顶点,物体就动了。唯一不动的就是世界坐标系(World Space)的中心。

world

现在,物体所有顶点都位于世界坐标系。下图中黑色箭头的意思是:从模型坐标系(Model Space)(顶点都相对于模型的中心定义)变换到世界坐标系(顶点都相对于世界坐标系中心定义)。

model_to_world

下图概括了这一过程:

M

视图矩阵

这里再引用一下《飞出个未来》:

引擎完全没有推动飞船。飞船静止在原处,而引擎推动了环绕着飞船的宇宙。

camera

仔细想想,相机的原理也是相通的。如果想换个角度观察一座山,你可以移动相机也可以……移动山。后者在生活中不可行,在计算机图形学中却十分方便。

起初,相机位于世界坐标系的原点。移动世界只需乘上一个矩阵。假如你想把相机向右(X轴正方向)移动3个单位,这和把整个世界(包括网格)向左(X轴负方向)移3个单位是等效的!脑子有点乱?来写代码:

// Use #include 

下图展示了:从世界坐标系(顶点都相对于世界坐标系中心定义)到观察坐标系(Camera Space,顶点都相对于相机定义)的变换。

model_to_world_to_camera

在脑袋撑爆前,来欣赏一下GLM伟大的glm::LookAt函数吧:

glm::mat4 CameraMatrix = glm::LookAt(
    cameraPosition, // the position of your camera, in world space
    cameraTarget,   // where you want to look at, in world space
    upVector        // probably glm::vec3(0,1,0), but (0,-1,0) would make you looking upside-down, which can be great too
);

下图解释了上述变换过程:

MV

还没完呢。

投影矩阵

现在,我们处于观察坐标系中。这意味着,经历了这么多变换后,现在一个坐标为(0,0)的顶点,应该被画在屏幕的中心。但仅有x、y坐标还不足以确定物体是否应该画在屏幕上:它到相机的距离(z)也很重要!两个x、y坐标相同的顶点,z值较大的一个将会最终显示在屏幕上。

这就是所谓的透视投影(perspective projection):

model_to_world_to_camera_to_homogeneous

好在用一个4×4矩阵就能表示这个投影¹ :

// Generates a really hard-to-read matrix, but a normal, standard 4x4 matrix nonetheless
glm::mat4 projectionMatrix = glm::perspective(
    FoV,         // The horizontal Field of View, in degrees : the amount of "zoom". Think "camera lens". Usually between 90° (extra wide) and 30° (quite zoomed in)
    4.0f / 3.0f, // Aspect Ratio. Depends on the size of your window. Notice that 4/3 == 800/600 == 1280/960, sounds familiar ?
    0.1f,        // Near clipping plane. Keep as big as possible, or you'll get precision issues.
    100.0f       // Far clipping plane. Keep as little as possible.
);

最后一个变换:

从观察坐标系(顶点都相对于相机定义)到齐次坐标系(Homogeneous Space)(顶点都在一个小立方体中定义。立方体内的物体都会在屏幕上显示)的变换。

最后一幅图示:

MVP

再添几张图,以便大家更好地理解投影变换。投影前,蓝色物体都位于观察坐标系中,红色的东西是相机的视域四棱锥(frustum):这是相机实际能看见的区域。

nondeforme

用投影矩阵去乘前面的结果,得到如下效果:

homogeneous

此图中,视域四棱锥变成了一个正方体(每条棱的范围都是-1到1,图上不太明显),所有的蓝色物体都经过了相同的形变。因此,离相机近的物体就显得大一些,远的显得小一些。和真实生活中一样!

让我们从视域四棱锥的“后面”看看它们的模样:

projected1

这就是你得出的图像了!看上去太方方正正了,因此,还需要做一次数学变换使之适合实际的窗口大小:

final1

这就是实际渲染的图像啦!

复合变换:模型视图投影矩阵(MVP)

… 再来一串亲爱的矩阵乘法:

// C++ : compute the matrix
glm::mat3 MVPmatrix = projection * view * model; // Remember : inverted !
// GLSL : apply it
transformed_vertex = MVP * in_vertex;

总结

第一步:创建模型视图投影(MVP)矩阵。任何要渲染的模型都要做这一步。

// Projection matrix : 45° Field of View, 4:3 ratio, display range : 0.1 unit  100 units
glm::mat4 Projection = glm::perspective(45.0f, 4.0f / 3.0f, 0.1f, 100.0f);
// Camera matrix
glm::mat4 View       = glm::lookAt(
    glm::vec3(4,3,3), // Camera is at (4,3,3), in World Space
    glm::vec3(0,0,0), // and looks at the origin
    glm::vec3(0,1,0)  // Head is up (set to 0,-1,0 to look upside-down)
);
// Model matrix : an identity matrix (model will be at the origin)
glm::mat4 Model      = glm::mat4(1.0f);  // Changes for each model !
// Our ModelViewProjection : multiplication of our 3 matrices
glm::mat4 MVP        = Projection * View * Model; // Remember, matrix multiplication is the other way around

第二步:把MVP传给GLSL

// Get a handle for our "MVP" uniform.
// Only at initialisation time.
GLuint MatrixID = glGetUniformLocation(programID, "MVP");

// Send our transformation to the currently bound shader,
// in the "MVP" uniform
// For each model you render, since the MVP will be different (at least the M part)
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
```
第三步:在GLSL中用MVP变换顶点

in vec3 vertexPosition_modelspace;
uniform mat4 MVP;

void main(){

// Output position of the vertex, in clip space : MVP * position
vec4 v = vec4(vertexPosition_modelspace,1); // Transform an homogeneous 4D vector, remember ?
gl_Position = MVP * v;

完成!三角形和第二课的一样,仍然在原点(0, 0, 0),然而是从点(4, 3, 3)透视观察的;相机的上方向为(0, 1, 0),视场角(field of view)45°。

perspective_red_triangle

第6课中你会学到怎样用键鼠动态修改这些值,从而创建一个和游戏中类似的相机。但我们会先学给三维模型上色(第4课)、贴纹理(第5课)。

练习

试着替换glm::perspective

不用透视投影,试试正交投影(orthographic projection )(glm::ortho)

把ModelMatrix改成先平移,再旋转,最后放缩三角形

其他不变,但把模型矩阵运算改成平移-旋转-放缩的顺序,会有什么变化?如果对一个人作变换,你觉得什么顺序最好呢?

附注

1 : [...]好在用一个4×4矩阵就能表示这个投影:实际上,这句话并不对。透视变换不是仿射(affine)的,因此,透视投影无法完全由一个矩阵表示。向量与投影矩阵相乘之后,它齐次坐标的每个分量都要除以自身的W(透视除法)。W分量恰好是-Z(投影矩阵会保证这一点)。这样,离原点更远的点,被除了较大的Z值;其X、Y坐标变小,点与点之间变紧,物体看起来就小了,这才产生了透视效果。

OpenGL3.0教程 第二课: 画第一个三角形


OpenGL3.0教程

免责申明:本文由 @Miss_晶姐 @zsh_0 @cybercser (sina微博)翻译,泰然网转载发布,原翻译github地址:https://github.com/cybercser/OpenGL_3_3_Tutorial_Translation

泰然网github贡献地址:https://github.com/iTyran/opengl-tutorials
原文链接:http://www.opengl-tutorial.org/beginners-tutorials/tutorial-2-the-first-triangle/

第二课: 画第一个三角形

这将又是一篇长教程。

用OpenGL 3实现复杂的东西很方便;为此付出的代价是,画一个简单的三角形变得比较麻烦。

不要忘了,定期复制粘贴,跑一下代码。

如果程序启动时崩溃了,很可能是你从错误的目录下运行了它。请仔细地阅读第一课中讲到的如何配置Visual Studio!

顶点数组对象(VAO)

你需要创建一个顶点数组对象,并将它设为当前对象(细节暂不深入):

GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);

当窗口创建成功后(即OpenGL上下文创建后),马上做这一步工作;必须在任何其他OpenGL调用前完成。

若想进一步了解顶点数组对象(VAO),可以参考其他教程;但这不是很重要。

屏幕坐标系

三点定义一个三角形。当我们在三维图形学中谈论“点(point)”时,我们经常说“顶点(Vertex)”。一个顶点有三个坐标:X,Y和Z。你可以用以下方式来想象这三个坐标:

X 在你的右方 Y 在你的上方 Z 是你背后的方向(是的,背后,而不是你的前方) 这里有一个更形象的方法:使用右手定则

X 是你的拇指 Y 是你的食指 Z 是你的中指。如果你把你的拇指指向右边,食指指向天空,那么中指将指向你的背后。 让Z指往这个方向很奇怪,为什么要这样呢?简单的说:因为基于右手定则的坐标系被广泛使用了100多年,它会给你很多有用的数学工具;而唯一的缺点只是Z方向不直观。

补充:注意,你可以自由地移动你的手:你的X,Y和Z轴也将跟着移动(详见后文)。

我们需要三个三维点来组成一个三角形;现在开始:

// An array of 3 vectors which represents 3 vertices
static const GLfloat g_vertex_buffer_data[] = {
   -1.0f, -1.0f, 0.0f,
   1.0f, -1.0f, 0.0f,
   0.0f,? 1.0f, 0.0f,
};

第一个顶点是(-1, -1, 0)。

这意味着除非我们以某种方式变换它,否则它将显示在屏幕的(-1, -1)位置。什么意思呢?屏幕的原点在中间,X在右方,Y在上方。屏幕坐标如下图:

1_screenCoordinates

该机制内置于显卡,无法改变。因此(-1, -1)是屏幕的左下角,(1, -1)是右下角,(0, 1)在中上位置。这个三角形应该占满了大部分屏幕。

画我们的三角形

下一步把这个三角形传给OpenGL。我们通过创建一个缓冲区完成:

// This will identify our vertex buffer
GLuint vertexbuffer;

// Generate 1 buffer, put the resulting identifier in vertexbuffer
glGenBuffers(1, &vertexbuffer);

// The following commands will talk about our 'vertexbuffer' buffer
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);

// Give our vertices to OpenGL.
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);

这只要做一次。

现在,我们的主循环中,那个之前啥都没有的地方,就能画我们宏伟的三角形了:

// 1rst attribute buffer : vertices
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
   0,                  // attribute 0. No particular reason for 0, but must match the layout in the shader.
   3,                  // size
   GL_FLOAT,           // type
   GL_FALSE,           // normalized?
   0,                  // stride
   (void*)0            // array buffer offset
);

// Draw the triangle !
glDrawArrays(GL_TRIANGLES, 0, 3); // Starting from vertex 0; 3 vertices total -> 1 triangle

glDisableVertexAttribArray(0);

结果如图:

2_triangle_no_shader

白色略显无聊。让我们来看看怎么把它涂成红色。这就需要用到一个叫『着色器(Shader)』的东西。

着色器

编译着色器

在最简单的配置下,你将需要两个着色器:一个叫顶点着色器,它将作用于每个顶点上;另一个叫片断(Fragment)着色器,它将作用于每一个采样点。我们使用4倍反走样,因此每像素有四个采样点。

着色器编程使用GLSL(GL Shader Language,GL着色语言),它是OpenGL的一部分。与C或Java不同,GLSL必须在运行时编译,这意味着每次启动程序,所有的着色器将重新编译。

这两个着色器通常放在单独的文件里。本例中,我们有SimpleFragmentShader.fragmentshader和SimpleVertexShader.vertexshader两个着色器。他们的扩展名是无关紧要的,可以是.txt或者.glsl。

以下是代码。完全理解它不是很重要,因为通常一个程序只做一次,看懂注释就够了。所有其他课程代码都用到了这个函数,所以它被放在一个单独的文件中:common/loadShader.cpp。注意,和缓冲区一样,着色器不能直接访问:我们仅仅有一个编号(ID)。真正的实现隐藏在驱动程序中。

GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){

    // Create the shaders
    GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);
    GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);

    // Read the Vertex Shader code from the file
    std::string VertexShaderCode;
    std::ifstream VertexShaderStream(vertex_file_path, std::ios::in);
    if(VertexShaderStream.is_open())
    {
        std::string Line = "";
        while(getline(VertexShaderStream, Line))
            VertexShaderCode += "n" + Line;
        VertexShaderStream.close();
    }

    // Read the Fragment Shader code from the file
    std::string FragmentShaderCode;
    std::ifstream FragmentShaderStream(fragment_file_path, std::ios::in);
    if(FragmentShaderStream.is_open()){
        std::string Line = "";
        while(getline(FragmentShaderStream, Line))
            FragmentShaderCode += "n" + Line;
        FragmentShaderStream.close();
    }

    GLint Result = GL_FALSE;
    int InfoLogLength;

    // Compile Vertex Shader
    printf("Compiling shader : %sn", vertex_file_path);
    char const * VertexSourcePointer = VertexShaderCode.c_str();
    glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
    glCompileShader(VertexShaderID);

    // Check Vertex Shader
    glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
    glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    std::vector VertexShaderErrorMessage(InfoLogLength);
    glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
    fprintf(stdout, "%sn", &VertexShaderErrorMessage[0]);

    // Compile Fragment Shader
    printf("Compiling shader : %sn", fragment_file_path);
    char const * FragmentSourcePointer = FragmentShaderCode.c_str();
    glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
    glCompileShader(FragmentShaderID);

    // Check Fragment Shader
    glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
    glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    std::vector FragmentShaderErrorMessage(InfoLogLength);
    glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
    fprintf(stdout, "%sn", &FragmentShaderErrorMessage[0]);

    // Link the program
    fprintf(stdout, "Linking programn");
    GLuint ProgramID = glCreateProgram();
    glAttachShader(ProgramID, VertexShaderID);
    glAttachShader(ProgramID, FragmentShaderID);
    glLinkProgram(ProgramID);

    // Check the program
    glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result);
    glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    std::vector ProgramErrorMessage( max(InfoLogLength, int(1)) );
    glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
    fprintf(stdout, "%sn", &ProgramErrorMessage[0]);

    glDeleteShader(VertexShaderID);
    glDeleteShader(FragmentShaderID);

    return ProgramID;
}

我们的顶点着色器

我们先写顶点着色器。

第一行告诉编译器我们将用OpenGL 3的语法。

#version 330 core

第二行声明输入数据:

layout(location = 0) in vec3 vertexPosition_modelspace;

具体解释一下这一行:

“vec3”在GLSL中是一个三维向量。类似于(但不相同)以前我们用来声明三角形的glm::vec3。最重要的是,如果我们在C++中使用三维向量,那么在GLSL中也使用三维向量。

“layout(location = 0)”指我们用来赋给vertexPosition_modelspace这个属性的缓冲区。每个顶点能有多种属性:位置,一种或多种颜色,一个或多个纹理坐标,等等。OpenGL不知道什么是颜色:它只是看到一个vec3。因此我们必须告诉它,哪个缓冲对应哪个输入。通过将glvertexAttribPointer函数的第一个参数值赋给layout,我们就完成了这一点。参数值“0”并不重要,它可以是12(但是不大于glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &v));重要的是两边参数值保持一致。

“vertexPosition_modelspace”这个变量名你可以任取,它将包含每个顶点着色器运行所需的顶点位置值。

“in”的意思是这是一些输入数据。不久我们将会看到“out”关键词。

每个顶点都会调用main函数(和C语言一样):

void main(){

我们的main函数只是将顶点的位置设为缓冲区里的值,无论这值是多少。因此如果我们给出位置(1,1),那么三角形将有一个顶点在屏幕的右上角。 在下一课中我们将看到,怎样对输入位置做一些更有趣的计算。

    gl_Position.xyz = vertexPosition_modelspace;
    gl_Position.w = 1.0;
}

gl_Position是为数不多的内置变量之一:你必须赋一个值给它。其他操作都是可选的,我们将在第四课中看到“其他操作”指的是什么。

我们的片断着色器

作为我们的第一个片断着色器,我们只做一个简单的事:设置每个片断的颜色为红色。(记住,每像素有4个片断,因为我们用的是4倍反走样)

out vec3 color;

void main(){
    color = vec3(1,0,0);
}

vec3(1,0,0)代表红色。因为在计算机屏幕上,颜色由红,绿,蓝这个顺序三元组表示。因此(1,0,0)意思是全红,没有绿色,也没有蓝色。

把它们组合起来

在main循环前,调用我们的LoadShaders函数:

// Create and compile our GLSL program from the shaders
GLuint programID = LoadShaders( "SimpleVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );

现在在main循环中,首先清屏:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

然后告诉OpenGL你想用你的着色器:

// Use our shader
glUseProgram(programID);

// Draw triangle...

…接着转眼间,这就是你的红色三角形!

3_red_triangle

下一课中我们将学习变换:如何设置你的相机,移动物体等等。

OpenGL3.0教程 第一课:新建一个窗口


OpenGL3.0教程

免责申明:本文由 @Miss_晶姐 @zsh_0 @cybercser (sina微博)翻译,泰然网转载发布,原翻译github地址:https://github.com/cybercser/OpenGL_3_3_Tutorial_Translation

本为github贡献地址:https://github.com/iTyran/opengl-tutorials

原文链接:http://www.opengl-tutorial.org/beginners-tutorials/tutorial-1-opening-a-window/

简介

欢迎来到第一课!

在学习OpenGL之前,我们将先学习如何生成,运行,和玩转(最重要的一点)课程中的代码。

预备知识

不需要特别的预备知识。如果你有编程语言(C、Java、Lisp、Javascript等)的经验,理解课程代码会更快;但这不是必需的;如果没有,仅仅是同时学两样东西(编程语言+OpenGL)会稍微复杂点而已。

课程全部用“傻瓜式C++”编写:我费了很大劲尽量让代码简单些。代码里没有模板(template)、类或指针。就是说,即使只懂Java,也能理解所有内容。

忘记一切

如前面所说,我们不需要预备知识;但请暂时把『老式OpenGL』先忘了吧(例如glBegin()这类东西)。
在这里,你将学习新式OpenGL(OpenGL 3和4),而多数网上教程还在讲『老式OpenGL』(OpenGL 1和2)。所以,在你的脑袋乱成一锅粥之前,把它们都搁在一边吧。

生成课程中的代码

所有课程代码都能在Windows、Linux、和Mac上生成,而且过程大体相同:

  1. 更新驱动 !!赶快更新吧。我可是提醒过你哟。
  2. 下载C++编译器。
  3. 安装CMake
  4. 下载全部课程代码
  5. 用CMake创建工程
  6. 编译工程
  7. 试试这些例子!

各平台的详细过程如下。可能需要一些改动。若不确定,看看Windows平台说明,按需改动一下。

在Windows上生成

  1. 更新驱动应该很轻松。直接去NVIDIA或者AMD的官网下载。若不清楚GPU的型号:控制面板->系统和安全->系统->设备管理器->显示适配器。如果是Intel集成显卡,一般由电脑厂商(Dell、HP等)提供驱动。
  2. 建议用Visual Studio 2010 Express来编译。 这里可以免费下载。 若喜欢用MinGW,推荐Qt Creator。安装哪个都行。下列步骤是用Visual Studio讲解的,其他IDE也差不多。
  3. 从这里下载安装 CMake 
  4. 下载课程源码 ,解压到例如C:/Users/XYZ/Projects/OpenGLTutorials .
  5. 启动CMake。让第一栏路径指向刚才解压缩的文件夹;若不确定,就选包含CMakeLists.txt的文件夹。第二栏,填CMake输出路径 (译者注:这里CMake输出一个可以在Visual Studio中打开和编译的工程)。例如C:/Users/XYZ/Projects/OpenGLTutorials-build-Visual2010-32bits,或者C:/Users/XYZ/Projects/OpenGLTutorials/build/Visual2010-32bits。注意,此处可随便填,不一定要和源码在同一文件夹。
  6. 点击Configure。由于是首次configure工程,CMake会让你选择编译器。根据步骤1选择。如果你的Windows是64位的,选64位。不清楚就选32位。
  7. 再点Configure直至红色行全部消失。点Generate。Visual Studio工程创建完毕。不再需要CMake了,可以卸载掉。
  8. 打开 C:/Users/XYZ/Projects/OpenGL/Tutorials-build-Visual2010-32bits会看到Tutorials.sln文件(译者注:这就是CMake生成的VS项目文件),用Visual Studio打开它。

    在 Build 菜单中,点Build All。每个课程代码和依赖项都会被编译。生成的可执行文件会出现在 C:/Users/XYZ/Projects/OpenGLTutorials。但愿不会报错。
  9. 打开C:/Users/XYZ/Projects/OpenGLTutorials/playground,运行playground.exe,会弹出一个黑色窗口。

也可以在Visual Studio中运行任意一课的代码,但得先设置工作目录:右键点击Playground,选择Debugging、Working Directory、Browse,设置路径为C:/Users/XYZ/Projects/OpenGLTutorials/playground。验证一下。再次右键点击Playground,“Choose as startup project”。按F5就可以调试了。

在Linux上生成

Linux版本众多,这里不可能列出所有的平台。按需变通一下吧,也不妨看一下发行版文档。

  1. 安装最新驱动。强烈推荐闭源的二进制驱动;它们不开源,但好用。如果发行版不提供自动安装,试试Ubuntu指南.
  2. 安装全部需要的编译器、工具和库。完整清单如下:cmake make g++ libx11-dev libgl1-mesa-dev libglu1-mesa-dev libxrandr-dev libxext-dev 。 用 sudo apt-get install ***** 或者 su && yum install ******。
  3. 下载课程源码 并解压到如 ~/Projects/OpenGLTutorials/
  4. 接着输入如下命令 :
    • cd ~/Projects/OpenGLTutorials/
    • mkdir build
    • cd build
    • cmake ..
  5. build/目录会创建一个makefile文件。
  6. 键入“make all”。每个课程代码和依赖项都会被编译。生成的可执行文件在~/Projects/OpenGLTutorials/。但愿不会报错。
  7. 打开~/Projects/OpenGLTutorials/playground,运行./playground会弹出一个黑色窗口。

提示:推荐使用Qt Creator作为IDE。值得一提的是,Qt Creator内置支持CMake,调试起来也顺手。如下是QtCreator使用说明:

  1. 在QtCreator中打开Tools->Options->Compile-&Execute->CMake
  2. 设置CMake路径。很可能像这样/usr/bin/cmake
  3. File->Open Project;选择 tutorials/CMakeLists.txt
  4. 选择生成目录,最好选择tutorials文件夹外面
  5. 还可以在参数栏中设置-DCMAKE_BUILD_TYPE=Debug。验证一下。
  6. 点击下面的锤子图标。现在教程可以从tutorials/文件夹启动了。
  7. 要想在QtCreator中运行教程源码,点击Projects->Execution parameters->Working Directory,选择着色器、纹理和模型所在目录。以第二课为例:~/opengl-tutorial/tutorial02_red_triangle/

在Mac上生成

Mac OS不支持OpenGL 3.3。最近,搭载MacOS 10.7 Lion和兼容型GPU的Mac电脑可以跑OpenGL 3.2了,但3.3还不行;所以我们用2.1移植版的课程代码。除此外,其他步骤和Windows类似(也支持Makefiles,此处不赘述):

  1. 从Mac App Store安装XCode
  2. 下载CMake,安装.dmg。无需安装命令行工具。
  3. 下载课程源码 (2.1版!!)解压到如~/Projects/OpenGLTutorials/ .
  4. 启动CMake (Applications->CMake)。让第一栏路径指向刚才解压缩的文件夹,不确定就选包含CMakeLists.txt的文件夹。第二栏,填CMake输出路径。例如~/Projects/OpenGLTutorials_bin_XCode/。注意,这里可以随便填,不一定要和源码在同一文件夹。
  5. 点击Configure。由于是首次configure工程,CMake会让你选择编译器。选择Xcode。
  6. 再点Configure直至红色行全部消失。点Generate。Xcode项目创建完毕。不再需要CMake了,可以卸载掉。
  7. 打开~/Projects/OpenGLTutorials_bin_XCode/会看到Tutorials.xcodeproj文件:打开它。
  8. 选择一个教程,在Xcode的Scheme面板上运行,点击Run按钮编译和运行:



在第二课及后续课程中,Run按钮就失效了。下一版本会解决这个bug。目前,请用Cmd-B键运行(双击源码文件夹/tutorialX/tutorialX,或者通过终端)。

关于Code::Blocks的说明

由于C::B和CMake中各有一个bug,你得在Project->Build->Options->Make commands中手动设置编译命令,如下图所示:

同时你还得手动设置工作目录:Project->Properties->Build targets->tutorial N->execution working dir(即src_dir/tutorial_N/)。

运行课程例子

一定要在正确的目录下运行课程例子:你可以双击可执行文件;如果爱用命令行,请用cd命令切换到正确的目录。

若想从IDE中运行程序,别忘了看看上面的说明——先正确设置工作目录。

如何学习本课程

每课都附有源码和数据,可在tutorialXX/找到。不过,建议您不改动这些工程,将它们作为参考;推荐在playground/playground.cpp中做试验,怎么折腾都行。要是弄乱了,就去粘一段课程代码,一切就会恢复正常。

我们会在整个教程中提供代码片段。不妨在看教程时,直接把它们复制到playground里跑跑看。动手实验才是王道。单纯看别人写好的代码学不了多少。即使仅仅粘贴一下代码,也会碰到不少问题。

新建一个窗口

终于!写OpenGL代码的时刻来了!
呃,其实还早着呢。有的教程都会教你以“底层”的方式做事,好让你清楚每一步的原理。但这往往很无聊也无用。所以,我们用一个外部的库——GLFW来帮我们处理窗口、键盘消息等细节。你也可以使用Windows的Win32 API、Linux的X11 API,或Mac的Cocoa API;或者用别的库,比如SFML、FreeGLUT、SDL等,请参见链接页。

我们开始吧。从处理依赖库开始:我们要用一些基本库,在控制台显示消息:

// Include standard headers
#include  
#include 

然后是GLEW库。这东西的原理,我们以后再说。

// Include GLEW. Always include it before gl.h and glfw.h, since it's a bit magic.
#include 

我们使用GLFW库处理窗口和键盘消息,把它也包含进来:

// Include GLFW
#include 

下面的GLM是个很有用的三维数学库,我们暂时没用到,但很快就会用上。GLM库很好用,但没有什么神奇的,你自己也可以写一个。添加“using namespace”是为了不用写“glm::vec3”,直接写“vec3”。

// Include GLM
#include 
using namespace glm;

如果把这些#include都粘贴到playground.cpp,编译器会报错,说缺少main函数。所以我们创建一个 :

int main(){

首先初始化GLFW :

// Initialise GLFW
if( !glfwInit() )
{
    fprintf( stderr, "Failed to initialize GLFWn" );
    return -1;
}

可以创建我们的第一个OpenGL窗口啦!

glfwOpenWindowHint(GLFW_FSAA_SAMPLES, 4); // 4x antialiasing
glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3); // We want OpenGL 3.3
glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 3);
glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //We don't want the old OpenGL

// Open a window and create its OpenGL context
if( !glfwOpenWindow( 1024, 768, 0,0,0,0, 32,0, GLFW_WINDOW ) )
{
    fprintf( stderr, "Failed to open GLFW windown" );
    glfwTerminate();
    return -1;
}

// Initialize GLEW
if (glewInit() != GLEW_OK) {
    fprintf(stderr, "Failed to initialize GLEWn");
    return -1;
}

glfwSetWindowTitle( "Tutorial 01" );

编译并运行。一个窗口弹出后立即关闭了。可不是嘛!还没设置等待用户Esc按键再关闭呢:

// Ensure we can capture the escape key being pressed below
glfwEnable( GLFW_STICKY_KEYS );

do{
    // Draw nothing, see you in tutorial 2 !

    // Swap buffers
    glfwSwapBuffers();

} // Check if the ESC key was pressed or the window was closed
while( glfwGetKey( GLFW_KEY_ESC ) != GLFW_PRESS &&
glfwGetWindowParam( GLFW_OPENED ) );

第一课就到这啦!第二课会教大家画三角形。

[Cocos2D-X官方文档]:在Android上使用OpenSL ES来播放音效

为什么使用OpenSL ES

一些开发者提醒在Samsung S2 i9100上面使用android原始的SoundPool来播放音效将会导致崩溃,所以我们决定使用OpenSL ES来解决这个问题。但是请注意目前为止只有在I9100上面SoundPool会引起崩溃,所以OpenSL ES只会替换掉I9100上面的SoundPool的实现方式。

怎么样在Cocos2d-x中使用OpenSL ES

实际上,你什么都不需要做。我们将会在你第一次使用SimpleAudioEngine的时候检测你设备类型。如果是I9100,就会自动切换为OpenSL ES,否则就使用SoundPool来播放音效的。

一些限制

OpenSL ES目前只用于samsung i9100。

阅读全文»

新起点:OpenGL ES 3.0和OpenGL 4.3

SIGGRAPH 2012专业图形大会在洛杉矶开幕第一天,Khronos Group组织就做出了多项重要宣布,其中最焦点的当属新一代移动3D图形标准规范“OpenGL ES 3.0”。

OpenGL ES 3.0汲取了桌面版规范OpenGL 3.3/4.2的不少营养,为这项免费开放的3D图形API带来了大量显著的功能性和移动性增强,同时向下兼容已有的OpenGL ES 2.0。

OpenGL ES 3.0主要新功能有:

1、渲染管线多重增强,实现先进视觉效果的加速,包括遮挡查询(Occlusion Query)、变缓反馈(Transform Feedback)、实例渲染(Instanced Rendering)、四个或更多渲染目标支持。

2、高质量ETC2/EAC纹理压缩格式成为一项标准功能,不同平台上不再需要需要不同的纹理集。

3、新版GLSL ES 3.0着色语言,全面支持整数和32位浮点操作。

4、纹理功能大幅增强,支持浮点纹理、3D纹理、深度纹理、顶点纹理、NPOT纹理、R/RG单双通道纹理、不可变纹理、2D阵列纹理、无二次幂限制纹理、阴影对比、调配(swizzle)、LOD与mip level clamps、无缝立方体贴图、采样对象、纹理MSAA抗锯齿渲染器。

5、一系列广泛的精确尺寸纹理和渲染缓冲格式,便携移动应用更简单。

OpenGL ES工作组会在六个月内更新OpenGL ES采纳者项目,提供广泛的兼容性测试,并提供相关源代码。

2D/3D图形知识产权IP提供商Digital Media Professionals Inc. (DMP)也同时宣布,其基于可扩展SMAPH-S着色器的图形IP产品系列也已支持OpenGL ES 3.0。

阅读全文»

在OpenGL ES 2.0中通过触摸来旋转3D对象

本文由泰然教程组出品,翻译:Sile,Sharyu,蓝羽;校对:yuezang;转载请通知泰然!
在这个教程中,你将学到用OpenGL ES 2.0GLKit通过触摸来旋转一个3D物体。

我们先从简单开始,首先向你介绍随着用户的拖拽,怎样通过沿着x轴或y轴旋转一定数量的度数来旋转一个3D物体。然后使用四元数介绍一个更高级的技术。

这个教程会从Beginning OpenGL ES 2.0 with GLKit Tutorial提取示例工程,如果你没有准备好这个工程请下载

请牢记我是自学的,我的数学相当生疏,所以如果犯了错或者没有很正确的解释一些事情请抱歉。如果有人有更好的或更正确的方法来说明,请指出!

言归正传,让我们开始旋转。

阅读全文»

?>