Tutorial 05: Patterns

This tutorial will cover the last paint type available for filling and/or stroking paths: patterns. Patterns are based on bitmap images, so before to start with the real tutorial, we have to introduce images as they are defined by OpenVG specifications.


Images in OpenVG

Images are rectangular collections of pixels. Image data may be inserted or extracted in a variety of formats with varying bit depths, color spaces, and alpha channel types. Images may be drawn to a drawing surface, used to define paint patterns, or operated on directly by image filter operations.

An image defines a coordinate system in which pixels are indexed using integer coordinates, with each integer corresponding to a distinct pixel. The lower-left pixel has a coordinate of (0, 0), the x coordinate increases horizontally from left to right, and the y coordinate increases vertically from bottom to top.

The VGImageFormat enumeration defines the set of supported pixel formats and color spaces for images. The letter A denotes an alpha channel, R denotes red, G denotes green, and B denotes blue. X denotes a padding byte that is ignored. L denotes grayscale, and BW denotes (linear) bi-level grayscale (black-and-white), with 0 representing black and 1 representing white in either case. A lower-case letter s represents a non-linear, perceptually-uniform color space, as in sRGB and sL; a lower-case letter l represents a linear color space using the sRGB primaries. Formats with a suffix of _PRE store pixel values in premultiplied format.

FormatBytes per pixelBits per pixel
VG_sRGBX_8888432
VG_sRGBA_8888432
VG_sRGBA_8888_PRE432
VG_sRGB_565216
VG_sRGBA_5551216
VG_sRGBA_4444216
VG_sL_818
VG_lRGBX_8888432
VG_lRGBA_8888432
VG_lRGBA_8888_PRE432
VG_lL_818
VG_A_1n/a1
VG_A_4n/a4
VG_A_818
VG_BW_1n/a1

Other available byteorder formats (A/XRGB, BGRA/X, A/XBGR) follow the same “bytes/bits per pixel” rules.

Images can be created through the vgCreateImage function, specifying format and dimensions (in pixels). The vgImageSubData function reads pixel values from memory, performs format conversion if necessary, and stores the resulting pixels into a rectangular portion of a given image; it’s the core function used to upload image pixels to the OpenVG backend (as a simplification, it could be though as the OpenVG counterpart of the OpenGL glTexSubImage2D / glTextureSubImage2D). For a more comprehensive overview, take a look at images section.


Patterns in OpenVG

Pattern paint defines a rectangular pattern of colors based on the pixel values of an image.

Pattern paints have a reference system that coincides with images: the lower-left pixel has a coordinate of (0, 0), the x coordinate increases horizontally from left to right, and the y coordinate increases vertically from bottom to top. Such system is then transported to the path coordinates system throught a “paint-to-user” affine matrix (VG_MATRIX_FILL_PAINT_TO_USER / VG_MATRIX_STROKE_PAINT_TO_USER). Finally the filled/stroked path is moved to the drawing surface system by another affine matrix, the so called “path-user-to-surface” (VG_MATRIX_PATH_USER_TO_SURFACE).

Pattern Matrices
Pattern Matrices

The tutorial defines a simple procedural image, then creates a pattern paint object and link the image to it (see genPaints function); here’s a simplified code version for a fixed 64x64 pattern image:

unsigned int pixels[64 * 64], x, y;
unsigned int colors[16] = {
    0xFF6030FF, 0xFFB060FF, 0xFF9090FF, 0xFF30B0FF,
    0x60FF30FF, 0xB0FF60FF, 0x90FF90FF, 0x30FFB0FF,
    0x6030FFFF, 0xB060FFFF, 0x9090FFFF, 0x30B0FFFF,
    0x303030FF, 0x606060FF, 0x909090FF, 0xB0B0B0FF
};

// create pattern image
VGImage image = vgCreateImage(VG_sRGBA_8888_PRE, 64, 64, VG_IMAGE_QUALITY_BETTER);

// generate procedural pixels (chessboard-like)
for (y = 0; y < 64; ++y) {
    unsigned int i = y / 16;
    for (x = 0; x < 64; ++x) {
        unsigned int j = x / 16;
        pixels[(y * 64) + x] = colors[(i * 4) + j];
    }
}

// upload pixels to the OpenVG backend
vgImageSubData(image, pixels, 64 * sizeof(unsigned int),
               VG_sRGBA_8888_PRE, 0, 0, 64, 64);

// create paint object
VGPaint pattern = vgCreatePaint();
// set the paint object to be a pattern paint
vgSetParameteri(pattern, VG_PAINT_TYPE, VG_PAINT_TYPE_PATTERN);
// link the image to the pattern paint object
vgPaintPattern(pattern, image);
64x64 pattern image
64x64 pattern image

As it can be seen from the code, to enable pattern paints, we use vgSetParameteri to set the paint type to VG_PAINT_TYPE_PATTERN. The vgPaintPattern function, instead, replaces any previous pattern image defined on the given paint object with a new pattern image.


Pattern tiling

Patterns may be extended (tiled) using one of four possible tiling modes, defined by the VGTilingMode enumeration:

Tile fill
Tile fill
Tile pad
Tile pad
Tile repeat
Tile repeat
Tile reflect
Tile reflect

The pattern tiling mode is set using vgSetParameteri with a paramType argument of VG_PAINT_PATTERN_TILING_MODE.

VGfloat tileColor[4] = { 0.1f, 0.6f, 0.3f, 1.0f };
// set tiling mode
vgSetParameteri(pattern, VG_PAINT_PATTERN_TILING_MODE, VG_TILE_FILL);
vgSetfv(VG_TILE_FILL_COLOR, 4, tileColor);

The tutorial code

The tutorial draws a circle path at the center of drawing surface, filled with a colored pattern. The path is created in object space with center at (0, 0) and a radius of 1 (see genPaths function):

// create the circle that will be filled by the radial gradient paint
filledCircle = vgCreatePath(VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_F,
                            1.0f, 0.0f, 0, 0, VG_PATH_CAPABILITY_ALL);
vguEllipse(filledCircle, 0.0f, 0.0f, 2.0f, 2.0f);

By having defined the path in this tricky way, it’s really easy to scale and center it in order to fit the drawing surface:

// find the minimum dimension between surface width and height, then halve it
int halfDim = (surfaceWidth < surfaceHeight) ? (surfaceWidth / 2) : (surfaceHeight / 2);
// calculate scale factor in order to cover 90% of it
userToSurfaceScale = halfDim * 0.9f;
// translate to the surface center
userToSurfaceTranslation[X] = surfaceWidth / 2;
userToSurfaceTranslation[Y] = surfaceHeight / 2;

// "user to surface" transformation (a uniform scale plus a translation),
// upload the matrix to the OpenVG backend
vgSeti(VG_MATRIX_MODE, VG_MATRIX_PATH_USER_TO_SURFACE);
vgLoadIdentity();
vgTranslate(userToSurfaceTranslation[X], userToSurfaceTranslation[Y]);
vgScale(userToSurfaceScale, userToSurfaceScale);

The user can set the pattern position and orientation through two control points, called center and target (defined in surface space). Such control points are highlighted by two white spots that can be moved by using mouse/touch.

In order to accomplish the task to map the pattern image to the desired bounds, we have to review some details (see setMatrices function):

// calculate pattern direction
float direction[] = {
    patternTarget[X] - patternCenter[X],
    patternTarget[Y] - patternCenter[Y]
};
// calculate scale factor
float dist = hypot(direction[X], direction[Y]);
float scale = dist / patternImageSize;
// calculate rotation factor
float rotation = atan2(direction[Y], direction[X]);

// "paint to user" transformation, upload the matrix to the OpenVG backend
vgSeti(VG_MATRIX_MODE, VG_MATRIX_FILL_PAINT_TO_USER);
vgLoadIdentity();
vgTranslate(patternCenter[X], patternCenter[Y]);
vgScale(scale, scale);
// OpenVG rotations must be expressed in degrees
vgRotate(rotation * 57.2957f);

The code represents the mapping between the pattern coordinates system and the drawing surface system (where control points live). But we know that, in between, there is the “user-to-surface” transformation too, which unfortunately interferes with the desired mapping. So, by setting the VG_MATRIX_FILL_PAINT_TO_USER matrix, we must take care to nullify the effect of VG_MATRIX_PATH_USER_TO_SURFACE matrix by appending its inverse transformation (i.e. essentially dividing scale and patternCenter values by userToSurfaceScale; see setMatrices function for the code details).