一、纹理压缩的核心逻辑:为什么要做纹理压缩

很多开发者在做Three.js项目时都会遇到一个头疼的问题:模型加载特别慢,尤其是带纹理的模型,打开页面要等好几秒,用户刷一下就走了。这背后的核心原因是未压缩的纹理文件太大——比如一张4096×4096的普通JPG纹理,体积可能有几十MB,要是一个场景里有十几张这样的纹理,总体积直接上百MB,加载速度自然快不起来。

但纹理压缩不是随便把文件压小就行,要是压得太狠,纹理会变模糊、出现色块,模型看起来特别廉价,反而影响体验。所以纹理压缩的核心逻辑就是在「纹理质量」和「加载速度」之间找一个平衡点,既要让文件足够小,加载快,又要保证纹理看起来足够清晰,满足项目的视觉要求。

1.1 纹理压缩的本质:重新编码像素数据

我们平时用的JPG、PNG这些图片格式,是为了电脑、手机等通用设备设计的,而Three.js里的纹理是给GPU(显卡)用的,GPU有自己的「专属语言」。纹理压缩的本质就是把通用格式的像素数据,转换成GPU能直接识别、读取的专属压缩格式,这样不仅能减小文件体积,还能让GPU加载纹理的速度更快。

举个例子:一张4096×4096的PNG纹理,未压缩的原始像素数据是64MB(每个像素占4字节,4096×4096×4=67108864字节≈64MB),用普通压缩格式压成JPG可能有10MB,而用GPU专属的压缩格式压完可能只有1MB左右,体积缩小了几十倍,加载速度自然能提上来。

二、主流纹理压缩格式对比:选对格式才是关键

目前Three.js支持的主流纹理压缩格式有好几种,每种都有自己的特点,适合不同的场景,下面我们就详细对比一下这些格式。

2.1 ETC系列:跨平台通用的基础选择

ETC(Ericsson Texture Compression)是最通用的纹理压缩格式之一,支持几乎所有的现代GPU,包括电脑、手机、平板等各种设备,兼容性拉满。

ETC又分ETC1和ETC2两个版本:ETC1是老版本,只能压缩RGB通道的纹理(也就是没有透明通道的纹理),不支持透明;ETC2是ETC1的升级版本,支持RGBA通道(带透明的纹理),还能压缩灰度图,功能更全。

ETC格式的优缺点很明显:优点是兼容性好,压缩比适中,压缩后的纹理质量也不错;缺点是压缩比不是最高的,比PVRTC、ASTC这些格式的压缩比低一些。

2.2 PVRTC系列:苹果生态的专属优化

PVRTC(PowerVR Texture Compression)是苹果公司主导开发的压缩格式,专门针对苹果的GPU做了优化,在苹果设备上的性能和表现都非常好,压缩比也比ETC高一些。

PVRTC也分PVRTC1和PVRTC2:PVRTC1支持2位和4位的压缩(位宽越小,压缩比越高,质量越差),PVRTC2是升级版本,支持更多的位宽和格式,质量更好。

PVRTC的优缺点:优点是在苹果设备上表现优异,压缩比高;缺点是兼容性差,只有苹果设备和部分安卓设备支持,非苹果设备上加载会出问题。

2.3 ASTC系列:高质量高压缩比的全能选手

ASTC(Adaptive Scalable Texture Compression)是目前最先进的纹理压缩格式之一,由ARM、AMD、NVIDIA等多家公司联合开发,支持几乎所有的现代GPU,兼容性和PVRTC一样好?不,ASTC的兼容性比PVRTC好,比ETC略差一点,但是压缩比和质量都非常高。

ASTC的最大特点是可以自定义压缩参数,比如可以设置块大小(从4×4到12×12),块越大,压缩比越高,质量越差,开发者可以根据自己的需求灵活调整,非常适合需要同时兼顾质量和压缩比的场景。

ASTC的优缺点:优点是压缩比高,质量好,支持自定义参数;缺点是压缩速度比较慢,压缩工具的配置也稍微复杂一点。

2.4 Basis Universal:跨格式的中间转换格式

Basis Universal是一种非常特殊的压缩格式,它不是直接给GPU用的,而是一种中间格式。简单来说,你可以把所有的原始纹理(PNG、JPG等)都转换成Basis Universal格式,然后在Three.js里加载的时候,根据用户的设备类型,自动转换成设备支持的最优压缩格式(比如在苹果设备上转成PVRTC,在安卓设备上转成ETC或ASTC)。

Basis Universal的优缺点:优点是兼容性极强,只需要维护一份纹理文件,就能适配所有设备,大大降低了开发者的维护成本;缺点是转换过程会稍微增加一点加载时间,压缩比也比原生的ASTC略低一点。

三、Three.js纹理压缩的实现步骤:从准备到加载

下面我们就用一个完整的示例,来演示如何在Three.js中使用纹理压缩,我们选择Basis Universal格式,因为它的兼容性最好,最适合大多数项目。

首先明确技术栈:Three.js + Basis Universal(压缩工具)

3.1 第一步:准备原始纹理

首先我们需要准备原始的纹理文件,比如一张4096×4096的PNG纹理,命名为model_diffuse.png,这张纹理是带透明通道的,用来给模型做漫反射纹理。

3.2 第二步:用Basis Universal压缩纹理

接下来我们需要用Basis Universal的压缩工具,把原始的PNG纹理转换成Basis Universal格式的纹理。Basis Universal提供了命令行工具,我们可以通过命令行来压缩纹理。

首先我们需要下载Basis Universal的命令行工具,地址是:https://github.com/BinomialLLC/basis_universal/releases,下载对应系统的版本后,解压到本地。

然后我们打开命令行,进入到Basis Universal工具的目录,执行下面的命令来压缩纹理:

# 压缩PNG纹理为Basis Universal格式,支持RGBA通道,压缩级别为中等(level=2)
./basisu -level 2 -rgba -output model_diffuse.basis model_diffuse.png

命令参数说明:

  • -level 2:压缩级别,范围是0到9,0是质量最高(压缩比最低),9是质量最低(压缩比最高),我们选2是兼顾质量和压缩比的中间值。
  • -rgba:表示原始纹理是带透明通道的RGBA格式。
  • -output model_diffuse.basis:指定压缩后的输出文件名为model_diffuse.basis。
  • model_diffuse.png:原始的输入纹理文件。

执行完这个命令后,我们就会得到一个压缩后的model_diffuse.basis文件,这个文件的体积比原始的PNG文件小很多。

3.3 第三步:在Three.js中加载压缩后的纹理

接下来我们需要在Three.js中加载这个Basis Universal格式的纹理,并应用到模型上。首先我们需要引入Three.js的Basis纹理加载器,代码如下:

// 引入Three.js的基础库
import * as THREE from 'three';
// 引入Basis纹理加载器
import { BasisTextureLoader } from 'three/addons/loaders/BasisTextureLoader.js';

// 初始化Three.js场景、相机、渲染器(基础配置)
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 初始化Basis纹理加载器
const basisLoader = new BasisTextureLoader();
// 设置Basis Universal解码器的路径(需要把Three.js的解码器文件放到项目的静态资源目录)
basisLoader.setTranscoderPath('three/addons/libs/basis/');
// 告诉加载器,根据设备的GPU类型,自动转成最优的压缩格式
basisLoader.detectSupport(renderer);

// 加载压缩后的Basis纹理
basisLoader.load(
  'model_diffuse.basis', // 压缩后的纹理路径
  (texture) => { // 加载成功的回调函数
    // 配置纹理的属性,和普通的Three.js纹理配置一样
    texture.encoding = THREE.sRGBEncoding; // 伽马编码,保证颜色显示正确
    texture.minFilter = THREE.LinearMipmapLinearFilter; // 纹理缩小过滤方式
    texture.magFilter = THREE.LinearFilter; // 纹理放大过滤方式

    // 创建一个几何体(比如一个立方体)
    const geometry = new THREE.BoxGeometry(2, 2, 2);
    // 创建材质,使用加载好的压缩纹理
    const material = new THREE.MeshStandardMaterial({
      map: texture, // 漫反射纹理
      metalness: 0.5, // 金属度
      roughness: 0.5 // 粗糙度
    });

    // 创建网格并添加到场景中
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    // 渲染场景
    function animate() {
      requestAnimationFrame(animate);
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;
      renderer.render(scene, camera);
    }
    animate();
  },
  (progress) => { // 加载进度的回调函数
    console.log('纹理加载进度:', (progress.loaded / progress.total) * 100 + '%');
  },
  (error) => { // 加载失败的回调函数
    console.error('纹理加载失败:', error);
  }
);

// 调整相机位置
camera.position.z = 5;

代码说明:

  • 我们用了BasisTextureLoader来加载压缩后的纹理,这个加载器会自动根据用户的设备类型,把Basis格式转成设备支持的最优压缩格式,比如在苹果设备上转成PVRTC,在安卓设备上转成ETC或ASTC。
  • 纹理的配置(比如encoding、minFilter、magFilter)和普通的Three.js纹理配置完全一样,不需要额外修改。

四、纹理压缩策略的选择原则:适配你的项目

不同的项目有不同的需求,所以纹理压缩策略也不一样,下面我们就来说说如何选择适合自己项目的压缩策略。

4.1 按项目类型选择

  • 如果是通用的Web项目(比如官网、在线展厅等),需要兼容所有设备,优先选择Basis Universal格式,兼容性最好,维护成本最低。
  • 如果是专门针对苹果生态的项目(比如苹果商店的Web应用),优先选择PVRTC格式,在苹果设备上的表现最好。
  • 如果是需要高质量纹理的项目(比如3D建模展示、虚拟仿真等),优先选择ASTC格式,压缩比和质量都非常高。
  • 如果是对兼容性要求极高的老项目(需要支持非常旧的设备),可以选择ETC格式,兼容性最好。

4.2 按纹理类型选择

  • 漫反射纹理(带颜色的纹理):可以选择任何一种压缩格式,优先选择压缩比高的格式。
  • 透明纹理(比如带Alpha通道的纹理):不能选择ETC1格式,只能选择ETC2、PVRTC、ASTC、Basis Universal格式。
  • 法线纹理、高度纹理(不带颜色的纹理):可以选择任何一种压缩格式,优先选择质量好的格式。

4.3 按目标设备选择

  • 针对电脑设备:可以选择ASTC格式,压缩比和质量都非常高。
  • 针对安卓设备:可以选择ETC2或ASTC格式,兼容性和质量都不错。
  • 针对苹果设备:可以选择PVRTC或ASTC格式,表现都很好。

五、纹理压缩的注意事项:避免踩坑

在使用纹理压缩的时候,有几个常见的坑需要注意,下面我们就来说说。

5.1 纹理尺寸必须是2的幂次方

Three.js中的纹理尺寸(宽度和高度)必须是2的幂次方(比如256、512、1024、2048、4096等),否则纹理无法正常显示,或者会出现拉伸、模糊等问题。所以在准备原始纹理的时候,一定要保证纹理的尺寸是2的幂次方。

5.2 压缩级别不要选太高

压缩级别越高,纹理的质量越差,所以不要为了追求小体积而把压缩级别选得太高,一般选2到4之间就可以了,这样既能保证压缩比,又能保证纹理质量。

5.3 解码器路径要正确

在使用BasisTextureLoader的时候,一定要把解码器的路径设置正确,否则加载器无法找到解码器,纹理就无法加载。解码器文件可以从Three.js的官方仓库里获取,地址是:https://github.com/mrdoob/three.js/tree/dev/examples/jsm/libs/basis。

5.4 测试不同设备的表现

不同的设备对不同的压缩格式支持不一样,所以在项目上线前,一定要在不同的设备上测试纹理的表现,比如在苹果手机、安卓手机、电脑上分别测试,确保纹理在所有设备上都能正常显示,并且质量符合要求。

六、应用场景分析:什么时候用纹理压缩

纹理压缩适合所有需要加载纹理的Three.js项目,尤其是以下几种场景:

  • 大型3D项目:比如在线游戏、虚拟展厅、数字孪生等,这类项目的纹理数量多、体积大,压缩后能大幅提升加载速度。
  • 移动端项目:手机的网络环境比电脑差,压缩后的纹理体积小,加载速度快,能提升移动端的用户体验。
  • 带宽有限的项目:比如在带宽有限的网络环境下(比如地铁、偏远地区),压缩后的纹理能更快加载。

七、技术优缺点总结

7.1 优点

  • 大幅减小纹理体积:压缩后的纹理体积比原始纹理小几倍到几十倍,加载速度大幅提升。
  • 提升GPU性能:压缩后的纹理是GPU专属格式,GPU读取和处理纹理的速度更快,能提升项目的帧率。
  • 降低带宽成本:对于有大量用户的项目,压缩后的纹理能大幅降低服务器的带宽成本。

7.2 缺点

  • 压缩过程需要额外的步骤:需要把原始纹理转换成压缩格式,增加了项目的开发流程。
  • 压缩会损失一定的质量:压缩后的纹理质量比原始纹理略差,需要在质量和压缩比之间找平衡。
  • 兼容性问题:不同的压缩格式兼容性不一样,需要根据项目的需求选择合适的格式。

八、文章总结

纹理压缩是提升Three.js项目加载速度和性能的重要手段,核心是在纹理质量和加载速度之间找一个平衡点。选择压缩格式的时候,需要根据项目类型、纹理类型、目标设备等因素综合考虑,优先选择兼容性好、压缩比高、质量好的格式。

在实现纹理压缩的时候,一定要注意纹理尺寸、压缩级别、解码器路径等细节,避免踩坑。最后,一定要在不同的设备上测试纹理的表现,确保项目的体验符合要求。