找回密码
 立即注册
查看: 757|回复: 8

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

[复制链接]
发表于 2021-1-11 11:49 | 显示全部楼层 |阅读模式
如何在Unity中实现一根有弹性的鱼竿?
发表于 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[i - 1]).magnitude;
        }
        for (int i = 1; i < RodeCount; i++)
        {
            rodeList *= RodeLength * (RodeCount - 1) / realLength;
        }
        //1.1.3*,绘制顶端到钓块的线
        Debug.DrawLine(
            fish.transform.position,
            transform.TransformPoint(rodeList[RodeCount - 1])
            );
        //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[i-1].y, z1 + rodeList[i-1].z),
                    new Vector3(x2, rodeList[i-1].y, z2 + rodeList[i-1].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[0].y, z2 + rodeList[0].z),
                new Vector3(x1, rodeList[0].y, z1 + rodeList[0].z),
                new Vector3(0, rodeList[0].y, 0f)
            );
            AddTriangleColor(Color.white);
            AddTriangle(
                new Vector3(x1, rodeList[RodeCount - 1].y, z1 + rodeList[RodeCount - 1].z),
                new Vector3(x2, rodeList[RodeCount - 1].y, z2 + rodeList[RodeCount - 1].z),
                new Vector3(0, rodeList[RodeCount - 1].y, rodeList[RodeCount - 1].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的翻译

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-1-11 12:07 | 显示全部楼层
果断插件实现
发表于 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 | 显示全部楼层
我选择让美术做
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 11:35 , Processed in 0.095409 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表