一、描述
1. 这是一个什么效果?
类似于PS里面的图层混合,这将在游戏运行过程中将多个Sprite合并为一个Sprite,可以应用于2D游戏物体或UI。
类似以下的效果:
需要注意的是在图片叠加时不仅仅是覆盖,还可以应用任意的图片混合类型。
2. 这有什么用?
你可能会说,不就个笑脸加个圆吗,我直接加两个游戏物体一边挂一个不就完事儿了吗?我直接PS混合导出不就完事了吗?
确实,如果你的图片叠加的情况数少且可确定,我建议还是直接用PS,或直接挂在几个游戏物体上做成预制体。
但如果叠加情况数多或者层数不确定,无论是PS还是做成预制体,相比之下都非常不优雅了。
试想:我有10张图,游戏进程中可能需要在其中选x张混合成一张
- 如果用PS导出,则需要预先导出张图,别说人工导出过程复杂,连硬盘都要蚌埠住了,而且游戏中并不一定出现全部情况,其实许多都是无用功。
- 如果用子物体,那么就需要在一瞬间生成10个子物体,而且很可能同时需要存在多个类似效果,那么场景就会多达成百上千个物体,届时电脑也直接蚌埠住了。
因此比较优雅的是,在代码里动态混合图片素材,游戏运行时,一个效果只挂载到一个游戏物体上。 这就是这篇文章需要达到的最终目的。
二、准备工作
1. 图片素材
准备需要在游戏中混合的图片素材,确保每张大小一致。
如不一致请使用PS等软件补充透明区域,再重新裁剪、导出为一致大小。
这是因为在代码里处理大小不一的图片比较复杂。
当然如果你有自己的想法,也可以大胆尝试,我是怕了。
2. 导入Unity
将所有素材拖入Unity。全选素材图片并在Inspector中勾选Read/Write Enabled
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:
好,现在就假定你已经获取到了你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)
因此我觉得用这种动态缝合非常爽
比如这个 冰火两重天
六、参考资料
How to Combine Sprites in Unity!Modular Textures in Unity