Learning iOS Game Programming_第05章(图片渲染)_第06.01节_图片渲染类_Texture2D类

          在第5章的示例项目(CH05_SLQTSOR)中,你会看到一个叫做Game Engine的组。这是放置游戏核心类的地方。这个组包含了另外的两个组:Image和Managers。
  在Image组中,你会看到Image类的类文件及一个包含了Texture2D类的Texture2D组。Texture2D类负责创建OpenGL ES纹理并关联从图片文件中加载的数据。Image类用来包装Texture2D类的实例对象,它提供了简单的API,允许我们在屏幕上旋转、缩放、转换(平移)、渲染图片。
  在Managers组中,你会看到两个组:Texture Manager和Image Render Manager。
  在Texture Manager组中的是TextureManager类。这个类允许我们缓存已经创建的纹理及与Image类的实例对象共享这些纹理。
  在Image Render Manager组中的就ImageRenderManager类。该类负责对需要渲染的图片进行批处理,然后使用最少的OpenGL ES API调用把他们渲染出来。当使用该类来渲染大量图片时,会减少OpenGL ES API调用,这对我们来说是非常重要的。
  我们现在已经到达创建及渲染图片到屏幕上的步骤上了,同时,去看看那些我们在Sir Lamorak's Quest中使用到的类,并把这些方法提取出来。
  尽管创建并渲染图片到屏幕上的实际所需的步骤并不是很复杂,而且可以压缩到几行代码中,但是,我们需要稍微更复杂的代码。这些额外的复杂性是必要的,它可以使得我们更高效地管理我们的资源,并且在渲染大量图片是保持性能。

  注意:从这个项目开始,Global Headers组中的Common.h文件已经更名为Global.h。

Texture2D类
  如先前描述的那样,实际加载图像数据和生成一个OpenGL ES纹理的工作是在Texture2D类内进行的。 现在,你应该知道了如何创建和使用这个类的头文件,所以我不会贯穿整个文件,除非文件中有一些特别需要我指明的东西。
  现在,请打开Texture2D.m文件,让我们看看它有哪些功能。
初始化
  Texture2D类在初始化方法之外并没有提供其它的功能。当通过一张图片把对象初始化后,它的唯一任务就是存储由图片数据生成的OpenGL ES纹理的信息。其它的类(如Image类)会使用到这个信息。所以,initWithImage:filter:是这个类中唯一的一个方法。
加载图片
  为了在OpenGL ES中创建一个纹理,我们需要一些被纹理使用的图片数据。Texture2D使用UIImage来加载一张图片,随后提取出OpenGL ES需要的图片数据。这是非常好的,因为UIImage有能力加载不同格式的图片文件,例如png、bmp、gif、jpg等。
  在initWithImage:filter:方法中,你可以看到一个UIImage对象被传递进去,这时,底层的位图图片数据被引用:

CGImageRef image;
image = [aImage CGImage];

CGImage给了我们被传入的UIImage对象的底层位图图片数据,同时我们把它放置到了一个CGImageRef结构中。当我们寻找图片格式及图片大小等内容时,我们就可以从这个结构中读取到相应的数据。
  然后,这个类会通过检查来确保图片数据已经被找到。如果没有,它会报出错误。
  使用图片,我们现在就可以检索图片的alpha(透明度)及颜色空间的信息。因为我们是通过CGImageRef来访问CGImage信息的,我们可以使用以下命令来获取信息:

CGImageGetAlphaInfo(image)
CGImageGetColorSpace(image)

有了这个信息,我们就可以探测到图片的alpha及颜色空间的信息。 随后,当我们为OpenGL ES准备图片数据时,我们会用到它。
调整图片大小
  让我们来看看关于OpenGL ES纹理的几个要点。
  首先,就是OpenGL ES纹理的大小,它的宽度及高度必须是2的N次方。这就意味着,你的图片的宽度及高度需要是以下值中的任意一个:
2,4,8,16,32,64,128,256,512,1024
  宽度与高度不必是相等的,你可以拥有一个512*64的纹理,但它们应该使用上面列表中的任意一个值。
  第二 ,就是我把列表终止在1024处的原因。在iPhone上,可以加载到OpenGL ES中的最大的纹理是1024*1024像素。如果你需要显示更大的图片,你需要把这个图片进行分解,分解出来的图片的大小不要大于1024*1024。使用多个小图片来创建一个大图片在游戏开发是是一个常用的技法,我们会在第9章(瓦片地图)中讲解瓦片地图时涵盖到该知识。
  注意:在iPhone 3GS、iPhone 4及iPad上,支持大小为2048*2048的纹理。
  现在,我们已经捕获到了被加载进去的图片的信息,我们需要确保图片的宽高都是2的N次方,并且不能大于1024。你可以在使用制图软件创作这些图片时就确保它们的大小是正确的,但是那是非常痛苦的,而且有时你需要一些宽高不是2的N次方的图片。
  为了让事情变得简单,Texture2D打算为我们处理图片的大小。我们只需获取被加载图片的当前大小,然后检查它们是否符合要求(宽高为2的N次方并且限制在1024*1024内)。
   获取图片的内容大小是再简单不过的:

contentSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image)); 

  已经得到图片大小了,现在需要去确保它是2的N次方。这里并没有多少美妙的地方---只是一些循环而已。在循环中计算图片的宽高,并确保它们是2的N次方,清单5.4展示了这个操作:
清单5.4

width = contentSize.width;
if((width != 1) && (width & (width - 1))) {
  pot = 1;
  while (pot < width) {
    pot *= 2;
  }
  width = pot;
}
height = contentSize.height;
if((height != 1) && (height & (height - 1))) {
  pot = 1;
  while(pot < height) {
    pot *= 2;
  }
  height = pot;
}

如果宽度大于1并且不是2的N次方,我们就从1开始,每次乘以2,直到新的宽度大于当前的宽度。这样就给了我们一个可以容纳当前宽度且又是2的N次方的宽度。我们对高度进行了相同的操作。新的宽高会在我们创建OpenGL ES纹理时用到,而且不会影响到被加载图片的实际大小。
  计算图像的尺寸(让它是2的N次方)后,我们需要确保它不会超过最大纹理的尺寸。
  为了达到这个目的,我们使用Core Graphics中的方法,创建一个CGAffineTransform,把单位矩阵存到里面。存入单位矩阵使用与第3章(The Journey Begins)中一样的方式:

CGAffineTransform transform =  CGAffineTransformIdentity;

加载这个矩阵后,我们检验一下宽高是否大于被允许的最大值。如果大于,就使用CGAffineTransformScale命令让其宽高减半,如清单5.5:
清单5.5

复制代码
while((width > kMaxTextureSize) || (height > kMaxTextureSize)) {
  width /= 2;
  height /= 2;
  transform = CGAffineTransformScale(transform, 0.5, 0.5);
  contentSize.width *= 0.5;
  contentSize.height *= 0.5;
}
复制代码

现在,我们就达到目的了---宽高为2的N次方并且不超过被允许的最大值。
生成图片数据
  已经把纹理的宽高计算为2的N次方并且正好足够包含住我们的图片,现在就要把这些数据添加到OpenGL ES中了。不幸的是,它并不像仅把几个参数传递给OpenGL ES那么简单。我们需要做的是实际生成一个新的位图图片,这个图片的宽高就是刚才我们计算出来的宽高,然后把加载进去的图片渲染到这个位图图片上。
  为了达到这个目的,我们需要创建一个新的CGContextRef,它被称为上下文,在这个上下文中,我们会对新的图片进行渲染。
  基于我们在加载图片时识别出来的像素格式,我们创建了一个用来存储位图数据的颜色空间及一个用来渲染图片的上下文。
  Texuture2D类基于像素格式使用一个switch语句来识别出正确的配置。清单5.6展示了为kTexture22DPixelFormat_RGBA8888像素格式配置的代码。
清单5.6

colorSpace = CGColorSpaceCreateDeviceRGB();
data = malloc(height * width * 4);
context = CGBitmapContextCreate(data, width, height, 8, 4 * width, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);

它创建了一个颜色空间,以及一个用来存储我们要渲染的位图图片数据的容器,这个容器是data。然后用它们来设置上下文。
  已经配置好一个上下文了,我们将会使用它来渲染我们的图片,现在就开始进行渲染,代码展示在清单5.7中。
清单5.7

CGContextClearRect(context, CGRectMake(0, 0, width, height));
CGContextTranslateCTM(context, 0, height - contentSize.height);
if (!CGAffineTransformIsIdentity(transform)) {
  CGContextConcatCTM(context, transform);
}
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);

正如你看到的那样,我们对已经创建出来的上下文先进行了清理,然后把起始位置移动到正确的位置。如果我们已经调整过图片的大小,它的宽高可能大于被允许的最大值,我们就使用先前创建出来的缩放变换对其进行一次修改。最后,我们把加载进去的位图图片数据实际绘制到这个上下文中。

注意:
  Core Animation及Quartz使用的坐标系与OpenGL ES使用的坐标系是不同的。在OpenGL ES中,y轴是在屏幕的底部从0开始的,其正方向指向上方,但是,在Core Animation中,y轴是在屏幕的顶部从0开始的,其正方向指向下方。这就意味着,加载到OpenGL ES纹理中的图片在显示的时候正好是倒置的。这个问题是很容易解决的,当我们讲解Image类的时候(在本章的后面),会涵盖到这个知识。

  在创建OpenGL ES纹理之前,还需要另外一道检测工序。如果图片的像素格式被识别为kTexture2DPixelFormat_RGB565,我们需要把数据从32位转换成16位,代码展示在清单5.8中。这段代码是从苹果公司的示例程序中直接拿来的,在很多示例中都可以找到这段代码。
清单5.8

复制代码
if(pixelFormat == kTexture2DPixelFormat_RGB565) {
  void* tempData = malloc(height * width * 2);
  unsigned int *inPixel32 = (unsigned int*)data;
  unsigned short *outPixel16 = (unsigned short*)tempData;
  for(int i = 0; i < width * height; ++i, ++inPixel32) {
    *outPixel16++ = ((((*inPixel32 >> 0) & 0xFF) >> 3) << 11) | ((((*inPixel32 >> 8) & 0xFF) >> 2) << 5) | ((((*inPixel32 >> 16) & 0xFF) >> 3) << 0);
  }
  free(data);
  data = tempData;
}
复制代码

在本节的结尾部分,我们有一个存储位图图片信息的数据,它的名字是data,这个位图图片是我们新创建出来的,并且它的宽高正好是2的N次方。看似有很多代码,但是使用Texture2D为我们处理图片时,会为我们一个一个地处理图片节省很多。

生成纹理名称
  创建OpenGL ES纹理的一个关键步骤就是生成纹理名称。这是非常重要的,因为它可以让我们告诉OpenGL ES,我们到底引用的是哪一个纹理。纹理名称是一个很容易混淆的地方。看到它时,你可能想到是,应该给它赋予一个描述性的名称,像"Bob"或"Shirley"。但是,并不是那回事儿---并不是因为"Bob"或"Shirley"这样的名字有点俗,而是因为在OpenGL ES中,纹理的名称是一个具有唯一性的数字,这个数字属于GLuint类型。
  现在我们知道了纹理的名称实际上是一个数字,我们就可以使用以下命令来让OpenGL ES给我们生成一个名称:

glGenTextures(1, &name);

实例变量name已经被定义在Texture2D的头文件中,它属于GLuint类型,并且在glGenTextures命令中使用它来请求OpenGL ES生成一个纹理名称。你可能注意到,这个命令是一个复数(以s结尾),那是因为你可以在某一时刻请求OpenGL ES生成多个纹理名称。这个命令的第一个参数用来指明你需要生成几个纹理名称。
  如果你要一次生成多个纹理名字,第二个参数就应该指向一个GLuint数据,而不是一个单独的GLuint。glGenTextures的原型是这样的:glGenTextures (GLsizei n, GLuint *textures)。
  在游戏开始的时候生成纹理而不在游戏过程中生成纹理是好的习惯。生成纹理名称需要额外的开销,更不用说实际加载图片数据及数据与纹理进行关联。要阻止它们对性能的影响,通常在游戏开始之前就完成这些任务。

绑定纹理与设置参数
  请记住,OpenGL ES是一个状态引擎,因此在纹理上进行任何操作之前,OpenGL ES需要知道那些操作针对的是什么样的纹理。绑定纹理的时候就需要用到这一点。在生成纹理名称语句的下面是这条命令:

glBindTexture(GL_TEXTURE_2D, name); 

第一个参数表示目标。当使用OpenGL ES时,它必须是GL_TEXTURE_2D。因为OpenGL ES只支持GL_TEXTURE_2D目标,而且我们会使用一张2维图片来创建我们的纹理。尽管OpenGL(不是OpenGL ES)支持其它种类的目标,但OpenGL ES目前只支持GL_TEXTURE_2D。第二个参数是我们想要绑定的纹理的名称,因此我们就提供先前生成的纹理名称。
注意:
  绑定纹理也会招致额外的开销,因此要尽可能地在每帧中减少绑定的次数。有种做法就是使用一个精灵表单(Sprite Sheet)或纹理地图集(Texture Atlas),使用它可以让你把许多不同的图片存储在一个单独的纹理之中。在第6章(Sprite Sheets)会涵盖到这个知识。

  当我们告诉OpenGL ES我们要使用哪一个纹理后,我们就可以开始与这个纹理相关联的参数了。当创建一个新的OpenGL ES纹理时,有两个关键的参数,它们必须被设置,如下所示:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, aFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, aFilter);

第一个参数是目标,在绑定纹理时,我们使用了相同的目标。第二个参数用来指明参数名称(我们要配置的参数的名称),第三个参数就是实际的参数值。
  我们配置的那些参数用来定义当图片被缩小(GL_TEXTURE_MIN_FILTER)或放大(GL_TEXTURE_MAG_FILTER)后,OpenGL_ES该如何处理图片。
  你也可以配置其它的参数名称(目前有GL_TEXTURE_MAG_FILTER、GL_TEXTURE_MIN_FILTER、GL_TEXTURE_WRAP_S、GL_TEXTURE_WRAP_T、GL_GENERATE_MIPMAP),但只有这两个是我们关注的。默认情况下,这个参数名称被设置为一个叫mipmap的东西,mipmap是一个预先计算并优化的图片集合,它会被纹理用到。mipmap中的图片基本上是同一个图片的不同大小的表现,这些可以基于纹理的大小被渲染出来。与现实世界中的图片收缩相比,OpenGL ES可以从mipmap可用的图片中产生出与它的需求最接近的一个图片。
  我们不会在我们的游戏中使用mipmap,因为iPhone只有在飞行模式下才会更好地利用它的图形芯片和浮点运算单元。
  这些参数名称可以使用下面的值:
    1、GL_NEAREST:返回与纹理像素中心点距离最近的纹理元素的值。
    2、GL_LINEAR:返回与纹理像素中心点距离最近的4个纹理元素的平均权重。如果你想让你的图片进行二次像素渲染,就使用这个过滤器。如果你想让图片低速平滑移动,它对于将图片渲染到精确的像素位置是非常重要的。
  简单地说,GL_NEAREST会给你一个图片缩放失常的效果,GL_LINEAR会给你一个平滑的抗锯齿效果。
  在Texture2D中,被这些参数名称使用的参数值是从初始化方法中传入的,这就可以让你改变过滤器。
  注意:在一个纹理中缩小倍数与放大倍数的值应该相匹配。这是先前使用PowerVR MBX芯片的iPhone 3GS、iPhone 4和iPad的一个限制。使用PowerVR SGX芯片的设备没有这个限制,iPhone模拟器也没有这个限制。

向OpenGL ES纹理中加载图片数据
  现在,我们就将先前生成的位图图片数据加载到我们当前绑定的纹理中,这样就可以完成OpenGL ES纹理的创建。
  在Texture2D类中,你会看到我们再次用到了一个基于像素格式来识别使用哪一个命令的switch语句。下面的OpenGL ES命令为 kTexture2DPixelForm_RGBA8888像素格式向纹理中加载图片数据:

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

在这个语句中使用到的参数如下:
1、Target:目标纹理。必须是GL_TEXTURE_2D。
2、Level:具体的等级值。等级0是是基本的图片等级。等级n是第n个mipmap缩版图片,n必须大于或等于0.
3、Internal format:在纹理中,颜色的组成部分。它必须与后面的Format参数一致。
4、Width:纹理图片的宽度。
5、Height:纹理图片的高度。
6、Border:边框的宽度。必须是0.
7、Format:像素数据的格式。
8、Type:在像素数据中使用的数据类型。
9、Pixels:一个指向图片数据的指针。
在发出上述命令的时候,存储在实例变量data中的图片数据被加载到OpenGL ES中,并与当前绑定的纹理相关联。
  Texture2D类中的最后几个动作用来建立一些参数,这些参数使得纹理映射管理起来更加方便。
  当我们在本章的前面学习纹理映射时,你看到了在纹理内使用的坐标轴名字是(s, t)。为了完成我们纹理的创建,我们需要定义我们的纹理的最大s值及最大t值。这是重要的,因为我们实际加载进去的图片可能会比我们创建出来的纹理的尺寸小一些。因此,我们要为纹理中的图片计算一下maxS及maxT。
  图5.9展示了一张56*48像素的图片位于一个64*64像素的兼容OpenGL ES纹理中的情况。纹理的宽高都是2的N次方,而且正好容纳住我们加载进去的图片。这意味着图片的右边缘具有一个s值为0.875(即56÷64)的纹理坐标,上边缘具有一个t值为0.75(即48÷64)的纹理坐标。
图5.9


Texture2D类中的最后几行代码被用来设置maxS及maxT的值,它们分别对应最大的s轴坐标值及最大的t轴坐标值。同时我们也计算了纹理中每个坐标轴的比率。当我们想把图片中的一个像素位置转化为一个纹理坐标时,它是非常有用的。使用图片的像素值乘以相应的纹理坐标轴比率就得到了纹理坐标。清单5.9展示了这些在Texture2D类中的计算:

maxS = contentSize.width / (float)width;
maxT = contentSize.height / (float)height;
textureRatio.width = 1.0f / (float)width;
textureRatio.height = 1.0f / (float)height;

最后一步就是释放创建出来的绘图上下文及生成的位图数据了。如下:

CGContextRelease(context);
free(data);

这样我们就完成了Texture2D类的编写。现在我们转移到TextureManager类中去,它可以让我们在多个Image实例之间共享一个Texture2D实例。如果有多个图片需要相同的纹理时,这就可以帮助我们减少大量内存消耗。当你使用精灵表单时,这种情况通常也会出现。精灵表单是一个单独的纹理,它包含了许多小的图片。通过设置适当的纹理坐标,我们可以创建出来多个使用了相同纹理的Image类实例,但只需引用纹理上面的一小片区域。
  与使用多个Texture2D实例相比,TextureManager使得我们只需一个Texture2D实例就足以应付从精灵表单上取得的所有图片。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值