EINFÜHRUNG

Original Author: Damiano Vitulli

Translation by: Click here

Vektoren und Normalen... große Worte! Sicherlich, die meisten von euch müssten in der Schule darauf gestoßen sein, entweder während des Physik- oder Geometrieunterrichts. Du hättest aber nie gedacht, dass du eines Tages diese Begriffe verwenden würdest, um eine 3D-Engine zu erstellen! Wie auch immer, mach dir nicht zu viele Sorgen, du wirst deine alten verstaubten Bücher nicht wieder aufschlagen müssen! In dieser Lektion werden wir uns mit Vektoren im Detail befassen, und wir werden eine Bibliothek programmieren, um sie zu verwalten, die Beleuchtung in der Welt.

Die erste praktische Anwendung von Vektoren beschäftigt sich mit den Berechnungen für die Beleuchtung in der Welt. Viele von euch könnten sich fragen, warum wir uns diese Mühe machen, weil klar ist, dass die Szene bereits gleichmäßig ausgeleuchtet ist! Warum müssen wir dann Lichter einfügen?

Bis jetzt haben wir unsere virtuelle Welt mit einer einheitlichen, unrealistischen Beleuchtung gezeichnet. Leuchtende Objekte meint, sie mit glänzenden und schattierten Bereichen zu zeichnen, was genauer zeigt, wie sie in der wirklichen Welt aussehen würden.

Unordnung und Chaos sind die Schlüsselwörter, auf die ihr euch bei eurer 3D-Engine besinnen solltet. Aber bitte, euer Quellcode muss sowohl geordnet als auch klar und gut sein. =)

Mit dieser Lektion werden wir den Realismus unserer virtuellen Welt erhöhen.

GRUNDLEGENDE BEGRIFFE ZU GERADEN LINIEN, VEKTOREN UND NORMALEN

Ok, Jungs, jetzt müssen wir uns etwas mit Theorie befassen.

Denkt daran, dass alles was wir studieren werden wesentlich ist, um die Berechnungen bezüglich der Beleuchtung durchzuführen. Also noch einmal, nehmt euch euer Lieblingsprogrammierungsgetränk und macht euch bereit!

Wißt Ihr, was gerade Linien und Strecken sind? Ich hoffe es! Hier ist trotzdem eine kurze Erläuterung der Grundlagen:

GERADE LINIE

Eine gerade Linie ist eine endlose Linie, die weder einen Anfang noch ein Ende hat, aber eine Richtung.

Sie kann durch die einfache Gleichung y = ax + b dargestellt werden.

Zwei Geraden, welche die gleiche Richtung haben, nennt man parallele Geraden.

Zwei Geraden, die einen Winkel von 90° bilden, nennt man senkrechte Geraden.

STRECKE

Wenn wir zwei Punkte A und B haben, die zu einer geraden Linie gehören, dann wird jener Linienteil dazwischen Strecke genannt. Zur Darstellung einer Strecke wird oft über die beiden Buchstaben ein Bindestrich gesetzt: Segment.png.

Die Strecke zwischen den Punkten A und B kann auf zwei Arten ausgeführt werden:

  1. von A nach B
  2. von B nach A

Eine Strecke mit einer zugeordneten Richtung heißt gerichtete Strecke, dargestellt durch einen Pfeil über den beiden Endpunkten: Oriented segment.png und wird Vektor genannt.

VEKTOR

Hier nun die Definition eines Vektors:

(1) Ein Vektor ist eine Größe und wird durch Strecken gleichen Betrags und gleicher Richtung dargestellt.

Dies bedeutet, zwei Vektoren sind gleich, wenn sie die gleiche Richtung und die gleiche Länge haben, unabhängig davon, ob sie die gleichen Anfangspunkte haben. Das war eine geometrische Definition eines Vektors und zeigt uns nicht den wahren Nutzen eines Vektors.

Das Konzept eines Vektors findet seinen Ursprung im Bereich der Physik. Es gibt einige Größen, die einfach mit einer Zahl gemessen werden können, die man als skalare Größen bezeichnet, wie z. B. Raumtemperatur oder Masse. Andere Größen, wie Geschwindigkeit oder Kraft, haben jedoch eine Richtung (Winkel und einen Richtungs- oder Durchlaufsinn) sowie eine numerische Komponente mit nur positiven Werten. Diese Größen heißen Vektoren.

Die grafische Darstellung eines Vektors ist der Pfeil. Er definiert auch die Lage (Winkel) und die Pfeilspitze zeigt den Richtungssinn. Die Länge des Pfeils stellt dabei die numerische Komponente, den Betrag eines Vektors dar.

Vektor-Pfeildarstellung   Vector gr.png

Zur Bestimmung eines Vektors werden unterschiedliche Schreibweisen verwendet.

  • Wenn wir den Anfang des Vektors als den Punkt O und das Ende als den Punkt P ausweisen, dann können wir den Vektor durch diese beiden Extrempunkte, die Punkte: OP algebraisch bestimmen.
  • Die meist benutzte Schreibweise zur Darstellung eines Vektors ist ein Großbuchstabe mit einem Pfeil darüber, Beispiel: Vector a.png

Man schreibt die numerische Komponente als Betrag eines Vektors (Betrag = Maßzahl der Länge), das Vektorsymbol wird dazu zwischen zwei senkrechte Striche (Absolutzeichen) gesetzt : BETRAG des Vektors Vector a.png = Vector a magnitude.png

Die letzte wichtige Definition ist hier der Einheitsvektor, auch Versor genannt:

EINHEITSVEKTOR (NORMIERTER VEKTOR, VERSOR)

Einheitsvektoren sind Vektoren mit der Norm (anschaulich: der Länge) Eins.

Oft benötigen wir in unserer 3D-Engine für einige Vektoren eine Umwandlung in Einheitsvektoren. Dieser Vorgang wird Normalisierung eines Vektors oder auch nur Normalisierung genannt. Einheitsvektoren gibt es deshalb nur in einem normierten Vektorraum.

Zur Bestimmung eines Einheitsvektors verwenden wir einen Kleinbuchstaben mit einem Pfeil darüber, Beispiel: Versor a.png

VEKTOREN IM DETAIL

Nun wollen wir beginnen, unsere Datenstrukturen zu organisieren, um diese seltsamen Vektoren zu verwalten! Das Beste, was zu tun ist, wenn man die Grafikmaschine um eine bestimmte Funktionalität erweitert, ist eine spezielle Bibliothek einzurichten, um den Code so sauber wie möglich zu machen. Deshalb fügen wir zwei leere Dateien: mat_vect.cpp und die zugehörige Header-Datei mat_vect.h in unsere Entwicklungsumgebung ein. Ich habe entschieden "mat" als prefix einzufügen, nur, um diese Bibliotheken als mathematische zu kennzeichnen.

Bitte, wirf einen Blick auf dieses Bild: Vector def.png

Wir haben unseren Vektor in drei Vektoren geteilt, die parallel sind zu den Achsen: x, y und z.

Jetzt können wir den Vektor mit dieser Schreibweise:

(2) Vector a.png = [(x2-x1),(y2-y1),(z2-z1)]

Und dann:

(3) Vector a.png = (Ax,Ay,Az)

(kartesische Darstellung eines Vektors)

Eine weitere Möglichkeit, einen Vektor darzustellen ist:

(4) Vector a.png = AxVersor i.png+AyVersor j.png+AzVersor k.png

Versor i.png, Versor j.png, Versor k.png sind die Einheitsvektoren (Versoren) der Koordinatenachsen

(Dreibein: senkrechtstehende Vektoren der Länge 1).


Und schließlich ist hier ein kleines Stück Code, mit (3) erstellen wir unsere Vektorstruktur:

typedef struct 
{
    float x,y,z;
} p3d_type, *p3d_ptr_type;

Wir haben den Vektortyp als einen einfachen 3D-Punkt bestimmt, warum?

Nun, wir haben den Job vereinfacht, weil wir mit Hilfe von (1) alle Vektoren vom Ursprung ausgehend betrachten. ;-)

ERZEUGUNG, NORMALISIERUNG UND LÄNGENBERECHNUNG

Um einen Vektor zu erstellen, benutzen wir Formel (2)

Wir schreiben eine Funktion mit drei Parametern Startpunkt, Endpunkt und finaler Vektor.

Alle Parameter sind Zeiger:

void VectCreate (p3d_ptr_type p_start, p3d_ptr_type p_end, p3d_ptr_type p_vector)
{
    p_vector->x = p_end->x - p_start->x;
    p_vector->y = p_end->y - p_start->y;
    p_vector->z = p_end->z - p_start->z;
    VectNormalize(p_vector);
}

Mach dir jetzt keine Sorgen über die Funktion VectNormalize, ich werde dies noch erklären, einen Augenblick noch.


Zur Berechnung der Länge eines Vektors (BETRAG) müssen wir den Satz des Pythagoras zweimal verwenden, und sag hier ja nicht, dass du davon nichts mehr weißt! Jedenfalls, hier nochmal eine kurze Zusammenfassung zur Erinnerung.


Wenn wir ein rechtwinkliges Dreieck haben, das ist ein Dreieck mit einem Innenwinkel von 90°, dann ist das Quadrat über der Hypotenuse, das ist die längste der 3 Seiten im ebenen rechtwinkligen Dreieck, gleich der Summe der beiden Kathetenquadrate über den beiden anderen Seiten dieses Dreiecks.


Jetzt ist es an der Zeit deine Phantasie zu nutzen...

ja, wir stellen uns einfach einen Vektor vor, frei im Raum liegend, ...der

am Achsenursprung O beginnt und beim Punkt P endet.

Wirf doch einfach einen Blick auf dieses Bild!

Das ist unser Vektor Vector a.png Vector length.png

Unser Vektor Vector a.png hat viele Eigenschaften. Eine wichtige Eigenschaft ist die Länge des Vektors, die uns jetzt besonders interessiert. Unser Bild zeigt diese Länge des Vektors als kürzeste Strecke zwischen den beiden Punkten O und P. Um diese Strecke rechnerisch zu bestimmen, wenden wir den Satz des Pythagoras auf das Dreieck OPQ an:

OP = Quadratwurzel von (Ay*Ay + OQ*OQ)

Nun ist Ay bekannt, um OQ zu berechnen, müssen wir den Satz des Pythagoras nochmals anwenden,

diesmal mit dem Dreieck OQR:

OQ*OQ = (Ax*Ax + Az*Az)

Wir setzen diesen Ausdruck in die erste Gleichung und finden die Länge des Vektors, seinen Betrag


Definitionen:

Unter dem Betrag eines Vektors versteht man die Maßzahl der Länge seiner Repräsentanten. 
|Vector a.png| =  Länge von Vector a.png =  Betrag von Vector a.png =  Maßzahl = {Zahlenwert} * [1]
|Vector a.png| = OP = Quadratwurzel von (Ax*Ax + Ay*Ay + Az*Az)

Und hier schreiben wir die Funktion in C:

float VectLenght (p3d_ptr_type p_vector)
{
    return (float)(sqrt(p_vector->x*p_vector->x + p_vector->y*p_vector->y + p_vector->z*p_vector->z));
}

Wir haben bereits gesagt, dass die Normalisierung eines Vektors die Umwandlung seiner Länge auf 1 bedeutet.

Dazu teilen wir alle Vektorkomponenten: Ax, Ay, Az durch ihre Länge (Betrag).

void VectNormalize(p3d_ptr_type p_vector)
{
    float l_lenght;

    l_lenght = VectLenght(p_vector);
    if (l_lenght==0) l_lenght=1;
    p_vector->x /= l_lenght;
    p_vector->y /= l_lenght;
    p_vector->z /= l_lenght;
}

SUMME UND DIFFERENZ

Es gibt vier grundlegende Operationen, die wir mit Vektoren ausführen können:

Summe, Differenz, Skalarprodukt und Vektorprodukt.

Die Summe zweier Vektoren Vector a.png und Vector b.png ist ein anderer Vektor Vector c.png.

An die Spitze von Vector a.png setzt man den Ursprung von Vector b.png und verbindet dann mit einer Linie den Ursprung von Vector a.png mit der Spitze von Vector b.png.

Spitze Vector a.png Ursprung Vector b.png Vector sum.png 

Wenn wir die beiden Vektoren mit der Schreibweise (3) darstellen, dann kann die Summe von zwei Vektoren auch mit Hilfe der analytischen Methode erhalten werden:

Vector a.png + Vector b.png = (Ax + Bx, Ay + By, Az + Bz)

Die Differenz zweier Vektoren ist ein besonderer Fall der Summe.

An die Spitze von Vector a.png setzt man den Ursprung von -Vector b.png und verbindet mit einer Linie den Ursprung von Vector a.png mit der Spitze von -Vector b.png.

Spitze Vector a.png Ursprung -Vector b.png Vector difference.png

Wir werden keine speziellen Funktionen für die Summe und die Differenz von Vektoren kreieren, weil es jetzt nichts nützt.

SKALARPRODUKT (PUNKTPRODUKT)

Wir haben zwei Möglichkeiten, um Vektoren zu multiplizieren:

  1. Skalarprodukt (Punktprodukt) sein Ergebnis ist eine einfache Zahl.
  2. Vektorprodukt (Kreuzprodukt) sein Ergebnis ist ein Vektor.

Das Skalarprodukt ist das Produkt der Beträge zweier Vektoren Vector a.png und Vector b.png mal dem Kosinus des Winkels zwischen ihnen.

Vector a.png dot Vector b.png = Vector a magnitude.png * Vector b magnitude.png * cos (Alpha.png) Vector scalar product.png

Dies ist ein geometrischer Ansatz, der nicht einfach in unseren Quellcode passt.

Wir müssen einen Weg finden und diese Definition in einem analytischen Ansatz zum Ausdruck bringen.

Mit unserer letzten Definition ist es leicht, diese Beziehungen für die Einheitsvektoren (Versoren) der Achsen zu erreichen:

(5) Versor i.png dot Versor i.png = Versor j.png dot Versor j.png = Versor k.png dot Versor k.png = 1

(6) Versor i.png dot Versor j.png = Versor j.png dot Versor k.png = Versor k.png dot Versor i.png = 0

...es werden zwei Vektoren definiert mit Hilfe der Definition (4):

Vector a.png = AxVersor i.png + AyVersor j.png + AzVersor k.png

Vector b.png = BxVersor i.png + ByVersor j.png + BzVersor k.png

...und wir bilden das Skalarprodukt

Vector a.png dot Vector b.png = (AxVersor i.png + AyVersor j.png + AzVersor k.png) dot (BxVersor i.png + ByVersor j.png + BzVersor k.png)

Mit (5) und (6) erhalten wir das Skalarprodukt als analytische Formel.

Vector a.png dot Vector b.png = Ax*Bx + Ay*By + Az*Bz

...und nun erstellen wir die Funktion VectScalarProduct:

float VectScalarProduct (p3d_ptr_type p_vector1,p3d_ptr_type p_vector2)
{
    return (p_vector1->x*p_vector2->x + p_vector1->y*p_vector2->y + p_vector1->z*p_vector2->z);
}

Wow! Es ist einfach verrückt!

VEKTORPRODUKT (KREUZPRODUKT)

Nach all diesen Vorstellungen fragt Ihr euch sicher, warum wir all diese Theorie untersuchen... aber Geduld Jungs, es wird alles zusammenkommen, wenn wir bei der "Beleuchtung" ankommen. =)

Nun, hier ist das Vektorprodukt...

Das Vektorprodukt zweier Vektoren, Vector a.png und Vector b.png, definiert einen anderen Vektor Vector c.png und ist auf folgende Weise zu berechnen:

  1. Das Vektorprodukt ist das Produkt der Beträge zweier Vektoren Vector a.png und Vector b.png mal dem Sinus des Winkels zwischen ihnen
  2. Die Richtung von Vector c.png ist senkrecht zur Ebene durch Vector a.png und Vector b.png gebildet und so ausgerichtet, dass eine rechtshändige Drehung um Vector c.png, Vector a.png in Richtung Vector b.png befördert, durch einen Winkelbereich von nicht mehr als 180°.


Vector c.png = Vector a.png x Vector b.png = Vector a magnitude.png * Vector b magnitude.png * sin (Alpha.png) Vector scalar product.png

Wie wir es bereits für das Skalarprodukt getan haben, müssen wir die analytische Herangehensweise an das Vektorprodukt finden.

...und wir schreiben einige Zusammenhänge auf:

(7) Versor i.png x Versor i.png = Versor j.png x Versor j.png = Versor k.png x Versor k.png = 0

(8) Versor i.png x Versor j.png = Versor k.png Versor j.png x Versor k.png = Versor i.png Versor k.png x Versor i.png = Versor j.png

Wir drücken jetzt die beiden Vektoren mit Hilfe der Definition (4) aus:

Vector a.png = AxVersor i.png + AyVersor j.png + AzVersor k.png

Vector b.png = BxVersor i.png + ByVersor j.png + BzVersor k.png

Vector a.png x Vector b.png = (AxVersor i.png + AyVersor j.png + AzVersor k.png) x (BxVersor i.png + ByVersor j.png + BzVersor k.png)

Nach ein paar Schritten mit Hilfe von (7) und (8) erhalten wir den Ausdruck des Vektorprodukts:

Vector a.png x Vector b.png = (AyBz - AzBy)Versor i.png + (AzBx - AxBz)Versor j.png + (AxBy - AyBx)Versor k.png

Schließlich erstellen wir die Funktion VectDotProduct:

void VectDotProduct (p3d_ptr_type p_vector1,p3d_ptr_type p_vector2,p3d_ptr_type p_vector3)
{
    p_vector3->x=(p_vector1->y * p_vector2->z) - (p_vector1->z * p_vector2->y);
    p_vector3->y=(p_vector1->z * p_vector2->x) - (p_vector1->x * p_vector2->z);
    p_vector3->z=(p_vector1->x * p_vector2->y) - (p_vector1->y * p_vector2->x);
}

Das war wie eine Vorlesung an der Universität, meinst du nicht?

Lasst uns jetzt ein wenig entspannen, weil wir das Licht einschalten werden. =D

LASST UNS UNSERE VIRTUELLE WELT BELEUCHTEN

Dies sind die wichtigsten Schritte, um unsere virtuelle Welt zu beleuchten:

  1. Zunächst ist es notwendig mindestens eine Lichtquelle im Raum zu definieren und zu aktivieren.
  2. Für jedes Polygon müssen wir die Menge des Lichts bestimmen, die es in die Lage versetzt dieses zu reflektieren und zum Auge unseres Betrachters zu übertragen.
  3. In der Renderingphase malen wir die Polygone nach ihrem Beleuchtungswert.

Wir werden jetzt diese verschiedenen Punkte im Detail analysieren.

LICHTPUNKTE DEFINIEREN UND AKTIVIEREN

Zur Umsetzung der Beleuchtung gibt es zwei wesentliche Elemente zu berücksichtigen:

die Lichtquelle und die Materialeigenschaften.

OpenGL enthält zur Lichterzeugung die 3 grundlegenden Komponenten: ambient, diffuse und specular.
  • Die Ambient Komponente ist das Licht, dessen Richtung unmöglich zu bestimmen ist, da es aus allen Richtungen zu kommen scheint. Es ist vor allem Licht, das mehrfach reflektiert. Ein Polygon ist immer gleichmäßig durch die Umgebung beleuchtet, es ist egal, welche Ausrichtung oder Position es im Raum hat.
  • Die Diffuse Komponente ist das Licht, das aus einer Richtung stammt, es hält unabdingbar den Winkel, den das Polygon in Bezug auf die Lichtquelle hat. Je senkrechter das Polygon zum Lichtstrahl steht, umso heller wird es werden. Die Position des Betrachters wird für diesen Einsatz nicht gebraucht und das Polygon ist immer gleichmäßig ausgeleuchtet.
  • Die Specular Komponente berücksichtigt den Grad der Neigung des Polygons und auch die Position des Beobachters. Specularlicht kommt aus einer Richtung und wird durch das Polygon nach seiner Neigung reflektiert.

Nun zurück zur main.c Datei... Für jeden Lichtpunkt, den wir umsetzen wollen, ist es notwendig, die verschiedenen Komponenten (ambient, diffuse, specular) dafür anzugeben:

GLfloat light_ambient[]=  { 0.1f, 0.1f, 0.1f, 0.1f };
GLfloat light_diffuse[]=  { 1.0f, 1.0f, 1.0f, 0.0f };
GLfloat light_specular[]= { 1.0f, 1.0f, 1.0f, 0.0f };

Wie Sie feststellen können, ist jede Komponente aus 4 Float-Werten zusammengesetzt,

welche die RGB und alpha "Kanäle" darstellen.

Wir haben gerade ein weißes Licht mit wenig Umgebungskomponente und eine maximale diffuse und spiegelnde Komponente definiert. Es ist genau das, was in den Tiefen des Weltraums geschieht!

Wir haben auch die Position der Lichtquelle anzugeben:

GLfloat light_position[]= { 100.0f, 0.0f, -10.0f, 1.0f };

Wir laden jetzt die eben erstellten Variablen in OpenGL mit der Funktion glLightfv:

glLightfv (GL_LIGHT1, GL_AMBIENT,  light_ambient);
glLightfv (GL_LIGHT1, GL_DIFFUSE,  light_diffuse);
glLightfv (GL_LIGHT1, GL_SPECULAR, light_specular);
glLightfv (GL_LIGHT1, GL_POSITION, light_position);
*void glLightfv( GLenum light, GLenum pname, const GLfloat *params );

...den Parametern einer einzelnen Lichtquelle werden Werte zugewiesen

Das erste Argument ist ein symbolischer Name für eine individuelle Lichtquelle. Das zweite Argument pname bestimmt die zu modifizierenden Parameter und können GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, GL_POSITION sein oder andere. Das dritte Argument ist ein Vektor und gibt an welchen Wert (oder Werte) dem Parameter zugeordnet werden.

Und los geht's, aktivier den Lichtpunkt:

glEnable (GL_LIGHT1);

...und die OpenGL-Beleuchtung:

glEnable (GL_LIGHTING);

SPIEGELVERMÖGEN DER POLYGONE BESTIMMEN

MATERIALIEN

Die Polygone, sowie die Beleuchtung haben einige grundlegende Eigenschaften. Sie können unterschiedliche Fähigkeiten haben, um das Licht zu reflektieren, so nennen wir diese Fähigkeit Material.

Die OpenGL-Materialien haben vier grundlegende Komponenten: ambient, diffus, specular und emittierende (emissive).

Die ersten drei Materialeigenschaften sind genau gleich den Lichteigenschaften, während die letzte Eigenschaft, die emittierende Komponente, das Licht aus dem Objekt simuliert. Das ist sehr nützlich, da es zur Simulation von Glühbirnen verwendet werden kann. Jedenfalls fügt sie keine anderen Lichtquellen hinzug und ist nicht von den anderen Lichtquellen beeinflusst.

Wir definieren nun ein Material mit all diesen grundlegenden Komponenten:

GLfloat mat_ambient[]= { 0.2f, 0.2f, 0.2f, 0.0f };
GLfloat mat_diffuse[]= { 1.0f, 1.0f, 1.0f, 0.0f };
GLfloat mat_specular[]= { 0.2f, 0.2f, 0.2f, 0.0f };
GLfloat mat_shininess[]= { 1.0f };

und los geht's, aktivier es durch die Funktion glMaterialfv:

glMaterialfv (GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv (GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv (GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv (GL_FRONT, GL_SHININESS, mat_shininess); 
  • void glMaterialfv( GLenum face, GLenum pname, const GLfloat *params ); Diese weist den Materialparametern Werte zu. Das Argument face kann sein GL_FRONT, GL_BACK or GL_FRONT_AND_BACK, das zweite Argument spezifiziert die zu modifizierenden Parameter, und kann sein GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, GL_SHININESS, GL_EMISSION. Das dritte Argument ist ein Vektor der spezifiziert, welcher Wert oder Werte werden dem Parameter zugeordnet.

Das gerade erstellte Material hat gute diffuse und spiegelnde Komponenten und eine niedrige Umgebungskomponente. Wir haben das Verhalten eines metallischen Materials simuliert, das von einer Lichtquelle in einem dunklen Raum beleuchtet wird... ;-)

INKLINATION DER POLYGONE IM VERGLEICH MIT DEM LICHTPUNKT

Neben den physikalischen Eigenschaften des Materials ist das wichtigste Element in der Beleuchtung der Neigungsgrad der Polygone in Bezug auf die Lichtquelle.

In der Tat ist es klar, dass je mehr die Polygonebene senkrecht zum "Strahl" des Lichtes steht, desto heller wird beleuchtet.

Zur Berechnung des Neigungsgrades ist es notwendig, zuerst den Polygon Normalenvektor zu berechnen:

Der Normalenvektor eines Polygons ist nichts als ein Einheitsvektor (normierter Vektor), der orthogonal zur Polygon Ebene steht.

Zur Berechnung des Normalenvektors (auch nur "Normalen") eines Polygons genügt es, das Vektorprodukt zweier Vektoren, die koplanar sind zu dem Polygon (wir können zum Beispiel zwei Seiten des Polygons nehmen) und dann das Ergebnis normalisieren. Es ist notwendig, auch einen anderen Vektor zu schaffen, dessen Ursprung mit dem Lichtpunkt korrespondiert und dessen Ende mit dem Ursprung unseres Normalenvektors. Lasst uns diesen neuen Vektor normalisieren.

Zuletzt ist der Beleuchtungsgrad zu berechnen, wir müssen nur das Skalarprodukt dieser beiden Vektoren bekommen!

Polygon_Normal dot Light_Vector Flat shading.png

Nichts leichter als das!

Der Endwert wird im Bereich 0.0 - 1.0 liegen und kann daher leicht zum Färben unseres Polygons (zum Beispiel als Faktor) verwendet werden.

FLAT SHADING

Wenn wir ein Verfahren des flachen Schattenwurfs auf jedes Polygon eines Objektes anwenden, so haben wir die Szene in einer bestimmten Methode beleuchtet, die man unter dem englichen Fachbegriff Flat Shading kennt.

Diese Methode wird heute nicht mehr benutzt, weil es die Objekte nicht in einer realistischen Art und Weise darstellt.

Beim Flat Shading wird jedem Polygon eine einheitliche Beschattung gegeben, wobei die Unterschiede zwischen den Farben benachbarter Polygone deutlich erkennbar sind.

GOURAUD SHADING

Eine realistischere Technik, die aber auch teurer ist, bietet Gouraud Shading, oft nur als Shading benannt.

Das wesentliche Merkmal dieses Verfahrens ist, anstatt die Polygone zu benutzen, werden die Normalen als Eckpunktnormalen verwendet.

Um die Wahrheit zu sagen, das Normal einer Ecke ist eine Definition, die nicht viel Sinn macht!

In der Tat, wir brauchen ein Flugzeug zur Berechnung eines Normalenvektors!

Die richtige Definition könnte stattdessen werden...

"Durchschnitt der Normalen der Polygone neben dem Scheitel (Spitze, Ecke)".

Denn zur Berechnung der Eckennormalen genügt das arithmetische Mittel, gebildet aus den Normalen der angrenzenden Polygone am Scheitelpunkt.

Hier siehst du sofort, was "Sache" ist: Gouraud shading.png

Wenn du erst einmal die Normalen der Eckpunkte erechnet hast, ist das Verfahren zur Berechnung der Beleuchtungskoeffizienten das gleiche, dennoch wird diesesmal jedes Polygon drei Beleuchtungskoeffizienten haben, nicht mehr nur einen.

Wie können wir drei Koeffizienten verwenden?

Einfach, wenn wir das Polygon zeichnen, setzen wir die Beleuchtung mit einer Linearinterpolation der Koeffizientenwerte mit den äußersten Punkten der einzelnen Scan-Linie in Beziehung. Das Ergebnis dieser schrittweisen Veränderungen ist viel realistischer und bietet eine Beleuchtung mit stufenweisen Variationen.

Und jetzt muss ich etwas gestehen: Wir werden nur noch die Normalen der Eckpunkte unseres Objektes berechnen müssen, OpenGL wird den Rest der Arbeit ausführen, Zeichnen der Szene in Gouraud Shading!

Warum nimmst du jetzt nicht eine Pause? ;-)

NORMALENBERECHNUNG

In diesem Absatz werden wir eine Funktion programmieren, die alle Normalen der Eckpunkte in ein 3D-Objekt berechnet. Wir werden diese Berechnung nur einmal während der Initialisierungsphase machen und während der Rendering-Phase diese Daten an OpenGL durchleiten.

Um den Code leichter zu verstehen, müssen wir eine neue Datei object.c anlegen und den zugehörigen Header, in den alle Funktionen der Objekte zu speichern sind. Zuerst fügen wir (in unsere Objekt-Struktur, in object.h) ein Array zur Aufnahme all der Normalen seiner Eckpunkte:

vertex_type normal[MAX_VERTICES];

Wir öffnen die Datei object.c und lasst uns schreiben...

void ObjCalcNormals(obj_type_ptr p_object)
{

Lasst uns einige "Unterstützungsvariablen" schaffen...

   int i;
   p3d_type l_vect1,l_vect2,l_vect3,l_vect_b1,l_vect_b2,l_normal;
   int l_connections_qty[MAX_VERTICES];

...und alle die Normalen der Eckpunkte setzen

   for (i=0; i<p_object->vertices_qty; i++)
   {
      p_object->normal[i].x = 0.0;
      p_object->normal[i].y = 0.0;
      p_object->normal[i].z = 0.0;
      l_connections_qty[i]=0;
   }

für jedes Polygon...

   for (i=0; i<p_object->polygons_qty; i++)
   {
      l_vect1.x = p_object->vertex[p_object->polygon[i].a].x;
      l_vect1.y = p_object->vertex[p_object->polygon[i].a].y;
      l_vect1.z = p_object->vertex[p_object->polygon[i].a].z;
      l_vect2.x = p_object->vertex[p_object->polygon[i].b].x;
      l_vect2.y = p_object->vertex[p_object->polygon[i].b].y;
      l_vect2.z = p_object->vertex[p_object->polygon[i].b].z;
      l_vect3.x = p_object->vertex[p_object->polygon[i].c].x;
      l_vect3.y = p_object->vertex[p_object->polygon[i].c].y;
      l_vect3.z = p_object->vertex[p_object->polygon[i].c].z; 

Wir erstellen zwei Koplanarvektoren mit Hilfe zweier Seiten des Polygons:

      VectCreate (&l_vect1, &l_vect2, &l_vect_b1);
      VectCreate (&l_vect1, &l_vect3, &l_vect_b2);

Berechnung des Vektorproduktes zwischen diesen Vektoren:

      VectDotProduct (&l_vect_b1, &l_vect_b2, &l_normal);

und Normalisierung des resultierenden Vektors, das ist das Polygonnormal:

      VectNormalize (&l_normal);

Für jede gemeinsame Ecke dieses Polygons erhöhen wir die Anzahl der Verbindungen ...

      l_connections_qty[p_object->polygon[i].a]+=1;
      l_connections_qty[p_object->polygon[i].b]+=1;
      l_connections_qty[p_object->polygon[i].c]+=1;

...und füge die Polygonnormalen:

      p_object->normal[p_object->polygon[i].a].x+=l_normal.x;
      p_object->normal[p_object->polygon[i].a].y+=l_normal.y;
      p_object->normal[p_object->polygon[i].a].z+=l_normal.z;
      p_object->normal[p_object->polygon[i].b].x+=l_normal.x;
      p_object->normal[p_object->polygon[i].b].y+=l_normal.y;
      p_object->normal[p_object->polygon[i].b].z+=l_normal.z;
      p_object->normal[p_object->polygon[i].c].x+=l_normal.x;
      p_object->normal[p_object->polygon[i].c].y+=l_normal.y;
      p_object->normal[p_object->polygon[i].c].z+=l_normal.z; 
   } 

Jetzt wollen wir den Durchschnitt Polygonnormalen, um die Eckennormalen zu erhalten!

   for (i=0; i<p_object->vertices_qty; i++)
   {
      if (l_connections_qty[i]>0)
      {
         p_object->normal[i].x /= l_connections_qty[i];
         p_object->normal[i].y /= l_connections_qty[i];
         p_object->normal[i].z /= l_connections_qty[i];
      }
   }
}

In der Datei object.c setzen wir auch eine andere Funktion, die jeden Aspekt des Objekts zu initialisieren ermöglicht:

char ObjLoad(char *p_object_name, char *p_texture_name)
{
   if (Load3DS (&object,p_object_name)==0) return(0);
   object.id_texture=LoadBMP(p_texture_name);
   ObjCalcNormals(&object);
   return (1);
}

LASST UNS NUN DIE ANGESTRAHLTE WELT ZEICHNEN

Die letzte Änderung betrifft die Zeichenroutine.

Wir müssen die Normalen des Objektes an OpenGL schicken. Die OpenGL Funktion dafür ist: glNormal3f

void glNormal3f( GLfloat nx, GLfloat ny, GLfloat nz );

...bestimmt die drei Koordinaten x', y, z' des gegenwärtigen Normals.

Hier ist die neue Zeichenroutine:

glBegin(GL_TRIANGLES);
   for (j=0;j<object.polygons_qty;j++)
   {
      //----------------- FIRST VERTEX -----------------
      //Normal coordinates of the first vertex
      glNormal3f( object.normal[ object.polygon[j].a ].x,
                  object.normal[ object.polygon[j].a ].y,
                  object.normal[ object.polygon[j].a ].z);
      // Texture coordinates of the first vertex
      glTexCoord2f( object.mapcoord[ object.polygon[j].a ].u,
                    object.mapcoord[ object.polygon[j].a ].v);
      // Coordinates of the first vertex
      glVertex3f( object.vertex[ object.polygon[j].a ].x,
                  object.vertex[ object.polygon[j].a ].y,
                  object.vertex[ object.polygon[j].a ].z);

      //----------------- SECOND VERTEX -----------------
      //Normal coordinates of the second vertex
      glNormal3f( object.normal[ object.polygon[j].b ].x,
                  object.normal[ object.polygon[j].b ].y,
                  object.normal[ object.polygon[j].b ].z);
      // Texture coordinates of the second vertex
      glTexCoord2f( object.mapcoord[ object.polygon[j].b ].u,
                    object.mapcoord[ object.polygon[j].b ].v);
      // Coordinates of the second vertex
      glVertex3f( object.vertex[ object.polygon[j].b ].x,
                  object.vertex[ object.polygon[j].b ].y,
                  object.vertex[ object.polygon[j].b ].z);

      //----------------- THIRD VERTEX -----------------
      //Normal coordinates of the third vertex
      glNormal3f( object.normal[ object.polygon[j].c ].x,
                  object.normal[ object.polygon[j].c ].y,
                  object.normal[ object.polygon[j].c ].z);
      // Texture coordinates of the third vertex
      glTexCoord2f( object.mapcoord[ object.polygon[j].c ].u,
                    object.mapcoord[ object.polygon[j].c ].v);
      // Coordinates of the Third vertex
      glVertex3f( object.vertex[ object.polygon[j].c ].x,
                  object.vertex[ object.polygon[j].c ].y,
                  object.vertex[ object.polygon[j].c ].z);
   }
glEnd();

RÜCKSCHLÜSSE

Schließlich haben wir das Ende erreicht und wir haben hoffnungsvoll überlebt! Dies war eine sehr technische Anleitung, aber wir haben von einer sehr wichtigen Theorie etwas gelernt. Die hier studierten Begriffe werden für die nächste Anleitung, wo wir Matrizen einführen wollen, sehr zweckdienlich sein. Matrizen werden uns die Handhabung der Objekte über die Position und die Orientierung erlauben.

Hast du die zufälligen Drehungen deines Raumschiffes satt? ;-)

SOURCE CODE

The Source Code of this lesson can be downloaded from the Tutorials Main Page