Lektion 25: Auslesen von Objekten aus Dateien und Morphing
Willkommen zu dieser hoffentlich spannenden Lektion. Es wird ein immer wieder
gern genutzter Effekt, das Morphing vorgestellt. Dabei verwandelt sich ein Objekt
nahezu stufenlos in ein anderes. Es gibt eine kleine Einschränkung, alle
Objekte müssen die gleiche Anzahl von Punkten haben.
Wie in der Überschrift "versprochen", werden diese Objekte aus
Textdateien gelesen, das Format stammt noch aus Lektion 10.
Es geht los mit dem inkludieren der Header-Dateien. Glaux.h wird nicht benötigt,
da es Punkte statt Texturen zu bestaunen gibt. Später kann natürlich
immernoch mit anderen Grundprimitiven rumprobiert werden (LINES, QUADS usw).
#include<windows.h> // Header File For Windows
#include<math.h> // Math Library Header File
#include<stdio.h> // Header File For Standard Input/Output
#include<gl\gl.h> // Header File For The OpenGL32 Library
#include<gl\glu.h> // Header File For The GLu32 Library
HDChDC=NULL; // Device Context Handle
HGLRChRC=NULL; // Rendering Context Handle
HWNDhWnd=NULL; // Window Handle
HINSTANCE hInstance; // Instance Handle
bool keys[256]; // Key Array
bool active=TRUE; // Program's Active
bool fullscreen=TRUE; // Default Fullscreen To True
Es werden weitere Variablen benötigt: xrot, yrot und zrot speichern die
Achsenrotation des aktuellen Objektes. xspeed, yspeed und zspeed die jeweiligen
Rotationsgeschwindigkeiten, cx, cy und cz die Prosition.
key soll sicherstellen das der Benutzer nicht sinnloserweise ein Objekt in
sich selber morphen läßt (wird am Code klar), steps legt fest wieviele
Schritte eine Animation haben soll. je größer der Wert destso langsamer
aber auch "weicher" der Effekt.
morph wird TRUE wenn die Animation läuft (Damit jedes Morphing bis zum
Ende durchläuft!).
GLfloat xrot,yrot,zrot, // X, Y & Z Rotation
xspeed,yspeed,zspeed, // X, Y & Z Spin Speed
cx,cy,cz=-15; // X, Y & Z Position
int key=1; // Used To Make Sure Same Morph Key Is Not Pressed
int step=0,steps=200; // Step Counter And Maximum Number Of Steps
bool morph=FALSE; // Default morph To False (Not Morphing)
Es wird eine Structure Vertex erstellt die einen einfachen geometrischen Punkt
(3D) beschreibt.
typedef struct // Structure For 3D Points
{
float x, y, z; // X, Y & Z Points
} VERTEX; // Called VERTEX
Jetzt muss noch eine Structure für die Objekte her. verts speichert die
Anzahl der Punkte des Objektes. Die eigentliche Anzahl wird später im Code
festgelegt. "points" ist eine Referenz auf einen beliebigen Punkt
der Form VERTEX. Das erleichtert den Zugriff.
typedefstruct // Structure For An Object
{
int verts; // Number Of Vertices For The Object
VERTEX*points; // One Vertice (Vertex x,y & z)
} OBJECT; // Called OBJECT
Der Integer maxver speichert die maximale (und gleiche) Anzahl der Punkte in
den Objekten. Für die 4 verschiedenen Objekte werden Instanzen von OBJECT
erstellt. helper ist ebenfalls ein Objekt, *sour und *dest sind Zeiger auf ein
solches Objekt. helper wird für das Morphing an sich genutzt, *sour zeigt
auf das Ausgangsobjekt, *dest auf das Ziel.
int max ver; // Will Eventually Hold The Maximum Number Of Vertices
OBJECT morph1,morph2,morph3,morph4, // Our 4 Morphable Objects (morph1,2,3 & 4)
helper,*sour,*dest; // Helper Object, Source Object, Destination Object
WndProc() wie immer:
LRESULTCALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);// Declaration
Der untere Code reserviert Speicher für die zu ladenden Objekte n. *k
zeigt auf das aktuell zu ladende Objekt. "sizeof" gibt die größe
des freizumachenden Objektes an. Jedes OBJECT besteht dabei aus einer bestimmten
Anzahl von Punkten (VERTEX) die jeweils aus 3 floats bestehen.
void objallocate(OBJECT *k,int n) // Allocate Memory For Each Object
{ // And Defines points
k->points=(VERTEX*)malloc(sizeof(VERTEX)*n);
// Sets points Equal To VERTEX* Number Of Vertices
} // (3 Points For Each Vertice)
Hier wird der Speicher nach erfolgreichem Laden aufgeräumt:
void objfree(OBJECT *k) // Frees The Object (Releasing The Memory)
{
free(k->points); // Frees Points
}
Der folgende Code liest eine Zeichenkette *string aus einer Date *f (Zeiger
auf die Datei) mithilfe von fgets(); Wenn die Zeile leer ist oder ein Zeilenumbruch
stattfindet, wird abgebrochen.
void readstr(FILE *f,char *string)// Reads A String From File (f)
{
do// Do This
{
fgets(string, 255, f);
// Gets A String Of 255 Chars Max From f (File)
} while ((string[0] == '/') || (string[0] == '\n'));
// Until End Of Line Is Reached
return; // Return
}
objload lädt ein Objekt (*k zeigt drauf) aus einer Datei mit dem Pfad *name.
ver speichert die Anzahl der Punkte des Objekts.
rx, ry & rz speichern die Werte der Einzelpunkte.
filein zeigt auf die Datei mit den Objektdaten, oneline speichert 255 Zeichen.
Die Datei wird geöffnet, dabei soll diese als Textdatei behandelt werden,
Strg+z bezeichnet dabei das Ende einer Zeile. readstr(filein,oneline) liest
eine zeile der Datei und speichert diese in onneline.
Jetzt wird die gespeicherte Zeichenkette nach dem Ausdruck "Vertices:
{Anzahl} {\n}" durchsucht. Ist die Zeile gefunden, wird die angegebene
Zahl in ver gespeichert. (Das läßt sich am besten anhand einer Objektdatei
demonstrieren, einfach mal mit nem Texteditor angucken.)
Jetzt können auch Objekte mit unterschiedlicher Punkanzahl geladen werden,
wovon ich aber trotzdem, abrate. Als letztes wird die Speicherreservierungsfunktion
von vorhin genutzt.
void objload(char *name,OBJECT *k) // Loads Object From File (name)
{
int ver; // Will Hold Vertice Count
floatrx,ry,rz; // Hold Vertex X, Y & Z Position
FILE*filein; // Filename To Open
charoneline[255]; // Holds One Line Of Text (255 Chars Max)
filein = fopen(name, "rt"); // Opens The File For Reading Text In Translated Mode
// CTRL Z Symbolizes End Of File In Translated Mode
readstr(filein,oneline); // Jumps To Code That Reads One Line Of Text From The File
sscanf(oneline, "Vertices: %d\n", &ver); // Scans Text For "Vertices: ". Number After Is Stored In ver
k->verts=ver; // Sets Objects verts Variable To Equal The Value Of ver
objallocate(k,ver); // Jumps To Code That Allocates Ram To Hold The Object
Die Datei wird Zeile für Zeile nach den Punktkoordinaten für das Objekt
durchsucht, die Anzahl der Zeilen stimmt (hoffentlich) mit der Anzahl der Punkte
überein. Mit i wird durch die Punkte geloopt.
In jeder Zeile stecken Koordinaten der Form {X Y Z} die hier als Floats von
sscanf() ausgelesen und in den jeweiligen Variablen zwischengespeichert werden.
for (int i=0;i<ver;i++) // Loops Through The Vertices
{
readstr(filein,oneline); // Reads In The Next Line Of Text
sscanf(oneline, "%f %f %f", &rx, &ry, &rz); // Searches For 3 Floating Point Numbers, Store In rx,ry & rz
Hier werden die Punktkoordinaten in das aktuelle Objekt übertragen:
k ist der Zeiger auf das aktuelle Objekt, i ist gleichzeitig die Zeilennummer
und die Nummer des Punktes im Objekt (daher ist points[i] kein Problem, da der
Speicher eben freigegeben wurde. Da es sich um Punkte handelt wird x, y und
z übergeben.
k->points[i].x = rx; // Sets Objects (k) points.x Value To rx
k->points[i].y = ry; // Sets Objects (k) points.y Value To ry
k->points[i].z = rz; // Sets Objects (k) points.z Value To rz
}
fclose(filein); // Close The File
if(ver>maxver) maxver=ver; // If ver Is Greater Than maxver Set maxver Equal To ver
} // Keeps Track Of Highest Number Of Vertices Used
Das nächste Stück Code mag zuerst verwirren, ist aber schnell erklärt:
Es wird für jeden Punkt seine neue Position während des Morphings
berechnet. Die Nummer des zu berechnenden Punktes ist i.
a wird als temporärer Punkt erzeugt, hat also eine x-, y- und z-Koordinate.
Die Punkte sollen sich während der Animation vom Start zum Zielpunkt bewegen,
wobei sie eine Gerade beschreiben werden. Da es von sour nach dest gehen soll,
wird einer der Punkte subtrahiert und durch den aktuellen Frame der Animation
geteilt. a wird mit seinen drei Koordinaten zurückgegeben.
VERTEX calculate(int i) // Calculates Movement Of Points During Morphing
{
VERTEX a; // Temporary Vertex Called a
a.x=(sour->points[i].x-dest->points[i].x)/steps; // a.x Value Equals Source x-Destination x Divided By Steps
a.y=(sour->points[i].y-dest->points[i].y)/steps; // a.y Value Equals Source y-Destination y Divided By Steps
a.z=(sour->points[i].z-dest->points[i].z)/steps; // a.z Value Equals Source z-Destination z Divided By Steps
return a; // Return The Results
} // This Makes Points Move At A Speed So They All Get To Their
ReSizeGLScene() bleibt so. Einige Werte werden initiailisiert.
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Resize And Initialize The GL Window
(...)
int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Set The Blending Function For Translucency
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // This Will Clear The Background Color To Black
glClearDepth(1.0); // Enables Clearing Of The Depth Buffer
glDepthFunc(GL_LESS); // The Type Of Depth Test To Do
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glShadeModel(GL_SMOOTH); // Enables Smooth Color Shading
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Really Nice Perspective Calculations
maxver wird sicherheitshalber mit 0 initialisiert, da noch nicht klar ist wieviele
Punkte die Objekte haben werden.
Drei Objekte werden geladen, eine Kugel, ein Ring und ein Zylinder.
maxver=0;// Sets Max Vertices To 0 By Default
objload("data/sphere.txt",&morph1);// Load The First Object Into morph1 From File sphere.txt
objload("data/torus.txt",&morph2);// Load The Second Object Into morph2 From File torus.txt
objload("data/tube.txt",&morph3);// Load The Third Object Into morph3 From File tube.txt
Das vierte Objekt wird nicht aus einer Datei geladen, sondern mit zufällig
erzeugten Punkten gefüllt.
Der Speicher muss manuell freigegeben werden, dies passiert mit objallocate(&morph4,468).
468 meint zum Beispiel, das 468 Punkte erzeugt werden sollen. Die drei Zeilen
(morph4.points...) erzeugen Zufallspunkte zwischen +7 und -7. (rand()%14000)/1000
erzeugt Punkte zwischen 0 und 14, Sieben abgezogen ergibt das Zufallszahlen
zwischen +7 und -7.
objallocate(&morph4,486);
// Manually Reserver Ram For A 4th 468 Vertice Object (morph4)
for(int i=0;i<486;i++)// Loop Through All 468 Vertices
{
morph4.points[i].x=((float)(rand()%14000)/1000)-7;
// morph4 x Point Becomes A Random Float Value From -7 to 7
morph4.points[i].y=((float)(rand()%14000)/1000)-7;
// morph4 y Point Becomes A Random Float Value From -7 to 7
morph4.points[i].z=((float)(rand()%14000)/1000)-7;
// morph4 z Point Becomes A Random Float Value From -7 to 7
}
Jetzt wird sphere.txt als helper-Objekt geladen. Die geladenen Objekte in morph{1/2/3/4}
sollen nie direkt verändert werden. helper übernimmt das und bekommt
die Daten des jeweils aktuellen Objektes. Da das Beispiel zuerst morph1 darstellen
soll, wird sphere.txt in helper geladen.
sour und dest werden auch auf morph1 gesetzt um diese mit gültigen Werten
zu versehen.
objload("data/sphere.txt",&helper); // Load sphere.txt Object Into Helper (Used As Starting Point)
sour=dest=&morph1; // Source & Destination Are Set To Equal First Object (morph1)
return TRUE; // Initialization Went OK
}
Dem Rendering!
Alles wie gehabt, Bildschirm und depth-Puffer werden gelöscht, die Modelview
Matrix zurückgesetzt.
Die Rotation wird mithilfe der vorher definierten Variablen gesteuert.
Die Drehwinkel werden in jedem Frame um die jeweiligen Geschwindigkeiten ({x,y,z}speed)
erhöht.
3 temporäre Variablen und ein neuer Punkt q werden erstellt.
void DrawGLScene(GLvoid)// Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Clear The Screen And The Depth Buffer
glLoadIdentity();// Reset The View
glTranslatef(cx,cy,cz);// Translate The The Current Position To Start Drawing
glRotatef(xrot,1,0,0);// Rotate On The X Axis By xrot
glRotatef(yrot,0,1,0);// Rotate On The Y Axis By yrot
glRotatef(zrot,0,0,1);// Rotate On The Z Axis By zrot
xrot+=xspeed; yrot+=yspeed; zrot+=zspeed;// Increase xrot,yrot & zrot by xspeed, yspeed & zspeed
GLfloat tx,ty,tz;// Temp X, Y & Z Variables
VERTEX q;// Holds Returned Calculated Values For One Vertex
Die Punkte werden nacheinander ausgegeben und, falls Morphing läuft, diese
berechnet. Da sowieso alle Objekte die gleiche Größe haben, wird
morph1.verts als Maximalwert genutzt.
In der Schleife wird getestet ob morph TRUE ist. Sollte das zutreffen, werden
die Punkte bewegt, andernfall wird ihre Bewegung (q.x=0).
Entsprechend der Ergebnisse von calculate() bewegen sich die Punkte.
glBegin(GL_POINTS); // Begin Drawing Points
for(int i=0;i<morph1.verts;i++) // Loop Through All The Verts Of morph1 (All Objects Have
{
// The Same Amount Of Verts For Simplicity, Could Use maxver Also)
if(morph) q=calculate(i); else q.x=q.y=q.z=0;// If morph Is True Calculate Movement Otherwise Movement=0
helper.points[i].x-=q.x; // Subtract q.x Units From helper.points[i].x (Move On X Axis)
helper.points[i].y-=q.y; // Subtract q.y Units From helper.points[i].y (Move On Y Axis)
helper.points[i].z-=q.z; // Subtract q.z Units From helper.points[i].z (Move On Z Axis)
tx=helper.points[i].x; // Make Temp X Variable Equal To Helper's X Variable
ty=helper.points[i].y; // Make Temp Y Variable Equal To Helper's Y Variable
tz=helper.points[i].z; // Make Temp Z Variable Equal To Helper's Z Variable
Diese werden, mit entsprechenden Farben, ausgeben. Die Farbe wird ein wenig
abgedunkelt und ein zweiter Punkt wird neben dem ersten platziert, daneben noch
ein Dunkelblauer. Das Ergebnis ist ein leichter 3D-Effekt an dem Punkt..
glColor3f(0,1,1); // Set Color To A Bright Shade Of Off Blue
glVertex3f(tx,ty,tz); // Draw A Point At The Current Temp Values (Vertex)
glColor3f(0,0.5f,1); // Darken Color A Bit
tx-=2*q.x; ty-=2*q.y; ty-=2*q.y;// Calculate Two Positions Ahead
glVertex3f(tx,ty,tz); // Draw A Second Point At The Newly Calculate Position
glColor3f(0,0,1); // Set Color To A Very Dark Blue
tx-=2*q.x; ty-=2*q.y; ty-=2*q.y;// Calculate Two More Positions Ahead
glVertex3f(tx,ty,tz); // Draw A Third Point At The Second New Position
} // This Creates A Ghostly Tail As Points Move
glEnd();// Done Drawing Points
Wenn Morphing aktiviert ist und step kleiner als steps (hier 200) ist, wird
step erhöht, Für Werte >= 200 wird step 0 und die Animation ist
nach 200 Frames beendet.
// If We're Morphing And We Haven't Gone Through All 200 Steps Increase Our Step Counter
// Otherwise Set Morphing To False, Make Source=Destination And Set The Step Counter Back To Zero.
if(morph && step<=steps)step++; else { morph=FALSE; sour=dest; step=0;}
}
An KillGLWindow hat sich nicht viel verändert, zusätzlich wird noch
der durch die Objekte belegte Speicher gesäubert, was sich immer empfiehlt.
GLvoid KillGLWindow(GLvoid)// Properly Kill The Window
{
objfree(&morph1);// Jump To Code To Release morph1 Allocated Ram
objfree(&morph2);// Jump To Code To Release morph2 Allocated Ram
objfree(&morph3);// Jump To Code To Release morph3 Allocated Ram
objfree(&morph4);// Jump To Code To Release morph4 Allocated Ram
objfree(&helper);// Jump To Code To Release helper Allocated Ram
(...)
CreateGLWindow() und WndProc() bleiben so wie sie sind.
BOOL CreateGLWindow() // Creates The GL Window
LRESULT CALLBACK WndProc()// Handle For This Window
In WinMain müssen ein paar Änderungen vorgenommen werden.
int WINAPI WinMain(HINSTANCEhInstance, // Instance
HINSTANCEhPrevInstance, // Previous Instance
LPSTRlpCmdLine, // Command Line Parameters
intnCmdShow) // Window Show State
{
MSGmsg; // Windows Message Structure
BOOL done=FALSE; // Bool Variable To Exit Loop
// Ask The User Which Screen Mode They Prefer
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?",
"Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE;// Windowed Mode
}
// Create Our OpenGL Window
if (!CreateGLWindow("Piotr Cieslak & NeHe's Morphing Points Tutorial",
640,480,16,fullscreen))
{
return 0;// Quit If Window Was Not Created
}
while(!done)// Loop That Runs While done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))// Is There A Message Waiting?
{
if (msg.message==WM_QUIT)// Have We Received A Quit Message?
{
done=TRUE;// If So done=TRUE
}
else // If Not, Deal With Window Messages
{
TranslateMessage(&msg);// Translate The Message
DispatchMessage(&msg);// Dispatch The Message
}
}
else// If There Are No Messages
{
// Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene()
if (active && keys[VK_ESCAPE])// Active? Was There A Quit Received?
{
done=TRUE;// ESC or DrawGLScene Signaled A Quit
}
else// Not Time To Quit, Update Screen
{
DrawGLScene();// Draw The Scene (Don't Draw When Inactive 1% CPU Use)
SwapBuffers(hDC);// Swap Buffers (Double Buffering)
Der untere Code fragt ab, ob der Benutzer bestimmte Tasten gedrückt hat:
if(keys[VK_PRIOR])// Is Page Up Being Pressed?
zspeed+=0.01f;// Increase zspeed
if(keys[VK_NEXT])// Is Page Down Being Pressed?
zspeed-=0.01f;// Decrease zspeed
if(keys[VK_DOWN])// Is Page Up Being Pressed?
xspeed+=0.01f;// Increase xspeed
if(keys[VK_UP])// Is Page Up Being Pressed?
xspeed-=0.01f;// Decrease xspeed
if(keys[VK_RIGHT])// Is Page Up Being Pressed?
yspeed+=0.01f;// Increase yspeed
if(keys[VK_LEFT])// Is Page Up Being Pressed?
yspeed-=0.01f;// Decrease yspeed
Die folgenden Taste bewegen das Objekt im Raum, die vorherigen haben die Drehung
verändert.
if (keys['Q'])// Is Q Key Being Pressed?
cz-=0.01f;// Move Object Away From Viewer
if (keys['Z'])// Is Z Key Being Pressed?
cz+=0.01f;// Move Object Towards Viewer
if (keys['W'])// Is W Key Being Pressed?
cy+=0.01f;// Move Object Up
if (keys['S'])// Is S Key Being Pressed?
cy-=0.01f;// Move Object Down
if (keys['D'])// Is D Key Being Pressed?
cx+=0.01f;// Move Object Right
if (keys['A'])// Is A Key Being Pressed?
cx-=0.01f;// Move Object Left
Jetzt werden die Tasten 1 bis 4 kontrolliert. Damit das gleiche Objekt nicht
zweimal nacheinander ineinander gemorpht wird (man sähe genau gar nichts
davon), speichert key die jeweils letzte Taste. Außerdem wird geprüft
ob morph überhaupt TRUE ist.
if (keys['1'] && (key!=1) && !morph)// Is 1 Pressed, key Not Equal To 1 And Morph False?
{
key=1;// Sets key To 1 (To Prevent Pressing 1 2x In A Row)
morph=TRUE;// Set morph To True (Starts Morphing Process)
dest=&morph1;// Destination Object To Morph To Becomes morph1
}
if (keys['2'] && (key!=2) && !morph)// Is 2 Pressed, key Not Equal To 2 And Morph False?
{
key=2;// Sets key To 2 (To Prevent Pressing 2 2x In A Row)
morph=TRUE;// Set morph To True (Starts Morphing Process)
dest=&morph2;// Destination Object To Morph To Becomes morph2
}
if (keys['3'] && (key!=3) && !morph)// Is 3 Pressed, key Not Equal To 3 And Morph False?
{
key=3;// Sets key To 3 (To Prevent Pressing 3 2x In A Row)
morph=TRUE;// Set morph To True (Starts Morphing Process)
dest=&morph3;// Destination Object To Morph To Becomes morph3
}
if (keys['4'] && (key!=4) && !morph)// Is 4 Pressed, key Not Equal To 4 And Morph False?
{
key=4;// Sets key To 4 (To Prevent Pressing 4 2x In A Row)
morph=TRUE;// Set morph To True (Starts Morphing Process)
dest=&morph4;// Destination Object To Morph To Becomes morph4
}
Mit F1 kann der Benutzer Vollbild und Fenster umschalten.
if (keys[VK_F1])// Is F1 Being Pressed?
{
keys[VK_F1]=FALSE;// If So Make Key FALSE
KillGLWindow();// Kill Our Current Window
fullscreen=!fullscreen;// Toggle Fullscreen / Windowed Mode
// Recreate Our OpenGL Window
if (!CreateGLWindow("Piotr Cieslak & NeHe's Morphing Points Tutorial",
640,480,16,fullscreen))
{
return 0;// Quit If Window Was Not Created
}
}
}
}
}
// Shutdown
KillGLWindow();// Kill The Window
return (msg.wParam);// Exit The Program
}
Das wars auch schon :). Ich hoffe das Tutorial ist nicht zu komplex geraten
und hat Spaß gemacht. Man kann mit QUADS und Texturen noch eine Menge
aus dem Morphing-Effekten machen, immer fleißig rumprobieren, bis die
Tage!
codeworx.
Piotr Cieslak : http://homepage.ntlworld.com/fj.williams/PgSoftware.html
Jeff Molofee (NeHe) http://nehe.gamedev.net
Die Source Codes und Ausführbaren Dateien zu den Kursen liegen auf der Neon
Helium Website
Übersetzt und modifiziert von Hans-Jakob Schwer 07.08.2k3, www.codeworx.org