Skybox und Cube Mapping

In diesem Kapitel liegt der Fokus auf dem Hintergrund der Szene. Wir laden eine Skybox und platzieren eine Sonne. Damit das funktioniert, müssen wir einiges beachten, denn weder die Sterne noch die Sonne sollen beim Zoomen ihre Position verändern.

Screenshot 2024-12-05 at 144043.png

Einführung

Eine Skybox basiert auf dem Prinzip des Cube Mappings. In Computerspielen wird dieses Verfahren genutzt, um Teile der weit entfernten Umgebung des Spielers mittels einer speziellen Projektionstechnik auf einen Würfel zu projizieren. Auf diese Weise können komplexe Landschaften oder weit entfernte Objekte angezeigt werden, ohne die tatsächliche Geometrie dieser Objekte laden zu müssen. In unserem Anwendungsfall nutzen wir es als einfache Möglichkeit, die Sterne zu rendern.

Texturen laden

  • Laden Sie sich diese Star Map Textur herunter und speichern Sie die Datei als Computergrafik/textures/cubemap8k.jpg
  • Zum Testen der Implementierung und zum besseren Verständnis können Sie die Cube Map Referenz nutzen.

Cube Map Mesh erzeugen

Zuerst erzeugen wir uns die nötige Geometrie, um eine Cube Map zu rendern. Im Prinzip handelt es sich dabei um einen einfachen Würfel, der jedoch von innen betrachtet wird und deshalb die Normalenvektoren invertiert hat. Ebenfalls hat die Cube Map andere Texturkoordinaten als der Würfel, den wir bisher verwendet haben.

cubemap_demo.png

Erstellen Sie eine neue Klasse Skybox.

skybox.h

#pragma once

#include "mesh.h"

class Skybox : public Mesh
{
  public:
    Skybox(std::shared_ptr<Texture> &texture);
};

skybox.cpp

#include "skybox.h"

Skybox::Skybox(std::shared_ptr<Texture> &texture)
    : Mesh(texture)
{
    vertices.reserve(24);

    // +y
    vertices.push_back(Vertex({1, 1, -1}, {0, -1, 0}, {2 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({1, 1, 1}, {0, -1, 0}, {2 / 3.f, 2 / 2.f}));
    vertices.push_back(Vertex({-1, 1, 1}, {0, -1, 0}, {1 / 3.f, 2 / 2.f}));
    vertices.push_back(Vertex({-1, 1, -1}, {0, -1, 0}, {1 / 3.f, 1 / 2.f}));

    // +z
    vertices.push_back(Vertex({1, -1, 1}, {0, 0, -1}, {2 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({-1, -1, 1}, {0, 0, -1}, {3 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({-1, 1, 1}, {0, 0, -1}, {3 / 3.f, 2 / 2.f}));
    vertices.push_back(Vertex({1, 1, 1}, {0, 0, -1}, {2 / 3.f, 2 / 2.f}));

    // -x
    vertices.push_back(Vertex({-1, -1, 1}, {1, 0, 0}, {0 / 3.f, 0 / 2.f}));
    vertices.push_back(Vertex({-1, -1, -1}, {1, 0, 0}, {1 / 3.f, 0 / 2.f}));
    vertices.push_back(Vertex({-1, 1, -1}, {1, 0, 0}, {1 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({-1, 1, 1}, {1, 0, 0}, {0 / 3.f, 1 / 2.f}));

    // -y
    vertices.push_back(Vertex({-1, -1, -1}, {0, 1, 0}, {1 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({-1, -1, 1}, {0, 1, 0}, {1 / 3.f, 0 / 2.f}));
    vertices.push_back(Vertex({1, -1, 1}, {0, 1, 0}, {2 / 3.f, 0 / 2.f}));
    vertices.push_back(Vertex({1, -1, -1}, {0, 1, 0}, {2 / 3.f, 1 / 2.f}));

    // +x
    vertices.push_back(Vertex({1, -1, -1}, {-1, 0, 0}, {0 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({1, -1, 1}, {-1, 0, 0}, {1 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({1, 1, 1}, {-1, 0, 0}, {1 / 3.f, 2 / 2.f}));
    vertices.push_back(Vertex({1, 1, -1}, {-1, 0, 0}, {0 / 3.f, 2 / 2.f}));

    // -z
    vertices.push_back(Vertex({-1, -1, -1}, {0, 0, 1}, {2 / 3.f, 0 / 2.f}));
    vertices.push_back(Vertex({1, -1, -1}, {0, 0, 1}, {3 / 3.f, 0 / 2.f}));
    vertices.push_back(Vertex({1, 1, -1}, {0, 0, 1}, {3 / 3.f, 1 / 2.f}));
    vertices.push_back(Vertex({-1, 1, -1}, {0, 0, 1}, {2 / 3.f, 1 / 2.f}));
}

Camera anpassen

Würden wir die Skybox einfach rendern, dann würde sie wie jedes andere Objekt in unserer 3D-Szene positioniert werden. Das heißt, wenn man die Kamera zoomt, wird der Sternenhimmel größer oder kleiner. Probieren Sie das gerne mal aus.

Um die Illusion zu erschaffen, dass die Sterne und auch die Sonne immer im Hintergrund sind, müssen wir die Kamera anpassen. Genauer gesagt benötigen wir eine zweite, spezielle View Matrix:

camera.h

class Camera
{
  public:
    ...
    void loadFixedViewMatrix() const;
    ...
};

camera.cpp

void Camera::loadFixedViewMatrix() const
{
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    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);
}

Können Sie erklären, wo der Unterschied zur normalen View Matrix liegt?

Scene anpassen

Um zu unterscheiden, welche Objekte diese neue View Matrix benutzen sollen, führen wir zwei entsprechende Flags in der Szene ein. So kann man verschiedene Szenen erstellen und je nach Einstellung der Szene werden alle Objekte mit der passenden View Matrix gerendert.

scene.h

class Scene
{
  public:
    ...
    void enableDepthIsolation();
    void enableFixedPosition();

  private:
    ...
    bool depthIsolation = false;
    bool fixedPosition = false;
};

scene.cpp

void Scene::enableDepthIsolation()
{
    depthIsolation = true;
}

void Scene::enableFixedPosition()
{
    fixedPosition = true;
}

Und natürlich noch der eigentliche Rendervorgang. Hier wird die jeweils passende Matrix geladen und am Ende bei Bedarf der Tiefenpuffer geleert.

scene.cpp

void Scene::render(const Camera &camera) const
{
    if (fixedPosition)
    {
        camera.loadFixedViewMatrix();
    }
    else
    {
        camera.loadViewMatrix();
    }

    glEnable(GL_LIGHT1);

    ...

    glDisable(GL_LIGHT1);

    if (depthIsolation)
    {
        glClear(GL_DEPTH_BUFFER_BIT);
    }
}

Renderer anpassen

In Renderer::start() erstellen wir nun eine zweite Szene background und laden dort Sterne und Sonne als neue Modelle. Anschließend rendern wir erst den Hintergrund, dann den Vordergrund.

renderer.cpp

namespace Colors
{
    ...
    Color sky = Color(0.5, 0.5, 0.5, 1.0);
}

renderer.cpp

void Renderer::start()
{
    auto starTexture = std::make_shared<Texture>("textures/cubemap8k.jpg");
    auto noTexture = std::shared_ptr<Texture>{};
    ...

    auto stars = std::make_shared<Skybox>(starTexture);
    auto sun = std::make_shared<Sphere>(noTexture);
    ...

    stars->setScale(5);
    sun->setScale(0.03);
    sun->setPosition(Vector3(0, 0, 3));
    sun->setMaterial(Colors::black, Colors::black, Colors::white, Colors::black, 0.0f);

    Scene background;
    background.addMesh(stars);
    background.addMesh(sun);
    background.enableDepthIsolation();
    background.enableFixedPosition();

    ...

    background.setLight(lightPosition, Colors::black, Colors::sky, Colors::black);

    while (!glfwWindowShouldClose(window))
    {
        ...
        background.render(activeCamera);
        foreground.render(activeCamera);
        ...
    }
}

Lernziele

Nach diesem Kapitel sollten Sie die folgenden Fragen beantworten können:

  • Was ist Cube Mapping und warum eignet es sich besonders für die Darstellung weit entfernter Umgebungen wie eines Sternenhimmels?
  • Warum werden die Normalenvektoren einer Skybox invertiert und was bedeutet es, einen Würfel von innen zu betrachten?
  • Wie unterscheiden sich die Texturkoordinaten einer Cube Map von denen eines regulären Würfels und wie wird eine einzelne Textur auf sechs Seiten aufgeteilt?
  • Was ist der Unterschied zwischen der normalen View Matrix und der Fixed View Matrix und warum erzeugt das Weglassen der Translation die Illusion unendlicher Entfernung?
  • Was ist Depth Isolation und warum muss der Tiefenpuffer zwischen dem Rendern von Hintergrund und Vordergrund geleert werden?
  • Warum ist die Reihenfolge beim Rendern mehrerer Szenen (Hintergrund vor Vordergrund) entscheidend für das korrekte Ergebnis?
  • Was ist ein emissives Material und wie unterscheidet es sich von diffus oder spekular beleuchteten Oberflächen?

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.