美文网首页
[Unity]带照明效果的2D激光束

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

作者: pamisu | 来源:发表于2021-05-09 20:04 被阅读0次

    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

    相关文章

      网友评论

          本文标题:[Unity]带照明效果的2D激光束

          本文链接:https://www.haomeiwen.com/subject/dvkvdltx.html