www.codeworx.org/opengl-tutorials/Tutorial 25: Auslesen von Objekten aus Dateien und Morphing

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