Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implicit Integration for Cloth Simulation using Mass-Spring Systems

Tech May 15 1

Implicit Time Integration

To simulate cloth realistically, explicit integration often requires tiny time steps to remain stable. Instead, we employ implicit integration, where the state at the next time step depends on forces calculated at that future state. The semi-implicit Euler equation is defined as:

$$v_{new} = v_{old} + \Delta t M^{-1} f(x_{new})$$
$$x_{new} = x_{old} + \Delta t v_{new}$$

Combining these yields a non-linear system of equations where $x_{new}$ appears on both sides inside the force function $f$:

$$x_{new} = x_{old} + \Delta t v_{old} + \Delta t^2 M^{-1} f(x_{new})$$

Energy Minimization Formulation

Rather than finding the root of the momentum equation directly, we can reframe the problem as minimizing the total energy of the system. We define an objective function $F(x)$:

$$F(x) = \frac{1}{2\Delta t^2} \|x - x_{old} - \Delta t v_{old}\|_M^2 + E_{potential}(x)$$

Where $\|x\|_M^2 = x^T M x$. The minimum of this energy function corresponds to the solution of the implicit integration step. Therefore, finding the optimal position $x_{new}$ is equivalent to solving $\nabla F(x_{new}) = 0$.

Newton-Raphson Optimization

To solve the minimization problem, we use the Newton-Raphson method. We approximate the function locally using a second-order Taylor expansion. Given a current guess $x^{(k)}$, we compute the step $\Delta x$ by solving:

$$H(x^{(k)}) \Delta x = -\nabla F(x^{(k)})$$
$$x^{(k+1)} = x^{(k)} + \Delta x$$

Here, $H(x)$ is the Hessian matrix (second derivatives), and $\nabla F(x)$ is the gradient. For the mass-spring system, these components are derived as follows:

$$\nabla F(x) = \frac{1}{\Delta t^2} M (x - x_{pred}) - f(x)$$
$$H(x) = \frac{1}{\Delta t^2} M + \frac{\partial f}{\partial x}$$

Where $x_{pred} = x_{old} + \Delta t v_{old}$. Constructing the exact Hessian for a cloth mesh is computationally expensive. A common approximation in computer graphics is to use a diagonalized Hessian, treating springs as independent entities, which simplifies the update rule significantly.

Mesh and Edge Initialization

The following C# code sets up a grid mesh and extracts structural edges for the simulation. It avoids duplicate edges by sorting and filtering.

void InitializeSimulation()
{
    MeshFilter filter = GetComponent<MeshFilter>();
    Mesh mesh = filter.mesh;

    int gridSize = 21;
    int vertCount = gridSize * gridSize;
    Vector3[] vertices = new Vector3[vertCount];
    Vector2[] uvs = new Vector2[vertCount];

    // Create Grid
    for (int y = 0; y < gridSize; y++)
    {
        for (int x = 0; x < gridSize; x++)
        {
            int idx = y * gridSize + x;
            float fx = (float)x / (gridSize - 1);
            float fy = (float)y / (gridSize - 1);
            vertices[idx] = new Vector3((fx - 0.5f) * 10, 0, (fy - 0.5f) * 10);
            uvs[idx] = new Vector2(fx, fy);
        }
    }

    // Create Triangles
    int trisCount = (gridSize - 1) * (gridSize - 1) * 6;
    int[] triangles = new int[trisCount];
    int t = 0;
    for (int y = 0; y < gridSize - 1; y++)
    {
        for (int x = 0; x < gridSize - 1; x++)
        {
            int tl = y * gridSize + x;
            int tr = tl + 1;
            int bl = (y + 1) * gridSize + x;
            int br = bl + 1;

            triangles[t++] = tl; triangles[t++] = bl; triangles[t++] = tr;
            triangles[t++] = tr; triangles[t++] = bl; triangles[t++] = br;
        }
    }

    mesh.vertices = vertices;
    mesh.triangles = triangles;
    mesh.uv = uvs;
    mesh.RecalculateNormals();

    // Extract Unique Edges
    List<(int, int)> rawEdges = new List<(int, int)>();
    for (int i = 0; i < triangles.Length; i += 3)
    {
        int a = triangles[i];
        int b = triangles[i + 1];
        int c = triangles[i + 2];
        rawEdges.Add(TupleSort(a, b));
        rawEdges.Add(TupleSort(b, c));
        rawEdges.Add(TupleSort(c, a));
    }

    // Sort edges to remove duplicates
    rawEdges.Sort((e1, e2) => 
    {
        if (e1.Item1 != e2.Item1) return e1.Item1.CompareTo(e2.Item1);
        return e1.Item2.CompareTo(e2.Item2);
    });

    List<(int, int)> uniqueEdges = new List<(int, int)>();
    for (int i = 0; i < rawEdges.Count; i++)
    {
        if (i == 0 || rawEdges[i] != rawEdges[i - 1])
        {
            uniqueEdges.Add(rawEdges[i]);
        }
    }

    // Store Edge Data
    springIndices = new int[uniqueEdges.Count * 2];
    restLengths = new float[uniqueEdges.Count];

    for (int i = 0; i < uniqueEdges.Count; i++)
    {
        int idxA = uniqueEdges[i].Item1;
        int idxB = uniqueEdges[i].Item2;
        springIndices[i * 2] = idxA;
        springIndices[i * 2 + 1] = idxB;
        restLengths[i] = Vector3.Distance(vertices[idxA], vertices[idxB]);
    }

    // Initialize velocities
    velocities = new Vector3[vertCount];
}

(int, int) TupleSort(int a, int b)
{
    return a < b ? (a, b) : (b, a);
}

Simulation Loop

The update loop performs the implicit integration. It uses a simplified solver where the Hessian is approximated as a diagonal matrix, avoiding the need for a global linear system solver.

void UpdateStep()
{
    Mesh mesh = GetComponent<MeshFilter>().mesh;
    Vector3[] currentPos = mesh.vertices;
    Vector3[] previousPos = (Vector3[])currentPos.Clone();
    Vector3[] predictedPos = new Vector3[currentPos.Length];
    Vector3[] gradient = new Vector3[currentPos.Length];

    float dt = Time.deltaTime;
    float invDt = 1.0f / dt;
    float invDtSq = invDt * invDt;

    // Predict position and apply damping
    for (int i = 0; i < currentPos.Length; i++)
    {
        // Pin two corners (e.g., top left and top right)
        if (IsPinned(i)) continue;

        velocities[i] *= dampingFactor;
        predictedPos[i] = currentPos[i] + velocities[i] * dt;
        currentPos[i] = predictedPos[i]; // Initial guess for Newton solver
    }

    // Precompute the inverse of the diagonal Hessian approximation
    // H_approx = (1/dt^2 * M) + 4*k
    float diagonalStiffness = invDtSq * mass + 4.0f * stiffness;
    float invHessian = 1.0f / diagonalStiffness;

    // Newton Iteration
    for (int iter = 0; iter < maxIterations; iter++)
    {
        CalculateEnergyGradient(currentPos, predictedPos, dt, gradient);

        for (int i = 0; i < currentPos.Length; i++)
        {
            if (IsPinned(i)) continue;
            // Newton update: x = x - H^-1 * Grad
            currentPos[i] -= invHessian * gradient[i];
        }
    }

    // Update velocities
    for (int i = 0; i < currentPos.Length; i++)
    {
        if (IsPinned(i)) continue;
        velocities[i] = invDt * (currentPos[i] - previousPos[i]);
    }

    mesh.vertices = currentPos;
    ResolveCollisions(currentPos, velocities, dt);
    mesh.RecalculateNormals();
}

bool IsPinned(int index)
{
    // Example: Pin first and last vertex of the first row
    return index == 0 || index == 20;
}

Gradient Calculation

The gradient of the objective function consists of the inertial term (difference between current and predicted position) and the internal spring forces.

void CalculateEnergyGradient(Vector3[] pos, Vector3[] pred, float dt, Vector3[] outGradient)
{
    float invDtSq = 1.0f / (dt * dt);

    // 1. Inertial Force and Gravity
    for (int i = 0; i < pos.Length; i++)
    {
        Vector3 delta = pos[i] - pred[i];
        outGradient[i] = invDtSq * mass * delta - mass * gravity;
    }

    // 2. Spring Forces
    for (int i = 0; i < springIndices.Length; i += 2)
    {
        int idxA = springIndices[i];
        int idxB = springIndices[i + 1];
        int edgeIdx = i / 2;

        Vector3 pA = pos[idxA];
        Vector3 pB = pos[idxB];
        Vector3 diff = pA - pB;
        float dist = diff.magnitude;

        if (dist < 1e-6f) continue; // Avoid division by zero

        // Hooke's Law: F = k * (current_len - rest_len)
        float stretch = dist - restLengths[edgeIdx];
        Vector3 force = (stiffness * stretch / dist) * diff;

        // Apply forces (Newton's 3rd Law)
        outGradient[idxA] += force;
        outGradient[idxB] -= force;
    }
}

Collision Handling

We use a Signed Distance Field (SDF) approach for simple spherical collision. When a vertex penetrates the collider, its position is projected to the surface, and its velocity is adjusted to reflect the impact.

void ResolveCollisions(Vector3[] pos, Vector3[] vel, float dt)
{
    Vector3 sphereCenter = Vector3.zero; // Assuming collider at origin
    float colliderRadius = 2.7f;

    for (int i = 0; i < pos.Length; i++)
    {
        if (IsPinned(i)) continue;

        Vector3 toVertex = pos[i] - sphereCenter;
        float dist = toVertex.magnitude;

        if (dist < colliderRadius)
        {
            Vector3 normal = toVertex / dist; // Normalized direction
            Vector3 surfacePoint = sphereCenter + normal * colliderRadius;

            // Positional Correction
            Vector3 correction = surfacePoint - pos[i];
            pos[i] = surfacePoint;

            // Velocity Update (Impulse)
            vel[i] += correction / dt;
        }
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.