Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Es ist an der Zeit, zu erfahren, wie Sie mit Shadern und Shaderressourcen bei der Entwicklung Ihres Microsoft DirectX-Spiels für Windows 8 arbeiten. Wir haben gesehen, wie Sie das Grafikgerät und die Grafikressourcen einrichten, und vielleicht haben Sie sogar mit dem Ändern der Pipeline begonnen. Sehen wir uns nun Pixel- und Vertex-Shader an.
Wenn Sie mit Shadersprachen nicht vertraut sind, ist eine kurze Diskussion in Ordnung. Shader sind kleine Programme auf niedriger Ebene, die kompiliert und in bestimmten Phasen der Grafikpipeline ausgeführt werden. Ihre Spezialität ist sehr schnelle mathematische Gleitkommaoperationen. Die gängigsten Shaderprogramme sind:
- Vertex-Shader – Ausgeführt für jeden Scheitelpunkt in einer Szene. Dieser Shader arbeitet mit Vertexpufferelementen, die von der aufrufenden App bereitgestellt werden, und führt minimal zu einem 4-Komponenten-Positionsvektor, der in eine Pixelposition gerastert wird.
- Pixelshader – Wird für jedes Pixel in einem Renderziel ausgeführt. Dieser Shader empfängt gerasterte Koordinaten aus früheren Shaderphasen (in den einfachsten Pipelines, dies wäre der Vertex-Shader) und gibt eine Farbe (oder einen anderen 4-Komponenten-Wert) für diese Pixelposition zurück, die dann in ein Renderziel geschrieben wird.
Dieses Beispiel enthält sehr einfache Vertex- und Pixelshader, die nur Geometrie zeichnen, und komplexere Shader, die einfache Beleuchtungsberechnungen hinzufügen.
Shaderprogramme werden in der Microsoft High Level Shader Language (HLSL) geschrieben. DIE HLSL-Syntax sieht ähnlich wie C aus, aber ohne die Zeiger. Shaderprogramme müssen sehr kompakt und effizient sein. Wenn ihr Shader zu viele Anweisungen kompiliert, kann er nicht ausgeführt werden, und es wird ein Fehler zurückgegeben. (Beachten Sie, dass die genaue Anzahl der zulässigen Anweisungen Teil der Direct3D-Featureebene ist.)
In Direct3D werden Shader zur Laufzeit nicht kompiliert. sie werden kompiliert, wenn der Rest des Programms kompiliert wird. Wenn Sie Ihre App mit Microsoft Visual Studio 2013 kompilieren, werden die HLSL-Dateien in CSO-Dateien (.cso) kompiliert, die Ihre App vor dem Zeichnen im GPU-Speicher laden und platzieren muss. Stellen Sie sicher, dass Sie diese CSO-Dateien beim Verpacken in Ihre App einschließen. sie sind Objekte wie Gitter und Texturen.
Grundlegendes zur HLSL-Semantik
Es ist wichtig, sich einen Moment Zeit zu nehmen, um die HLSL-Semantik zu besprechen, bevor wir fortfahren, da sie häufig ein Verwirrungspunkt für neue Direct3D-Entwickler sind. HLSL-Semantik sind Zeichenfolgen, die einen Wert identifizieren, der zwischen der App und einem Shaderprogramm übergeben wird. Obwohl sie eine Vielzahl möglicher Zeichenfolgen sein können, empfiehlt es sich, eine Zeichenfolge wie POSITION eine Zeichenfolge zu verwenden oder COLOR die die Verwendung angibt. Sie weisen diese Semantik zu, wenn Sie einen Konstantenpuffer oder ein Eingabelayout erstellen. Sie können auch eine Zahl zwischen 0 und 7 an die Semantik anfügen, sodass Sie separate Register für ähnliche Werte verwenden. Beispiel: COLOR0, COLOR1, COLOR2...
Semantik, die dem Präfix "SV_" vorangestellt ist, sind systemwertsemantische Semantik, die von Ihrem Shaderprogramm geschrieben werden; Ihr Spiel selbst (auf der CPU ausgeführt) kann sie nicht ändern. In der Regel enthalten diese Semantik Werte, die Eingaben oder Ausgaben aus einer anderen Shaderstufe in der Grafikpipeline sind oder vollständig von der GPU generiert werden.
Darüber hinaus weisen Semantik unterschiedliche Verhaltensweisen auf, SV_ wenn sie zum Angeben von Eingaben oder Ausgaben aus einer Shaderstufe verwendet werden. (Ausgabe) enthält z SV_POSITION . B. die während der Vertex-Shaderphase transformierten Vertexdaten und SV_POSITION (Eingabe) die Pixelpositionswerte, die von der GPU während der Rasterungsphase interpoliert wurden.
Nachfolgend finden Sie einige allgemeine HLSL-Semantik:
-
POSITION(n) für Vertexpufferdaten.SV_POSITIONstellt eine Pixelposition für den Pixelshader bereit und kann nicht von Ihrem Spiel geschrieben werden. -
NORMAL(n) für normale Daten, die vom Vertexpuffer bereitgestellt werden. -
TEXCOORD(n) für Textur-UV-Koordinatendaten, die für einen Shader bereitgestellt werden. -
COLOR(n) für RGBA-Farbdaten, die für einen Shader bereitgestellt werden. Beachten Sie, dass sie identisch mit Koordinatendaten behandelt wird, einschließlich der Interpolierung des Werts während der Rasterung; Mit der Semantik können Sie einfach erkennen, dass es sich um Farbdaten handelt. -
SV_Target[n] zum Schreiben aus einem Pixelshader in eine Zieltextur oder einen anderen Pixelpuffer.
Wir werden einige Beispiele für HLSL-Semantik sehen, während wir das Beispiel überprüfen.
Lesen aus den Konstantenpuffern
Jeder Shader kann aus einem Konstantenpuffer lesen, wenn dieser Puffer als Ressource an die Phase angefügt ist. In diesem Beispiel wird nur dem Vertex-Shader ein Konstantenpuffer zugewiesen.
Der Konstantenpuffer wird an zwei Stellen deklariert: im C++-Code und in den entsprechenden HLSL-Dateien, die darauf zugreifen.
Hier erfahren Sie, wie die Konstantenpufferstruktur im C++-Code deklariert wird.
typedef struct _constantBufferStruct {
DirectX::XMFLOAT4X4 world;
DirectX::XMFLOAT4X4 view;
DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;
Stellen Sie beim Deklarieren der Struktur für den Konstantenpuffer im C++-Code sicher, dass alle Daten ordnungsgemäß entlang der Grenzen von 16 Byte ausgerichtet sind. Die einfachste Möglichkeit hierfür ist die Verwendung von DirectXMath-Typen wie XMFLOAT4 oder XMFLOAT4X4, wie im Beispielcode gezeigt. Sie können auch vor falsch ausgerichteten Puffern schützen, indem Sie eine statische Assertion deklarieren:
// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");
Diese Codezeile verursacht zur Kompilierungszeit einen Fehler, wenn ConstantBufferStruct nicht 16 Byte ausgerichtet ist. Weitere Informationen zur Ausrichtung und Verpackung von Konstantenpuffern finden Sie unter Packregeln für Konstantenvariablen.
Hier erfahren Sie, wie der Konstantenpuffer im Vertex-Shader-HLSL deklariert wird.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix mWorld; // world matrix for object
matrix View; // view matrix
matrix Projection; // projection matrix
};
Alle Puffer – Konstante, Textur, Sampler oder andere – müssen über ein Register verfügen, damit die GPU darauf zugreifen kann. Jede Shaderstufe ermöglicht bis zu 15 Konstantenpuffer, und jeder Puffer kann bis zu 4.096 Konstantenvariablen enthalten. Die Syntax der Registerverwendungsdeklaration lautet wie folgt:
- b*#*: Ein Register für einen Konstantenpuffer (cbuffer).
- t*#*: Ein Register für einen Texturpuffer (tbuffer).
- s*#*: Ein Register für einen Sampler. (Ein Sampler definiert das Nachschlageverhalten für Texel in der Texturressource.)
Beispielsweise kann die HLSL für einen Pixelshader eine Textur und einen Sampler als Eingabe mit einer Deklaration wie dieser verwenden.
Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);
Es liegt an Ihnen, konstanten Puffern zu registrieren – wenn Sie die Pipeline einrichten, fügen Sie einen Konstantenpuffer an denselben Steckplatz an, dem Sie sie in der HLSL-Datei zugewiesen haben. Im vorherigen Thema gibt der Aufruf von VSSetConstantBuffers beispielsweise "0" für den ersten Parameter an. Dies weist Direct3D an, die Konstantenpufferressource anzufügen, um 0 zu registrieren, was der Zuordnung des Puffers zum Registrieren(b0) in der HLSL-Datei entspricht.
Lesen aus den Vertexpuffern
Der Vertexpuffer stellt die Dreiecksdaten für die Szenenobjekte für die Vertex-Shader bereit. Wie bei dem Konstantenpuffer wird die Vertexpufferstruktur im C++-Code mit ähnlichen Packregeln deklariert.
typedef struct _vertexPositionColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 color;
} VertexPositionColor;
Es gibt kein Standardformat für Vertexdaten in Direct3D 11. Stattdessen definieren wir unser eigenes Vertexdatenlayout mit einem Deskriptor; die Datenfelder werden mithilfe eines Arrays von D3D11_INPUT_ELEMENT_DESC Strukturen definiert. Hier zeigen wir ein einfaches Eingabelayout, das das gleiche Vertexformat wie die vorhergehende Struktur beschreibt:
D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayout
);
Wenn Sie beim Ändern des Beispielcodes Daten zum Vertexformat hinzufügen, müssen Sie auch das Eingabelayout aktualisieren, oder der Shader kann es nicht interpretieren. Sie können das Vertexlayout wie folgt ändern:
typedef struct _vertexPositionColorTangent
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;
In diesem Fall ändern Sie die Eingabelayoutdefinition wie folgt.
D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayoutExtended
);
Jeder der Eingabelayoutelementdefinitionen wird eine Zeichenfolge vorangestellt, z. B. "POSITION" oder "NORMAL", d. h. die Semantik, die wir weiter oben in diesem Thema behandelt haben. Es ist wie ein Handle, mit dem die GPU dieses Element bei der Verarbeitung des Scheitelpunkts identifizieren kann. Wählen Sie allgemeine, aussagekräftige Namen für Ihre Vertexelemente aus.
Genau wie beim Konstantenpuffer verfügt der Vertex-Shader über eine entsprechende Pufferdefinition für eingehende Vertexelemente. (Deshalb haben wir beim Erstellen des Eingabelayouts einen Verweis auf die Vertex-Shaderressource bereitgestellt . Direct3D überprüft das Datenlayout pro Vertex mit der Eingabestruktur des Shaders.) Beachten Sie, wie die Semantik zwischen der Eingabelayoutdefinition und dieser HLSL-Pufferdeklaration übereinstimmt.
COLOR Es wurde jedoch ein "0" angefügt. Es ist nicht erforderlich, die 0 hinzuzufügen, wenn sie nur ein COLOR Element im Layout deklariert haben, aber es empfiehlt sich, es anzufügen, falls Sie in Zukunft weitere Farbelemente hinzufügen möchten.
struct VS_INPUT
{
float3 vPos : POSITION;
float3 vColor : COLOR0;
};
Übergeben von Daten zwischen Shadern
Shader verwenden Eingabetypen und geben Ausgabetypen aus ihren Hauptfunktionen bei der Ausführung zurück. Für den im vorherigen Abschnitt definierten Vertex-Shader war der Eingabetyp die VS_INPUT Struktur, und wir haben ein übereinstimmende Eingabelayout und eine C++-Struktur definiert. Ein Array dieser Struktur wird verwendet, um einen Vertexpuffer in der CreateCube-Methode zu erstellen.
Der Vertex-Shader gibt eine PS_INPUT Struktur zurück, die die endgültige Vertexposition der 4-Komponente (float4) minimal enthalten muss. Dieser Positionswert muss die Systemwertsemantik aufweisen, die für ihn deklariert ist, SV_POSITIONdamit die GPU über die Daten verfügt, die sie zum Ausführen des nächsten Zeichnungsschritts benötigt. Beachten Sie, dass keine 1:1-Entsprechung zwischen Vertex-Shaderausgabe und Pixel-Shadereingabe vorhanden ist; Der Vertex-Shader gibt eine Struktur für jeden angegebenen Scheitelpunkt zurück, der Pixelshader wird jedoch einmal für jedes Pixel ausgeführt. Das liegt daran, dass die Daten pro Vertex zuerst die Rasterungsphase durchlaufen. In dieser Phase wird festgelegt, welche Pixel die Geometrie, die Sie zeichnen, interpolierte Daten pro Vertex für jedes Pixel berechnen und dann den Pixelshader einmal für jedes dieser Pixel aufrufen. Interpolation ist das Standardverhalten beim Rastern von Ausgabewerten und ist insbesondere für die korrekte Verarbeitung von Ausgabevektordaten (Lichtvektoren, Per-Vertexnormalen und Tangenten und anderen) unerlässlich.
struct PS_INPUT
{
float4 Position : SV_POSITION; // interpolated vertex position (system value)
float4 Color : COLOR0; // interpolated diffuse color
};
Überprüfen des Vertex-Shaders
Der Beispiel-Vertex-Shader ist sehr einfach: Nehmen Sie einen Scheitelpunkt (Position und Farbe) ein, transformieren Sie die Position von Modellkoordinaten in perspektivische projizierte Koordinaten, und geben Sie ihn (zusammen mit der Farbe) an den Rasterizer zurück. Beachten Sie, dass der Farbwert direkt zusammen mit den Positionsdaten interpoliert wird und für jedes Pixel einen anderen Wert bereitstellt, obwohl der Vertex-Shader keine Berechnungen für den Farbwert ausgeführt hat.
VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
VS_OUTPUT Output;
float4 pos = float4(input.vPos, 1.0f);
// Transform the position from object space to homogeneous projection space
pos = mul(pos, mWorld);
pos = mul(pos, View);
pos = mul(pos, Projection);
Output.Position = pos;
// Just pass through the color data
Output.Color = float4(input.vColor, 1.0f);
return Output;
}
Ein komplexerer Vertex-Shader, z. B. ein Shader, der die Scheitelpunkte eines Objekts für die Phong-Schattierung einrichte, könnte wie folgt aussehen. In diesem Fall nutzen wir die Tatsache, dass die Vektoren und Normalwerte interpoliert werden, um eine glatt aussehende Oberfläche anzunähern.
// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix model;
matrix view;
matrix projection;
};
cbuffer LightConstantBuffer : register(b1)
{
float4 lightPos;
};
struct VertexShaderInput
{
float3 pos : POSITION;
float3 normal : NORMAL;
};
// Per-pixel color data passed through the pixel shader.
struct PixelShaderInput
{
float4 position : SV_POSITION;
float3 outVec : POSITION0;
float3 outNormal : NORMAL0;
float3 outLightVec : POSITION1;
};
PixelShaderInput main(VertexShaderInput input)
{
// Inefficient -- doing this only for instruction. Normally, you would
// premultiply them on the CPU and place them in the cbuffer.
matrix mvMatrix = mul(model, view);
matrix mvpMatrix = mul(mvMatrix, projection);
PixelShaderInput output;
float4 pos = float4(input.pos, 1.0f);
float4 normal = float4(input.normal, 1.0f);
float4 light = float4(lightPos.xyz, 1.0f);
//
float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);
// Transform the vertex position into projected space.
output.gl_Position = mul(pos, mvpMatrix);
output.outNormal = mul(normal, mvMatrix).xyz;
output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
output.outLightVec = mul(light, mvMatrix).xyz;
return output;
}
Überprüfen des Pixelshaders
Dieser Pixelshader in diesem Beispiel ist möglicherweise die absolute Mindestmenge an Code, die Sie in einem Pixelshader haben können. Es verwendet die interpolierten Pixelfarbdaten, die während der Rasterung generiert werden, und gibt sie als Ausgabe zurück, wo sie in ein Renderziel geschrieben wird. Wie langweilig!
PS_OUTPUT main(PS_INPUT In)
{
PS_OUTPUT Output;
Output.RGBColor = In.Color;
return Output;
}
Der wichtige Teil ist die SV_TARGET Systemwertsemantik für den Rückgabewert. Es gibt an, dass die Ausgabe in das primäre Renderziel geschrieben werden soll, bei dem es sich um den Texturpuffer handelt, der für die Anzeige an die Swapchain übergeben wird. Dies ist für Pixelshader erforderlich – ohne die Farbdaten des Pixelshaders hätte Direct3D nichts anzuzeigen!
Ein Beispiel für einen komplexeren Pixelshader zum Ausführen einer Phong-Schattierung könnte wie folgt aussehen. Da die Vektoren und Normalzahlen interpoliert wurden, müssen sie nicht pro Pixel berechnet werden. Wir müssen sie jedoch aufgrund der Funktionsweise der Interpolation neu normalisieren; Konzeptionell müssen wir den Vektor schrittweise von der Richtung an Vertex A bis hin zur Richtung bei Vertex B drehen, wobei die Länge beibehalten wird, während die Interpolation stattdessen eine gerade Linie zwischen den beiden Vektorendpunkten überschneidet.
cbuffer MaterialConstantBuffer : register(b2)
{
float4 lightColor;
float4 Ka;
float4 Kd;
float4 Ks;
float4 shininess;
};
struct PixelShaderInput
{
float4 position : SV_POSITION;
float3 outVec : POSITION0;
float3 normal : NORMAL0;
float3 light : POSITION1;
};
float4 main(PixelShaderInput input) : SV_TARGET
{
float3 L = normalize(input.light);
float3 V = normalize(input.outVec);
float3 R = normalize(reflect(L, input.normal));
float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
diffuse = saturate(diffuse);
float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
specular = saturate(specular);
float4 finalColor = diffuse + specular;
return finalColor;
}
In einem anderen Beispiel verwendet der Pixelshader eigene Konstantenpuffer, die Licht- und Materialinformationen enthalten. Das Eingabelayout im Vertex-Shader würde erweitert werden, um normale Daten einzuschließen, und die Ausgabe dieses Vertex-Shaders wird erwartet, dass transformierte Vektoren für den Scheitelpunkt, das Licht und die Vertexnormal im Ansichtskoordinatensystem enthalten werden.
Wenn Sie Texturpuffer und Sampler mit zugewiesenen Registern (t bzw. s) haben, können Sie auch im Pixelshader darauf zugreifen.
Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float3 norm : NORMAL;
float2 tex : TEXCOORD0;
};
float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
float3 lightDirection = normalize(float3(1, -1, 0));
float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
return texelColor * lightMagnitude;
}
Shader sind sehr leistungsstarke Tools, mit denen prozedurale Ressourcen wie Schattenkarten oder Rauschtexturen generiert werden können. In der Tat erfordern erweiterte Techniken, dass Sie sich Texturen abstrakter vorstellen, nicht als visuelle Elemente, sondern als Puffer. Sie enthalten Daten wie Höheninformationen oder andere Daten, die im endgültigen Pixelshaderdurchlauf oder in diesem bestimmten Frame als Teil eines mehrstufigen Effektdurchlaufs erfasst werden können. Multi-Sampling ist ein leistungsstarkes Tool und das Rückgrat vieler moderner visueller Effekte.
Nächste Schritte
Hoffentlich sind Sie mit DirectX 11 an diesem Punkt vertraut und sind bereit, mit der Arbeit an Ihrem Projekt zu beginnen. Hier sind einige Links, mit denen Sie andere Fragen zur Entwicklung mit DirectX und C++ beantworten können:
- Entwickeln von Spielen
- Verwenden von Visual Studio-Tools für die DirectX-Spieleprogrammierung
- DirectX-Spieleentwicklung und Beispielexemplarische Vorgehensweisen
- Zusätzliche Ressourcen für die Spieleprogrammierung
Zugehörige Themen