Updates on the development of the Skeleton Graphics Library project.
(Click the post title for a direct link)
SkeletonGL v2.0 has been released. The first stable and feature-complete build of the
library, it has slowly but surely grown into exactly what it was envisioned to be and
then some.
The engine is now apt for more than just hobby projects by merit of its stability,
performance and simplicity, it offers enough rendering capabilities to give
the established, C++ 2D rendering choices like allegro, SFML & SDL2 some competition.
This latest version has seen major changes to almost every
section of the source to accommodate for some of the more elaborate additions, but
the interface has remained practically the same. Updating an SGL project to v2.0
should be almost as easy as pulling from the git and recompiling, however, the engine internals
have been overhauled so if your build relies on custom changes I'd advise to take a look
at SGL_DataStructures.hpp which now contains all the internal OpenGL resources names.
namespace SGL_OGL_CONSTANTS
{
// GL_LINES uses this value top set the line's width, note that if AA is enabled it limits the linw width
// support to 1.0f
const float MAX_LINE_WIDTH = 20.0f;
const float MIN_LINE_WIDTH = 1.0f;
const float MAX_PIXEL_SIZE = 20.0f;
const float MIN_PIXEL_SIZE = 1.0f;
const float MAX_CIRCLE_WIDTH = 1.0f;
const float MIN_CIRCLE_WIDTH = 0.01f;
// These rendering consatants are the maximum amount of simultaneous instances to be rendered in a batch
// and MUST NOT BE EXCEEDED
const std::uint32_t MAX_SPRITE_BATCH_INSTANCES = 10000;
const std::uint32_t MAX_PIXEL_BATCH_INSTANCES = 10000;
const std::uint32_t MAX_LINE_BATCH_INSTANCES = 10000;
// Names assigned to the OpenGL objects used by the SGL_Renderer
const std::string SGL_RENDERER_PIXEL_VAO = "SGL_Renderer_pixel_VAO";
const std::string SGL_RENDERER_PIXEL_VBO = "SGL_Renderer_pixel_VBO";
const std::string SGL_RENDERER_PIXEL_BATCH_INSTANCES_VBO = "SGL_Renderer_pixel_batch_instances_VBO";
const std::string SGL_RENDERER_PIXEL_BATCH_VAO = "SGL_Renderer_pixel_batch_VAO";
const std::string SGL_RENDERER_PIXEL_BATCH_VBO = "SGL_Renderer_pixel_batch_VBO";
const std::string SGL_RENDERER_LINE_VAO = "SGL_Renderer_line_VAO";
const std::string SGL_RENDERER_LINE_VBO = "SGL_Renderer_line_VBO";
const std::string SGL_RENDERER_LINE_BATCH_INSTANCES_VBO = "SGL_Renderer_line_batch_instances_VBO";
const std::string SGL_RENDERER_LINE_BATCH_VAO = "SGL_Renderer_line_batch_VAO";
const std::string SGL_RENDERER_LINE_BATCH_VBO = "SGL_Renderer_line_batch_VBO";
const std::string SGL_RENDERER_SPRITE_VAO = "SGL_Renderer_sprite_VAO";
const std::string SGL_RENDERER_SPRITE_VBO = "SGL_Renderer_sprite_VBO";
const std::string SGL_RENDERER_SPRITE_BATCH_INSTANCES_VBO = "SGL_Renderer_sprite_batch_instances_VBO";
const std::string SGL_RENDERER_SPRITE_BATCH_VAO = "SGL_Renderer_sprite_batch_VAO";
const std::string SGL_RENDERER_SPRITE_BATCH_VBO = "SGL_Renderer_sprite_batch_VBO";
const std::string SGL_RENDERER_TEXT_VAO = "SGL_Renderer_text_VAO";
const std::string SGL_RENDERER_TEXT_VBO = "SGL_Renderer_text_VBO";
const std::string SGL_RENDERER_TEXTURE_UV_VBO = "SGL_Renderer_texture_uv_VBO";
// POST PROCESSOR EXCLUSIVE
const std::string SGL_POSTPROCESSOR_PRIMARY_FBO = "SGL_PostProcessor_primary_FBO";
const std::string SGL_POSTPROCESSOR_SECONDARY_FBO = "SGL_PostProcessor_secondary_FBO";
const std::string SGL_POSTPROCESSOR_TEXTURE_UV_VBO = "SGL_PostProcessor_UV_VBO";
const std::string SGL_POSTPROCESSOR_VAO = "SGL_PostProcessor_VAO";
const std::string SGL_POSTPROCESSOR_VBO = "SGL_PostProcessor_VBO";
// Default shader uniform names, make sure they match your custom shaders.
const std::string SHADER_UNIFORM_V4F_COLOR = "color";
const std::string SHADER_UNIFORM_F_DELTA_TIME = "deltaTime";
const std::string SHADER_UNIFORM_F_TIME_ELAPSED = "timeElapsed";
const std::string SHADER_UNIFORM_V2F_WINDOW_DIMENSIONS = "windowDimensions";
const std::string SHADER_UNIFORM_M4F_MODEL = "model";
const std::string SHADER_UNIFORM_M4F_PROJECTION = "projection";
const std::string SHADER_UNIFORM_F_CIRCLE_BORDER_WIDTH = "circleBorder";
// POST PROCESSOR EXCLUSIVE
const std::string SHADER_UNIFORM_I_SCENE = "scene";
const std::string SHADER_UNIFORM_V2F_FBO_TEXTURE_DIMENSIONS = "fboTextureDimensions";
const std::string SHADER_UNIFORM_V2F_MOUSE_POSITION = "mousePosition";
};
Moving on to new features, the SGL_Renderer can now render GPU accelerated circles. Drawing circles in modern OpenGL is rather complicated since there is no native OpenGL function to do so, they must be manually computed and rendered using the available primitive types which can be unnecessarily costly. After some experimentation, it became apparent that the fastest way to render circles is to form a square by joining two mirrored triangles and using the surface as a canvas to render the circle on with a special fragment shader. Basically, circles are just sprites using a shader that renders a circle on top.
To complement the other primitive renderers, however, the new SGL_Circle object is a straightforward representation of a circle and can be rendered calling the renderCircle function, abstracting away the internal SGL_Sprite. Conversely, it's possible to draw a circle on top of a sprite by specifying the SGL_Sprite shader as a circle shader, the circle's width is parsed as a renderDetails.circleBorder.
struct SGL_Circle
{
glm::vec2 position; ///< Circle position
SGL_Color color; ///< Circle color
SGL_Shader shader; ///< Shader to process the circle (because why the fuck not)
float radius; ///< Circle size
BLENDING_TYPE blending; ///< Blending type
};
// Added to SGL_Renderer
void renderCircle(float x, float y, float radius, float width, SGL_Color color);
void renderCircle(const SGL_Circle &circle) const; // Circles are just invisible sprites used as canvas
Rendering a batch of circles is as easy as calling renderSpriteBatch() with an SGL_Sprite that has been assigned either a custom circle shader or the included SGL::DEFAULT_CIRCLE_BATCH_SHADER (which is in reality a modified SGL::DEFAULT_SPRITE_BATCH_SHADER).
SGL_Sprite avatar;
avatar.texture = _upWindowManager->assetManager->getTexture("avatar");
avatar.shader = _upWindowManager->assetManager->getShader(SGL::DEFAULT_CIRCLE_BATCH_SHADER);
avatar.shader.renderDetails.timeElapsed = _fts._timeElapsed/1000;
avatar.shader.renderDetails.circleBorder = 0.06;
avatar.position.x = 40;
avatar.position.y = 10;
avatar.size.x = 32;
avatar.size.y = 32;
avatar.color = SGL_Color(1.0f,1.0f,1.0f,1.0f);
avatar.blending = BLENDING_TYPE::DEFAULT_RENDERING;
avatar.resetUVCoords();
// Note the mismatch in shader and render call, in this case it will default to rendering a sprite with
// default settings
_upWindowManager->renderer->renderSprite(avatar);
// Generate 4000 sprites worth of model data
std::vector sBatch;
for (int i = 0; i < 4000; ++i)
{
// Prepare transformations
glm::mat4 model(1.0f);
float r2 = static_cast (rand()) / (static_cast (RAND_MAX / 3.12f));
model = glm::translate(model, glm::vec3(rand() % 320, rand() % 180, 0.0f)); //move
// rotate
model = glm::translate(model, glm::vec3(avatar.rotationOrigin.x, avatar.rotationOrigin.y, 0.0f));
model = glm::rotate(model, r2, glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::translate(model, glm::vec3(-avatar.rotationOrigin.x, -avatar.rotationOrigin.y, 0.0f));
// scale
model = glm::scale(model, glm::vec3(avatar.size, 1.0f)); //scale
sBatch.push_back(std::move(model));
}
// In this case the sprite batch renderer matches with the CIRCLE_BATCH_SHADER and will draw a circle
// on top of the sprite each instance render
_upWindowManager->renderer->renderSpriteBatch(avatar, &sBatch);
This flexibility allows circles to be rendered on top of any sprite and for the circle's border width to be specified, all in a single draw call.
It's also possible to fill the circle by simply specifying a bigger border width value.
To better showcase what the library is capable of, the original plan was to release the v.20 update alongside an arcade game called Risk Vector. However, time constraints and the 2020 global fuckery in general left me with little time to develop the game. Opting instead to polish SkeletonGL as much as possible to then upgrade the software already using it and only then moving on to develop some games.
The few client projects using SGL as a means to render graphics have already been updated (check your email for notifications) and both CAS-SGL and Snake-SGL will hopefully soon follow.
The biggest ones being:
■ Polished the instance renderers, they should work much faster now
■ Added a limit of 10,000 items per call, see SGL_DataStructures.hpp to change it
■ Added blending options to the SGL_Pixel, SGL_Line and their respective batch renderers
■ Added new blending functions
■ All instance renderers are now compatible with their base shaders (ie, sprite shaders work with sprite batching)
■ The getTextureMemoryGPU function now includes the instance renderers pre-allocated memory
■ Added a makefile / compile time macro to add or remove error checking after render calls
■ SGL_Pixel can now specify pixel size
■ SGL_Line can now specify line width
■ Optimized away many unnecessary calls to OpenGL VBOs
The library is shaping up very nicely, the performance boosts alone allows for some truly crazy shit. This will most likely be the final release before 2.0 and Risk Vector are done.
- ■ Game state management & responsive UI
- ■ Sprite batching & texture atlas
- ■ Custom shaders and post-processing effects
- ■ Both TTF and bitmap font rendering
- ■ Updates to the real time debugging panel & production build settings
- ■ Support for both VSYNC & unlocked framerates
Fortunately enough, the amount of internal changes to accommodate for most updates has been minimal and will most likely stay this way until version 2.0 releases with the finished game. For now, all dependencies have been updated to their latest standards and tested to ensure compatibility, their respective credits and licenses are listed in deps/deps_credits.txt.
Note that if you're using GLM for your own project's math (and you probably should) starting with version 0.9.9.0, matrices and vectors are no longer initialized by default. Meaning that all glm::vec & glm::mat must be manually initialized to 1.0f to imitate its previous behavior.
Updated to 1.4.
SkeletonGL is currently being developed alongside a more elaborate showcase of its capabilities and features than its previous example program, Snake-SGL. The idea is to balance the relationship between the underlying library and the user's application so that SGL provides only what's essential for a high performance rendering engine without forcing any particular design upon the programmer. Features like FPS control and networking thus lie outside the scope of what SkeletonGL was made to do; render shit to the screen. To accomplish this predictably, however, the system needs to manage the logic's internal refresh rate.
This can be a far more complex problem to solve than it may initially appear. Many of the solutions that are often tried first fail due to subtle miscalculations like floating point inaccuracy when working out the frame's delta time or the inherent unreliability of sleeping a thread for longer than a system specific amount of time (~1ms). But the real headaches are the variables outside the programmer's control.
What happens if the user opens up 40 Chrome tabs while simultaneously watching a movie and playing your game, creating a resource consumption spike that lags the reactivation of the game's sleeping thread? What if the user has a monitor with a refresh rate different from that of your application and activates VSYNC (no, you can't stop them)? What if the user has a machine so powerful it processes the game at 1000Hz and his monitor at 255Hz, far higher than your game's update loop of 60Hz? What if the host OS is running under some sort of compositor and messing with the screen's refresh rate? What if the user is running the game on a toaster and can't even reach, let alone maintain the minimal required FPS? What if despite all this, the user still activates VSYNC?
I'll write a proper explanation and tutorial on a separate entry, suffice to say, SkeletonGL now only accounts for its own delta time and no longer offers FPS control settings, time management has been moved from the SGL library to the application's source. Updated to ver 1.3.
Updated to ver 1.12.
This update introduces a few bug fixes and sprite batching, which originally was supposed to be part of ver 1.0. The SGL_Renderer class can currently instance render sprites, pixels and lines, with a few more primitives coming in ver 1.12. For the time being I won't be cleaning away whatever development examples I test SGL with, they are good documentation after all.
Update will go live next week.
Some changes were made to the original vision of the project, namely that SGL would be far more appropriate to stay as an engine rather than a library, the makefile that compiles the static library of the SGL engine is still in the git project, however it is there for development purposes shall the necessity arise. Ideally, SGL is to be compiled alongside the rest of the application. This also encourages user modification to the SGL core, not that farfetched considering how much can be done by directly working with OpenGL. Finally, SGL will be packaged with a default example game of snake, in the future such example applications may be moved to their own repos or simply included in the default SGL repo, we'll see.
LINK ---> SKELETONGL
This shall be the foundation for future projects.