Throughout this weird year I managed to accomplished a lot of different milestones when refactoring the rendering system in Crimild. Yet, the year was coming to an end and there was one feature in particular that was still missing: compute operations.

Then, this happened:

That, my friends, is the very first image created by using a compute pass in Crimild. The image is then used as a texture that is presented to the screen. Both compute and rendering passes are managed by the frame graph and executed every frame in real-time.

At the time of this writing I haven't implemented true synchronization between the graphics and compute queues, meaning that the compute shader might still be writing the image by the time it is read by the rendering engine, which produce some visual artifacts every once in a while. 

Of course, I had to push forward.

A few hours passed and the next compute shader that I made was used to implement a very basic path tracer completely in the GPU:

It’s not a true real-time ray tracing solution (since I don’t have a GPU with proper RTX support), but sampling is done incrementally, allowing me to reposition the camera in real-time:

I’m still amazed about how easy it was to port my software-based path tracer to the GPU.

So much power…

So much potential…

I wanted more…

I needed more…

I became greedy.

I flew too close to the Sun.

And I got burnt.

Then I learned a valuable lesson. It turns out that if I screwed up the shader code in some specific way (which I’m still trying to understand), weird things happens. Like my compute crashing… bad (as in having to turn it off and on again bad).

Next steps

I’m planning on (finally) merging the Vulkan branch at this point, since all major features are done. Sure, there are things that still need to be fixed and cleaned up, yet they don’t really depend on Vulkan itself, like behaviors, animations and sound, which is broken (again).

Plus, I really want to release Crimild v5.0 in the next decade.

See you next year!

Sponsored Post Learn from the experts: Create a successful blog with our brand new courseThe Blog is excited to announce our newest offering: a course just for beginning bloggers where you’ll learn everything you need to know about blogging from the most trusted experts in the industry. We have helped millions of blogs get up and running, we know what works, and we want you to to know everything we know. This course provides all the fundamental skills and inspiration you need to get your blog started, an interactive community forum, and content updated annually.

Keeping things Simple(r)

Crimild never really had an easy way in. The building process itself is a bit more complex that I would like and once you pass that, creating simulations is no walk in the park either. Regardless of the complexity of the scene or the rendering composition, you usually end up with a lot of boilerplate code just to run the application:

  1. You need a main function (or in the case of OSX/iOS, a whole new project).
  2. You need to create not only a Simulation instance, but also add any System implementation you require based on what you want to accomplish (which is usually the same set of systems every time).
  3. You need to call some helper functions to initialize builders and other factory objects. And those must be called before creating the simulation itself, which is error prone.
  4. Don’t forget to set the Logger level.
  5. You also need to create asset managers and settings. The later must parse command line arguments before the simulation is created.
  6. While we could implement the main loop ourselves, we usually end up calling the helper function Simulation::run() most of the time.

Only after completing all those steps correctly we can start creating the actual scene and rendering compositions. Oh, did I forget to mention that the later one is pretty much the same in most situations?

If you’re wondering how these process looks in practice, here’s an example of how to implement a simple simulation that loads an OBJ file and renders it on screen.

In the past, I tried to justify these complexity by arguing that it provides a lot of flexibility, which is true. But here I am today, rewriting all demo and realizing that there’s a lot of duplicated code in our applications. And that code needs to be updated whenever the related classes change in the engine (which happens quite frequently. Sorry about that).

I spent the last week doing a lot of simplifications here and there, which resulted in a much simpler simulation workflow. You don’t believe me? Then check out how the same code looks now.

A lot of things are happening under the hood now:

  1. The engine will take care of the main function and all of the internal initialization.
  2. The System class has been upgraded to provide a lot of hooks that are executed during the different stages of the Simulation lifetime.
  3. The Simulation class itself provides two virtual functions that can be used to configure our simulation in a very easy way:
    1. onAwake() is called when the Simulation is about to start, just before any system is initialized. This is a great point to attach your own systems or to remove the default ones and configure the simulation as much as you need.
    2. As the name implies, onStarted() is called after all systems have been started. Here you can create your initial scene and/or a rendering composition. If no rendering composition is provided, the Simulation will use the default one.
  4. CRIMILD_CREATE_SIMULATION() is a macro that is used to tell the engine the class that implements our Simulation, plus a name that will be used as the title of the window.

And that’s it.

No more complicated main files with lots of code that we don’t really care about. Now we can focus on building beautiful scenes without having to worry about the target platform. Did you notice that we’re only include the only the core header file now? That means that the same simulation code could be executed on desktop, web or mobile, since the engine is the one setting up the target platform based on the build settings.

Speaking of building settings, I mentioned that that the building process is complicated and requires some manual steps. Well, guess what?…

Still is.

I didn’t have time to fix that yet, sorry. But I do have it on my list (which for some reason it gets bigger and bigger every day).

The Road to PBR (II)

Once all of the technical requirements were taken care of, it was time to start moving forward with the actual PBR implementation.

A New Material Is Born

By using PBR, we need to specify geometry properties in a more, well, physically correct fashion. Regardless of the object’s color (aka albedo), we also need two new concepts: metalness and roughness. As their name implies, the first one is used to define if an object is made of metal, while the seconds tells us how rough (or soft) its surface is.

With those new concepts in mind, I created a new material class, named LitMaterial. This will be the new default material from now on for all geometries in Crimild.

In the following image, we can see both metalness (decreasing from top to bottom) and roughness (increasing from left to right) in action. The higher the metalness, the more the surface behaves like a mirror. A high roughness makes the surface behave like a rubber ball with no reflections.

From there, adding support for textures was quite straightforward:

In the image above, I’m using textures not only for colors, but also to indicate per-pixel metalness and roughness using individual maps (in a similar way as with specular maps).

But that’s not all…

Image-Based Lighting

A PBR workflow also allows us to use image-based lighting. This means that an objects is lit not only from direct lights, but also based on the current environmental map (i.e. skybox).

The following images show spheres (believe me, they’re plain white spheres), being affected by the corresponding environment.

There’s more to it, though. The environment not only affects how objects are colored (aka, diffuse lighting). We can compute specular reflections as well based on the material’s metalness and roughness values:

Importing Models with PBR properties

Spheres are nice, but nothing better to showcase the power of PBR than an actual model with some great textures:

And that’s how Crimild finally got PBR support.

Is it over?

PBR support is a huge milestone for Crimild and the rendering refactor that I’ve been doing this past (weird) months.

At the moment, performance is not good enough since I’m calculating the irradiance maps every frame (instead of doing it once at the very beginning and reusing it). This is a known issue and it’s related with the fact that the frame graph does not support dynamic scenes (as in adding/removing objects) at the moment.

But there’s one more thing I need to finish before fixing that.

To be continued…