Rosario 3D

My developer blog

Category Archives: openGL

Lezione su gli shader, seconda parte [ITA]

Illuminazione dinamica

Con le tecniche visto fino adesso l’illuminazione era statica invariante dalle condizioni ambientali o di illuminazione. Il primo shader con illuminazione dinamica che vedremo è lo shader di tipo lambertiano. Lambert ha creato un modello di illuminazione dove la luminosità di una superficie dipende solo dal coseno dell’angolo formato tra la normale della superficie e la direzione di provenienza della luce. In questo vertex shader prendiamo la posizione della luce e un colore base.

Da notare che la normale viene letta tramite la variabile gl_Normal, siccome la normale è un vettore di tre elementi non può essere moltiplicata per la model view (che è 4×4), quindi bisogna usare la matrice gl_NormalMatrix.

uniform vec3 lightPos;
uniform vec3 baseColor;

varying vec3 color;

void main(){
  // posizione del vertice in eye space
  vec3 vPos = (gl_ModelViewMatrix * gl_Vertex).xyz;
  // direzione di provenienza della luce
  // attenzione, la posizione della luce deve essere passata in eye space
  vec3 lDir = normalize(lightPos - vPos);
  vec3 n = gl_NormalMatrix * gl_Normal;
  // L'intensità dell'illuminazione è data dal coseno tra i due vettori e non può
  // assumere valori negativi.
  color = baseColor * max(0.0, dot(n, lDir));

  gl_Position = ftransform();
}

Il fragment shader non farà altro che scrivere il colore calcolato sul frame buffer.

varying vec3 color;

void main()
{
  gl_FragColor = vec4(color, 1.0);
}

Al posto del colore uniforme possiamo usare una texture, lo shader cambia poco (l’impatto visivo cambia molto).

uniform vec3 lightPos;
varying vec2 uvMap;
varying float diff;

void main()
{
  vec3 vPos = (gl_ModelViewMatrix * gl_Vertex).xyz;
  vec3 lDir = normalize(lightPos - vPos);
  vec3 n = gl_NormalMatrix * gl_Normal;
  uvMap = gl_MultiTexCoord0.xy;
  diff = max(0.0, dot(n, lDir));

  gl_Position = ftransform();
}

Anche il fragment shader è simile all’esempio della texture visto nella prima parte, la cosa da notare è che il colore è modulato in base all’illuminazione.

sampler2D diffuse;
varying float diff;
varying vec2 uvMap;

void main(){
  vec4 diffMap = texture2D(diffuse, uvMap);
  gl_FragColor = vec4(diff* diffMap.xyz, 1.0);
}

Questo modello di illuminazione è molto semplice ma poco realistico, la luce dalla superficie è come se fosse emessa uniformemente in tutte le direzioni. Questo shader si adatta a pochi materiali, principalmente a oggetti non riflettenti (terracotta, intonaco, pietre), in cui per motivi di performance non vengono considerate le micro sfaccettature.

Phong shader

Questo shader aggiunge alla componente diffusiva una componente speculare. Si basa sull’assunzione che la luce verrà riflessa principalmente lungo vettore simmetrico a quello di direzione della luce (usando come asse di simmetria la normale della superficie) e l’intensità andrà diminuendo man mano che ci si scosta da questo vettore. Per calcolare quanto la telecamera è vicina alla luce riflessa possiamo calcolare la bisettrice tra posizione della telecamera e direzione della luce, useremo il coseno tra la bisettrice e la normale come indice di vicinanza tra i due vettori. La luce speculare decade più o meno velocemente in base alla rugosità della superficie, useremo un fattore per regolare il decadimento della specularità.

uniform vec3 lightPos;
uniform float shininess;

varying vec2 uvMap;
varying float lum;

void main(){
  // calcolo la componente diffusiva, niente di nuovo
  vec3 vPos = (gl_ModelViewMatrix * gl_Vertex).xyz;
  vec3 lDir = normalize(lightPos - vPos);
  vec3 n = gl_NormalMatrix * gl_Normal;
  uvMap = gl_MultiTexCoord0.xy;
  float diff = max(0.0, dot(n, lDir));

  float specular = 0.0;
  // la componente speculare va calcolata solo se la parte diffusiva è maggiore di zero
  if(diff > 0.0){
    // calcolo la bisettrice
    vec3 vHalf = normalize(lDir - vPos);
    // coseno tra bisettrice e normale limitato inferiormente a zero (non vogliamo valori negativi)
    float nDotH = max(dot(n, vHalf), 0.0);
    // applico il decadimento (phong usa una potenza)
    specular = pow(nDotH, shininess);
  }
  // sommo le due componenti
  lum = diff + specular;
  gl_Position = ftransform();
}

Il fragment shader a parte il cambio di nome di una variabile è uguale al precedente. Il vero problema di questo shader è che tutte le componenti del illuminazione vengono calcolate nel vertex shader. La specularità è fortemente non lineare (c’è un coseno e una potenza) e le interpolazioni lineari causano dei vistosi artefatti.

Per risolvere il problema possiamo spostare i calcoli nel fragment shader e lasciare il vertex shader l’unico compito di passare i parametri da uno stage all’altro. Questo vertex shader non fa altro che passare i parametri (direzione della luce e della telecamera) da vertice a fragment.

uniform vec3 lightPos;

varying vec3 eyeDir;
varying vec3 lightDir;
varying vec3 norm;
varying vec2 uvMap;

void main(){
  eyeDir   = -(gl_ModelViewMatrix * gl_Vertex).xyz;
  lightDir = normalize(lightPos + eyeDir);
  norm = gl_NormalMatrix * gl_Normal;
  uvMap = gl_MultiTexCoord0.xy;

  gl_Position = ftransform();
}

Tutti i calcoli vengono fatti per pixel. Il fragment shader è molti simile al vertex shader precedente, la cosa importante da notare è che i vettori norm, lightDir, eyeDir che sono assunti come normalizzati nel vertex shader devono essere nuovamente normalizzati perché la loro lunghezza potrebbe essere modificata dall’interpolazione tra vertex e fragment.

uniform float shininess;
uniform sampler2D diffuse;

varying vec3 eyeDir;
varying vec3 lightDir;
varying vec3 norm;
varying vec2 uvMap;

void main(){
  vec3 diffMap = texture2D(diffuse, uvMap).xyz;
  vec3 n = normalize(norm);
  vec3 ld = normalize(lightDir);
  vec3 ed = normalize(eyeDir);
  float diff = max(0.0, dot(n, ld));
  float sp = 0.0;
  if(diff > 0.0){
    // per calcolare la "distanza" dal vettore riflesso si calcola proprio il vettore
    // usando la funzione reflect
    vec3 R = reflect(-ld, n);
    float nDotH = max(dot(ed, R), 0.0);
    sp = pow(nDotH, shininess);
  }
  gl_FragColor = vec4((diff + sp) * diffMap.xyz, 1.0);
}

Gli oggetti non sono quasi mai speculari uniformemente, usare una mappa per la specularità aumenta notevolmente il realismo dell’oggetto e toglie l’effetto plasticoso tipico dello shader phong.

Potete trovare un ottimo tutorial su come creare empiricamente le specular map a questo link.

uniform float shininess;
uniform sampler2D diffuse;
uniform sampler2D specular;

//...

  gl_FragColor = vec4(diff *diffMap + sp * specMap, 1.0);
}

Normal mapping

Le normal map sono delle texture su cui viene salvata la normale perturbata, cioè il discostamento tra la normale dei poligoni e la vera normale della superficie. Le normal map vengono salvate spesso con riferimento al poligono, quindi tutte le posizioni e direzioni che sono normalmente in eye coordinate devono essere trasformate in quello che viene chiamato tangent space. Per usare il tangent space oltre alla normale bisogna avere una terna ortonormale formata da normale, tangente e binormale. Questa terna viene ricavata dalle normali e le uvMap (se ci sono più uvMap ci possono essere più tangent space per oggetto) e indica lungo quali direzioni sono orientate le normali nella normal map.

La tangente non è un attributo tra quelli predefiniti, dobbiamo usare un nuovo tipo di variabile, il tipo attribute. Un attribute è una variabile che rappresenta un valore associato ad un vertice. Ovviamente anche se semanticamente scorretto potremmo usare uno dei dati non usati (per esempio una texture coordinate) per passare i valori di tangent.

uniform vec3 lightPos;
attribute vec3 tangent;

varying vec3 eyeDir;
varying vec3 lightDir;
varying vec2 uvMap;

void main(){
  vec3 n = gl_NormalMatrix * gl_Normal;
  // anche tangent è un vettore di 3 ed è solidale alla normale
  vec3 t = gl_NormalMatrix * tangent;
  // la binormale può essere passata con il suo attribute o può essere ricavata come prodotto vettoriale
  vec3 b = cross(n, t);

  vec3 v = (gl_ModelViewMatrix * gl_Vertex).xyz;
  vec3 temp = -v;
  // trasformo la eyeDir in tangent space
  eyeDir.x   = dot(temp, t);
  eyeDir.y   = dot(temp, b);
  eyeDir.z   = dot(temp, n);

  temp = normalize(lightPos - v);
  // anche la direzione della luce deve essre in tantent space
  lightDir.x = dot(temp, t);
  lightDir.y = dot(temp, b);
  lightDir.z = dot(temp, n);

  uvMap = gl_MultiTexCoord0.xy;

  gl_Position = ftransform();
}

Notare che il varying per la normale è stato tolto visto che leggeremo la normale dal una texture. A parte questo il fragment shader non cambia molto rispetto all’esempio precedente.

uniform float shininess;
uniform sampler2D diffuse;
uniform sampler2D specular;
uniform sampler2D normal;

varying vec3 eyeDir;
varying vec3 lightDir;
varying vec2 uvMap;

void main(){
  vec3 diffMap = texture2D(diffuse, uvMap).xyz;
  vec3 specMap = texture2D(specular, uvMap).xyz;
  // La texture può memorizzare sono valori positivi, per questo dobbiamo fare
  // questo tipo di conversione
  vec3 normMap = texture2D(normal, uvMap).xyz * 2.0 - 1.0;

  vec3 n = normalize(normMap);
  vec3 ld = normalize(lightDir);
  vec3 ed = normalize(eyeDir);
  float diff = max(0.0, dot(n, ld));
  float sp = 0.0;
  if(diff > 0.0){
    vec3 R = reflect(-ld, n);
    float nDotH = max(dot(ed, R), 0.0);
    sp = pow(nDotH, shininess);
  }

  gl_FragColor = vec4(diff *diffMap + sp * specMap, 1.0);
}

Potete trovare un tutorial molto dettagliato che spiega GLSL (in inglese) su questo sito.

Advertisements

Lezione su gli shader, parte prima ITA

Questo post è in italiano e serve per spiegare la lezione che ho fatto oggi (4 Maggio) al corso di realtà virtuale. Appena ho un po’ di tempo la tradurrò in inglese.

I primi shaders

Tramite vertex shader potete leggere i dati provenienti da openGL manipolarli e inviare altri dati agli stadi successivi della pipeline. Il vertex shader deve scrivere sulla variabile gl_Position, in questo prendo la posizione del vertice (non trasformata) e la invio alla pipeline

void main(){
  gl_Position = gl_Vertex;
}

La pileline da i vertici genererà i poligoni che poi verranno renderizzati tramite fragment shader. Il fragment shader deve scrivere su la variabile gl_FragColor

Attenzione!! gl_FragColor e’ stata deprecata in openGL 3.0 e viene sostituita da variabili custom di output. Per ulterio dettagli potete leggere la sezione 3.9.2 delle specifiche openGL (Shader output)

void main(){
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Le trasformazioni possono essere applicate tramite le matrici si puo’ accedere alle matrici tramite le variabile gl_ProjectionMatrix e gl_ModelViewMatrix. Queste due variabili sono invariati per tutti i vertici della geometria, questo tipo di variabili sono dette uniform.

void main(){
  gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
}

È possibile specificare dei parametri agli shader tramite degli uniform personalizzate. Il nome non può iniziare con gl_, visto che questi nomi sono riservati alle variabili predefinite. In questo caso si e’ passato un colore di riempimento.

uniform vec3 fillColor;

void main(){
  gl_FragColor = vec4(fillColor, 1.0);
}

Dal programma che usa lo shader si dovrà specificare il valore dell’uniform, sia il nome che il tipo dovrà corrispondere a quello della variabile

Applichiamo una texture

Per applicare le texture abbiamo bisogno delle coordinate UV, coordinate che possiamo prendere dalla variabile gl_MultiTexCoord0 (si possono usare fino a 8 texture coordinate)
Siccome useremo le UV map nel fragment shader dobbiamo usare una variabile di passaggio. Le variabili di tipo varying permettono di passare i valori i diversi stage della pipeline. Tra vertex shader e fragment i valori verranno
interpolati linearmente.

Attenzione!! Con l’aumentare degli stage della pipeline la keyword varying e’ stata deprecata, ed e’ stata sostituita dalle piu’ esplicite in e out. In questo caso nel vertex shader avremmo una variabile di tipo out e nel fragment una variabile di tipo in.

varying vec2 uvMap;

void main(){
  uvMap = gl_MultiTexCoord0.xy;
  gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
}

La variabile di tipo varying deve essere presente anche nel fragment shader altrimenti la fase di linking genererà un errore. Per leggere la texture bisogna usare un tipo particolare di dato, il sampler2D. Il sampler2D è un intero che può essere usato solo per specificare quale texture usare. In particolare indica quale texture unit usare tra le texture 2D. Ci sono sampler anche per le texture 1D, 3D, cubeMap e altri tipi particolari come per le shadow, nelle versioni più recenti di openGL ci sono ulteriori tipi di sampler per accedere ai moderni textureBuffer e multi sample texture.

Tramite il sampler e le coordinate uv possiamo leggere il valore della texture (texel) usando la funzione texture2D. Di questa funzione esistono diverse varianti, questa è la più semplice che calcola in automatico il livello mipmap da usare.

uniform sampler2D diffuse;
varying vec2 uvMap;

void main(){
  gl_FragColor = texture2D(diffuse, uvMap);
}

Possiamo usare due texture e combinarle assieme, nel fragment shader dovremmo dichiarare un’altra variabile varying e leggere un’altra texCoord

varying vec2 uvMap;
varying vec2 uvLightmap;

void main(){
  uvMap       = gl_MultiTexCoord0.xy;
  uvLightmap  = gl_MultiTexCoord1.xy;
  gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
}

Nel fragment shader dobbiamo accedere alle due texture e moltiplicare una per l’altra. Attenzione, la lightmap potrebbe contenere delle informazioni sull’alpha. Se vogliamo usare solo l’alpha delle texture diffusiva dobbiamo separare i calcoli tra colore e alpha.

uniform sampler2D diffuse;
uniform sampler2D lightmap;

varying vec2 uvMap;
varying vec2 uvLightmap;

void main(){
  vec4 diff = texture2D(diffuse, uvMap);
  vec3 light= texture2D(lightmap, uvLightmap).xyz;
  gl_FragColor = vec4(diff.xyz * light , diff.a);
}

Vertex Color

Le informazioni sull’illuminazione posso essere lette anche da vertice. In particolare possiamo leggere il colore per vertice nella variabile gl_Color (e gl_SecondaryColor). Supponiamo di aver memorizzato le informazioni dell’ambient occlusion ne il vertex color.

varying vec4 occlusion;

void main(){
  occlusion = gl_Color;
  gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
}

nel fragment possiamo banalmente copiare il colore per vertice nella variabile di output.

varying vec4 occlusion;

void main(){
  gl_FragColor = vec4(occlusion);
}

Questa tecnica si presta bene per mesh molto dense altrimenti si potranno notare artefatti dovuti all’interpolazione tra i vertici.

Un altra cosa che possiamo fare con i vertex shader è animare la mesh, possiamo spostare i vertici a piacimento con la tecnica che preferiamo. Le animazioni tramite bones vengono eseguite nel vertex shader. In questo caso vediamo un animazione procedurale tramite due sinusoidi. Da programma possiamo modificare l’uniform phase per eseguire l’animazione.

uniform float phase;
varying vec4 occlusion;

void main(){
  occlusion = gl_Color;
  vec4 pos = gl_Vertex;
  pos.y = gl_Vertex.y + 0.2*cos(gl_Vertex.y*5.0+phase);
  gl_Position = gl_ModelViewProjectionMatrix * pos;
}

Oltre alle variabili gl_ModelViewMatrix e gl_ProjectionMatrix abbiamo a disposizione la matrice gl_ModelViewProjectionMatrix in cui le due sono già moltiplicate a priori.

Nel caso il vertice non debba essere spostato possiamo usare la funzione ftransform() che trasformerà in maniera efficiente e accurata usando le matrici modelview e projection.

Glus for linux

Recently Norbert Nopper have released GLUS, a framework similar to (free)Glut. The first version was only for windows, but recently Pablo Alonso-Villaverde Roza wrote a version that work with X11.

I have tested this evening on Ubuntu 9.10 with a G8800 and it works perfectly.

How to build the code

GLUS use Glew to load all the openGL extension, the Glus packet that you download from the site already include Glew. But if you have Glew already installed it can generate some conflict. The Glew library that came with Ubuntu is not the most recently updated and support openGL up to 3.0. So you can remove Glew and use the one that came with Glus or download the latest Glew source and build it by yourself (it take 5 min.)

svn co https://glew.svn.sourceforge.net/svnroot/glew/trunk/glew glew
cd glew
#Probably you will need these package
sudo apt-get install libxmu-dev xorg-dev
make extensions
sudo make install

Now you can go in you Glus directory and type

make all

Now you can run the examples, make sure you have the latest driver installed.

About Glus

I’m not a big fan of Glut, it imposes a programming style that leads to a tons of global variables (I’ll write an article about that)  but is very simple and for a rough test is very good. Glus use the same program style but removing all the deprecated stuff and add some utility function to:

  • create openGL 3.2 context
  • read and create shaders from files
  • operate on matrix and vector
  • load TGA texture
  • create some basic shapes (cubes, sphere, plane)

Pro:

  • Very easy to learn, if you already had programmed with glut you don’t have to learn anything new.
  • First framework that support openGL 3.2 natively.

Cons:

  • Very young, no documentation yet.
  • It’s not object oriented
  • No support for multi-threading
  • No license… MIT? ZLIB? Need to ask.

I still don’t know why people in 2010 don’t program with object, but I think it’s a matter of taste. The lack of documentation is fullfilled by some example. I’m a bit worried about the license I hope it will adopt LGPL3.

Conclusion

IMHO Glus is very good to create some basic example, with the utility function you don’t have to bother about texture loading or shader creation, making prototyping faster. I don’t know if it’s good to create more complex program. In this case you usually create your own code for matrix, image and shader manipulation, also it lack of multi-thread support that really limit the possibilities.