04.03 - Pixel Manipulation

Started by
47 comments, last by Teej 22 years, 3 months ago
Starting With the Basics So far, we’ve looked at our game template code and how it uses DirectDraw to get us into a full-screen mode with our desired resolution and color depth. We then created DirectDrawSurface objects for the primary display (video card), and a backbuffer for our actual drawing. You know, I really like the overhead projector analogy with transparencies (clear plastic sheets). Our primary surface is the transparency currently on the overhead projector, and our backbuffer is an extra sheet on our desk. We’ll draw whatever we want onto this backbuffer, and quickly trade transparencies so that the backbuffer becomes the active display, and we start drawing all over again on the new backbuffer. We’re going to start drawing things on surfaces, and we’re going to start from scratch. What that means is that you take the contents of Game_Main() and clean it out so that it looks like this: void Game_Main() { } You can go ahead and run this program, and as you’d expect, nothing will be displayed. Thankfully the ESC key press is intercepted by Windows and passed to our template code via WindowProc() so that the program can terminate properly. Remember now, this function gets called over and over so long as the program is running. We know that there’s a primary DirectDrawSurface object for the active display and a secondary DirectDrawSurface object that’s our backbuffer. We also know that the plan is to draw onto the backbuffer and flip the two DirectDraw surfaces so that the backbuffer surface becomes the active display surface. Okay, so let’s paint some pixels! Pixel Painting Our first objective is to get our hands on the memory held by the backbuffer DirectDrawSurface object. The online SDK docs tell us that there are two methods for this purpose, Lock() and Unlock() . Okay, so let’s take a look at Lock() : HRESULT Lock ( LPRECT lpDestRect; LPDDSURFACEDESC2 lpDDSurfaceDesc; DWORD dwFlags, HANDLE hEvent ); You really need to have the online SDK docs open in order to be able to fill out method calls like this correctly – especially when flags are involved. We’ll fill in the parameters together, assuming that you are reading along:
  1. We can ask for a particular region of surface memory to lock, but we really want the entire thing, so we use NULL (as per the doc directions).
  2. This method requires a pointer to a valid DDSURFACEDESC2 structure. Lock() will fill in certain fields in this structure for us to use. So, we create a DDSURFACEDESC2 variable on the stack (‘on the stack’ means a local variable) and pass a pointer to it to the Lock() method as our second parameter.
  3. Taking a look at the possible flags in the docs, we see that DDLOCK_SURFACEMEMORYPTR is a must-have. Later in the tutorial we’ll explore the use of other flags, but for now this is all we need.
  4. We are told to use NULL here. No problem.
Here’s the finished product:

DDSURFACEDESC2 ddsd;

// Lock the backbuffer surface
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);

G.lpDDSBack->Lock(NULL, &ddsd, 
                  DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);
  
First, I’ve cleared the DDSURFACEDESC2 structure using ZeroMemory() , which is the same thing as calling memset() – we’re just setting the entire structure to zeros. Then, we’re placing the size of the structure into the structure itself in the dwSize field. This is important, as many DirectX methods use structures as parameters and require us to supply the size in this field. Although it’s not important to us why, it’s worth mentioning that this is the only way a DirectX method can tell what structure we’re giving it is to provide the structure’s size. If you consider the fact that there’s a DDSURFACEDESC structure and a DDSURFACEDESC2 structure available, you can see why telling the difference might be important. And then, we call Lock() with our parameters. All DirectX methods returns values of type HRESULT, but for simplicity’s sake I’m not taking the return values into account for now. Rest assured however that we’ll need to, because so many things can go wrong and if you’re not checking the results of method calls, you get what you deserve in terms of headaches when things don’t work right (or worse yet, crash). There are two fields inside of the DDSURFACEDESC2 structure that we’re interested in, lpSurface and lPitch . With these two fields, we have everything we need to draw onto the backbuffer. Before I get to their use however, I’d like to get the rest of the DirectDraw methods out of the way. Okay, so assuming we’ve done our drawing, we are then supposed to unlock the backbuffer. This is done with Unlock() , which looks like this: HRESULT Unlock ( LPRECT lpRect ); This one’s easy – if you used a RECT in the call to Lock() , give it here as well. For us, we’ve locked the entire buffer, so we simply use NULL here as well: G.lpDDSBack->Unlock(NULL); The last thing we need to do is to actually get the backbuffer onto the active display. The method is called Flip() , and its prototype looks like this: HRESULT Flip ( LPDIRECTDRAWSURFACE7 lpDDSurfaceTargetOverride, DWORD dwFlags ); If you read the paragraph in the SDK docs describing the first parameter, you quickly realize that it’s an advanced feature we don’t need. For us, the default action is to flip the primary and secondary buffer, so we use NULL here. As for the flags, we don’t need any of them, so we’ll use 0. That’s it for DirectDraw calls! If you were to run the program now, you’d still see nothing (maybe some memory garbage), but the template for pixel manipulation is in place. Our last task in Game_Main() now is to actually alter the backbuffer memory using the surface pointer provided by Lock() . Accessing Surface Memory How large do you think the area of memory is that we now have access to? Well, what’s involved in figuring it out? Here’s the criteria:
  • screen width in pixels
  • screen height in pixels
  • color depth (in bits)
The logic is as follows: If I have 10 pixels across and 10 pixels down, I therefore have 100 pixels on the display. If each pixel needs 16 bits (= 2 bytes) of memory, then I’d need 100 pixels * 2 bytes = 200 bytes of memory. In our particular case, we’d like to use a 640x480 screen with 16-bit color, which would make our memory needs 614,400 bytes. For this article, I’m going to assume that your video card can handle 16-bit color. I’m also going to assume that it maps 5 bits for red and blue, and 6 bits for green (the most popular combination). Some video cards prefer to use 5 bits for each color component and use the remaining bit as an alpha component, and there’s a way to check in your code for what type of video card you have, but it’s really outside of the scope of this article. Now, it’s a fact that pointers to areas of memory can also be used as arrays. As a matter of fact, an array name is equivalent to a pointer. What we’d like to do is take our surface memory pointer and use it like an array, with each array element corresponding to a single pixel. This way, pixels would be accessed like this: buffer[0] = color; // First pixel (0,0) buffer[10] = color; // (10,0) So, the first row of pixels in a 640x480 display would be numbered 0-639. Therefore, the first pixel in the second row would be number 640: buffer[640] = color; // (0,1) …and the first pixel in the third row would be 640 + 640 = 1280. Generalizing, we can use the following: int x, y; buffer[y * SCREEN_WIDTH + x] = color; // (x,y) Make sure that the above line makes perfect sense to you. Every time we go SCREEN_WIDTH pixels across, we end up directly underneath ourselves, so we move an additional x pixels over to get to any location on the display. There are two outstanding issues at this point: the surface memory pointer and the actual color value composition. We’re almost there… Typecasting the Buffer Pointer The surface memory pointer given to us by Lock() is of type LPVOID. In other words, it can’t be used until it is ‘cast’ to a proper data type. Just like with an array, the pointer needs a type so that when we do something like this: buffer[3] = color; …the compiler knows how far over in memory the fourth element is (0,1,2,3 = 4th). Is it a byte? Ten bytes? Forty-three? When we declare an array like this: int Array[20]; …the compiler knows that each element is four bytes wide because that’s how large an int is. The answer here is to figure out how big a pixel is, and typecast our surface pointer to some data type that matches. Since we’re playing with 16-bit color, any 16-bit unsigned data type will do (there are no negative color values). Now, I realize that some people would like assistance with all of these data types, so check in 03.03 – Selected C Topics for an overview. I can tell you that either USHORT* or WORD* would work fine, so let’s use WORD. It’s now time to take possession of the surface pointer hidden in that DDSURFACEDESC2 structure we got back from Lock() : WORD *pwBuffer; pwBuffer = (WORD *)ddsd.lpSurface; That’s it! The (WORD *) is the typecast that changes the pointer from LPVOID (VOID*) to WORD*. Now, every time we access pwBuffer elements, we’ll be moving 16-bits at a time – exactly the size of a pixel in 16-bit color mode. Using the Memory Pitch In our illustrations, screen memory (in bytes) is exactly 640x480 multiplied by the number of bytes per pixel. In reality however, there’s a notable difference – the video adapter may reserve some extra memory for itself. If you picture the surface memory as being a rectangle (just like the monitor screen), then picture the video card’s private memory as being a rectangular ‘growth’ coming out of the right-hand side of the monitor. In other words, the number of bytes per horizontal line is equal to the number of pixels per line times the number of bytes per pixel, PLUS the video card’s reserved memory. This is where the lPitch field comes in. This value represents the total number of bytes used by the video card of each horizontal line of memory for the display (which equals the visible display memory plus the reserved memory). Don’t let it bake your brain too much; just read this next line a few times over: buffer[ y * lPitch + x] = color; Of course, this assumes that the pitch is in pixels. DirectDraw gives it to us in bytes, so if you’re not using 8-bit color you’d better fix that value like so: long lPitch; // Get the pitch for 16-bit pixels lPitch = ddsd.lPitch / 2; The reasoning is like this: If there are x bytes in each row, then there are x / 2 WORDs in each row. It’s like in math – if your unit of measurement is metres, make sure you convert any miles and inches before evaluating anything. For us, we’ve decided that a pixel is one WORD wide, so we need our pitch in WORDs, not bytes. Put another way, think: “If the video card uses x bytes per row, how many pixels per row is that?”. Color Composition To some extent, I’ve already covered this subject in the Learning Ladder series, but I’ll reiterate some of it here for clarity. Our pixels are 16-bits wide, and out buffer has been typecast to 16-bit elements. This means that whatever we assign to an array element will be 16 bits wide. Recall that in 5,6,5 16-bit color mode we have 5 bits available for red (intensity (brightness) from 0-31), 6 bits for green (0-63), and 5 bits for blue (0-31). Once you’ve decided on your values for these three, you need to combine them into a 16-bit number. Since I’ve already discussed how this is done, I’ll just show you a macro that’s included in GAMEMAIN.CPP to make life a little easier: // This converts an R,G,B set to a 16-bit color (565) #define RGB16(r,g,b) ((r << 11) + (g << 5) + b) Here’s how you would use it with a buffer: buffer[0] = RGB16(0, 0, 0); // Black buffer[1] = RGB16(31, 63, 31); // White buffer[2] = RGB16(31, 0, 0); // Red buffer[3] = RGB16(031, 0, 31); // Purple (Red + Blue) Just think of each color component as a brightness knob that you adjust from 0% to 100%. The color component’s highest allowed value is 100% bright, and 0 is always 0%. It may seem odd that green has an extra bit to it, and therefore can produce a higher maximum value, but don’t forget that 0 is 0, 63 is 100% brightness, and 50% brightness is ~32, just like ~16 is 50% bright for red or blue. Putting it all Together Let’s bring out the complete Game_Main() as it stands:
    
void Game_Main()
{
    DDSURFACEDESC2 ddsd;
    WORD *buffer;
    long lPitch;
 
    // Prepare a DDSURFACEDESC2 structure for Lock()

    ZeroMemory(&ddsd, sizeof(ddsd));
    ddsd.dwSize = sizeof(ddsd);
 
    // Lock the backbuffer surface

    G.lpDDSBack->Lock(NULL, &ddsd, 
                      DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);
 
    // Get the surface pointer and the memory pitch (in WORDs)

    buffer = (WORD *)ddsd.lpSurface;
    lPitch = ddsd.lPitch / 2;   // (bytes / 2 = WORDs)

 
    // Draw onto the display

    // Fill the entire screen with white pixels

    int x, y;
    for (y = 0; y < SCREEN_HEIGHT; y++)
    {
        for (x = 0; x < SCREEN_WIDTH; x++)
        {
            buffer[y * lPitch + x] = RGB16(31, 63, 31);
        }
    }
 
    // Unlock the surface

    G.lpDDSBack->Unlock(NULL);
 
    // Flip the surfaces

    G.lpDDSPrimary->Flip(NULL, 0);
}
    
Pop that into your GAMEMAIN.CPP, and take a quick look at GLOBALS.H to make sure that you’re in 16-bit color mode (i.e. SCREEN_BITDEPTH is 16). As it says, this function will color every pixel on the display white. Go Play With Yourself It’s absolutely vital that you take the time (the more the better) to really play with this function – especially the actual pixel coloring. I can think of dozens of cool effects to try and create right off the top of my head, so there’s plenty that can be done here with a few simple variables and some creative thinking. Here’s some little tidbits that might come in handy in your adventures: The rand() function returns a pseudo-random number between 0 and 32,767. When used with the modulo operator (%), you can get a number in any range. Here’s an example: buffer[0] = RGB16(rand()%32, rand()%64, rand()%32); Since % gives you a remainder, something like rand()%32 will give you a number between 0 and 31, which is the exact range for a 5-bit color component. You might be interested in 'seeding' the random-number generator, which basically gives you fresh, unique streams of random numbers -- look in your online help for more information. The static keyword, when used with a local variable (i.e. a variable declared inside of a function) will retain its value between function calls. A function like this:

void SomeFunction()
{
    static int i = 0;

    printf(“Number = %d”, i);
    i++;
}
  
…will output increasing values for i with every call, instead of always printing zeros if the variable wasn’t static. With variables of this type, you can do some interesting things with Game_Main() . SCREEN_WIDTH and SCREEN_HEIGHT are your friends. Never use hard-coded values in Game_Main() . Also, many interesting effects can be generated by using these definitions in your drawing code. Some Things to Try If you’re feeling like a challenge, try the following:
  • Create solid diagonal lines of varying color on the display
  • Create a sequence of boxes around the screen, steadily decreasing in size down to the middle
  • Emulate gray-scale “off-air” television noise (think Poltergeist)
  • Have single pixels or lines race from the top-left to the bottom right of the display
  • Scatter pixels on the display that pulsate in varying colors
We don’t want to get too complicated here, but I’d love to have anyone who comes up with a cool creation using simple variables, loops and math functions to submit it for everyone to see and try. Questions? Comments? Do you have a wicked demo to show off? Please reply to this topic! Edited by - teej on May 10, 2001 10:59:32 AM
Advertisement
man, i am kinda confused but i am determined, as i have said my goal is to learn Direct X. anyway, can someone list the skills i should learn, i know arrays, pointers, classes, but my book says learn new and delete and some other stuff, can you guys help me out, when i make my game, its dedicated to Teej and anyone else who helps me, -kaptan

p.s. thank you much

-kaptan

"If you cant handle the heat, Get off the car hood"
-kaptan"If you cant handle the heat, Get off the car hood"
Well explained teej! Suddenly directX makes about a zillion times more sense. Thank you! But...
I guess my post boils down to a newbie C question, but it could also be a question about game structure. I''m a little confused where to put bits of code.
I am trying to do a 3D starfield (not too tricky!) by following an old pascal file (I''m sure a lot of you remember those ASPHIXIA demo trainers of old!)
It is very simple code. Basically this is its main procedure:
init; <- sets up the structures etc...
Repeat
Calcstars; <- Calculate new star positions
Drawstars; <- Draw new stars
Until KeyPressed
The variables etc I need are:
"numberstars" - constant for the number of stars in the field;
A "star" structure (containing int x,y,z variables)
a "Stars" array of type "star".
Where can I define and initialise these with out giving them new random values several times a second?
Please help!
Yo da man, Teej!
( Or something along those lines.. )
This was exactly the material I wanted to learn, thank you very much.

------------------
It''s me again!
Nmoog:To do setup for each star structure, you''d probably want a loop in the initialization routine that goes through each structure in the array. One way to do it would be to give the stars a standard starting position (x,y,z), and also include a velocity variable for each direction (dx, dy, dz). Then, in the initialization routine, you could either standardize or randomize position and velocity, and in your main routine, do a check to see if the star is off screen, and if it is, re-initialize it. Of course, this is just one way to do it, and I''m sure there are infinite ways. Hope this helps though.

My question is that I wrote a quick little "white noise" program (it''s literally 2 lines of code added to the base Game_Main routine), but the "randomness" of the noise comes into question, because when the program runs, there appears vertical lines on the screen (meaning those values don''t change at all), about 3-4 pixels thick, every 20-30 pixels horizontally. Now, if I seed the random number generator with the tick count, it completely takes the randomness out of it (it''s literally no longer white noise...no value ever changes). So, I''m wondering how to make those vertical lines disappear. I would guess it has to do with seeding the generator, but if I do it with the system clock, it makes things worse.
PiotyrSoftware Engineer by day, Game developer by nightThe early bird may get the worm, but the second mouse gets the cheese.
kaptan: It''s my duty to ensure that the skills one needs in order to get anywhere with game development are covered in this forum. For C-related skills, articles will be placed in 03.03 - Selected C Topics. Anything having to go with game development itself will come eventually in the main sections of this forum. Of course, you are always encouraged to do some learning on your own -- especially if your ''C'' isn''t the greatest. In a nutshell, I can tell you that arrays, pointers, data types, memory management and overall modular organization are real hotspots for us, and as I''ve said I won''t leave any of them out.

nmoog: Once you hear the answer you''re probably going to laugh to yourself:
  • Game_Initialize() - one-time initialization

  • Game_Main() - per-frame game main

  • Game_Terminate() - post-game teardown




Teej
Hi Teej!

I have one major question about these, otherwise magnificent, tutorials. Where are we supposed to end? Tetris or Quake 4? I hope we start with Tetris, but I do want to go on after that (ok, well, maybe not Quake 4).
Thanks

*** Hi! I''''m a signature virus. Copy me into your signature to help me spread! ***
*** Hi! I''m a signature virus. Copy me into your signature to help me spread! ***
If anyone is interested in the code to find out how many bits your video card uses for green I'm fairly sure the following code will accomplish that.

    ZeroMemory(&ddsd, sizeof(ddsd));ddsd.dwSize = sizeof(ddsd);G.lpDDSBack->GetSurfaceDesc(&ddsd);HDC hDC;char buffer[30];WORD bits = 0;DWORD mask = ddsd.ddpfPixelFormat.dwGBitMask;while (mask){        mask = mask & (mask-1);	bits++;}sprintf(buffer, "%d", bits);hDC = GetDC(G.hWnd);SetTextColor(hDC, RGB(0, 255, 0));SetBkColor(hDC, RGB(0,0,0));SetBkMode(hDC, OPAQUE);TextOut(hDC, 0, 0, buffer, strlen(buffer));ReleaseDC(G.hWnd, hDC);    


Just paste that into game main and it should output a number in the top left corner of the screen. You will also have to #include string.h and make sure you don't draw any graphics after this or you may overwrite the number.
I'm not 100% sure this is the right code, but it gave me an output of 6, and when i tried the Red and Blue bit masks I got an output of 5.

Edited by - Dionysis on May 9, 2001 7:30:02 PM
I also have a question about lPitch. I still don''t understand the whole conversion thing. If I were to use 32-bit color what would I divide lPitch by to get the correct WORD value??
Dionysis I guess you would have to divide it by 4 (or >>2)...

But if I''m posting here it is because I have a question... I made a very simple function plotting a pixel... i''ve been told I''d better inline it. But it wont link if the functions is inline...

All I do is add the word inline before void DessinePixel(...)
and I also add inline in the prototype declaration in my header file...

error LNK2001: unresolved external symbol "void __cdecl DessinePixel(int,int,unsigned char,unsigned char,unsigned char,unsigned short *,int)" (?DessinePixel@@YAXHHEEEPAGH@Z)

Also... I''m always getting an alert box displaying an error when I close my program (memory could not be written, do you want to debug)

Anyone can help me?
Biere et punk

This topic is closed to new replies.

Advertisement