Skip to content

Getting Started

Jeremiah van Oosten edited this page Oct 19, 2026 · 2 revisions

Getting started

This page is the shortest path from a fresh clone to a globe on screen. It builds vgl with a CMake preset, runs the OpenGL sample on the small sample data that ships in the repository, and then walks through a minimal integration in C: a context with the defaults module's thread pool and file reader, an elevation pack, a geoid pack and an imagery pack, and a per-frame loop of view, update and draw with the OpenGL backend. It ends with pointers to the pages that go deeper. It describes vgl 1.0; the changelog lists what changes after it.

What you need

  • CMake 3.25 or later (for the presets; a plain configure needs 3.24) and a C++20 compiler: Visual Studio 2022 on Windows, GCC or Clang on Linux, Apple Clang on macOS.

  • Ninja on Linux and macOS (the presets use the Ninja Multi-Config generator).

  • On Linux, the X11 and Wayland development headers that GLFW needs for the samples, for example on Ubuntu:

    sudo apt-get install ninja-build libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev \
        libxi-dev libwayland-dev libxkbcommon-dev libgl-dev
  • A network connection for the first configure: with VGL_FETCH_DEPENDENCIES (on by default), CMake downloads Zstandard, GLFW and Dear ImGui at pinned versions when they are not installed.

Nothing else is needed for the runtime. GDAL is only needed for the Baker, which is off by default. More options and platforms are on Building vgl.

1. Build with a preset

Clone the repository and run a workflow preset, which configures, builds and runs the tests in one step:

git clone https://github.com/3dgep/vgl.git
cd vgl
cmake --workflow --preset windows-msvc-static-release   # Windows
cmake --workflow --preset linux-gcc-static-release      # Linux (or linux-clang-static-release)
cmake --workflow --preset macos-clang-static-release    # macOS

Or run the steps yourself, which is handier while you change things:

cmake --preset windows-msvc-static
cmake --build --preset windows-msvc-static-release
ctest --preset windows-msvc-static-release

cmake --list-presets=all lists every preset. Each configure preset builds into build/<preset>, and executables land in build/<preset>/bin/<configuration>, for example build/windows-msvc-static/bin/Release/vgl-gl.exe or build/linux-gcc-static/bin/Release/vgl-gl.

Tip: run the tests from a Release build. They run several times faster than in Debug.

2. Run the OpenGL sample

The samples read the sample set in samples/data from the source tree by default: GEBCO elevation with bathymetry, the EGM2008 geoid, NASA Blue Marble imagery, and Copernicus GLO-30 elevation and Sentinel-2 cloudless 2016 imagery over the Sognefjord in Norway. They are small, coarse packs meant for the samples and tests only (see samples/data/README.md).

build/windows-msvc-static/bin/Release/vgl-gl          # Windows
build/linux-gcc-static/bin/Release/vgl-gl             # Linux

A window opens with the globe. The navigation works like Google Earth:

Input What it does
Left drag Grab the globe: the point under the cursor stays under it. Over space, spin the globe like a disk.
Scroll, double-click Zoom towards the point under the cursor
Shift + left drag, middle drag Orbit and tilt around the point under the cursor
Ctrl + left drag Look around from where the camera is
Right drag, Page Up, Page Down Zoom
Arrows, WASD Move; with Shift, turn and tilt
N, U, R North up, look straight down, or both
1 to 7 Fly to places (the Sognefjord detail is among them)
F1 The panel: statistics, credits, layers and the camera
F2 to F9 Wireframe, level colours, imagery source, frozen selection, heightmap, contours, sea surface, atmosphere

Useful options:

Option Meaning
--es Use an OpenGL ES 3.0 context
--depth logarithmic|reversed|conventional The depth convention (logarithmic by default; reversed needs clip control)
--data <directory> Read the sample set from another directory (or set VGL_SAMPLE_DATA)
--pack <uri> Load these packs instead of the sample set; repeat it, later packs win where they overlap
--cache <directory> Keep what is read from web servers in a disk cache (with --cache-budget <MiB>)
--check Run hidden, check that the view loads and draws, and exit with the result

The other samples take the same data: vgl-c (the pure-C sample this page is based on), vgl-headless (no GPU), vgl-d3d11 on Windows and vgl-ogre when the Ogre backend is built. See Samples.

3. A minimal integration in C

The design keeps the minimal integration to four steps: create a context, add packs, pass the camera each frame and call the backend's draw function. The host owns everything else: the window, the OpenGL context, the main loop, the camera and the framebuffer. vgl draws into the host's current framebuffer and never clears, swaps or presents.

The program below is a trimmed version of samples/c/main.c. It uses GLFW for the window, but any way of making an OpenGL 3.3 core or OpenGL ES 3.0 context current works.

The CMake project

After cmake --install, or with the vcpkg port or Conan recipe (see Installing vgl):

cmake_minimum_required(VERSION 3.25)
project(my_globe LANGUAGES C CXX)

find_package(vgl REQUIRED)
find_package(glfw3 REQUIRED)

add_executable(my_globe main.c)
target_link_libraries(my_globe PRIVATE vgl::vgl vgl::defaults vgl::gl glfw)
if(UNIX)
    target_link_libraries(my_globe PRIVATE m)
endif()

vgl::vgl is the core, vgl::defaults the optional Defaults module (thread pool and file reader), and vgl::gl the OpenGL backend. The backend links no OpenGL library: it loads every function through a loader the host passes. CXX is enabled because vgl is implemented in C++ and a static build links the C++ runtime. With pkg-config instead: pkg-config --cflags --libs vgl vgl-defaults vgl-gl.

The program

#define GLFW_INCLUDE_NONE
#include <vgl/defaults.h>
#include <vgl/gl.h>
#include <vgl/vgl.h>

#include <GLFW/glfw3.h>

#include <math.h>
#include <stdio.h>

/* The OpenGL functions this host calls itself, to clear its framebuffer. A real host would
 * use its own loader, such as glad. */
#define GL_DEPTH_BUFFER_BIT 0x0100
#define GL_COLOR_BUFFER_BIT 0x4000
typedef void (*clear_color_fn)(float, float, float, float);
typedef void (*clear_fn)(unsigned int);
typedef void (*viewport_fn)(int, int, int, int);

/* The backend loads OpenGL through this. */
static vgl_gl_proc load(void* user_data, const char* name)
{
    (void)user_data;
    return (vgl_gl_proc)glfwGetProcAddress(name);
}

/* Without a log callback, vgl is silent. Error messages say what went wrong and how to fix it. */
static void log_message(void* user_data, vgl_log_level level, const char* message)
{
    (void)user_data;
    (void)level;
    fprintf(stderr, "vgl: %s\n", message);
}

/* A right-handed view matrix (ECEF to camera space) from eye towards the Earth's centre,
 * with +z up. Column-major: element (row r, column c) is m[c * 4 + r]. */
static vgl_dmat4 look_at_earth(vgl_dvec3 eye)
{
    vgl_dmat4 m = {{0.0}};
    const double l = sqrt(eye.x * eye.x + eye.y * eye.y + eye.z * eye.z);
    const vgl_dvec3 f = {-eye.x / l, -eye.y / l, -eye.z / l};
    const double sl = sqrt(f.y * f.y + f.x * f.x);
    const vgl_dvec3 s = {f.y / sl, -f.x / sl, 0.0};
    const vgl_dvec3 u = {s.y * f.z - s.z * f.y, s.z * f.x - s.x * f.z, s.x * f.y - s.y * f.x};
    m.m[0] = s.x;  m.m[4] = s.y;  m.m[8] = s.z;
    m.m[1] = u.x;  m.m[5] = u.y;  m.m[9] = u.z;
    m.m[2] = -f.x; m.m[6] = -f.y; m.m[10] = -f.z;
    m.m[12] = -(s.x * eye.x + s.y * eye.y + s.z * eye.z);
    m.m[13] = -(u.x * eye.x + u.y * eye.y + u.z * eye.z);
    m.m[14] = f.x * eye.x + f.y * eye.y + f.z * eye.z;
    m.m[15] = 1.0;
    return m;
}

/* An OpenGL perspective projection. With the logarithmic depth convention (the default), the
 * near and far planes do not matter. */
static vgl_dmat4 perspective(double fovy, double aspect)
{
    vgl_dmat4 m = {{0.0}};
    const double f = 1.0 / tan(fovy / 2.0);
    const double near_plane = 1.0;
    const double far_plane = 1.0e9;
    m.m[0] = f / aspect;
    m.m[5] = f;
    m.m[10] = (far_plane + near_plane) / (near_plane - far_plane);
    m.m[11] = -1.0;
    m.m[14] = 2.0 * far_plane * near_plane / (near_plane - far_plane);
    return m;
}

int main(void)
{
    /* Pack directories: GEBCO terrain, the geoid, Blue Marble imagery. */
    const char* uris[3] = {"samples/data/packs/gebco",
                           "samples/data/packs/egm2008",
                           "samples/data/packs/bluemarble"};
    vgl_thread_pool* pool = NULL;
    vgl_file_reader* reader = NULL;
    vgl_file_reader_create_info reader_info = {.struct_size = sizeof(reader_info)};
    vgl_task_interface tasks;
    vgl_io_interface io;
    vgl_context_create_info context_info = {.struct_size = sizeof(context_info)};
    vgl_context* context = NULL;
    vgl_pack_create_info pack_info = {.struct_size = sizeof(pack_info)};
    vgl_pack* packs[3] = {NULL, NULL, NULL};
    vgl_gl_backend_create_info backend_info = {.struct_size = sizeof(backend_info)};
    vgl_gl_backend* backend = NULL;
    vgl_gl_draw_info draw_info = {.struct_size = sizeof(draw_info)};
    GLFWwindow* window;
    clear_color_fn gl_clear_color;
    clear_fn gl_clear;
    viewport_fn gl_viewport;
    int frame = 0;
    int i;

    /* The window and its OpenGL 3.3 core context belong to the host. */
    if (!glfwInit())
    {
        return 1;
    }
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
    window = glfwCreateWindow(1280, 720, "my globe", NULL, NULL);
    if (window == NULL)
    {
        glfwTerminate();
        return 1;
    }
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);
    gl_clear_color = (clear_color_fn)glfwGetProcAddress("glClearColor");
    gl_clear = (clear_fn)glfwGetProcAddress("glClear");
    gl_viewport = (viewport_fn)glfwGetProcAddress("glViewport");

    /* Step 1: a context, with the defaults module's thread pool (decoding and reading run on
     * its threads) and file reader. */
    if (vgl_thread_pool_create(NULL, &pool) != VGL_RESULT_SUCCESS)
    {
        return 1;
    }
    reader_info.pool = pool;
    if (vgl_file_reader_create(&reader_info, &reader) != VGL_RESULT_SUCCESS)
    {
        return 1;
    }
    vgl_thread_pool_task_interface_get(pool, &tasks);
    vgl_file_reader_io_interface_get(reader, &io);
    context_info.tasks = &tasks;
    context_info.io = &io;
    context_info.log = log_message;
    if (vgl_context_create(&context_info, &context) != VGL_RESULT_SUCCESS)
    {
        return 1;
    }

    /* Step 2: the packs. What a pack is (elevation, imagery or geoid) comes from its
     * manifest. At one priority, each pack wins over those created before it. */
    for (i = 0; i < 3; ++i)
    {
        pack_info.uri = uris[i];
        if (vgl_pack_create(context, &pack_info, &packs[i]) != VGL_RESULT_SUCCESS)
        {
            fprintf(stderr, "could not add the pack %s\n", uris[i]);
            return 1;
        }
    }

    /* The OpenGL backend, on the OpenGL context that is current. */
    backend_info.get_proc_address = load;
    if (vgl_gl_backend_create(context, &backend_info, &backend) != VGL_RESULT_SUCCESS)
    {
        fprintf(stderr, "could not create the OpenGL backend\n");
        return 1;
    }
    draw_info.sun_direction.x = 1.0; /* Sunlight from +x in ECEF; 0 lights from the camera. */

    while (!glfwWindowShouldClose(window))
    {
        int width;
        int height;
        vgl_view view = {.struct_size = sizeof(view)};
        /* Circle the globe once a minute, at about 20,000 km above it. */
        const double angle = 2.0 * VGL_PI * frame / 3600.0;
        const vgl_dvec3 eye = {2.6e7 * cos(angle), 2.6e7 * sin(angle), 0.8e7};

        glfwPollEvents();
        glfwGetFramebufferSize(window, &width, &height);
        if (width == 0 || height == 0)
        {
            glfwWaitEvents(); /* Minimized: a view needs a non-empty viewport. */
            continue;
        }

        /* Step 3: the camera, in double precision, then an update, which takes what has
         * loaded, selects tiles for the view and requests the missing ones. */
        view.view_matrix = look_at_earth(eye);
        view.projection_matrix = perspective(1.0, (double)width / height);
        view.viewport_width = (uint32_t)width;
        view.viewport_height = (uint32_t)height;
        vgl_context_view_set(context, &view);
        vgl_context_update(context, NULL);

        /* Step 4: the host clears its framebuffer (depth to 1 for the logarithmic
         * convention), and the backend draws the globe into it. */
        gl_viewport(0, 0, width, height);
        gl_clear_color(0.02f, 0.02f, 0.05f, 1.0f);
        gl_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        vgl_gl_backend_draw(backend, &draw_info);
        glfwSwapBuffers(window);
        ++frame;
    }

    /* Leave in order: the backend while its OpenGL context exists, the packs and the context,
     * then the reader and the pool. */
    vgl_gl_backend_destroy(backend);
    for (i = 0; i < 3; ++i)
    {
        vgl_pack_destroy(packs[i]);
    }
    vgl_context_destroy(context);
    vgl_file_reader_destroy(reader);
    vgl_thread_pool_destroy(pool);
    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}

The pack URIs are relative to the working directory, so run it from the repository root, or give absolute paths. The file reader takes UTF-8 paths, with or without a file:// prefix.

What happens when it runs

  • vgl_pack_create returns at once. The pack reads its manifest (pack.vglm) in the background and is VGL_PACK_STATE_LOADING until then; vgl_pack_state_get tells you when it is VGL_PACK_STATE_READY or VGL_PACK_STATE_FAILED, and why.
  • Each vgl_context_update processes the reads and decodes that completed since the last one, selects tiles by screen-space error for the view, and requests what is missing.
  • The first frames show the bare ellipsoid, shaded by height, then coarse terrain and imagery, refining level by level as tiles arrive. No region is ever left empty while it loads.
  • vgl_gl_backend_draw takes a frame from the context, uploads new meshes and textures within an upload budget, and draws one call per tile. It restores the OpenGL state it changes (the list is in vgl/gl.h).

Core Concepts explains this lifecycle in full.

Checking results

Every vgl function returns a vgl_result. Zero is success, positive values are statuses that still count as success (VGL_RESULT_INCOMPLETE, VGL_RESULT_NOT_READY), and negative values are errors. Use VGL_SUCCEEDED(r) and VGL_FAILED(r), and vgl_result_name_get for a printable name. The example above checks only creation, to stay short; a real host should check vgl_context_view_set too, which rejects a non-invertible view matrix or an empty viewport.

4. The same in C++

The header-only C++ wrapper adds RAII handles and returns vgl::expected<T> instead of using out parameters. It throws nothing. The setup from the OpenGL sample (samples/gl/main.cpp) looks like this:

#include <vgl/defaults.hpp>
#include <vgl/gl.hpp>
#include <vgl/vgl.hpp>

vgl::expected<vgl::thread_pool> pool = vgl::thread_pool::create();
vgl::expected<vgl::file_reader> reader = vgl::file_reader::create(*pool);
const vgl_task_interface tasks = pool->task_interface_get();
const vgl_io_interface io = reader->io_interface_get();

auto info = vgl::init<vgl_context_create_info>(); // Zeroed, with struct_size set.
info.tasks = &tasks;
info.io = &io;
info.log = [](void*, vgl_log_level, const char* message) {
    std::fprintf(stderr, "vgl: %s\n", message);
};
vgl::expected<vgl::context> ctx = vgl::context::create(&info);
if (!ctx)
{
    std::fprintf(stderr, "no context: %s\n", vgl::result_name_get(ctx.error()));
    return 1;
}
vgl::expected<vgl::pack> terrain = vgl::pack::create(*ctx, "samples/data/packs/gebco");
vgl::expected<vgl::pack> geoid = vgl::pack::create(*ctx, "samples/data/packs/egm2008");
vgl::expected<vgl::pack> imagery = vgl::pack::create(*ctx, "samples/data/packs/bluemarble");

auto backend_info = vgl::init<vgl_gl_backend_create_info>();
backend_info.get_proc_address = [](void*, const char* name) {
    return reinterpret_cast<vgl_gl_proc>(glfwGetProcAddress(name));
};
vgl::expected<vgl::gl_backend> backend = vgl::gl_backend::create(*ctx, backend_info);

// Each frame:
//     ctx->view_set(&view);
//     ctx->update();
//     backend->draw(&draw_info);

Destroy in reverse order: the backend (with its OpenGL context current), the packs, the context, the reader and the pool. Members declared in creation order do this by themselves.

Common first-run problems

Symptom Likely cause
Nothing in the log at all No log callback is set; vgl is silent without one. Set log_level to VGL_LOG_LEVEL_INFO to see more.
vgl_pack_create returns VGL_RESULT_INVALID_OPERATION The context has no I/O interface (context_info.io is null).
A pack goes to VGL_PACK_STATE_FAILED with VGL_RESULT_NOT_FOUND The URI does not name a pack directory with a pack.vglm; relative paths are relative to the working directory.
A black or empty window No view was set, or the depth buffer was not cleared to 1 for the logarithmic convention.
vgl_gl_backend_create returns VGL_RESULT_NOT_SUPPORTED The OpenGL context is older than 3.3 (or OpenGL ES 3.0), or VGL_DEPTH_CONVENTION_REVERSED_Z was asked for without clip control. The log says which.
A crash at exit The backend was destroyed after its OpenGL context, or the reader or pool before the context.

More on Troubleshooting.

Where to go next

Clone this wiki locally