Skip to main content
This tutorial shows how to build a complete glTF 2.0 viewer with Filament. You’ll learn how to load glTF files, handle textures and materials, set up proper lighting, and add UI controls.

Overview

The glTF viewer demonstrates:
  • Loading glTF 2.0 files (.gltf and .glb)
  • Asynchronous resource loading (textures, buffers)
  • Material variants and optimization
  • Camera controls and animation
  • IBL and lighting setup
  • Interactive UI with ImGui
  • Debugging tools and visualization

Architecture

The viewer is built with several key components:
struct App {
    Engine* engine;
    ViewerGui* viewer;              // UI and settings
    AssetLoader* assetLoader;       // Loads glTF structure
    FilamentAsset* asset;           // Loaded glTF asset
    ResourceLoader* resourceLoader;  // Loads textures/buffers
    MaterialProvider* materials;    // Provides materials
};

Complete Implementation

1

Initialize Asset Loader

Set up the glTF asset loader with material provider:
// Choose material source: JIT compilation or ubershaders
app.materials = (app.materialSource == JITSHADER)
    ? createJitShaderProvider(engine, OPTIMIZE_MATERIALS)
    : createUbershaderProvider(engine, UBERARCHIVE_DEFAULT_DATA, 
                               UBERARCHIVE_DEFAULT_SIZE);

app.assetLoader = AssetLoader::create({ engine, app.materials, app.names });
Material sources:
  • JIT Shader: Compiles materials on-demand (slower load, optimized shaders)
  • Ubershader: Pre-compiled uber-shader (faster load, more shader complexity)
2

Load glTF File

Read the glTF file and create the asset:
auto loadAsset = [&app](const utils::Path& filename) {
    // Read file into buffer
    std::ifstream in(filename.c_str(), std::ifstream::binary);
    std::vector<uint8_t> buffer(static_cast<unsigned long>(contentSize));
    in.read((char*) buffer.data(), contentSize);

    // Parse glTF and create Filament entities
    app.asset = app.assetLoader->createAsset(buffer.data(), buffer.size());
    if (!app.asset) {
        std::cerr << "Unable to parse " << filename << std::endl;
        exit(1);
    }

    app.instance = app.asset->getInstance();
};
The createAsset function:
  • Parses the glTF JSON structure
  • Creates Filament entities for all nodes
  • Sets up materials and renderables
  • Does NOT load textures yet (async operation)
3

Set Up Resource Loader

Configure resource loader with texture providers:
ResourceConfiguration configuration = {};
configuration.engine = app.engine;
configuration.gltfPath = gltfPath.c_str();
configuration.normalizeSkinningWeights = true;

app.resourceLoader = new gltfio::ResourceLoader(configuration);

// Add texture decoders
app.stbDecoder = createStbProvider(app.engine);      // PNG, JPEG
app.ktxDecoder = createKtx2Provider(app.engine);     // KTX2
app.resourceLoader->addTextureProvider("image/png", app.stbDecoder);
app.resourceLoader->addTextureProvider("image/jpeg", app.stbDecoder);
app.resourceLoader->addTextureProvider("image/ktx2", app.ktxDecoder);

if (isWebpSupported()) {
    app.webpDecoder = createWebpProvider(app.engine);
    app.resourceLoader->addTextureProvider("image/webp", app.webpDecoder);
}
4

Load Resources Asynchronously

Start async loading and monitor progress:
// Start loading textures and buffers
if (!app.resourceLoader->asyncBeginLoad(app.asset)) {
    std::cerr << "Unable to start loading resources" << std::endl;
    exit(1);
}

// In the animate callback, update loading
auto animate = [&app](Engine*, View*, double now) {
    app.resourceLoader->asyncUpdateLoad();
    
    // Check progress
    float progress = app.resourceLoader->asyncGetLoadProgress();
    if (progress < 1.0) {
        // Still loading...
    }
};
Async loading prevents blocking the main thread during texture decompression.
5

Pre-compile Material Variants

Compile common material variants to reduce runtime stuttering:
// Collect all unique materials
std::set<Material*> materials;
RenderableManager const& rcm = app.engine->getRenderableManager();
for (Entity const e : app.asset->getRenderableEntities()) {
    auto ri = rcm.getInstance(e);
    size_t const count = rcm.getPrimitiveCount(ri);
    for (size_t i = 0; i < count; i++) {
        MaterialInstance* const mi = rcm.getMaterialInstanceAt(ri, i);
        materials.insert(const_cast<Material*>(mi->getMaterial()));
    }
}

// Pre-compile high priority variants
for (Material* ma : materials) {
    ma->compile(Material::CompilerPriorityQueue::HIGH,
            UserVariantFilterBit::DIRECTIONAL_LIGHTING |
            UserVariantFilterBit::DYNAMIC_LIGHTING |
            UserVariantFilterBit::SHADOW_RECEIVER);

    // Compile remaining variants at low priority
    ma->compile(Material::CompilerPriorityQueue::LOW,
            UserVariantFilterBit::FOG |
            UserVariantFilterBit::SKINNING |
            UserVariantFilterBit::SSR);
}
6

Configure Image-Based Lighting

Set up IBL from the configuration:
auto setupIBL = [&app]() {
    auto ibl = FilamentApp::get().getIBL();
    if (ibl) {
        app.viewer->setIndirectLight(ibl->getIndirectLight(), 
                                    ibl->getSphericalHarmonics());
        app.viewer->getSettings().view.fogSettings.fogColorTexture = 
            ibl->getFogTexture();
    }
};
FilamentApp loads IBL from config.iblDirectory.
7

Set Up Viewer GUI

Initialize the viewer with UI controls:
app.viewer = new ViewerGui(engine, scene, view, 410);
app.viewer->setAsset(app.asset, app.instance);
app.viewer->getSettings().viewer.autoScaleEnabled = !app.actualSize;
ViewerGui provides:
  • Material editor
  • Lighting controls
  • Camera settings
  • Debug visualization
  • Animation controls
8

Handle Camera Selection

Support glTF cameras and free camera:
auto preRender = [&app](Engine* engine, View* view, Scene* scene, Renderer*) {
    view->setCamera(app.mainCamera);  // Default free camera

    int const currentCamera = app.viewer->getCurrentCamera();
    size_t const cameraCount = app.asset->getCameraEntityCount();
    
    if (currentCamera > 0 && currentCamera <= cameraCount) {
        // Use glTF camera
        const utils::Entity* cameras = app.asset->getCameraEntities();
        Camera* camera = engine->getCameraComponent(cameras[currentCamera - 1]);
        view->setCamera(camera);

        // Adjust aspect ratio
        const Viewport& vp = view->getViewport();
        double aspectRatio = (double) vp.width / vp.height;
        camera->setScaling({1.0 / aspectRatio, 1.0});
    }
};
9

Add Animation Support

Apply glTF animations:
auto animate = [&app](Engine*, View*, double now) {
    auto const& animSettings = app.viewer->getSettings().animation;
    if (animSettings.enabled) {
        double animTime = now;
        if (animSettings.time >= 0.0f) {
            animTime = animSettings.time;  // Manual time
        } else {
            animTime *= animSettings.speed;  // Scaled time
        }
        app.viewer->applyAnimation(animTime);
    }
};
10

Handle Model Scaling

Optionally fit models into a unit cube:
app.viewer->updateRootTransform();
This centers and scales the model for consistent viewing, unless --actual-size is specified.
11

Implement Entity Picking

Enable clicking on objects to show their names:
static void onClick(App& app, View* view, ImVec2 pos) {
    view->pick(pos.x, pos.y, [&app](View::PickingQueryResult const& result) {
        if (const char* name = app.asset->getName(result.renderable); name) {
            app.notificationText = name;
        }
    });
}
12

Add Ground Plane

Create a shadow-receiving ground plane:
static void createGroundPlane(Engine* engine, Scene* scene, App& app) {
    // Load shadow material
    Material* shadowMaterial = Material::Builder()
        .package(GLTF_DEMO_GROUNDSHADOW_DATA, GLTF_DEMO_GROUNDSHADOW_SIZE)
        .build(*engine);

    // Calculate plane size based on model bounds
    Aabb aabb = app.asset->getBoundingBox();
    float3 planeExtent{10.0f * aabb.extent().x, 0.0f, 10.0f * aabb.extent().z};

    // Create plane geometry
    const static float3 vertices[] = {
        { -planeExtent.x, 0, -planeExtent.z },
        { -planeExtent.x, 0,  planeExtent.z },
        {  planeExtent.x, 0,  planeExtent.z },
        {  planeExtent.x, 0, -planeExtent.z },
    };

    // Build and add to scene...
}
13

Cleanup Resources

Properly destroy all resources:
auto cleanup = [&app](Engine* engine, View*, Scene*) {
    app.resourceLoader->asyncCancelLoad();
    app.assetLoader->destroyAsset(app.asset);
    app.materials->destroyMaterials();
    
    delete app.viewer;
    delete app.materials;
    delete app.resourceLoader;
    delete app.stbDecoder;
    delete app.ktxDecoder;
    delete app.webpDecoder;
    
    AssetLoader::destroy(&app.assetLoader);
};

Key Features

Material Optimization

Choose between two material strategies:
# JIT shaders (default) - optimized but slower load
./gltf_viewer model.gltf

# Ubershaders - faster load but more complex shaders
./gltf_viewer --ubershader model.gltf

Camera Modes

Two camera control modes:
# Orbit mode (default) - rotate around model
./gltf_viewer --camera=orbit model.gltf

# Flight mode - free flying camera
./gltf_viewer --camera=flight model.gltf
Flight controls:
  • Mouse drag: Pan camera
  • Scroll wheel: Adjust speed
  • W/S: Forward/backward
  • A/D: Left/right
  • E/Q: Up/down

Feature Levels

Specify rendering feature level:
# Feature level 1 (OpenGL ES 3.0, WebGL 2.0)
./gltf_viewer --feature-level=1 model.gltf

# Feature level 2 (more advanced features)
./gltf_viewer --feature-level=2 model.gltf

# Feature level 3 (highest quality)
./gltf_viewer --feature-level=3 model.gltf

Automation and Testing

Run automated tests:
# Run with default test suite
./gltf_viewer --batch=default --headless model.gltf

# Run with custom test spec
./gltf_viewer --batch=tests.json --headless model.gltf

glTF Support

Filament supports glTF 2.0 features:

Core Features

  • Meshes with multiple primitives
  • Node hierarchy and transforms
  • PBR metallic-roughness materials
  • Normal maps, occlusion, emissive
  • Texture transforms
  • Multiple UV sets

Extensions

  • KHR_materials_unlit
  • KHR_materials_emissive_strength
  • KHR_texture_transform
  • KHR_mesh_quantization
  • KHR_lights_punctual

Animations

  • Translation, rotation, scale
  • Morph targets
  • Skinning (with normalization)

Performance Tips

Texture Compression

Use KTX2 for faster loading:
# Convert textures to KTX2
toktx --t2 --bcmp texture.ktx2 texture.png

Material Compilation

Pre-compile materials for your target platform to avoid runtime compilation.

Asset Optimization

Optimize glTF files:
  • Remove unused materials and textures
  • Use Draco compression for geometry
  • Combine meshes where possible
  • Use appropriate texture resolutions

Building and Running

# Build the viewer
cmake --build . --target gltf_viewer

# Run with built-in model
./gltf_viewer

# Load custom model
./gltf_viewer path/to/model.gltf

# With custom IBL
./gltf_viewer --ibl=path/to/ibl/ model.gltf

Next Steps

Build docs developers (and LLMs) love