Flexible Precision Images
December 3, 2000
This document describes the FPBM (Flexible Precision Buffer Map) file format for images and animations introduced with LightWave 6.0.
- Introduction
- IFF Chunks
- Data Basics
- File Structure
- File Header
- Frame Header
- Layer Header
- Layer Data
- Run-Length Compression
- Delta Compression
- Example Source Code
Introduction
An image is a rectangular array of values. The image data generated by LightWave includes not only the red, green and blue levels of each pixel in the rendered image, but also values at each pixel for the alpha level, z-depth, shading, reflectivity, surface normal, 2D motion, and other buffers used during rendering. Most of these quantities are represented internally as floating-point numbers, and all may change over time. Existing image and animation file formats are inadequate for storing all of this information, which is the motivation for the new FPBM format.
The data channels in an FPBM are called layers, and each layer can store values as 8-bit or 16-bit integers or as 32-bit floating-point numbers. A set of these layers for a given animation time is called a frame. An FPBM containing a single frame is a still image, and one containing a time sequence of frames is an animation.
(The descriptions here of the animation features of FPBM should be considered preliminary. They haven't been implemented in LightWave yet.)
Chunks
The FPBM format is based on the metaformat for binary files described in EA IFF 85 Standard for Interchange Format Files.
(See also ILBM, an earlier IFF image format.) The basic structural element in
an IFF file is the chunk. A chunk consists of a four-byte ID tag, a four-byte
chunk size, and size bytes of data. If the size is odd, the chunk is followed by
a 0 pad byte, so that the next chunk begins on an even byte boundary. (The pad byte isn't
counted in the size.)
A chunk ID is a sequence of 4 bytes containing 7-bit ASCII values, usually upper-case printable characters, used to identify the chunk's data. ID tags can be interpreted as unsigned integers for comparison purposes. They're typically constructed using macros like the following.
#define CKID_(a,b,c,d) (((a)<<24)|((b)<<16)|((c)<<8)|(d)) #define ID_FORM CKID_('F','O','R','M') #define ID_FPBM CKID_('F','P','B','M') ...
FPBM files start with the four bytes
followed by a four-byte
integer giving the length of the file (minus 8) and the four byte ID FORM
The remainder of the file is a collection of chunks containing layer data. FPBM
.
To be read, IFF files must be parsed. FPBM files are pretty uniform, but in general the order in which chunks can occur in an IFF file isn't fixed. You may encounter chunks or layer types that aren't defined here, which you should be prepared to skip gracefully if you don't understand them. You can do this by using the chunk size to seek to the next chunk. And you may encounter chunk sizes that differ from those implied here. Readers must respect the chunk size. Missing data should be given default values, and extra data, which the reader presumably doesn't understand, should be skipped.
Data Basics
The data in an FPBM will be described in this document using C language conventions. Chunks will be represented as structures, and the values within each structure will be defined as the C basic types short or float. As used here, a short is a signed, two's complement, 16-bit integer, and a float is a 32-bit IEEE floating-point number. All data in an FPBM is written in big-endian (Motorola, Internet) byte order. Programs running in environments (primarily Microsoft Windows) that use a different byte order must swap bytes after reading and before writing.
File Structure
Structurally, FPBMs are quite simple.
FORM formsize FPBM FPHD 28 FPHeader for each frame FLEX 2 numLayers for each layer LYHD 20 LayerHeader LAYR datasize data
The header is followed by one or more frames. Each frame begins with a layer count, and this is followed by the layers. Each layer begins with a header describing the data it contains.
The following sections describe the FPHD
, FLEX
and LYHD
chunks.
FPHD - Flexible Precision Header
The FPHeader contains information that applies globally to all of the frames in the
file. It appears first in the file, after the FORM
prefix and before the first
frame.
typedef struct st_FPHeader { short width; short height; short numLayers; short numFrames; short numBuffers; short flags; short srcBytesPerLayerPixel; short pad2; float pixelAspect; float pixelWidth; float framesPerSecond; } FPHeader;
width, height
- Pixel dimensions of the image.
numLayers
- The maximum number of layers per frame. Some layers may not be stored for all frames, since their contents may not differ from frame to frame.
numFrames
- Number of frames. For still images, this will be 1.
numBuffers
- Number of animation buffers. This affects the interpretation of delta encoded layer
data. When the file is written for single buffered playback, the number of buffers is 1
and the deltas are relative to the previous frame. For double buffered playback, the
deltas are relative to the frame
two frames back,
since the new frame is drawn over the contents of the back buffer. This field is ignored for still images. flags
- One or more of the following flag values, combined using bitwise-or.
Source_Int (0 << 0)
Source_FP (1 << 0)- The
natural
representation of the data, or the way the data was stored before being written to the file, either integer (the default) or floating-point. This can differ from the way the data is actually stored in the file (specified for each layer in the LayerHeaderflags
field). Readers may wish to restore the data to its original representation using this information. It can also be used to indicate the precision of the source data. InterlaceFlag (1 << 1)
- Scanlines should be interlaced (field rendered) for playback. (The actual interlace state is stored for each layer in the LayerHeader.)
srcBytesPerLayerPixel
- Use this in combination with the
Source_Int
andSource_FP
flags to determine thenatural
data type for the data in the file, or the type in which the data was stored before it was written to the file. The most common values for this field are 1, 2 and 4. The actual size of a pixel in each layer is in the LayerHeader'sbytesPerLayerPixel
field. pad2
- Reserved for future use.
pixelAspect
- Pixel aspect ratio expressed as width divided by height.
pixelWidth
- Pixel width in millimeters. This fixes the size of the image for print. To calculate
horizontal and vertical DPI (dots per inch) from this value,
hdpi = 25.4 / pixelWidth vdpi = 25.4 / (pixelWidth * pixelAspect)
framesPerSecond
- Number of frames per second for animations. Writers may set this to 0.0 for still images.
FLEX - Frame Header
The FrameHeader appears at the start of each frame. This chunk may grow in the future to include other information.
typedef struct st_FrameHeader { short numLayers; } FrameHeader;
numLayers
- Number of layers in this frame.
LYHD - Layer Header
The LayerHeader appears at the start of each layer to describe the layer's contents.
typedef struct st_LayerHeader { short flags; short layerType; short bytesPerLayerPixel; short compression; float blackPoint; float whitePoint; float gamma; } LayerHeader;
flags
- One or more of the following flags, combined using bitwise-or.
Layer_Int (0 << 0)
Layer_FP (1 << 0)- Data in the layer is integer (the default) or floating-point.
Layer_Interlace (1 << 1)
- Scanlines are interlaced (field rendered).
Layer_EvenField (0 << 2)
Layer_OddField (1 << 2)- Field dominance for interlaced layers. This indicates which field is displayed first in time.
layerType
- The data channel contained in the layer. Possible values include
Layer_MONO 0
- Monochrome (grayscale) image channel.
Layer_RED 1
Layer_GREEN 2
Layer_BLUE 3
Layer_ALPHA 4- Color and alpha channels.
Layer_OBJECT 5
- Object ID.
Layer_SURFACE 6
- Surface or material ID.
Layer_COVERAGE 7
- Object transparency/antialiasing.
Layer_ZDEPTH 8
Layer_WDEPTH 9- The Z depth is the distance from the camera to the nearest object visible in a pixel. Strictly speaking, this is the perpendicular distance from the plane defined by the camera's position and view vector. The W depth buffer contains the inverse of Z.
Layer_GEOMETRY 10
- The values in this buffer are the dot-products of the surface normals with the eye vector (or the cosine of the angle of the surfaces to the eye). They reveal something about the underlying shape of the objects in the image. Where the value is 1.0, the surface is facing directly toward the camera, and where it's 0, the surface is edge-on to the camera.
Layer_SHADOW 11
- Indicates where shadows are falling in the final image. It may also be thought of as an illumination map, showing what parts of the image are visible to the lights in the scene.
Layer_SHADING 12
- A picture of the diffuse shading and specular highlights applied to the objects in the scene. This is a component of the rendering calculations that depends solely on the angle of incidence of the lights on a surface. It doesn't include the effects of explicit shadow calculations.
Layer_DFSHADING 13
Layer_SPSHADING 14- Like the
Layer_SHADING
buffer, but these store the amount of diffuse and specular shading (highlighting) separately, rather than adding them together. Layer_TEXTUREU 15
Layer_TEXTUREV 16
Layer_TEXTUREW 17- Texture coordinates.
Layer_NORMALX 18
Layer_NORMALY 19
Layer_NORMALZ 20- Normal vector. This is the geometric normal of the object surface visible in each pixel.
Layer_REFLECT 21
- Reflection.
Layer_MOTIONX 22
Layer_MOTIONY 23- Support for 2D vector-based motion blur. These buffers contain the pixel distance moved by the item visible in each pixel. The amount of movement depends on the camera exposure time and includes the effects of the camera's motion.
bytesPerLayerPixel
- Number of bytes per pixel, usually 1, 2 or 4.
compression
- One of the following compression codes.
NoCompression 0
- Data is uncompressed.
HorizontalRLE 1
VerticalRLE 3- Run-length encoding (RLE). The horizontal type is identical to the
byteRun1
RLE encoding used in ILBM and the output of the MacintoshPackBits
function. The vertical type compresses along columns rather than rows. The compressor treats the data as a sequence of bytes, regardless of the data type of the layer's values. HorizontalDelta 2
VerticalDelta 4- Delta encoding for animation. Only the parts of the image that differ from a previous frame are written.
The RLE and delta methods are described in more detail below.
blackPoint, whitePoint
- The nominal minimum and maximum buffer levels. These define the dynamic range of the data in the layer. Typical values for RGB layers are 0.0 and 1.0.
gamma
- Linearity of the data. This and the black and white points are used primarily to encode RGB levels for different display devices. The default is 1.0.
Layer Data
The data for a layer is written in a LAYR
chunk that immediately follows the
layer's LayerHeader. The data is a rectangular array of values. The origin is the top left
corner, and before compression, values are stored from left to right, and rows from top to
bottom. No padding is added to the end of any row.
When the compression type is NoCompression
, this is also how the layer is
written in the file. The number of bytes in one row is
rowbytes = LayerHeader.bytesPerLayerPixel * FPHeader.width;
The number of rows is FPHeader.Height
, and the total number of bytes of layer
data (and the LAYR
chunk size) is
layerbytes = rowbytes * FPHeader.height;
RLE Compression
The following psuedocode illustrates how RLE-compressed bytes are unpacked.
loop read the next source byte into n if n >= 0 copy the next n + 1 bytes literally else if n < 0 replicate the next byte -n + 1 times until the row or column is full
The unpacker reads from the source (compressed data in a LAYR
chunk) and
writes to a destination (a memory buffer). For horizontal RLE, the destination pointer is
incremented by 1 for each decoded byte, while for vertical RLE, the destination pointer is
incremented by rowbytes
bytes.
Each row (or column) is separately packed. In other words, runs never cross rows (or columns).
In the inverse routine (the packer), it's best to encode a 2 byte repeat run as a replicate run except when preceded and followed by a literal run, in which case it's best to merge the three into one literal run. Always encode 3 byte repeats as replicate runs.
Delta Compression
The delta compression method uses RLE, but it adds a mechanism for skipping bytes that haven't changed. This is used when storing animation frames. The skipped bytes retain the values stored there by a previous frame.
loop read the next source byte into nc if nc < 0 skip ahead -nc columns else for i = 0 to nc read the next source byte into nr if nr < 0 skip ahead -nr rows else unpack rle encoded span of size nr + 1 until the layer is full
Example Code
The unpackRLE
function decodes RLE compressed data. psrc
points to the source pointer. The function advances the source pointer as it decodes the
compressed bytes. dst
is the destination buffer where decoded
bytes are written. size
is the RLE span, or the number of
destination bytes that should be produced. This is typically rowbytes
for
horizontal RLE and FPHeader.Height
for vertical RLE. step
is the number of bytes that the destination pointer should be moved after each decoded
byte is written, typically 1 for horizontal and rowbytes
for vertical. The
function returns TRUE
if it succeeds and FALSE
otherwise.
int unpackRLE( char **psrc, char *dst, int size, int step ) { int c, n; char *src = *psrc; while ( size > 0 ) { n = *src++; if ( n >= 0 ) { ++n; size -= n; if ( size < 0 ) return FALSE; while ( n-- ) { *dst = *src++; dst += step; } } else { n = -n + 1; size -= n; if ( size < 0 ) return FALSE; c = *src++; while ( n-- ) { *dst = c; dst += step; } } } *psrc = src; return TRUE; }
The packRLE
function reads uncompressed bytes from the source buffer and
writes encoded bytes to the destination. It returns the number of bytes written to the
destination (the packed size of the source bytes).
#define DUMP 0 #define RUN 1 #define MINRUN 3 #define MAXRUN 128 #define MAXDUMP 128 int packRLE( char *src, char *dst, int size, int step ) { char c, lastc; int mode = DUMP, rstart = 0, putsize = 0, sp = 1, i; lastc = *src; size--; while ( size > 0 ) { c = *( src + sp * step ); sp++; size--; switch ( mode ) { case DUMP: if ( sp > MAXDUMP ) { *dst++ = sp - 2; for ( i = 0; i < sp - 1; i++ ) *dst++ = *( src + i * step ); putsize += sp; src += ( sp - 1 ) * step; sp = 1; rstart = 0; break; } if ( c == lastc ) { if (( sp - rstart ) >= MINRUN ) { if ( rstart > 0 ) { *dst++ = rstart - 1; for ( i = 0; i < rstart; i++ ) *dst++ = *( src + i * step ); putsize += rstart + 1; } mode = RUN; } else if ( rstart == 0 ) mode = RUN; } else rstart = sp - 1; break; case RUN: if (( c != lastc ) || ( sp - rstart > MAXRUN )) { *dst++ = rstart + 2 - sp; *dst++ = lastc; putsize += 2; src += ( sp - 1 ) * step; sp = 1; rstart = 0; mode = DUMP; } } lastc = c; } switch ( mode ) { case DUMP: *dst++ = sp - 1; for ( i = 0; i < sp; i++ ) *dst++ = *( src + i * step ); putsize += sp + 1; break; case RUN: *dst++ = rstart + 1 - sp; *dst = lastc; putsize += 2; } return putsize; }
The unpackDelta
function decodes delta-compressed data. After skipping to a
part of the layer containing changes, it calls unpackRLE
.
int unpackDelta( char *src, char *dst, int size, int vstep, int hstep ) { int n, nn; while ( size > 0 ) { n = *src++; --size; if ( n < 0 ) dst += -n * vstep; else { for ( ; n >= 0; n-- ) { nn = *src++; --size; if ( nn < 0 ) nn = -nn; else { ++nn; if ( !unpackRLE( &src, dst, nn, hstep )) return FALSE; } dst += nn * hstep; } } } return TRUE; }