Graphics Programming.
© 1998, Particle
http://www.theparticle.com
This article is translated to Serbo-Croatian language by Web Geeks.
Introduction...
Programming graphics is a lot harder than most people think. It
involves tons of knowledge. More precisely, you need to know the concepts behind data
structures, math, and be fairly good at some programming language. The most important
thing however, is that you have to want to learn it! It's is next to impossible to learn
anything if you don't want to learn!
If you look at your computer screen (look closer), you'll notice that
it is made up of little colored dots. These dots are called pixels. Each pixel has a
specific color, and location on screen. Now, a simple definition of graphics programming
is to make sure that right pixels appear in right places at right times. If we can make
this happen, we can consider ourselves graphics programmers.
The above definition is easier said than done. Keeping track of each
pixel is next to impossible without some well designed algorithms. And that's what most of
this document is all about, algorithms!
Code examples will mostly be in Java, since it's the only system
independent language capable of graphics. I will however, illustrate some things in C/C++
as well.
The Basics (getting it going)...
Before we get too deep into the topic of graphics programming, I'd like
to illustrate a few basics. If you already know how to set pixels on the screen, change
video mode, etc., then you can simply skip this section. This section is here so that you
know how to experiment. I don't just want to give you an algorithm to draw a line, with no
method of actually drawing it.
These next few sections may be system dependent, hardware dependent,
compiler dependent, etc. But that's the trouble with most graphics, it's next to
impossible to create 100% system independent graphics (excluding Java).
Ploting Pixels...
Your computer screen is two dimensional; each pixel on the screen has
some location, illustrated by the x , and y values. Where x
is the horizontal offset from the left side of the screen, and y is the
vertical offset from the top (or sometimes the bottom), of the screen. So, a location of x
= 0 , and y = 0 , is the top left corner of your screen. Knowing about
the x , and y , you can think of your screen as a two dimensional
array. With x , horizontal locations and y vertical locations.
Each value inside the array represents a pixel. To plot a pixel on the screen at say x
= 70 and y = 100 , you'd put pixel information into that two
dimensional array, at location [100][70] .
Now, lets make it even more abstract! Think of your screen as a one
dimensional array, kind of like starting at the upper left corner going right till end of
line, and starting again on the other line, and continuing like that until it hits the
lower right corner. To use this approach to plot pixels, we'll also need to know the width
of the screen. Lukily for us, most of the time we'll know the width and height of the
screen exactly. For example, lets name our one dimentional array "screen ",
so, to plot a pixel of color "color " at location x = 70 ,
and y = 100 , we'd do something like this: screen[y * width + x] = color .
As you can see (or trace), it is exactly the same thing as screen[y][x] = color ,
only we're specifying the width manually.
Most of the times, when we work with graphics, we're working with a
pointer. A pointer can be to memory, or directly to a video device. Our functions that
"do graphics" don't have to be concerned with where the pointer points, we just
need to draw pixels into that pointer (or it's offsets) as though it was a real screen.
I'll show later where drawing to memory can be very useful.
DOS Graphics...
Modern day Operating Systems have outlawed drawing directly to devices.
However, it is still possible to do in Real Mode DOS programs (and with a little trickery,
under Protected Mode as well). For the time being, assume we're working in Real Mode DOS,
using Borland C++ v3.1 (or DOS version of Turbo C++). No, windows compilers will not work!
Before we can do anything graphical however, we need to set the
graphics mode. We do this by calling BIOS. The function to change the mode is AH =
00h , AL = mode , using a 0x10 interrupt. If you are not
familiar with assembler or low level programming, don't worry; most things here are fairly
easy to understand. A C language function to set the video mode in DOS is:
typedef unsigned char byte_t;
void vid_mode(byte_t mode){
union REGS regs;
regs.h.ah = 0;
regs.h.al = mode;
int86(0x10,®s,®s);
}
As you see, I've defined a special type byte_t , which is
exactly the same thing as unsigned char . Sometimes, it helps to abstract
things like that... I'm also using union REGS to work with CPU registers. The
REGS union is defined inside the <dos.h > file. I'm setting
regs.h.ah to 0 , and regs.h.al to the video mode
I'd like to switch to, and then calling int 0x10 . It is worth mentioning that
this code will only work in DOS. And now, for a simple example using the code above. (code
for DOS version of Borland C++ v3.1 follows)
#include <stdlib.h>
#include <conio.h>
#include <dos.h>
typedef unsigned char byte_t;
byte_t far* video_buffer = (byte_t far*)0xA0000000;
void vid_mode(byte_t mode){
union REGS regs;
regs.h.ah = 0;
regs.h.al = mode;
int86(0x10,®s,®s);
}
void set_pixel(int x,int y,byte_t color){
video_buffer[y * 320 + x] = color;
}
int main(){
vid_mode(0x13);
while(!kbhit())
set_pixel(rand()%320,rand()%200,rand()%256);
vid_mode(0x03);
return 0;
}
Remember all the stuff I've said about working with a pointer? Well,
this code illustrates that as well. Under DOS (using a VGA), we can address the screen
directly, and that "special address" is 0xA0000000 for video mode 13h ,
which in my opinion the easiest video mode to work with.
We define a pointer video_buffer , to point directly to 0xA0000000 ,
and the set_pixel(int, int, byte_t) function simply plots the color at
appropriate location on the screen. The video resolution of mode 13h is 320x200 .
Meaning, that the width of the screen is 320 pixels wide, and 200
pixels high. So, as you can see, our plot pixel function multiplies the y
coordinate by the width, and adds x , and later setting that location to
color.
It is important to note that you wouldn't want to plot pixels outside
the 320x200 range, since if you do, you can easily erase parts of DOS, and
crash your computer. (Of course, most of the "modern day" Operating Systems
won't crash, and will simply kick your program out of the system.)
Inside our main() , we're first setting the mode to 13h ,
then we loop until a key is pressed, drawing random pixels at random locations on the
screen. At the end, we simply return to the DOS video mode, which is nicely defined to be 0x03 .
0x03 is a text based mode (it displays text), with text resolution of 80x25 .
This 0x03 mode is a 16 color mode, while the 0x13
mode is a 256 color mode.
Most of the graphics people work with is in 256 colors,
but there seems to be a trend moving towards 16 bit color (that's 65535
colors).
Now, a bit about "convention," and speed. Using the video_buffer
pointer to plot pixels is slower than using other pointers to other memory. This is due to
the fact that when you plot pixels into video memory, the system performs memory mapped
I/O, so, most of the time, we'd like to avoid that. The common way to avoid it is to plot
pixels into regular memory, and later "blitting" (quickly copying) that memory
onto the screen. This reduces flicker and increases overall speed of the process.
We do this by first allocating enough memory for the whole screen and
clearing that memory. We then write to that memory, and blit (copy it) it to the video
memory. This approach is commonly know as double buffering.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <dos.h>
#include <string.h>
#include <alloc.h>
typedef unsigned char byte_t;
byte_t far* video_buffer = (byte_t far*)0xA0000000;
void vid_mode(byte_t mode){
union REGS regs;
regs.h.ah = 0;
regs.h.al = mode;
int86(0x10,®s,®s);
}
void blit(byte_t far* what){
_fmemcpy(video_buffer,what,320*200);
}
int main(){
int x,y;
byte_t far* double_buffer;
double_buffer = (byte_t far*)farmalloc((unsigned long)320*200);
if(double_buffer == NULL){
printf("sorry, not enough memory.\n");
return 1;
}
_fmemset(double_buffer,0,(unsigned long)320*200);
vid_mode(0x13);
while(!kbhit()){
x = rand()%320;
y = rand()%200;
double_buffer[y * 320 + x] = (byte_t)(rand()%256);
blit(double_buffer);
}
vid_mode(0x03);
farfree(double_buffer);
return 0;
}
The code above first allocates a memory buffer, the size of the screen,
it then checks to see if we actually did allocate memory. Error checking in real mode
memory allocation is VERY important, since there isn't much memory to go around. While
testing the above program, I must have gotten "sorry, not enough memory" message
dozens of times, and I have 64 megs of ram! Real Mode only uses the 640k
of DOS memory.
We then continue by clearing the memory we've allocated. This is
important, since it DOES need clearing (you can try without it, and see what you get). We
then fall into the !kbhit() loop, create random x , and y ,
and plot a random pixel in double_buffer at these random x , and y .
We then call blit(double_buffer) , which copies the double_buffer
to video_buffer , which in turn is set to 0xA0000000 .
You'll also notice that this program is much slower than the one
before. This is not because we're using the double buffer, but because we're copying it
too much. Every time though the hoop, we're blitting. Most of the times, we'll be drawing
a whole lot to the double_buffer (not just one pixel), and blitting as rarely
as possible.
One little note about the program before we go on. To make it work,
you'll need to compile it under at least a medium memory model . If you don't,
it will not be able to allocate memory using farmalloc() . In Borland
C++ v3.1 , you go into Options| Compiler| Code Generation| and select
the Medium Model .
The Palette...
Working with 256 colors may seem restrictive, but most of
the time, it's not. The reason is because the restriction is only on the number of colors
that can be displayed at once. The VGA is actually capable of displaying 262,144
colors. The trick to this is that the pixels we're plotting into the video memory, are not
exactly pixels, they're references into a color look-up table. This color look-up
table is referred to as the palette, and is made up of 3 bytes for each
reference. The 3 bytes are Red , Green , and Blue ,
(RGB ) respectively. So, to plot a definite red pixel at x
= 100 , and y = 100 , we'd set, for example, 50th element
in this color look-up table to 0xFF , 0x00 , 0x00 ,
(this is RGB , where R <red> is 0xFF , B
<blue> is 0x00 , and B <blue> is 0x00 ).
This should make pixel value of 50 , red . So, we then plot a
value of 50 into memory location x = 100 , and y = 100 ,
and we have a red pixel at that location.
This may sound complicated, but it's not. Most of the time, we set the
palette once, and work with a fixed palette throughout. Before we can set that
"one" palette, we need to create it (or use a "standard" one). For
this document, we'll mostly use a nicely defined "standard" Windows palette.
(I've created a BMP file with standard windows colors, saved it, and later extracted the
palette out of it using my own program.)
There are operations on the palette. We can set one index in the color
look-up table to a specific 3 byte entry, or we can retrieve these 3
bytes from the specific index in the palette. There is a catch under VGA however. The
VGA's palette is made up of three 6 bit entries (not 8 bit
entries as a byte would suggest). So, 6 * 3 = 18 , and 2^18 is 262,144 .
And that's where the maximum number of colors comes in. Where it possible to store a byte,
we'd have 8 * 3 = 24 , and 2^24 is 16,777,216 ;
that's a lot more colors than the standard VGA. Most non-VGA approaches support a byte (8
bits) based palette (a good example is JPEG images with 24 bit
color, they're not exactly palette based, but RGB based).
From the information you have, you can already calculate the size of
the 256 color palette. It's 3 bytes per pixel, 256
different pixels, so, 3 * 256 = 768 ! And in fact, 768 is the
size of most 256 color palettes, including the one in PCX files.
Sometimes, however, they store 4 bytes per pixel for speed efficiency (as in BMP
files). The last byte is not used. Most times the palette order is RGB , but
sometimes, it's BGR (again, as in BMP files) or something even
more abstract... There are many conventions for this; you only need to know the basic few,
and most you'll be able to figure our pretty easily if you have the basic understanding of
the concept.
Now, lets write a program that reads and sets the palette, and does
some cool effects simply by playing around with the palette (without actually drawing
anything inside the main loop).
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <dos.h>
#include <string.h>
#include <alloc.h>
typedef unsigned char byte_t;
byte_t far* video_buffer = (byte_t far*)0xA0000000;
void vid_mode(byte_t mode){
union REGS regs;
regs.h.ah = 0;
regs.h.al = mode;
int86(0x10,®s,®s);
}
void blit(byte_t far* what){
_fmemcpy(video_buffer,what,320*200);
}
void setpalette(byte_t c,byte_t r,byte_t g,byte_t b){
outp(0x3c6,0xff);
outp(0x3c8,c);
outp(0x3c9,r);
outp(0x3c9,g);
outp(0x3c9,b);
}
void getpalette(byte_t c,byte_t* r,byte_t* g,byte_t* b){
outp(0x3c6,0xff);
outp(0x3c7,c);
*r = inp(0x3c9);
*g = inp(0x3c9);
*b = inp(0x3c9);
}
void setpalette_all(byte_t palette[256][3]){
int i;
for(i=0;i<256;i++)
setpalette(i,palette[i][0]>>2,palette[i][1]>>2,
palette[i][2]>>2);
}
void vline(int x,int y1,int y2,byte_t color,byte_t far* where){
for(;y1 <= y2;y1++)
where[y1*320+x] = color;
}
void movepalette(){
byte_t r,g,b,r1,g1,b1;
int i;
getpalette(0,&r1,&g1,&b1);
for(i=0;i<255;i++){
getpalette(i+1,&r,&g,&b);
setpalette(i,r,g,b);
}
setpalette(255,r1,g1,b1);
}
int main(){
FILE* input;
int i;
byte_t palette[256][3];
byte_t far* double_buffer;
double_buffer =
(byte_t far*)farmalloc((unsigned long)320*200);
if(double_buffer == NULL){
printf("sorry, not enough memory.\n");
return 1;
}
_fmemset(double_buffer,0,(unsigned long)320*200);
input = fopen("palette.dat","rb");
if(input == NULL){
printf("can't read file...\n");
exit(1);
}
if(fread(palette,sizeof(palette),1,input) != 1){
printf("can't read file...\n");
exit(1);
}
fclose(input);
vid_mode(0x13);
setpalette_all(palette);
for(i=0;i<320;i++)
vline(i,0,199,i%256,double_buffer);
blit(double_buffer);
while(!kbhit())
movepalette();
vid_mode(0x03);
farfree(double_buffer);
return 0;
}
If you look closely at the source, you'll see that it's just an
extension of the old source. The extensions are functions that allow us to manipulate the
palette. We have a setpalette() function, and a getpalette()
function. These set and get the specified palette value respectively. Inside the main() ,
we read our palette.dat file, which is Windows default palette. We then set
this palette. Notice that inside our setpalette() function, we're shifting
the palette value by 2 . This is because the actual number of bits a standard VGA
palette has is 6 (as mentioned earlier). So, we shift by 2 , to
eliminate the insignificant two bits, and preserve the significant ones.
The vline() function draws a vertical line. This is not
the best way to draw lines, and we'll discuss lots of different line drawing algorithms
when we're done with this part. We then blit() the double_buffer
to the screen, and start playing around with the palette. Notice that we're not writing
anything to video memory, and yet, the whole screen is moving (that's the beauty of
palettes).
I guess if you go over the above program, you'll know (and figure out)
quite a lot about palettes. So, don't hesitate, and go over it! (definitely compile it,
and see the effect) The final distribution of this document will also include the palette
file as well, but if you don't have it, you can simply create your own palette. Something
in the lines of:
for(i=0;i<256;i++){
palette[i][0] = palette[i][1] = palette[i][2] = i;
}
Or you can play around with for loops, so see what looks
best. That's it for the basics of getting the graphics working under Real Mode DOS under
Borland C++ v3.1!
Protected Mode Graphics...
Graphics under Real Mode DOS are nice, but sometimes, there are just
too many restrictions in plain vanilla DOS. Most notable of these restrictions is the
drastic limitations on memory. (sometimes, I want my programs to use much more than just 640k
of RAM!) Not to mention the anoyance of dealing with far pointers! Well, you
get the picture, doing anything serious in Real Mode DOS is hell!
To the rescue, comes the safe and cool 32 bit Protected Mode. The
compiler we'll be using in this part is DJGPP v2. It is a FREE 32 bit Protected Mode
compiler for DOS. Don't let that "DOS" throw you off track, DJGPP works
perfectly under Win95/98, and WinNT as well. Although you can't compile anything using
Win32 API, or MFC, you can, however, write DOS based programs that are very powerful and
cool looking, and use as much memory as they need (up to 4 gigabytes of RAM if
needed).
You can download DJGPP from http://www.delorie.com,
simply click on the DJGPP link, and follow download instructions. If you have any problems
with the setup, review the FAQ which is also available at that site (or e-mail me). There
is also a tool called RHIDE, which nicely simulates Borland's IDE (so, you can feel right
at home when you start using DJGPP).
Assuming you've downloaded (and installed) DJGPP, lets now port our
Real Mode DOS program from the previous section to 32 bit Protected Mode DOS, using DJGPP.
This may sound more complex than it really is.
Since most of the source is in C, we dont' have to worry about most of
it. The parts we do have to worry about are the memory writing parts. Because now we're in
protected mode, we can't just write to 0xA0000000 , since that memory is
protected (just as every other memory in protected mode). We have several options however,
we can use a DJGPP specific system call to copy the double buffer to video memory
(replacing our blit() function), or we can temporarily disable memory
protection, write to the screen, and then enable memory protection.
There are advantages and disadvantages to both! Using a system call to
copy memory is safe and compatible with many system, including DOS/Win95/98/NT (yes, it
works under NT4!), but it's too slow! Disabling memory protection, copying, and later
enabling memory protection is not very compatible (nor safe), but it's a lot faster. This
second approach works under DOS/Win95/98, but NOT under WinNT! Well, lets write the code
that gives us the option of using both of these approaches, and I'll let you pick the one
you want to use...
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <string.h>
#include <conio.h>
typedef unsigned char byte_t;
void vid_mode(byte_t mode){
union REGS regs;
regs.h.ah = 0;
regs.h.al = mode;
int86(0x10,®s,®s);
}
void blit0(byte_t* what){
dosmemput(what,320*200,0xA0000);
}
#include <sys/nearptr.h>
/* VERY unsafe, but fast! */
void blit1(byte_t* what){
/* disable memory protection */
__djgpp_nearptr_enable();
/* write to memory */
memcpy((void*)(0xA0000+__djgpp_conventional_base),
what,320*200);
/* enable memory protection */
__djgpp_nearptr_disable();
}
void setpalette(byte_t c,byte_t r,byte_t g,byte_t b){
outp(0x3c6,0xff);
outp(0x3c8,c);
outp(0x3c9,r);
outp(0x3c9,g);
outp(0x3c9,b);
}
void getpalette(byte_t c,byte_t* r,byte_t* g,byte_t* b){
outp(0x3c6,0xff);
outp(0x3c7,c);
*r = inp(0x3c9);
*g = inp(0x3c9);
*b = inp(0x3c9);
}
void setpalette_all(byte_t palette[256][3]){
int i;
for(i=0;i<256;i++)
setpalette(i,palette[i][0]>>2,palette[i][1]>>2,
palette[i][2]>>2);
}
void vline(int x,int y1,int y2,byte_t color,byte_t* where){
for(;y1 <= y2;y1++)
where[y1*320+x] = color;
}
void movepalette(){
byte_t r,g,b,r1,g1,b1;
int i;
getpalette(0,&r1,&g1,&b1);
for(i=0;i<255;i++){
getpalette(i+1,&r,&g,&b);
setpalette(i,r,g,b);
}
setpalette(255,r1,g1,b1);
}
int main(){
FILE* input;
int i;
byte_t palette[256][3];
byte_t* double_buffer;
double_buffer = (byte_t*)malloc(320*200);
if(double_buffer == NULL){
printf("sorry, not enough memory.\n");
return 1;
}
memset(double_buffer,0,320*200);
input = fopen("palette.dat","rb");
if(input == NULL){
printf("can't read file...\n");
exit(1);
}
if(fread(palette,sizeof(palette),1,input) != 1){
printf("can't read file...\n");
exit(1);
}
fclose(input);
vid_mode(0x13);
setpalette_all(palette);
for(i=0;i<320;i++)
vline(i,0,199,i%256,double_buffer);
blit0(double_buffer);
while(!kbhit())
movepalette();
vid_mode(0x03);
free(double_buffer);
return 0;
}
As you can see, the code didn't change much. Actually, only memory
handling changed. The farmalloc() became a simple old malloc() , farfree()
turned into free() , etc. The main attention should be towards the blit#()
functions... There are currently two of them. (I'm sure there are many more approaches to
write to video memory in protected mode, one that's worth looking into on your own is _farnspokel()
defined in <sys/farptr.h> file.)
The blit0() is the simple and easy one. It simply calls
the system to perform the copy, and that's it. What you should notice is that we're
copying to 0xA0000 , that's because this is the physical memory of
the screen (notice four 0s). In protected mode, we don't have segments or offsets as in
real mode (actually, there are segments and offsets in protected mode, but they have
different meaning there), so, in protected mode we use a full physical address
whenever we need to address a memory mapped device such as the VGA.
The blit1() is the more complicated one. It first calls __djgpp_nearptr_enable() ,
which effectively disables memory protection. The function does no error checking to look
at return value of __djgpp_nearptr_enable() . Once memory protection is gone,
we can simply write anywhere (including the screen). (or overwrite the Operating System if
we wished <or had a bug that did it>!) We use regular memcpy() to copy
the buffer to the screen, which is the 0xA0000 , from a __djgpp_conventional_base
(conventional memory base) offset. We then continue by enabling memory protection by
calling __djgpp_nearptr_disable() .
As it is, this method is not much faster than the one before. The
reason is that functions that enable and disable memory protection are quite slow. So,
*usually,* once you've gotten past the debug stage of your project, you'll simply disable
memory at the beginning of your program, and enable it at the end. This is VERY unsafe
though, and can lead to some serious problems (including complete loss of everything).
Windows Graphics...
Windows is the future (or so they say), so, apparently, doing graphics
in plain old DOS is not a good idea (or so they say). I still think it's a good idea,
since only in DOS you can isolate yourself from the stupidity of the Operating System, and
worry only about YOUR program, and not how Windows runs your program. In DOS, most of the
times, your program becomes the operating system. Most games ARE operating systems; they
handle the I/O, keyboard, everything! Anyway, it seem Windows is winning the war, and it's
time to learn Windows programming.
One of the hardest things for DOS programmers to comprehend about
Windows is that you're not in charge anymore; Windows is! If your program wants to do
ANYTHING, it has to ask Windows to do it. You can't just go around plotting pixels into
video memory, you have to ask windows to do it for you. Everything in Windows is under the
control of Windows; and your program has to respect this "understanding," or it
will be kicked out of the system.
For this section, you'll need Microsoft Visual C++ v5 Pro (or later).
This section will cover basic Windows graphics, which are widely portable to all Windows
systems (no DirectX stuff).
Every one of the tutorials and books (ones I've seen) start out the
easy way, they say "use a wizard to create a shell MFC program, and go from
there"... well, this document will not do this. I'll write this code in plain
old C, using plain old Win32 API. This is not because I hate MFC (although it is a
factor), but because it doesn't teach you Windows programming. A lot of the so called
Windows programmers only use MFC, and have no idea of the underlying Win32 interface (or
how a Windows program is constructed).
Lets get to the point of actually writing it (and not bashing Microsoft
for the buggy OS). Every Windows program starts out inside a WinMain()
function, not a main() function, as it is the case with most C programs. A
Windows program first creates a window class (a WNDCLASS structure),
registers it, creates a window of that class, and displays that window. Simple?
Then it should create a DIB section to draw into (the double buffer)
(the DIB stands for Device Independent Bitmap). This is done by first setting elements of BITMAPINFO
structure, creating and realizing the palette, etc., and then calling CreateDIBSection()
which gives us the double buffer to draw into.
After all that, we also need a blit() function! This is
harder than it is in DOS! We start by creating a device context, by calling CreateCompatibleDC() .
Selecting our HBITMAP (bitmap handler) for that context. Blitting using BitBlt()
function (which is quite fast most of the times), and returning settings to normal (doing
clean up, etc.).
The program runs inside a while loop (in WinMain()
function), which loops until the program receives the WM_QUIT message. Now is
a good time to remember that everything in Windows is done using messages ;-) Our program
will receive this message whenever we try to destroy the application's window. (since
we're calling PostQuitMessage() function whenever WndProc() gets
a WM_DESTROY message.)
This may be more complex than in DOS, and it is IMHO! But that's how it
is and there is very little you or me can do about it. Now, shall I leave you to figure
out the details of all this, or shall I give you the source as an example? (you'd be
surprised how many books "leave out the details" of actual implementation of
something)
#include <windows.h>
typedef BYTE byte_t;
#define W_WIDTH 320
#define W_HEIGHT 240
struct pBITMAPINFO{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[256];
}BMInfo;
struct pLOGPALETTE{
WORD palVersion;
WORD palNumEntries;
PALETTEENTRY palPalEntry[256];
}PalInfo;
HBITMAP hBM;
byte_t* double_buffer;
void blit(void);
void vline(int x,int y1,int y2,byte_t color,byte_t* where){
for(;y1 <= y2;y1++)
where[y1*320+x] = color;
}
void p_update(){
int i;
static unsigned int c=0;
for(i=0;i<W_WIDTH;i++){
vline(i,0,W_HEIGHT-1,(byte_t)(c%256),double_buffer);
c = c > 512 ? 0:c+1;
}
blit();
}
void getpalette(RGBQUAD* p){
int i;
for(i=0;i<256;i++){
p[i].rgbRed = i;
p[i].rgbGreen = i;
p[i].rgbBlue = i;
p[i].rgbReserved = 0;
}
}
void init_double_buffer(){
HPALETTE PalHan;
HWND ActiveWindow;
HDC hDC;
RGBQUAD palette[256];
int i;
ActiveWindow = GetActiveWindow();
hDC = GetDC(ActiveWindow);
BMInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
BMInfo.bmiHeader.biWidth = W_WIDTH;
BMInfo.bmiHeader.biHeight = -abs(W_HEIGHT);
BMInfo.bmiHeader.biPlanes = 1;
BMInfo.bmiHeader.biBitCount = 8;
BMInfo.bmiHeader.biCompression = BI_RGB;
BMInfo.bmiHeader.biSizeImage = 0;
BMInfo.bmiHeader.biXPelsPerMeter = 0;
BMInfo.bmiHeader.biYPelsPerMeter = 0;
BMInfo.bmiHeader.biClrUsed = 256;
BMInfo.bmiHeader.biClrImportant = 256;
getpalette(palette);
for(i=0;i<256;i++)
BMInfo.bmiColors[i] = palette[i];
PalInfo.palVersion = 0x300;
PalInfo.palNumEntries = 256;
for(i=0;i<256;i++){
PalInfo.palPalEntry[i].peRed = palette[i].rgbRed;
PalInfo.palPalEntry[i].peGreen = palette[i].rgbGreen;
PalInfo.palPalEntry[i].peBlue = palette[i].rgbBlue;
PalInfo.palPalEntry[i].peFlags = PC_NOCOLLAPSE;
}
/* create the palette */
PalHan = CreatePalette((LOGPALETTE*)&PalInfo);
/* select it for that DC */
SelectPalette(hDC,PalHan,FALSE);
/* realize a palette on that DC */
RealizePalette(hDC);
/* delete palette handler */
DeleteObject(PalHan);
hBM = CreateDIBSection(hDC,(BITMAPINFO*)&BMInfo,
DIB_RGB_COLORS,(void**)&double_buffer,0,0);
ReleaseDC(ActiveWindow,hDC);
}
void blit(){
HWND ActiveWindow;
HDC hDC,Context;
RECT Dest;
HBITMAP DefaultBitmap;
if((ActiveWindow = GetActiveWindow()) == NULL)
return;
hDC = GetDC(ActiveWindow);
GetClientRect(ActiveWindow,&Dest);
Context = CreateCompatibleDC(0);
DefaultBitmap = SelectObject(Context,hBM);
BitBlt(hDC,0,0,Dest.right,Dest.bottom,Context,0,0,SRCCOPY);
SelectObject(Context,DefaultBitmap);
DeleteDC(Context);
DeleteObject(DefaultBitmap);
ReleaseDC(ActiveWindow,hDC);
}
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,
WPARAM wParam,LPARAM lParam){
switch(iMessage){
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd,iMessage,wParam,lParam);
}
}
int WINAPI WinMain(HANDLE hInstance,HANDLE hPrevInstance,
LPSTR lpszCmdParam,int nCmdShow){
WNDCLASS WndClass;
char szAppName[] = "CrazyWindow";
HWND hWnd;
MSG msg;
BOOL running = TRUE;
WndClass.style = CS_HREDRAW|CS_VREDRAW|CS_OWNDC;
WndClass.lpfnWndProc = WndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hbrBackground = GetStockObject(BLACK_BRUSH);
WndClass.hIcon = LoadIcon(hInstance,NULL);
WndClass.hCursor = LoadCursor(NULL,IDC_ARROW);
WndClass.hInstance = hInstance;
WndClass.lpszClassName = szAppName;
WndClass.lpszMenuName = 0;
RegisterClass(&WndClass);
hWnd = CreateWindow(szAppName,szAppName,
WS_CAPTION|WS_MINIMIZEBOX|WS_SYSMENU,
CW_USEDEFAULT,CW_USEDEFAULT,W_WIDTH,W_HEIGHT,
0,0,hInstance,0);
ShowWindow(hWnd,nCmdShow);
init_double_buffer();
while(running == TRUE){
p_update();
Sleep(250);
if(PeekMessage(&msg,0,0,0,PM_NOREMOVE) == TRUE){
running = GetMessage(&msg,0,0,0);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
Well, don't expect a huge explanation of all this stuff here; after
all, this is not a Windows programming tutorial. If you have problems understanding the
above code, I suggest you get a good Windows Programming book (although they're pretty
hard to come by). I'm hoping that if you have the compiler (Visual C++ v5 Pro) to compile
this source, you'll be somewhat comfortable with Windows stuff... (And have a MSDN CD,
which has TONS of useful stuff about Windows, and references to all the functions and
variables used in this program.)
Besides, I think I've made it as simple as it can possibly get!
DirectX Graphics...
For somebody who've never programmed anything using DirectX, it can be
mind boggling to actually try to understand all this stuff. It is complex (there is too
much of it)! Think of the entire DirectX as a collection of libraries for all kinds of
"game" related stuff (sound, graphics, input, etc.) The part we'll be dealing
with in this section is DirectDraw. We'll be using it to enter a full screen 256 color,
double buffered, mode under Windows, and draw stuff to the screen.
You should know that DirectX is not too portable (at least not at the
time of this writing). A lot of the DirectX programs simply don't run under WinNT4! (and
ones that do run, only support some limited video modes, and not others) Then, there are
system that still don't have DirectX installed! (like my Win95 machine at home) All this
is about to change however, NT5 and Win98 both come with DirectX 5 support, and by the
time NT5 ships, it will probably support DX6.
To write anything using DirectX however, you need to have it! So, if
you haven't already, go to the Microsoft web-site, and download DirectX SDK (or order it
on a CD-ROM) (it's quite large!). The place to start looking for it is: http://www.microsoft.com/directx. If you have
one of later versions of Visual C++, chances are, that some version of DirectX is already
hanging around your hard disk (or at least the library files to link to) (and not the
examples, etc.)
Before we start, I'd like to point out a few things. Although DirectX
programs run only under Windows, their power is still in their full screen modes. (so,
don't expect to be writing a windowed application using DirectX any time soon...) If you
need your graphics program to run in a window, use the approach described earlier in a Windows
Graphics section.
In DirectX, most of the time, we're dealing with a thing called a
Surface. We draw to surfaces, etc. Before we can do anything however, we need to create a
DirectDraw object. Then we specify whether we'd like to run full screen, etc. Then we
create the primary surface (think of primary surface as kind of
a video_buffer in our previous examples), and then we create a back
surface (think of back surface as kind of a double_buffer ). To
draw, we first get a memory pointer the back surface (locking the surface),
we draw to that pointer, and later unlock the surface (and do something which is kind of
like the blit() function...)
The explanation above is too simplistic! The code below is a bit more
detailed.
#include <windows.h>
#include <ddraw.h>
typedef BYTE byte_t;
#define W_WIDTH 640
#define W_HEIGHT 480
int b_pitch;
byte_t *double_buffer;
HWND hWnd;
PALETTEENTRY palette[256];
DDSURFACEDESC ddsd;
LPDIRECTDRAW lpDD;
LPDIRECTDRAWSURFACE lpDDSPrimary;
LPDIRECTDRAWSURFACE lpDDSBack;
LPDIRECTDRAWPALETTE lpDDPal;
void big_error(char* string){
MessageBox(hWnd,string,"big & bad error",
MB_OK|MB_ICONEXCLAMATION|MB_APPLMODAL);
ExitProcess(1);
}
void init_directdraw(){
DDSURFACEDESC ddsd_tmp;
HRESULT ddrval;
ddrval = DirectDrawCreate(NULL,&lpDD,NULL);
if(ddrval != DD_OK)
big_error("couldn't init DirectDraw!");
ddrval = IDirectDraw_SetCooperativeLevel(lpDD,hWnd,
DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN|DDSCL_ALLOWREBOOT);
if(ddrval != DD_OK)
big_error("couldn't set DirectDraw exclusive mode!");
ddrval = IDirectDraw_SetDisplayMode(lpDD,W_WIDTH,W_HEIGHT,8);
if(ddrval != DD_OK)
big_error("couldn't set DirectDraw display mode!");
ddsd_tmp.dwSize = sizeof(ddsd_tmp);
ddsd_tmp.dwFlags = DDSD_CAPS;
ddsd_tmp.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
ddrval = IDirectDraw_CreateSurface(lpDD,&ddsd_tmp,
&lpDDSPrimary,NULL);
if(ddrval != DD_OK)
big_error("couldn't create primary surface!");
ddsd_tmp.dwSize = sizeof(ddsd_tmp);
ddsd_tmp.dwFlags = DDSD_CAPS |DDSD_WIDTH|DDSD_HEIGHT;
ddsd_tmp.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN|DDSCAPS_SYSTEMMEMORY;
ddsd_tmp.dwWidth = W_WIDTH;
ddsd_tmp.dwHeight = W_HEIGHT;
ddrval = IDirectDraw_CreateSurface(lpDD,&ddsd_tmp,
&lpDDSBack,NULL);
if(ddrval != DD_OK)
big_error("couldn't create back surface!");
}
void kill_directdraw(){
if(lpDD != NULL){
if(lpDDSPrimary != NULL){
IDirectDrawSurface_Release(lpDDSPrimary);
lpDDSPrimary = NULL;
}
if(lpDDSBack != NULL){
IDirectDrawSurface_Release(lpDDSBack);
lpDDSBack = NULL;
}
if(lpDDPal != NULL){
IDirectDrawPalette_Release(lpDDPal);
lpDDPal = NULL;
}
IDirectDraw_Release(lpDD);
lpDD = NULL;
}
}
void set_palette(RGBQUAD* pal){
PALETTEENTRY black = {0,0,0,0},white = {0xFF,0xFF,0xFF,0};
int i;
if(pal != NULL){
for(i=1;i<0xFF;i++){
pal++;
palette[i].peRed = pal[i].rgbRed;
palette[i].peGreen = pal[i].rgbGreen;
palette[i].peBlue = pal[i].rgbBlue;
palette[i].peFlags = (byte_t)0;
}
}else{
for(i=1;i<0xFF;i++){
palette[i].peRed = i;
palette[i].peGreen = i;
palette[i].peBlue = i;
palette[i].peFlags = (byte_t)0;
}
}
palette[0] = black;
palette[0xFF] = white;
IDirectDraw_CreatePalette(lpDD,DDPCAPS_8BIT,palette,
&lpDDPal,NULL);
if(lpDDPal != NULL)
IDirectDrawSurface_SetPalette(lpDDSPrimary,lpDDPal);
}
BOOL lock_surface(){
HRESULT ddrval;
ddsd.dwSize = sizeof(ddsd);
for(;;){
ddrval = IDirectDrawSurface_Lock(lpDDSBack,NULL,
&ddsd,DDLOCK_WAIT,NULL);
if(ddrval == DD_OK)
break;
if(ddrval == DDERR_SURFACELOST){
ddrval = IDirectDrawSurface_Restore(lpDDSPrimary);
if(ddrval == DD_OK)
return FALSE;
}
if(ddrval != DDERR_WASSTILLDRAWING)
return FALSE;
}
double_buffer = (byte_t*)(ddsd.lpSurface);
b_pitch = ddsd.lPitch;
return TRUE;
}
void unlock_surface(){
HRESULT ddrval;
RECT rc = {0,0,W_WIDTH,W_HEIGHT};
double_buffer = NULL;
for(;;){
ddrval = IDirectDrawSurface_Unlock(lpDDSBack,
ddsd.lpSurface);
if(ddrval == DD_OK)
break;
if(ddrval == DDERR_SURFACELOST){
ddrval = IDirectDrawSurface_Restore(lpDDSPrimary);
if(ddrval != DD_OK)
return;
}
if(ddrval != DDERR_WASSTILLDRAWING)
return;
}
for(;;){
ddrval = IDirectDrawSurface_BltFast(lpDDSPrimary,0,0,
lpDDSBack,&rc,DDBLTFAST_NOCOLORKEY);
if(ddrval == DD_OK)
break;
if(ddrval == DDERR_SURFACELOST){
ddrval = IDirectDrawSurface_Restore(lpDDSPrimary);
if(ddrval != DD_OK)
return;
}
if(ddrval != DDERR_WASSTILLDRAWING)
return;
}
}
void vline(int x,int y1,int y2,byte_t color,byte_t* where){
for(;y1 <= y2;y1++)
where[y1*b_pitch+x] = color;
}
void p_update(){
int i;
static unsigned int c=0;
if(lock_surface()){
for(i=0;i<W_WIDTH;i++){
vline(i,0,W_HEIGHT-1,(byte_t)(c%256),double_buffer);
c = c > 512 ? 0:c+1;
}
unlock_surface();
}
}
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,
WPARAM wParam,LPARAM lParam){
switch(iMessage){
case WM_DESTROY:
case WM_LBUTTONDOWN:
case WM_KEYDOWN:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd,iMessage,wParam,lParam);
}
}
int WINAPI WinMain(HANDLE hInstance,HANDLE hPrevInstance,
LPSTR lpszCmdParam,int nCmdShow){
WNDCLASS WndClass;
char szAppName[] = "CrazyWindow";
MSG msg;
BOOL running = TRUE;
WndClass.style = CS_HREDRAW|CS_VREDRAW|CS_OWNDC;
WndClass.lpfnWndProc = WndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hbrBackground = GetStockObject(BLACK_BRUSH);
WndClass.hIcon = LoadIcon(hInstance,NULL);
WndClass.hCursor = LoadCursor(NULL,IDC_ARROW);
WndClass.hInstance = hInstance;
WndClass.lpszClassName = szAppName;
WndClass.lpszMenuName = 0;
RegisterClass(&WndClass);
hWnd = CreateWindow(szAppName,szAppName,
WS_CAPTION|WS_MINIMIZEBOX|WS_SYSMENU,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
0,0,hInstance,0);
ShowWindow(hWnd,nCmdShow);
init_directdraw();
set_palette(NULL);
while(running == TRUE){
p_update();
Sleep(250);
if(PeekMessage(&msg,0,0,0,PM_NOREMOVE) == TRUE){
running = GetMessage(&msg,0,0,0);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
kill_directdraw();
return msg.wParam;
}
I suggest you go over the source (several times); eventually, you'll
start understanding it. The program does exactly the same effect as the program in the Windows
Graphics section (only full screen!). To compile/link the program, you'll need
DirectX libraries, and link this program with ddraw.lib
Another thing you might consider; using DirectX in your program is a
LOT easier if you're writing your programs using C++. Lots of these long functions, become
fairly short methods of objects. (it's a lot cleaner to use C++ with DirectX) (I just hate
C++ <I actually used to love it a few years back.>, so, I did it this way!)
Notice before we go on: This program works on most Windows platforms,
but this is due to the fact that it's mode 640x480 (standard Windows mode)!
If you set the mode to 320x200 , or to Mode X (which is: 320x240 ),
the program will NOT work under NT4. However, the program will work under Win95/98. To
change these different modes, modify the W_WIDTH and W_HEIGHT
defines at the top of the code. (I really hate it when a popular game doesn't run full
screen at 320x240 under NT4; i.e.: Quake 2 <hey, not all of us have a
Pentium 2, 400Mhz with a Voodoo 2 Accelerator!>) There are approaches to make this
defect go away. You can still use 640x480 mode, and do a stretch upon
blitting. I doubt a 2x stretch would hit the performance too harshly. Anyway,
I hope NT5 will support all these low resolution modes... (I'm definitely not going back
to Win98 just to play games full screen!)
Java Graphics...
Yeah! Java. Most people think Java is too slow and useless for
graphics, and they may be right (I have yet to come across a really really cool graphics
program that outperformed it's C/C++ counterparts.) Nonetheless, Java is the most elegant
Object Oriented language I've seen (C++ is junk compared to Java!).
"Fast graphics in Java" is somewhat an oxymoron. But it is
getting faster, and in some cases can compete with C/C++. There are two ways of doing
graphics in Java, using the libraries, or creating a double buffer to draw into (similar
to what we've been doing in previous sections). Using libraries is a lot easier and in
some cases an easy solution. It's much easier to simply say g.drawLine(0,0,100,100);
to draw a line, than to setup tons of stuff, and write the code for an optimized line
algorithm yourself.
First, we'll worry about the Java graphics library approach, since
we'll probably be using it later to illustrate a few quick algorithms. (It's easy to use!)
Once we're done learning the "library" approach, I'll show you a trick most
books, tutorials, and examples try to avoid. To implement our own ImageProducer ,
and create fairly fast graphics under Java.
For the examples in this section, you'll need a copy of the JDK, which
you can get for free from http://java.sun.com. I suggest
you get the latest version (even if it's beta). Microsoft Visual J++ will also work for
most examples here, but I've found that J++ is very outdated when it comes to Java
libraries, and I think that most people are better off with a plain and simple JDK.
Well, lets get to writing the code! We will write a regular application
which opens a window, and draws tons of random polygons into it. Here it is:
import java.awt.*;
import java.awt.event.*;
public class jDraw extends Frame
implements WindowListener,Runnable{
boolean m_running;
public void windowOpened(WindowEvent e){}
public void windowClosing(WindowEvent e){
m_running = false;
}
public void windowClosed(WindowEvent e){}
public void windowIconified(WindowEvent e){}
public void windowDeiconified(WindowEvent e){}
public void windowActivated(WindowEvent e){}
public void windowDeactivated(WindowEvent e){}
public jDraw(String name){
super(name);
setSize(320,240);
setResizable(false);
addWindowListener(this);
show();
}
public void paint(Graphics g){
int n = (int)(Math.random()*8);
int[] x = new int[n];
int[] y = new int[n];
for(int i=0;i<n;i++){
x[i] = (int)(Math.random()*getWidth());
y[i] = (int)(Math.random()*getHeight());
}
g.setColor(new Color((int)(Math.random()*0xFFFFFF)));
g.fillPolygon(x,y,n);
}
public void update(Graphics g){
paint(g);
}
public void run(){
m_running = true;
while(m_running){
repaint();
try{
Thread.currentThread().sleep(50);
}catch(InterruptedException e){
System.out.println(e);
}
}
}
public static void main(String[] args){
Thread theApp = new Thread(new jDraw("Crazy!"));
theApp.start();
try{
theApp.join();
}catch(InterruptedException e){
System.out.println(e);
}
System.exit(0);
}
}
As you can see, the code is fairly simple. We open a window, set it's
size, add a listener, etc. Draw to that window, etc. and that's it! (a lot easier than
previous examples) Not to mention this program is system independent, and will even work
under UNIX as well as Windows. I doubt this program needs much explanation (it's fairly
simple) (trace it if you don't understand it). The reason we don't need a palette in this
example is because the colors are in RGB format. When we create a new Color ,
we're specifying the RGB integer. Now, lets move onto something more complex, our own ImageProdicer !
Right now, it's time to learn how to draw to a double_buffer
under Java! In reality, it's actually going to be a triple buffer, since we'll first setup
a double_buffer for a java.awt.Image , and then (after blitting
our byte[] buffer to the Image ), we'll blit the Image
to the screen. There are more elegant ways of handling that, but I just didn't want to
spend much time on this (Gonna leave on an extended vacation pretty soon, and want to get
as much done as possible ;-)
Later (if I feel like it), I'll show a JDK 1.2 approach to this, which
in my opinion is much better. Unfortunately, it's not as widely compatible as the ImageProducer
one. Well, lets get right to the source!
import java.awt.*;
import java.applet.*;
import java.awt.image.*;
public class jDrawApplet extends Applet
implements Runnable,ImageProducer{
int m_width;
int m_height;
Thread m_thread = null;
Image m_screen = null;
/* ImageProducer variables */
byte[] double_buffer;
ColorModel color_model;
ImageConsumer consumer = null;
/* ImageProducer interface */
public void addConsumer(ImageConsumer ic){
if(consumer != ic){
consumer = ic;
consumer.setDimensions(m_width,m_height);
consumer.setColorModel(color_model);
consumer.setHints(ImageConsumer.TOPDOWNLEFTRIGHT);
}
blit();
}
public boolean isConsumer(ImageConsumer ic){
return ic == consumer;
}
public void removeConsumer(ImageConsumer ic){}
public void startProduction(ImageConsumer ic){
addConsumer(ic);
}
public void requestTopDownLeftRightResend(ImageConsumer ic){}
/* our blit() method */
void blit(){
if(consumer != null)
consumer.setPixels(0,0,m_width,m_height,
color_model,double_buffer,0,m_width);
}
/* our vertial line method ;-) */
void vline(int x,int y1,int y2,byte color,byte[] where){
for(;y1 <= y2;y1++)
where[y1*m_width+x] = color;
}
int c=0;
void do_interesting_stuff(){
for(int i=0;i<m_width;i++){
vline(i,0,m_height-1,(byte)(c%256),double_buffer);
c = c > 512 ? 0:c+1;
}
}
public void paint(Graphics g){
blit();
g.drawImage(m_screen,0,0,null);
}
public void update(Graphics g){
paint(g);
}
/* setup stuff... */
void setupImageProducer(){
byte[] r = new byte[256];
byte[] g = new byte[256];
byte[] b = new byte[256];
for(int i=0;i<256;i++)
r[i] = g[i] = b[i] = (byte)i;
color_model = new IndexColorModel(8,256,r,g,b);
double_buffer = new byte[m_width*m_height];
}
public void init(){
m_width = getSize().width;
m_height = getSize().height;
setupImageProducer();
m_screen = createImage(this);
}
public void start(){
if(m_thread == null){
m_thread = new Thread(this);
m_thread.start();
}
}
public void run(){
while(true){
do_interesting_stuff();
repaint();
try{
m_thread.sleep(250);
}catch(InterruptedException e){
System.out.println(e);
}
}
}
}
Yes, it is an applet! You have to create an HTML file to run it. The
code that goes into that HTML file is:
<html>
<body>
<applet code="jDrawApplet.class" width="320"
height="240"></applet>
</body>
</html>
Ok, lets briefly go over the source, and what it does, etc., and try to
move on. The code starts it's execution inside the init() method, due to the
way java.applet.Applet initializes stuff. Inside init() , we get width
and height , and call setupImageProducer() . Since this whole
class is implementing java.awt.image.ImageProducer , we're simply initializing
class data members inside this setupImageProducer() method. More
specifically, we're creating a gray scale palette (in Java known as a ColorModel ),
and allocate memory for the double_buffer .
ColorModel is not exactly a palette, it's an Object
that describes color in specific "mode". In our 256 color mode,
it's VERY similar to a palette, but in high color modes (most of standard Java stuff), it
has different meanings and purposes.
After our return from setupImageProducer() , we create an Image
with this as the parameter (to createImage() ). This means that
we're creating an Image , using this ImageProducer .
This action, calls addConsumer() method or our ImageProducer ,
giving it a consumer that will let us setPixels() in that ImageConsumer
(the m_screen Image ).
We then fall into the run() method, loop forever, calling repaint() ,
and do_interesting_stuff() . The repaint() method, asynchronously
calls update() , which in turn, calls paint() . Inside the paint()
method, we call blit() , which blits our double_buffer into the m_screen
Image , and then we g.drawImage() the m_screen Image
(another kind of blit() ) to the screen.
Inside the do_interesting_stuff() , we're doing nothing
special that we haven't done before. We're drawing vertical lines to the double_buffer ,
the same way we were doing in the DirectX Graphics demo above.
This approach may seem complicated at first (maybe because I did a bad
job at describing it...), but it's pretty simple. If you think about it, it will start
making a lot of sense.
An example...
We've talked about the fact that most of the time, graphics is simply
plotting pixels to memory. This may not be too dramatic, nor interesting, until you
realize that plotting pixels to memory can produce quite interesting and cool looking
effects. This section is not to teach you any specific algorithm, but just to show you
that simply by plotting pixels into memory, a very small program can achieve very
interesting results. I don't feel that what we've been doing so far is interesting enough
to be inspiring; so, here's where this simple example comes in...
Have you ever played chess? Doesn't matter what your answer is (in
fact, it was meant to be a rhetorical question ;-). Anyway, the examples we're about to
create is going to kind of a chess board (not of standard size), as seen underwater! How
complex do you think the program will be? Well, let me give you the source...
#include <stdlib.h>
#include <conio.h>
#include <math.h>
#include <dos.h>
int sin_x_table[640];
int sin_y_table[400];
void vid_mode(unsigned char mode){
union REGS regs;
regs.h.ah = 0;
regs.h.al = mode;
int86(0x10,®s,®s);
}
void make_chess(unsigned char far* where){
int x,y,tx,ty;
static int xcounter = 0;
static int ycounter = 0;
xcounter = xcounter > 311 ? 0:xcounter+8;
ycounter = ycounter > 195 ? 0:ycounter+4;
for(y=0;y<200;y++)
for(x=0;x<320;x++){
tx = x + sin_y_table[y + ycounter]+40;
ty = y - sin_x_table[x + xcounter]+40;
if((tx % 40 >= 20) ^ (ty % 40 >= 20))
where[y*320+x] = 0;
else where[y*320+x] = 15;
}
}
int main(){
int i;
for(i=0;i<320;i++)
sin_x_table[i] = (int)(sin(i * 6.28/320) * 10);
for(;i<640;i++)
sin_x_table[i] = sin_x_table[i-320];
for(i=0;i<200;i++)
sin_y_table[i] = (int)(sin(i * 6.28/200) * 10);
for(;i<400;i++)
sin_y_table[i] = sin_y_table[i-200];
vid_mode(0x13);
while(!kbhit())
make_chess((unsigned char far*)0xA0000000);
vid_mode(0x03);
return 0;
}
That's it! If you compile and run this program (under Borland C++ v3.1
DOS) (easily portable anywhere, as described in previous sections) you'll see the effect.
And it is very effective, considering that this program is so small.
As you see, most of this program is spent inside the make_chess()
function. It is writing directly to video memory. But as you'll notice, the make_chess()
function doesn't know it's writing to video memory. We can actually replace that pointer
buy something else, and have it do the effect to some buffer!
Another important thing you should notice in the source is the use of sin_x_table[]
and the sin_y_table[] . Most of the time, when we deal with sin()
or cos() in our code, we'd like to avoid these functions (they're VERY
slow!). There are many approaches to avoiding them, and the most common one is to create
these "look-up" tables of these values. Sometimes, it is worth the
effort, but sometimes, considering CPU cache speeds and FPU sin and cos
speeds, it's not worth it. In this example it's definitely worth it, it even works nice
under a 486 (slow FPU processor). Integer math is fast on any processor, so, try to use
that most of the time, and only use floating point (FPU) when you know you can make it
work in parallel with the CPU, thus, getting the FPU calculations for free. (but this
subject is best left to an optimization tutorial)
I guess that's it for this example. I greatly earge you to compile and
run it. A good exercise would be to make this program do the same exact effect only using
some picture (not a chess board). (In fact, we'll probably end up doing it sometime in
this tutorial...)
- Under Construction -
Copyright © 1998, Particle
|