0.4 is out!

Cubos 0.4 Release!

New 0.4 Features

This blog post covers the most important changes, but you can check out the full changelog in our repository.

On the Editor

Project Management and Debugging (@RiscadoA)

In this release, we were finally able to make our editor, Tesseratos, a standalone application! Previously, the editor was a part of the engine itself, which meant if you made changes to the game's code, you had to recompile the editor as well, and restart it. Now the editor has its own executable, and through it, you can open projects, edit their assets, and even debug them - all without having to restart the editor when recompiling the game. At this time, the UX is still a bit rough, as we've focused on making it work first.

One major hurdle we had to overcome was how to make the editor aware of the project's data types, such as its components and resources. Before, they were part of the same application, and thus, the editor could simply access them directly. Now, with the editor being a separate application, we needed a way to share this information across different processes. You can read more about it in the "Type Client and Type Server" section at the end of this post, but in short, with the new reflection functionalities, it is now possible to share type data across different processes, through, for example, a socket.

In practice, this means that games made with Cubos can now be launched with a debug flag which makes them act as a server at a given port. Then, the editor is able to connect to this server and extract all of the required debug information. We also make use of this new connection to allow the editor to send debugging commands to the game, such as pausing, resuming, and stepping the game's execution.

So, essentially, we've added two new tools to the editor:

  • A debugger tool, which allows you to connect to a running game and debug it (pause, resume, step, etc.).
  • A project manager tool, which allows you to open projects, edit their assets, and launch them.

You can see them in action in the video below:

Voxel Model Importing Tool (@Scarface1809)

In this release, we've added a highly useful plugin to Tesseratos: a Voxel Model Importing Tool. With this new tool, you can now import .qb files directly within the editor, bypassing the need for the external conversion tool Quadrados.

Previously, importing voxel models required converting .qb files into .pal and .grd files using Quadrados, a CLI tool, before they could be used in the engine. Now, with the Voxel Model Importer Plugin, the process has been simplified, acting as an interface to handle the conversion process within the editor.

The tool allows you to:

  • Assign names to the .pal and .grd files.
  • Choose the file paths where the .grd files and the .pal file should be saved.
  • Specify how many .grd files supported by the .qb file you want to import.

With this tool, importing voxel models into your projects is now faster and easier, allowing for a smoother workflow and less reliance on Quadrados.

On the Engine

Physics with Rotation (@fallenatlas)

We have rotation!

Since the start of the physics plugin, we always treated rigid bodies as simple particles. This is because it made the calculations more intuitive, and it reduced the potential amount of mistakes we could make when first starting out. It also made debugging - of penetration solving for example - easier. With the particle behaviour stabilized, we decided it was time to expand the plugin to consider rotation, and for that we had to:

  • Add Collision Manifold and Collision Points computation.
  • Add Components for Rotation.
  • Expand the Integrator and Solver to compute rotations.
Collision Manifold and Collision Points

Previously, since everything was a particle, we simply computed if there was a collision using SAT, which also returned the normal along which the penetration was the smallest, along with that penetration value. For rotations we need a ContactManifold relation, that holds all information about the collision. A Contact Manifold is effectively a 2D polygon that details the surface of contact between the 2 bodies. In computer physics the bodies inter-penetrate each other, which does not happen in the real world. The manifold is the aproximation of the real contact surface the bodies would have. This polygon is described by its vertices, in the form of Contact Points, all of which contain their position and penetration in ContactPointData.

To compute the manifold we use the Sutherland Hodgman algorithm. Our current implementation was mostly inspired by this tutorial, which you can check out, to see how it works.

struct ContactPointData
{
    cubos::core::ecs::Entity entity; ///< Entity to which the normal is relative to.
    glm::vec3 globalOn1;             ///< Position on the entity of the contact in global coordinates.
    glm::vec3 globalOn2;             ///< Position on the other entity of the contact in global coordinates.
    glm::vec3 localOn1;              ///< Position on the entity of the contact in local coordinates.
    glm::vec3 localOn2;              ///< Position on the other entity of the contact in local coordinates.
    float penetration;               ///< Penetration of the contact point. Always positive.
};

struct ContactManifold
{
    cubos::core::ecs::Entity entity;      ///< Entity to which the normal is relative to.
    glm::vec3 normal;                     ///< A contact normal shared by all contacts in this manifold,
                                          ///< expressed in the local space of the first entity.
    std::vector<ContactPointData> points; ///< Contact points of this manifold.
};

With this change, our narrow phase firstly checks for collision with SAT, since it's cheaper, and only then computes the manifold.

The collisions sample was also enhanced so we could visualize this information.

New Components

The next thing to add were Components to support rotations, such as Inertia, Torque, and AngularVelocity, just to name a few. To facilitate the creation of entities with box collision shapes, we also added automatic computation of the inertia tensor for the BoxCollisionShape, which is performed upon adding the Inertia component and every time Mass or the dimentions of the shape are changed.

Expand the Integrator and Solver

The last step was to add rotation in the Integrator and PenetrationConstraint Solving.

The integrator simply takes the AngularImpulse and Torque components and handles them similarly to the linear movement components. We want to note that the Force component also holds a torque vector, in our current design. This is because the method we supply to apply a force on a point of the body belongs to Force, and that method may also apply some torque, if the force isn't aligned with the center of mass. This hidden torque vector is also used in the Integration phase.

struct Force
{
    void addForceOnPoint(glm::vec3 force, glm::vec3 localPoint, glm::vec3 centerOfMass)
    {
        mForce += force;
        mTorque = glm::cross(localPoint - centerOfMass, force);
    }

private:
    glm::vec3 mForce = {0.0F, 0.0F, 0.0F};
    glm::vec3 mTorque = {0.0F, 0.0F, 0.0F};
};

The PenetrationConstraint is now solved for each contact point, using their local anchor for relative velocity, separation, and angular velocity calculation at each step. This involved changing the constraint itself to also have a number of PenetrationConstraintPointData, which are the same as the manifold points but with extra information to keep track of the constraint solving. The constraint is now created when the ContactManifold relation is present between 2 entities, instead of CollidingWith.

struct PenetrationConstraintPointData
{
    float initialSeparation; ///< The separation of the contact point. Negative separation indicates
                             ///< penetration.
    float normalSpeed; ///< The relative velocity of the bodies along the normal at the contact point the begging of
                       ///< the collision.

    glm::vec3 localAnchor1; ///< The local contact point relative to the center of mass of the first body.
    glm::vec3 localAnchor2; ///< The local contact point relative to the center of mass of the second body.

    /// Store fixed world-space anchors.
    /// This improves rolling behavior for shapes like balls and capsules. Used for restitution and friction.
    glm::vec3 fixedAnchor1; ///< The world-space contact point relative to the center of mass of the first body.
    glm::vec3 fixedAnchor2; ///< The world-space contact point relative to the center of mass of the second body.

    // separation
    float normalMass;    ///< Mass to use for normal impulse calculation.
    float normalImpulse; ///< Accumulated impulse for separation.

    // friction
    float frictionMass1;    ///< Mass to use for friction impulse calculation along the first tangent..
    float frictionMass2;    ///< Mass to use for friction impulse calculation along the second tangent..
    float frictionImpulse1; ///< Accumulated impulse for friction along the first tangent.
    float frictionImpulse2; ///< Accumulated impulse for friction along the second tangent.
};

struct PenetrationConstraint
{
    cubos::core::ecs::Entity entity; ///< Entity to which the normal is relative to.
    glm::vec3 normal;                ///< Normal of contact on the surface of the entity.
    float friction;                  ///< Friction of the constraint.
    float restitution;               ///< Restitution coefficient of the constraint.

    std::vector<PenetrationConstraintPointData> points; ///< Contact points in the contact manifold.
};

And that's it for this very short summary of the physics changes. Our implementation is still far from perfect. As you might have seen in the sample clip, some movements still don't look very natural, and the boxes should have stayed on top of each other, which is something we'll be working on over the next releases.

To wrap up, as we promised in the previous release blog post, we've added the PhysicsMaterial component, which allows users to fine tune the friction and bounciness behaviour of each body. Check it out in our documentation!

Cascading Shadow Maps (@tomas7770)

In the previous release, we introduced shadows. As shown in the release blog post, this had a significant impact on the quality of Cubos' graphics. However, due to the complexity and time it took to implement this feature, we were only able to support spot lights, the type of light for which it's easiest to cast shadows. This obviously restricted the scenarios where shadows were visible. For this release we aimed to extend shadows support to directional lights, leaving only point light shadows to be implemented.

Here are some comparison screenshots showing the difference directional shadows can make. Try dragging the slider in the middle to compare the before and after!

As you can see, this adds a whole new level of depth to the engine's graphics.

Similarly to spot shadows, directional shadows can be enabled by adding a component to the respective light, in this case a DirectionalShadowCaster. The Shadows sample has been updated to show this.

Because directional shadows are more complex than spot shadows, there are more options that can be configured, namely the maximum and minimum distances for which shadows are casted (directional lights have an unlimited range, but shadows have a limited range), the distances at which the shadow quality level drops ("splits"), and the resolution of the shadow map. You may want to tweak the distance values for better results: a lower value will result in better shadow quality at the expense of range, and vice-versa.

Due to a technical limitation in our renderer, only one directional light can cast shadows at a given time; other directional shadow casters will simply be ignored.

The way directional shadows are implemented is similar to spot shadows. One of the main differences is that, because of directional lights' unlimited range, a technique known as Cascaded Shadow Mapping is employed, rendering the world from a point which varies with the camera's position, and at multiple distances to balance quality with draw range. This means that instead of just a single shadow map texture for the light, there is a texture for each camera, multiplied by the number of distances at which the world is rendered. It's worth noting that each directional shadow caster has its own textures, instead of using a shared shadow atlas like spot casters. Below are screenshots of the directional shadow map used to draw shadows in Scraps vs Zombies, as shown earlier.

Input Axis Deadzones (@kuukitenshi)

Previously, dealing with input sources that exhibited drift, like older gamepad joysticks, required developers to manually filter out noise from input data.

In this release, input deadzones can now be configured directly within the bindings asset, allowing players to adjust it in the settings for their controllers to filter out unwanted noise.

This enhancement significantly simplifies input handling and ensures a smoother and more reliable gameplay experience, especially for games that heavily rely on precise controller input.

Ortographic Cameras (@mkuritsu)

Previously we only had support for perspective cameras in Cubos, and additionally, perspective matrix computations were duplicated all over the code. To address this, we've added a new generic Camera component that holds the projection matrix of the current camera in use. Now, the PerspectiveCamera component is only used to fill in the Camera component with the correct projection matrix, and code needing the projection matrix can simply query the Camera component.

With this, we decoupled the camera type from the rest of code, allowing us to add a new OrtographicCamera component which uses an ortographic projection instead of a perspective one.

On the Core

Spans for Profiling and Tracing (@roby2014)

In our ongoing efforts to improve metrics and address performance issues, we are excited to announce the implementation of a new feature for telemetry: Tracing. This addition will significantly enhance our ability to monitor and understand the execution flow of applications built with Cubos.

Tracing allows developers to track the execution of their code by creating spans that represent specific periods of execution. This capability makes it easier to log messages and visualize the flow of an application, providing valuable insights into performance and behaviour.

In Cubos, tracing is facilitated through a set of macros defined in core/tel/tracing.hpp: - CUBOS_SPAN_TRACE - CUBOS_SPAN_DEBUG - CUBOS_SPAN_INFO

From now on, all telemetry components share the same logging level. You can set it by using cubos::core::tel::level method. This means that, as an example, trace spans will only be registered if the logger level is set to trace. We also moved the other components to the tel namespace (metrics and logging).

Here's a simple code snippet on how it works and its output:

int main()
{
    cubos::core::tel::level(Level::Debug);
    CUBOS_SPAN_TRACE("this_wont_exist!"); // wont exist because trace < debug

    CUBOS_INFO("hello from root span!");

    CUBOS_SPAN_INFO("main_span");
    // With this macro, a new RAII guard is created. When dropped, exits the span.
    // This indicates that we are in the span for the current lexical scope.
    // Logs and metrics from here will be associated with 'main' span.
    CUBOS_INFO("hello!");

    CUBOS_SPAN_DEBUG("other_scope");
    CUBOS_INFO("hello again!");

    SpanManager::begin("manual_span", cubos::core::tel::Level::Debug);
    CUBOS_INFO("entered a manual span");
    SpanManager::end();

    CUBOS_INFO("after exit manual span");
}
[16:03:31.966] [main.cpp:20 main] [thread11740] info: hello from root span!
[16:03:31.967] [main.cpp:26 main] [thread11740:main_span] info: hello!
[16:03:31.967] [main.cpp:29 main] [thread11740:main_span:other_scope] info: hello again!
[16:03:31.968] [main.cpp:34 main] [thread11740:main_span:other_scope:manual_span] info: entered a manual span
[16:03:31.969] [main.cpp:37 main] [thread11740:main_span:other_scope] info: after exit manual span

Looking ahead, we aim to develop a Tesseratos plugin that will allow developers to debug and view all possible spans and their execution times (e.g: https://github.com/bwrsandman/imgui-flame-graph).

This UI will enable developers to interact with the tracing data, providing a comprehensive view of the entire game flow.

Swapping OpenAL for Miniaudio (@diogomsmiranda, @Dageus)

This release marks the beginning of an exciting new chapter for Cubos: the Audio Plugin. But before we could start working on the plugin itself, we had to make some changes to the audio backend. Previously, we were using OpenAL for audio, which no longer aligned with our vision for Cubos, so we decided to switch to miniaudio.h, a lightweight, single-file audio library easy to integrate and use while also sharing the same license as Cubos. We implemented a new AudioContext abstraction on the core library, which hides the underlying audio library from the rest of the engine.

In the next release of Cubos, we're planning to have a fully functional audio plugin, which will allow you to play sounds and music in your games!

Type Client and Type Server (@RiscadoA)

To separate Tesseratos from the engine into a separate process, we needed a way to share type data across different processes. This is because the editor needs to know about the project's data types, such as its components and resources, to be able to do work with them, such as showing them in the editor's UI, or editing scene assets.

To solve this problem, we've added the TypeServer and TypeClient classes to the core's reflection module. The editor contains a TypeClient, the game contains a TypeServer and both communicate through an abstract stream, which currently is implemented using a TCP socket.

Our reflection system is based on the concept of "traits". A trait is a piece of metadata that describes a type, such as its name, its fields, and its methods. Both the TypeClient and the TypeServer allow registering serialization and deserialization functions for each trait, so that they can be sent across the stream.

The protocol itself is relatively simple:

  1. The client - the editor - sends a list with the names of the traits it supports.
  2. The client sends a list with the types it already knows about (for example, primitive types like int and float).
  3. The server - the game - sends a list with the types that the client doesn't know about yet. This includes: - The type name. - A list of the serialized traits that describe the type. - The serialized default value for the type, if it has one.

Most of the complexity of this system is on the client side, as it needs to figure out the memory layout of the types it receives from the server. We do this by basing the memory layout on one of the received traits. For example, if the server sends a trait that describes an object type with fields, then the client will create a new type which stores these fields in memory. If the trait describes an array type, then the client will create a new type that stores an array in memory, and so on.

One major limitation of the current implementation is that it lacks support for traits like NullableTrait. This trait contains a function which determines if a value is null or not, and another to make a value null. How can we pass a function through a stream? We can't. One thing we could do would be to communicate again with the server whenever the client needs to know whether a value is null or not. For now, we've decided to simply ignore these traits, but we'll eventually need to tackle this issue, as it would allow for better UX in the editor.

Stacktraces on Crashes (@RiscadoA)

Previously, when the engine crashed, it would simply print an error message to the console, and that was it. We would not get a lot of information about the crash's context, other than the previous log messages. To debug a crash, we usually spun up a debugger and tried to reproduce it. This way we could get a stack trace, but it was a bit cumbersome, and when the crash happened in a release build or in a different environment, we were out of luck.

In order to make our lives easier, we've integrated cpptrace into the core library. Now, whenever the engine aborts, it prints a pretty stack trace to stderr, which includes the function names, the file names, and the line numbers of the functions that were called.

New Team Organization

We're currently undergoing a major reorganization of the team itself, as it has grown significantly over the past few months. We have now over 20 developers working on the project, and we're planning to expand even further.

Previusly, we all met weekly to showcase what each member had been working, and to discuss the project's direction. Of course, with such a large team, these meetings were becoming increasingly long and unproductive.

So, we decided to split the big team into smaller teams, each with its own focus. As of now, we have the following teams:

  • Community: responsible for managing the more meta aspects of the project, such as the blog, the Discord server, and the social media.
  • Graphics: responsible for any graphics-related features, such as voxels, UI and gizmos rendering.
  • Physics: responsible for the physics and collisions plugins.
  • Tools: responsible for Tesseratos, Quadrados and all other kinds of tooling.
  • Wildcard: catch-all team for any other kind of feature that doesn't fit in the other teams.

Now each team meets weekly to discuss their progress and plans, and we have a big meeting less frequently to discuss the project as a whole.

Next Steps

In the next release, which should be out by the end of November, we're planning to work on the following features:

  • Scene editing through the new standalone editor, Tesseratos.
  • An Audio plugin, using the new Audio Context abstraction we've added in this release.
  • Point light shadows.
  • Anti-aliasing.
  • MSDF text rendering on our UI plugin.
  • Reduce shadow artifacts such as shadow acne and peter panning.
  • Toggleable gravity on the physics plugin.
  • Voxel collision shapes, where the collision shape is the same as the voxel model.
  • Contact point caching to avoid recomputing the collision manifolds every frame.
  • Saving settings after changing them in the UI.
  • An Active component to enable and disable all kinds of behaviors in entities.
  • Refactoring the whole CMake configuration to support installation and packaging.

Additionally, we're planning to work on a new game project using our engine - now with online multiplayer support!

You can check out the full list of stuff we want to get done in the milestone for the next release.