原文链接地址:http://www.raywenderlich.com/4404/opengl-es-2-0-for-iphone-tutorial-part-2-textures 免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作! 教程截图: 在这个系列教程中,我们的目标是帮助大家揭开OpenGL ES 2.0的神秘面纱,同时给大家提供一个手把手的例子,能带领大家步入OpenGL ES 2.0的开发世界。 如果你还没有上一篇教程的工程的话,你可以从这里先下载样例工程。 下载完后,编译并运行工程,你将会看到一个旋转的立方体: 现在,我们的立方体看起来是红绿相间的,因为我们指定顶点的颜色就是这么做的---还没有使用任何纹理贴图。
读取像素数据 我们的第一步就是把图片数据读取到OpenGL中来。
这里有许多代码,让我们一段一段来看:- (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; }
另一种非常简单的思考方式是,把GL_NEAREST看作是“pixel art-like”,而GL_LINEAR是“平滑”。 使用纹理数据 这里,我们声明了一个新的属性,叫做TexCoordIn。记住,属性就是一个值,你可以把它赋值给每一个顶点。因此,对于每一个顶点,我们为它指定需要映射的纹理坐标。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 } 纹理坐标看起来有点奇怪,它们的取值范围总是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: 之前我们老是把目标色直接赋值给输出色--现在,我们把这个目标色乘以纹理图片中相应的坐标点处的颜色。texture2D是Opengl内置的一个函数,它可以得到一个纹理。varying lowp vec4 DestinationColor; varying lowp vec2 TexCoordOut; // New uniform sampler2D Texture; // New void main(void) { gl_FragColor = DestinationColor * texture2D(Texture, TexCoordOut); // New } 现在,我们新的shaders准备就绪了,让我们来使用它们吧!打开OpenGLView.h,然后添加下面几个实例变量: 这些变量来用保存我们之前添加进来的两张图片的纹理名字,同时声明了新的输入属性槽(input attribute slot)和一个新的纹理统一槽(new texture uniform slot)。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); 这里唯一值得详细指出来的就是最后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里面定义下面的宏:
因此,现在,我们给立方体每一个表面从左下角(0,0)到右上角(4,4)都映射了纹理贴图。 当映射纹理坐标的时候,它的行为看起来好像是1的模---比如,如果你的纹理坐标是1.5,那么映射的纹理坐标就会是0.5. 编译并运行,现在你可以看到立方体上有非常好看的重复纹理啦。 注意: 这个能够工作的原因是因为GL_TEXTURE_WRAP_S 和GL_TEXTURE_WRAP_T 默认值是GL_REPEAT 。如果你不想让纹理这样子重复的知,你可以调用函数glTexParameteri来覆盖默认的行为。
|