Plasmodial Slime Simulation

An aesthetic real-time simulation of Physarum polycephalum slime mold growth powered by Unity compute shaders.


Unity simulation of plasmodial slime
Example slime simulation output in Unity.

Getting Started

Plasmodial Slime Simulation is a Unity project that models the behavior of plasmodial slime molds like physarum polycephalum. It was heavily inspired by Sebastian Lague's video, Coding Adventure: Ant and Slime Simulations.

Credits: This project was originally created by Praccho Muna-McQuay, Alaina Lin, and myself for our CSCI 1230 (Computer Graphics) final project at Brown University.


Features

  • Support for up to four different slime species at once.
  • Foraging behavior based on an attractor-field of food sources.
  • Brushes to paint and erase food/slime on the simulation canvas.
  • Starting seed library to begin the simulation, including a circle, big-bang, and starburst.
  • Rapid simulation supporting over 1-million concurrent slime agents using parallel computations on the GPU.
  • Customizable parameters for nearly every aspect of the simulation displayed on an interactive GUI.

Installation

  1. Install Unity 2022.3.14f1.
  2. Clone the project repository:
Terminal
git clone https://github.com/starboi-63/plasmodial-slime
  1. Open the cloned project's root directory, plasmodial-slime, in Unity and double click the GUI scene in Assets/Scenes near the bottom of the screen.
The GUI scene in Assets/Scenes.
The GUI scene in Assets/Scenes.

Usage

Running the Simulation

To start the simulation, simply click the play button at the top of the screen (shown below). Alternatively, click Build and Run to execute the simulation outside of the editor.

Unity play button.
Unity play button.

Modifying Settings

Use the panel on the left to adjust simulation settings, including slime agent initialization patterns, species parameters, food sources, and brush settings. Each of these settings is described in detail below.

Left panel controlling simulation settings.
Left panel controlling simulation settings.

Demo

Once the simulation is running, you can freely interact with it in real-time by clicking and dragging with your mouse on the canvas.

Demo of slime simulation features.
Demo of slime simulation features.

How it Works

Slime Agents

The slime that appears in the simulation is composed of hundreds of thousands of individual entities called slime agents. These agents move according to a fixed set of rules that determine their behavior frame by frame.

As slime agents move, they deposit chemoattractant on the canvas which is detected by other nearby agents via three sensors (front-left, front, and front-right). This slime agent behavior is implemented based on Characteristics of pattern formation and evolution in approximations of physarum transport networks (Jones).

In particular, a slime agent is defined by the following properties:

SlimeSimulation.compute
1struct SlimeAgent {
2    float2 position; // (x,y) in canvas space
3    float angle;     // direction agent faces in radians
4    int speciesID;   // identifying index into species buffer 
5    float hunger;    // number in range [0-1]: 1 is full, 0 is starving
6};
7
8struct SpeciesSettings {
9    float sensorAngle;     // angle offset of left/right sensors in radians
10   float rotationAngle;   // should be bigger than sensorAngle to avoid convergence; adjusted by random offset in compute shader 
11   int sensorDist;        // distance from agent position to sensor
12   int sensorRadius;      // radius of an individual sensor circle (similar to SW)
13   float velocity;        // speed of agent
14   float trailWeight;     // weight of deposited trail (i.e. chemoattractant)
15   float hungerDecayRate; // rate at which hunger increases
16   float4 color;          // RGBA color of agent
17};

Visually, a slime agent of a particular species is represented by the following diagram from Jones:

Slime agent diagram on pg. 133 of Artificial Life, Volume 16, Number 2.
Slime agent diagram on pg. 133 of Artificial Life, Volume 16, Number 2.

It's important to note that we use circular sensors to detect deposited chemoattractant on the trail-map rather than the square sensors used in the original paper. Hence, our implementation uses a parameter called sensorRadius to define the radius of an individual sensor circle instead of the Sensor Width (SW) in the diagram.

Each iteration of the simulation, an agent can either turn left, turn right, turn in a random direction, or stay facing in the same direction. The agent's movement is determined by comparing the chemoattractant levels detected by its three sensors. Chemoattractant deposited on the canvas is stored in an image texture called the trail-map which is updated every iteration.

The precise calculation of an agent's movement is given by the following snippet:

SlimeSimulation.compute
1// random weight for turning 
2int rand = hash(agent.position.y * width + agent.position.x + hash(id.x + time * 100000));
3
4// sample the trail-map at the three sensor positions
5float leftSample = sampleTrail(agent, agentSettings.sensorAngle);
6float midSample = sampleTrail(agent, 0);
7float rightSample = sampleTrail(agent, -agentSettings.sensorAngle);
8
9// calculate turn weight and angle
10float turnWeight = scaleToRange01(rand);
11float turnAngle = agentSettings.rotationAngle;
12
13// determine agent movement based on sensor readings
14if ((midSample > leftSample) && (midSample > rightSample)) {
15    // don't turn
16    slimeAgents[id.x].angle += 0;
17} else if ((midSample < leftSample) && (midSample < rightSample)) {
18    // turn randomly
19    slimeAgents[id.x].angle += 2 * (turnWeight - 0.5) * turnAngle * dt;
20} else if (leftSample > rightSample) {
21    // turn left
22    slimeAgents[id.x].angle += turnWeight * turnAngle * dt;
23} else if (rightSample > leftSample) {
24    // turn right
25    slimeAgents[id.x].angle -= turnWeight * turnAngle * dt;
26} else {
27    // do nothing
28    slimeAgents[id.x].angle += 0;
29}

After updating the agent's angle, we calculate the new position of the agent based on its velocity, effectively stepping the agent forward in the direction it is facing similar to an Euler integration step.

Food Sources

Food sources in the simulation are discrete points on the canvas which create an attractor field that affects slime agent direction based on the inverse-square law. This food behavior is implemented based on Stepwise slime mould growth as a template for urban design (Kay, Mattacchione, Katrycz, Hatton).

A food source is defined by the following properties:

SlimeSimulation.compute
1struct FoodSource {
2    float2 position;         // (x,y) in canvas space
3    float attractorStrength; // strength of attractor field
4    int amount;              // amount of food left at the source
5};

While the simulation is running, each slime agent calculates the direction from its position, ps\vec{p}_{s}, to that of the nearest food source, pf\vec{p}_{f}, and adjusts its movement based on the attractor field strength, cc. If it's close enough to the food source, the agent will also consume the food and reset its hunger to full, allowing it to continue foraging away from the source.

We calculate the perceived food force on an agent, F\vec{F}, by the following formula:

d^=pfpspfpsandF=cpfps2d^ \hat{d} = \frac{\vec{p}_{f} - \vec{p}_{s}}{\|\vec{p}_{f} - \vec{p}_{s}\|} \quad \text{and} \quad \vec{F} = \frac{c}{\|\vec{p}_{f} - \vec{p}_{s}\|^2} \cdot \hat{d}

The implementation of this calculation is given below:

SlimeSimulation.compute
1// after finding the closest food source, calculate direction and distance
2float2 posToClosestFood = closestFood.position - agent.position;
3float dist = length(posToClosestFood);
4float2 dir = normalize(posToClosestFood);  
5
6// if the agent is close enough to the food source, eat it
7if (dist < 1) {
8    if (foodDepletionEnabled && closestFood.amount > 0) {
9        // atomic decrement to avoid race conditions
10        InterlockedAdd(foodSources[closestFoodIndex].amount, -1);
11    }
12
13    if (foodSources[closestFoodIndex].amount <= 0) {
14        foodSources[closestFoodIndex].attractorStrength = 0;
15    }
16
17    // reset agent hunger to full
18    slimeAgents[agentIdx].hunger = 1.0;
19}
20
21return dir * (closestFood.attractorStrength / (dist * dist));

The angle of this scaled direction vector in radians, θF=atan2(Fy,Fx)\theta_F=\mathrm{atan2}(\vec{F}_y, \vec{F}_x), is then blended with the slime agent's current angle, θs\theta_s, based on the agent's hunger level. The agent's hunger level, hh, is a value between 0 and 1, where 1 is full and 0 is starving.

Let β\beta denote the blending constant. Then, the agent's new angle after food force is considered is:

θnew=βθF+(1β)θswhereβ=F(1h)8 clamped to [0,1] \theta_{\text{new}} = \beta \cdot \theta_F + (1 - \beta) \cdot \theta_s \quad \text{where} \quad \beta = \|\vec{F}\| \cdot (1 - h)^8 \text{ clamped to } [0, 1]

Like before, here is the implementation of this calculation:

SlimeSimulation.compute
1// initialize force on agent to zero
2float2 forceOnAgent = float2(0.0, 0.0);
3    
4// calculate attractive force from nearest food source (snippet above)
5if (numFoodSources > 0) {
6    forceOnAgent = foodForce(id.x);
7}
8
9// blend force with agent's current angle based on hunger level
10if (length(forceOnAgent) > 0) {
11    float forceComponent = length(forceOnAgent);
12    float hungerComponent = pow(1.0 - agent.hunger, 8);
13    float blendValue = clamp(forceComponent * hungerComponent, 0.0, 1.0);
14    slimeAgents[id.x].angle = (blendValue * atan2(forceOnAgent.y, forceOnAgent.x)) + ((1.0 - blendValue) * slimeAgents[id.x].angle);
15}
16
17// move agent forward in the direction it is facing
18step(id.x);
19
20// decay agent hunger
21slimeAgents[id.x].hunger = clamp(slimeAgents[id.x].hunger - agentSettings.hungerDecayRate * dt, 0.0, 1.0);

Project Structure

C# Files

In Assets/Scripts:

  • SlimeSimulation.cs: main entry point running the simulation.
  • SimulationSettings.cs: global settings for the simulation.
  • SlimeAgent.cs: definition of a single slime agent.
  • ComputeUtilities.cs: utilities for creating and writing to textures.
  • FoodSource.cs: definition of a single food source.

Compute Shaders

In Assets/Shaders:

  • SlimeSimulation.compute: multiple entry points (i.e. kernels) for slime agent logic and reading/writing from textures using parallel computations on the GPU.

Final Remarks

Thank you for reading this documentation! If you have any questions or feedback, please feel free to create an issue on the project repository. I hope you enjoy experimenting with the simulation and creating your own unique patterns with slime friends :).

Slime simulation credits.