In diesem Kapitel werden wir Mauseingaben auswerten um die Kamera steuerbar zu machen. Anschließend steigen wir in die Mathematik der Transformation im Raum ein, damit wir diese Bewegung auch optisch sichtbar machen.

Inhalt


Mathe Bibliothek

Da wir im Laufe dieser Veranstaltung einige Mathematische Formeln verstehen müssen, werden wir eine neue Translation Unit einführen: cgmath.o

Sie kennen den Drill: Erstellen Sie die beiden benötigten Dateien und passen Sie Ihr Makefile entsprechend an.

Anmerkung: Einige der Funktionen die wir hier implementieren, unterstützt die OpenGL API auch selbst. Für andere Funktionen würde es sich anbieten auf eine speziell dafür entworfene und auf Geschwindigkeit optimierte Mathe Bibliothek zurückzugreifen. Trotzdem werden wir alle Funktionen die wir für diese Veranstaltung brauchen, selber implementieren. Unser Ziel ist dabei nicht, besonders effizient zu programmieren sondern besonders lesbar. So sind wir am Ende des Semesters in der Lage, genau zu verstehen was unser Programm tut und können die einzelnen Schritte auch von Hand nachrechnen.

Funktionsumfang

Im Laufe des Semesters werden wir diese Bibliothek Schrittweise jeweils um die Funktionen erweitern die wir benötigen.

In diesem Kapitel beginnen wir mit der Implementierung von Rotations- und Translationsmatritzen

Fügen Sie folgendes in die Header Datei ein:

cgmath.h

#ifndef CGMATH
#define CGMATH
typedef struct {
    float m11;
    float m12;
    float m13;
    float m14;
    float m21;
    float m22;
    float m23;
    float m24;
    float m31;
    float m32;
    float m33;
    float m34;
    float m41;
    float m42;
    float m43;
    float m44;
} matrix;
#endif

matrix matrixTranslate(float x, float y, float z);
matrix matrixRotateX(float a);
matrix matrixRotateY(float a);
matrix matrixRotateZ(float a);

float deg2rad(float deg);

Hier sehen wir direkt zwei neue Elemente die wir bisher nicht kennengelernt haben. Einerseits definieren wir hier einen neuen Datentyp matrix. Das struct kennen Sie bereits aus Java. Unter C funktioniert es ähnlich, allerdings benötigt man hier zusätzlich das typedef statement, damit man die Matrix auch unter ihrem Namen verwenden kann. Falls das neu für Sie ist, lesen Sie sich in die Thematik ein. Wir werden das später noch häufiger sehen.

Außerdem wird hier eine Präprozessor Direktive angewandt. #ifndef ist ein if-Statement welches während des Kompilierens ausgewertet wird, nicht zur Laufzeit. Dies ist nötig, weil wir unsere Mathe Bibliothek an mehreren Stellen einbinden wollen, die Typendefinition aber nur einmal stattfinden soll. Das Konstrukt aus #ifndef, #define und #endif sorgt also dafür, dass alles dazwischen nur genau einmal stattfindet. Lesen Sie auch hier nach, wenn dieses Thema für Sie neu ist. In der Prüfung am Ende des Semesters sollten Sie das auf jeden Fall verstanden haben.

Ansonsten sehen wir bekanntes. Hier definieren wir zwei Funktionen mit denen wir eine Rotationsmatrix erzeugen können, eine Funktion um eine Translationsmatrix zu erzeugen und eine deg2rad Funktion, die einen Wert vom Gradmaß ins Bogenmaß umwandelt. (Klicken Sie die Links für weitergehende Infos)

Hier die Implementierung:

cgmath.c

#include <math.h>
#include "cgmath.h"

matrix matrixTranslate(float x, float y, float z)
{
    matrix m = {};
    m.m11 = 1;  m.m21 = 0;  m.m31 = 0;  m.m41 = x;
    m.m12 = 0;  m.m22 = 1;  m.m32 = 0;  m.m42 = y;
    m.m13 = 0;  m.m23 = 0;  m.m33 = 1;  m.m43 = z;
    m.m14 = 0;  m.m24 = 0;  m.m34 = 0;  m.m44 = 1;
    return m;
}

matrix matrixRotateX(float a)
{
    matrix m = {};
    m.m11 = 1;  m.m21 =      0;  m.m31 =       0;  m.m41 = 0;
    m.m12 = 0;  m.m22 = cos(a);  m.m32 = -sin(a);  m.m42 = 0;
    m.m13 = 0;  m.m23 = sin(a);  m.m33 =  cos(a);  m.m43 = 0;
    m.m14 = 0;  m.m24 =      0;  m.m34 =       0;  m.m44 = 1;
    return m;
}

matrix matrixRotateY(float a)
{
    matrix m = {};
    m.m11 =  cos(a);  m.m21 = 0;  m.m31 = sin(a);  m.m41 = 0;
    m.m12 =       0;  m.m22 = 1;  m.m32 =      0;  m.m42 = 0;
    m.m13 = -sin(a);  m.m23 = 0;  m.m33 = cos(a);  m.m43 = 0;
    m.m14 =       0;  m.m24 = 0;  m.m34 =      0;  m.m44 = 1;
    return m;
}

float deg2rad(float deg)
{
    return deg * M_PI / 180.0f;
}

Selbststudium & Erweiterungen

Untersuchen Sie den Code und lesen Sie die passenden, verlinkten Artikel. Auch Ihre Unterlagen aus den passenden Mathematikvorlesungen kann sich jetzt wieder als Hilfreich herausstellen. Zur Auffrischung hilft das Kapitel Transformations auf LearnOpenGL und der Artikel zur Drehmatrix auf Wikipedia.

Können Sie die fehlende Rotation um die Z-Achse selber implementieren oder müssen Sie die Lösung von GitHub kopieren?

matrix matrixRotateZ(float a) { ... }

Kamerasteuerung

Im Rahmen dieser Übung wollen wir eine 3rd Person Kamera Steuerung implementieren. Das heißt die Kamera hat einen unveränderlichen Zielpunkt und ihre Position kann um diesen Zielpunkt rotiert werden. Zusätzlich werden wir mit dem Mausrad oder der Scrollfunktion in der Lage sein den Abstand zum Ziel zu verändern.

Erstellen Sie eine neue Translation Unit camera.o

Beginnen wir mit einem Blick in die einfachste Version der Header Datei.

camera.h

#include <GLFW/glfw3.h>

void loadCameraProjectionMatrix(float aspect);
void loadCameraViewMatrix();

Hier definieren wir zwei Methoden mit deren Hilfe wir zukünftig die Projektionsmatrix und die Viewmatrix laden. Lesen Sie hierzu auch diesen Artikel auf codinglabs.net. Hier unsere Implementierung:

camera.c

#include <math.h>
#include "cgmath.h"
#include "camera.h"

static double cameraPitch = 0.0;
static double cameraYaw = 0.0;
static double cameraDistance = 3.0;

void loadCameraProjectionMatrix(float aspect)
{
    float zNear = 0.1f;
    float zFar = 100.0f;
    float fov = deg2rad(45.0f);

    float h = zNear * tanf(fov * 0.5f);
    float w = h * aspect;

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glFrustum(-w, w, -h, h, zNear, zFar);   
}

void loadCameraViewMatrix()
{
    glMatrixMode(GL_MODELVIEW);

    glLoadIdentity();
    matrix translationMatrix = matrixTranslate(0.0f, 0.0f, -cameraDistance);
    glMultMatrixf((float*)&translationMatrix);
    matrix pitchRotationMatrix = matrixRotateX(deg2rad(-cameraPitch));
    glMultMatrixf((float*)&pitchRotationMatrix);
    matrix yawRotationMatrix = matrixRotateY(deg2rad(-cameraYaw));
    glMultMatrixf((float*)&yawRotationMatrix);
}

In der Projektionsmatrix nutzen wir glFrustum um eine perspektivische Transformation zu realisieren. Zusätzlich kommt eine bisschen Pythagoras ins Spiel um aus einem FOV (=FieldOfView) die Höhe der Near Plane zu berechnen. Diese Matrix ändert sich nur, falls sich das Seitenverhältnis des Fensters ändert. Lesen Sie sich in diese Materie ein und stellen Sie sicher, dass Sie alles verstehen.

Die ViewMatrix dient nun dazu die eigentliche Bewegung der Camera umzusetzen. Sie sehen, dass wir hier mehrere Matritzen laden und nacheinander zusammen multiplizieren.

Integration in den bestehenden Code

Um von diesen beiden Matrizen Gebrauch zu machen, entfernen Sie den bisherigen Code, die die Orthogonale Projektion aus Ihrer scene.c und fügen Sie die gerade erstellen Methoden hinzu:

scene.c

#include "scene.h"
#include "camera.h"

void renderScene(GLFWwindow* window)
{
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    float ratio = width / (float) height;
    glViewport(0, 0, width, height);
    
    glClearColor(0.29f,0.36f,0.4f,1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-ratio, ratio, -1.0f, 1.0f, 1.0f, -1.0f);
    
    loadCameraProjectionMatrix(width / (float) height);
    loadCameraViewMatrix();

    glBegin(GL_QUADS);
    glColor3f(0.5f, 0.73f, 0.14f);
    glVertex3f(-0.6f, 0.0f, 0.0f);
    glVertex3f(0.0f, -0.6f, 0.0f);
    glVertex3f(0.6f, 0.0f, 0.0f);
    glVertex3f(0.0f, 0.6f, 0.0f);
    glEnd();
}

Wenn Sie alles richtig gemacht habe, dann können Sie die Anwendung jetzt starten und alles sollte exakt genauso aussehen wie vorher. Falls Ihr Rechteck verschwindet oder ein anderes Problem auftritt sollten Sie auf Fehlersuche gehen.


Mauseingaben verarbeiten

Wenn bis hierhin alles geklappt hat, können Sie nun mit der Verarbeitung der Mauseingaben weiter machen.

Hierzu erweitern wir die Camera um 3 neue Funktionen:

camera.h

#include <GLFW/glfw3.h>

void loadCameraProjectionMatrix(float ratio);
void loadCameraViewMatrix();
void enableCameraMouseControl(GLFWwindow* window);

static void cursorPositionCallback(GLFWwindow* window, double xpos, double ypos);
static void scrollCallback(GLFWwindow* window, double xoffset, double yoffset);

Die Funktion enableCameraMouseControl wird alle Funktionen beinhalten, die wir benötigen um die Maussteuerung zu initialisieren. Die beiden anderen Funktionen sind Callbacks, die ausgelöst werden, sobald der Benutzer die Maus bewegt.

Hier die Implementierungen der Reihe nach.

camera.c

#include <math.h>
#include "cgmath.h"
#include "camera.h"

static double cameraPitch = 0.0;
static double cameraYaw = 0.0;
static double cameraDistance = 3.0;

static double mouseLastX = 0.0;
static double mouseLastY = 0.0;

static double scrollSpeed = 0.1;
static double mouseSpeed = 0.05;

void enableCameraMouseControl(GLFWwindow* window)
{
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    if (glfwRawMouseMotionSupported())
    {
        glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
    }
    glfwSetScrollCallback(window, scrollCallback);
    glfwSetCursorPosCallback(window, cursorPositionCallback);
    glfwGetCursorPos(window, &mouseLastX, &mouseLastY);
}

void loadCameraProjectionMatrix(float aspect)
{
    float zNear = 0.1f;
    float zFar = 100.0f;
    float fov = deg2rad(45.0f);

    float h = zNear * tanf(fov * 0.5f);
    float w = h * aspect;

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glFrustum(-w, w, -h, h, zNear, zFar);
}

void loadCameraViewMatrix()
{
    glMatrixMode(GL_MODELVIEW);

    glLoadIdentity();
    matrix translationMatrix = matrixTranslate(0.0f, 0.0f, -cameraDistance);
    glMultMatrixf((float*)&translationMatrix);
    matrix pitchRotationMatrix = matrixRotateX(deg2rad(-cameraPitch));
    glMultMatrixf((float*)&pitchRotationMatrix);
    matrix yawRotationMatrix = matrixRotateY(deg2rad(-cameraYaw));
    glMultMatrixf((float*)&yawRotationMatrix);
}

static void cursorPositionCallback(GLFWwindow* window, double xpos, double ypos)
{
    double deltaX = xpos - mouseLastX;
    mouseLastX = xpos;

    double yaw = cameraYaw - deltaX * mouseSpeed;
    
    while (yaw < 0) yaw += 360;
    while (yaw > 360) yaw -= 360;

    cameraYaw = yaw;

    double deltaY = ypos - mouseLastY;
    mouseLastY = ypos;

    double pitch = cameraPitch - deltaY * mouseSpeed;
    
    if (pitch < -90) pitch = -90;
    if (pitch > 90) pitch = 90;

    cameraPitch = pitch;
}

static void scrollCallback(GLFWwindow* window, double xoffset, double yoffset)
{
    double distance = cameraDistance - yoffset * scrollSpeed;

    if (distance < 1.4) distance = 1.4;
    if (distance > 20.0) distance = 20.0;

    cameraDistance = distance;
}

Maussteuerung aktivieren

Damit wir etwas sehen, müssen wir nun zuletzt noch in der graphics.c unsere Maussteuerung aktivieren. Dies tun wir am besten kurz bevor unser Message Loop beginnt.

graphics.c

#include <stdio.h>
#include "graphics.h"
#include "scene.h"
#include "camera.h"

static GLFWwindow* window;

int startGraphics(int width, int height)
{

    ...

    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);
    glEnable(GL_MULTISAMPLE);
    enableCameraMouseControl(window);
    glfwSetKeyCallback(window, keyCallback);

    ...

}

Testen Sie ob Sie die Kamera mit der Maus steuern können. Passen Sie bei Bedarf die Werte von scrollSpeed und mouseSpeed an Ihre Umgebung / Hardware an.

Prüfungsvorbereitung

  • Beschreiben Sie was eine Rotationsmatrix ist.
  • Wissen Sie wie eine Einheitsmatrix aufgebaut ist und welche Auswirkung sie auf Rotation und Transformation hat?
  • Erklären Sie die Begriffe Near-Plane und Far-Plane.
  • Was bezeichnen wir als FOV und was bedeutet der Wert den wir hier vergeben?
  • Was ist der Unterschied zwischen glLoadMatrixf() und glMultMatrixf()?