为什么要用压缩纹理

下面这张图是一辆陆虎越野车模型所用的纹理,原始分辨率为 1024 x 1024。

WebGL纹理详解——压缩纹理的使用 - 图1

浏览器从服务端加载这样一张图片时,其格式通常为 JPEG,文件尺寸只有 166KB,但是当 WebGL 处理一张纹理时就需要按照位图处理(这里所说的位图是指没有使用任何压缩算法的原始图片数据),如果图像中每个像素需要 RGB 三个通道,每个通道需要 8 位空间,那么整张图片就需要使用 1024 x 1024 x 8 x 3 位的信息,也就是 3M,这 3M 的信息都需要加载到 GPU 缓存当中,这和图片文件采用什么样的压缩格式没有任何关系。当调用 gl.texImage2D 方法时,浏览器内部就会将图片文件进行解压,转换成位图格式。如果图片包含透明信息,那么 RGBA 格式那么还要额外增加内存使用。

在简单了解 WebGL 处理纹理的过程后,会知道使用 JPEG 或者 PNG 文件作为纹理时会有这样的问题:

  1. 需要有图片解压过程,比较耗时。
  2. 因为纹理数据较大,所以传输纹理数据耗时较多。
  3. 纹理数据占用内存较多。通常是浏览器和 GPU 各自保存一份位图数据。

压缩纹理的出现就是来解决这些问题,经过某种算法压缩之后的纹理可直接被 GPU 使用,只有当 shader 进行纹理查询(texture lookup)才会进行解压操作,找到对应位置的像素颜色。GPU 通常会对解压过程进行优化从而提升性能。

浏览器的图片缓存策略

为了更好的了解 WebGL 处理纹理时如何分配和使用内存,请看下面的实验。

首先,页面加载前文提到的陆虎车的纹理,打开 Chrome 的任务管理器(Task Manager),把 Image Cache 和 GPU Memory 这两项勾选,查看图片缓存和 GPU 内存用量。你可以打开这个 demo 自行观察。

这时 Image Cache 显示:4262K,GPU Memory 显示:17.7M。

现在,我们注释掉加载纹理的语句,这时候图片不会加载,纹理也不会被绘制出来。

此时 Image Cache 显示:0K,GPU Memory 显示:13.7M。

如果加载图片,但是不创建纹理会如何?

结果是 Image Cache 显示:166K,GPU Memory 显示:13.7M。

通过以上观察可以推出以下结论。

  • 当仅下载图片时 Image Cache 以图片原始格式进行缓存,当创建纹理时,浏览器会解压成为位图格式,并将位图数据进行缓存。
  • GPU Memory 以位图格式保存纹理数据。
  • 通过 Chrome 的 Profile 工具可以看到在 JavaScript 层面,其内存消耗只包含 HTMLImageElement 对象所占用的内存大小。

以上结论可以用下面这张图来概括:

WebGL纹理详解——压缩纹理的使用 - 图2

当使用压缩纹理时,内存使用状况变为:

WebGL纹理详解——压缩纹理的使用 - 图3

压缩纹理的种类

不同的 GPU 厂商会有不同的纹理压缩格式,具体如下:

  • S3TC/DXTn/BCn:桌面计算机常见的压缩格式。名字虽然有不同叫法,但都是指同一种压缩方式。通常以 DDS 文件格式保存。
  • PVRTC/PVRTC2:iOS 设备上使用的压缩纹式。
  • ETC/ETC2:随着 OpenGL ES 2.0 出现。
  • ASTC:2012 年出现的一种新压缩格式。
  • ATC:Adreno GPU 支持的一种压缩格式,Android 手机上常用。

压缩纹理支持情况

WebGL1.0 里,压缩纹理是通过扩展支持的,因此要看当前浏览器支持哪些扩展。具体判断方法如下:

  1. var availableExtensions = gl.getSupportedExtensions();
  2. for (var i = 0; i < availableExtensions.length; i++) {
  3. if (availableExtensions[i].indexOf('texture') >= 0
  4. && availableExtensions[i].indexOf('compressed') >= 0) {
  5. // show in console
  6. console.log(availableExtensions[i]);
  7. }
  8. }

在我的 Macbook Pro Chrome 上,上面代码片段会输出如下结果:

WEBGL_compressed_texture_s3tc

也就是说,Macbook Pro Chrome 支持 S3TC 格式的压缩纹理。

下面表格列举了一些设备和浏览器对压缩纹理的支持情况:

压缩纹理工具

下面的工具可以将 JPEG、PNG 等常见的图片转换为压缩纹理。此外还有很多在线转换工具,这里就不一一列出了。

压缩纹理实战

下面我们来通过实例来看如何实现压缩纹理。由于兼容性,我们首先准备不同格式的压缩纹理(可以用上面提到的工具提前对纹理进行压缩)。

完整代码请见这里

下面来分析一下核心代码,初始化时首先判断当前浏览器支持什么格式的压缩纹理,判断方法如下:

  1. var availableExtensions = gl.getSupportedExtensions();
  2. for (var i = 0; i < availableExtensions.length; i++) {
  3. if (availableExtensions[i].indexOf('texture') >= 0
  4. && availableExtensions[i].indexOf('compressed') >= 0) {
  5. // show in console
  6. console.log(availableExtensions[i]);
  7. }
  8. }

我的 Chrome 浏览器运行后会打印如下信息:

formats 是一个数组,可以看到里面有四个数值:[33776, 33777, 33778, 33779]

遍历 extension 会打印如下信息:
COMPRESSED_RGB_S3TC_DXT1_EXT 33776 0x83f0
COMPRESSED_RGBA_S3TC_DXT1_EXT 33777 0x83f1
COMPRESSED_RGBA_S3TC_DXT3_EXT 33778 0x83f2
COMPRESSED_RGBA_S3TC_DXT5_EXT 33779 0x83f3

这些信息就是浏览器支持压缩纹理的具体格式,可以看到 formats 和 extension 中的信息是一样的。只不过一个是数组,一个是对象。可以根据这些信息提前准备好压缩格式。

在创建纹理的代码中,使用 gl.compressedTexImage2D 替换原有的 gl.texImage2D 方法:

  1. gl.compressedTexImage2D(gl.TEXTURE_2D, 0, type, width, height, 0, source);

这里 type 是前面通过 extension 获取到的那些常量,需要根据当前纹理的具体格式选择。source 就是压缩纹理的数据,这里需要注意的是不同压缩格式获取数据的方法都不一样,通常压缩纹理开头部分存放一些描述信息,之后才是真正的纹理数据。

在 Chrome 中使用压缩纹理后,打开 Task Manager 后可以看到 Image Cache 变为 0K,GPU Memory 也比之前有所下降。

压缩纹理的优势是节省内存开销,但是纹理文件的尺寸却比原来的 JPG 或者 PNG 要大,因此会影响加载的时间。那么项目当中是否要用压缩纹理就需要权衡了。如果你的 3D 场景同时使用的纹理较多或者其他因素导致内存消耗较大,同时需要长时间运行,那么你可以考虑使用压缩纹理,在场景初始化时需要预先加载必要的纹理,同时通过进度条指示状态,这种做法类似大型 3D 游戏的初始化。要注意控制纹理的分辨率,过大的纹理压缩之后仍然会上兆,这非常影响加载速度。

转载自:http://www.jiazhengblog.com/blog/2017/02/16/3076/