Terrain 101: Neue Sichtweisen

Der erste Schritt ist getan und im vorletzten Teil dieser Serie konnten wir im wahrsten Sinne des Wortes endlich Land sehen. Wir haben den ersten Chunk unseres Terrains gerendert und nach einigen grundlegenden Erklärungen im vorherigen Teil steht uns nun die ganze Welt offen. Noch nicht wirklich, denn wir haben noch ein kleines Problem. Wir sind auf einer Stelle des Terrains praktisch „festgenagelt“. Wir können weder die Blickrichtung verändern, noch können wir uns über die Landschaft bewegen, was wirklich schade ist, denn es gibt vieles zu entdecken.

In diesem Teil meiner Terrain 101-Serie möchte ich auf die Kamera eingehen, die uns die absolute Freiheit bringt. Am Ende dieses Artikels werden wir uns über die Landschaft bewegen können und in alle Richtungen schauen können. Dabei werde ich ein paar interessante Konzepte zur Kamera liefern, die in anderen Tutorials so nicht vorkommen und damit eine Wiederverwendbarkeit und Flexibilität schaffen, die die Kamera einfach wartbar und universell einsetzbar macht.

Wichtiges Update: Im Artikel hatte sich ein Fehler eingeschlichen, der dazu geführt hat, dass auf ATI-Grafikkarten nichts gerendert wurde und nur ein blauer Bildschirm erschienen ist. Ich habe den Fehler im Artikel korrigiert.

Mit zunehmender Artikelzahl wird das Inhaltsverzeichnis immer länger, dafür das Vorwort zum Inhaltsverzeichnis immer kürzer. Also kurz und knapp: Hier ist das Inhaltsverzeichnis, das alle bisherigen Teile der Reihe zusammenhält.

Das Inhaltsverzeichnis

Terrain 101: Eine Einführung in dreidimensionale Landschaften in Echtzeit
Terrain 101: Die Entwicklungsumgebung und das XNA 4.0 Projekt
Terrain 101: Das erste Dreieck
Terrain 101: Transformationen
Terrain 101: Vertex- und Index-Buffer
Terrain 101: Land in Sicht
Terrain 101: Technische Rafinessen
Terrain 101: Neue Sichtweisen
Terrain 101: Kamera ab und Action

Grundlagen und Vorarbeit

Eine virtuelle Kamera zu erzeugen ist sehr einfach und im Grunde genommen haben wir dies auch in dieser Artikel-Reihe bereits gemacht. In ihrer einfachsten Form besteht die Kamera nämlich lediglich aus der Projektions- und der View-Matrix.

Die Projektionsmatrix legt dabei die grundlegenden Parameter unserer Kamera fest. Dies sind der Anfang (Near-Plane) und das Ende (Far-Plane) des Sichtbereiches, der Blickwinkel (Field of View) und das Seitenverhältnis (AspectRatio). Dies sind also die eher technischen Parameter, die es uns z.B. ermöglichen eine Art Fischaugeneffekt mit der Kamera einzustellen. Diese Matrix muss in der Regel nur bei der Erzeugung der Kamera eingestellt werden und wird danach nicht mehr verändert.

Die View-Matrix bestimmt den Blickwinkel und die Position der Kamera. Diese Matrix muss immer dann aktualisiert werden, wenn sich die Kamera bewegt, sowohl in der Position, als auch im Winkel.

Um unser Ziel zu erreichen erzeugen wir zunächst eine neue Kamera-Klasse in unserem Projekt das wir im vorletzten Teil der Serie erzeugt haben. Dies sollte mittlerweile jeder können: Rechte Maustaste auf das Projekt im Projekt-Explorer von Visual Studio und Hinzufügen/Klasse auswählen. Wir nenn die neue Klasse schlicht und einfach Camera. In der Klasse erzeugen wir zwei Member-Variablen vom Typ Matrix die wir projectionMatrix und viewMatrix nennen. Wir erzeugen auch gleich noch zwei Eigenschaften für die beiden, damit wir von aussen darauf zugreifen können. Da wir die Matrizen nicht direkt ändern wollen werden wir nur Getter implementieren, damit die Matrizen vor dem Zugriff von aussen geschützt bleiben. Die Klasse machen wir natürlich noch öffentlich zugreifbar, so dass diese nun wie folgt aussieht.

    public class Camera
    {
        private Matrix projectionMatrix;
        private Matrix viewMatrix;

        public Matrix ProjectionMatrix
        {
            get
            {
                return this.projectionMatrix;
            }
        }

        public Matrix ViewMatrix
        {
            get
            {
                return this.viewMatrix;
            }
        }
    }

An dieser Stelle ein kleiner Tipp: Während des Abtippens dieses Sourcecodes merkt der Einsteiger vermutlich, dass beim festlegen des Typs Matrix für die Member-Variablen diese rot geschlängelt unterstrichen sind. Der Grund dafür ist, dass der Typ (noch) nicht bekannt ist. Dies können wir sehr einfach auflösen, indem wir das Wort Matrix mit der rechten Maustaste anklicken, dann auf „Auflösen“ klicken und dann den passenden Namespace „Microsoft.XNA.Framework“ auswählen. Dieser wird dann automatisch zu den Using-Statements hinzugefügt.

Bisher ist der Code noch sehr übersichtlich und bedarf nicht vieler Erklärungen. Trotzdem möchte ich an dieser Stelle ein paar Worte über den Code verlieren, da diese Artikelreihe sowohl für Einsteiger, als auch Fortgeschrittene geeignet sein soll. Dies ist nun mal die erste Klasse die wir erzeugen und der ein oder andere hat vielleicht noch ein paar Schwierigkeiten damit. Der Fortgeschrittene kann damit einfach sein Wissen überprüfen und festigen.

Die häufigste Frage ist sicherlich warum die Eigenschaften (die man auch Properties nennt) verwendet werden und wozu diese gut sind. Die Properties sind dazu da, den Zugriff auf einen Wert von ausserhalb der Klasse zu ermöglichen. Man könnte aber doch auch den Zugriffsmodifizierer der Member-Variablen einfach auf public stellen und damit könnte man auch auf den Wert zugreifen? Das ist vollkommen richtig, aber doch ein Unterschied. Bei den Properties habe ich zwei Möglichkeiten und zwar den Getter (Get) und den Setter (Set) für die beiden Zugriffsarten „lesen“ und „schreiben“. Für beide kann ich einen eigenen Zugriffsmodifizierer festlegen und habe so deutlich mehr Kontrolle darüber. Auch habe ich die Möglichkeit sowohl beim lesenden als auch beim schreibenden Zugriff auf die Eigenschaft zusätzlichen Code auszuführen, was bei einfachen Variablen nicht möglich ist. Dies werden wir uns nun auch zunutze machen, damit dieses Konzept direkt klarer wird.

Eingangs hatte ich ja erwähnt, dass die View-Matrix nur dann erzeugt werden muss, wenn sich die Kamera bewegt hat. Wir gehen nun sogar noch einen Schritt weiter und erweitern diese Regel. Wir müssen die View-Matrix nur dann berechnen, wenn sich die Position oder der Blickwinkel der Kamera geändert hat und wir auf die Matrix zugreifen. Dies ist sehr einfach in Code auszudrücken. Wir benötigen dazu eine Zustandsvariable vom Typ Boolean die festlegt ob die Matrix berechnet werden muss oder nicht. Wenn sich die Position oder der Blickwinkel ändert, so werden wir diese Variable einfach auf den Wert true setzen und in der Eigenschaft für die Matrix prüfen wir diesen Wert und berechnen die Matrix ggfls. neu. In Code ausgedrückt sieht dieser Sachverhalt schon deutlich einfach aus. Hier nochmal die gesamte Klasse zwecks Übersichtlichkeit:

    public class Camera
    {
        private Matrix projectionMatrix;
        private Matrix viewMatrix;
        private bool viewMatrixDirty;

        public Matrix ProjectionMatrix
        {
            get
            {
                return this.projectionMatrix;
            }
        }

        public Matrix ViewMatrix
        {
            get
            {
                if (viewMatrixDirty)
                {
                    UpdateViewMatrix();
                }

                return this.viewMatrix;
            }
        }

        private void UpdateViewMatrix()
        {
            // View Matrix erzeugen

            viewMatrixDirty = false;
        }
    }

Dieser Code sollte verständlich sein und ist nicht weiter kompliziert. Beim Zugriff auf die View-Matrix-Eigenschaft von aussen wird zunächst geprüft, ob die ViewMatrix (neu) erzeugt werden muss. Ist dies notwendig, so wird die Matrix vor der Rückgabe nach aussen neu berechnet, was durch die Methode UpdateViewMatrix erfolgen wird.

Kapselung

Der nächste Schritt ist nun die Kapselung des gesamten Kamera-Codes aus unserem Hauptprogramm in der Kamera-Klasse, sowie die Verwendung der Kamera-Klasse aus der Game-Klasse heraus. Damit erreichen wir mehrere Ziele: Wir erhöhen zum Beispiel die Übersichtlichkeit. In der Game-Klasse werden wir am Ende dieses Abschnittes nicht mehr mit „Kamera-Code“ hantieren, sondern wir verwenden einfach unser Objekt Kamera. Aus Sicht der Game-Klasse müssen wir nicht mehr wissen, wie die Kamera funktioniert, sondern wir müssen lediglich wissen, wie man die Kamera verwendet. Die Klasse selbst gibt dabei die Regeln vor und überwacht diese. Dies ist eines der wichtigsten Konzepte der objektorientierten Programmierung. Der nette Nebeneffekt dabei ist, dass wir die Kamera zum einen Wiederverwenden können und zwar in jedem Projekt, dass solch eine Kamera benötigt, wir haben aber auch die Möglichkeit, die Kamera selbst auszutauschen. Wenn wir also zum Beispiel eine andere Kameraart verwenden möchten, z.B. eine Kamera, die dem Spieler in einigem Abstand folgt, dann können wir einfach die Kamera-Klasse austauschen um dieses Ziel zu erreichen. Wir müssen nicht mehr im Code herumwühlen und suchen wo wir etwas ändern müssen.

Wenn wir uns den bisherigen Code zur Erzeugung unserer Matrizen ansehen, so entdecken wir, dass wir sechs Informationen zur Erzeugung der Kamera benötigen:

  1. Field of View (Blickwinkel)
  2. Aspect Ratio (Seitenverhältnis)
  3. Near-Plane
  4. Far-Plane
  5. Position des Betrachters
  6. Zielpunkt des Betrachters (Target)

Zur initialen Erzeugung der Kamera werden wir diese Parameter im Konstruktor verlangen. Die Position und das Target benötigen wir zur Erzeugung der View-Matrix und daher speichern wir diese in jeweils einer Member-Variable. Der Konstruktor sieht wie folgt aus:

        public Camera(float fieldOfView, float aspectRatio, float nearPlane, float farPlane, Vector3 position, Vector3 target)
        {
            this.projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlane, farPlane);

            this.position = position;
            this.target = target;
            this.viewMatrixDirty = true;
        }

Da die Projection-Matrix sich später nicht mehr wirklich ändert, können wir diese sofort im Konstruktor berechnen. Für die View-Matrix speichern wir nur die notwendigen Parameter (Position und Target) und legen fest, dass diese neu berechnet werden muss mittels viewMatrixDirty. Die View-Matrix wird nun in der zugehörigen Eigenschaft berechnet und dann zurückgegeben. Damit haben wir unsere sogenannte „Business-Logik“, die wir zu Beginn dieses Kapitels festgelegt hatten bereits erfüllt.

Position und Target können sich natürlich jederzeit ändern. Daher stellen wir dafür noch zwei Eigenschaften zur Verfügung. Ändert sich die Position oder das Target, so muss die View-Matrix neu erzeugt werden. Im Setter der zugehörigen Eigenschaft legen wir dies also auch wieder über die Variable viewMatrixDirty fest. Damit ist sichergestellt, dass die ViewMatrix exakt einmal unmittelbar vor dem Zugriff neu berechnet wird, aber nur, wenn dies auch notwendig ist. Die Eigenschaften sind sehr einfach und sehen wie folgt aus.

       public Vector3 Position
        {
            get
            {
                return this.position;
            }
            set
            {
                if (this.position != value)
                {
                    this.position = value;
                    this.viewMatrixDirty = true;
                }
            }
        }

        public Vector3 Target
        {
            get
            {
                return this.target;
            }
            set
            {
                if (this.target != value)
                {
                    this.target = value;
                    this.viewMatrixDirty = true;
                }
            }
        }

Um die einfache Variante der Kamera-Klasse zu vollenden, werden wir nun noch die Erzeugung der View-Matrix in der entsprechenden Methode vornehmen.

        private void UpdateViewMatrix()
        {
            // View Matrix erzeugen
            this.viewMatrix = Matrix.CreateLookAt(this.position, this.target, Vector3.Up);

            this.viewMatrixDirty = false;
        }

Diesen Code kennen wir im Grunde genommen auch bereits, daher verzichte ich auf eine nähere Erklärung.

Der nächste Schritt wäre nun, den gesamten Kameracode aus der Game-Klasse zu entfernen und dort eine Instanz der Kamera zu erzeugen und diese dann zu verwenden. Dies werden wir nun zum Abschluss dieses Kapitels auch machen. Um dieses Ziel zu erreichen, entfernen wir zunächst die beiden Member-Variablen die die Projection- und View-Matrix in der Game-Klasse enthalten. An deren Stelle tritt eine neue Variable, die die Instanz der Kamera-Klasse enthält:

private Camera camera;

In der Initialize-Methode der Game-Klasse entfernen wir die beiden Zeilen mit denen wir bisher die beiden Matrizen erzeugt haben und ersetzen diese Zeile durch den Konstruktor-Aufruf unserer Camera-Klasse.

            this.camera = new Camera(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1.0f, 2000.0f, new Vector3(640.0f, 640.0f, 1280.0f), new Vector3(640.0f, 0.0f, 640.0f));

Der letzte Schritt ist nun, dass wir in der Draw-Methode auch die Matrizen der Kamera verwenden und nicht mehr die lokalen Member-Variablen. Dazu müssen wir nur die Parameter-Zuweisung des Effektes leicht abändern.

            triangleEffect.Parameters["View"].SetValue(camera.ViewMatrix);
            triangleEffect.Parameters["Projection"].SetValue(camera.ProjectionMatrix);

Wenn wir nun das Programm mit F5 starten, dann sieht alles so aus wie vorher. Dies ist auch vollkommen richtig, denn wir wollten ja auch noch nichts verändern. Unter der Haube hat sich jedoch etwas geändert und zwar haben wir die Kamera in einem objektorientierten Ansatz austauschbar gemacht und in eine eigene Klasse ausgelagert. Dies ermöglicht es uns ab sofort Arbeiten an der Kamera zu machen ohne die Game-Klasse anfassen zu müssen. Weiterhin haben wir eine Optimierung vorgenommen. Die Matrizen werden nur noch dann berechnet, wenn dies auch tatsächlich notwendig ist. Dies wird bei Kamera-Bewegungen wichtig werden.

Schlusswort und Ausblick

Ich werde diesen Teil an dieser Stelle abschliessen, da es sich nun anbietet. Wir haben erfolgreich die Kamera in eine eigene Klasse ausgelagert. Dies war nicht sonderlich schwer und eher ein Anfängerthema, aber im nächsten Teil wird dies extrem hilfreich sein. Nahezu alle weiteren Arbeiten im nächsten Teil werden sich nämlich genau auf diese neue Kamera-Klasse beziehen. Wir werden uns im nächsten Teil darum kümmern, dass wir die Kamera bewegen können. Die Kamera wird verschoben werden, sie wird rotiert werden und wir werden eine rudimentäre Kollisionsüberprüfung implementieren, damit die Kamera nicht durch die Landschaft fallen kann. Diese Themen sind so umfangreich, dass sie diesen Teil sprengen würden und da ich euch nicht noch länger auf einen neuen Teil warten lassen wollte, habe ich mich dazu entschlossen zwei Teile daraus zu machen auch wenn der Titel „Neue Sichtweisen“ noch nicht wirklich Programm geworden ist.

Übungen für den Leser

Um die Motivation des Lesers meiner Artikelreihe weiter zu erhöhen und um dich zum mitarbeiten zu bewegen, stelle ich immer dann ein paar Aufgaben, wenn sich dies anbietet. Diese Aufgaben sind vollkommen optional, führen aber dazu, dass du dich mit dem bisher gelernten besser auseinander setzen kannst. Sie helfen dabei, dein Wissen zu festigen.

In diesem Teil habe ich mir die folgenden drei Aufgaben überlegt:

  • Führe die Parameter der Projektion durch Eigenschaften „nach aussen“ und aktualisiere die Projektionsmatrix nur dann, wenn sich diese Parameter geändert haben.
  • Spiele mit den Parametern der Projektionsmatrix herum um ein Gefühl dafür zu erlangen, was diese bewirken.
  • Verändere Position und Blickrichtung über die entsprechenden Parameter der Kamera und finde interessante Blickrichtungen.

Sourcecode

Wie immer werde ich nun nochmal den gesamten Sourcecode zusammenhängend auflisten. Da wir mittlerweile zwei Klassen haben (Game und Camera) sind dies nun auch zwei Blöcke mit Code.

Game-Klasse

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Terrain_101
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Effect triangleEffect;

        VertexBuffer vertexBuffer;
        IndexBuffer indexBuffer;

        Camera camera;

        Texture2D heightmap;

        RasterizerState rs;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            this.Window.Title = "http://www.MitOhneHaare.de - Terrain 101 - Neue Sichtweisen (Teil 8)";
        }

        protected override void Initialize()
        {
            rs = new RasterizerState();
            rs.FillMode = FillMode.WireFrame;

            this.camera = new Camera(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1.0f, 2000.0f, new Vector3(640.0f, 640.0f, 1280.0f), new Vector3(640.0f, 0.0f, 640.0f));

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            triangleEffect = Content.Load<Effect>("Triangle");

            heightmap = Content.Load<Texture2D>("ChunkHeightmap");

            SetupFirstChunk();
        }

        protected override void UnloadContent()
        {
            if (indexBuffer != null)
            {
                indexBuffer.Dispose();
                indexBuffer = null;
            }

            if (vertexBuffer != null)
            {
                vertexBuffer.Dispose();
                vertexBuffer = null;
            }

            base.UnloadContent();
        }

        private void SetupFirstChunk()
        {
            Color[] heights = new Color[heightmap.Width * heightmap.Height];
            heightmap.GetData<Color>(heights);

            VertexPositionColor[] vertices = new VertexPositionColor[128 * 128];
            int index = 0;

            for (int z = 0; z < 128; z++)
            {
                for (int x = 0; x < 128; x++)
                {
                    vertices[z * 128 + x] = new VertexPositionColor(new Vector3(x * 10.0f, heights[z * 128 + x].R, z * 10.0f), Color.Green);
                }
            }

            this.vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), 128 * 128, BufferUsage.WriteOnly);
            this.vertexBuffer.SetData<VertexPositionColor>(vertices);

            int[] indices = new int[127 * 127 * 6];
            index = 0;

            for (int z = 0; z < 127; z++)
            {
                for (int x = 0; x < 127; x++)
                {
                    indices[index + 0] = z * 128 + x;
                    indices[index + 1] = indices[index + 0] + 128 + 1;
                    indices[index + 2] = indices[index + 0] + 128;
                    indices[index + 3] = indices[index + 0];
                    indices[index + 4] = indices[index + 0] + 1;
                    indices[index + 5] = indices[index + 0] + 128 + 1;

                    index += 6;
                }
            }

            this.indexBuffer = new IndexBuffer(GraphicsDevice, IndexElementSize.ThirtyTwoBits, 127 * 127 * 6, BufferUsage.WriteOnly);
            this.indexBuffer.SetData<int>(indices);
        }

        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            GraphicsDevice.RasterizerState = rs;

            triangleEffect.Parameters["World"].SetValue(Matrix.Identity);
            triangleEffect.Parameters["View"].SetValue(camera.ViewMatrix);
            triangleEffect.Parameters["Projection"].SetValue(camera.ProjectionMatrix);

            triangleEffect.CurrentTechnique.Passes[0].Apply();

            GraphicsDevice.SetVertexBuffer(this.vertexBuffer);
            GraphicsDevice.Indices = indexBuffer;

            GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 128 * 128 * 6, 0, 127 * 127 * 2);

            base.Draw(gameTime);
        }
    }
}

Camera-Klasse

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace Terrain_101
{
    public class Camera
    {
        private Matrix projectionMatrix;
        private Matrix viewMatrix;
        private bool viewMatrixDirty;
        private Vector3 position;
        private Vector3 target;

        public Camera(float fieldOfView, float aspectRatio, float nearPlane, float farPlane, Vector3 position, Vector3 target)
        {
            this.projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlane, farPlane);

            this.position = position;
            this.target = target;
            this.viewMatrixDirty = true;
        }

        public Vector3 Position
        {
            get
            {
                return this.position;
            }
            set
            {
                if (this.position != value)
                {
                    this.position = value;
                    this.viewMatrixDirty = true;
                }
            }
        }

        public Vector3 Target
        {
            get
            {
                return this.target;
            }
            set
            {
                if (this.target != value)
                {
                    this.target = value;
                    this.viewMatrixDirty = true;
                }
            }
        }

        public Matrix ProjectionMatrix
        {
            get
            {
                return this.projectionMatrix;
            }
        }

        public Matrix ViewMatrix
        {
            get
            {
                if (viewMatrixDirty)
                {
                    UpdateViewMatrix();
                }

                return this.viewMatrix;
            }
        }

        private void UpdateViewMatrix()
        {
            // View Matrix erzeugen
            this.viewMatrix = Matrix.CreateLookAt(this.position, this.target, Vector3.Up);

            this.viewMatrixDirty = false;
        }
    }
}
Advertisements

Veröffentlicht am 19.10.2011 in 3D Terrain, Tutorial, XNA, XNA 4.0 und mit , , , , , , , , , , getaggt. Setze ein Lesezeichen auf den Permalink. 26 Kommentare.

  1. internetfreak413

    Wie immer bei deinen Beiträgen ein guter Artikel 😀
    Da dies ja eine 3D-Kamera ist, hab ich eine Frage.
    Wie muss ich die umbauen bzw was muss ich umbauen, wenn ich diese Kamera auf 2D umstellen will? (Brauche demnächst ja ne 2D-Cam, daher wäre sowas schon gut)

    Ansonsten warte ich mal auf den nächsten Teil, auch wenn ich ein 2D-Coder bin, so les ich hier mit 😀

    • internetfreak413

      Kurzer Nachtrag (kannst du löschen, nachdem du ihn gelesen hast, wenn du willst)
      Glatzemann, kann man dich irgendwie erreichen per Messenger oder so?
      Oder bliebe mir nur Email bzw XNAMag?
      Ich hätte an Messengern ICQ und MSN sowie Steam anzubieten (auch wenn Steam in dem SInne kein Messenger ist xD)
      Der Grund für die Frage ist, dass ich n paar kleinere Fragen hab und damit jmd „belästigen“ will, der Ahnung hat (Und du hast 100% welche, sonst kämen nicht so gute Artikel zustande)
      Und nur wegen 1-2 Fragen wollt ich mich jetzt nich extra iwo anmelden oder so (auch wenns nich weh tut)

      • Per Messenger oder ähnliches bin ich zwar erreichbar, aber erstens nur sehr, sehr selten und zweitens nicht für „allgemeinen“ Support, sorry…

        Du kannst natürlich jederzeit hier konkrete Fragen in den Kommentaren stellen, ansonsten auch gerne über meine EMail-Adresse und/oder im XNAmag. Per EMail werde ich aber durchaus auch auf das XNAmag verweisen, wenn ich der Meinung bin, dass es eine Frage ist, deren Antwort auch noch andere interessieren könnte. Der Vorteil ist halt einfach, dass man (und andere) später nochmal nachlesen kann, wenn es in einem Forum steht und ausserdem sind da ja noch viel mehr „Wissende“ 🙂

    • Danke sehr 🙂

      Im Grunde genommen ist eine 2D-Kamera exakt so wie auch eine 3D-Kamera, nur das sich alles auf lediglich zwei Achsen abspielt und nicht auf drei. Einige Dinge wie LookAt-Target gehen natürlich nicht. Spiel mal ein wenig mit einer Transformations-Matrix (Matrix.CreateTranslation) herum. Diese übergibst du als letzten oder vorletzten Parameter an SpriteBatch.Begin. Damit verschiebst du praktisch das Koordinaten-System und das ist die Grundlage einer 2D-Kamera.

  2. Warum disposed du die Vertex- und Indexbuffer?

    • Dafür muss ich ein klein wenig ausholen: Alle Klassen, die das IDisposable-Interface implementieren teilen damit mit, dass verwaltete Resourcen vorhanden sind, die über Dispose freigegeben werden können, sollten und müssen. Normalerweise (so macht es zumindest XNA) gibt es einen Dispose-Aufruf im Destruktor, zumindest ist dies guter Stil, aber eine Garantie dafür gibt es nicht. Dies kann also von Library zu Library unterschiedlich sein. Bei XNA ist es nun so, dass die nativen HardwareBuffer von DirectX bei einem Aufruf von Dispose freigegeben werden. Dies passiert zwar auch, wenn der GarbageCollector seine Arbeit tut, aber dann habe ich keinerlei Kontrolle darüber wann das passiert. Wenn ich explizit Dispose aufrufe, dann habe ich die Kontrolle wann der Grafikkartenspeicher freigegeben wird (der wird nicht vom GarbageCollector verwaltet, sondern nur das Proxy-XNA-Objekt VertexBuffer).

      In diesem Fall macht es natürlich nicht sehr viel Sinn Dispose aufzurufen, da dies sowieso erst bei Programmende passiert und damit im Grunde genommen unerheblich ist, wann die Garbage Collection tatsächlich stattfindet. Trotzdem habe ich mir aber angewöhnt – und das ist sehr empfehlenswert – das ich jedes Objekt, dass IDisposable implementiert explizit mit Dispose freigebe. So bin ich auf der sicheren Seite und es kann zumindest nicht durch einen unterlassenen Dispose-Aufruf zu Memory-Leaks kommen.

  3. Hey,

    ich hoffe bald kommen neue Tutorials von dir 🙂 Jetzt allerdings zu meiner Frage… ich habe heute angefangen bisschen mit 3D rumzuspielen und hab mir sozusagen ein kleines Framework aufgebaut. Dort wollte ich verschiedene Klassen bauen z.B. eine um Primitive Sachen darstellen zu können wie z.B. Triangles oder auch ein Koordinatenkreuz mithilfe von Vertices.

    Es funktioniert auch alles super und kann z.B. per:

    Triangle triangle2 = new Triangle(3, new Vector3(-2.0f, 0f, 0f));
    triangle2.LoadContent(Content);
    WorldManager.AddActor(triangle2);

    Axis axis = new Axis(4, Vector3.Zero);
    axis.LoadContent(Content);
    WorldManager.AddActor(axis);

    Triangle anlegen oder auch ein Axis anlegen. Allerdings funktioniert etwas sehr entscheidenes nicht sobald ich diese zwei Typen anlege liegt triangle2 bei der Position: Vector3.Zero und das Axes bei Vector3(-2.0f…);. Jetzt weiß ich nicht genau woran es liegen könnte.

    Es ist richtig, dass jedes Objekt seine eigene WorldMatrix hat richtig? Und auch seinen eigenen IndexBuffer und VertexBuffer? Oder liegt es daran, dass ich nur einen globalen VertexBuffer haben darf und einen globalen IndexBuffer?

    Irgendwie konnte ich dazu nicht so viel finden wäre super wenn du mir antworten würdest.

    Schöne Grüße
    Gavin

    • Schön, dass dir meine Tutorials gefallen 🙂 Bald geht es weiter, versprochen.

      Zu deinen Fragen:

      Aufgrund des Codes den du gepostet hast und aufgrund der Tatsache, dass du die ContentPipeline verwendest, gehe ich davon aus, dass du Triangle und Axis in einem 3D-Programm erzeugt hast? Wenn dem so ist, dann ist vermutlich der Ursprung bei Axis verschoben. Der Nullpunkt muss vermutlich im Zentrum des Axis-Meshes liegen. Bei Blender erreichst du das mit Strg-A und Auswahl von Location, Rotation und Scale (also insgesamt 3 Aufrufe).

      Ja, jedes Objekt hat seine eigene WorldMatrix.

      Index- und VertexBuffer müssen nicht eindeutig sein. Wenn diese aufgrund gleicher Daten geteilt werden können, dann ist es vollkommen ok, diese für mehrere Objekte zu verwenden. Dies ist sogar sehr zu empfehlen, da das Umschalten von Buffern Zeit kostet und die Wiederverwendung gut für die Caches ist und ausserdem so weniger Speicher verbraucht wird, was im allgemeinen nie verkehrt ist 🙂

  4. Okay,

    also die LoadContent Methode hat dich vielleicht etwas verwirrt. Ich erzeuge das Triangle und das / der / die Axis schon mithilfe von VertexPositionColor manuell. Mein Problem ist vielleicht etwas schwer zu beschreiben. Ich hatte noch ein weiteres Beispiel bei dem ich das Triangle um die YAxis rotieren lassen wollte.

    Nun es hat auch soweit funktioniert allerdings nur bis zu dem Punkt bis ich mein Axis zeichnen wollte. Dann wurde nämlich die Axis um das Triangle rotiert und nicht mehr nur das Triangle um die Axis.

    Hier kannst du auch ein kleines Bild sehen: http://www.cique.de/3dbasics.png obwohl vom Triangle Object der Winkel verändert wird dreht sich der / die / das Axis… und ich weiß einfach nicht wieso.

    Vielen Dank für deine Hilfe.

    • Das ist natürlich schwer einfach so zu lösen. Wenn du magst, schick mir deinen Source-Code mal per eMail mit einer möglichst genauen Beschreibung, was wie passieren soll. Ich schaue mir das dann bei Gelegenheit mal an und schreib dir, was falsch gelaufen ist.

      • Hey,

        also brauchst dir meinen Code nicht mehr anschauen. Ich habe für die Axis jetzt eine neue Shader-Klasse plus neue Shader Datei angelegt mit einem Basis Effect und jetzt geht es auf einmal. Aber es kann doch nicht sein, dass ich für jede Klasse, die ein Objekt darstellt, das gezeichnet wird einen eigenen Shader brauche oder?

        Komischweise löst das allerdings mein Problem … was mich zum neuen Problem führt bei dem ich einfach nicht weiß wieso.

      • Selbstverständlich kann ein Shader wiederverwendet werden. Das Verhalten sollte mit einem Shader das Gleiche sein wie mit mehreren. Vermutlich liegt das Problem darin, dass RenderStates noch nicht korrekt gesetzt sind oder die Effekt-Parameter.

  5. Sehr schöne Serie bisher =)
    Hab bis hierhin alles durchgearbeitet und es funktioniert einwandfrei…
    Freue mich schon auf den nächsten Artikel und hoffe es dauert nicht mehr lange bis dieser erscheint =)
    Und eine Frage noch vorweg: Wirst du in dieser Serie auf LoD so eingehen wie auf alles andere bisher? Weil genau dafür findet man keine bis kaum gute Tutorials im Netz..

    mfG Blain

    • Danke sehr, freut mich, dass es dir bisher gefällt 🙂

      Ja, ich werde auf LOD sehr detailliert eingehen. Der aktuelle Teil an dem ich arbeite ist die Vorbereitung dafür. Es geht um eine bewegliche Kamera. In den Teilen die darauf folgen werde ich dann mit dem LOD beginnen.

  6. Sehr schönes Tutorial
    Deine Artikelreihe hat mir als 3D-Neueinsteiger sehr vieles beigebracht…
    Nur schade das die Terrains nicht unendlich groß sein können.. ändert sich das noch?

    Hoffe der nächste Artikel lässt nicht mehr allzulange auf sich warten? Bin nämlich gespannt wie es weitergeht 😉

  7. Ist der nächste Artikel bereits in Planung? Bin schon ganz gespannt, wie die Serie weitergeht 😀 Ich hoffe er kommt bald…

  1. Pingback: Terrain 101: Transformationen « "Mit ohne Haare"

  2. Pingback: Terrain 101: Das erste Dreieck « "Mit ohne Haare"

  3. Pingback: Terrain 101: Land in Sicht « "Mit ohne Haare"

  4. Pingback: Terrain 101: Kamera ab und Action « "Mit ohne Haare"

  5. Pingback: Terrain 101: Technische Rafinessen « "Mit ohne Haare"

  6. Pingback: Terrain 101: Vertex- und Index-Buffer « "Mit ohne Haare"

  7. Pingback: Terrain 101: Die Entwicklungsumgebung und das XNA 4.0 Projekt « "Mit ohne Haare"

  8. Pingback: Terrain 101: Transformationen « "Mit ohne Haare"

  9. Pingback: Terrain 101: Eine Einführung in dreidimensionale Landschaften in Echtzeit « "Mit ohne Haare"

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: