参考

动态合图 | Cocos Creator

Cocos Creator 性能优化:DrawCall(全面!) - Creator 2.x - Cocos中文社区

【乐府】突破动态合图-你真的把动态合图用对了吗? - Creator 2.x - Cocos中文社区

3.8.2动态合图问题 - Creator 3.x - Cocos中文社区

 

一  什么是动态合图

二 怎么使用动态合图

三 动态合图源码

四 动态合图的图集排列规则

五 怎么在场景中查看动态合图的图集

六 一些问题

 

 

一 什么是动态合图

动态合图是cocos提供的功能,在项目运行时动态将贴图合并到一张大图,以此减少drawcall。

 

二 怎么使用动态合图

动态合图条件

合图条件在源码atlas-manager.ts中,条件是 开启动态合图 + 图集未满 + 图片未参与动态合图 + 图片勾选packable + 采样符合条件,符合这些条件即可参与动态合图。

atlas-manager.ts:

        if (!this._enabled || this._atlasIndex >= this._maxAtlasCount
            || !spriteFrame || spriteFrame.original) return null;

        if (!spriteFrame.packable) return null;

        // hack for pixel game,should pack to different sampler atlas
        const sampler = spriteFrame.texture.getSamplerInfo();
        if (sampler.minFilter !== Filter.LINEAR || sampler.magFilter !== Filter.LINEAR || sampler.mipFilter !== Filter.NONE) {
            return null;
        }

 

开启动态合图

将动态合图设置代码加入项目脚本最外层,不要写在onLoad/start函数中,确保项目加载过程中即使生效。

const { ccclass, property, menu } = _decorator;

//是否在将贴图上传至 GPU 之后删除原始图片缓存,删除之后图片将无法进行 动态合图。
//web平台不需要开启,因为web平台Image占用内存小。
//小游戏平台默认开启,避免内存占用过高。
macro.CLEANUP_IMAGE_CACHE = false;
//动态合图开关
DynamicAtlasManager.instance.enabled = true;

export default class Main {

}

 

图片需要勾选Packable选项

该选项默认勾选,所以无需自己去逐个设置

 

动态合图类的设置

maxFrameSize:可以添加进图集的最大尺寸,如果图片高或宽大于512,那么就无法进行动态合图。默认512。

maxAtlasCount:可以创建的图集最大数量,一张图集满了以后,会创建一张新的进行合图,最大支持5张。默认5。

textureSize:创建图集的宽高,默认是2048x2048。

       DynamicAtlasManager.instance.maxFrameSize = 512;    //可以添加进图集的图片的最大尺寸
       DynamicAtlasManager.instance.maxAtlasCount = 5;     //可以创建的最大图集数量
       DynamicAtlasManager.instance.textureSize = 2048;    //创建的图集的宽高

  

三 动态合图源码

1 源码位置

Creator\3.8.2\resources\resources\3d\engine\cocos\2d\utils\dynamic-atlas\atlas-manager.ts

 

2 动态合图调用流程

 

3 源代码

调用动态合图管理类进行合图
atlas-manager.ts:
    public packToDynamicAtlas (comp, frame): void {
        if (EDITOR_NOT_IN_PREVIEW || !this._enabled) return;
        
        //图片未合图过_original=null,勾选了可合并packagble=true,高宽>0,则进行合图。
        //_original={_texture, _x, _y} 保存着原纹理。
        if (frame && !frame._original && frame.packable && frame.texture && frame.texture.width > 0 && frame.texture.height > 0) {
            const packedFrame = this.insertSpriteFrame(frame);
            if (packedFrame) {
                frame._setDynamicAtlasFrame(packedFrame);
            }
        }
    }

  

动态图集管理类将小图合并到大图
altas-manager.ts
    public insertSpriteFrame (spriteFrame):  {
        x: number;
        y: number;
        texture: DynamicAtlasTexture;
    } | null {
        if (EDITOR_NOT_IN_PREVIEW) return null;
        //已合图过_original!=null,图集达最大值_maxAtlasCount=5不可合图
        if (!this._enabled || this._atlasIndex === this._maxAtlasCount
            || !spriteFrame || spriteFrame._original) return null;
        
        //图片可合并packable=false,不可合图
        if (!spriteFrame.packable) return null;

        // hack for pixel game,should pack to different sampler atlas
        const sampler = spriteFrame.texture.getSamplerInfo();
        //采样器sampler为线性LINEAR时不可合图
        if (sampler.minFilter !== Filter.LINEAR || sampler.magFilter !== Filter.LINEAR || sampler.mipFilter !== Filter.NONE) {
            return null;
        }
        
        //图集不存在,则创建一张
        let atlas = this._atlases[this._atlasIndex];
        if (!atlas) {
            atlas = this.newAtlas();
        }
        
        //图集无空白位置可合并图片,则创建一张新的图集合并
        const frame = atlas.insertSpriteFrame(spriteFrame);
        if (!frame && this._atlasIndex !== this._maxAtlasCount) {
            atlas = this.newAtlas();
            return atlas.insertSpriteFrame(spriteFrame);
        }
        return frame;
    }

  

大图将小图合并
atlas.ts:
    public insertSpriteFrame (spriteFrame: SpriteFrame): {
        x: number;
        y: number;
        texture: DynamicAtlasTexture;
    } | null {
        const rect = spriteFrame.rect;
        // Todo:No renderTexture
        const texture = spriteFrame.texture as Texture2D;
        
        //根据小图纹理id保存纹理,info[texture.getId()] = {texture,x,y},根据x,y、texture.width、texture.height可从大图上获取小图
        const info = this._innerTextureInfos[texture.getId()];
        
        //小图起始位置
        let sx = rect.x;
        let sy = rect.y;

        if (info) {
            sx += info.x;
            sy += info.y;
        } else {
            const width = texture.width;
            const height = texture.height;
            
            //小图在大图上排列方式从左到右,从上到下,第2张图在第1张图右边2像素
            //当第1行排满了,从第2行开始排,第2行起始位置x=2,y=第1行最高的图往下
            if ((this._x + width + space) > this._width) {
                this._x = space;
                this._y = this._nexty;
            }
            
            //获取当前行height最高的图,当作下一行y起始位置。例如第1行排列有3张图,height分别是100,200,150,则以200为下一行起始y位置。
            if ((this._y + height + space) > this._nexty) {
                this._nexty = this._y + height + space;
            }
            
            //y轴排列不下,则返回null,表示大图没有位置合并小图了
            if (this._nexty > this._height) {
                return null;
            }
            
            //textureBleeding默认true,较小的帧更有可能受到线性滤波器的影响,这里可能是处理线性滤波器造成的影响
            if (cclegacy.internal.dynamicAtlasManager.textureBleeding) {
                // Smaller frame is more likely to be affected by linear filter
                if (width <= 8 || height <= 8) {
                    //texture.image! 中的 ! 是一个 非空断言操作符(Non-null Assertion Operator),它告诉编译器:
                    //"我确定 texture.image 不是 null 或 undefined,请直接使用它,不要报错。"
                    this._texture.drawTextureAt(texture.image!, this._x - 1, this._y - 1);
                    this._texture.drawTextureAt(texture.image!, this._x - 1, this._y + 1);
                    this._texture.drawTextureAt(texture.image!, this._x + 1, this._y - 1);
                    this._texture.drawTextureAt(texture.image!, this._x + 1, this._y + 1);
                }

                this._texture.drawTextureAt(texture.image!, this._x - 1, this._y);
                this._texture.drawTextureAt(texture.image!, this._x + 1, this._y);
                this._texture.drawTextureAt(texture.image!, this._x, this._y - 1);
                this._texture.drawTextureAt(texture.image!, this._x, this._y + 1);
            }
            
            //把小图绘制到大图上
            this._texture.drawTextureAt(texture.image!, this._x, this._y);
            
            //缓存小图,以便下次复用
            this._innerTextureInfos[texture.getId()] = {
                x: this._x,
                y: this._y,
                texture,
            };

            this._count++;
            
            //保存当前小图在大图上起始位置
            sx += this._x;
            sy += this._y;
            
            //更新下一次合图时的起始位置
            this._x += width + space;
        }
        
        //返回小图所在的大图以及大图上的起始位置,通过以下数据可以从大图上找到小图
        const frame = {
            x: sx,
            y: sy,
            texture: this._texture,
        };
        
        //保存小图,用于销毁图集时删除小图上的合图相关数据
        this._innerSpriteFrames.push(spriteFrame);

        return frame;
    }

  

小图将原本纹理保存起来_original, 然后将大图设置为自己的纹理,这样合批判断时小图们都来自大图图集。
sprite-frame.ts:
    public _setDynamicAtlasFrame (frame): void {
        if (!frame) return;

        this._original = {
            _texture: this._texture,
            _x: this._rect.x,
            _y: this._rect.y,
        };

        this._texture = frame.texture;
        this._rect.x = frame.x;
        this._rect.y = frame.y;
        this._calculateUV();
    }

  

四 动态合图的图集排列规则

合图排列源码在atlas.ts中,排列规则是从左到右,从上到下。

第一行排满了,就从第二行开始排,第二行y起点以第一行最高的图height为起点。

atlas.ts:

            const width = texture.width;
            const height = texture.height;
            
            //小图在大图上排列方式从左到右,从上到下,第2张图在第1张图右边2像素
            //当第1行排满了,从第2行开始排,第2行起始位置x=2,y=第1行最高的图往下
            if ((this._x + width + space) > this._width) {
                this._x = space;
                this._y = this._nexty;
            }
            
            //获取当前行height最高的图,当作下一行y起始位置。例如第1行排列有3张图,height分别是100,200,150,则以200为下一行起始y位置。
            if ((this._y + height + space) > this._nexty) {
                this._nexty = this._y + height + space;
            }
            
            //y轴排列不下,则返回null,表示大图没有位置合并小图了
            if (this._nexty > this._height) {
                return null;
            }

  

如下打印的动态合图,第二行有一个非常高的方块图,那么第三行会以这个高的方块height为y轴起点,导致第二行有大量空白的地方。

 

 五 怎么查看动态合图的图集

项目运行后,3.x版本没有提供显示动态图集到场景的接口,需要效仿2.x的cc.dynamicAtlasManager.showDebug(true) 接口写一个。

 

六 一些问题

1 自动图集的图片可以参与动态合图吗?

可以。但是动态合图是将整个自动图集合并,而不是单个图片。

例如下图,是物品的自动图集,即使场景中只使用了金币,但是自动合图会将整个图集合并,包括武器、钥匙都会合并到自动合图中,而不是只合并金币。

 

还有就是通常项目会把通用按钮、图标等合成一张大图,这个大图基本明显都会超过1024x1024,所以无法进行动态合图。

 

2 Label的Bitmap和Char模式可以动态合图吗?

Char模式不能。

Bitmap模式可以。

 

Char模式是将文字合并到char自己的1024x1024(2.4.x版本是2048x2048)图集上,这个图集尺寸已经超出了自动合图条件,所以不能。

Bitmap模式可以,使用描边、阴影、加粗后都能合图。但是会重复合图,不能复用。

例如下图,一个弹窗中有一个Bitmap模式的Label,内容是“Bitmap文本”,当不停的打开和关闭这个弹窗时,文本内容会重复的进行动态合图。

 

因为动态合图缓存图片时是用texture.id来识别的,缓存[texture.id] = 图片,文本每次的texture.id不一样,所以无法复用。

若要复用,必须修改源码,效仿char模式,char模式能复用是因为使用文字的颜色、尺寸、字体等属性hashCode后作为识别缓存的key值,缓存[code] = 图片,每次hashCode值一致,所以能复用。

 

3 BMFont可以动态合图吗?

可以。

 

4 骨骼动画可以动态合图吗?

不能。

 

5 资源release后会重复动态合图

资源release释放后,再次加载,会重复进行动态合图。

 

6 怎么重建图集?

主动调用reset会重建图集

DynamicAtlasManager.instance.reset();

  

切换场景时,引擎会自动重建图集

director.loadScene或runScene

 

atlas-manager.ts:

    /**
     * @en
     * Reset all dynamic atlases, and all existing ones will be destroyed.
     *
     * @zh
     * 重置所有动态图集,已有的动态图集会被销毁。
     *
     * @method reset
    */
    public reset (): void {
        for (let i = 0, l = this._atlases.length; i < l; i++) {
            this._atlases[i].destroy();
        }
        this._atlases.length = 0;
        this._atlasIndex = -1;
    }

 

7 textureBleeding是什么?

atlas.ts中有一段代码,涉及到一个属性textureBleeding。

atlas.ts:

            if (cclegacy.internal.dynamicAtlasManager.textureBleeding) {
                // Smaller frame is more likely to be affected by linear filter
                if (width <= 8 || height <= 8) {
                    //texture.image! 中的 ! 是一个 非空断言操作符(Non-null Assertion Operator),它告诉编译器:
                    //"我确定 texture.image 不是 null 或 undefined,请直接使用它,不要报错。"
                    this._texture.drawTextureAt(texture.image!, this._x - 1, this._y - 1);
                    this._texture.drawTextureAt(texture.image!, this._x - 1, this._y + 1);
                    this._texture.drawTextureAt(texture.image!, this._x + 1, this._y - 1);
                    this._texture.drawTextureAt(texture.image!, this._x + 1, this._y + 1);
                }

                this._texture.drawTextureAt(texture.image!, this._x - 1, this._y);
                this._texture.drawTextureAt(texture.image!, this._x + 1, this._y);
                this._texture.drawTextureAt(texture.image!, this._x, this._y - 1);
                this._texture.drawTextureAt(texture.image!, this._x, this._y + 1);
            }

  

TextureBleeding是纹理渗色。

当多个小纹理(Sprite)被合并到一个大纹理(Atlas)中时,由于 纹理滤波(Texture Filtering,如线性插值) 或 纹理坐标舍入误差,相邻的小纹理可能会在边缘处出现 颜色渗色(Bleeding),即本不属于它的像素被显示出来。
如果两个相邻的纹理,一个红色、一个蓝色,由于滤波计算,边缘可能显示为 紫色(混合了红和蓝)。
 
所以在当前位置的 四个对角方向(±1, ±1) 各绘制一次纹理,扩展纹理的边缘像素,避免滤波采样到相邻纹理。
 
 

 

posted on 2025-07-15 14:27  gamedaybyday  阅读(171)  评论(0)    收藏  举报