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.

Screenshot 2024-11-28 at 162817.png

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.