Repository navigation
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.
-
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.
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 # macOSOr 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-releasecmake --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.
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 # LinuxA 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.
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.
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.
#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.
-
vgl_pack_createreturns at once. The pack reads its manifest (pack.vglm) in the background and isVGL_PACK_STATE_LOADINGuntil then;vgl_pack_state_gettells you when it isVGL_PACK_STATE_READYorVGL_PACK_STATE_FAILED, and why. - Each
vgl_context_updateprocesses 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_drawtakes 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.
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.
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.
| 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.
- Core Concepts: the architecture, coordinate frames, depth conventions, budgets, threading and the frame lifecycle.
- Context API: every function of the context, packs, queries and statistics.
- Host Interfaces: your own I/O, tasks, allocator and log instead of the defaults.
- Defaults Module: the thread pool, file reader, HTTP reader and disk cache.
- OpenGL Backend, Direct3D 11 Backend, Ogre Backend, or the Render Interface to draw with your own renderer.
- Atmosphere, Stars and Clouds: the backends' optional extras.
- Baker and Hosting Packs: make your own packs and serve them. The project hosts no data; you bake and host the packs you need.
- Web Builds and Mobile Platforms.
Getting vgl
Using the library
Rendering
Data
Reference
Specifications