libMini, a terrain rendering library

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

Version 8.6 as of 12.December.2007
Copyright (c) 1995-2007 by Stefan Roettger

Table of Contents

Terms of Usage

The terrain renderer is licensed under the terms of the LGPL 2.1. No warranty WHATSOEVER is expressed; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE!

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 version of libMini is available here:

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

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
README2.html: the libMini usage guide
LICENSE.txt: the LGPL 2.1 license
minibase.h: contains basic declarations
mini.cpp/P.h/.h: contains the core of the terrain renderer
miniOGL.cpp/P.h/.h: contains all OpenGL dependent stuff
miniv??.h: contains vector class definitions
minitime.cpp/.h: contains system time abstraction
miniio.cpp/.h: contains additional file io stuff
minihsv.cpp/.h: contains additional hsv conversion stuff
miniutm.cpp/.h: contains additional utm conversion 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
minilayer.cpp/.h: wrapper class rendering a single layer
miniterrain.cpp/.h: wrapper class rendering multiple layers
minicache.cpp/.h: cache for speeding up tiled terrain rendering
minishader.cpp/.h: shader support for the render cache
miniwarp.cpp/.h: warp kernel for global coordinate systems
miniray.cpp/.h: ray/triangle intersection code
ministrip.cpp/.h: vertex array container for triangle strips
minipoint.cpp/.h: geographical point of interest management
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
minibrick.cpp/.h: contains algorithms for C-LOD volume rendering
minilod.cpp/.h: contains algorithms for S-LOD volume rendering
pnmbase.cpp/.h: methods for handling PNM images
pnmsample.cpp/.h: methods for multi-resolution terrain resampling
database.cpp/.h: universal 1D/2D/3D/4D data buffer object
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
example.png: a screen shot of the example described below
example.cpp: the glut example (use "build.sh example" to compile)
stubtest.cpp: the stub example (use "build.sh stubtest" to compile)
viewer.cpp: the libMini Viewer (use "build.sh viewer" to compile)
viewerbase.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
squishbase.cpp/.h: squish support for the libMini Viewer
greycbase.cpp/.h: greycstoration support for the libMini Viewer (optional)
convbase.cpp/.h: image conversion support for the libMini Viewer
luna*.cpp/.h: interpreter for LUNA, an RPN-style language for LUNAtics and nerds alike
SkyDome.ppm: a sample sky dome texture (from Philo's Sky Collection)
Cone.db: a sample DB volume (implicitly defined cone)
freeglut.dll/.lib: Windows GLUT libraries for building the example and the libMini Viewer
*.sln/.vcproj: Windows Visual C++ project files
Makefile: Makefile for Irix, Linux and MacOS X
build.sh: the Unix build script

Additionally, the Yukon Ground Fog Demo, the Stuttgart Demo, the Hawaii Demo and the Fraenkische Schweiz Demo can be downloaded (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 so called libMini Viewer (see Section (O)). This is a generic viewing utility for any kind of terrain data. For example, it can be used to display the data of the demos that are mentioned above or to stream data over the internet.

On Windows a pthreads compatible library is required for the libMini viewer (also recommended for the Hawaii Demo and the Fraenkische Demo) since a POSIX threads compatible implementation like pthreads-win32 considerably improves paging performance.

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:

The build.sh shell script compiles the library on Irix, Linux and MacOS X. Simply type "./build.sh" in your Unix shell. To install the library and the necessary include files in /usr/local on your Unix machine type "./build.sh install" as superuser.

The library also compiles on Windows using the supplied VC++ 7.1 project files or by using cygwin in the following way:

Other than the specified operating systems are not supported.

Back to top

(C) TERRAIN RENDERING API:

In this section the low level API of the Mini Library is described. Convenient high level methods for the most commonly encountered terrain rendering scenarios (as implemented in the minitile/miniload, ministub, minicache and datacloud classes) are described in Section (F), (G), (I) and (I). Please first check out whether or not the high level APIs suffice your needs before you bother reading about the low level API. A good starting point would be also to have a look at the reference implementation of the libMini tile set viewer which is described in Section (O).

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. The example code as denoted below in a series of eight steps is included in the distribution and can be compiled by typing "build.sh example" in a shell.

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

Normally, the fine tuning parameters are initialized to suitable values. However, for more flexibility the parameters can be set by using mini::setparams(minres,maxd2,ginfo,maxcull). In the paper minres is referenced to as C, maxd2 is the maximum value for the linear mapping of the d2-values, ginfo is the relative importance of gradients in comparison to the d2-values, and maxcull defines the number of base quadtree levels for which 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: fovy<0 triggers the orthographic projection mode
use glOrtho(fovy/2*aspect,-fovy/2*aspect,fovy/2,-fovy/2,near,far);

Step 6.5) Swap buffers:

Now we are basically done. 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 by means of the following functions:

   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 with the following function:

   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 in order to tune the triangulation (the focus can be set directly in the drawlandscape call of the libMini core or with ministub::setfocus or minitile::setfocus).

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 and the grid is resampled accordingly. 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 this 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 a array of file names is passed which defines 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 texture without going into the details of the low level API described in Section (C). For an example use 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 the 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 more or less latency whenever the LODs need to be updated from disk. To hide the latency the so-called preloading can be enabled so that the requested LODs are already available before they are actually needed for rendering.

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 (6A)).

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 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 (6C)). 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 (5)). 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. Thus, the textures may not fit completely into texture memory. In such a case, the prange parameter should be decreased until the rendering performance is sufficient again. We have to remember that 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. 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 <OpenGL headers>

   #include "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 "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 also 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. 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 update of the scene 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 (O)).

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 char *vtxprog="!!ARBvp1.0 \n\
      PARAM t=program.env[0]; \n\
      PARAM e=program.env[1]; \n\
      PARAM c0=program.env[2]; \n\
      PARAM c1=program.env[3]; \n\
      PARAM c2=program.env[4]; \n\
      PARAM c3=program.env[5]; \n\
      PARAM c4=program.env[6]; \n\
      PARAM c5=program.env[7]; \n\
      PARAM c6=program.env[8]; \n\
      PARAM c7=program.env[9]; \n\
      PARAM mat[4]={state.matrix.mvp}; \n\
      PARAM invtra[4]={state.matrix.modelview.invtrans}; \n\
      TEMP vtx,col,nrm,pos,vec; \n\
      ### fetch actual vertex \n\
      MOV vtx,vertex.position; \n\
      MOV col,vertex.color; \n\
      MOV nrm,vertex.normal; \n\
      ### transform vertex with 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 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 parameter vectors t and e are supplied automatically by the minicache, but the parameter vectors c0-c7 may hold four additional constants that can be supplied by the user via minicache::setvtxshaderparams(x,y,z,w[,n]).

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 multiplied with the current fragment color to mimic the standard modulating texture environment.

   // default pixel shader
   static char *fragprog="!!ARBfp1.0 \n\
      PARAM c0=program.env[0]; \n\
      PARAM c1=program.env[1]; \n\
      PARAM c2=program.env[2]; \n\
      PARAM c3=program.env[3]; \n\
      PARAM c4=program.env[4]; \n\
      PARAM c5=program.env[5]; \n\
      PARAM c6=program.env[6]; \n\
      PARAM c7=program.env[7]; \n\
      PARAM t=program.env[8]; \n\
      TEMP col; \n\
      ### fetch texture color \n\
      TEX col,fragment.texcoord[0],texture[0],2D; \n\
      ### modulate with fragment color \n\
      MUL result.color,col,fragment.color; \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 char *fragprog="!!ARBfp1.0\
      PARAM c0=program.env[0];\
      TEMP col,vtx;\
      TEX col,fragment.texcoord[0],texture[0],2D;\
      MUL vtx.x,fragment.texcoord[0].z,c0.x;\
      FRC vtx.y,vtx.x;\
      MAD vtx.y,vtx.y,c0.z,-c0.w;\
      ABS vtx.y,vtx.y;\
      SUB vtx.y,c0.w,vtx.y;\
      MUL_SAT vtx.y,vtx.y,c0.y;\
      CMP vtx.y,vtx.x,vtx.y,c0.w;\
      MUL col,col,vtx.y;\
      MUL result.color,col,fragment.color;\
      END";

   // enable pixel shader plugin
   cache.setpixshader(fragprog);
   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 and denoised textures for best image quality:

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

   #include "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(unsigned char *mapfile,databuf *map,int istexture,int background,void *data)
      {map->loaddata((char *)mapfile);}

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

   void inquiry_callback(int col,int row,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,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 could be used as well:

   #include <pthread.h>

   #include "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 changes. 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=cloud->getpending();
   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 process. 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 (6C)).

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(char *file,int istexture,databuf *buf,void *data)
      {buf->loaddata(file);}

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

   int check_callback(char *src_url,char *src_id,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 (O)) 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,
                     databuf *obj,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*obj->xsize*obj->ysize);
         if (rgbadata==NULL) ERRORMSG();

         for (i=0; i<obj->xsize*obj->ysize; 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(obj->xsize,obj->ysize,mode);
      *s3tcdata=(unsigned char *)malloc(*s3tcbytes);
      if (*s3tcdata==NULL) ERRORMSG();

      squish::CompressImage(rawdata,obj->xsize,obj->ysize,*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 process via datacloud::configure_autocompress(1). This is the default setting.

Please note that the auto-compression hook is triggered from the background process. Therefore it cannot use any OpenGL functionality and must use a library like squish because the background process has no OpenGL context.

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:

The libMini Viewer is a tool for viewing tile sets. 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 vtb (virtual terrain builder of vterrain.org).

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), libcurl, libjpeg, libpng and squish to be installed. On Windows it is recommended to use a pthreads compatible implementation like pthreads-win32.

See the examples below how to use the viewer 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/ tiles landsat

   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 has been resampled with vtb, the two ini files for the elevation and the imagery should be 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 used this 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 euqivalent to the multi-argument usage of "viewer url/ path/ elev imag", so that the corresponding directory layout for the short use case must be as follows:
   url/
       path/
            elev/
                 tile.0-0.db
                 ...
            elev.ini
            imag/
                 tile.0-0.db
                 ...
            imag.ini
The initial viewing settings are very conservative, so that the first view probably will not look adequate. To get a better visualization, you can adjust the following 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). The HUD also displays information about the available keyboard controls.

Optionally, you can view waypoints that were gathered with a GPS unit, for example. The waypoints must be contained in a definition file named "Waypoints.txt" in the base directory of the tile set. Each 5 consecutive lines separated by a single empty line define one waypoint. Here is an example:

   Pali Lookout, Oahu
   UTM 04
   0625143
   2363262
   379m

   Hubertusklause, Deckersberg
   LL
   49.470846
   11.440757
   542m
The coordinate system of the first waypoint in the definition file must match the coordinate system of the tile set. Then the waypoints will be displayed as signposts at the corresponding position in the tile set. The first waypoint also determines the initial point of view when starting the libMini Viewer.

You can also 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.

Back to top

(P) 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 renderer).

Back to top

(Q) 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

(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 (O)).

Back to top

(2) MINIPOINT

This class organizes a collection of way points. The points could have been collected with a GPS receiver, for example, or just exported from a GIS software. Each way point has the following attributes:

For an example how to load and display the way points please check out the libMini Viewer (see Section (O)). Press 'p' in the viewer to toggle the way points on or off. The Hawaii Demo data contains a small collection of geocache locations on Oahu, so you need to visit this island with the libMini Viewer to see the way points.

Back to top

(3) 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 (O)) to render the text of the way points and the HUD.

Back to top

(4) 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

(5) 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 heavy floating point math 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

(5A) 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), grey-scale images have the extension .pgm (Portable Grey-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 grey-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  = ED79 (Mean European Datum of 1979)
      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 = Geodetic1949 (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 understands 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 usually 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 may indeed contain some really dark colors but hardly ever real black. In case it does though, the black pixels can be substituted easily with (0,0,1). If you think this still makes a visual difference then please remember not to fire cannons at chickadees.

Back to top

(5B) 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. Back to top

(5C) 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=13048  ::: 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)
   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)
   extformat=%u ::: external format indicator: a value!=0 triggers conversion hook (default 0, 1 reserved for JPEG, 2 for PNG)
   bytes=%u     ::: mandatory byte length of the following data chunk
The data chunk is appended to the above description. The description must end with a NUL character. 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.

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 (O)) 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.

Back to top eof