Fast Animation with the Windows GDI

William Hemsworth 1 Tallied Votes 7K Views Share

Quite a while ago, I made this snippet. This code is basically the same, except that it adds animation.

This method of blitting is very fast assuming you don't use a surface that's too large. On a 500 x 500 surface, I managed 350fps using only 0-1% of the cpu. This snippet may appear much slower because of the amount of CPU it's applying to each pixel, but the blitting itself is very fast. Also, don't forget that an average game will only redraw parts of the window that need redrawing, this redraws the whole surface every time.

So, as long as you know what you're doing, Windows GDI isn't actually that slow :icon_lol:

Attached executable:

GDI Animation.zip


Preview Image:

edit: Running in Debug may reduce speed by a lot, set it as Release.

#include <windows.h>
#include <iostream>
#include <cmath>

struct pixel {
  union {
    struct {
      /* 'a' unused, used for 32-bit alignment,
       * could also be used to store pixel alpha
       */
      unsigned char b, g, r, a;
    };
    int val;
  };

  pixel() {
    val = 0;
  }
};

// Window client size
const int width = 375;
const int height = 375;

/* Target fps, though it's hard to achieve this fps
 * without extra timer functionality unless you have
 * a powerfull processor. Raising this value will
 * increase the speed, though it will use up more CPU.
 */
const int fps = 50;

// Global Windows/Drawing variables
HBITMAP hbmp;
HANDLE hTickThread;
HWND hwnd;
HDC hdcMem;

// Pointer to pixels (will automatically have space allocated by CreateDIBSection
pixel *pixels;


void onFrame(pixel *pixels) {
  // This is where all the drawing takes place

  pixel *p;

  // +0.005 each frame
  static float frameOffset = 0;

  float px; // % of the way across the bitmap
  float py; // % of the way down the bitmap

  for (int x = 0; x < width; ++x) {
    for (int y = 0; y < height; ++y) {
      p = &pixels[y * width + x];

      px = float(x) / float(width);
      py = float(y) / float(height);

      p->r = unsigned char(((cos(px + frameOffset * 10) / sin(py + frameOffset)) * cos(frameOffset * 3) * 10) * 127 + 127);
      p->g = ~p->r;
      p->b = 255;
    }
  }

  frameOffset += 0.005f;
}



DWORD WINAPI tickThreadProc(HANDLE handle) {
  // Give plenty of time for main thread to finish setting up
  Sleep( 50 );
  ShowWindow( hwnd, SW_SHOW );

  // Retrieve the window's DC
  HDC hdc = GetDC( hwnd );

  // Create DC with shared pixels to variable 'pixels'
  hdcMem = CreateCompatibleDC( hdc );
  HBITMAP hbmOld = (HBITMAP)SelectObject( hdcMem, hbmp );

  // Milliseconds to wait each frame
  int delay = 1000 / fps;

  for ( ;; ) {
    // Do stuff with pixels
    onFrame( pixels );

    // Draw pixels to window
    BitBlt( hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY );

    // Wait
    Sleep( delay );
  }

  SelectObject( hdcMem, hbmOld );
  DeleteDC( hdc );
}



void MakeSurface(HWND hwnd) {
  /* Use CreateDIBSection to make a HBITMAP which can be quickly
   * blitted to a surface while giving 100% fast access to pixels
   * before blit.
   */

  // Desired bitmap properties
  BITMAPINFO bmi;
  bmi.bmiHeader.biSize = sizeof(BITMAPINFO);
  bmi.bmiHeader.biWidth = width;
  bmi.bmiHeader.biHeight =  -height; // Order pixels from top to bottom
  bmi.bmiHeader.biPlanes = 1;
  bmi.bmiHeader.biBitCount = 32; // last byte not used, 32 bit for alignment
  bmi.bmiHeader.biCompression = BI_RGB;
  bmi.bmiHeader.biSizeImage = 0;
  bmi.bmiHeader.biXPelsPerMeter = 0;
  bmi.bmiHeader.biYPelsPerMeter = 0;
  bmi.bmiHeader.biClrUsed = 0;
  bmi.bmiHeader.biClrImportant = 0;
  bmi.bmiColors[0].rgbBlue = 0;
  bmi.bmiColors[0].rgbGreen = 0;
  bmi.bmiColors[0].rgbRed = 0;
  bmi.bmiColors[0].rgbReserved = 0;

  HDC hdc = GetDC( hwnd );

  // Create DIB section to always give direct access to pixels
  hbmp = CreateDIBSection( hdc, &bmi, DIB_RGB_COLORS, (void**)&pixels, NULL, 0 );
  DeleteDC( hdc );

  // Create a new thread to use as a timer
  hTickThread = CreateThread( NULL, NULL, &tickThreadProc, NULL, NULL, NULL );
}



LRESULT CALLBACK WndProc(
      HWND hwnd,
      UINT msg,
      WPARAM wParam,
      LPARAM lParam)
{
  switch ( msg ) {
    case WM_CREATE:
      {
        MakeSurface( hwnd );
      }
      break;
    case WM_PAINT:
      {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint( hwnd, &ps );

        // Draw pixels to window when window needs repainting
        BitBlt( hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY );

        EndPaint( hwnd, &ps );
      }
      break;
    case WM_CLOSE:
      {
        DestroyWindow( hwnd );
      }
      break;
    case WM_DESTROY:
      {
        TerminateThread( hTickThread, 0 );
        PostQuitMessage( 0 );
      }
      break;
    default:
      return DefWindowProc( hwnd, msg, wParam, lParam );
  }

  return 0;
}



int WINAPI WinMain(
      HINSTANCE hInstance,
      HINSTANCE hPrevInstance,
      LPSTR lpCmdLine,
      int nShowCmd)
{
  WNDCLASSEX wc;
  MSG msg;

  // Init wc
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.cbSize = sizeof( WNDCLASSEX );
  wc.hbrBackground = CreateSolidBrush( 0 );
  wc.hCursor = LoadCursor( NULL, IDC_ARROW );
  wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
  wc.hIconSm = LoadIcon( NULL, IDI_APPLICATION );
  wc.hInstance = hInstance;
  wc.lpfnWndProc = WndProc;
  wc.lpszClassName = "animation_class";
  wc.lpszMenuName = NULL;
  wc.style = 0;

  // Register wc
  if ( !RegisterClassEx(&wc) ) {
    MessageBox( NULL, "Failed to register window class.", "Error", MB_OK );
    return 0;
  }

  // Make window
  hwnd = CreateWindowEx(
    WS_EX_APPWINDOW,
    "animation_class",
    "Animation",
    WS_MINIMIZEBOX | WS_SYSMENU | WS_POPUP | WS_CAPTION,
    300, 200, width, height,
    NULL, NULL, hInstance, NULL );


  RECT rcClient, rcWindow;
  POINT ptDiff;

  // Get window and client sizes
  GetClientRect( hwnd, &rcClient );
  GetWindowRect( hwnd, &rcWindow );

  // Find offset between window size and client size
  ptDiff.x = (rcWindow.right - rcWindow.left) - rcClient.right;
  ptDiff.y = (rcWindow.bottom - rcWindow.top) - rcClient.bottom;

  // Resize client
  MoveWindow( hwnd, rcWindow.left, rcWindow.top, width + ptDiff.x, height + ptDiff.y, false);

  UpdateWindow( hwnd );

  while ( GetMessage(&msg, 0, 0, NULL) > 0 ) {
    TranslateMessage( &msg );
    DispatchMessage( &msg );
  }

  return 0;
}
William Hemsworth 1,339 Posting Virtuoso

Here's a speed test :) mine stays at about 480fps with only 2-3% CPU.

William Hemsworth 1,339 Posting Virtuoso

Another really nice effect using this method :icon_smile:

William Hemsworth 1,339 Posting Virtuoso

Got it working with objects and bitmaps, I think with some optimizations I could use this to make some neat games :)

optimumgaint 0 Newbie Poster
p->r = unsigned char(((cos(px + frameOffset * 10) / sin(py + frameOffset)) * cos(frameOffset * 3) * 10) * 127 + 127);
      p->g = ~p->r;

was error in my DEV C++

William Hemsworth 1,339 Posting Virtuoso

Sorry, I should have tested it with multiple compilers, replace that line with:

p->r = (unsigned char)(((cos(px + frameOffset * 10) / sin(py + frameOffset)) * cos(frameOffset * 3) * 10) * 127 + 127);

and it should work.

mrnutty 761 Senior Poster

Pretty cool. Looks something someone will see if they were high of drugs.

William Hemsworth 1,339 Posting Virtuoso

Pretty cool. Looks something someone will see if they were high of drugs.

Haha, what have you been smoking? I'd go crazy if anything turned out that intense.
Thanks alot for the feedback :icon_razz:

Dave Sinkula 2,398 long time no c Team Colleague

Issues when building with mingw:

main.cpp:12: error: ISO C++ prohibits anonymous structs
main.cpp: In function `void onFrame(pixel*)':
main.cpp:60: error: expected primary-expression before "unsigned"
main.cpp:60: error: expected `;' before "unsigned"
main.cpp: In function `void MakeSurface(HWND__*)':
main.cpp:134: warning: passing NULL used for non-pointer converting 2 of `void* CreateThread(_SECURITY_ATTRIBUTES*, DWORD, DWORD (*)(void*), void*, DWORD, DWORD*)'
main.cpp:134: warning: passing NULL used for non-pointer converting 5 of `void* CreateThread(_SECURITY_ATTRIBUTES*, DWORD, DWORD (*)(void*), void*, DWORD, DWORD*)'
main.cpp: In function `int WinMain(HINSTANCE__*, HINSTANCE__*, CHAR*, int)':
main.cpp:237: warning: passing NULL used for non-pointer converting 4 of `BOOL GetMessageA(tagMSG*, HWND__*, UINT, UINT)'

This includes the cast you've already corrected. So it's mostly just that anonymous struct you may want to look at.

[edit]Also, the necessary library to link with:

-LD:\Programs\CodeBlocks\MinGW\lib -lgdi32

Where the path to the library will be whatever your install might be.

William Hemsworth 1,339 Posting Virtuoso

This includes the cast you've already corrected. So it's mostly just that anonymous struct you may want to look at.

Does giving the anonymous structure a name fix that?
Never thought there would be so many compiler issues, and I didn't realize that anonymous structs weren't allowed.

Thanks :)

Dave Sinkula 2,398 long time no c Team Colleague

Does giving the anonymous structure a name fix that?

Yup. I just named it s , for example.

Bill Dreschel 0 Newbie Poster

Hi, thanks for the cool code. I got it working in a Win32 app (project type choice) in VC++ Express 2010 by making the includes:
#include "stdafx.h"
#include "GDIWin32.h"
#include <windows.h>
#include <iostream>
#include <cmath>

and putting the L in front of the quoted text, as in:
// Init wc
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.cbSize = sizeof( WNDCLASSEX );
wc.hbrBackground = CreateSolidBrush( 0 );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wc.hIconSm = LoadIcon( NULL, IDI_APPLICATION );
wc.hInstance = hInstance;
wc.lpfnWndProc = WndProc;
wc.lpszClassName = L"animation_class";
wc.lpszMenuName = NULL;
wc.style = 0;

// Register wc
if ( !RegisterClassEx(&wc) ) {
MessageBox( NULL, L"Failed to register window class.", L"Error", MB_OK );
return 0;
}

Very cool, thanks very much. Any thoughts on how this could be implemented in a Windows Forms Application? (Sorry if this is a really stupid question, just starting in VC++.) Thanks again, Bill

Bill Dreschel 0 Newbie Poster

Finally got around to putting this animation code into a Visual C++ 2010 Express Windows form application. To use it, start a new form build, add a PictureBox, name it "src" and make it some even dimension like 400,400. Add a button, name it "btnStart", text to "Start", add a timer to the form, set it to 20ms and disabled.
Add the attached code.

void DrawMath(void){
           // Create a new bitmap.
   Bitmap^ bmp = gcnew Bitmap(src->Size.Width,src->Size.Height);
   //bmp->PixelFormat=Format32bppArgb;
   // Lock the bitmap's bits.  
   Rectangle rect = Rectangle(0,0,src->Size.Width,src->Size.Height);
   System::Drawing::Imaging::BitmapData^ bmpData = bmp->LockBits( rect, System::Drawing::Imaging::ImageLockMode::ReadWrite, bmp->PixelFormat );

   // Get the address of the first line.
   IntPtr ptr = bmpData->Scan0;

   // Declare an array to hold the bytes of the bitmap.
   // This code is specific to a bitmap with 24 bits per pixels.
   int bytes = bmpData->Stride * bmp->Height;
   array<Byte>^rgbValues = gcnew array<Byte>(bytes);
   array<Byte>^rgbValues1;
   rgbValues1 = rgbValues;

   // Copy the RGB values into the array.

  // This is where all the drawing takes place


  // +0.005 each frame
  static double frameOffset = 0;

  double px; // % of the way across the bitmap
  double py; // % of the way down the bitmap
  double mycosoff;
  int width = src->Size.Width;
  int height = src->Size.Height;
  double frame10 = (10.0) * frameOffset;
  double cosoff = (1270) * cos(frameOffset * 3);
  double pyinc = (1.0) / double(height);
  int counter=0;
  for (int x = 0; x < width; ++x) {
    px = double(x) / double(width);
    mycosoff = cos(px + frame10) * cosoff;
    int ywidth=x;
    py=pyinc+frameOffset;
    for (int y = 0; y < height; ++y ) {
      unsigned char red = unsigned char(((mycosoff / sin(py))) + 127);
      rgbValues[counter++] = 0xff;
      rgbValues[counter++] = red;
      rgbValues[counter++] = ~red;
      rgbValues[counter++] = 0xff;
      ywidth+= width;
      py+=pyinc; 
    }
  }

  frameOffset += 0.005;

   // Copy the RGB values back to the bitmap
   System::Runtime::InteropServices::Marshal::Copy( rgbValues1, 0, ptr, bytes );

   // Unlock the bits.
   bmp->UnlockBits( bmpData );

   // Draw the modified image.
   //e->Graphics->DrawImage( bmp, 0, 150 );
        src->Image = bmp;
        src->Invalidate();
}
#pragma endregion
    private: System::Void timer1_Tick(System::Object^  sender, System::EventArgs^  e) {
                 DrawMath();
             }
    private: System::Void btnStart_Click(System::Object^  sender, System::EventArgs^  e) {
                 static int OnOff=0;
                 if (OnOff==0){
                     btnStart->Text = L"Stop";
                     timer1->Enabled = true;
                     OnOff=1;
                 }
                 else{
                     btnStart->Text = L"Start";
                     timer1->Enabled = false;
                     OnOff=0;
                 }

             }

Have fun! THANKS AGAIN FOR POSTING THE FAST ANIMATION SNIPPET!

thetukiet 0 Newbie Poster

Hi,
And this is a C Sharp version from Bill Dreschel's code. The picture box I used named 'pictureBox1'. It looks pretty good.
I post this here because I found this article while I was searching for a CSharp solution about something like this.
And I think it may help someone like me :-)

static double frameOffset = 0;
private void Draw()
{
    var bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);
    var drawingRect = new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height);
    var bmpData = bmp.LockBits(drawingRect, ImageLockMode.ReadWrite, bmp.PixelFormat);

    // Get the address of the first line.
    IntPtr ptr = bmpData.Scan0;

    // Declare an array to hold the bytes of the bitmap.
    // This code is specific to a bitmap with 24 bits per pixels.
    int bytes = bmpData.Stride*bmp.Height;
    Byte[] rgbValues = new Byte[bytes];
    Byte[] rgbValues1;
    rgbValues1 = rgbValues;


    // Copy the RGB values into the array.
    // This is where all the drawing takes place
    // +0.005 each frame

    double px; // % of the way across the bitmap
    double py; // % of the way down the bitmap
    double mycosoff;
    int width = pictureBox1.Width;
    int height = pictureBox1.Height;
    double frame10 = (10.0) * frameOffset;
    double cosoff = (1270) * Math.Cos(frameOffset*3);
    double pyinc = (1.0)/height;
    int counter = 0;
    for (int x = 0; x < width; ++x)
    {
        px = (double) x/width;
        mycosoff = Math.Cos(px + frame10)*cosoff;
        int ywidth = x;
        py = pyinc + frameOffset;
        for (int y = 0; y < height; ++y)
        {
            int red = (int) ((mycosoff/Math.Sin(py))) + 127;
            rgbValues[counter++] = 0xff;
            rgbValues[counter++] = (byte) red;
            rgbValues[counter++] = (byte) ~red;
            rgbValues[counter++] = 0xff;
            ywidth += width;
            py += pyinc;
        }
    }

    frameOffset += 0.005;
    // Copy the RGB values back to the bitmap
    System.Runtime.InteropServices.Marshal.Copy(rgbValues1, 0, ptr, bytes);
    // Unlock the bits.
    bmp.UnlockBits(bmpData);
    // Draw the modified image.
    //e->Graphics->DrawImage( bmp, 0, 150 );
    pictureBox1.Image = bmp;
    pictureBox1.Invalidate();
}

private void timer1_Tick(object sender, EventArgs e)
{
    Draw();
}
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.