0.3 is here!

Cubos 0.3 Release!

This blog post came a bit late due to the summer break, but we're back with a new release! Features-wise, this release was a tad smaller than the previous one as we spent a lot of time working on non-code related stuff, such as rebranding and marketing the project.

A New Brand Identity (Nuno Baptista)

Cubos had grown out of its cocoon and needed new wings to fly out of its embryonic phase. The new visual identity aimed at providing a more cohesive and professional look to an increasingly defined and completed game engine. A new logo was designed, alongside a set of visual rules and principals that form Cubos new identity.

The process started with the logo. The team wished to keep the 3D aspect of the former logo, but with a more slick and modern twist. Furthermore, as the project was aimed at a software (a game engine), an icon was necessary for the application, the branding the launch screen, etc. A stylized wordmark, like the previous version, was insufficient. So we set to design the icon. At first, the design followed a more intellectual and less literal approach, adapting an isometric view with 3 sides to establish a deconstructed view of a Cube. The concept of this design was to incorporate the movement and change in the symbol, while representing the assembly purpose of a game engine. In this case, the assembly of a cube, with is faces at the moment they are sliding into place.

The team received this design with mixed feelings. The logo had little of the 3D aspect that made their previous logo and that also constitutes the main selling point of the engine - voxel base game engine. Therefore, we jumped to a second design, that retained the original while representing a shape made of literal isometric cubes. The final shape was a 3-armed spiral, inspired by the Keltic symbol the Trisquel, without copying the Trisquel GNU/Linux software.

Unfortunately, to some members of the team, the new icon reminded them of some unpleasant symbol. Consequently, we're back to square one. A brainstorming session ensued. Various shapes were tested and debated, but the icons kept getting too complex and unrecognizable.

Eventually, we arrived at an icon that was composed of cubes, that did not bring any bad connotations and that was relatively simple and memorable. As it can be seen in the experiment above, the spiral/movement idea was forsaken in favor of a greater emphasis in the assembly aspect. Three cubes merged to form something more complex, suggesting ideas of union and strength.

Parallel to the icon design process was the font selection. The goals we set at the start were that the font had to transmit a feel of modernity and a certain degree of formality, since those were the goals of this redesign, while conveying the slickness and dynamism of an innovative product. Other concerns that were considered were the versatility and rights of the chosen font. To address this last point, we opted for google fonts library, a largely integrated and open-source collection of great fonts, that complements the open-source aspect of the project. The fonts selected were Russo One for the logo mark and displays/headers, and Roboto font family for text body. Russo One conveys energy and strength while maintaining a dynamic feel to it, while Roboto is a well-established in the space of tech and innovation.

About the logo's colors, it can be observed that these remained virtually unchanged thought out the design process. The contrast of neutral colors with a cyan blue, that aim to represent knowledge and innovation, was well received. Additional colors were added to the pallet for software and marketing purposes. Speaking of which, a Brand Guidelines Document was enacted to specify how the brand and marketing material should be designed and to which principles it should adhere.

New 0.3 Features

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

On the Editor

Groundwork for a Standalone Editor (@RiscadoA)

The original main goal for this version was to get a first prototype of our standalone editor working. Sadly, we didn't manage to get it done in time, as it was a bigger task than we anticipated.

The initial approach, to which we dedicated most of the time, was to have two instances of Cubos running at the same time, on the same process - one for the editor and one for the game. The game instance would be running within the editor instance, and would be launched by loading the game's code from a shared library (such as a DLL). With this separation, it would be possible for the editor to interact with the game, pause it, restart it and so on, even across game code changes, without having to restart the editor.

So, with this in mind, we restructured some parts of the code base to get it flexible enough to support this kind of setup. Namely, it is now possible to step Cubos instances, reset them, restart them, and other similar operations. We also added cross platform support for loading Cubos instances from shared libraries, so that we could load the game from the editor.

This all seems, at first, perfect, but after a team discussion, we realized that this approach of having two Cubos instances in the same process is not ideal at all. Any game crash would bring down the editor, any memory leak in the game would affect the editor, and infinite loops in the game would freeze the editor. Since solving the halting problem isn't in our roadmap, we decided that we should discard this approach, and instead try to keep the editor and the game in two separate processes.

So, for the next release, we'll be focusing on following this new approach. It will be a big task, and it will be a lot more difficult, as it will require a lot of inter-process communication, but it will be worth it in the end. It will allows us to have a stable editor, whose performance is not affected (that much at least) by the game, and vice-versa.

On the Engine

A New Physics Solver (@fallenatlas)

Upon discovering some potential issues with regards to NVidia's patent of XPBD, some problems with how XPBD handled friction, and stability, we decided that this would be the ideal moment to switch to another solver. Recently, Erin Catto shared his comparison of multiple solvers, and the results of TGS Soft seemed quite promising, so we decided to give it a try... And at this point, our simulation is already more stable than before!

In simple terms, it differs from XPBD in the sense that it uses impulses instead of changing the position of the bodies directly, however, like XPBD it also uses substeps. On top of this it also uses soft constraints, which is what Soft stands for in it's name.

Since these changes were mostly in internal systems, from a user stand point, everything works the same way.

Additionally, we added Friction and Bounciness. For now, these are a predefined value for every body, but will be customizable in the next release with a PhysicsMaterial.

Raycasting (@diogomsmiranda)

Raycasting is a commonly used tool in game development which Cubos was lacking until now - it is a technique used to determine the intersection of a ray with an object in a scene. This allows for a wide range of applications, such as shooting mechanics, object selection, and more.

This new utility was implemented as a system argument, which means it can be used in any system, by simply adding it as an argument:

cubos.system("raycast").call([](Raycast raycast)
{
    // raycast from the origin to -50,0,0
    auto hit = Raycast.fire({{0.0F,0.0F,0.0F},{-50.0F,0.0F,0.0F}});
    if (hit.contains())
    {
        CUBOS_INFO("Hit entity {} at point {}", hit->entity, hit->point);
    }
});

To implement this, we had to handle the two types of colliders that we currently have in Cubos: boxes and capsules.

Intersection with a Box

The intersection test with a box is based on the Cyrus-Beck algorithm, which is a line clipping algorithm that is used to find the intersection of a line segment with a convex polygon.

We can easily define a box by the minimum and maximum values of x,y,z and the ray by its origin and the direction.

A ray is defined then by the line formula:

point = ray.origin + t * ray.direction

Being t a scalar value that represents the distance from the ray's origin to the point.

Our objective is to find t, and check if the point is in the "right" side of the ray (the side that the ray is pointing to).

For that we can rearrange the previous formula to:

t = (point - ray.origin) / ray.direction

If the point is in the right side of the ray, then the intersection point is the point that is closest to the ray's origin.

Now, the only thing that we still need to account is, that most of the times, we have 2 intersection points, one going in, and one going out.

For this we can change the way we use this formulas by instead of using the point, we use the minimum and maximum values of the box.

If both our t's make sense, then we have an intersection.

Here is an excerpt taken from the Raycast class:

static float intersects(cubos::engine::Raycast::Ray ray, cubos::core::geom::Box box)
{
    (...)

    glm::vec3 max = corners[1];
    glm::vec3 min = corners[0];

    float tMinX = (min.x - ray.origin.x) / ray.direction.x;
    float tMaxX = (max.x - ray.origin.x) / ray.direction.x;
    float tMinY = (min.y - ray.origin.y) / ray.direction.y;
    float tMaxY = (max.y - ray.origin.y) / ray.direction.y;
    float tMinZ = (min.z - ray.origin.z) / ray.direction.z;
    float tMaxZ = (max.z - ray.origin.z) / ray.direction.z;

    // find the maximum of the min
    float tMin = std::max(std::max(std::min(tMinX, tMaxX), std::min(tMinY, tMaxY)), std::min(tMinZ, tMaxZ));

    // find the minimum of the max
    float tMax = std::min(std::min(std::max(tMinX, tMaxX), std::max(tMinY, tMaxY)), std::max(tMinZ, tMaxZ));

    if (tMax < 0 || tMin > tMax)
    {
        return -1.0F;
    }

    return tMin < 0.0F ? tMax : tMin;
};
Intersection with a Capsule

The intersection with a capsule is more straight forward than the collision with a box, as we can separate a capsule into 3 parts, a cylinder and the two spheres at the ends.

We then can check for a point of intersection by checking if the ray intersects the cylinder, and if it doesn't, we check if it intersects the spheres.

We can determine both intersections by simply subbing the the ray's equation for x and z in the cylinder and sphere equations, and then solving it for t.

Code excerpt from raycast.cpp for the cylinder intersection:

static float intersects(cubos::engine::Raycast::Ray ray, float radius, float top, float bottom)
{
    // We are gonna use the quadratic equation made by subbing the ray equation into the cylinder equation
    // The cylinder equation is:
    // x^2 + z^2 = r^2
    // The ray equation is:
    // x = x0 + t * dx
    // z = z0 + t * dz

    float a = ray.direction.x * ray.direction.x + ray.direction.z * ray.direction.z;
    float b = 2.0F * (ray.direction.x * ray.origin.x + ray.direction.z * ray.origin.z);
    float c = ray.origin.x * ray.origin.x> + ray.origin.z * ray.origin.z - radius * radius;

    float discriminant = b * b - 4.0F * a * c;
    if (discriminant < 0)
    {
        return -1.0F; // no intersection with the cylinder
    }

    float t1 = (-b + std::sqrt(discriminant)) / (2.0F * a);
    float t2 = (-b - std::sqrt(discriminant)) / (2.0F * a);

    float max = std::max(t1, t2);
    float min = std::min(t1, t2);

    float t = min > 0.0F ? min : max;

    if (t < 0.0F)
    {
        return -1.0F; // no valid intersection
    }

    float y = ray.origin.y + t * ray.direction.y;

    if (y < bottom || y > top)
    {
        return -1.0F; // intersection is outside the finite cylinder
    }

    return t;
};

Spot Light Shadows (@tomas7770)

Our graphics renderer has received a new major feature in this release: shadows!

It should go without saying that this feature has a big impact on the visuals of games developed with Cubos. We've tried it on Scraps vs Zombies and the result is stunning! It's an important step towards the kind of appealing graphics that we hope to achieve.

For the time being, shadows support is limited to spot lights. To enable them, all you need to do is add a SpotShadowCaster component to the spot lights for which you want shadows to be cast, as shown in the Shadows sample. Both hard and soft shadows are supported, with a configurable blurRadius.

Behind the scenes, this works by rendering the world from each light's perspective to determine which parts are occluded, and making these parts unlit. A large texture known as the "shadow atlas" holds this information for every light in a quadtree structure, reducing expensive texture switching. Finally, soft shadows are implemented as a post-processing step that effectively blurs out the shadows. Below is a screenshot of the shadow atlas with 5 spot lights. Lines have been drawn separating the areas of the atlas reserved for each light.

Initial UI Plugin (@DiogoMendonc-a)

Cubos now has a UI system!

Add a UICanvas to your Render Target, and set a UIElement as its child. The UIElement will determine where the entity is drawn, and other UI components you add to the entity, such as a UIImage, will determine what it is that is drawn. For this initial version of the plugin, there are only two types of elements: UIColorRect, which simply fills the element with a solid colour, and UIImage, which draws an image asset.

To make using the UIElement easier, there are also a number of components meant to dynamically change its size: UIHorizontalStretch and UIVerticalStretch will make the element expand to its parent's size.

One more problem that was tackled was the question of how to handle different aspect ratios. As it stands, Cubos now has five different settings for how to handle that, that can be read in detail on the sample page.

Finally, there is a UINativeAspectRatio that, when paired with an UIImage, will ensure that the UIElement will retain the proportions of the original source file.

On the Core

Metrics (@roby2014)

We are excited to introduce the new metrics and profiling utilities! This started becoming a priority since we detected lots of performance issues in the last Game Jam we participated.

These tools are designed to help track performance and gather valuable insights about code execution and data.

How it looks:

static void compute()
{
    CUBOS_PROFILE();
    // ...
} // after the scope ends, a new metric `compute` will be added, with the duration of this scope

static void myFunction()
{
    /// simulate profiling loop
    for (int i = 0; i < 1337; ++i)
    {
        // simulate frame by calling a function that does work..
        compute();

        // register some metrics, this could be FPS, entities count, ....
        CUBOS_METRIC("count", i);
    }
}

Currently, metrics can be accessed manually through the singleton class. However, we plan to integrate these metrics into our editor for a more streamlined experience.

Learn more about it on our metrics documentation.

Networking Utilities (@roby2014)

In the 0.3 release, Cubos finally has networking! This is a big step forward for our engine, bringing powerful networking utilities such as Address, UdpSocket, TcpListener, and TcpStream.

Creating an UDP client and sending a message is as simple as:

UdpSocket client;
client.bind(8080, Address::LocalHost);

const char* msg = "Hello, I'm a Cubos UDP client!";
client.send(msg, std::strlen(msg), Address::from("server.com"), 8081);

Learn more about it on our networking documentation.

Next Steps

Although this was a smaller release, we managed to get some important features done! In the next release, which should be out by the end of this month, we're planning to add:

  • A standalone editor application. Our tools are currently integrated into the games themselves which is not ideal.
  • Audio support, as there's no sound at all in the engine right now.
  • Actual voxel collisions, as we currently only check the bounding boxes.
  • Basic rigidbody physics, with rotation and friction.
  • Shadows for other light types.
  • Tracing and spans, for better tracking of program execution.

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

We're currently a team of 13 people, and we're looking to expand! If you're interested in joining us, or just want to learn more about the project, join our Discord server!