Slime simulation

This is a compute shader that simulates thousands or millions of agents and observe emergent behaviours as you change their parameters. Largely inspired by Sebastian Lague's video on the subject you can watch here.

The folder containing the project can be downloaded and run from here or the demo code at the bottom of the page (limited to only one colour palette and no shortcuts for starting patterns) can be run provided you have the necessary python extensions.

import moderngl
import numpy as np
from math import *
np.set_printoptions(threshold=np.inf)
import struct
import glfw
glfw.init()
window = glfw.create_window(1000,1000, "Slime Simulation", None, None)
glfw.make_context_current(window)
ctx = moderngl.create_context()
# draw a fullscreen quad to the framebuffer to write the texture to the buffer
program = ctx.program(vertex_shader='''
#version 430

in vec2 in_vert;
out vec2 pos;

void main() {
    pos = in_vert * 0.5 + 0.5;
    gl_Position = vec4(in_vert, 0.0, 1.0);
}
''', fragment_shader='''
#version 430

in vec2 pos;
out vec4 out_color;

uniform bool to_screen;
uniform bool left_click;
uniform bool right_click;
uniform float radius;
uniform vec2 Mpos;
uniform vec2 res;
uniform float fade;
uniform float blur;
uniform sampler2D u_texture;

void main() {
    vec2 lum = texture(u_texture, pos).xy;
    if (to_screen){
        out_color = vec4(0,0.1,0.2,1) + vec4(sin(lum.y*3),0,cos(lum.y*9),1)*lum.x + vec4(0.7,0.8,0.8,1)*lum.x;
    }
    else{
        lum *= (1/blur);
        lum += texture(u_texture, pos + vec2(1,0)/res.x).xy;
        lum += texture(u_texture, pos + vec2(0,1)/res.y).xy;
        lum += texture(u_texture, pos + vec2(-1,0)/res.x).xy;
        lum += texture(u_texture, pos + vec2(0,-1)/res.y).xy;
        lum /= 4+1/blur;
        if (left_click && distance(Mpos,pos)<radius){
            out_color.x = 40;
        }
        else if (right_click && distance(Mpos,pos)<radius){
            out_color.x = -10;
        }
        else if (lum.x==1.1){
            out_color.x = 1;
            }
        else{
            out_color.xy = lum*(1.0-fade);
            }
    }
}
''')
vbo = ctx.buffer(struct.pack('8f', -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0))
vao = ctx.vertex_array(program, vbo, 'in_vert')

compute_shader = ctx.compute_shader("""
#version 430

layout (local_size_x = 64, local_size_y = 1) in;
layout(std430,binding = 0) buffer agent_buffer{ vec3 agents[];};
layout(std430,binding = 1) buffer pixel_buffer{ vec2 pixels[1000][1000];};
layout(binding=2) uniform sampler2D u_texture;

#define TAU 3.1415926538*2
uniform float turn_rate;
uniform float sensor_angle;
uniform float sensor_dist;
uniform float randomness;
uniform float stepsize;
uniform uvec2 resolution;
uniform uint frame;

float rand(vec2 co){
    return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453)-0.5;
}

void main() {
    uint ID = gl_GlobalInvocationID.x;
    vec3 agent = agents[ID];
    // Update agent position and velocity
    if (randomness != 0){
        agent.z += rand(vec2(ID,frame))/(1/randomness*100)*TAU;
        }
    vec2 sensor_a_pos;
    sensor_a_pos.x = (sin((agent.z+sensor_angle)*TAU)*sensor_dist+agent.x*resolution.x);
    sensor_a_pos.y = (cos((agent.z+sensor_angle)*TAU)*sensor_dist+agent.y*resolution.y);
    float sensor_a = pixels[int(sensor_a_pos.x)][int(sensor_a_pos.y)].x;
    vec2 sensor_b_pos;
    sensor_b_pos.x = (sin((agent.z-sensor_angle)*TAU)*sensor_dist+agent.x*resolution.x);
    sensor_b_pos.y = (cos((agent.z-sensor_angle)*TAU)*sensor_dist+agent.y*resolution.y);
    float sensor_b = pixels[int(sensor_b_pos.x)][int(sensor_b_pos.y)].x;
    agent.z -= (sensor_b-sensor_a)*turn_rate;
    
    vec2 step = vec2(sin(agent.z*TAU),cos(agent.z*TAU))*stepsize/1000;
    agent.xy += step;
    if (agent.x >= 1||agent.x <= 0){
        //agent.z = 1 - agent.z;
        agent.x -= 1*sign(agent.x);
    }
    if (agent.y >= 1||agent.y <= 0){
        agent.y -= 1*sign(agent.y);
    }
    // Store the updated particle data
    agents[ID] = agent;
    pixels[int(agent.x*resolution.x)][int(agent.y*resolution.y)].x = 1.1;
    pixels[int(agent.x*resolution.x)][int(agent.y*resolution.y)].y = agent.z;
}
"""
)
#Initialises window
glfw.set_time(0)
program["u_texture"] = 2
resolution = glfw.get_window_size(window)
compute_shader["resolution"] = resolution
program["res"] = resolution
#------------------------------
compute_shader["randomness"] = 0.1 #Controls randomness of agents
sensor_dist = 50 #Generally controls scale of paths formed
turn_rate = 0.015 #Controls smoothness of paths formed
sensor_angle = 0.1#Sensor angle in number of turns away from direction
num_agents = 65535 #max is 65535 on laptops it is the max 16bit number
#Sets fade and blur amount
program["blur"] = 1
fade = 0.015
stepsize = 2
program["radius"] = 0.013 #Radius of mouse pixel influence
#------------------------------

#initialises buffer of agents positions and velocities to be read by the compute shader
agent_data = np.random.rand(num_agents, 3).astype(dtype=np.float32)
#for agent in agent_data:
 #   agent[0] = cos(agent[2]*2*pi)/2+0.5
  #  agent[1] = sin(agent[2]*2*pi)/2+0.5

agent_buffer = ctx.buffer(agent_data.tobytes())
agent_buffer.bind_to_storage_buffer(0)
  
#Sets a buffer of all pixels to be modified by the compute shader  
pixel_data = np.zeros((resolution[0],resolution[1],2),np.float32)
pixel_buffer = ctx.buffer(pixel_data.tobytes())
pixel_buffer.bind_to_storage_buffer(1)
#Creates a texture for the pixels
pixel_texture = ctx.texture(resolution, 2, dtype = "f4")
pixel_texture.use(2)

#Sets the framebuffer as the render target and renders to the last_frame texture
last_frame = ctx.texture(resolution, 2, dtype = "f4")
fbo = ctx.framebuffer(last_frame)
fbo.use()

frame = 0
time = 0
while not glfw.window_should_close(window):
    delttime = glfw.get_time() - time
    print(round(delttime*1000,3))
    time = glfw.get_time()
    glfw.poll_events()
    resolution = glfw.get_window_size(window)
    #Get cursor
    x,y = glfw.get_cursor_pos(window)
    program["Mpos"] = [max(0, min(x/resolution[0],1)),max(0, min(1-y/resolution[1],1))]
    program["left_click"] = glfw.get_mouse_button(window, 0)
    program["right_click"] = glfw.get_mouse_button(window, 1)
    
    if glfw.get_key(window,glfw.KEY_MINUS):
        turn_rate *= 0.99
        print("Turn_rate:",round(turn_rate,5))
    if glfw.get_key(window,glfw.KEY_EQUAL):
        turn_rate *= 1.01
        print("Turn_rate:",round(turn_rate,5))
    compute_shader["turn_rate"] = turn_rate
    
    if glfw.get_key(window,glfw.KEY_UP):
        fade *= 0.99
        print("Fade:",round(fade,5))
    if glfw.get_key(window,glfw.KEY_DOWN):
        fade *= 1.01
        print("Fade:",round(fade,5))
    program["fade"] = fade
    if glfw.get_key(window,glfw.KEY_LEFT):
        sensor_dist *= 0.99
        print("Sensor_dist:",round(sensor_dist,2))
    if glfw.get_key(window,glfw.KEY_RIGHT):
        sensor_dist *= 1.01
        print("Sensor_dist:",round(sensor_dist,2))
    compute_shader["sensor_dist"] = sensor_dist

    if glfw.get_key(window,glfw.KEY_COMMA):
        sensor_angle *= 0.99
        print("Sensor_angle:",round(sensor_angle,5))
    if glfw.get_key(window,glfw.KEY_PERIOD):
        sensor_angle *= 1.01
        print("Sensor_angle:",round(sensor_angle,5))
    compute_shader["sensor_angle"] = sensor_angle
    

    compute_shader["stepsize"] = stepsize#*(sin(frame/30)+1.5)#*delttime*10
    compute_shader["frame"] = frame
    ctx.viewport = (0,0) + resolution
    #Dispatch compute shader
    compute_shader.run(int(num_agents/64)+1,1,1)
    pixel_texture.write(pixel_buffer)#Write the pixel positions to a texture


    #Renders to framebuffer
    fbo.use()
    program["to_screen"] = False
    vao.render(mode=moderngl.TRIANGLE_STRIP)
    #Sets the pixel buffer to the dissapated and blurred texture
    pixel_buffer.write(last_frame.read())
    #Renders to screen
    ctx.screen.use()
    program["to_screen"] = True
    vao.render(mode=moderngl.TRIANGLE_STRIP)
    glfw.swap_buffers(window)
    frame += 1
glfw.terminate()

Comments