How to 0-to-RTX in 20 lines of code

How to 0-to-RTX in 20 lines of code

and not die tryin'

ยท

6 min read

Tough times for the computer graphics hobbyists who don't love making AAA game engines, or don't want to use one to stitch together a bunch of render passes and drawing a simple scene. Modern APIs such as Vulkan and DirectX 12 are made for those big engines that are backed by teams of people working full time on it, and let plebs like us with their limited free time in the dust.

There are many good abstractions to do even heavy weight, top notch rendering, but the easiest ones are generally based on "old gen" APIs (OpenGL, DirectX 11, โ€ฆ), which is perfectly fine for the majority of people and use cases. As such, I highly recommend Sokol. It ticks all the right boxes for me, being simple, lightweight, easy to integrate to any workflow (C API: use it with any language you want), and he even shows that it's also nice and convenient to use with idiomatic modern C. Plus it's truly multiplatform and can run on Windows, Linux, Mac OS, iOS, the web.

It's already amazing for a bright mind to come up with such an abstraction layer, but as mentioned by Sokol's author, adding support of Vulkan would require more code to write and maintain than (possibly a multiple of) all of the other backends combined. And then you would also want to add Metal support (which is not as complex, to be fair, but still), etc.

Raylib is also excellent. It's not lightweight but it bundles everything you will ever need and it's also super simple and convenient to use. But same thing applies regarding the support of modern graphics APIs (no rays in Raylib ๐Ÿ˜†).

So if you want the hottest features, like ray tracing, or vendor agnostic mesh shaders, variable rate shading, tensor cores, etc, you have to look at what is done on the side of modern APIs. It's a lot more difficult to find something that suits your needs, as modern APIs expose so much flexibility and details that it's probably impossible to solve every use cases for everybody in an optimal way, so quirks and oddities will inevitably bubble up in any abstraction. And it requires so much work to solve even seemingly mundane tasks (memory/resource management, synchronization, โ€ฆ) that many promising projects are either dead or missing important modern features (the ones that makes you move on from old gen APIs).

You could do your own and start with raw Vulkan/DX12, I did and yeah you can end up (eventually) with something that is tailor made to your needs if you make it through... and also a mountain of code to maintain all by yourself, and clearly it was too much for me. So that's why I ended up using NVRHI.

Reasons for choosing this one is that it will likely be maintained in the foreseeable future as even Nvidia needs an abstraction layer to make demos to showcase their shiny new hardware. So it's also pretty guaranteed for new features to be exposed too. And it's also quite nice to use. For example, I like not to have to worry too much about resources life cycle, synchronizations, without resorting to a big gun frame graph.

After abstracting it some more (shader I/O, C API, โ€ฆ) and wrapping it with anything I need to make a demo / application (windowing, events, inputs, ...), I could put it to the test with several demos, like meta balls (compute marching cubes, then it gets ray traced for the refractions/reflections), volumetric fog (using tessellation, as its an omnidirectional version of this), and even some ReSTIR:

Ray traced marching cubes (if it makes sense...)

And it was refreshing not constantly having to do tedious paperwork to please the French administration. I'm currently working toward making it usable for other people, which is not so easy because of some of my weird choices, such as having a C API that is incompatible with MSVC, even though I work on Windows (so I can't easily make it work for Linux), I use some GNU extensions to C (destructors on variables, nested functions, arithmetic operators on vectors (SSE), ...). And to avoid asking to install more than a Mingw compiler, I plan on using plain old Makefiles. Yeah, it's not that sexy anymore huh? ๐Ÿ˜…

Now that you are warned, here is a classic hello triangle:

And all of the C code specific for this demo (excluding shaders):

#include <engine.h>
int main(int, char**) {
    SCOPED(Application) app = initApplication("Hello Triangle", 1024, 768, SRGB_FLAG | VSYNC_FLAG);
    SCOPED(Shader) shader = loadShader(SHADER_DIR "shader");

    void myDraw() {
        useFramebuffer(getSwapchainFramebuffer());
        useShader(shader);
        drawSubMesh(NullMesh, 0, 3);
    }

    bool myEvents() {return KEY_PRESSED(ESCAPE);}
    launchApplication(myDraw, myEvents, NULL);
    return 0;
}

That's orders of magnitude less painful than doing it in raw Vulkan.

But hey! We didn't read this wall of text just to see some lame old fashioned rasterization, did we?

Fair enough:

#include <engine.h>

static Mesh createHelloTriangleMesh() {
    const float vertices[][2] = {{-0.5f, 0.5f}, {0.0f, -0.5f}, {0.5f, 0.5f}};
    Mesh mesh = createMesh(PrimitiveType_Triangles);
    addMeshAttrib(mesh, RG32_FLOAT, false, vertices, ARRAY_SIZE(vertices));
    return mesh;
}

int main(int, char**) {
    SCOPED(Application) app = initApplication("Hello Triangle Raytracing", 1024, 768, VSYNC_FLAG | RAYTRACING_FLAG);
    SCOPED(Shader) shader = loadShader(SHADER_DIR "shader");
    SCOPED(Mesh) mesh = createHelloTriangleMesh();
    SCOPED(AccelerationStructure) as = createAccelerationStructure(mesh, false);

    void myDraw() {
        useShader(shader);
        setUniformAccelerationStructure(as);
        setUniformTexture(getSwapchainTexture(), "Framebuffer");
        dispatch2D(getWidth(), getHeight());
    }

    bool myEvents() {return KEY_PRESSED(ESCAPE);}
    launchApplication(myDraw, myEvents, NULL);
    return 0;
}

I didn't expose ray tracing pipelines, as ray queries are much simpler, and seemingly without drawbacks (it seems they are at least as fast, with one notable exception that they don't support the Shader Execution Reordering feature of the RTX 40 family). So here is the compute shader, featuring ray queries:

#extension GL_EXT_ray_query: require
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;

uniform accelerationStructureEXT Accel;
uniform writeonly image2D Framebuffer;

void main() {
    vec2 Resolution = vec2(gl_NumWorkGroups.xy * gl_WorkGroupSize.xy);
    vec3 Color = vec3(0.0);

    vec3 O = vec3(2.0 * gl_GlobalInvocationID.xy / Resolution - 1.0, -1.0);
    vec3 D = vec3(0.0, 0.0, 1.0);
    rayQueryEXT query;
    rayQueryInitializeEXT(query, Accel, 0, 0xFF, O, 1e-3, D, 1e3);
    rayQueryProceedEXT(query);

    if (rayQueryGetIntersectionTypeEXT(query, true) != 0) {
        // we touched something
        Color.gb = rayQueryGetIntersectionBarycentricsEXT(query, true);
        Color.r = 1.0 - Color.g - Color.b;
    }

    imageStore(Framebuffer, ivec2(gl_GlobalInvocationID), vec4(Color, 1.0));
}

Hard to believe that this underwhelming triangle is actually ray traced. And it's darker because we can't write to sRGB textures from compute shaders (and I didn't bother doing the gamma correction). So note that contrary to last time, you should not ask for an sRGB swapchain at application initialization, or else anything can happen. Also, we need to request ray tracing feature, and thus this sample won't run if your GPU doesn't support hardware ray tracing.

Hey, it's longer than the 20 loc your clickbait-y title promised!

Yeah... And on that bombshell, it's time to conclude. Overall, this framework fulfills my needs very well, although it's not complete and won't be usable to everybody. But I would like to use it when I want to accompany a blog post with a sample application, and have something that is easy to read and to the point. At least I hope I can spark some interest in improving the average hobbyist ecosystem, we need more lightweight and simple things!

For now, I started a Github repository showing some more basic examples. You may start using this framework today, but uncommented header files and basic examples are all the documentation you'll have for now. But I'll be glad to help if you have questions.

I would also be glad to hear about your experience if you have been in the same situation, what do you like to use, or what would you dream to have.

ย