使用压缩纹理

无法减小显存占用

先看一下下面这张纹理图,前端习惯叫做雪碧图,在游戏界一般叫做 Atlas 图集,原始分辨率为 512*512,文件大小为 53KB。

一般情况下,浏览器内部会将 JPEG/PNG 图片文件进行解压,并解码成显卡认识的 RGB(A) 位图格式纹理,如果带 alpha 通道,图片就需要占用内存存储 512*512*4,即 1M,同时这 1M 的信息也需要加载到 GPU 缓存中(虽然文件格式、磁盘占用、内存占用大小不一样,即无论将图片的存储体积压缩到多么小,其显存开销总是固定的),即:一张图片在引擎中运行时占用的显存大小,与图片本身占用磁盘大小无关,只和图片的宽和高相关。 所以,类似 JPEG/PNG 文件作为纹理时会有以下问题:

  • 浏览器解压图片存在耗时,图片越大,耗时越多
  • 内存占用较多,除了原始文件的存储,浏览器和 GPU 还会各自保存一份位图数据(如图示1)

图示1:img 对象创建纹理的内存使用情况

可以看出,第二个问题如果在大体量图片应用场景下,这个瓶颈会越来越明显,在移动设备上很容易造成 OOM。

压缩纹理(Compressed Textures)

压缩纹理是一种游戏领域常用的纹理压缩技术,其依赖于特定硬件实现,经过某种算法压缩之后的纹理可直接以固定速率交由 GPU 即时解压使用,只有当 shader 进行纹理查询(texture lookup)才会进行解压操作,找到对应位置的像素颜色。GPU 通常会对解压过程进行优化从而提升性能。

那么,它在内存占用上有质的优势,详细如下:

  • 内存占用很少,除了原始文件的存储,只有 GPU 保存的一份压缩纹理数据(如图示2)
  • 省去了使用 JPEG/PNG 图片文件作为纹理时的图片解码耗时
  • 可以精准控制内存,如图示2中 JSHeap 中的占用可以手动控制释放

图示2:压缩纹理的内存使用情况

有优点肯定也有条件和代价,也可以说是规范or约束:

  • 压缩纹理是有损压缩,会对图片的质量有一定减损
  • 压缩纹理的传输体积可能比 JPG/PNG 要高1~4倍
  • 压缩纹理要求POT,即长宽都是二的幂次,同时 PVRTC 也有长宽相等的要求
  • 压缩纹理支持格式在不同平台不一,加上降级需要三份资源

整体来说,这些约束对于大体量图片项目影响不大,收益却相当可观。

如何使用?

制作 POT 等宽 Atlas 图集

鉴于业界优秀软件的合并效果更好,建议使用相关软件/工具制作,比如:TexturePacker,这里推荐 ShoeBox(好用、免费)。

以下是通过 ShoeBox 生成的图集资源:

  • png: https://gw.alipayobjects.com/os/tiny/resources/1.0.5/compressedtexture/hao.png
  • json: https://gw.alipayobjects.com/os/tiny/resources/1.0.5/compressedtexture/hao.json

生成压缩纹理

通过上面制作生成的 .png.json,你可以使用 tinyjs-cli 来快速生成压缩纹理。

$ tiny texture-compressed res/hao.png

通过以上命令,会在 res 目录下生成以下格式压缩纹理:

  • astc 格式: https://gw.alipayobjects.com/os/tiny/resources/1.0.5/compressedtexture/hao.astc.ktx
  • pvr 格式: https://gw.alipayobjects.com/os/tiny/resources/1.0.5/compressedtexture/hao.pvr.ktx

Tips

  • 生成 KTX 纹理的功能,tinyjs-cli 版本需要 >=1.3.0,详细命令请移步:生成压缩纹理

使用压缩纹理

const app = new Tiny.Application({...});
const loader = new Tiny.loaders.Loader();

// 初始化压缩纹理插件
Tiny.plugins.compressedTexture.init(app.renderer);
// 加载 Atlas 图集
loader.add('./res/hao.json', {
  metadata: { useCompressedTexture: true }
});
loader.load((loaderInstance, resources) => {
  const textures = [];
  for (let i = 0; i < 4; i++) {
    // 通过 Texture 的 fromFrame 方法创建纹理。frame 名就是 tileset 资源文件里的 frameId
    textures.push(Tiny.Texture.fromFrame('hao' + i + '.png'));
  }
});

Tips

  • 压缩纹理只有 WebGL 模式下才有效,如果当前环境不支持 WebGL 会自动降级到同目录下的 .png
  • TinyJS 压缩纹理插件 Tiny.plugins.compressedTexture 会自动识别当前环境支持的压缩纹理格式并在加载时使用,如安卓上会加载 .astc.ktx,iOS 上会加载 .pvr.ktx,不支持的会加载 .png
  • 在创建并使用 Tiny.Texture 时,和使用普通的 tileset 一样