在Cocos Creator中使用 Basis Universal 纹理压缩 (超级压缩)

在Cocos Creator中使用 Basis Universal 纹理压缩 (超级压缩)

前言

cocos creator 项目中的纹理压缩中,通常需要根据设备类型选择不同的压缩格式。比如 web 项目,设置纹理压缩格式就需要设置pvr、etc等不同的压缩格式,这里带给我们的问题就是多种压缩格式配置下构建时间明显增长以及资源包体变大,可能一个项目构建下来都大几十分钟甚至一个小时,这显然不是我们想要的。

所以就想有没有一种可以支持所有设备的压缩格式?

关于Basis Universal

Basis Universal 是一个开源的 GPU 纹理压缩库,它提供了一种高效的纹理压缩方法,能够在保持高质量图像的同时,显著减小文件大小。

Basis Universal 的特点包括:

  • 跨平台支持:支持多种平台和设备,包括 PC、游戏机和移动设备。

  • 多种压缩模式:提供了不同的压缩级别,以适应不同的图像质量和性能需求。

  • 透明支持:能够处理包含透明信息的图像。

  • 实时转码:可以在运行时将 Basis 压缩的纹理解码为 GPU 可理解的格式。

  • 开源:允许开发者自由使用和修改,以适应特定的项目需求。

看到这里【跨平台支持】的特点,这不就是我要找的吗。

实现

实现思路分以下步骤:

  1. 正常构建项目:项目中不设置纹理压缩格式

  2. 将构建后的资源压缩成.basis文件:可用 Node.js 或其他工具批量操作

  3. 实现.basis文件的加载和解析

压缩.basis文件

使用 Basis Universal提供工具 basisu 对图片资源进行压缩。

basisu -file xxx.png

当然,这是最简单的压缩命令,压缩不是压得越小越好,需要根据实际项目的表现去平衡压缩的质量,更多压缩参数参见 Basisu.exe命令行工具选项

加载 .basis

新建脚本BasisDownloader.js,并将脚本勾选为插件。也可以在启动流程中自行加载


    //BasisDownloader.js

    //...

    //cc 原始的加载方法

    var cccImageDownloader = function(url, options, onComplete){

        if(cc.sys.capabilities.imageBitmap && cc.macro.ALLOW_IMAGE_BITMAP){

            options.responseType = "blob";

            cc.assetManager.downloader.downloadFile(url, options, options.onFileProgress, onComplete);

        }else{

            cc.assetManager.downloader.downloadDomImage(url, options, onComplete);

        }

    }

    var downloadImage = function (url, options, onComplete) {

        if (window['CC_PREVIEW']) {

            cccImageDownloader(url, options, onComplete);

            return

        }

        let newFile = cc.path.changeExtname(url, '.basis');//替换文件后缀

        options.responseType = 'arraybuffer';

        //加载.basis文件

        cc.assetManager.downloader.downloadFile(newFile, options, (error, data) => {

            if (error) {

                //加载basis失败则加载原图

                cccImageDownloader(url, options, onComplete);

            } else {

                //解析.basis

                parseBasis(data, onComplete);

            }

        });

    }

    cc.assetManager.downloader.register({

        '.png': downloadImage,

        '.jpg': downloadImage,

    });

解析 .basis

下载basis 转码器 ,将basis_transcoder.jsbasis_transcoder.wasm放到同一文件夹下,并确保wasm模块在 BasisDownloader.js 脚本之前实例化完成,否则可能导致.basis加载成功了却无法解析。

parseBasis的实现:代码太长,这里就不贴全代码了。主要参考https://github.com/BinomialLLC/basis_universal/blob/master/webgl/texture/index.html


 var parseBasis = function(data,onComplete){

    //解析basis参考 https://github.com/BinomialLLC/basis_universal/blob/master/webgl/texture/index.html

    //...

    //输出

    let err = null,out = null;

    out = {

        _data: dst,//解析出来的数据

        _compressed: true,//是否压缩

        width: alignedWidth,

        height: alignedHeight,

        format: formatFromBasis//从basis中解析出的格式

    }

    onComplete && onComplete(err, out);

 }

有几点需要注意的地方:

  1. 因为是压缩纹理,所以从basis解析出来的数据需要让 gl 去调用 compressedTexImage2D 方法,所以可以重写方法去实现自己想要的逻辑。

    Object.defineProperty(cc.Texture2D.prototype, '_nativeAsset', {

        set: function (data) {

            if (data._compressed && data._data) {

                // 将 //从basis中解析出的格式 传入

                this.initWithData(data._data, data.format || this._format, datawidth, data.height);

            }

            else {

                this.initWithElement(data);

            }

        },

        configurable: true,

        enumerable: true

    });

    cc.renderer.Texture2D.prototype._setMipmap = function (images, flipY, premultiplyAlpha) {

        let glFmt = glTextureFmt(this._format);

        if (this._format == '从basis中解析出的格式') {

            //这里将image的数据作为压缩格式,那么在调用 _setImage 的时候 gl 就会调用 compressedTexImage2D  

            this._compressed = true;

        }

        //...

    }

    // 这里 需要对_textureFmtGL添加 【从basis中解析出的格式 】相关的数据

    //如: _textureFmtGL[70] = { format: 70, internalFormat: 36492, pixelType: 5121 };

    const glTextureFmt = function (fmt) {

        let result = _textureFmtGL[fmt];

        if (result === undefined) {

            return _textureFmtGL[enums.TEXTURE_FMT_RGBA8];

            }

        return result;

    }

  1. 解析的时候需要先判断当前设备支持的压缩格式,代码如下:

    var gl = cc.game.canvas.getContext('webgl');

    astcSupported = !!gl.getExtension('WEBGL_compressed_texture_astc');

    etcSupported = !!gl.getExtension('WEBGL_compressed_texture_etc1');

    dxtSupported = !!gl.getExtension('WEBGL_compressed_texture_s3tc');

    pvrtcSupported = !!(gl.getExtension('WEBGL_compressed_texture_pvrtc')) || !!(gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'));

    bc7Supported = !!gl.getExtension('EXT_texture_compression_bptc');

这里需要注意的是需要在 cc.game.EVENT_GAME_INITED 之后再调用,不然会出现canvas颜色不对。也可以自己创建一个canvas标签去判断当前支持哪种格式。

结语

这是一次关于纹理压缩的尝试,项目工程基于2.4.9版本,构建发布为web-mobile版本,实际项目运行效果有待验证。另外因为条件有限,测试并未覆盖全平台,只测试了pc webAndroid Web,欢迎各位大神一起交流验证。

cocos_basis_universal_loader_demo

资料参考:

关于BasisU

纹理解码

感谢 @550939351 大佬提供的 cocos_ktx2_demo

6赞

这个前些日子我也看到了,感谢楼主搞出了 Cocos 解决方案。不过这个格式有个坏处,为了一些低端设备,反而让其他设备也要跟着做一次解码,等 astc 一统江湖的一天 :grinning:

1赞

确实存在这个问题,所有设备都需要解码。我想的是解码时间能不能从加载时间挤出来,毕竟从文件大小来说还是挺占优势的

是,还想问问楼主,basis这个方案的实时解码的设备兼容性还有运行时长是否较长呢?

兼容性的话我自己测的机型不多,目前没遇到不兼容的。
时长的话相对不需要解码的设备肯定是会更长时间的,毕竟解码需要时间。这个就得根据实际项目自己衡量了。毕竟咱不是策划,不能既要又要对吧:joy:

了解 :ok_woman: