在 Unity 3D 中实施 XPBD 时遇到的问题

问题描述 投票:0回答:1

我一直在尝试基于 XPBD(基于扩展位置的动力学)在 Unity 中实现软体模拟。我一直遵循的教程来自 Youtube 频道“十分钟物理”的 Matthias Muller。您可以在此处找到本教程的代码:https://github.com/matthias-research/pages/blob/master/tenMinutePhysics/10-softBodies.html。这是视频的链接:https://www.youtube.com/watch?v=uCaHXkS2cUg

我尝试将某些事情转换为更“团结”的做事方式。比如Vectors对象的使用。但无论我做什么,模拟要么会爆炸,要么会因为不抵抗重力而保持其形状而失败。我最终将模拟简化为一行距离约束,但它仍然无法按预期工作。

这是我的代码,仅使用距离约束。该脚本附加到位于世界原点的空游戏对象:

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class RopeDynamics : MonoBehaviour
{
    public MeshFilter meshFilter;

    public bool showIndexes = true;
    public int indexLabelSize = 16;

    public int subSteps = 10;
    public float edgeCompliance = 100f;
    public float mass = 1f;


    public Vector3[] nodes;
    public int[] edgeNodeIds;

    [SerializeField]
    private float[] _restEdgeLengths;    
    [SerializeField]
    private float[] _inverseMasses;
    [SerializeField]
    private Vector3[] _velocities;
    [SerializeField]
    private Vector3[] _prevPositions;

    private Vector3 _gravity;
    private Vector3[] _gradients = new Vector3[4];

    public int NodeCount
    {
        get
        {
            return nodes.Length;
        }
    }

    public int EdgeCount
    {
        get
        {
            return edgeNodeIds.Length / 2;
        }
    }

    private void Awake()
    {
        //Create the Rope Nodes/Vertices.
        nodes = new Vector3[]
        {
            new Vector3(0f,2f,0f),
            new Vector3(0f,1.75f,0f),
            new Vector3(0f,1.5f,0f),
            new Vector3(0f,1.25f,0f),
            new Vector3(0f,1f,0f),
            new Vector3(0f,0.75f,0f),
            new Vector3(0f,0.5f,0f)
        };

        edgeNodeIds = new int[]
        {
            0,1,
            1,2,
            2,3,
            3,4,
            4,5,
            5,6
        };

        var mesh = new Mesh();
        mesh.vertices = nodes;
        mesh.SetIndices(edgeNodeIds, MeshTopology.Lines, 0);
        mesh.RecalculateBounds();
        meshFilter.mesh = mesh;

        //Get Local Vector of Gravity.
        _gravity = transform.InverseTransformDirection(Physics.gravity);

        InitializeSimulation();
    }


    private void FixedUpdate()
    {
        //Get the Substep.
        var sdt = Time.fixedDeltaTime / subSteps;

        for (int i = 0; i < subSteps; i++)
        {

            PreSolve(sdt, _gravity);
            Solve(sdt);
            PostSolve(sdt);
        }
    }


    private void InitializeSimulation()
    {
        //Initialize the Arrays.
        _restEdgeLengths = new float[EdgeCount];
        //_restVolumes = new float[tetMesh.TetrahedronCount];
        _inverseMasses = new float[NodeCount];
        _velocities = new Vector3[NodeCount];
        _prevPositions = new Vector3[NodeCount];

        //Get all of the Rest Lengths for each Edge.
        for (int i = 0; i < EdgeCount; i++)
        {
            //_restEdgeLengths[i] = tetMesh.GetSqrEdgeLength(i);
            _restEdgeLengths[i] = GetMagEdgeLength(i);
        }

        var w = 1f / (mass / NodeCount);

        //Get all of the Inverse Masses.
        for (int i = 0; i < NodeCount; i++)
        {
            _inverseMasses[i] = w;            
        }

        _inverseMasses[0] = 0f;
    }

    private void PreSolve(float dTime, Vector3 gravity)
    {
        //For each node.
        for (int i = 0; i < NodeCount; i++)
        {
            //Skip if the Inverse Mass is Zero/Infinite.
            if (_inverseMasses[i] == 0f)
                continue;

            //Get selected Velocity and add the Gravity vector.
            _velocities[i] += gravity * dTime;

            //Cache Previous Position.
            _prevPositions[i] = nodes[i];

            //Add Velocity vector to the Nodes Position vector.
            nodes[i] += _velocities[i] * dTime;
        }

    }

    private void Solve(float dTime)
    {
        SolveEdges(dTime, edgeCompliance);        
    }

    private void PostSolve(float dTime)
    {
        for (int i = 0; i < NodeCount; i++)
        {
            //Skip if the Inverse Mass is Zero/Infinite.
            if (_inverseMasses[i] == 0f)
                continue;

            //Update the selected Velocity.
            _velocities[i] = (nodes[i] - _prevPositions[i]) / dTime;
        }

        //Update the Mesh.
        UpdateMesh();
    }

    private void SolveEdges(float dTime, float compliance)
    {
        //Calculate the alpha for stiffness.
        var alpha = compliance / (dTime * dTime);

        //For each Edge.
        for (int i = 0; i < EdgeCount; i++)
        {
            //Get the Node Id's for the selected Edge.
            var ids = GetEdgeNodeIds(i);
            var eNodes = GetEdgeNodes(i);

            //Get the Inverse Masses for each Node.
            var w0 = _inverseMasses[ids[0]];
            var w1 = _inverseMasses[ids[1]];

            var w = w0 + w1;

            if (w == 0f)
                continue;

            //Get the current length of the edge.
            //var len = tetMesh.GetSqrEdgeLength(i);
            var len = GetMagEdgeLength(i);

            //Check if current length is Zero.
            if (len == 0f)
                continue;

            //Get the error between the current length and the rest length.
            var c = len - _restEdgeLengths[i];

            //Get the Vector difference between the two Nodes.
            var diff = eNodes[1] - eNodes[0];

            //Calculate the Gradient.
            _gradients[0] = diff / len;
            _gradients[1] = -_gradients[0];

            var gradMag0 = Vector3.Magnitude(_gradients[1]);
            var gradMag1 = Vector3.Magnitude(_gradients[0]);

            var gradSqr0 = gradMag0 * gradMag0;
            var gradSqr1 = gradMag1 * gradMag1;

            var s = -c / ((w0 * gradSqr0) + (w1 * gradSqr1) + alpha);

            var dX0 = (s * w0) * _gradients[1];
            var dX1 = (s * w1) * _gradients[0];
            

            nodes[ids[0]] += dX0;
            nodes[ids[1]] += dX1;
        }
    }

    private void UpdateMesh()
    {
        meshFilter.mesh.vertices = nodes;
    }

    public float GetMagEdgeLength(int index)
    {
        //Get the Nodes of the selected Edge.
        var node1 = nodes[edgeNodeIds[2 * index]];        
        var node2 = nodes[edgeNodeIds[2 * index + 1]];

        //Return the Squared Manitude of the 2 Points.
        return Vector3.Magnitude(node2 - node1);
    }

    public Vector3[] GetEdgeNodes(int index)
    {
        return new Vector3[2]
        {
            nodes[edgeNodeIds[2 * index]],
            nodes[edgeNodeIds[2 * index + 1]],
        };
    }

    public int[] GetEdgeNodeIds(int index)
    {
        return new int[2]
        {
            edgeNodeIds[2 * index],
            edgeNodeIds[2 * index + 1]
        };
    }

    //Used to draw spheres at the Nodes/Vertices.
    private void OnDrawGizmos()
    {
        Gizmos.color = Color.blue;
        GUI.contentColor = Color.blue;

        if (showIndexes)
        {

            for (int i = 0; i < NodeCount; i++)
            {
                var point = transform.TransformPoint(meshFilter.sharedMesh.vertices[i]);

                Gizmos.DrawSphere(point, 0.008f);

                Handles.Label(point, i.ToString(), new GUIStyle(GUI.skin.label) { fontSize = indexLabelSize });
            }
        }

    }

}

我似乎无法弄清楚出了什么问题。这可能很简单,但我就是看不到。任何帮助将不胜感激。

谢谢!

c# unity-game-engine game-physics physics-engine
1个回答
0
投票

因此,为了防止其他人陷入困境,我将在这里发布我的解决方案。

首先,我找到了概述 PBDXPBD 定义的实际论文。我还发现文章“基于扩展位置的动力学的详细刚体模拟”中的实现是最有帮助的。

此外,Github 上有一个名为 Q-Minh 的用户的项目,名为 “position-based-dynamics”。他们将 XPD 求解器的原理实现到 C++ 程序中,使我的代码更容易运行。

如果您使用这些脚本,您将需要一个具有 MeshFilter 和 MeshRenderer 组件的 GameObject。

using System;    
using UnityEngine;

public class RopeDynamics : MonoBehaviour
{
    public MeshFilter meshFilter;

    public int subSteps = 10;
    public int iterations = 3;

    public float edgeCompliance = 0.5f;
    public float mass = 1f;


    public Vector3[] nodes;
    public int[] edgeNodeIds;

    [SerializeField]
    private float[] _restEdgeLengths;    
    [SerializeField]
    private float[] _inverseMasses;
    [SerializeField]
    private Vector3[] _velocities;
    [SerializeField]
    private Vector3[] _prevPositions;

    private Vector3 _gravity;

    private float[] _lagrange;

    private bool _isInitialized = false;

    public int NodeCount
    {
        get
        {
            return nodes.Length;
        }
    }

    public int EdgeCount
    {
        get
        {
            return edgeNodeIds.Length / 2;
        }
    }

    private void Awake()
    {
        //Create the Rope Nodes/Vertices.
        nodes = new Vector3[]
        {
            new Vector3(0f,2f,0f),
            new Vector3(0f,1.75f,0f),
            new Vector3(0f,1.5f,0f),
            new Vector3(0f,1.25f,0f),
            new Vector3(0f,1f,0f),
            new Vector3(0f,0.75f,0f),
            new Vector3(0f,0.5f,0f)
        };

        edgeNodeIds = new int[]
        {
            0,1,
            1,2,
            2,3,
            3,4,
            4,5,
            5,6
        };

        var mesh = new Mesh();
        mesh.vertices = nodes;
        mesh.SetIndices(edgeNodeIds, MeshTopology.Lines, 0);
        mesh.RecalculateBounds();
        meshFilter.mesh = mesh;

        //Get Local Vector of Gravity.
        _gravity = transform.InverseTransformDirection(Physics.gravity);

        InitializeSimulation();
    }


    private void FixedUpdate()
    {
        if (!_isInitialized)
            return;

        //Get the Substep.
        var sdt = Time.fixedDeltaTime / subSteps;

        for (int i = 0; i < subSteps; i++)
        {

            PreSolve(sdt, _gravity);

            //Clear the Values of the Lagrange Multipliers.
            Array.Clear(_lagrange, 0, _lagrange.Length);
            for (int n = 0; n < iterations; n++)
            {
                Solve(sdt);
            }
        
            PostSolve(sdt);
        }
    }


    private void InitializeSimulation()
    {
        //Initialize the Arrays.
        _restEdgeLengths = new float[EdgeCount];        
        _inverseMasses = new float[NodeCount];
        _velocities = new Vector3[NodeCount];
        _prevPositions = new Vector3[NodeCount];
        _lagrange = new float[EdgeCount];
    

        //Get all of the Rest Lengths for each Edge.
        for (int i = 0; i < EdgeCount; i++)
        {
            //_restEdgeLengths[i] = tetMesh.GetSqrEdgeLength(i);
            _restEdgeLengths[i] = GetMagEdgeLength(i);
        }

        var w = 1f / (mass / NodeCount);

        //Get all of the Inverse Masses.
        for (int i = 0; i < NodeCount; i++)
        {
            _inverseMasses[i] = w;            
        }

        _inverseMasses[0] = 0f;
        //_inverseMasses[6] = 0f;

        _isInitialized = true;
    }


    private void PreSolve(float dTime, Vector3 gravity)
    {
        //For each node.
        for (int i = 0; i < NodeCount; i++)
        {
            var w = _inverseMasses[i];

            //Skip if the Inverse Mass is Zero/Infinite.
            if (w == 0f)
                continue;

            //Get selected Velocity and add the Gravity vector.
            _velocities[i] += (gravity * dTime) / w;

            //Cache Previous Position.
            _prevPositions[i] = nodes[i];

            //Add Velocity vector to the Nodes Position vector.
            nodes[i] += _velocities[i] * dTime;
        }

    }

    private void Solve(float dTime)
    {
        SolveEdges(dTime, edgeCompliance);                
    }

    private void PostSolve(float dTime)
    {
        for (int i = 0; i < NodeCount; i++)
        {
            //Skip if the Inverse Mass is Zero/Infinite.
            if (_inverseMasses[i] == 0f)
                continue;

            //Update the selected Velocity.
            _velocities[i] = (nodes[i] - _prevPositions[i]) / dTime;
        }

        //Update the Mesh.
        UpdateMesh();
    }

    private void SolveEdges(float dTime, float compliance)
    {
        //Calculate the alpha for stiffness.
        var alpha = compliance / (dTime * dTime);

        //For each Edge.
        for (int i = 0; i < EdgeCount; i++)
        {
            //Get the Node Id's for the selected Edge.
            var nodeIds = GetEdgeNodeIds(i);            

            //Get the Inverse Masses for each Node.
            var w0 = _inverseMasses[nodeIds[0]];
            var w1 = _inverseMasses[nodeIds[1]];

            //Sum the Inverse Masses.
            var w = w0 + w1;

            //If they are Infinite, Skip.
            if (w == 0f)
                continue;

            //Get the selected Edge Nodes/Vertices.
            var eNodes = GetEdgeNodes(i);

            //Get the current length of the edge.
            var len = GetMagEdgeLength(i);

            //Check if current length is Zero.
            if (len == 0f)
                continue;

            //Get the Constraint Error between the Current Length and the Rest length.
            var c = len - _restEdgeLengths[i];

            //Get the Direction of the Edge.
            var n = Vector3.Normalize((eNodes[0] - eNodes[1]));                        

            //Calculate the Delta Lagrange Multiplier.
            var dLag = -(c + alpha * _lagrange[i]) / (w + alpha);

            //This version also worked and I liked its result better.
            //But it doesnt match the source documentation.
            //var dLagrange = -(c - alpha * _lagrange[i]) / (w + alpha);

            //Add to the delta Lagrange Multiplier to Lagrange Multiplier.
            _lagrange[i] += dLag;

            //Cacluate the Delta Positions for each point.
            var p0 = (dLag * n);
            var p1 = (dLag * -n);

            //Add Delta Positions to Update the Positions.
            nodes[nodeIds[0]] += p0 * w0;
            nodes[nodeIds[1]] += p1 * w1;            
        }
    }

    /// <summary>
    /// Applies the changes performed by the simulation to the mesh.
    /// </summary>
    private void UpdateMesh()
    {
        //Update the Mesh Object Vertices.
        meshFilter.mesh.vertices = nodes;
    }

    /// <summary>
    /// Calculates the length of a selected edge.
    /// </summary>
    /// <param name="index">Id of the selected edge.</param>
    /// <returns>The length of the selected edge. Also known as its magnitude.</returns>
    public float GetMagEdgeLength(int index)
    {
        //Get the Nodes of the selected Edge.
        var node1 = nodes[edgeNodeIds[2 * index]];        
        var node2 = nodes[edgeNodeIds[2 * index + 1]];

        //Return the Manitude of the 2 Points.
        return Vector3.Magnitude(node2 - node1);
    }

    /// <summary>
    /// Gets the nodes/vertices of the selected edge.
    /// </summary>
    /// <param name="index">Id of the selected edge.</param>
    /// <returns>The nodes/vertices of the selected edge.</returns>
    public Vector3[] GetEdgeNodes(int index)
    {
        return new Vector3[2]
        {
            nodes[edgeNodeIds[2 * index]],
            nodes[edgeNodeIds[2 * index + 1]],
        };
    }

    /// <summary>
    /// Gets the Id's of the Nodes/Vertices of the selected edge.
    /// </summary>
    /// <param name="index">Id of the selected edge.</param>
    /// <returns>The Id's of the Nodes/Vertices of the selected edge</returns>
    public int[] GetEdgeNodeIds(int index)
    {
        return new int[2]
        {
            edgeNodeIds[2 * index],
            edgeNodeIds[2 * index + 1]
        };
    }
}

用于预览网格顶点的脚本:

using UnityEngine;
using UnityEditor;

[RequireComponent(typeof(MeshFilter))]
public class MeshDebugger : MonoBehaviour
{
    public MeshFilter meshFilter;

    public bool showVertices = true;
    public float vertexSize = 0.01f;

    public bool showVertexIndexes = true;
    public int indexLabelSize = 16;

    /// <summary>
    /// If enabled by the Unity Inspector. It draw somehelp debug UI in the Scene view.
    /// </summary>
    private void OnDrawGizmos()
    {
        if (showVertices) 
        {            
            if (meshFilter != null && meshFilter.sharedMesh != null)
            {
                Gizmos.color = Color.blue;
                GUI.contentColor = Color.blue;

                for (int i = 0; i < meshFilter.sharedMesh.vertexCount; i++)
                {
                    //Covert Vertex position from Local to World Space.
                    var point = transform.TransformPoint(meshFilter.sharedMesh.vertices[i]);

                    Gizmos.DrawSphere(point, vertexSize);

                    //Draw Vertex Index?
                    if (showVertexIndexes)
                        Handles.Label(point, i.ToString(), new GUIStyle(GUI.skin.label) { fontSize = indexLabelSize });
                }
            }
        }        
    }
}

我仍在研究体积约束。然后我需要优化模拟的性能。但至少它有效。

无论如何,我希望这可以帮助任何需要它的人开始。

© www.soinside.com 2019 - 2024. All rights reserved.