glTF Loading

#include <safi/render/model.h>       // SafiModel — recommended
#include <safi/render/gltf_loader.h> // safi_gltf_load — low-level, single primitive

SafiEngine provides two glTF loading paths built on cgltf and stb_image:

APIScopeUse case
safi_model_loadAll meshes, all primitives, multi-materialLoading complete models
safi_gltf_loadFirst mesh, first primitive onlyQuick single-geometry tests

SafiModel is a self-contained renderable that holds merged geometry, per-primitive draw ranges, and per-material textures for an entire glTF file.

Types

typedef struct SafiModelPrimitive {
    uint32_t index_offset;     /* first index (element offset, not bytes) */
    uint32_t index_count;      /* number of indices */
    uint32_t material_index;   /* into SafiModel.base_colors[] */
} SafiModelPrimitive;

typedef struct SafiModel {
    /* Shared GPU geometry */
    SDL_GPUBuffer *vbo;
    SDL_GPUBuffer *ibo;
    uint32_t       vertex_count;
    uint32_t       index_count;

    /* Per-primitive draw ranges */
    SafiModelPrimitive *primitives;
    uint32_t            primitive_count;

    /* Per-material textures (pipeline + sampler are shared) */
    SDL_GPUTexture         **base_colors;    /* [material_count] */
    uint32_t                 material_count;
    SDL_GPUGraphicsPipeline *pipeline;       /* shared unlit pipeline */
    SDL_GPUSampler          *sampler;        /* shared sampler */
    SDL_GPUTexture          *white_fallback; /* 1x1 white for untextured materials */

    float aabb_min[3];
    float aabb_max[3];
} SafiModel;

Functions

safi_model_load

bool safi_model_load(SafiRenderer *r,
                     const char   *path,
                     const char   *shader_dir,
                     SafiModel    *out);
ParameterDescription
rActive renderer
pathPath to .gltf or .glb file
shader_dirDirectory containing compiled unlit shader artifacts
outZero-initialised; receives the loaded model

The loader:

  1. Parses all meshes and primitives in the glTF file
  2. Merges vertices and indices into a single shared VBO/IBO
  3. Deduplicates materials — each unique cgltf_material gets one texture slot
  4. Loads base-color textures (embedded or external URI); falls back to a solid color from baseColorFactor or a 1x1 white texture
  5. Computes an axis-aligned bounding box across all vertices
  6. Creates a shared unlit pipeline and sampler

Returns false on parse/load/upload error.

safi_model_draw

void safi_model_draw(SafiRenderer    *r,
                     const SafiModel *model,
                     const float     *mvp);

Must be called inside an active render pass. Binds the shared VBO/IBO once, then loops over primitives — binding the correct material texture and issuing an indexed draw call for each.

ParameterDescription
rActive renderer (with open render pass)
modelA loaded SafiModel
mvpPointer to 16 floats (4x4 model-view-projection matrix)

safi_model_destroy

void safi_model_destroy(SafiRenderer *r, SafiModel *model);

Releases all GPU resources (VBO, IBO, pipeline, sampler, textures) and zeroes the struct.

Example

main.c
SafiModel model;
if (!safi_model_load(&app.renderer, "assets/models/scene.glb",
                     SAFI_DEMO_SHADER_DIR, &model)) {
    SAFI_LOG_ERROR("failed to load model");
    return 1;
}

// In the render pass:
safi_model_draw(&app.renderer, &model, (const float *)mvp);

// Cleanup:
safi_model_destroy(&app.renderer, &model);

safi_gltf_load (low-level)

Minimal single-primitive loader. Loads only the first mesh's first primitive into a SafiMesh + SafiMaterial pair.

bool safi_gltf_load(SafiRenderer *r,
                    const char   *path,
                    SafiMesh     *out_mesh,
                    SafiMaterial *inout_material);
ParameterDescription
rActive renderer
pathPath to .gltf or .glb
out_meshZero-initialised; receives the uploaded VBO/IBO
inout_materialMust already be created; base-color texture is set if present

Limitations

  • Only the first primitive of the first mesh
  • No node graph / transforms
  • No skeletal animation or morph targets
  • Missing normals are replaced with (0, 1, 0)
  • Indices are always converted to uint32_t

For multi-primitive or multi-material models, use safi_model_load instead.

Example

SafiMaterial mat;
safi_material_create_unlit(&app.renderer, &mat, SAFI_DEMO_SHADER_DIR);

SafiMesh mesh;
if (!safi_gltf_load(&app.renderer, "assets/models/BoxTextured.glb",
                    &mesh, &mat)) {
    SAFI_LOG_ERROR("failed to load model");
    return 1;
}

Supported attributes

Both loaders extract the same vertex attributes from glTF accessors:

AttributeglTF accessorFallback
PositionPOSITIONRequired
NormalNORMAL(0, 1, 0)
Texture coordinateTEXCOORD_0(0, 0)

See also