Archivi tag: updatePixels()

Array di pixel: loadPixels() e updatePixels()

Avevo parlato di pixel in uno dei primissimi post su Processing in questo blog. Grazie alle competenze che abbiamo acquisito nelle ultime settimane, possiamo fare un ulteriore passo in avanti.

Le funzioni che abbiamo usato fino a oggi ci hanno permesso di disegnare linee e forme sullo schermo o, come visto di recente, di mostrare un’immagine. Queste funzioni che, all’apparenza, sembrano eseguire operazioni molto semplici, in realtà nascondono un principio molto complesso: stabilire se ciascun pixel sullo schermo deve essere accesso o spento e, se acceso, il colore che deve rappresentare.

Array di pixel

Siamo abituati a pensare ai pixel come ad una griglia sullo schermo; quindi ad un array bidimensionale.

In Processing, però, il valore di ciascuno di essi viene salvato in un classico array monodimensionale.

È possibile accedere a queste informazioni e modificarle a nostro piacimento? Ovviamente si, utilizzando le funzioni loadPixels()updatePixels().

loadPixels() e updatePixels()

Con la prima funzione stiamo dicendo a Processing che intendiamo lavorare sull’array di pixel e che, quindi, deve caricarlo in una variabile. updatePixels(), invece, la usiamo quando abbiamo apportato tutte le modifiche e vogliamo che il programma aggiorni le informazioni sullo schermo.

Facciamo un esempio pratico:

/*
 * Array di pixel: loadPixels() e updatePixels()
 * Federico Pepe, 26.03.2017
 * http://blog.federicopepe.com/processing
 */

void setup() {
  size(500, 500);
  loadPixels();  
 
  for (int i = 0; i < pixels.length; i++) {
    float rand = random(255);
    color c = color(0, 0, rand);
    pixels[i] = c;
  }
  
  updatePixels();
}

In questo programma carichiamo tutti i valori di una finestra di 500x500 pixel. Con il ciclo for partiamo dal primo valore presente nel nostro array e arriviamo fino all'ultimo, identificato, per comodità, dalla funzione pixel.length e assegniamo a ciascuno un colore casuale float rand = random(255); nella scala dei blu color c = color(0, 0, rand);.

Assegniamo il nuovo colore al pixel pixels[i] = c; e, una volta usciti dal loop, chiediamo a Processing di aggiornare e mostrare tutte le modifiche che abbiamo effettuato updatePixels();.

Il risultato sarà il seguente:

Pixel Array Blu

Modificare un pixel conoscendone la posizione x e y

A questo punto, immagino, potrà esservi sorta spontaneamente una domanda: è possibile accedere a un determinato pixel conoscendone la posizione x e y all'interno della finestra come siamo stati abituati a fare fino a ora?

Riprendendo l'immagine della griglia più sopra, proviamo a capire come accedere al pixel 19: la sua posizione è x = 5, y = 2. A questo punto è importante ricordare che, per questi valori, partiamo a contare da 0 mentre, per la larghezza della finestra, da 1.

Ora dobbiamo sommare il valore di x al valore di y moltiplicato alla larghezza.

La formula è, dunque: x + (y * width)

5 + (2 * 7) = 19

I conti tornano!

Proviamo a modificare il codice di prima come segue:

/*
 * Array di pixel: loadPixels() e updatePixels()
 * Federico Pepe, 26.03.2017
 * http://blog.federicopepe.com/processing
 */

void setup() {
  size(500, 500);
  loadPixels();  
  color c;
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      float rand = random(255);
      int pos = x + y * width;
      if(x % 2 == 0) {
        c = color(0, 0, rand);
      } else {
        c = color(rand, 0, 0);
      }
      pixels[pos] = c;
    }
  }
  
  updatePixels();
}

Zoomando l'immagine noterete che una riga è rossa e una riga è, invece, blu. Il prossimo passo sarà applicare quello che abbiamo appena imparato su un'immagine. Provate a pensare a tutte le operazioni che potremmo compiere andando a modificare individualmente ogni singolo pixel.

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.