Il presente articolo è stato tradotto automaticamente.

DirectX Factor

Pixel Shader e riflessi di luce

Charles Petzold

Scaricare il codice di esempio

Charles PetzoldSe tu potessi vedere fotoni... Beh, si possono vedere fotoni, o almeno alcuni di loro. I fotoni sono particelle che compongono la radiazione elettromagnetica, e l'occhio è sensibile ai fotoni con lunghezze d'onda all'interno della gamma della luce visibile.

Ma non si possono vedere fotoni come volano circa tutto il luogo. Sarebbe sicuramente interessante. A volte fotoni passare proprio attraverso oggetti; a volte sono assorbiti; a volte essi stai riflessa; ma spesso è una combinazione di tutti questi effetti. Alcuni dei fotoni che rimbalzano contro gli oggetti alla fine raggiungono gli occhi, dando ogni oggetto suo particolare colore e texture.

Per la grafica 3D di altissima qualità, una tecnica chiamata raytracing può effettivamente trama la una simulazione dei percorsi di questi fotoni di una miriade di mimare gli effetti di riflessione e di ombre. Ma molto più semplici tecniche sono disponibili per le esigenze più convenzionale. Questo è spesso il caso quando si utilizza Direct3D — o, nel mio caso, scrivendo gli effetti personalizzati in Direct2D che fanno uso del 3D.

Riutilizzando l'effetto

Come si è visto nelle puntate precedenti di questa colonna, un effetto di Direct2D è fondamentalmente un wrapper per il codice che viene eseguito su GPU. Tale codice è conosciuto come uno shader, e le più importanti sono il vertex shader e pixel shader. Il codice in ciascuno di questi shader viene chiamato presso la frequenza di aggiornamento del display. Per ciascuno dei tre vertici in ogni triangolo che compongono gli oggetti grafici visualizzati dall'effetto, mentre il pixel shader viene chiamato per ogni pixel all'interno di questi triangoli è chiamato vertex shader.

Ovviamente, il pixel shader è chiamato molto più frequentemente di vertex shader, quindi ha senso mantenere quanto più elaborazione possibile nel vertex shader, piuttosto che il pixel shader. Questo non è sempre possibile, tuttavia, e quando si utilizzano questi shader per simulare il riflesso della luce, è solitamente l'equilibrio e l'interazione tra questi due shader che governa la raffinatezza e la flessibilità di ombreggiatura.

Nel numero di agosto di questa rivista, presentato un effetto Direct2D chiamato RotatingTriangleEffect costruito un vertex buffer costituito da punti e colori, che ha permesso di applicare trasformazioni standard modello e macchina fotografica per i vertici. Ho usato questo effetto per rotazione tre triangoli. Che non è un sacco di dati. Tre triangoli coinvolgono solo un totale di nove vertici, e ho detto al momento che lo stesso effetto potrebbe essere utilizzato per un molto più grande vertex buffer.

Proviamo: Il programma scaricabile (msdn.microsoft.com/magazine/msdnmag1014) per questa colonna è chiamata ShadedCircularText, e RotatingTriangleEffect usa senza una singola modifica.

Il programma ShadedCircularText ritorna al problema che ho cominciato ad esplorare quest'anno di visualizzazione tassellata testo 2D in tre dimensioni. Il costruttore della classe ShadedCircularTextRenderer viene caricato in un file di font, crea un carattere tipografico da esso e quindi chiama GetGlyphRunOutline per ottenere una geometria del percorso dei contorni di carattere. Questa geometria del percorso è quindi a mosaico utilizzando una classe creata chiamata InterrogableTessellationSink che si accumula i triangoli effettivi.

Dopo la registrazione RotatingTriangleEffect, ShadedCircularText­Renderer crea un oggetto ID2D1Effect basato su questo effetto. Converte quindi i triangoli del testo tassellato in vertici sulla superficie di una sfera, fondamentalmente avvolgendo il testo intorno all'equatore e piegarlo verso i poli. Il colore di ciascun vertice è basato su una tonalità derivata dalla coordinata X della geometria del testo originale. Questo crea un effetto arcobaleno, e Figura 1 Mostra il risultato.

testo 3D arcobaleno da ShadedCircularText
Figura 1 testo 3D arcobaleno da ShadedCircularText

Come potete vedere, un piccolo menu adorna la parte superiore. Il programma incorpora effettivamente tre ulteriori effetti di Direct2D che implementano modelli più tradizionali di ombreggiatura. Tutti li utilizzano gli stessi punti, la stessa si trasforma e le stesse animazioni, quindi può passare tra di loro per vedere la differenza. Le differenze riguardano solo l'ombreggiatura di colore dei triangoli.

L'angolo inferiore destro ha un display di prestazioni in fotogrammi al secondo, ma scoprirete che nulla in questo programma fa sì che a goccia molto di sotto 60 tranne se qualcos'altro sta succedendo.

Gouraud Shading

Come fotoni stanno volando intorno a noi, essi spesso rimbalzano molecole di azoto e ossigeno nell'aria. Anche in una giornata nuvolosa con nessuna luce diretta del sole, c'è ancora una considerevole quantità di luce ambientale. Luce ambiente tende ad illuminare gli oggetti in modo molto uniforme.

Forse hai un oggetto che è un blu verdastro, con un valore RGB (0, 0.5, 1.0). Se la luce ambientale è un quarto di intensità completo bianco, è possibile assegnare un valore RGB alla luce (0.25, 0.25, 0.25). Il colore percepito dell'oggetto è il prodotto delle componenti rossi, verde e blu di questi numeri, o (0, 0.125, 0.25). È ancora un blu verdastro, ma molto più scuro.

Ma semplici scene 3D non vivono di luce ambiente da solo. Nella vita reale, gli oggetti normalmente hanno un sacco di variazione colore sulle loro superfici, quindi, anche se essi sono uniformemente illuminati, gli oggetti hanno ancora visibile texture. Ma in una semplice scena 3D, un oggetto verdastro-blu illuminato solo dalla luce ambiente semplicemente assomiglierà un indifferenziato lastra di colore uniforme.

Per questo motivo, scene 3D semplice beneficiano enormemente da qualche luce direzionale. È più semplice supporre che questa luce proviene da una lunga distanza (come il sole), quindi la direzione della luce è solo un singolo vettore che si applica all'intera scena. Se c'è solo una fonte di luce, generalmente si presume a venire da oltre la spalla sinistra dello spettatore, quindi forse che il vettore è (1, -1, -1) in un sistema di coordinate di destro. Questa luce direzionale ha anche un colore, forse (0.75, 0.75, 0.75), così insieme con la luce ambiente (0.25, 0.25, 0.25), massima illuminazione è talvolta raggiunto.

La quantità di luce direzionale, che riflette una superficie dipende dall'angolo che la luce fa con la superficie. (Questo è un concetto esplorato nel mio maggio 2014 colonna fattore DirectX.) La riflessione di massima si verifica quando la luce direzionale è perpendicolare alla superficie, e la luce riflessa si riduce a zero quando la luce è tangente alla superficie o provenienti da qualche parte dietro la superficie.

La legge di coseno del Lambert — chiamato dopo il matematico e fisico Johann Heinrich Lambert (1728-1777) — dice che la frazione di luce riflettuto da una superficie è il coseno dell'angolo tra la direzione della luce e la direzione di un vettore perpendicolare alla superficie, che è denominata una superficie normale negativo. Se questi due vettori sono normalizzati — che è, se hanno una grandezza di 1 — questo coseno dell'angolo tra due vettori è lo stesso come il prodotto scalare dei vettori.

Ad esempio, se la luce colpisce una superficie particolare ad un angolo di 45 gradi, il coseno è circa 0.7, così che moltiplicare per il diret­zionale luce colore (0.75, 0.75, 0.75) e il colore dell'oggetto (0, 0.5, 1.0) per ricavare il colore dell'oggetto da luce direzionale di (0, 0,26 0,53). Aggiungere che per il colore dalla luce ambiente.

Tuttavia, tieni presente che curvo oggetti nelle scene 3D in realtà non sono curve. Tutto nella scena è costituito da triangoli piatte. Se l'illuminazione di ogni triangolo è basato su una superficie perpendicolare normale per il triangolo stesso, ogni triangolo avrà un diverso colore uniforme. Questo è bene per i solidi platonici come quelli visualizzati nel mio articolo di maggio 2014, ma non così buono per superfici curve. Per superfici curve, si desidera che i colori dei triangoli per fondersi con l'altro.

Questo significa che è necessario per ogni triangolo avere un colore graduato, piuttosto che un colore uniforme. Il colore dalla luce direzionale non può basarsi su una singola superficie normale per il triangolo. Invece, ogni vertice del triangolo deve avere un colore diverso, basato sulla superficie normale in quel Vertice. Questi colori di vertice possono essere interpolati poi sopra tutti i pixel del triangolo. Triangoli adiacenti poi fondono con l'altro per assomigliare ad una superficie curva.

Questo tipo di ombreggiatura è stato inventato da informatico francese Henri Gouraud (b. 1944) in un articolo pubblicato nel 1971 ed è pertanto conosciuta come Gouraud shading.

Gouraud shading è la seconda opzione, implementata nel programma ShadedCircularText. L'effetto si è chiamato GouraudShadingEffect, e richiede un vertex buffer con un po ' più dati:

struct PositionNormalColorVertex
{
  DirectX::XMFLOAT3 position;
  DirectX::XMFLOAT3 normal;
  DirectX::XMFLOAT3 color;
  DirectX::XMFLOAT3 backColor;
};

Interessante, perché il testo è effettivamente essere avvolti intorno a una sfera centrata nel punto (0, 0, 0), la superficie normale in ogni vertice è la stessa come la posizione, ma normalizzata per avere una grandezza di 1. L'effetto permette colori unici per ogni vertice, ma in questo programma ogni vertice si ottiene lo stesso colore, che è (0, 0.5, 1) e la stessa proprietà backColor del (0.5, 0.5, 0.5), che è il colore da utilizzare se il visualizzatore si affaccia il retro di una superficie.

Il GouraudShadingEffect richiede anche più proprietà degli effetti. Deve essere possibile impostare il colore della luce ambiente, luce direzionale di colore e la direzione del vettore della luce direzionale. Il GouraudShadingEffect trasferisce tutti questi valori a un buffer più grande costante per il vertex shader. Il vertex shader si è mostrato Figura 2.

Figura 2 per Gouraud Shading Vertex Shader

// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 normal : NORMAL;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Constant buffer provided by effect.
cbuffer VertexShaderConstantBuffer : register(b1)
{
  float4x4 modelMatrix;
  float4x4 viewMatrix;
  float4x4 projectionMatrix;
  float4 ambientLight;
  float4 directionalLight;
  float4 lightDirection;
};
// Called for each vertex.
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Get the input vertex, and include a W coordinate
  float4 pos = float4(input.position.xyz, 1.0f);
  // Pass through the resultant scene space output value
  //  (not necessary -- can be removed from both shaders)
  output.sceneSpaceOutput = pos;
  // Apply transforms to that vertex
  pos = mul(pos, modelMatrix);
  pos = mul(pos, viewMatrix);
  pos = mul(pos, projectionMatrix);
  // The result is clip space output
  output.clipSpaceOutput = pos;
  // Apply model transform to normal
  float4 normal = float4(input.normal, 0);
  normal = mul(normal, modelMatrix);
  // Find angle between light and normal
  float3 lightDir = normalize(lightDirection.xyz);
  float cosine = -dot(normal.xyz, lightDir);
  cosine = max(cosine, 0);
  // Apply view transform to normal
  normal = mul(normal, viewMatrix);
  // Check if normal pointing at viewer
  if (normal.z > 0)
  {
    output.color = (ambientLight.xyz + cosine *
                    directionalLight.xyz) * input.color;
  }
  else
  {
    output.color = input.backColor;
  }
  return output;
}

Pixel shader è lo stesso per quanto riguarda la RotatingTriangleEffect ed è mostrato Figura 3. L'interpolazione dei colori vertice sopra l'intero triangolo avviene dietro le quinte tra vertex shader e pixel shader, così il pixel shader passa semplicemente il colore per essere visualizzato.

Figura 3 Pixel Shader per Gouraud Shading

// Per-pixel data input to the pixel shader
struct PixelShaderInput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Called for each pixel
float4 main(PixelShaderInput input) : SV_TARGET
{
  // Simply return color with opacity of 1
  return float4(input.color, 1);
}

Il risultato è mostrato Figura 4, questa volta su Windows Phone 8.1 anziché 8.1 di Windows. La soluzione di ShadedCircularText è stata creata in Visual Studio con il nuovo modello di App universale e può essere compilata per ogni piattaforma. Tutto il codice è condiviso tra le due piattaforme, fatta eccezione per le classi di App e DirectXPage. La differenza tra i layout dei due programmi suggerisce perché avere definizioni diverse della pagina è spesso una buona idea, anche se le funzionalità del programma è fondamentalmente lo stesso.

la visualizzazione del Gouraud Shading Model
Figura 4 la visualizzazione del Gouraud Shading Model

Come potete vedere, la figura è più leggera nella sua area in alto a sinistra, chiaramente mostrando l'effetto di luce direzionale e favoreggiamento nell'illusione di un aspetto arrotondato alla superficie.

I miglioramenti di Phong

Gouraud shading è una tecnica antica, ma ha un difetto fondamentale: Gouraud shading, la quantità di luce direzionale riflettuto nel centro di un triangolo è un valore interpolato della luce riflettuto ai vertici. La luce riflettuta ai vertici si basa sul coseno dell'angolo tra la direzione di luce e le normali a quei vertici.

Ma la luce riflettuta nel centro del triangolo veramente dovrebbe essere basata sulla normale alla superficie in quel punto. In altre parole, non dovrebbero essere di interpolazione dei colori sopra il triangolo; invece, le normali dovrebbero essere interpolate sopra la superficie del triangolo, e la luce riflessa è calcolato per ogni pixel basato su quella normale.

Immettere vietnamita-nato informatico Pui Tuong Phong (1942-1975), che morì di leucemia all'età di 32 anni. Nella sua tesi di dottorato del 1973, Phong descritto un algoritmo alquanto diversa ombreggiatura. Piuttosto che interpola i colori vertice sopra il triangolo, le normali di vertice sono interpolate sul triangolo, e quindi la luce riflessa viene calcolata da quelli.

In senso pratico, Phong shading richiede il calcolo della luce riflessa da spostare dal vertex shader di pixel shader, insieme con la sezione del buffer costante dedicata a quel lavoro. Questo aumenta la quantità di elaborazione immensamente per pixel, ma, fortunatamente, è stato fatto sulla GPU dove speri che non sembra fare molta differenza.

Per il modello di Phong shading vertex shader è mostrato Figura 5. Alcuni dei dati di input — quali il colore e il colore posteriore — sono semplicemente passata su pixel shader. Ma è ancora utile applicare tutte le trasformazioni. La trasformazione del mondo e due trasformazioni di fotocamera devono essere applicati per le posizioni, mentre due normali vengono calcolate anche — uno con solo la trasformazione di modello per la luce riflessa e un altro con la trasformazione della visualizzazione per determinare se una superficie affaccia verso o dal visualizzatore.

Figura 5 Vertex Shader per il Phong Shading Model

// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 normal : NORMAL;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 normalModel : NORMAL0;
  float3 normalView : NORMAL1;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Constant buffer provided by effect
cbuffer VertexShaderConstantBuffer : register(b1)
{
  float4x4 modelMatrix;
  float4x4 viewMatrix;
  float4x4 projectionMatrix;
};
// Called for each vertex
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Get the input vertex, and include a W coordinate
  float4 pos = float4(input.position.xyz, 1.0f);
  // Pass through the resultant scene space output value
  // (not necessary — can be removed from both shaders)
  output.sceneSpaceOutput = pos;
  // Apply transforms to that vertex
  pos = mul(pos, modelMatrix);
  pos = mul(pos, viewMatrix);
  pos = mul(pos, projectionMatrix);
  // The result is clip space output
  output.clipSpaceOutput = pos;
  // Apply model transform to normal
  float4 normal = float4(input.normal, 0);
  normal = mul(normal, modelMatrix);
  output.normalModel = normal.xyz;
  // Apply view transform to normal
  normal = mul(normal, viewMatrix);
  output.normalView = normal.xyz;
  // Transfer colors
  output.color = input.color;
  output.backColor = input.backColor;
  return output;
}

Come l'uscita dal vertex shader diventa ingresso a pixel shader, queste normali sono interpolati sopra la superficie del triangolo. Pixel shader possono poi finire il lavoro calcolando la luce riflessa, come mostrato Figura 6.

Figura 6 Pixel Shader per il Phong Shading Model

// Per-pixel data input to the pixel shader
struct PixelShaderInput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 normalModel : NORMAL0;
  float3 normalView : NORMAL1;
  float3 color : COLOR0;
  float3 backColor : COLOR1;
};
// Constant buffer provided by effect
cbuffer PixelShaderConstantBuffer : register(b0)
{
  float4 ambientLight;
  float4 directionalLight;
  float4 lightDirection;
};
// Called for each pixel
float4 main(PixelShaderInput input) : SV_TARGET
{
  // Find angle between light and normal
  float3 lightDir = normalize(lightDirection.xyz);
  float cosine = -dot(input.normalModel, lightDir);
  cosine = max(cosine, 0);
  float3 color;
  // Check if normal pointing at viewer
  if (input.normalView.z > 0)
  {
    color = (ambientLight.xyz + cosine *
      directionalLight.xyz) * input.color;
  }
  else
  {
    color = input.backColor;
  }
  // Return color with opacity of 1
  return float4(color, 1);
}

Tuttavia, non ho intenzione di mostrarvi uno screenshot del risultato. È praticamente visivamente identico per il Gouraud shading. Gouraud shading è davvero una buona approssimazione.

Speculari

La reale importanza di Phong shading è che rende possibili altre caratteristiche che fanno affidamento su una normale superficie più accurata.

In questo articolo, finora hai visto ombreggiatura appropriata per superfici diffuse. Queste sono le superfici che sono piuttosto ruvido e opaco e che tendono a diffondere la luce riflettuta dalle loro superfici.

Una superficie che è un po' lucida riflette la luce in modo un po' diverso. Se una superficie è inclinata luce direzionale, solo così potrebbe rimbalzare e andare dritto all'occhio dello spettatore. Questo è solitamente percepito come luce bianca, ed è conosciuto come una luce speculare. Si può vedere l'effetto piuttosto esagerata in Figura 7. Se la figura aveva curve più nitide, la luce bianca sarebbe più localizzata.

la visualizzazione evidenziazione speculare
Figura 7 la visualizzazione evidenziazione speculare

Ottenere questo effetto sembra in un primo momento come se potrebbe essere complessa, ma è solo poche righe di codice nel pixel shader. Questa particolare tecnica è stata sviluppata da maven di grafica NASA Jim Blinn (b. 1949).

Abbiamo prima bisogno di un vettore che indica la direzione che sta guardando lo spettatore della scena 3D. Questo è molto facile, perché la trasformazione della visualizzazione telecamera ha regolato tutte le coordinate quindi lo spettatore è bello perpendicolare all'asse Z:

float3 viewVector = float3(0, 0, -1);

Successivamente, calcolare il vettore che è a metà strada tra quel vettore di vista e la direzione della luce:

float3 halfway = -normalize(viewVector + lightDirection.xyz);

Si noti il segno negativo. Questo rende il vettoriale puntare in direzione opposta, a metà strada tra la fonte della luce e lo spettatore.

Se un triangolo particolare contiene una normale alla superficie che corrisponde esattamente con questo vettore a metà, significa che luce è rimbalzando sulla superficie direttamente nell'occhio dello spettatore. Questo si traduce in massima evidenziazione speculare.

Minore evidenziando risultati da angoli diversi da zero fra il vettore a metà e la normale alla superficie. Questa è un'altra applicazione per il coseno tra i due vettori, che è lo stesso come il prodotto scalare se i due vettori sono normalizzati:

float dotProduct = max(0.0f, dot(input.normalView, halfway));

Questo valore di dotProduct varia da 1 per l'evidenziazione speculare massima quando l'angolo tra due vettori è 0, 0 per nessuna evidenziazione speculare, che si verifica quando i due vettori sono perpendicolari.

Tuttavia, l'evidenziazione speculare non dovrebbe essere visibile per tutti gli angoli tra 0 e 90 gradi. Esso deve essere localizzata. Esso dovrebbe esistere solo per angoli molto piccoli tra quei due vettori. Avete bisogno di una funzione che non influenzerà un prodotto di puntino di 1, ma causerà valori inferiore a 1 a diventare molto più bassa. Questa è la funzione pow:

float specularLuminance = pow(dotProduct, 20);

Questa funzione pow prende il prodotto di puntino alla potenza XX. Se il prodotto scalare è 1, la funzione pow restituisce 1. Se il prodotto scalare è 0,7 (che deriva da un angolo di 45 gradi tra i due vettori), quindi la funzione pow restituisce 0.0008, che effettivamente è 0 per quanto va di illuminazione. Utilizzare valori superiori esponente per rendere l'effetto ancora più localizzato.

Ora tutto ciò che è necessario è quello di moltiplicare questo fattore per il colore della luce direzionale e aggiungerlo al colore già calcolato dalla luce ambiente e direzionale di luce:

color += specularLuminance * directionalLight.xyz;

Che crea una spruzzata di luce bianca, come l'animazione si trasforma la figura.

Addio

E con questo, si chiude la colonna fattore di DirectX. Questo tuffo in DirectX è stato uno dei lavori più impegnativi della mia carriera, ma di conseguenza anche uno dei più gratificante, e spero di avere la possibilità un giorno di ritornare a questa potente tecnologia.


Charles Petzold è un collaboratore di lunga data di MSDN Magazine e autore di "Programmazione Windows, 6a edizione" (Microsoft Press, 2013), un libro sulla scrittura di applicazioni per Windows 8. Il suo sito Web è charlespetzold.com.

Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: Doug Erickson

Questo numero segna di Charles Petzold ultima come editorialista regolare di MSDN Magazine. Charles lascia per unirsi al team a Novell, un fornitore leader di strumenti multipiattaforma sfruttando Microsoft .NET Framework. Charles è stato associato con MSDN Magazine per decenni ed è autore di numerose colonne regolari, comprese le fondazioni, UI Frontiers e fattore di DirectX. Gli auguriamo bene sul suoi nuovi sforzi.