This all started with a desire to learn about how to model the springs and dampers in the context of a vehicle suspension. In my Linear Signals and Systems class during undergrad, we had done some things with second order modeling and Laplace transforms (similar to what I’ve found called the quarter-car model), but I was not sure how this would be implemented in a discrete-time context like programming a simulation of it. I was also inspired by a game called Bridge Builder that I was introduced to at a very young age by my dad who was a bridge inspector for Conrail.
Doing some research I found that Pixar has a series of courses hosted on Kahn Academy called Pixar in a Box – I went through the one on simulation, hosted by Haley Iben, and I was surprised to find just how simple it really was. The course is designed to be used for 5th grade and up but I didn’t find it at all tedious going into it with more experience.
The logic draws from the physics concept of a free body diagram – you can figure out the net forces on an object by summing all forces acting upon it. Once you know the net force on the object, you can use Newton’s second law to find acceleration (f=ma becomes a=f/m). I’ve included here a screenshot of one of the interactive damped spring examples on the Kahn Academy course.
In order to experiment with this more, I put together a simple C++ program using the SDL 2d renderer. My solution looked at the softbody object as a graph composed of nodes and edges – for any given node, the force upon it will come from the sum of gravity and the list of edges that it is a part of. These forces are spring constant and damping rate, and are set by parameters for each edge. This system generalizes to arbitrary graphs of interconnected nodes – that being said, it can become very unstable with distances between nodes being too small or spring constants and damping rates being set to values outside of a pretty limited range.
Each edge keeps a ‘base length’ or the distance between the two points upon initialization. This is used along with Hooke’s law and the spring constant to determine whether the force imparted by that edge will be a push or a pull along the vector between the positions of the two points during the previous update. Similarly, the damping force is based upon the damping rate times the velocity of the node during the previous update. Keeping and using the data from the previous update removes any sequential dependence when evaluating the graph nodes, allowing this to be easily parallelizable.
In the following, links in compression are red, those in tension are blue – this is done by comparing each edge’s current length to the base length. The yellow line goes between the mouse position and the nearest node – when you click, it applies force to the graph.
struct edge
{
float k; //spring constant
float damping_rate; //damping rate
float base_length; //base distance, for compressible springs
int node1; //index of the first node involved in this edge
int node2; //index of the second node involved in this edge
}
struct node
{
vec3 position; //position of the node
vec3 old_position; //previous value of the node's position
vec3 velocity; //velocity of the node
vec3 old_velocity; //previous value of the node's velocity
float mass; //mass of this node
bool anchored; //if anchored, this node does not move
std::vector<edge> edges; //the list of edges this node is part of
}
//these are populated with your nodes and edges
std::vector<node> nodes;
std::vector<edge> edges; //this is redundant, but kept for rendering purposes
//only including the update function, not getting into support functions/init, etc.
void update(int mouse_x, int mouse_y, bool mouse_clicked)
{
//back up position and velocity
for(node in nodes)
{
node.old_position = node.position;
node.old_velocity = node.velocity;
}
if(mouse_clicked)
{
//determine the index of the node that lies closest to vec3(mouse_x, mouse_y, 0)
}
//this loop could be parallelized
for(node in nodes)
{
if(!node.anchored)
{
vec3 accumulated_force = vec3(0,0,0); //initially zero force
for(edge in node.edges)
{
//each edge can impart spring and damping forces
float k = edge.k;
float d = edge.damping_rate;
vec3 my_position = node.old_position;
vec3 ur_position = nodes[edge.node2];
float spring_ratio = distance(my_position, ur_position)/edge.base_length;
vec3 springforce = -k * (my_position-ur_position) * (spring_ratio-1);
vec3 my_velocity = node.old_velocity;
vec3 dampforce = d * my_velocity;
force += (springforce - dampforce);
}
force += node.mass * vec3(0,-GRAVITY,0);
if(mouse_clicked && current node index == index of the closest node to the mouse)
{
//apply some force based on an edge between mouse and that node
}
vec3 acceleration = force/node.mass; //Newton's second law
node.velocity = node.old_velocity + acceleration * TIMESTEP; //update velocity
node.position = node.old_position + node.velocity * TIMESTEP; //update position
}
}
}
All the computation for this project was already done with vec3’s – everything’s just in the z=0 plane. This was a put together as a relatively quick prototype, and there’s certainly room for improvement. In the writeup on the 3D version, I detail some of the directions I explored – I want to explore this parallelizability further once I figure out std::thread and see if it’s as significant a speedup as I suspect it would be.
Last updated 5/28/2020