add guide
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user