Chapter 8. 2D Graphics

Table of Contents

Overview
Creating Surfaces
XFcGLSurface
XFcGLTexture
XFcGLResourceSurface
Getting Surface Information
Locking Surfaces
Blitting Surfaces
Color Keys
Basic Blitting
Blending
Transforms
2D Graphics Primitives
Filled Rectangles
Lines
RLE compressed sprites
XFcRLESprite
Using RLE compressed sprites
XFcGLSurfaceToolkit
Surface resampling

Overview

Two-dimensional graphics under X-Forge core is based mainly on graphics surfaces. A graphics surface is practically a wrapper for the actual pixel data, with additional functionality. Surfaces can be blitted on surfaces with optional scaling, blending and color keying, and they clip automatically to the target surface.

Additionally, rectangle filling and line drawing is supported. Surfaces can also be locked and the surface data accessed directly.

In order to display graphics on screen, the back buffer is also a surface, and can be used just like any other surface.

When locking a surface, you can select the pixel format in which you want the surface to be locked. If this format is not the same as the surface's native format, automatic pixel conversion will take place.

Warning

Locking a surface with alpha channel data in a format that does not have alpha data will destroy the alpha channel data.

Warning

Pixel format conversions are heavy operations and should not be done on a per-frame basis.

Creating Surfaces

An XFcGLSurface object is created via the static XFcGLSurface::create() call.

Alternatively, the application can use the XFcImageLoader interface to load an image file directly into a surface. Yet another way is to create a XFcGLResourceSurface instead, which creates a surface which is also a resource (and can be destroyed and re-created by the system as space is needed).

X-Forge application can also gain access to surfaces via texture objects, or by asking for secondary surface from XFcGL.

XFcGLSurface

An application has two basic ways to create XFcGLSurface objects; either by using the create() method, or by using the XFcImageLoader class.

XFcGLSurface::create()

The XFcGLSurface class static method create() takes three parameters:

XFcGLSurface * create(INT32 aWidth = 256,
                      INT32 aHeight = 256,
                      INT32 aFormat = XFCGF_DEFAULT);

The width and height parameters describe the desired width and height of the surface, in pixels. The format is one of the following:

XFCGF_DEFAULT
XFCGF_R5G6B5 
XFCGF_X8R8G8B8
XFCGF_A1R5G5B5
XFCGF_A8R8G8B8
XFCGF_X6X5X5  
XFCGF_1X5X5X5

The enumerations describe the bit patterns of the pixel formats. R5G6B5 format has 5 bits for red, 6 for green and 5 for blue, in that order, totaling 16 bits per pixel. X bits are ignored.

The default mode is platform dependent, but currently all platforms use the R5G6B5 pixel format.

The last two pixel formats are special formats that can be used for some special effects. The X6X5X5 format, for example, is actually 32 bit format with bit masks X5G6X5R5X6B5. This format can be used, for example, to calculate average of two pixels with one 'add', 'shift' and 'and' instruction. The 1X5X5X5 format is the same, except that it also contains one bit for alpha.

Note

These enumerations, like all other 2D graphics enums, can be found in the XFcCoreEnums.h header file.

The create function, like all other create functions, will return NULL if failed.

XFcImageLoader

A much more typical way to create surfaces is to load image files directly into surfaces. This can be done through the XFcImageLoader class. The class includes several static member functions which can be used to load images in different ways. The most typical way is to just load image into a surface:

XFcGLSurface * load(const CHAR *aFilename,
                    INT32 aSurfaceFormat = XFCGF_DEFAULT);

The filename is the name of the file to be loaded, and the desired surface format is one of the surface format flags, above.

Note

With XFCGF_DEFAULT, if the source image has a alpha channel (32bit TGA file, for instance), the target pixel format will be 1555 instead of 565.

The XFcImageLoader class also contains functions to load image files in several other ways. You can query information about a file with loadImageInfo():

void loadImageInfo(const CHAR *aFilename, 
                   INT32 *aWidth,
                   INT32 *aHeight, 
                   INT *aAlphaLayer,
                   INT *aPaletted);

With the exception of aFilename, all of the parameters are pointers to variables which will receive the information. If aWidth or aHeight are reported as zero, image loading has failed. If some information is not desired, the parameter can be left as NULL. The aAlphaLayer and aPaletted parameters are flags.

To load the image data itself, you can load it in paletted image or in 32bit ARGB form.

UINT32 * loadImage8888(const CHAR *aFilename);

The 32bit loader will always work, if the image can be loaded at all, and there's enough memory. If the image file is paletted, it is automatically converted to 32bit format. If some error occurs, the returning pointer will be NULL.

void loadImagePaletted(const CHAR *aFilename,
                       UINT8 **aPalettePtr,
                       UINT8 **aBitmapPtr);

You can only load paletted image files with the paletted image loader. No automatic conversion is done from direct-color images. If you try to load a direct-color image with the paletted loader, the pointers will be set to NULL.

Finally, you can load an image directly to a buffer.

INT loadImageToBuffer(const CHAR *aFilename, void *aBuffer,
                      INT32 aPitch,
                      INT32 aFormat = XFCGF_DEFAULT);

The pitch is in bytes. The target buffer must be big enough to contain the image.

Extending ImageLoader

In order to enable XFcImageLoader to support other image file formats than supported by default, you can extend the XFcImageLoader class.

At minimum, you must extend three functions, and call one method in the constructor.

    INT validateImage(const CHAR *aFilename, XFcFile *aFile)

This function should, with minimum effort, check whether this filter can handle the image file. The aFile parameter is an already opened file handle to the image file. The file pointer may point at any place in the file, so it is best to seek it to the beginning. (It is also considered good manners to seek it to the beginning after you've done with it). If for some reason you cannot use the ready file pointer, you can use the filename instead. In any case you should not close the file handle.

void getImageInfo(const CHAR *aFilename, XFcFile *aFile,
                  INT32 *aWidth, INT32 *aHeight, INT *aAlphaLayer,
                  INT *aPaletted)

This member will be called to get file information. Please note that the pointers may be NULL, in which case the parameters should be ignored. The file pointer rules are the same as with the validateImage() method. If an error occurs, the width and height should be set to 0.

UINT32 * getImage8888(const CHAR *aFilename, XFcFile *aFile)

This member should allocate 32bit buffer big enough to contain the image data, and then load the data into it. If the source file is always paletted, you do not need to extend this member at all; the default implementation calls the paletted image loader and does the conversion as needed. The file pointer rules are the same as with validateImage(). If an error occurs, the function should clean up any allocated space and then return NULL.

void getImagePaletted(const CHAR *aFilename, XFcFile *aFile,
                      UINT8 **aPalettePtr, UINT8 **aBitmapPtr);

This member should allocate 8bit buffer for the image and 768-byte buffer for the palette, and then load the image to it. The palette should be 8-bit triplets of RGB. Some paletted image formats only use 6 bits of the color components; these should be shifted left by two bits. If the source file format is always direct-color, you should not extend this function at all. If an error occurs, the function should clean up any allocated memory and then set the target pointers to NULL. The rules for the file pointer are the same as with validateImage().

void registerImageLoader(XFcImageLoader *aFilter)

Upon construction, the class should call this member function to register itself in the list of image loading filters.

Please note that due to the fact that the applications should not use any static data, the class should be constructed at application start. The core will delete the object on shutdown.

Note

There is an example implementation of a BMP loader in the core examples.

XFcGLTexture

A texture is a special collection of surfaces. Internally, a texture is an array of surface headers and a single resource containing all of the actual image data. The different surfaces in a texture object represent the mipmaps of the texture.

The easiest way to create texture objects is to use the XFcGLTextureFromFile class. You can also create textures manually by extending XFcGLTextureBuilder, and then calling XFcGLTexture::create with the texture builder object as a parameter. It is usually easier to use the XFcImageLoader extendibility, though.

Note

Texture resolution must be in the powers of two, and the maximum resolution is 256. Legal texture resolutions include 256x128, 64x8, 2x32 etc.

In order to access a surface in a texture object, you must lock the texture. A simple example that draws all the mipmap levels of a texture onto the screen:

INT32 surfCount = tex->getSurfaceCount();
XFcGLSurface *fb = mGL->getSecondary();
INT32 y = 0;
INT32 i = 0;
while (i < surfcount)
{
    XFcGLSurface *texSurf = tex->lock(i);
    if (texSurf)
    {
        fb->drawImage(texSurf, 0, y);
        y += texSurf->getHeight();
        tex->unlock(i);
    }
    i++;
}

Note

While the surfaces in a texture can be used just like any other GLSurface, it should be noted that any changes drawn to the surface will be lost if the resource manager decides to remove it from memory.

Extending TextureBuilder

In order to extend the TextureBuilder class, you must extend the following five methods.

INT32 getSurfaceCount()

This method returns the number of surfaces required for the texture. If mipmaps are not in use, this value should be one. Otherwise it should return the number of mipmap layers the texture builder will generate.

void getSurfaceParams(INT32 aSurfaceNumber, INT32 &aWidth,
                      INT32 &aHeight, INT32 &aFormat)

This method returns the surface parameters for a given surface number. Typically, all surfaces share a common pixel format, but have different width and height.

INT fillSurface(INT32 aSurfaceNumber, 
                void *aBuffer,
                XFcGLComplexSurfaceResource *aComplexSurfaceResource)

This method fills the requested surface. The surfaces are always requested in linear order, and thus you can trust that the earlier surfaces exist. See below for an example on filling out all of the mipmap levels.

INT sameClassEquals(XFcGLTextureBuilder *aBuilder)

Since the resource manager tries not to load a single texture several times, it needs a method of identifying duplicate textures, and it does this by comparing texture builders. First check is to see whether the two texture builders have the same class; this check is done via the mBuilderUniqueID member variable, which should be set to some unique value. (The value for the TextureFromFile class is 1). Once two builders with the same class are found, they are compared with sameClassEquals(). It is up to the texture builder itself to figure out if the two builders are equal.

~XFcGLTextureBuilder()

The destructor should clean up any data the TextureBuilder object has allocated.

An easy way to fill out all of the mipmap levels of the texture is to lock the earlier (bigger) surface and use XFcGLTexture's mipmap filters to calculate the smaller surfaces. Here's the code that does that:

XFcGLSurface *upper = aComplexSurfaceResource->lock(aSurfaceNumber - 1);
if (mTextureFormat == XFCGF_A1R5G5B5)
{
    if (mFlags & XFCTC_MIPMAP_POINTSAMPLE)
    {
        XFcGLTexture::filterMapPointSample1555(upper, aBuffer);
    }
    else
    {
        XFcGLTexture::filterMapLinear1555(upper, aBuffer);            
    }
}
else 
if (mTextureFormat == XFCGF_R5G6B5)
{
    if (mFlags & XFCTC_MIPMAP_POINTSAMPLE)
    {
        XFcGLTexture::filterMapPointSample565(upper, aBuffer);
    }
    else
    {
        XFcGLTexture::filterMapLinear565(upper, aBuffer);            
    }
}
aComplexSurfaceResource->unlock(aSurfaceNumber - 1);

You do, naturally, still have to fill out the first surface yourself.

Note

If you wish to mimic TextureFromFile in functionality, ie. to have the create() call return XFcGLTexture objects directly, make sure that you initialize the texture builder completely before calling the XFcGLTexture::create() method.

Warning

When defining flags, make sure that you do not overlap flags with the resource manager. As we did with XFCTC flags, leave the bottom 8 bits to zero.

XFcGLTextureFromFile

The only usable public member of the XFcGLTextureFromFile is the create function. Since it returns XFcGLTexture objects, the other functions are never exposed to the application.

XFcGLTexture * create(const CHAR *aFilename,
                      INT32 aFlags = XFCTC_MIPMAP_POINTSAMPLE,
                      UINT32 aColorKey = 0xff000000);

The filename is the name of the file to be loaded. The XFcGLTextureFromFile uses the XFcImageLoader class internally, so all image formats supported by the system work.

The flags can be a combination of the following:

XFCTC_MIPMAP_NOMIPMAPS
XFCTC_MIPMAP_POINTSAMPLE
XFCTC_MIPMAP_LINEAR
XFCTC_UNIQUE
XFCTC_COLORKEY

If NOMIPMAPS is used (and POINTSAMPLE or LINEAR are not used), the resulting texture will not have any mipmaps. The POINTSAMPLE and LINEAR alter the way the mipmaps are generated. The POINTSAMPLE mode is faster, but usually results in uglier mipmaps.

If UNIQUE is used, the resource manager will create a new resource for this texture even if a texture was created from the same image file with same parameters earlier. This is useful mostly if you wish to edit the textures at runtime (but keep in mind that the resource manager may still decide to re-create the texture).

The colorkey flag must be added if you wish to use the color key parameter.

Additionally, the flags may contain resource manager flags. It is recommended that those are not used unless absolutely neccessary, however.

The color key can be used to create a texture with alpha layer even though the source image contains no alpha information. Select a color to be transparent, set that as the colorkey parameter and include the XFCTC_COLORKEY flag.

XFcGLResourceSurface

The XFcGLResourceSurface is a surface that is also a resource. It uses XFcImageLoader internally to recreate itself if needed.

XFcGLResourceSurface * create(const CHAR *aPictureFilename,
                              INT32 aFormat = XFCGF_DEFAULT,
                              INT32 aFlags = 0);

The picture filename and format are the same as with the XFcGLTextureFromFile::load() function, above. The flags are sent to the resource manager, and it is recommended that they are left as zero.

Note

While the ResourceSurface can be used just like any other XFcGLSurface, it should be noted that any changes drawn to the surface will be lost if the resource manager decides to remove it from memory.

Getting Surface Information

The XFcGLSurface class contains three functions with which the application can query the width, height and pixel format of the surface. The functions are getWidth(), getHeight() and getFormat(), respectively.

Locking Surfaces

In order to manipulate pixel data directly, a surface must be locked. This can be done via the lock() call. The method prototype is as follows:

INT32 lock(void ** aBuffer, INT32 aMode = XFCGF_DEFAULT,
           INT32 aFlags = XFCGFX_DISCARDCONTENTS);

The first parameter is a pointer to the pointer variable that receives the address of the pixel data. Mode is the pixel format; default means the surface's pixel format. Flags can contain XFCGFX_DISCARDCONTENTS, XFCGFX_DISCARDCHANGES or nothing.

If the graphics mode is different from the surface's internal format, automatic pixel conversion occurs. If XFCGFX_DISCARDCONTENTS is used, the conversion from original format is skipped, but the resulting data is converted to internal format on unlock(); if XFCGFX_DISCARDCHANGES is used, the original data is converted to desired format, but conversion back is skipped.

The return value is the pitch of the surface, in bytes. In some cases the pitch may be different from surface width.

Always remember to unlock() your surfaces after you're finished with it. You cannot use the surface in any other operation (including blitting) while it's locked.

Typical surface locking code looks like this:

XFcGLSurface *surf = XFcGLSurface::create(16, 16);
UINT16 *p;
INT32 pitch = surf->lock((void**)&p);
if (p != NULL)
{
    pitch /= 2;
    INT32 i, j;
    for (i = 0; i < 16; i++)
    {
        for (j = 0; j < 16; j++)
        {
            p[i * pitch + j] = (i << 2) + (j << 12);
        }
    }
    surf->unlock();
}

Warning

XFCGFX_DISCARDCHANGES and XFCGFX_DISCARDCONTENTS flags are meant only for optimization, and should not be trusted; ie. if you lock a surface with XFCGFX_DISCARDCHANGES, but the pixel format you choose is equal to the format used internally by the surface, the changes will NOT be discarded.

Warning

Locking a surface with alpha channel data in a format that does not have alpha data will destroy the alpha channel data.

Note

On X-Forge version 0.24.1 and earlier, the default locking pixel format always meant the RGB565 format.

Blitting Surfaces

One major use for surfaces is the blitting of surfaces, or drawing the contents of one surface on another. The XFcGLSurface object supports multiple different ways of blitting.

Color Keys

The surface object supports making parts of the image transparent by selecting a color key. The "color keying" is controlled via the following function calls:

void setColorKey(INT32 aColorKey);    
void enableColorKey(INT aFlag);
INT32 getColorKey();

Color key, ie. the color which will become transparent, is set via the setColorKey() call. The color key must be in XRGB8888 format. The color keying can be set on or off via the enableColorKey() call; the default is off. Finally, you can get the current color key value with the getColorKey() call.

Note

In X-Forge versions 0.24.1 and earlier, this functionality was called 'alpha masking'; the setAlphaMask(), enableAlphaMask() and getAlphaMask() functions no longer exist.

Note

The color keying refers to the surface you're about to draw over some other surface, and not vice versa. In other words, the pixels in source image surface that are "color keyed" do not get drawn on the target surface.

Basic Blitting

The surface blitting is performed using the drawImage() calls on the target surface. In the easiest form, the blitting looks like this:

targetSurface.drawImage(sourceSurface);

If you wish to draw the source surface to some other place than upper left corner of the target surface, you can add the coordinates:

targetSurface.drawImage(sourceSurface, tX, tY);

Please note that these coordinates can also be negative; the blitter performs clipping as needed.

The blitter can also scale your images, using either only width or both width and height value (if only width is used, height is calculated so that aspect ratio stays the same)

targetSurface.drawImage(sourceSurface, tX, tY, tWidth, tHeight);

If you don't want to draw the whole source surface to the target, you can use the variants that support source rectangle:

targetSurface.drawImage(sourceSurface, 
                        XFcRectangle(5,5,10,10));    
targetSurface.drawImage(sourceSurface, 
                        XFcRectangle(5,5,10,10), tX, tY);    
targetSurface.drawImage(sourceSurface, 
                        XFcRectangle(5,5,10,10), tX, tY, tWidth);    

Blending

For somewhat more interesting effects, you can do blitting with different blending modes. These blending modes are separate from the 3D pipeline. The currently supported blending modes are:

XFCBLEND_NONE

No blending; default

XFCBLEND_ALPHA

Cross-fade blend. At 0%, the target pixels are not changed at all, and at 100% the target pixels are completely covered.

XFCBLEND_ALPHA_FAST

Cross-fade blend special case; same as alpha blend at 50%.

XFCBLEND_MUL

Multiplicative blend. Source and target pixels are multiplied together. At 0%, the result is always completely black, and at 100% the result is plain srcPixel * tgtPixel.

XFCBLEND_MUL_FAST

Multiplicative blend special case; same as multiplicative at 100%

XFCBLEND_ADD

Additive blend. The source and target pixels are added together and guarded against overflow, so 60% + 60% = 100%.

XFCBLEND_ADD_FAST

Additive blend special case; same as additive at 100%

XFCBLEND_INVMUL

Inverse multiplicative blend. More useful than its inverse, at 0% the target pixels are not changed at all, and at 100%, the source and target are multiplied together.

XFCBLEND_INVMUL_FAST

Inverse multiplicative special case; same as inverse multiplicative at 100%

Note

These enumerations, like all other 2D graphics enums, can be found in the XFcCoreEnums.h header file.

To use the blending modes, use the drawImageBlend() family of functions. Their functionality is the same as the simple ones, with two additional parameters.

void drawImageBlend(XFcGLSurface *aImage, 
                    INT32 aBlendType, INT32 aBlendValue);

Blend type is one of the enums listed above, and blend value is the value mentioned above. The range for the blend value is 0..255.

Transforms

The blitter also supports simple geometry transforms:

XFCTRANSFORM_FLIP_HORIZONTAL

Flip contents horizontally

XFCTRANSFORM_FLIP_VERTICAL

Flip contents vertically

XFCTRANSFORM_ROTATE_0
XFCTRANSFORM_ROTATE_90
XFCTRANSFORM_ROTATE_180
XFCTRANSFORM_ROTATE_270

Rotate the surface by N degrees.

Note

These enumerations, like all other 2D graphics enums, can be found in the XFcCoreEnums.h header file.

The flags can also be combined. Please note, for example, that combining ROTATE_180 with FLIP_VERTICAL and FLIP_HORIZONTAL nulls the effects of all three flags.

To use the transformations, use the drawImageTransform() family of functions. They work exactly like the simple ones, with the additional flags parameter.

void drawImageTransform(XFcGLSurface * aImage, 
                        UINT32 aFlags);

2D Graphics Primitives

Currently, the XFcGLSurface supports only filled rectangle and line drawing. The support for other graphics primitives are in the plans, but are still under construction.

Filled Rectangles

The XFcGLSurface class supports filling rectangles with a single color using the drawFilledRect() call.

void drawFilledRect(INT32 aX, INT32 aY,
                    INT32 aWidth, INT32 aHeight, 
                    UINT32 aColor);

The color is in ARGB8888 format.

If used on back buffer and graphics device supports it, this call is accelerated by hardware.

Note

The color format used to be XRGB8888 instead of ARGB8888 in X-Forge versions 0.24.1 and earlier. When alpha is set to 0, nothing is drawn, so older programs may fail to draw filled rectangles correctly.

Lines

The XFcGLSurface class supports drawing lines and anti-aliased lines.

void drawAALine(XFcFixed aX1, XFcFixed aY1, 
                XFcFixed aX2, XFcFixed aY2,
                UINT32 aColor);

void drawLine(XFcFixed aX1, XFcFixed aY1, 
              XFcFixed aX2, XFcFixed aY2,
              UINT32 aColor);  

The color is in ARGB8888 format.

RLE compressed sprites

XFcRLESprite

XFcRLESprite is a packed form of a surface, suitable for 2D blitting. XFcRLESprite is suitable especially for drawing overlay graphics, since it has several benefits over using plain surfaces in some situations. XFcRLESprite compresses transparent areas and uniformly colored spans to smaller space. Therefore it usually consumes much less memory than a plain surface, and is also faster to blit. XFcRLESprite also supports alpha channel, thus allowing anti-aliased or semi-transparent graphics. There are a few drawbacks also when using XFcRLESprites: blitting can't be scaled and it's not possible to blit into the XFcRLESprite. Once created, the XFcRLESprite can't be modified.

Using RLE compressed sprites

XFcRLESprite can be constructed from a surface. The construction takes a pointer to the surface, a base mask and a base color. Base values can be used to specify the transparent color for the surface. For instance mask value of 0x00ffffff will mean that all color components (but not alpha) will be included in the transparent color check. In a case like this, having base color of 0xff0000 will mean that bright red will be the transparent key color.

To construct XFcRLESprite from a 32-bit ARGB surface so that alpha channel would be compressed as the transparent colour, the base mask should be 0xff000000 and the base color 0. The graphics file should be in 32-bit Targa format (to save disk space, use RLE-compressed Targa files), and the image should be loaded in 32-bit ARGB mode. The following code sample demonstrates this:

XFcGLSurface *tempSurface;
XFcRLESprite *hudSprite;
tempSurface = XFcImageLoader::load(XFCSTR("hud.tga"),XFCGF_A8R8G8B8);
hudSprite = XFcRLESprite::create(tempSurface, 0xff000000, 0x00000000);
delete tempSurface;

XFcGLSurfaceToolkit

XFcGLSurfaceToolkit provides methods for manipulating surfaces.

Surface resampling

XFcGLSurfaceToolkit has a method resampleSurface() that can be used for scaling images with higher quality than just scaled blit. Scaling with resampleSurface() uses bilinear interpolation and box filtering algorithms, whereas scaled blit uses just point sampling. The scaling can be used for instance when the same graphical content should be used for various screen sizes. It is adviced that the scaling is done after loading the content, not in real time, as the algorithms can't necessarily provide interactive speeds.

static XFcGLSurface * resampleSurface(XFcGLSurface *aSurface,
                                      REAL aXScale,
                                      REAL aYScale);