In diesem Kapitel erweitern wir die Engine so, dass wir komplexe Szenen aus mehreren Objekten laden und entladen können.
Vereinfachte Rotationsmatrix
Der folgende Schritt dient lediglich dazu unseren späteren Code übersichtlicher zu gestalten. Hier schreiben wir eine Funktion, die eine Rotationsmatrix um alle 3 Achsen erzeugt.
cgmath.h
struct Matrix4
{
...
static Matrix4 rotate(double x, double y, double z)
{
return rotateX(x) * rotateY(y) * rotateZ(z);
}
...
};
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/sphere.obj
s shaders/vertex_shader.glsl shaders/fragment_shader.glsl
t Diffuse textures/earth_diffuse.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 im Renderer vergleichen.
Model Header
Die Header Datei der Model Klasse veranschaulicht die Funktionsweise der Klasse. Der Konstruktor erwartet den Pfad zur .model Datei. Mit transform() kann das Modell bewegt und rotiert werden. Die Funktion render() zeichnet das Modell.
model.h
#pragma once
#include "cgmath.h"
#include "mesh.h"
#include "shader.h"
#include "texture.h"
#include <map>
#include <optional>
#include <string>
class Model
{
public:
Model(const std::string &filename);
~Model();
void transform(const Vector3 &position, const Vector3 &rotation, double scale);
void render(const Matrix4 &projectionMatrix, const Matrix4 &viewMatrix, const Vector3 &sunDirection);
private:
Vector3 position = Vector3(0, 0, 0);
Vector3 rotation = Vector3(0, 0, 0);
double scale = 1.0;
std::optional<Shader> shader = {};
std::optional<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 Grunde genau das getan, was wir vorher im Renderer gemacht haben. Das gleiche gilt für den Destruktor.
model.cpp
#include "model.h"
#include "filereader.h"
Model::Model(const std::string &filename)
{
FileReader reader(filename);
while (reader.hasLine())
{
std::string type = reader.getString();
if (type == "m")
{
// mesh
std::string meshName = reader.getString();
mesh.emplace(meshName);
}
else if (type == "s")
{
// shader
std::string vertexShaderName = reader.getString();
std::string fragmentShaderName = reader.getString();
shader.emplace(vertexShaderName, fragmentShaderName);
}
else if (type == "t")
{
// texture
std::string uniformName = reader.getString();
std::string textureName = reader.getString();
textures.emplace(uniformName, textureName);
}
}
}
Model::~Model()
{
mesh.reset();
shader.reset();
textures.clear();
}
void Model::transform(const Vector3 &position, const Vector3 &rotation, double scale)
{
this->position = position;
this->rotation = rotation;
this->scale = scale;
}
void Model::render(const Matrix4 &projectionMatrix, const Matrix4 &viewMatrix, const Vector3 &sunDirection)
{
Matrix4 worldMatrix = Matrix4::translate(position.x, position.y, position.z) * Matrix4::rotate(rotation.x, rotation.y, rotation.z) * Matrix4::scale(scale);
shader->activate();
shader->setMatrix4("ProjectionMatrix", projectionMatrix);
shader->setMatrix4("ViewMatrix", viewMatrix);
shader->setMatrix4("WorldMatrix", worldMatrix);
shader->setVector3("SunDirection", sunDirection);
for (const auto &[uniformName, texture] : textures)
{
shader->setTexture(uniformName, texture);
}
mesh->draw();
}
Renderer anpassen
Nun da wir Meshes, Shader und Texturen komplett in der Model Klasse gekapselt haben, können wir alle Verweise darauf aus dem Renderer entfernen. Durch den 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, den Renderer komplett von außen steuerbar zu machen.
renderer.h
#pragma once
#include "cgmath.h"
#include "model.h"
#include "settings.h"
#include "window.h"
#include <map>
#include <string>
class Renderer
{
public:
...
size_t loadModel(const std::string &filename);
void updateModel(size_t modelId, const Vector3 &position, const Vector3 &rotation, double scale);
void unloadModel(size_t modelId);
private:
...
Vector3 sunDirection = Vector3(1.0, 1.0, 2.0);
std::map<size_t, Model> models = {};
size_t currentModelId = 0;
};
renderer.cpp
...
Renderer::Renderer(const Settings &settings, Window &window)
{
...
size_t thmModelID = loadModel("models/thm.model");
updateModel(thmModelID, Vector3(-1, 0, 0), Vector3(0, 0, 0), 1.0);
size_t earthModelID = loadModel("models/earth.model");
updateModel(earthModelID, Vector3(1, 0, 0), Vector3(0, 0, 0), 1.0);
}
Renderer::~Renderer()
{
models.clear();
}
void Renderer::loop()
{
setViewport();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
for (auto &[key, model] : models)
{
model.render(projectionMatrix, viewMatrix, sunDirection);
}
}
...
size_t Renderer::loadModel(const std::string &filename)
{
size_t modelId = ++currentModelId;
models.emplace(modelId, filename);
return modelId;
}
void Renderer::updateModel(size_t modelId, const Vector3 &position, const Vector3 &rotation, double scale)
{
if (auto it = models.find(modelId); it != models.end())
{
it->second.transform(position, rotation, scale);
}
}
void Renderer::unloadModel(size_t modelId)
{
models.erase(modelId);
}
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 sphere.obj herunter und speichern Sie sie unter Computergrafik/cgm_07/meshes/sphere.obj
Laden Sie sich die Textur earth_diffuse.jpg herunter und speichern Sie sie unter Computergrafik/cgm_07/textures/earth_diffuse.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.