NeHe - Lektion 25 - Morphing und Objekte aus einer Datei laden

Lektion 25



Willkommen zu einem weiteren aufregendem Tutorial! Dieses Mal konzentrieren wir uns eher auf den Effekt als auf die Grafiken, obwohl das finale Ergebnis wirklich cool aussieht! In diesem Tutorial werden Sie lernen wie man nahtlos von einem Objekt in ein anderes morpht. Ähnlich dem Effekt, den ich im Dolphin Demo verwendet habe. Obwohl es ein paar Haken gibt. Als erstes ist anzumerken, dass jedes Objekt die selbe Anzahl Punkte haben muss. Es muss schon ziemlicher Zufall sein, 3 Objekte zu haben, die exakt die selbe Anzahl an Vertices haben, aber wie es nun mal so ist, haben wir in diesem Tutorial 3 Objekte, mit genau der selben Anzahl an Punkten :) Verstehen Sie mich nicht falsch, Sie können Objekte mit verschiedener Anzahl verwenden, aber der Übergang von einem Objekt in ein anderes, sieht komisch aus und nicht so weich.

Sie werden ebenso lernen, wie man Objekt-Daten aus einer Datei liest. Ähnlich wie das Format, welches in Lektion 10 verwendet wurde, obwohl es nicht schwer sein sollte, den Code so zu modifizieren, dass .ASC Dateien oder andere Text basierte Daten-Dateien einzulesen. Es ist ein wirklich cooler Effekt, ein wirklich cooles Tutorial, lassen Sie uns also beginnen!

Wir fangen wie gewohnt an. Inkludieren alle benötigten Header Dateien, zusammen mit den Math und Standard Input / Output Headern. Beachten Sie, dass wir glaux nicht inkludieren. Das rührt daher, dass wir Punkte anstatt von Texturen in diesem Tutorial zeichnen. Nachdem Sie dieses Tutorial durch haben, können Sie versuchen etwas mit Polygonen, Linien und Texturen zu spielen!

#include    <windows.h>                                // Header Datei für Windows
#include    <math.h>                                // Math Library Header Datei
#include    <stdio.h>                                // Header Datei für Standard Input/Output
#include    <gl\gl.h>                                // Header Datei für die OpenGL32 Library
#include    <gl\glu.h>                                // Header Datei für die GLu32 Library

HDC        hDC=NULL;                                // Device Context Handle
HGLRC        hRC=NULL;                                // Rendering Context Handle
HWND        hWnd=NULL;                                // Fenster Handle
HINSTANCE    hInstance;                                // Instanz Handle

bool        keys[256];                                // Tasten Array
bool        active=TRUE;                                // Programm ist aktiv
bool        fullscreen=TRUE;                            // Fullscreen Flag ist standardmäßig auf TRUE gesetzt

Nachdem wir unsere Standard Variablen haben, fügen wir ein paar neue Variablen ein. xrot, yrot und zrot werden den aktuellen Rotationswert für die X, Y und Z-Achse für das Objekt auf dem Screen enthalten. xspeed, yspeed und zspeed werden kontrollieren, wie schnell das Objekt sich auf jeder Achse dreht. cx, cy und cz kontrollieren die Position des Objektes auf dem Screen (wo es gezeichnet wird, cx für links nach rechts, cy für hoch und runter und cz für's rein und rauszoomen).

Die Variable key habe ich eingefügt, um sicherzustellen, dass der Benutzer nicht versucht von der ersten Form in die erste Form zu morphen. Das wäre ziemlich sinnlos und würde nur eine Verzögerung verursachen, während die Punkte versuchen würden, sich in die Position zu morphen, in der sie bereits sind.

step ist eine Zählervariable, der beim Durchzählen der einzelnen Schritte, die durch steps spezifiziert sind, verwendet wird. Wenn Sie den Wert von steps erhöhen, wird es länger dauern, bis das Objekt morpht, aber die Bewegung der Punkte, wird weicher sein, wenn sie morphen. Wenn step gleich steps ist, wissen wir, dass das Morphen beendet wurde.

Die letzte Variable morph lässt unser Programm wissen, ob es die Punkte morphen soll oder sie so lassen so, wie sie sind. Wenn sie auf TRUE gesetzt ist, ist das Objekt in dem Prozess von einer Form in einer andere zu morphen.

GLfloat        xrot,yrot,zrot,                                // X, Y & Z Rotation
        xspeed,yspeed,zspeed,                            // X, Y & Z Rotationsgeschwindigkeit
        cx,cy,cz=-15;                                // X, Y & Z Position

int        key=1;                                    // wird verwendet um sicherzustellen, dass nicht die selbe Morph Taste gedrückt wurde
int        step=0,steps=200;                            // Step Counter und maximale Anzahl Schritten (Steps)
bool        morph=FALSE;                                // standardmäßig ist morph auf False gesetzt (es wird nicht gemorpht)

Nun erzeugen wir eine Struktur, um einen Vertex kontrollieren zu können. Die Struktur wird die x, y und z Werte für jeden Punkt auf dem Screen enthalten wird. Die Variablen x, y & z sind alles Fließkommazahlen, weshalb wir den Punkt mit höchster Genauigkeit auf dem Screen zu positionieren. Der Strukturenname ist VERTEX.

typedef struct                                        // Struktur für 3D Punkte
{
    float    x, y, z;                                // X, Y & Z Punkte
} VERTEX;                                        // namens VERTEX

Da wir nun bereits eine Struktur haben, um unsere Vertices zu kontrollieren und wir wissen, dass ein Objekt aus vielen Vertices besteht, erzeugen wir nun eine OBJECT Struktur. Die erste Variable verts ist ein Integer-Wert, der die Anzahl der Vertices enthält, die benötigt werden, um das Objekt darzustellen. Wenn Ihr Objekt also aus 5 Punkten besteht, wird der Wert von verts gleich 5 sein. Wir setzen diesen Wert später im Code. Zum jetzigen Zeitpunkt müssen Sie nur wissen, dass verts die Anzahl der Punkte enthält, die wir benutzen werden, um das Objekt zu erzeugen.

Die Variable points wird einen einzelnen VERTEX (x, y und z Werte) referenzieren. Das erlaubt uns, den x, y oder z Wert von jeden Punkt zu ermitteln und zwar mittels points[{Punkt auf den wir zugreifen wollen}].{x, y oder z}.

Der Name dieser Struktur ist... sie werden es bereits wissen... OBJECT!

typedef    struct                                        // Struktur für eine Objekt
{
 int        verts;                                    // Anzahl der Vertices für das Objekt
 VERTEX        *points;                                // Ein Vertex (Vertex x,y & z)
} OBJECT;                                        // namens OBJECT

Nun, da wir eine VERTEX Struktur und eine OBJECT Struktur erzeugt haben, können wir einige Objekte erzeugen.

Die Variable maxver wird verwendet, um die maximale Anzahl der Variablen, die in einem der Objekte verwendet wird, zu kontrollieren. Wenn ein Objekt nur 5 Punkte hat, ein anderes 20 und das letzte Objekt 15 hat, wird der Wert von maxver gleich der größten Anzahl der verwendeteten Punkte sein. In diesem Fall wäre maxver gleich 20.

Nachdem wir maxver definiert haben, können wir die Objekte definieren. morph1, morph2, morph3, morph4 & helper werden alle als OBJECT definiert. *sour & *dest sind als OBJECT* definiert (Zeiger auf ein Objekt). Das Objekt besteht aus Vertices (VERTEX). Die ersten 4 morph{Nummer} Objekte enthalten die 4 Objekte, in die oder aus denen wir morphen wollen. helper wird dazu verwendet, um die Änderungen an dem Objekt zu verfolgen, während das Objekt morpht. *sour wird auf das Quell-Objekt zeigen und *dest zeigt auf das Objekt, in das wir morphen wollen (Ziel-Objekt).

int        maxver;                                    // wird eventuell die maximale Anzahl der Vertices enthalten
OBJECT        morph1,morph2,morph3,morph4,                        // unsere 4 morphbaren Objekte (morph1,2,3 & 4)
        helper,*sour,*dest;                             // Helper Objekt, Quell-Objekt, Ziel-Objekt

Wie immer deklarieren wir WndProc().

LRESULT    CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);                    // Deklaration

Der folgende Code alloziiert Speicher für jedes Objekt, basierend auf der Anzahl der Vertices, die wir in n übergeben. *k zeigt auf das Objekt, für das wir Speicher alloziieren wollen.

Die Zeile in den { } alloziiert den Speicher für die k Punkte des Objekts. Ein Punkt ist ein gesamtes VERTEX (3 floats). Der alloziierte Speicher ist die Größe von VERTEX (3 floats) multipliziert mit der Anzahl der Punkte (n). Wenn wir also 10 Punkte (n=10) haben, würden wir Speicherplatz für 30 floats (3 floats * 10 Punkte) alloziieren.

void objallocate(OBJECT *k,int n)                            // Alloziiere Speicher für jedes Objekt
{                                            // und definiere Punkte
    k->points=(VERTEX*)malloc(sizeof(VERTEX)*n);                    // Setze Punkte gleich auf VERTEX * Anzahl der Vertices
}                                            // (3 Punkte für jeden Vertex)

Der folgende Code gibt das Objekt und den Speicher wieder frei, der bei der Erzeugung des Objekts genutzt wurde. Das Objekt wird in k übergeben. Der free Befehl teilt unserem Programm mit, dass alle Punkte freigegeben werden sollen, aus denen unser Objekt (k) besteht.

void objfree(OBJECT *k)                                    // gibt das Objekt frei (und den Speicher)
{
    free(k->points);                                // gibt Zeiger frei
}

Der folgende Code liest einen String aus einer Datei ein. Der Zeiger auf unsere file Struktur wird in *f übergeben. Die Variable string wird den Text enthalten, den wir einlesen wollen.

Wir fangen mit der Erzeugung einer do / while Schleife an. fgets() wird bis zu 255 Zeichen aus unserer Datei f einlesen und die Zeichen in *string speichern. Wenn die eingelesene Zeile leer war (carriage return \n), wird die Schleife erneut durchlaufen, mit dem Versuch eine Zeile mit Text zu finden. Das while() Statement überprüft auf leere Zeilen und wenn eine gefunden wurde, wird neu gestartet.
Nachdem der String eingelesen wurde, kehren wir zurück.

void readstr(FILE *f,char *string)                            // Liest einen String aus einer Datei (f)
{
    do                                        // mache folgendes
    {
        fgets(string, 255, f);                            // hole einen String mit maximalen 255 Zeichen aus f (Datei)
    } while ((string[0] == '/') || (string[0] == '\n'));                // bis das Ende der Zeile erreicht wurde
    return;                                        // kehre zurück
}

Nun laden wir ein Objekt. *name zeigt auf den Dateinamen. *k zeigt auf das Objekt, in das wir die Daten laden wollen.

Wir fangen mit einer Integer Variable namens ver an. ver wird die Anzahl der Vertices enthalten, die gebraucht werden, um das Objekt zu erzeugen.

Die Variablen rx, ry & rz werden die x, y & z Werte für jeden Vertex enthalten.

Die Variabel filein ist der Zeiger auf unsere file Struktur und oneline[ ] wird dazu verwendet, 255 Zeichen aufzunehmen.

Wir öffnen die Datei name, mit Lesezugriff im Textmodus (was bedeutet, dass STRG-Z das Ende einer Zeile repräsentiert). Dann lesen wir eine Zeile Text mittels readstr(filein,oneline) ein. Die Textzeile wird in oneline gespeichert.

Nachdem wir den Text eingelesen haben, durchsuchen wir die Textzeile (oneline) nach der Phrase "Vertices: {eine Zahl}{carriage return}. Wenn der Text gefunden wurde, wird die Zahl in der Variable ver gespeichert. Diese Zahl ist die Anzahl der Vertices, die zur Erzeugung des Objekts verwendet wird. Wenn Sie einen Blick auf die Objekt-Text-Dateien werfen, werden Sie sehen, dass die erste Textzeile wie folgt lautet: Vertices: {eine Zahl}.

Nachdem wir wissen, wie viele Vertices benutzt werden, speichern wir die Ergebnisse in der Objekt Variable verts. Jedes Objekt kann einen anderen Wert haben, wenn jedes Objekt eine verschiedene Anzahl an Vertices hat.

Das letzte was wir in diesem Codeabschnitt machen, ist die Alloziierung des Speichers für das Objekt. Wir machen das, indem wir objallocate({objekt name},{Anzahl an verts}) aufrufen.

void objload(char *name,OBJECT *k)                            // Lädt Objekt aus Datei (name)
{
    int    ver;                                    // wird die Vertice Anzahl enthalten
    float    rx,ry,rz;                                // enthält Vertex X, Y & Z Position
    FILE    *filein;                                // zu öffnender Dateiname
    char    oneline[255];                                // enthält eine Textzeile (255 Zeichen maximal)

    filein = fopen(name, "rt");                            // öffnet die Datei zum lesen
                                            // STRG Z symbolisiert das Ende der Datei
    readstr(filein,oneline);                            // springt zum Code, der eine Textzeile aus der Datei einliest
    sscanf(oneline, "Vertices: %d\n", &ver);                    // durchsuche Text auf "Vertices: ".  Zahl wird danach in ver gespeichert
    k->verts=ver;                                    // Setze Objekt verts Variable gleich auf den Wert von ver
    objallocate(k,ver);                                // springt zum Code, der den Speicher alloziiert, um das Objekt zu speichern

Wir wissen nun wie viele Vertices das Objekt hat. Wir haben den Speicher alloziiert und nun müssen wir nur noch die Vertices einlesen. Wir erzeugen eine Schleife, die die Variable i verwendet. Die Schleife wird alle Vertices durchlaufen.

Als nächstes lesen wir eine Textzeile eine. Das wird die erste gültige Textzeile sein, die nach der "Vertices: {eine Zahl}" Zeile kommt. Was wir dann einlesen sollten, ist eine Zeile mit Fleißkommawerten für x, y & z.

Die Zeile wird mit sscanf() analysiert und die drei Fließkommawerte werden extrahiert und in rx, ry und rz gespeichert.

    for (int i=0;i<ver;i++)                                // iteriert durch die Vertices
    {
        readstr(filein,oneline);                        // liest die nächste Textzeile ein
        sscanf(oneline, "%f %f %f", &rx, &ry, &rz);                // sucht nach 3 Fließkommazahlen, die in rx,ry & rz gespeichert werden

Die folgenden drei Zeilen sind recht schwer verbal zu erklären, wenn Sie Strukturen, etc. nicht verstehen, aber ich werde mein bestes geben :)

Die Zeile k->points[i].x=rx kann wie folgt aufgeteilt werden:

rx ist der Wert auf der X-Achse für einen der Punkte.
points[i].x ist die X-Achsen Position von point[i].
Wenn i gleich 0 ist, dann setzen wir den X-Achsen-Wert für den Punkt 1, wenn i gleich 1 ist, setzen wir den X-Achsen-Wert für Punkt 2 und so weiter.
points[i] ist Teil unseres Objekts (welches durch k repräsentiert wird).

Wenn i also gleich 0 ist, sagen wir also: Die X-Achse des Punkt 1 (point[0].x) in unserem Objekt (k) ist gleich dem X-Achsen-Wert den wir gerade aus der Datei eingelesen haben (rx).

Die anderen beiden Zeilen setzen die y & z Achsen-Werte für jeden Punkt in unserem Objekt.

Wir iterieren durch alle Vertices. Wenn es nicht genügend Vertices gibt, könnte ein Fehler auftreten, stellen Sie also sicher, dass der Text am Anfang der Datei "Vertices: {eine Zahl}" tatsächlich die Anzahl der Vertices in der Datei ist. Wenn die erste Zeile der Datei also "Vertices: 10" lautet, sollten sich dort also auch 10 Vertices (x, y und z Werte) befinden!

Nachdem alle Vertices eingelesen wurden, schließen wir die Datei und überprüfen, obe die Variable ver größer als die Variable maxver ist. Wenn ver größer als maxver ist, setzen wir maxver auf ver. Auf diese Weise lesen wir ein Objekt ein und wenn es 20 Vertices hat, wird maxver auf 20 gesetzt. Wenn wir ein weiteres Objekt einlesen und dieses 40 Vertices hat, wird maxver auf 40 gesetzt. Auf diese Weise wissen wir, wieviele Vertices unser größtes Objekt hat.

        k->points[i].x = rx;                            // Setze Objekt (k) points.x Wert auf rx
        k->points[i].y = ry;                            // Setze Objekt (k) points.y Wert auf ry
        k->points[i].z = rz;                            // Setze Objekt (k) points.z Wert auf rz
    }
    fclose(filein);                                    // schließe die Datei

    if(ver>maxver) maxver=ver;                            // wenn ver größer als maxver ist, dann setze maxver auf ver
}                                            // damit verfolgen wir die höchste verwendete Anzahl an Vertices

Das nächste Codestück sieht vielleicht etwas einschüchternd aus... ist es NICHT :) Ich werde es Ihnen so klar erklären, dass Sie darüber lachen werden, wenn Sie das nächste mal einen Blick darauf werfen.

Der folgende Code berechnet eine neue Position für jeden Punkt wenn Morphing aktiviert wird. Die Nummer des zu berechnenden Punktes ist in i gespeichert. Die Ergebnisse werden in der VERTEX Struktur zurückgegeben.

Die erste Variable die wir erzeugen ist vom Typen VERTEX namens a. Dann werden a die Werte für x, y und z zugewiesen.

Schauen wir uns die erste Zeile an. Der X-Wert von VERTEX a wird auf den X Wert von point[i] (point[i].x) in unserem QUELL Objekt minus den X-Wert von point[i] (point[i].x) in unserem ZIEL Objekt dividiert durch steps gesetzt.

Lassen Sie uns ein paar Zahlen einsetzen. Sagen wir, dass unser erster X-Wert des Quell-Objekts gleich 40 ist und unser erster X-Wert unseres Ziel Objektes ist 20. Wir wissen bereits, dass steps gleich 200 ist!. Das bedeutet, dass a.x=(40-20)/200... a.x=(20)/200... a.x=0.1 ist.

Das bedeutet, dass wenn wir uns von 40 nach 20 in 200 Schritten bewegen wollen, wir uns um 0.1 Einheiten bei jeder Berechnung bewegen müssen. Um das mit dieser Rechnung zu beweisen, multiplizieren wir 0.1 mit 200 und Sie erhalten 20. 40-20=20 :)

Wir machen das Selbe, um zu berechnen, wie viele Einheiten auf der Y-Achse und der Z-Achse für jeden Punkt sich bewegt werden muss. Wenn Sie den Wert von steps erhöhen, wird die Bewegung noch feiner (weicher), aber es wird länger dauern, von einer Position in eine andere zu morphen.

VERTEX calculate(int i)                                    // berechne Bewegung der Punkte während des Morphings
{
    VERTEX a;                                    // Temporärer Vertex namens a
    a.x=(sour->points[i].x - dest->points[i].x) / steps;                // a.x Wert gleich Quell x - Ziel x Dividiert durch Steps
    a.y=(sour->points[i].y - dest->points[i].y) / steps;                // a.y Wert gleich Quell y - Ziel y Dividiert durch Steps
    a.z=(sour->points[i].z - dest->points[i].z) / steps;                // a.z Wert gleich Quell z - Ziel z Dividiert durch Steps
    return a;                                    // gebe die Ergebnisse zurück
}

Der ReSizeGLScene() Code hat sich nicht geändert, weshalb wir ihn übespringen.

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)                    // verändert die Größe und initialisiert das GL-Fenster

Im folgenden Code setzen wir Blending für Translucency. Die erlaubt uns nett aussehende Schweife zu erzeugen, wenn die Punkte sich bewegen.

int InitGL(GLvoid)                                    // Der ganze Setup Kram für OpenGL kommt hier rein
{
    glBlendFunc(GL_SRC_ALPHA,GL_ONE);                        // Setze die Blending Funktion für Translucency
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);                        // dies setzt die Hintergrundfarbe auf schwarz
    glClearDepth(1.0);                                // aktiviert die Löschung des Depth Buffers
    glDepthFunc(GL_LESS);                                // Die Art des auszuführenden Depth Test
    glEnable(GL_DEPTH_TEST);                            // aktiviert Depth Testing
    glShadeModel(GL_SMOOTH);                            // aktiviert Smooth Color Shading
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);                // wirklich nette Perspektiven Berechnungen

Wir setzen die maxver Variable anfangs auf 0. Wir haben bisher keine Objekte eingelesen und so wissen wir auch noch nicht, was die maximale Anzahl der Vertices sein wird.

Als nächstes laden wir 3 Objekte. Das erste Objekt ist eine Sphere. Die Daten für die Sphere ist in der Datei sphere.txt gespeichert. Die Daten werden in das Objekt namens morph1 gespeichert. Wir laden außerdem einen Torus und eine Röhre in die Objekte morph2 und morph3.

    maxver=0;                                    // Setze maximale Vertices standardmäßig auf 0
    objload("data/sphere.txt",&morph1);                        // Lade das erste Objekt nach morph1 aus der Datei sphere.txt
    objload("data/torus.txt",&morph2);                        // Lade das zweite Objekt nach morph2 aus der Datei torus.txt
    objload("data/tube.txt",&morph3);                        // Lade das dritte Objekt nach morph3 aus der Datei tube.txt

Das vierte Objekt wird nicht aus einer Datei eingelesen. Es ist eine Menge Punkte die zufällig über den Screen wuseln. Da wir die Daten nicht aus einer Datei laden, müssen wir den Speicher manuell alloziieren, indem wir objallocate(&morph4,468) aufrufen. 468 bedeutet, dass wir genügend Speicher für 468 Vertices reservieren (die selbe Anzahl an Vertices, die auch die anderen 3 Objekte haben).

Nachdem der Speicher alloziiert wurde, erzeugen wir eine Schleife, die jedem Punkt einen zufälligen x, y und z Wert zuweist. Der zufällige Wert wird ein Fließkomma Wert zwischen +7 und -7 sein. (14000/1000=14... minus 7 gibt uns einen maximalen Wert zwischen 0-7 oder -7).

    objallocate(&morph4,486);                            // Manuelle Speicherresevierung für ein 4tes 468 Vertices Objekt (morph4)
    for(int i=0;i// iteriere durch alle 468 Vertices
    {
        morph4.points[i].x=((float)(rand()%14000)/1000)-7;            // morph4 x Punkt wird ein zufälliger Wert zwischen -7 und 7 zugewiesen
        morph4.points[i].y=((float)(rand()%14000)/1000)-7;            // morph4 y Punkt wird ein zufälliger Wert zwischen -7 und 7 zugewiesen
        morph4.points[i].z=((float)(rand()%14000)/1000)-7;            // morph4 z Punkt wird ein zufälliger Wert zwischen -7 und 7 zugewiesen
    }

Wir laden dann die sphere.txt als helper Objekt. Wir wollen die Objekt Daten in morph{1/2/3/4} nicht direkt modizifizeren. Wir modifzieren die helper Daten, um daraus eins der 4 Formen zu machen. Da wir anfangs morph1 (eine Sphere) anzeigen, setzen wir helper ebenfalls auf eine Sphere.

Nachdem alle Objekte geladen wurden, setzen wir Quell und Ziel Objekte (sour und dest) gleich morph1, welches die Sphere ist. Auf diesem Weg fängt alles mit einer Sphere ein.

    objload("data/sphere.txt",&helper);                        // Lade sphere.txt Object nach Helper (wird als Startpunkt verwendet)
    sour=dest=&morph1;                                // Quell & Ziel werden auf das erste Objekt (morph1) gesetzt

    return TRUE;                                    // Initialisierung verlief OK
}

Nun zum spaßigen Teil. Dem aktuellen Rendering Code :)

Wir fangen ganz normal an. Löschen des Screens, Depth Buffer und dem resetten der Modelview Matrix. Dann positionieren wir das Objekt auf dem Screen, unter der Benutzung der Werte, die in cx, cy und cz gespeichert sind.

Rotationen werden mittels xrot, yrot und zrot gemacht.

Der Rotationswinkel wird basierend auf xspeed, yspeed und zspeed erhöht.

Letztlich werden 3 temporäre Variablen, tx, ty und tz erzeugt, zusammen mit einem neuen VERTEX namens q.

void DrawGLScene(GLvoid)                                // Hier kommt der ganze Zeichnen-Kram hin
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);                // Lösche den Bildschirm und den Depth-Buffer
    glLoadIdentity();                                // Resette die aktuelle Modelview Matrix
    glTranslatef(cx,cy,cz);                                // Translatiere die aktuelle Position um mit dem Zeichnen zu beginnen
    glRotatef(xrot,1,0,0);                                // Rotatiere auf der X-Achse um xrot
    glRotatef(yrot,0,1,0);                                // Rotatiere auf der Y-Achse um yrot
    glRotatef(zrot,0,0,1);                                // Rotatiere auf der Z-Achse um zrot

    xrot+=xspeed; yrot+=yspeed; zrot+=zspeed;                    // erhöhe xrot,yrot & zrot um xspeed, yspeed & zspeed

    GLfloat tx,ty,tz;                                // Temporäre X, Y & Z Variablen
    VERTEX q;                                    // enthält zurückgelieferte berechnete Werte für ein Vertex

Nun zeichnen wir die Punkte und machen unsere Berechnungen, wenn Morphing aktiviert ist. glBegin(GL_POINTS) teilt OpenGL mit, dass jeder Vertex, den wir angeben, als ein Punkt auf den Screen gezeichnet wird.

Wir erzeugen eine Schleife, die durch alle Vertices iteriert. Sie könnten maxver verwenden, da aber alle Objekte die selbe Anzahl an Vertices hat, werden wir morph1.verts verwenden.

Innerhalb der Schleife überprüfen wir, ob morph gleich TRUE ist. Wenn ja, berechnen wir die Bewegung für den aktuellen Punkt (i). q.x, q.y und q.z werden dann die Ergebnisse enthalten. Wenn morph gleich false ist, werden q.x, q.y und q.z auf 0 gesetzt (was eine Bewegung verhindert).

Die Punkte in dem helper Objekt werden basierend auf den Ergebnissen, die wir von calculate(i) erhalten, bewegt. (erinnern Sie sich daran, dass wir ausgerechnet haben, dass ein Punkt sich um 0.1 Einheiten bewegen müsste, um es von 40 nach 20 in 200 Schritten zu machen).

Wir adjustieren jeden einzelnen Punkte Wert auf der x, y und Z Achse indem wir die Anzahl der zu bewegenden Einheiten von helper subtrahieren.

Der neue helper Punkt wird in tx, ty und tz gespeichert. (t{x/y/z}=helper.points[i].{x/y/z}).

    glBegin(GL_POINTS);                                // fange an Punkte zu zeichnen
        for(int i=0;i<morph1.verts;i++)                        // iteriere durch alle Verts von morph1 (Alle Objekte haben
        {                                    // die selbe Anzahl an Verts, nur der Einfachkeit halber, Sie könnten auch maxver verwenden)
            if(morph) q=calculate(i); else q.x=q.y=q.z=0;            // wenn morph gleich True ist, berechne Bewegung ansonsten Movement=0
            helper.points[i].x-=q.x;                    // Subtrahiere q.x Einheiten von helper.points[i].x (Bewegung auf der X Achse)
            helper.points[i].y-=q.y;                    // Subtrahiere q.y Einheiten von helper.points[i].y (Bewegung auf der Y Achse)
            helper.points[i].z-=q.z;                    // Subtrahiere q.z Einheiten von helper.points[i].z (Bewegung auf der Z Achse)
            tx=helper.points[i].x;                        // setze temporäre X Variable gleich auf die Helper's X Variable
            ty=helper.points[i].y;                        // setze temporäre Y Variable gleich auf die Helper's Y Variable
            tz=helper.points[i].z;                        // setze temporäre Z Variable gleich auf die Helper's Z Variable

Nun da wir die neue Position berechnet haben, ist es an der Zeit, unsere Punkte zu zeichnen. Wir setzen die Farbe auf ein helles Blau und zeichnen dann den ersten Punkt mit glVertex3f(tx,ty,tz). Dies zeichnet einen Punkt an der neu berechneten Position.

Wir verdunkeln dann die Farbe ein wenig und bewegen uns 2 Schritte in die Richtung, die wir gerade berechnet haben, anstatt nur einem. Dies bewegt den Punkt an die neu berechneten Position und bewegt ihn dann erneut in die selbe Richtung. Wenn er also um 0.1 Einheiten nach links wandert, würde der nächste Punkt bei 0.2 Einheiten sein. Nachdem wir 2 Positionen vorraus berechnet haben, zeichnen wir den zweiten Punkt.

Zu letzt setzen wir die Farbe auf ein dunkles blau und berechnen weiter vorraus. Dieses Mal würden wir in unserem Beispiel uns 0.4 Einheiten nach links anstatt 0.1 oder 0.2 Einheiten bewegen. Das Endergebniss ist ein kleiner Schweif aus Partikeln der dem Punkt folgt, der sich bewegt. Mit Blending erzeugt das einen ziemlich coolen Effekt!

glEnd() teilt OpenGL mit, dass wir fertig mit dem Zeichnen der Punkte sind.

            glColor3f(0,1,1);                        // Setze Farbe auf hellen Blauton
            glVertex3f(tx,ty,tz);                        // Zeichne einen Punkt an der Stelle der aktuellen Temp Werte (Vertex)
            glColor3f(0,0.5f,1);                        // verdunkle die Farbe ein wenig
            tx-=2*q.x; ty-=2*q.y; ty-=2*q.y;                // berechne zwei Positionen vorraus
            glVertex3f(tx,ty,tz);                        // zeichne einen zweiten Punkt an der neu berechneten Position
            glColor3f(0,0,1);                        // Setze Farbe auf ein sehr dunkles Blau
            tx-=2*q.x; ty-=2*q.y; ty-=2*q.y;                // Berechne zwei Positionen vorraus
            glVertex3f(tx,ty,tz);                        // zeichne einen dritte Punkt an der zweiten neuen Position
        }                                    // Dies erzeugt einen gespenstischen Schweif, während die Punkte sich bewegen
    glEnd();                                    // fertig mit dem Zeichnen der Punkte

Das letzte was wir machen, wir überprüfen, ob morph gleich TRUE ist und step kleiner als steps (200) ist. Wenn step kleiner als 200 ist, inkrementieren wir step um 1.

Wenn morph gleich false ist oder step größer oder gleich steps (200) ist, morph wird auf FALSE gesetzt, das sour (Quell) Objekt wird gleich dem dest (Ziel) Objekt gesetzt und step wird zurück auf 0 gesetzt. Dies teilt dem Programm mit, dass nicht gemorpht wird oder es beendet wurde.

    // Wenn wir morphen und wir noch nicht durch alle 200 Schritte durch sind, inkrementieren wir unseren Step-Zähler
    // Ansonsten setze Morphing auf False, Setze Source=Destination und setze den Step-Zähler zurück auf null.
    if(morph && step<=steps)step++; else { morph=FALSE; sour=dest; step=0;}
}

Der KillGLWindow() Code hat sich nicht sonderlich viel verändert. Der einzige wirkliche Unterschied ist, dass wir alle Objekte im Speicher freigeben, bevor wir das Fenster killen. Das beugt Speicherlecks vor und es ist guter Stil ;)

GLvoid KillGLWindow(GLvoid)                                // Entferne das Fenster korrekt
{
    objfree(&morph1);                                // springe zum Code um den für morph1 alloziierten Speicher freizugeben
    objfree(&morph2);                                // springe zum Code um den für morph2 alloziierten Speicher freizugeben
    objfree(&morph3);                                // springe zum Code um den für morph3 alloziierten Speicher freizugeben
    objfree(&morph4);                                // springe zum Code um den für morph4 alloziierten Speicher freizugeben
    objfree(&helper);                                // springe zum Code um den für helper alloziierten Speicher freizugeben

    if (fullscreen)                                    // Sind wir im Fullscreen Modus?
    {
        ChangeDisplaySettings(NULL,0);                        // Wenn ja, wechsle zurück zum Desktop
        ShowCursor(TRUE);                            // Zeige den Maus-Zeiger
    }

    if (hRC)                                    // Haben wir einen Rendering Context?
    {
        if (!wglMakeCurrent(NULL,NULL))                        // Können wir den DC und RC Kontext freigeben?
        {
            MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        }

        if (!wglDeleteContext(hRC))                        // Können wir den RC löschen?
        {
            MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        }
        hRC=NULL;                                // Setze RC auf NULL
    }

    if (hDC && !ReleaseDC(hWnd,hDC))                        // Können wir DC freigeben?
    {
        MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hDC=NULL;                                // Setze DC auf NULL
    }

    if (hWnd && !DestroyWindow(hWnd))                        // Können wir das Fenster zerstören?
    {
        MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hWnd=NULL;                                // Setze hWnd auf NULL
    }

    if (!UnregisterClass("OpenGL",hInstance))                    // Können wir die Klasse de-registrieren?
    {
        MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hInstance=NULL;                                // Setze hInstance auf NULL
    }
}

Der Code von CreateGLWindow() und WndProc() hat sich nicht geändert. Deswegen werde ich diesen überspringen.

BOOL CreateGLWindow()                                    // erzeugt das GL Fenster

LRESULT CALLBACK WndProc()                                // Handle für dieses Fenster

In WinMain() gab es ein paar Änderungen. Als erstes ist der neue Titel zu erwähnen :)

int WINAPI WinMain(    HINSTANCE    hInstance,                    // Instanz
            HINSTANCE    hPrevInstance,                    // vorherige Instanz
            LPSTR        lpCmdLine,                    // Kommandozeilen Parameter
            int        nCmdShow)                    // Fenster Anzeige Status
{
    MSG    msg;                                    // Windows Nachrichten Struktur
    BOOL    done=FALSE;                                // Bool Variable um die Schleife zu beenden

    // Frage den Benutzer, in welchen Modus er starten will
    if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
    {
        fullscreen=FALSE;                            // Fenster-Modus
    }

    // erzeuge unser OpenGL Fenster
    if (!CreateGLWindow("Piotr Cieslak & NeHe's Morphing Points Tutorial",640,480,16,fullscreen))
    {
        return 0;                                // Beende, wenn Fenster nicht erzeugt wurde
    }

    while(!done)                                    // Schleife die so lange läuft, bis done=TRUE
    {
        if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))                // Wartet eine Nachricht?
        {
            if (msg.message==WM_QUIT)                    // Haben wir eine Nachricht zum beenden erhalten?
            {
                done=TRUE;                        // Wenn ja done=TRUE
            }
            else                                // Wenn nicht, bearbeite die Fenster-Nachrichten
            {
                TranslateMessage(&msg);                    // Übersetze die Nachricht
                DispatchMessage(&msg);                    // bearbeite die Nachricht
            }
        }
        else                                    // Wenn da keine Nachrichten sind
        {
            // Zeichne die Szene. Schau nach der ESC-Taste und Beendigungs-Nachrichten von DrawGLScene()
            if (active && keys[VK_ESCAPE])                    // Aktiv? Wurde ein Signal zum Beenden erhalten?
            {
                done=TRUE;                        // ESC oder DrawGLScene Signalisiert, dass Beendet werden soll
            }
            else                                // Es ist noch nicht Zeit zum beenden, zeichne Screen neu
            {
                DrawGLScene();                        // Zeichne die Szene (zeichne nicht neu, wenn inaktiv, nur 1% CPU Benutzung)
                SwapBuffers(hDC);                    // Swap Buffers (Double Buffering)

Der folgende Code überwacht die Tastendrücke. Bis jetzt sollten Sie den Code recht leicht verstanden haben. Wenn Bild hoch gedrückt wurde, inkrementieren wir zspeed. Das hat zum Ergebniss, dass das Objekt schneller auf der Z-Achse in eine positive Richtung rotiert.

Wenn Bild runter gedrückt wurde, dekrementieren wir zspeed. Das hat zum Ergebniss, dass das Objekt schneller auf der Z-Achse in eine negative Richtung rotiert.

Wenn Pfeil runter gedrückt wurde, inkrementieren wir xspeed. Das hat zum Ergebniss, dass das Objekt schneller auf der X-Achse in eine positive Richtung rotiert.

Wenn Pfeil hoch gedrückt wurde, dekrementieren wir xspeed. Das hat zum Ergebniss, dass das Objekt schneller auf der X-Achse in eine negative Richtung rotiert.

Wenn Pfeil rechts gedrückt wurde, inkrementieren wir yspeed. Das hat zum Ergebniss, dass das Objekt schneller auf der Y-Achse in eine positive Richtung rotiert.

Wenn Pfeil links gedrückt wurde, dekrementieren wir yspeed. Das hat zum Ergebniss, dass das Objekt schneller auf der Y-Achse in eine negative Richtung rotiert.



                if(keys[VK_PRIOR])                    // wurde 'Bild hoch' gedrückt?
                    zspeed+=0.01f;                    // inkrementiere zspeed

                if(keys[VK_NEXT])                    // wurde 'Bild runter' gedrückt?
                    zspeed-=0.01f;                    // dekrementiere zspeed

                if(keys[VK_DOWN])                    // wurde Pfeil runter gedrückt?
                    xspeed+=0.01f;                    // inkrementiere xspeed

                if(keys[VK_UP])                        // wurde Pfeil hoch gedrückt?
                    xspeed-=0.01f;                    // dekrementiere xspeed

                if(keys[VK_RIGHT])                    // wurde Pfeil rechts gedrückt?
                    yspeed+=0.01f;                    // inkrementiere yspeed

                if(keys[VK_LEFT])                    // wurde Pfeil links gedrückt?
                    yspeed-=0.01f;                    // dekrementiere yspeed

Die folgenden Tasten bewegen das Objekt. 'Q' bewegt es in den Screen hinein, 'Z' bewegt es zum Betrachter hin, 'W' bewegt das Objekt nach oben, 'S' bewegt es nach unten, 'D' bewegt es nach rechts und 'A' bewegt es nach links.

                if (keys['Q'])                        // wurde die Q Taste gedrückt?
                 cz-=0.01f;                        // bewege das Objekt vom Betrachter weg

                if (keys['Z'])                        // wurde die Z Taste gedrückt?
                 cz+=0.01f;                        // bewege das Objekt zum Betrachter hin

                if (keys['W'])                        // wurde die W Taste gedrückt?
                 cy+=0.01f;                        // bewege das Objekt nach oben?

                if (keys['S'])                        // wurde die S Taste gedrückt?
                 cy-=0.01f;                        // bewege das Objekt nach unten

                if (keys['D'])                        // wurde die D Taste gedrückt?
                 cx+=0.01f;                        // bewege das Objekt nach rechts

                if (keys['A'])                        // wurd die Taste A gedrückt?
                 cx-=0.01f;                        // bewege das Objekt nach links

Nun überwachen wir, ob die Tasten 1 bis 4 gedrückt wurden. Wenn 1 gedrückt wurde und key nicht gleich 1 ist (noch nicht das aktuelle Objekt ist) und morph gleich false ist (noch nicht im Morphing-Prozess), setzen wir key auf 1, so dass unser Programm weiß, dass wir gerade das Objekt 1 ausgewählt haben. Wir setzen dann morph auf TRUE, was unser Programm wissen lässt, dass es an der Zeit ist, mit dem Morphen zu beginnen und als letztes setzen wir das Ziel-Objekt (dest) gleich dem Objekt 1 (morph1).

Das Drücken der Tasten 2, 3 und 4 machen das Selbe. Wenn 2 gedrückt wurde, setzen wir dest auf morph2 und wir setzen key auf 2. Das Drücken der Taste 3 setzt dest auf morph3 und key auf 3.

Indem key auf den Wert der Taste, die wir gerade gedrückt haben, setzen, verhindern wir, dass der Benutzer versucht von einer Sphere in einer Sphere oder von einem Kegel in einen Kegel zu morphen!

                if (keys['1'] && (key!=1) && !morph)            // wurde 1 gedrückt, key ungleich 1 und morph gleich False?
                {
                    key=1;                        // Setze key auf 1 (um zu verhindern, dass 1 zweimal hinter einander gedrückt wird)
                    morph=TRUE;                    // Setze morph auf True (Startet den Morphing Prozess)
                    dest=&morph1;                    // Ziel Objekt in das gemorpht werden soll, dass dann morph1 wird
                }
                if (keys['2'] && (key!=2) && !morph)            // wurde 2 gedrückt, key ungleich 2 und morph gleich False?
                {
                    key=2;                        // Setze key auf 2 (um zu verhindern, dass 2 zweimal hinter einander gedrückt wird)
                    morph=TRUE;                    // Setze morph auf True (Startet den Morphing Prozess)
                    dest=&morph2;                    // Ziel Objekt in das gemorpht werden soll, dass dann morph2 wird
                }
                if (keys['3'] && (key!=3) && !morph)            // wurde 3 gedrückt, key ungleich 3 und morph gleich False?
                {
                    key=3;                        // Setze key auf 3 (um zu verhindern, dass 3 zweimal hinter einander gedrückt wird)
                    morph=TRUE;                    // Setze morph auf True (Startet den Morphing Prozess)
                    dest=&morph3;                    // Ziel Objekt in das gemorpht werden soll, dass dann morph3 wird
                }
                if (keys['4'] && (key!=4) && !morph)            // wurde 4 gedrückt, key ungleich 4 und morph gleich False?
                {
                    key=4;                        // Setze key auf 4 (um zu verhindern, dass 4 zweimal hinter einander gedrückt wird)
                    morph=TRUE;                    // Setze morph auf True (Startet den Morphing Prozess)
                    dest=&morph4;                    // Ziel Objekt in das gemorpht werden soll, dass dann morph4 wird
                }

Zu guter letzt schauen wir, ob F1 gedrückt wurde und wenn ja, wechseln wir vom Fullscreen- in den Fenster-Modus oder vom Fenster-Modus in den Fullscreen-Mode!

                if (keys[VK_F1])                    // Wurde F1 gedrückt?
                {
                    keys[VK_F1]=FALSE;                // Wenn ja, setze Taste auf FALSE
                    KillGLWindow();                    // Kill unser aktuelles Fenster
                    fullscreen=!fullscreen;                // Wechsel zwischen Fullscreen und Fester-Modus
                    // Erzeuge unser OpenGL Fenster erneut
                    if (!CreateGLWindow("Piotr Cieslak & NeHe's Morphing Points Tutorial",640,480,16,fullscreen))
                    {
                        return 0;                // Beenden, wenn das Fenster nicht erzeugt wurde
                    }
                }
            }
        }
    }

    // Shutdown
    KillGLWindow();                                    // Kill das Fenster
    return (msg.wParam);                                // Beende das Programm
}

Ich hoffe Sie haben dieses Tutorial genossen. Obwohl es nicht gerade ein unheimlich komplexes Tutorial ist, können Sie eine Menge aus dem Code lernen. Die Animation in meinem Dolphin Demo wurde auf eine ähnliche Weise wie das Morphen in diesem Demo realisiert. Wenn Sie etwas mit dem Code herumspielen, sollten dabei ein paar wirklich coole Effekte herauskommen. Punkte, die sich in Buchstaben verwandeln. Gefakete Animationen und mehr! Sie können sogar vielleicht solide Polygone oder Linien ausprobieren anstatt von Punkten. Der Effekt kann ziemlich beeindruckend sein!

Piotr's Code ist neu und erfrischend. Ich hoffe, dass Sie, nachdem Sie dieses Tutorial gelesen haben, ein besseres Verständnis darüber haben, wie man Objekt-Daten in einer Datei speichert und wieder lädt und wie man die Daten manipuliert, um coole GL Effekte in Ihren eigenen Programmen zu erzeugen! Die .html für dieses Tutorial wurde in 3 Tagen geschrieben. Wenn Sie Fehler bemerken, lassen Sie es mich wissen. Vieles davon wurde spät in der Nacht geschrieben, was bedeutet, dass sich vielleicht ein paar Fehler eingeschlichen haben. Ich möchte, dass diese Tutorials so gut sind, wie sie sein können. Feedback ist erwünscht!

RabidHaMsTeR hat ein Demo namens "Morph" veröffentlicht, bevor dieses Tutorial geschrieben wurde, welches eine fortgeschrittenere Version dieses Effektes zeigt. Sie können es sich selbst unter http://homepage.ntlworld.com/fj.williams/PgSoftware.html ansehen.

Piotr Cieslak

Jeff Molofee (NeHe)

* DOWNLOAD Visual C++ Code für diese Lektion.

* DOWNLOAD Borland C++ Builder 6 Code für diese Lektion. ( Conversion by Christian Kindahl )
* DOWNLOAD Code Warrior 5.3 Code für diese Lektion. ( Conversion by Scott Lupton )
* DOWNLOAD Delphi Code für diese Lektion. ( Conversion by Michal Tucek )
* DOWNLOAD Dev C++ Code für diese Lektion. ( Conversion by Dan )
* DOWNLOAD Euphoria Code für diese Lektion. ( Conversion by Evan Marshall )
* DOWNLOAD LCC Win32 Code für diese Lektion. ( Conversion by Robert Wishlaw )
* DOWNLOAD Linux Code für diese Lektion. ( Conversion by Jay Groven )
* DOWNLOAD Linux/GLX Code für diese Lektion. ( Conversion by Patrick Schubert )
* DOWNLOAD Linux/SDL Code für diese Lektion. ( Conversion by DarkAlloy )
* DOWNLOAD LWJGL Code für diese Lektion. ( Conversion by Mark Bernard )
* DOWNLOAD Mac OS X/Cocoa Code für diese Lektion. ( Conversion by Bryan Blackburn )
* DOWNLOAD Visual Studio .NET Code für diese Lektion. ( Conversion by Grant James )



Deutsche Übersetzung: Joachim Rohde
Der original Text ist hier zu finden.
Die original OpenGL Tutorials stammen von NeHe's Seite.