[Unity]2D颜料泼溅效果

做一个类似于《INK》的2D颜料泼溅效果:

INK
效果

表面与颜料

利用模板测试,让颜料污渍能在物体表面上重叠显示且不超出物体轮廓。在物体表面的Shader中,总是通过模板测试并替换参考值,同时裁剪掉透明度为0的像素;颜料污渍的Shader中,如果与参考值相等则通过测试。

项目用了URP,这里直接将Sprite-Lit-Default.shader拷贝两份出来修改。

被染色表面Surface-Lit.shader

SubShader中加入Stencil配置

SubShader
{
    Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }

    Stencil
    {
        Ref 2
        Comp Always
        Pass Replace
    }
    ...

透明度裁剪在原来的Sprite-Lit-Default.shader中已经做好了所以不用再写。

颜料污渍Stain-Lit.shader

SubShader
{
    Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }

    Stencil
    {
        Ref 2
        Comp Equal
    }
    ...

建一个场景测试效果,Tilemap使用Surface-Lit材质,污渍Sprite使用Stain材质,二者在同一个Sorting Layer,Tilemap的Order in Layer需要比污渍Sprite小。

Tilemap:

颜料污渍:

颜料污渍已经可以显示在Tilemap上并且不会超出其轮廓。

喷溅

添加一把玩具水枪,简单写一个发射颜料子弹的逻辑,子弹爆开后围绕当前位置随机生成多个颜料污渍预制体。颜料污渍预制体用一张1像素的白色图片,通过缩放来显示出不同大小的污渍。子弹中设置多种颜色随机应用到子弹与污渍的SpriteRenderer。

Stain Radius为喷溅半径,Stain Scale为最大污渍缩放。

编写StainGenerator类用来生成颜料污渍:

public class StainGenerator
{
    /// <summary>
    /// 在中心点生成向四周发散的污渍
    /// </summary>
    /// <param name="prefab">污渍预制体</param>
    /// <param name="color">颜色</param>
    /// <param name="position">中心点位置</param>
    /// <param name="direction">冲击方向</param>
    /// <param name="scale">污渍缩放</param>
    /// <param name="radius">污渍分布半径</param>
    public static void Generate(GameObject prefab, Color color, Vector3 position, Vector3 direction, float scale, float radius)
    {
        // 以position为中心分裂到若干个方向,每个分裂的角度随机
        int splitNum = Random.Range(4, 9);  // 分裂数量随机,数值暂时写死
        Vector3[] splitDirs = new Vector3[splitNum];
        float angleDelta = 360f / splitNum;
        for (int i = 0; i < splitDirs.Length; i++)
        {
            var lastDir = i == 0? direction : splitDirs[i - 1];
            var angle = RandomNum(angleDelta, .2f);
            splitDirs[i] = Quaternion.AngleAxis(angle, Vector3.forward) * lastDir;
        }

        // 每个分裂方向生成若干个污渍
        foreach (var dir in splitDirs)
        {
            int stainNum = Random.Range(3, 6);
            float stainScale;    // 污渍
            float radiusDelta = radius / 6f;    // 每个污渍间距
            Vector3 stainPos = position;    // 污渍位置
            for (int i = 0; i < stainNum; i++)
            {
                stainScale = scale - (i * scale / stainNum);    // 缩放随距离衰减
                stainPos += dir * RandomNum(radiusDelta, .4f);
                stainPos += (Vector3) Random.insideUnitCircle * RandomNum(radiusDelta * .2f, radiusDelta * .1f); // 位置随机
                var go = Object.Instantiate(prefab);
                go.transform.position = stainPos;
                go.transform.right = dir;
                go.transform.localScale = new Vector3(stainScale, stainScale, 1f);
                go.GetComponent<SpriteRenderer>().color = color;
            }
        }
    }

    public static float RandomNum(float num, float randomness)
    {
        return num + Random.Range(-num * randomness, num * randomness);
    }
}

子弹与地面发生碰撞时调用,传入污渍预制体、颜色、冲击位置、冲击方向、自定义的污渍缩放与喷溅半径:

private void OnCollisionEnter2D(Collision2D other) 
{
    ...
    StainGenerator.Generate(stainPrefab, color, transform.position, transform.right, stainScale, stainRadius);
}

比较简单的实现就完成了,但这种不断生成Sprite的方法将导致场景里分分钟就会多出上千个游戏对象。

优化

由于污渍的材质都一样,Unity对它们做了动态合批,这里是否还可以通过自定义mesh来优化,暂时没有什么头猪。

好在对游戏对象的数量优化还是比较简单的,打在同一个位置的颜料污渍将会发生重叠,被遮挡住的污渍是不再需要的。定义一个同一位置最大可叠加层数,在创建新的污渍时,先判断当前位置共叠加了几层,如果超过允许的最大层数,则将最底层的污渍对象回收,再从对象池中取出已回收的污渍对象重复利用。

这种做法的缺点是只判断重叠,而不是判断污渍是否被完全覆盖,显示效果上不太好,最大层数设置较低时会出现有些污渍还未被完全覆盖,却依然被回收了的情况。

检测重叠可以使用SpriteRenderer中Bounds的Intersects方法,但每次生成都要遍历当前所有污渍对象做判断,感觉过于繁琐。最终还是选择用了Physics2D.OverlapBoxAll,这样要先在污渍预制体中添加BoxCollider2D。

修改StainGenerator,令它继承MonoBehaviour。叠加层数利用SpriteRenderer的Order in Layer属性实现,假设三个污渍重叠,且它们的Order in Layer分别是2、3、4,如果此时已达到了最大叠加层数,同时有新的污渍即将覆盖它们,则回收Order为2的,其余的Order减1,新的污渍Order设为4,这样依然能保持2、3、4的重叠顺序。

public class StainGenerator : MonoBehaviour
{
    
    public static StainGenerator Instance { get; private set;}

    [Tooltip("污渍Prefab")]
    public GameObject prefab;
    [Tooltip("最小Order in Layer")]
    public int minOrderInLayer;
    [Tooltip("最大Order in Layer")]
    public int maxOrderInLayer;
    [Tooltip("污渍大小")]
    public Vector2 stainSize;

    // 污渍对象池
    [SerializeField] List<GameObject> stainPool;
    // 临时污渍对象列表,用来记录本次生成已处理过的对象,避免重复处理
    List<GameObject> tempStains;

    void Awake() 
    {
        // 初始化单例和列表
        ...
    }  
    ...
}

修改Generate方法,去除多余的参数,仅改动污渍对象生成部分,子弹碰撞中相应修改对其的调用。

public void Generate(Color color, Vector3 position, Vector3 direction, float scale, float radius)
{
    // 以position为中心分裂到若干个方向,每个分裂的角度随机
    ...
    // 每个分裂方向生成若干个污渍
    tempStains.Clear(); // 每次生成时清空临时列表
    foreach (var dir in splitDirs)
    {
        ...
        for (int i = 0; i < stainNum; i++)
        {
            ...
            // var go = Object.Instantiate(prefab);
            var go = GetStain(stainPos, stainScale, dir);   // 替换为GetStain方法
            ...
        }
    }
}

编写GetStain方法。

/// <summary>
/// 获取当前污渍对象,若当前位置发生重叠则调整回收
/// </summary>
/// <param name="pos">位置</param>
/// <param name="scale">缩放</param>
/// <param name="dir">朝向</param>
/// <returns></returns>
GameObject GetStain(Vector3 pos, float scale, Vector3 dir)
{
    int order = minOrderInLayer;   // 当前污渍需要设置的sortingOrder
    var angle = Vector2.SignedAngle(Vector2.right, dir);
    var size = stainSize * scale;   // 实际大小
    var cols = Physics2D.OverlapBoxAll(pos, size, angle, LayerMask.GetMask("Stain"));
    if (cols.Length != 0)
    {
        // 若检测到污渍重叠,获取当前最顶层污渍的sortingOrder
        SpriteRenderer spriteRenderer;
        foreach (var item in cols)
        {
            spriteRenderer = item.GetComponent<SpriteRenderer>();
            if (spriteRenderer.sortingOrder > order)
            {
                order = spriteRenderer.sortingOrder;
                // 如果即将超出最大叠加层数则直接快进到处理重叠的污渍
                if (order + 1 > maxOrderInLayer)
                    break;
            }
        }
        if (order + 1 > maxOrderInLayer)
        {
            // 回收最底层的污渍,其余层sortingOrder减1,并标记为已处理,避免被重复处理
            foreach (var item in cols)
            {
                spriteRenderer = item.GetComponent<SpriteRenderer>();
                if (spriteRenderer.sortingOrder == minOrderInLayer)
                    item.gameObject.SetActive(false);
                else if (!tempStains.Contains(item.gameObject))
                {
                    spriteRenderer.sortingOrder--;
                    tempStains.Add(item.gameObject);
                }
            }
        }
        order = Mathf.Clamp(order + 1, minOrderInLayer, maxOrderInLayer);
    }

    // 简易对象池
    GameObject go = null; 
    foreach (var item in stainPool)
    {
        if (!item.activeInHierarchy)
        {
            go = item;
            go.SetActive(true);
            break;
        }
    }
    if (go == null)
    {
        go = Instantiate(prefab, transform);
        stainPool.Add(go);
    }
    go.GetComponent<SpriteRenderer>().sortingOrder = order;
    
    return go;
}

脚本挂到场景中,设置相应值:

调整之后:

调整后

和调整前对比:

调整前

可以发现不再生成那么多对象了,帧数也相对稳定,但出现了上面提到的显示问题,将最大叠加层数调高基本可以解决。

用到的素材:Cavernas by Adam Saltsman8 Guns + Projectiles by KingKelpo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容