ENG | Random ChatGPT abstract art animation (slime mold sim)
Agent-based simulation where particles leave and follow trails on an evaporating field, forming organic branching patterns.
Today I wanted to create some abstract art and noticed it remotely resembles reaction-diffusion pattern which I implemented in C++ few years ago (and it certainly deserves article about optimizations) and which is on my github. So after some discussion what I want, I specified I want something like a slime mold simulation. Slime mold can be seen for example in this YouTube video.
ChatGPT generated code which required only minor changes such as resolution and number of frames. It might be optimization challenge for future.
Description
The program simulates a group of agents moving in a 2D field. Each agent senses pheromone levels ahead, slightly left, and slightly right, then adjusts its direction to follow stronger signals. As it moves, it deposits pheromone into the field. The field evaporates slowly, so trails fade over time. This feedback loop causes trails to strengthen where many agents travel and disappear elsewhere, producing branching patterns that can merge, split, or fade depending on agent movement.
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// slime_mold.cpp
#include <SDL2/SDL.h>
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
constexpr int WIDTH = 1280;
constexpr int HEIGHT = 720;
constexpr int NUM_AGENTS = 120000;
constexpr float SENSOR_ANGLE = 0.5f;
constexpr float SENSOR_DIST = 5.0f;
constexpr float TURN_ANGLE = 0.3f;
constexpr float STEP_SIZE = 1.0f;
constexpr float EVAPORATE = 0.85f;
constexpr int FRAMES = 5000;
struct Agent {
float x, y, angle;
};
std::vector<Agent> agents(NUM_AGENTS);
std::vector<float> field(WIDTH * HEIGHT, 0.0f);
inline int idx(int x, int y) {
return (y % HEIGHT) * WIDTH + (x % WIDTH);
}
float sampleField(float x, float y) {
int xi = ((int)std::round(x) + WIDTH) % WIDTH;
int yi = ((int)std::round(y) + HEIGHT) % HEIGHT;
return field[idx(xi, yi)];
}
void deposit(Agent &a) {
int xi = ((int)std::round(a.x) + WIDTH) % WIDTH;
int yi = ((int)std::round(a.y) + HEIGHT) % HEIGHT;
field[idx(xi, yi)] += 1.0f;
}
void updateAgents() {
for (auto &a : agents) {
// Sample sensors
float sx = std::cos(a.angle);
float sy = std::sin(a.angle);
float c = sampleField(a.x + sx * SENSOR_DIST, a.y + sy * SENSOR_DIST);
float l = sampleField(a.x + std::cos(a.angle - SENSOR_ANGLE) * SENSOR_DIST,
a.y + std::sin(a.angle - SENSOR_ANGLE) * SENSOR_DIST);
float r = sampleField(a.x + std::cos(a.angle + SENSOR_ANGLE) * SENSOR_DIST,
a.y + std::sin(a.angle + SENSOR_ANGLE) * SENSOR_DIST);
// Adjust angle
if (c > l && c > r) {
// Straight
} else if (l > r) {
a.angle -= TURN_ANGLE;
} else if (r > l) {
a.angle += TURN_ANGLE;
} else {
a.angle += (rand() % 2 ? 1 : -1) * TURN_ANGLE;
}
// Move
a.x += std::cos(a.angle) * STEP_SIZE;
a.y += std::sin(a.angle) * STEP_SIZE;
// Wrap around
if (a.x < 0) a.x += WIDTH;
if (a.x >= WIDTH) a.x -= WIDTH;
if (a.y < 0) a.y += HEIGHT;
if (a.y >= HEIGHT) a.y -= HEIGHT;
deposit(a);
}
}
void diffuse() {
// Evaporation only for simplicity
for (auto &v : field) v *= EVAPORATE;
}
void renderToPixels(std::vector<uint8_t> &pixels) {
for (int i = 0; i < WIDTH * HEIGHT; i++) {
uint8_t c = (uint8_t)std::min(field[i] * 10.0f, 255.0f);
pixels[i * 3 + 0] = c; // Blue
pixels[i * 3 + 1] = 0; // Green
pixels[i * 3 + 2] = c; // Red
}
}
void saveTGA(const char *filename, const std::vector<uint8_t> &pixels) {
FILE *f = fopen(filename, "wb");
if (!f) return;
uint8_t header[18] = {};
header[2] = 2; // uncompressed true-color
header[12] = WIDTH & 0xFF;
header[13] = (WIDTH >> 8) & 0xFF;
header[14] = HEIGHT & 0xFF;
header[15] = (HEIGHT >> 8) & 0xFF;
header[16] = 24; // bits per pixel
fwrite(header, 1, 18, f);
fwrite(pixels.data(), 1, pixels.size(), f);
fclose(f);
}
int main() {
srand((unsigned)time(0));
for (auto &a : agents) {
a.x = rand() % WIDTH;
a.y = rand() % HEIGHT;
a.angle = (rand() / (float)RAND_MAX) * 2.0f * M_PI;
}
SDL_Init(SDL_INIT_VIDEO);
SDL_Window *window = SDL_CreateWindow("Slime Mold", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WIDTH, HEIGHT, 0);
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
std::vector<uint8_t> pixels(WIDTH * HEIGHT * 3, 0);
for (int frame = 0; frame < FRAMES; frame++) {
updateAgents();
diffuse();
renderToPixels(pixels);
// Update window
SDL_UpdateTexture(texture, NULL, pixels.data(), WIDTH * 3);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
// Save every 10 frames
if (frame % 10 == 0) {
char filename[64];
sprintf(filename, "frame_%04d.tga", frame);
saveTGA(filename, pixels);
}
// Handle quit
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) frame = FRAMES;
}
}
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
Useful commands
1
2
3
4
5
6
7
8
# Compile (maybe add -O3 -march=native)
g++ slime_mold.cpp -o slime_mold `sdl2-config --cflags --libs`
# Run
./slime_mold
# Encode video
ffmpeg -framerate 30 -pattern_type glob -i "frame_*.tga" -c:v libsvtav1 -an slime_mold.webm