Schreiben von HLSL-Shadern in Direct3D 9

Vertex-Shader Grundlagen

Im Betrieb ersetzt ein programmierbarer Vertex-Shader die Vertexverarbeitung durch die Microsoft Direct3D-Grafikpipeline. Bei Verwendung eines Vertex-Shaders werden Zustandsinformationen zu Transformations- und Beleuchtungsvorgängen von der festen Funktionspipeline ignoriert. Wenn der Scheitelpunktshader deaktiviert ist und eine feste Funktionsverarbeitung zurückgegeben wird, gelten alle aktuellen Statuseinstellungen.

Die Tessellation von Grundtypen hoher Ordnung sollte vor der Ausführung des Vertex-Shaders erfolgen. Implementierungen, die Oberflächentessellation nach der Shaderverarbeitung durchführen, müssen dies auf eine Weise tun, die für die Anwendung und den Shadercode nicht ersichtlich ist.

Mindestens muss ein Vertex-Shader die Vertexposition im homogenen Clipraum ausgeben. Optional kann der Vertex-Shader Texturkoordinaten, Vertexfarbe, Vertexbeleuchtung, Nebelfaktoren usw. ausgeben.

Pixel-Shader Grundlagen

Die Pixelverarbeitung wird von Pixel-Shadern auf einzelnen Pixeln ausgeführt. Pixel-Shader arbeiten mit Vertex-Shadern zusammen; die Ausgabe eines Vertex-Shaders stellt die Eingaben für einen Pixel-Shader bereit. Andere Pixelvorgänge (Nebelmischung, Schablonenvorgänge und Renderzielmischung) treten nach der Ausführung des Shaders auf.

Texturstufe und Samplerzustände

Ein Pixel-Shader ersetzt vollständig die Pixelmischungsfunktion, die durch den Multitexturmixer angegeben wird, einschließlich Vorgängen, die zuvor durch die Texturstufenzustände definiert wurden. Texture Sampling- und Filtervorgänge, die von den Standardmäßigen Texturstufenzuständen für Minifizierung, Vergrößerung, MIP-Filterung und die Wrap-Adressierungsmodi gesteuert wurden, können in Shadern initialisiert werden. Die Anwendung kann diese Zustände ändern, ohne dass der aktuell gebundene Shader neu generiert werden muss. Das Festlegen des Zustands kann noch einfacher werden, wenn Ihre Shader innerhalb eines Effekts entworfen werden.

Pixel-Shader-Eingaben

Bei Pixel-Shaderversionen ps_1_1 - ps_2_0 werden diffuse und spiegelförmige Farben im Bereich von 0 bis 1 gesättigt (geklemmt), bevor sie durch den Shader verwendet werden.

Farbwerte, die in den Pixel-Shader eingegeben werden, werden als perspektivisch korrekt angenommen, aber dies ist nicht garantiert (für alle Hardware). Farben, die aus Texturkoordinaten abgetastet werden, werden perspektivisch korrekt durchlaufen und während der Iteration an den Bereich von 0 bis 1 geklemmt.

Pixel-Shaderausgaben

Bei Pixel-Shaderversionen ps_1_1 - ps_1_4 ist das vom Pixel-Shader ausgegebene Ergebnis der Inhalt des Registers r0. Was er enthält, wenn der Shader die Verarbeitung abgeschlossen hat, wird an die Nebelphase und den Renderzielmixer gesendet.

Bei Pixel-Shaderversionen ps_2_0 und höher wird die Ausgabefarbe von oC0 - oC4 ausgegeben.

Shadereingaben und Shadervariablen

Deklarieren von Shadervariablen

Die einfachste Variablendeklaration enthält einen Typ und einen Variablennamen, z. B. diese Gleitkommadeklaration:

float fVar;

Sie können eine Variable in derselben Anweisung initialisieren.

float fVar = 3.1f;

Ein Array von Variablen kann deklariert werden,

int iVar[3];

oder in derselben Anweisung deklariert und initialisiert.

int iVar[3] = {1,2,3};

Im Folgenden finden Sie einige Deklarationen, die viele Der Merkmale von HLSL-Variablen (High-Level Shader Language) veranschaulichen:

float4 color;
uniform float4 position : POSITION; 
const float4 lightDirection = {0,0,1};

Datendeklarationen können jeden gültigen Typ verwenden, einschließlich:

Ein Shader kann Variablen, Argumente und Funktionen der obersten Ebene aufweisen.

// top-level variable
float globalShaderVariable; 

// top-level function
void function(
in float4 position: POSITION0 // top-level argument
              )
{
  float localShaderVariable; // local variable
  function2(...)
}

void function2()
{
  ...
}

Variablen der obersten Ebene werden außerhalb aller Funktionen deklariert. Argumente der obersten Ebene sind Parameter für eine Funktion der obersten Ebene. Eine Funktion der obersten Ebene ist eine beliebige Funktion, die von der Anwendung aufgerufen wird (im Gegensatz zu einer Funktion, die von einer anderen Funktion aufgerufen wird).

Uniform Shader-Eingaben

Vertex- und Pixel-Shader akzeptieren zwei Arten von Eingabedaten: variierend und einheitlich. Die unterschiedliche Eingabe sind die Daten, die für jede Ausführung des Shaders eindeutig sind. Bei einem Vertex-Shader stammen die unterschiedlichen Daten (z. B. Position, Normal usw.) aus den Vertexstreams. Die einheitlichen Daten (z. B. Materialfarbe, Welttransformation usw.) sind für mehrere Ausführungen eines Shaders konstant. Für diejenigen, die mit den Assembly-Shadermodellen vertraut sind, werden einheitliche Daten durch Konstantenregister und unterschiedliche Daten durch die v- und t-Register angegeben.

Einheitliche Daten können mit zwei Methoden angegeben werden. Die häufigste Methode besteht darin, globale Variablen zu deklarieren und in einem Shader zu verwenden. Jede Verwendung globaler Variablen in einem Shader führt dazu, dass diese Variable der Liste der für diesen Shader erforderlichen uniformen Variablen hinzugefügt wird. Die zweite Methode besteht darin, einen Eingabeparameter der Shaderfunktion der obersten Ebene als einheitlich zu markieren. Diese Markierung gibt an, dass die angegebene Variable der Liste der einheitlichen Variablen hinzugefügt werden soll.

Einheitliche Variablen, die von einem Shader verwendet werden, werden über die Konstantentabelle an die Anwendung zurück kommuniziert. Die Konstantetabelle ist der Name für die Symboltabelle, die definiert, wie die von einem Shader verwendeten uniformen Variablen in die Konstantenregister passen. Die einheitlichen Funktionsparameter werden im Gegensatz zu den globalen Variablen in der Konstantentabelle mit einem Dollarzeichen ($) angezeigt. Das Dollarzeichen ist erforderlich, um Namenskonflikte zwischen lokalen einheitlichen Eingaben und globalen Variablen mit demselben Namen zu vermeiden.

Die Konstantetabelle enthält die Konstantenregisterpositionen aller vom Shader verwendeten uniformen Variablen. Die Tabelle enthält auch die Typinformationen und den Standardwert, sofern angegeben.

Variierende Shadereingaben und Semantik

Unterschiedliche Eingabeparameter (einer Shaderfunktion der obersten Ebene) müssen entweder mit einer semantischen oder einer einheitlichen Schlüsselwort (keyword) gekennzeichnet werden, die angibt, dass der Wert für die Ausführung des Shaders konstant ist. Wenn eine Shadereingabe der obersten Ebene nicht mit einer semantischen oder einheitlichen Schlüsselwort (keyword) gekennzeichnet ist, kann der Shader nicht kompiliert werden.

Die Eingabesemantik ist ein Name, der verwendet wird, um die angegebene Eingabe mit einer Ausgabe des vorherigen Teils der Grafikpipeline zu verknüpfen. Beispielsweise wird die Eingabesemantik POSITION0 von den Vertex-Shadern verwendet, um anzugeben, wo die Positionsdaten aus dem Vertexpuffer verknüpft werden sollen.

Pixel- und Vertex-Shader weisen aufgrund der verschiedenen Teile der Grafikpipeline, die in jede Shadereinheit einfließen, unterschiedliche Mengen von Eingabesemantiken auf. Vertex-Shader-Eingabesemantik beschreibt die Pro-Vertex-Informationen (z. B. Position, Normal, Texturkoordinaten, Farbe, Tangente, Binormal usw.), die aus einem Vertexpuffer in ein Formular geladen werden sollen, das vom Vertex-Shader verwendet werden kann. Die Eingabesemantik wird direkt der Vertexdeklarationsverwendung und dem Verwendungsindex zugeordnet.

Pixel-Shadereingabesemantik beschreibt die Informationen, die pro Pixel von der Rasterungseinheit bereitgestellt werden. Die Daten werden durch Interpolieren zwischen Ausgaben des Vertex-Shaders für jeden Scheitelpunkt des aktuellen Grundtyps generiert. Die grundlegende Pixel-Shadereingabesemantik verknüpft die Informationen zur Ausgabefarbe und Texturkoordinate mit Eingabeparametern.

Eingabesemantik kann Shadereingaben mit zwei Methoden zugewiesen werden:

  • Anfügen eines Doppelpunkts und des semantischen Namens an die Parameterdeklaration.
  • Definieren einer Eingabestruktur mit Eingabesemantik, die jedem Strukturmember zugewiesen ist.

Vertex- und Pixel-Shader stellen Ausgabedaten für die nachfolgende Grafikpipelinephase bereit. Ausgabesemantik wird verwendet, um anzugeben, wie vom Shader generierte Daten mit den Eingaben der nächsten Phase verknüpft werden sollen. Beispielsweise werden die Ausgabesemantik für einen Vertex-Shader verwendet, um die Ausgaben der Interpolatoren im Rasterizer zu verknüpfen, um die Eingabedaten für den Pixel-Shader zu generieren. Bei den Pixel-Shaderausgaben handelt es sich um die Werte, die für die Alphamischungseinheit für jedes Renderziel oder den in den Tiefenpuffer geschriebenen Tiefenwert bereitgestellt werden.

Vertex-Shaderausgabesemantik wird verwendet, um den Shader sowohl mit dem Pixel-Shader als auch mit der Rasterisierungsphase zu verknüpfen. Ein Vertex-Shader, der vom Rasterisierer verwendet wird und nicht dem Pixel-Shader verfügbar gemacht wird, muss mindestens Positionsdaten generieren. Vertex-Shader, die Texturkoordinaten- und Farbdaten generieren, stellen diese Daten nach der Interpolation für einen Pixel-Shader bereit.

Pixel-Shaderausgabesemantik bindet die Ausgabefarben eines Pixel-Shaders an das richtige Renderziel. Die Ausgabefarbe des Pixel-Shaders ist mit der Alpha-Blend-Phase verknüpft, die bestimmt, wie die Zielrenderziele geändert werden. Die Ausgabe der Pixel-Shadertiefe kann verwendet werden, um die Zieltiefewerte an der aktuellen Rasterposition zu ändern. Die Tiefenausgabe und mehrere Renderziele werden nur bei einigen Shadermodellen unterstützt.

Die Syntax für Ausgabesemantik ist identisch mit der Syntax zum Angeben der Eingabesemantik. Die Semantik kann entweder direkt für Parameter angegeben werden, die als "Out"-Parameter deklariert sind, oder während der Definition einer Struktur zugewiesen werden, die entweder als "out"-Parameter oder als Rückgabewert einer Funktion zurückgegeben wird.

Semantik gibt an, wo daten stammen. Semantik sind optionale Bezeichner, die Shadereingaben und -ausgaben identifizieren. Semantik wird an einer von drei Stellen angezeigt:

  • Nach einem Strukturmember.
  • Nach einem Argument in der Eingabeargumentliste einer Funktion.
  • Nach der Eingabeargumentliste der Funktion.

In diesem Beispiel wird eine Struktur verwendet, um einen oder mehrere Vertex-Shadereingaben bereitzustellen, und eine andere Struktur, um mindestens eine Vertex-Shaderausgabe bereitzustellen. Jedes der Strukturmember verwendet eine Semantik.

vector vClr;

struct VS_INPUT
{
    float4 vPosition : POSITION;
    float3 vNormal : NORMAL;
    float4 vBlendWeights : BLENDWEIGHT;
};

struct VS_OUTPUT
{
    float4  vPosition : POSITION;
    float4  vDiffuse : COLOR;

};

float4x4 mWld1;
float4x4 mWld2;
float4x4 mWld3;
float4x4 mWld4;

float Len;
float4 vLight;

float4x4 mTot;

VS_OUTPUT VS_Skinning_Example(const VS_INPUT v, uniform float len=100)
{
    VS_OUTPUT out;

    // Skin position (to world space)
    float3 vPosition = 
        mul(v.vPosition, (float4x3) mWld1) * v.vBlendWeights.x +
        mul(v.vPosition, (float4x3) mWld2) * v.vBlendWeights.y +
        mul(v.vPosition, (float4x3) mWld3) * v.vBlendWeights.z +
        mul(v.vPosition, (float4x3) mWld4) * v.vBlendWeights.w;
    // Skin normal (to world space)
    float3 vNormal =
        mul(v.vNormal, (float3x3) mWld1) * v.vBlendWeights.x + 
        mul(v.vNormal, (float3x3) mWld2) * v.vBlendWeights.y + 
        mul(v.vNormal, (float3x3) mWld3) * v.vBlendWeights.z + 
        mul(v.vNormal, (float3x3) mWld4) * v.vBlendWeights.w;
    
    // Output stuff
    out.vPosition    = mul(float4(vPosition + vNormal * Len, 1), mTot);
    out.vDiffuse  = dot(vLight,vNormal);

    return out;
}

Die Eingabestruktur identifiziert die Daten aus dem Vertexpuffer, der die Shadereingaben bereitstellt. Dieser Shader ordnet die Daten aus den Positions-, Normal- und Mischgewichtselementen des Vertexpuffers den Vertex-Shaderregistern zu. Der Eingabedatentyp muss nicht genau mit dem Vertexdeklarationsdatentyp übereinstimmen. Wenn sie nicht genau übereinstimmen, werden die Scheitelpunktdaten automatisch in den HLSL-Datentyp konvertiert, wenn sie in die Shaderregister geschrieben werden. Wenn für instance die normalen Daten vom Typ UINT von der Anwendung definiert wurden, werden sie beim Lesen durch den Shader in eine float3 konvertiert.

Wenn die Daten im Vertexstream weniger Komponenten als der entsprechende Shaderdatentyp enthalten, werden die fehlenden Komponenten mit 0 initialisiert (mit Ausnahme von w, das mit 1 initialisiert wird).

Eingabesemantik ähnelt den Werten in D3DDECLUSAGE.

Die Ausgabestruktur identifiziert die Vertex-Shaderausgabeparameter von Position und Farbe. Diese Ausgaben werden von der Pipeline für die Dreiecksrasterung (in der primitiven Verarbeitung) verwendet. Die als Positionsdaten markierte Ausgabe gibt die Position eines Scheitelpunkts im homogenen Raum an. Mindestens muss ein Vertex-Shader Positionsdaten generieren. Die Position des Bildschirmraums wird berechnet, nachdem der Vertex-Shader abgeschlossen ist, indem die Koordinate (x, y, z) durch w dividiert wird. Im Bildschirmbereich sind -1 und 1 die minimalen und maximalen x- und y-Werte der Grenzen des Viewports, während z für z-Puffertests verwendet wird.

Die Ausgabesemantik ähnelt auch den Werten in D3DDECLUSAGE. Im Allgemeinen kann eine Ausgabestruktur für einen Vertex-Shader auch als Eingabestruktur für einen Pixel-Shader verwendet werden, sofern der Pixel-Shader keine Variable liest, die mit der Position, Punktgröße oder Nebelsemantik gekennzeichnet ist. Diese Semantik sind skalaren Werten pro Vertex zugeordnet, die nicht von einem Pixel-Shader verwendet werden. Wenn diese Werte für den Pixel-Shader benötigt werden, können sie in eine andere Ausgabevariable kopiert werden, die eine Pixel-Shadersemantik verwendet.

Globale Variablen werden Registern automatisch vom Compiler zugewiesen. Globale Variablen werden auch als einheitliche Parameter bezeichnet, da der Inhalt der Variablen für alle Pixel identisch ist, die jedes Mal verarbeitet werden, wenn der Shader aufgerufen wird. Die Register sind in der Konstantentabelle enthalten, die mithilfe der ID3DXConstantTable-Schnittstelle gelesen werden kann.

Eingabesemantik für Pixel-Shader ordnen Werte bestimmten Hardwareregistern für den Transport zwischen Vertex-Shadern und Pixel-Shadern zu. Jeder Registertyp verfügt über bestimmte Eigenschaften. Da es derzeit nur zwei Semantiken für Farb- und Texturkoordinaten gibt, ist es üblich, dass die meisten Daten als Texturkoordinate markiert werden, auch wenn dies nicht der Grund ist.

Beachten Sie, dass die Vertex-Shaderausgabestruktur eine Eingabe mit Positionsdaten verwendet hat, die vom Pixel-Shader nicht verwendet wird. HLSL lässt gültige Ausgabedaten eines Vertex-Shaders zu, bei dem es sich um ungültige Eingabedaten für einen Pixel-Shader handelt, sofern nicht im Pixel-Shader darauf verwiesen wird.

Eingabeargumente können auch Arrays sein. Semantik wird vom Compiler für jedes Element des Arrays automatisch erhöht. Berücksichtigen Sie für instance die folgende explizite Deklaration:

struct VS_OUTPUT
{
    float4 Position   : POSITION;
    float3 Diffuse    : COLOR0;
    float3 Specular   : COLOR1;               
    float3 HalfVector : TEXCOORD3;
    float3 Fresnel    : TEXCOORD2;               
    float3 Reflection : TEXCOORD0;               
    float3 NoiseCoord : TEXCOORD1;               
};

float4 Sparkle(VS_OUTPUT In) : COLOR

Die oben angegebene explizite Deklaration entspricht der folgenden Deklaration, die vom Compiler automatisch inkrementiert wird:

float4 Sparkle(float4 Position : POSITION,
                 float3 Col[2] : COLOR0,
                 float3 Tex[4] : TEXCOORD0) : COLOR0
{
   // shader statements
   ...

Genau wie die Eingabesemantik identifiziert die Ausgabesemantik die Datenverwendung für Pixel-Shaderausgabedaten. Viele Pixel-Shader schreiben nur in eine Ausgabefarbe. Pixel-Shader können auch einen Tiefenwert gleichzeitig in ein oder mehrere Renderziele schreiben (bis zu vier). Wie Vertex-Shader verwenden Pixel-Shader eine Struktur, um mehr als eine Ausgabe zurückzugeben. Dieser Shader schreibt 0 in die Farbkomponenten sowie in die Tiefenkomponente.

struct PS_OUTPUT
{
    float4 Color[4] : COLOR0;
    float  Depth  : DEPTH;
};

PS_OUTPUT main(void)
{
    PS_OUTPUT out;

   // Shader statements
   ...

  // Write up to four pixel shader output colors
  out.Color[0] =  ...
  out.Color[1] =  ...
  out.Color[2] =  ...
  out.Color[3] =  ...

  // Write pixel depth 
  out.Depth =  ...

    return out;
}

Die Ausgabefarben des Pixel-Shaders müssen vom Typ float4 sein. Beim Schreiben mehrerer Farben müssen alle Ausgabefarben zusammenhängend verwendet werden. Mit anderen Worten: COLOR1 kann keine Ausgabe sein, es sei denn , COLOR0 wurde bereits geschrieben. Die Ausgabe der Pixel-Shadertiefe muss vom Typ float1 sein.

Sampler und Texturobjekte

Ein Sampler enthält den Samplerzustand. Der Samplerzustand gibt die Textur an, die stichprobeniert werden soll, und steuert die Filterung, die während der Stichprobenentnahme erfolgt. Drei Dinge sind erforderlich, um eine Textur zu testen:

  • Eine Textur
  • Ein Sampler (mit Samplerstatus)
  • Eine Samplinganweisung

Sampler können wie hier gezeigt mit Texturen und Samplerzustand initialisiert werden:

sampler s = sampler_state 
{ 
  texture = NULL; 
  mipfilter = LINEAR; 
};

Hier sehen Sie ein Beispiel für den Code zum Beispiel für eine 2D-Textur:

texture tex0;
sampler2D s_2D;

float2 sample_2D(float2 tex : TEXCOORD0) : COLOR
{
  return tex2D(s_2D, tex);
}

Die Textur wird mit einer Texturvariable tex0 deklariert.

In diesem Beispiel wird eine Samplervariable namens s_2D deklariert. Der Sampler enthält den Samplerzustand in geschweiften Klammern. Dies umfasst die Textur, die stichprobeniert wird, und optional den Filterzustand (d. h. Umbruchmodi, Filtermodi usw.). Wenn der Samplerzustand ausgelassen wird, wird ein Standard-Samplerzustand angewendet, der die lineare Filterung und einen Umbruchmodus für die Texturkoordinaten angibt. Die Samplerfunktion übernimmt eine Gleitkommakoordinate mit zwei Komponenten und gibt eine Zweikomponentenfarbe zurück. Dies wird mit dem Float2-Rückgabetyp dargestellt und stellt Daten in den roten und grünen Komponenten dar.

Vier Typen von Samplern werden definiert (siehe Schlüsselwörter) und Textursuche werden von den intrinsischen Funktionen ausgeführt: tex1D(s, t) (DirectX HLSL),tex2D(s, t) (DirectX HLSL), tex3D(s, t) (DirectX HLSL), texCUBE(s, t) (DirectX HLSL)). Hier sehen Sie ein Beispiel für die 3D-Stichprobenerstellung:

texture tex0;
sampler3D s_3D;

float3 sample_3D(float3 tex : TEXCOORD0) : COLOR
{
  return tex3D(s_3D, tex);
}

Diese Samplerdeklaration verwendet den Standard-Samplerzustand für die Filtereinstellungen und den Adressmodus.

Hier sehen Sie das entsprechende Beispiel für das Cubesampling:

texture tex0;
samplerCUBE s_CUBE;

float3 sample_CUBE(float3 tex : TEXCOORD0) : COLOR
{
  return texCUBE(s_CUBE, tex);
}

Und schließlich sehen Sie hier das Beispiel für die 1D-Stichprobenentnahme:

texture tex0;
sampler1D s_1D;

float sample_1D(float tex : TEXCOORD0) : COLOR
{
  return tex1D(s_1D, tex);
}

Da die Runtime keine 1D-Texturen unterstützt, verwendet der Compiler eine 2D-Textur mit dem Wissen, dass die y-Koordinate unwichtig ist. Da tex1D(s, t) (DirectX HLSL) als 2D-Textursuche implementiert ist, kann der Compiler die y-Komponente effizient auswählen. In einigen seltenen Szenarien kann der Compiler keine effiziente y-Komponente auswählen. In diesem Fall gibt er eine Warnung aus.

texture tex0;
sampler s_1D_float;

float4 main(float texCoords : TEXCOORD) : COLOR
{
    return tex1D(s_1D_float, texCoords);
}

Dieses beispiel ist ineffizient, da der Compiler die Eingabekoordinate in ein anderes Register verschieben muss (da ein 1D-Lookup als 2D-Lookup implementiert und die Texturkoordinate als float1 deklariert wird). Wenn der Code mithilfe einer float2-Eingabe anstelle von float1 neu geschrieben wird, kann der Compiler die Eingabetexturkoordinate verwenden, da er weiß, dass y für etwas initialisiert ist.

texture tex0;
sampler s_1D_float2;

float4 main(float2 texCoords : TEXCOORD) : COLOR
{
    return tex1D(s_1D_float2, texCoords);
}

Alle Textursuchen können mit "bias" oder "proj" (d. h. tex2Dbias (DirectX HLSL), texCUBEproj (DirectX HLSL)) angefügt werden. Mit dem Suffix "proj" wird die Texturkoordinate durch die w-Komponente geteilt. Bei "Bias" wird die mip-Ebene durch die w-Komponente verschoben. Daher nehmen alle Textur-Lookups mit einem Suffix immer eine float4-Eingabe an. tex1D(s, t) (DirectX HLSL) und tex2D(s, t) (DirectX HLSL) ignorieren die yz- bzw. z-Komponenten.

Sampler können auch im Array verwendet werden, obwohl derzeit kein Back-End den dynamischen Arrayzugriff von Samplern unterstützt. Daher ist Folgendes gültig, da es zur Kompilierzeit aufgelöst werden kann:

tex2D(s[0],tex)

Dieses Beispiel ist jedoch ungültig.

tex2D(s[a],tex)

Der dynamische Zugriff auf Sampler ist in erster Linie für das Schreiben von Programmen mit Literalschleifen nützlich. Der folgende Code veranschaulicht den Zugriff auf Samplerarrays:

sampler sm[4];

float4 main(float4 tex[4] : TEXCOORD) : COLOR
{
    float4 retColor = 1;

    for(int i = 0; i < 4;i++)
    {
        retColor *= tex2D(sm[i],tex[i]);
    }

    return retColor;
}

Hinweis

Die Verwendung der Microsoft Direct3D-Debugruntime kann Ihnen helfen, Unstimmigkeiten zwischen der Anzahl der Komponenten in einer Textur und einem Sampler zu erkennen.

 

Schreibfunktionen

Funktionen unterteilen große Aufgaben in kleinere Aufgaben. Kleine Aufgaben sind einfacher zu debuggen und können wiederverwendet werden, sobald sie sich bewährt haben. Funktionen können verwendet werden, um Details anderer Funktionen auszublenden, wodurch ein Programm, das aus Funktionen besteht, einfacher zu verfolgen ist.

HLSL-Funktionen ähneln C-Funktionen auf verschiedene Weise: Beide enthalten eine Definition und einen Funktionstext, und sie deklarieren rückgabetypen und Argumentlisten. Wie C-Funktionen führt die HLSL-Überprüfung die Typüberprüfung für die Argumente, Argumenttypen und den Rückgabewert während der Shaderkompilierung durch.

Im Gegensatz zu C-Funktionen verwenden HLSL-Einstiegspunktfunktionen Semantik, um Funktionsargumente an Shaderein- und -ausgaben zu binden (HLSL-Funktionen, die intern Semantik ignorieren). Dies erleichtert das Binden von Pufferdaten an einen Shader und das Binden von Shaderausgaben an Shadereingaben.

Eine Funktion enthält eine Deklaration und einen Textkörper, und die Deklaration muss dem Text vorangestellt werden.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
    return mul(inPos, WorldViewProj );
};

Die Funktionsdeklaration enthält alles vor den geschweiften Klammern:

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION

Eine Funktionsdeklaration enthält Folgendes:

  • Ein Rückgabetyp
  • Ein Funktionsname
  • Eine Argumentliste (optional)
  • Ausgabesemantik (optional)
  • Eine Anmerkung (optional)

Der Rückgabetyp kann jeder der grundlegenden HLSL-Datentypen sein, z. B. float4:

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
   ...
}

Der Rückgabetyp kann eine struktur sein, die bereits definiert wurde:

struct VS_OUTPUT
{
    float4  vPosition        : POSITION;
    float4  vDiffuse         : COLOR;
}; 

VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
   ...
}

Wenn die Funktion keinen Wert zurückgibt, kann void als Rückgabetyp verwendet werden.

void VertexShader_Tutorial_1(float4 inPos : POSITION )
{
   ...
}

Der Rückgabetyp wird immer zuerst in einer Funktionsdeklaration angezeigt.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION

Eine Argumentliste deklariert die Eingabeargumente für eine Funktion. Es kann auch Werte deklarieren, die zurückgegeben werden. Einige Argumente sind sowohl Eingabe- als auch Ausgabeargumente. Hier sehen Sie ein Beispiel für einen Shader, der vier Eingabeargumente akzeptiert.

float4 Light(float3 LightDir : TEXCOORD1, 
             uniform float4 LightColor,  
             float2 texcrd : TEXCOORD0, 
             uniform sampler samp) : COLOR 
{
    float3 Normal = tex2D(samp,texcrd);

    return dot((Normal*2 - 1), LightDir)*LightColor;
}

Diese Funktion gibt eine endgültige Farbe zurück, d. h. eine Mischung aus einem Texturbeispiel und der Hellen Farbe. Die Funktion akzeptiert vier Eingaben. Zwei Eingaben verfügen über Semantik: LightDir hat die TEXCOORD1-Semantik und texcrd die TEXCOORD0-Semantik . Die Semantik bedeutet, dass die Daten für diese Variablen aus dem Vertexpuffer stammen. Obwohl die LightDir-Variable über eine TEXCOORD1-Semantik verfügt , ist der Parameter wahrscheinlich keine Texturkoordinate. Der semantische Typ TEXCOORDn wird häufig verwendet, um eine Semantik für einen Typ anzugeben, der nicht vordefiniert ist (es gibt keine Vertexshader-Eingabesemantik für eine Lichtrichtung).

Die anderen beiden Eingänge LightColor und samp sind mit dem einheitlichen Schlüsselwort (keyword) beschriftet. Hierbei handelt es sich um einheitliche Konstanten, die sich zwischen Denkaufrufen nicht ändern. Die Werte für diese Parameter stammen aus globalen Shadervariablen.

Argumente können als Eingaben mit in Schlüsselwort (keyword) und Ausgabeargumente mit dem out-Schlüsselwort (keyword) bezeichnet werden. Argumente können nicht als Verweis übergeben werden. Ein Argument kann jedoch sowohl eine Eingabe als auch eine Ausgabe sein, wenn es mit dem inout-Schlüsselwort (keyword) deklariert wird. An eine Funktion übergebene Argumente, die mit dem inout-Schlüsselwort (keyword) gekennzeichnet sind, gelten als Kopien des Originals, bis die Funktion zurückgegeben wird, und sie werden zurück kopiert. Hier sehen Sie ein Beispiel für die Verwendung von inout:

void Increment_ByVal(inout float A, inout float B) 
{ 
    A++; B++;
}

Diese Funktion inkrementiert die Werte in A und B und gibt sie zurück.

Der Funktionstext ist der gesamte Code nach der Funktionsdeklaration.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
    return mul(inPos, WorldViewProj );
};

Der Text besteht aus Anweisungen, die von geschweiften Klammern umgeben sind. Der Funktionstext implementiert alle Funktionen mithilfe von Variablen, Literalen, Ausdrücken und Anweisungen.

Der Shadertext führt zwei Dinge aus: Er führt eine Matrix multipliziert aus und gibt ein float4-Ergebnis zurück. Die Matrixmultiplikation wird mit der Funktion mul (DirectX HLSL) erreicht, die eine 4x4-Matrix multipliziert. mul (DirectX HLSL) wird als systeminterne Funktion bezeichnet, da sie bereits in die HLSL-Bibliothek von Funktionen integriert ist. Systeminterne Funktionen werden im nächsten Abschnitt ausführlicher behandelt.

Die Matrix multiplizieren kombiniert einen Eingabevektor Pos und eine zusammengesetzte Matrix WorldViewProj. Das Ergebnis sind Positionsdaten, die in den Bildschirmbereich transformiert werden. Dies ist die minimale Vertex-Shaderverarbeitung, die wir durchführen können. Wenn wir die Pipeline für feste Funktionen anstelle eines Vertexshaders verwenden, könnten die Vertexdaten nach dieser Transformation gezeichnet werden.

Die letzte Anweisung in einem Funktionstext ist eine return-Anweisung. Genau wie C gibt diese Anweisung die Steuerung von der Funktion an die Anweisung zurück, die die Funktion aufgerufen hat.

Funktionsrückgabetypen können jeder der einfachen Datentypen sein, die in HLSL definiert sind, einschließlich bool, int half, float und double. Rückgabetypen können einer der komplexen Datentypen sein, z. B. Vektoren und Matrizen. HLSL-Typen, die auf Objekte verweisen, können nicht als Rückgabetypen verwendet werden. Dazu gehören Pixelshader, Vertexshader, Texture und Sampler.

Hier sehen Sie ein Beispiel für eine Funktion, die eine -Struktur für einen Rückgabetyp verwendet.

float4x4 WorldViewProj : WORLDVIEWPROJ;

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
};

VS_OUTPUT VS_HLL_Example(float4 inPos : POSITION )
{
    VS_OUTPUT Out;

    Out.Pos = mul(inPos,  WorldViewProj );

    return Out;
};

Der float4-Rückgabetyp wurde durch die Struktur VS_OUTPUT ersetzt, die jetzt ein einzelnes float4-Element enthält.

Eine return-Anweisung signalisiert das Ende einer Funktion. Dies ist die einfachste return-Anweisung. Es gibt die Steuerung von der Funktion an das aufrufende Programm zurück. Es wird kein Wert zurückgegeben.

void main()
{
    return ;
}

Eine return-Anweisung kann einen oder mehrere Werte zurückgeben. In diesem Beispiel wird ein Literalwert zurückgegeben:

float main( float input : COLOR0) : COLOR0
{
    return 0;
}

In diesem Beispiel wird das Skalarergebnis eines Ausdrucks zurückgegeben:

return  light.enabled;

In diesem Beispiel wird ein float4-Objekt zurückgegeben, das aus einer lokalen Variablen und einem Literal erstellt wurde:

return  float4(color.rgb, 1) ;

In diesem Beispiel wird ein float4-Wert zurückgegeben, der aus dem von einer systeminternen Funktion zurückgegebenen Ergebnis erstellt wird, sowie einige Literalwerte:

float4 func(float2 a: POSITION): COLOR
{
    return float4(sin(length(a) * 100.0) * 0.5 + 0.5, sin(a.y * 50.0), 0, 1);
}

In diesem Beispiel wird eine Struktur zurückgegeben, die mindestens ein Element enthält:

float4x4 WorldViewProj;

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
};

VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
    VS_OUTPUT out;
    out.Pos = mul(inPos, WorldViewProj );
    return out;
};

Flusssteuerung

Die meisten aktuellen Vertex- und Pixelshaderhardware sind für die Zeilenausführung eines Shaders konzipiert, wobei jede Anweisung einmal ausgeführt wird. HLSL unterstützt die Flusssteuerung, einschließlich statischer Verzweigung, prädizierter Anweisungen, statischer Schleifen, dynamischer Verzweigung und dynamischer Schleifen.

Zuvor führte die Verwendung einer if-Anweisung zu Assemblysprache-Shadercode, der sowohl die If-Seite als auch die else-Seite des Codeflows implementiert. Hier sehen Sie ein Beispiel für den in HLSL-Code, der für vs_1_1 kompiliert wurde:

if (Value > 0)
    oPos = Value1; 
else
    oPos = Value2; 

Und hier ist der resultierende Assemblycode:

// Calculate linear interpolation value in r0.w
mov r1.w, c2.x               
slt r0.w, c3.x, r1.w         
// Linear interpolation between value1 and value2
mov r7, -c1                      
add r2, r7, c0                   
mad oPos, r0.w, r2, c1  

Einige Hardware ermöglicht entweder statische oder dynamische Schleifen, aber die meisten erfordern eine lineare Ausführung. Bei Modellen, die keine Schleifen unterstützen, müssen alle Schleifen aufgehoben werden. Ein Beispiel hierfür ist das DepthOfField-Beispiel , das nichtrollte Schleifen auch für ps_1_1 Shader verwendet.

HLSL bietet jetzt Unterstützung für jede dieser Arten von Flusssteuerung:

  • Statische Verzweigung
  • prädizierte Anweisungen
  • Statische Schleife
  • Dynamische Verzweigung
  • Dynamische Schleifen

Statische Verzweigung ermöglicht das Ein- oder Ausschalten von Shadercodeblöcken basierend auf einer booleschen Shaderkonstante. Dies ist eine praktische Methode zum Aktivieren oder Deaktivieren von Codepfaden basierend auf dem Typ des derzeit gerenderten Objekts. Zwischen Draw-Aufrufen können Sie entscheiden, welche Features Sie mit dem aktuellen Shader unterstützen möchten, und dann die booleschen Flags festlegen, die zum Abrufen dieses Verhaltens erforderlich sind. Alle Anweisungen, die von einer booleschen Konstante deaktiviert werden, werden während der Shaderausführung übersprungen.

Die bekannteste Verzweigungsunterstützung ist das dynamische Verzweigen. Bei dynamischer Verzweigung befindet sich die Vergleichsbedingung in einer Variablen, was bedeutet, dass der Vergleich für jeden Scheitelpunkt oder jedes Pixel zur Laufzeit durchgeführt wird (im Gegensatz zum Vergleich zur Kompilierzeit oder zwischen zwei Zeichnungsaufrufen). Der Leistungstreffer sind die Kosten der Verzweigung plus die Kosten für die Anweisungen auf der Seite des Branchs. Dynamische Verzweigung wird im Shadermodell 3 oder höher implementiert. Das Optimieren von Shadern, die mit diesen Modellen arbeiten, ähnelt der Optimierung von Code, der auf einer CPU ausgeführt wird.

Programmieranleitung für HLSL