[Unity]带照明效果的2D激光束

2D中激光束算是比较常见了,实现起来也较为简单,但为了让它能真正达到照明的效果还是得花些功夫,这里记录一下实现过程。

整体思路:

  • 使用Line Renderer制作激光束。
  • 使用类型为Freeform的Light 2D实现光照,通过代码动态修改光照形状。
  • 加一些特技。

制作激光

激光束的实现思路基本参考油管上一个印度小哥的教程,有一些修改。

材质

先进行连连看环节,制作激光的材质。项目使用URP,新建一个Sprite Unlit Shader Graph,取名Laser。

图形部分,对Voronoi节点在x轴上稍微拉伸,并且让它随时间在x轴上偏移,这样看起来会有一种电流的感觉:

Speed属性控制运动速度,Scale属性控制拉伸。这里也可以根据需要使用其他的噪声图,好看就行。

激光的边缘需要有柔和渐变,通过sin(uv.y * PI)可以得到,再用指数函数控制边缘的厚度即可:

将两者相乘,再与颜色混合得到最终效果:

激光混合.png

颜色模式为HDR,在Bloom后处理下会有不错的效果;另外个人觉得有透明度更好些,所以顺便连接了Alpha。

Shader就做好了,以这个Shader新建一个材质Laser,调整各项参数:

Line Renderer

场景中新建一个名为Laser的物体,添加Line Renderer组件,拖入刚才的Laser材质;Texture Mode改为Tile,以避免不同激光长度下拉伸不一致的问题。

可以顺便在场景中新建一个Volume,开启Bloom后处理:

临时修改一下Positions,场景中可以看到效果:

交互

接下来让它可以随着角色施法而改变位置,这里角色使用的是Asset Store里的一个小魔女素材,自带骨骼动画和控制脚本。

期望效果是玩家点击鼠标左键,角色举起法杖,随后激光向鼠标方向发射。编写激光脚本,并挂在Laser物体下:

Laser2D.cs

[RequireComponent(typeof(LineRenderer))]
public class Laser2D : MonoBehaviour
{
    LineRenderer line;

    void Awake()
    {
        line = GetComponent<LineRenderer>();
        SetEnable(false);
    }

    public void SetEnable(bool b)
    {
        line.enabled = b;
    }

    public void SetPositions(Vector3 start, Vector3 end)
    {
        line.SetPosition(0, start);
        line.SetPosition(1, end);
    }
}

之后将在角色控制脚本中调用这些方法。

激光发射需要一个发射起始点,找到法杖的骨骼,在法杖头上添加发射点FirePoint:

做一个施法的骨骼动画,并在最后一帧添加动画事件,触发角色脚本中的OnCastAnim方法:

修改原有的角色控制脚本SimplePlayerController.cs,加入施法相关代码:

PlayerController.cs

public class PlayerController : MonoBehaviour
{
    ...
    public Transform firePoint;
    public Laser2D laser;
    public LayerMask laserBlockLayer;
    ...
    private void Update()
    {
        ...
        if (alive)
        {
            ...
            Cast();
            ...
        }
    }
    ...
    void Cast()
    {
        if (Input.GetMouseButton(0))
        {
            var mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            var direction = mousePos - firePoint.position;
            var hit = Physics2D.Raycast(firePoint.position, direction, float.PositiveInfinity, laserBlockLayer);
            if (hit)
            {
                laser.SetPositions(firePoint.position, hit.point);
                anim.SetBool("isCasting", true);
            }
        }
        else
        {
            laser.SetEnable(false);
            anim.SetBool("isCasting", false);
        }
    }

    public void OnCastAnim()
    {
        laser.SetEnable(true);
    }
}

这部分比较简单就不详细说明了,总之就是先这样这样,然后再那样那样。

Laser物体放到角色之下,为各个变量赋好值,可以看到初步效果:

光照

在Bloom效果下,这道激光看起来熠熠生辉,然而它并不能照亮周围的物体,为了让激光具有照明效果,还需要添加光源。

动态修改光照形状

给Laser物体添加一个Light 2D脚本,Light Type为Freeform,点击Edit Shape按钮可以编辑它的形状:

由于激光的形状会不断变化,固定的形状不能满足要求,因此需要在代码中根据激光的形状动态修改Light 2D的形状。

然而翻了一下API文档,Unity似乎并没有打算将形状属性开放给开发者修改,唯一和形状相关的属性只有一个shapePath,只允许get:

那么只能去Light 2D的源码中找找蛛丝马迹,在Light2DShape.cs中可以看到,shapePath被定义在Light2D的一个部分类中:

m_ShapePath在Light2D.cs中的UpdateMesh方法中被使用:

可以看到当光照类型为Freeform时,它将根据m_ShapePath更新光照的mesh。

继续阅读源码可知,UpdateMesh方法在Awake、光照类型改变、Falloff改变、多边形光形状改变及Cookie的Sprite改变时会被调用,而光照类型为Freeform时形状改变的情况下不会被调用,这意味着更改m_ShapePath后,必须要手动调用UpdateMesh方法,否则光照的形状不会被更新。

回到Laser2D.cs,编写一个SetShapePath方法,之后将通过它更新Light 2D的形状:

Laser2D.cs

void SetShapePath(Light2D light, Vector3[] path)
{
    var field = light.GetType().GetField("m_ShapePath", BindingFlags.NonPublic | BindingFlags.Instance);
    field?.SetValue(light, path);
    var method = light.GetType().GetMethod("UpdateMesh", BindingFlags.NonPublic | BindingFlags.Instance);
    method?.Invoke(light, null);
}

通过反射获取到m_ShapePath,设值之后再调用UpdateMesh方法。

继续编写,加入根据起点与终点更改Light2D形状的处理:

Laser2D.cs

[RequireComponent(typeof(LineRenderer))]
public class Laser2D : MonoBehaviour
{
    [Tooltip("光照半径")]
    public float lightRadius = .5f;

    LineRenderer line;
    Light2D lit;

    void Awake()
    {
        line = GetComponent<LineRenderer>();
        lit = GetComponent<Light2D>();
        SetEnable(false);
    }

    public void SetEnable(bool b)
    {
        line.enabled = b;
        lit.enabled = b;
    }

    public void SetPositions(Vector3 start, Vector3 end)
    {
        line.SetPosition(0, start);
        line.SetPosition(1, end);
        // 更改Light2D形状
        if (start != end)
        {
            var direction = end - start;
            var localUp = Vector3.Cross(Vector3.forward, direction).normalized;
            localUp = transform.InverseTransformDirection(localUp) * lightRadius;
            var localStart = transform.InverseTransformPoint(start);
            var localEnd = transform.InverseTransformPoint(end);
            // 构造形状路径
            var path = new Vector3[]
            {
                localStart - localUp,
                localEnd - localUp,
                localEnd + localUp,
                localStart + localUp,
            };
            SetShapePath(lit, path);
        }
    }
    ...
}

这里将起点和终点转化为本地坐标(Light 2D形状使用本地坐标),分别给它们加、减一个方向相对于激光垂直向上、模长为光照半径的向量localUp,计算出四个顶点,且按逆时针顺序排列。四个顶点形成一个矩形,运行可以看到初步效果:

白色粗框为动态生成的形状,白色细框为Light 2D根据形状自动生成的Falloff区域。

完善形状

光是一个矩形还是难看了些,光照的边角看起来相当突兀。再给两边加上半圆,形成一个类似胶囊的形状。

画圆本质上是画多边形,先定义好圆的顶点数量:

Laser2D.cs

[RequireComponent(typeof(LineRenderer))]
public class Laser2D : MonoBehaviour
{

    [Tooltip("圆的顶点数")]
    public int circleVertices = 10;
    ...

删去原有构造矩形代码,改为构造胶囊形状:

Laser2D.cs

public void SetPositions(Vector3 start, Vector3 end)
{
    ...
    // 更改Light2D形状
    if (start != end)
    {
        var direction = end - start;
        var localUp = Vector3.Cross(Vector3.forward, direction).normalized;
        localUp = transform.InverseTransformDirection(localUp) * lightRadius;
        var localStart = transform.InverseTransformPoint(start);
        var localEnd = transform.InverseTransformPoint(end);
        // 构造形状路径
        Vector3[] path = new Vector3[circleVertices + 2];
        float deltaAngle = 2 * Mathf.PI / circleVertices;
        float axisAngleOffset = Vector2.SignedAngle(Vector2.right, direction);
        // 当前圆上顶点对应角度
        float theta = Mathf.PI / 2 + Mathf.Deg2Rad * axisAngleOffset;
        int index = 0;
        // 起点处的半圆
        path[index] = localStart + localUp;
        for (int i = 0; i < circleVertices / 2; i++)
        {
            theta += deltaAngle;
            path[++index] = localStart + new Vector3(lightRadius * Mathf.Cos(theta), lightRadius * Mathf.Sin(theta), 0);
        }
        // 终点处的半圆
        path[++index] = localEnd - localUp;
        for (int i = 0; i < circleVertices / 2; i++)
        {
            theta += deltaAngle;
            path[++index] = localEnd + new Vector3(lightRadius * Mathf.Cos(theta), lightRadius * Mathf.Sin(theta), 0);
        }

        SetShapePath(lit, path);
    }
}

效果:

处理翻转

当她转身朝向另一面时,光照显示会有错误:

继续修改形状生成部分,加入对翻转的处理:

Laser2D.cs

public void SetPositions(Vector3 start, Vector3 end)
{
    ...
    // 更改Light2D形状
    if (start != end)
    {
        ...
        // 构造形状路径
        Vector3[] path = new Vector3[circleVertices + 2];
        float deltaAngle = 2 * Mathf.PI / circleVertices;
        float axisAngleOffset = Vector2.SignedAngle(Vector2.right, direction);
        // 处理翻转情况,改变角度计算方向
        if (transform.lossyScale.x < 0)
        {
            deltaAngle = -deltaAngle;
            axisAngleOffset = -axisAngleOffset;
        }
        // 当前圆上顶点对应角度
        ...
        // 起点处的半圆
        ...
        // 终点处的半圆
        ...
        // 处理翻转情况,将所有顶点倒序
        if (transform.lossyScale.x < 0)
            System.Array.Reverse(path);
        SetShapePath(lit, path);
    }
}

修复后:

加一些特技

再加上一些粒子:

至此就基本完成了,还可以继续完善如发射时的粒子爆发、亮度变化、激光颜色设置等等。

项目中用到的素材:

Cute 2D Girl - Wizard by ClearSky

2D DarkCave Assets by Maaot

Demo项目地址:
2D-Laser

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

推荐阅读更多精彩内容

  • 做一个类似于《INK》的2D颜料泼溅效果: 表面与颜料 利用模板测试,让颜料污渍能在物体表面上重叠显示且不超出物体...
    pamisu阅读 2,032评论 0 3
  • 简单的介绍与起步 Unity的Playground是一个用来制作拥有物理引擎的2D游戏开发框架(framework...
    超级超级小天才阅读 5,937评论 0 4
  • 一:什么是协同程序? 在主线程运行的同时开启另一段逻辑处理,来协助当前程序的执行,协程很像多线程,但是不是多线程,...
    胤醚貔貅阅读 2,066评论 0 13
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,520评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,178评论 4 8