www.rolandk.de
- Aktuelle Themen zu .Net -
Achtung: Hier handelt es sich um meine alte Seite.
Die aktuelle ist unter folgendem Link erreichbar: www.rolandk.de/wp/
Home Tutorials DirectX 11 Basics Chapter 4: Texturen




















































Chapter 4: Texturen
Sonntag, den 28. Oktober 2012 um 09:01 Uhr

 

 

Textur ist ungleich Textur

In diesem Kapitel beschäftigen wir uns mit Texturen. Aber, was sind Texturen eigentlich? Im Prinzip nichts anderes als normale Bitmaps, wie wir sie auch aus der 2D-Programmierung kennen. Ich muss mich korrigieren, eine 2D-Textur ist nichts anderes als eine normale Bitmap. Unter DirectX 11 gibt es nämlich auch 1D- und 3D-Texturen. Der Unterschied zur normalen 2D-Textur liegt lediglich in der Dimension. Während eine 1D-Textur nur X Komponenten lang ist, ist eine 2D-Textur X Komponenten lang und Y Komponenten breit. Eine 3D-Textur natürlich dementsprechend X Komponenten lang, Y Komponenten breit und Z Komponenten hoch. Folgendes Bild stellt die verschiedenen Arten etwas deutlicher dar:

 

 

Nach genauerer Betrachtung der Bilder sieht man, dass die Koordinatenachsen nicht X, Y und Z heißen, sondern U, V und W. Das ist kein versehen, es handelt sich dabei um die gängigen Bezeichnungen der Achsen einer Textur. Für uns relevant sind erst mal nur die 2D-Texturen.

 

Der Shader

Mit der Codierung beginnen wir wie in den anderen Kapiteln auch mit dem Shader. Als Grundlage dient uns der Shader des letzten Kapitels. Eine Textur wird zunächst als Konstante im Shader angelegt:

  1. Texture2D Texture;

Bei dieser Zeile fällt sofort auf, dass die Semtantic fehlt. Diese brauchen wir nicht, da die Grafikkarte aufgrund des Typs Texture2D schon alles weiß, was sie braucht. Alles? Ne, nicht ganz. Wir brauchen noch einen SamplerState. Dieser sagt der Grafikkarte, wie auf die Textur zugegriffen werden soll. So wird darin beispielsweise der Texturfilter festgelegt:

  1. SamplerState stateLinear
  2. {
  3. Filter = MIN_MAG_MIP_LINEAR;
  4. AddressU = Wrap;
  5. AddressV = Wrap;
  6. };

Wie muss man diese Zeilen überhaupt lesen? Im Prinzip verhält sich ein SamplerState wie eine Konstante. SamplerState ist dabei der Typ und stateLinear der Name. Die 3 Zeilen zwischen den geschweiften Klammern bilden den Inhalt. Beim Filter handelt es sich um den verwendeten Texturfilter, AddressU und AddressV legen fest, wie die Texturkoordinaten verarbeitet werden. 

Weiter geht es mit der Struktur VS_IN. Wir erinnern uns: Dabei handelt es sich um die Struktur der Vertices, die vom C# Programm an die Grafikkarte übergeben werden. Den Farbwert aus den vorherigen Tutorials brauchen wir nicht mehr, stattdessen brauchen wir jetzt Texturkoordinaten:

  1. struct VS_IN
  2. {
  3. float3 pos : POSITION;
  4. float2 tex : TEXCOORD;
  5. };

Texturkoordinaten werden durch einen 2D-Vektor abgebildet. Wie genau diese zu füllen sind, sehen wir später im C# Coding. Die selbe Änderung müssen wir auch noch an der PS_IN Struktur durchführen:

  1. struct PS_IN
  2. {
  3. float4 pos : SV_POSITION;
  4. float2 tex : TEXCOORD0;
  5. };

An der Umrechnung der Koordinaten im VertexShader ändert sich selbstverständlich nichts. Eine Kleinigkeit müssen wir aber trotzdem auch hier überarbeiten. Da wir in den Strukturen die Farbe durch eine Texturkoordinate ausgetauscht haben, müssen wir im VertexShader diese auch weiterreichen:

  1. PS_IN VS(VS_IN input)
  2. {
  3. PS_IN output = (PS_IN)0;
  4.  
  5. output.pos = mul(float4(input.pos.xyz, 1.0), WorldViewProj);
  6. output.tex = input.tex;
  7.  
  8. return output;
  9. }

Die eigentliche Verarbeitung der Texturkoordinaten findet schließlich im PixelShader statt. Hier wird auch unser weiter oben angelegter SamplerState erstmals verwendet:

  1. float4 PS( PS_IN input ) : SV_Target
  2. {
  3. return Texture.Sample(stateLinear, input.tex);
  4. }

Das sieht jetzt auf den ersten Blick etwas komplizierter aus, daher eine kurze Erklärung: Es wird die Sample Methode der Konstante Texture (enthält unsere Textur) aufgerufen. Der erste Übergabeparameter ist der verwendete SamplerState, der Zweite ist die Texturkoordinate, welche wir vom VertexShader bekommen haben. Kurz gesagt, für jeden Pixel wir hier dessen Farbe aus der Textur mithilfe der Koordinaten ermittelt.

 

Anpassungen im 3D-Objekt

Entgegen der Überschrift kümmern wir uns erst einmal nicht direkt ums 3D-Objekt, sondern zunächst um die Vertex Struktur. Wie gewohnt müssen wir diese so ändern, damit sie zum Shader passt.

  1. [StructLayout(LayoutKind.Sequential)]
  2. public struct Vertex
  3. {
  4. public Vector3 Position;
  5. public Vector2 TexCoord;
  6.  
  7. public Vertex(Vector3 position, Vector2 texCoord)
  8. {
  9. this.Position = position;
  10. this.TexCoord = texCoord;
  11. }
  12. }

Die Anpassungen in der Klasse SimpleBox halten sich diesmal in Grenzen. Zuerst benötigen wir 3 neue Membervariablen:

  1. private DX11.Texture2D m_texture;
  2. private DX11.EffectResourceVariable m_resourceVariable;
  3. private DX11.ShaderResourceView m_textureView;

Den Typ der ersten Variable haben wir bereits im ersten Kapitel kennengelernt. Kurz gesagt handelt es sich dabei um unsere Textur. Die anderen beiden kennen wir zwar noch nicht, aber dessen Bedeutung kann man vielleicht schon vermuten. Die EffectResourceVariable ist unsere Schnittstelle zur Konstante Texture im Shader - also fast das Selbe, wie die EffectMatrixVariable im letzten Kapitel. Die ShaderResourceView ist sowas Ähnliches wie die RenderTargetView, die wir im Hauptfenster verwenden. Diese Variable benötigen wir, um die Textur an den Shader zu binden. 

Die nächste Änderung betrifft die Vertices. Da wir die Vertex Struktur ändern mussten, müssen wir die Vertices in der LoadResources Methode natürlich auch etwas anders erzeugen:

  1. Vertex[] vertices = new Vertex[]
  2. {
  3. new Vertex(new Vector3(-1, -1, -1), new Vector2(0f,0f)),
  4. new Vertex(new Vector3(1, -1, -1), new Vector2(1f, 0f)),
  5. new Vertex(new Vector3(1, -1, 1), new Vector2(0f, 0f)),
  6. new Vertex(new Vector3(-1, -1, 1), new Vector2(1f, 0f)),
  7. new Vertex(new Vector3(-1, 1, -1), new Vector2(0f, 1f)),
  8. new Vertex(new Vector3(1,1,-1), new Vector2(1f, 1f)),
  9. new Vertex(new Vector3(1,1,1), new Vector2(0f, 1f)),
  10. new Vertex(new Vector3(-1, 1,1), new Vector2(1f, 1f)),
  11.  
  12. new Vertex(new Vector3(-1, -1, -1), new Vector2(0f,0f)),
  13. new Vertex(new Vector3(1, -1, -1), new Vector2(1f, 0f)),
  14. new Vertex(new Vector3(1, -1, 1), new Vector2(1f, 1f)),
  15. new Vertex(new Vector3(-1, -1, 1), new Vector2(0f, 1f)),
  16. new Vertex(new Vector3(-1, 1, -1), new Vector2(0f, 0f)),
  17. new Vertex(new Vector3(1,1,-1), new Vector2(1f, 0f)),
  18. new Vertex(new Vector3(1,1,1), new Vector2(1f, 1f)),
  19. new Vertex(new Vector3(-1, 1,1), new Vector2(0f, 1f))
  20. };

Wie man sieht, werden jetzt Texturkoordinaten anstatt Farben übergeben. Aber warum sind die Koordinaten immer 0 oder 1? Das liegt daran, dass bei einer Textur die obere linke Ecke der Koordinate 0,0 und die untere rechte Ecke der Koordinate 1,1 entspricht. Der Vorteil dieses Mappings ist, dass es unabhängig von der Größe der Textur ist. Man kann also beispielsweise eine 64x64 Pixel große Textur durch eine Textur mit 128x128 Pixel austauschen, ohne die Koordinaten ändern zu müssen. 

Weiter geht es ein bisschen weiter unten in derselben Methode, oder genauer gesagt dort, wo wir den Shader laden. Da wir jetzt im Shader 2 Konstanten haben, müssen wir uns noch eine Schnittstelle für die 2. erzeugen:

  1. m_resourceVariable = m_effect.GetVariableByName("Texture").AsResource();

Ist doch der Zeile mit der EffectMatrixVariable sehr ähnlich, oder? Die einzigen Unterschiede sind der andere Name der Konstante und das "AsResource" anstelle von "AsMatrix". 

Als nächstes müssen wir aus bekanntem Grund die Stelle mit dem InputLayout etwas abändern. Dieser Abschnitt sieht dann so aus:

  1. DX11.InputElement[] inputElements = new DX11.InputElement[]
  2. {
  3. new DX11.InputElement("POSITION",0,SlimDX.DXGI.Format.R32G32B32_Float,0,0),
  4. new DX11.InputElement("TEXCOORD",0,SlimDX.DXGI.Format.R32G32_Float,12,0)
  5. };

Wie gehabt verändert sich nicht wirklich viel. Statt einem Format.R8G8B8A8_UNorm (Farbe) übergeben wir ein Format.R32G32_Float (Texturkoordinate). Wie wir sehen, ist eine Änderung an einer Vertex-Struktur zwar nicht schwierig, man muss aber an viele Kleinigkeiten denken, damit danach auch alles funktioniert. 

Was fehlt denn jetzt überhaupt noch? Natürlich, die Textur. Eine Textur zu laden ist in Direct3D prinzipiell nicht schwer. Man kann Texturen beispielsweise händisch erstellen, also quasi alle Bytes aus einer Bilddatei auswerten und die Textur entsprechend aufbauen. Ein viel einfacherer Weg ist es allerdings, diese Arbeit an Direct3D weiterzugeben. Solange man Standardformate (z. B. png, jpg, bmp) verwendet, kann man die Methode Texture2D.FromFile verwenden. In der Praxis sieht das dann so aus:

  1. m_texture = DX11.Texture2D.FromFile(device, "Textures/Texture.png");
  2. m_textureView = new DX11.ShaderResourceView(device, m_texture);

Diese beiden Zeilen machen alles was wir brauchen. In der Ersten wird eine Textur geladen (kann gerne auch in einem anderen Ordner liegen) und in der Zweiten wird eine View für den Shader erzeugt. Das View-Objekt müssen wir in der Rendermethode der Textur-Konstante im Shader zuweisen. 

Weiter geht es mit der Render-Methode. Aufgrund des nach wie vor geringen Umfangs dieser Funktion zeige ich sie hier in einem Rutsch an:

  1. DX11.DeviceContext deviceContext = device.ImmediateContext;
  2.  
  3. deviceContext.InputAssembler.InputLayout = m_vertexLayout;
  4. deviceContext.InputAssembler.PrimitiveTopology = DX11.PrimitiveTopology.TriangleList;
  5. deviceContext.InputAssembler.SetIndexBuffer(m_indexBuffer, DXGI.Format.R16_UInt, 0);
  6. deviceContext.InputAssembler.SetVertexBuffers(
  7. 0,
  8. new DX11.VertexBufferBinding(m_vertexBuffer, Marshal.SizeOf(typeof(Vertex)), 0));
  9.  
  10. m_transformVariable.SetMatrix(world * viewProj);
  11. m_resourceVariable.SetResource(m_textureView);
  12.  
  13. m_effectPass.Apply(deviceContext);
  14. deviceContext.DrawIndexed(36, 0, 0);

Ein kurzer Blick auf diesen Codeausschnitt zeigt auch schon alles, was wir brauchen. Neu ist lediglich eine einzige Zeile, und zwar m_resourceVariable.SetResource(m_textureView). Was diese Zeile macht? Im Prinzip passiert das Gleiche wie eine Zeile weiter oben, nur dass hier anstatt einer Matrix eine Textur übergeben wird.

br /

 

Kommentar hinzufügen

Ihr Name:
Kommentar: