记录:ASTC远端图集加载

本次使用的Cocos版本为3.8.7

前期准备:图片处理与格式转换

在游戏开发或图形应用中,高效的资源管理对性能提升至关重要,而纹理处理是其中关键一环。

思路

1.使用TexturePacker 工具导出包含 test.png 和 test.plist 的图集文件。

2.得到 test.png 后,为进一步降低纹理内存占用、提升加载与渲染效率,将其转换为 ASTC 格式,本次选用 6x6 转的参数配置。

附加转astc格式方法 参考:【分享】图片工具将png转pkm或astc 且可还原

3.使用脚本 将 plist 内容融入 astc文件中

在完成图片格式转换后,一个巧妙的操作是将 plist 文件内容写入到 test.astc 中,这里自己将plist内容写入到astc文件中。将plist写入文件的目的是为了减少文件下载量

我写入的 plist 内容如下:
子纹理文件名 | x 坐标 | y 坐标 | 宽度 | 高度 | 偏移 x | 偏移 y | 原始宽度 | 原始高度 | 是否旋转 | 图集宽|图集高

path=test.png

test_01.png|802|2|154|136|26|46|300|300|0|958|140

test_02.png|486|2|158|136|28|46|300|300|0|958|140

test_03.png|164|2|160|136|30|46|300|300|0|958|140

test_04.png|2|2|160|136|30|46|300|300|0|958|140

test_05.png|326|2|158|136|27|46|300|300|0|958|140

test_06.png|646|2|154|136|25|46|300|300|0|958|140

在这个新的内容结构里,第一行 “path=stand1_4.png” 明确了纹理图集对应的原始图片文件名,就像给一个档案库贴上了总标签,表明里面的文件来自何处。后续每一行以 “子纹理文件名 | x 坐标 | y 坐标 | 宽度 | 高度 | 偏移 x | 偏移 y | 原始宽度 | 原始高度 | 是否旋转 | 其他信息” 的格式,详细记录了每个子纹理在图集中的位置、自身尺寸、偏移量以及旋转状态等关键信息

后缀转换与文件准备

完成 plist 内容写入 test.astc 后,将 test.astc 的后缀改成 test.bin ,这是为了个体assetManager.loadRemote下载二进制数据

核心加载:Cocos Creator 3.8.7 大显身手

加载流程解析

在 Cocos Creator 3.8.7 的项目开发中,loadBin函数承担着远程加载 test.bin 文件并解析其中 plist 内容的关键职责,是资源加载流程的核心环节 。由于将plist内容写到test.bin中了,因此我们只需要下载test.bin一个文件

interface PlistInfo {
    name: string
    x: number
    y: number
    w: number
    h: number
    offsetX: number
    offsetY: number
    srcSizeX: number
    srcSizeY: number
    rotated: number
    textureWidth: number
    textureHeight: number
}

loadBin(URL) {

  assetManager.loadRemote(URL, (Err, pAsset:BufferAsset) => {
       if (Err == null && pAsset) {
           let pBuffView = new DataView(pAsset.buffer())
           let nLen = pBuffView.byteLength
           let pTail = ""
           let PlistData = { Name: "", Info: \[] }
           for (let i = nPlistLen; i >= 1; i--) {
               let V = pBuffView.getUint8(nLen - i - 12)
               let B = String.fromCharCode(V)
               if (B == '\n') {
                   pTail = pTail.replace('\r', '')
                   let StrList = pTail.split("|")
                   if (StrList.length == 1) {
                       PlistData.Name = StrList[0]
                   } else {
                       PlistData.Info.push({
                           name: StrList[0],
                           x: Number(StrList[1]),
                           y: Number(StrList[2]),
                           w: Number(StrList[3]),
                           h: Number(StrList[4]),
                           offsetX: Number(StrList[5]),
                           offsetY: Number(StrList[6]),
                           srcSizeX: Number(StrList[7]),
                           srcSizeY: Number(StrList[8]),
                           rotated: Number(StrList[9]),
                       })
                   }

                   pTail = ""
               } else {
                   pTail = pTail + B
               }

           }
           console.log("得到的Plist数据=>>>", PlistData)
           this.CreateNativeImageBitmap(pAsset, PlistData).then((ImageAtils) => {
               if (ImageAtils) {
                    this.msprite.spriteFrame = ImageAtils\[0]
               } else {
                   console.log("图集加载失败")
               }
           }).catch(() => {
               console.log("图集加载失败")
           })
       } else {
           console.log("下载失败:", Err)
       }
   })
}

上述方法是通过远程下载test.bin得到pAsset:BufferAsset类型的数据,再通过解析pAsset得到plist的内容。

图片构建与显示

在成功解析出 plist 内容后,CreateNativeImageBitmap函数登场,它负责将加载的文件数据转化为可供显示的纹理和精灵帧,是实现图片最终显示在界面上的关键步骤 。

CreateNativeImageBitmap(Data: BufferAsset, PlistData: { Name: string; Info: Array<PlistInfo> }) {
        return new Promise((resolve, reject) => {
            try {
                const dataView = new DataView(Data.buffer(), 0, 16);

                const read3LE = (view: DataView, off: number) =>
                    view.getUint8(off) | (view.getUint8(off + 1) << 8) | (view.getUint8(off + 2) << 16);

                const widthASTC = read3LE(dataView, 7);
                const heightASTC = read3LE(dataView, 10);

                const width = PlistData.Info[0].textureWidth ?? widthASTC;
                const height = PlistData.Info[0].textureHeight ?? heightASTC;

                console.log(`ASTC 文件头尺寸: ${widthASTC}x${heightASTC}, 原图尺寸: ${width}x${height}`);

                const texture = new Texture2D();
                texture.reset({
                    width: width,
                    height: height,
                    format: Texture2D.PixelFormat.RGBA_ASTC_6x6,
                })
                // 上传有效数据部分(跳过16字节文件头)
                const validData = new DataView(Data.buffer(), 16);
                texture.uploadData(validData);

                let ImageAtils = new Array<SpriteFrame>()
                let len = PlistData.Info.length
                for (let i = 0; i < len; i++) {
                    const Info: PlistInfo = PlistData.Info[i]
                    const pSpriteFrame = new SpriteFrame()
                    pSpriteFrame.name = Info.name
                    pSpriteFrame.texture = texture
                    pSpriteFrame.offset = new Vec2(Info.offsetX, Info.offsetY)
                    pSpriteFrame.rect = new Rect(Info.x, Info.y, Info.w, Info.h)
                    pSpriteFrame.originalSize = new Size(Info.srcSizeX, Info.srcSizeY)
                    ImageAtils.push(pSpriteFrame)
                }
                resolve(ImageAtils)
            } catch (error) {
                console.error('从ArrayBuffer加载图像失败:', error)
                reject(null)
            }
        })
    }

最后,根据解析出的PlistData.Info数组中的信息,循环创建SpriteFrame对象 。为每个SpriteFrame对象设置名称、关联之前创建的texture、偏移量、矩形区域以及原始尺寸等属性 ,这些属性就像为每个子图像在画布上贴上了精准的位置和尺寸标签 ,将创建好的SpriteFrame对象添加到ImageAtils数组中 。当所有SpriteFrame创建完成后,通过resolve返回ImageAtils数组,若在整个过程中出现错误,通过reject返回错误信息 。

兼容性检测:确保 astc 畅行无阻

多平台支持判断

在跨平台应用开发中,ASTC 格式虽有显著优势,但不同平台对其支持程度存在差异,所以检测设备对 ASTC 格式的支持情况是至关重要的前置步骤,这就像在出发前检查车辆是否能适应不同路况一样 。

export enum ASTCSIZE {
    '4x4'='4x4',
    '5x5'='5x5',
    '6x6'='6x6',
    '8x8'='8x8',
    '10x10'='10x10'
}
isASTCSupported(blockSize: ASTCSIZE): boolean {
        if (sys.isBrowser) {
            return this.checkWebASTC(blockSize);
        } else {
            return this.checkNativeASTC(blockSize);
        }
    }

上述isASTCSupported函数是整个兼容性检测的总入口 ,它就像一个智能的导航仪,根据当前运行环境判断是 Web 平台还是原生平台 。若运行在浏览器环境(sys.isBrowsertrue),则调用checkWebASTC函数进行 Web 平台的支持性检测;若在原生平台(sys.isBrowserfalse),则调用checkNativeASTC函数开展原生平台的检测 ,这种分平台检测的方式,能精准地针对不同平台特性进行支持性判断,提高检测的准确性与可靠性 。

Web 平台检测是否支持astc

checkWebASTC(blockSize: string): boolean {
        // 获取 WebGL 2.0 上下文(ASTC 仅支持 WebGL 2.0+)
        const canvas = document.getElementById('GameCanvas') as HTMLCanvasElement;
        if (!canvas) return false;
        const gl = canvas.getContext('webgl2') as WebGL2RenderingContext;
        if (!gl) {
            console.log('WebGL 2.0 不支持,无法使用 ASTC');
            return false;
        }

        // 检测所有可能的 ASTC 扩展(浏览器兼容性差异)
        const astcExtensions = [
            'WEBGL_compressed_texture_astc',    // 通用扩展(Chrome 等主流浏览器)
            'GL_KHR_texture_compression_astc_ldr', // KHR 标准扩展
            'GL_OES_texture_compression_astc'   // OpenGL ES 扩展(移动浏览器)
        ];
        let astcExt: any = null;
        for (const extName of astcExtensions) {
            astcExt = gl.getExtension(extName);
            if (astcExt) break;
        }
        if (!astcExt) {
            console.log('浏览器不支持任何 ASTC 扩展');
            return false;
        }

        // 检查扩展是否支持目标块大小的格式
        const blockSizeToFormat: Record<string, string> = {
            '4x4': 'COMPRESSED_RGBA_ASTC_4x4_KHR',
            '5x5': 'COMPRESSED_RGBA_ASTC_5x5_KHR',
            '6x6': 'COMPRESSED_RGBA_ASTC_6x6_KHR',
            '8x8': 'COMPRESSED_RGBA_ASTC_8x8_KHR',
            '10x10': 'COMPRESSED_RGBA_ASTC_10x10_KHR',
        };
        const formatKey = blockSizeToFormat[blockSize];
        if (!formatKey || !astcExt[formatKey]) {
            console.log(`ASTC 块大小 ${blockSize} 不被支持`);
            return false;
        }

        return true;
    }

原生平台检测是否支持Astc

checkNativeASTC(blockSize: string): boolean {
        // 确保 gfx 设备已初始化
        if (!gfx.deviceManager || !gfx.deviceManager.gfxDevice) {
            console.log('gfx 设备未初始化,无法检测 ASTC');
            return false;
        }

        // 映射块大小到 gfx.Format 枚举
        const blockSizeToFormat: Record<string, gfx.Format> = {
            '4x4': gfx.Format.ASTC_RGBA_4X4,
            '5x5': gfx.Format.ASTC_RGBA_5X5,
            '6x6': gfx.Format.ASTC_RGBA_6X6,
            '8x8': gfx.Format.ASTC_RGBA_8X8,
            '10x10': gfx.Format.ASTC_RGBA_10X10,
        };
        const targetFormat = blockSizeToFormat[blockSize];
        if (targetFormat === undefined) {
            console.log(`不支持的 ASTC 块大小:${blockSize}`);
            return false;
        }

        // 查询 GPU 是否支持该格式作为纹理
        const formatFeatures = gfx.deviceManager.gfxDevice.getFormatFeatures(targetFormat);
        const support = (formatFeatures & 1) !== 0;
        if (!support) {
            console.log(`GPU 不支持 ASTC ${blockSize} 格式`);
        }
        return support;
    }

这两个检测函数再API文档里面查不到,我还是论坛上找到的!!!

使用的时候大家可以根据项目情况参考修改。
我用windows的goole浏览器不支持Astc,预览我都是使用手机浏览器测试的!!

以上的方式有什么不对或者由更好的方式可以发言告知。

2赞

1. 不是可以设置压缩纹理吗?

即便是外部生成好图集( .plist + .png),但是导入到编辑器中,也可以(.png)设置为压缩纹理

为什么要自己生成呢?:thinking:

2. 引擎本身有做纹理加载优先级处理,为什么要自己检测是否支持 ASTC呢?

1.远端图集,不在编辑器中显示
2.检测ASTC是为了下载不同的HTTP路径。支持ASTC的就下载ASTC目录下的图

1赞

修复下显示黑边问题

let texture = new Texture2D()
texture.setFilters(Texture2D.Filter.NEAREST, Texture2D.Filter.NEAREST); // 修正采样方式,防止边缘插值出黑边
if (sys.isBrowser) {
    texture.reset({
        width: PlistData.Info[0].textureWidth,
        height: PlistData.Info[0].textureHeight,
        format: Texture2D.PixelFormat.RGBA_ASTC_6x6,
    })
    texture.uploadData(uint8Data)
} else {
    let imgAss = new ImageAsset({
        _data: uint8Data,
        width: PlistData.Info[0].textureWidth,
        height: PlistData.Info[0].textureHeight,
        format: Texture2D.PixelFormat.RGBA_ASTC_6x6,
        _compressed: true,
        mipmapLevelDataSize: [byteLength],
    })
    texture.image = imgAss
}

之前的显示在android上有黑边。修正下

感谢 无私分享 这样的干货!学习中… 求教一下 这里面为什么只选择了 远程,本地的加载不采用这个方案的原因是什么?是因为本地本身加载就很快吗?本地采用一致有什么问题

我编辑器的图片没有多少,编辑器的图片是可以直接在编辑器里面设置的。
我将远端图片改成astc,下载和加载都会快一些
我主要是为了解决人物和怪物在下载显示时卡顿问题

多谢,多谢解惑