In diesem Kapitel erweitern wir die Engine so, dass wir komplexe Szenen aus mehreren Objekten laden und entladen können.

Inhalt


Neue Klasse Model

Im letzten Kapitel haben wir die Grundlage geschaffen um beliebige Meshes aus Dateien zu laden. Die Klasse Mesh beinhaltet dabei lediglich die Vertices eines Objektes aber keine Transformationen, Shader oder Texturen. Auf diese Weise halten wir uns die Möglichkeit offen, das gleiche Mesh mit mehrmals mit unterschiedlichem Aussehen zu rendern.

Wir erstellen nun eine neue Klasse Model. Ein Model ist die Kombination aus Mesh + Shader + Texturen und beinhaltet Angaben zu Position und Rotation. Ein Model ist also die Repräsentation eines logischen Objektes in der 3D Welt.

Modelldateien

Ein Model wird genau wie ein Mesh aus einer Datei geladen. Hierfür verwenden wir ein eigenes Dateiformat welches wir syntaktisch an die OBJ Dateien anlehnen. Als Dateiendung verwenden wir .model

Erstellen Sie die beiden folgenden Modelldateien:

Computergrafik/cgm_07/models/earth.model

m meshes/earth.obj
s shaders/vertex_shader.glsl shaders/fragment_shader.glsl
t Diffuse textures/earth8k.jpg

Computergrafik/cgm_07/models/thm.model

m meshes/thm.obj
s shaders/vertex_shader.glsl shaders/fragment_shader.glsl
t Diffuse textures/thm_colors.jpg

Die Syntax sollte selbsterklärend sein wenn Sie die Datei thm.model mit unserem bisherigen Code in der Grafikeinheit vergleichen.

Model Header

Die Header Datei der Model Klasse veranschaulicht die Funktionsweise der Klasse. Der Konstruktor erwartet den Pfad zur .model Datei. Mit setTransform() kann das Modell bewegt und rotiert werden. Die Funktion render() zeichnet das Modell.

model.h

#pragma once
#include <map>
#include <string>
#include "cgmath.h"
#include "mesh.h"
#include "shader.h"
#include "texture.h"

class Model
{
    public:
    Model(std::string filename);
    ~Model();
    void setTransform(Vector3, Vector3);
    void render(Matrix projectionMatrix, Matrix viewMatrix, Vector3 sunLight);
    
    private:
    Vector3 position;
    Vector3 rotation;
    Shader* shader;
    Mesh* mesh;
    std::map<std::string, Texture*> textures;
};

Die Implementierung

Im Konstruktor entdecken Sie erneut die Verwendung des FileReaders. Hier werden die einzelnen Komponenten des Models geladen. Der Inhalt der render() Methode sollte Ihnen bekannt vorkommen, denn hier wird im Grund genau das getan, was wir vorher in der Grafikeinheit gemacht haben. Das gleiche gilt für den Destruktor.

model.cpp

#include "model.h"
#include "filereader.h"
#include <iostream>

Model::Model(std::string filename)
{
    FileReader* reader = new FileReader(filename);
    while (reader->hasLine())
    {
        std::string type = reader->getString();
        if (type == "m")
        {
            //mesh
            std::string m = reader->getString();
            this->mesh = new Mesh(m);
        }
        else if (type == "s")
        {
            //shader
            std::string vs = reader->getString();
            std::string fs = reader->getString();
            this->shader = new Shader(vs, fs);
        }
        else if (type == "t")
        {
            //textures
            std::string slot = reader->getString();
            Texture* tex = new Texture(reader->getString().c_str());
            this->textures.insert(std::pair<std::string,Texture*>(slot, tex));
        }
    }
}

void Model::render(Matrix projectionMatrix, Matrix viewMatrix, Vector3 sunLight)
{
    shader->activate();
    shader->setMatrix("ProjectionMatrix", projectionMatrix);
    shader->setMatrix("ViewMatrix", viewMatrix);
    shader->setMatrix("WorldMatrix", matrixMultiply(matrixTranslate(position.x, position.y, position.z), matrixMultiply(matrixRotateX(rotation.x), matrixMultiply(matrixRotateY(rotation.y), matrixRotateZ(rotation.z)))));
    shader->setVector3("SunLight", sunLight);
    for (std::map<std::string,Texture*>::iterator itr = textures.begin(), itr_end = textures.end(); itr != itr_end; ++itr)
    {
        shader->setTexture(itr->first.c_str(), itr->second);
    }
    mesh->draw();
}

void Model::setTransform(Vector3 position, Vector3 rotation)
{
    this->position = position;
    this->rotation = rotation;
}

Model::~Model()
{
    delete mesh;
    delete shader;
    for (std::map<std::string,Texture*>::iterator itr = textures.begin(), itr_end = textures.end(); itr != itr_end; ++itr)
    {
        delete itr->second;
    }
}

Grafikeinheit anpassen

Nun da wir Meshes, Shader und Texturen komplett in der Model Klasse gekapselt haben, können wir alle Verweise darauf aus der Grafikeinheit entfernen. Durch dein Einsatz von Modellen kann der Code hier sehr viel allgemeiner formuliert werden und mehr und mehr Inhaltliche Details verschwinden in die Konfigurationsdateien.

Langfristig ist das Ziel, die Grafikeinheit komplett von außen steuerbar zu machen. Die Funktionen graphicsLoadModel(), graphicsUpdateModel() und graphicsUnloadModel() sind die Basis dafür.

#include <glad.c>
#include <GLFW/glfw3.h>
#include <iostream>
#include <map>
#include "cgmath.h"
#include "settings.h"
#include "vertex.h"
#include "model.h"

static Settings settings;
static int viewportWidth = 0;
static int viewportHeight = 0;
static int resizeViewport = false;
static Matrix viewMatrix;
static Matrix projectionMatrix;
static Vector3 sunLight = (Vector3){0.577f,-0.577f,-0.577f};
static std::map<int, Model*> models;
static int modelId = 0;

static void setViewport()
{
    if (resizeViewport)
    {
        resizeViewport = false;

        glViewport(0,0, viewportWidth, viewportHeight);
        double zNear = 0.1;
        double zFar = 100.0;
        double fov = 0.785; //45deg
        double aspect = (double)viewportWidth / (double)viewportHeight;
        projectionMatrix = matrixPerspective(fov, aspect, zNear, zFar);
    }
}

void graphicsUpdateCamera(Matrix m)
{
    viewMatrix = m;
}

int graphicsLoadModel(std::string filename)
{
    int id = modelId++;
    Model* m = new Model(filename);
    models.insert(std::pair<int,Model*>(id, m));
    return id;
}

void graphicsUpdateModel(int id, Vector3 position, Vector3 rotation)
{
    models[id]->setTransform(position, rotation);
}

void graphicsUnloadModel(int id)
{
    delete models[id];
    models.erase(id);
}

bool graphicsStart(Settings props)
{
    settings = props;
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return false;
    }
    
    glfwSwapInterval(props.vsync ? 1 : 0);
    if (settings.msaa) glEnable(GL_MULTISAMPLE);
    if (settings.culling) glEnable(GL_CULL_FACE);
    if (settings.depth) glEnable(GL_DEPTH_TEST);

    glClearColor(0.2f,0.2f,0.2f,1.0f);

    int thmModel = graphicsLoadModel("models/thm.model");
    graphicsUpdateModel(thmModel, (Vector3){-1,0,0}, (Vector3){});
    int earthModel = graphicsLoadModel("models/earth.model");
    graphicsUpdateModel(earthModel, (Vector3){1,0,0}, (Vector3){});

    return true;
}

void graphicsLoop()
{
    setViewport();
    
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    for (std::map<int, Model*>::iterator itr = models.begin(), itr_end = models.end(); itr != itr_end; ++itr)
    {
        itr->second->render(projectionMatrix, viewMatrix, sunLight);
    }
}

void graphicsTerminate()
{
    for (std::map<int, Model*>::iterator itr = models.begin(), itr_end = models.end(); itr != itr_end; ++itr)
    {
        delete itr->second;
    }
    models.clear();
}

void graphicsSetWindowSize(int width, int height)
{
    std::cout << "Resolution: " << width << "x" << height << "\n";
    viewportWidth = width;
    viewportHeight = height;
    resizeViewport = true;
}

Mesh und Textur der Erde

Nun fehlen lediglich noch zwei Dateien damit wir zusätzlich zum THM Logo auch noch eine Erde sehen.

Laden Sie sich die Mesh Datei earth.obj herunter und entpacken Sie sie nach Computergrafik/cgm_07/meshes/earth.obj

Laden Sie sich diese Textur der Erde von der NASA herunter. Achten Sie darauf die Originaldatei zu laden und nicht die verkleinerten Versionen. Speichern Sie die Textur anschließend unter Computergrafik/cgm_07/textures/earth8k.jpg

Wenn Sie nun Ihre Anwendung starten, sollte Ihr THM Logo noch immer unverändert funktionieren. Dies ist auch gleich das wichtigste Testkriterium um den neuen Code zu validieren. Außerdem sollte daneben eine Erde auftauchen, so wie oben auf dem Screenshot.


Prüfungsvorbereitung

  • Können Sie den Unterschied zwischen ++i und i++ erklären?
  • Können Sie erklären wie der Iterator einer Map bedient wird?
  • Was tut die delete Anweisung?
  • Wenn Sie erklären müssten, was der Sinn dieses Kapitels war, was würden Sie sagen?