In diesem Kapitel vollenden wir das Projekt. Wir laden zusätzliche Modelle, fügen einfache Skripte hinzu damit sich unsere Objekte bewegen können und ergänzen die Erde um einen neuen spezialisierten Shader.

Neue Modelle laden

Wir beginnen damit unsere Szene mit etwas mehr Inhalt zu versehen.

Sonne

Um die Sonne zu zeichnen benötigen wir eine neue Modell Datei. Hier wird der einfache Shader ohne Beleuchtung verwendet damit die Sonne immer gleichmäßig hell leuchtet.

models/sun.model

m meshes/sphere.obj s shaders/vertex_shader.glsl shaders/fragment_shader.glsl t Diffuse textures/white.png

Außerdem benötigen wir eine einfarbig weiße Textur. Speichern Sie die Datei in den passenden Ordner:

THM Box

models/box.model

m meshes/box.obj s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit_drn.glsl t Diffuse textures/box_diffuse.png t NormalMap textures/box_normal.png t Roughness textures/box_roughness.png

Hier finden Sie die Mesh Datei

  • Computergrafik/cgm_10/meshes/box.obj

und die benötigten Texturen

Landing Pad

models/landingpad.model

m meshes/landingpad.obj s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit.glsl t Diffuse textures/landingpad.jpg

Hier finden Sie die Mesh Datei

und die benötigte Textur

Erde 2.0

models/earth.model

m meshes/sphere.obj s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit_earth.glsl t Diffuse textures/earth_diffuse.jpg t Roughness textures/earth_roughness.jpg t NormalMap textures/earth_normal.jpg

shaders/fragment_shader_lit_earth.glsl

#version 330 core out vec4 FragColor; in vec2 TexCoord; in vec3 NormVec; in vec3 VertPos; in vec3 SunDirectionObjSpc; in vec3 CameraPosObjSpc; uniform sampler2D Diffuse; uniform sampler2D Roughness; uniform sampler2D NormalMap; void main() { vec3 lightColor = vec3(1.0, 1.0, 1.0); vec3 textureColor = vec3(texture(Diffuse, TexCoord)); float roughness = vec3(texture(Roughness, TexCoord)).r; vec3 normal = normalize(texture(NormalMap, TexCoord).rgb * 2.0 - 1.0); // ambient vec3 ambient = 0.025 * lightColor * textureColor; // diffuse float lightIntensity = max(dot(normal, SunDirectionObjSpc), 0.0); vec3 diffuse = lightIntensity * lightColor * textureColor; // specular vec3 viewDir = normalize(VertPos - CameraPosObjSpc); vec3 reflectDir = reflect(SunDirectionObjSpc, normal); vec3 specular = 0.5 * pow(max(dot(viewDir, reflectDir), 0.0), 16) * lightColor * (1.0 - roughness); // refraction vec3 athmosphereColor = vec3(0.3, 0.4, 0.65); float refractionIntensity = (1.0 - max(dot(NormVec, -viewDir), 0.0)) * lightIntensity; FragColor = vec4(mix(ambient + diffuse + specular, athmosphereColor, refractionIntensity), 1.0); }

Neue Szene

Zuletzt fügen wir alle neuen Modelle zusammen in einer überarbeiteten Szene.

scenes/main.scene

e Sky m models/skybox.model fixed p 0.0 0.0 0.0 r 0.0 0.0 0.0 e Sun m models/sun.model fixed p 1.0 1.0 2.0 r 0.0 0.0 0.0 s 0.03 e Earth m models/earth.model fixed p 2.0 0.0 -3.0 r 0.0 0.0 0.0 l earthRotation e THM m models/thm.model p 0.0 0.0 12.0 r 0.0 180.0 0.0 e THM m models/thm.model p 0.0 0.0 -12.0 r 0.0 0.0 0.0 e THM m models/thm.model p -12.0 0.0 0.0 r 0.0 90.0 0.0 e THM m models/thm.model p 12.0 0.0 0.0 r 0.0 -90.0 0.0 e LandingPad m models/landingpad.model p 0.0 -1.7 0.0 r 0.0 0.0 0.0 e Box1 m models/box.model s 0.5 p -4.8 -1.7 -7.5 r 0.0 -11.1 0.0 e Box2 m models/box.model s 0.5 p -6.0 -1.7 -7.2 r 0.0 18.0 0.0 e Box3 m models/box.model s 0.5 p -5.8 -1.7 -8.4 r 0.0 5.6 0.0 e Box4 m models/box.model s 0.5 p -5.4 -0.7 -7.8 r 0.0 60.8 0.0

Hotfix für die Texturen am Südpol der Erde

Wenn Sie den Südpol der Erde genauer betrachten, dann fällt ihnen sicher auf, dass es dort zu seltsamen Artefakten kommt. Das tritt insbesondere auf, wenn man die Erde aus größerer Distanz anschauen. Das Problem hat mit der Art zu tun wie unsere Texturkoordinaten an den Rändern umgebrochen werden.

Ein möglicher Fix für dieses Problem ist folgender:

texture.cpp

Texture::Texture(const std::string &filename) { ... glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); ... }

Vergleichen Sie den Unterschied und lesen Sie nach falls Sie nicht ganz sicher sind, was hier genau vor sich geht.

Skripte und Animationen

Um unsere Objekte dynamisch steuerbar zu machen, führen wir Skripte ein. Ein Skript ist nicht weiter als ein Callback welches pro Frame von einer Entity aufgerufen wird. In diesem Callback kann dann z.B. die Position oder Rotation einer Entity verändert werden.

Die Integration zieht sich durch die komplette Hierarchie von Simulation, Scene und Entity.

Simulation

Da die Simulation die einzige extern erreichbare Klasse bleiben soll, wird hier das registrieren von Skripten abgehandelt.

simulation.h

class Simulation { public: ... void registerScript(const std::string &name, ScriptCallback script); private: ... std::map<std::string, ScriptCallback> scripts = {}; };

Über Simulation::registerScript kann ein benanntes Callback registriert werden.

simulation.cpp

void Simulation::registerScript(const std::string &name, ScriptCallback script) { scripts.emplace(name, script); }

Und im Loop übergeben wir alle registrierten Skripte an die Scene damit die einzelnen Entities auf diese Callbacks Zugriff erhalten.

simulation.cpp

void Simulation::loop(const double time, UpdateModelCallback updateModelCallback) { ... for (auto &[i, scene] : scenes) { scene.update(time, scripts, updateModelCallback); } }

Scene

Die Scene selbst ist verantwortlich den Entities ihre Scripte beim Laden zuzuweisen. Im Loop reicht sie die scripts-Map einfach nur durch.

scene.h

class Scene { public: ... void update(double time, const std::map<std::string, ScriptCallback> &scripts, UpdateModelCallback updateModelCallback); ... };

scene.cpp

Scene::Scene(const std::string &filename, LoadModelCallback loadModelCallback) { FileReader reader(filename); while (reader.hasLine()) { ... else if (type == "l") { entities[entities.size() - 1].setScript(reader.getString()); } } }

scene.cpp

void Scene::update(double time, const std::map<std::string, ScriptCallback> &scripts, UpdateModelCallback updateModel) { for (auto &entity : entities) { entity.update(time, scripts, updateModel); } }

Entity

Die gesamte Logik des Aufrufs befindet sich in der Entity selbst.

entity.h

... using ScriptCallback = std::function<void(Vector3 &position, Vector3 &rotation, double time)>; class Entity { public: ... void update(double time, const std::map<std::string, ScriptCallback> &scripts, UpdateModelCallback updateModel); void setScript(const std::string &script); ... private: ... std::string script = ""; };

entity.cpp

void Entity::update(double time, const std::map<std::string, ScriptCallback> &scripts, UpdateModelCallback updateModel) { if (script != "" && scripts.contains(script)) { scripts.at(script)(position, rotation, time); } if (updateModel.has_value()) { updateModel.value()(modelId, position, rotation, scale); } }

entity.cpp

void Entity::setScript(const std::string &script) { this->script = script; }

Neue Klasse App

Wir führen noch eine letzte neue Klasse ein. Hier laufen Simulation und Renderer zusammen und eigener Code für eine eigene Anwendung findet hier Platz. In einem "echten" Spiel würde eine Klasse dazu vermutlich nicht ausreichen. Daher kann man die App auch als konzeptionellen Platzhalter betrachten.

Wichtig ist die Zielsetzung, dass man eine klare Trennung zwischen Engine und Applikation schafft.

app.h

#pragma once #include "renderer.h" #include "simulation.h" class App { public: App(Renderer &renderer, Simulation &simulation); ~App(); void update(double time); private: Renderer &renderer; Simulation &simulation; size_t sceneId = 0; void earthRotation(Vector3 &position, Vector3 &rotation, double time); };

app.cpp

#include "app.h" #include <chrono> App::App(Renderer &renderer, Simulation &simulation) : renderer(renderer), simulation(simulation) { simulation.registerScript("earthRotation", std::bind(&App::earthRotation, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); sceneId = simulation.loadScene("scenes/main.scene", std::bind(&Renderer::loadModel, &renderer, std::placeholders::_1, std::placeholders::_2)); } App::~App() { simulation.unloadScene(sceneId, std::bind(&Renderer::unloadModel, &renderer, std::placeholders::_1)); } void App::update(double deltaTime) { simulation.loop(deltaTime, std::bind(&Renderer::updateModel, &renderer, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); } void App::earthRotation(Vector3 &position, Vector3 &rotation, double deltaTime) { auto time = std::chrono::system_clock::now(); auto timeSinceEpoch = time.time_since_epoch(); double secondsSinceEpoch = std::chrono::duration<double>(timeSinceEpoch).count(); double timeOfDay = std::fmod(secondsSinceEpoch, 86400); double earthRotation = timeOfDay / 86400.0 * deg2rad(360) + deg2rad(112.5); int timeOfYear = std::fmod(secondsSinceEpoch + 864000.0, 31557600); double earthEcliptic = cos(timeOfYear / 31557600.0 * deg2rad(360)) * deg2rad(-23.4) - deg2rad(22.5); rotation = Vector3(earthEcliptic, earthRotation, 0); }

Main 2.0

Im letzten Schritt räumen wir nun die main() auf. Dabei werden alle vorherigen Callbacks entfernt, denn diese sind ja nun in der App Klasse. Unser Loop ist damit sehr übersichtlich und alle Aufräumarbeiten passieren in den Destruktoren von App, Simulation, Renderer und Window.

main.cpp

int main() { ... Window window("Computergrafik", settings); Renderer renderer(settings, window); Simulation simulation(settings, window); App demoApp(renderer, simulation); double deltaTime = 0.0; while (window.loop(deltaTime)) { demoApp.update(deltaTime); renderer.setViewMatrix(simulation.getCameraViewMatrix()); renderer.setCameraPosition(simulation.getCameraPosition()); renderer.loop(); } ... }

Das wars!

Mit diesem Kapitel endet das Modul Computergrafik. Wir haben ein solides Fundament für eine eigene 3D Engine erstellt. Aber es gibt noch einiges zu tun um die Software in ein produktiv einsetzbares Tool zu verwandeln.

Hier ein paar Gedanken was aus meiner Sicht noch fehlt.

  • Benutzereingaben sollten auch in der App Klasse ankommen.
  • Settings müssen natürlich vom Benutzer zur Laufzeit geändert werden können. Auch hier könnte die App Klasse das Laden und Speichern einer Settings-Datei übernehmen.
  • Besseres Verhalten wenn man mit Alt+Tab die Anwendung verlässt.
  • Verwendung von komprimierten Texturen kann die Ladezeit auf unter eine Sekunde verkürzen.
  • Caching um Texturen und Meshes nur einmal zu laden aber mehrfach zu nutzen.
  • uvw.

Haben Sie auch Ideen was man noch verbessern könnte oder auch was man ganz anders lösen könnte? Bringen Sie die Themen mit ins Plenum und lassen Sie uns darüber diskutieren.

Effiziente Algorithmen in der Computergrafik

Wenn Ihnen die Veranstaltung gefallen hat, dann kann ich Ihnen auch meine andere Masterveranstaltung Effiziente Algorithmen in der Computergrafik empfehlen.

Dort erweitern wir diese Engine um viele spannende Features. Unter anderem werden wir die Applikation schneller machen und den Speicherverbrauch reduzieren aber wir werden uns auch mit Physik beschäftigen und die Steuerung eines Raumschiffs nachbauen. Die ideale Mischung aus C++, Grafik und Gaming.

Entwicklungsprojekt / Masterarbeit

Wer noch tiefer in die Materie einsteigen will, dem kann ich anbieten ein Entwicklungsprojekt oder die Masterarbeit im Bereich Computergrafik zu machen. Ich arbeite ja gerade selber an meinem ersten kommerziellen Spiel und die Faszination für das Thema und die Technologie hat bei mir auch im Masterstudium an der THM begonnen. Vielleicht haben Sie ja eine tolle Idee für ein neues Feature?!

Wenn Sie Interesse haben, schreiben Sie mir gerne eine E-Mail.