Unity动态图集技术

.背景
首先,为什么需要动态图集?主要有两个原因:

游戏中的icon数量相当大,数量达到上千个。如果打成静态图集,需要好几张2048x2048的图集。运行时内存占用较大。同时,在这种情况下,由于icon会跨几个图集,在几个图集间穿插渲染有可能会提高drawcall。
若icon不打图集,每个icon就是一个单独的drawcall,会导致drawcall很高。
而动态图集,就是为了解决以上提到的问题。它的主要思路就是在运行时将需要使用的icon动态更新到一张共享的图集中去。由于同时渲染的icon数量往往远远小于实际icon的资源数量,因此通常一张1024或者2048大小的动态图集即可以满足大多数的游戏需求。该方法的好处在于,内存在可控范围,并且icon可以共享纹理从而省drawcall。

Unity动态图集技术

在 Unity 中,动态图集(Dynamic Atlas)技术是一种优化纹理资源管理和渲染性能的方法。动态图集通过将多个小纹理动态合并到一个大纹理中,减少了渲染过程中纹理切换的次数,从而提高了渲染效率。以下是关于 Unity 动态图集技术的详细介绍和实现方法。

动态图集的优势

  1. 减少纹理切换:在渲染过程中,频繁的纹理切换会导致性能下降。通过将多个小纹理合并到一个大纹理中,可以减少纹理切换的次数。
  2. 提高渲染效率:减少纹理切换和批处理次数,可以显著提高渲染效率,特别是在移动设备上。
  3. 简化资源管理:动态图集可以简化纹理资源的管理,使得纹理的加载和使用更加高效。

实现动态图集

在 Unity 中实现动态图集可以通过以下几种方法:

1. 使用 Unity 的 Sprite Atlas

Unity 提供了内置的 Sprite Atlas 工具,可以方便地创建和管理图集。以下是使用 Sprite Atlas 的步骤:

  1. 创建 Sprite Atlas

    • 在 Unity 编辑器中,右键点击项目窗口,选择 Create > 2D > Sprite Atlas 创建一个新的 Sprite Atlas。
  2. 添加精灵到图集

    • 选择创建的 Sprite Atlas,在 Inspector 窗口中,点击 Objects for Packing 下的 + 按钮,将需要合并的精灵(Sprite)拖动到列表中。
  3. 打包图集

    • 点击 Pack Preview 按钮,Unity 会自动将精灵合并到一个图集中。
  4. 使用图集中的精灵

    • 在代码或编辑器中使用图集中的精灵时,Unity 会自动从图集中提取相应的纹理。
2. 动态生成图集

如果需要在运行时动态生成图集,可以使用 Texture2DRect 类来实现。以下是一个简单的示例,展示了如何在运行时动态生成图集:

using UnityEngine;
using System.Collections.Generic;

public class DynamicAtlas : MonoBehaviour
{
    public List<Texture2D> textures; // 要合并的纹理列表
    public int atlasWidth = 1024; // 图集宽度
    public int atlasHeight = 1024; // 图集高度

    private Texture2D atlasTexture;
    private List<Rect> uvRects;

    void Start()
    {
        GenerateAtlas();
    }

    void GenerateAtlas()
    {
        atlasTexture = new Texture2D(atlasWidth, atlasHeight);
        uvRects = new List<Rect>();

        for (int i = 0; i < textures.Count; i++)
        {
            Texture2D texture = textures[i];
            Rect uvRect = new Rect(0, 0, texture.width, texture.height);
            uvRects.Add(uvRect);

            atlasTexture.SetPixels((int)uvRect.x, (int)uvRect.y, texture.width, texture.height, texture.GetPixels());
        }

        atlasTexture.Apply();
    }

    void OnGUI()
    {
        // 在屏幕上显示图集
        GUI.DrawTexture(new Rect(0, 0, atlasWidth, atlasHeight), atlasTexture);
    }
}
3. 使用第三方插件

除了 Unity 内置的工具和自定义实现外,还有一些第三方插件可以帮助你更方便地创建和管理动态图集。例如:

  • TexturePacker:一个流行的纹理打包工具,支持多种平台和格式,可以与 Unity 无缝集成。
  • Asset Store 插件:Unity Asset Store 上有许多插件提供了动态图集的功能,可以根据需要选择合适的插件。

动态图集的注意事项

  1. 纹理大小限制:在创建图集时,需要注意图集的大小限制。不同平台对纹理大小有不同的限制,通常建议图集的大小不超过 2048x2048 或 4096x4096。
  2. 纹理压缩:为了减少内存占用和提高加载速度,可以对图集进行纹理压缩。Unity 提供了多种纹理压缩格式,可以根据平台选择合适的压缩格式。
  3. 纹理边缘处理:在合并纹理时,需要注意纹理边缘的处理,避免出现纹理缝

动态图集pipeline

游戏应用逻辑根据资源路径向动态图集服务请求icon。
如果该icon已经在该图集中,直接返回给程序使用;否则进入第3步。
去硬盘上请求加载该icon。
加载完成以后将该icon载入内存。
动态图集服务会根据某种分块算法,在其剩余可用的区域里划分一块矩形区域,同时将第4步中的icon像素拷贝到该矩形区域。
将关联第5步中矩形区域的sprite返回给应用程序使用。
在该6个步骤中,第5步是核心步骤,因为分块算法的好坏和像素的拷贝直接决定该pipeline的性能瓶颈。

动态生成图集流程

动态生成图集(Dynamic Atlas)的流程涉及多个步骤,包括纹理的收集、图集的创建、纹理的合并以及图集的使用。以下是一个详细的流程和示例代码,展示如何在 Unity 中动态生成图集。

动态生成图集的流程

  1. 收集纹理:收集需要合并到图集中的所有纹理。
  2. 创建图集:创建一个空的图集纹理(Texture2D)。
  3. 合并纹理:将收集到的纹理逐个合并到图集中,并记录每个纹理在图集中的位置(UV 坐标)。
  4. 应用图集:将生成的图集应用到需要使用的对象上。

示例代码

以下是一个完整的示例代码,展示了如何在 Unity 中动态生成图集:

using UnityEngine;
using System.Collections.Generic;

public class DynamicAtlasGenerator : MonoBehaviour
{
    public List<Texture2D> textures; // 要合并的纹理列表
    public int atlasWidth = 1024; // 图集宽度
    public int atlasHeight = 1024; // 图集高度
    public int padding = 2; // 纹理之间的间距

    private Texture2D atlasTexture;
    private List<Rect> uvRects;

    void Start()
    {
        GenerateAtlas();
    }

    void GenerateAtlas()
    {
        // 创建一个空的图集纹理
        atlasTexture = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);
        uvRects = new List<Rect>();

        // 初始化图集纹理为透明
        Color[] clearPixels = new Color[atlasWidth * atlasHeight];
        for (int i = 0; i < clearPixels.Length; i++)
        {
            clearPixels[i] = Color.clear;
        }
        atlasTexture.SetPixels(clearPixels);

        // 当前图集的填充位置
        int currentX = padding;
        int currentY = padding;
        int maxRowHeight = 0;

        // 合并纹理到图集中
        foreach (Texture2D texture in textures)
        {
            if (currentX + texture.width + padding > atlasWidth)
            {
                // 换行
                currentX = padding;
                currentY += maxRowHeight + padding;
                maxRowHeight = 0;
            }

            if (currentY + texture.height + padding > atlasHeight)
            {
                Debug.LogError("图集空间不足,无法容纳所有纹理");
                break;
            }

            // 复制纹理到图集
            atlasTexture.SetPixels(currentX, currentY, texture.width, texture.height, texture.GetPixels());

            // 记录纹理在图集中的位置
            Rect uvRect = new Rect((float)currentX / atlasWidth, (float)currentY / atlasHeight, (float)texture.width / atlasWidth, (float)texture.height / atlasHeight);
            uvRects.Add(uvRect);

            // 更新当前填充位置
            currentX += texture.width + padding;
            maxRowHeight = Mathf.Max(maxRowHeight, texture.height);
        }

        // 应用图集纹理
        atlasTexture.Apply();

        // 在屏幕上显示图集
        GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
        quad.transform.localScale = new Vector3(10, 10, 1);
        quad.GetComponent<Renderer>().material.mainTexture = atlasTexture;
    }

    void OnGUI()
    {
        // 在屏幕上显示图集
        GUI.DrawTexture(new Rect(0, 0, atlasWidth, atlasHeight), atlasTexture);
    }
}

详细解释

  1. 收集纹理

    • public List<Texture2D> textures;:定义一个列表来存储需要合并的纹理。
  2. 创建图集

    • atlasTexture = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);:创建一个空的图集纹理。
    • 初始化图集纹理为透明:使用 SetPixels 方法将图集纹理初始化为透明。
  3. 合并纹理

    • 使用嵌套的循环遍历所有纹理,并将它们逐个复制到图集中。
    • 记录每个纹理在图集中的位置(UV 坐标),以便后续使用。
  4. 应用图集

    • atlasTexture.Apply();:调用 Apply 方法将所有的像素更改应用到图集纹理中。
    • 在屏幕上显示图集:创建一个四边形(Quad)并将图集纹理应用到其材质上,以便在场景中可视化图集。

使用图集中的纹理

在生成图集后,你可能需要在游戏中使用图集中的纹理。为了方便管理和使用这些纹理,可以创建一个简单的管理器类来存储纹理的 UV 坐标,并提供获取纹理 UV 坐标的方法。

以下是一个示例,展示如何使用生成的图集和 UV 坐标:

using UnityEngine;
using System.Collections.Generic;

public class DynamicAtlasManager : MonoBehaviour
{
    public DynamicAtlasGenerator atlasGenerator; // 引用动态图集生成器
    private Dictionary<string, Rect> textureUVs; // 存储纹理名称和 UV 坐标的字典

    void Start()
    {
        textureUVs = new Dictionary<string, Rect>();

        // 假设纹理名称与列表中的索引对应
        for (int i = 0; i < atlasGenerator.textures.Count; i++)
        {
            string textureName = atlasGenerator.textures[i].name;
            Rect uvRect = atlasGenerator.uvRects[i];
            textureUVs.Add(textureName, uvRect);
        }
    }

    public Rect GetTextureUV(string textureName)
    {
        if (textureUVs.ContainsKey(textureName))
        {
            return textureUVs[textureName];
        }
        else
        {
            Debug.LogError("纹理名称不存在:" + textureName);
            return Rect.zero;
        }
    }
}

使用图集中的纹理示例

假设你有一个 2D 游戏,并且需要在精灵渲染器(SpriteRenderer)中使用图集中的纹理。以下是一个示例,展示如何使用 DynamicAtlasManager 获取纹理的 UV 坐标,并应用到精灵渲染器中:

using UnityEngine;

public class SpriteFromAtlas : MonoBehaviour
{
    public DynamicAtlasManager atlasManager; // 引用动态图集管理器
    public string textureName; // 要使用的纹理名称

    void Start()
    {
        // 获取图集纹理
        Texture2D atlasTexture = atlasManager.atlasGenerator.atlasTexture;

        // 获取纹理的 UV 坐标
        Rect uvRect = atlasManager.GetTextureUV(textureName);

        // 创建一个新的精灵
        Sprite sprite = Sprite.Create(atlasTexture, new Rect(uvRect.x * atlasTexture.width, uvRect.y * atlasTexture.height, uvRect.width * atlasTexture.width, uvRect.height * atlasTexture.height), new Vector2(0.5f, 0.5f));

        // 应用精灵到精灵渲染器
        SpriteRenderer spriteRenderer = GetComponent<SpriteRenderer>();
        spriteRenderer.sprite = sprite;
    }
}

详细解释

  1. DynamicAtlasManager

    • textureUVs:一个字典,用于存储纹理名称和对应的 UV 坐标。
    • Start 方法:在开始时,将 DynamicAtlasGenerator 中的纹理名称和 UV 坐标存储到字典中。
    • GetTextureUV 方法:提供一个方法,根据纹理名称获取对应的 UV 坐标。
  2. SpriteFromAtlas

    • atlasManager:引用 DynamicAtlasManager,用于获取图集和 UV 坐标。
    • textureName:要使用的纹理名称。
    • Start 方法:在开始时,获取图集纹理和对应的 UV 坐标,创建一个新的精灵,并将其应用到精灵渲染器中。

总结

动态生成图集是一种有效的优化技术,可以减少纹理切换,提高渲染效率。通过收集纹理、创建图集、合并纹理和应用图集,可以在 Unity 中实现动态图集的生成和使用。通过管理器类,可以方便地管理和使用图集中的纹理,从而在游戏中实现高效的纹理管理和渲染。

动态生成图集性能问题

在 Unity 中动态生成图集虽然可以带来很多好处,但也可能引发一些性能问题。以下是一些常见的性能问题及其解决方案:

性能问题

  1. 图集生成时间

    • 动态生成图集需要时间,特别是当有大量纹理需要合并时,可能会导致明显的延迟。
    • 解决方案:尽量在非关键帧或加载屏幕期间生成图集,避免在游戏运行时频繁生成图集。
  2. 内存占用

    • 动态生成图集会占用额外的内存,特别是当图集尺寸较大时。
    • 解决方案:合理规划图集的尺寸,避免生成过大的图集。可以考虑将图集分割成多个较小的图集。
  3. 纹理上传到 GPU

    • 每次生成或更新图集后,需要将图集纹理上传到 GPU,这可能会导致性能瓶颈。
    • 解决方案:尽量减少图集的更新频率,避免频繁上传纹理到 GPU。
  4. 纹理拼接和边缘问题

    • 在合并纹理时,可能会出现纹理拼接不当或边缘渗色的问题。
    • 解决方案:在生成图集时添加适当的边距(padding),并使用纹理边缘扩展技术。

优化策略

  1. 预生成图集

    • 尽量在编辑器中预生成图集,而不是在运行时动态生成。可以使用 Unity 的 Sprite Packer 或第三方工具如 TexturePacker 来预生成图集。
  2. 异步生成图集

    • 如果必须在运行时生成图集,可以考虑使用异步方法来生成图集,避免阻塞主线程。
    • 示例代码:
      using UnityEngine;
      using System.Collections;
      using System.Collections.Generic;
      
      public class AsyncDynamicAtlasGenerator : MonoBehaviour
      {
          public List<Texture2D> textures;
          public int atlasWidth = 1024;
          public int atlasHeight = 1024;
          public int padding = 2;
      
          private Texture2D atlasTexture;
          private List<Rect> uvRects;
      
          void Start()
          {
              StartCoroutine(GenerateAtlasAsync());
          }
      
          IEnumerator GenerateAtlasAsync()
          {
              atlasTexture = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);
              uvRects = new List<Rect>();
      
              Color[] clearPixels = new Color[atlasWidth * atlasHeight];
              for (int i = 0; i < clearPixels.Length; i++)
              {
                  clearPixels[i] = Color.clear;
              }
              atlasTexture.SetPixels(clearPixels);
      
              int currentX = padding;
              int currentY = padding;
              int maxRowHeight = 0;
      
              foreach (Texture2D texture in textures)
              {
                  if (currentX + texture.width + padding > atlasWidth)
                  {
                      currentX = padding;
                      currentY += maxRowHeight + padding;
                      maxRowHeight = 0;
                  }
      
                  if (currentY + texture.height + padding > atlasHeight)
                  {
                      Debug.LogError("图集空间不足,无法容纳所有纹理");
                      break;
                  }
      
                  atlasTexture.SetPixels(currentX, currentY, texture.width, texture.height, texture.GetPixels());
      
                  Rect uvRect = new Rect((float)currentX / atlasWidth, (float)currentY / atlasHeight, (float)texture.width / atlasWidth, (float)texture.height / atlasHeight);
                  uvRects.Add(uvRect);
      
                  currentX += texture.width + padding;
                  maxRowHeight = Mathf.Max(maxRowHeight, texture.height);
      
                  yield return null; // 每次处理一个纹理后暂停,避免阻塞主线程
              }
      
              atlasTexture.Apply();
          }
      
          void OnGUI()
          {
              GUI.DrawTexture(new Rect(0, 0, atlasWidth, atlasHeight), atlasTexture);
          }
      }
      
  3. 分块生成图集

分块生成图集是一种将大图集分割成多个较小图集的方法,以减少单个图集的生成和更新时间。这种方法可以有效地管理内存和提高性能。

以下是一个示例代码,展示如何实现分块生成图集:

using UnityEngine;
using System.Collections.Generic;

public class ChunkedAtlasGenerator : MonoBehaviour
{
    public List<Texture2D> textures; // 要合并的纹理列表
    public int chunkSize = 512; // 每个图集块的大小
    public int padding = 2; // 纹理之间的间距

    private List<Texture2D> atlasTextures; // 存储生成的图集块
    private List<List<Rect>> uvRectsList; // 存储每个图集块中纹理的 UV 坐标

    void Start()
    {
        GenerateChunkedAtlas();
    }

    void GenerateChunkedAtlas()
    {
        atlasTextures = new List<Texture2D>();
        uvRectsList = new List<List<Rect>>();

        int currentX = padding;
        int currentY = padding;
        int maxRowHeight = 0;

        Texture2D currentAtlas = CreateNewAtlas();
        List<Rect> currentUVRects = new List<Rect>();

        foreach (Texture2D texture in textures)
        {
            if (currentX + texture.width + padding > chunkSize)
            {
                // 换行
                currentX = padding;
                currentY += maxRowHeight + padding;
                maxRowHeight = 0;
            }

            if (currentY + texture.height + padding > chunkSize)
            {
                // 当前图集块已满,创建新的图集块
                atlasTextures.Add(currentAtlas);
                uvRectsList.Add(currentUVRects);

                currentAtlas = CreateNewAtlas();
                currentUVRects = new List<Rect>();

                currentX = padding;
                currentY = padding;
                maxRowHeight = 0;
            }

            // 复制纹理到当前图集块
            currentAtlas.SetPixels(currentX, currentY, texture.width, texture.height, texture.GetPixels());

            // 记录纹理在当前图集块中的位置
            Rect uvRect = new Rect((float)currentX / chunkSize, (float)currentY / chunkSize, (float)texture.width / chunkSize, (float)texture.height / chunkSize);
            currentUVRects.Add(uvRect);

            // 更新当前填充位置
            currentX += texture.width + padding;
            maxRowHeight = Mathf.Max(maxRowHeight, texture.height);
        }

        // 添加最后一个图集块
        atlasTextures.Add(currentAtlas);
        uvRectsList.Add(currentUVRects);

        // 应用所有图集块
        foreach (var atlas in atlasTextures)
        {
            atlas.Apply();
        }
    }

    Texture2D CreateNewAtlas()
    {
        Texture2D atlas = new Texture2D(chunkSize, chunkSize, TextureFormat.RGBA32, false);
        Color[] clearPixels = new Color[chunkSize * chunkSize];
        for (int i = 0; i < clearPixels.Length; i++)
        {
            clearPixels[i] = Color.clear;
        }
        atlas.SetPixels(clearPixels);
        return atlas;
    }

    void OnGUI()
    {
        // 在屏幕上显示所有图集块
        for (int i = 0; i < atlasTextures.Count; i++)
        {
            GUI.DrawTexture(new Rect(i * chunkSize, 0, chunkSize, chunkSize), atlasTextures[i]);
        }
    }
}

详细解释

  1. 创建新图集块

    • CreateNewAtlas 方法用于创建一个新的图集块,并初始化为透明。
  2. 生成分块图集

    • GenerateChunkedAtlas 方法遍历所有纹理,将它们逐个复制到当前图集块中。
    • 如果当前图集块已满,则创建一个新的图集块,并继续复制剩余的纹理。
  3. 记录 UV 坐标

    • 每个纹理在图集块中的位置(UV 坐标)被记录下来,以便后续使用。
  4. 应用图集块

    • 调用 Apply 方法将所有的像素更改应用到每个图集块中。
  5. 显示图集块

    • OnGUI 方法中,使用 GUI.DrawTexture 方法在屏幕上显示所有生成的图集块。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值