5

I am attempting to make an elementary soft body engine in C++ using SDL2. It works by considering all the vertices of the soft body to be interconnected by springs of same length and rigidity (with the same spring constant k and length natural_length). To make it more realistic, I also introduced a damping constant c.
However, I encountered a frustrating problem. I have been trying to debug it for past 6-7 hours but to no avail. The soft body encounters many strange bugs which I don't understand

  • Firstly, the "soft body" is not at all "soft". It becomes a crumpled mess of points every time. I have tried calculating the force of neighbouring points instead only, but it still becomes a crumpled mess.
  • The soft body flies up to the top corner (origin) every time even though I haven't implemented any external forces.

Both of these bugs are visible in this image - bug

The 2 following functions (they are in the same class as with all the variables, hence need not take in any arguments) are the actual simulation part of the code. (I have omitted the rest of the code as it is unnecessary.)
I used a vector of SDL_Points to store every point and a vector of Vector to store their velocities. If you are wondering what Vector is, it is simply a struct I created which simply has 2 float members x and y.
The acceleratePoints() function assigns velocity and position to each point and checkCollision() well... checks for collisions with the walls of the window, which has width scr_w and height scr_h.

    void acceleratePoints()
    {
        vector<SDL_Point> soft_body_copy=soft_body;
        vector<Vector> velocity_copy=velocity;
        for(int i=0;i<soft_body.size();++i)
        {
            for(int j=0;j<soft_body.size();++j)
            {
                if(i!=j)
                {
                    Vector d={(soft_body[j].x-soft_body[i].x)/100.0,(soft_body[j].y-soft_body[i].y)/100.0};
                    float t=atan2(d.y,d.x);
                    float disp=fabs(magnitude(d))-natural_length/100.0;
                    velocity_copy[i].x+=(k*disp*cos(t))/10000.0;
                    velocity_copy[i].y+=(k*disp*sin(t))/10000.0;
                    velocity_copy[i].x-=c*velocity_copy[i].x/100.0;
                    velocity_copy[i].y-=c*velocity_copy[i].y/100.0;
                    soft_body_copy[i].x+=velocity_copy[i].x;
                    soft_body_copy[i].y+=velocity_copy[i].y;
                }
            }
            soft_body=soft_body_copy;
            velocity=velocity_copy;
        }
    }
    void checkCollision()
    {
        for(int k=0;k<soft_body.size();++k)
        {
            if(soft_body[k].x>=scr_w||soft_body[k].x<=0)
            {
                velocity[k].x*=e;
                soft_body[k].x=soft_body[k].x>scr_w/2?scr_w-1:1;
            }
            if(soft_body[k].y>=scr_h||soft_body[k].y<=0)
            {
                velocity[k].y*=e;
                soft_body[k].y=soft_body[k].y>scr_h/2?scr_h-1:1;
            }
        }
    }

The magnitude() function returns the magnitude of a Vector.
The values for coefficient of restitution e, damping constant c and spring constant k, which I used for the image are 0.5, 10 and 100 respectively. Thank you for taking the time to read this! Help would be greatly appreciated.

Edit

Here is the entire code if anyone wants to test it. You'll need SDL and a folder 'img' with a '.bmp' file in 'img/point.bmp'.

Community
  • 1
  • 1
AvZ
  • 997
  • 3
  • 14
  • 25
  • 1
    What does the body look like at rest? Also, you ought to consider wrapping the vector algebra with appropriate operators; that will help to make higher level mistakes easier to spot... – defube Mar 18 '15 at 18:19
  • @defube The body is a collection of points that is inputted by the user. I inputted a collection of a few points roughly in the shape of ellipse. – AvZ Mar 18 '15 at 18:21
  • For this to work, the body needs a "rest shape" from which the length of each spring is calculated. If you are allowing a random point cloud, you could connect all approximate 1-neighbors of points with springs. EDIT: Each spring needs its own length! – defube Mar 18 '15 at 18:26
  • @defube I input the shape in roughly a regular polygon manner to ensure that it is not deformed too much initially. I think it should work even if it was not perfectly inputted as it should get it's regular polygon shape back after being deformed (because it's a soft body). I tried using only the two connected neighbours directly next to the point but it still got crumpled – AvZ Mar 18 '15 at 18:32
  • @defube Each spring has the same length `natural_length` – AvZ Mar 18 '15 at 18:33
  • The single length parameter is the problem, which I'll explain in my answer. – defube Mar 18 '15 at 18:33

1 Answers1

4

Spring-based soft body simulation requires a "rest configuration", wherein without gravity or other external forces, the body will remain internally motionless.

Each spring in the simulation needs its own length. A shared natural_length will cause springs to exert forces within the body where the input separation between connected points is different, which is what appears to be causing the problems you describe.

A typical approach to generate a soft-body (or "point blob") out of a set of points looks like this:

  • Generate the Delaunay Triangulation (or Tetrahedralization, in 3D) of the point set.
  • Create springs on every edge of the triangulation, using the length of the edge as the spring's rest length.
  • Simulate with "gravity" and other external forces!

For simplicity, you can just connect all pairs of points, rather than generating a triangulation.

The simulation itself can be performed in three steps:

  • Clear vertex force accumulators
  • Add spring forces to their connected vertices
  • Update the velocity and position of each vertex

In the spirit of the code you've posted, a Spring might look like:

struct Spring
{
    int point_index[2];
    float rest_length;
};

ASIDE

In your code, you can replace t = atan2(d.y, d.x) and the following sin(t) cos(t) with a unit-length image of d:

float mag_d = magnitude(d); // should not become negative...
float r = (0.0f != mag_d) ? 1.0f/mag_d : 0.0f;
Vector d_hat{d.x*r, d.y*r};
float w = k * (mag_d - spring[i].rest_length);
velocity_copy[i].x += w*d_hat.x;
velocity_copy[i].y += w*d_hat.y;

Not completely optimized, but just a little more stable.

EDIT

I've also noticed you were scaling the rest length (dividing it by 100). Don't do that.

ANOTHER EDIT

Change this:

    }
    soft_body=soft_body_copy;
    velocity=velocity_copy;
}

To this:

    }
}
soft_body = move(soft_body_copy);
velocity = move(velocity_copy);

You were changing vertex positions while calculating forces.

defube
  • 2,395
  • 1
  • 22
  • 34
  • How do I store the rest lengths between 2 points? The number of points will be determined by the user's input during runtime. Also, why does the object get dragged to the top-left? – AvZ Mar 18 '15 at 18:53
  • 2
    Use the distance between them. You'll need a separate collection of spring structures; each one stores the indices of its endpoints and its length. When the body is updated, the simplest thing to do would be to add one new spring for each existing point, all connected to the new point (insert the new point first to get its index, then construct the new springs). – defube Mar 18 '15 at 18:58
  • Say, I wanted to refer to springs between two points in `soft_body` with indices `i` and `j`. How would I get it? If you could induct it into your answer, it would help very much. Thanks – AvZ Mar 18 '15 at 19:05
  • 1
    `unordered_map,spring_type> springmap;` and then `spring_type& spring = springmap[{i,j}];`? – Mooing Duck Mar 18 '15 at 19:08
  • @MooingDuck Ah, I didn't know about `unordered_map`. They seem perfect for this application. Thanks. – AvZ Mar 18 '15 at 19:11
  • @defube I divided the lengths by hundred to obtain the screen as 5 meter by 5 meter in physical terms as the window was 500x500. Why will it be bad? – AvZ Mar 18 '15 at 19:12
  • 1
    @AvZ Perform the simulation in an abstract model space (map cursor to model space before adding a point), or scale the lengths before storing them. The point here is to keep the code as simple as possible (not letting details about the screen resolution sneak into the simulation code). – defube Mar 18 '15 at 19:14
  • @defube I put it so the screen would be scaled to 1 meter per 100 pixels. This way I would be allowed to put in constants in SI units. – AvZ Mar 18 '15 at 19:17
  • 2
    @AvZ Don't worry about the units or scale when working on the simulation code. Screen space coordinates can be scaled using a separate transformation. – defube Mar 18 '15 at 19:19
  • @defube Okay thanks. I will accept your answer. I will try your suggestions tomorrow as it is almost 1AM here. – AvZ Mar 18 '15 at 19:22
  • 1
    I'd like to add a strong reinforcement to the principle "first calculate all forces, then update velocities and positions". You might even work some Verlet or Leapfrog mechanism into this. The point being, the forces depend on the position. Updating some positions while still computing forces introduces an unphysical bias towards the evaluation order that is responsible for the observed drift. – Lutz Lehmann Mar 18 '15 at 21:06
  • @LutzL That is exactly what I did. I used the `soft_body_copy` for this purpose. – AvZ Mar 19 '15 at 05:28
  • Okay, this I tried everything you said. Still doesn't work. Something else is up. – AvZ Mar 20 '15 at 13:16
  • @AvZ: Your `_copy` structures do not really help. The velocity contribution to [1] from the interaction of [1] and [2] gets added again during the interaction update of [1] and [3], then again for the interaction of [1] and [4]... and then gets doubles while computing the interaction of [2] and [1], then [3] and [1]... While the interaction of the last two points gets only added twice. So the principle remains: First compute all the force terms. Then, with the interactions out of the way, the velocity and position updates can be computed in one loop. – Lutz Lehmann Mar 21 '15 at 08:49