OpenGL: Rotierender Würfel mit Phong Shading
Um eine kleine Grundlage für die nachfolgenden Artikel zu bestimmten 3D-Programmierungsdetails zu geben, hab ich hier ein kleines, minimalistisches 3D Beispiel erstellt. Die Implementierung ist als komplettes Beispiel herunterladbar:
Das Beispielprojekt zeigt ein rotierendes, beleuchtetes 11×11×11-Gitter aus Würfeln mit Qt 6 und OpenGL 3.3 Core Profile. Jeder Würfel wird mit Phong Shading gerendert. Die Lichtquelle und die Kamera ist fix, das Modell wird um 2 Achsen gedreht.
Das Beispiel illustriert die Trennung von Mesh-Erzeugung und Übertragung mehrer Meshes in zusammenhängende VBO/EBO-Puffer, die dann mit einem einzigen draw-Call gerendert werden können (was sehr schnell ist).
Projektstruktur
1
2
3
4
5
6
7
8
main.cpp — Einstiegspunkt, Qt-Anwendung aufsetzen
GLWidget — Qt-Widget, das den OpenGL-Kontext verwaltet
Vertex.h — VertexVNC-Struct (Position, Normale, Farbe)
CubeMesh — Geometriegenerator für einen einzelnen Würfel (CPU-seitig)
CubeObject — GPU-Puffer für alle Würfel (VAO/VBO/EBO)
ShaderProgram — Wrapper um QOpenGLShaderProgram
vertex.glsl — Vertex-Shader (Transformation)
fragment.glsl — Fragment-Shader (Phong-Beleuchtung)
Bauen
1
cmake -B build && cmake --build build
Voraussetzungen: CMake ≥ 3.16, Qt 6 (Core, Gui, Widgets, OpenGL, OpenGLWidgets), C++17.
main.cpp — Einstiegspunkt
1
2
3
4
5
QSurfaceFormat format;
format.setVersion(3, 3);
format.setProfile(QSurfaceFormat::CoreProfile);
format.setDepthBufferSize(24);
QSurfaceFormat::setDefaultFormat(format);
Bevor das Fenster erstellt wird, wird das OpenGL-Format global festgelegt: Version 3.3, Core Profile (kein veraltetes OpenGL 1/2-API), 24-Bit-Tiefenpuffer. Danach wird ein QMainWindow mit einem GLWidget als zentrales Widget erstellt.
GLWidget — Das OpenGL-Widget
GLWidget erbt von QOpenGLWidget und von QOpenGLFunctions_3_3_Core. Letzteres stellt alle gl…()-Funktionen direkt als Methoden bereit, ohne glew oder ähnliche Bibliotheken.
Konstruktor
1
2
3
4
5
6
7
connect(&m_timer, &QTimer::timeout, this, [this]() {
m_angleX += 0.5f;
m_angleY += 0.8f;
m_lightAngle += 1.2f;
update();
});
m_timer.start(16); // ~60 fps
Ein QTimer feuert alle 16 ms (~60 FPS). Bei jedem Tick werden die Rotationswinkel für X- und Y-Achse sowie der Lichtwinkel erhöht, und update() fordert ein Neuzeichnen an.
initializeGL()
Wird von Qt einmalig aufgerufen, sobald der OpenGL-Kontext bereit ist.
1
2
3
4
glClearColor(0.12f, 0.12f, 0.15f, 1.0f); // dunkelgrauer Hintergrund
glEnable(GL_DEPTH_TEST); // verdeckte Flächen korrekt ausblenden
m_shaderProgram.create(":/shaders/vertex.glsl", ":/shaders/fragment.glsl");
m_cube.initialize(m_shaderProgram);
resizeGL(int w, int h)
Wird bei jeder Größenänderung aufgerufen. Hier wird die Projektionsmatrix neu berechnet:
1
m_projection.perspective(45.0f, float(w) / float(h), 0.1f, 100.0f);
Perspektivprojektion mit 45° Sichtfeld, Seitenverhältnis des Fensters, Nahebene 0.1, Fernebene 100.
paintGL()
Wird bei jedem Neuzeichnen aufgerufen.
1
2
QMatrix4x4 view;
view.lookAt({0,0,3}, {0,0,0}, {0,1,0}); // Kamera bei z=3, schaut auf Ursprung
1
2
3
QMatrix4x4 model;
model.rotate(m_angleX, 1, 0, 0); // Rotation um X-Achse
model.rotate(m_angleY, 0, 1, 0); // dann um Y-Achse
Die MVP-Matrix (Model × View × Projection) wird an den Shader übergeben, ebenso wie die Modellmatrix und normalMatrix (für korrekte Normalentransformation bei nicht-uniformer Skalierung).
Vertex.h — Vertex-Datenstruktur
1
2
3
4
5
struct VertexVNC {
float v[3]; // Position — Offset 0 Byte
float m[3]; // Normale — Offset 12 Byte
float r, g, b; // Farbe — Offset 24 Byte
};
VertexVNC (Vertex/Normal/Color) fasst alle pro-Eckpunkt-Daten in einem Struct zusammen. Die festen Offsets werden in CubeObject::initialize() direkt per offsetof an den Shader übergeben.
CubeMesh — Geometriegenerator
CubeMesh erzeugt die CPU-seitige Geometrie für einen einzelnen achsenparallelen Würfel:
- 24 Eckpunkte — 4 pro Seite (jede Seite hat eigene Eckpunkte für separate Normalen)
- 36 Indizes — 6 pro Seite (2 Dreiecke × 3 Indizes)
Der Konstruktor nimmt Mittelpunkt und Kantenlänge entgegen. copy2Buffer() schreibt die Daten in vom Aufrufer bereitgestellte Puffer und rückt beide Zeiger sowie den Startindex weiter:
1
2
3
4
5
CubeMesh(const QVector3D ¢er, float size);
void copy2Buffer(VertexVNC *&vertexBuffer,
GLuint *&elementBuffer,
unsigned int &elementStartIndex) const;
Die sechs Seiten erhalten feste Farben:
| Seite | Farbe | Normale |
|---|---|---|
| Vorderseite | Rot | ( 0, 0, +1) |
| Rückseite | Grün | ( 0, 0, -1) |
| Linke Seite | Blau | (-1, 0, 0) |
| Rechte Seite | Gelb | (+1, 0, 0) |
| Oberseite | Cyan | ( 0, +1, 0) |
| Unterseite | Magenta | ( 0, -1, 0) |
CubeObject — GPU-Pufferverwaltung
CubeObject verwaltet die GPU-Ressourcen für alle Würfel gemeinsam in einem einzigen VAO/VBO/EBO.
Konstruktor — Gitter aufbauen
1
2
3
4
for (int i = 0; i < 11; ++i)
for (int j = 0; j < 11; ++j)
for (int k = 0; k < 11; ++k)
m_cubes.emplace_back(QVector3D(i-5, j-5, k-5), 0.7f);
Es entsteht ein 11×11×11-Gitter (1331 Würfel) mit Kantenlänge 0.7, zentriert um den Ursprung (Koordinaten −5 bis +5). Anschließend füllt eine Schleife über CubeMesh::copy2Buffer() die CPU-seitigen Vertex- und Indexpuffer.
initialize(ShaderProgram &program)
Lädt die CPU-Daten einmalig auf die GPU:
1
2
m_vbo.allocate(m_vertexBufferData.data(), ...); // 1331 × 24 Eckpunkte
m_ebo.allocate(m_elementBufferData.data(), ...); // 1331 × 36 Indizes
Die drei Shader-Attribute werden anhand der VertexVNC-Offsets registriert:
| Attribut | Location | Offset | Größe |
|---|---|---|---|
| Position | 0 | 0 Byte | 3 float |
| Farbe | 1 | 24 Byte (r) |
3 float |
| Normale | 2 | 12 Byte (m) |
3 float |
render()
1
2
3
m_vao.bind();
glDrawElements(GL_TRIANGLES, GLsizei(m_elementBufferData.size()), GL_UNSIGNED_INT, nullptr);
m_vao.release();
Statt glDrawArrays wird indexiertes Rendering per glDrawElements verwendet. Das EBO (Element Buffer Object) enthält die Reihenfolge, in der die Eckpunkte zu Dreiecken zusammengesetzt werden.
ShaderProgram — Shader-Wrapper
Schlanker Wrapper um QOpenGLShaderProgram. create() lädt Vertex- und Fragment-Shader aus Qt-Ressourcen (:/shaders/…), kompiliert und verlinkt sie. Bei Fehler bricht qFatal() das Programm ab.
Die Template-Methode setUniformValue<T> reicht beliebige Typen (Matrix, Vektor, Float …) direkt an Qt weiter.
Vertex-Shader (vertex.glsl)
1
2
3
4
gl_Position = mvp * vec4(position, 1.0); // Clip-Koordinaten
fragPos = vec3(model * vec4(position, 1.0)); // Weltkoordinaten für Beleuchtung
fragNormal = normalMatrix * normal; // transformierte Normale
fragColor = color;
Die Weltposition und die Normale werden als out-Variablen an den Fragment-Shader weitergegeben, damit die Beleuchtung im Weltkoordinatensystem berechnet wird.
Fragment-Shader (fragment.glsl) — Phong-Beleuchtung
Implementiert das klassische Phong-Beleuchtungsmodell mit drei Komponenten:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Ambient — gleichmäßiges Grundlicht (15 %)
vec3 ambient = 0.15 * fragColor;
// Diffus — lambertsche Streuung (abhängig vom Einfallswinkel)
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * fragColor;
// Spekular — glänzender Reflex (weiß, Shininess = 32)
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = 0.5 * spec * vec3(1.0);
outColor = vec4(ambient + diffuse + specular, 1.0);
- Ambient: verhindert komplett schwarze Schattenseiten
- Diffus: heller, je flacher das Licht auf die Fläche trifft
- Spekular: weißer Glanzpunkt;
pow(..., 32.0)bestimmt die Schärfe des Reflexes
