Animation und Simulation
In diesem Kapitel konzentrieren wir uns darauf, unsere Erde gut in Szene zu setzen. Das heißt, wir räumen die Szene etwas auf und fügen der Erde Animationen hinzu, sodass sie sich abhängig von Tageszeit und Monat passend zur Sonne dreht. So entsteht die Illusion von echten Jahreszeiten und Sonnenständen. Außerdem platzieren wir einen kleinen Satelliten im Orbit.

Skalierungsmatrix
Wir benötigen die Möglichkeit, Objekte zu skalieren. Hierfür implementieren wir eine Skalierungsmatrix.
cgmath.h
struct Matrix4
{
...
static Matrix4 scale(double a)
{
Matrix4 m = {
a, 0, 0, 0,
0, a, 0, 0,
0, 0, a, 0,
0, 0, 0, 1
};
return m;
}
...
};
Anschließend implementieren wir eine neue Methode setScale() in der Mesh-Klasse, die diese Funktion aufruft.
mesh.h
class Mesh
{
public:
...
void setScale(const double scale);
protected:
...
Matrix4 scale = Matrix4::scale(1.0);
};
mesh.cpp
void Mesh::setScale(const double scale)
{
this->scale = Matrix4::scale(scale);
}
Mit dieser Änderung können wir Modelle nun in verschiedenen Größen darstellen. Damit wir dies aber auch sehen, muss die Skalierungsmatrix beim Rendern auch verwendet werden.
mesh.cpp
void Mesh::render() const
{
Matrix4 worldMatrix = position * rotation * scale;
...
}
Neue Klasse Simulation
In der Klasse Simulation soll alles stattfinden, was mit dem Bewegen der geladenen Meshes zu tun hat. Dazu legen wir zwei neue Dateien simulation.h und simulation.cpp an.
Wenn Sie ein Computerspiel entwickeln möchten, dann wäre diese Klasse die richtige Stelle für die Logik und Physik, aber auch für das Anwenden von Benutzereingaben auf die virtuelle Welt.
simulation.h
#pragma once
#include "mesh.h"
#include <memory>
class Simulation
{
public:
Simulation(const std::shared_ptr<Mesh> &earth, const std::shared_ptr<Mesh> &satellite);
void update();
private:
void updateEarthRotation(double time);
void updateSatellitePosition(double time);
std::shared_ptr<Mesh> earth;
std::shared_ptr<Mesh> satellite;
};
simulation.cpp
#include "simulation.h"
#include <chrono>
Simulation::Simulation(const std::shared_ptr<Mesh> &earth, const std::shared_ptr<Mesh> &satellite)
: earth(earth), satellite(satellite)
{
}
void Simulation::update()
{
auto time = std::chrono::system_clock::now();
auto timeSinceEpoch = time.time_since_epoch();
double secondsSinceEpoch = std::chrono::duration<double>(timeSinceEpoch).count();
updateEarthRotation(secondsSinceEpoch);
updateSatellitePosition(secondsSinceEpoch);
}
...
Mithilfe des Wertes in secondsSinceEpoch erhalten wir die aktuelle Uhrzeit. Damit sind wir in der Lage, innerhalb unserer beiden Funktionen die Rotation der Erde und die Position des Satelliten zu bestimmen. Dies geschieht in jedem Frame.
Ekliptik und Rotation der Erde
Als nächstes wollen wir die Drehung und Neigung der Erde zur Sonne berechnen. Wir könnten dies entweder durch eine tatsächliche Bewegung des Planeten entlang seiner Bahn erreichen oder wir vereinfachen die Logik so weit, dass die Erde still steht und sich lediglich dreht und neigt. Letzteres erscheint für unseren konkreten Anwendungsfall praktikabler.
Die erste Komponente unserer Animation ist die Tageszeit. Wir wollen, dass unsere Erde in 24 Stunden exakt einmal um ihre eigene Achse rotiert. Als Orientierungspunkt für die Ausrichtung der Erde legen wir fest, dass jeden Tag um 12 Uhr mittags in der UTC-Zeitzone die Sonne im Zenit über dem Nullmeridian steht.
Die zweite Komponente ist die Neigung der Erde gegen die Ebene der Ekliptik. Die Erde bewegt sich auf einer elliptischen Bahn um die Sonne. Wir vereinfachen diese Bahn zu einer Kreisbahn und nutzen die Cosinusfunktion zum Berechnen der Neigung. Wir wissen, dass am 21. Dezember die Südhalbkugel am stärksten zur Sonne geneigt ist und am 21. Juni die Nordhalbkugel die größte Sonneneinstrahlung erfährt.
simulation.cpp
void Simulation::updateEarthRotation(double time)
{
double timeOfDay = std::fmod(time, 86400);
double earthRotation = timeOfDay / 86400.0 * deg2rad(360);
double timeOfYear = std::fmod(time + 864000.0, 31557600);
double earthEcliptic = std::cos(timeOfYear / 31557600.0 * deg2rad(360)) * deg2rad(-23.4);
earth->setRotation(Vector3(earthEcliptic, earthRotation, 0));
}
Umlaufbahn eines Satelliten
Um die Szene ein bisschen aufzulockern, wollen wir einen Satelliten um die Erde kreisen lassen. Was eignet sich dafür besser als ein riesiger THM-Würfel?
Wir verkleinern unseren Würfel auf eine Größe von 50x50x50 Kilometern und platzieren ihn in einer Umlaufbahn um den Äquator in einer Höhe von 400 km. Das entspricht in etwa der Höhe der Internationalen Raumstation. In dieser Höhe dauert ein Umlauf etwa 90 Minuten.
simulation.cpp
namespace
{
constexpr double earthRadiusKm = 6370.0;
constexpr double satelliteHalfKm = 25.0;
constexpr double satelliteOrbitKm = 6770.0; // 6370 + 400 km altitude
constexpr double satelliteScale = satelliteHalfKm / earthRadiusKm;
constexpr double satelliteOrbit = satelliteOrbitKm / earthRadiusKm;
}
void Simulation::updateSatellitePosition(double time)
{
double scale = satelliteScale;
double orbitTime = 5400.0;
double orbitProgress = std::fmod(time, orbitTime);
double orbitRadius = satelliteOrbit;
Vector4 position = Vector4(0.0, 0.0, orbitRadius, 1.0);
Matrix4 orbit = Matrix4::rotateX(deg2rad(45)) * Matrix4::rotateY(orbitProgress / orbitTime * deg2rad(360.0));
position = orbit * position;
double tumbleTime = 60.0;
double tumbleProgress = std::fmod(time, tumbleTime);
double tumble = tumbleProgress / tumbleTime * deg2rad(360.0);
satellite->setScale(scale);
satellite->setPosition(position.xyz());
satellite->setRotation(Vector3(tumble, tumble, tumble));
}
Verwendung der neuen Klasse im Renderer
Im Renderer werden nun nur noch Erde und Satellit mit ihren Texturen geladen. Die beiden Meshes übergeben wir dem Konstruktor von Simulation. In der Hauptschleife wird unsere Szene in jedem Frame aktualisiert.
renderer.cpp
namespace Colors
{
...
Color black = Color(0.0, 0.0, 0.0, 1.0);
}
renderer.cpp
void Renderer::start()
{
auto earthTexture = std::make_shared<Texture>("textures/earth_diffuse.jpg");
auto satelliteTexture = std::make_shared<Texture>("textures/thm2k.png");
auto earth = std::make_shared<Sphere>(earthTexture);
auto satellite = std::make_shared<Cube>(satelliteTexture);
satellite->setScale(0.01);
satellite->setMaterial(Colors::black, Colors::black, Colors::black, Colors::white, 0.0f);
Scene foreground;
foreground.addMesh(earth);
foreground.addMesh(satellite);
Simulation simulation(earth, satellite);
setViewportSize();
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
Vector4 lightPosition(0, 0, 1, 0);
foreground.setLight(lightPosition, Colors::ambientLight, Colors::sunLight, Colors::white);
while (!glfwWindowShouldClose(window))
{
simulation.update();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
foreground.render(activeCamera);
glfwSwapBuffers(window);
glfwPollEvents();
printFps();
if (resized)
{
resized = false;
setViewportSize();
}
}
}
Versuchen Sie nachzuvollziehen, was hier im Einzelnen passiert. Eine Neuerung führen wir auch mit den Materialeigenschaften des Satelliten ein. Können Sie erklären, was wir da machen?
Lernziele
Nach diesem Kapitel sollten Sie die folgenden Fragen beantworten können:
- Was ist eine Skalierungsmatrix und warum wird sie in der World-Matrix in der Reihenfolge Position × Rotation × Skalierung angewendet?
- Was ist die Ekliptik und wie beeinflusst die Achsneigung der Erde von 23,4° den Wechsel der Jahreszeiten?
- Warum verwendet man für Animationen die Systemzeit statt einen Frame-Zähler und welches Problem vermeidet man damit?
- Wie lässt sich die Position eines Objekts auf einer Kreisbahn mithilfe trigonometrischer Funktionen und Rotationsmatrizen berechnen?
- Was beschreiben die Materialeigenschaften Ambient, Diffus, Spekular und Emissiv und wie beeinflussen sie das Erscheinungsbild eines Objekts?
- Warum trennt man Simulationslogik und Renderlogik in separate Klassen und welchen Vorteil bringt diese Trennung?
Bearbeitungshinweise
Arbeiten Sie dieses Kapitel eigenständig vor dem zugehörigen Veranstaltungstermin durch. Wie die Vorbereitung abläuft und worauf es dabei ankommt, erfahren Sie im Konzept der Veranstaltung.