免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文链接地址:http://www.raywenderlich.com/4404/opengl-es-2-0-for-iphone-tutorial-part-2-textures
教程截图:
在这个系列教程中,我们的目标是帮助大家揭开OpenGL ES 2.0的神秘面纱,同时给大家提供一个手把手的例子,能带领大家步入OpenGL ES 2.0的开发世界。
在第一部分教程中,我们介绍了基本的初始化OpenGL,创建简单的顶点和片断shaders,并且在屏幕上面画了一个简单的旋转立方体。
在这部分教程中,我们将步入更高的级别,给立方体进行纹理贴图!
声明:我并不是一个Open GL的专家,所有这些知识都是我自学来的,这篇教程是我在学习的过程中写的。如果我不小心犯了一些很煞笔的错误,欢迎大家可以给我指正!(译者:当然,其实我也是刚学了一点OpenGL的知识,如果翻译有问题,也肯请大牛帮助指正)
好了,让我们一起进入纹理贴图的世界吧!
如果你还没有上一篇教程的工程的话,你可以从这里先下载样例工程。
下载完后,编译并运行工程,你将会看到一个旋转的立方体:
现在,我们的立方体看起来是红绿相间的,因为我们指定顶点的颜色就是这么做的---还没有使用任何纹理贴图。
但是,不用担心---这正是本教程接下来要做的事!
首先,下载本教程所需要使用的纹理图片,下载完后解压之。然后把它们拖到Resource分组中,同时确保 “Copy items into destination group’s folder” 被选中,然后点击Finish。
你会看到,新添加了两张图片---一张看起来像地板砖,另一张看起来像条鱼。我们将把立方体的每一个面都贴上这个地板砖。
我们的第一步就是把图片数据读取到OpenGL中来。
这里有个问题,就是OpenGL不能直接使用png图片数据,而是,你首先获得png图片的像素数据buffer,并且你需要为这些数据指定格式。
幸运的是,你可以轻松地使用Quartz2D的函数来得到图片的像素数据buffer。如果你看过Core Graphics 101系列教程的话,你肯定对下面的调用很熟悉了。
要完全读取像素buffer的工作,你需要以下4步:
好了,让我们看看具体代码是怎么写的吧。打开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位图上下文. 为了创建位图上下文,你将不得不手动分配内存空间。这里我们使用image的函数来得到宽度和高度,然后分配 width*height*4个字节的数据空间。
”为什么要乘以4?“你可能会问。当我们调用方法来绘制图片数据的时候,我们要为red,green,blue和alpha通道,每一个通道准备一个字节,所以就要乘以4.
“为什么要为每一个通道准备一个字节?”你可能又会问。好吧,因为我们将使用Core Graphics来建立绘图上下文。而CGBitmapContextCreate函数里面的第4个参数指定的就是每一个通道要采用几位来表现,我们使用8位,所以是一个字节。
3) 把图片数据绘制到context中. 这也是一个非常简单的操作---我们只需要告诉Core Graphics在一个指定的矩形区域内来绘制这些图像即可。因为我们完成了绘制图片的工作,所以用完要release。
4) 把像素数据发送给OpenGL. 我们首先调用 glGenTextures来创建一个纹理对象,并且得到一个唯一的ID,由“name”保存着。然后,我们调用glBindTexture来把我们新建的纹理名字加载到当前的纹理单元中。接下来的步骤是,为我们的纹理设置纹理参数,使用glTexParameteri函数。这里我们设置函数参数为GL_TEXTURE_MIN_FILTER(这个参数的意思是,当我们绘制远距离的对象的时候,我们会把纹理缩小)和GL_NEAREST(这个参数的作用是,当绘制顶点的时候,选择最邻近的纹理像素)。
另一种非常简单的思考方式是,把GL_NEAREST看作是“pixel art-like”,而GL_LINEAR是“平滑”。
注意:虽然本例子中没有使用mipmaps,但是,还是需要设置GL_TEXTURE_MIN_FILTER。我刚开始不知道要这样做,而且并没有设置这个参数,结果屏幕上什么也看不到!后来我在OpenGL 常见错误中发现在这个问题--走运啊!
最后一步就是把像素buffer中的数据发送给OpenGL,通过调用函数glTexImage2D。当你调用这个函数的时候,你需要指定像素格式。这里我们指定的是GL_RGBA和GL_UNSIGNED_BYTE。它的意思是说,红绿蓝alpha道具都有,并且他们占用的空间是1个字节,也就是每一个通道8位。
OpenGL还支持其它的像素格式(你也可以查一查cocos2d支持的像素格式有哪些)。但是,对于本教程来说,我们就只使用这种RGBA了,其它的留给读者自己去探究。
一旦我们把图片数据发送给OpenGL之后,我们就可以释放掉像素buffer了---我们不再需要它了,因为Opengl已经把纹理存储到GPU中去了。最后,我们返回纹理的名字,这个名字我们之后的程序要使用到。
现在,我们拥有一个辅助方法来加载图片并能够把它发送给OpenGL了。接下来,让我们使用这个给立方体做件“嫁衣”吧!
我们将从顶点和片断着色器开始。打开 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
}
这里,我们声明了一个新的属性,叫做TexCoordIn。记住,属性就是一个值,你可以把它赋值给每一个顶点。因此,对于每一个顶点,我们为它指定需要映射的纹理坐标。
纹理坐标看起来有点奇怪,它们的取值范围总是0-1.因此(0,0)就代表纹理的左下角,而(1,1)则代表纹理的右上角。
但是,CoreGraphics在加载图片的时候会垂直翻转图片。所以,在代码中(0,1)是左下角,而(0,0)是左上角,够奇怪了吧!
我们也创建一个新的varying,叫做TexCoordOut,并且把TexCoordIn赋值给它。记住,一个varying也是一个值,OpenGL在进行片断着色的时候会为我们自动进行运算,得到正确的坐标点。因此,打个比方,如果我们把一个正方形的左下角映射纹理坐标为(0,0),而把右上角映射为(1,0)。如果我们在渲染左下角和右上角中间的某个像素的时候,片断着色器会自动计算得到(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
}
之前我们老是把目标色直接赋值给输出色--现在,我们把这个目标色乘以纹理图片中相应的坐标点处的颜色。texture2D是Opengl内置的一个函数,它可以得到一个纹理。
现在,我们新的shaders准备就绪了,让我们来使用它们吧!打开OpenGLView.h,然后添加下面几个实例变量:
GLuint _floorTexture;
GLuint _fishTexture;
GLuint _texCoordSlot;
GLuint _textureUniform;
这些变量来用保存我们之前添加进来的两张图片的纹理名字,同时声明了新的输入属性槽(input attribute slot)和一个新的纹理统一槽(new texture uniform slot)。
然后打开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);
基于我们前面教程中所介绍的内容,这里的大部分内容应该非常容易理解。
这里唯一值得详细指出来的就是最后3行代码。这也是我们如何把之前在片断着色器中定义的Texture uiniform映射到我们代码中的texture中来的。
首先,我们激活我们想要加载进入的纹理单元。在IOS上面,我们可以拥有至少2个纹理单元,最多是8个。当我们一次需要对不止一个纹理进行计算的时候,这个就发挥作用了。然后,在本教程中,我们并不需要多于一个的纹理单元,所以我们只需要第一个纹理单元(GL_TEXTURE0)。
然后,我们把纹理绑定到当前的纹理单元中(GL_TEXTURE0)。最后,把纹理单元0的索引设置为_textureUniform。
注意:其实第1和3句调用其实并不是必须的,有时候,你可能会看见别人的代码里面可能并没有包含这几行代码。这是因为我们假设GL_TEXTURE0就是当前激活的纹理单元了,我们也不需要设置uniform,因为它默认就是0.我在这篇教程中添加这3行代码的意思是方便初学者更好地理解代码。
编译并运行代码,这时你可以看到一个拥有纹理贴图的立方体啦!
恩。。。这个立方体的正面看起来还算ok,但是其它面看起来有点被拉伸了---这是怎么回事呢?
这个问题的原因是,因为我们当前只是为每一个顶点设置一个纹理坐标,然后重复使用这些顶点。
举个例子,我们把第一面的左下角映射到(0,0)。但是,在左边那一面,同样的顶点数据却变成了右上角,所以这时候如果使用(0,0)纹理坐标去贴图的话就没有意义了,这时候应该要使用(1,0)。(为什么不是(1,1),因为图片垂直翻转了!!!)
在OpenGL里面,你不能简单的把一个顶点当成是一个顶点坐标---而应该是把坐标、颜色和纹理坐标绑定到一起,统一属于某一个顶点。
继续并把你的顶点和索引数组替换成下面的内容,它为每一面都定义了顶点坐标、颜色和纹理坐标数据。
#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里面,如果你喜欢的话,你可以把一张图片重复地贴在某一个表面上。但是,保证重复纹理时能够拼接得很好,我们需要一些无缝纹理。
有了纹理之后,我们在OpenGLView.m里面定义下面的宏:
#define TEX_COORD_MAX 4
因此,现在,我们给立方体每一个表面从左下角(0,0)到右上角(4,4)都映射了纹理贴图。
当映射纹理坐标的时候,它的行为看起来好像是1的模---比如,如果你的纹理坐标是1.5,那么映射的纹理坐标就会是0.5.
编译并运行,现在你可以看到立方体上有非常好看的重复纹理啦。
注意: 这个能够工作的原因是因为GL_TEXTURE_WRAP_S 和GL_TEXTURE_WRAP_T 默认值是GL_REPEAT 。如果你不想让纹理这样子重复的知,你可以调用函数glTexParameteri来覆盖默认的行为。
我们将以在立方体的某一个面添加一个鱼骨头的纹理作为教程的结束。为什么呢?因为Grand Cat Dispatch 要上场啦!
实现这个功能的步骤可能和本教程前面部分差不了太多。所以,让我们直接开始吧。
打开OpenGLView.h,然后添加下面几个实例变量:
GLuint _vertexBuffer;
GLuint _indexBuffer;
GLuint _vertexBuffer2;
GLuint _indexBuffer2;
之前,我们只有一个顶点buffer和一个索引buffer,因此,我们需要再创建它们。现在我们需要2个顶点/索引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);
在第一部分中,我们为矩形定义了一组顶点,我们将在这些顶点所在的范围内绘制鱼骨的纹理。注意,我把它弄得比前表面要稍微小一点,同时,我也把z坐标弄得稍微大一点。这样子我们就会有深度视觉效果了。
在第二部分中,我们保存顶点/索引 buffer。我们也以鱼骨矩形区域创建第一个顶点/索引buffer。
在第三部分中,我们在绘制纹理之前先绑定顶点/索引buffer。
在第四部分中,我们绑定鱼矩形区域的顶点/索引buffer,加载鱼的纹理,然后设置所有的属性。注意,这里我们绘制三角形使用一种新的模式--GL_TRIANGLE_STRIP。
GL_TRIANGLE_STRIP会生成三角形带,具体的解释可以参考《如何使用cocos2d制作一个tiny wings游戏》。使用这个参数可以减少索引buffer的大小。
编译并运行,然后你会看到下面的输出。
这里绘制了鱼的图片,但是,渲染的效果并不是很好。为了使之效果更好看,我们需要激活blend,在渲染函数里面添加下面2行:
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
第一行使用glBlendFunc 来设置混合算法。设置源为GL_ONE的意思是渲染所有的源图像的像素,而GL_ONE_MINS_SRC_ALPHA意思是渲染所有的目标图像数据,但是源图像存在的时候,就不渲染。得到的效果就是,源图片覆盖在目的图像上面。为了更好地理解混合及其参数的含义,请参考这篇文章。
当然,我们在之前的教程中也有提到过,详情请查看这篇教程。
第2行代码是激活混合模式。就这么多!编译并运行,现在,你可以看到一个更加真实的渲染结果啦。
这里有本教程的完整源代码。
到目前为止,通过这2篇教程,相信大家对于OpenGL ES 2.0已经有一个基本的了解了,你应该学会如何添加顶点,如何创建顶点缓冲对象,如何建仓shaders,如何创建纹理对象等等。
其实,在OpenGL领域,你需要学习的还很多很多,但是,我希望这里是你非常好的一个开端。
如果你想学习更多有关Ipengles的内容的话,推荐你看iPhone 3D Programming。我也是从阅读这本书来学习的。
上一周投票结果:
我每周都会翻译1~2篇教程,题目上一周是我自己定的,如果大家想要我翻译自己想看的教程,就给这篇贴子留言吧,我到时候再弄一个投票,票多者胜!
我会在每周日晚上把投票弄上去,如果大家没有建议的话,我就会自己决定4篇候选教程,欢迎大家参与。
每周一篇教程的投票链接在主页的右方,投票结果与技术问题汇总也在右方有个链接,方便大家查阅。
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!