Provate a mettervi tra due specchi, uno davanti e uno dietro. La vostra immagine verrà riflessa, il riflesso dell'immagina verrà riflesso, il riflesso del riflesso dell'immagina verrà riflesso, e così via all'infinito. Ora provate ad accendere una candela. Le micro variazioni della fiamma creeranno, attraverso l'infinita riflessione, un effetto bizzarro. Questa è la ricorsione (spiegata senza usare il fattoriale!).
La ricorsione e i sistemi complessi hanno molte cose in comune. Anche la ricorsione è un fenomeno che si ritrova spesso in natura: sotto forma di frattali, la ritroviamo negli alberi, nel profilo delle coste, nel profilo delle montagne, nelle nubi, nei cristalli di ghiaccio, in alcune foglie e fiori.
La ricorsione, inoltre, sfruttando la potenza riflessiva, produce risultati complessi a partire da semplice regole codificate. E' infatti facile perdere il controllo della ricorsione e creare codice che satura le risorse o che esegue all'infinito. Anche in questo caso l'artista generativo deve essere un buon programmatore: la ricorsione deve essere ben fondata, vale a dire deve terminare dopo un numero finito di passaggi riflessivi. Infine, micro variazioni nelle regole della ricorsione possono produrre effetti di vasta portata.
Vediamo alcuni esempi. Cominciamo con il disegnare un albero: Tree1
Node tree; int max_fanout = 3; // maximum node fanout int max_level = 3; // maximum tree depth void setup() { size(500, 500); tree = new Node(width/2, height/2, 1, max_fanout, 0, 0); } void draw() { background(255); tree.picture(); } void mouseReleased() { tree = new Node(width/2, height/2, 1, max_fanout, 0, 0); } class Node { float x, y; // node screen coordinates float angle, radius; // node polar coordinates wrt parent node int level; // node level int fanout; // node fanout Node[] children; // list of children Node(float _x, float _y, int _level, int _fanout, float _angle, float _radius) { x = _x; y = _y; level = _level; fanout = _fanout; angle = _angle; radius = _radius; children = new Node[fanout]; for (int i = 0; i < fanout; i++) { int clevel = level + 1; int cfanout; if (level < (max_level - 1)) { // node with internal children cfanout = max_fanout; } else { // node with leaf children cfanout = 0; } float cradius = random(50, width/3); float cangle = random(0, TWO_PI); float cx = x + cos(cangle) * cradius; float cy = y + sin(cangle) * cradius; children[i] = new Node(cx, cy, clevel, cfanout, cangle, cradius); } } void picture() { for (int i = 0; i < fanout; i++) { if (level > 0) { fill(0); ellipse(x, y, 20 / level, 20 / level); // draw node if (level == (max_level - 1)) { ellipse(children[i].x, children[i].y, 20 / (level+1), 20 / (level+1)); // draw node } stroke(0); strokeWeight(5 / level); line(x, y, children[i].x, children[i].y); // draw edge } children[i].picture(); } } }
Facciamo ruotare i nodi attorno ai loro padri con verso e velocità casuali: Tree2
Node tree; int max_fanout = 3; // maximum node fanout int max_level = 3; // maximum tree depth void setup() { size(500, 500); tree = new Node(width/2, height/2, 1, max_fanout, 0, 0); } void draw() { background(255); tree.picture(); tree.update(); } void mouseReleased() { tree = new Node(width/2, height/2, 1, max_fanout, 0, 0); } class Node { float x, y; // node positions int level; // node level int fanout; // node fanout float angle, radius; // node polar coordinates wrt parent node float rotation; // node rotation speed and direction Node[] children; // list of children Node(float _x, float _y, int _level, int _fanout, float _angle, float _radius) { x = _x; y = _y; level = _level; fanout = _fanout; angle = _angle; radius = _radius; rotation = radians(random(-1, 1)); children = new Node[fanout]; for (int i = 0; i < fanout; i++) { int clevel = level + 1; int cfanout; if (level < (max_level - 1)) { // node with internal children cfanout = max_fanout; } else { // node with leaf children cfanout = 0; } float cradius = random(50, width/3); float cangle = random(0, TWO_PI); float cx = x + cos(cangle) * cradius; float cy = y + sin(cangle) * cradius; children[i] = new Node(cx, cy, clevel, cfanout, cangle, cradius); } } void picture() { for (int i = 0; i < fanout; i++) { if (level > 0) { fill(0); stroke(0); strokeWeight(5 / level); ellipse(x, y, 20 / level, 20 / level); // draw node if (level == (max_level - 1)) { ellipse(children[i].x, children[i].y, 20 / (level+1), 20 / (level+1)); // draw node } line(x, y, children[i].x, children[i].y); // draw edge } children[i].picture(); } } void update() { for (int i = 0; i < fanout; i++) { children[i].angle += children[i].rotation; children[i].x = x + cos(children[i].angle) * children[i].radius; children[i].y = y + sin(children[i].angle) * children[i].radius; children[i].update(); } } }
Modificando progressivamente le lunghezze degli archi creiamo un effetto big bang / big crunch: Tree3
Node tree; int max_fanout = 7; // maximum node fanout int max_level = 5; // maximum tree depth int span = 800; int jump = -8; void setup() { size(500, 500); tree = new Node(width/2, height/2, 1, 1, 0, 0); } void draw() { background(255); tree.picture(); tree.update(); } class Node { float x, y; // node positions int level; // node level int fanout; // node fanout float angle, radius; // node polar coordinates wrt parent node float rotation; // node rotation speed Node[] children; // list of children Node(float _x, float _y, int _level, int _fanout, float _angle, float _radius) { x = _x; y = _y; level = _level; fanout = _fanout; angle = _angle; radius = _radius; rotation = radians(random(-1, 1)); children = new Node[fanout]; for (int i = 0; i < fanout; i++) { int clevel = level + 1; int cfanout; if (level < (max_level - 1)) { // node with internal children cfanout = max_fanout; } else { // node with leaf children cfanout = 0; } float cradius = span; float cangle = random(0, TWO_PI); float cx = x + cos(cangle) * cradius; float cy = y + sin(cangle) * cradius; children[i] = new Node(cx, cy, clevel, cfanout, cangle, cradius); } } void picture() { for (int i = 0; i < fanout; i++) { if (level > 1) { fill(0); stroke(0); strokeWeight(5 / level); ellipse(x, y, 20 / level, 20 / level); // draw node if (level == (max_level - 1)) { ellipse(children[i].x, children[i].y, 20 / (level+1), 20 / (level+1)); // draw node } line(x, y, children[i].x, children[i].y); // draw edge } children[i].picture(); } } void update() { for (int i = 0; i < fanout; i++) { children[i].angle += children[i].rotation; children[i].radius += jump; if (children[i].radius < 0 || children[i].radius > 3 * span) jump *= -1; children[i].x = x + cos(children[i].angle) * children[i].radius; children[i].y = y + sin(children[i].angle) * children[i].radius; children[i].update(); } } }
Una colorata reinterpretazione: coeur.
Un buon esempio di ricorsione è il frattale: un oggetto autosomigliante, cioè che si ripete nella sua forma allo stesso modo su scale diverse, ovvero non cambia aspetto anche se visto con una lente d'ingrandimento. E' un oggetto a prima vista complesso, ma generato da una sorgente ordinata molto semplice. I frattali appartengono al mondo della natura ma vengono spesso studiati dai matematici e riprodotti dagli artisti (non solo generativi).
"Si ritiene che in qualche modo i frattali abbiano delle corrispondenze con la struttura della mente umana, è per questo che la gente li trova così familiari. Questa familiarità è ancora un mistero e più si approfondisce l'argomento più il mistero aumenta." (Benoit Mandelbrot)
Frattale di Sutcliffe
Vediamo come costruire il frattale di Sutcliffe mediante la ricorsione e come animarlo oltre i propri limiti naturali introducendo semplici modifiche al codice originale: Fractus (codice scritto da Matt Pearson).