ask:

to make a sketch with cellular automata / fractals.


thought:

a c.a. has the following:

A cellular automaton (cellular automata plural, or CA for short) is a model of a system of cell objects with the following characteristics:

  • The cells live on a grid. (I’ll include examples in both one and two dimensions in this chapter, though a CA can exist in any finite number of dimensions.)
  • Each cell has a state, though a cell’s state can vary over time. The number of possible states is typically finite. The simplest example has the two possibilities of 1 and 0 (otherwise referred to as on and off, or alive and dead).
  • Each cell has a neighborhood. This can be defined in any number of ways, but it’s typically all the cells adjacent to that cell.

from nature of code.

so, i’ll make a simulation-system for cells sharing something on a grid, but do so with ping-ponging shader buffers.


outputs:

sketch


spent time revising ping-pong buffering. made a p5 template for it: https://github.com/arjunmakesthings/p5-webgl_ping-pong-template

spent a long time on this algorithm, and realized that shaders can’t read from other pixels in a pass.

#ifdef GL_ES
precision mediump float;
#endif
 
//receive from vertex shader:
varying vec2 vTexCoord;
 
//custom uniforms:
uniform vec2 u_res;
uniform sampler2D u_prev;
uniform vec2 u_seed_coords; //passed in pixel-space. 
uniform float u_inject_toggle; 
 
//parameters:
float seed = 1.0;
 
float capacity = 1.0;
float rate = 0.01;
 
void main() {
    //globals:
    vec2 px_coord = vTexCoord * u_res;
 
    //local for calculations:
    float curr_self = texture2D(u_prev, vTexCoord).r;
 
    // 1) check for seed distribution. 
    float d = distance(px_coord, u_seed_coords);
 
    if(d < 10.0 && u_inject_toggle == 1.0) {
        //this is the seed one.
        curr_self = seed;
    }
 
    // 2) take half of what you have, and give it to your neighbours.
    float keep = curr_self;
    float received = 0.0;
    float offload = 0.0; 
 
    // if you have more than you can take, offload.
    if(curr_self > capacity) {
        offload = curr_self * rate;
        keep -= offload; //remove from what you are going to keep.
    } else if(curr_self < capacity) {
        //current self is less than what it can take. so we receive from neighbours.
        vec2 px = 1.0 / u_res; // 1px in uv-space. 
 
        //the g value contains offloads for each pixel.
        float neighbors[8];
        neighbors[0] = texture2D(u_prev, vTexCoord + vec2(0.0, px.y)).g;   // up
        neighbors[1] = texture2D(u_prev, vTexCoord + vec2(0.0, -px.y)).g;  // down
        neighbors[2] = texture2D(u_prev, vTexCoord + vec2(-px.x, 0.0)).g;  // left
        neighbors[3] = texture2D(u_prev, vTexCoord + vec2(px.x, 0.0)).g;   // right
        neighbors[4] = texture2D(u_prev, vTexCoord + vec2(-px.x, px.y)).g; // up-left
        neighbors[5] = texture2D(u_prev, vTexCoord + vec2(px.x, px.y)).g;  // up-right
        neighbors[6] = texture2D(u_prev, vTexCoord + vec2(-px.x, -px.y)).g; // down-left
        neighbors[7] = texture2D(u_prev, vTexCoord + vec2(px.x, -px.y)).g; // down-right
 
        for(int i = 0; i < 8; i++) {
            float n = neighbors[i];
            if(n > 0.0) {
                received += n/8.0; //add to my receivals. 
            }
        }
        received = min(received, rate); 
    }
    curr_self = keep + received; 
    //pass the thing:
    gl_FragColor = vec4(curr_self, offload, 0.0, 0.0);
}
 
 

reverted to simpler rules.

something interesting happened here:

#ifdef GL_ES
precision mediump float;
#endif
 
//receive from vertex shader:
varying vec2 vTexCoord;
 
//custom uniforms:
uniform vec2 u_res;
uniform sampler2D u_prev;
uniform vec2 u_seed_coords; //passed in pixel-space. 
uniform float u_inject_toggle;
 
//local: 
float seed_amt = 1.0;
float capacity = 0.5;
 
float curr = 0.0;
 
float give = 0.0; 
 
//helpers: 
 
//to get neighbours:
/* usage: 
 
vec4 neighbours[8];
get_neighbours(u_prev, vTexCoord, u_res, neighbours);
 
*/
void get_neighbours(sampler2D tex, vec2 uv, vec2 res, out vec4 neighbours[8]) {
    vec2 px = 1.0 / res;
 
    neighbours[0] = texture2D(tex, uv + vec2(0.0, px.y));
    neighbours[1] = texture2D(tex, uv + vec2(0.0, -px.y));
    neighbours[2] = texture2D(tex, uv + vec2(-px.x, 0.0));
    neighbours[3] = texture2D(tex, uv + vec2(px.x, 0.0));
    neighbours[4] = texture2D(tex, uv + vec2(-px.x, px.y));
    neighbours[5] = texture2D(tex, uv + vec2(px.x, px.y));
    neighbours[6] = texture2D(tex, uv + vec2(-px.x, -px.y));
    neighbours[7] = texture2D(tex, uv + vec2(px.x, -px.y));
}
 
void inject() {
    curr = seed_amt;
}
 
void main() {
    //globals: 
    vec2 px_coord = vTexCoord * u_res; // convert to px space.
    //previous state:
    vec4 prev = texture2D(u_prev, vTexCoord);
 
    //inject seed: 
    if(u_inject_toggle == 1.0) {
        float d = distance(px_coord, u_seed_coords);
        if(d < 1.0) {
            inject();
        }
    } else {
        curr = prev.r;
    }
 
    //two simple rules: if you have more than you can take, share with neighbours. if you have less, take from neighbours.
 
    vec4 neighbours[8];
    get_neighbours(u_prev, vTexCoord, u_res, neighbours);
 
    if(curr > capacity) {
        float excess = curr - capacity;
 
        for(int i = 0; i < 8; i++) {
            float other = neighbours[i].r;
            give += excess / 8.0; //excess divided across all neighbours.
        }
        //we assume that all were given across all neighbours. so:
        curr -= give;
    } else if(curr < capacity) {
        //on the other hand, if your current value is less than your capacity, you seek:
 
        for(int i = 0; i < 8; i++) {
            float other_r = neighbours[i].r;
            float other_g = neighbours[i].g;
 
            if(other_g > 0.0) {
                //if they had something to give, take.
                curr += other_g;
            }
        }
    }
        //send out as rgba: 
    gl_FragColor = vec4(curr, give, 0.0, 1.0);
}
 
//     //send out as rgba: 
// gl_FragColor = vec4(curr, prev.r, 0.0, 1.0);
 
// else {
// vec4 neighbours[8];
// get_neighbours(u_prev, vTexCoord, u_res, neighbours);
// if(curr >= capacity) {
//             //you have more than you can take. share with neighbours.
// float amt_to_offload = curr - capacity;
// for(int i = 0;
// i < 8;
// i ++) {
// float n = neighbours[i].r;
// if(n < capacity) {
// give += amt_to_offload;
// }
// }
// } else if(curr < capacity) {
//             //you are needy. take from neighbours. 
 
// for(int i = 0;
// i < 8;
// i ++) {
// float n = neighbours[i].r;
// if(n > capacity) {
//                     //this means it has something to give.
// curr += n / 7.0;
// }
// }
//             // curr -= prev.g;
// }
 
// }
 

#ifdef GL_ES
precision mediump float;
#endif
 
//receive from vertex shader:
varying vec2 vTexCoord;
 
//custom uniforms:
uniform vec2 u_res;
uniform sampler2D u_prev;
uniform vec2 u_seed_coords; //passed in pixel-space. 
uniform float u_inject_toggle;
 
//local: 
float seed_amt = 1.0;
float capacity = 0.5;
 
float curr = 0.0;
 
float give = 0.0; 
 
//helpers: 
 
//to get neighbours:
/* usage: 
 
vec4 neighbours[8];
get_neighbours(u_prev, vTexCoord, u_res, neighbours);
 
*/
void get_neighbours(sampler2D tex, vec2 uv, vec2 res, out vec4 neighbours[8]) {
    // size of one pixel in UV space
    vec2 px = 1.0 / res;
 
    // axis-aligned neighbors
    neighbours[0] = texture2D(tex, uv + vec2(0.0, px.y));   // up
    neighbours[1] = texture2D(tex, uv + vec2(0.0, -px.y));  // down
    neighbours[2] = texture2D(tex, uv + vec2(-px.x, 0.0));  // left
    neighbours[3] = texture2D(tex, uv + vec2(px.x, 0.0));   // right
 
    // diagonal neighbors
    neighbours[4] = texture2D(tex, uv + vec2(-px.x, px.y));  // up-left
    neighbours[5] = texture2D(tex, uv + vec2(px.x, px.y));   // up-right
    neighbours[6] = texture2D(tex, uv + vec2(-px.x, -px.y)); // down-left
    neighbours[7] = texture2D(tex, uv + vec2(px.x, -px.y));  // down-right
}
 
void inject() {
    curr = seed_amt;
}
 
void main() {
    //globals: 
    vec2 px_coord = vTexCoord * u_res; // convert to px space.
    //previous state:
    vec4 prev = texture2D(u_prev, vTexCoord);
 
    //inject seed: 
    if(u_inject_toggle == 1.0) {
        float d = distance(px_coord, u_seed_coords);
        if(d < 1.0) {
            inject();
        } else {
            // curr = prev.r;
        }
    } else {
        curr = prev.r;
    }
 
    //two simple rules: if you have more than you can take, share with neighbours. if you have less, take from neighbours.
 
    vec4 neighbours[8];
    get_neighbours(u_prev, vTexCoord, u_res, neighbours);
 
    if(curr > capacity) {
        float excess = curr - capacity;
 
        for(int i = 0; i < 8; i++) {
            float other = neighbours[i].r;
            give += excess / 8.0; //excess divided across all neighbours.
        }
        //we assume that all were given across all neighbours. so:
        curr -= give;
    } else if(curr < capacity) {
        //on the other hand, if your current value is less than your capacity, you seek:
        for(int i = 0; i < 8; i++) {
            float other_r = neighbours[i].r;
            float other_g = neighbours[i].g;
                curr += other_g; 
            
        }
    }
        //send out as rgba: 
    gl_FragColor = vec4(curr, give, u_inject_toggle, 1.0);
}
 
// for(int i = 0;
// i < 8;
// i ++) {
// curr += neighbours[i].g * weights[i];
// }