XNA in 3-D

3-D-Spiele mit XNA erstellen
Kommentare

Obwohl man mit OrbUI und SpriteBatches durchaus ansprechende Spiele entwerfen kann, führt der Weg in die Herzen der Spieletester in der Regel über die dritte Dimension.

Als der Autor in der Mobilcomputerbranche begann, war das Erstellen von 3-D-Engines eine hohe Kunst, die enorme mathematische Fertigkeiten voraussetzte. Gott sei Dank erspart uns XNA einen Gutteil der arithmetischen Arbeit. Ein wenig theoretische Vorbereitung muss trotzdem sein.

Bzz, sprach die Fliege …

Der dreidimensionale Raum wird – nomen est omen – durch drei Koordinaten namens x, y und z beschrieben. Sie sind in Abbildung 1 grafisch dargestellt. Gibt man die Abstände vom Ursprung in die Richtung x, y oder z an, ist der Aufenthaltsort eines Punkts in Relation zum Ursprungspunkt der Koordinaten eindeutig beschrieben. Klassisch werden dreidimensionale Objekte (in der Fachsprache als Modelle bezeichnet) durch Punktwolken dargestellt. Ein einzelner Punkt wird dabei als Vertex bezeichnet. Edges verbinden Vertices. Von drei oder vier Vertices begrenzte Flächen werden als Faces bezeichnet. Oft haben Modelle einen eigenen Mittelpunkt, von dem aus alle Dimensionen der Vertices und Faces angegeben werden. Das klingt zunächst verwirrend, hat aber Sinn, denn nur so kann man dasselbe Modell an mehreren Stellen im dreidimensionalen Raum „recyceln“.

Abb. 1: Die Z-Achse
Abb. 1: Die Z-Achse „wächst“ aus dem Bildschirm heraus
… und flog davon

Wie in der Geodäsie gibt es auch im Bereich der dreidimensionalen Grafik drei grundlegende Transformationen, die ein Objekt verändern können. Sie werden durch so genannte Matrizen dargestellt, die man sich als spezielles 3×3- oder 4×4-Array von Zahlen vorstellen kann. Wer sich für Matrixmathematik interessiert, kann sich im Internet weiterbilden. Hinter der Skalierung verbirgt sich das Vergrößern oder Verkleinern eines Objekts um einen Wert. Die Rotation dreht das Objekt, die Translation schiebt es im dreidimensionalen Raum herum. Wie weiter oben festgestellt, sind die Punkte eines Modells in der Regel in sich selbst geschlossen und haben als Ursprung einen Punkt innerhalb des Modells. Durch die Weltmatrix wird das Modell an seinen Aufenthaltsort in der 3-D-Szene geschoben, die View-Matrix verschiebt es an einen darstellbaren Ort. Bleibt nur noch das Problem der Darstellung: Die meisten Bildschirme sind nach wie vor nur zweidimensional. Also muss die dreidimensionale „Welt“ in ein zweidimensionales Abbild transformiert werden. Dabei kommen in 3-D-Spielen so gut wie immer perspektivische Projektionen zum Einsatz. Sie zeichnen sich dadurch aus, dass alle Punkte zu einem „Fluchtpunkt“ laufen, wie in Abbildung 2 dargestellt. Zwei Ebenen begrenzen den sichtbaren Bereich, der in einem Punkt zusammenläuft. Sichtbar ist also nur, was sowohl innerhalb des „Kegels“ als auch innerhalb der beiden Begrenzungsebenen liegt.

Abb. 2: Der Fluchtpunkt ist klar erkennbar
Abb. 2: Der Fluchtpunkt ist klar erkennbar
Modelle aus der Dose

Damit können wir uns an das Erstellen einer primitiven Klasse wagen. Sie soll unsere Modelle verwalten. Die Klasse SmartModel enthält folgende Felder:

public Vector3 myPos;
public Vector3 myRot;
public Vector3 myScale;
private Model myModel;
private Matrix[] myTransforms;

Translation, Rotation und Skalierung werden durch je einen dreidimensionalen Vektor abgebildet. Insbesondere bei der Skalierung mag das nutzlos klingen, allerdings gibt es axonometrische Ansichten, in denen jede Achse seperat skaliert zur Darstellung kommt, das ist insbesondere in den Ingenieurwissenschaften nützlich.

Die Membervariable Model enthält das eigentliche Modell, das auf den Bildschirm gezeichnet wird. In myTransforms werden die auf jeden Teil des Modells anzuwendenden Transformationen gespeichert. Im Konstruktor werden die Parameter in die Variablen übertragen (Listing 1). Während das Kopieren des Modells und der Vektoren keiner weiteren Erklärung bedarf, ist die Erstellung der Transformationsmatrix schon wesentlich interessanter. Im ersten Schritt wird sie erstellt. Als Größe wird die Anzahl der Bones des Modells gezählt. Die Bezeichnung „Bones“ klingt etwas skurril und macht erst Sinn, wenn man versteht, wie ein XNA-Modell intern aufgebaut ist: Jeder Teil des Modells (als Mesh bezeichnet) steht auf einem „Knochen“, der ihn an seine vorgesehene Position „hievt“.

Listing 1

public SmartModel(Model _model, Vector3 _pos, Vector3 _rot, Vector3 _scale)
{
  myModel = _model;
  myTransforms = new Matrix[myModel.Bones.Count];
  myModel.CopyAbsoluteBoneTransformsTo(myTransforms);

  this.myPos = _pos;
  this.myRot = _rot;
  this.myScale = _scale;
}
  

Durch den Befehl CopyAbsoluteBoneTransformsTo wird dieses „Knochen-Array“ aus dem Modell erstellt. Nun fehlt die Zeichenroutine, die das Modell auf den Bildschirm malt. Sie sieht wie in Listing 2 aus.

Listing 2

public void Draw(Matrix _view, Matrix _pro)
{
  Matrix absoluteWorld = Matrix.CreateScale(myScale) * Matrix.CreateFromYawPitchRoll(myRot.Y, myRot.X, myRot.Z) * Matrix.CreateTranslation(myPos);
  foreach (ModelMesh m in myModel.Meshes)
  { 
    Matrix localWorld=myTransforms[m.ParentBone.Index] * absoluteWorld;
    foreach (ModelMeshPart mp in m.MeshParts)
    {
      BasicEffect effect = (BasicEffect) mp.Effect;
      effect.Projection = _pro;
      effect.View = _view;
      effect.World = localWorld;
      effect.EnableDefaultLighting();
    }
    m.Draw();
  }
}
  

Diese Routine verlangt zwei Parameter: eine Ansichtsmatrix und eine Projektionsmatrix. Erstere wird später durch den Aufruf von CreateLookAt erstellt. Der erste Parameter gibt dabei die Position der Kamera im Raum an, der zweite den Blickpunkt und der dritte legt fest, wo oben ist. Die Projektionsmatrix _pro folgt aus CreatePerspectiveFieldOfView. Sie legt fest, wie viel Bereich um den von der Kameramatrix festgelegten Sichtstrahl sichtbar sein wird (und wird später im Detail besprochen).

Im nächsten Schritt wird die so genannte Weltmatrix erstellt. Sie legt fest, welche Transformationen auf den Ursprung des Modells angewendet werden müssen, um ihn an seinen endgültigen Standpunkt im dreidimensionalen Raum zu schieben. Die Weltmatrix entsteht dabei aus dem Ineinandermultiplizieren der Skalierungsmatrix, der Rotationsmatrix und der Verschiebungsmatrix. Die Reihenfolge ist wichtig und sollte nicht verändert werden.

Durch die foreach-Iteration wird das Modell in seine Einzel-Meshes zerlegt. Wie bereits erwähnt, hat jeder Teil des Meshes einen eigenen Knochen, der nun zur allgemeinen Weltmatrix „addiert“ wird, um jeden Teil an seinen Platz zu bringen. Die innereforeach-Iteration zerlegt den Mesh in seine Einzelteile und weist jedem Einzeilteil einen so genannten Effekt zu. Wir werden uns mit diesen Effekten im nächsten Teil genauer befassen. Zum jetzigen Zeitpunkt genügt es, wenn Sie Effekte als Zeichenbefehle betrachten. Sie legen fest, wie der Mesh auf den Bildschirm gezeichnet werden soll.

BasicEffect braucht eine Welt-, eine Projektions- und eine Sichtmatrix, zusätzlich eine Anweisung über die Art der Berechnung der Beleuchtung. Wird all das übergeben, kann der Mesh durch den Aufruf von Draw() zum Zeichnen freigegeben werden. Damit ist die Modellklasse komplett und wir können wieder zur Game Loop zurückkehren. Zuerst laden wir dabei das Modell in LoadContent (Listing 3).

Listing 3

protected override void LoadContent()
{
  spriteBatch = new SpriteBatch(GraphicsDevice);

  Model aModel = Content.Load("affe");
  SmartModel aSmartModel = new SmartModel(aModel, new Vector3(0, 0, 0), new   Vector3(MathHelper.ToRadians(90), 0, 0), new Vector3(200, 200, 200));
  myModels = new List(1);
  myModels.Add(aSmartModel);
}
  

Der Aufruf von Content.Load wurde bereits im ersten Teil der Serie besprochen. Er weist die XNA Content Pipeline an, das Content-Projekt nach dem angegebenen Inhalt zu durchsuchen. In diesem Fall handelt es sich um eine mit dem 3-D-Modellierungsprogramm „Blender“ (automatisch) erstellte Affenfigur, die im .fbx-Format abgespeichert ins Content-Projekt kopiert wurde (Abb. 3).

Nach dem Laden des Modells wird sie in eine SmartModel-Klasse gepackt. Als Translation und Rotation werden Nullwerte übergeben. Die von Blender sehr klein erstellte Figur wird durch das Übergeben eines relativ großen Zoomfaktors „angenehm“ hochskaliert. Das Zeichnen erfolgt in Draw() (Listing 4). Wie im ersten Teil der Serie wird auch hier zunächst der Hintergrund gelöscht. Danach wird eine Ansichts- und eine Projektionsmatrix erstellt. Die Ansichtsmatrix wurde bereits erläutert. Die Erstellung der Projektionsmatrix erfolgt durch den Aufruf von CreatePerspectiveFieldOfView. Diese Methode übernimmt vier Parameter: das Gesichtsfeld in Radiant, das Größenverhältnis (Länge zu Breite) sowie das „nahe“ und „ferne“ Ende des Sichtfelds. Der Sehbereich von 60 Grad wird durch den Aufruf von MathHelper.ToRadians in Radiant konvertiert. Die Aspektrate folgt aus der GraphicsDevice-Klasse. Als Nah- und Ferngrenze werden mehr oder minder beliebige Werte festgelegt. Sind beide Matrizen komplett, folgt das Aufrufen der Draw()-Routine aller SmartModels.

Listing 4

protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);
  Matrix view = Matrix.CreateLookAt(new Vector3(0, 200, 1500), new Vector3(0, 0, 0), Vector3.Up);
  Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(60), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10000f);
  
  foreach (SmartModel sm in myModels)
  {
sm.Draw(view, projection);
  }

  base.Draw(gameTime);
}
  

Die Steuerung erfolgt wie schon im Beispiel zur 2-D-Grafik in der Methode Update. In unserem Beispiel sieht sie wie in Listing 5 aus. Im ersten Schritt wird hier, wie in allen anderen XBox-Spielen, geprüft, ob der Back-Button gedrückt wurde. Ist das der Fall, wird die Anwendung geschlossen. Im nächsten Schritt holt sich das Programm eine Instanz des GamePadState-Objekts für den ersten Spieler. Obwohl eine derartige Festverdrahtung in XNA-Spielen zum Ausschluss aus dem Vertrieb führt, reicht eine solche Vorgangsweise für unser Beispiel völlig. Im nächsten Schritt wird der Skalierungs-Level des Affen angepasst, wenn die „Nach oben“- oder „Nach unten“-Taste des Steuerkreuzes am Controller gedrückt wurde. Danach werden die Werte der beiden Analog-Controller abgefragt und in Positionsparameter für den Affen umgewandelt. Wenn man die Anwendung nun ausführen würde, könnte man den Affen nach Belieben auf dem Bildschirm herumschieben.

Listing 5

protected override void Update(GameTime gameTime)
{
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

  GamePadState gs = GamePad.GetState(PlayerIndex.One);

  if (gs.DPad.Down == ButtonState.Pressed)
  {
myModels[0].myScale.X = myModels[0].myScale.X - 10;
myModels[0].myScale.Y = myModels[0].myScale.Y - 10;
myModels[0].myScale.Z = myModels[0].myScale.Z - 10;
  
  }
  if (gs.DPad.Up == ButtonState.Pressed)
  {
myModels[0].myScale.X = myModels[0].myScale.X + 10;
myModels[0].myScale.Y = myModels[0].myScale.Y + 10;
myModels[0].myScale.Z = myModels[0].myScale.Z + 10;
  }

  Vector2 myLeftVect = gs.ThumbSticks.Left;
  myModels[0].myPos.X += myLeftVect.X*10;
  myModels[0].myPos.Y += myLeftVect.Y*10;

  Vector2 myRightVect = gs.ThumbSticks.Right;
  myModels[0].myRot.X += myRightVect.Y*-0.01f;
  myModels[0].myRot.Y += myRightVect.X*0.01f;

  base.Update(gameTime);
}
  
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -