In diesem Kapitel erzeugen wir eine Sky Box als Hintergrund unserer Szene. Dazu nutzen wir eine Textur der Sterne die wir selbst erzeugen. Außerdem implementieren wir das Konzept von Render Layern.


Einführung

Eine Sky Box 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/cgb_09/res/cubemap2k.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.

Hier der Würfel für die Cube Map, links ohne Backface Culling und rechts mit.

scene.h

...
static mesh createCubeMesh(color col);
static mesh createSphereMesh(color col);
static mesh createCubeMap(color col);
...

scene.c

static mesh createCubeMap(color col)
{
    vertex* vertices = (vertex *)calloc(24, sizeof(vertex));

    // +y
    vertices[ 0] = (vertex){ {  1,  1, -1 }, {  0, -1,  0 }, col, { 2/3.f, 1/2.f } };
    vertices[ 1] = (vertex){ {  1,  1,  1 }, {  0, -1,  0 }, col, { 2/3.f, 2/2.f } };
    vertices[ 2] = (vertex){ { -1,  1,  1 }, {  0, -1,  0 }, col, { 1/3.f, 2/2.f } };
    vertices[ 3] = (vertex){ { -1,  1, -1 }, {  0, -1,  0 }, col, { 1/3.f, 1/2.f } };

    // +z
    vertices[ 4] = (vertex){ {  1, -1,  1 }, {  0,  0, -1 }, col, { 2/3.f, 1/2.f } };
    vertices[ 5] = (vertex){ { -1, -1,  1 }, {  0,  0, -1 }, col, { 3/3.f, 1/2.f } };
    vertices[ 6] = (vertex){ { -1,  1,  1 }, {  0,  0, -1 }, col, { 3/3.f, 2/2.f } };
    vertices[ 7] = (vertex){ {  1,  1,  1 }, {  0,  0, -1 }, col, { 2/3.f, 2/2.f } };

    // -x
    vertices[ 8] = (vertex){ { -1, -1,  1 }, {  1,  0,  0 }, col, { 0/3.f, 0/2.f } };
    vertices[ 9] = (vertex){ { -1, -1, -1 }, {  1,  0,  0 }, col, { 1/3.f, 0/2.f } };
    vertices[10] = (vertex){ { -1,  1, -1 }, {  1,  0,  0 }, col, { 1/3.f, 1/2.f } };
    vertices[11] = (vertex){ { -1,  1,  1 }, {  1,  0,  0 }, col, { 0/3.f, 1/2.f } };

    // -y
    vertices[12] = (vertex){ { -1, -1, -1 }, {  0,  1,  0 }, col, { 1/3.f, 1/2.f } };
    vertices[13] = (vertex){ { -1, -1,  1 }, {  0,  1,  0 }, col, { 1/3.f, 0/2.f } };
    vertices[14] = (vertex){ {  1, -1,  1 }, {  0,  1,  0 }, col, { 2/3.f, 0/2.f } };
    vertices[15] = (vertex){ {  1, -1, -1 }, {  0,  1,  0 }, col, { 2/3.f, 1/2.f } };

    // +x
    vertices[16] = (vertex){ {  1, -1, -1 }, { -1,  0,  0 }, col, { 0/3.f, 1/2.f } };
    vertices[17] = (vertex){ {  1, -1,  1 }, { -1,  0,  0 }, col, { 1/3.f, 1/2.f } };
    vertices[18] = (vertex){ {  1,  1,  1 }, { -1,  0,  0 }, col, { 1/3.f, 2/2.f } };
    vertices[19] = (vertex){ {  1,  1, -1 }, { -1,  0,  0 }, col, { 0/3.f, 2/2.f } };

    // -z
    vertices[20] = (vertex){ { -1, -1, -1 }, { 0,  0,  1 }, col, { 2/3.f, 0/2.f } };
    vertices[21] = (vertex){ {  1, -1, -1 }, { 0,  0,  1 }, col, { 3/3.f, 0/2.f } };
    vertices[22] = (vertex){ {  1,  1, -1 }, { 0,  0,  1 }, col, { 3/3.f, 1/2.f } };
    vertices[23] = (vertex){ { -1,  1, -1 }, { 0,  0,  1 }, col, { 2/3.f, 1/2.f } };

    return (mesh){
        vcount: 24,
        vertices: vertices
    };
}

Laden der Cube Map

Anschließend sorgen wir noch dafür, dass beim Laden der Szene die Sterne als Cube Map geladen werden.

scene.c

static mesh earthMesh;
static mesh satelliteMesh;
static mesh cubeMap;

static GLuint earthTexture;
static GLuint satelliteTexture;
static GLuint cubeMapTexture;

scene.c

void loadScene(GLFWwindow* window)
{
    setViewportSize(window);

    glClearColor(0, 0, 0, 0);

    earthMesh = createSphereMesh(white);
    satelliteMesh = createCubeMesh(white);
    cubeMap = createCubeMap(white);

    earthTexture = loadTexture("res/earth8k.jpg");
    satelliteTexture = loadTexture("res/thm2k.png");
    cubeMapTexture = loadTexture("res/cubemap2k.jpg");

    glLightfv(GL_LIGHT1, GL_DIFFUSE, (float*)&sunLight);
    glLightfv(GL_LIGHT1, GL_AMBIENT, (float*)&ambientLight);
    glLightfv(GL_LIGHT1, GL_SPECULAR, (float*)&white);

    glLightfv(GL_LIGHT2, GL_DIFFUSE, (float*)&noLight);
    glLightfv(GL_LIGHT2, GL_AMBIENT, (float*)&sunLight);

    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, (float*)&noLight);
}

Falls Sie eine der hochauflösenderen Versionen der Sterne verwenden, denken Sie daran den Dateinamen entsprechend (z.B. in cubemap8k.jpg) anzupassen.

Und Aufräumen nicht vergessen…

scene.c

void unloadScene()
{
    free(earthMesh.vertices);
    free(satelliteMesh.vertices);
    free(cubeMap.vertices);
}

Kamera anpassen

Damit der gewünschte Eindruck von Tiefe entsteht ist es wichtig, dass unsere Cube Map unabhängig von ihrer echten Geometrie immer im Hintergrund erscheint. Dies erreichen wir, indem wir in der ViewMatrix die Bewegung der Kamera ignorieren. Die CubeMap ist damit immer genau um die Kamera zentriert. Wir implementieren dies als möglichst allgemeingültiges Verfahren mit sogenannten Render Layern.

Wir erweitern dafür die Funktion loadCameraViewMatrix() um einen Parameter für den Render Layer und definieren, dass Layer 0 immer diese statischen auf die Kamera zentrierten Objekte beinhaltet.

camera.h

void loadCameraViewMatrix(int layer);

camera.c

void loadCameraViewMatrix(int layer)
{
    glMatrixMode(GL_MODELVIEW);

    glLoadIdentity();
    if (layer > 0)
    {
        matrix translationMatrix = matrixTranslate(0.0f, 0.0f, -cameraDistance);
        glMultMatrixf(&translationMatrix);
    }
    matrix pitchRotationMatrix = matrixRotateX(deg2rad(-cameraPitch));
    glMultMatrixf(&pitchRotationMatrix);
    matrix yawRotationMatrix = matrixRotateY(deg2rad(-cameraYaw));
    glMultMatrixf(&yawRotationMatrix);
}

Render Layer nutzen

Nun müssen wir diese neue Funktion der Render Layer noch sinnvoll anwenden. Wir zeichnen zuerst die Sky Box in Layer 0 mit der gleichen Beleuchtung die wir im letzten Kapitel für den Satelliten genutzt haben. Dann löschen wir den Tiefenpuffer und rendern die restlichen Objekte in Layer 1.

scene.c

void renderScene()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    loadCameraViewMatrix(0);
    glDisable(GL_LIGHT1);
    glEnable(GL_LIGHT2);
    renderMesh(cubeMap, matrixScale(1), cubeMapTexture);
    glClear(GL_DEPTH_BUFFER_BIT);

    loadCameraViewMatrix(1);

    vector4 lightPosition = {0, 0, 50000, 0};
    glLightfv(GL_LIGHT1, GL_POSITION, &lightPosition);

    glDisable(GL_LIGHT2);
    glEnable(GL_LIGHT1);
    renderMesh(earthMesh, calculateEarthRotation(), earthTexture);

    glDisable(GL_LIGHT1);
    glEnable(GL_LIGHT2);
    renderMesh(satelliteMesh, calculateSatellitePosition(), satelliteTexture);
}

Prüfungsvorbereitung

  • Können Sie erklären, was eine Sky Box ist und wie sie funktioniert?
  • Können Sie die Projektionstechnik erklären wie eine Sky Box erzeugt wird? Verwenden Sie dafür die Begriffe “Field Of View”, “Focal Length” und “Aspect Ratio”.
  • Wie nennt man die Matrix die mit matrixScale(1) erzeugt wird?
  • Beschreiben Sie, wie man mit Hilfe eines dritten Render Layers, User Interface Elemente rendern könnte.
  • Nennen Sie einen Anwendungsfall für einen Render Layer in dem lediglich die Bewegung der Kamera berücksichtigt wird, nicht aber deren Rotation.