Skeleton Graphic Library Devlog

Updates on the development of the Skeleton Graphics Library project.
(Click the post title for a direct link)

[#105] [Sat, 05 Dec 2020 11:44:37 CST][sgl_devlog]
// SkeletonGL ver 2.0 finally released

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.

Internally managed OpenGL resources

    // 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
    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";

    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";

    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).

Circle batch example

    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);

    // Note the mismatch in shader and render call, in this case it will default to rendering a sprite with
    // default settings

    // 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

    // 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);

GPU accelerated primitives (including circles!)

Pixel, Line, Circle & Sprite batch rendering test

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.

Behind the scenes, circles are just custom shaders and can be applied to any sprite

It's also possible to fill the circle by simply specifying a bigger border width value.

Same circle, different border widths

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.

[#98] [Wed, 28 Sep 2020 15:40:13 CST][sgl_devlog]
// SkeletonGL ver 1.9 released

SkeletonGL was recently used as a starting point for a client's data visualization software (closed source at their request, unfortunately) that ended up serving as a catalyst for a few changes.

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

Testing the instrance renderers (FPS lowered due to compression)

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.
[#84] [Sat, 02 May 2020 21:24:55 CST][sgl_devlog]
// Dependencies updated & 2.0 features

Quarantined at home, I've been using the extra time to work on the next SGL example project and the SGL engine itself. The game will cover a lot of ground for future projects, as well as providing a more thorough showcase of SGL capabilities and features. Including ones not present in the previous SGL game, like:

  • ■ 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, 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.
[#82] [Sat, 25 Apr 2020 00:01:37 CST][sgl_devlog]
// Time control | VER 1.3

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.
[#77] [Thu, 26 Mar 2020 21:42:06 CST][sgl_devlog]
// Dynamic VSYNC options -> VER 1.12

While working on a yet to be announced project, I came across the necessity of offering an in-game option to toggle VSYNC. The SGL_Window class can now do this through toggleVsync(); I also added an example orthographic shader configuration to the SGL_Camera class that flips the vertex shader's [Y] axis.

Updated to ver 1.12.
[#65] [Mon, 20 Jan 2020 04:52:05 CST][sgl_devlog]
// SGL v1.1 Released

Admittedly late, but SGL ver 1.1 is finally done.
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.
[#63] [Thu, 09 Jan 2020 19:32:49 CST][sgl_devlog]
// Incoming v1.1 updates

Next update, I'll be adding instanced rendering to finally make the SkeletonGL toolkit feature complete. With sprite batching done, the focus will then shift to developing more example applications aka vidya games, some of which have been nothing more than hopeful daydreams precisely because SGL v1.0 lacks batching. While there are alternatives to get around having to rely on such advanced techniques, all game programming solutions must be flexible enough for the developer to have the last word.

Update will go live next week.
[#52] [Sat, 31 Jul 2019 07:34:00 CST][sgl_devlog]
// SGL v1.0 officially released

I've been using SGL as a template for all sorts of different projects for long enough to be sure that the effort of cleaning and commenting the code is worth it. Time was always the biggest challenge, the development oscillated between spending entire days polishing it out to months long abandonment due to daily life distractions throughout its entire cycle. Being so close to having something I could publicly release, I had to cut some corners but it's finally done. SGL ver1.0 is officially released.
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.


This shall be the foundation for future projects.
[#50] [Fri, 27 Jul 2019 17:01:00 CST][sgl_devlog]
// Almost done with SGL v1.0

Though slow, I've been steadily working on finishing up SGL for its official public release. The library is practically done, but I want to add an example program to use as a template for noobs to work with. Nothing fancy, just enough to showcase a very crude, simple game running on the engine. SGL shall be released with a snake game as its first example. Soon™.
[#46] [Sat, 03 Jul 2019 12:32:15 CST][sgl_devlog]
// Lost the last one

I haven't found the time to work on SGL, with it being so close to completion I'd rather finish it off asap and move on to other projects, but lately I've been too taxed by work to focus on anything but. I'll get it done before vacation.