愿为素心人 发表于 2021-1-11 11:49

如何在Unity中实现一根有弹性的鱼竿?

如何在Unity中实现一根有弹性的鱼竿?

123456911 发表于 2021-1-11 11:50

问题可以简化, 变成一个数学问题. (UE4用着熟, 就拿这个当例子, 参考资料是Unity的)
对于鱼竿来说, 其实我们只需要保证杆尾这个点是弹性运动即可, 那么问题就是变成了直杆与弯杆的端点位置的弹性运动, 这个只用一个PhysicsConstraint就可以搞定了:

在这里我只用一个角度约束

那剩下的问题就简单了, 由三点确定一条曲线. 根据Euler-Bernoulli beam theory, 这个在工程力学中常用的理论, 参考其中的Cantilever beams(悬臂梁)弯曲算法, 很容易就能得出我们想要的曲线:

因为我们只是想求弯杆在白杆与黄杆之间插值的比率w, 那么把公式进一步简化为:
w = x^2 * (3 * L- x) / (2 * L^3).其中x是离起点的距离, L是杆长度.

用BP实现一下就是:

这样我们就把曲线上的细分点位置求解出来了:

至此已经解答完毕题主关于"弹性"的问题了. 至于"鱼竿"的表现部分, 我也想到两种实现方法, 一种是SplineMesh, 一种是SkeletalMesh. 下面使用比较通用的蒙皮骨骼模型实现一下看看效果.
Maya里做根简单的鱼杆, 绑定上一串骨骼:

那导入引擎之后, 就是需要把之前计算的曲线上的点, 设置到骨骼上就可以了:







原作者为了保持杆的长度不变多加了一步, 把直线插值变成了弧形插值, 这个实现起来也很容易, 就不再做试验了:





参考资料:
https://www.youtube.com/watch?v=SYSeoTwG_qk

无人岛屿颈 发表于 2021-1-11 11:58

先上最终效果



*最后在挥动方面的角度计算仍有问题,有问题的代码放在最后请大手子们指点一下。
===================================================
昨晚看到了这题,但是觉得可能会上头导致失眠,所以没写,结果还是失眠了……
如果要我个人说的话,我建议让美术制作“轻度弯曲”“中度弯曲”“重度弯曲”三个动画,然后让程序根据挥动力度在这三者间切换就好。
现在我写的是另一个解决方案,即通过代码更新Mesh来实现。
废话不多说,关键代码如下:
首先是计算插值的代码,也是照搬的逍遥剑客的算法:
private float GetCantileverW(float x, float L)
{
    return Mathf.Pow(x, 2f) * (3 * L - x) / (2 *Mathf.Pow(L, 3f));
}
其次是更新Mesh用到的代码,代码都在注释里我就不废话解释了:
public void RefreshRod(float distance)
    {
      CantileverDistance = distance;
      //第一步,制造一个鱼竿的Mesh
      //1.0,清理数据
      RodMesh.Clear();
      colors.Clear();
      vertices.Clear();
      triangles.Clear();
      //1.1,拿到每个节点的长度
      //1.1.1,计算弯曲之后的鱼竿节点
      List<Vector3> rodeList = new List<Vector3>();
      for (int i = 0; i < RodeCount; i++)
      {
            rodeList.Add(new Vector3(0, RodeLength * i, CantileverDistance * GetCantileverW(RodeLength * i, RodeLength * (RodeCount -1))));
      }
      //1.1.2,由于弯曲之后鱼竿长度会变相增长,所以进行一次缩放
      float realLength = 0f;
      for (int i = 1; i < RodeCount; i++)
      {
            realLength += (rodeList - rodeList).magnitude;
      }
      for (int i = 1; i < RodeCount; i++)
      {
            rodeList *= RodeLength * (RodeCount - 1) / realLength;
      }
      //1.1.3*,绘制顶端到钓块的线
      Debug.DrawLine(
            fish.transform.position,
            transform.TransformPoint(rodeList)
            );
      //1.2,根据每个节点长度,计算出面片
      float unitEuler = 2 * Mathf.PI / (float) SplitCount;
      for (int i = 1; i < RodeCount; i++)
      {
            for (int j = 0; j < SplitCount; j++)
            {
                float x1 = Radius * Mathf.Sin(unitEuler * j);
                float x2 = Radius * Mathf.Sin(unitEuler * (j + 1));
                float z1 = Radius * Mathf.Cos(unitEuler * j);
                float z2 = Radius * Mathf.Cos(unitEuler * (j + 1));
                AddQuad(
                  new Vector3(x1, rodeList.y, z1 + rodeList.z),
                  new Vector3(x2, rodeList.y, z2 + rodeList.z),
                  new Vector3(x1, rodeList.y, z1 + rodeList.z),
                  new Vector3(x2, rodeList.y, z2 + rodeList.z)
                  );
                AddQuadColor(Color.gray, Color.white);
            }
      }
      //1.3,计算上下顶面
      for (int j = 0; j < SplitCount; j++)
      {
            float x1 = Radius * Mathf.Sin(unitEuler * j);
            float x2 = Radius * Mathf.Sin(unitEuler * (j + 1));
            float z1 = Radius * Mathf.Cos(unitEuler * j);
            float z2 = Radius * Mathf.Cos(unitEuler * (j + 1));
            AddTriangle(
                new Vector3(x2, rodeList.y, z2 + rodeList.z),
                new Vector3(x1, rodeList.y, z1 + rodeList.z),
                new Vector3(0, rodeList.y, 0f)
            );
            AddTriangleColor(Color.white);
            AddTriangle(
                new Vector3(x1, rodeList.y, z1 + rodeList.z),
                new Vector3(x2, rodeList.y, z2 + rodeList.z),
                new Vector3(0, rodeList.y, rodeList.z)
            );
            AddTriangleColor(Color.gray);
      }
      //1.4,给Mesh赋值
      RodMesh.vertices = vertices.ToArray();
      RodMesh.triangles = triangles.ToArray();
      RodMesh.colors = colors.ToArray();
      RodMesh.RecalculateBounds();
      RodMesh.RecalculateNormals();
    }
效果如下:




(所以如果不缩放长度,弯曲程度越大则钓竿实际长度会无止境的增加)
这个函数只提供了单一方向的挥动,所以挥动方向由父物体提供,我们要另写一个新的挥杆者计算挥杆方向:
……
    //由于FPSController的问题,不能从鼠标位置正确的得到旋转的速度,所以直接从GameObject的Rotation上拿
    public Transform PlayerTrans;
    public Transform CameraTrans;
    public FishingRod Rod;
    //挥动系数
    public float Force;
    //偏移累计量
    public Vector2 Offset;
    //目前偏移量
    public Vector2 CurOffset;
    //偏移回振力度
    public float Resistance;
    //偏移方向
    public Vector3 OffsetDir;
    //上次的角度
    public Vector2 LastAngle;
    public float test;
这里有一个问题,那就是我使用的是默认的FpsController,导致不能正确的用Input.mousePosition拿到旋转的方向和速度大小,所以这里我从Player的y轴上取到水平方向的旋转分量,再从Camera的x轴上取到垂直旋转的分量。
// Update is called once per frame
        void Update ()
        {
      //获得当前挥动角度
          Vector2 nowAngle = new Vector2(PlayerTrans.localRotation.y, CameraTrans.localRotation.x);
          if (nowAngle != LastAngle)
          {
            //加入最大偏移限制,刚进入游戏会有一个1000+的巨额偏移
                Offset += ((nowAngle - LastAngle).magnitude > 10f ? Vector2.zero : (nowAngle - LastAngle));
          }
      //对偏移累积量进行修正
          float temp = 1 - (Resistance / Offset.magnitude);
          Offset *= (temp >= 0 ? temp : 0);
      //目前偏移量向累积量进行修正
          Vector2 offsetBetweenTotalAndCurrent = Offset - CurOffset;
          if (offsetBetweenTotalAndCurrent.magnitude < Resistance)
                CurOffset = Offset;
          else
                CurOffset += offsetBetweenTotalAndCurrent.normalized * Resistance;
      //根据得到的偏移量之模刷新Mesh
      Rod.RefreshRod(CurOffset.magnitude * Force);
          Rod.transform.localRotation = Quaternion.Euler(new Vector3(0f, - Vector2Angle(CurOffset) - 90f,0f));
          LastAngle = nowAngle;
        }
好吧,下面就是有问题的代码了,拿到这水平和垂直的旋转分量后我应该能计算出鱼竿的y轴欧拉角,不过不知道为啥总有一个方向挥不动,请大手子指教一下:
private float Vector2Angle(Vector2 e)
    {
//e.x是水平旋转分量 e.y是垂直旋转分量
      float res;
      if (e.y == 0f)
      {
            res = e.x > 0 ? 0 : 180f;
            print(res);
            return res;
      }
      res = (float) Math.Atan(e.y / e.x);
      res = res / Mathf.PI * 180;
      if (e.y > 0)
            res = Mathf.Abs(res);
      else
            res = -Mathf.Abs(res);
      print(res);
      return res;
    }
我太累了,失眠+写完代码的空虚感……我先出去吃个饭然后睡会,下午还要写自己的东西……
P.S.顺带一提,实际上作为挥动钓竿的约束条件的CantileverDistance,实际上可以用正切函数从挥动角度上拿到的,因为挥动角度更好量化而挥动距离确不行。
P.S.2.还有就是实际上为了保证鱼竿在视野内的效果,在弯曲程度小的时候应该抬高鱼竿,在弯曲程度大的时候应放低鱼竿,保证鱼竿都在视野范围内。
P.S.N.还有一堆的东西,不说了,吃饭了……
===================================================
感谢 @逍遥剑客 提供的公式
感谢Unity C# Tutorials
感谢腾讯GAD的翻译

123456790 发表于 2021-1-11 12:07

果断插件实现

123456881 发表于 2021-1-11 12:16

可以用贝塞尔曲线实现,修改模型的mesh顶点:
GitHub:
https://github.com/dxxia/TestFishRod_BezierMesh

忆困血馆闻 发表于 2021-1-11 12:21

翻不了墙,看不了youtub,只能根据猜测回答题目
根据我模型的经验是
鱼竿模型的面数要给足
动画绑定的时候骨骼给足
应对多种情况要做出相应的动画片段
交给程序猿
玩家使用什么操作,就播放相应的动画

当当当当裤裆坦 发表于 2021-1-11 12:30

没看视频
实现方法两种 一种用代码实现动态骨骼 类似辫子和衣服摆件 控制硬度就可以

另外使用IK机制也可以模拟拉线弯曲

却写杂布计 发表于 2021-1-11 12:32

可以分享一下这个小Demo吗

我爱霍启刚掖 发表于 2021-1-11 12:39

我选择让美术做
页: [1]
查看完整版本: 如何在Unity中实现一根有弹性的鱼竿?