Einfache Formen

Beispiel

Visual Studio Projekt herunterladen

In unserem ersten grafischen Projekt gehen wir einen kleinen Schritt zurück und arbeiten vorerst mit Funktionen statt mit Klassen. In der Datei main.cpp gibt es fünf Funktionen:

renderSquare
renderCircle
renderStar
display
main

Funktionen müssen wie Variablen erst deklariert werden, bevor sie verwendet werden können. In der Beschreibung der einzelnen Funktionen möchte ich allerdings anders herum vorgehen.

Die main-Funktion

int main(int argc, char* argv[])
{
    // GLUT mit Kommandozeilenparametern initialisieren
    glutInit(&argc, argv);

Die Funktion main ist dieses Mal in vollständiger Form mit den Kommandozeilenparametern argc und argv angegeben (argument count und argument value). Wir benötigen sie nur, um GLUT korrekt zu initialisieren. Abgesehen davon sind sie vorerst uninteressant für uns.

    // RGB-Modus mit Hintergrundpuffer wählen
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);

Nach der Initialisierung von GLUT setzen wir unseren Darstellungsmodus auf RGB-Farben mit Hintergrundpuffer (Doublebuffering). Doublebuffering ermöglicht uns, in einen unsichtbaren Hintergrundspeicher zu zeichnen und diesen Speicherbereich erst nach dem Zeichnen darzustellen, womit unser Bild auf einen Schlag angezeigt wird.

Wir besitzen also zwei Puffer, einen zum Zeichnen und einen, der gerade angezeigt wird. Würden wir direkt in den gerade angezeigten Speicherbereich schreiben, könnte der Benutzer zusehen, wie das Bild gezeichnet wird.

    // Fenster erzeugen
    glutCreateWindow("Formen");

Nachdem wir den Darstellungsmodus gesetzt haben, können wir ein Fenster, hier mit dem Titel "Formen" erzeugen.

    // unsere eigene Anzeigefunktion setzen
    glutDisplayFunc(display);

Jedes Mal, wenn das Fenster neu gezeichnet werden soll, z.B. wenn es erzeugt wird oder sich seine Größe ändert, soll GLUT unsere eigene Funktion display aufrufen. Dabei habdelt es sich um einen sogenannten Callback-Mechanismus. Unsere display Funktion hat die Aufgabe, alle Zeichenaufgaben zu erledigen, die notwendig sind.

    // Hauptschleife des Programms aufrufen
    glutMainLoop();

    // Alles ok, Programm beenden
    return 0;

} // main()

Anschließend rufen wir die Hauptschleife des Programms auf. In unserem Fall läuft das Programm solange, bis unser von GLUT verwaltetes Fenster wieder geschlossen wird. Dummerweise springt GLUT danach nicht zurück in unsere main Funktion, sondern beendet das Programm selbständig. Zum return-Befehl wird unser Programm daher leider gar nicht vordringen.

Die display-Funktion

void display(void)
{
    // Hintergrundfarbe (rgba) setzen
    glClearColor(0.0, 0.0, 0.0, 1.0);

    // Hintergrund löschen
    glClear(GL_COLOR_BUFFER_BIT);

Die ersten beiden Befehle löschen den Hintergrund des Anzeigefensters mit der Farbe Schwarz.

    // orthogonale Projektion setzen (2D)
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1.5, 1.5,  // X Achsen Koordinaten von..bis
            -1.5, 1.5,  // Y Achsen Koordinaten von..bis
            -1.0, 1.0); // Z Achsen Koordinaten von..bis

OpenGL arbeitet mit verschiedenen Matrizen, um geometrische Transformationen durchzuführen. Wir werden noch mehrere dieser Matrizen kennenlernen. In unserem Beispiel möchten wir eine orthogonale (nicht perspektivische) Projektion wählen.

Dazu schalten wir mit glMatrixMode zunächst in einen Modus, der das Bearbeiten der Projektionsmatrix ermöglicht. Anschließend löschen wir alle Transformationen mit Hilfe des Befehls glLoadIdentity.

Mit dem Befehl glOrtho geben wir danach für alle Achsen die sichtbaren Abschnitte an. In unserem Fall entspricht die linke untere Ecke des Fensters dem Vektor (-1.5, 1.5), die rechte obere Ecke dem Vektor (1.5, 1.5). Die Z-Achse ist zwar angegeben, wird allerdings in diesem Beispiel nicht von Belang sein.

    // Quadrat zeichnen
    glColor3f(0.2f, 0.2f, 0.2f);
    renderSquare();
    // Kreis zeichnen
    glColor3f(0.8f, 0.8f, 0.8f);
    renderCircle(50);
    // Stern zeichnen
    glColor3f(0.5f, 0.5f, 0.5f);
    renderStar(5);

Nachdem das Ausgabefenster mit einer Farbe gelöscht wurde und unsere Achsen vorbereitet sind, setzen wir nacheinander verschiedene Farben und rufen wir unsere eigenen Zeichenroutinen auf.

    // Hinteren Bildschirmpuffer nach vorne bringen
    glutSwapBuffers();

} // display()

Gezeichnet wird alles, wie gesagt, in einen Hintergrundpuffer. Durch den Aufruf von glutSwapBuffers bringen wir den Hintergrundpuffer zur Anzeige und der bisher angezeigte Puffer wird zum neuen Hintergrundpuffer. Das erklärt auch, warum wir den Puffer vor dem Zeichnen jedes Mal neu löschen müssen.

Unsere display-Funktion besitzt keinen return-Befehl, da sie keinen Wert zurückliefert.

renderCircle etc.

Stellvertretend für die Zeichenroutinen sei hier nur renderCircle erläutert.

void renderCircle(unsigned int steps)
{

OpenGL liefert selbst keine Routinen zum Zeichnen von Kurven. Wir müssen das schon selbst in die Hand nehmen und einen Kreis durch viele Dreiecke annähern. Die Anzahl der Dreiecke übergeben wir der Funktion durch den step Parameter.

    // Dreiecksfächer zeichnen
    glBegin(GL_TRIANGLE_FAN);

    // Zentrum (ertsten Punkt) setzen
    glVertex2f(0.0f, 0.0f);

Wir möchten einen Dreiecksfächer mit dem Ursprung (0, 0) als Zentrum zeichnen. Den Ursprung setzen wir zuerst, danach fehlen uns noch die umlaufenden Punkte des Kreises, die in einer for-Schleife berechnet werden.

    for (unsigned int i = 0; i <= steps; i++)
    {
        // Umlaufenden Winkel berechnen
        float angle = float(i)/float(steps) * 2.0f * PI;

        // Kreisfunktionen Sinus und Cosinus verwenden
        float x = sinf(angle);
        float y = cosf(angle);

        // Punkt setzen
        glVertex2f(x, y);
    }

Trigonometrische Funtkionen wie Sinus und Cosinus werden in C++ nicht in Grad sondern in Radian angegeben, also in Werten zwischen 0 und zwei PI. OpenGL arbeitet größtenteils in Grad.

Um begrenzt viele Winkel zwischen 0 und zwei PI zu druchlaufen, teilen wir zuerst i durch die Anzahl der Dreiecke und multiplizieren danach mit 2*PI. Sinus und Kosinus dieses Winkels verwenden wir danach, um einen Punkt mit seinen Koordinaten auf der x- bzw. y-Achse zu setzen (Einheitskreis).

Wichtig ist, dass wir die ganzzahligen Werte i und steps vor dem Teilen in Gleitkommazahlen umrechnen müssen. C++ würde sonst davon ausgehen, dass das Ergebnis zweier miteinander verknüpfter, ganzer Zahlen wieder eine ganze Zahl ergeben soll. Standardmäßig rundet C++ bei Ganzzahldivisionen ab, 2/3 ergäbe also z.B. 04/3 ergäbe 1. i/steps allein würde uns also, bis auf den Fall i==steps immer 0 zurückliefern.

    // Ende des Dreiecksfächers
    glEnd();

} // renderCircle()

Wir verwenden hier durchgehend die Funtkion glVertex2f und nicht glVertex3f. Als Regel gilt: So wenige Daten wie möglich sollten an OpenGL weitergegeben werden, da jede weitere Koordinate verarbeitet werden muss und Zeit kosten würde.

Selbständige Programmierung

  • Viel Spaß bei eigenen Formen...

zurück