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

Error1024 10 января 2013 в 16:16 15,2k
Здравствуйте!
Эта статья, прежде всего для новичков, тех, кто только решил заняться демосценой, если статья будет положительно принята сообществом, то я сделаю цикл из нескольких статей о создании демок. Каждая статья будет короткой, но в конце каждой статьи будет вполне рабочий пример.
Сразу предупрежу, эта статья не о том как делать 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();
  }
}


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