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.

Mathe Bibliothek

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

#pragma once #include <cmath> #include <numbers> inline double deg2rad(double deg) { return deg * std::numbers::pi / 180.0; } struct Matrix4 { double m11; double m21; double m31; double m41; double m12; double m22; double m32; double m42; double m13; double m23; double m33; double m43; double m14; double m24; double m34; double m44; /** * Creates a translation matrix that translates points * by the given x, y, and z values. * * @param x The translation distance along the x-axis. * @param y The translation distance along the y-axis. * @param z The translation distance along the z-axis. * @return The translation matrix. */ static Matrix4 translate(double x, double y, double z) { Matrix4 m = { 1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1 }; return m; } /** * Creates a rotation matrix for a rotation around the X-axis. * * @param a The angle of rotation in radians. * @return The rotation matrix. */ static Matrix4 rotateX(double a) { Matrix4 m = { 1, 0, 0, 0, 0, cos(a), -sin(a), 0, 0, sin(a), cos(a), 0, 0, 0, 0, 1 }; return m; } /** * Creates a rotation matrix for a rotation around the Y-axis. * * @param a The angle of rotation in radians. * @return The rotation matrix. */ static Matrix4 rotateY(double a) { Matrix4 m = { cos(a), 0, sin(a), 0, 0, 1, 0, 0, -sin(a), 0, cos(a), 0, 0, 0, 0, 1 }; return m; } /** * Creates a rotation matrix for a rotation around the Z-axis. * * @param a The angle of rotation in radians. * @return The rotation matrix. */ static Matrix4 rotateZ(double a) { Matrix4 m = { cos(a), -sin(a), 0, 0, sin(a), cos(a), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; return m; } /** * Converts the matrix to column-major order and stores the result in the provided array. * * @param values A reference to a 16-element array of floats where the column-major matrix elements will be stored. */ void toColumnMajor(float (&values)[16]) const { values[0] = static_cast<float>(m11); values[1] = static_cast<float>(m12); values[2] = static_cast<float>(m13); values[3] = static_cast<float>(m14); values[4] = static_cast<float>(m21); values[5] = static_cast<float>(m22); values[6] = static_cast<float>(m23); values[7] = static_cast<float>(m24); values[8] = static_cast<float>(m31); values[9] = static_cast<float>(m32); values[10] = static_cast<float>(m33); values[11] = static_cast<float>(m34); values[12] = static_cast<float>(m41); values[13] = static_cast<float>(m42); values[14] = static_cast<float>(m43); values[15] = static_cast<float>(m44); } };

Wir definieren hier ein struct Matrix, das verschiedene Funktionen zum Rotieren und Verschieben enthält. Außerdem wird eine Funktion toColumnMajor() angelegt, die die gespeicherten double-Werte in float-Werte castet und column-major ordnet. Dies ist notwendig, da OpenGL eine Matrix in diesem Format erwartet.

Ansonsten sehen wir eine deg2rad Funktion, die einen Wert vom Gradmaß ins Bogenmaß umwandelt.

Selbststudium

Untersuchen Sie den Code. Auch Ihre Unterlagen aus den passenden Mathematikvorlesungen können 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?

static Matrix4 rotateZ(double 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.

Die Klasse Camera

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

camera.h

#pragma once class Camera { public: Camera(); Camera(double pitch, double yaw, double cameraDistance); ~Camera(); void changePosition(double x, double y); void changeDistance(double deltaZ); void loadProjectionMatrix(double aspectRatio) const; void loadViewMatrix() const; private: double pitch = 0.0; double yaw = 0.0; double cameraDistance = 0.0; double mouseLastX = 0.0; double mouseLastY = 0.0; double scrollSpeed = 0.1; double mouseSpeed = 0.05; };

Hier definieren wir zwei Methoden mit deren Hilfe wir zukünftig die Projektionsmatrix und die Viewmatrix laden. Eine gute Erklärung zu diesen Transformationen finden Sie in diesem Artikel auf LearnOpenGL.

camera.cpp

#define GLFW_INCLUDE_GLEXT #include "camera.h" #include "cgmath.h" #include <GLFW/glfw3.h> Camera::Camera() { } Camera::Camera(double pitch, double yaw, double cameraDistance) : pitch(pitch), yaw(yaw), cameraDistance(cameraDistance) { } Camera::~Camera() { } void Camera::changePosition(double x, double y) { double deltaX = x - mouseLastX; mouseLastX = x; double yaw = this->yaw - deltaX * mouseSpeed; while (yaw < 0) { yaw += 360; } while (yaw > 360) { yaw -= 360; } this->yaw = yaw; double deltaY = y - mouseLastY; mouseLastY = y; double pitch = this->pitch - deltaY * mouseSpeed; if (pitch < -90) pitch = -90; if (pitch > 90) pitch = 90; this->pitch = pitch; } void Camera::changeDistance(double deltaZ) { double distance = cameraDistance - deltaZ * scrollSpeed; if (distance < 1.4) distance = 1.4; if (distance > 20.0) distance = 20.0; cameraDistance = distance; } void Camera::loadProjectionMatrix(double aspectRatio) const { double zNear = 0.1; double zFar = 100.0; double fov = deg2rad(45.0); double h = zNear * tanf(fov * 0.5); double w = h * aspectRatio; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(-w, w, -h, h, zNear, zFar); } void Camera::loadViewMatrix() const { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); Matrix4 translation = Matrix4::translate(0.0, 0.0, -cameraDistance); float translationF[16]; translation.toColumnMajor(translationF); glMultMatrixf(translationF); Matrix4 pitchRotation = Matrix4::rotateX(deg2rad(-pitch)); float pitchRotationF[16]; pitchRotation.toColumnMajor(pitchRotationF); glMultMatrixf(pitchRotationF); Matrix4 yawRotation = Matrix4::rotateY(deg2rad(-yaw)); float yawRotationF[16]; yawRotation.toColumnMajor(yawRotationF); glMultMatrixf(yawRotationF); }

In der Projektionsmatrix nutzen wir glFrustum um eine perspektivische Transformation zu realisieren. Zusätzlich kommt ein 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 Matrizen laden und nacheinander zusammen multiplizieren.

Integration in den bestehenden Code

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

scene.h

class Scene { public: ... void render(GLFWwindow *window, const Camera &camera) const; };

scene.cpp

void Scene::render(GLFWwindow *window, const Camera &camera) const { int width, height; glfwGetFramebufferSize(window, &width, &height); glViewport(0, 0, width, height); glClearColor(0.29f, 0.36f, 0.4f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); camera.loadProjectionMatrix(width / static_cast<double>(height)); camera.loadViewMatrix(); 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(); }

renderer.h

class Renderer { ... private: ... Camera activeCamera = Camera(0.0, 0.0, 3.0); };

renderer.cpp

void Renderer::start() { ... foreground.render(window, activeCamera); ... }

Mauseingaben verarbeiten

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

Hierzu erweitern wir Camera um neue Funktionen:

camera.h

class Camera { public: ... void changePosition(double x, double y); void changeDistance(double deltaZ); private: ... double mouseLastX = 0.0; double mouseLastY = 0.0; double scrollSpeed = 0.1; double mouseSpeed = 0.05; };

camera.cpp

void Camera::changePosition(double x, double y) { double deltaX = x - mouseLastX; mouseLastX = x; double yaw = this->yaw - deltaX * mouseSpeed; while (yaw < 0) { yaw += 360; } while (yaw > 360) { yaw -= 360; } this->yaw = yaw; double deltaY = y - mouseLastY; mouseLastY = y; double pitch = this->pitch - deltaY * mouseSpeed; if (pitch < -90) pitch = -90; if (pitch > 90) pitch = 90; this->pitch = pitch; } void Camera::changeDistance(double deltaZ) { double distance = cameraDistance - deltaZ * scrollSpeed; if (distance < 1.4) distance = 1.4; if (distance > 20.0) distance = 20.0; cameraDistance = distance; }

Maussteuerung aktivieren

Im Renderer werden diese Funktionen der Camera nun innerhalb der entsprechenden GLFW-Callbacks aufgerufen:

renderer.cpp

Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height) { ... glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); if (glfwRawMouseMotionSupported()) { glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); } double mousePositionX, mousePositionY; glfwGetCursorPos(window, &mousePositionX, &mousePositionY); activeCamera.changePosition(mousePositionX, mousePositionY); glfwSetCursorPosCallback(window, [](GLFWwindow *window, double x, double y) { Renderer *self = static_cast<Renderer *>(glfwGetWindowUserPointer(window)); self->activeCamera.changePosition(x, y); }); glfwSetScrollCallback(window, [](GLFWwindow *window, double xOffset, double yOffset) { Renderer *self = static_cast<Renderer *>(glfwGetWindowUserPointer(window)); self->activeCamera.changeDistance(yOffset); }); }

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.

Hausaufgabe

  • Räumen Sie Ihren Code auf, schreiben Sie Kommentare und machen Sie sich Notizen für die Prüfung.
  • Bereiten Sie eigenständig bis zur nächsten Veranstaltung Kapitel 5 vor: Implementieren Sie dazu das Tutorial von meiner Website und ziehen Sie weitere Quellen zu Rate um exakt zu verstehen was die Software macht und warum.