Tercera y última continuación espiritual de shaders en corto. Con este post me voy de vacaciones de los shaders. Probablemente los siga usando, pero no escribiré más posts enfocados en GLSL.
Un adelanto del resultado final:
Viendo la complejidad que tiene Lenia para ser implementado, en este post se experimentará específicamente haciendo una versión del juego de la vida más "suave" (ya existen btw).
Las reglas del juego de la vida se pueden encontrar en Wikipedia 🗿:
El juego de la vida es algo determinante y el primer experimento es cambiar las reglas a algo más flexible. Las reglas quedarían así:
Siendo n
la suma de la vida de los vecinos, la celda cambia respecto a n
en los siguientes rangos:
[0, 1]
: Vida disminuye en n
.[1, 3]
: Vida aumenta en n/3
.3 a más
: Vida disminuye en n/8
(el máximo de n
es 8).Para ver la diferencia, se usará el siguiente estado inicial:
Y con las nuevas reglas:
Viene con trucos:
[1, 3]
a [1, 1.5]
. Simplemente porque se ve mejor 👍. Se puede probar otros valores moviendo el slider, representa el gap
para aumentar la vida. El máximo es 8, lo cual hace que crezca sin parar.1 / sqrt(2)
(solo se considera la vida proporcionalmente a la distancia horizontal y vertical).Valores cercanos a 4 (como el del ejemplo) forman patrones rectos.
En este experimento se aumentará el área considerada para los vecinos. Con ello se tendrían 2 variables gap
(que representa la formación de vida) y r
(el radio considerado para los vecinos).
Las reglas ahora son, siendo n
la suma de la vida de los vecinos, la celda cambia respecto a n
en los siguientes rangos:
[0, 1]
: Vida disminuye en n
.[1, gap]
: Vida aumenta en n/gap
.gap a más
: Vida disminuye en n/totalN
.No hay una explicación "lógica" para totalN
, solo se sigue la corriente de lo que se propuso antes con 1 / sqrt(2)
para las esquinas.
totalN
: Representa la suma de las distancias proporcionales de los vecinos.
Por ejemplo:
float dist = length(vec2(i, j) - vec2(k, l)); // Distance de la celda evaluada (i, j) al vecino (k, l)
float factor = 1.0 / dist; // Variable propuesta
float curLive = getState(k, l) * factor;
n += curLive;
totalN += factor;
Dos puntos a tener en cuenta:
gap
de vida y el segundo el radio r
para considerar vecinos.En el experimente anterior se percibe un cambio drástico entre estados (por eso se redujo a 4 frames por segundo), a este punto solo se busca que se formen patrones más consistentes entre frames (ya ni se aplican las reglas propuestas previamente).
live += (uGap - n) * n
gap
r
).Hay muchos sacrilegios tomados para hacer que luzca genial 🙂. No deberían ser problema, el código principal es el siguiente:
void main() {
float i = vXY.x * uWidth - 0.5;
float j = uHeight - vXY.y * uHeight - 0.5;
float live = getState(i, j);
float n = 0.0;
for(float r = 1.0; r <= uR; r++) {
float points = 8.0 * r;
for(float p = 0.0; p < points; p++) {
float k = i + r * cos(2.0 * PI * p / points);
float l = j + r * sin(2.0 * PI * p / points);
float dist = length(vec2(i, j) - vec2(k, l));
float factor = 100.0 / dist; // 😉
float klLive = getState(k, l) * factor;
n += klLive;
}
}
live += (uGap - n) * n;
fragColor = vec4(vec3(live), 1.0);
}
El factor tomó un papel importante, reduce el impacto de los vecinos más lejanos. El cálculo del nuevo estado se redujo a live += (uGap - n) * n;
, se omitió el caso de aislamientos.
Pero parece que no son suficientes cambios para ver una interacción más suave. Tiene el aspecto que hay mucha probabilidad de sobrevivir, pero el aspecto es más orgánico.
Con estos resultados se ven 2 oportunidades:
Una fórmula que me interesó mucho es la siguiente, da patrones más consistentes.
live += -n * log(n) + n * uGap
Pero los cambios entre estados ahora son más fuerte fuertes (ósea se mueve más rápido). Por lo que hay que dar más chance a la muerte o reducir el impacto de los vecinos.
live += -pow(n - uGap, 2.0) + uGap
Después de rebuscar valores que generen patrones orgánicos, se encontró que con esta regla se pueden conseguir. Los valores son algo rebuscados pero se consiguió una función que representa bien un juego suave:
Se dejó el factor en float factor = 1.0 / dist;
(tiene más sentido) y los parámetros son los siguientes:
gap
: 12.9r
: 5Sin embargo, después de unas iteraciones el patrón se vuelve caótico. Aunque, ese inicio es un logro.
En este ejemplo se renderizan varios puntos aleatorios como estado inicial. Dale play (tanto como gustes 🙂) para ver distintos patrones.
Último experimento. Darle un poco de vida con colores. Podrían modificarse las reglas para generar distintos estados dependiendo del color y es justo lo que se muestra a continuación:
Ahora los patrones son más estables y quedan varias células autosuficientes. Los parámetros son:
gap
: 13r
: 5Se tienen 3 dimensiones por celda. Se calculan vecinos por cada dimension, por lo que n
ahora es un vec3
. La interacción en este caso es pasar una dimensión distinta para el cálculo de la celda evaluada:
live.r += -pow(n.b - uGap, 2.0) + uGap;
live.g += -pow(n.r - uGap, 2.0) + uGap;
live.b += -pow(n.g - uGap, 2.0) + uGap;
Algo a notar es que hay patrones de expansión rectos, esto es por el cálculo de la vida en la vecindad. Se usan coordenadas polares, se da una vuelta y se va ampliando el radio. El problema está en la cantidad de puntos que se evalúan por radio, como se muestra:
float points = 8.0 * r;
Aumentando la cantidad de puntos por radio requiere buscar otros parámetros, pero esto mejora en la estabilidad de los patrones:
gap
: 17.5r
: 4Para estabilizar los colores se puede hacer más dependiente a cada dimensión de las demás, un buen patron encontrado es el siguiente:
live.r += -pow((n.r) / 2.0 - uGap, 2.0) + uGap;
live.g += -pow((n.r + n.g) / 3.0 - uGap, 2.0) + uGap;
live.b += -pow((n.r + n.g + n.b) / 4.0 - uGap, 2.0) + uGap;
Ejecútalo varias veces, se obtienen patrones bastante suaves pero hay momentos caóticos.
Nota final:
Muchos de los cálculos desde tunear son buscados al ojo. Son experimentos para pasar el rato (uno bueno). Si tienes alguna propuesta, feliz de escucharla o probarla!
No puedo terminar sin antes recrear el primer ejemplo con el nuevo autómata creado!
Por esta ocasión pondré todo el código aquí (siempre está disponible inspeccionando el iframe), puedes pegarlo y editarlo aquí:
p5.RendererGL.prototype._initContext = function () {
try {
this.drawingContext =
this.canvas.getContext("webgl2", this._pInst._glAttributes) ||
this.canvas.getContext("experimental-webgl", this._pInst._glAttributes);
if (this.drawingContext === null) {
throw new Error("Error creating webgl context");
} else {
const gl = this.drawingContext;
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
this._viewport = this.drawingContext.getParameter(
this.drawingContext.VIEWPORT
);
}
} catch (er) {
throw er;
}
};
const vertShader = `#version 300 es
in vec3 aPosition;
out vec2 vXY;
void main() {
vec4 pos = vec4(aPosition, 1.0);
vXY = pos.xy;
pos = pos * 2.0 - 1.0;
gl_Position = pos;
}`;
const fragShader = `#version 300 es
precision mediump float;
in vec2 vXY;
uniform float uWidth;
uniform float uHeight;
uniform float uR;
uniform float uLGap;
uniform float uGap;
uniform sampler2D uState;
uniform vec2 uCircle;
out vec4 fragColor;
const float PI = 3.14159264;
vec3 getState(float i, float j) {
vec4 data = texture(
uState,
vec2(
(i + 0.5) / uWidth,
(j + 0.5) / uHeight
)
);
return data.rgb;
}
void main() {
float i = vXY.x * uWidth - 0.5;
float j = uHeight - vXY.y * uHeight - 0.5;
vec3 live = getState(i, j);
vec3 n = vec3(0.0);
for(float r = 1.0; r <= uR; r++) {
float points = 16.0 * r;
for(float p = 0.0; p < points; p++) {
float k = i + r * cos(2.0 * PI * p / points);
float l = j + r * sin(2.0 * PI * p / points);
float dist = length(vec2(i, j) - vec2(k, l));
float factor = 1.0 / dist;
vec3 klLive = getState(k, l) * factor;
n.r += klLive.r;
n.g += klLive.g;
n.b += klLive.b;
}
}
vec2 pos = (vXY * 2.0 - 1.0);
pos.x *= (uWidth / uHeight);
float circleDist = length(pos - uCircle);
if(circleDist < 0.1) {
live = vec3(1.0);
} else {
live.r += -pow((n.r) / 2.0 - uGap, 2.0) + uGap;
live.g += -pow((n.r + n.g) / 3.0 - uGap, 2.0) + uGap;
live.b += -pow((n.r + n.g + n.b) / 4.0 - uGap, 2.0) + uGap;
}
fragColor = vec4(live, 1.0);
}
`;
let myShader;
let g;
function setup() {
createCanvas(350, 320, WEBGL);
g = createGraphics(320, 320, WEBGL);
myShader = createShader(vertShader, fragShader);
g.shader(myShader);
g.stroke(255);
g.fill(255);
function restart() {
g.background(0);
const points = 3000;
for (let i = 0; i <= points; i++) {
const x = width * (random(2) - 1);
const y = height * (random(2) - 1);
g.stroke(color(random(255), random(255), random(255)));
g.point(x, y);
}
imageMode(CENTER);
image(g, 0, 0, width, height);
}
restart();
}
function draw() {
const angle = frameCount / 80;
const uCircle = [cos(angle) * 0.6, sin(angle) * 0.6];
myShader.setUniform("uState", g);
myShader.setUniform("uCircle", uCircle);
myShader.setUniform("uR", 5);
myShader.setUniform("uGap", 13.7);
myShader.setUniform("uWidth", width);
myShader.setUniform("uHeight", height);
g.rect(0, 0, 0, 0);
imageMode(CENTER);
image(g, 0, 0, width, height);
}