A simple OpenGL renderer, aimed to get any game project up and running with simple but satisfying graphics.
I'm not a graphics programmer and, although I do find it fun when I get pretty things to appear on-screen, I much prefer metaprogramming and engine architecture. However, whenever I've wanted to get some proof-of-concept graphics, I've always been frustrated to see there's no 3D equivalent to SFML: a simple 3D graphics library, letting me load meshes and draw them without having to write my own shaders.
Eventually I came to accept this, and started implementing a custom OpenGL system for the kengine. This was a learning process for me, as it was my first time toying with graphics programming.
Now that I'm relatively satisfied with the final result (although it's far from perfect), I figured it was time to move the functionality out of the kengine
and have it become what I was initially looking for, so that future engine programmers don't have to write their own renderer from scratch.
Take a look at the example code. There are two examples:
- a simple one which shows how to quickly draw and animate a model
- a more complex one, which makes use of most available features but may be a bit harder to follow
The examples can be built by setting the KREOGL_EXAMPLE
CMake option.
Below is a snippet of the important parts of the simple example:
kreogl::window window; // create a window
window.get_default_camera().set_position({ 0.f, 0.f, -5.f }); // move the camera back to see the centered scene
kreogl::world world; // the world that will be used to draw into the window
const kreogl::skybox_texture skybox_texture{ // load the skybox
"resources/skybox/left.jpg",
"resources/skybox/right.jpg",
"resources/skybox/top.jpg",
"resources/skybox/bottom.jpg",
"resources/skybox/front.jpg",
"resources/skybox/back.jpg",
};
world.skybox.texture = &skybox_texture; // add it to the world
kreogl::directional_light light; // create a light
world.add(light); // add it to the world
light.direction = { 0.f, -1.f, -1.f };
light.cast_shadows = false; // disable shadows for our scene
const auto model = kreogl::assimp::load_animated_model("resources/funnyman/funnyman.fbx"); // load a 3d model
assert(model && model->animations.size() == 1);
kreogl::animated_object object; // create an object
object.model = model.get(); // base it on the loaded 3d model
object.transform = glm::translate(glm::mat4{1.f}, glm::vec3{ 0.f, -2.5f, 5.f }); // move it forward and down a bit
object.transform = glm::rotate(object.transform, glm::pi<float>(), glm::vec3{ 0.f, 1.f, 0.f }); // rotate it to face the camera
object.animation = kreogl::animation{ // play an animation
.model = model->animations->animations[0].get(), // use the animation that was baked into the 3d model
.loop = true
};
world.add(object); // add the object to the world
// main loop
auto previous_time = std::chrono::system_clock::now();
while (!window.should_close()) {
const auto now = std::chrono::system_clock::now();
const auto delta_time = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - previous_time).count()) / 1000.f;
previous_time = now;
object.tick_animation(delta_time); // play the object's animation
window.poll_events(); // process input
window.draw(world); // draw the world into the window
window.display(); // present the new window contents
}
And here's a screenshot of the result:
These are objects that will typically live for as long as the application is running.
These provide functions to load models from files.
The rest of these types are less user-facing, and understanding them isn't required for basic animation code.
This describe the internal implementation of the rendering engine. You don't need to be aware of these to make use of kreogl
, but if you wish to improve/extend/understand its behavior, this is a good starting point.
Each camera has an associated viewport, which represents the on-screen area used to display the camera.
Each viewport has an underlying G-buffer, which contains the intermediate rendering data generated when drawing the camera content. It can be used to query the position, color, or custom user data that was drawn in a specific pixel.
RAII wrappers to OpenGL resources
Shaders are instances of shader, grouped into a shader_pipeline, which can be passed to window::draw
. The default pipeline contains all the pre-implemented shaders:
These are shaders in charge of filling the gbuffer.
These are shaders in charge of applying lighting to what was previously written into the gbuffer, and writing the result to the main framebuffer.
These are shaders that run after the lighting pass, and can render effects to alter the lighting of the scene.
These are shaders that run after the post-lighting pass, and can add the "final touches" to the scene.
These are shaders that implement the shadow_map_shader or shadow_cube_shader interfaces, and are called by the lighting shaders to fill lights' ShadowMaps.
- position_shadow_map_shader
- position_shadow_cube_shader
- skeletal_shadow_map_shader
- skeletal_shadow_cube_shader
The code is instrumented using Tracy. Profiling can be enabled by setting the KREOGL_PROFILING
CMake option.