Немного о создании демок, часть 1

от автора

Здравствуйте!
Эта статья, прежде всего для новичков, тех, кто только решил заняться демосценой, если статья будет положительно принята сообществом, то я сделаю цикл из нескольких статей о создании демок. Каждая статья будет короткой, но в конце каждой статьи будет вполне рабочий пример.
Сразу предупрежу, эта статья не о том как делать Demo с помощью OpenGL, DirectX, и миллионов шейдеров, об этом есть много хороших статей, я буду писать о рисовании в памяти.

На чем писать?

Для начала надо разобраться, как и на чём мы будем писать демку.
Писать мы будем на C, с помощью Visual Studio 2008.

Несколько организационных моментов

Давайте сначала опишем в модуле Images.h структуру TImage, она будет хранить информацию об изображении:

struct TImage {   int width;   int height;   unsigned char *pBitMap; }; 
Images.h, Images.c

#pragma once  struct TImage {   int width;   int height;   unsigned char *pBitMap; };  void imgClearRGBA(struct TImage Image, unsigned char R, unsigned char G, unsigned char B, unsigned char A ); void imgClear(struct TImage Image, unsigned long color ); 

/*   Images */ #include "Images.h"  void imgClearRGBA(struct TImage Image, unsigned char R, unsigned char G, unsigned char B, unsigned char A ) {   int i;   for(i=0;i!=Image.width*Image.height*4;i=i+4)   {     Image.pBitMap[  i  ] = B;     Image.pBitMap[ i+1 ] = G;     Image.pBitMap[ i+2 ] = R;     Image.pBitMap[ i+3 ] = A;   } }  void imgClear(struct TImage Image, unsigned long color ) {   unsigned long *pBitMap;   int i;   pBitMap = (unsigned long*)Image.pBitMap;   for(i=0;i!=Image.width*Image.height;i++)   {     pBitMap[  i  ] = color;   } } 

А непосредственно битовая карта изображения будет храниться в массиве, на который будет указывать pBitMap.

Поскольку мы не будем использовать OpenGL и DirectX, надо определиться, как мы будем выводить пиксели на экран.
Вариант с SetPixel() нам не подходит из-за своей медлительности.
На помощь приходит функция WinApi StretchDIBits(), она выводит на Handle массив пикселей, попутно производя масштабирование, если это необходимо.
Вот как выглядит функция, которая выводит массив, где каждый пиксель состоит из 4-х байт, на экран:

void DrawBuffer(struct TImage Image) {    BITMAPINFO BitMapInfo;    DC=GetDC(Wnd);    BitMapInfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);   BitMapInfo.bmiHeader.biWidth=Image.width;   BitMapInfo.bmiHeader.biHeight=Image.height;   BitMapInfo.bmiHeader.biPlanes=1;   BitMapInfo.bmiHeader.biBitCount=32;   BitMapInfo.bmiHeader.biCompression=BI_RGB;    StretchDIBits( DC,     0, 0, Image.width*PIXEL_SIZE, Image.height*PIXEL_SIZE,     0, 0, Image.width, Image.height,     Image.pBitMap,     &BitMapInfo,     DIB_RGB_COLORS,     SRCCOPY );    ReleaseDC(Wnd, DC); } 
SystemUtils.h, SystemUtils.c

#pragma once #define PIXEL_SIZE 1 #define DISP_WIDTH 640 #define DISP_HEIGHT 480  void DrawBuffer(struct TImage Image); #include "windows.h" void SetHWND( HWND _Wnd );  

/* SystemUtils */ #include "windows.h" #include "Images.h" #include "SystemUtils.h"  static HWND Wnd;  void SetHWND( HWND _Wnd ) {   Wnd = _Wnd; }  void DrawBuffer(struct TImage Image) {    BITMAPINFO BitMapInfo;   HDC DC;    DC = GetDC(Wnd);    BitMapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);// размер структуры   BitMapInfo.bmiHeader.biWidth = Image.width;// ширина картинки   BitMapInfo.bmiHeader.biHeight = -Image.height;// высота картинки, минус нужен чтобы изображение не было перевернутым   BitMapInfo.bmiHeader.biPlanes = 1;// количество слоев - всегда 1   BitMapInfo.bmiHeader.biBitCount = 32;// кол-во бит на пиксель   BitMapInfo.bmiHeader.biCompression = BI_RGB;// формат   BitMapInfo.bmiHeader.biSizeImage = Image.width*Image.height*32/8;// размер картинки    StretchDIBits( DC,     0, 0, Image.width*PIXEL_SIZE, Image.height*PIXEL_SIZE, // прямоугольник куда выводить     0, 0, Image.width, Image.height, // прямоугольник откуда выводить     Image.pBitMap, // указатель на массив пикселей     &BitMapInfo, // параметры     DIB_RGB_COLORS, // формат вывода     SRCCOPY ); // режим вывода    ReleaseDC(Wnd, DC); } 

Классический цикл выглядит так:

while(1) {   Рисуем;   Копируем  на экран;   Pause(); } 

Но мы работаем в Windows, следовательно постоянно должны обрабатывать сообщения окна иначе у пользователя создастся впечатление, что программа зависла, поскольку мы в любом случае будем вызывать Pause() то почему бы не производить обработку сообщений окна в нем?
Нет конечно вполне можно использовать State Machine, но в кому нужна гигантская и неповоротливая State Machine в демке (я не говорю об играх).
Выглядеть pause.c будет так:

Pause.h, Pause.c

#pragma once #include "windows.h"  void SetMsg( MSG _Msg ); void SetPause( DWORD value ); void Pause(void); 

/*   Тут реализована примитивная синхронизация по времени */  // функция timeGetTime намного точнее GetTickCount #define _USE_TIMEGETTIME  #include "windows.h"  #ifdef _USE_TIMEGETTIME #include "mmsystem.h" #pragma comment (lib,"winmm") #endif  static MSG Msg;  DWORD Time; DWORD OldTime;  void SetMsg( MSG _Msg ) {   Msg = _Msg; }  void SetPause( DWORD value ) {   if(value == 0)value = 1;   Time = value; #ifdef _USE_TIMEGETTIME   timeBeginPeriod(1);// устанавливаем максимальную точность timeGetTime   OldTime = timeGetTime()+Time; #else   OldTime = GetTickCount()+Time; #endif }  void Pause(void) {    // главный цикл    while (PeekMessage(&Msg, 0, 0, 0, PM_NOREMOVE) != 0 )   {     if (GetMessage(&Msg, 0, 0, 0) )     {       TranslateMessage(&Msg);       DispatchMessage(&Msg);     }   }  #ifdef _USE_TIMEGETTIME 	while( timeGetTime()<OldTime)Sleep(1);//ждем пока не придет время следующего кадра 	OldTime = timeGetTime()+Time; #else 	while( GetTickCount()<OldTime)Sleep(1);//ждем пока не придет время следующего кадра 	OldTime = GetTickCount()+Time; #endif } 

Это не очень красиво, но работает вполне надежно.
Любители многопоточности могут просто сделать отдельный поток.

Осталось создать окно:

Main.c

/* Главный модуль здесь мы создаем окно и запускаем саму демку */ #include "windows.h" #include "windowsx.h"  #include "Demo.h" #include "SystemUtils.h" #include "Pause.h"  static HWND Wnd; static MSG Msg; static WNDCLASS wndclass;  /*   Мы вручную перетаскиваем окно, т.к. если это будет делать Windows   то при перетаскивании система забирает управление себе и демка "зависает".   Выход - по правому клику */ static POINT MouseInWin; static RECT WinRect; static int MoveWin = 0;  // window procedure LONG WINAPI WndProc (   HWND    hWnd,   UINT    uMsg,   WPARAM  wParam,   LPARAM  lParam) {   LONG lRet = 1;   POINT point;   switch (uMsg)    {     case WM_LBUTTONDOWN:       MoveWin = 1;//move window=true       GetWindowRect( Wnd, &WinRect ); 	         point.x = GET_X_LPARAM(lParam);       point.y = GET_Y_LPARAM(lParam);       ClientToScreen( Wnd, (LPPOINT)&point );              MouseInWin.x = point.x-WinRect.left;       MouseInWin.y = point.y-WinRect.top;              SetCapture(Wnd);       break;     case WM_MOUSEMOVE:       GetCursorPos( (LPPOINT)&point );       if(MoveWin)SetWindowPos( Wnd,0,         point.x-MouseInWin.x, point.y-MouseInWin.y,         WinRect.right-WinRect.left, WinRect.bottom-WinRect.top,         0);       break;     case WM_LBUTTONUP:       MoveWin = 0;//move window=false       ReleaseCapture();       break;     case WM_RBUTTONUP:       PostMessage(Wnd, WM_DESTROY, 0, 0);       break;     case WM_DESTROY:       PostQuitMessage (0);       ExitProcess( 0 );       break;     default:       lRet = DefWindowProc (hWnd, uMsg, wParam, lParam);       break;   }      return lRet; }  void CreateWin( HINSTANCE hInstance, HWND *Wnd) {   const int ClientWidth = DISP_WIDTH*PIXEL_SIZE;//resolution * pixel size   const int ClientHeight = DISP_HEIGHT*PIXEL_SIZE;      RECT Rect = {0,0,ClientWidth,ClientHeight};    wndclass.style         = CS_BYTEALIGNCLIENT;   wndclass.lpfnWndProc   = &WndProc;   wndclass.cbClsExtra    = 0;   wndclass.cbWndExtra    = 0;   wndclass.hInstance     = 0;   wndclass.hIcon         = LoadIcon(0, L"idi_Application");   wndclass.hCursor       = LoadCursor (0,IDC_ARROW);   wndclass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);   wndclass.lpszMenuName  = L"";   wndclass.lpszClassName = L"MainWindow";      RegisterClass(&wndclass);    *Wnd=CreateWindow(      L"MainWindow",     L"Demo",     WS_POPUPWINDOW, //стиль     CW_USEDEFAULT,//x     CW_USEDEFAULT,//y     ClientWidth,//width     ClientHeight,//height     0,// parent win     0,//menu     hInstance,     0//other     );        GetWindowRect(*Wnd,&Rect);   Rect.bottom = Rect.left+ClientHeight;//ClientHeight   Rect.right = Rect.top+ClientWidth;//ClientWidth   AdjustWindowRect(&Rect, GetWindowLong(*Wnd,GWL_STYLE) ,0);        SetWindowPos(*Wnd,0,Rect.left,Rect.top,     Rect.right-Rect.left,     Rect.bottom-Rect.top,0);        ShowWindow(*Wnd, SW_SHOW );   UpdateWindow (*Wnd);  }  int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {   CreateWin(hInstance, &Wnd);//создаем окно      SetHWND(Wnd);   SetMsg(Msg);      StartDemo();    return 0; } 

И создать модуль Demo.c, где и будет сама демка:

Demo.h, Demo.c

void StartDemo(void); 

#include "Pause.h" #include "SystemUtils.h" #include "Images.h" #include "math.h"  static unsigned char BitMap[640*480*4];//битовая карта изображения static struct TImage Disp = {640,480,BitMap};//изображение  void StartDemo(void) {   SetPause(16);//fps ~= 60   while(1)   {     //              AARRGGBB     imgClear(Disp,0xFF000000);//очищаем экран      DrawBuffer(Disp);//копируем в окно     Pause();   } } 

Теперь можно запускать!

Но пока кроме пустого окна мы ничего не увидим, все верно ведь мы в цикле только очищаем буфер и копируем его на экран:

void StartDemo(void) {   SetPause(16);//fps ~= 60   while(1)   {     //              AARRGGBB     imgClear(Disp,0xFF000000);//очищаем экран      DrawBuffer(Disp);//копируем в окно     Pause();   } } 

Рисуем точку


Давайте для начала нарисуем красную точку с координатами x=1, y=1.
Экран для нас будет выглядеть вот так:

Где каждый пиксель изображения можно представить в виде 4-х unsigned char либо одним unsigned long:


То, что серое — это альфа, мы ее пока трогать не будем.

Теперь надо вычислить по формуле (x + y*disp.width)<<2 положение точки в массиве и прибавить к нему смещение для красного цвета, у нас оно равно 2-м. (<<2 – это просто быстрое умножение на 4 сдвигом)

void StartDemo(void) {   const int x=1, y=1;   SetPause(16);//fps ~= 60   while(1)   {     //              AARRGGBB     imgClear(Disp,0xFF000000);//очищаем экран      Disp.pBitMap[( (x + y*Disp.width)<<2 )+2] = 255;//рисуем красную точку      DrawBuffer(Disp);//копируем в окно     Pause();   } } 

Теперь при запуске вы действительно увидите красную точку.

Рисуем градиент


Теперь давайте нарисуем градиент.
Для этого мы пройдемся по всем пикселям массива с помощью двух вложенных циклов,
и с помощью формулы Value = X / ( Max_X / Max_Value ), нарисуем градиент, причем для красного и синего канала свой, что даст красивую картинку.

void StartDemo(void) {   int x,y,line;     SetPause(16);//fps ~= 60    imgClear(Disp,0xFF000000);//очищаем экран      while(1)   {     for(y=0;y!=Disp.height;y++)     {       line = y*Disp.width;       for(x=0;x!=Disp.width;x++)       {         Disp.pBitMap[( (x + line)<<2 )+0] = (unsigned char)( y/(Disp.height/256.0) );//B         Disp.pBitMap[( (x + line)<<2 )+2] = (unsigned char)( x/(Disp.width/256.0) );//R       }     }     DrawBuffer(Disp);//копируем в окно     Pause();   } } 

Сразу становится, очевидно, что этот код можно сильно оптимизировать, вы можете это сделать, если захотите.

Простейшая плазма


Давайте нарисуем простейшую плазму, правда, пока я не буду объяснять принцип ее работы, это тема следующей статьи. Отмечу лишь то, что вместо функции sin() тут используется таблица синусов, что в несколько раз повышает быстродействие.

Demo.c

#include "Pause.h" #include "SystemUtils.h" #include "Images.h" #include "math.h"  static unsigned char BitMap[640*480*4];//битовая карта изображения static struct TImage Disp = {640,480,BitMap};//изображение  static unsigned char SinT[256];//таблица синусов  static void CreatetSinT(void) {   int i;   for(i=0;i!=256;i++)   {     SinT[i] = (int)( sin( (i*3.14*2.0)/256.0) *128+128 );   } }  void StartDemo(void) {   int x,y,line;   int tx1=0,ty1=0,tx2=0,ty2=0,tx3=0,ty3=0;   int px1,py1,px2,py2,px3,py3;     SetPause(16);//fps ~= 60    CreatetSinT();    imgClear(Disp,0xFF000000);//очищаем экран      while(1)   {     py1=ty1;     py2=ty2;     py3=ty3;     for(y=0;y!=Disp.height;y++)     {       line = y*Disp.width;       px1=tx1;       px2=tx2;       px3=tx3;       for(x=0;x!=Disp.width;x++)       {         px1=px1+1;         px2=px2+1;         px3=px3+1;         Disp.pBitMap[( (x + line)<<2 )+2] = (SinT[ px1&255 ]+SinT[ py1&255 ])>>1;//R         Disp.pBitMap[( (x + line)<<2 )+1] = (SinT[ px2&255 ]+SinT[ py2&255 ])>>1;//G         Disp.pBitMap[( (x + line)<<2 )+0] = (SinT[ px3&255 ]+SinT[ (py3+63)&255 ])>>1;//B       }       py1=py1+1;       py2=py2+1;       py3=py3+1;     }     tx1=tx1+1;     ty1=ty1+1;     tx2=tx2+2;     ty2=ty2+2;     tx3=ty3+3;     ty3=ty3+3;     DrawBuffer(Disp);//копируем в окно     Pause();   } } 

Скачать исходники:
Пустое окно
Точка
Градиент
Плазма
В следующей статье мы поговорим о рисовании плазмы.
Надеюсь, вам было интересно.
Спасибо за внимание!

ссылка на оригинал статьи http://habrahabr.ru/post/165279/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *