Verwendung von virtuellen Dateien

In den bisherigen OpenGL-Tutorials erfolgte das Einlesen der Mesh-Daten stets zeilenweise aus einer Textdatei heraus. Dieses Vorgehen ist alles andere als effizient und schnell – mit anderen Worten, wir sollten zukünftig einen anderen Weg einschlagen.

Bisher haben wir Dateien immer nur bei Programmstart eingelesen. Normalerweise sind Dateizugriffe jedoch auch zur Laufzeit notwendig, da sich komplexere 3D-Welten nicht komplett im Speicher halten lassen. Texturen, 3D-Modelle und selbstverständlich auch die Hintergrundmusik müssen dynamisch nachgeladen werden (Data Streaming). Das Auslesen einer Datei ist jedoch eine zeitaufwendige Angelegenheit, die nicht beliebig optimiert werden kann, da die notwendigen Festplattenzugriffe mechanisch (!) erfolgen. Es bietet sich an, eine Datei zunächst vollständig auszulesen und ihren Inhalt für den späteren Gebrauch in einem Daten-Array zwischenzuspeichern. Das Auslesen einer Datei kann selbstverständlich in einem separaten Thread erfolgen, um das Hauptprogramm nicht unnötig auszubremsen.

Zum Auslesen einer Textdatei verwenden wir die nachfolgende ReadFile()-Funktion. Als Rückgabewert liefert uns die Funktion ein Daten-Array vom Typ char, in welchem die komplette Datei gespeichert ist. Ein solches Speicherabbild wird auch als virtuelle Datei bezeichnet. Aus einer virtuellen Datei lassen sich einzelne Daten später sehr viel schneller auslesen, da hierbei die mechanischen und damit langsamen Festplattenzugriffe entfallen.

inline char* ReadFile(char *pFileName, bool ignoreLineBreaks = true)
{
    FILE *fp;
    char *content = NULL;
    int count=0;

    Add_To_Log(pFileName,"opening...");

    if(pFileName != NULL)
    {
        // Datei öffnen:
        fp = fopen(pFileName, "rt");

        if(fp != NULL)
        {
            // Lesezeiger ans Dateiende setzen:
            fseek(fp, 0, SEEK_END);
            count = ftell(fp); //Leseposition ermitteln
            rewind(fp); // Lesezeiger wieder auf Anfang setzen

            if(count > 0)
            {
                // Speicher reservieren, um die Daten aus der Datei
                // aufzunehmen:

                content = (char *)malloc(sizeof(char) * (count+2));
                // Daten aus Datei auslesen:
                count = fread(content,sizeof(char),count,fp);
                content[count] = ' ';
                content[count+1] = '\0';
            }

            // Datei schließen:
            fclose(fp);
        }
        else
            Add_To_Log(pFileName,"file not found");
    }

    if(content && ignoreLineBreaks ==
true)
    {
        count++;

        // Zeilenumbrüche entfernen:
        for(long i = 0; i < count; i++)
        {
            if(content[i] == '\n')
                content[i] = ' ';
        }
    }

    return content;
}


Sollen anstelle von Textdateien binäre Dateien eingelesen werden, so kann die ReadBinaryFile()-Funktion verwendet werden, die uns als Rückgabewert ebenfalls ein Daten-Array vom Typ char liefert.

inline char* ReadBinaryFile(char *pFileName)
{
    FILE *fp;
    char *content = NULL;
    int count=0;

    Add_To_Log(pFileName,"opening...");

    if(pFileName != NULL)
    {
        fp = fopen(pFileName, "rb");

        if(fp != NULL)
        {
            fseek(fp, 0, SEEK_END);
            count = ftell(fp);
            rewind(fp);

            if(count > 0)
            {
                content = (char *)malloc(sizeof(char) * (count+2));
                count = fread(content,sizeof(char),count,fp);
                content[count] = ' ';
                content[count+1] = '\0';
            }

            fclose(fp);

        }
        else
            Add_To_Log(pFileName,"file not found");
    }

    return content;
}


Bei allen Dateioperationen haben wir bisher immer einen Zeiger vom Typ FILE benötigt. Für die Arbeit mit unseren virtuellen Dateien benötigen wir eine ähnliche Struktur bzw. Klasse – CVirtualFile. Die zentrale Aufgabe der CVirtualFile-Klasse besteht in der Speicherung der aktuellen Leseposition beim Auslesen einer virtuellen Datei.

class CVirtualFile
{
public:

    char* ptr; // Leseposition
    long  length;

    CVirtualFile(char* pData)
    {
        ptr = pData;
        length = strlen(pData);
    }

    ~CVirtualFile()
    {}
    void SetData(char* pData)
    {
        ptr = pData;
        length = strlen(pData);
    }

};


Textdateien

Zum Auslesen der Daten aus einer virtuellen Textdatei verwenden wir die vierfach überladene ReadFileData()-Funktion. Das Ergebnis der Leseoperation wird in den Funktionsparameter pData zurückgeschrieben. Je nach Überladung kann pData ein Zeiger vom Typ char, long, BOOL(int) oder float sein.
Bevor wir uns mit den Einzelheiten der ReadFileData()-Funktionen befassen, sollten wir uns zunächst anschauen, wie diese Funktionen überhaupt in der Praxis eingesetzt werden:

char strBuffer[200];
float xOffset, yOffset, zOffset;
BOOL  GenerateNormalsPerTriangle;

// Textdatei auslesen und im Data-Array speichern:
char* Data = ReadFile(pMeshFile);
CVirtualFile FileData(Data);

. . .

// Daten aus der virtuellen Textdatei auslesen:
ReadFileData(&FileData, strBuffer);
ReadFileData(&FileData, &xOffset);

ReadFileData(&FileData, strBuffer);
ReadFileData(&FileData, &yOffset);

ReadFileData(&FileData, strBuffer);
ReadFileData(&FileData, &zOffset);

ReadFileData(&FileData, strBuffer);
ReadFileData(&FileData, &GenerateNormalsPerTriangle);

. . .

// Data-Array wieder löschen:
free(Data);
Data = NULL;


Mithilfe der nachfolgenden Funktion lässt sich eine Zeichenkette aus einer virtuelle Datei auslesen. Der Datenzeiger ptr wird zunächst so lange verschoben, bis das erste Element der Zeichenkette gefunden ist. Im Anschluss daran wird die Zeichenkette Zeichen für Zeichen in den Funktionsparameter pData kopiert, bis das Ende der Kette erreicht ist.

inline void ReadFileData(CVirtualFile* pVirtualFile, char* pData,
                         long length = 200)
{
    long i, firstchar;
    firstchar = -1;

    char* ptr = pVirtualFile->ptr;

    i = 0;

    while(1)
    {
        if(i == length-1)
            break;

        // erstes Element der Zeichenkette suchen:
        if(firstchar == -1)
        {
            // erstes Element gefunden:
            if(*ptr != ' ')
                firstchar = i;
        }

        // Falls das erste Element bereits gefunden wurde . . .
        if(firstchar != -1)
        {
            // letztes Element gefunden:
            if(*ptr == ' ')
                break; // Ende der Zeichenkette

            // Zeilenende erreicht:
            if(*ptr == '\n')
                break; // Ende der Zeichenkette

            // neues Zeichenketten-Element ins pData-Array
            // an die Arrayposition (i-firstchar) kopieren:
            sscanf(ptr,"%1s", pData+i-firstchar);
        }

        ptr++;
        i++;
    }

    pVirtualFile->ptr = ptr + 1;
}


Zum Auslesen einer Ganzzahl dient die nachfolgende Funktion. Auch hier wird zunächst nach einem neuen Eintrag gesucht. Ist dieser gefunden, wird der Eintrag in eine Variable vom Typ long konvertiert und im Funktionsparameter pData gespeichert. Im Anschluss daran werden alle Trennzeichen (Komma, Semikolon, usw.) übersprungen, die ohne Leerzeichen direkt auf den numerischen Eintrag folgen:

inline void ReadFileData(CVirtualFile* pVirtualFile, long* pData)
{
    char* ptr = pVirtualFile->ptr;

    do
    {
        // Nach einem neuen Eintrag suchen:
        if(*ptr != ' ')
        {
            // Eintrag in eine Variable vom Typ long konvertieren
            // und in pData speichern:
            *pData = atol(ptr);
            ptr++;
            break;
        }

        ptr++;

    }while(1);

    // Alle Trennzeichen (Komma, Semikolon, usw.) überspringen,
    // die ohne Leerzeichen direkt auf den numerischen Eintrag folgen:
    do
    {
        if(*ptr == ' ')
            break;

        ptr++;

    }while(1);

    pVirtualFile->ptr = ptr + 1;
}


Die Funktion zum Auslesen einer Variablen vom Typ BOOL arbeitet exakt wie die Funktion zum Auslesen einer Variablen vom Typ long:

inline void ReadFileData(CVirtualFile* pVirtualFile, BOOL* pData)
{
    char* ptr = pVirtualFile->ptr;

    do
    {
        // Nach einem neuen Eintrag suchen:
        if(*ptr != ' ')
        {
            // Eintrag in eine Variable vom Typ BOOL konvertieren
            // und in pData speichern:
            *pData = atol(ptr);
            ptr++;
            break;
        }

        ptr++;

    }while(1);

    // Alle Trennzeichen (Komma, Semikolon, usw.) überspringen,
    // die ohne Leerzeichen direkt auf den numerischen Eintrag folgen:
    do
    {
        if(*ptr == ' ')
            break;

        ptr++;

    }while(1);

    pVirtualFile->ptr = ptr + 1;
}


Auch die Funktion zum Auslesen einer Variable vom Typ float arbeitet exakt wie die Funktion zum Auslesen einer Variable vom Typ long, lediglich die Konvertierung in eine float-Variable erfolgt mithilfe einer anderen Funktion (atof() anstelle von atol()).

inline void ReadFileData(CVirtualFile* pVirtualFile, float* pData)
{
    char* ptr = pVirtualFile->ptr;

    do
    {
        // Nach einem neuen Eintrag suchen:
        if(*ptr != ' ')
        {
            // Eintrag in eine Variable vom Typ float konvertieren
            // und in pData speichern:
            *pData = atof(ptr);
            ptr++;
            break;
        }

        ptr++;

    }while(1);

    // Alle Trennzeichen (Komma, Semikolon, usw.) überspringen,
    // die ohne Leerzeichen direkt auf den numerischen Eintrag folgen:
    do
    {
        if(*ptr == ' ')
            break;

        ptr++;

    }while(1);

    pVirtualFile->ptr = ptr + 1;
}


Binärdateien

Zum Auslesen der Daten aus einer virtuellen Binärdatei verwenden wir die dreifach überladene ReadFileDataBin()-Funktion. Das Ergebnis der Leseoperation wird in den Funktions-Parameter pData zurückgeschrieben. Je nach Überladung kann pData ein Zeiger vom Typ long, BOOL oder float sein.

Hinweis:
Binärdateien werden wir zum Speichern von Mesh-Daten einsetzen. Da wir in diesem Zusammenhang lediglich numerische Werte speichern (Kommentare und dergleichen sind hier überflüssig und daher Platzverschwendung), sparen wir uns an dieser Stelle eine Funktion zum Auslesen von Zeichenketten.

Bevor wir uns mit den Einzelheiten der ReadFileDataBin()-Funktionen befassen, sollten wir uns zunächst wiederum anschauen, wie diese Funktionen in der Praxis eingesetzt werden:

// Binärdatei auslesen und im Data-Array speichern:
char* Data = ReadBinaryFile(pMeshFile);
CVirtualFile FileData(Data);

. . .

// Daten aus der virtuellen Binärdatei auslesen:
for(i = 0; i < NumMeshVertices; i++)
{
    ReadFileDataBin(&FileData, &tempLong); // Vertexnummer
    ReadFileDataBin(&FileData, &VertexArray[i].x);
    ReadFileDataBin(&FileData, &VertexArray[i].y);
    ReadFileDataBin(&FileData, &VertexArray[i].z);

. . .
}

. . .

// Data-Array wieder löschen:
free(Data);
Data = NULL;


Das Auslesen von numerischen Daten aus einer Binärdatei funktioniert sehr viel einfacher als bei einer Textdatei. Die einzelnen Dateneinträge sind direkt aneinandergereiht und können daher ohne Umweg in den Funktions-Parameter pData kopiert werden:

inline void ReadFileDataBin(CVirtualFile* pVirtualFile, long* pData)
{
    char* ptr = pVirtualFile->ptr;
    // Dateneintrag in pData speichern:
    memcpy(pData, ptr, 4);
    pVirtualFile->ptr = ptr + 4;
}

inline void ReadFileDataBin(CVirtualFile* pVirtualFile, BOOL* pData)
{
    char* ptr = pVirtualFile->ptr;
    // Dateneintrag in pData speichern:
    memcpy(pData, ptr, sizeof(BOOL));
    pVirtualFile->ptr = ptr + sizeof(BOOL);
}

inline void ReadFileDataBin(CVirtualFile* pVirtualFile, float* pData)
{
    char* ptr = pVirtualFile->ptr;
    // Dateneintrag in pData speichern:
    memcpy(pData, ptr, 4);
    pVirtualFile->ptr = ptr + 4;
}



inline void ReadFileDataBin(CVirtualFile* pVirtualFile, char* pData,
                            long length = 200)
{
    char* ptr = pVirtualFile->ptr;
    memcpy(pData, ptr, length);
    pVirtualFile->ptr = ptr + length;
}