一、描述

1. 这是一个什么效果?

类似于PS里面的图层混合,这将在游戏运行过程中将多个Sprite合并为一个Sprite,可以应用于2D游戏物体或UI

类似以下的效果:

unity 生成精灵 unity精灵图_游戏开发


需要注意的是在图片叠加时不仅仅是覆盖,还可以应用任意的图片混合类型

2. 这有什么用?

你可能会说,不就个笑脸加个圆吗,我直接加两个游戏物体一边挂一个不就完事儿了吗?我直接PS混合导出不就完事了吗?
确实,如果你的图片叠加的情况数少且可确定,我建议还是直接用PS,或直接挂在几个游戏物体上做成预制体。

但如果叠加情况数多或者层数不确定,无论是PS还是做成预制体,相比之下都非常不优雅了。

试想:我有10张图,游戏进程中可能需要在其中选x张混合成一张

  • 如果用PS导出,则需要预先导出unity 生成精灵 unity精灵图_unity_02张图,别说人工导出过程复杂,连硬盘都要蚌埠住了,而且游戏中并不一定出现全部情况,其实许多都是无用功。
  • 如果用子物体,那么就需要在一瞬间生成10个子物体,而且很可能同时需要存在多个类似效果,那么场景就会多达成百上千个物体,届时电脑也直接蚌埠住了

因此比较优雅的是,在代码里动态混合图片素材,游戏运行时,一个效果只挂载到一个游戏物体上。 这就是这篇文章需要达到的最终目的。

二、准备工作

1. 图片素材

准备需要在游戏中混合的图片素材,确保每张大小一致
如不一致请使用PS等软件补充透明区域,再重新裁剪、导出为一致大小。

这是因为在代码里处理大小不一的图片比较复杂。
当然如果你有自己的想法,也可以大胆尝试,我是怕了。

2. 导入Unity

将所有素材拖入Unity。全选素材图片并在Inspector中勾选Read/Write Enabled

unity 生成精灵 unity精灵图_混合模式_03

3. 创建脚本

创建用于执行该操作的脚本。生成的过程只需要访问素材和混合规则。
因此无需继承MonoBehaviour,设置为static类即可。

using UnityEngine;
public static class SpriteCreator
{
    //将在此添加所有字段和方法
}

三、脚本编写

1. 函数参数

最关键的就是生成的函数了,它需要返回最终生成的Sprite。

参数用于控制最终Sprite的样式,因此应包含所有生成规则,比如指定叠加的源?叠加几次?混合模式? 这由你决定

public static Sprite GetSprite(/*添加你的生成规则,或传入素材*/)
{

}

2. 获取、创建Texture2D

Texture2D是2D图片的材质,说白了它记录了一张图片所有像素点的颜色数据(以及其它信息)。我们需要读取素材Sprite的Texture2D,并以此为依据生成新的Texture2D。

获取Sprite的Texture2D:
如果你有一个名为mySprite的Sprite类型引用,可以这么做:

Texture2D texFromSprite = mySprite.texture;

事实上,你可以通过定义Texture2D的变量并序列化至Inspector,然后拖动你的Sprite到该变量位置,可以直接提取到图片的Texture2D:

unity 生成精灵 unity精灵图_游戏开发_04


好,现在就假定你已经获取到了你Sprite的Texture2D啦

然后我们需要创建新的Texture2D,用于存放我们组合后的材质
因此生成函数的画风现在是这样的:

public static Sprite GetSprite(/*添加你的生成规则,或传入素材*/)
{
	Texture2D background = //你的Texture2D,这是叠在下面的那张
	Texture2D cover = //你的Texture2D,这是叠在上面的那张
	//新tex,大小用哪个都无所谓,因为之前保证了所有素材大小一致
	Texture2D newTex = new Texture2D(background.width, background.height);
}

3. 遍历像素

我们需要遍历每个像素点,并处理它们各自的颜色:

先获得素材的颜色
Texture2D中有两个获取颜色的函数,分别是
Color GetPixel(int x, int y):获取指定坐标值的颜色
Color[] GetPixels():按行遍历顺序获取所有坐标值的颜色

为了减少函数调用,这里直接用GetPixels一次性全部获取
类似地,设置新Texture2D的颜色时也一次性全部设置,以减少函数调用
因此需要定义3个数组存放颜色

开始处理

Color[] bgColors = background.GetPixels();
Color[] cvColors = cover.GetPixels();
Color[] newColors = new Color[background.width * background.height];

for (int x = 0; x < newTex.width; x++)
    for (int y = 0; y < newTex.height; y++)
    {
        int index = x + y * background.width;
        //覆盖背景层
        newColors[index] = bgColors[index];
        //覆盖封面层
        newColors[index] = cvColors[index];//这合理吗?
    }

上面的“这合理吗”合理吗?
那必不合理,因为假如背景层是一张涩图,但是封面层是完全透明的。一次循环下来,涩图全被透明的像素点覆盖了,打码也不需要这么赶尽杀绝吧?(bushi)

上述情况,我们是不希望封面的透明像素点覆盖背景的
总之,我们需要加入图片的混合模式,而不是单纯的覆盖。

4. 混合模式

混合模式不仅能解决上述的问题,还能制造出多种好康的效果。比如Photoshop中的图层类型,除了接下来要讲的“正常”以外,还有“溶解”、“变暗”等多种混合效果,这些东西其实都可以在代码里实现,只是混合的公式不同。

正常混合
混合出来的结果和PS“正常”模式一样。当不透明时封面完全取代背景,当有透明时背景会透过封面。

从视觉上讲,正常混合下,背景不会影响封面层(封面透明时也没被影响,因为透明本身就是封面的特性,它自然会使背景颜色透过自身,背景颜色并没有影响透明颜色)

混合函数
其实就是直接套公式
公式详见我的这篇文章 这里为了简化,直接使用第二条 (仅背景色为完全不透明的情形适用)

//使用方程比较清晰,如果追求效率可以直接内嵌在循环中
static Color NormalBlend(Color background, Color cover)
{
    float CoverAlpha = cover.a;
    Color blendColor;
    blendColor.r = cover.r * CoverAlpha + background.r * (1 - CoverAlpha);
    blendColor.g = cover.g * CoverAlpha + background.g * (1 - CoverAlpha);
    blendColor.b = cover.b * CoverAlpha + background.b * (1 - CoverAlpha);
    blendColor.a = 1;
    return blendColor;
}

更新循环

for (int x = 0; x < newTex.width; x++)
    for (int y = 0; y < newTex.height; y++)
    {
        int index = x + y * background.width;
        /* removed
        //覆盖背景层
        newColors[index] = bgColors[index];
        //覆盖封面层
        newColors[index] = cvColors[index];//这合理吗?
        */
        //混合背景和封面
        newColors[index] = NormalBlend(bgColors[index], cvColors[index]);
    }

其它混合模式
你可以使用其它混合模式代替NormalBlend的操作,以达到各种效果,具体的公式就不在此展开了。

5. 完成更改

使用SetPixels(Color[])对新的Texture2D设置像素信息,并且:
记得调用Apply(),否则不会生效!!!
记得调用Apply(),否则不会生效!!!
记得调用Apply(),否则不会生效!!!

newTex.SetPixels(newColors);
newTex.Apply();

由Texture创建Sprite
利用Sprite.Create函数,有三个参数
第一个是Texture2D源
第二个是Rect,作为Sprite的大小
第三个是Vector2,Sprite的锚点,取值空间是(0,0)~(1,1),设为中心使用(0.5,0.5)即可

Sprite newSprite = Sprite.Create(newTex, new Rect(0, 0, newTex.width, newTex.height), new Vector2(0.5f, 0.5f));
return newSprite;

6. 脚本模板

整合了上面说的所有。
想直接使用那必不可能,但是可以利用这个模板快速加入自己的逻辑

using UnityEngine;
public static class SpriteCreator
{
    public static Sprite GetSprite(/*添加你的生成规则,或传入素材*/)
    {
        Texture2D background = ;//你的Texture2D,这是叠在下面的那张
        Texture2D cover = ;//你的Texture2D,这是叠在上面的那张
        //新tex,大小用哪个都无所谓,因为之前保证了所有素材大小一致
        Texture2D newTex = new Texture2D(background.width, background.height);

        Color[] bgColors = background.GetPixels();
        Color[] cvColors = cover.GetPixels();
        Color[] newColors = new Color[background.width * background.height];

        for (int x = 0; x < newTex.width; x++)
            for (int y = 0; y < newTex.height; y++)
            {
                int index = x + y * background.width;
                //混合背景和封面
                //注意:这个函数只适用于背景色完全不透明
                newColors[index] = NormalBlend(bgColors[index], cvColors[index]);
            }

        newTex.SetPixels(newColors);
        newTex.Apply();

        return Sprite.Create(newTex, new Rect(0, 0, newTex.width, newTex.height), new Vector2(0.5f, 0.5f));
    }
	
	//注意:这个函数只适用于背景色完全不透明
	//如果需要考虑背景色透明的函数,请看“混合模式”的链接
    static Color NormalBlend(Color background, Color cover)
    {
        float CoverAlpha = cover.a;
        Color blendColor;
        blendColor.r = cover.r * CoverAlpha + background.r * (1 - CoverAlpha);
        blendColor.g = cover.g * CoverAlpha + background.g * (1 - CoverAlpha);
        blendColor.b = cover.b * CoverAlpha + background.b * (1 - CoverAlpha);
        blendColor.a = 1;
        return blendColor;
    }
    
    //你可以增加其它Blend模式,替换NormalBlend即可
}

至此,我们结束了脚本的创建

四、一点优化

可以根据你创建Sprite的逻辑,用字典等方式把已创建的Sprite存储起来,当创建相同类型Sprite时,直接返回已有的Sprite,可以避免重复运算。

public static Sprite GetSprite(/*添加你的生成规则,或传入素材*/)
{
    if (/*已有相同Sprite*/)
        return /*字典或列表里面的Sprite*/;
    //否则照常创建Sprite
    //把Sprite加入字典或列表,下次有相同直接重用
    return newSprite;
}

五、我的应用

我把一个钻石分成了5个图片,分别是4个部分和1个光影,根据游戏逻辑给4个部分上不同的颜色,然后叠加上光影让它看起来没那么廉价(bushi)

因此我觉得用这种动态缝合非常爽

unity 生成精灵 unity精灵图_unity_05


unity 生成精灵 unity精灵图_游戏_06


unity 生成精灵 unity精灵图_unity 生成精灵_07


比如这个 冰火两重天

六、参考资料

How to Combine Sprites in Unity!Modular Textures in Unity