[译] --- OpenGL ES 2.0 for iPhone Tutorial Part 2: Textures

本文译自:OpenGL ES 2.0 for iPhone Tutorial Part 2: Textures。其中主要介绍了OpenGLES纹理相关的内容。

在这系列教程中,我们旨在通过一步步的实际操作来探索OpenGL ES 2.0的神秘之处及其难点所在。
在第一部分OpenGL Tutorial for iOS: OpenGL ES 2.0中,我们学习了初始化OpenGL的基本步骤,创建Vertex和Fragment着色器,在屏幕上渲染一个简单的旋转立方体。
在这一部分中,我们将要进入下一阶段,给立方体上添加一些纹理。
敬告:我不是一个OpenGL的专家!我也正在自学,同时做些笔记而已。如果出现一些愚蠢的错误,敬请指正。
OK,我们来深入学习OpenGL ES 2.0 纹理!

这里写图片描述

开始

你可以先下载或自己编写一份第一部分的sample demo。编译运行,你将看到一个旋转的立方体。

这里写图片描述

现在这个立方体中有绿色和红色,因为我们将顶点设置了相关颜色,这上边没有使用任何纹理。
不过别担心,这恰恰就是我们这一部分将要做的。
首先下载这些纹理,解压到一个文件夹。将该文件夹拖拽至你的Xcode工程中,确保勾选“Copy items into destination group’s folder”,点击Finish。
你将看到两个图片,一个看起来像是一小片地毯,另一个看起来像条鱼。我们就从这里开始吧,将地毯片应用到立方体的每一个面上去。

读取像素数据

我们的第一步是先将图片数据传递到OpenGL中。
可问题在于,OpenGL并不接受程序员通常使用的图像格式(如PNGs)。相反,OpenGL要求你先将图片转换成像素数据,存放到一块内存buffer中传递给它,并且要指定固定的格式。
幸运的是,你可以非常容易地通过一些内建的Quartz2D函数来获取包含像素数据的buffer。如果你事先看过Core Graphics 101系列教程,那么你会对这里的许多函数调用感到非常熟悉。
一共有如下4个主要步骤:

  1. 获取Core Graphics图片引用。因为我们要使用Core Graphics来获取原始像素数据,我们需要这个图片的引用。这很简单,我们可以使用UIImage的CGImageRref属性。
  2. 生成Core Graphics位图上下文(bitmap context)。这一步用来生成Core Graphics的 bitmap context,该context可以指代内存中的一块buffer,用来存储原始像素数据。
  3. 将图像绘制到context中。我们使用简单的Core Graphics函数调用即可实现。接下来,该buffer中即包含了原始像素数据。
  4. 将像素数据传递给OpenGL。我们需要创建OpenGL纹理对象,获取其唯一的ID(名字),然后调用相关函数将像素数据传递给OpenGL。

OK,现在我们来看看代码长成什么样子了。在OpenGLView.m文件中,在initWithFrame上边添加一个新的方法:

- (GLuint)setupTexture:(NSString *)fileName {
    // 1
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }

    // 2
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);

    GLubyte * spriteData = (GLubyte *) calloc(width*height*4, sizeof(GLubyte));

    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,
        CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);

    // 3
    CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);

    CGContextRelease(spriteContext);

    // 4
    GLuint texName;
    glGenTextures(1, &texName);
    glBindTexture(GL_TEXTURE_2D, texName);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);

    free(spriteData);
    return texName;
}

上边有这么多代码!但基本的四个步骤跟上边总结的一样,让我们一段一段来看。

  1. 获取Core Graphics图片引用。我们使用UIImage的imageNamed方法来生成一个UIImage对象,然后获取其CGImage属性。
  2. 生成Core Graphics位图上下文(bitmap context)。为了创建一个bitmap context,先要申请内存空间。这里,我们使用一些函数调用来获取图像的宽度和高度,然后申请width*height*4个bytes的内存空间。
    为啥要乘以4呢?当我们调用相应方法来绘制图像数据的时候,将会为red、greed、blue、alpha分别准备一个byte的空间,所以一共4个bytes。
    为啥分别都是1个byte呢?当我们设置context的时候,使用Core Graphics来做这些。CGBitmapContextCreate的第四个参数是每个元素的bits数,我们设置了8个bits(一个byte)。
  3. 将图像绘制到context中。这一步也非常简单。我们只需要调用Core Graphics的相关函数将图像绘制在指定的矩形中即可。一旦做到了这一步,就可以将context释放掉了。
  4. 将像素数据传递给OpenGL。首先需要调用glGenTextures来创建一个texture对象,设置唯一的ID(或名字)。
    然后调用glBindTexture来将新建的纹理的名字与当前纹理单元绑定。
    接下来使用glTexParameteri设置texture的参数。这里,我们设置GL_TEXTURE_MIN_FILTER(the case where we have to shrink the texture for far away objects)为GL_NEAREST(绘制顶点时,选择最近的纹理像素)。
    我们还可以简单地将GL_NEAREST视为像素接近的(pixel art-like),而GL_LINEAR是平滑的(smooth)。

注意:如果你不使用mipmap,则设置GL_TEXTURE_MIN_FILTER是必须的。我一开始不知道这一点,没加这行代码,就什么也没显示出来。后来才发现这在OpenGL常见错误-OpenGL common mistakes中已经列出来了。
最后一步是使用glTexImage2D函数,将之前创建的包含像素数据的buffer传递给OpenGL。当你调用这个函数时,需指定传入像素数据的格式。这里我们设置为GL_RGBA和GL_UNSIGNED_BYTE,意思是R、G、B、alpha分别有一个byte。
OpenGL支持其他的像素格式(这里是Cocos2D pixel formats的工作原理)。但在本文中,我们选择了最简单的情况。
一旦将图像数据传递给了OpenGL,我们就可以释放像素buffer了。之后不再需要该buffer了,因为OpenGL已将纹理存储在了GPU中。结束后,返回texture的ID(名字),这在接下来的绘制中会用到。

使用纹理数据

现在我们已经封装了上边的方法:加载一个图像,将其传递给OpenGL,返回纹理的名字。接下来我们用它来给立方体加上皮肤。
我们将从vertex和fragment着色器开始。将SimpleVertex.glsl替换成如下:

attribute vec4 Position;
attribute vec4 SourceColor;

varying vec4 DestinationColor;

uniform mat4 Projection;
uniform mat4 Modelview;

attribute vec2 TexCoordIn; // New
varying vec2 TexCoordOut; // New

void main(void) {
    DestinationColor = SourceColor;
    gl_Position = Projection * Modelview * Position;
    TexCoordOut = TexCoordIn; // New
}

这里,我们声明了一个新的attribute变量TexCoordIn。记住:每一个attribute变量都是可以给每一个vertex设置的一个属性。所以,对于每一个vertex,我们可以指定其应该对应的纹理坐标(coordinate)。
纹理坐标有点奇怪,因为它们的范围是在0-1之间。所以(0, 0)是纹理的左下角,(1, 1)是纹理的右上角。
然而,当图片在加载的时候,Core Graphics会将该图片翻转。所以,在这里的代码中,(0, 1)实际上是左下角,(0, 0)是左上角,很奇怪吧。
我们同样声明一个新的varying变量TexCoordOut,将TexCoordIn传递给TexCoordOut。记住:每一个varying变量都会被OpenGL自动插入到fragment shader中。所以,如果我们设置正方形的左下角为(0, 0),右下角为(1, 0),如果要渲染底部的像素,则fragment shader会自动传递(0.5, 0)。(这一段坐标相关的暂未看懂,原文如下)

So for example if we set the bottom left corner of a square we’re texturing to (0,0) and the bottom right to (1, 0), if we’re rendering the pixel in-between on the bottom, our fragment shader will be automatically passed (0.5, 0).

将SimpleFragment.glsl替换如下:

varying lowp vec4 DestinationColor;

varying lowp vec2 TexCoordOut; // New
uniform sampler2D Texture; // New

void main(void) {
    gl_FragColor = DestinationColor * texture2D(Texture, TexCoordOut); // New
}

我们之前直接使用目标颜色DestinationColor来作为输出颜色gl_FragColor,现在我们将目标颜色乘以纹理指定坐标的系数。texture2D是一个内建的GLSL函数,用于采样一个纹理。

现在,我们的新着色器脚本已经准备好使用了。打开OpenGLView.h文件,添加如下新的实例变量:

GLuint _floorTexture;
GLuint _fishTexture;
GLuint _texCoordSlot;
GLuint _textureUniform;

这些变量可以跟踪两个纹理(地板和鱼)的名字,新的输入变量,新的纹理常量。

然后打开OpenGLView.m文件,作如下改动:

// Add texture coordinates to Vertex structure as follows
typedef struct {
    float Position[3];
    float Color[4];
    float TexCoord[2]; // New
} Vertex;

// Add texture coordinates to Vertices as follows
const Vertex Vertices[] = {
    {{1, -1, 0}, {1, 0, 0, 1}, {1, 0}},
    {{1, 1, 0}, {1, 0, 0, 1}, {1, 1}},
    {{-1, 1, 0}, {0, 1, 0, 1}, {0, 1}},
    {{-1, -1, 0}, {0, 1, 0, 1}, {0, 0}},
    {{1, -1, -1}, {1, 0, 0, 1}, {1, 0}},
    {{1, 1, -1}, {1, 0, 0, 1}, {1, 1}},
    {{-1, 1, -1}, {0, 1, 0, 1}, {0, 1}},
    {{-1, -1, -1}, {0, 1, 0, 1}, {0, 0}}
};

// Add to end of compileShaders
_texCoordSlot = glGetAttribLocation(programHandle, "TexCoordIn");
glEnableVertexAttribArray(_texCoordSlot);
_textureUniform = glGetUniformLocation(programHandle, "Texture");

// Add to end of initWithFrame
_floorTexture = [self setupTexture:@"tile_floor.png"];
_fishTexture = [self setupTexture:@"item_powerup_fish.png"];

// Add inside render:, right before glDrawElements
glVertexAttribPointer(_texCoordSlot, 2, GL_FLOAT, GL_FALSE,
    sizeof(Vertex), (GLvoid*) (sizeof(float) * 7));    

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _floorTexture);
glUniform1i(_textureUniform, 0);

有了之前的这些学习,以上的绝大部分代码都是很直观易懂的。
唯一值得注意的是最后三行。这是将fragment shader中定义的Texture uniform设置为代码中加载的纹理。
首先,激活将要向其中加载纹理的纹理单元。在iOS平台,我们有至少2个纹理单元,最多时候有8个。如果你需要一次在多个纹理上执行计算任务,这会确保足够的性能。然而,在本文,我们实际上不需要一次使用多个纹理单元,所以我们就使用第一个纹理单元(GL_TEXTURE0)。
然后,绑定纹理对象_floorTexture到当前纹理单元(GL_TEXTURE0)上。最后,设置texture uniform为当前纹理单元的序号(这里为0)。
注意:第一行和第三行不是严格必须的,很多情况下你压根看不到这些代码。因为OpenGL默认使用GL_TEXTURE0作为当前激活的纹理单元,并且已将texture uniform设置为默认的序号0。因此,我加入这几行是因为其对于初学者很有帮助。
编译运行以上代码,你就会看到一个有纹理的立方体。

这里写图片描述

如上图,立方体的正面看起来不错,但是侧面有点歪斜和变形。这是为啥呢?

修正变形效果

这个问题在于我们当前仅为每一个顶点设置了一个纹理坐标,然后复用了这些顶点。
例如,我们将正面的左下角映射为(0, 0)。但是在左侧,这个相同的顶点却是右下角,这样使用(0, 0)的纹理坐标就不合理了,应该是(1, 0)。
在OpenGL中,我们不能将一个顶点仅仅视为其顶点坐标。而是其坐标、颜色、纹理坐标和其他属性结合的唯一的组合结果。
将你的Vertices和Indices数组替换如下,对每一个面(face)使用了不同的vertex/color/texture coord组合,以确保正确地映射纹理坐标:

#define TEX_COORD_MAX   1

const Vertex Vertices[] = {
    // Front
    {{1, -1, 0}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}},
    {{1, 1, 0}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}},
    {{-1, 1, 0}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}},
    {{-1, -1, 0}, {0, 0, 0, 1}, {0, 0}},
    // Back
    {{1, 1, -2}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}},
    {{-1, -1, -2}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}},
    {{1, -1, -2}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}},
    {{-1, 1, -2}, {0, 0, 0, 1}, {0, 0}},
    // Left
    {{-1, -1, 0}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}},
    {{-1, 1, 0}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}},
    {{-1, 1, -2}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}},
    {{-1, -1, -2}, {0, 0, 0, 1}, {0, 0}},
    // Right
    {{1, -1, -2}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}},
    {{1, 1, -2}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}},
    {{1, 1, 0}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}},
    {{1, -1, 0}, {0, 0, 0, 1}, {0, 0}},
    // Top
    {{1, 1, 0}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}},
    {{1, 1, -2}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}},
    {{-1, 1, -2}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}},
    {{-1, 1, 0}, {0, 0, 0, 1}, {0, 0}},
    // Bottom
    {{1, -1, -2}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}},
    {{1, -1, 0}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}},
    {{-1, -1, 0}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}},
    {{-1, -1, -2}, {0, 0, 0, 1}, {0, 0}}
};

const GLubyte Indices[] = {
    // Front
    0, 1, 2,
    2, 3, 0,
    // Back
    4, 5, 6,
    4, 5, 7,
    // Left
    8, 9, 10,
    10, 11, 8,
    // Right
    12, 13, 14,
    14, 15, 12,
    // Top
    16, 17, 18,
    18, 19, 16,
    // Bottom
    20, 21, 22,
    22, 23, 20
};

就像上一次,我通过在纸上拉伸立方体,把这个问题解决掉了。这是一个非常好的练习方式,推荐使用。
注意:相比上次而言,这次我们重复了大量的数据。我不知道有没有更聪明的方法来解决这个问题, 同时允许纹理坐标保持不同 (如果有的话,请告诉我!)
编译运行,我们就得到了非常漂亮的纹理立方体:

这里写图片描述

重复纹理

在OpenGL中,可以非常容易地在一个表面上重复贴上一个纹理。我们使用的这个地毯片恰好是一个无缝连接的纹理seamless texture,所以试着在每个面上都重复几次吧。

简单修改OpenGLView.m文件如下:

#define TEX_COORD_MAX   4

现在,我们映射立方体的每一个面,左下角是(0,0),右下角是(4,4)。

当映射纹理坐标时,it will behave as if it was a modulo of 1 – for example if a texture coordinate is 1.5, it will map to the texture as if it was 0.5.

编译运行,将能看到纹理在立方体上完美地重复了。

这里写图片描述

注意:这样就自动完成了。因为GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T的默认值是GL_REPEAT。如果想要将纹理这样重复(maybe you want them clamped to the last pixel value),可以使用 glTexParameteri 重写这个参数。

添加贴纸

我们接下来要把小鱼骨头放到立方体的上面。
接下来的代码就是对之前所学内容的额外的练习了。
在OpenGLView.h中添加新的实例变量:

GLuint _vertexBuffer;
GLuint _indexBuffer;
GLuint _vertexBuffer2;
GLuint _indexBuffer2;

在之前,我们仅有一个vertex buffer和一个index buffer,所以当创建的时候就将其作为active buffer,并不需要引用。现在,我们将需要两组vertex/index buffer,一组用于立方体,另一组用于放置鱼骨头的面。所以需要一些引用。

在OpenGLView.m中作如下修改:

// 1) Add to top of file
const Vertex Vertices2[] = {
    {{0.5, -0.5, 0.01}, {1, 1, 1, 1}, {1, 1}},
    {{0.5, 0.5, 0.01}, {1, 1, 1, 1}, {1, 0}},
    {{-0.5, 0.5, 0.01}, {1, 1, 1, 1}, {0, 0}},
    {{-0.5, -0.5, 0.01}, {1, 1, 1, 1}, {0, 1}},
};

const GLubyte Indices2[] = {
    1, 0, 2, 3
};

// 2) Replace setupVBOs with the following
- (void)setupVBOs {

    glGenBuffers(1, &_vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

    glGenBuffers(1, &_indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);

    glGenBuffers(1, &_vertexBuffer2);
    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices2), Vertices2, GL_STATIC_DRAW);

    glGenBuffers(1, &_indexBuffer2);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer2);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices2), Indices2, GL_STATIC_DRAW);

}

// 3) Add inside render:, right after call to glViewport
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);

// 4) Add to bottom of render:, right before [_context presentRenderbuffer:...]
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer2);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer2);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _fishTexture);
glUniform1i(_textureUniform, 0);

glUniformMatrix4fv(_modelViewUniform, 1, 0, modelView.glMatrix);

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*) (sizeof(float) * 3));
glVertexAttribPointer(_texCoordSlot, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*) (sizeof(float) * 7));

glDrawElements(GL_TRIANGLE_STRIP, sizeof(Indices2)/sizeof(Indices2[0]), GL_UNSIGNED_BYTE, 0);

在第一段代码中,我们定义了新的vertices数组用于绘制小鱼纹理的矩形。注意这个矩形比立方体表面稍小一点,并在z坐标稍微高出一点。否则,在深度测试中,它将被忽略掉。
在第二段代码中,我们将vertex/index buffer存于新的实例变量中,而非局部变量。同时为小鱼矩形创建了第二组vertex/index buffer,用的是新的vertices/indexes数组。
在第三段代码中,在绘制之前绑定立方体的vertex/index buffer。
在第四段代码中,绑定小鱼矩形的vertex/index buffer,在小鱼纹理中加载,设置所有的属性。注意我们使用了新的方式GL_TRIANGLE_STRIP来绘制三角形。在每三个顶点中,GL_TRIANGLE_STRIP会通过组合前两个顶点,再加上接下来的新顶点,构成一个新的三角形。这样就可以通过复用顶点减少index buffer的大小。在这里就是要给你看它是如何使用的。
关于该参数的详细解释和使用,可参考iOS — OpenGLES之顶点缓存对象VBO

编译运行,结果如下:

这里写图片描述

绘制了我们想要的鱼图片,但是与其他的内容的混合效果不是非常好。在render之前,使用如下两行来设置混合模式:

glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);

第一行使用glBlendFunc来设置混合模式。为源设置了GL_ONE(即取得源图片的所有内容),为目标设置了GL_ONE_MINS_SRC_ALPHA(即取出目标图片的所有内容,除了源图片已设置的部分)
想要了解关于混合模式的更多讨论,请查看这篇教程 中的内容,其中有一个非常酷的在线工具可帮助理解混合模式。
第二行开启混合模式。
编译运行,现在你会得到一个有着奇怪纹理的立方体,上边有一个奇怪的鱼骨头。

这里写图片描述

接下来做什么呢?

这是我们的sample project
至此,你正在开始接触一些OpenGL ES 2.0中最为重要的部分:添加顶点,创建vertex buffer对象,创建着色器程序,添加纹理,等等。接下来,依然还有非常多要去学习的,如果想了解更多,我推荐Philip Rideout的这本书iPhone 3D Programming。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值