BLOG

Incoherent remblings and other topics of interest

ENTRY #88

---



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 it's 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 accomodate 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 you're build relies on custom changes I'd advice to take a look at SGL_DataStructures.hpp which now contains all the internal OpenGL resources names.


Internally managed OpenGL resources

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 rendererd calling the renderCircle function, asbtracting 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);
    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);


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.