Repository navigation
OpenGL Backend
The OpenGL backend (vgl/gl.h, C++ wrapper vgl/gl.hpp, CMake target vgl::gl, sources in backends/gl) draws a context's globe into the host's OpenGL 3.3, OpenGL ES 3.0 or WebGL 2 framebuffer. This page covers what it needs from the host (a current context and a function loader), its create and draw parameters, the depth setup per convention, the state it changes and restores, the sea surface and debug views, how to fit it into an existing OpenGL renderer (pass order, MSAA, sRGB), device loss, and the OpenGL and pure-C samples. It is a consumer of the render interface in a library of its own, built on the public C API only, like a third-party backend would be.
- Requirements
- Building and linking
- Quick start
- Loading OpenGL functions
- Creating the backend
- Drawing
- Depth setup per convention
- State it changes and restores
- Sea surface, contours and debug views
- Integrating with an existing OpenGL renderer
- Lost contexts
- The OpenGL and C samples
- Troubleshooting
| Item | Requirement |
|---|---|
| API | OpenGL 3.3 or later (the samples use a core profile) or OpenGL ES 3.0 or later. WebGL 2 through Emscripten counts as OpenGL ES 3.0 |
| Context | Created by the host and current on the calling thread. The backend never touches EGL, WGL, GLX or any windowing library |
| Loader | A function that returns OpenGL entry points, including OpenGL 1.x ones |
| Reversed-Z | Clip control: OpenGL 4.5, GL_ARB_clip_control, or GL_EXT_clip_control on OpenGL ES. Not available on WebGL |
| Optional | Anisotropic filtering (OpenGL 4.6, GL_EXT_texture_filter_anisotropic or GL_ARB_texture_filter_anisotropic), used up to 8x when present |
The shaders are written once, in the dialect shared by GLSL 3.30 and GLSL ES 3.00, with a version and precision preamble chosen at run time (#version 330 core, or #version 300 es with highp precision). The backend links no OpenGL library, so the same binary serves desktop OpenGL and OpenGL ES.
The backend is on by default (VGL_BACKEND_GL, see Building vgl). Link it next to the core:
find_package(vgl CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE vgl::vgl vgl::gl)When vgl is built shared (BUILD_SHARED_LIBS=ON), the backend is a shared library too (vgl-gl). On platforms with pkg-config, the module is vgl-gl. See Installing vgl for vcpkg (feature gl) and Conan (option with_gl).
The four-step integration, in C, as the pure-C sample does it with GLFW:
#include <vgl/defaults.h>
#include <vgl/gl.h>
#include <vgl/vgl.h>
#include <GLFW/glfw3.h>
static vgl_gl_proc load(void* user_data, const char* name)
{
(void)user_data;
return (vgl_gl_proc)glfwGetProcAddress(name);
}
/* After creating the window, making its context current, and creating the vgl context
* and its packs (Getting-Started): */
vgl_gl_backend_create_info backend_info = {0};
backend_info.struct_size = sizeof(backend_info);
backend_info.get_proc_address = load;
vgl_gl_backend* backend = NULL;
if (vgl_gl_backend_create(context, &backend_info, &backend) != VGL_RESULT_SUCCESS)
{
/* the context's log says why */
}
vgl_gl_draw_info draw_info = {0};
draw_info.struct_size = sizeof(draw_info);
draw_info.sun_direction.x = 1.0; /* towards the sun, along the ECEF axes */
while (!glfwWindowShouldClose(window))
{
int width, height;
vgl_view view = {0};
glfwPollEvents();
glfwGetFramebufferSize(window, &width, &height);
view.struct_size = sizeof(view);
view.view_matrix = my_view_matrix(); /* vgl_dmat4, ECEF to camera, double */
view.projection_matrix = my_projection(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);
glViewport(0, 0, width, height);
glClearColor(0.02f, 0.02f, 0.05f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* depth cleared to 1: logarithmic */
vgl_gl_backend_draw(backend, &draw_info);
glfwSwapBuffers(window);
}
vgl_gl_backend_destroy(backend); /* while the OpenGL context exists, before the vgl context */In C++, with the wrapper:
#include <vgl/gl.hpp>
#include <vgl/vgl.hpp>
vgl_gl_proc get_proc_address(void* /*user_data*/, const char* name)
{
return reinterpret_cast<vgl_gl_proc>(glfwGetProcAddress(name));
}
auto info = vgl::init<vgl_gl_backend_create_info>();
info.depth_convention = VGL_DEPTH_CONVENTION_LOGARITHMIC;
info.get_proc_address = get_proc_address;
vgl::expected<vgl::gl_backend> backend = vgl::gl_backend::create(context, info);
if (!backend)
{
std::fprintf(stderr, "backend: %s\n", vgl::result_name_get(backend.error()));
}
// Each frame, after context.update(), in your render pass:
auto draw = vgl::init<vgl_gl_draw_info>();
draw.view_matrix = &view_matrix; // optional, defaults to the context's view
draw.projection_matrix = &projection_matrix; // optional, defaults to the context's view
draw.features = VGL_DRAW_FEATURE_ATMOSPHERE;
static_cast<void>(backend->draw(&draw));vgl::gl_backend destroys the backend in its destructor; make sure that runs while the OpenGL context is still current and before the vgl::context is destroyed.
The host passes a loader in vgl_gl_backend_create_info.get_proc_address:
typedef void (*vgl_gl_proc)(void);
typedef vgl_gl_proc (*vgl_gl_get_proc_address_fn)(void* user_data, const char* name);It is called with names such as "glDrawElements", inside vgl_gl_backend_create and vgl_gl_backend_device_reset only, on the calling thread, with get_proc_address_user_data as user_data. It must also return OpenGL 1.x functions (glGetString, glEnable, glDrawElements...). Loaders that do:
| Loader | Notes |
|---|---|
glfwGetProcAddress |
Used by the samples, natively and on the web through Emscripten's GLFW port |
SDL_GL_GetProcAddress |
Returns void*; cast it to vgl_gl_proc
|
eglGetProcAddress (EGL 1.5) |
EGL 1.5 returns core functions too |
wglGetProcAddress |
Not enough on its own: it returns no OpenGL 1.1 functions. Fall back to GetProcAddress on opengl32.dll, as below |
/* Raw WGL: user_data is the HMODULE of opengl32.dll (LoadLibraryA("opengl32.dll")). */
static vgl_gl_proc load_wgl(void* user_data, const char* name)
{
PROC p = wglGetProcAddress(name);
if (p == NULL || p == (PROC)1 || p == (PROC)2 || p == (PROC)3 || p == (PROC)-1)
{
p = GetProcAddress((HMODULE)user_data, name);
}
return (vgl_gl_proc)p;
}If you already use glad, GLEW or another loader, you can still pass the platform loader: the backend keeps its own table of the functions it uses and never relies on your loader's globals. The version comes from glGetString(GL_VERSION): a string starting with OpenGL ES selects the ES path, and one containing WebGL the WebGL path.
vgl_gl_backend_create_info is a versioned structure: zero-initialize it, set struct_size, then fill in.
| Field | Meaning | Default |
|---|---|---|
struct_size |
sizeof(vgl_gl_backend_create_info) |
Required |
depth_convention |
VGL_DEPTH_CONVENTION_LOGARITHMIC, _REVERSED_Z or _CONVENTIONAL (Render Interface) |
Logarithmic (0) |
get_proc_address |
The loader | Required |
get_proc_address_user_data |
Passed to the loader | Null |
vgl_gl_backend_create(context, &info, &backend) must run on the render thread with the OpenGL context current. It loads the functions, checks the version, compiles and links its programs (terrain, sky and stars), creates a sampler, and tells the context which texture formats it samples (RGBA8). Its objects belong to the current OpenGL context or its share group. It allocates through the context's allocator and logs through the context's log.
| Result | Cause |
|---|---|
VGL_RESULT_SUCCESS |
Created; the log says "drawing with OpenGL [ES] major.minor" at info level |
VGL_RESULT_INVALID_ARGUMENT |
A null pointer, a small struct_size, no loader, or an unknown depth convention |
VGL_RESULT_NOT_SUPPORTED |
Fields of a newer vgl set; or the version is below 3.3 / ES 3.0, a function is missing, no context is current, or reversed-Z without clip control. The log names which |
VGL_RESULT_BACKEND_ERROR |
A shader failed to compile or link; the log has the driver's message |
VGL_RESULT_OUT_OF_MEMORY |
Allocation failed |
A context has at most one render-interface consumer at a time. vgl_gl_backend_destroy deletes the backend's OpenGL objects and calls vgl_context_resources_reset, so a backend created later receives every resource again. Destroy it with its OpenGL context (or one sharing its objects) current, and before the vgl context.
Call vgl_gl_backend_draw(backend, &info) once per frame, after vgl_context_update, inside your render pass, with the target framebuffer bound and the viewport set. It begins a frame, applies the resource lists (deleting and creating buffers, vertex arrays and textures), draws one indexed draw call per tile with up to four imagery layers, then the stars and the sky if enabled, restores your state, and ends the frame. It does not clear or present. Without a view it draws nothing.
vgl_gl_draw_info fields (all optional; info itself may be null for all defaults):
| Field | Meaning | 0 or null selects |
|---|---|---|
struct_size |
sizeof(vgl_gl_draw_info) |
Required when info is given |
mirrored |
VGL_TRUE when the projection mirrors the image (a flipped y axis for rendering into a texture): the backend culls the other side |
Not mirrored |
view_matrix |
const vgl_dmat4*, the camera to draw from when it differs from the last update's view |
The context's view |
projection_matrix |
const vgl_dmat4*, camera to clip space, OpenGL depth -1 to 1 for logarithmic and conventional, 0 to 1 for reversed-Z |
The context's view's projection |
sun_direction |
Towards the sun along the ECEF axes, need not be normalized | Zero: light from the camera (a headlight); with the atmosphere, a sun straight above the camera |
logarithmic_far |
The logarithmic convention's far distance, metres |
VGL_LOGARITHMIC_DEPTH_FAR_DEFAULT (1.0e8) |
upload_budget |
Bytes of resources to upload in this draw |
VGL_UPLOAD_BUDGET_DEFAULT (16 MiB) |
features |
vgl_draw_feature bits: sea surface, atmosphere, stars, clouds, contours |
Terrain and imagery only |
debug_views |
vgl_debug_view bits |
Normal drawing |
earth_rotation_angle |
Radians, from vgl_earth_rotation_angle_get, for the stars |
0 |
contour_interval |
Metres between contour lines |
VGL_CONTOUR_INTERVAL_DEFAULT (500) |
cloud_height |
The cloud shell's height above the ellipsoid, metres |
VGL_CLOUD_HEIGHT_DEFAULT (6000) |
| Result | Cause |
|---|---|
VGL_RESULT_SUCCESS |
Drawn |
VGL_RESULT_INVALID_ARGUMENT |
A matrix or sun element not finite, unknown feature or debug bits, or a distance, interval or height not finite or negative |
VGL_RESULT_NOT_SUPPORTED |
Fields of a newer vgl set |
VGL_RESULT_INVALID_OPERATION |
A frame of the context is already open, or the last vgl_gl_backend_device_reset failed |
VGL_RESULT_OUT_OF_MEMORY |
The frame could not begin |
Each tile blends up to four imagery layers (the top four if a tile has more), bottom first, by each texel's alpha times the layer's opacity, over colours by height where imagery is missing, lit by the normal from the mesh's full-resolution samples. One sampler serves every layer: trilinear, anisotropic up to 8x where available, clamped to the edge. Textures are created as GL_RGBA8 and sampled as they are (sRGB-encoded bytes, unconverted); see sRGB.
Texture units 0 to 3 hold the layers, unit 4 the mesh's samples (a GL_RG32UI texture), and unit 5 the clouds. All are restored afterwards.
The host owns the depth buffer, its clearing and the projection. Set them up to match the convention you created the backend with, and use the same convention for your own geometry (Render Interface has the shader snippet for logarithmic depth).
| Convention | Depth buffer | Clear | Projection | Clip control |
|---|---|---|---|---|
| Logarithmic | Any (24-bit is enough) | glClearDepth(1) |
Conventional OpenGL, depth -1 to 1; near and far do not matter (the samples use 1 m and 1e9 m) | None |
| Reversed-Z | GL_DEPTH_COMPONENT32F |
glClearDepth(0) (or glClearDepthf(0) on ES) |
Reversed, near to 1 and far to 0, ideally infinite | The backend sets GL_ZERO_TO_ONE while it draws, keeping your origin, and restores yours |
| Conventional | Any | glClearDepth(1) |
Yours as it is; keep near and far tight around the view | None |
The backend enables the depth test and writes, and compares with GL_LESS (logarithmic and conventional) or GL_GREATER (reversed-Z). The sky and stars draw at the far depth with GL_LEQUAL or GL_GEQUAL and no depth writes, so they show only where nothing was drawn.
A reversed projection with an infinite far plane, as the samples build it (column-major):
// Near maps to 1 and infinity to 0, in a depth range from 0 to 1.
vgl_dmat4 perspective_reversed(double fovy, double aspect, double near_plane)
{
const double f = 1.0 / std::tan(fovy / 2.0);
vgl_dmat4 m{};
m.m[0] = f / aspect;
m.m[5] = f;
m.m[11] = -1.0;
m.m[14] = near_plane;
return m;
}For reversed-Z, also set clip control for your own geometry; the backend keeps whatever origin you set and restores your depth mode:
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); /* glClipControlEXT on OpenGL ES */Default framebuffers usually have a 24-bit integer depth buffer, which defeats reversed-Z. The OpenGL sample therefore draws into its own framebuffer object with a GL_DEPTH_COMPONENT32F renderbuffer for reversed-Z (GL_DEPTH_COMPONENT24 otherwise) and blits the colour to the window.
The backend saves this state when it draws and restores it before returning (requirement NFR-08):
- the current program, the vertex array binding, and the
GL_ARRAY_BUFFER,GL_COPY_WRITE_BUFFERandGL_PIXEL_UNPACK_BUFFERbindings - the active texture unit, and the 2D texture and sampler bindings of units 0 to 5
- the unpack alignment, row length, skip rows and skip pixels
-
GL_DEPTH_TEST, the depth function and the depth write mask -
GL_CULL_FACE, the cull face mode and the front face -
GL_BLEND, the blend functions and equations - primitive restart (
GL_PRIMITIVE_RESTARTon desktop,GL_PRIMITIVE_RESTART_FIXED_INDEXwhere it exists), which it switches off while drawing - for the reversed-Z convention, the clip control origin and depth mode
It draws with your framebuffer, viewport, scissor, depth range, colour mask and stencil state unchanged: a scissor, stencil test or colour mask you leave enabled applies to the globe too. It does not save or change GL_FRAMEBUFFER_SRGB, GL_MULTISAMPLE, polygon offset or polygon mode, so leave those as the globe needs them (off, except multisampling). The element array buffer binding belongs to the vertex array; the backend uploads index lists through its own vertex array and leaves yours untouched.
The OpenGL sample's check sets sentinel state (blending on, depth test off, GL_ALWAYS, culling off, front face clockwise, a vertex array of its own) before each draw and verifies it afterwards.
Pass vgl_draw_feature bits in features and vgl_debug_view bits in debug_views (Render Interface has the tables).
draw_info.features = VGL_DRAW_FEATURE_SEA_SURFACE | VGL_DRAW_FEATURE_CONTOURS;
draw_info.contour_interval = 100.0; /* metres */
draw_info.debug_views = VGL_DEBUG_VIEW_LEVEL_COLORS | VGL_DEBUG_VIEW_IMAGERY_SOURCE;-
Sea surface (
VGL_DRAW_FEATURE_SEA_SURFACE): terrain below mean sea level is raised to it in the vertex shader and shaded as water, with the imagery's ocean colour where there is imagery and a glint of the sun. Mean sea level comes from the geoid's heights at each tile's corners, so add a geoid pack (the sample set'spacks/egm2008); without one, the ellipsoid is sea level, up to about 100 m off. Skirts are raised only as far as the tile's lowest point, so they still cover cracks. -
Contours (
VGL_DRAW_FEATURE_CONTOURS): lines everycontour_intervalmetres above mean sea level, every fifth heavier, anti-aliased, fading out where they crowd. The samples pick an interval from the altitude. -
Debug views: the wireframe (
VGL_DEBUG_VIEW_WIREFRAME), level colours, imagery source and height colours, combinable.
The atmosphere, stars and clouds have a page of their own: Atmosphere, Stars and Clouds.
A typical frame:
- Bind your framebuffer, set the viewport, clear colour and depth (to 1, or 0 for reversed-Z).
- Draw any background that sits behind the globe and the sky (a space skybox, for example) without writing depth. The sky is blended over what the framebuffer holds where the atmosphere is thin, and the stars are added to it.
- Draw your opaque geometry with the same depth convention. The globe depth-tests against it, and the sky does not cover it, since the sky draws only where the depth is still at the far plane.
-
vgl_gl_backend_draw. - Draw transparent geometry and effects that should appear in front of the sky, then your UI.
Drawing opaque geometry after the globe also works for depth testing; drawing it before lets the sky skip those pixels. The globe receives no haze over your geometry: the atmosphere's haze is applied to the globe's own terrain only.
The backend draws into whatever framebuffer is bound, including a multisampled one; resolve it yourself (for example with glBlitFramebuffer). With the logarithmic convention, the fragment shader writes gl_FragDepth, which gives every sample of a pixel the same depth and disables early depth rejection on most hardware: edges against the background are still antialiased, but intersections with your geometry inside a pixel are not. The reversed-Z convention writes no fragment depth and avoids both, where clip control exists.
The backend asks the core for VGL_TEXTURE_FORMAT_RGBA8 and samples imagery as stored, sRGB-encoded, so the colour it writes is already display-encoded. Draw into an GL_RGBA8 (unorm) target, or keep GL_FRAMEBUFFER_SRGB disabled while it draws; with sRGB conversion on, the globe looks washed out. If your renderer works in linear HDR, draw the globe into a separate unorm target and composite it, or convert its output yourself.
When you render into a texture with a flipped y axis (to read it the right way up), triangles face the other way: set mirrored = VGL_TRUE.
One context selects tiles for one view. To draw the same globe from a second camera (a minimap, a reflection), pass that camera in view_matrix and projection_matrix: it draws the tiles selected for the context's view, as the samples' "freeze selection" mode shows. For a fully independent view, use a second context.
Create, draw, set stars and clouds, reset and destroy on the thread where the OpenGL context is current. The draw must not overlap vgl_context_update; update and draw on one thread, or serialize them.
When the OpenGL context is lost (an Android app returning from the background, a destroyed and recreated context), make the new context current and call:
if (vgl_gl_backend_device_reset(backend) != VGL_RESULT_SUCCESS)
{
/* draws fail with VGL_RESULT_INVALID_OPERATION until a reset succeeds */
}It forgets the old objects without deleting them, loads the functions and builds its programs again on the current context, and resets the context's resources, which upload again over the next frames. Stars and clouds set before are kept on the CPU and upload again too.
| Sample | Source | Executable | What it shows |
|---|---|---|---|
| OpenGL | samples/gl/main.cpp | vgl-gl |
GLFW window, OpenGL 3.3 or (--es) OpenGL ES 3.0, navigation like Google Earth, a Dear ImGui panel with statistics and debug views, the atmosphere, stars and clouds; also builds for the browser on WebGL 2 |
| Pure C | samples/c/main.c | vgl-c |
The core of the integration in C99: GEBCO, the geoid and Blue Marble, a camera circling the globe |
vgl-gl [--data <directory> | --pack <uri>...] [--cache <directory> [--cache-budget <MiB>]]
[--es] [--egl] [--depth logarithmic|reversed|conventional] [--check]
[--save <directory>] [--stars <catalog>]
vgl-c [--data <directory> | --pack <uri>...] [--check]-
--escreates an OpenGL ES 3.0 context,--eglcreates it through EGL (for example to reach ANGLE). -
--depthpicks the convention; the default is logarithmic. Reversed-Z exits with a message on a device without clip control. -
--checkruns hidden: it flies to a few views with each convention the device supports, compares the images across conventions, checks for OpenGL errors and that the backend restored the host's state, and exits with 1 on failure.--savewrites the checked images as PPM files. -
--starsreads the Yale Bright Star Catalogue (see Atmosphere, Stars and Clouds). - Keys: F1 panel, F2 wireframe, F3 level colours, F4 imagery source, F5 freeze selection, F6 height colours, F7 contours, F8 sea surface, F9 atmosphere, L sunlight or headlight, 1 to 7 places.
The checks are CTest tests (gl, gl_es, gl_c) when configured with -DVGL_TEST_GL=ON, since they need a display. The browser build is described on Web Builds; data and options on Samples.
| Symptom | Likely cause |
|---|---|
Create fails with VGL_RESULT_NOT_SUPPORTED, "the loader returned no glGetString" (or another function) |
The loader returns no OpenGL 1.x functions (raw wglGetProcAddress), or the context is older than it claims |
| Create fails, "glGetString(GL_VERSION) failed; is a context current?" | No current context on the calling thread |
| Create fails for reversed-Z | No clip control; use the logarithmic convention |
| Nothing drawn | No view set (vgl_context_view_set), no packs loaded yet, or the depth buffer cleared to the wrong value for the convention |
| Terrain flickers in the distance | Conventional convention at planetary scale, or reversed-Z with a 24-bit depth buffer |
| Colours washed out |
GL_FRAMEBUFFER_SRGB enabled on an sRGB target |
| Your geometry disappears behind the globe | Your geometry uses a different depth convention than the backend |
More on Troubleshooting.
Render Interface, Atmosphere, Stars and Clouds, Direct3D 11 Backend, Ogre Backend, Web Builds, Mobile Platforms, Samples, and the header backends/gl/include/vgl/gl.h.
Getting vgl
Using the library
Rendering
Data
Reference
Specifications