Post

OpenGL: Rotierender Würfel mit Phong Shading

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.

Würfel

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 &center, 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

This post is licensed under CC BY 4.0 by the author.