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 DirectX10 Basics Chapter 3: Das erste echte 3D-Objekt




















































Chapter 3: Das erste echte 3D-Objekt
Freitag, den 03. Juli 2009 um 09:40 Uhr

 

 

Koordinaten

Bevor wir anfangen, echte 3D-Objekte zu rendern, müssen wir erst einmal ein paar Dinge darüber wissen. Wie funktioniert das überhaupt mit den Koordinaten in 3D? In 2D ist es immer gleich, die X-Achse geht nach links (horizontale Bildschirmkante) und die Y-Achse nach unten (vertikale Bildschirmkante). In 3D wird im Prinzip bloß eine 3. Achse, die Z-Achse, hinzugefügt. Diese geht "in den Bildschirm hinein". Problem ist aber, dass es in der 3D-Grafik zwei mögliche Koordinatensysteme gibt, das Rechtshändige und das Linkshändige. Glücklicherweise brauchen wir uns darum nicht zu sehr kümmern, da Direct3D grundsätzlich ein Rechtshändiges bevorzugt, doch erwähnt sollte das ganze schon sein. Zum Vergleich der beiden Systeme zeigt folgende Grafik beide Koordinatensysteme:

 

 

Der einzige Unterschied der beiden Systeme ist die Richtung der Z-Achse. Wie in der Grafik abgebildet, kann man sich die Ausrichtung der Achsen an der jeweiligen Hand recht schnell herleiten.

 

Räume

Jetzt kommt die wirklich graue Theorie: Wie werden überhaupt die 3D-Koordinaten in Bildschirmkoordinaten umgerechnet? Im letzten Kapitel haben wir beim Shader gesehen, dass wir uns darum kümmern müssen, darum brauchen wir auch eine Antwort auf diese Frage. Generell ist das Ganze nicht wirklich schwer, der 3D-Vektor wird lediglich mittels einer Transformationsmatrix umgerechnet. Übrigens: Als ich mit der 3D-Programmierung angefangen habe, wusste ich gar nicht, was eine Matrix ist, geschweige denn, was man hier unter Transformation versteht. Es ist also kein Problem, wenn auch Sie mit diesen Begriffen nicht viel anfangen können. Wieder zurück zum Thema: Wir brauchen also eine Transformationsmatrix, doch wo kriegen wir die her? In Direct3D gibt es 3 verschiedene Matrizen, aus denen schließlich die endgültige berechnet wird:

  • World-Matrix: Speichert die Ursprungskoordinaten des aktuellen 3D-Objekts, somit hat jedes 3D-Objekt i. d. R. seine eigene World-Matrix.
  • View-Matrix: Enthält die Position und Rotation der 3D-Kamera.
  • Projection-Matrix: Beinhaltet Projektionsdaten der Kamera. Dabei handelt es sich beispielsweise um den Blickwinkel und die Sichtweite.

Hört sich doch nicht mal so schwer an. In den Codeausschnitten weiter unten wird die Funktionsweise dieser Matrizen nochmal klarer.

 

Der Shader

In der Theorie sind wir damit fit, also machen wir wie im letzten Kapitel mit dem Shader weiter. In diesem Kapitel werden wir den Shader des Vorherigen etwas abändern. Allem voran brauchen wir jetzt im Shader-Code eine Shaderkonstante. Dabei handelt es sich um eine Variable, dessen Wert vom C# Programm gesetzt wird und vom Shader nicht verändert werden kann. In dieser Konstante speichern wir unsere Transformationsmatrix:

  1. float4x4 WorldViewProj : WorldViewProjection;

Die Syntax kennen wir noch von den beiden Strukturen, die wir im letzten Kapitel angelegt haben. Beim Typ float4x4 handelt es sich um eine Matrix mit 4 Zeilen und 4 Spalten. Aus dem Namen der Konstante kann man schon auf den Sinn und Zweck schließen. Es ist die Verschmelzung der World-, View- und Projectionmatrix und wird zur Umrechnung unserer 3D-Koordinaten in Bildschirmkoordinaten verwendet. 

Die nächste Änderung betrifft unsere VS_IN Struktur. Hier kriegen wir jetzt einen Vektor mit 3 Komponenten (X-, Y- und Z-Koordinate):

  1. struct VS_IN
  2. {
  3. float3 pos : POSITION;
  4. float4 col : COLOR0;
  5. };

Im Vertexshader müssen wir schließlich noch die Berechnung durchführen:

  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.col = input.col;
  7.  
  8. return output;
  9. }

Hier hat sich lediglich eine einzige Zeile geändert: Wo wir in der vorherigen Version lediglich die Position weitergereicht haben, wird jetzt eine Transformation mithilfe des Befehls mul durchgeführt. Einfach gesagt wird lediglich die aktuelle 3D-Koordinate mit der WorldViewProj Matrix multipliziert. Ergebnis ist ein Vektor, welcher die Bildschirmkoordinaten enthält. 

Das wars dann auch schon mit dem Shader. Mehr müssen wir hier nicht tun. Zusammenfassend noch einmal der gesamte neue Shadercode:

  1. float4x4 WorldViewProj : WorldViewProjection;
  2.  
  3. struct VS_IN
  4. {
  5. float3 pos : POSITION;
  6. float4 col : COLOR0;
  7. };
  8.  
  9. struct PS_IN
  10. {
  11. float4 pos : SV_POSITION;
  12. float4 col : COLOR0;
  13. };
  14.  
  15. PS_IN VS(VS_IN input)
  16. {
  17. PS_IN output = (PS_IN)0;
  18.  
  19. output.pos = mul(float4(input.pos.xyz, 1.0), WorldViewProj);
  20. output.col = input.col;
  21.  
  22. return output;
  23. }
  24.  
  25. float4 PS( PS_IN input ) : SV_Target
  26. {
  27. return input.col;
  28. }
  29.  
  30. technique10 Render
  31. {
  32. pass P0
  33. {
  34. SetGeometryShader( 0 );
  35. SetVertexShader( CompileShader( vs_4_0, VS() ) );
  36. SetPixelShader( CompileShader( ps_4_0, PS() ) );
  37. }
  38. }

 

Das erste echte 3D-Objekt

Jetzt kümmern wir uns um die C# Seite dieses Beispiels. Im Shadercode haben wir die Datenstruktur verändert, welche die Daten, die von dem C# Programm zur Grafikkarte übergeben werden, beschreibt. Das Gleiche müssen wir jetzt natürlich auch auf C#-Seite machen:

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

Genauso wie im Shader ersetzen wir lediglich den Vektor4 mit einem Vector3, es ändert sich also nicht wirklich viel. Ganz anders sieht es schon am 3D-Objekt aus. 

Für den 3D-Würfel legen wir uns eine neue Klasse "SimpleBox" an. Genauso wie im letzten Beispiel erstellen wir die Methoden LoadResources und Render, jedoch müssen wir ein paar neue Felder deklarieren:

  1. private DX10.Buffer m_vertexBuffer;
  2. private DX10.Buffer m_indexBuffer;
  3. private DX10.InputLayout m_vertexLayout;
  4. private DX10.Effect m_effect;
  5. private DX10.EffectTechnique m_effectTechnique;
  6. private DX10.EffectPass m_effectPass;
  7. private DX10.EffectMatrixVariable m_transformVariable;

Neu sind hier 2 Felder: Zum Einen m_indexBuffer und zum Anderen m_transformVariable. Beim IndexBuffer handelt es sich um eine Folge von Indizes, doch wofür brauchen wir das? Die Antwort ist zwar kurz, aber für den Einsteiger erst mal vielleicht etwas verwirrend: Um Vertices mehrmals zu verwenden. Schauen wir uns doch mal folgendes Bild an:

 

 

Was wir hier sehen, ist ein Rechteck, bei dem nur die Seitenränder der Dreiecke gezeichnet werden. Wenn wir dieses Rechteck so aufbauen, wie wir das Dreieck im letzten Beispiel erstellt haben, bräuchten wir 6 Vertices. Und zwar 3 für das erste Dreieck und 3 für das Zweite. Doch was einem dabei sofort auffällt ist, dass die Vertices 2 und 4 jeweils in beiden Dreiecken verwendet werden. Also ist es nur logisch, diese Vertices auch möglichst für beide Triangles zu nutzen. Genau dazu benötigen wir den IndexBuffer. Er enthält mehrere Indices, welche auf den VertexBuffer zeigen. Für jeden Index wird in der Grafikkarte schließlich ein Vertex erzeugt. Für den 3D-Würfel in diesem Beispiel bedeutet das konkret, dass wir 8 Vertices benötigen und 36 indices. Das Coding dazu befindet sich in der LoadResources Methode uns sieht so aus:

  1. Vertex[] vertices = new Vertex[]
  2. {
  3. new Vertex(new Vector3(-1, -1, -1), Color.Red.ToArgb()),
  4. new Vertex(new Vector3(1, -1, -1), Color.LightBlue.ToArgb()),
  5. new Vertex(new Vector3(1, -1, 1), Color.LightCyan.ToArgb()),
  6. new Vertex(new Vector3(-1, -1, 1), Color.CadetBlue.ToArgb()),
  7. new Vertex(new Vector3(-1, 1, -1), Color.Red.ToArgb()),
  8. new Vertex(new Vector3(1,1,-1), Color.Orange.ToArgb()),
  9. new Vertex(new Vector3(1,1,1), Color.Goldenrod.ToArgb()),
  10. new Vertex(new Vector3(-1, 1,1), Color.Yellow.ToArgb())
  11. };
  12.  
  13. short[] indices = new short[]
  14. {
  15. 4,1,0,4,5,1,
  16. 5,2,1,5,6,2,
  17. 6,3,2,6,7,3,
  18. 7,0,3,7,4,0,
  19. 7,5,4,7,6,5,
  20. 2,3,0,2,0,1
  21. };

Achtung: Zu beachten ist die Reihenfolge der Indices/Vertices. Standardmäßig wird nur die Vorderseite von Dreiecken gerendert. Sieht man also ein Dreieck von hinten, sieht man es gar nicht. Vorder- und Rückseite werden durch die Anordnung der Vertices unterschieden. Sind die Vertices vom Blickwinkel des Betrachters aus im Uhrzeigersinn angeordnet, sieht man dessen Vorderseite. Andersherum handelt es sich um die Rückseite, sobald die Vertices gegen den Uhrzeigersinn hinzugefügt wurden. Grund für dieses Verfahren ist schlicht Performance. Damit wird verhindert, dass Dreiecke komplett durchgerechnet werden, welche man im eigentlichen Bild überhaupt nicht sehen kann. 

Anschließend müssen wir diese Daten noch in für DirectX verständliche Streams schreiben. Wir legen 2 Stück an, einen für den VertexBuffer und einen für den IndexBuffer:

  1. DataStream outStream = new DataStream(8 * Marshal.SizeOf(typeof(Vertex)), true, true);
  2. DataStream outStreamIndex = new DataStream(36 * Marshal.SizeOf(typeof(short)), true, true);
  3.  
  4. outStream.WriteRange(vertices);
  5. outStreamIndex.WriteRange(indices);
  6.  
  7. outStream.Position = 0;
  8. outStreamIndex.Position = 0;
  9.  

Ich denke, auf diesen Code-Ausschnitt brauch ich jetzt nicht näher eingehen. Weiter geht es erst mal wie gehabt. Wir erzeugen uns für beide Buffer jeweils eine BufferDescription. Die des IndexBuffers sieht auch ziemlich gleich aus:

  1. DX10.BufferDescription bufferDescription = new DX10.BufferDescription();
  2. bufferDescription.BindFlags = DX10.BindFlags.VertexBuffer;
  3. bufferDescription.CpuAccessFlags = DX10.CpuAccessFlags.None;
  4. bufferDescription.OptionFlags = DX10.ResourceOptionFlags.None;
  5. bufferDescription.SizeInBytes = 8 * Marshal.SizeOf(typeof(Vertex));
  6. bufferDescription.Usage = DX10.ResourceUsage.Default;
  7.  
  8. DX10.BufferDescription bufferDescriptionIndex = new DX10.BufferDescription();
  9. bufferDescriptionIndex.BindFlags = DX10.BindFlags.IndexBuffer;
  10. bufferDescriptionIndex.CpuAccessFlags = DX10.CpuAccessFlags.None;
  11. bufferDescriptionIndex.OptionFlags = DX10.ResourceOptionFlags.None;
  12. bufferDescriptionIndex.SizeInBytes = 36 * Marshal.SizeOf(typeof(short));
  13. bufferDescriptionIndex.Usage = DX10.ResourceUsage.Default;

Zwischen den beiden BufferDescriptions gibt es lediglich 2 Unterschiede: Zum Einen die Eigenschaft BindFlags, in welcher festgelegt wird, um was es sich überhaupt handelt, zum Anderen natürlich die Anzahl an Bytes. Als nächstes müssen wir die Buffer anlegen:

  1. m_vertexBuffer = new DX10.Buffer(device, outStream, bufferDescription);
  2. m_indexBuffer = new DX10.Buffer(device, outStreamIndex, bufferDescriptionIndex);
  3. outStream.Close();
  4. outStreamIndex.Close();

Nichts weltbewegendes. Dieser Code ist für IndexBuffer und VertexBuffer identisch. Als nächstes laden wir den Shader, wo allerdings eine neue Zeile hinzukommt:

  1. m_effect = DX10.Effect.FromFile(
  2. device,
  3. "Shaders/SimpleRendering.fx",
  4. "fx_4_0",
  5. DX10.ShaderFlags.None,
  6. DX10.EffectFlags.None,
  7. null, null);
  8. m_effectTechnique = m_effect.GetTechniqueByIndex(0);
  9. m_effectPass = m_effectTechnique.GetPassByIndex(0);
  10. m_transformVariable = m_effect.GetVariableByName("WorldViewProj").AsMatrix();

Im Shader haben wir eine Konstante hinzugefügt, welche wir später über C# füllen können müssen. Damit man das kann, brauchen wir ein Objekt, welches eben diese Funktion erfüllt. Genau das ist der Sinn der neuen Zeile, wir holen uns ein EffectMatrixVariable Objekt mithilfe der Methode Effect.GetVariableByName. Aber halt, GetVariableByName gibt uns ein EffectVariable Objekt zurück. Der Grund dafür ist einfach, es gibt schließlich neben Matrizen auch andere Datentypen im Shader. Mit dem Aufruf von AsMatrix am Ende der Zeile kriegen wir erst das EffectMatrixVariable Objekt, klappt allerdings nur, wenn es sich auch wirklich um eine Matrix handelt. Wir können diese Methode hier bedenkenlos aufrufen, da wir den Aufbau unseres Shaders genau kennen. 

Zum Abschluss der LoadResources Methode müssen wir noch das InputLayout erzeugen:

  1. DX10.InputElement[] inputElements = new DX10.InputElement[]
  2. {
  3. new DX10.InputElement("POSITION",0,SlimDX.DXGI.Format.Format.R32G32B32_Float,0,0),
  4. new DX10.InputElement("COLOR",0,SlimDX.DXGI.Format.R8G8B8A8_UNorm,12,0)
  5. };
  6.  
  7. m_vertexLayout = new DX10.InputLayout(
  8. device,
  9. inputElements,
  10. m_effectPass.Description.Signature);

Die Änderungen halten sich auch hier wieder in Grenzen. In unserer Vertex-Struktur hat sich lediglich der Datentyp der Position geändert (von Vector4 in Vector3), was wir auch im Input-Layout entsprechend anpassen müssen. Dazu reich es, lediglich dem InputElement POSITION ein anderes Format zuzuweisen. Ich benutze hier R32G32B32_Float, was für 3 aneinander folgende Kommazahlen steht. Das war es dann auch schon, der Rest bleibt gleich. 

Als nächstes folgt die Render-Methode. Im Gegensatz zum letzten Mal brauchen wir jetzt mehrere Parameter, und zwar die Matrizen. Wie weiter oben schon erwähnt, brauchen wir 3 Stück: Die World-, die View- und die Projectionmatrix. Der Methodenkopf sieht dann so aus:

  1. public void Render(DX10.Device device, Matrix world, Matrix viewProj)
  2. {
  3. //TODO: Rendern
  4. }

Aber halt, wir übergeben bloß 2 Matrizen. Was soll das denn, eben hab ich doch noch von 3 geschrieben? Das liegt daran, dass die View- und die Projectionmatrix in einem Stück übergeben werden (Parameter viewProj). Die Worldmatrix bleibt aber erst mal in einer eigenen Variable, damit man diese auch noch manipulieren könnte. 

Schauen wir uns doch mal den Inhalt der Rendermethode an. Zuerst müssen wir dasselbe machen, wie beim letzten Mal. Also das InputLayout setzen, den Vertexbuffer setzen, usw..:

  1. device.InputAssembler.SetInputLayout(m_vertexLayout);
  2. device.InputAssembler.SetPrimitiveTopology(DX10.PrimitiveTopology.TriangleList);
  3. device.InputAssembler.SetIndexBuffer(m_indexBuffer, DXGI.Format.R16_UInt, 0);
  4. device.InputAssembler.SetVertexBuffers(
  5. 0,
  6. new DX10.VertexBufferBinding(m_vertexBuffer, Marshal.SizeOf(typeof(Vertex)), 0));

Das einzig neue hier ist der Aufruf von SetIndexBuffer, welcher, wie der Name schon sagt, den IndexBuffer setzt. Der zweite Übergabeparameter gibt den Typ der Werte im IndexBuffer an und der Dritte den Startindex (Wird erst dann gebraucht, falls man einen IndexBuffer für mehrere Objekte verwendet). 

Weiter geht es mit der Matrix. Im Shader haben wir die Konstante WorldViewProj angelegt, hier müssen wir diese mit dem gewünschten Wert füllen. Das ganze sieht dann so aus:

  1. m_transformVariable.SetMatrix(world * viewProj);

Wie man sieht, wird die Variable mittels der SetMatrix Methode mit einer Matrix gefüllt. Das * zwischen world und viewProj verschmilzt die beiden Matrizen schließlich. Andere Variablentypen können auf ähnliche Weise gefüllt werden.

Natürlich müssen wir unser Objekt auch Zeichnen. Dazu müssen wir wie beim letzten Mal die Apply Methode der Variable m_effectPass aufrufen, um den Shader zu aktivieren. Anschließend nutzen wir nichtmehr die Methode Draw, sondern DrawIndexed des Device Objekts, da wir dieses mal mit einem IndexBuffer arbeiten:

  1. m_effectPass.Apply();
  2. device.DrawIndexed(36, 0, 0);

In der DrawIndexed Methode sagen wir der Grafikkarte noch, wie viele Index-Werte verarbeitet werden sollen und von wo aus gestartet werden soll. Damit sind wir nicht nur mit der Rendermethode, sondern auch mit dieser kompletten Klasse fertig. Zusammenfasend nochmal der komplette Sourcecode der Klasse SimpleBox:

  1. private DX10.Buffer m_vertexBuffer;
  2. private DX10.Buffer m_indexBuffer;
  3. private DX10.InputLayout m_vertexLayout;
  4. private DX10.Effect m_effect;
  5. private DX10.EffectTechnique m_effectTechnique;
  6. private DX10.EffectPass m_effectPass;
  7. private DX10.EffectMatrixVariable m_transformVariable;
  8.  
  9. public void LoadResources(DX10.Device device)
  10. {
  11. Vertex[] vertices = new Vertex[]
  12. {
  13. new Vertex(new Vector3(-1, -1, -1), Color.Red.ToArgb()),
  14. new Vertex(new Vector3(1, -1, -1), Color.LightBlue.ToArgb()),
  15. new Vertex(new Vector3(1, -1, 1), Color.LightCyan.ToArgb()),
  16. new Vertex(new Vector3(-1, -1, 1), Color.CadetBlue.ToArgb()),
  17. new Vertex(new Vector3(-1, 1, -1), Color.Red.ToArgb()),
  18. new Vertex(new Vector3(1,1,-1), Color.Orange.ToArgb()),
  19. new Vertex(new Vector3(1,1,1), Color.Goldenrod.ToArgb()),
  20. new Vertex(new Vector3(-1, 1,1), Color.Yellow.ToArgb())
  21. };
  22.  
  23. short[] indices = new short[]
  24. {
  25. 4,1,0,4,5,1,
  26. 5,2,1,5,6,2,
  27. 6,3,2,6,7,3,
  28. 7,0,3,7,4,0,
  29. 7,5,4,7,6,5,
  30. 2,3,0,2,0,1
  31. };
  32.  
  33. DataStream outStream =
  34. new DataStream(8 * Marshal.SizeOf(typeof(Vertex)), true, true);
  35. DataStream outStreamIndex =
  36. new DataStream(36 * Marshal.SizeOf(typeof(short)), true, true);
  37.  
  38. outStream.WriteRange(vertices);
  39. outStreamIndex.WriteRange(indices);
  40.  
  41. outStream.Position = 0;
  42. outStreamIndex.Position = 0;
  43.  
  44. DX10.BufferDescription bufferDescription = new DX10.BufferDescription();
  45. bufferDescription.BindFlags = DX10.BindFlags.VertexBuffer;
  46. bufferDescription.CpuAccessFlags = DX10.CpuAccessFlags.None;
  47. bufferDescription.OptionFlags = DX10.ResourceOptionFlags.None;
  48. bufferDescription.SizeInBytes = 8 * Marshal.SizeOf(typeof(Vertex));
  49. bufferDescription.Usage = DX10.ResourceUsage.Default;
  50.  
  51. DX10.BufferDescription bufferDescriptionIndex = new DX10.BufferDescription();
  52. bufferDescriptionIndex.BindFlags = DX10.BindFlags.IndexBuffer;
  53. bufferDescriptionIndex.CpuAccessFlags = DX10.CpuAccessFlags.None;
  54. bufferDescriptionIndex.OptionFlags = DX10.ResourceOptionFlags.None;
  55. bufferDescriptionIndex.SizeInBytes = 36 * Marshal.SizeOf(typeof(short));
  56. bufferDescriptionIndex.Usage = DX10.ResourceUsage.Default;
  57.  
  58. m_vertexBuffer = new DX10.Buffer(device, outStream, bufferDescription);
  59. m_indexBuffer = new DX10.Buffer(device, outStreamIndex, bufferDescriptionIndex);
  60. outStream.Close();
  61. outStreamIndex.Close();
  62.  
  63. m_effect = DX10.Effect.FromFile(
  64. device,
  65. "Shaders/SimpleRendering.fx",
  66. "fx_4_0",
  67. DX10.ShaderFlags.None,
  68. DX10.EffectFlags.None,
  69. null, null);
  70. m_effectTechnique = m_effect.GetTechniqueByIndex(0);
  71. m_effectPass = m_effectTechnique.GetPassByIndex(0);
  72. m_transformVariable = m_effect.GetVariableByName("WorldViewProj").AsMatrix();
  73.  
  74. DX10.InputElement[] inputElements = new DX10.InputElement[]
  75. {
  76. new DX10.InputElement("POSITION",0,SlimDX.DXGI.Format.R32G32B32_Float,0,0),
  77. new DX10.InputElement("COLOR",0,SlimDX.DXGI.Format.R8G8B8A8_UNorm,12,0)
  78. };
  79.  
  80. m_vertexLayout = new DX10.InputLayout(
  81. device,
  82. inputElements,
  83. m_effectPass.Description.Signature);
  84. }
  85.  
  86. public void Render(DX10.Device device, Matrix world, Matrix viewProj)
  87. {
  88. device.InputAssembler.SetInputLayout(m_vertexLayout);
  89. device.InputAssembler.SetPrimitiveTopology(DX10.PrimitiveTopology.TriangleList);
  90. device.InputAssembler.SetIndexBuffer(m_indexBuffer, DXGI.Format.R16_UInt, 0);
  91. device.InputAssembler.SetVertexBuffers(
  92. 0,
  93. new DX10.VertexBufferBinding(m_vertexBuffer, Marshal.SizeOf(typeof(Vertex)), 0));
  94.  
  95. m_transformVariable.SetMatrix(world * viewProj);
  96.  
  97. m_effectPass.Apply();
  98. device.DrawIndexed(36, 0, 0);
  99. }

 

Anpassungen im Hauptfenster

Damit wir unser neues 3D-Objekt am Bildschirm sehen können, müssen wir im Hauptfenster noch ein paar Kleinigkeiten codieren. So benötigen wir jetzt zusätzlich noch diese Variablen (Die Variable vom Typ SimpleTriangle kann entfernt werden):

  1. private SimpleBox m_simpleBox;
  2.  
  3. private Matrix m_viewMatrix;
  4. private Matrix m_projMatrix;
  5. private Matrix m_worldMatrix;
  6. private Matrix m_viewProjMatrix;

Was ist das jetzt alles? Der erste Member ist klar, das ist unser neues 3D-Objekt. Die nächsten 3 speichern die 3 Matrizen View, Projection und World. Die letzte Variable schließlich ist eine Verschmelzung der View- und Projectionmatrix, brauchen wir zwar nicht dringend, muss aber dadurch nicht für jedes Objekt neu berechnet werden. 

Weiter geht es mit einer Anpassung der Initialize3D Methode. Als erstes können wir hier anstatt des SimpleTriangle Objekts das SimpleBox Objekt erstellen und laden:

  1. m_simpleBox = new SimpleBox();
  2. m_simpleBox.LoadResources(m_device);

Ich denke, dazu brauch ich jetzt nicht viel schreiben. Wir rufen bloß die Methoden auf, welche wir eben erst codiert haben. Das nächste Coding hingegen ist neu:

  1. m_viewMatrix = Matrix.LookAtLH(
  2. new Vector3(0f, 0f, -4f),
  3. new Vector3(0f, 0f, 1f),
  4. new Vector3(0f, 1f, 0f));
  5. m_projMatrix = Matrix.PerspectiveFovLH(
  6. (float)Math.PI * 0.5f,
  7. this.Width / (float)this.Height,
  8. 0.1f, 100f);
  9. m_viewProjMatrix = m_viewMatrix * m_projMatrix;
  10. m_worldMatrix = Matrix.RotationYawPitchRoll(0.85f, 0.85f, 0f);

In diesem kurzen Codeausschnitt werden alle Matrix Variablen gesetzt. Die Viewmatrix bekommt seinen Wert von der Matrix.LookAtLH Methode (LH steht für Linkshändiges Koordinatensystem). Die Übergabeparameter sind hier sprechend: Eye ist die Position der Kamera oder des "Auges", Target ist der Zielpunkt, zu dem die Kamera schaut und Up sagt DirectX, wo oben ist. Die Matrix.PerspectiveFovLH erzeugt die Projectionmatrix. Die Übergabeparameter dieser Methode sind allerdings alles andere als sprechend für den Anfänger. Der erste, FOV, setzt das Sichtfeld in Y-Richtung. Der zweite beschreibt das Seitenverhältnis und errechnet sich i. d. R. mittels Anzeigebreite / Anzeigehöhe. Die letzten beiden Parameter geben die kürzeste und die weiteste Distanz an, die der Benutzer sehen kann. Achtung: 0 darf hier nicht als kürzeste Distanz angegeben werden, da das zu einer Division durch 0 führen würde. Die Verschmelzung der beiden Matrizen läuft genauso ab, wie wir es weiter oben schon gesehen haben. Sie werden einfach miteinander Multipliziert. In der letzten Zeile wird die Worldmatrix noch so gesetzt, dass unser 3D-Objekt rotiert wird. Genauer gesagt mit 0.85 Rad um die X- und Y-Achse. 

So, fast geschafft. Jetzt müssen wir noch unser Objekt in der OnPaint Methode zeichnen. Folgendes Coding ist dazu notwendig:

  1. base.OnPaint(e);
  2.  
  3. if (m_initialized)
  4. {
  5. m_device.ClearRenderTargetView(m_renderTarget, new Color4(Color.CornflowerBlue));
  6.  
  7. m_simpleBox.Render(m_device, m_worldMatrix, m_viewProjMatrix);
  8.  
  9. m_swapChain.Present(0, DXGI.PresentFlags.None);
  10. }

Wie schon so oft hat sich hier nicht viel verändert. Dort wo vorher die Rendermethode des SimpleTriangle Objekts aufgerufen wurde, wird jetzt dieselbe Methode der SimpleBox Instanz ausgeführt. Somit ist dieses Kapitel auch zu Ende.

 

Kommentar hinzufügen

Ihr Name:
Kommentar:

Kommentare (3)

3. Pascal
Mittwoch, den 26. Oktober 2011 um 14:48 Uhr
Danke viel mal für deine antwort, leider war das "multisampling" schon hoch gedreht (8).

mein bild (auf dein tutorial basiert) sieht so aus:
http://dl.dropbox.com/u/44885276/cube.png

Meinst du das man eine bessere qualität erreichen kann?

2. Roland König
Sonntag, den 23. Oktober 2011 um 15:36 Uhr
Stichwort "Multisampling". Schau dir mal diesen Forum-Beitrag dazu an:
http://www.gamedev.net/topic/566533-directx-10-multisample-anti-aliasing/

1. pascal
Donnerstag, den 20. Oktober 2011 um 08:28 Uhr
Wie könnte man das "antialiazing" function anschalter?