Design Hub

    • 注册
    • 登录
    • 搜索
    • 版块
    • 最新

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

    游戏开发
    1
    1
    165
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • Pamisu
      Pamisu 管理员 最后由 编辑

      2D中激光束算是比较常见了,实现起来也较为简单,但为了让它能真正达到照明的效果还是得花些功夫,记录一下实现过程。思路基本参考油管上一个印度小哥的教程,有一些修改,小哥的连连看部分略显复杂,我简化了一下;小哥没有做照明效果,这里额外进行实现。

      视频在比比汗丽丽上有人搬运,不过机翻质量比较糟糕:
      BV1pM4y1L7C6

      整体思路:

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

      准备

      要使用ShaderGraph做Shader,那么先得把它装好,这里使用统一渲染管线(URP),可以直接新建一个URP项目,也可以在Package Manager里安装然后配置。如果不知道URP是啥、不清楚要怎么配置,推荐看一下麦扣的保姆级教学:

      https://www.bilibili.com/video/BV1t54y1d7DW

      激光部分

      材质

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

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

      什么是Voronoi?它是一种程序生成的纹理,节点输入UV可以控制对这张纹理的采样,这里自定义了两个参数,Scale用来调节这张纹理的缩放,Speed乘以时间控制纹理的移动。这里也可以根据需要使用其他的噪声纹理,好看就行。

      激光的边缘部分需要有柔和渐变,按照下图的效果,只要让颜色在y轴方向由0渐变到1再渐变到0即可,用sin函数可以达到这个效果,再用指数函数控制边缘的厚度,即pow(sin(uv.y * PI), Edge):

      将两者相乘,再与自定义的颜色参数Color混合得到最终效果:

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

      完整的ShaderGraph:

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

      Line Renderer

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

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

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

      交互

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

      如果对制作2D骨骼动画不太了解,推荐看看麦扣的这套骨骼动画教程:

      https://www.bilibili.com/video/BV1w7411F7qb

      期望效果是玩家点击鼠标左键,角色举起法杖,随后启用激光的LineRenderer组件,并设置它的起点与终点,向鼠标方向发射。编写激光脚本,并挂在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的形状。

      SetShapePath

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

      更新:URP13.0(Unity2022.1)版本后,新增了官方的SetShapePath方法,不需要再用反射去设置形状了:

      与此同时还在用2020版本的我:

      如果你用的是新版,那么使用官方提供的SetShapePath就行,可以直接跳到下一节;如果你汗我一样还在用低版本,那么不妨看看我的解决思路,其实也不复杂。

      去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);
          }
      }
      

      简单的初中数学,就不过多解释了,效果:

      处理翻转

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

      原因是翻转后光照Mesh的顶点顺序错误,此时需要逆序一下,继续修改形状生成部分,加入对翻转的处理:

      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);
          }
      }
      

      修复后:

      加一些特技

      按小哥教程中的做法,加上一些粒子效果:

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

      Demo项目地址:

      https://github.com/pmisu/2D-Laser

      项目中用到的素材:

      Cute 2D Girl - Wizard by ClearSky

      2D DarkCave Assets by Maaot

      1 条回复 最后回复 回复 引用
      • 1 / 1
      • First post
        Last post
      版权所有 ©2023 Design Hub 保留所有权利
      鄂ICP备2021004164号-2