Posts
Search
Contact
Cookies
About
RSS

BGFX C API taking it further

Added 18 Apr 2021, 11:08 a.m. edited 18 Jun 2023, 1:12 a.m.

Initially just to start getting to grips with BGFX I simply ported a simple C++ example to C, while avoiding using various parts of the example framework. While this proved that BGFX was indeed a viable prospect when used from C, a few multi-coloured cubes isn't so useful, so I decided to take a scratch written Wavefront OBJ loader I wrote some time ago, and get that working with BGFX. I've (eventually!) managed to add a very simple lighting shader (which needs work...) That can handle multiple point lights of different colours. Both a diffuse colour and diffuse texture is used from the OBJ's material file, You might recognise in the screen shot an ambulance from Kenney.nl This is completely untextured relying just on diffuse colours (a great trick for low-poly style models)

As BGFX has no "framework" around it, it is by necessity fairly low level, to begin with its worth looking a just what we need to do to get BGFX initialised from a C perspective, before we even render anything. I'm using GLFW to provide support like window creation and handling, mainly because I like it and I'm familiar with it. When creating a window for use with Vulkan its sufficient just to use the GLFW_NO_API window hint. One gotcha I found quite quickly was that when providing info for BGFX to create a Vulkan context, is that you need to give it the X11 Display as well as the native window handle, where as when creating an OpenGL context you have to supply the GLX window and context. In either case simply supplying a native windows handle isn't sufficient, and unfortunately results in a seg fault without any other feedback.

Setting up a shader is probably a little different than you're used to, while its common to load shader source for a vertex and fragment shader and have OpenGL compile them into a shader program, with BGFX you must supply the shaders in binary form. While this might seem an issue, fortunately the tools supplied make it easy to pre-compile your shaders (a simple make rule to call the BGFX shader compiler is all that's needed), once you have a shader program created, the usual routine of looking up uniforms is straight forward.

Telling BGFX what format you want for your vertex buffer is interesting and diverges a little from the C++ syntax so it worth look at in detail. This is the main reason its necessary to initialise my model loader, but I do also use the opportunity to create a "default" texture too.

// call this before any other model function
void initModels()
{
    bgfx_vertex_layout_begin(&objvLayout, bgfx_get_renderer_type());
    bgfx_vertex_layout_add(&objvLayout, BGFX_ATTRIB_POSITION, 3, BGFX_ATTRIB_TYPE_FLOAT, false, false);
    bgfx_vertex_layout_add(&objvLayout, BGFX_ATTRIB_NORMAL, 3, BGFX_ATTRIB_TYPE_FLOAT, false, false);
    bgfx_vertex_layout_add(&objvLayout, BGFX_ATTRIB_TEXCOORD0, 2, BGFX_ATTRIB_TYPE_FLOAT, false, false);
    bgfx_vertex_layout_end(&objvLayout);
    
    layoutH = bgfx_create_vertex_layout(&objvLayout);
    
    // default texture
    const bgfx_memory_t* txMem = bgfx_make_ref(defaultTxData, 4); 
    defaultTx = bgfx_create_texture_2d(1, 1, false, 0, BGFX_TEXTURE_FORMAT_RGBA8, 0 , txMem);
}

Successive calls to layout_add, allow you to specify any layout you like, in this case I only need the vertex position, a normal and texture coordinate. I'm not using indexes instead just passing a whole heap of triangles, while initially this might seem wasteful and even inefficient, alas things are not as straightforward as they first appear. While what looks like a single vertex on the corner of a cube has to be in fact three vertexes. While the position data is the same there are different normal and UV coordinates, with later versions of GL there are tricks you can do to get around this, however its much simpler and usually faster to render if you just accept some replication of data.

The model loader makes use of a single 1x1 white pixel "default" texture for models that don't specify a diffuse map in the material as mentioned earlier. A fair bit of the loader after parsing is concerned with breaking out the vertices into separate meshes per material. When a model is submitted for rendering, each material mesh is rendered in turn so that the complete model is built from separate material meshes.

    Model amb = LoadObj("data/ambulance.obj");
    applyObjShader(&amb, program, diffcU, difftxU);

All this detail is taken care of by the model code, so once you have loaded a model and applied shaders to the different materials (here I'm just using the same simple shader for every material) then all you need to do is set up the appropriate matrix for the model and call a function to submit the model for rendering.

Before looking at rendering its worth a quick look at the model structures

typedef struct Model {
    int meshCount;
    int materialCount;
    
    int *meshMaterial;
    Material *materials;
    Mesh *meshes;
    
    mat4_t transform;
} Model;

After loading the mesh and material count are basically counting the same thing as there is a material for each mesh the array of meshMaterial indexes is often just a flat 0-n but it need not be it depends on how the OBJ has been created.

typedef struct Material 
{
    bgfx_program_handle_t shader;
    
    Colour diffuse;
    bgfx_texture_handle_t difftx;
    
    bgfx_uniform_handle_t diffcU;
    bgfx_uniform_handle_t difftxU;
    
    // TODO other properties here like specular etc
} Material;

There is some set up that needs to be done to the materials before they can be rendered, a BGFX shader program handle is needed for the material and also uniform locations for the shaders diffuse colour and texture are needed, as above if you are just using a single shader for all the models materials you can set them all up at once with applyObjShader which simply loops through all the models materials setting the required BGFX handles for the material

typedef struct Mesh {
    int vertexCount;
    float *buffer;
    bgfx_vertex_buffer_handle_t vbh;
} Mesh;

I've kept hold of the vertex data, but as its already with BGFX (on the GPU) once loaded, you could free this straight away and it does render just fine (you'd have to adjust freeModel accordingly!) I haven't decided yet if there really is any point keeping hold of it, possibly you might want to reference it when altering a dynamic buffer but that's something for a later date.

About time we got on with some actual rendering!

        bgfx_set_view_rect(0, 0, 0, (uint16_t)WNDW_WIDTH, (uint16_t)WNDW_HEIGHT);
   
        vec3_t at = vec3(0.0f, 1.0f,  0.0f);
        vec3_t eye = vec3(sin(camAng) * 12, 8, cos(camAng)*12 );
        vec3_t up = vec3(0.0f, 1.0f, 0.0f);
        mat4_t view = m4_look_at( eye, at, up );
        mat4_t proj = m4_perspective(60.0f, (float)(WNDW_WIDTH) / (float)(WNDW_HEIGHT),  0.01f, 1000.f);

        bgfx_set_view_transform(0, &view, &proj);

Setting up the "camera" is straight forward, BGFX just requires a view and projection matrix, which are easily supplied by any 3D maths routines that can produce the appropriate matrices. I ended up quickly grabbing a single header set of math functions math_3d.h I added a few functions for my own convenience and a padding float to make passing vec3's to shaders easier (where the only input seems to be in blobs of 4 floats at a time...)

As mentioned thanks to the support routines provided in the model loader, actually submitting a model is equally straight forward

        mtx = m4_rotation_zyx(rv);
        mtx = m4_set_translation(mtx, vec3(-3,2,0));
        bgfx_set_transform(&mtx, 1);

        submitModel(&amb);

The only detail left really is tidying up when the application wants to quit. The vertex layout is freed with a call to shutdownModels each model should be freed with freeModel which releases all resources including any textures in use, naturally the shader uniforms, and shader need to be freed with the appropriate BGFX calls. One of the many things that I intend to add is to bundle the shader and uniforms into a stuct to simplify initialisation and shutdown.

Its been a fair bit of work to use BGFX from C without wrapping any of the BGFX example framework, but what I've ended up with, although it still bares improvement, is already shaping up to be a lightweight yet powerful framework for using BGFX.

The full project is available on bitbucket here but do please look at readme.md or you'll get nowhere fast!

Enjoy!