About

This project implements a parallel mass-spring-damper system to make squishy pixel-art spaceships. All of the calculations are done by GLSL shaders on the GPU.

Fly!
(I am not a very good spaceship pilot)

Physics

The chosen model turns each pixel into an array of four masses, connected and cross-linked by spring-dampers. Nodes are shared between neighbouring pixels.

Pixel nodes

Each node is defined by a state that contains $xy$ position and velocity: $(x, y, \dot x, \dot y)$. When integrating, we need to find the derivative of this state vector. The derivative of position is simply velocity; acceleration is trickier to calculate.

Consider nodes $a$ and $b$. The system knows their current state and the nominal $xy$ vector between the two nodes, which we will call $d$. We want to find the forces exerted by $b$ (the far point) on $a$ (the near point).


For convenience, we'll define the symbols shown in the illustration below:

Pixel values

$a$ and $b$ are the current position of the near and far points, with components $x,y$. Their derivatives $\dot a$ and $\dot b$ contain the velocity components of the current state.

$d$ is the original $xy$ vector between the points (so $|d|$ is the rest length of the spring).

$v$ is the actual vector between the points in their current state (i.e. $v = b - a$). Like $d$, it is an $xy$ vector.

$v'$ (not shown) is a normalized version of $v$ (i.e. $v' = v / |v|$ )

Finally, we'll define $k$ and $c$ as spring and damper constants, respectively.


The force on each node can be broken down into two components.

The spring force varies with the difference between the rest length and distance between the nodes:

$$ F_{k} = -k\left( |d| - |v| \right) v' $$

The damper force varies with the relative velocity between the points (projected onto the segment joining the two points):

$$ F_{c} = -v'c\left( \left(\dot a - \dot b\right)\cdot v'\right) $$

These two components are calculated for every node-node linkage and summed to give the total force on each node. Dividing by mass gives acceleration on each node.

With the complete $(\dot x, \dot y, \ddot x, \ddot y)$ derivative, numerical integration can be used to find the state at the next timestep.

Integration

State is stored as an RGBA32F texture (i.e. four floating-point texture channels). R and G are x and y position; B and A are x and y velocity. Each texture is one texel wider and taller than the ship image (because each ship pixel maps to four nodes).

Six of these textures are used in total. Two store state, using a ping-pong strategy where one is being read and the other being written. The other four store derivatives needed for RK4 integration.

Each RK4 integration stage requires 8 render passes, using various GLSL shaders to calculate intermediate derivatives and states. The target texture for each pass is mapped to a framebuffer object for writing.

When the new state is calculated, that texture is read back to the CPU so that the camera can track the center of the ship. This is the only bus traffic during the entire process.

With an NVIDIA GeForce GT 750M 2048 GPU, the system maintains 60 FPS while running up to 75 RK4 stages per frame. That comes out to 36000 render-to-framebuffer and 60 render-to-screen operations per second -- not bad for a middle-of-the-range laptop GPU.

Ships

Ships are stored as .png images with an alpha channel. Every pixel with a non-zero alpha channel is treated as part of the ship. Pure red pixels (255, 0, 0) are thrust engines; almost-pure red pixels with one or two bits of blue are left and right engines, respectively.

Yellow ship

Purple ship

Viper ship

Challenges

Ships much larger than those shown above need very large spring and damper constants to be sufficiently stiff. These high constants produce very stiff differential equations, which require many stages of RK4 to integrate accurately.

RK4 was chosen as a good compromise between accuracy and complexity; for large, stiff simulations, another integration strategy may be necessary.

Implementation

As a learning exercise, this code was intentionally written close to the OpenGL metal. It requires glfw and libpng, as well as CMake and a C++11-compliant compiler.

OpenGL 3.3 is required, although it could work on older OpenGL versions if GL_ARB_framebuffer_object is present; this would require back-porting the shaders to an older version of GLSL.

As always, the code is available on Github. Comments, issues, and pull requests are welcome.

References

GLSL render to texture

Integration basics

Cloth simulation

Learning Modern OpenGL Graphics Programming