Archivi categoria: Matematica

Funzioni matematiche in Processing.

Funzioni ricorsive

In programmazione definiamo ricorsive quelle funzioni che richiamo se stesse all’interno di un programma. A parole si tratta di un concetto molto semplice ma è bene fare qualche esempio per fugare ogni possibile dubbio.

Prima di procedere è bene puntualizzare che quando creiamo e usiamo queste funzioni dobbiamo fare attenzione a inserire sempre una condizione di uscita altrimenti lo sketch andrà in StackOverflowError.

Funzioni ricorsive: removeOne();

Ecco il primo esempio:

/*
 * Funzioni Ricorsive
 * by Federico Pepe
 *
*/

void setup() {
  noLoop();
}

void draw() {
  removeOne(5);
}

void removeOne(int n) {
  println(n);
  if(n > 1) {
    n--;
    removeOne(n);
  }
}

Analizziamo il funzionamento: tramite il comando noLoop() in setup() sappiamo che la funzione draw() verrà ripetuta una sola volta. Dopodiché creiamo la nostra funzione removeOne() che, ogni volta che viene chiamata, eseguirà i seguenti comandi:

  • stampa in console il valore di n
  • controlla se il valore di n è maggiore di 1 se true
    • sottrai 1 a n
    • richiama la funzione removeOne passando il nuovo valore di n

Il risultato in console sarà, ovviamente:

5
4
3
2
1

Nota: spostando n–; dopo removeOne(n), il programma andrà in un loop infinito generando un errore: StackOverflowError. Gli sviluppatori di Processing sono stati così furbi da riconoscere questo tipo di errore e da far stoppare automaticamente il programma evitando, così, di far bloccare il computer.

Fino a qui niente di eccezionale, avremmo potuto ottenere lo stesso risultato in mille modi diversi utilizzando, ad esempio, un ciclo for. Le cose possono diventare interessanti quando cominciamo a utilizzare le funzioni ricorsive per disegnare qualcosa sullo schermo come nel secondo esempio:

Funzioni ricorsive: drawCircle();

Funzioni Ricorsive

/*
 * Funzioni Ricorsive: drawCircle()
 * by Federico Pepe
 *
*/

void setup() {
  size(700, 400);
  noLoop();
  noFill();
  background(255);
}

void draw() {
  drawCircle(width/2, height/2, width/2, 7);
}

void drawCircle(int x, int y, int radius, int recursion) {
  ellipse(x, y, radius, radius);
  if (recursion > 1) {
    recursion--;
    drawCircle(x + radius/2, y, radius/2, recursion);
    drawCircle(x - radius/2, y, radius/2, recursion);
  }
}

Il classico esempio che si vede quando si parla di ricorsività: utilizziamo una variabile per definire quante ripetizioni dovranno esserci (in questo caso 7) e poi disegniamo dei cerchi la cui posizione x e il cui raggio vengono dimezzati ogni volta. Per vedere i singoli passaggi potete modificare manualmente l’ultimo parametro in drawCircle() oppure creare una nuova variabile che viene incrementata alla pressione del mouse.

sin() e cos(): onde e funzioni cicliche

Rimaniamo nel campo della trigonometria e analizziamo le funzioni sin() e cos() che, rispettivamente, ci consentono di calcolare il seno e il coseno di un angolo.

Entrambe le funzioni necessitano di un solo parametro: la misura di un angolo espressa in radianti e restituiscono un numero float il cui valore è sempre compreso tra -1.0 e 1.0.

Con questo snippet di codice potete verificare voi stessi nella console di Processing:

for(int angle = 0; angle < 360; angle++) {
 println(sin(radians(angle)));
}

Entrambe queste funzioni sono importanti per due motivi: restituendo valori compresi tra -1.0 e 1.0 è possibile sfruttarle per controllare altri parametri all’interno di uno sketch, il secondo è che sono funzioni cicliche.

Esempi utilizzo funzione sin()

Esempio 1:

Processing, funzione sin()

L’immagine qui sopra è generata utilizzando questo codice:

/*
 * Angoli II: sin() e cos()
 * by Federico Pepe
 *
*/

void setup() {
  size(640, 360);
  background(255);
  fill(0);
  noStroke();
}

void draw() {
  float angle = 0;
  
  for(int x = 0; x <= width; x+=5) {
    float y = height/2 + (sin(radians(angle))*35);
    rect(x, y, 2, 4);
    angle += 10;
  }
 
}

Non credo che il codice abbia bisogno di molte spiegazioni: con un semplice ciclo for, disegniamo dei piccoli rettangoli la cui posizione è determina dal ciclo for stesso, per il valore di x e, per quanto riguarda la y, da un valore calcolato con la funzione sin(). Ovviamente a ogni ciclo for dobbiamo incrementare la variabile angle.

Esempio 2: controllo di altri parametri

Come dicevo prima, le cose si fanno più interessanti quando sfruttiamo il valore ciclico del seno all'interno di draw() per controllare altri parametri:

Animazione utilizzando sin()

/*
 * Sin() with Circle
 * by Federico Pepe
 *
 */
float angle = 0;
float diameter;

void setup() {
  size(500, 500);
}

void draw() {
  background(255);
  diameter = sin(angle)*200;
  ellipse(width/2, height/2, diameter, diameter);
  angle += 0.1;
}

In questo esempio animato, ad esempio, utilizziamo la funzione sin() per controllare la grandezza del cerchio in modo ciclico. Se avessimo incrementato la variabile diameter in modo standard diameter++, avremmo dovuto inserire un controllo if per determinare la grandezza massima e per far cambiare segno al valore... insomma un sacco di righe di codice in più.

Esempio 3

Basta una piccolissima modifica al codice per ottenere un effetto completamente diverso:

Processing sin() e cos()

/*
 * Sin() e Cos() with Circle
 * by Federico Pepe
 *
 */
float angle = 0;
float sin, cos;

void setup() {
  size(500, 500);
}

void draw() {
  background(255);
  sin = sin(angle)*200;
  cos = cos(angle)*200;
  ellipse(width/2, height/2, sin, cos);
  angle += 0.1;
}

Queste funzioni sono così divertenti da usare che potrei fare molti esempi; l'obiettivo di oggi era comunque darvi un'idea delle potenzialità e lasciarvi la possibilità di sperimentare liberamente. Come al solito, se volete potete lasciare un commento con le vostre animazioni!

Angoli: randians() e degrees()

Prima di passare a studiare funzioni matematiche come sin(), cos() e tan() che, come vedremo, potranno essere molto utili per i nostri esperimenti con Processing, è necessario fare una breve premessa sugli angoli.

La maggior parte delle persone è abituata a misurare gli angoli con i gradi (in inglese degrees): un angolo retto è 90°, un angolo piatto è 180° mentre un angolo giro è 360°. Quando si lavora con la trigonometria è più semplice usare i radianti (in inglese radians).

Quando usiamo i radianti dobbiamo tenere presente che i valori degli angoli sono espressi in relazione al valore del π. Ragionando in radianti, dunque, un angolo retto è π/2, un angolo piatto è π mentre un angolo giro è 2π.

Per chi fosse spaventato da tutto questo e non più molto fresco con l’argomento che stiamo trattando, ricordo che il valore del pi greco è:

il rapporto tra la misura della lunghezza della circonferenza e la misura della lunghezza del diametro di un cerchio

Per aiutarci, gli sviluppatori di Processing hanno inserito una serie di variabili che possiamo utilizzare nei nostri sketch: PI, QUARTER_PI, HALF_PI, TWO_PI (nota bene: devono essere scritte in maiuscolo così come le ho riportate) oltre, ovviamente, a due funzioni che ci permettono di passare dai gradi ai radianti e viceversa:

  • La funzione radians() accetta come parametro un valore in gradi e restituisce il valore in radianti.
  • La funzione degrees() accetta come parametro un valore in radianti e restituisce i gradi.

Ecco un semplice programma che vi aiuterà a familiarizzare con quanto appena detto:

/*
 * Angoli: radians() e degrees();
 * by Federico Pepe
 *
 */
// COSTANTI
println("QUARTER_PI = "+ QUARTER_PI);
println("HALF_PI = " + HALF_PI);
println("PI = "+ PI);
println("TWO_PI = " + TWO_PI);

// CONVERSIONE
// Gradi -> Radianti
println();
println("Gradi -> Radianti");
println("90 gradi in radianti: " + radians(90));
println("180 gradi in radianti: " + radians(180));

// Radianti -> Gradi
println();
println("Radianti -> Gradi");
println("PI in gradi: " + degrees(PI));
println("TWO_PI in gradi:" + degrees(TWO_PI));

Spero di non avervi spaventato con questa parentesi matematica. Prima di procedere oltre ci tengo a sottolineare che al liceo ho avuto sempre molte difficoltà in questa materia quindi cercherò sempre di utilizzare spiegazioni semplici che permettano a chiunque di capire il punto a cui voglio arrivare. Ti auguro, come è successo con me, che la programmazione sia il mezzo per riavvicinarsi alla matematica e, perché no, per capirla un po’ di più.

2D Perlin Noise

Nei due post precedenti abbiamo sempre usato la funzione noise() in modo mono dimensionale. È arrivato il momento di fare un passo in avanti e aggiungere la seconda dimensione.

Ripasso: 1D Perlin Noise

Quando parliamo di Perlin Noise mono dimensionale dobbiamo immaginare i valori su una linea temporale orizzontale; quando noi chiediamo alla funzione di restituirci un determinato valore di noise, tale valore sarà correlato a quello precedente e a quello successivo. Nell’immagine viene rappresentato il valore di noise con parametro xOff pari a 3.32. Come nel post precedente, l’incremento che ho usato per generare l’immagine è 0.02.

Perlin Noise Monodimensionale

2D Perlin Noise

Quando aggiungiamo una dimensione, invece, dobbiamo pensare a una griglia i cui valori sono correlati tra loro sia sull’asse delle x che su quello delle y.

2D Perlin Noise

Come si evince dall’immagine, la situazione diventa più complicata perché se analizziamo singolarmente i punti centrali rosso o blu intuiamo facilmente la correlazione con i punti che li circondano. Se li consideriamo entrambi contemporaneamente, invece, notiamo subito come ci siano dei punti in comune che dovranno essere a loro volta correlati tra loro.

Questo è un esempio molto semplificato: provate a immaginare se ciascuno dei punti disegnati fosse un pixel!

Generiamo una texture

È arrivato il momento di sporcarsi le mani e scrivere un po’ di codice: lo scopo del programma di oggi è generare una texture con Processing utilizzando Perlin Noise bidimensionale.

Partiamo da un’analisi del risultato finale per capire tutti i passaggi e il codice che ci serve:

Texture generata con Perlin Noise bidimensionale

Per generare la texture dobbiamo prendere ciascun pixel della nostra finestra facendo in modo che il suo colore sia correlato ai pixel vicini: è facile notare come ci siano delle aree più scure e altre più chiare e che, in generale, la texture generata sia uniforme.

Per farvi capire la differenza, nell’immagine qui sotto ho sostituito nel programma la funzione noise() con random():

Random Texture Processing
Sostituendo la funzione noise() con random() il risultato è molto differente.

Per lavorare con i pixel utilizzeremo le funzioni loadPixels() updatePixels() che avremo modo di analizzare in modo approfondito in futuro. Per il momento vi basti sapere che la prima funzione carica tutti i pixel della finestra in un array chiamato pixels[] e che la seconda ricarica i pixel nella finestra dopo che sono stati modificati.

Un altro punto molto importante è sapere che, benché i pixel presenti in una finestra siano un insieme di righe e colonne, nell’array pixels[] vengono salvati con numero progressivo.

Per modificare il colore di ciascun pixel della finestra abbiamo bisogno di due cicli for annidati: uno per i valori di x, l’altro per quelli di y. La funzione noise() entra in gioco proprio in questa fase: attraverso due variabili – xOff e yOff – imposteremo il colore di ciascun pixel.

Ecco il codice finale:

/*
 * Texture con Perlin Noise 2D
 * by Federico Pepe
 *
 */

float increment = 0.015;

void setup() {
  size(500, 500);
}

void draw() {
  // Carico i pixel della finestra
  loadPixels();
  float xOff = 0;
  for(int x = 0; x < width; x++) {
    xOff += increment;
    float yOff = 0;
    for (int y = 0; y < height; y++) {
      yOff += increment;
      // Ottengo il valore noise passando le variabili xOff e yOff.
      float bright = noise(xOff, yOff) * 255;
      // Riassegno a ogni pixel il nuovo colore
      pixels[x+y*width] = color(bright);
    }
  }
  // Aggiorno tutti i pixel della finestra
  updatePixels();
}

Domanda: perché non ho inizializzato le due variabili xOff e yOff all'inizio del programma? Cosa succede se non reimposto a 0 la variabile yOff a ogni ciclo?

Per aggiungere l'interazione con l'utente, aggiungiamo subito dopo updatePixels() la seguente riga: increment = map(mouseX, 0, width, 0.1, 0.01);

In questo modo rimapperemo la posizione X del mouse che può andare da 0 alla larghezza della finestra in un nuovo range compreso tra 0.1 e 0.01.

Altri esempi con la funzione noise()

Nell’ultimo post abbiamo analizzato la differenza tra la funzione random()noise(). La prima, vi ricordo, restituisce dei valori assolutamente casuali che dipendono dai parametri che passiamo alla funzione; la seconda, invece, ci permette di avere una sorta di casualità controllata.

I valori che vengono restituiti dalla funzione noise() hanno due caratteristiche fondamentali: sono correlati tra loro e hanno sempre un valore compreso tra 0 e 1.

Maggiore sarà la vicinanza tra i parametri che passiamo alla funzione, maggiore sarà, ovviamente, la correlazione tra i valori restituiti in output. È possibile verificare quanto ho appena detto utilizzando un paio di righe di codice:

println(noise(100));
println(noise(100.01));

Questo il risultato visualizzato in console:

0.3187103
0.318551

I risultati sono davvero molto vicini tra loro. Proviamo ad aumentare la distanza tra i due valori:

println(noise(100));
println(noise(101));

Ora la correlazione tra i due output è meno evidente:

0.56860673
0.48344862

Aumentando ulterioremente la distanza tra i parametri di input, avremo risultati in output sempre più simili a quelli ottenuti dalla funzione random():

println("Noise:");
println(noise(100));
println(noise(500));
println("Random:");
println(random(1));
println(random(1));

Può generare risultati simili a questo:

Noise:
0.16914257
0.68422705
Random:
0.48342746
0.89731866

Creare un grafico di valori restituiti da noise()

Il primo esempio che voglio realizzare oggi è un grafico che mi rappresenti, per valori di x crescenti, un valore y restituito da noise().

Ecco il codice:

/*
 * Noise examples 1
 * by Federico Pepe
*/

float xoff = 0;
float increment = 0.02;

void setup() {
  size(500, 500);
  background(255);
  noFill();
  noLoop();
}

void draw() {
  beginShape();
  for(int x = 0; x <= width; x++) {
    stroke(0);
    vertex(x, noise(xoff)*height);
    xoff += increment;
  }
  endShape();
}

Analizziamo velocemente il codice riportato qui sopra: per prima cosa creo le variabile di tipo float xoff e increment, dopodiché all'interno di setup() imposto la grandezza della finestra, il colore di sfondo, imposto che non ci sia nessun colore di riempimento, con noFill(), e che il programma non vada in loo  una volta avviato, con noLoop().

In draw() utilizzo una funzione che non abbiamo ancora visto: beginShape(). Questa funzione dice a Processing che vogliamo disegnare una forma complessa che avrà un'insieme di vertici – aggiunti con la funzione vertex() – che dovranno essere uniti in ordine gli uni agli altri finché non chiuderemo la forma con endShape().

Perché ho deciso di usare questa funzione? Perché così, estraendo per valori di x crescenti dei valori y generati dalla funzione noise(), tutti i punti saranno già collegati tra loro e genereranno un grafico come quello rappresentato qui sotto:

Grafico della funzione noise

Animiamo il grafico

/*
 * Noise example 2
 * by Federico Pepe
*/

float xoff;
float increment = 0.015;
float startPoint;

void setup() {
  size(500, 500);
  background(255);
  noFill();
}

void draw() {
  background(255);
  xoff = startPoint;
  beginShape();
  for(int x = 0; x <= width; x++) {
    stroke(0);
    vertex(x, noise(xoff)*height);
    xoff += increment;
  }
  endShape();
  startPoint += increment;
}

Con una veloce modifica al codice è possibile animare il grafico: per prima cosa eliminiamo il noLoop() e aggiungiamo una variabile per stabilire di volta in volta quale sarà il nostro punto di inizio: startPoint. Ovviamente, al termine del loop, dobbiamo incrementare quest'ultima variabile in modo da avere lo scorrimento orizzontale.

Ecco il risultato (in formato gif):

Grafico noise animato