QGL1 – OpenGL mit Qt: Grundgerüst

In der QGL Tutorial Serie möchte ich euch mit diesem Post beginnend die Grundlagen von OpenGL unter Benutzung des Qt Frameworks näher bringen.
Da dies ein Tutorial sein soll und kein Roman, werde ich nicht weiter darauf eingehen was genau OpenGL und Qt sind und worin ihre Vorteile liegen. Dafür könnt ihr euch gerne entsprechende Wikipedia Artikel über OpenGL und Qt durchlesen. Kurz zusammengefasst könnte man sagen, dass wir möglichst offen und platformunabhängig arbeiten wollen.
Ziel dieses ersten Tutorials ist es ein OpenGL fähiges Fenster anzuzeigen und darauf ein farbiges Dreieck zu zeichnen.

Die Qt Project Datei – qgl1.pro

QT += opengl
TARGET = qgl1
TEMPLATE = app
SOURCES += main.cpp \
    glwidget.cpp
HEADERS  += \
    glwidget.h

Qt hat ein eigenes Modul für OpenGL spezifische features wie z.b. das QGLWidget welches wir als Ausgabefenster benutzen werden.
Mit der Zeile Qt += opengl teilen wir Qt mit, dass wir das Modul laden und benutzen wollen. Sonst gibt es an der Projekt Datei nichts besonderes zu beachten.

Der GLWidget Header – glwidget.h

#ifndef GLWIDGET_H
#define GLWIDGET_H
#include <QGLWidget>
 
class GLWidget : public QGLWidget
{
    Q_OBJECT
public:
    explicit GLWidget(QWidget *parent = 0);
protected:
    void initializeGL();
    void resizeGL(int width, int height);
    void paintGL();
};
#endif // GLWIDGET_H

Qt stellt mit QGLWidget ein sehr schönes Widget zu Verfügung, dass uns viel Arbeit bei der Initialisierung und Benutzung von OpenGL abnimmt.
Wir includen QGLWidget, erstellen eine eigene Klasse und vererben ihr QGLWidget.

Wie in der Qt QGLWidget Dokumentation zu sehen ist besitzt QGLWidget eine Reihe von virtueller Funktionen. Von diesen Funktionen interessieren uns im Moment nur drei, nämlich initializeGL, resizeGL und paintGL.

Der GLWidget Code – glwidget.cpp

#include "glwidget.h"
 
/* konstruktor */
GLWidget::GLWidget() {
    setWindowTitle("QGL1");
}

Der Konstruktor sollte selbst erklärend sein. Wir setzen nur den Fenster Titel und das wars auch schon.

/* initialisierung von OpenGL */
void GLWidget::initializeGL() {
    // schaltet smooth shading an
    glShadeModel(GL_SMOOTH);
    // stellt die Farbe zum clearen des bildschirms auf schwarz
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 
    // stellt die beste perspektiven korrektion ein
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
}

Bitte entschuldigt sollten die Kommentare etwas schwer verständlich sein. Normalerweise kommentiere ich ausschließlich in Englisch und es ist etwas ungewohnt sie in Deutsch zu schreiben.

Die Funktion initializeGL() wird immer vor dem ersten Aufruf von paintGL() oder resizeGL() und nach jeder GLContext Zuweisung aufgerufen. In ihr sollen alle Aufgaben zur Initialisierung von OpenGL ausgeführt werden. In unserem einfachen Beispiel müssen wir nur ein paar Einstellungen festlegen.

Smooth shading sorgt dafür das die Farben von Polygonen sauber ineinander überlaufen. Desweiteren hat es Effekte auf Lighting Effekte die wir im Moment außen vor lassen.
Die clear color wird dafür benutzt um am Anfang von jedem Frame die Zeichenfläche komplett zu übermalen. Sie dient uns also auch indirekt als Hintergrundfarbe.
Als letztes teilen wir OpenGL noch mit, dass wir die beste Perspektiven Korrektion benutzen möchten. Das benötigt zwar etwas mehr Rechenaufwand liefert aber ein deutlich besseres Ergebnis.

/* fenster größe ändern */
void GLWidget::resizeGL(int width, int height) {
    // aktuellen viewport resetten
    glViewport(0, 0, width, height);
 
    // projektions matrix auswählen
    glMatrixMode(GL_PROJECTION);
    // matrix resetten
    glLoadIdentity();
 
    // orthogonale ansicht einstellen
    glOrtho(0, width, height, 0, -1, 1);
 
    // modelview matrix auswählen
    glMatrixMode(GL_MODELVIEW);
    // matrix resetten
    glLoadIdentity();
}

resizeGL() wird nicht nur ausgeführt wenn sich die Größe des Widgets ändert, sondern auch einmal bei deren Erstellung. Dementsprechend kümmern wir uns hier um den Viewport, die Transformationsmatrizen usw.

Als erstes wählen wir unsere Projektionsmatrix aus und setzen diese mit glLoadIdentity() auf die Einheitsmatrix zurück. Die folgende glOrtho() Funktion berechnet dann für uns eine parallele Projektionsmatrix.
Mit der Parallelprojektion sorgen wir dafür, dass die Z Koordinate keinen Einfluss mehr auf die Größe hat, in der ein Objekt gezeichnet wird. Wir schalten OpenGL also in einen 2D Modus. Mit den Parametern der Funktion definieren wir die Dimensionen unseres View Volumes.

OpenGL hat kein striktes Koordinatensystem wie man sie aus anderen Grafik librarys kennt, denn es unterteilt die Zeichenfläche nicht in Pixel sondern in Einheiten die man selbst definieren kann.
Ich persöhnlich definiere es gerne aber trotzdem in der Pixelgröße des Ausgabefensters mit dem Ursprung in der oberen linken Ecke.
In folgenden Tutorials werde ich nach und nach tiefer in die Materie vordringen. Vorallem wenn wir in 3D zeichnen werden wird es noch etwas komplizierter. Im Moment soll es uns aber nicht weiter stören.
Zum Schluss resetten wir die Modelview Matrix und sind mit der Funktion fertig.

/* zeichnet die szene */
void GLWidget::paintGL() {
    // setzt den color und depth buffer zurück
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // resettet die modelview matrix
    glLoadIdentity();
 
    glBegin(GL_TRIANGLES);
        // oben
        glColor3f(1.0f, 0.0f, 0.0f);
        glVertex2d(width()/2, 0.0f);
        // unten links
        glColor3f(0.0f, 1.0f, 0.0f);
        glVertex2d(0, height());
        // unten rechts
        glColor3f(0.0f, 0.0f, 1.0f);
        glVertex2d(width(), height());
    glEnd();
}

Nun kommen wir endlich zum eigentlichen zeichnen unseres tollen Dreiecks. In jedem Frame rufen wir als erstes glClear() und glLoadIdentity() auf um die Buffer und Modelview Matrix zurück zu setzen.
glBegin(GL_TRIANGLES) teilt OpenGL mit, dass wir Dreiecke zeichnen möchten und glEnd() signalisiert am Ende logischerweise das wir fertig sind.
Ein Dreieck besteht wie der Name schon sagt aus drei Ecken also müssen wir innerhalb des glBegin/glEnd Blocks 3 Vertices definieren.

Ein Vertex (Plural: Vertices) ist ein Punkt im Raum und besteht aus zwei bis vier Koordinaten. Ein Weg um in OpenGL Vertices zu definieren ist die glVertex Funktion. Diese hat verschiedene Suffixe jenachdem wieviele Koordinaten von welchem Typ wir angeben wollen.
In unserem Fall benutzen wir glVertex2d(), weil wir ein Vertex mit 2 Koordinaten (x, y) des Typs Interger definieren möchten.
Um dem ganzen noch etwas Glanz zu verleihen verändern wir vor jedem Vertex die Zeichenfarbe um einen Verlauf zu bekommen.

Die main Methode und das Schlusswort

#include <QtGui/QApplication>
#include "glwidget.h"
 
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
 
    GLWidget window;
    window->show();
 
    return a.exec();
}

Die main Methode sollte wohl von jedem ohne zusätzliche Worte zu verstehen sein. Ich habe sowieso schon wieder viel zu viel geschrieben.
Hoffentlich kann euch dieses kleine Tutorial bei euren Anfängen mit OpenGL weiterhelfen.
In den nächsten Tutorials wird es dann etwas interesanter und wir werden Themen wie 3D, Transparenz, Translation, Texturen usw behandeln. Außerdem werde ich euch andere Zeichentechniken zeigen, die um einiges besser sind als den hier aus Einfachheit benutzten Intermediate Mode. Dann hoffentlich auch knackiger mit weniger Worten ;)

Bitte korrigiert mich sollten sich Fehler eingeschlichen haben. Kommentare und Feedback sind immer erwünscht und bei Fragen könnt ihr mich auch gerne auf Twitter anschreiben. Anschließend verlinke ich euch noch den kompletten code als tar Archiv.

Bis zum nächsten mal

Download source code

flattr this!