1、为什么要使用纹理集?
纹理集是将多张小图合成一张大图,使用纹理集有以下优点:
1、减少内存占用,减少磁盘占用;
2、减少磁盘读取次数,一次性读取一张大图比多次读取多张小图速度更快;
3、减少OpenGL绘制次数;
可以参照这里的讲解https://blog.csdn.net/tonny_guan/article/details/26232685
2、SpriteKit自带的纹理集工具SKTextureAtlas
SKTextureAtlas的用法比较简单,接口也很简单。将纹理图片放入文件夹,并将文件夹命名为xxx.atlas,然后导入工程,项目中就可以使用方法:
+ (instancetype)atlasNamed:(NSString *)name;
创建纹理集,之后使用方法
- (SKTexture *)textureNamed:(NSString *)name;
获取纹理集中的纹理。
3、TexturePacker
实际上大多数的游戏开发者都会用TexturePacker工具来打包纹理集,TexturePacker可以将一系列的小图整合成一张大的.png图片,外加一个描述文件,描述文件可以是.plist格式或者其他格式。描述文件的作用是描述每张小图在大图中的位置,旋转等信息。TexturePacker支持市面上流行的Unity,Cocos等游戏引擎,在导出的时候可以选择导出不同的游戏引擎。
遗憾的是SKTextureAtlas不支持TexturePacker导出的格式,而.atlas的文件夹的形式也不适合图片从服务器端加载的情况。因此我想自己写一个工具,支持读取TexturePacker导出的支持Cocos格式的.plist文件,如果需要支持其他引擎,可以分析相应的描述文件做相应的调整。
4、TexturePacker导出的Cocos格式的.plist描述文件分析
.plist文件是一个字典,主要分为两部分:
1、metadata:主要包含格式,png图片大小,png图片的名称等信息
2、frames:这一部分就是原来的各个小图片在合成到一张大图片中之后的控制信息,这是一个数组。每个数组的数据如下:
aliases:不懂,不重要
spriteOffset:TexturePacker在合图的时候可以选择修剪模式(自动裁减掉原图的透明部分),表示裁剪之后的图中心相对于原图中心的偏移量
spriteSize:合图之后的大小,这个值会小于或等于原图大小
spriteSourceSize:原图的大小
textureRect:这张图在整张大图里面的位置(要注意这个坐标是以左上角为原点的)
textureRotated:是否顺时针旋转90度,合图的时候可以勾选rotated,有的小图有可能会被旋转以最大限度利用空余部分
更详细的介绍可以参考https://www.codeandweb.com/blog/2016/01/29/cocos2d-plist-format-explained
5、WPTeatureAtlas和WPTexture
我写的WPTeatureAtlas主要负责解析.plist文件,存储解析出的纹理,提供便捷方法访问纹理
@interface WPTextureAtlas : NSObject
@property(nonatomic, strong, readonly) NSString *plistFile;
@property(nonatomic, strong, readonly) NSString *imageFile;
@property(nonatomic, strong, readonly) SKTexture *totalTexture;
@property(nonatomic, strong, readonly) NSMutableDictionary<NSString *, WPTexture *> *texturesDict;
@property(nonatomic, strong, readonly) NSArray<WPTexture *> *sortTexturesArr;
- (instancetype)initWithPlistFile:(NSString *)plistFile
imageFile:(NSString *)imageFile;
- (WPTexture *)textureWithName:(NSString *)name;
@end
WPTexture继承自SKTexture,并添加前面描述的.plist文件中的属性
@interface WPTexture : SKTexture
// 裁剪后图片的中心相对于裁剪前图片的中心偏移量
@property(nonatomic, assign, readonly) CGPoint spriteOffset;
// 裁剪之后的大小
@property(nonatomic, assign, readonly) CGSize spriteSize;
// 裁剪之前的大小
@property(nonatomic, assign, readonly) CGSize spriteSourceSize;
// 是否顺时针旋转90度
@property(nonatomic, assign, readonly) BOOL textureRotated;
+ (instancetype)textureWithDict:(NSDictionary *)dict
totalTexture:(SKTexture *)totalTexture;
@end
下面是解析代码,这里面主要用到了SKTexture的textureWithRect:inTexture:方法;这里对textureRect做了转换,因为在OpenGL里面纹理坐标是左下角为原点,宽和高都是1,textureWithRect:inTexture:方法里面的rect参数就是纹理坐标下面的rect。这个方法返回的texture就是一整张大图中的一部分,达到我们截取一部分纹理的目的。
+ (instancetype)textureWithDict:(NSDictionary *)dict
totalTexture:(SKTexture *)totalTexture
{
BOOL textureRotated = [dict[@"textureRotated"] boolValue];
CGRect rect = CGRectFromString(dict[@"textureRect"]);
// TexturePacker中获取的rect是原点在左上角点,但是SpriteKit、OpenGL默认加载的纹理坐标是原点在左下角点
CGFloat x, y, w, h;
if (textureRotated) { // 如果顺时针旋转了90度,那么width和heigth调换了
x = rect.origin.x / totalTexture.size.width;
y = (totalTexture.size.height - rect.origin.y - rect.size.width) / totalTexture.size.height;
w = rect.size.height / totalTexture.size.width;
h = rect.size.width / totalTexture.size.height;
} else {
x = rect.origin.x / totalTexture.size.width;
y = (totalTexture.size.height - rect.origin.y - rect.size.height) / totalTexture.size.height;
w = rect.size.width / totalTexture.size.width;
h = rect.size.height / totalTexture.size.height;
}
WPTexture *texture = [WPTexture textureWithRect:CGRectMake(x, y, w, h) inTexture:totalTexture];
texture.spriteOffset = CGPointFromString(dict[@"spriteOffset"]);
texture.spriteSize = CGSizeFromString(dict[@"spriteSize"]);
texture.spriteSourceSize = CGSizeFromString(dict[@"spriteSourceSize"]);
texture.textureRotated = textureRotated;
return texture;
}
6、支持纹理的裁剪和旋转
TexturePacker在合成图片的时候是支持裁剪和旋转的,裁剪可以裁剪掉透明像素,而旋转则可以更大限度利用空间。但是前面的SKTexture创建的纹理仅仅是截取大图中的一部分,如果这个图被裁剪,或者旋转了,那么我们直接在SKSpriteNode中使用SKTexture会导致贴图的位置或方向不正确,这个时候我们需要对SKSpriteNode做几何变换才能达到正确的效果。我写了个WPSpriteNode来处理这种情况:
@interface WPSpriteNode : SKSpriteNode
+ (instancetype)spriteNodeWithWPTexture:(WPTexture *)texture;
- (instancetype)initWithWPTexture:(WPTexture *)texture;
@end
@implementation WPSpriteNode
+ (instancetype)spriteNodeWithWPTexture:(WPTexture *)texture
{
return [[WPSpriteNode alloc] initWithWPTexture:texture];
}
- (instancetype)initWithWPTexture:(WPTexture *)texture
{
if (self = [super initWithTexture:texture]) {
if (texture.textureRotated) {
// 顺时针转了90度之后,原来的Y转成与X轴同向,而原来的X变成与-Y同向
self.anchorPoint = CGPointMake(0.5 - texture.spriteOffset.y / texture.spriteSize.height, 0.5 + texture.spriteOffset.x / texture.spriteSize.width);
// 逆时针转回90度
self.zRotation = M_PI_2;
} else {
// 根据偏移量将裁剪后的图反向移回原图的中心
self.anchorPoint = CGPointMake(0.5 - texture.spriteOffset.x / texture.spriteSize.width, 0.5 - texture.spriteOffset.y / texture.spriteSize.height);
}
}
return self;
}
@end