13 KiB
Engine — gebruikshandleiding
Deze handleiding legt uit hoe de engine in elkaar zit, hoe je hem gebruikt, hoe je eigen elementen (objecten die getekend/geüpdatet worden) toevoegt, en hoe je input afhandelt. De engine bestaat uit vier onderdelen:
| Klasse | Bestand | Verantwoordelijkheid |
|---|---|---|
Engine |
include/Engine.hpp / src/Engine.cpp |
Venster, GL-context, main loop, ruwe input → Camera |
Camera |
include/Camera.hpp / src/Camera.cpp |
Arcball-camera (orbit/pan/zoom, cinematisch smoothed) |
Shader |
include/Shader.hpp / src/Shader.cpp |
RAII-wrapper om een gelinkt GLSL-programma |
Mesh |
include/Mesh.hpp / src/Mesh.cpp |
VAO/VBO/EBO-wrapper + procedurele geometrie (sphere/ring/circle) |
Alle vier zitten in de engine namespace, zodat er geen naamsbotsingen
ontstaan met GLFW/OpenGL-symbolen of andere libraries. Voeg je eigen
elementen (zie sectie 3) toe aan diezelfde namespace.
1. Basisgebruik
#include "Engine.hpp"
int main() {
engine::Engine engine;
if (!engine.initialize(1280, 720, "Mijn App")) {
return 1;
}
engine.setUpdateCallback([](float deltaTime) {
// hier zit jouw simulatie/game-state update, één keer per frame
});
engine.setRenderCallback([](int framebufferWidth, int framebufferHeight) {
// hier zit jouw OpenGL-tekenwerk, één keer per frame
});
engine.run();
return 0;
}
initialize() doet alles wat met GLFW/OpenGL-opstart te maken heeft:
venster aanmaken, context activeren, de OpenGL 3.3-functiepointers laden
(gl::loadGLFunctions()), en de GLFW-callbacks aan de instantie koppelen.
run() is de main loop: pollen van events, delta-time berekenen, update()
en render() aanroepen, buffers swappen. Dat blijft zo doorlopen tot het
venster gesloten wordt (kruisje, of Escape — zie hieronder).
Je hoeft Engine.hpp niet aan te passen om iets te laten gebeuren: alles
gaat via de twee callbacks. Dat houdt de engine zelf generiek en jouw
project-specifieke code gescheiden.
2. De Camera gebruiken
Engine bezit al een Camera en stuurt muisinput er automatisch naartoe:
- linker muisknop + slepen → oriënteren (yaw/pitch)
- middelste muisknop + slepen → pannen
- scrollwiel → in-/uitzoomen
- R → camera terugzetten naar standaardstand
- Escape → venster sluiten
Je haalt de camera op met engine.camera() en gebruikt hem in je
render-callback:
glm::mat4 view = engine.camera().viewMatrix();
glm::vec3 eye = engine.camera().position();
Wil je de camera ergens naartoe laten bewegen (bijv. "focus op dit object"),
gebruik dan focusOn(worldPoint, suggestedDistance) — dat zet alleen het
doel van de smoothing, update() (elke frame al door Engine aangeroepen)
beweegt er geleidelijk naartoe.
engine.camera().focusOn(objectPosition, 8.0f);
3. Een eigen element maken
Met "element" bedoelen we een zelfstandig object dat iets voorstelt in je
scene: een blokje, personage, deeltjeseffect, wat dan ook. Het patroon dat
deze engine gebruikt (en dat je zelf ook aanhoudt) is steeds: data +
gedrag in een klasse, header in include/, implementatie in src/,
buiten de klasse gebeurt niets rechtstreeks met OpenGL.
3.1 Bestandsstructuur
Voor een nieuw element Cube maak je:
include/Cube.hpp
src/Cube.cpp
Cube.hpp:
#pragma once
#include "Mesh.hpp"
#include "Shader.hpp"
#include <glm/glm.hpp>
namespace engine {
// -----------------------------------------------------------------------
// Cube
//
// Eén gekleurd blokje: eigen transform + eigen shader-uniforms, tekent
// zichzelf met de gedeelde Mesh-geometrie.
// -----------------------------------------------------------------------
class Cube {
public:
bool initialize(); // compileert shader, bouwt mesh — kan mislukken
void update(float deltaTime);
void draw(const glm::mat4& view, const glm::mat4& projection,
const glm::vec3& viewPos) const;
glm::vec3 position{0.0f};
glm::vec3 baseColor{0.8f, 0.3f, 0.2f};
private:
Mesh mesh_;
Shader shader_;
float spinDeg_ = 0.0f;
};
} // namespace engine
Cube.cpp:
#include "Cube.hpp"
#include <glm/gtc/matrix_transform.hpp>
namespace engine {
bool Cube::initialize() {
// Mesh::createUVSphere/createRing/createOrbitCircle bestaan al; voor een
// echte kubus zou je een Mesh::createBox() toevoegen aan Mesh (zie
// sectie 4). Voor dit voorbeeld hergebruiken we de bol als placeholder.
mesh_ = Mesh::createUVSphere(16, 16);
return shader_.loadFromFiles("assets/shaders/basic.vert", "assets/shaders/basic.frag");
}
void Cube::update(float deltaTime) {
spinDeg_ += deltaTime * 45.0f;
}
void Cube::draw(const glm::mat4& view, const glm::mat4& projection,
const glm::vec3& viewPos) const {
glm::mat4 model = glm::translate(glm::mat4(1.0f), position);
model = glm::rotate(model, glm::radians(spinDeg_), glm::vec3(0.0f, 1.0f, 0.0f));
shader_.use();
shader_.setMat4("uModel", model);
shader_.setMat4("uView", view);
shader_.setMat4("uProjection", projection);
shader_.setVec3("uBaseColor", baseColor);
shader_.setVec3("uLightDir", glm::vec3(-0.4f, -1.0f, -0.3f));
shader_.setVec3("uViewPos", viewPos);
mesh_.draw();
}
} // namespace engine
3.2 Aanhaken in main.cpp
#include "Cube.hpp"
#include "Engine.hpp"
engine::Cube cube;
int main() {
engine::Engine engine;
engine.initialize(1280, 720, "Mijn App");
cube.initialize();
cube.position = glm::vec3(0.0f, 0.0f, 0.0f);
engine.setUpdateCallback([&](float dt) {
cube.update(dt);
});
engine.setRenderCallback([&](int w, int h) {
glViewport(0, 0, w, h);
glClearColor(0.05f, 0.06f, 0.09f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
float aspect = static_cast<float>(w) / static_cast<float>(h);
glm::mat4 proj = glm::perspective(glm::radians(50.0f), aspect, 0.05f, 200.0f);
cube.draw(engine.camera().viewMatrix(), proj, engine.camera().position());
});
engine.run();
return 0;
}
Zodra je meerdere elementen hebt, wordt dit al snel een simpele lijst:
std::vector<std::unique_ptr<Cube>> cubes;
// update: for (auto& c : cubes) c->update(dt);
// render: for (auto& c : cubes) c->draw(view, proj, eye);
Er zit geen "Scene"- of "Entity"-systeem in deze engine — dat is bewust
weggelaten, zodat je zelf kiest hoe zwaar je dat wilt maken (een simpele
std::vector, of iets uitgebreiders als je project groeit).
4. Nieuwe geometrie toevoegen aan Mesh
Mesh heeft nu createUVSphere, createRing en createOrbitCircle als
static factory-methodes. Een nieuwe vorm (bijv. een kubus) voeg je op
dezelfde manier toe:
In include/Mesh.hpp, bij de andere static methodes:
static Mesh createBox(float halfExtent);
In src/Mesh.cpp, een nieuwe functie die Vertex-en unsigned int-arrays
vult en via upload() naar de GPU stuurt (zoals createUVSphere doet):
Mesh Mesh::createBox(float halfExtent) {
std::vector<Vertex> vertices;
std::vector<unsigned int> indices;
// ... vul vertices/indices met de 8 hoekpunten + 12 driehoeken ...
Mesh mesh;
mesh.upload(vertices, indices);
return mesh;
}
Het patroon is steeds: bouw CPU-side arrays van Vertex{position, normal, uv}
en unsigned int-indices, en geef die aan upload(). Voor een lijnvorm
(zoals een orbit-pad) gebruik je uploadLineStrip() in plaats daarvan.
5. Eigen shaders toevoegen
Shaders zijn losse .vert/.frag-bestanden in assets/shaders/. Om een
nieuwe te gebruiken:
- Zet
mijnshader.vertenmijnshader.fraginassets/shaders/. Shader shader; shader.loadFromFiles("assets/shaders/mijnshader.vert", "assets/shaders/mijnshader.frag");- Uniforms zet je via
setMat4/setVec3/setVec4/setFloat/setInt— de namen moeten exact overeenkomen met deuniform-declaraties in de shader.
Er is geen build-stap nodig voor nieuwe shaders: CMakeLists.txt kopieert
gewoon de hele assets/-map naast de executable na elke build
(add_custom_command(TARGET ... POST_BUILD ...)), dus een nieuw bestand
daarin verschijnt vanzelf mee.
6. Input afhandelen
6.1 Wat de Engine al doet
Engine routeert standaard alleen muis (orbit/pan/zoom) naar de Camera,
plus R (camera reset) en Escape (venster sluiten) als toetsen. Dat
zit in de private GLFW-callbacks in Engine.cpp
(cursorPosCallback, mouseButtonCallback, scrollCallback, keyCallback).
6.2 Eigen input toevoegen zonder Engine aan te passen
Voor input die niet met de camera te maken heeft (bijv. spatie om te pauzeren,
een klik om iets te selecteren) poll je zelf, direct in je update-callback,
via engine.window():
engine.setUpdateCallback([&](float dt) {
if (glfwGetKey(engine.window(), GLFW_KEY_SPACE) == GLFW_PRESS) {
paused = !paused;
}
});
Let op: dit is polling (elke frame checken of een toets nu ingedrukt is), wat prima werkt voor "hou ingedrukt"-gedrag maar bij "één druk = één actie" kan dubbel triggeren zolang de toets ingedrukt blijft. Hou daarvoor zelf een "was dit al ingedrukt vorige frame"-vlag bij, of ga naar optie 6.3.
6.3 Eigen input toevoegen mét een discrete "press"-callback
Wil je events (niet polling) zoals de originele game had
(keyCallback die éénmalig reageert op GLFW_PRESS), dan breid je Engine
zelf uit met een extra callback-slot, op dezelfde manier als
updateCallback_/renderCallback_:
In include/Engine.hpp:
using KeyCallback = std::function<void(int key, int action)>;
void setKeyCallback(KeyCallback cb) { keyCallback_ = std::move(cb); }
// ...
KeyCallback keyCallback_;
In src/Engine.cpp, in de bestaande static Engine::keyCallback
(de GLFW-trampoline), roep je hem aan:
void Engine::keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
auto* engine = static_cast<Engine*>(glfwGetWindowUserPointer(window));
if (!engine) return;
if (action == GLFW_PRESS) {
switch (key) {
case GLFW_KEY_R: engine->camera_.reset(); break;
case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(window, GLFW_TRUE); break;
default: break;
}
}
if (engine->keyCallback_) engine->keyCallback_(key, action);
}
Dan in je eigen main.cpp:
engine.setKeyCallback([&](int key, int action) {
if (key == GLFW_KEY_SPACE && action == GLFW_PRESS) {
paused = !paused;
}
});
Dit is exact het patroon dat de engine al gebruikt voor update/render —
een std::function-lid + een setter + een aanroep op de juiste plek in de
bestaande GLFW-trampoline. Muisklikken (bijv. voor object-picking) volgen
dezelfde aanpak via mouseButtonCallback.
7. Hoe je .hpp/.cpp-bestanden zo opzet dat ze echt bij de engine horen
Drie regels houden nieuwe code consistent met wat er al staat:
- Eén klasse per bestandspaar, header in
include/, implementatie insrc/, zelfde bestandsnaam als de klasse (Cube.hpp↔Cube.cpp).#include "Cube.hpp"bovenaanCube.cppals eerste include. - Alles in de
engine-namespace (of je eigen naam, zie hieronder), zodat er geen naamsbotsingen ontstaan met GLFW/OpenGL-symbolen of andere libraries. - RAII voor GPU-resources. Kijk naar
MeshenShader: een destructor die opruimt,= deleteop de copy-constructor/-assignment (GPU-handles mogen niet per ongeluk gekopieerd worden), en een move-constructor/ -assignment die de handle overneemt en de bron op0/leeg zet. Elk nieuw element dat zelf een VAO/VBO/shader-programma bezit volgt hetzelfde patroon.
Je hoeft CMakeLists.txt niet aan te passen voor nieuwe .cpp-bestanden:
file(GLOB ENGINE_SOURCES ${CMAKE_SOURCE_DIR}/src/*.cpp) pakt alles in
src/ automatisch mee bij de volgende keer dat je cmake .. opnieuw
draait (nodig omdat GLOB niet automatisch herconfigureert bij een build —
dus na een nieuw bestand: eenmalig opnieuw cmake .. in je build-map).
8. Namespace hernoemen
Alles zit in de generieke engine-namespace, zonder verwijzingen naar het
project waar deze code oorspronkelijk uit kwam. Wil je toch een andere naam
(bijv. de naam van je eigen project), dan is dat een kwestie van een
projectbrede find-and-replace van engine naar jouw naam in alle
.hpp/.cpp-bestanden — er zit verder geen afhankelijkheid aan die
specifieke naam. Let op: de klasse heet ook Engine (met hoofdletter) —
die hoef je niet mee te hernoemen, jouwnaam::Engine blijft prima werken.
9. Bouwen
sudo apt install cmake libglfw3-dev libglm-dev libgl1-mesa-dev libglu1-mesa-dev xorg-dev
cd engine && mkdir build && cd build
cmake .. && make -j$(nproc)
./engine_demo
Na het toevoegen van nieuwe .cpp-bestanden: draai cmake .. opnieuw in de
build-map voordat je make draait (zie sectie 7).