基本概念
** 纹理 **
概念:纹理是一个用来保存图像颜色元素值的OpenGL ES缓存。应该尽量使用最小的图像来产生可以接受的渲染结果,因为嵌入式系统的可用内存相对较小
** 纹素(texel)**
概念:当用一个图像初始化一个纹理缓存后,在这个图像中的每个像素变成了纹理中的一个纹素。
纹素保存颜色数据:纹素存在于一个虚拟的没有尺寸的数学坐标系中纹理坐标系有一个命名为S何T的2D轴。在一个纹理中无论有多少纹素,纹理的尺寸永远是S、T上从0.0-1.0。从一个1像素高64像素宽的图像初始化来的纹理会沿着整个T轴有1纹素,沿着S轴有64纹素。
像素
概念:像素通常表示计算机屏幕上的一个实际的颜色点
视口(viewport)
概念:在渲染时,GPU会转换纯数学OpenGL ES坐标系中的每个顶点的X、Y、Z坐标为帧缓存中所对应的真实像素位置,而这个像素位置就叫做视口。顶点坐标被转换为视口坐标后,GPU会设置转换生成的三角形内的每个像素的颜色
** 点阵化/光栅化(rasterizing)**
概念:转换几何形状数据为帧缓存中的颜色像素的渲染步骤叫做点阵化。
** 片元(fragment)**
概念:光栅化得到的每个颜色像素叫做片元。
** 映射(mapping)**
概念:程序需要指定怎么对齐纹理和顶点,以便让GPU知道每个片元的颜色由哪些纹素决定,这个对齐就叫做映射。
** 取样(sampling)**
概念:每个顶点的U和V坐标会附加到每个顶点在视口坐标中的最终位置。然后GPU会根据计算出来的每个片元的U、V位置从绑定的纹理中选择纹素。这个选择过程就叫做取样。
** MIP贴图**
拉丁语:Multum In Parvo-放置很多东西的小空间
为纹理存储多个细节级别的技术,与取样密切相关。使用MIP贴图通常会减少GPU取样数量来提高渲染性能,但是MPI贴图使每个纹理所需要的内存增加了1/3。在iOS设备上,内存限制可能优先于渲染性能。最好的策略是测试使用MIP和不使用的性能来决定用不用。
纹理映射
功能启动
为使用纹理,我们需要打开OpenGL的一些开关以启动我们需要的一些功能:
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_SRC_COLOR);
第一个函数打开所有两维图像的功能。这个调用是必不可缺的;如果你没有打开此功能,那么你就无法将图像映射到多边形上。它可以在需要时打开和关闭,但是通常不需要这样做。你可以启动此功能而在绘图时并不使用它,所以通常只需在setup方法中调用一次。
下一个调用打开了混色(blending)功能。 混色提供了通过指定源和目标怎样组合而合成图像的功能。例如,它可以允许你将多个纹理映射到多边形中以产生一个有趣的新的纹理。然而在OpenGL中,“混色”是指合成任何图像或图像与多边形表面合成,所以即使你不需要将多个图像混合,你也需要打开此功能。
最后一个调用指定了使用的混色方法。混色函数定义了源图像怎样与目标图像或表面合成。OpenGL将计算出(根据我们提供的信息)怎样将源纹理的一个像素映射到绘制此像素的目标多边形的一部分。
一旦 OpenGL ES 决定怎样把一个像素从纹理映射到多边形,它将使用指定的混色函数来确定最终绘制的各像素的最终值。 glBlendFunc()函数决定我们将怎样进行混色运算,它采用了两个参数。第一个参数定义了怎样使用源纹理。第二个则定义了怎样使用目标颜色或纹理。在本文简单的例子中,我们希望绘制的纹理完全不透明而忽略多边形中现存的颜色或纹理,所以我们设置源为 GL_ONE,它表示源图像(被映射的纹理)中各颜色通道的值将乘以1.0或者换句话说,以完全颜色密度使用。目标设置为 GL_SRC_COLOR,它表示要使用源图像中被映射到多边形特定点的颜色。此混色函数的结果是一个完全不透明的纹理。这可能是最常用情况。我可能会在以后的文章中更详细地介绍一下混色功能,但这可能是你使用最多的一种组合,而且是今天使用的唯一混色功能。
创建纹理
一旦你启动了纹理和混色,就可以开始创建纹理了。通常纹理是在开始显示3D物体给用户前程序开始执行时或游戏每关开始加载时创建的。这不是必须的,但却是一个好的建议,因为创建纹理需要占用一些处理器时间,如果在你开始显示一些复杂的几何体时进行此项工作,会引起明显的程序停顿。
OpenGL中的每一个图像都是一个纹理,纹理是不能直接显示给最终用户的,除非它映射到物体上。但是有一个小小的例外,就是对允许你将图像绘制于指定点的所谓点精灵(point sprites),但它有自己的一套规则,所以那是一个单独的主题。通常的情况下,任何你希望显示给用户的图像必须放置在由顶点定义的三角形中,有点像贴在上面的粘帖纸。
生成纹理名:
为创建一个纹理,首先必须通知OpenGL ES生成一个纹理名称。这是一个令人迷惑的术语,因为纹理名实际上是一个数字:更具体的说是一个GLuint。尽管“名称”可以指任何字符串,但对于OpenGL ES纹理并不是这样。它是一个代表指定纹理的整数值。每个纹理由一个独一无二的名称表示,所以传递纹理名给OpenGL是我们区别所使用纹理的方式。.
然而在生成纹理名之前,我们要定义一个保存单个或多个纹理名的GLuint数组:
GLuint texture[1];
尽管只有一个纹理,但使用一个元素的数组而不是一个GLuint仍是一个好习惯。当然,仍然可以定义单个GLuint进行强制调用。
在过程式程序中,纹理通常存于一个全局数组中,但在Objective-C程序中,使用例程变量保存纹理名更为常见。下面是代码:
glGenTextures(1, &texture[0]);
你可以调用glGenTextures()生成多个纹理;传递给OpenGL ES的第一个参数指示了要生成几个纹理。第二个参数需要是一个具有足够空间保存纹理名的数组。我们只有一个元素,所以只要求OpenGL ES产生一个纹理名。此调用后, texture[0] 将保持纹理的名称,我们将在任何与纹理有关的地方都使用texture[0]来表示这个特定纹理。
纹理绑定
在为纹理生成名称后,在为纹理提供图像数据之前,我们必须绑定纹理。绑定使得指定纹理处于活动状态。一次只能激活一个纹理。活动的或“被绑定”的纹理是绘制多边形时使用的纹理,也是新纹理数据将加载其上纹理,所以在提供图像数据前必须绑定纹理。这意味着每个纹理至少被绑定一次以为OpenGL ES提供此纹理的数据。运行时,可能再次绑定纹理(但不会再次提供图像数据)以指示绘图时要使用此纹理。纹理绑定很简单:
glBindTexture(GL_TEXTURE_2D, texture[0]);
因为我们使二维图像创建纹理,所以第一个参数永远是 GL_TEXTURE_2D。常规OpenGL支持其他类型的纹理,但目前分布在iPhone上的OpenGL ES版本只支持二维纹理,坦白地说,甚至在常规OpenGL中,二维纹理的使用也远比其他类型要多得多。
第二个参数是我们需要绑定的纹理名。调用此函数后,先前生成了纹理名称的纹理将成为活动纹理。
图像配置
在第一次绑定纹理后,我们需要设置两个参数。需要的话,有一些参数可以设置,但在iPhone上,这两个参数必须设定,否则纹理将不会正常显示。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
必须设置这两个参数的原因是默认状态下OpenGL设置了使用所谓mipmap。今天我将不讨论mipmap,简单地说,我们不准备使用它。Mipmap是一个图像不同尺寸的组合,它允许OpenGL选择最为接近的尺寸版本以避免过多的插值计算并且在物体远离观察者时通过使用更小的纹理来更好地管理内存。感谢矢量单元和图形芯片,iPhone在图像插值方面做得很好,所以我们不需要考虑mipmap。我以后可能会专门撰写一篇文章讨论它,但我们今天讨论的是怎样让OpenGL ES通过线性插值调整图像到所需的尺寸。因为GL_TEXTURE_MIN_FILTER 用于纹理需要被收缩到适合多边形的尺寸的情形,而 GL_TEXTURE_MAG_FILTER 则用于纹理被放大到适合多边形的尺寸的情况下,所以必须进行两次调用。在两种情况下,我们传递GL_LINEAR 以通知OpenGL以简单的线性插值方法调整图像。
加载图像数据
在我们第一次绑定纹理后,必须为OpenGL ES提供纹理的图像数据。在iPhone上,有两种基本方法加载图像数据。如果你在其他书籍上看到使用标准 C I/O方法加载数据的代码,那也是不错的选择,然而这两种方法应该覆盖了你将遇到的各种情形。
UIImage方法:
如果你想使用JPEG, PNG或其他UIImage支持的格式,那么你可以简单地使用图像数据实例化一个UIImage,然后产生图像的RGBA 位图数据:
NSString *path = [[NSBundle mainBundle] pathForResource:@"texture" ofType:@"png"];
NSData *texData = [[NSData alloc] initWithContentsOfFile:path];
UIImage *image = [[UIImage alloc] initWithData:texData];
if (image == nil)
NSLog(@"Do real error checking here");
GLuint width = CGImageGetWidth(image.CGImage);
GLuint height = CGImageGetHeight(image.CGImage);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
void *imageData = malloc( height * width * 4 );
CGContextRef context = CGBitmapContextCreate( imageData, width, height, 8, 4 * width,
colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big );
CGColorSpaceRelease( colorSpace );
CGContextClearRect( context, CGRectMake( 0, 0, width, height ) );
CGContextTranslateCTM( context, 0, height - height );
CGContextDrawImage( context, CGRectMake( 0, 0, width, height ), image.CGImage );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, imageData);
CGContextRelease(context);
free(imageData);
[image release];
[texData release];
前面几行代码很容易理解 – 从程序包中加载一个叫做 texture.png 的图像。然后使用一些 core graphics 调用将位图以 RGBA 格式存放。此基本方法是让我们使用任何UIImage支持的图像数据然后转换成OpenGL ES接受的数据格式。
注意:只是因为 UIImage 不支持一种文件类型并不意味着你不能使用此方法。你仍然有可能通过使用Objective-C的分类来增加 UIImage 对额外的图像文件类型的支持。
一旦具有了正确格式的位图数据,我们就可以调用 glTexImage2D() 传递图像数据给 OpenGL ES。完成后,我们释放了一些内存,包括图像数据和实际 UIImage 的实例。一旦你传递图像数据给 OpenGL ES,它就会分配内存以拥有一份自己的数据拷贝,所以你可以释放所有使用的与图像有关的内存,而且你必须这样做除非你的程序有更重要的与数据相关的任务。即使是来自压缩过图像的纹理也会占用程序相当多的内存。每个像素占用四个字节,所以忘记释放纹理图像数据的内存会导致内存很快被用尽。
PVRTC方法:
iPhone的图形芯片(PowerVR MBX)对一种称为 PVRTC 的压缩技术提供硬件支持,Apple推荐在开发iPhone应用程序时使用 PVRTC 纹理。他们甚至提供了一篇很好的 技术笔记 描述了怎样通过使用随开发工具安装的命令行程序将标准图像文件转换为 PVRTC 纹理的方法。
你应该知道当使用 PVRTC 时与标准JPEG或PNG图像相比有可能有些图像质量的下降。是否值得在你的程序中做出一些牺牲取决于一些因素,但使用 PVRTC 纹理可以节省大量的内存空间。
尽管因为没有Objective-C类可以解析 PVRTC 数据获取其宽和高1
信息,你想要手工指定图像的高和宽,但加载 PVRTC 数据到当前绑定的纹理实际上甚至比加载普通图像文件更为简单。
下面的例子使用默认的texturetool设置加载一个 512×512 的PVRTC纹理:
NSString *path = [[NSBundle mainBundle] pathForResource:@"texture" ofType:@"pvrtc"];
NSData *texData = [[NSData alloc] initWithContentsOfFile:path]; // This assumes that source PVRTC image is 4 bits per pixel and RGB not RGBA
// If you use the default settings in texturetool, e.g. texturetool -e PVRTC -o texture.pvrtc texture.png
// then this code should work fine for you.
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG, 512, 512, 0, [texData length], [texData bytes]);
就这么简单。使用glCompressedTexImage2D()从文件加载数据并传送给OpeNGL ES。而随后怎样处理纹理则绝对没有任何区别。
** 纹理限制**
用于纹理的图像宽和高必须为乘方,比如 2, 4, 8, 16, 32, 64, 128, 256, 512, 或 1024。例如图像可能为 64×128 或 512×512。
当使用 PVRTC 压缩图像时,有一个额外的限制:源图像必须是正方形,所以你的图像应该为 2×2, 4×4 8×8, 16×16, 32×32, 64×64, 128×128, 256×256, 等等。如果你的纹理本身不是正方形,那么你只需为图像加上黑边使图像成为正方形,然后映射纹理使得你需要的部分显示在多边形上。我们现在看看纹理是怎样映射到多边形的。
纹理坐标
当纹理映射启动后绘图时,你必须为OpenGL ES提供其他数据,即顶点数组中各顶点的 纹理坐标。纹理坐标定义了图像的哪一部分将被映射到多边形。它的工作方式有点奇怪。你有一个正方形或长方形的纹理,其左下角为二维平面的原点,高和宽的单位为一。
这就是我们的“纹理坐标系统”,不使用x 和 y 来代表二维空间,我们使用 s 和 t 作为纹理坐标轴,但原理上是一样的。
除了 s 和 t 轴外,被映射的纹理在多边形同样有两个轴,它们称为 u 和 v轴。这是源于许多3D图像程序中的UV 映射 的术语。
当我们指定顶点数组中的顶点时,我们需要在另一个数组中提供纹理坐标,它称为纹理坐标数组。 每个顶点,我们将传递两个 GLfloats (s, t) 来指定顶点在上图所示坐标系统的位置。
网友评论