Guides - jopo86/onyx GitHub Wiki

Onyx - Guides

Boring old documentation for every function can be found in the code headers themselves. Here, though, you will find useful guides covering the use of the library and it's main classes. However, if you are new to the library, I strongly recommend checking out the Basic Tutorial and Advanced Tutorial first.

Guides:

Core

Important but self-explanatory core functions include:

// all functions listed here are in the Onyx namespace
void Init();
void Init(ErrorHandler&); // uses specified error handler for the lifetime of the library, see Debugging Guide for more info
void Terminate();

Window Guide

Windows are represented by an object of the Window class. The library must be initialized before creating a window.
Window Properties | How to Use | Useful Functions | Window Icons

Window Properties

Windows are created with the WindowProperties struct, containing many useful options, most of which can be changed at any time. The properties are as follows:

  • title: The title of the window.
  • width: The width of the window.
  • height: The height of the window.
  • position: The position of the window in screen coordinates, where (0, 0) is the top-left.
  • resizable: Whether the window can be resized by the user.
  • visible: Whether the window is visible or hidden (hidden is different than minimized, it's completely hidden).
  • focused: Whether the window is focused by the user.
  • fullscreen: Whether the window is fullscreen.
  • decorated: Whether the window is decorated, meaning there is a border & widgets.
  • topmost: Whether the window is topmost, AKA always-on-top / floating.
  • focusOnShow: Whether the window becomes focused by the user when it is shown.
  • nSamplesMSAA: The number of samples for Multi Sample Anti Aliasing.
  • opacity: The opacity of the window & its decorations.
  • backgroundColor: The background color of the window. This is the color it's cleared to at the beginning of a frame.

How to Use

Once a window is created using the constructor with a WindowProperties struct as its argument, it must be initialized using init() to initialize OpenGL (if another window has not already initialized it). Once the window is initialized, it can be displayed and used to render to. To create a mainloop for rendering to the window, loop while isOpen() is true. In the loop, call startRender(), render, and then call endRender(). Dispose of the window before the library is terminated.

Onyx::Window window(Onyx::WindowProperties{/*...*/});  // need to have C++17 or higher to initialize WindowProperties struct anonymously like this
window.init();

// ...

while (window.isOpen())
{
    // stuff that needs to be done every frame - input handling, object transformations, etc...
    window.startRender();
    // render...
    window.endRender();
}

// ...

window.dispose();

Useful Functions

void maximize();
void minimize();
void restore(); // restores window from maximized/minimized state
void requestAttention(); // requests attention from the user. Use to notify user of an event without interrupting.
// getters & setters for properties
int getFrame(); // returns the current frame #
int getFPS(); // returns the current frames per second
double getDeltaTime(); // returns the time since the last frame (in seconds)
void fullscreen(); // sets to fullscreen mode
void windowed(); // sets to windowed mode
// most settings where it's either on/off also have a toggle function

Window Icons

Window icons are created with the WindowIcon class. Create a window icon with its static Load function, which takes an initializer list of file paths (strings). Multiple filepaths allows for different resolutions, because the automatic scaling sucks. The most appropriate size will be used when possible. The icon of a window can simply be set with setIcon. The icon can be dispose after it is set to the window's icon.

// window creation...

Onyx::WindowIcon icon = Onyx::WindowIcon::Load({
    Onyx::Resources("icons/icon-16x.png"), // example paths
    Onyx::Resources("icons/icon-24x.png"),
    Onyx::Resources("icons/icon-32x.png"),
    Onyx::Resources("icons/icon-48x.png"),
    Onyx::Resources("icons/icon-256x.png"),
});

window.setIcon(icon);
icon.dispose();

Input Guide

Input handling is done with an InputHandler object. After creating an object, it must be linked to a window with Window::linkInputHandler(). Every frame, update() must be called on the input handler. This updates deltas and key cooldowns.
Keyboard Input | Mouse Input | Cooldowns | Controller Input

Keyboard Input

Keys are represented by the Key enum. All keys have a current state, represented by the KeyState enum. States may be any of the following:

KeyState::Untouched // all keys & mouse buttons start with this
KeyState::Release
KeyState::Press // whether the key/button is held down, not long enough to be considered repeated
KeyState::Repeat // whether the key/button is held down long enough to be considered repeated

You can query the state of a key with any of the following InputHandler member functions:

KeyState getKeyState(Key);
bool isKeyPressed(Key);
bool isKeyRepeated(Key);
bool isKeyDown(Key); // pressed OR repeated

Mouse Input

Mouse buttons are represented by the MouseButton enum. They have KeyStates just like keys, and their states can be queried in pretty much the exact same way:

KeyState getMouseButtonState(MouseButton);
bool isMouseButtonPressed(MouseButton);
bool isMouseButtonRepeated(MouseButton);
bool isMouseButtonDown(MouseButton); // pressed OR repeated

The position of the cursor in screen coordinates, where (0, 0) is the bottom-left of the window, can be accessed with getMousePos(), returning a 2-component double vector of coordinates. The change in mouse position since the last update can be accessed with getMouseDeltas(), and the change in scrolling since the last update can be accessed with getScrollDeltas(), both also returning a DVec2.

Cooldowns

Cooldown durations can be put in place for any key or mouse button. To add a cooldown, use setKeyCooldown(Key, float) and setMouseButtonCooldown(MouseButton, float), the second argument being the cooldown duration in seconds. After a cooldown is set, it will be in place permanently until otherwise set. The key/mb's state will remain accurate, regardless of cooldowns, but state-querying functions that return a boolean will factor in the cooldown, and they will return true if the key/mb is down and the cooldown has run out, but if the cooldown has not run out they will return false.

Controller/Gamepad Input

Added as of v1.1, getting input events from a controller is a little different than a keyboard or mouse. When Onyx is initialized, it identifies up to 16 gamepads based on the controllers connected to the system, stored as a vector of Gamepad objects. This vector can be accessed with InputHandler::getGamepads(), returning a const reference to the vector.

The Gamepad class contains the following relevant info: a name, an array of button states, and an array of axis states. The name, not likely to be unique, can be retrieved with Gamepad::getName(). The states of buttons and axes are not the same as that of keys/mouse buttons, so they do not have their own getButtonState or getAxisState functions, the gamepad instead has isButtonPressed, which returns true or false, and getAxis, which returns a float ranging from 0-1 (explained below).

The arguments of isButtonPressed and getAxis are an Onyx::GamepadButton and Onyx::GamepadAxis, respectively. Gamepad buttons include: A, B, X, Y, Cross, Circle, Square, Triangle, LeftBumper, RightBumper, Back, Start, Guide, LeftStick, RightStick, DpadUp, DpadRight, DpadDown, and DpadLeft. Cross, Circle, Square, and Triangle are identical to A, B, X, and Y, respectively. Gamepad axes include: LeftX, LeftY, RightX, RightY, LeftTrigger, and RightTrigger.

Here is an example of how gamepad input may be processed (this is how it is processed in the demo):

// before mainloop
const double MOVE_SPEED = 6.0; //       choose whatever you want here, I like these values
const double MOUSE_SENS = 30.0; //      ^
//                                      ^
const float DEAD_ZONE_LEFT = 0.1f; //   ^
const float DEAD_ZONE_RIGHT = 0.1f; //  ^

// in mainloop
float lsx = 0.0f, lsy = 0.0f, rsx = 0.0f, rsy = 0.0f;
bool a = false, b = false, x = false, y = false, rs = false;

for (const Gamepad& gp : input.getGamepads())
{
    if (abs(gp.getAxis(GamepadAxis::LeftX)) > lsx) lsx = gp.getAxis(GamepadAxis::LeftX); // choose the greatest influence for each axis between all the controllers
    if (abs(gp.getAxis(GamepadAxis::LeftY)) > lsy) lsy = gp.getAxis(GamepadAxis::LeftY); //   ^
    if (abs(gp.getAxis(GamepadAxis::RightX)) > rsx) rsx = gp.getAxis(GamepadAxis::RightX); // ^ 
    if (abs(gp.getAxis(GamepadAxis::RightY)) > rsy) rsy = gp.getAxis(GamepadAxis::RightY); // ^
    if (gp.isButtonPressed(GamepadButton::A)) a = true;
    if (gp.isButtonPressed(GamepadButton::B)) b = true;
    if (gp.isButtonPressed(GamepadButton::X)) x = true;
    if (gp.isButtonPressed(GamepadButton::Y)) y = true;
    if (gp.isButtonPressed(GamepadButton::RightStick)) rs = true;
}

lsx = abs(lsx) < DEAD_ZONE_LEFT ? 0.0f : lsx;
lsy = abs(lsy) < DEAD_ZONE_LEFT ? 0.0f : lsy;
rsx = abs(rsx) < DEAD_ZONE_RIGHT ? 0.0f : rsx;
rsy = abs(rsy) < DEAD_ZONE_RIGHT ? 0.0f : rsy;

// 'cam' is the camera, 'dt' is delta time
cam.translateFB(MOVE_SPEED * dt * lsy);
cam.translateLR(MOVE_SPEED * dt * lsx);
if (a) cam.translateUD(MOVE_SPEED * dt);
if (b || rs) cam.translateUD(-MOVE_SPEED * dt);
cam.rotate(MOUSE_SENS * 10.0f * dt * rsx, MOUSE_SENS * 10.0f * dt * rsy); // the 10.0f is just an arbitrary value that works well

Gamepads also have the isConnected() function. If a gamepad created on initialization is disconnected (and/or reconnected) while the program is running, this value will be updated. Disconnected gamepads are not removed from the vector to preserve indices. If a gamepad that was not identified on initialization is connected, a the input handler automatically creates a new Gamepad object representing said gamepad and adds it to the end of the vector.

Rendering Guide

Rendering is achieved through the use of Renderable classes, a Renderer, and a Camera (not technically necessary). You can render normal 3D objects with the Renderable class, models with the ModelRenderable class, UI elements with the UiRenderable class, and text with the TextRenderable class.

Camera | Renderer/Lighting/Fog | Renderables | UI Renderables | Text Renderables | Model Loading & Rendering

Camera

After initializing the library and creating a window, we can begin to set up the renderer. The renderer, ideally, uses a Camera and Lighting. Creating a camera involves specifying the Projection to use, which is either orthographic or perspective. Orthographic means objects do not get smaller as they get further away, and perspective means they do. So, you would basically just use orthographic for 2D games and perspective for 3D ones, unless you choose to do otherwise for some reason. Perspective involves specifying a field of view and screen dimensions (which will be updated when the window is resized, as long as the camera is linked to the window), while orthographic involves screen dimensions (also updated accordingly). The Projection constructor is not used, it instead uses the static Perspective and Orthographic functions.

If perspective projection is used, coordinates will be sort of arbitrary units which you can choose what to do with. It's all relative anyways, so you could make 1 unit = 1 meter in your game, or you could make 100 units = 1 meter and make all the objects large and the movement speed fast. If orthographic projection is used, however, XY coordinates refer directly to pixels on your screen, with the Z coordinate still being arbitrary - but the Z coordinate will have no effect since objects will not change size if further away. (Unless you rotate the camera, then the Z coordinate can be seen but it will just look strange since it's orthographic.)

You can also specify a pitch limit for the camera (the max degrees you can look up or down), but it is set to 88 degrees by default. Like the input handler, it needs to be linked to a window so the dimensions of the projection can be updated.
Sample code for creating a camera:

Onyx::Camera cam(Onyx::Projection::Perspective(60.0f, 1280, 720), 89.0f);
window.linkCamera(cam);

The camera needs to be updated every frame with update() to apply transformations. Speaking of transformations, here is how you may transform the camera:

void translateLR(float dist); // translates left(-)/right(+) by dist, IS NOT the same as moving along the global x-axis.
void translateUD(float dist); // translates up(+)/down(-) by dist, IS the same as moving along the global y-axis.
void translateFB(float dist); // translates forward(+)/backward(-) by dist, IS NOT the same as moving along the global z-axis.
void translate(Vec3 LR_UD_FB); // combines translateLR, UD, and FB into one function
void translateGlobal(Vec3 xyz); // translates GLOBALLY by xyz, IS the same as moving along global axes

void rotate(float yaw, float pitch); // rotates by specified yaw (horizontal) and pitch (vertical) degrees
void rotate(float yaw, float pitch, Vec3 origin); // rotates around an origin, CURRENTLY BUGGED - SEE ISSUES
// and there are separate pitch() and yaw() functions

void lookAt(Vec3 target); // automatically rotates the camera so it is looking at the specified point

Renderer/Lighting/Fog

Before creating the renderer, we can create the (optional) Lighting and Fog (as of v1.1) objects and assign them to the renderer. Onyx supports basic ambient and directional lighting, and linear fog.

The Lighting constructor takes a color, ambient strength, and direction. The color of the lighting is multiplied by the color of the object it hits, which is important to keep in mind because if any RGB channel of the lighting's color is zero then that channel will never get shown on any object. So, for colored light, make the channels you don't want just a little less prominent, not zero or anything close to it. Ambient strength is the brightness of an object where no light is hitting it, on a scale of 0-1. It can be whatever you want; I like keeping it around 0.3-0.4. The direction is just a vector specifying the direction the light is going, it's a little mathy to think about so I recommend just using this: (-0.2, -1, -0.3).

The Fog constructor is much simpler, it takes a color, start distance, and end distance. Fog will begin to take effect when the distance between a fragment of an object and the camera is greater then the start distance. Between the start and end distances, the color of the object is mixed with the fog color linearly. At and past the end distance, the object's color is completely the fog color. For most purposes you would want to use the window's background color as the fog color.

Once lighting and fog are created, a renderer can be created using a reference to the camera, the lighting, and the fog. The renderer also needs to be linked to the window, since it has its own copy of an orthographic projection to render UI, which needs to be updated when the window is resized, just like the camera.

// camera creation...
Onyx::Lighting lighting(Vec3::White(), 0.3f, Vec3(-0.2f, -1.0f, -0.3f));
Onyx::Fog fog(window.getBackgroundColor(), 10.0f, 20.0f); // start and end distances are just preference obviously
Onyx::Renderer renderer(cam, lighting, fog);
window.linkRenderer(renderer);

To actually render something, we add different types of renderables to the renderer, and call renderer.render() in between window.startRender() and window.endRender(). Also, remember to dispose of the renderer when you're done with it!

Renderables

To render normal 3D objects, the Renderable class is used. To create a custom renderable, you may give its constructor a Mesh, Shader, and/or Texture. For in-depth information on custom renderables, see the Advanced Tutorial. Alternatively, you can use one of the many renderable presets, implemented as static functions of the Renderable class.

Rendering a triangle:

// set up renderer...
Onyx::Renderable triangle = Onyx::Renderable::ColoredTriangle(1.0f, Vec3::Red());
renderer.add(triangle);

// ... in mainloop
window.startRender();
renderer.render();
window.endRender();

// ... before program exits
renderer.dispose();

UI Renderables

UI Renderables are simply renderables that ignore the camera, and are just rendered orthographically. There are no presets (yet) for the UiRenderable class, you have to specify a Mesh for it and then either a color or texture. UI Renderables, as well as all other types of renderables, are added to the renderer with its overloaded add() function. See the Advanced Tutorial for in-depth information on UI rendering.

Text Renderables

TextRenderables are UI elements specifically meant to render text. However, they also depend on a Font object. Text rendering is covered in-depth in the Basic Tutorial.

Model Loading & Rendering

You may have guessed it, models are rendered using the ModelRenderable class. However, this requires loading a Model with its static LoadOBJ() function - only the OBJ format is currently supported. Once again, model loading & rendering is covered in-depth in the Advanced Tutorial.

Debugging Guide

The easiest way to catch errors is with the ErrorHandler class, used before Onyx is initialized. Onyx can catch most errors that occur in the backends, as well as some errors in the way a user is using the library.
Errors | Warnings | Error Handler | Result Argument | Custom Error Handling

Errors

Errors are represented by the Error struct. They consist of a source function, the exact function the error came from; a message, providing specific information about the error; and (most of the time) information on how to fix the error. The error handler, by default, logs all this information in a formatted manner.

Warnings

Warnings are represented by the Warning struct. They consist of everything an error does, as well as a severity level - represented by the Warning::Severity enum class, which can either be Low, Med, or High. The error handler also logs all this information, including the severity, in a formatted manner.

Error Handler

Error handlers should be created before the library is initialized, and then passed to the initialization function. When creating an error handler, we can specify whether to log warnings, whether to log errors, and the minimum warning severity to log. After creating it, we initialize the library and pass it as an argument. It is recommended, in the development stage, that you simply log all warnings and errors.

Sample error handler creation:

Onyx::ErrorHandler errorHandler(true, true, Warning::Severity::Med); // log all errors, log only warnings of medium or higher severity
Onyx::Init(errorHandler);

Result Argument

Most functions that may cause an error have an additional bool* result argument at the end, set to nullptr by default. If this argument is provided, the given boolean will be set to true if the function was successful, and false if it was not. This is just an additional way of checking errors, and it is useful for actually responding to errors in your code.

Custom Error Handling

If you want to handle errors/warnings in your own way, you can do so with error & warning callbacks. These callbacks are defined as follows:
Error Callback: void (const Onyx::Error&)
Warning Callback: void (const Onyx::Warning&)
So they are simply void functions that receive an error/warning.

You can set these callbacks with ErrorHandler::setWarningCallback() and setErrorCallback(), simply pass the name of your callback function as the argument. In the functions, you can access the values of the error/warning structs directly, namely: sourceFunction, message, howToFix, all strings, and then (for warnings only) severity, which is of enum class Warning::Severity.

A quick note about callbacks - the callback functions have to be static, so they can't be nonstatic members of a class, which is not desirable if you are using a class to represent your game. Onyx provides the user pointer system to mitigate this - in your game class, you can call Onyx::SetUserPtr(this), and then in any static callback function, you can access your game class with Onyx::GetUserPtr(), casting it to a pointer of your game class.

Custom Shaders Guide

Shaders are represented by the Shader object, and are used by the Renderable class to transform and color objects. This section is for those who know how to create shaders using GLSL, the GL Shading Language. There are several ways to create shaders, and we will go over them all in this section, as well as more useful shader info.
Source Code (separate) | Source Code (combined) | Binary (precompiled) | Uniform Variables

Source Code (separate)

One way to create shaders is by writing the separate vertex and fragment shader GLSL code. If the shader source code is hardcoded as a string literal, you can use the Shader constructor, taking the vertex and fragment source code as its two arguments (as well as the error-checking result argument), to compile a shader out of it. If the code is stored in files, you can take a shortcut by using the shader's static LoadSource function, taking the vertex and fragment file paths as its two arguments, again with an additional result arg.

Source Code (combined)

This shader creation method is very similar to the previous one, just with a shortcut. You can write the vertex and fragment source code in the same file, separating them with a #switch statement (not a GLSL keyword, just unique to Onyx's parser), and then load the shader using the shader's static LoadSource function, only taking the combined shader path and a result arg.

Binary (precompiled)

After a shader has been compiled from source code, you can save it as a precompiled binary file with the shader's saveBinary function, taking a directory string, filename string, and a result arg. It doesn't matter whether there is a slash at the end of the directory, and any extension at the end of the filename will be ignored and replaced with .bin. Once the binary file has been saved, a shader can be created from it with the shader's static LoadBinary function, taking the filepath and a result arg. Please note - binary shaders saved by Onyx will not be compatible with other applications that may load shader binaries, because Onyx stores the binary format at the start of the file which is ignored by its parser but will not be ignored by third-party parsers.

Uniform Variables

To set uniform variables, you have to first use the shader with its use() function, and then you can set any type of uniform variable with all of the shader's functions beginning with set. The functions to set vectors and matrices use Onyx's Vec and Mat classes. If you are using GLM, all Vec and Mat classes have a constructor taking their respective GLM classes.