Files
2026-09-06 21:30:55 +02:00

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:

  1. Zet mijnshader.vert en mijnshader.frag in assets/shaders/.
  2. Shader shader; shader.loadFromFiles("assets/shaders/mijnshader.vert", "assets/shaders/mijnshader.frag");
  3. Uniforms zet je via setMat4/setVec3/setVec4/setFloat/setInt — de namen moeten exact overeenkomen met de uniform-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:

  1. Eén klasse per bestandspaar, header in include/, implementatie in src/, zelfde bestandsnaam als de klasse (Cube.hppCube.cpp). #include "Cube.hpp" bovenaan Cube.cpp als eerste include.
  2. Alles in de engine-namespace (of je eigen naam, zie hieronder), zodat er geen naamsbotsingen ontstaan met GLFW/OpenGL-symbolen of andere libraries.
  3. RAII voor GPU-resources. Kijk naar Mesh en Shader: een destructor die opruimt, = delete op de copy-constructor/-assignment (GPU-handles mogen niet per ongeluk gekopieerd worden), en een move-constructor/ -assignment die de handle overneemt en de bron op 0/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).