I frattali possono essere considerate immagini a risoluzione infinita, perchè essi mantengono lo stesso livello di dettaglio quando sono ingrandite. L'unico modo per visualizzare un frattale in tempo finito è dunque quello di calcolare l'immagine per ogni pixel che dovrà essere effettivamente visualizzato.
Ovviamente, un fragment shader è già istanziato una volta per ogni pixel dell'immagine visualizzata, per cui può essere usato facilmente per ottenere un frattale. Basta calcolare il valore del frattale nel pixel e poi convertire secondo qualche regola quel valore in un colore.
Per cominciare, si deve scegliere la formula matematica che descrive il frattale. Ad esempio, una delle formule più semplici è la successione:
I vari e sono numeri complessi, mentre gli indici e sono numeri naturali. In modo simile alle rappresentazioni degli insiemi di Julia, si pone uguale a costante, variabile e si ci domanda quale sia il comportamento di per tendente all'infinito.
L'unico modo per sapere in un tempo finito se la successione converge oppure no è calcolarne un gran numero di valori e decidere che è divergente se il modulo di uno di essi supera un valore molto grande rispetto ai dati iniziali. Questo algoritmo approssimato ha due svantaggi: è necessario decidere arbitrariamente il valore massimo, e soprattutto bisogna definire un numero massimo di iterazioni, altrimenti nei casi convergenti l'algoritmo proseguirebbe all'infinito.
Per rappresentare il frattale si deve inizializzare, per ciascun punto da calcolare, con la posizione bidimensionale del punto, opportunamente convertita in un numero complesso.
Nell'implementazione in GLSL, questa conversione diviene implicita. Siccome il linguaggio non supporta numeri complessi come tipo primitivo, essi devono essere rappresentati come vec2 esattamente come i vettori dello spazio bidimensionale, dove la x contiene la parte reale e la y quella immaginaria.
Nelle implementazioni più comuni, il colore è assegnato al frattale proporzionalmente al numero di iterazioni necessarie per far superare alla successione il valore massimo definito arbitrariamente, cioè il numero di iterazioni necessarie per decidere che la successione diverge. Se la successione non diverge, è assegnato un colore prestabilito.
Per questo esempio, il colore è assegnato al frattale in funzione del valore di quando il frattale è giudicato divergente dall'algoritmo. Il numero di iterazioni è irrilevante.
Per prima cosa, si definisce la funzione che genera il frattale. Essa riceve in ingresso la posizione bidimensionale del pixel come parametri e il valore di c come variabile uniform.
uniform vec2 initC; uniform int numIter; vec2 Fractal(float x,float y) { vec2 c = initC; vec2 z = vec2(x,y);
Ad ogni iterazione, fino al massimo a numIter, si deve elevare al quadrato z e sommare c. La formula per l'elevamento al quadrato di un numero complesso in GLSL è ottenuta sviluppando il quadrato:
Il codice GLSL è:
for (int i = 0; i < numIter; i++) { vec2 qr = vec2(z.x*z.x - z.y*z.y,2.0*z.x*z.y); z = qr + c;
La condizione di uscita prevede che se il modulo di z è maggiore della radice quadrata di 30 (numero arbitrario), allora la funzione restituisce il valore di z, opportunamente scalato in modo che nessuna delle due componenti abbia valore maggiore di 1.0. Se una delle due è negativa, viene portata a 0.0 con la funzione max.
if (abs(dot(z,z)) > 30.0)
return max(z / (abs(z.x) + abs(z.y)),vec2(0.0,0.0));
}
Se la successione converge, la funzione ritorna un valore intermedio:
return vec2(0.5,0.5); }
Lo shader principale passa alla funzione le prime due componenti delle model coordinates provenienti dal dal vertex shader, mischia due colori (verde e blu) secondo il risultato e applica l'illuminazione. Lo shader completo diventa:
#version 120 uniform vec2 initC; uniform int numIter; varying vec3 veyeposition; varying vec3 vnormal; varying vec3 oPosition; vec4 ApplyLight(in vec4 oColor, in vec3 relativepos, in vec3 normal,in vec3 matspecular,in float matshininess); vec2 Fractal(float x,float y) { vec2 c = initC; vec2 z = vec2(x,y); for (int i = 0; i < numIter; i++) { vec2 qr = vec2(z.x*z.x - z.y*z.y,2.0*z.x*z.y); z = qr + c; if (abs(dot(z,z)) > 30.0) return max(z / (abs(z.x) + abs(z.y)),vec2(0.0,0.0)); } return vec2(0.5,0.5); } void main() { vec2 f = Fractal(oPosition.x,oPosition.y); vec3 color = f.x * vec3(0.0,1.0,0.0) + f.y * vec3(0.0,0.0,1.0); color = clamp(color,0.0,1.0); // solo per sicurezza vec4 rs = ApplyLight(vec4(color,1.0),veyeposition,normalize(vnormal),vec3(0.0,0.0,0.0),16.0); // niente componente speculare per questo esempio gl_FragColor = rs; }
#define SHADER_FOLDER "shaders/fractal/" #define USE_VERTEX_SHADER true #define USE_FRAGMENT_SHADER true #define USE_GEOMETRY_SHADER false #define USE_NORMAL_SHADER false #define USE_VERTEX_LIGHT_SHADER false #define USE_FRAGMENT_LIGHT_SHADER true #define USE_TESSEVAL_LIGHT_SHADER false #define USE_TESSELLATION_SHADERS false #define GEOMETRY_INPUT_TYPE GL_TRIANGLES #define GEOMETRY_OUTPUT_TYPE GL_TRIANGLE_STRIP #define GEOMETRY_MAX_VERTICES 3 #define ENABLE_BACK_FACE_CULLING true #define LOAD_NORMALS true #define TIMER_MS 0
È opportuno non usare una forma tridimensionale come l'icosaedro per visualizzare il frattale, ma piuttosto un semplice piano:
#define DRAW_SHAPE 1
Si possono anche modificare i parametri numIter e initC. Quest'ultimo è impostato inizialmente a uno dei valori più famosi per gli insiemi di Julia, ma se ne possono provare altri.
const GLfloat UNIFORM_INITC[2] = {0.285,0.013}; #define UNIFORM_NUMITER 400
Questo è il risultato:
Il frattale subisce l'illuminazione, per come è stato definito il fragment shader. Perciò, l'intensità del colore cambia se il piano viene ruotato.
Ad un ingrandimento molto elevato, l'immagine comincia a corrompersi, perché è stata superata la precisione delle variabili float della GPU.
Copyright © 2011 Riccardo Monica
Eccetto ove espressamente indicato altrimenti, il contenuto di questa pagina è disponibile secondo la licenza Creative Commons Attribuzione - Condividi allo stesso modo 3.0.