分享针对iOS设备执行纹理加载过程的方法

这能解决哪些问题?

由于内存的限制,我们在创造游戏玩法的过程中总是需要整合纹理内容。可能是出于复杂的3d世界的约束,开发者必须将对象顺畅地整合进关卡中,或者在我们自己的案例中,为了使用2d渲染,我们就必须整合进更多纹理(我们预先渲染了游戏世界,对象和角色动画的纹理集,并将其映射到布告牌上以获得更高质量的视觉效果,灯光以及更复杂的环境/角色)。

如果你的问题是以毫秒的速度在3d世界中加载纹理,你便可以使用压缩PVR纹理去解决这一问题。除此之外关于这一方法还存在其它更棒的性能优势。

如果你和我一样是为了呈现布告牌纹理去加载这些内容,你便可以基于无损耗模式,如PNG(最普遍的一种),BMP,TGA或未压缩的PVR去加载它们。基于本篇文章所提及的技术,你便能够通过改变你所使用的格式或加载方式而获得大量的性能优势。

步骤1:文件格式

如果你打算在一个3d世界中使用对象纹理,你便可以选择PVRTC格式,因为这是一种有损耗且固定的纹理压缩格式,同时支持4bpp和2bpp的ARGB数据。

而对于无损耗纹理,你的最佳选择则是未压缩的PVR文件格式。这一格式包含了PVR文件标题以及基于RGBA4444,RGBA8888或者BGRA8888等格式的数据。如果你使用的是32bit数据,你就没有理由使用RGBA8888格式,因为驱动程序将处理其中的cpu内容,这时候你可以预先将数据转变成BGRA8888格式,从而将其直接上传至内存中。

Apple_A6(from macdailynews)

Apple_A6(from macdailynews)

注释

–如果你使用RGB8数据格式去处理CPU,那便等于你在执行一些无用功,因为你必须为此添加一个填充字节,所以你必须确保始终使用RGBA/BGRA格式,即使你并不需要使用这一渠道。

–使用未压缩纹理格式意味着将占据更多磁盘空间。虽然这不会影响你的应用下载规格(IPA/APK则会压缩你的内容),但是在安装时却会占用更多的磁盘空间。如果你发现磁盘空间的使用已经超出了自己的预想,你便可以将纹理整合进.zip格式的程序包中,在初始加载时压缩你所需要的内容,并在应用终止时删除这些内容。

步骤2:文件I/O

在我阅读过的大多数网上文章或者别人所编写的代码,我发现他们都遵循着相同的纹理数据加载过程,即打开一份文件,阅读数据,根据需要压缩数据,然后调用glTexImage2d(…) 并明确RGBA格式。如果你是预先加载所有纹理数据,那么这种方法便非常有效,但是当你需要在游戏运行过程中整合纹理内容,你便会遇到一些严重的瓶颈。同时你还需要想办法避免一些不必要的分配/复制(基于格式可能会出现压缩)操作——可能会影响你在建造框架时的时间分配。

一种方法便是使用内存映射文件I/O。也就意味着文件内容并不是从磁盘中读取,并且也未使用物理内存,而是在内存空间经由OS进行缓存,并在需要的时候置入或置出。这可能会造成文件访问的延迟,假设你的内核加载页面所需要的平均时间为0.012毫秒(我甚至看过最长时间为0.135毫秒),但是如果它减少了内存分配/内存块拷贝(将占用内存页面加载时间),你便能因此获得较高的性能(更别说当你使用平台分配内存/自由调用时无需再担心内存碎片问题)。

为了做到这一点你可以遵循以下内容(为了简化例子我省略了错误处理):

#include < sys/stat.h >

#include < sys/mman.h >
#include < fcntl.h >
#include < unistd.h >

int32_t file = open(“my_texture_file.pvr”, O_RDONLY);
struct stat file_status;
fstat(file, &file_status);
int32_t file_size = (int32_t)file_status.st_size;
void* data = mmap(0, file_size, PROT_READ, MAP_PRIVATE, file, 0);
// Note this will not close the file/mapping right now, as it will be held until unmapped.
close(file);

// When finished with this data you call this to unmap/close.
munmap(data, file_size);

现在我们已经创造一种虚拟地图,并承诺只会在只读访问时使用这一地图,从而帮助我们获得了最优化的利益。

注释:

大多数情况下,内存映射文件只有在面对拥有比页面大小多出几倍规格(如4096字节)的大型文件时才能起作用,这也是为了避免浪费页面空间。显然很多情况下纹理都未能遵守这一规定,但是在我们的案例中这却不会造成多大影响,我们也总是能够获得最佳性能。

步骤3:纹理加载

首先你需要从文件中获得PVR标题结构(游戏邦注:包含了所有你所需要的信息)。如此你便可以使用4CC去核实格式,并获得所需的元数据。当你获得这一数据后你便可以将纹理数据加载到GPU以供使用。

以下方法能够帮助你轻松地做到这一点(为了简化例子我省略了错误处理,并在常量上做了假设等)

struct PvrHeader
{

uint32_t    header_length;
uint32_t    height;
uint32_t    width;
uint32_t    mipmap_count;
uint32_t    flags;
uint32_t    data_length;
uint32_t    bpp;
uint32_t    bitmask_red;
uint32_t    bitmask_green;
uint32_t    bitmask_blue;
uint32_t    bitmask_alpha;
uint32_t    pvr_tag;
uint32_t    surface_count;
};

static const uint32_t kPVRTC2 = 24;
static const uint32_t kPVRTC4 = 25;
static const uint32_t kBGRA8888 = 26;

PvrHeader* header = (PvrHeader*)data;    // data being your mapped file from step two
uint32_t pvr_tag = header->_pvr_tag;
// Here you would check the pvr_tag against the 4CC “PVR!” to verify

uint32_t flags = header->flags;
uint32_t format_flag = flags & 0xFF;

void* data_start = data + sizeof(PvrHeader);
if(format_flags == kBGRA8888)
{
// Note: I am assuming that you have already generated, bound, and set texture parameters.
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, header->width, header->height, 0, GL_BGRA, GL_UNSIGNED_BYTE, data_start);
}
else if(format_flags == kPVRTC4 || format_flags == kPVRTC2)
{
// You would do the same as above but using glCompressedTexImage2d(…);
}

步骤4:有效利用

这时候你便能够根据自己的需要使用最有效的纹理格式,你拥有映射文件,你可以使用内核去加载页面并进行复制而将数据上传到GPU。但是你应该不希望每次加载纹理时都重复这几个步骤。为了达到高效利用,你需要在最初加载游戏时映射所有纹理文件,而以此创建一个映射纹理文件缓存(游戏邦注:John Carmack发现在iOS平台上开发者只拥有700 MB的空间,如果你需要更多空间,就要使用mmap/munmap更谨慎地管理缓存)。当一个纹理需要你调用glTexImage2d并使用内核去加载页面,并基于原生格式将数据上传到GPU时,你就应该为其做好准备。当运行终止时,你可以通过取消所有文件的映射而摧毁缓存。

接下来呢?

根据你所整合的纹理数量以及它们的规格,你可以通过遵循上述解决方法而获得最佳性能,尽管使用iOS 6有可能帮助你获得更出色的性能。而对于那些愿意执行这些步骤的人来说,我敢保证他们将无需经历平常的绑定过程便能够有效地加载纹理数据。这种方法能够推动驱动程序发挥最佳功效,从而确保内存的有效管理,并避免内存分配或存储残片的出现。你可以创建更有效率的缓存系统并预先完成GPU的内存分配,而无需重复劳作。

via:游戏邦/gamerboom.com

感谢支持199IT
我们致力为中国互联网研究和咨询及IT行业数据专业人员和决策者提供一个数据共享平台。

要继续访问我们的网站,只需关闭您的广告拦截器并刷新页面。
滚动到顶部