The big dummies guide to graphics programming ---------------------------------------------
This Document tries to give some enlightenment about pixel oriented graphics programming with lowlevel graphics libraries.
It is intended for people that have never ever used a pixel oriented graphics library before. If you have, it might be a bit boring.
For the examples presented here, we use the LibGGI library which is available at http://www.ggi-project.org/. That's also, what links it to "C", as LibGGI is written entirely in C, and intended to be used by C Programs, though wrappers for a bunch of languages exist.
We also use the GGI terminology, as LibGGI covers a very wide range of possible hard- and software to draw on, which causes it to have a very broad set of features that are not supported in many other libraries.
If you are using anything but LibGGI, you will probably find, that LibGGI at times has abstraction layers for stuff that you have to access directly on your library. This at times simplifies the task, at times makes it harder, depending on how much you give about portability.
But let's get started ...
A visual is something you can draw on. Imagine it as a piece of paper.
This is actually a pretty good comparision, as pieces of paper tend to be rectangular in shape. When we are talking about visuals in this document we mean a rectangular area that can be drawn to.
Most simple graphics libraries (as compared to those connected to windowing systems) have no notion of a "visual handle" - that is a "name" for a visual. This comes from the fact, that most graphics libraries can only handle a single such visual at a time.
LibGGI was designed to be capable of handling multi-headed applications, like a CAD system showing the model on one high-res screen while displaying the menus on another one.
For this reason you need to allocate a visual handle when working with LibGGI. Even if you do not use the multihead capabilities.
Example 2.1: Getting at a visual and getting rid of it againggi_visual_t vis; /* a place to store the visual handle */ if (ggiInit()) { /* Initialize LibGGI */ printf("Ouch - ggiInit failed.\n"); /* oh - we failed ??? */ return 1; } /* Allocate a visual handle for the default visual (NULL) */ if (NULL==(vis=ggiOpen(NULL))) { ggiExit(); /* Deinitialize the library properly */ printf("Ouch - ggiOpen failed.\n"); return 2; } /* Now here we can actually start drawing - or ? * No. We can't. Read on, if you said "yes" :-). */ /* Now we close it down again */ ggiClose(vis); ggiExit();
O.K. - we now have a visual, so we can draw on it - right ?
No. We can't. We first need to think about the way a computer can store something drawn on a visual.
One way would be something like:
- go to the top-left corner
- draw a line to the middle of the sheet
- draw a line straight up to the top border
- ...
This is called "vector oriented" drawing, as you basically give a list of motion vectors you would follow with a pencil.
You might have noticed, that giving positions with phrases like "top-left corner" isn't very flexible.
So the first thing we need is a coordinate system.
That is, we take a point on our visual and call that the "origin". Then we draw two perpendicular lines through that origin and call that the axes x and y.
To keep things simple, we use a corner of our rectangular drawing area for the origin and the edges that join there for the axes.
In computer graphics, the chosen corner is usually the top-left one, and the top edge, spanning left->right, is called the x, the right edge, spanning top->bottom is called the y axis.
If you now apply some kind of ruler-marks to the axis you can describe any point on the visual with a pair of values (x,y), which you get by going straight up from the point in question and reading the x-ruler and as well going to the right and reading the y-ruler.
Up to now we can describe an infinite number of points. That's not good, as computers have only a finite amount of memory, so we make our "points" a little bigger.
We draw a rectangular grid on our visual that divides it into many small rectangles and renumber our rulers, so that each row and column of the grid gets a number ranging from 0-maxx for the columns and from 0-maxy for the rows.
Beginning at 0 instead of 1 has mathematical advantages later, though it brings the at first sight strange property of a 320x200 grid being addressed with pairs that use the ranges 0-319 and 0-199.
In LibGGI, we can set that coordinate System, which is commonly called a "mode" using something like:
if (ggiSetSimpleMode(vis, GGI_AUTO, GGI_AUTO, GGI_AUTO, GT_AUTO) != 0) { ggiPanic("Cannot set default mode!\n"); }
Now - what's all that "AUTO"-stuff ?
Well, the prototype for the function is:
int ggiSetSimpleMode(ggi_visual_t visual, int xsize, int ysize, int frames, ggi_graphtype type);
What does that mean ?
O.K. - first of all we have the visual, we want to operate on. (Remember ? LibGGI allows multiple visuals to be active at the same time, so we have to give that parameter for every call).
The xsize and ysize members are the sizes for the grid we just talked about. Now what the heck is "GGI_AUTO" ? It is a special value, that is used to specify that you don't really care and the library may pick a value based on user preferences or system constraints.
We use that, because LibGGI can run on such a variety of different platforms. On a PC, 320x200 might be a commonly accessible mode, but on higher-end hardware, it might not even be possible to set such a tiny mode.
Thus, you are advised to try not to rely on a given resolution to be present. You can of course insist on 320x200 by calling with (vis,320,200,...), but should be prepared that it could fail.
And what are frames ?
Many graphics cards have the ability to store multiple images at the same time (while only displaying one of them). Imagine that as a stack of sheets, all the same size and with the same grid. The advantage is, that you can draw on the "invisible" sheets that are not on top of the stack, without the user seeing the drawing going on (as he is always looking at the top of the stack). When you are done drawing, you can (the video hardware can actually) quickly swap the sheets and the user will see the final result. This is what is called doublebuffering. It makes for a much steadier look. But actually that's an advanced topic, we will pick up again later in the series.
And a graphtype ?
You have probably heard about those: It's about how different pixels can be, how many different colors you can use and such. You might have heard about terms like "TrueColor" and "HighColor". That's what it is about, but to understand that concept fully, we'll have to look closer at the individual pixels:
The little rectangles we just made by introducing the coordinate grid is what we call "pixels".
Usually pixels can be given attributes like a color, being bright, blinking being transparent etc. individually [there are a few cases where hardware limitations cause pixels to depend on each other, but these are very rare nowadays].
Now if you can give each such pixel a color, you can form an image from it.
A simple example:1 0123456789012 0 ####### 1 ## ## 2 # # # # 3 # # # 4 # # # # # 5 # # # # 6 # ##### # 7 ## ## 8 #######
I only used two "colors" here : ' ' and '#'.
You can see, that the image is quite crude. This is due to the very low resolution of 13x9 I was using. (Note the rulers on the sides ...)
As you can probably imagine, the image quality gets better, if you increase resolution. Of course this will also need more memory and thus as well be slower, if you change the picture.
As said, pixels can have attributes. Attributes tell pixels how they should look like. As computers always just work with numbers, there is normally a number for each pixel that describes the attribute.
For a red pixel, this might for example be the number 0xff0000 (hex) or 16711680 (dez), or it might be 0x4 or whatever.
Many graphics libraries assume that you understand the inner workings of the hardware to determine that number, which we will call the "pixelvalue". If you are lucky, they provide a few #defines for some common colors.
LibGGI does not do so, as when writing a LibGGI program, you cannot know, what sick hardware someone out there might have. LibGGI thus has a way of determining the pixelvalue from an abstract description of the wanted attributes.
Now how does abstractly but still precisely describe a color ? ("white" is not enough. The eskimo people are said to have about 200 different words for different shades of white ...)
The most common attribute of a pixel is it's color.
Speaking from the standpoint of physics, the color of a light source is determined by its wavelength. One could in theory specify a color by giving it's wavelength and the intensity.
This is however not what hardware can easily do, as wavelength-tuneable devices are very complex stuff.
What is done in reality, is to have a set of basic colors - usually red, green and blue - which can be mixed to get almost any color impression.
The read/green/blue model (RGB) is used in systems that emit light, like monitors. This is called additive color mixing, as the three colored lights red, green and blue add up to white when turned on simultaneously.
Systems that can only absorb light, like printed colors on a white paper, use the so called CMYK model, where the colors Cyan, Magenta and Yellow mixed together absorb all light thus causing a black impression. The K in CMYK stands for black, which is used in addition to the CMY, as existing colors usually do not give a perfect black, which is often a very wanted color.
There are a few other color spaces the most important of which I'll describe briefly here, just that you know what the terms mean, when you stumble across them:
a) HSV: Hue Saturation Value This is a very convenient model for photo-retouching and such, as it separates the brightness (Value) from the color information, and the color-contrast (Saturation) from the color (Hue) itself.
b) YUV: This one is used for video and similar devices, as it again separates the brightness (Y) info from the rest, which is useful, as the eye is much more sensitive to the brightness information than it is to the color. So for lossy compression, you start with reducing the data stored in the UV components.
As said, computer graphics almost always uses the RGB model, as it is how their hardware works. Thus most programmers are used to it and most image formats are based on it as well. This is why LibGGI uses it by default as well.
First of all, you need to know what color you want to display and how to describe it as an (R,G,B) triplet. To give you a place to start, I'll list a few common colors as such triplets:
( 0%, 0%, 0%) - Black ( 0%, 0%,100%) - Blue ( 0%,100%, 0%) - Green ( 0%,100%,100%) - Cyan (100%, 0%, 0%) - Red (100%, 0%,100%) - Purple (100%,100%, 0%) - Yellow (100%,100%,100%) - White
You can scale down a color in its brightness by multiplying all of its values with a given factor (note, that you cannot exceed 100% ... :-), so e.g. (0%,50%,50%) gives dark cyan. And you can move from one color to another by adjusting the differing values smoothly. E.g. to get a greenish Cyan, you use something like (0%,100%,50%).
Now percentile values are a bit inaccurate unless you are using fixedpoint, as the eye can distinguish more than 100 levels of brightness. You can see that on older VGAs with 6 Bit palette entries which only give 64 different shades. You can't get a really smooth shading on them.
Computers work with bits and bytes, so the most common thing to do, is to just use one byte per color giving 256 shades. However there are a few special purpose cards that can do more then 256 shades. For that reason LibGGI always uses _two_ bytes per color to be safe.
To ensure you do not have to know how many shades the underlying hardware can do, LibGGI always uses the range 0-65535 for 0-100%. The value will be rounded appropriately as the hardware permits.
LibGGI provides a call to make a pixelvalue from such an abstract color description:
ggi_pixel white; ggi_color map; map.r=0xFFFF; /* this is the highest intensity value possible */ map.g=0xFFFF; map.b=0xFFFF; white=ggiMapColor(vis, &map); printf("white=%d\n", white);
This might look inconvenient to people used to other graphics libs that assume you know how the hardware encodes pixels. You'll love it, once you are trying to make your application portable. Other people have other hardware, and the assumption often doesn't hold there ... you'll get a BIG bunch of #ifdefs or simply wrong colors without it.
Yes, we are finally there. We draw the first pixel:
ggiPutPixel(vis, 0, 0, white);
O.K. - that example is basically to avoid boring you with too much theory before you can see the first result. It draws a white pixel (you remember what a pixel is, right ?) in the top left corner (remember that talk about coordinate systems ? 0, 0, are the two coordinates in the order x,y or to say it more simply [left],[down]) of the visual.
Now we can basically do everything we need. opening a visual, setting up a coordinate system (also known as "mode"), and drawing colored pixels is all you really need to do computerized graphics.
Anything else is just convenience. We will talk about the convenient things like drawing lines and boxes, copying areas and rendering text as well as some other advanced topics like palettes(see Footnote 1) in upcoming continuations of the series.
If you have any problems, the GGI people are usually pretty glad to help you out. Instructions on how to get onto their discussion lists are also on the website.
But note, that LibGGI has very powerful internal debugging mechanisms. Try them first. Often setting the environment variable GGI_DEBUG to 255 and some common sense is all you need to set things straight.
---------------------
Footnote 1: We have not yet covered palettes, which might give you strange results, when executing your programs on palettized modes (often called 8-bit modes). You need to include the line:
ggiSetColorfulPalette(vis);
somewhere after the mode setup to make LibGGI work properly on such modes.