Files
bens-engine/GUIDE.md
T
2026-09-06 21:30:55 +02:00

407 lines
13 KiB
Markdown

# 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 <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`:
```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
```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:
```cpp
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:
```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<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()`:
```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(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:
```cpp
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`:
```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).