libMini, a terrain rendering library

The Mini Library (libMini) is the core of the high-performance terrain rendering system which is described in the paper "Real-Time Generation of Continuous Levels of Detail for Height Fields".

Version 11.6.2 as of 16.November.2020
Copyright (c) 1995-2020 by Stefan Roettger

Table of Contents

Terms of Usage

The libMini software is licensed under the terms of the GNU LGPL 2.1 with the static linking exception or (at your option) any later version with the exception. No warranty WHATSOEVER is expressed; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE! See the file "LICENSE.txt" for more details.

Back to top

General Information

The Mini Library is included in the virtual terrain project of Ben Discoe (vterrain.org) and an early version is utilized in the DX 8 underwater game AquaNox.

The author's contact address is:

stefan:at:stereofx.org
www.stereofx.org

Back to top

Getting the Package

The latest stable version of libMini is available here:

http://stereofx.org/download
For compilation instructions see Section (B).

The latest development version of libMini is available via SVN (Subversion):

   svn co http://svn.code.sf.net/p/libmini/code/libmini/mini mini
The original terrain rendering paper and the corresponding talk are available here:

http://stereofx.org/papers/TERRAIN.PDF
http://stereofx.org/papers/WSCG98.PPT

The ground fog rendering paper is available here:

http://stereofx.org/papers/PROJECTION.PDF
http://stereofx.org/papers/VG03.PPT

The vegetation rendering paper is available here:

http://stereofx.org/papers/VEGETATION.PDF
http://stereofx.org/papers/CGIM07.PPT

Additional conceptual papers about libMini are available here:

http://stereofx.org/download/libMini-Modules.pdf (libMini Module Overview)
http://stereofx.org/download/libMini-VolRen.pdf (libMini Volume Rendering Overview)
Back to top

Package Contents

Within the libMini package the following files are contained:

README.html: this file
LICENSE.txt: the libMini license
libMini.css: the README style sheet
libMini.ppm/.jpg: the libMini logo
libMini.ico: the libMini icon
mini.cpp/.h: contains the interface to the terrain rendering core
minicore.cpp/P.h/.h: contains the core of the terrain renderer
minidefs.h: contains basic definitions
minibase.h: contains basic declarations
miniOGL.cpp/P.h/.h: contains OpenGL rendering interface
miniARB.cpp/.h: contains ARB shader program interface
mini_*.h: contains group headers
miniv??.cpp/.h: contains vector class definitions
minivec.h: contains templated vector class definition
minimtx.h: contains templated matrix class definition
minimath.cpp/.h: contains mathematical definitions
minicomplex.h: contains header-only complex number definition
minicrypt.cpp/.h: contains cryptographic definitions
glslmath.h: contains header-only glsl math classes and operators
glslmath.txt: contains the README.txt for GLSLmath
glvertex*.h: contains header-only legacy OpenGL wrapper
glvertex.txt: contains the README.txt for glVertex
mini3D.cpp/.h: contains 3D software rendering pipeline
miniwls.cpp/.h: contains weighted least squares fit
mininoise.cpp/.h: contains perlin noise generator
minimpfp.cpp/.h: contains templated mp-fp definitions
minidyna.h: contains templated dynamic array
minisort.h: contains templated sorting method
minikeyval.h: contains templated key-value pairs
minikdtree.h: contains templated kd-tree
ministring.h: contains dynamic string methods
minilog.cpp/.h: contains logging methods
miniref.h: contains ref-counted item
mininode.h: contains ref-linked graph node
mininodes.cpp/.h: contains basic node definitions
mininode_geom.h: contains special geometry base class
mininode_teapot.cpp/.h: contains geometry node for the Utah teapot
mininode_path.cpp/.h: contains geometry node for geo-referenced paths
mininode_clod.cpp/.h: contains c-lod geometry node for geo-referenced paths
minitime.cpp/.h: contains system time abstraction
minirgb.cpp/.h: contains additional rgb/hsv conversion stuff
minicrs.cpp/.h: contains additional crs conversion stuff
miniio.cpp/.h: contains additional file io stuff
minidir.cpp/.h: contains additional directory listing stuff
minidds.cpp/.h: contains DDS compression stuff
ministub.cpp/.h: simplified stub class of the mini library
minitile.cpp/.h: wrapper class for tiled terrain rendering
miniload.cpp/.h: wrapper class for paged terrain rendering
minicoord.cpp/.h: container for geo-referenced coordinates
minimeas.cpp/.h: container for geo-referenced measurements
minicurve.cpp/.h: container for geo-referenced coordinate paths
minipath.cpp/.h: container for gps tracks
miniclod.cpp/.h: c-lod core class for geo-referenced paths
minixml.cpp/.h: container for xml data
miniwarp.cpp/.h: warp kernel for global coordinate systems
minicam.cpp/.h: camera coordinate system
minilayer.cpp/.h: wrapper class rendering a single layer
miniterrain.cpp/.h: wrapper class rendering multiple layers
miniearth.cpp/.h: wrapper class for rendering the earth
miniscene.cpp/.h: wrapper class for rendering the whole scene
minicache.cpp/.h: cache for speeding up tiled terrain rendering
minishader.cpp/.h: shader support for the render cache
miniray.cpp/.h: ray/triangle intersection code
ministrip.cpp/.h: vertex array container for triangle strips
minipoint.cpp/.h: simple point of interest renderer
minitext.cpp/.h: simple text renderer
minisky.cpp/.h: simple sky dome renderer
miniglobe.cpp/.h: night/day renderer of the earth or other textured globes
minitree.cpp/.h: contains algorithms for vegetation rendering
minipano.cpp/.h: contains panoramic addon for the minipoint renderer
minibrick.cpp/.h: contains algorithms for C-LOD volume rendering
minilod.cpp/.h: contains algorithms for S-LOD volume rendering
minislicer.h: contains algorithms for volume slicing
minigeom.h: contains geometric class templates
minimesh.cpp/.h: contains tetrahedral mesh class definition
minibspt.cpp/.h: contains BSP tree class definition
miniproj.cpp/.h: contains tetrahedral projection methods
geoid.cpp/.h: contains geoid height interpolation function
wmm.cpp/.h: contains magnetic declication interpolation function
pnmbase.cpp/.h: methods for handling PNM images
pnmsample.cpp/.h: methods for multi-resolution terrain resampling
rawbase.cpp/.h: methods for handling RAW volumes
rekbase.cpp/.h: methods for handling REK volumes (Fraunhofer EZRT)
database.cpp/.h: universal 1D/2D/3D/4D data buffer object
datafill.cpp/.h: generic 1D/2D/3D/4D fill-in algorithm
datacalc.cpp/.h: calculator for procedural images and implicit volumes
dataparse.cpp/.h: parser and interpreter of implicit functions
datacloud.cpp/.h: decouples terrain rendering from paging
datacache.cpp/.h: stores remote files in a local file cache
datagrid.cpp/.h: stores a grid of databuf objects
example.png: a screen shot of the example described below
example.cpp: the glut example (use "build.sh example" to compile)
example.cmake/: the cmake example (use "cmake . && make" to compile)
stubtest.cpp: the stub example (use "build.sh stubtest" to compile)
viewer.cpp: the libMini Viewer (use "build.sh viewer" to compile)
miniview.cpp/.h: the libMini Viewer base class
threadbase.cpp/.h: multi-threading support for the libMini Viewer
curlbase.cpp/.h: http protocol support for the libMini Viewer
jpegbase.cpp/.h: JPEG support for the libMini Viewer
pngbase.cpp/.h: PNG support for the libMini Viewer
zlibbase.cpp/.h: ZLIB support for the libMini Viewer
squishbase.cpp/.h: squish support for the libMini Viewer
greycbase.cpp/.h: CImg/GREYCstoration support for image denoising
dataconv.cpp/.h: image conversion support for external formats
miniimg.cpp/.h: image reading and writing support for image formats
lunascan.cpp/.h: text scanner for LUNA, an RPN-style computer language
lunaparse.cpp/.h: token parser for LUNA, an RPN-style computer language
lunacode.cpp/.h: code interpreter for LUNA, an RPN-style computer language
lunafunctor.cpp/.h: function evaluator based on LUNA
qt_viewer.h: Qt viewer base class
data/SkyDome.ppm: a sample sky dome texture (from Philo's Sky Collection)
data/EarthDay.db: a daylight earth texture (from NASA BlueMarble)
data/EarthNight.db: a night earth texture (from NASA BlueMarble)
data/Cone.db: a sample DB volume (implicitly defined cone)
GL/glext.h: backup copy of OpenGL extension header
MiniMakefile: Makefile for Irix, Linux and MacOS X
build.sh: the Unix build script
build.bat: the Windows build batch file
autogen.sh: the autogen script file
configure.ac: the autoconf definition file
Makefile.am: the automake definition file
CMakeLists.txt: the main cmake definition file
libMini.cmake: the libMini header and source list for cmake
libMini-config.cmake: the libMini build configuration for cmake
libMini-app.cmake: the libMini build configuration for simple applications
libMini.pro: the libMini build configuration for qmake
sources*.pri: the libMini source include list for qmake
tools/*: various command line tools
tabify.sh: a tool to clean up tab inconsistencies
md5check.sh: a tool to check the integrity of the distro
.md5: the md5 file list of the distro

Additionally, the Yukon Ground Fog Demo, the Stuttgart Demo, the Hawaii Demo and the Fraenkische Schweiz Demo can be downloaded from here (please follow the usage instructions in the README):

http://stereofx.org/download/Yukon.zip
http://stereofx.org/download/Stuttgart.zip
http://stereofx.org/download/Hawaii.zip
http://stereofx.org/download/Fraenkische.zip

The libMini package also contains the libMini Viewer (see Section (P)). This application is a generic viewing utility for terrain data and tile sets. For example, it can be used to display the demo data mentioned above, load tilesets generated with the libGrid library or stream terrain data over the internet that has been produced with the Virtual Terrain Builder from vterrain.org.

Back to top

(A) Introduction

The Mini Library applies a view-dependent mesh simplification scheme to render large-scale terrain data at real-time. For this purpose, a quadtree representation of a height field is built. This quadtree is also utilized for fast view frustum culling and geomorphing.

Within this distribution the files needed to build the basic terrain rendering library are included. In order to keep the library portable any system dependent stuff like window management is not part of this distribution. Nevertheless, the Mini Library implements all the necessary graphics algorithms to setup a high-performance terrain rendering system.

The main goal for developing the library was to keep it as compact, stable and efficient as possible and not to blow it up by adding unnecessary features. Thus, Mini stands for "Mini Is Not Immense!" in a rather positive sense.

Back to top

(B) Compilation

It is assumed that the libMini zip package has been downloaded and uncompressed to a "mini" folder. On a Unix system, the preferred download option is to check out the entire libMini source repository with subversion:
   svn co http://svn.code.sf.net/p/libmini/code/libmini libmini
The build.sh shell script compiles the library on Irix, Linux and MacOS X. Simply type "./build.sh" in your Unix shell (requires the tcsh to be installed). OpenGL is required as the single dependency of the library. To install the library and the necessary include files in /usr/local on your Unix machine type "./build.sh install" as a super-user.

Optionally, you can use autoconf or CMake to build libMini. For the former, type "./configure && make" and for the latter type "cmake . && make" in a Unix shell.

The library also compiles on Windows by using CMake. For Windows it is recommended to check out the whole libMini package with Tortoise SVN from "http://svn.code.sf.net/p/libmini/code/libmini".

To compile, simply specify the "mini" directory as the source and binary directory in the CMake GUI and click the generator button. Then open the generated VC++ project and compile with Visual Studio. You can also run the build on the MSVC Command prompt via the "build.bat" batch file.

Alternatively, you can use cygwin in the following way:

As another option, you can use MinGW in the following way:

Other than the specified operating systems are not officially supported, but there is a good chance that libMini will build via automake or CMake.

As described in the following section, the distribution contains a libMini application example. The example can be compiled by typing "./build.sh example" in a shell. It requires GLUT to be installed. On Windows it is recommended to use the freeGLUT library as a replacement for the standard GLUT library. For convenience, the libMini distribution already contains the freeGLUT headers and the prebuilt static freeGLUT library.

Back to top

(C) Terrain Rendering API

In this section the low level API of the Mini Library (the libMini core) is described. Convenient high level methods for a variety of commonly encountered terrain rendering scenarios are described in Sections (F), (G), (I), (K) and (O). The recommended starting point for experienced graphics programmers is the libMini Viewer API and its reference implementation, the libMini tile set viewer, which is described in Section (P). Please check out whether or not the high level scenarios suffice your needs before you dive into the low level API:

The main include file is "mini.h" which contains the definition of the low level terrain rendering API (the libMini core). In order to port it to a different graphics architecture, only the file "miniOGL.cpp" needs to be adapted. It encapsulates all OpenGL calls that are made inside of the libMini core.

In the following the low level terrain rendering API is explained by drawing a small example height field. This task is broken down into eight basic steps. The full example code is included in the distribution (see "example.cpp").

Step 0) Include the libMini core header:

   #include <mini/mini.h>
Note: On Linux the libMini headers are usually placed in the "/usr/local/include/mini" directory after installation (via build.sh; build.sh install). So the libMini headers should be included with the "mini/" prefix, since "/usr/local/include" is already in the default search path. On Windows the libMini installation path can be added with the VisualStudio-> Tools-> Options-> Projects-> Directories menu entry.

[Optional] Step 1) Specify the fine tuning parameters:

The fine tuning parameters are initialized to suitable values, so normally this step can be omitted. However, for more flexibility the parameters can be set explicitly via mini::setparams(minres,maxd2,sead2,minoff,maxcull). In the terrain rendering paper minres is referenced to as C, maxd2 is the maximum value for the linear mapping of the d2-values, sead2 determines the influence of the extracted sea surface onto the d2-values, and maxcull defines the number of quadtree levels for which the view frustum culling is performed.

Step 2) Open a window:

For that purpose the glut library is used in the example.

Step 3) Height field initialization:

   // a small example height field
   short int hfield[]={0,0,0,
                       0,5,0,
                       0,0,0};

   int size=3; // grid size of the square height field
               // (can be any number>2, but preferably 2^n+1, n>0)

   float dim=10.0f; // cell dimension = horizontal grid point spacing
   float scale=1.0f; // vertical scaling of the elevations

   void *map,*d2map; // spare void pointers

   map=mini::initmap(hfield,&d2map,&size,&dim,scale);

Step 4) Texture map initialization:

   // a small example RGB texture map
   unsigned char texture[]={255,63,63, 255,63,63, 255,63,63, 255,63,63,
                            255,63,63, 63,63,255, 63,63,255, 255,63,63,
                            255,63,63, 63,63,255, 63,63,255, 255,63,63,
                            255,63,63, 255,63,63, 255,63,63, 255,63,63};

   int width=4,height=4; // width and height of the texture map
                         // (can be any number>1, but preferably 2^n, n>0)

   int texid; // id of the texture map

   int mipmaps=1; // enable mip-mapping

   texid=mini::inittexmap(texture,&width,&height,mipmaps);

[Optional] Step 5) Ground fog map initialization:

Optionally, a ground fog layer is rendered by stacking prisms onto each triangle that is generated by the terrain rendering algorithm. The height of the fog layer is defined by the ground fog map. With the assumption of an uniform fog density and the application of an emissive optical model the volumetric projections of the prisms can be composed efficiently (see ground fog paper for more details).

   // a small example ground fog map
   unsigned char ffield[]={2,3,2,
                           3,1,3,
                           2,3,2};

   int fogsize=3; // size of the ground fog map
                  // (can be any number>1, but same size as height field preferred)

   void *fogmap; // spare void pointer

   float lambda=1.0f; // vertical dimension of the ground fog layer
   float displace=5.0E-3f; // vertical displacement of the ground fog layer
   float emission=0.05f; // optical emission of ground fog per unit length
   float attenuation=1.0f; // triangulation importance of ground fog
   float fogR=1.0f,fogG=1.0f,fogB=1.0f; // fog color

   fogmap=mini::initfogmap(ffield,fogsize,lambda,displace,emission,
                           attenuation,fogR,fogG,fogB);
Remarks: For the ground fog to be rendered correctly an alpha channel is required in the frame buffer. Calling mini::inittexmap() with no parameters results in the omission of the terrain which is useful for the display of additional fog layers. An optical emission of zero triggers maximum intensity projection (MIP). Compared to an emissive optical model MIP is much faster and does not require an alpha channel.

Step 6) For each frame:

Step 6.1) Clear window.

Step 6.2) Setup model-view and projection matrix according to viewing coordinates.

[Optional] Step 6.3) Set actual tile:

This step can be omitted if only a single height field is used.

   mini::setmaps(map,d2map,size,dim,scale,texid,width,height[,mipmaps,cellaspect]);
The optional mipmaps parameter determines whether or not mipmaps are enabled. The optional cellaspect parameter can be used to define a non-uniform spacing of the grid, that is the spacing along the x-axis is dim units while the spacing along the z-axis is dim*cellaspect units.

If a ground fog map is used the following parameters need to be passed:

   mini::setmaps(map,d2map,size,dim,scale,
                 texid,width,height,mipmaps,
                 cellaspect,0.0f,0.0f,0.0f,NULL,NULL,
                 fogmap,lambda,displace,
                 emission);

Step 6.4) Render the terrain:

   float res=1000.0f; // global resolution of the triangulation (in the range [1..infty])
   float ex=0.0f,ey=10.0f,ez=30.0f; // eye point
   float fx=0.0f,fy=10.0f,fz=30.0f; // focus of interest (should be equal to eye point)
   float dx=0.0f,dy=-0.25f,dz=-1.0f; // view direction
   float ux=0.0f,uy=1.0f,uz=0.0f; // up vector
   float fovy=60.0f; // vertical field of view
   float aspect=1.0f; // window width/window height
   float nearp=1.0f; // distance of near clipping plane
   float farp=100.0f; // distance of far clipping plane

   mini::drawlandscape(res,
                       ex,ey,ez,
                       fx,fy,fz,
                       dx,dy,dz,
                       ux,uy,uz,
                       fovy,aspect,
                       nearp,farp);
For typical height fields the parameter res should be set to 100-10000 depending on the desired density of the generated mesh.

Hint: A zero field of view disables the view frustum culling, whereas a negative field of view triggers the orthographic projection mode. In the latter case, use glOrtho(fovy/2*aspect,-fovy/2*aspect,fovy/2,-fovy/2,near,far) instead of the usual gluPerspective call to set up the projection matrix.

Side Note:
You can also use the orthographic mode to perform a view-independent mesh decimation. Then the resolution parameter determines the mesh accuracy.

Step 6.5) Swap buffers:

Now we are basically done. For comparison, a screen shot of the result is included in the distribution (example.png).

[Optional] Step 6.6) Query functions:

Between each rendered frame the elevation and the normal at position (x,z) of the height field can be queried via:

   float height=mini::getheight(i,j); // (i,j) = integer grid position
   float height=mini::getheight(x,z); // (x,z) = floating point world coordinates
   float nx,ny,nz; mini::getnormal(x,z,&nx,&ny,&nz);
Similarly, the height of the ground fog layer (if present) can be queried via:

   float fogheight=mini::getfogheight(x,z);

Step 7) After the last frame, delete all used maps:

   mini::deletemaps();

Step 8) Finally, close the window.

Back to top

(D) Additional Comments

The size of the small height field in the above example surely is too small for realistic terrain rendering. For convenience, a real data set of Kluane National Park in Yukon Territory, Canada, is included in the Yukon Demo. See also Section (H) about loading real data.

The accuracy of the rendered terrain can be controlled by setting the res parameter from the minimum value of 1 to larger values of say 1,000-100,000 depending on the actual performance of the graphics hardware.

Normally, the focus of interest, that is the point with the highest resolution, should be equal to the eye point in order to minimize the screen space error of the dynamic triangulation. In some cases, however, it might be advantageous to set the focus to a different location.

Instead of choosing the default short signed integer representation (16 bit) of the height field, a floating point representation can be chosen by using the Mini namespace (capital first letter). This is equivalent to calling the constructor of the ministub class with a float height field as the first parameter.

Since the algorithm uses uniform grids of size 2^n+1 in both dimensions, a height field with this size should be supplied whenever possible. Other sizes are scaled up internally to the next possible size. A data set with unequal grid dimensions must be resampled uniformly or broken up into uniform tiles prior to passing it to the Mini Library.

Back to top

(E) Tiled Terrain (Tile Sets)

In the case that a terrain data set does not consist of a single height field but rather of several tiled patches, each of the tiles is setup separately:

   for (int i=0; i<tiles; i++)
      {
      map[i]=mini::initmap(hfield[i],&d2map[i],&size[i],&dim[i],scale);
      texid[i]=mini::inittexmap(texture[i],&width[i],&height[i]);
      }
Now, for each frame, the terrain is rendered in two passes:

   for (phase=1; phase<=2; phase++)
      for (int i=0; i<tiles; i++)
         {
         mini::setmaps(map[i],d2map[i],size[i],dim[i],scale,
                       texid[i],width[i],height[i],mipmaps,
                       cellaspect,ox[i],oy[i],oz[i],
                       d2map2[i],size2[i]);

         mini::drawlandscape(res,
                             ex,ey,ez,fx,fy,fz,dx,dy,dz,ux,uy,uz,
                             fovy,aspect,nearp,farp,
                             NULL,NULL,phase);
         }
Here, the additional parameters ox[i], oy[i], and oz[i] specify the origin (center) of each tile. The remaining additional parameters d2map2[i][0..3] and size2[i][0..3] denote the d2maps of the four adjacent tiles and the grid size of the neighbours, respectively. The neighbours must be specified for each tile to ensure crack-free rendering of the entire scene. Viewed from above, the indices 0..3 correspond to the following locations of the adjacent tiles: left, right, bottom, and top. If a neighbour does not exist, the NULL pointer may be passed instead. In order to ensure a conforming mesh, the elevations of shared grid points of adjacent tiles must be identical, meaning that the shared edges need to be duplicated. After the last frame, the allocated memory is released by subsequently passing each tile map[i] to the function mini::setmaps() and by calling mini::deletemaps() afterwards.

As an example, the center tile of a height field can be surrounded by tiles with a lower resolution. These low resolution tiles can be used to represent the horizon of a scenery without consuming a large amount of extra memory.

Preferably, each terrain data set should be broken up into tiles that fit into the L2-cache of the processor. For example a tile of size 129x129 easily fits into the L2-cache of most modern processors. In this setup each tile consumes only 48 kilobytes of memory which leads to a significantly improved cache coherency and performance.

Back to top

(F) Minitile And Miniload Frontend

As mentioned above, a tiled terrain can be used to save memory and gain speed. In such a case the minitile class provides a starting point how to render a tiled terrain with the Mini Library. In the constructor of the minitile class an array of file names is passed which define the PGM height image of each tile and its corresponding PPM texture (in column first order). The tiles must have the same geometric extent but need not have equal resolution which means that grid size or cell dimension may differ.

Besides rendering a tiled terrain, the minitile class is also a convenient way to just display a single height field plus a texture without going into the details of the low level API as described in Section (C). For an example use case of the minitile class please check out the Yukon Demo or the Stuttgart Demo.

If the landscape that should be visualized is extremely large and detailed the terrain may not fit entirely into main memory. This situation is dealt with the miniload class. It offers almost the same functionality as the minitile class but dynamically pages visible and invisible tiles in and out. There are two paging modes: The first one just loads all visible tiles and displays all data up to the far clipping plane. For this to work efficiently, the distance to the far plane should be chosen to be considerably smaller than the actual extent of the entire scene. The second paging mode loads the appropriate LOD for each tile if a resolution pyramid is present. This reduces the memory foot print drastically but may also increase the latency during a movement of the viewer, since all the visible LODs need to be updated dynamically. The second mode allows much larger viewing distances as the first mode. Depending on the tile size this comes at the expense of some latency whenever a different LOD needs to be loaded from disk. To hide the latency the so-called preloading can be enabled so that the requested LODs are already available before they actually need to be rendered.

By default, the tiles are stored in the PNM (PGM/PPM) format of the netpbm library which is available at netpbm.sourceforge.net. The netpbm library, however, is not required for linking, since libMini has built-in support for the PNM format (see Section Appendix (A)).

The LODs are identified by adding the number of the corresponding LOD to the base file name which has LOD 0 by definition. As an example, let the tile with file name "tile.x-y.pgm" be the base LOD at column x and row y of the grid. Then the next corresponding LOD with level 1 is named "tile.x-y.pgm1" and so forth. If the base LOD (or LOD0) has size 2^n+1 the LODs with level l=1..n-1 (or LOD1, LOD2, ...) have size 2^(n-l)+1. Let the texture with file name "tile.x-y.ppm" be the base texture at column x and row y of the grid. Then the next corresponding texture LOD with level 1 is named "tile.x-y.ppm1" and so forth. If the base texture has size 2^m the texture LODs with level l=1..m-1 have size 2^(m-l). For different tiles the base LOD may have different tile or texture size. While one or more levels from the top of each LOD pyramid may be missing the base LOD has to be present in any case. Both the height fields and the texture maps use a corner centric (not cell centric) data representation.

A basic grid resampler which is able to produce the required pyramids is available via the pnmsample::resample(...,int pyramid) call. The pyramid parameter controls the number of generated LODs in addition to the base level. If the described file conventions are met the output of any GIS program can be used, too. In fact, the latter should be the preferred way of resampling, since the built-in resampler has several limitations and is intended to be only a minimal "reference" implementation. Among its limitations is the restriction to Lat/Lon coordinates as world coordinate system and the missing out-of-core support. The addition of these features would have blown up libMini significantly, so if one of these features is needed in a particular situation the tiles should be resampled with a more advanced GIS application such as libGrid or VTBuilder from vterrain.org. The output of VTBuilder is compatible with libMini so you are free to import the data into your own libMini project or visualize it directly with the VTEnviro application which also builds on top of libMini. However, if the restrictions of the built-in resampler are not crucial, you can call the resampler with a list of georeferenced PNM files, which will be resampled within the range of the first file on the list (for information about georeferencing see Section (H).

To load a tiled terrain consisting of c columns and r rows one needs to construct two string pointer arrays hf and tx containing the file names of the PGM height fields and the ppm textures of each tile (column first order, north-west corner first, missing tiles indicated by null pointers). Let cd be the width of each column and rd be the height of each row and let s be the vertical scaling of the elevations and let (cx,cy,cz) be the offset of the center of the entire terrain (all constants measured in meters). Then the following call does the job with the terrain lying in the (x,-z) plane and the elevations corresponding to the y-coordinates:

   miniload *terrain=new miniload(hf,tx,c,r,cd,rd,s,cx,cy,cz);
In order to load tiles that have been generated with the built-in resampler, we simply pass the number of columns and rows and the directory where the tiles have been stored to the miniload:load method. Then the missing parameters are determined automatically by looking at the georeferencing information of the specified tiles.

To render the scene use the following call:

   terrain->draw(res, // resolution
                 ex,ey,ez, // eye point
                 dx,dy,dz, // view direction
                 ux,uy,uz, // up vector
                 fovy,aspect, // field of view and aspect
                 nearp,farp, // near and far plane
                 update); // optional incremental update
The library will now load all visible tiles and page in and out the appropriate LODs automatically.

By default, the tiles are rendered directly using the built-in OpenGL graphics engine. Alternatively, a render cache (e.g. the minicache described in Section (I)) can be attached to the miniload class. Then the tiles are displayed indirectly by rendering the contents of the cache. However, we do not attach the cache to the miniload object but rather to the minitile object encapsulated in it (use terrain->getminitile() to get it).

The update parameter determines the number of frames for which the cache persists and how long it takes to completely fill the cache. A value of 1 causes the cache to be filled within a single frame and can be used to flush the cache.

Non-standard graphics effects can be implemented by using the hook mechanism or the shader plugins of the minicache backend (see Section (J)).

During run-time, the terrain can be rescaled in the range from 0% to 100% of the original elevation by calling miniload::setrelscale.

Also, the sea surface can be rendered at real-time. In order to interactively extract a specific sea level (e.g. 0) we use miniload::setsealevel(level).

For implementation reasons, this is only supported if a render cache is attached. The contour line of the sea surface is extracted precisely so that it matches the shore line and therefore does not intrude into the terrain. This approach efficiently eliminates Z-buffer fighting artifacts.

By default, texture compression is enabled which means that the textures will be compressed on-the-fly in the OpenGL driver. Since this is a very time consuming task it can be turned off via miniOGL::configure_compression(0). Texture mip-mapping is also enabled by default, but it can be switched off via minitile::configure_mipmaps(0). This further improves the texture loading performance.

The paging mechanism can be controlled via the following call:

   terrain->setloader(void (*request)(...),void *data,
                      void (*preload)(...),
                      void (*deliver)(...),
                      int paging,
                      float pfarp,
                      float prange,int pbasesize,
                      int plazyness,int pupdate,
                      int expire);
Normally, the first four arguments should be set to NULL. Then each tile is loaded if it is within the viewing range (that is the distance to the far clipping plane farp). In order to ensure that invisible tiles are already available before they actually become visible preloading can be enabled by passing a function pointer as third argument. The referenced function is called subsequently to notify all tiles which need to be preloaded. However, if preloading is disabled, visible tiles are just requested on the fly. This is recommended if the data is available on a fast medium (e.g. a hard disk).

Each requested tile is loaded either automatically by the library or manually via the callback mechanism. The callback passes height fields, texture maps and optional fog maps by encapsulating them into a databuf object. The object format is flexible and can be used for byte, short int, float and even pre-compressed texture data with an optional alpha channel (as opposed to the PNM format which only supports plain RGB images and only 8- or 16-bit height fields). The databuf class has methods to load and store data in its native DB file format (see Section Appendix (C)). The extension for the native format is ".db". The databuf class also has convenience functions for reading PNM images and PVM volumes which are the standard format for the minibrick module (see Section Appendix (4)). Pre-compressed texture maps are preferred over uncompressed textures, because loading is much faster. This is due to the fact that neither the texture data has to be compressed nor a mipmap pyramid has to be generated on-the-fly. Currently, only S3TC/DXT1 texture pre-compression is supported.

Remark: A workaround to get pre-compressed PPM images is to use the databuf::loadPPMcompressed method instead of the databuf::loadPNMdata method. For the first time the PPM images are accessed, the uncompressed data is loaded as usual. But the texture data will also be compressed and written to a DB file. If the data is accessed a second time it will be already pre-compressed and loading will be much faster. Optimally, this procedure should be applied to all resampled tiles before the renderer is launched.

For slow media or internet access preloading should be enabled so that each call of the preload callback can be used to spawn a thread which silently receives and stores the incoming data until it is collected by the deliver callback. While requested data should be returned instantly the delivery of preloaded data can be delayed until an arbitrary point in the future. The Mini Library has a reference implementation of an asynchronous file cache. With this cache the rendering task can be decoupled from the loading task which leads to a much smoother visual experience for large paged data sets. In such an ambitious use case, please also read Sections (K) and (L).

If a resolution pyramid is present, the library also tries to page in the appropriate LOD l from the pyramid. If preloading is enabled, the library requests level l as usual but also tries to silently preload level l+1 so that the next level is delivered before it actually becomes visible.

The other arguments of miniload::setloader have the following meaning:

Note: A possible reason for slow rendering performance is the limited amount of available texture RAM. If the prange parameter is too large most of the texture tiles will be loaded at the highest resolution. Then the textures may not fit completely into texture memory leading to excessive bus traffic. Halving the prange results in 25% of texture memory usage!

Back to top

(G) Library Stub

In some cases the built-in texture mapping setup or the explicit dependency on OpenGL may be too restrictive. In order to gain more flexibility, the internal management of the OpenGL state including the automatic generation of texture coordinates can be disabled by calling mini::inittexmap() with no parameters. Then all generated vertices are passed to a callback function allowing the entire graphics state to be handled externally. This feature also allows compatibility with graphics standards such as DirectX or rendering engines like Irrlicht. Using the callback mechanism from within the minitile and miniload frontends is also possible and works analogue to the case described in the following.

If the internal OpenGL state management of the Mini Library is not needed, one can access the library through the ministub class as shown in the code example given below. It demonstrates the external handling of the graphics state using explicit calls to OpenGL. If a different graphics library should handle the graphics state we can use "build.sh stub" to compile a library that does not contain any references to OpenGL specific functions (use the switch -DNOOGL on Windows). Otherwise the library must be linked against "-lGL -lGLU -lm" to resolve the OpenGL dependencies.

   #include <mini/ministub.h>

   // height field is a float array
   float hfield[]={0,0,0,0,0,
                   0,3,3,3,0,
                   0,3,5,3,0,
                   0,3,3,3,0,
                   0,0,0,0,0};

   int size=5; // grid size

   float dim=5.0f; // cell dimension
   float scale=1.0f; // vertical scaling
   float cellaspect=1.0f; // cell aspect ratio
   float cx=0.0f,cy=0.0f,cz=0.0f; // grid center

   ministub *stub;

   int myfancnt;

   void mybeginfan()
      {
      // mandatory "beginfan" callback
      // called for each generated triangle fan
      // followed by the vertex callbacks

      if (myfancnt++>0) glEnd();
      glBegin(GL_TRIANGLE_FAN);
      }

   void myfanvertex(float i,float y,float j)
      {
      // mandatory "fanvertex" callback
      // called for each vertex of a triangle fan
      // glVertex3f directly qualifies as a fast "fanvertex" callback
      // (i,j) is the grid coordinate of the vertex
      // y is the unscaled elevation interpolated from the height field
      // these coordinates are transformed by the OpenGL modelview matrix
      // therefore, the real world coordinates of each vertex are
      // (vx,vy,vz)=((i-size/2)*dim+cx,y*scale+cy,(size/2-j)*dim+cz)

      glVertex3f(i,y,j);
      }

   void mynotify(int i,int j,int s)
      {
      // optional "notify" callback
      // triggered during quadtree traversal
      // called for each visible node of the quadtree
      // to disable the callback pass the NULL pointer to the ministub
      // (i,j) is the center of the actual node in grid coordinates
      // s is the size of the actual node in grid units

      // only add extra code here if you know what you are doing
      // ...
      }

   float mygetelevation(int i,int j,int S,void *data=NULL)
      {
      // optional "getelevation" callback
      // if image=NULL is passed to the ministub constructor
      // this callback is evaluated separately for each grid point
      // use this for the sequential access of a height field
      // e.g. for memory efficient reading from an input stream
      // as a reference to the calling object an optional
      // data pointer can be passed to the callback

      // return the elevation at grid position (i,j) here
      return(hfield[i+j*S]); // the size of the grid must be equal to SxS
      }

   int main(int argc,char *argv[])
      {
      stub=new ministub(hfield,
                        &size,&dim,scale,
                        cellaspect,cx,cy,cz,
                        mybeginfan,myfanvertex,
                        mynotify,
                        mygetelevation,
                        NULL);

      float res=1000.0f; // resolution
      float ex=0.0f,ey=10.0f,ez=30.0f; // eye point
      float dx=0.0f,dy=-0.25f,dz=-1.0f; // view direction
      float ux=0.0f,uy=1.0f,uz=0.0f; // up vector
      float fovy=60.0f; // field of view
      float aspect=1.0f; // aspect of view
      float nearp=1.0f; // near plane
      float farp=100.0f; // far plane

      // open window and create OpenGL context here
      // ...

      // change OpenGL state here
      // (for example, setup automatic texture coordinate generation)
      // ...

      // setup OpenGL modelview matrix
      glScalef(dim,scale,-dim); // scale vertices
      glTranslatef(-size/2+cx,cy,-size/2+cz); // translate vertices

      myfancnt=0;

      stub->draw(res,
                 ex,ey,ez,
                 dx,dy,dz,
                 ux,uy,uz,
                 fovy,aspect,
                 nearp,farp);

      glEnd();

      // delete OpenGL context and close window here
      // ...

      delete stub;

      return(0);
      }
Since the Mini Library optionally supports ground fog rendering, the fog mesh which consists of vertically aligned prisms have to be passed to the calling framework as well. Three subsequent calls of the "prismedge" callback define one fog prism by describing the ground position (x,y,z) and the vertical size (yf) of the three vertical prism edges. The edges are already transformed into the world coordinate system.

A test version above code can be compiled by first stripping the Mini Library off its OpenGL dependent calls (type "build.sh stub"). Then the stub test is compiled with the command "build.sh stubtest".

For comparison, the text output of the stub test is:

   beginfan();
   fanvertex(1,1,1); // realvertex=(0,5,0)
   fanvertex(2,0,1); // realvertex=(10,0,0)
   fanvertex(2,0,2); // realvertex=(10,0,-10)
   prismedge(0,5.005,6.005,-0);
   prismedge(10,0.005,3.005,-0);
   prismedge(10,0.005,2.005,-10);
   fanvertex(1,0,2); // realvertex=(0,0,-10)
   prismedge(0,5.005,6.005,-0);
   prismedge(10,0.005,2.005,-10);
   prismedge(0,0.005,3.005,-10);
   fanvertex(0,0,2); // realvertex=(-10,0,-10)
   prismedge(0,5.005,6.005,-0);
   prismedge(0,0.005,3.005,-10);
   prismedge(-10,0.005,2.005,-10);
   fanvertex(0,0,1); // realvertex=(-10,0,0)
   prismedge(0,5.005,6.005,-0);
   prismedge(-10,0.005,2.005,-10);
   prismedge(-10,0.005,3.005,-0);
   fanvertex(0,0,0); // realvertex=(-10,0,10)
   prismedge(0,5.005,6.005,-0);
   prismedge(-10,0.005,3.005,-0);
   prismedge(-10,0.005,2.005,10);
   fanvertex(1,0,0); // realvertex=(0,0,10)
   prismedge(0,5.005,6.005,-0);
   prismedge(-10,0.005,2.005,10);
   prismedge(0,0.005,3.005,10);
   fanvertex(2,0,0); // realvertex=(10,0,10)
   prismedge(0,5.005,6.005,-0);
   prismedge(0,0.005,3.005,10);
   prismedge(10,0.005,2.005,10);
   fanvertex(2,0,1); // realvertex=(10,0,0)
   prismedge(0,5.005,6.005,-0);
   prismedge(10,0.005,2.005,10);
   prismedge(10,0.005,3.005,-0);
Back to top

(H) Real Terrain Maps And Textures

A good starting point for real world terrain data is

The Global Land Cover Facility
glcf.umiacs.umd.edu

Free sky dome textures can be downloaded at

Philo's Sky Collection
www.philohome.com/skycollec/skycollec.htm

In order to load a real height field or texture use the PNM reader via

   #include <mini/pnmbase.h>

   unsigned char *data;
   int width,height,components;

   data=readPNMfile(pnmfilename,&width,&height,&components);
If components==1 the function returns an unsigned char height field
else if components==2 16 bit signed values are returned in MSB format
else if components==3 an RGB color image is returned. else if components==4 an RGBA color image is returned.

If the PNM image contains an 8- or 16-bit height field we first copy it to a short array. Then we can pass this array to the libMini core or the ministub class for example:

   if (width!=height) ERRORMSG(); // height field must be quadratic

   short int *hfield=new short int[width*height];

   if (components==1) // 8-bit
      for (int j=0; j<height; j++)
         for (int i=0; i<width; i++)
            hfield[i+j*width]=data[i+(height-1-j)*width];
   else if (components==2) // 16-bit
      for (int j=0; j<height; j++)
         for (int i=0; i<width; i++)
            hfield[i+j*width]=(short int)(256*data[2*(i+(height-1-j)*width)]+data[2*(i+(height-1-j)*width)+1]);
   else ERRORMSG();

   free(data);

   ministub stub=new ministub(hfield,...);

   delete[] hfield;
Alternatively, we can pass the array via the getelevation callback which prevents the array from being copied twice:

   short int mygetelevation(int i,int j,int S)
      {
      if (components==1) return(data[i+(S-1-j)*S]);
      else if (components==2) return((short int)(256*data[2*(i+(S-1-j)*S)]+data[2*(i+(S-1-j)*S)+1]));
      return(0);
      }

   ministub stub=new ministub(NULL,...,mygetelevation,...);

   free(data);
The same callback mechanism is implemented in the libMini core.

In order to georeference a PNM image, we have to put its geographic location into the comment of the PNM header. This is achieved by specifying the four corners of the image in either the geographic world coordinate system (also known as Lat/Lon) or in Universal Transverse Mercator coordinates (UTM). The built-in resampler of the library exclusively uses this extended PNM format (for more details see Appendix (A)). An example of a georeferenced header is shown below:

   P6
   # BOX
   # description=PPM example
   # coordinate system=LL
   # coordinate zone=0
   # coordinate datum=0
   # SW corner=198721.93993200/-75123.60940800 arc-seconds
   # NW corner=198722.01794400/-75081.99117600 arc-seconds
   # NE corner=198766.29376800/-75082.06288800 arc-seconds
   # SE corner=198766.21917600/-75123.68115600 arc-seconds
   # cell size=.086482/.086482 arc-seconds
   # vertical scaling=0 meters
   # missing value=-9999
   512 512
   255
The identifier "P6" stands for an RGB image and the numbers at the end of the header define the width, the height and the maximum pixel value of the image. For 8-bit data the maximum value is 255, for signed 16-bit data it is 32767 (or 65535). The identifier "P5" stands for height fields (and gray scale images). The raw data of an image is appended after the header. 16-bit data is stored in MSB format.

Back to top

(I) High Performance Rendering Using The Minicache Backend

The minicache backend improves the rendering performance of the libMini core by exploiting the frame to frame coherency of a scene.

Principally, the Mini Library generates a new triangle mesh for each frame. This is necessary to suppress the popping effect by applying the geomorphing technique. As a consequence, the dynamically generated mesh prohibits the use of high performance rendering primitives such as vertex arrays or vertex buffer objects, because there is virtually no frame to frame coherency of the vertex data.

However, we do not need to perform the geomorphing operation for each and every frame. Usually 5-10 morphing operations per second appear to be visually smooth to a human observer. If the terrain is rendered with 50 frames per second then we can cache the generated vertices for at least 5 consecutive frames.

This dramatically reduces the CPU load, since the triangle mesh can be updated over consecutive frames. For this to work, a tiled terrain needs to be used, so that the mesh update can be triggered tile after tile. The GPU load is also reduced dramatically, since the cache can be rendered in an optimized fashion. The minicache uses vertex arrays for this purpose.

To enable the minicache we simply pass the minitile object to be cached to the minicache:

   minitile *tileset=new minitile(hfields,textures,cols,rows,...);
   minicache *cache=new minicache;
   cache->attach(tileset);
Then, for each frame, we trigger a partial scene update with:

   int update=5; // number of frames per update
   tileset->draw(...,update); // nothing is rendered yet
   cache->rendercache(); // render the cached vertex buffer
This is illustrated in the Hawaii Demo and in the libMini Viewer (see Section (P)).

The raw performance on a Linux box with an AMD Athlon 2.2 GHz CPU and an NVIDIA GeForce FX 5800 graphics accelerator is about 20 million geomorphed vertices per second.

Back to top

(J) Using The libMini Shader Plugins

The standard behaviour of the minicache which basically only drapes textures on the height fields can be extended easily by supplying vertex and pixel shaders. If no application-specific shaders are given, the built-in shaders just implement the standard behaviour and can be used as a basis to write own advanced shaders as described in the following.

The default vertex shader multiplies the incoming vertices with the combined modelview and projection matrix and computes the appropriate 2D texture coordinates for each tile. It is selected via minicache::setshader() and enabled via minicache::useshader(). Own vertex shaders are selected by passing a program string via minicache::setshader("!!ARBvp...").

   // default vertex shader
   static const char *vtxprog="!!ARBvp1.0 \n\
      PARAM t=program.env[0]; \n\
      PARAM e=program.env[1]; \n\
      PARAM u=program.env[2]; \n\
      PARAM v=program.env[3]; \n\
      PARAM d=program.env[4]; \n\
      PARAM c0=program.env[5]; \n\
      PARAM c1=program.env[6]; \n\
      PARAM c2=program.env[7]; \n\
      PARAM c3=program.env[8]; \n\
      PARAM c4=program.env[9]; \n\
      PARAM c5=program.env[10]; \n\
      PARAM c6=program.env[11]; \n\
      PARAM c7=program.env[12]; \n\
      PARAM mat[4]={state.matrix.mvp}; \n\
      PARAM matrix[4]={state.matrix.modelview}; \n\
      PARAM invtra[4]={state.matrix.modelview.invtrans}; \n\
      TEMP vtx,col,nrm,pos,vec,gen; \n\
      ### fetch actual vertex \n\
      MOV vtx,vertex.position; \n\
      MOV col,vertex.color; \n\
      MOV nrm,vertex.normal; \n\
      ### transform vertex with combined modelview \n\
      DP4 pos.x,mat[0],vtx; \n\
      DP4 pos.y,mat[1],vtx; \n\
      DP4 pos.z,mat[2],vtx; \n\
      DP4 pos.w,mat[3],vtx; \n\
      ### transform normal with inverse transpose \n\
      DP4 vec.x,invtra[0],nrm; \n\
      DP4 vec.y,invtra[1],nrm; \n\
      DP4 vec.z,invtra[2],nrm; \n\
      DP4 vec.w,invtra[3],nrm; \n\
      ### write resulting vertex \n\
      MOV result.position,pos; \n\
      MOV result.color,col; \n\
      ### calculate tex coords \n\
      MAD result.texcoord[0].x,vtx.x,t.x,t.z; \n\
      MAD result.texcoord[0].y,vtx.z,t.y,t.w; \n\
      MUL result.texcoord[0].z,vtx.y,e.y; \n\
      ### pass normal as tex coords \n\
      MOV result.texcoord[1],vec; \n\
      ### calculate eye linear coordinates \n\
      DP4 pos.x,matrix[0],vtx; \n\
      DP4 pos.y,matrix[1],vtx; \n\
      DP4 pos.z,matrix[2],vtx; \n\
      DP4 pos.w,matrix[3],vtx; \n\
      DP4 gen.x,pos,u; \n\
      DP4 gen.y,pos,v; \n\
      MAD result.texcoord[2].x,gen.x,d.x,d.y; \n\
      MAD result.texcoord[2].y,gen.y,d.z,d.w; \n\
      ### calculate spherical fog coord \n\
      DP3 result.fogcoord.x,pos,pos; \n\
      END \n";
The parameter t holds bias and scaling constants to compute the 2D texture coordinates in the x- and y-component of the result texture coordinate vector. The parameter e holds the scaling factor of the incoming elevations to compute the current true elevation. These true elevation values are passed to the pixel shader in the z-component of the texture coordinate vector so that per-fragment computations can easily depend on elevation. The parameters u,v and d are used for eye linear texture coordinate generation with texture unit #2. The parameter vectors t, e and d are supplied automatically by the minicache, but the parameter vectors u/v and c0-c7 may hold user-definable constants that can be supplied via minicache::setvtxshadertexgen(tileset,s1..t4) and minicache::setvtxshaderparams(x,y,z,w[,n]), respectively.

The default pixel shader takes the actual 2D texture coordinates and fetches the corresponding color from texture #0 which holds the current texture tile. After that the texture color is shaded and multiplied with the current fragment color to mimic the standard modulating texture environment.

   // default pixel shader
   static const char *frgprog="!!ARBfp1.0 \n\
      PARAM a=program.env[0]; \n\
      PARAM t=program.env[1]; \n\
      PARAM l=program.env[2]; \n\
      PARAM p=program.env[3]; \n\
      PARAM o=program.env[4]; \n\
      PARAM c0=program.env[5]; \n\
      PARAM c1=program.env[6]; \n\
      PARAM c2=program.env[7]; \n\
      PARAM c3=program.env[8]; \n\
      PARAM c4=program.env[9]; \n\
      PARAM c5=program.env[10]; \n\
      PARAM c6=program.env[11]; \n\
      PARAM c7=program.env[12]; \n\
      TEMP col,colt,nrm,len; \n\
      ### fetch fragment color \n\
      MOV col,fragment.color; \n\
      ### fetch texture color \n\
      TEX colt,fragment.texcoord[0],texture[0],2D; \n\
      MAD colt,colt,a.x,a.y; \n\
      ### modulate with fragment color \n\
      MUL col,col,colt; \n\
      ### modulate with directional light \n\
      MOV nrm,fragment.texcoord[1]; \n\
      DP3 len.x,nrm,nrm; \n\
      RSQ len.x,len.x; \n\
      MUL nrm,nrm,len.x; \n\
      DP3_SAT nrm.z,nrm,l; \n\
      MAD nrm.z,nrm.z,p.x,p.y; \n\
      MUL_SAT col.xyz,col,nrm.z; \n\
      ### write resulting color \n\
      MOV result.color,col; \n\
      END \n";
As with vertex shaders, the parameter vectors c0-c7 may hold four additional user-specific constants that can be set via minicache::setpixshaderparams(x,y,z,w[,n]).

Here is a simple usage example which adds contour lines to the bathymetry of a data set, which means that only negative elevations will show contours:

   // declare the cache
   minicache cache;

   // enable default vertex shader plugin
   cache.setvtxshader();
   cache.usevtxshader();

   // fragment program for adding contour lines to the bathymetry
   static const char *frgprog="!!ARBfp1.0 \n\
      PARAM a=program.env[0]; \n\
      PARAM c0=program.env[5]; \n\
      TEMP col,colt,vtx; \n\
      MOV col,fragment.color; \n\
      TEX colt,fragment.texcoord[0],texture[0],2D; \n\
      MAD colt,colt,a.x,a.y; \n\
      MUL col,col,colt; \n\
      MUL vtx.x,fragment.texcoord[0].z,c0.x; \n\
      FRC vtx.y,vtx.x; \n\
      MAD vtx.y,vtx.y,c0.z,-c0.w; \n\
      ABS vtx.y,vtx.y; \n\
      SUB vtx.y,c0.w,vtx.y; \n\
      MUL_SAT vtx.y,vtx.y,c0.y; \n\
      CMP vtx.y,vtx.x,vtx.y,c0.w; \n\
      MUL col.xyz,col,vtx.y; \n\
      MOV result.color,col; \n\
      END \n";

   // enable pixel shader plugin
   cache.setpixshader(frgprog);
   cache.setpixshaderparams(contourspacing,contourwidth,2.0f,1.0f);
   cache.usepixshader();

   // render actual content of the cache
   cache.render(...);

   // disable programs
   cache.usevtxshader(0);
   cache.usepixshader(0);
The example is part of the Hawaii Demo (see Section (I)), so you can actually watch the shaders working together by pressing 'c' during the demo.

Hint: If for any reason you need the rendered triangle mesh to be semi-transparent, set its opacity with cache.setopacity(alpha). The blended mesh, however, might show artifacts due to incorrect blending order. To avoid these artifacts we can use a two-pass algorithm. First, we render the mesh with alpha=0 to update the Z-buffer only. Then we render the mesh a second time with alpha>0 to blend the mesh without ordering artifacts.

Another usage example is per-fragment lighting: Let us first assume that the RGB texture contains the horizontal components x and z of the normal vector mapped to the R and G channels. Let us also assume that the B channel contains a gray scale image. Then the vertical component y of the normal vector can be computed from the horizontal components using y=sqrt(1-x*x-z*z). For diffuse shading we supply a light direction in the shader parameters and compute the dot product of the light direction with the normal vector. Then we multiply this with the B channel to get a shaded gray value. The elevations provided in the z-component of the texture coordinate vector may be additionally used to derive a color mapping which modulates the shaded gray values giving a final shaded color. The advantage of using a pixel shader for the calculation of the lighting equations is that the light conditions can be changed interactively.

Hint: Normal maps can be computed with pnmsample::normalize. The method takes a collection of grids and computes a georeferenced normal map for each of them. Afterwards the normal maps can be resampled just like texture maps. Both the resampled normal maps and the resampled texture maps are loaded by the databuf::loadPPMnormalized method which produces a texture with the normal map in the R and G and the gray value of the original texture in the B channel. Start the Hawaii Demo with the -n option to see the result of this approach.

Back to top

(K) Asynchronous Paging

Many high-resolution terrain data sets do not fit into main memory. In such a case out-of-core methods are needed which operate on tile sets. This has been described in detail in the previous sections. To give an example, we've got a data set of entire Oahu, Hawai'i, which has a resolution of less than 0.5 meters for the texture maps. The total uncompressed size of the data set is more than 70 GB. This clearly doesn't fit neither into main memory nor into the texture memory of the graphics card.

In order to view this data set in real-time we resampled it to a 100x80 tile set. This tile set is visualized out-of-core using the described libMini paging callback concept. Whenever a tile needs to be paged into memory, the callback is triggered and the corresponding tile is loaded. However, while loading the requested data most of the time is wasted with busy waiting for the hard disk to seek and spin to the correct file position. This can take up 150ms even for the tiniest files. Since we cannot continue rendering while we wait for the data to arrive the frame rate usually drops down to a mere 5-10 fps.

Therefore, we need to decouple the disk access from rendering in order to get a smooth rendering experience. For this purpose, the Mini Library contains an asynchronous tile cache which loads the requested tiles in a background thread without blocking the main rendering thread.

We first assume that the tile set is defined via a miniload object. The tile set should contain S3TC compressed textures for best paging performance or uncompressed textures for best image quality:

   miniload *tileset=new miniload;
Then we enable the asynchronous paging mechanism (with a single background thread):

   #include <mini/datacloud.h>

   static const int numthreads=1;

   datacloud *cloud=new datacloud(tileset);
   cloud->setloader(request_callback,NULL,check_callback,1,1.25f*farp,0.01f*farp,pbasesize,1,10,1000);
   cloud->getterrain()->setradius(0.03f*farp,1.0f); // optional non-linear texture LOD drop-off distance
   cloud->setinquiry(inquiry_callback,NULL); // optional callback for better paging performance
   cloud->setquery(query_callback,NULL); // optional callback for better paging performance
   cloud->setschedule(0.02,0.5,1.0); // upload for 20ms, keep for 30sec, invalidate after 1sec
   cloud->setmaxsize(128.0); // allow 128 MB tile cache size
   cloud->setthread(startthread,NULL,jointhread,lock_cs,unlock_cs,lock_io,unlock_io);
   cloud->setmulti(numthreads);
   threadinit();
We additionally need to define two mandatory and two optional callbacks (one for loading data, one for checking file existence, one for optionally checking the elevation range of a height field and one for optionally querying the image size of a texture map):

   void request_callback(const unsigned char *mapfile,databuf *map,int istexture,int background,void *data)
      {map->loaddata((char *)mapfile);}

   int check_callback(const unsigned char *mapfile,int istexture,void *data)
      {return(checkfile((const char *)mapfile));}

   void inquiry_callback(int col,int row,const unsigned char *mapfile,int hlod,void *data,float *minvalue,float *maxvalue)
      {
      *minvalue=0.0f;
      *maxvalue=10000.0f;
      return(1);
      }

   void query_callback(int col,int row,const unsigned char *texfile,int tlod,void *data,int *tsizex,int *tsizey)
      {
      int tbasesize=2048; // size of texture LOD 0
      while (tlod-->0) tbasesize/=2;
      *tsizex=*tsizey=tbasesize;
      }
We finally have to define the callbacks for creating and locking the background thread. In the following example implementation we are using POSIX threads (pthreads), but any other multi-threading library like OpenThreads could be used as well:

   #include <pthread.h>

   #include <mini/datacloud.h>

   pthread_t pthread[numthreads];
   pthread_mutex_t mutex,iomutex;
   pthread_attr_t attr;

   void threadinit()
      {
      pthread_mutex_init(&mutex,NULL);
      pthread_mutex_init(&iomutex,NULL);

      pthread_attr_init(&attr);
      pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_JOINABLE);
      }

   void threadexit()
      {
      pthread_mutex_destroy(&mutex);
      pthread_mutex_destroy(&iomutex);

      pthread_attr_destroy(&attr);
      }

   void startthread(void *(*thread)(void *background),backarrayelem *background,void *data)
      {pthread_create(&pthread[background->background-1],&attr,thread,background);}

   void jointhread(backarrayelem *background,void *data)
      {
      void *status;
      pthread_join(pthread[background->background-1],&status);
      }

   void lock_cs(void *data)
      {pthread_mutex_lock(&mutex);}

   void unlock_cs(void *data)
      {pthread_mutex_unlock(&mutex);}

   void lock_io(void *data)
      {pthread_mutex_lock(&iomutex);}

   void unlock_io(void *data)
      {pthread_mutex_unlock(&iomutex);}
The loaddata, loadPNMdata and loadPVMdata methods of a databuf object are reentrant. In principle these methods can be used safely to load data in the background thread. However, they rely on the file IO functions of the operating system (fopen, fread, fwrite, fclose, fscanf and fprintf of the stdlibc++) to be thread-safe as well. This is usually the case but it is not guaranteed for all operating systems. As a safety measurement, libMini uses the lock_io and unlock_io functions to lock the request callback which is performing the IO in the background thread. If it is known in advance that the entire request callback is thread-safe, the two functions may be omitted in the setthread call.

Similarly, the loadPPMcompressed method is not reentrant because it is using OpenGL to compress the incoming data on-the-fly. If you want to pass S3TC compressed textures in the background thread you need to store pre-compressed data on the hard disk and load this data with a standard loaddata call. This approach saves a lot of disk space and is much faster than compressing the data on-the-fly.

In order to start multiple background threads we simply set numthreads to a higher value (e.g. 10). Typically, this is not needed if data is stored on a fast hard disk, but it has a performance advantage if the data is arriving over a slow network connection.

Adding these lines to the code will lead to a very smooth out-of-core visualization experience even for the mentioned 70 GB data set of Oahu. To give some performance details, the demo is running at a consistent 25 fps on my Apple Powerbook Pro with 1.5 GB of main memory, 1.83GHz Core Duo and ATI X1600 with 128MB VRAM.

An application that is using the asynchronous tile cache should update the scene each time the view point is changed. Additionally, it should update the scene until there are no pending tiles left to be paged in. This can be achieved in the following way:

   static int pending=1;
   if (pending!=0) tileset->draw(...);
   pending=cloud->getpending();
If a render cache is attached to the tile set the cache should be flushed after all pending tiles have been processed. As an alternative, the application could just render continuously with a given target frame rate. This obviously ensures that all arriving tiles will be displayed eventually.

Information about the actual streaming status can be printed by the following example code snippet:

   printf("streaming: pending=%d mem=%gMB\n",
          cloud->getpending(), // total number of pending tiles
          cloud->getmem()); // total memory foot print
In order to quit an application which is using the asynchronous tile cache the background thread needs to be stopped beforehand. It is stopped implicitly if the datacloud object is deleted but it can be stopped at any time with an explicit call of cloud->stopthread(). Before the application can quit it also needs to release the background thread (see threadexit() in the above example).

Note: By default, the tile cache module releases all the databuf objects that are passed to it. If this is not the desired behaviour, the memory chunks encapsulated into a databuf object can be configured not to be released via cloud->configure_dontfree(1).

The performance of loading tiles from disk is mainly limited by the number of tiles and only to a certain degree by the tile size. This is different if the tiles arrive over a network connection (see also Section (L)). In such a case the startup time is determined by the size and the number of the tiles that need to be loaded initially. We can reduce the number of initially loaded tiles (and minimize startup time) by telling libMini that only a subset of the visible tiles is mandatory for startup. After these tiles have been loaded initially the remaining tiles are paged in consecutively in the background thread. Typically, a useful startup subset is a small area around the initial point of view (ex,ey,ez):

   tileset->restrictroi(ex,ez,farp/3);
The tile size quadratically depends on the distance to the point of view. Therefore, the selection of a view point high above the scene (bird's eye view) additionally leads to a reduced initial traffic on the net. If the initial point of view is lying on the terrain, the traffic will be much higher, but we can mimic a high point of view by applying the following trick before the first frame is rendered:

   tileset->updateroi(res,
                      ex,ey+10*farp,ez,
                      ex,ez,farp);
This has the effect, that only low resolution tiles are loaded initially. These are replaced by higher resolution tiles as soon as they are coming in over the net. To apply the trick to the entire tile set use tileset->updateall().

For very large tile sets it is also important to save disk space. With S3TC compression only a compression of 1:6 is possible. In order to go beyond this compression ratio, the tiles need to be stored in JPEG format, for example. This is not a native format of libMini so that the conversion hook of the databuf object must be registered with a function that is able to to export and reimport that data (see Appendix (C)).

Since the textures are now stored in JPEG format on disk, they need to be decoded and uploaded in raw format to the graphics memory. If S3TC compression is required to fit the textures into the graphics memory, we also need to recompress the textures on-the-fly. For this purpose the squish library of Simon Brown is highly recommended. See Section (M) how to use this library. Back to top

(L) Remote Paging

Those who have been reading until this point, can be truly called libMini experts. In the following we are approaching the next level: in the previous section we have seen how to use a background thread to page data concurrently to rendering. To do so we need to implement the callback API of the datacloud class. In our previous example our implementation was just loading files from disk, but in principle it doesn't make a difference if the data is coming from a local disk or from a remote server. The only difference is that data will be arriving much slower over a network connection, meaning that we should use highly compressed data whenever possible. And of course the implementation needs to use a library like libcurl to transfer the files from the remote server to the client.

The libMini library contains a sample implementation of a transfer module. To use this module we make the following modifications to the example code of the previous section:

   tilecache=new datacache(tileset);
   tilecache->setremoteid(REMOTEID);
   tilecache->setremoteurl(REMOTEURL);
   tilecache->setlocalpath(LOCALPATH);
   tilecache->setstartupfile(STARTUPFILE);
   tilecache->setloader(request_callback,NULL);
   tilecache->getcloud()->setschedule(0.02,5.0,1.0); // upload for 20ms, keep for 5min, invalidate after 1sec
   tilecache->getcloud()->setmaxsize(256.0); // allow 256 MB tile cache size
   tilecache->getcloud()->setthread(startthread,NULL,jointhread,lock_cs,unlock_cs);
   tilecache->configure_netthreads(numthreads);
   tilecache->setreceiver(receive_callback,NULL,check_callback);
   tilecache->load();
The callbacks used by the transfer module are slightly different:

   void request_callback(const char *file,int istexture,databuf *buf,void *data)
      {buf->loaddata(file);}

   void receive_callback(const char *src_url,const char *src_id,const char *src_file,const char *dst_file,int background,void *data)
      {geturl(src_url,src_id,src_file,dst_file,background);}

   int check_callback(const char *src_url,const char *src_id,const char *src_file,void *data)
      {return(checkurl(src_url,src_id,src_file));}
The geturl and checkurl functions use libcurl to negotiate and transfer data over the net. Please see the libMini Viewer (Section (P)) how this can be done in detail.

Back to top

(M) Automatic S3TC Compression

For high-resolution imagery, texture compression is crucial. For that reason, the resampled tiles are typically stored in S3TC format (see also previous sections). The compression ratio of S3TC is 1:6 for RGB images. In order to achieve a higher compression ratio, JPEG can be used as an external format.

With that approach a compression ratio of up to 1:20 can be achieved with still good image quality. However, the images now have to be recompressed with S3TC on-the-fly. For that purpose, libMini features the auto-compression hook. Whenever libMini encounters an uncompressed texture and the auto-compression hook is set it automatically tries to run the texture data through the compression hook. Below is a reference implementation of the S3TC compression hook using the squish library of Simon Brown.

   void autocompress(int isrgbadata,unsigned char *rawdata,unsigned int bytes,
                     unsigned char **s3tcdata,unsigned int *s3tcbytes,int width,int height,
                     void *data)
      {
      int i;

      int mode;

      unsigned char *rgbadata;

      static const int modefast=squish::kDxt1 | squish::kColourRangeFit; // fast but produces artifacts
      static const int modegood=squish::kDxt1 | squish::kColourClusterFit; // almost no artifacts though much slower
      static const int modeslow=squish::kDxt1 | squish::kColourIterativeClusterFit; // no artifacts but really sluggish

      mode=modefast; // we strive to compress as fast as possible

      if (isrgbadata==0)
         {
         rgbadata=(unsigned char *)malloc(4*width*height);
         if (rgbadata==NULL) ERRORMSG();

         for (i=0; i<width*height; i++)
            {
            rgbadata[4*i]=rawdata[3*i];
            rgbadata[4*i+1]=rawdata[3*i+1];
            rgbadata[4*i+2]=rawdata[3*i+2];
            rgbadata[4*i+3]=255;
            }

         rawdata=rgbadata;
         }

      *s3tcbytes=squish::GetStorageRequirements(width,height,mode);
      *s3tcdata=(unsigned char *)malloc(*s3tcbytes);
      if (*s3tcdata==NULL) ERRORMSG();

      squish::CompressImage(rawdata,width,height,*s3tcdata,mode);

      if (isrgbadata==0) free(rawdata);
      }
To register the above compression hook we use the following one-liner:

   databuf::setautocompress(autocompress,NULL);
Finally, the S3TC auto-compression is turned on in the background thread via datacloud::configure_autocompress(1).

The auto-compression also applies to mip-mapped textures. In order to automatically generate mip-maps prior to compressing the texture data we use datacloud::configure_automipmap(1).

Please note that the auto-compression hook is triggered from the background thread. Therefore it cannot use OpenGL functionality, because the background thread has no OpenGL context. This is the reason why we need to utilize a compression library like squish instead of the OpenGL driver.

Back to top

(N) Dynamic Terrain

For certain applications such as utility or telegraph pole placement it is necessary to modify the terrain at run time. Depending on the extent of the modified area libMini offers the following options:

Back to top

(O) The libMini Viewer API

The libMini Viewer API incorporates the previously descibed techniques into a single package: the rendering core, the vertex buffer and the asynchronous pager are hidden under the hood of the API. This provides convenient access to the core capabilities of libMini.

Given that an OpenGL rendering window has been setup with an appropriate viewing matrix, the task of rendering a tileset with the libMini Viewer API is as simple as follows:

   #include <mini/miniview.h>

   miniview *viewer=new miniview;

   viewer->getearth()->load("url");

   minicoord eye(miniv3d(ex,ey,ez)); // eye point in ECEF
   miniv3d dir(dx,dy,dz); // viewing direction
   miniv3d up(ux,uy,uz); // up vector

   float aspect=width/height; // aspect ratio of viewing window

   viewer->get()->nearp=10.0; // near clipping plane
   viewer->get()->farp=10000.0; // far clipping plane

   viewer->initeyepoint(eye); // prefetch data

   viewer->clear(); // clear frame buffer
   viewer->cache(eye,dir,up,aspect); // fill vertex cache
   viewer->render(); // render vertex cache
The coordinates eye/dir/up specify the camera coordinate system in OpenGL notation. The camera coordinates are interpreted as geo-referenced ECEF coordinates.

The terrain geometry can be queried by shooting rays at the displayed scene. The rays are specified by a geo-referenced starting point (in ECEF coordinates) and a shooting direction:

   double dist=viewer->shoot(eye,dir); // shoot ray from eye point
   if (dist==MAXFLOAT) dist=0.0; // nothing hit
   minicoord hitpoint=eye+dist*dir; // calculate hit point in ECEF
Displayable tilesets can be generated with libGrid or VTB. For more details, see the following section.

Back to top

(P) The libMini Viewer

The libMini Viewer is an application for viewing tile sets based on the libMini Viewer API. It can load local and remote tile sets stored on a web server. The supported format is either PNM which is exported by the built-in libMini resampler or DB which is output by the gridding core of libGrid or VTB (virtual terrain builder of vterrain.org).

The libMini viewer also comes as a QT based version with intuitive mouse steering and navigation around the earth globe. For the QT based version the viewer usage guide lines apply in an analog way, except that it needs to be compiled with QT's qmake. To get the whole package, checkout the entire libMini svn repository with:

   svn co http://svn.code.sf.net/p/libmini/code/libmini libmini
Then the libMini QTViewer is located in the "qtviewer" directory and the libMini libraries in the "mini" directory. For more details see the QTViewer README.

For conceptual information on the modules used by the viewer see the libMini Module Overview.

To compile the viewer type "./build.sh viewer" on the command line. It requires POSIX threads (pthreads), libjpeg, libpng / zlib, libcurl and squish to be installed.

On Windows the freeGLUT library must be installed, as well. On MacOS X and Linux it comes with the default OpenGL installation. As a substitute for POSIX threads it is recommended to use either pthreads-win32 or openthreads from the OpenSceneGraph repository.

The libMini Viewer can also be compiled with CMake. To do so, enable the option BUILD_MINI_APPS in the CMake configuration (via ccmake or the Windows CMake GUI). The cmake configuration has the additional options BUILD_MINI_WITHOUT_SQUISH, BUILD_MINI_WITH_GREYC and BUILD_MINI_WITH_OPENTHREADS to compile without squish, with CIMG/GREYCstoration or with openthreads, respectively.

If some of the required or optional dependencies cannot be found, because they have not been installed at a standard location like /usr/local, you can specify each directory separately or override the standard installation directory with the variable LIBMINI_THIRDPARTY_DIR in the CMake configuration.

Once you managed to install the dependencies and compile the libMini Viewer, see the following examples how to use it from the command line:

   local usage:
      viewer <local.base.path> <tileset.path> <elevation.subpath> imagery.subpath { <options> }

   local example (for loading the data of the Hawaii Demo):
      viewer ~user/.../Hawaii/ data/HawaiiTileset/ elev imag

   remote usage:
      viewer "<http-address>" <tileset.path> <elevation.subpath> <imagery.subpath> { <options> }

   remote example:
      viewer "http://server.inter.net/.../" tileset/ elevation imagery
If the data is resampled with VTB make sure that a metric coordinate system like UTM is chosen. Also make sure that the two ini files for the elevation and the imagery are made available in the tile set path, because the viewer automatically retrieves necessary information from the ini files. The libMini Viewer tries to guess their names by adding the .ini suffix to the respective subpath. The default setting for the vtp subpaths is "elev" and "imag". If you stick to that naming convention, when generating the elevation and imagery tile sets with vtp, you can start the libMini Viewer with just one argument:

   short usage:
      viewer <url/path>
This is equivalent to the multi-argument usage of "viewer url/ path/ elev imag", so that the directory layout has to be as follows:

   url/
       path/
            elev/
                 tile.0-0.db
                 ...
            elev.ini
            imag/
                 tile.0-0.db
                 ...
            imag.ini
For multiple tilesets we use the following command line:

   multi usage:
      viewer -m { <url/path> [ <detailtex.db> ] }
There is no limit on the maximum number of displayable tilesets. All tilesets will be shown at their corresponding geo-referenced place on earth. But keep in mind that the performance is mainly limited by the number of visible tiles, so that the total number of visible tilesets and tiles should be kept as low as possible.

As an option, the Mini Viewer supports one geo-referenced detail texture per tileset. Currently, the only supported file format for the detail textures is libMini's internal DB format.

The initial viewing settings favor rendering performance over quality. To increase the visualization quality, you can increase the following quality parameters interactively: the far clipping distance (farp), the triangle mesh resolution (res) and the texture detail level (range). To check these parameters press the h key. This turns on the head up display (HUD) which displays information about the available keyboard controls.

Optionally, you can run the libMini Viewer in anaglyph stereo mode by appending "-s -a" to the command line. You need to put on red/cyan glasses to get the stereo effect. To start the viewer in full-screen mode use the -f option. For full usage information on all available command line options start the viewer without arguments.

On a side note, the libMini Viewer can be configured to use OpenThreads instead of pthreads by typing "./build.sh viewer useopenth". On Windows, the libMini Viewer uses OpenThreads by default but can be configured to use a pthread implementation such as pthreads-win32.

There is also the option to disable squish support ("./build.sh viewer nosquish") which provides S3TC compression on the fly. Another option is to enable CImg/GREYCstoration support ("./build.sh viewer usegreyc") to denoise uncompressed imagery on the fly.

Back to top

(Q) Error Handling

Normally, libMini will run silently doing just what it is ought to do. However, if it encounters insufficient resources (either insufficient memory or disk space) it will print an error on the console and quit. For that purpose it uses the macro ERRORMSG().

If it is required to catch these errors, a signal handler can be provided via setminierrorhandler() as defined in minibase.h to safely handle the exceptions (e.g. by closing or restarting the terrain renderer without shutting down the main application).

Back to top

(R) Linkage with CMake

If you want to link your own project with libMini using CMake, copy the CMakeModule/FindMINI.cmake module into your project and let CMake find the libMini package and the required OpenGL installation in the cmake file "CMakeLists.txt":

   SET(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules;${CMAKE_MODULE_PATH}")

   FIND_PACKAGE(OpenGL)

   FIND_PACKAGE(MINI)
   INCLUDE_DIRECTORIES(${MINI_INCLUDE_DIR})

   IF (MINI_FOUND)
      MESSAGE(STATUS found libMini headers: ${MINI_INCLUDE_DIR})
      MESSAGE(STATUS found libMini libs: ${MINI_LIBRARIES})
   ENDIF (MINI_FOUND)
   IF (MINISFX_FOUND)
      MESSAGE(STATUS found libMiniSFX libs: ${MINISFX_LIBRARIES})
   ENDIF (MINISFX_FOUND)
Then add libMini for basic libMini support or libMiniSFX for additional viewer support and the OpenGL libraries as additional link target for your application, e.g. "myapp":

   TARGET_LINK_LIBRARIES(myapp ${MINI_LIBRARIES} ${OPENGL_LIBRARIES})
For an example, see the "example.cmake" folder, which contains an example libMini CMake project with the dependencies OpenGL and GLUT. To compile it, type the following command line in the Unix terminal:

   cmake . && make
Back to top

(S) Final Acknowledgements

In particular, I would like to thank Ben Discoe of vterrain.org for his suggestions and valuable feedback on the terrain rendering API during his efforts to include the Mini Library into the VTP.

I also would like to thank Ingo Frick of Massive Development for many interesting discussions on implementation specific details while porting the terrain renderer to the AquaNox game engine. Many thanks also go to Olivier Pascal for his valuable feedback and to the folks at Makai Ocean Engineering for their great support: Jose Andres, Tie Fang and Greg Gillenwaters.

Comments or suggestions are highly appreciated. Please do not hesitate to contact the author at the given email address.

Have fun,
Stefan

Back to top

Appendix Of Optional Modules

Appendix (1) Minisky

This class implements a sky dome which is textured by a 2D texture parametrized with polar coordinates. For an example please see the libMini Viewer (Section (P)).

Back to top

Appendix (2) Minitext

This module implements a 3D OpenGL text renderer which uses a minimalistic vector representation of the ASCII character set. All characters used by the C programming language are supported. The minitext is mainly intended for prototyping purposes where fully-fledged anti-aliased text would be sort of an overkill. For example, it is used in the libMini Viewer (see Section (P)) to render the HUD.

Back to top

Appendix (3) Vegetation Rendering

The Mini Library also supports vegetation rendering. The height of the vegetation layer is defined by an additional height field. Now for all rendered triangles a prism is generated that stacks on top of each triangle and fits in between the terrain and the upper boundary of the vegetation layer. This is a volumetric description of the vegetation. The so-defined vegetation volume is used to place plants of varying height. As a result, wood, shrub and meadow is generated in a procedural way from the additional input height field.

A more detailed description is given in the paper. The reader is also encouraged to try the Fraenkische Demo which applies the described approach.

Back to top

Appendix (4) Volume Rendering With The Minibrick

The minibrick class implements volume rendering of regular time-dependent data by displaying multiple shaded semi-transparent iso surfaces. The complexity of the scene is controlled by using a volumetric C-LOD approach and an octree for the efficient culling of sub-volumes that do not contain any iso surface.

For conceptual information on the volume rendering approach see the libMini Volume Rendering Overview.

A volume is given by a tile set with r rows and c columns that extend in the horizontal plane and form what is called a minibrick. The preferable tile size is 2^n+1 (n may vary to yield varying size along the tile edges). The tile data needs to be provided in a databuf object container which is passed to the library using a callback mechanism. The load callback is triggered for each visible tile. In the callback the tile to be loaded is identified by its row and column. The availability of each tile is checked with the isavailable callback. Currently only two methods are provided that load a PVM (see Section Appendix (6B)) or a MOE volume and store the data in the databuf object. So usually the application layer will implement its own methods for setting up the databuf objects being passed in the load callback. Use the minibrick.setloader method to register your own callbacks with the library. The tiles do not need to be axis-aligned, but must have a rectangular basis. Therefore, please ensure that the corner coordinates of each databuf object are set to suitable values. Otherwise seams will be visible.

The appearance of a minibrick volume is determined by a so called spectrum of iso surfaces. Each single iso surface of the spectrum is defined by using the minibrick.addiso(iso,R,G,B,A) method which specifies the iso value and the corresponding RGB color and opacity of each iso surface.

Three different rendering methods can be configured. These methods implement either 2-, 3- or 4-pass rendering. The 2-pass method renders the opaque triangles in the first pass and the semi-transparent triangles in the second pass. This is the fastest available method, but artifacts may arise because the semi-transparent geometry is only sorted by iso surface number and not by depth order. In order to suppress these artifacts, the 3-pass method accumulates the opacity in the second pass and sums up the emission in the third pass. The 4-pass method improves image quality even further by selectively neglecting the emissions behind the first encountered back-face. The 3-pass method is a good compromise between speed and visual quality, thus it is enabled by default.

It is possible to render an arbitrary number a bricks simultaneously. The bricks could even intersect each other. For this to work, the render passes of each single brick have to be interleaved in the following way:

   // declare n bricks
   minibrick bricks[n];

   // render the bricks in an interleaved fashion
   for (int i=MINIBRICK_FIRST_RENDER_PHASE; i<=MINIBRICK_LAST_RENDER_PHASE; i++)
      for (int j=0; i<n; j++)
         brick[j].render(ex,ey,ez,rad,farp,fovy,aspect,time,i);
Additionally, each brick can have up to six clipping planes that are set via minibrick::setclip. The clip planes are defined by a number, an origin and a normal vector.

The level of detail of the visualization is determined by the radius parameter rad. Within this radius around the view point the maximum level of detail is enabled. Outside the radius the resolution gradually decreases. The library interpolates smoothly between the level of details so that the popping effect is suppressed efficiently. Since this involves a good deal of floating point arithmetic the user should use the multi-threading support of the library to decouple the update of the iso surface geometry from rendering. This means that one thread continuously updates the geometry (if the view point has changed) while the other thread is busy rendering the latest cached geometry. This approach has the advantage that the frame rate only depends on the speed of the graphics hardware and is not limited by the update time that is needed to interpolate and extract the iso surfaces. Multi-threading is enabled by passing appropriate callbacks to the minibrick::setthread method as illustrated in the Hawaii Demo (see Section (I)).

In order to get a better understanding of the capabilities of the minibrick module please check out the Hawaii Demo. Start it with the -b option, press 'm' to go to Makai Pier in Waimanalo at the east side of Oahu and look at the scene with a bird's eye view. Then you see a time-dependent visualization of the evolution of a thunder storm with one opaque and two semi-transparent iso-surfaces.

Back to top

Appendix Of File Formats

Appendix (A) PNM Image Format Description

The Mini Library supports the PNM image format (PNM = Portable aNy-Map) to read tile sets from disk. Color images have the file extension .ppm (Portable Picture Map = PPM), gray-scale images have the extension .pgm (Portable Gray-scale Map = PGM). The format consists of an ASCII header that defines type, size and bit depth in an easily readable way (see netpbm.sourceforge.net). The raw image data follows directly after the header. In contrast to the original netpbm library libMini does not optionally support ASCII image data and it also does not support image types other than color and gray-scale. With these restrictions a plain PNM image is defined as follows:

   <TYPE>\n
   <WIDTH> <HEIGHT>\n
   <MAXVAL>\n
   ...DATA...
with

   <TYPE>   = P5 | P6 | P8 ::: P5 = PGM, P6 = PPM, P8 = RGBA
   <WIDTH>  = %d           ::: width of texture/heightmap
   <HEIGHT> = %d           ::: height of texture/heightmap
   <MAXVAL> = %d           ::: maximum value
For 8 bit images MAXVAL is 255, for 16 bit images MAXVAL is either 32767 or 65535. In the 16 bit case libMini always assumes the data to be signed 16 bit (stored in MSB format). The header may additionally contain comments starting with a '#' in each line.

The plain PNM format does not contain georeferencing information. For this purpose, libMini is using a comment section after the TYPE identifier to include the missing information (thanks to Kyle Dickerson for the compilation):

   <TYPE>
   # description=<DESCRIPTION>
   # coordinate system=<COORD_SYS>
   # coordinate zone=<COORD_ZONE>
   # coordinate datum=<COORD_DATUM>
   # SW corner=<SW_X>/<SW_Y> <SW_UNITS>
   # NW corner=<NW_X>/<NW_Y> <NW_UNITS>
   # NE corner=<NE_X>/<NE_Y> <NE_UNITS>
   # SE corner=<SE_X>/<SE_Y> <SE_UNITS>
   # cell size=<CELL_X>/<CELL_Y> <CELL_UNITS>
   # vertical scaling=<VERT_SCALE> <VS_UNITS>
   # missing value=<MISSING_VAL>
   <WIDTH> <HEIGHT>
   <MAXVAL>
   ...DATA...
with

   <MAGIC DESCRIPTOR> = BOX | DEM | TEX ::: BOX = bounding box, DEM = digital elevation model, TEX = texture map
   <DESCRIPTION> = %s
   <COORD_SYS> = LL | UTM
   <COORD_ZONE> = %d
      if (<COORD_SYS> == LL) then <COORD_ZONE> = 0
      if (<COORD_SYS> == UTM) then (<COORD_ZONE> != 0 && <COORD_ZONE> > -60 && <COORD_ZONE> < 60)
   <COORD_DATUM> = %d
      if (<COORD_SYS> == LL) then <COORD_DATUM> = 0 (assuming WGS84 datum)
      else if (<COORD_DATUM> <1 || <COORD_DATUM> >14) then <COORD_DATUM> = 3 (WGS84)

      1  = NAD27 (Mean North American Datum of 1927)
      2  = WGS72 (World Geodetic System of 1972)
      3  = WGS84 (World Geodetic System of 1984)
      4  = NAD83 (Mean North American Datum of 1983)
      5  = Sphere (with radius 6370997 meters)
      6  = ED50 (Mean European Datum of 1950, centered at the Munich Frauenkirche)
      7  = ED87 (Mean European Datum of 1987)
      8  = OldHawaiian (mean datum for Hawaii/Maui/Oahu/Kauai)
      9  = Luzon (Philippine Datum)
      10 = Tokyo (Mean Japanese Datum)
      11 = OSGB1936 (mean datum of the Ordnance Survey Great Britain 1936)
      12 = Australian1984 (Mean Australian Geodetic Datum of 1984)
      13 = NewZealand1949 (New Zealand Datum of 1949)
      14 = SouthAmerican1969 (Mean South American Datum of 1969)

   <SW_X>, <SW_Y>, <NW_X>, <NW_Y>, <NE_X>, <NE_Y>, <SE_X>, <SE_Y>, <CELL_X>, <CELL_Y> = %g
   <SW_UNITS>, <NW_UNITS>, <NE_UNITS>, <SE_UNITS>, <CELL_UNITS> = radians | feet | meters | decimeters | arc-seconds
      if (<COORD_SYS> == LL) then <SW|NW|NE|SE|CELL_UNITS> == radians | arc-seconds
      if (<COORD_SYS> == UTM) then <SW|NW|NE|SE|CELL_UNITS> == feet | meters | decimeters
   <VERT_SCALE> = %g
   <VS_UNITS> = feet | meters | decimeters
   <MISSING_VAL> = %d
The libMini core only supports the BOX georeferencing type meaning that the contained data is enclosed exactly within the bounding box spanned by the four corner points. The built-in resampler also distinguishes between the two following types: DEM means that the contained data is a height field and that the corner coordinates define the exact position of the four corner vertices (corner-centric grid representation). TEX means that the contained data is a texture map and that the corner coordinates define the position of the midpoint of the four corner pixels (cell-centric grid representation). The missing value field is used by DEM formats to identify cells with unknown or unspecified elevation. A typical value is -9999. The no-data value for imagery assumed by the built-in resampler is absolute black (0,0,0). This is a safe assumption since real world imagery hardly contains true black. If this is not the case, the black pixels can be easily substituted with an RGB value of (0,0,1), which is visually equal to black.

Back to top

Appendix (B) PVM Volume Format Description

Similar to the PNM image format, the PVM volume format defines volumetric data in an easily readable fashion:

   <MAGIC>\n
   <WIDTH> <HEIGHT> <DEPTH>\n
   <COMPONENTS>\n
   ...DATA...
with

   <MAGIC>      = PVM ::: magic identifier
   <WIDTH>      = %d  ::: width of volume
   <HEIGHT>     = %d  ::: height of volume
   <COMPONENTS> = %d  ::: number of components
For 8 bit data the number of components is 1, for 16 bit data 2 and for RGB movies it is 3. The voxel spacing is assumed to be uniform.

A PVM example header for a 256x256x256 volume with 1 byte per voxel:

   PVM
   256 256 256
   1
   raw byte data...
If the magic identifier is PVM2 or PVM3, this indicates newer versions of the format, which include the voxel spacing in the header:

   <MAGIC>\n
   <WIDTH> <HEIGHT> <DEPTH>\n
   <VOXEL_WIDTH> <VOXEL_HEIGHT> <VOXEL_DEPTH>\n
   <COMPONENTS>\n
   ...DATA...
A PVM3 example header for a 256x256x256 volume with a non-uniform voxel spacing (the voxel spacing is 150% in z-direction) :

   PVM3
   256 256 256
   1 1 1.5
   1
   raw byte data...
PVM files often come in a DDS compressed data format. In that case the data startes with the prefix "DDS v3d". You can uncompress those files with the dds tool.

Back to top

Appendix (C) DB Data Format Description

While the built-in image format for tile sets is PNM, it is clear that libMini needs to support other file formats as well. This is achieved by registering a callback with libMini which handles loading the requested data in a proprietary format. The registered function copies the required information into a generic data buffer object which is returned to the Mini Library. In this way the data retrieval from the terrain data base can be handled completely in the application and is decoupled entirely from libMini. The data buffer is realized by the databuf class. It can contain 1D, 2D, 3D and 4D data. The class has methods to load and save its content in its native DB format but it is able to load from PNM files, too. The file extension of the native format is .db. Similar to PNM, the header is human readable consisting of the following fields:

   MAGIC=13761    ::: magic number
   xsize=%u       ::: mandatory width
   ysize=%u       ::: mandatory height for 2+D, 1 for 1D data
   zsize=%u       ::: mandatory depth for 3+D, 1 for 1D and 2D data
   tsteps=%u      ::: mandatory number of time steps for 4D, 1 for 1D, 2D and 3D data
   type=%u        ::: mandatory cell type:
                         0 = unsigned byte, 1 = signed short, 2 = float,
                         3 = RGB, 4 = RGBA,
                         5 = compressed RGB (S3TC DXT1), 6 = compressed RGBA (S3TC DXT1 with 1-bit alpha),
                         7 - mip-mapped RGB, 8 - mip-mapped RGBA,
                         9 - compressed mip-mapped RGB, 10 - compressed mip-mapped RGBA
   swx=%g         ::: x-component of south west corner (should be supplied for tile sets)
   swy=%g         ::: y-component of south west corner (should be supplied for tile sets)
   nwx=%g         ::: x-component of north west corner (should be supplied for tile sets)
   nwy=%g         ::: y-component of north west corner (should be supplied for tile sets)
   nex=%g         ::: x-component of north east corner (should be supplied for tile sets)
   ney=%g         ::: y-component of north east corner (should be supplied for tile sets)
   sex=%g         ::: x-component of south east corner (should be supplied for tile sets)
   sey=%g         ::: y-component of south east corner (should be supplied for tile sets)
   h0=%g          ::: base elevation of 3D or 4D data cube
   dh=%g          ::: height of the 3D or 4D cube
   t0=%g          ::: starting time of 4D series
   dt=%g          ::: time step of 4D series
   scaling=%g     ::: elevation scaling parameter for height fields (default is 1)
   bias=%g        ::: elevation bias parameter for height fields (default is 0)
   crs=%i         ::: coordinate reference system:
                         0 = Linear,
                         1 = Lat/Lon (LLH)
                         2 = UTM
                         3 = Mercator
                         4 = Oblique Gnomonic (OGH)
   zone=%i        ::: crs zone (for UTM and OGH)
   datum=%i       ::: crs datum:
                         0  = unspecified
                         1  = NAD27 (Mean North American Datum of 1927)
                         2  = WGS72 (World Geodetic System of 1972)
                         3  = WGS84 (World Geodetic System of 1984)
                         4  = NAD83 (Mean North American Datum of 1983)
                         5  = Sphere (with radius 6370997 meters)
                         6  = ED50 (Mean European Datum of 1950, centered at the Munich Frauenkirche)
                         7  = ED87 (Mean European Datum of 1987)
                         8  = OldHawaiian (mean datum for Hawaii/Maui/Oahu/Kauai)
                         9  = Luzon (Philippine Datum)
                         10 = Tokyo (Mean Japanese Datum)
                         11 = OSGB1936 (mean datum of the Ordnance Survey Great Britain 1936)
                         12 = Australian1984 (Mean Australian Geodetic Datum of 1984)
                         13 = NewZealand1949 (New Zealand Datum of 1949)
                         14 = SouthAmerican1969 (Mean South American Datum of 1969)
   nodata=%g      ::: no-data value
   extformat=%u   ::: external format indicator: a value!=0 triggers conversion hook (default 0, 1 reserved for JPEG, 2 for PNG, 3 for Z)
   implformat=%u  ::: implicit format indicator: a value!=0 triggers implict evaluation mode
   LLWGS84_swx=%g ::: optional Lat/Lon/WGS84 x-coordinate of south-west corner point
   LLWGS84_swy=%g ::: optional Lat/Lon/WGS84 y-coordinate of south-west corner point
   LLWGS84_nwx=%g ::: optional Lat/Lon/WGS84 x-coordinate of north-west corner point
   LLWGS84_nwy=%g ::: optional Lat/Lon/WGS84 y-coordinate of north-west corner point
   LLWGS84_nex=%g ::: optional Lat/Lon/WGS84 x-coordinate of north-eash corner point
   LLWGS84_ney=%g ::: optional Lat/Lon/WGS84 y-coordinate of north-east corner point
   LLWGS84_sex=%g ::: optional Lat/Lon/WGS84 x-coordinate of south-east corner point
   LLWGS84_sey=%g ::: optional Lat/Lon/WGS84 y-coordinate of south-east corner point
   bytes=%u       ::: mandatory byte length of the following data chunk
The data chunk is appended as raw data to the above description.

Data type 1 and 2 is stored in MSB format. After loading the data into main memory it is automatically converted into the native MSB or LSB format of the CPU. The description must end with a NUL character.

Previous versions of the DB format with magic identifier 13091 or less did not include the fields crs, zone, datum, nodata, and LLWGS84.

A value other than zero for extformat indicates that the data chunk is stored in an external format. When calling databuf::loaddata on such an object it automatically tries to trigger an external conversion hook to transform the input data into the corresponding raw format. The hook can be set via databuf::setconversion. As an example, extformat=1 in the header of a DB file means that the appended data chunk is encoded as a JPEG image. For more information on this issue, please have a look at the libMini Viewer (as described in Section (P)) which demonstrates how to use the conversion hook mechanism in order to decode JPEG images. If the extfmt parameter of databuf::savedata is set, the conversion hook is also triggered to convert the raw data into the external format.

A value other than zero for implformat indicates that the data chunk is stored in an implicit format, in which the data is interpreted as LUNA program. For example, the following db file produces a checkboard pattern in a procedural way:

   MAGIC=13091
   xsize=512
   ysize=512
   zsize=1
   tsteps=1
   type=3
   swx=0
   swy=0
   nwx=0
   nwy=0
   nex=0
   ney=0
   sex=0
   sey=0
   h0=0
   dh=0
   t0=0
   dt=0
   scaling=1
   bias=0
   extformat=0
   implformat=1
   bytes=0

   # implicit checker board function
   func checker(par x, par y)
      {
      return(((x*7.999)%2<1) ^ ((y*7.999)%2<1));
      }

   # procedural checker board evaluation
   main(par x, par y, par z, par t)
      {
      var c = checker(x, y);
      return(c, c, c);
      }
Back to top eof