Matteo Basei

Una collezione di piccoli programmi realizzati a scopo didattico.

Moddot

Interessante effetto originariamente realizzato con un pixel shader scritto in GLSL utilizzando le funzioni mod e dot. Non essendo riuscito a trovare informazioni a riguardo ho pensato di battezzarlo moddot. L'effetto è dovuto all'interferenza che si crea tra la griglia dei pixel dell'immagine e una funzione dipendente dalla distanza dei pixel dal centro dell'immagine. La cosa più simile che mi viene in mente sono i vari tipi di effetti moiré (forse queste figure di interferenza possono effettivamente essere incluse in questa categoria).

Moddot 1.698518.
Moddot $1.698518$.

In termini matematici data una costante $k \in \mathbb{R}$ si considera una funzione $f_k$ da $\mathbb{Z}^2$ in $\left[ 0 , 1 \right)$ che associa ad ogni coppia $\left( i , j \right) \in \mathbb{Z}^2$ il valore $\left( k \, v \right)^2 - \left\lfloor \left( k \, v \right)^2 \right\rfloor$ dove $\left( k \, v \right)^2$ indica il prodotto scalare $\left( k \, v \right) \cdot \left( k \, v \right) = k^2 \left( {v_x}^2 + {v_y}^2 \right)$ dove il vettore $v \in \mathbb{R}^2$ tale che $v_x = i , v_y = j$.

Un altro approccio (del tutto equivalente a livello numerico) è considerare, invece di una famiglia di funzioni $f_k$, un'unica funzione $f: \mathbb{R}^2 \to \left[ 0 , 1 \right)$ data da $v \mapsto v^2 - \left\lfloor v^2 \right\rfloor$ e considerare l'effetto come un artefatto dovuto al sottocampionamento di tale funzione. Moltiplicare ogni coppia di $\mathbb{Z}^2$ per un fattore $k \in \mathbb{R}$ equivale infatti a considerare la funzione $f$ con dominio $\mathbb{R}^2$ campionandola con frequenza $1 / k$.

Forse questo approccio è migliore in quanto permette di utilizzare tecniche proprie della teoria dei segnali (quali?). Potrebbe essere interessante iniziare a valutare il sottocampionamento anche solo per la funzione $x \mapsto x^2 - \left\lfloor x^2 \right\rfloor$ con $x \in \mathbb{R}$ (vedi grafico nella sezione Origine dell'effetto).

Implementazione GPU

La funzione

float mod(float a, float n)

restituisce a modulo n (nient'altro che una versione a virgola mobile del modulo dell'aritmetica modulare), mentre la funzione

float dot(vec2 v, vec2 w)

restituisce il prodotto scalare tra v e w, cioè

dot(v, w) = v.x * w.x + v.y * w.y

dot si riferisce al termine inglese "dot product" con cui si indica il prodotto scalare nei paesi anglosassoni, che a sua volta fa riferimento alla notazione standard $v \cdot w$ per indicare il prodotto scalare tra $v$ e $w$.

La luminosità di ogni pixel, da nero a bianco, è dato da mod(dot(v, v), 1.0), che restituisce un numero tra 0.0 e 1.0. Il modulo $1$ di un numero reale è equivalente al prenderne la parte frazionaria (in GLSL esiste anche la funzione fract, ma fractdot non suona bene tanto quanto moddot). dot(v, v) restituisce il quadrato della lunghezza di v. L'origine dello spazio è al centro dell'immagine e le coordinate del vettore vengono riscalate di un fattore k per ottenere le differenti figure.

void main()
{
    const float k = 1.698518;
    vec2 v = gl_FragCoord.xy - resolution.xy / 2.0;
    v *= k;
    float moddot = mod(dot(v, v), 1.0);
    vec3 color = vec3(moddot);
    gl_FragColor = vec4(color, 1.0);
}
Moddot 8.964009.
Moddot $8.964009$.

Implementazione CPU

Anche se originariamente derivato da uno shader, il calcolo per generare i moddot non è molto pesante e si possono facilmente generare immagini in tempo reale (nell'ordine di diverse decine al secondo) anche senza ricorrere a tecnologie particolari come gli shader programmabili delle schede video. Attualmente ad esempio sto usando un programma che ho scritto in C# con cui riesco facilmente a generare e salvare immagini singole e le GIF mostrate in questa pagina.

In un linguaggio C-style (come C, C++, C#, eccetera) il codice sarà:

int i0 = width / 2;
int j0 = height / 2;

for (int i = 0; i < width; ++i)
{
    for (int j = 0; j < height; ++j)
    {
        double x = k * (i - i0);
        double y = k * (j - j0);

        double dot = x * x + y * y;
        double mod = dot - (int)dot;

        // set pixel color
    }
}
Moddot 10.809853.
Moddot $10.809853$.

Periodicità

Cambiando il valore di $k$ c'è uno schema che si ripete in continuazione. Un pattern emerge e inizia a riempire parti di piano circolari sempre più grandi. Quando sembra ricoprire l'intero piano, cambia, e progressivamente si riduce in cerchi sempre più piccoli. Per fare un paio di esempi tra i tanti: intorno a $3.9250796$ e a $10.80985385$. Cos'hanno di particolare questi valori critici e tutti gli altri in cui avviene questo fenomeno?

Una sequenza di 41 moddot compresi tra 3.925078 e 3.925082.
Una sequenza di 41 moddot compresi tra $3.925078$ e $3.925082$.

La mia ipotesi è che in realtà i moddot siano sempre periodici e i valori critici corrispondano semplicemente a valori di $k$ per i quali la dimensione del pattern ripetuto è piccola rispetto alla dimensione dell'immagine generata. Se è vero sarebbe interessante trovare la dimensione del pattern ripetuto in funzione di $k$.

Per rendere davvero periodici i moddot è importante creare immagini con un numero pari di pixel (o piastrelle) e calcolare la distanza dal centro in modo che non sia mai zero, a differenza da come ho fatto inizialmente. Questo si ottiene semplicemente prendendo come centro per il calcolo del prodotto scalare size / 2 + 0.5. In caso contrario molti pattern sarebbero periodici a meno del pixel centrale nero (vedi ad esempio $k = 10.809853892$).

Una sequenza di 40 moddot compresi tra 10.809853 e 10.80985495.
Una sequenza di 40 moddot compresi tra $10.809853$ e $10.80985495$.

Origine dell'effetto

Le immagini in questa pagina sono $800 \times 450$ e sono state ingrandite di un fattore $2$, quindi sono in realtà griglie di $400 \times 225$ punti (o, se preferite, griglie di "piastrelle" $2 \times 2$). L'ascissa varia quindi da $-112$ a $112$. Prendendo $k = 1 / 56$ si ottiene al centro un'area circolare che occupa metà dell'immagine in senso verticale.

Moddot 1 / 56, prima che inizino le figure di interferenza.
Moddot $1 / 56$, prima che inizino le figure di interferenza.

All'interno di quest'area c'è un gradiente dal nero al bianco con dipendenza quadratica rispetto alla distanza dal centro. Questo semplicemente perché prima che il modulo del vettore $k \, v$ raggiunga il valore $1$ la funzione $f_k$ si riduce a $\left( k \, v \right)^2$. Al di fuori di questo cerchio si avranno delle onde concentriche sempre più piccole, visto che il quadrato della distanza cresce sempre più rapidamente mano a mano che aumenta la distanza.

Il grafico...
Il grafico di $x^2$ in blu e di $x^2 - \left\lfloor x^2 \right\rfloor$ in rosso.

Aumentare il valore di $k$ ha l'effetto di rimpicciolire sempre di più questa semplice immagine. Ad un certo punto la successione di onde concentriche e la griglia di pixel inizieranno a generare figure di interferenza. Inizialmente (da $k = 0.02$ a $k = 0.2$ circa) non si ottengono figure particolarmente complesse, in sostanza cerchi ripetuti in modo periodico in tutte le direzioni.

Moddot 0.05 con le prime figure di interferenza banali.
Moddot $0.05$ con le prime figure di interferenza banali.

Aumentando ancora il valore di $k$ si passa da questi primi pattern banali ad altri più interessanti. Superato $k = 0.5$ la dimensione dell'area circolare iniziale diventa uguale alla dimensione di un pixel. A questo punto le figure che si ottengono si fanno davvero interessanti e diventa più difficile ricondurle all'immagine di partenza.