【Unity 3D绳索】基于图形渲染的3D绳索模拟

        前一段被要求模拟一根绳索,因为种种原因,笔者最后决定通过数学函数和Mesh模拟出一根绳索,具体的思路是首先利用Verlet函数模拟绳索的刚性,之后利用Mesh渲染圆柱体将绳索模拟出来。

        

        首先,利用Verlet进行逻辑上的绳索创建,具体思路参考了这篇文章:

在Unity使用Verlet积分实现逼真的绳索 - 知乎

        不过这篇文章的作者是利用LineRenderer模拟的2D绳索,简单来讲就是将绳子看成许多节,每个节点都包含两个端点,端点会产生一个力来约束绳索,有了这些基础就可以模拟绳子的物理特性了。

/// <summary>
/// Verlet最小节点
/// </summary>
public class Particle
{
    public Vector3 position;
    public Vector3 oldPosition;
    public bool locked;
}

/// <summary>
/// 抓钩段长
/// </summary>

public class Stick
{
    public Particle particleA;
    public Particle particleB;
    public float length;

    public Stick(Particle a, Particle b)
    {
        particleA = a;
        particleB = b;
        length = (a.position - b.position).magnitude;
    }
}

        而因为需要建立在三维空间中,所以不采用LineRenderer,而是直接用一组Vector3来代表这些绳子的节点,在逻辑上将绳子模拟出来,以待后续的渲染。

public class Hook : MonoBehaviour
{
    [Header("节点数")]
    [Range(30, 100)]
    public float points = 80;
    [Header("半径")]
    public float lineRadius = 0.02f;
    [Header("重力")]
    [SerializeField] public float gravity = 1f;
    [Header("作用力")]
    [SerializeField] public int stiffness = 200;

    [SerializeField] private bool startPointLock; //锁定节点
    [SerializeField] private bool endPointLock;
    [SerializeField] private bool isfollow; //起始点随父物体移动

    private Vector3 _startPosition; //开始位置
    private Vector3 _endPosition; //结束位置

    public Material _material;
    private List<Vector3> _vector3s = new List<Vector3>();     //抓钩信息存储列表
    private List<Particle> _particles = new List<Particle>();
    private List<Stick> _sticks = new List<Stick>();
    private Dictionary<int, UnityEngine.Mesh> _meshMap = new Dictionary<int, UnityEngine.Mesh>();

    private bool _isExist = false; //是否已存在抓钩
    private int _index = 0; //抓钩节点计数

    /// <summary>
    /// 初始化
    /// </summary>
    public void Init(Vector3 startV3, Vector3 endV3)
    {
        _startPosition = startV3;
        _endPosition = endV3;
        Initialization();
        SetParticlesLockSet();
        _isExist = true;
    }

    private void FixedUpdate()
    {
        Simulation();
    }

    /// <summary>
    /// 抓钩节点设置
    /// </summary>
    private void Initialization()
    {
        _vector3s.Clear();
        _particles.Clear();
        _sticks.Clear();
        _meshMap.Clear();
        for (int i = 0; i <= points; i++)
        {
            float t = i / points;
            _vector3s.Add(Vector3.Lerp(_startPosition, _endPosition, t));
        }
        for (int i = 0; i < _vector3s.Count; i++)
        {
            _particles.Add(new Particle() { position = _vector3s[i], oldPosition = _vector3s[i] });
        }
        for (int i = 0; i < _particles.Count - 1; i++)
        {
            _sticks.Add(new Stick(_particles[i], _particles[i + 1]));
        }
    }

    /// <summary>
    /// 抓钩节点锁定设置
    /// </summary>
    private void SetParticlesLockSet()
    {
        if (startPointLock)
        {
            _particles[0].locked = true;
        }
        if (endPointLock)
        {
            _particles[_particles.Count - 1].locked = true;
        }
        if (isfollow)
        {
            _particles[0].locked = true;
        }
    }

    /// <summary>
    /// 抓钩特性赋值
    /// </summary>
    private void Simulation()
    {
        //遍历
        for (int i = 0; i < _particles.Count; i++)
        {
            Particle p = _particles[i];
            if (p.locked == false)
            {
                Vector3 temp = p.position;
                //Verlet积分
                p.position = p.position + (p.position - p.oldPosition) + Time.fixedDeltaTime * Time.fixedDeltaTime * new Vector3(0, -gravity, 0);
                p.oldPosition = temp;
            }
        }

        //迭代次数,控制刚性
        for (int i = 0; i < stiffness; i++)
        {
            for (int j = 0; j < _sticks.Count; j++)
            {
                Stick stick = _sticks[j];

                Vector3 delta = stick.particleB.position - stick.particleA.position;
                float deltaLength = delta.magnitude;
                float diff = (deltaLength - stick.length) / deltaLength;
                if (stick.particleA.locked == false)
                    stick.particleA.position += 0.5f * diff * delta;
                if (stick.particleB.locked == false)
                    stick.particleB.position -= 0.5f * diff * delta;
            }
        }

        if (isfollow && _particles.Count > 0)
        {
            _particles[0].position = Player.instance.transform.position;
        }
    }
}

        接下来便是考虑如何渲染3D的绳索了,最优的方案如下,利用所得节点渲染不平行的柱体,这是以下这篇Unreal的绳索文档思路,不过这个文章并没有提供源码,所有笔者准备改一种更简单的方法。

https://docs.unrealengine.com/4.27/en-US/Basics/Components/CableComponent/

        将两个节点之间渲染成不平行圆柱体实现起来比较麻烦,而渲染成平行的就简单很多了,但随之面临的问题是不平行的圆柱体之间会有缝隙在绳索弯曲的时候,但利用积分的思想,假设绳子上的节点越多,那么绳子间的缝隙影响就会越小,当节点够多时 (目前尝试的,其实50-80个的时候基本就看不到了,前提时绳子不要太粗),就看不到缝隙了。

        所以说,接下来只要对各个节点进行圆柱体绘制就行了:

    private void LateUpdate()
    {
        if (_isExist)
        {
            StartCoroutine(HookRendering(true));
        }
        Rendering();
    }
    
    /// <summary>
    /// 绘制抓钩
    /// </summary>
    private void Rendering()
    {
        for (int i = 0; i < _index; i++)
        {
            DrawCylinder(_particles[i].position, _particles[i + 1].position);
        }
    }

    /// <summary>
    /// 绘制抓钩携程,true为延伸,false为收缩
    /// </summary>
    /// <param name="isExtend"></param>
    /// <returns></returns>
    IEnumerator HookRendering(bool isExtend)
    {
        endPointLock = isExtend;
        _isExist = false;
        if (isExtend)
        {
            while (_index < _particles.Count - 1)
            {
                _index++;
                yield return new WaitForSeconds(0.01f);
            }
            //Player.instance.MoveControl(_endPosition, true);
            //StartCoroutine(HookRendering(false));
        }
        else
        {
            while (_index > 0)
            {
                _index--;
                yield return new WaitForSeconds(0.01f);
            }
        }
    }    
    /// <summary>
    /// Mesh绘制
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    private void DrawCylinder(Vector3 a, Vector3 b)
    {
        if (isNaN(a) || isNaN(b)) { return; }

        float length = (a - b).magnitude;

        if ((a - b).magnitude > 0.001f)
        {
            Graphics.DrawMesh(GetCylinderMesh(length),
                              Matrix4x4.TRS(a,
                                            Quaternion.LookRotation(b - a),
                                            new Vector3(transform.lossyScale.x, transform.lossyScale.x, 1)),
                              _material,
                              gameObject.layer,
                              null, 0, null, true);
        }
    }

    /// <summary>
    /// Mesh获取
    /// </summary>
    /// <param name="length"></param>
    /// <returns></returns>
    private UnityEngine.Mesh GetCylinderMesh(float length)
    {
        const float CYLINDER_MESH_RESOLUTION = 0.1f;
        int lengthKey = Mathf.RoundToInt(length * 100 / CYLINDER_MESH_RESOLUTION);

        UnityEngine.Mesh mesh;
        if (_meshMap.TryGetValue(lengthKey, out mesh))
        {
            return mesh;
        }

        mesh = new UnityEngine.Mesh();
        mesh.hideFlags = HideFlags.DontSave;

        List<Vector3> verts = new List<Vector3>();
        List<Color> colors = new List<Color>();
        List<int> tris = new List<int>();

        Vector3 p0 = Vector3.zero;
        Vector3 p1 = Vector3.forward * length;
        int _cylinderResolution = 12;
        float _cylinderRadius = lineRadius;
        for (int i = 0; i < _cylinderResolution; i++)
        {
            float angle = (Mathf.PI * 2.0f * i) / _cylinderResolution;
            float dx = _cylinderRadius * Mathf.Cos(angle);
            float dy = _cylinderRadius * Mathf.Sin(angle);

            Vector3 spoke = new Vector3(dx, dy, 0);

            verts.Add(p0 + spoke);
            verts.Add(p1 + spoke);

            colors.Add(Color.white);
            colors.Add(Color.white);

            int triStart = verts.Count;
            int triCap = _cylinderResolution * 2;

            tris.Add((triStart + 0) % triCap);
            tris.Add((triStart + 2) % triCap);
            tris.Add((triStart + 1) % triCap);

            tris.Add((triStart + 2) % triCap);
            tris.Add((triStart + 3) % triCap);
            tris.Add((triStart + 1) % triCap);
        }

        mesh.SetVertices(verts);
        mesh.SetIndices(tris.ToArray(), MeshTopology.Triangles, 0);
        mesh.RecalculateBounds();
        mesh.RecalculateNormals();
        mesh.UploadMeshData(true);

        _meshMap[lengthKey] = mesh;

        return mesh;
    }
    private bool isNaN(Vector3 v)
    {
        return float.IsNaN(v.x) || float.IsNaN(v.y) || float.IsNaN(v.z);
    }

最后,如果这篇文章帮助到你,麻烦点个赞吧。

猜你喜欢

转载自blog.csdn.net/qq_45360148/article/details/132760700