Irrlicht - from Noob to Pro: 3D-Objekte verstehen


Um zu verstehen, wie Objekte in einer Engine dargestellt werden, muss man das System kennen, nach welchem vorgegangen wird. Mit der Einführung der Tiefenachse, welche je nach verwendeten Koordinatensystem die Z- oder die X-Achse ist, wurde der Raum der Computerspiele auf die dritte Dimension erweitert.

In dieser dritten Dimension werden Objekte erstellt, geometrischen Transformationen unterworfen, werden unsichtbar gemacht, bekommen Eltern, an deren Ursprungskoordinaten sie sich orientieren, unterliegen dem Culling, werden in Ihre Einzelteile zerlegt und erliegen schließlich wieder dem Löschvorgang, wenn sie nicht mehr gebraucht werden.

Es werden in diesem Buch noch viele dieser Vorgänge beschrieben, doch behandeln wir nun mal die grundlegenden Dinge.

Das Koordinatensystem von Irrlicht

Bearbeiten

Irrlicht verwendet das linkshändige Koordinatensystem, welches vorschreibt, das

  • die Y-Achse nach oben,
  • die X-Achse nach rechts und
  • die Z-Achse nach hinten

zeigt. Das sieht als Darstellung so aus :
 

Ein kleines Beispiel

Bearbeiten

Hierzu ein kleines Beispiel zur Darstellung des Koordinatensystems. Wir wollen ein Kreuz entlang aller 3 Achsen in verschiedenen Farben zeichnen. Der besseren Darstellung halber, verwenden wir für die Achsen die Farben wie im Bild hier angegeben (rot für X, grün für Y, blau für Z). Irrlicht stellt uns eine Funktion zur Verfügung, welche es vereinfacht, Linien im 3-dimensionalen Raum zu zeichnen. Die Deklaration lautet:

draw3DLine (const core::vector3df &start, const core::vector3df &end, SColor color=SColor(255, 255, 255, 255))

Die Funktion erwartet von uns 2 Vektoren (auch "Vertex" genannt), welche bestimmen, von wo nach wo die Linie gezeichnet werden soll sowie einen Farbwert.

Tipp:

 

Es handelt sich hier also um Positionsvektoren. Es gibt des weiteren noch Richtungsvektoren, welche eine Richtung beschreiben. Diese werden später im Buch behandelt.

Um zu bestimmen, welche Farbe die Linie hat, muss sie in einer SColor-Variable übergeben werden. Diese ist im ARGB-Format, was für Alpha (Helligkeit), Red (Rotanteil der Farbe), Green (Grünanteil der Farbe) und Blue (Blauanteil der Farbe) steht. Wollen wir also eine rote Farbe, so müssten wir video::SColor(255,255,0,0) als Farbwert angeben. Es soll ein Strich in der jeweiligen Farbe entlang seiner Achse mit einer Länge von 1 Einheiten gezeichnet werden, wobei der Mittelpunkt des Strichs auf dem Nullpunkt ist. Das würde dann so aussehen :
 
Hierzu der entsprechende Code :

//Ein Material deklarieren
video::SMaterial material;

//Das Material nimmt kein Licht an
material.Lighting = false;

//Während das Device aktiv ist ...
	while(device->run())
	{		
		//Szene beginnen
		driver->beginScene(true, true, SColor(3,150,203,255));
		
		//Das Material festlegen
		driver->setMaterial(material);
		
		//Hier wird die Szene gezeichnet (Objekte, Meshes, 2D Bitmaps usw.)
		//Rote Linie (X-achse)
		driver->draw3DLine(core::vector3df(-0.5f, 0, 0), //Startvektor
				   core::vector3df(0.5f, 0, 0),  //Schlußvektor
				   video::SColor(255,255,0,0));  //Farbwert
		//Grüne Linie (Y-Achse)
		driver->draw3DLine(core::vector3df(0, -0.5f, 0), //Startvektor 
				   core::vector3df(0, 0.5f, 0),  //Schlußvektor
			           video::SColor(255,0,255,0));	 //Farbwert
		//Blaue Linie (Z-Achse)
		driver->draw3DLine(core::vector3df(0, 0, -0.5f), //Startvektor
				   core::vector3df(0, 0, 0.5f),  //Schlußvektor
				   video::SColor(255,0,255,0));	 //Farbwert

		//Szene beenden
		driver->endScene();
	}
Tipp:

 

Wie Sie sicherlich bemerkt haben, habe ich mit
//Ein Material deklarieren
video::SMaterial material;

//Das Material nimmt kein Licht an
material.Lighting = false;

ein Material deklariert, welches kein Licht annimmt. Das bewirkt, das auch keine Beleuchtung für dieses Material berechnet wird, wodurch es als voll beleuchtet behandelt wird. Würde das nicht passieren, dann erscheinen die Linien schwarz eingefärbt, egal welche Farbe ihnen gegeben wurde, da in unserem Beispiel noch keine Lichtquelle vorhanden ist.


Striche sind von hinten/vorne nicht sichtbar

Bearbeiten

Wenn Sie nun in Ihrem Projekt auf "Erstellen" -> "Projektmappe erstellen" klicken, wird der Quellcode kompiliert und zu einer Exe-Datei gelinkt. Wenn Sie diese nun ausführen, dann können Sie zwar die X- und Y- Achse dargestellt sehen, jedoch nicht die Z-Achse. Das liegt daran, dass wir in der Kameragrundstellung in Irrlicht in Richtung Z-Achse blicken.
 

Momentan haben wir noch keine Kamerasteuerung implementiert, aber uns kann dieser kleine "Trickbetrug" helfen, unsere Z-Achse sichbar zu machen :

//Blaue Linie (Z-Achse)
driver->draw3DLine(core::vector3df(-0.25f, -0.25f, -0.5f), //Startvektor
		   core::vector3df(0.25f, 0.25f, 0.5f),  //Schlußvektor
		   video::SColor(255,0,0,255));	 //Farbwert

Dies macht die Z-Achse nun sichtbar, da der Start- u. Schlußvektor der blauen Linie nun schräg entlang der X- u. Y-Achse läuft. Dies ist momentan allerdings nur zur Sichtbarmachung angewandt worden und ist keine gängige Praxis.
 

Dreiecke in der 3D-Welt

Bearbeiten

In jedem 3D-Spiel bestehen Objekte aus Dreiecken, da dies die kleinste zu definierende Fläche ist. Alles was weniger als 3 Eckpunkte hat ist entweder ein Strich oder ein Punkt. Objekte bestehen aus Flächen, welche beliebig oft in einzelne Dreiecke unterteilt werden können (solange es die Computerleistung zulässt).

Tipp:

 

Der Faktor, wie stark ein Objekt in Dreiecke bzw. Polygone unterteilt ist, nennt man Level of Detail (LOD). Es gibt meist LOD-Stufe 0 (einfach dargestellt) bis LOD-Stufe 4 (präzise Darstellung), welche es erlaubt, Objekte auf Computer mit verschiedenen Rechenleistungen darzustellen.

Vektoren, Indicies & Co.

Bearbeiten

Wir haben nun ein Dreieck, welches aus 3 Eckpunkten besteht. Wollen wir nun eine Raute zeichnen, dann haben wir 6 Eckpunkte, richtig ? Leider nicht ganz, denn die 2 Dreiecke hängen ja zusammen und es kommt ja nur ein Eckpunkt hinzu. Also müssten wir nur 4 Eckpunkte zu definieren, um das gewünschte Ergebnis zu erreichen. Doch was machen wir, wenn wir 2 Rauten definieren wollen ? Nun, dann erleichtern wir uns die Arbeit und vergeben jedem Eckpunkt einen Index (in der Mehrzahl Indicies genannt), wodurch wir eine tabellenartige Struktur in die Definition unseres Objekts bekommen.

Ein Dreieck definieren

Bearbeiten

Wir wollen nun anhand einer vorgegebenen Zeichnung ein Dreieck definieren. Hierzu folgendes Bild :
 
In diesem Beispiel lassen wir der Einfachheit halber die Z-Achse auf 0,0. Hieraus ergeben sich dann folgende Werte :

Punkt X Y Z
A -0,5 -0,3 0,0
B 0,5 -0,3 0,0
C 0,0 0,5 0,0

Nun haben wir also unsere 3 Vektoren, welche aus den Punkten A, B und C bestehen und ein Dreieck definieren.

Vom Dreieck zur Raute

Bearbeiten

Würden wir nun eine Raute definieren wollen, so müssten wir nur einen Index mehr definieren, denn die anderen zwei Eckpunkte des zweiten Dreiecks sind ja schon durch das erste Dreieck definiert. Also erhalten wir eine Liste von 4 Vektoren, welche zwei Dreiecke darstellen.
 
Anhand dieser Darstellung wissen wir, aus welchen Dreiecken die Raute besteht. Definieren wir nun mal unsere Raute in C++ ...

Vektorenliste aka Vertex-Liste

Bearbeiten

Im der englischen Sprache ist es üblich, einen Positionsvektor als Vertex (Mz. Vertices) zu bezeichnen. Daher werde ich nun zur im WWW üblichen Wortwahl wechseln. Definieren wir also nun die Vertices unserer Raute :

float fVertices[12] = {-0.5f, -0.3f, 0.0f, //Punkt A
			0.5f, -0.3f, 0.0f, //Punkt B
			0.0f,  0.5f, 0.0f, //Punkt C
			0.6f,  0.6f, 0.0f};//Punkt D

Nun wollen wir noch festlegen, aus welchen Eckpunkten unsere Dreiecke bestehen. Dazu verwenden wir eine Indicies-Liste :

unsigned short int uiIndicies [6] = {0, 1, 2, //ABC (=linkes Dreieck)
				     2, 1, 3};//CBD (=rechtes Dreieck)

Wir haben nun im Array fVertices unsere Vertices definiert. Das Array uiIndicies legt fest, aus welchen Vertices die Dreiecke bestehen und in welcher Reihenfolge sie gezeichnet werden. Leider ist damit noch nicht alles abgetan, denn in dieser Reihenfolge würde uns Irrlicht nichts anzeigen. Und das hat einen recht guten Grund: das Culling.

(Backface) Culling

Bearbeiten

Das Culling bestimmt, welche Dreiecke gezeichnet werden und welche nicht. Das wird dadurch erkannt, ob die Dreiecke im Uhrzeigersinn oder gegen den Uhrzeigersinn gezeichnet werden. Stellen wir uns folgendes Beispiel vor :
Sie stehen vor einer Plexiglasscheibe und malen im Uhrzeigersinn ein Dreieck darauf, indem Sie von Punkt A nach C und dann nach B eine Linie zeichnen. Hinter der Plexiglasscheibe steht auch jemand und malt wie Sie ein Dreieck in der selben Reihenfolge. Für Sie zeichnet der andere das Dreieck im Gegenuhrzeigersinn, da er anders herum steht. Bei dem jenigen hinter der Scheibe ist es genau so, wenn er Ihre Bewegungen verfolgt.
Dieses Prinzip kann man auch für 3D-Objekte verwenden und es spart enorm an Rechenleistung, wenn man nicht sichtbare Dreiecke nicht zeichnet. Jedes Dreieck, welches in der 3D-Welt um 180 Grad gedreht wird, kehrt dadurch aus der Sicht des Betrachters die Darstellungsreihenfolge um, was es unsichtbar macht. Würde man das Culling hier nun nicht anwenden, würde man die Innenseite eines Objekts zeichnen, welche sowieso nicht sichtbar ist, da sie durch die Vorderseite des Objekts verdeckt wird (wenn nicht, dann sieht man ein Loch im Objekt, da die Innenseiten beim Culling dann ja ausgeblendet werden).

Das Culling am Dreieck

Bearbeiten

In Irrlicht ist das Culling als Standard so eingestellt, dass Dreiecke nur dargestellt werden, wenn Sie im Uhrzeigersinn dargestellt werden. Dies lässt sich aber auch anders einstellen :

 //In den Materialeigenschaften
 material.BackfaceCulling = false;
 material.FrontfaceCulling = true;
 //In den Objekteigenschaften
 Node1->setMaterialFlag(EMF_BACK_FACE_CULLING, false); //oder true
 Node1->setMaterialFlag(EMF_FRONT_FACE_CULLING, true); //oder false
Tipp:

 

Falls mal das Culling nicht so funktioniert, wie es sollte, dann können Sie abwechseln Frontface-Culling und Backface-Culling einschalten, um zu sehen, welches Dreieck falsch definiert wurde. Schalten Sie nicht versehentlich beides ein, da sonst alle Dreiecke unsichtbar werden.

Wenn wir unsere Raute (aus dem vorhergehenden Bild) darstellen wollen, so müssten wir über die Reihenfolge der Indicies festlegen, in welcher Reihenfolge gezeichnet wird. Im Uhrzeigersinn wären für das linke Dreieck die Strecke CBA und für das rechte Dreieck die Strecke CDB möglich. Im Indicies-Array würde das wie folgt aussehen :

unsigned short int uiIndicies [6] = {2, 1, 0, //CBA (linkes Dreieck)
				     2, 3, 1};//CDB (rechtes Dreieck)

Wenn Sie nun alles verstanden haben, können wir unser erstes Dreieck in Irrlicht darstellen ! Bitte lesen Sie hierzu auf der nächsten Seite weiter.

Der Quellcode zum Beispiel

Bearbeiten
//Einbinden der Header-Datei von Irrlicht
#include <irrlicht.h>

//Einbinden der Namespaces
using namespace irr;
using namespace core;
using namespace video;
//Die Hauptprozedur main()
int main()
{
	//Unser Irrlicht-Device erstellen und initialisieren
	IrrlichtDevice *device =
		createDevice( video::EDT_OPENGL, dimension2d<u32>(640, 480), 32,
			false, false, false, 0);
	
	//Konnte das Device erstellt werden ?
	if (!device)
		return 1; //Falls nicht, Fehlercode zurückgeben und Programm abbrechen
	
	//Den Text des Hauptfensters festlegen
	device->setWindowCaption(L"Das Koordinatensystem");
	
	//Den Videotreiber erstellen und Zeiger aus dem Device abholen
	IVideoDriver* driver = device->getVideoDriver();	
	
	//Ein Material deklarieren
	video::SMaterial material;
	//Das Material nimmt kein Licht an
	material.Lighting = false;
	//Das Material soll voll gezeichnet werden
	material.Wireframe = false;
	
	//Während das Device aktiv ist ...
	while(device->run())
	{		
		//Szene beginnen
		driver->beginScene(true, true, SColor(3,150,203,255));

		driver->setMaterial(material);
		
		
		//Hier wird die Szene gezeichnet (Objekte, Meshes, 2D Bitmaps usw.)
		//Rote Linie (X-achse)
		driver->draw3DLine(core::vector3df(-0.5f, 0, 0), //Startvektor
	                           core::vector3df(0.5f, 0, 0),  //Schlußvektor
		                   video::SColor(255,255,0,0));  //Farbwert
		//Grüne Linie (Y-Achse)
		driver->draw3DLine(core::vector3df(0, -0.5f, 0), //Startvektor 
				   core::vector3df(0, 0.5f, 0),  //Schlußvektor
			           video::SColor(255,0,255,0));	 //Farbwert
		//Blaue Linie (Z-Achse)
		driver->draw3DLine(core::vector3df(-0.25f, -0.25f, -0.5f), //Startvektor
				   core::vector3df(0.25f, 0.25f, 0.5f),  //Schlußvektor
				   video::SColor(255,0,0,255));	 //Farbwert
		//Szene beenden
		driver->endScene();
	}
	//Das Device freigeben
	device->drop();
	
	//Keinen Fehler zurückgeben
	return 0;
}