|
Heightfield Clipboard Format
Last updated: August 5, 2006
Introduction
The following is the current specification for the public clipboard format used by Leveller to exchange elevation data. Such formats allow applications to easily exchange data via standard Cut/Copy/Paste operations, and help developers avoid having to write or otherwise implement file translators. Documentation on experimental/upcoming features appears in a gray typeface, like this paragraph. Leveller 2.2 adds public clipboard support using (under Windows) the CF_TEXT format. However, this is a very simple tab-delimited text representation of elevation data. Not only is the interpretation of the data subjective, but since each elevation is expressed using ASCII digits in floating-point form, siginficant memory is used. A binary format, however, requires careful design in order to be portable and scalable (Leveller's own private clipboard format is unsuitable since it is closely tied to the way Leveller represents heightfield data). Related data types such as bumpmaps are not covered by this proposal (although elevation data can be used to convey a bumpmap by having the receiver use the elevations to compute surface normals).
The tag structure is encapsulated by the provided read/write code, for those who don't need or want to worry about the details. Once you have a pointer to the clipboard data and its length, you can just provide them to the code and it will take it from there. The data block on the clipboard is an unpadded array of variable-sized tag blocks. Every tag starts with the following structure. The tag's value data is stored immediately after the structure.
{
char szName[16]; // Tag's name, including null terminator.
unsigned int32 valueSize; // Size of value, in bytes.
unsigned int32 reserved; // In case we need blobs over 4GB.
// For now, must be zero.
unsigned int32 tagSize; // Size of tag, in bytes.
unsigned int32 reserved; // In case we need tags over 4GB.
// For now, must be zero.
__int16 relationFlags; // Relationship of next tag
// to this one.
__int16 valueKind; // Type of data.
}
relationFlags is a combination of the bitflags
TAGRELATION_CHILD and TAGRELATION_SIBLING. If both are
set, it means the next tag is a child, but after all
the children, the next tag is a sibling.
If valueKind is a scalar, and valueSize is greater than
sizeof(valueKind), then the value is a scalar array
of valueSize / sizeof(valueKind) elements.
tagSize is not currently used by the clipboard format,
but is available to improve tag skipping performance for
file-based implementations. After writing all the tags,
one iterates through them and computes their aggregate sizes
(i.e., the size of a tag and all of its children) and
updates the tagSize members.
Types are: c (char), i16, i32, i64, f (float), d (double), b (binary).
A "u" prefix means type is unsigned.
A "[]" suffix denotes an array.
When type is not specified, tag is a parent or empty.
Tag names are limited to fifteen singlebyte characters,
ASCII codepoints 32 through 127. Case-sensitive.
Endianness of all tag values is the same as the tag structure itself,
i.e., matching the host platform.
Tag Type Description
------------------- ----- ----------------------------------------------------
hf01 Format and endianness identifier, when equal to host.
OR
hf10 Format and endianness identifier, when unequal to host.
[header]
[version] ui32 Version number; useful for apps that want to
save time by not looking for secondary tags
that don't exist in certain versions.
body
heixels
extents
width ui32 Number of columns in heightfield.
breadth ui32 Number of rows in heightfield.
[lowest] b Lowest elevation, in heixelformat.
[highest] b Highest elevation, in heixelformat
(Note: tags below marked with '*' should be omitted since
they are superceded by body/coordsys).
[scale]* Relative scaling along 3D axes.
[x]* d West-east direction.
[y]* d Up-down.
[z]* d North-south.
[size]* Absolute size of extents, in meters.
width* d
breadth* d
span* d
format
depth ui32 Bits per heixel.
[fp] i32 TRUE if heixels are floating-point (IEEE 754).
[unsigned] i32 FALSE if topmost bit is sign bit.
[fracsize] ui32 If not floating-point, number of bits in fractional
part of possible fixed-point representation.
[readoffsets]
[first] ui32 Bit offset from data start to first heixel.
[column] ui32 Bit offset between adjacent heixels.
[row] ui32 Bit offset between adjacent scanlines.
[specialelevs]
[void] b Value of "void" heixel, in heixel format.
If not present, no heixels are void.
data b Elevation data, taking
depth * width * breadth / 8,
readoffsets/column * width * breadth / 8, or
readoffsets/row * breadth / 8 bytes (rounded up).
[alpha] Alpha channel. If ommitted, all heixels are pasteable.
100% transparency = maximum alpha value.
[extents]
[width] ui32 Number of columns in alpha mask.
[breadth] ui32 Number of rows in alpha mask.
[offsets] Where mask overlays heightfield. Assume (0, 0).
[column] ui32 Which heightfield column first mask column maps to.
[row] ui32 Which heightfield row first mask row maps to.
[format]
[depth] ui32 Bits per pixel (8 is assumed).
[fp] i32 TRUE if pixels are floating-point (IEEE 754).
[readoffsets]
[first] ui32 Bit offset from data start to first pixel.
[column] ui32 Bit offset between adjacent pixels.
[row] ui32 Bit offset between adjacent scanlines.
data b Alpha mask pixel data.
[coordsys] Coordinate system.
[geometry] ui32 Base shape (0=flat, 1=planet Earth).
[projection] Shape/projection mapping.
[format] ui32 Projection format (0=WKT if geometry=1).
data b Projection string.
[pixelmapping] Raster-to-projection mapping.
units ui32 Daylon UOM code (refer to daylon_mcodes.h)
[transform] Affine transform matrix.
[origin]
[x] d
[z] d
[scale]
[x] d
[z] d
[altitude] Elevation scaling.
units ui32 Daylon UOM code (refer to daylon_mcodes.h)
[scale] d Raw-to-units scale.
[offset] d Post-scaling offset.
[privateuse] Private use scope. Tags here should be scoped
by organization's name.
[trailer] Reserved tagname.
[checksum] Reserved tagname.
Tag Details The following is an explanation of each tag, in the order listed above.
Tag: "hf01" This is the only tag whose physical placement matters; since its name forms the format signature, it must be at the start of the memory block (or file) holding the tagset. It is also the only tag that can be guaranteed to be readable without a normal tag search, since its position (zero) is known in advance. The first two bytes of the tag's name must be "hf". The next two are "01" written by assigning the 16-bit value 0x3031 into the tagname, e.g.:
*((__int16*)&szTagname[2]) = 0x3031;
On Intel (little-endian) platforms, this will create a tagname of "hf10". If, when reading the 16-bit value back by assigning the tagname's third and fourth bytes to an __int16, and you get 0x3130 instead, then the rest of the data stream needs to be byteswapped.
This optional field is provided for tag writers who want to provide a version hint to other readers. If one or more tags were added under a specific version, then testing for that version is usually easier (and always faster) than searching for those tags and discovering they are not present. The value, if set, is currently zero. The maximum version number possible is incremented by one whenever a new set of tags is defined in the format. Tag definitions are currently managed by Daylon Graphics.
Advanced sizing information such as geospatial coordinate systems and projections, because it is not heixel-centric, is stored elsewhere.
The number of heixels in each heightfield scanline. Since each heixel maps to a mesh vertex when rendering, the number of mesh patches between vertices is width less one. For the same reason, although a heightfield having a width of one heixel is meaningful informationally, it is not visible when rendered (except perhaps as a line).
In the absence of size information, Leveller assumes that heixels and their elevations occupy unit voxel space, where equal values imply the same distance along all three spatial dimensions. A 100 x 100 heightfield with a span of 99, for example, occupies a perfect cube. However, since elevation values are not always in the same space as heixel counts, providing other measurement info is necessary to avoid slope distortion, or vertical under- or overexaggeration. The "scale" tag provides a 3D scaling transform. If the children are <1.0, 1.0, 1.0>, then the size implied by the width/breadth heixel counts with unit cube voxels is used. For example, a 200 x 100 heightfield would be twice as wide as its breadth. If a scale tag of <0.5, 1.0, 1.0> or <1.0, 1.0, 2.0> were provided, however, the heightfield would be square. Omitted child tags are assumed to be 1.0. When pasting, Leveller resamples scaled heightfields upwards to avoid data loss. For either of the above child tag values, Leveller would scale the pasted heightfield to 200 x 200. The alpha channel, if present, is scaled also.
This optional tag specifies a real-world size for the heightfield. All three extents must be specified. Although you can also provide a "scale" tag, it will be overridden by "size" by applications that use it. If you only know how far apart two adjacent heixels are, then the extent would be that distance multiplied by all the heixels along that axis less one. Or, in forumla notation:
extent = distance × (heixels - 1)
To a program only interested in heixels, real-world size has no meaning. Since world units in Leveller 2.2 can have measurement labels attached, however, Leveller will use the size info to make pasted heixels fit the real-world size of the receiving heightfield. This may require resampling of the heixels. A situation where resampling would not be needed, for example, is if the Leveller document's world unit label is "metres", and the pasted heightfield happens to have a heixel distance that matches the document's world scale. To know in advance if resampling would occur, you need to determine heixel distance, which is the inverse of the previous formula:
distance = extent ÷ (heixels - 1)
The upward resampling Leveller uses to avoid data loss with scaled heightfields is overridden if a "size" tag is used, because the whole idea of a real-world size is to force only one interpretation of the heightfield's extents. An extreme example would be if a pasted heightfield measured 100 meters, but the Leveller document had a world spacing of 100 meters: the pasted heightfield would be resampled into a single heixel. Since Leveller works with heixels, a slight loss of precision will occur if the scaled heightfield does not fit evenly on a heixel boundary. The alpha channel, if present, is scaled also.
This value specifies how much precision each heixel encodes, including the sign bit, if the unsigned tag is false. Leveller currently handles bit depths of 8, 16, 32, and 64. 64-bit heixels must be floating-point since they are interpreted as double-precision numbers. Bit depths of 16, 32, and 64 are treated as endian-sensitive.
If the value is one, heixels of 32/64 bits are treated as floating-point numbers, and the unsigned tag is ignored. Otherwise, they are interpreted as integers.
If the value is one, integer heixels are composed of value bits only. Otherwise, the most significant bit is the sign bit.
If integer heixels are fixed-point, this tag indicates how many of the least significant bits represent the fractional portion. The remaining bits represent the whole number portion.
Tag: "body/coordsys" Type: parent Purpose: Coordinate system data. This tag contains several subtags that define how the heightfield maps to a geometric shape of possibly realworld size. It can be used to supercede the information in the body/heixels/extents/scale and body/heixels/extents/size tags.
This tag defines onto which base shape the heightfield is mapped onto (e.g., flat plane, sphere, cone, planetary body, etc.). For most shapes, projections are simple since the parameters are few (such as sphere radius) and are handled as straightforward UV mappings. For code 1 (planet Earth), projections require more definition.
This tag contains the parameters on how coordinates on the base shape are represented in projection space. This can be omitted for geometry code zero (flat plane). For geometry code 1 (planet Earth), the "format" subtag indicates what format the "data" subtag uses (e.g., 0 for Well Known Text in 8-bit ASCII).
For geometry code 1 (planet Earth), projection space is often latitude/longitude or a UTM zone. The subtags are basically an affine transform matrix that convert pixel coordinates (whose origin is at the northwest corner) to projection coordinates. The units subtag indicates the realworld measure for ground coordinates (which can differ from elevation units).
Daylon Graphics programs use an OEM-defined list of measurement units defined in the file daylon_mcodes.h. Earlier versions of the clipboard spec used EPSG codes, but these do not cover all the units used by Leveller.
Elevation data is provided in whatever range the application prefers. The "units" subtag indicates in which units the elevations are ultimately expressed, after being scaled and offset. For example, one may prefer to provide elevations in raw heightfield grid units such that an elevation delta of 1.0 is as tall as a pixel is wide. Such values, however, have little utility for geographic datasets and require a mapping to a realworld measure. The "scale" subtag lets one scale the raw elevations to the desired unit space (e.g., meters) and the "offset" subtag provides a realworld base elevation to be added after the scaling. One would also set the "units" subtag to 9001, which is the EPSG code for meters. If providing elevations directly in the specified space, then the scale and offset can be ommitted or set to 1.0 and 0.0, respectively.
Same encoding as pixelmapping units; refer to daylon_mcodes.h for specifics. Sometimes it's easier to understand a spec by reading code, so here's a sample parser in C-ish pseudocode:
width = clip.val("/body/heixels/extents/width");
breadth = clip.val("/body/heixels/extents/width");
vscale = 1.0 OR clip.val("/body/heixels/extents/scale/y");
depth = clip.val("body/heixels/format/depth");
datastart = clip.val("body/heixels/format/readoffsets/first");
isFP = FALSE OR clip.val("body/heixels/format/fp");
isSigned= FALSE OR not clip.val("body/heixels/format/unsigned");
fracsize= 0 OR clip.val("/body/heixels/format/fracsize");
if(isFP and depth != 32 and depth != 64)
throw unknownFormatException;
coloff = depth OR clip.val("/body/heixels/readoffsets/column");
rowoff = depth * width OR clip.val("/body/heixels/readoffsets/column");
data = clip.val("/body/heixels/data");
data.forwardbits(datastart);
for(z = 0; z < breadth; z++)
{
bitsPerScanline = coloff * width;
for(x = 0; x < width; x++)
{
heixel = data.readbits(depth);
data.forwardbits(coloff - depth);
double dElev;
if(bigendian and host isn't)
byteswap(heixel, (depth + 7) / 8);
if(isFP)
{
if(depth == 32)
dElev = (double)(float)heixel;
else if(depth == 64)
dElev = (double)heixel;
}
else
{
isNeg = FALSE;
if(isSigned)
{
isNeg = heixel.bit(depth-1);
heixel.bit(depth-1) = 0;
}
dElev = (double)heixel / (2 ** fracsize);
if(isNeg)
dElev *= -1;
}
dElev *= vscale;
// Store dElev in our heightfield.
} // next heixel on scanline
data.forwardbits(rowoff - bitsPerScanline);
} // next scanline
The above is a generalized parser that will handle
all heixelformat possibilities. An application
that prefers supporting specific formats, however,
needs to check for them. For example, if Leveller 2.4
insisted on only pasting its own internal heightfield format,
it would do this:
width = clip.val("/body/heixels/extents/width");
breadth = clip.val("/body/heixels/extents/width");
vscale = 1.0 OR clip.val("/body/heixels/extents/scale/y");
depth = clip.val("body/heixels/format/depth");
datastart = clip.val("body/heixels/format/readoffsets/first");
isFP = FALSE OR clip.val("body/heixels/format/fp");
isSigned= FALSE OR not clip.val("body/heixels/format/unsigned");
fracsize= 0 OR clip.val("/body/heixels/format/fracsize");
coloff = depth OR clip.val("/body/heixels/readoffsets/column");
rowoff = depth * width OR clip.val("/body/heixels/readoffsets/column");
data = clip.val("/body/heixels/data");
// Check for 16.16 fixed point heixelformat.
if(isFP or
depth != 32 or
datastart != 0 or
vscale != 1.0 or
!isSigned or
fracsize != 16 or
coloff != depth or
rowoff != depth * width)
throw unknownFormatException;
LEV_HEIGHTVAL* pData = (LEV_HEIGHTVAL*)data.GetAddress();
for(z = 0; z < breadth; z++)
{
for(x = 0; x < width; x++)
{
LEV_HEIGHTVAL heixel = pData[z * width + x];
if(bigendian and host isn't)
byteswap(heixel, sizeof(LEV_HEIGHTVAL));
// Store heixel in our heightfield.
} // next heixel on scanline
} // next scanline
Tags in [] indicate optional items, to make writing the most common output easy. If omitted, appropriate defaults are assumed. extents/lowest and extents/highest can be computed by scanning the elevations. The default heixelformat (aside from bpp) is signed little-endian integers with no fixed-point interpretation. If readoffsets is missing, the pixels are assumed to be bytealigned with no scanline padding (bpp is rounded up to the nearest multiple of eight). The tag names in the pixelformat block were chosen so that apps exporting data could initialize internal structures by zero-filling them. This way, the most common options for the fp, unsigned, bigendian, and fracsize members tend to be immediately set (i.e., pixels are integers, signed, little endian, and not fixed-point). pixelformat/mask is used for apps that want to just copy their internal pixel buffers, which may include non-elevation data in each pixel such as transparency, surface normal references, material flags, etc. Simply logical-AND'ing the pixel bits with the mask and shifting down obtains the elevation. The default mask is the same bit length as the pixel and is all one's. If readoffsets/column equals depth, it means pixels are tightly packed on each scanline. If readoffsets/row equals depth * width, it means scanlines are unpadded even on a bit basis. This supports applications using fractional byte pixel depths (e.g., 4, 11, etc. bpp) seeking optimal (but uncompressed) storage. The scale and size tags provide optional data on what shape and size the heightfield has. The scale data is primarily of interest to apps that need to know how the span relates to the width/breadth (e.g., when making surface normals for bumpmaps). If scale.y is 1.0, for example, it means that one unit of elevation equals the distance between adjacent pixels. The size tag is for apps that need to know the heightfield's "real" size. A 1 x 1 heightfield has no extent. Pixelformats using bit depths greater than eight but which are multiples of eight (e.g., 16/24/32) may present endian issues. For irregular bit depths, the usual solution is to enforce big-endian byte order since the streaming is on a bit basis, i.e., regardless of the platform, the bytes storing a bitstream are encoded identically. But for regular depths, the assumption is that 16-bit and 32-bit values are being assigned using 16-bit/32-bit datatypes, which are endian-dependant. Hence, using a bit parser as a general solution is not possible unless we perform byteswapping for regular depths. For those depths, a "byteorder" tag should be added, and if nonzero, it means the bytes in each pixel are in the reverse order of the platform's endianness. Although the reference code does not optimize tag reading (it always starts from the top of the tree and iterates through every tag), tagSize members were added to the tag struct for future enhancement. If one computes a tag's full size and sets its tagSize field with it, then skipping unmatched tags can be done in a single operation. For now, it's not a problem, since the clipboard is in memory, and using the tag system for file storage is likely not around the corner. Moved alpha/offsets into alpha/extents, since offsets are never used if extents are unspecified. i.e., the mask is implicitly the same size as the heightfield, and therefore must be at (0,0). Dropped the endianness tags from the pixelformats. All tag values simply share the endianness of the clipboard. There is a global endianness indicator in the third and fourth bytes of the first tag's name. Using the first tag as a signature works because tags have their names as their first member. Dropped fracsize from alpha/pixelformat, since it's unnecessary. Whatever the bit pattern in an alpha pixel is, it's simply normalized to 0...1 using the max value given by its bit depth. The requirement that all non-binary tags have their values expressed as text was removed. Instead, everything is stored in binary form. This made the requirement for a lightweight parser much easier. Added the "alpha" tag since Leveller cannot copy/paste its heightfields without knowing what the selection is shaped like. Otherwise, all pastings would result in rectangular floating selections. Removed the "mask" tag since the depth and column offset take care of it. Added the "readoffsets/first" tag since we still need to know how many bits into the data block the first pixel starts at. Renamed "pixel" to "heixel" in the tag names. The advantage is less confusion with future tags that refer to true pixels, such as picture textures, alpha masks, etc. The "bpp" tag was renamed "depth" since bpp means bits per pixel, and "depth" sounded better than "bph". Changed readoffsets/row to use bits instead of bytes, since it simplifies reading past scanline padding. Applications such as 3D modelers and renderers tend to apply local 4 x 4 transform matrices to scene objects including heightfields. This is a simple type of linear CS mapping and supports basic 3D space transforms such as translation, rotation, shearing, and proportional/non-proportional scaling. Projections are unused because heightfields are not interpreted as being part of a planetary surface. GIS applications and geo-specific modelers/renderers (such as WCS) support nonlinear CS and projections. The heightfield can be curved, and user coordinates depend not only on the CS (which may follow a UV mapping such as latitude/longitude), but also on projections (Mercator, Polar, etc.) and even datums (the particular ellipsoid used to model a planet's almost-spherical shape). The experimental "body/coordsys" tag will contain data to ease supporting GIS applications. The "body/heixels/extents/scale" and "body/heixels/extents/size" tags have been dropped for Leveller 2.6 and replaced with the more comprehensive "body/coordsys" tag. A small SDK has been developed containing
- A reference C++ class that reads/writes the
format's tag structure. Preliminary documentation also.
- C sample pasting code derived from Leveller's
own paste event handler, demonstrating how to
build a format tag structure for elevations and
optimized selection masks.
- A Win32 test app that copies Leveller TER files
to the clipboard with options for size, scale,
and heixelformat tags.
Daylon has implemented the
minimum tagset above in Leveller 2.2 and has
provided working code to participants for
testing and evaluation purposes. Leveller 2.2
can currently copy the format and paste formats
using byte-multiple bit depths.
Daylon has implemented georeferencing support via the "body/coordsys" tag for Leveller 2.6. Testing is available via the Leveller 2.6 alpha.
Frank Barchard, Electronic Arts Canada |