Silohuette

Un'applicazione interessante degli shader riguarda l'estrazione del contorno (silohuette) da una mesh di poligoni. Intuitivamente, il contorno è definito come l'insieme degli spigoli le cui facce adiacenti sono una rivolta verso il punto di vista e l'altra rivolta nel verso opposto.

Lavorando nel volume canonico, dopo le trasformazioni di vista e di proiezione, la regola si semplifica. Una faccia è rivolta lontano dall'osservatore se è rivolta nella direzione in cui la profondità aumenta, cioè la componente z della normale è positiva. Al contrario, se la componente z è negativa la faccia è rivolta verso l'osservatore.
Ciò permette di affermare che se la normale di una faccia ha segno diverso da quella della faccia adiacente, lo spigolo tra le due è parte della silohuette.

In questa immagine è spiegato il ragionamento. Lo spigolo tra le due facce a sinistra non è parte della silohuette, perché le proiezioni lungo z (in nero) delle normali (in rosso) hanno lo stesso segno. Al contrario, lo spigolo a destra è parte della silohuette.

Schema normali silohuette

Esempio

Il vertex shader si limita a calcolare le coordinate nel volume canonico e a passare avanti il colore.

#version 120

varying vec3 position;

void main()
  {
  vec4 newpos = gl_ModelViewProjectionMatrix * gl_Vertex;
  position = newpos.xyz / newpos.w;

  gl_FrontColor = gl_Color;
  gl_BackColor = vec4(0.0,0.0,0.0,0.0);
  }

Il fragment shader è ancora più semplice, perché deve solo applicare il colore. Siccome si stanno disegnando delle linee, non c'è nessuna illuminazione.

#version 120

void main()
  {
  gl_FragColor = gl_Color;
  }

L'elaborazione è eseguita tutta nel geometry shader. Esso riceve in ingresso un triangolo con informazioni di adiacenza (GL_TRIANGLES_ADJACENCY_EXT, vedere sezione sul Geometry Shader) e produce in uscita i lati del triangolo (con GL_LINE_STRIP) che soddisfano la condizione di appartenere alla silohuette.

Oltre a quanto detto in precedenza, è stata applicata un'ottimizzazione. Per evitare che ciascuno spigolo della silohuette sia elaborato due volte, uno per una faccia e uno per l'altra, il geometry shader si interrompe subito se la faccia non è orientata verso l'osservatore. Ciò ha effetto anche se tutti gli spigoli della faccia non fanno parte della silohuette, ma questo non ha importanza, perché non sarebbe stata visualizzata comunque.

#version 120
#extension GL_EXT_geometry_shader4: enable

varying in vec3 position[6];

// FaceNormal.txt
vec3 ComputeNormal(in vec3 v1,in vec3 v2,in vec3 v3); 

// emette il vertice che ha indice "index"
void Emit(in int index)
  {
  gl_FrontColor = gl_FrontColorIn[index]; 
  gl_BackColor = gl_BackColorIn[index];
  gl_Position = vec4(position[index],1.0);
  EmitVertex();
  }

void main()
  {
  // calcolo della z della normale alla faccia
  float fz = ComputeNormal(position[2],position[0],position[4]).z;

  // ottimizzazione
  if (fz < 0.0)
    return;

  // calcolo della z delle normali alle facce adiacenti
  float z021 = ComputeNormal(position[0],position[2],position[1]).z;
  float z432 = ComputeNormal(position[4],position[3],position[2]).z;
  float z540 = ComputeNormal(position[5],position[4],position[0]).z;
  
  // sappiamo già, per l'ottimizzazione, che la z della normale alla faccia corrente è positiva
  // basta che quella adiacente sia negativa
  if (z021 < 0.0)
    {
    Emit(2);
    Emit(0);
    EndPrimitive();
    }

  if (z432 < 0.0)
    {
    Emit(2);
    Emit(4);
    EndPrimitive();
    }
 
  if (z540 < 0.0)
    {
    Emit(0);
    Emit(4);
    EndPrimitive();
    }
  }

Configurazione dell'esempio

#define SHADER_FOLDER "shaders/silohuette/"

#define USE_VERTEX_SHADER true
#define USE_FRAGMENT_SHADER true
#define USE_GEOMETRY_SHADER true
#define USE_NORMAL_SHADER true
#define USE_VERTEX_LIGHT_SHADER false
#define USE_FRAGMENT_LIGHT_SHADER false
#define USE_TESSEVAL_LIGHT_SHADER false
#define USE_TESSELLATION_SHADERS false

#define GEOMETRY_INPUT_TYPE GL_TRIANGLES_ADJACENCY_EXT         
#define GEOMETRY_OUTPUT_TYPE GL_LINE_STRIP   
#define GEOMETRY_MAX_VERTICES 6 // massimo 6 vertici (tre lati) prodotti per triangolo                

#define ENABLE_BACK_FACE_CULLING true
#define LOAD_NORMALS false // le normali sono calcolate internamente                
#define TIMER_MS 0

Risultato:

Risultato silohuette

Come verifica, si può provare a visualizzare anche il poligono all'interno (dopo aver riattivato il caricamento delle normali):

#define LOAD_NORMALS true
...
#define DRAW_WITHOUT_SHADER true
Risultato silohuette verificato

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.