Post

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.

ENG | Random ChatGPT abstract art animation (slime mold sim)

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
This post is licensed under CC BY 4.0 by the author.