# 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 ```cpp #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: ```cpp 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. ```cpp 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`: ```cpp #pragma once #include "Mesh.hpp" #include "Shader.hpp" #include 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`: ```cpp #include "Cube.hpp" #include 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 ```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(w) / static_cast(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: ```cpp std::vector> 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: ```cpp 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): ```cpp Mesh Mesh::createBox(float halfExtent) { std::vector vertices; std::vector 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()`: ```cpp 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`:** ```cpp using KeyCallback = std::function; 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: ```cpp void Engine::keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { auto* engine = static_cast(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`: ```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.hpp` ↔ `Cube.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 ```bash 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).