matf-rg-engine 1.0.0
Base for project for the Computer Graphics course at Faculty of Mathematics, University of Belgrade
|
matf-rg-project
is the base project for the Computer Graphics course at the Faculty of Mathematics, University of Belgrade for the school year of 2024/2025.
To setup the necessary libraries, run:
./setup.sh
To generate docs, run:
doxygen Doxyfile
Open the documentation file in your browser: docs/html/index.html
engine_setup
- here, the engine controllers are setupapp_setup
- the function that the user of the App
overrides and implements a custom setup for the Appinitialize
- App
should gather whatever Resources
it needs and initialize its state.loop
- App
can check whether it should continue running. If the loop
method returns false
, the Main loop
stops, and the App
terminates.poll_events
- App
collects information about the events that happened at the Platform
and collects user input for the upcoming frame.update
- App
updates the world state, processes physics, events, and world logic, and reacts to the user inputs.draw
- App
uses OpenGL
and draws the current state of the world.terminate
- App
terminates its stateon_exit
- do a final cleanup, and return an exit codeHere is the interface of the engine/core/App.hpp
class:
For a basic app setup, you need to:
MyApp
, in the app/src/
.engine::core::App
and implement app_setup()
.MyApp
object in the main
function and call run
on it.Controllers
are a way to hook into the engine execution. To create a custom controller:
engine::core::Controller
initialize
, loop
, poll_events
, update
, begin_draw
, draw
, end_draw
, terminate
) for which you want to execute custom code.MainApp::user_setup
.Here is the example of creating the MainController
that enables depth testing
.
Resources currently include: textures
, shaders
, models
, skyboxes
. The ResourcesController
manages the loading, storing, and accessing the resource objects. During the App::initialize
, the ResourcesController
will load all the resources in the resources
directory.
For every type of resource, the ResourcesController
has a corresponding function that retrieves it:
Model* ResourcesController::model("backpack")
Shader* ResourcesController::shader("basic")
Texture* ResourcesController::texture("awesomeface")
Skybox* ResourcesController::skybox("skybox")
The argument is always the resource name without the file extension. For textures and shaders, the name is just the name of the file without the extension. For models and skyboxes, it's the name of the directory, because they have multiple files associated with them.
The pointer to the resource
that the ResourcesController
returns is a non-owning pointer, meaning you should never call delete on it. All the memory is managed internally by the ResourcesController.
The resources/models/
directory stores all the models. Let's add a backpack model from the course.
resources/models/backpack.
backpack
model files into the resources/models/backpack
.ResourcesController
will automatically load this model during ResourcesController::initialize()
; you should see a log:awesomeface.png
to the resources/textures
directoryResourcesController
will automatically load it)your_shader.glsl
in the resources/shaders/your_shader.glsl
.vertex
, fragment
, and geometry
(optional), shaders in the same file.Vertex, fragment, and geometry shaders are written in the same file. Use the // #shader vertex|fragment|geometry
to declare the start of the shader. Here is an example:
ResourcesController
will load and compile all the shaders in the resources/shaders
directory.
Engine
uses the imgui library to draw a GUI. See the library page for more examples. For GUI to be visible it should be drawn last, after all the world objects. Here is an example of displaying camera info in a GUI.
The Engine
defines a base Error
type with two subclasses, EngineError
and UserError
. They serve as a graceful way to terminate the application and provide the user with some helpful information on how to fix the error.
For example, the ResourcesController
will throw AssetLoadingError
if it can't read the asset file.
Exceptions
shouldn't be used as a control-flow mechanism, instead they should be used to inform the user of an exceptional
event that the program can't do anything about, like the missing asset file.
The PlatformController
differentiates between four types of button/key states:
engine::platform::Key::State::JustPressed
-> Only in the first frame when the button was registered as pressedengine::platform::Key::State::Pressed
-> Every subsequent frame if the button is still heldengine::platform::Key::State::JustReleased
-> Just the first frame in which the button was registered as releasedengine::platform::Key::State::Released
-> Button is not pressedUse the PlatformController::key
method to get the state of the key in a given frame:
Here is an example of turning GUI on/off drawing using the F2
key:
Keys have a unique identifier: via engine::platform::KeyId
.
engine::platform::PlatformEventObserver
, and override methods you'd like to have a custom operation executed once the event happensobserver
instance in the PlatformController
.Now, for every keyboard event, the PlatformController
will call MainPlatformEventObserver::on_keyboard
and pass the key
on which the event occurred as an argument.
PlatformController
initializes and stores the Window
handle, which you can access via:
Also, the PlatformController
will update the window properties if the size of the window changes.
Rendering actions that require more than one OpenGL call should be abstracted in the engine::graphics::OpenGL
class. For example, OpenGL::compile_shaer
compiles a GLSL shader and returns the shader_id.
If the OpenGL call fails, the CHECKED_GL_CALL
macro throws an OpenGLError
in DEBUG mode. The engine will print the error description of and the source location in which it occurred.
Why this way? It's less error-prone and more straightforward to add debugging assertions and error checks if needed.
You can configure some parts of the engine
in the config.json
. For example, we can configure the size and the title of the window when the application starts.
You can also add your custom configuration options:
and use them:
The Configuration
class uses nlohmann/json library to load and store json
files, see the library page for documentation.
You use the ArgParser
to parse the command line arguments anywhere from the program. For example, for the invocation command: ./matf-rg-engine ... --fps 120
, the parser->arg("--fps")
will return the value 60. If the argument is not present, it will return the default value passed as the second argument to the arg
method.
Here you can find a walkthrough tutorial for recreating the engine/test/app
that demonstrates how to use different engine systems.