-
Notifications
You must be signed in to change notification settings - Fork 1
Home
Welcome to the Wiki for the Zenderer 2D OpenGL framework. Here you will find details on functionality of a variety of APIs provided by the framework. Each page typically details API philosophy, addresses use-cases and "gotchas", provides technical specifications on the implementation, and gives examples of the API in action.
This page is intended to provide a brief introduction to the framework, giving examples of the high-level interfaces most are looking for when developing games, and providing a short demonstration of lower-level capabilities of the framework.
Zenderer was designed with a couple of things in mind:
- Utilization of modern OpenGL rendering techniques for maximum performance.
- Easy to use API for a variety of polygon shapes and sprites.
- Easy to use APIs for adding lighting and shader effects to scenes.
- High-level wrappers for simple GUI tools.
All of these have been achieved, in my opinion. There is some future work I want to do on the first objective, such as adding multiple VAOs (see zen::gfx::zVertexArray
) to scene objects (see zen::gfx::zScene
). This would allow for separation and optimization of geometry, such as GL_DYNAMIC_DRAW
for a buffer for firing bullets, and GL_STATIC_DRAW
for regular scene geometry.
The second objective allows for very easy primitive creation through high-level entity interfaces that require next to no knowledge of the inner workings of the framework. Here is an example of a use case: say you want to create a scene that has two paddles and a ball, like Pong. If you want various special effects, this can only be achieved through use of the zen::gfx::zScene
class. If not, then there are several other ways to approach the problem.
// Example 1.1
using namespace zen;
// Boilerplate setup code.
// Error-checking omitted for brevity.
Init();
asset::zAssetManager GameAssets;
GameAssets.Init();
gfx::zWindow Window(800, 600, "Test Window", GameAssets, false);
Window.Init();
// We may want fancy effects later.
gfx::zScene Pong(Window.GetWidth(), Window.GetHeight(), GameAssets);
Pong.Init();
// Create the empty in-game objects.
obj::zEntity& Player = Pong.AddEntity();
obj::zEntity& Opponent = Pong.AddEntity();
obj::zEntity& Ball = Pong.AddEntity();
// Create a 8x64 paddle primitive.
gfx::zQuad Paddle(GameAssets, 8, 64);
// The player will be on the left and have a blue paddle.
Paddle.SetColor(color4f_t(0.0, 0.0, 1.0));
Player.AddPrimitive(Paddle.Create());
Player.Move(0, Window.GetHeight() / 2 - Player.GetH() / 2);
// We can easily just re-use the old primitive, since it still
// contains the relevant shape data. AddPrimitive() makes a copy
// internally, and we never offloaded things to the GPU.
// The opponent will be on the right and have a red paddle.
// We don't need the primitive after this anymore, so we can
// move it directly.
Paddle.SetColor(color4f_t(1.0, 0.0, 0.0));
Opponent.AddPrimitive(std::move(Paddle.Create()));
Opponent.Move(Window.GetWidth() - Opponent.GetW(),
Window.GetHeight() / 2 - Opponent.GetH() / 2);
// The ball will be a white hexagon in the center of the screen.
gfx::zPolygon Hex(GameAssets);
for (uint8_t i = 0; i < 6; ++i)
{
// Hexagon has 6 sides with an angle of 60 degrees (pi/3).
// We can use the framework's built-in tools if we didn't know that,
// which is shown for the y-value.
math::vector_t point(5 * std::cos(i * math::PI / 3),
5 * std::sin(i * math::rad(60)));
Hex.AddVertex(point);
}
Ball.AddPrimitive(std::move(Hex.Create()));
Ball.Move(Window.GetWidth() / 2 - Ball.GetW() / 2,
Window.GetHeight() / 2 - Ball.GetH() / 2);
// Now our scene is ready to show things exactly as we specified.
// All it takes is the following line in the game loop (in addition
// to event handling and window updating, of course):
Pong.Render();
This is an efficient technique that uses the high-level scene wrapper for easy drawing. The following example doesn't use the scene, and draws primitives directly. In the third example, we will get to an even lower level, essentially implementing our own scene wrapper.
// Example 1.2
// -snip-
// Same boilerplate
// -snip-
// This is instead of the .AddEntity() calls in the above example.
// Create the empty in-game objects.
obj::zEntity Player(GameAssets),
Opponent(GameAssets),
Ball(GameAssets);
// -snip-
// Creating primitives and adding them to the entities stays the same.
// -snip-
// Now in our game loop, we do the following:
Player.Draw();
Opponent.Draw();
Ball.Draw();
Finally, we can do things without using the entity wrapper at all.
In the following example, I only use primitive objects to do rendering.
I also demonstrate how to use the low-level vertex array API, though
it's also not necessary. Merely calling Draw()
will do the necessary
work. This example requires a little more general computer graphics knowledge.
// Example 1.3
// -snip-
// Same boilerplate
// -snip-
// Create primitives.
// We use the copy constructor on the opponent to avoid
// re-typing the size information.
obj::zQuad Player(GameAssets, 8, 64),
Opponent(Player);
// Pre-create 6 vertices, so AddVertex() performs better.
obj::zPolygon Ball(GameAssets, 6);
for (uint8_t i = 0; i < 6; ++i)
{
// Hexagon has 6 sides with an angle of 60 degrees (pi/3).
// We can use the framework's built-in tools if we didn't know that,
// which is shown for the y-value.
math::vector_t point(5 * std::cos(i * math::PI / 3),
5 * std::sin(i * math::rad(60)));
Ball.AddVertex(point);
}
// Create a vertex object to hold our primitive data.
gfxcore::zVertexArray Pong;
Pong.Init();
Player.Create();
Opponent.Create();
Ball.Create();
Player.LoadIntoVAO(Pong, gfx::VAOState::NO_PRESERVE_DATA);
Opponent.LoadIntoVAO(Pong, gfx::VAOState::NO_PRESERVE_DATA);
Ball.LoadIntoVAO(Pong, gfx::VAOState::NO_PRESERVE_DATA);
// Now our vertex array contains all data necessary necessary
// for drawing our scene.
// But we also need a shader in order to display things on the
// screen properly. We can use the high-level gfx::zEffect
// interface for this (as opposed to hand-loading files via
// gfxcore::zShaderSet, though that's also valid).
gfx::zEffect Shader(gfx::EffectType::DEFAULT_EFFECT, GameAssets);
Shader.Init();
// We need to set up matrices for proper rendering.
Shader.Enable();
Shader.SetParameter("mv", math::matrix4x4_t::GetIdentityMatrix());
Shader.SetParameter("proj", Window.GetProjectionMatrix());
Shader.Disable();
// Now, in our game loop, rendering looks like this:
math::matrix4x4_t position;
Pong.Bind();
Shader.Enable();
position.Translate(Player.GetPosition());
Shader.SetParameter("mv", position);
Player.Draw(gfx::RenderState::READY);
position.Translate(Opponent.GetPosition());
Shader.SetParameter("mv", position);
Opponent.Draw(gfx::RenderState::READY);
position.Translate(Ball.GetPosition());
Shader.SetParameter("mv", position);
Ball.Draw(gfx::RenderState::READY);
Shader.Disable();
Pong.Unbind();
There we have it. Three examples of performing the same general function, advancing to a lower level interface every time. The lower you go, the less is available, such as easy movement of objects, special effects, and proper texturing.
Zenderer makes it extremely easy to add nice effects to scenes. This section demonstrates adding lighting effects to scenes, and then a simple Gaussian blur for post-processing.
Let's say that we wanted to add a point light to our game, one that follows the ball as it moves. Zenderer aims to make this process incredibly easy for the user, provided that they use the gfx::zScene
API. Assuming we have the same setup as in Example 1.1 above, this is all we need to do:
// -snip-
// Everything prior to the game loop.
// -snip-
// Before the game loop starts.
Pong.EnableLighting();
// Let's add a couple of lights.
gfx::zLight& Amb = Pong.AddLight(gfx::LightType::ZEN_AMBIENT);
gfx::zLight& Light = Pong.AddLight(gfx::LightType::ZEN_POINT);
// Set various parameters. There are more but the
// defaults will do for our case.
Amb.Enable();
Amb.SetColor(1.0, 1.0, 1.0);
Amb.SetBrightness(0.5);
Light.Enable();
Light.SetColor(1.0, 1.0, 1.0);
Light.SetPosition(Ball.GetPosition());
Light.SetBrightness(2.0);
Light.Disable();
// -snip-
// Within our game loop all we need is this to track the ball:
Light.Enable();
Light.SetPosition(Ball.GetPosition());
Light.Disable();
// -snip-
// Finally, because we have no background geometry, and we want
// lighting to actually have an effect on the scene, rather than
// being consumed by darkness, we set a custom scene clearing
// color (a near-black-but-not-quite-full-black) in order to
// achieve what we want.
// Instead of Pong.Render(), we call:
Pong.Render(color4f_t(0.1, 0.1, 0.1));
// -snip-
The resulting effect is something like this:
It's doesn't look that great, but tweaking the attenuation settings can improve visual appeal. Regardless, you can see that adding lighting effects is incredibly easy, and can add a pleasant effect to any game.
Let's get rid of those lights and add a Gaussian blur filter to our scene. The Gaussian blur effect in Zenderer is split into a horizontal and vertical pass, because it is faster and simpler to make two passes than to combine them into a single shader.
Adding an effect to a scene is absolutely trivial. Let's assume we have our pong scene as in Example 1.1. The following needs to be added before the main game loop:
Pong.EnablePostProcessing();
gfx::zEffect& HBlur = Pong.AddEffect(gfx::EffectType::GAUSSIAN_BLUR_H);
gfx::zEffect& VBlur = Pong.AddEffect(gfx::EffectType::GAUSSIAN_BLUR_V);
real_t value[] = { 1 / 512.0 };
HBlur.Enable();
HBlur.SetParameter("radius", value);
VBlur.Enable();
VBlur.SetParameter("radius", value);
VBlur.Disable();
And that's it! The rest is taken care of by gfx::zScene
. Keep in mind that performance and frame-rate is inversely proportional to the amount of post-processing and lighting you do. The resulting rendering looks like this:
The third objective is to create an easy API for user-interface elements, such as menus and buttons. Here we create a simple menu for our barebones Pong game from above. I will show basic menu creation, then show a lower-level adding a "Pause" button to the in-game Pong scene.
// Example 2.1
// -snip-
// Identical boilerplate to the above examples.
// -snip-
// Create the menu.
gui::zMenu MainMenu(Window, GameAssets);
MainMenu.Init();
// Do initial setup. We want our buttons to appear on the
// left-hand side of the screen, as white text, high-lighted
// with red when the user mouses over them.
// Assume that this font path exists.
MainMenu.SetFont("Assets/Fonts/MenuFont.ttf", 24);
MainMenu.SetInitialButtonPosition(math::vector_t(32, Window.GetHeight() / 3));
MainMenu.SetNormalButtonTextColor(color4f_t(1.0, 1.0, 1.0));
MainMenu.SetActiveButtonTextColor(color4f_t(1.0, 0.0, 0.0));
// Assume that these are our triggers.
bool play = false, quit = false;
// We only have two simple options: "Play" and "Quit".
// Here you can see that we attach callbacks to the menu
// functions so that they can trigger events in our game loop.
MainMenu.AddButton("Play", [&play](size_t){ play = true; });
MainMenu.AddButton("Play", [&quit](size_t){ quit = true; });
// We're ready to wait for user interaction with the menu.
// This is a minimalistic game loop that doesn't use the zTimer
// interface to regulate frame rate.
//
// We wait until a trigger is hit like so:
while (!play && !quit)
{
// Poll events and pass all to the menu.
evt::event_t Evt;
evt::zEventHandler::PollEvents();
while(evt::zEventHandler::GetInstance().PopEvent(Evt))
MainMenu.HandleEvent(Evt);
Window.Clear();
MainMenu.Update();
Window.Update();
}
// Once this loop exits, we know the user has done something,
// and can proceed with our game loop.
Now that we have that established, let's take a closer look at the gui::zButton
interface, which we can use to add "buttons" in-game. We begin with the assumption that we've created a scene as we did for Pong above, but haven't entered the game loop yet. We will simply set a pause=true
flag when the button is hit. This example will also demonstrate basic font loading (which is somewhat convoluted), basic event handling, frame rate regulation via util::zTimer
, and mouse state polling.
// Example 2.2
// -snip-
// Assume everything from Example 1.1 has occurred, except for the game loop.
// -snip-
// Load a font.
gui::zFont& ButtonFont = GameAssets.Create<gui::zFont>("Assets/Fonts/MenuFont.ttf");
// Create a button and attach it directly to the game scene.
gui::zButton PauseButton(Pong);
PauseButton.SetFont(ButtonFont);
PauseButton.SetNormalColor(color4f_t(0.0, 0.0, 0.0));
PauseButton.SetActiveColor(color4f_t(1.0, 0.0, 0.0));
PauseButton.Prepare("Pause Game");
// We want it in the top-right corner of the game screen.
// We approximate the width of the button via the rendered text,
// then add a small padding just to be safe.
PauseButton.Place(Window.GetWidth() - ButtonFont.GetTextWidth("Pause Game") - 16, 0);
// Our pause flag.
bool pause = false;
// Frame rate regulator.
util::zTimer Frames(60);
// Within our game loop, we can do the following to catch button events.
// Poll events and pass all to the menu.
while (Window.IsOpen())
{
Frames.Start();
// Poll events and handle mouse clicks on the button if needed.
evt::event_t Evt;
evt::zEventHandler::PollEvents();
while(evt::zEventHandler::GetInstance().PopEvent(Evt))
{
if (Evt.type == evt::EventType::WINDOW_CLOSE)
Window.Close();
else if (Evt.type == evt::EventType::MOUSE_DOWN &&
Evt.mouse.button == evt::MouseButton::LEFT &&
PauseButton.IsOver(Evt.mouse.position))
pause = true;
}
// Show rollover effect on the button.
if (PauseButton.IsOver(evt::GetMousePosition()))
PauseButton.SetActive();
else
PauseButton.SetDefault();
// Remainder of game loop, maybe doing something with the pause flag.
// -snip-
// Regulate frame rate.
Frames.Delay();
}
Quit();
We get something that looks like this:
This was a summation of the functionality built into the Zenderer game framework. I aim to offer high-level interfaces for a variety of tasks that occur throughout the development of a game, including GUI work, special effects, and generic creation of sprites, primitives, and textures.
The following list directs you to pages describing various tools in the framework, complete with architectural explanations, technical details, and use-case examples. The list is in descending order in terms of low->high level APIs.