iOS开发之OpenGL ES(三)—纹理

前言:

这篇文章是作者iOS开发之OpenGL ES系列文章的第三篇,首先将把之前的GLKit的案例进行一下简单的重构以简化代码,提高代码的复用率,然后主要深入探讨一下纹理的底层概念和常用选项。
注:(如果你觉得我的文章有所帮助,点个喜欢或关注。您的每一份鼓励都是我前进的动力)

本文系列第一篇:初见篇已经完结,感兴趣可以看看:
iOS开发之OpenGL ES—初见
iOS开发之OpenGL ES—GLKit

正文:

GLKit案例的重构:
  • 重构OpenGL ES 01-GLKit 的可重用OpenGL ES代码为两个新类:AGLKContext和AGLKVertexAttribArrayBuffer。
  • AGLKContext类是一个在例子OpenGL ES 01-GLKit中使用的内建的EAGLContext类的简单子类。重构后的AGLKContext仅仅添加了一个clearColor属性和一个用来告诉OpenGL ES去设置上下文的帧缓存中的每个像素颜色为clearColor的元素值得“-clear:”的方法。
#import 
@interface AGLKContext : EAGLContext
{
    GLKVector4 _clearColor;
}
@property (nonatomic,assign)GLKVector4 clearColor;

- (void)clear:(GLbitfield)mask;
@end

#import "AGLKContext.h"
@implementation AGLKContext

//设置当前OpenGL ES的上下文的“清除颜色”(set存方法)
- (void)setClearColor:(GLKVector4)clearColor{
    _clearColor = clearColor;
    
    glClearColor(clearColor.r,
                 clearColor.g,
                 clearColor.b,
                 clearColor.a);
    
}
//获得当前的OpenGL ES的上下文的“清除颜色”(get取方法)
- (GLKVector4)clearColor{
    return _clearColor;
}
//清除颜色缓冲
- (void)clear:(GLbitfield)mask{
    glClear(mask);
}
@end
  • AGLKVertexAttribArrayBuffer类封装了使用OpenGL ES2.0的顶点属性数组缓存(或者简称“顶点缓存”)的所有7个步骤。这个函数减少了应用需要调用的OpenGL ES函数的数量。
//下面的实现在三个方法中封装了7个缓存管理步骤
#import 
#import 

@interface AGLKVertexAttribArrayBuffer : NSObject

- (id)initWithAttribStride:(GLsizei)stride numberOfVertices:(GLsizei)count data:(const GLvoid *)dataPtr usage:(GLenum)usage;

- (void)prepareToDrawWithAttrib:(GLint)index numberOfCoordinates:(GLint)count attribOffset:(GLsizei)offset shouldEnable:(BOOL)shouldEnable;

- (void)drawArrayWithMode:(GLenum)mode startVertexIndex:(GLint)first numberOfVertices:(GLsizei)count;

//.m文件实现
- (id)initWithAttribStride:(GLsizei)stride numberOfVertices:(GLsizei)count data:(const GLvoid *)dataPtr usage:(GLenum)usage{
    NSParameterAssert(0 < stride);
    NSParameterAssert(0 < count);
    NSParameterAssert(NULL != dataPtr);
    
    self = [super init];
    if (self) {
        _stride = stride;
        _bufferSizeBytes = _stride*count;
        
        glGenBuffers(1, &_glName);//1、为缓存生成一个独一无二的标识符
        glBindBuffer(GL_ARRAY_BUFFER, _glName);//2、为接下来的应用绑定缓存
        glBufferData(GL_ARRAY_BUFFER, _bufferSizeBytes, dataPtr, usage);//3、复制数据到缓存中

        
        NSAssert(0 != _glName, @"生成唯一标识失败");
    }
    return self;
}

- (void)prepareToDrawWithAttrib:(GLint)index numberOfCoordinates:(GLint)count attribOffset:(GLsizei)offset shouldEnable:(BOOL)shouldEnable{
    NSParameterAssert((0= (first+count)*self.stride, @"试图渲染多于可用数量的顶点");
    glDrawArrays(mode, first, count);//6、绘图
}

- (void)dealloc{
    if (0 != _glName) {
        glDeleteBuffers(1, &_glName);//7、删除不再需要的顶点缓存
        _glName = 0;
    }
}
纹理
  • 定义:纹理是一个用来保存图像的颜色元素值的OpenGL ES缓存。纹理可以使用任何图像,包含树木、面孔、砖块、云彩等。在纹理的缓存中保存的颜色值可能要耗费很多的内存。所有的嵌入式系统都为纹理设定了内存的最大尺寸的限制。由于嵌入式系统可用的内存相对较小,应尽量使用最小的图像来产生可以接受的渲染效果。
  • 纹素:当一个图像初始化一个纹理缓存后,在这个图像中的每个像素就变成了纹理中的一个纹素(texel)。与像素类似,纹素保存颜色数据。纹理坐标系有一个命名为S和T的2D轴。在一个我纹理中无论有多少个纹素,纹理的尺寸永远是在S轴上从0.0到1.0,在T轴上从0.0到1.0。从一个1像素高的64像素宽的图像初始化来的纹理会沿着整个T轴有1个纹素,沿着S轴有64个像素。
  • 对齐纹理和几何图形:我们需要告诉OpenGL ES如何使用一个纹理来给几何图形对象着色。帧缓存中的像素位置叫做视口坐标。转换为视口坐标的结果是所有绘制的几何图形都被拉伸以适应屏幕的大小,在每个顶点的X、Y、Z坐标被转换成视口坐标后GPU会设置转换生成的图形内的每个像素的颜色。转换几何形状数据为帧缓存中的颜色像素的渲染步骤叫做点阵化,每个颜色像素叫做片元。当OpenGL ES没有使用纹理的时候,GPU会根据包含该片元的对象的顶点的颜色来计算每个片元的颜色。当设置使用纹理后,GPU会根据在当前绑定的纹理缓存中的纹素来计算每个片元的颜色。程序需要指定怎么对齐纹理和顶点,以便让GPU知道每个片元的颜色由哪些纹素决定。这个对齐又叫做映射,是通过扩展为每个顶点保存的数据来实现的:除了X、Y、Z坐标,每个顶点还给出了U和V坐标值。每个U坐标会映射顶点在视口中的最终位置到纹理中沿着S轴的一个位置。V坐标映射到T轴。
  • 纹理的取样模式。每个顶点的U和V坐标会附加到每个顶点在视口坐标中的最终位置然后GPU会根据计算出来的每个片元的U、V位置从绑定的纹理中选择纹素。这个选择过程叫做取样。取样会把纹理的S和T坐标系与每个渲染的三角形的顶点的U、V坐标匹配起来。
  • OpenGL ES支持多个不同的取样模式:一个拥有大量纹素的纹理被映射到帧缓存内的一个只覆盖几个像素的三角形中,这种情况会在任何时间发生。相反的情况也会发生。一个包含少量纹素的纹理可能会被映射到一个帧缓存中产生很多个片元的三角形。程序会使用如下的glTexParameteri()函数来配置每个绑定的纹理,以便使OpenGL ES知道怎么处理可用纹素的数量与需要被着色的片元的数量之间的不匹配。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//告诉OpenGL ES无论何时出现多个纹素对应一个片元时,与片元的U、V坐标最接近的纹素颜色会被取样
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//告诉OpenGL ES无论何时出现多个纹素对应一个片元时,从相配的多个纹素中取样颜色,然后使用线性内插法来混合这些颜色以得到片元的颜色。
    
    //GL_TEXTURE_MAG_FILTER参数用于没有足够的可用纹素来唯一性的映射一个或者多个纹素到每个片元时配置取样
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);//会有一个放大纹理的效果,并让它模糊的出现在渲染的三角形上
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//仅仅会拾取与片元的U、V位置接近的纹素的颜色,并放大纹理,这会使它有点像素化地出现在渲染的三角形上

//下一篇文章会演示一些由这些纹理参数值产生的效果
  • 除了减小和放大过滤选项,当U、V坐标值小于0或者大于1时,程序会指定要发生什么。有两个选择,要么尽可能多的重复纹理以填满映射到几何图形的整个U、V区域,要么每当片元的U、V坐标的值超出纹理的S、T坐标系的范围时,取样纹理边缘的纹素。纹理的循环模式是为S和T分别设置的,参考代码如下:
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);//取样纹理边缘的纹素
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);//重复纹理填满
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);//取样纹理边缘的纹素
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);//重复纹理填满

//下一篇文章会演示一些由这些纹理参数值产生的效果
  • MIP贴图:是与取样密切相关的。内存存取是现代图形处理的薄弱环节,当有多个纹素对应一个片元的时候,线性取样会导致GPU仅仅为了计算一个片元的最终颜色而读取多个纹素的颜色值。MIP贴图是一个为一个纹理储存多个细节级别的技术。高细节的纹理会沿着S轴和T轴存储很多纹素。低细节的纹理沿着每个轴存储很少的纹素。最低细节的纹理只保存一个纹素。多个细节级别增加了在S、T轴上的纹素和每个片元的U、V坐标之间有紧密的对应关系的可能性。当存在一个紧密的对应关系时,GPU会减少取样纹素的数量,进而会减少内存访问的次数。使用MIP贴图通常会通过减少GPU取样的数量来提高渲染的性能,但是MIP贴图使每个纹理所需要的内存增加了1/3。
纹理Demo

在上一篇文章Demo的基础上先简单的用纹理控制一个渲染的图形的每个像素的颜色。如图1-1:

iOS开发之OpenGL ES(三)—纹理_第1张图片
图1-1.png
  • 首先纹理坐标被加入到SceneVertex类型的声明中:
typedef struct{
    GLKVector3 positionCoords;//GLKVector3类型的positionCoords
    GLKVector2 textureCoords;//GLKVector2类型的纹理坐标
}SceneVertex;
  • 纹理坐标定义了几何图形中的每个顶点的纹理映射。为例子保存顶点数据的vertices数组初始化了纹理坐标和位置坐标。
//初始化位置坐标和纹理坐标
static const SceneVertex vertices[] = {
    {{-0.5f,-0.4f,0.0},{0.0f,0.0f}},//前面三个数字是位置坐标,后面2个数字是纹理坐标
    {{0.5f,-0.4f,0.0},{1.0f,0.0f}},
    {{-0.5f,0.4f,0.0},{0.0f,1.0f}},
    {{0.5f,0.4f,0.0},{1.0f,1.0f}}
};
  • GLKit提供了GLKTextureLoader类,这个类将一个纹理图像加载到一个OpenGL ES纹理缓存中。
    CGImageRef imageRef = [[UIImage imageNamed:@"butterfly"] CGImage];
    GLKTextureInfo *info = [GLKTextureLoader textureWithCGImage:imageRef options:nil error:NULL];//接受一个CGImageRef并创建一个新的包含CGImageRef的像素数据的OpenGL ES纹理缓存,options参数接受一个存储了用于指定GLKTextureLoader怎么解析加载的图像数据的键值对的NSDictionary。可用选项之一是指示GLKTextureLoader为加载的图像生成MIP贴图。
    self.baseEffect.texture2d0.name = info.name;//设置baseEffect的texture2d0属性和使用一个新的纹理缓存。GLKTextureInfo类封装了与刚创建的纹理缓存相关的信息,包含他的尺寸、是否包含MIP贴图、OpenGL ES标识符、名字以及用于纹理的OpenGL ES目标等。
    self.baseEffect.texture2d0.target = info.target;
  • 在glkView: drawInRect:方法实现首先告诉vertexBuffer让OpenGL ES为渲染顶点位置做好准备,然后添加第二个对于“prepareToDrawWithAttrib:(GLint)index numberOfCoordinates:(GLint)count attribOffset:(GLsizei)offset shouldEnable:(BOOL)shouldEnable:”方法的调用为每个顶点的两个纹理坐标的渲染做好准备。
    /*4、启动纹理缓存渲染操作
     *5、告诉OpenGL ES纹理数据在哪里,以及解释为每个纹理保存的数据
     */
    [self.vertexBuffer prepareToDrawWithAttrib:GLKVertexAttribTexCoord0 numberOfCoordinates:2 attribOffset:offsetof(SceneVertex, textureCoords) shouldEnable:YES];
  • 在一个纹理被赋给baseEffect,同时OpenGL ES为使用位置和纹理坐标属性做好准备之后,调用AGLKVertexAttribArrayBuffer的“
  • drawArrayWithMode: startVertexIndex: numberOfVertices:”方法指示OpenGL ES去渲染有纹理的图形。
 /*
 *指示OpenGL ES去渲染图形
 */
[self.vertexBuffer drawArrayWithMode:GL_TRIANGLE_STRIP startVertexIndex:0 numberOfVertices:4];

源码已上传至fenglinyunshi-git,欢迎下载,并提出宝贵意见。

结语:

本文是iOS开发之OpenGL ES的第三篇文章,主要关于GLKit的重构以及引入了一种纹理的技术,它可以控制一个渲染的图形的每个像素的颜色。介绍了一下它的底层概念,后序还会介绍一些关于它的一些常用选项。如果看后你有所收获,那么我会非常高兴,如果文中有不准确的地方还望提出指证,笔者将非常感谢!
未完待续 ...

有志者事竟成,破釜沉舟,百二秦关终属楚

你可能感兴趣的:(iOS开发之OpenGL ES(三)—纹理)