IT knowledge base
CTRL+F per cercare la tua parola chiave

Impara OpenGL. Lezione 4.11 - Anti-aliasing

OGL3

levigante

Nella tua ricerca sul rendering 3D, probabilmente hai riscontrato frastagliature pixelate attorno ai bordi dei tuoi modelli renderizzati. Questi segni appaiono inevitabilmente a causa del principio della conversione dei dati dei vertici in frammenti dello schermo da un rasterizzatore da qualche parte in profondità nella pipeline OpenGL. Ad esempio, anche su una figura così semplice come un cubo, questi artefatti sono già evidenti:
Una rapida occhiata, forse, non noterà nulla, ma vale la pena guardare più da vicino e le tacche indicate appariranno sui bordi del cubo. Proviamo ad ingrandire l'immagine:
No, questo non va bene. Vuoi davvero vedere una tale qualità dell'immagine nella versione di rilascio della tua applicazione?
Soddisfare
Parte 1. Inizio
  1. OpenGL
  2. Crea una finestra
  3. Ciao finestra
  4. Ciao triangolo
  5. Shader
  6. trame
  7. trasformazioni
  8. Sistemi di coordinate
  9. telecamera

Parte 2. Illuminazione di base
  1. Colori
  2. Nozioni di base sull'illuminazione
  3. Materiali (modifica)
  4. Mappe texture
  5. Fonti di luce
  6. Molteplici fonti di illuminazione

Parte 3. Caricamento di modelli 3D
  1. Libreria Assimp
  2. Classe poligono mesh
  3. Classe modello 3D

Parte 4. Funzionalità OpenGL avanzate
  1. Prova di profondità
  2. Prova dello stampino
  3. Miscelazione dei colori
  4. Ritaglio facce
  5. Buffer frame
  6. Carte cubiche
  7. Manipolazione avanzata dei dati
  8. GLSL avanzato
  9. Shader geometrico
  10. Istanza
  11. levigante

Parte 5. Illuminazione avanzata
  1. Illuminazione avanzata. Modello Blinn-Fong.
  2. Correzione gamma
  3. Mappe delle ombre
  4. Mappe ombre omnidirezionali
  5. Mappatura normale
  6. Mappatura della parallasse
  7. HDR
  8. fioritura
  9. Rendering differito
  10. SSAO

Parte 6. PBR
  1. Teoria
  2. Sorgenti luminose analitiche
  3. IBL. Irraggiamento diffuso.
  4. IBL. Irraggiamento speculare.

L'effetto della visibilità esplicita della struttura pixel per pixel dell'immagine ai bordi degli oggetti è chiamato aliasing. L'industria della computer grafica ha già accumulato molte tecniche chiamate tecniche anti-aliasing o anti-aliasing che combattono contro questo effetto, consentendo di garantire transizioni fluide ai confini degli oggetti.
Ad esempio, una delle prime è stata la tecnica dell'anti-aliasing di supercampionamento ( SSAA ). L'implementazione viene eseguita in due passaggi: primo, il rendering passa a un frame buffer fuori schermo con una risoluzione notevolmente superiore a quella dello schermo; l'immagine è stata quindi ridimensionata al frame buffer sullo schermo. Questa ridondanza di dati dovuta alla differenza di risoluzione è stata utilizzata per ridurre l'effetto dell'aliasing e il metodo ha funzionato correttamente, ma c'era un "ma": le prestazioni. La visualizzazione di una scena in alta risoluzione ha richiesto molta potenza alla GPU e il secolo di gloria di questa tecnologia è stato di breve durata.
Ma dalle ceneri della vecchia tecnologia è nata una nuova, più avanzata tecnologia: l' anti-aliasing multicampionamento ( MSAA ). Si basa sulle idee di SSAA, ma le implementa in modo molto più efficiente. In questo tutorial, daremo un'occhiata più da vicino all'approccio MSAA disponibile in modo nativo in OpenGL.

Multicampionamento

Per comprendere l'essenza del multisampling e come funziona, dobbiamo prima approfondire le viscere di OpenGL e guardare il lavoro del suo rasterizzatore.
Un rasterizzatore è un complesso di algoritmi e procedure che si frappongono tra i dati di vertice elaborati finali e il frammento di ombreggiatura. Il rasterizzatore riceve in input tutti i vertici relativi alla primitiva e converte questi dati in un insieme di frammenti. Le coordinate del vertice, in teoria, possono essere assolutamente qualsiasi cosa, ma non le coordinate dei frammenti: sono strettamente limitate dalla risoluzione del dispositivo di output e dalle dimensioni della finestra. E quasi mai le coordinate del vertice della primitiva saranno sovrapposte ai frammenti uno a uno: in un modo o nell'altro, il rasterizzatore dovrà in qualche modo decidere in quale frammento ea quale coordinata dello schermo sarà ciascuno dei vertici.
L'immagine mostra una griglia che rappresenta i pixel dello schermo. Al centro di ciascuno c'è un punto di campionamento/campionamento , che viene utilizzato per determinare se un triangolo copre un dato pixel. I punti campione coperti da un triangolo sono contrassegnati in rosso: il rasterizzatore genererà un frammento corrispondente per loro. Nonostante il fatto che i bordi del triangolo si sovrappongano ad alcuni pixel in alcuni punti, non si sovrappongono al punto di campionamento: qui il frammento non verrà creato e lo shader del frammento per questo pixel non verrà eseguito.
Penso che tu abbia già intuito le ragioni dell'aliasing. Il rendering di questo triangolo sullo schermo sarà simile a questo:
A causa del numero finito di pixel sullo schermo, alcuni lungo i bordi del triangolo verranno dipinti, altri no. Di conseguenza, si scopre che i primitivi non sono affatto resi con bordi lisci, il che si manifesta sotto forma di quelle stesse dentellature.
Quando si utilizza il multicampionamento, non viene utilizzato un punto per determinare la sovrapposizione di un pixel con un triangolo, ma diversi (da cui il nome). Invece di un punto di campionamento al centro del pixel, verranno utilizzati 4 punti di sottocampionamento per determinare la sovrapposizione, situati in un determinato schema. La conseguenza è che anche la dimensione del buffer di colore deve quadruplicare (in base al numero di punti di sottocampionamento utilizzati).
A sinistra c'è un tipico approccio di rilevamento delle sovrapposizioni. Per il pixel selezionato, il frammento di ombreggiatura non verrà eseguito e rimarrà non dipinto perché non è stata registrata alcuna sovrapposizione. Sulla destra c'è un caso di multicampionamento in cui ogni pixel contiene 4 punti di sottocampionamento. Qui puoi vedere che il triangolo si sovrappone solo a 2 punti del sottocampione.

Il numero di punti di sottocampionamento può essere modificato entro certi limiti. Più punti significano una migliore qualità di anti-aliasing.

Da quel momento in poi, tutto ciò che accade diventa più interessante. Dopo aver determinato che due punti del sottocampione di un pixel sono stati coperti da un triangolo, è necessario emettere il colore finale per quel pixel. La prima ipotesi sarebbe quella di eseguire uno shader di frammenti per ogni punto di sottocampionamento sovrapposto a triangolo e quindi fare la media dei colori di tutti i punti di sottocampionamento in un pixel. In questo caso, dovresti eseguire lo shader frammento più volte con i dati del vertice interpolati alle coordinate di ciascuno dei punti sovrapposti del sottocampione (due volte in questo esempio) e memorizzare i colori risultanti in questi punti. Fortunatamente, non è così che funziona il processo di multicampionamento, altrimenti dovremmo effettuare un numero considerevole di chiamate aggiuntive allo shader dei frammenti, il che avrebbe un grande impatto sulle prestazioni.
Quando si utilizza lo shader di frammenti MSAA viene eseguito solo una volta indipendentemente dal numero di punti di sottocampione primitivi chiusi. Il frammento shader viene eseguito con i dati del vertice interpolati al centro del pixel e il colore ottenuto durante la sua esecuzione viene memorizzato in ciascuno dei punti del sottocampionamento. Quando tutti i punti del sottocampionamento del frame buffer sono riempiti con i colori delle primitive che abbiamo renderizzato, i colori vengono mediati per pixel a un valore per pixel. In questo esempio, solo due punti del sottocampione sono stati sovrapposti e, di conseguenza, riempiti con il colore del triangolo. I restanti due sono stati riempiti con un colore di sfondo trasparente. Mescolando i colori di questi sottocampioni si ottiene un colore azzurro.
Il framebuffer risultante contiene un'immagine di primitive con bordi molto più uniformi. Guarda come appare la definizione della copertura dei sottocampioni sul triangolo già familiare:
Si può notare che ogni pixel contiene quattro punti di sottocampionamento (i pixel non importanti per l'esempio vengono lasciati vuoti), con i punti di sottocampionamento coperti di blu e quelli grigi - scoperti. All'interno del perimetro del triangolo, verrà chiamato uno shader di frammenti una volta per tutti i pixel, il cui risultato verrà salvato in tutti e quattro i sottocampioni. Sui bordi, non tutti i sottocampioni saranno coperti, quindi il risultato dell'esecuzione del frammento di ombreggiatura verrà salvato solo in alcuni di essi. A seconda del numero di punti di sottocampionamento coperti da un triangolo, il colore del pixel finale viene determinato in base al colore del triangolo stesso e ad altri colori memorizzati nei punti di sottocampionamento.
In poche parole, più sottocampioni copre il triangolo, più il colore del pixel corrisponderà al colore del triangolo. Se ora riempite i colori dei pixel nello stesso modo dell'esempio con un triangolo senza utilizzare il multicampionamento, verrà fuori la seguente immagine:
Come puoi vedere, meno sottocampioni di pixel appartengono a un triangolo, meno il suo colore corrisponde al colore del triangolo. I bordi nitidi del triangolo sono ora circondati da pixel di una tonalità leggermente più chiara, che crea un effetto di anti-alias se visti da lontano.
Ma non sono solo i valori del colore a essere influenzati dall'algoritmo di multicampionamento: anche il buffer di profondità e stencil stanno iniziando a utilizzare più sottocampionamenti per pixel. Il valore della profondità del vertice viene interpolato per ciascuno dei punti di sottocampionamento prima di eseguire il test di profondità. I valori dello stencil non vengono memorizzati per l'intero pixel, ma per ciascuno dei punti di sottocampionamento. Per noi questo significa anche un aumento della quantità di memoria occupata da questi buffer, in funzione del numero di sottocampioni utilizzati.
Qui abbiamo trattato le basi del funzionamento del multicampionamento. La vera logica interna del rasterizzatore sarà più complessa della panoramica fornita qui. Tuttavia, ai fini di una comprensione generale del principio e del funzionamento del multicampionamento, questo è abbastanza.

Multicampionamento OpenGL

Per utilizzare il multicampionamento in OpenGL, è necessario utilizzare un buffer di colore in grado di memorizzare più di un valore di colore per pixel (dopotutto, MSAA significa memorizzare un valore di colore in punti di sottocampionamento). Pertanto, abbiamo bisogno di un tipo speciale di buffer in grado di memorizzare un determinato numero di sottocampioni: un buffer multicampione.
La maggior parte dei sistemi a finestre può fornirci un buffer multicampione invece del buffer di colore standard. GLFW ha anche questa funzionalità, tutto ciò che serve è impostare uno speciale flag di segnalazione che si vuole utilizzare il buffer con N punti di sottocampionamento invece di quello standard:
glfwWindowHint(GLFW_SAMPLES, 4);
Ora chiamando glfwCreateWindow verrà creata una finestra di output con un buffer di colore contenente quattro sottocampioni per coordinata dello schermo. Inoltre, GLFW creerà automaticamente buffer di profondità e stencil utilizzando gli stessi quattro punti di sottocampionamento per pixel. E la dimensione di ciascuno dei buffer menzionati quadruplicherà.
Dopo aver creato buffer multicampione tramite GLWL, resta da abilitare la modalità multicampionamento già in OpenGL:
glEnable(GL_MULTISAMPLE);  
Nella maggior parte dei driver OpenGL, il multicampionamento è attivo per impostazione predefinita, quindi questa chiamata sarà ridondante, ma includere esplicitamente le funzioni necessarie è una buona pratica e consentirà anche di attivare la modalità indipendentemente dalle impostazioni predefinite di una particolare implementazione.
In realtà, dopo aver ordinato un buffer multicampione e aver attivato la modalità, tutto il nostro lavoro è terminato, poiché tutto il resto ricade sui meccanismi del rasterizzatore OpenGL e avviene senza la nostra partecipazione. Se ora proviamo a disegnare un cubo verde, familiare fin dall'inizio della lezione, vedremo che i suoi bordi sono ora molto più levigati:
In effetti, i bordi di questo cubo sembrano molto più attraenti. E lo stesso effetto influenzerà qualsiasi oggetto nella tua scena.
La fonte per l'esempio è qui .

Multicampionamento fuori schermo

Creare un framebuffer principale con MSAA abilitato è facile grazie a GLFW. Se vogliamo creare il nostro buffer, ad esempio, per il rendering fuori schermo, dovremo prendere in mano questo processo.
Ci sono due metodi principali per creare buffer multicampionamento per ulteriori allegati al framebuffer, simili agli esempi già discussi nella lezione corrispondente: allegati texture e allegati renderbuffer.

Allegato texture con multicampionamento

Per creare una trama che supporti più sottocampioni, vengono utilizzati il tipo di destinazione della trama GL_TEXTURE_2D_MULTISAPLE e la funzione glTexImage2DMultisample invece del solito glTexImage2D :
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);  
Il secondo argomento specifica il numero di sottocampioni nella trama generata. Se l'ultimo argomento è GL_TRUE , la trama utilizzerà lo stesso numero e posizione di punti di sottocampionamento per ogni texel.
Per associare una tale trama a un oggetto framebuffer, viene utilizzata la stessa chiamata glFramebufferTexture2D , ma questa volta, con il tipo di trama specificato GL_TEXTURE_2D_MULTISAMPLE:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
Di conseguenza, l'attuale framebuffer verrà fornito con un buffer di colore basato su texture con supporto per il multicampionamento.

Renderbuffer con multicampionamento

Creare un buffer di rendering con molti punti di sottocampionamento non è più difficile che creare una tale texture. Inoltre, è ancora più semplice: tutto quello che dovete fare è modificare la chiamata al glRenderbufferStorage glRenderbufferStorageMultisample durante la preparazione di memoria per l'oggetto renderbuffer attualmente vincolati:
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height); 
Tra i nuovi, c'è solo un parametro aggiuntivo che viene dopo il tipo di destinazione del binding del buffer di rendering, che specifica il numero di punti di sottocampionamento. Qui abbiamo indicato quattro di questi punti.

Rendering su framebuffer con multicampionamento

Il frame buffer viene reso automaticamente in un frame multicampionato, senza alcuna azione richiesta da parte nostra. Ogni volta che eseguiamo il rendering su un framebuffer associato, il rasterizzatore stesso esegue le operazioni necessarie. E otteniamo in uscita un buffer di colore (profondità, stencil) con molti punti di sottocampionamento. Poiché un frame buffer con molti punti di sottocampionamento è ancora in qualche modo diverso dal solito, non sarà possibile utilizzare direttamente i suoi buffer individuali per varie operazioni, come il recupero di texture in uno shader.
Un'immagine con supporto multicampionamento contiene più informazioni rispetto a quella normale, quindi è necessario risolvere ( risolvere ) questa immagine, o, in altre parole, convertirne la risoluzione in una più bassa. Questa operazione, come al solito, viene effettuata utilizzando una chiamata a glBlitFramebuffer , che permette di copiare un'area di un frame buffer in un'altra con la relativa risoluzione dei buffer presenti con molti punti di sottocampionamento.
Questa funzione traduce l'area di origine, specificata da quattro coordinate nello spazio dello schermo, nell'area di destinazione, anch'essa specificata da quattro coordinate dello schermo. Permettetemi di ricordarvi la lezione sui framebuffer : se associamo un oggetto framebuffer al target GL_FRAMEBUFFER , allora il binding viene implicitamente eseguito sia al target di lettura dal framebuffer che al target di scrittura al framebuffer . Per eseguire il binding a questi target separatamente, vengono utilizzati identificatori di target speciali: GL_READ_FRAMEBUFFER e GL_DRAW_FRAMEBUFFER, rispettivamente.
Durante l'esecuzione, glBlitFramebuffer utilizza questi punti di ancoraggio per determinare quale frame buffer è l'origine dell'immagine e quale è la destinazione. Di conseguenza, potremmo semplicemente trasferire l'immagine dal frame buffer multicampione a quello standard usando il blitting:
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
Dopo aver assemblato e lanciato l'applicazione, otterremmo un'immagine identica all'esempio precedente, che non utilizzava il frame buffer: un cubo verde acido, reso usando MSAA, come puoi vedere esaminando i suoi bordi - sono ancora lisci:
Le fonti per l'esempio sono qui .
Ma cosa succede se volessimo usare un'immagine da un framebuffer con molti punti di sottocampionamento come fonte di dati per la post-elaborazione? Non possiamo usare direttamente un multicampione di una texture in uno shader. Ma puoi provare a trasferire un'immagine da un frame buffer multicampione usando blitting a un altro, con buffer ordinari, non multicampione. E poi puoi usare un'immagine normale come risorsa per la post-elaborazione, in effetti, ottenendo tutti i vantaggi di MSAA e aggiungendo la post-elaborazione su di essa. Sì, per l'intero processo dovrai avviare un frame buffer separato, che funge esclusivamente da oggetto ausiliario per la risoluzione delle trame MSAA in trame ordinarie che possono essere utilizzate nello shader. In forma di pseudocodice, il processo si presenta così:
unsigned int msFBO = CreateFBOWithMultiSampledAttachments ();
// then create another FBO with a regular texture as a color attachment
...
glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
...
while (! glfwWindowShouldClose (window))
{
    ...
    
    glBindFramebuffer (msFBO);
    ClearFrameBuffer ();
    DrawScene ();
    // resolution of the multisample buffer using an auxiliary
    glBindFramebuffer (GL_READ_FRAMEBUFFER, msFBO);
    glBindFramebuffer (GL_DRAW_FRAMEBUFFER, intermediateFBO);
    glBlitFramebuffer (0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
    // now the scene image is saved in a regular texture, which is used for post-processing
    glBindFramebuffer (GL_FRAMEBUFFER, 0);
    ClearFramebuffer ();
    glBindTexture (GL_TEXTURE_2D, screenTexture);
    DrawPostProcessingQuad ();
  
    ...
}
Se aggiungiamo questo codice agli esempi di post-elaborazione di una lezione sul frame buffer , possiamo applicare tutti gli effetti all'immagine della scena, senza bordi frastagliati. Ad esempio, con un effetto di sfocatura, otterrai qualcosa del genere:

Poiché per la post-elaborazione viene utilizzata una texture standard con un punto di sottocampionamento, alcuni metodi di elaborazione (ricerca di bordi, ad esempio) possono nuovamente introdurre nella scena evidenti bordi taglienti e dentellature. Per aggirare questo artefatto, devi sfocare il risultato o implementare il tuo algoritmo di anti-aliasing.

Come puoi vedere, la combinazione di MSAA e tecniche di rendering fuori schermo ha alcuni dettagli da considerare. Ma tutto lo sforzo extra viene ripagato con la qualità molto più elevata dell'immagine risultante. Tuttavia, tieni presente che l'abilitazione del multicampionamento può comunque influire in modo significativo sulle prestazioni finali, specialmente quando viene impostato un numero elevato di punti di sottocampionamento.

Proprio metodo di anti-alias

In effetti, puoi trasferire un multicampione di una trama direttamente agli shader, senza blitting in uno ausiliario. In questo caso, le funzionalità GLSL forniscono l'accesso a singoli punti di sottocampionamento nella trama, che possono essere utilizzati per creare i propri algoritmi di anti-aliasing (che è spesso nelle applicazioni grafiche di grandi dimensioni).
Per prima cosa, devi creare un campionatore speciale come sampler2DMS , invece del solito sampler2D :
uniform sampler2DMS screenTextureMS;
E per ottenere il valore del colore nel punto di sottocampionamento, viene utilizzata la seguente funzione:
vec4 colorSample = texelFetch (screenTextureMS, TexCoords, 3); // read from the 4th point of the subsample
Qui puoi vedere un argomento aggiuntivo: il numero del punto del sottocampione (contando da zero) a cui si fa riferimento.
Non prenderemo in considerazione i dettagli della creazione di algoritmi anti-aliasing qui: questo non è altro che un punto di partenza per la tua ricerca su questo argomento.
PS : abbiamo una telegramma-conferenza per coordinare i trasferimenti. Se hai un serio desiderio di aiutare con la traduzione, allora sei il benvenuto!