creator2.4.11项目资源进行批量纹理压缩实践

creator2.4.11项目资源进行批量纹理压缩实践

浅谈纹理压缩

​ 在游戏开发中对资源的处理是非常重要的,而资源最多的一般是图片,所以针对图片的处理会有很多方案,总体的原则就是保证图片质量的前提下,尽量减少其占用磁盘的大小和内存大小。前者可以使我们的包体变小(如果是web端则可以减少网络传输数据量,从而节省流量),后者可以减少游戏运行时对内存的占用,以及提高cpu向gpu传输数据的效率。
​ 我们对于图片在磁盘中存储优化,一般会采用图片压缩的方式,比如进行PNG、JPEG等等。但是这种处理不能对内存进行优化,等到图片加载到内存的时候会转换为原始大小,这个大小往往比在磁盘上大很多,一张图在内存中占用的大小的计算公式大概如下:
​ 内存中图片大小 = 图片像素分辨率宽 * 图片像素分辨率高 * 一个像素所占字节大小

其中一个像素所占字节大小又根据不同的纹理格式会有出入,一般带透明通道的png使用ARGB8888(32bit(位),argb四个通道各占8bit(位),总共占4byte(字节)),不带透明通道的jpg使用RGB888(24位,占3字节),所以对于png和jpg而言有一个简化的公式,如

​ png内存大小 = 像素分辨率宽 * 高 * 4;

​ jpg内存大小 = 像素分辨率宽 * 高 * 3;

例如:一张分辨率为1024*1024的png压缩图片,其占用内存的大小为
size = 1024 * 1024 * 4 (byte) 如果转换为MB的话size/1024/1024,就是4MB,如果是存储在磁盘,这个大小根据不同图片有差异,但是会比在内存中小很多,可以自行测验一下。大概只会占用1. 多MB的空间。

​ 使用png或者jpg压缩的图片虽然在存储空间上更有优势,但在内存中确没法优化,而且在内存中CPU会先对图片进行转换,处理成GPU能识别的纹理,然后传输到显存,GPU在进行渲染,这一步转换还会占用CPU的时间。所以后面出来专门针对纹理的压缩技术,俗称纹理压缩。纹理压缩过的图片不进减少了其在内存中占用的大小,而且因为它能够直接被GPU识别,所以也大大的提高了整个渲染流程的效率,因为不需要在做CPU那一步转换。

​ 关于纹理压缩的文章网上有很多,这里不多说了,目前主流的有astc、etc等等。在creator2.4中支持纹理压缩,但是引擎没有提供批量处理的操作,资源一多的话处理起来很不方便,所以自己用node写了一个进行批量纹理压缩的工具,其中有些注意事项在此记录分享一下。

使用工具node

​ node的使用安装这些不说了,这里以astc纹理压缩为例子,astc目前总体压缩性价比较好,支持Android、ios、微信小游戏等多个平台,机型也适配较广,如果不是需要适配特别低的机型,一般够用了,其它方式的纹理压缩过程大体相同,只是具体压缩那一步各个工具使用不同。

准备阶段

​ 1、准备需要压缩的项目资源,一般在自己项目下面

​ 2、准备好压缩的工具,比如我这边是用astc,在引擎的安装目录下面有提供

​ 3、用nodejs写一个批量处理文件的流程,将每一个文件进行纹理压缩

注意事项

​ 1、如果是需要预乘的图片资源我们不能进行压缩,不然会有问题

​ 2、设置纹理压缩的参数,在压缩和解析的时候一定要配套起来

​ 2、如果是和打包一起使用,通常会将打包后的资源进行压缩并且进行资源替换,然后还会缓存压缩过的纹理,可以提高后续打包的效率,不用重复压缩。

使用过程

astcenc应用程序提供了可用命令行参数的完整列表,最基本的命令是:
astcenc.exe -cl ${filePath} ${path.dirname(filePath)}/${path.basename(filePath, '.png')}.astc 6x6 -fast; // 压缩
astcenc.exe -dl ${filePath} ${path.dirname(filePath)}/${path.basename(filePath, '.astc')}.png; // 解压缩
更多压缩指令可以直接在cmd下运行对应的应用查看
To compress an image use:
astcenc {-cl|-cs|-ch|-cH} [options]
e.g. using LDR profile, 8x6 blocks, and the thorough quality preset:
astcenc -cl kodim01.png kodim01.astc 8x6 -thorough
To decompress an image use:
astcenc {-dl|-ds|-dh|-dH}
Supported 2D block sizes are:

​ 4x4: 8.00 bpp 10x5: 2.56 bpp
​ 5x4: 6.40 bpp 10x6: 2.13 bpp
​ 5x5: 5.12 bpp 8x8: 2.00 bpp
​ 6x5: 4.27 bpp 10x8: 1.60 bpp
​ 6x6: 3.56 bpp 10x10: 1.28 bpp
​ 8x5: 3.20 bpp 12x10: 1.07 bpp
​ 8x6: 2.67 bpp 12x12: 0.89 bpp

​ Supported 3D block sizes are:

​ 3x3x3: 4.74 bpp 5x5x4: 1.28 bpp
​ 4x3x3: 3.56 bpp 5x5x5: 1.02 bpp
​ 4x4x3: 2.67 bpp 6x5x5: 0.85 bpp
​ 4x4x4: 2.00 bpp 6x6x5: 0.71 bpp
​ 5x4x4: 1.60 bpp 6x6x6: 0.59 bpp

​ The quality level configures the quality-performance tradeoff for
​ the compressor; more complete searches of the search space improve
​ image quality at the expense of compression time. The quality level
​ can be set to any value between 0 (fastest) and 100 (thorough), or
​ to a fixed quality preset:

​ -fastest (equivalent to quality = 0)
​ -fast (equivalent to quality = 10)
​ -medium (equivalent to quality = 60)
​ -thorough (equivalent to quality = 98)
​ -exhaustive (equivalent to quality = 100)

话不多说,关键代码如下:

外面调用这个方法就行 startCompress()

const path = require('path');
const fs = require('fs');

// RGBA_ASTC_4x4: 30
// RGBA_ASTC_5x4: 31
// RGBA_ASTC_5x5: 32
// RGBA_ASTC_6x5: 33
// RGBA_ASTC_6x6: 34
// RGBA_ASTC_8x5: 35
// RGBA_ASTC_8x6: 36
// RGBA_ASTC_8x8: 37
// RGBA_ASTC_10x5: 38
// RGBA_ASTC_10x6: 39
// RGBA_ASTC_10x8: 40
// RGBA_ASTC_10x10: 41
// RGBA_ASTC_12x10: 42
// RGBA_ASTC_12x12: 43
const CompressQuality = { // 这里可以根据项目情况,做不同的压缩质量等级设置
    'none': ['0', '0', 0],   // 不压缩
    'high': ['4x4', '7@30', 30],
    'middle': ['6x6', '7@34', 34],
    'low': ['8x8', '7@37', 37],
}

// 默认压缩质量
const DefaultQuality = 'middle';

var AstcTools = {
    _cacheDir: '',
    _cacheMap: {},
    _compressDir: '',
    _comprExt: '.astc',  // 压缩文件后缀名
    project: '',  // 项目路径
    defaultAstcFormat: 34,
    compressionQualityConfig: {}, // 质量配置
    textureCompressCfg: '', // 配置了资源压缩质量等级的文件路径,针对不同资源做不同等级的压缩 
    /**
    示例:
    {
        "high": 
            [
                "assets/test0.png",  // 项目下面的路径
                "assets/resources/test"   // 文件夹形式
            ],
        "middle":["assets/test1.png"],
        "low": ["assets/test2.png"]
    }
    */

    /**
     * 遍历文件夹
     * @param {*} dir 
     * @param {*} callback 
     */
    walkSync(dir, callback) {
        if (fs.existsSync(dir)) {
            fs.readdirSync(dir).forEach((file) => {
                var filePath = path.join(dir, file);
                var stat = fs.statSync(filePath);
                if (stat.isFile()) {
                    callback(filePath);
                } else if (stat.isDirectory()) {
                    this.walkSync(filePath, callback);
                }
            });
        } else {
            console.log(`walkSync error ${dir}`);
        }
    },

    /**
     * 递归创造目录
     * @param {*} dirname 
     * @returns 
     */
    mkdirsSync(dirname) {
        if (fs.existsSync(dirname)) {
            return true;
        } else {
            if (this.mkdirsSync(path.dirname(dirname))) {
                fs.mkdirSync(dirname);
                return true;
            }
        }
        return false;
    },

    /**
     * 读取文件
     * @param {*} file 
     * @returns 
     */
    readFile(file) {
        if (fs.existsSync(file)) {
            return JSON.parse(fs.readFileSync(file));
        } else {
            console.error(`readFile error ${file}`)
        }
        return null;
    },

    getMD5(file) {
        return crypto.createHash('md5').update(fs.readFileSync(file)).digest('hex');
    },

    /**
     * 同步创建cmd命令
     * @param {*} cmd 
     * @param {*} callfunc 
     */
    createCmdSync(cmd, doWork, callfunc) {
        child_process.execSync(cmd, { cwd: doWork, stdio: 'inherit' });
        if (callfunc)
            callfunc();
    },

    /**
     * 处理引擎编辑器下的资源文件,生成对应的md5路径
     * @param {string} from 
     * @param {object} qualityCfg 压缩map对象
     * @param {string} quality 压缩质量 
     */
    haldleCreatorImgMd5Path(from, qualityCfg, quality) {
        if (fs.statSync(from).isDirectory()) {
            this.walkSync(from, (file) => {
                let extname = path.extname(file);
                if (extname == '.png' || extname == '.jpg') {
                    // 生成的md5名字就是对应.meta文件里面的uuid
                    let metaFile = file + '.meta'
                    let metaData = this.readFile(metaFile)
                    let md5 = metaData.uuid
                    qualityCfg[md5] = quality
                }
            });
        } else {
            let extname = path.extname(from);
            if (extname == '.png' || extname == '.jpg') {
                // 生成的md5名字就是对应.meta文件里面的uuid
                let metaFile = from + '.meta'
                let metaData = this.readFile(metaFile)
                let md5 = metaData.uuid
                qualityCfg[md5] = quality
            }
        }
    },

    // 初始化压缩配置
    initCompress() {
        this._cacheDir = path.join(this.project, 'temp', '.cache-texture', this._comprExt.slice(1));
        let cacheCfg = path.join(this._cacheDir, 'cacheMap.json');
        if (fs.existsSync(cacheCfg)) {
            this._cacheMap = JSON.parse(fs.readFileSync(cacheCfg));
        } else {
            this._cacheMap = {};
        }
        if (!fs.existsSync(this._cacheDir)) {
            this.mkdirsSync(this._cacheDir);
        }

        this.handleCompressLv()
    },

    handleCompressLv() {
        this.defaultAstcFormat = CompressQuality[DefaultQuality][2]
        this.compressionQualityConfig = {}
        this.extFormatConfig = {}
        let qualityPath = this.textureCompressCfg
        if (qualityPath && fs.existsSync(qualityPath) && fs.statSync(qualityPath).isFile()) {
            try {
                let qualityConfig = JSON.parse(fs.readFileSync(qualityPath));
                if (qualityConfig) {
                    let highs = qualityConfig.high
                    let middles = qualityConfig.middle
                    let lows = qualityConfig.low
                    let nones = qualityConfig.none
                    if (lows && lows.length > 0) {
                        for (let i = 0; i < lows.length; i++) {
                            this.handleQualityCfg(lows[i], this.compressionQualityConfig, 'low')
                        }
                    }

                    if (middles && middles.length > 0) {
                        for (let i = 0; i < middles.length; i++) {
                            this.handleQualityCfg(middles[i], this.compressionQualityConfig, 'middle')
                        }
                    }

                    if (highs && highs.length > 0) {
                        for (let i = 0; i < highs.length; i++) {
                            this.handleQualityCfg(highs[i], this.compressionQualityConfig, 'high')
                        }
                    }

                    if (nones && nones.length > 0) {
                        for (let i = 0; i < nones.length; i++) {
                            this.handleQualityCfg(nones[i], this.compressionQualityConfig, 'none')
                        }
                    }
                }
                console.log('纹理压缩配置生成完成, 配置路径正常!')
            } catch (err) {
                console.error('astc 纹理压缩配置读取失败', err)
            }
        }
    },

    handleQualityCfg(filePath, qualityCfg, quality) {
        let src = path.join(this.project, filePath)
        this.haldleCreatorImgMd5Path(src, qualityCfg, quality)
    },

    /**
     * 异步开始压缩指定目录下的图片。
     * @param {string} dir - 需要压缩的图片所在的目录。
     * @param {Function} callfunc - 压缩完成后的回调函数。
     */
    async startCompress(dir, callfunc) {
        this.initCompress()
        this._compressDir = dir;

        let texture = [];
        this.walkSync(dir, (file) => {
            let extname = path.extname(file);
            if (extname == '.png' || extname == '.jpg') {
                if (this.whiteList(file, extname)) {
                    let md5 = this.getMD5(file);
                    let size = fs.statSync(file).size;
                    let quality = this.getQuality(file)   // 这里获取一个质量
                    let params = {
                        'md5': md5,
                        'filePath': file,
                        'size': size,
                        'quality': quality
                    }
                    texture.push(params);
                } else {
                    console.log(`图片通过白名单检测 ${file}`);
                }
            }
        });

        for (let i in texture) {
            let params = texture[i];
            let md5 = params.md5;
            let filePath = params.filePath;
            let quality = params.quality;
            //检查纹理压缩缓存
            if (this.queryCache(params)) {
                await this.replaceImageToTxture(filePath, quality);
            } else {
                await this.transform(filePath, md5, quality);
            }
            console.log(`正在压缩,进度: ${Number(i) + 1}/${texture.length}\n`);
        }

        if (callfunc) {
            callfunc();
        }
    },

    /**
     * 获取当前图片需要压缩的质量
     * @param {string} filePath 
     * @returns 
     */
    getQuality(filePath) {
        let key = path.basename(filePath, path.extname(filePath))
        if (this.compressionQualityConfig[key]) {
            return this.compressionQualityConfig[key]
        } else {
            return DefaultQuality
        }
    },

    // 白名单检查
    whiteList(filePath, extname) {
        // 开了纹理预乘的不压缩
        let importJson = filePath.replace(extname, '.json');
        importJson = importJson.replace('\\native', '\\import');
        if (fs.existsSync(importJson)) {
            let json = this.readFile(importJson);
            let config = json[5][0].split(',');
            if (config[5] != 0) {
                // 图片开启预乘不进行压缩
                return false;
            }
        } else {
            // 未找到图片关联json文件
            return false;
        }

        return true;
    },

    // 纹理压缩缓存查询
    queryCache(params) {
        let md5 = params.md5;
        let size = params.size;
        let filePath = params.filePath;
        let quality = params.quality;
        let assetsIndex = filePath.lastIndexOf('assets\\');
        let key = filePath.substring(assetsIndex);
        if (this._cacheMap[key]) {
            let textureUrl = this._cacheMap[key].textureUrl;
            let textureCompressType = this._cacheMap[key].textureCompressType;
            let cacheSize = this._cacheMap[key].size;
            let cacheMd5 = this._cacheMap[key].md5;
            let cacheQuality = this._cacheMap[key].quality;

            let hasTextureUrl = textureUrl && fs.existsSync(textureUrl);
            if (hasTextureUrl) {
                return textureCompressType == 'astc' && cacheSize == size && cacheMd5 == md5
                    && CompressQuality[cacheQuality][0] == CompressQuality[quality][0];
            } else {
                return false;
            }
        } else {
            return false;
        }
    },

    /**
     * 将普通图片替换成压缩后的纹理
     * @param {string} filePath 
     * @param {string} quality high | middle | low
     * @returns 
     */
    replaceImageToTxture(filePath, quality) {
        return new Promise(resolve => {
            let assetsIndex = filePath.lastIndexOf('assets\\');
            let key = filePath.substring(assetsIndex);
            let extname = path.extname(filePath);
            let textureUrl = filePath.replace(extname, this._comprExt);
            fs.writeFileSync(textureUrl, fs.readFileSync(this._cacheMap[key].textureUrl));
            fs.unlinkSync(filePath);

            let importJson = filePath.replace(extname, '.json');
            importJson = importJson.replace('\\native', '\\import');
            let json = JSON.parse(fs.readFileSync(importJson));
            let config = json[5][0].split(',');
            config[0] = CompressQuality[quality][1];
            //关闭预乘
            config[5] = 0;
            config = config.join(',');
            json[5][0] = config;
            fs.writeFileSync(importJson, JSON.stringify(json));
            // console.log(`build textureUrl ---> ${textureUrl}`);
            resolve();
        })
    },

    /**
     * 压缩纹理
     * @param filePath
     * @param md5
     * @returns {Promise<void>}
     */
    transform(filePath, md5, quality) {
        return new Promise(resolve => {
            let astc = `astcenc.exe -cl ${filePath} ${path.dirname(filePath)}/${path.basename(filePath, path.extname(filePath))}.astc ${CompressQuality[quality][0]} -medium`;
            // 在安装的引擎目录下面
            let textureCompressTool = "D:\\Creator\\2.4.11\\resources\\static\\tools\\texturecompress\\mali\\Windows_64"
            this.createCmdSync(astc, textureCompressTool, () => {
                // console.log(`---------压缩纹理---------\n${stdout}`)
                let extname = path.extname(filePath);
                let textureUrl = filePath.replace(extname, this._comprExt);
                if (fs.existsSync(textureUrl)) {
                    this.textureCache(textureUrl, md5, filePath, quality);
                    this.replaceImageToTxture(filePath, quality);
                    resolve();
                }
            });
        })
    },

    // 缓存纹理
    textureCache(url, md5, filePath, quality) {
        let cacheDir = this._cacheDir;
        if (!fs.existsSync(cacheDir)) {
            this.mkdirsSync(cacheDir);
        }

        let fileCacheDir = path.join(cacheDir, 'assets');
        if (fs.existsSync(fileCacheDir)) {
            let toDir = path.dirname(url).replace(this._compressDir, fileCacheDir);
            let toPath = path.join(toDir, path.basename(url));
            if (fs.existsSync(toDir)) {
                this.cacheTexture(url, toPath, md5, filePath, quality);
            } else {
                this.mkdirsSync(toDir);
                this.cacheTexture(url, toPath, md5, filePath, quality);
            }
        } else {
            fs.mkdirSync(fileCacheDir);
            this.textureCache(url, md5, filePath, quality);
        }
    },

    cacheTexture(fromPath, toPath, md5, filePath, quality) {
        fs.writeFileSync(toPath, fs.readFileSync(fromPath));
        let size = fs.statSync(filePath).size;
        let params = {
            textureUrl: toPath,
            size: size,
            md5: md5,
            textureCompressType: 'astc',
            quality: quality
        }
        let assetsIndex = filePath.lastIndexOf('assets\\');
        let key = filePath.substring(assetsIndex);
        this._cacheMap[`${key}`] = params;
        this.writeCacheInfo();
    },

    writeCacheInfo() {
        let cacheCfg = path.join(this._cacheDir, 'cacheMap.json');
        fs.writeFileSync(cacheCfg, JSON.stringify(this._cacheMap, null, 4));
    }
}

module.exports = AstcTools;

如果是etc,在引擎目录下也有工具,使用原理同上,只需要替换掉具体执行压缩的地方就行,其它压缩方式也是同理

顺带复制一下相关指令
etcpack <input_filename> <output_directory> [Options]
Options:
-s {fast|slow} Compression speed. Slow = exhaustive
search for optimal quality
(default: fast).
-e {perceptual|nonperceptual} Error metric: Perceptual (nicest) or
nonperceptual (highest PSNR)
(default: perceptual).
-c {etc1|etc2} Codec: etc1 (most compatible) or
etc2 (highest quality)
(default: etc2).
-f {R|R_signed|RG|RG_signed| Compressed format: one, two, three
RGB|RGBA1|RGBA8 or RGBA} or four channels, and 1 or 8 bits
for alpha (1 equals punchthrough)
(default: RGB).
-mipmaps Generate mipmaps.
-ext {PPM|PGM|JPG|JPEG|PNG|GIF| Uncompressed formats
BMP|TIF|TIFF|PSD|TGA|RAW| (default PPM).
PCT|SGI|XPM}
-ktx Output ktx files, not pkm files.
-v Verbose mode. Prints additional
information during execution.
-progress Prints compression progress.
-version Prints version number.

Options to be used only in conjunction with etc codec (-c etc):
-aa Use alpha channel and create a
texture atlas.
-as Use alpha channel and create a
separate image.
-ar Use alpha channel and create a
raw image.

Examples:
etcpack img.ppm myImages Compresses img.ppm to myImages\img.pkm
in ETC2 RGB format.
etcpack img.ppm myImages -ktx Compresses img.ppm to myImages\img.ktx
in ETC2 RGB format.
etcpack img.pkm myImages Decompresses img.pkm to
myImages\img.ppm.
etcpack img.ppm myImages -s slow Compress img.ppm to myImages\img.pkm
using the slow mode.
etcpack img.tga MyImages -f RGBA Compresses img.tga to MyImages\img.pkm
using etc2 + alpha.
etcpack img.ppm MyImages -f RG Compresses red and green channels of
img.ppm to MyImages\img.pkm.
etcpack img.pkm MyImages -ext JPG Decompresses img.pkm to
MyImages\img.jpg.
etcpack orig.ppm images\copy.ppm -p Calculate PSNR between orig.ppm and
images\copy.ppm.
Instead of output directory a full
file path is given as a second
parameter.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值