[Unity 3D Rope] 3D rope simulation based on graphics rendering

        In the previous paragraph, I was asked to simulate a rope. Due to various reasons, the author finally decided to simulate a rope through mathematical functions and Mesh. The specific idea was to first use the Verlet function to simulate the rigidity of the rope, and then use Mesh to render the cylinder to simulate the rope. .

        

        First, use Verlet to create logical ropes. For specific ideas, refer to this article:

Using Verlet integration to achieve realistic ropes in Unity - Zhihu

        However, the author of this article uses LineRenderer to simulate a 2D rope. Simply put, the rope is regarded as many nodes. Each node contains two endpoints. The endpoints will generate a force to constrain the rope. With these foundations, you can simulate The physical properties of the rope.

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

        Because it needs to be established in a three-dimensional space, LineRenderer is not used. Instead, a set of Vector3s are directly used to represent the nodes of these ropes, and the ropes are logically simulated for subsequent rendering.

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

        The next step is to consider how to render 3D ropes. The optimal solution is as follows. Use the obtained nodes to render non-parallel cylinders. This is the idea of ​​the following Unreal rope document. However, this article does not provide source code. All the author prepared Change to a simpler method.

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

        It is troublesome to render non-parallel cylinders between two nodes, but it is much simpler to render them parallel. However, the problem is that there will be gaps between non-parallel cylinders when the rope is bent. But using the idea of ​​​​integration, assuming that the more nodes on the rope, the smaller the impact of the gaps between the ropes will be. When there are enough nodes (currently tried, in fact, when there are 50-80, it is basically invisible. The premise is that the rope Not too thick), so you won’t be able to see the gap.

        So, all you need to do is draw cylinders for each node:

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

Finally, if this article helped you, please give it a like.

Guess you like

Origin blog.csdn.net/qq_45360148/article/details/132760700