Многим программистам, пишущим программы для Windows на Visual C++ или на Borland C++, наверняка приходилось составлять такие программы, в которых существенное значение имеет скорость вывода на экран графических данных. Примеров можно привести множество: воспроизведение фильма, создание анимации, каких-нибудь визуальных спецэффектов на экране, проигрывание сейсмограммы и т.д.

Самый очевидный метод решения проблемы вывода графики на экран – использование функции Windows API CreateDC() для создания дескриптора графического контекста, затем применение функций прорисовки типа MoveTo(), LineTo(), Polyline(), PolyDraw(), Arc() и т.п. (более подробно об этих функциях см. в MSDN). Именно так начинающий программисты и поступают. Однако, им рано или поздно приходится сталкиваться с такой проблемой: окна-то в Windows накладываются друг на друга, изображения перекрываются, причем не сохраняются, и их приходится заново каждый раз перерисовывать. А если «затолкать» весь набор необходимых функций прорисовки в оконный обработчик OnDraw(), то получим ситуацию, где окно будет перерисовываться примерно так: вначале рисуется одна линия, потом другая, потом дуга, и так последовательно, шаг за шагом каждый раз будет рисоваться окно, и все это видит конечный пользователь, чего нам, естественно, не хотелось бы. А с каждым новым добавлением какого-нибудь графического элемента этот эффект проявляется все сильнее и сильнее. Поэтому программисты поступают так: создают какой-нибудь графический образ в памяти, «обрисовывают» его и затем «одним махом» выводят на экран. Вот тут и встает проблема: как же максимально быстро его вывести на экран?

Первое, что приходит в голову – это использовать функцию BitBlt(), входящую в API. Такой способ для большинства случаев остается вполне приемлемым. Однако, если при помощи стандартных функций прорисовки рисовать в bitmap-изображение в памяти, а затем вызывать BitBlt(), то мы приходим к выводу, что прорисовка «затянется на полсекунды». Ситуация усложняется, если нужно произвести ряд последовательно быстро сменяющих друг друга изображений, будь то воспроизведение сигнала или фильма. Да еще воспроизвести так, чтобы при наложении окна поверх текущего изображение сохранялось, а не стиралось. Если в этом случае использовать функцию BitBlt(), то мы увидим нечто вроде «тяжеловесно меняющегося изображения». Т.е. изображение будет обновляться «каждые полсекунды». В принципе, мы можем все так и оставить. Но с каким чувством мы будем смотреть на приложения конкурентов, в которых и фильмы воспроизводятся быстро, и сигнал обновляется ну прямо как в реальном времени!.. Это обстоятельство рано или поздно нас подталкивает к изучению новых, более эффективных, методов прорисовки.

Сразу вспоминается ситуация в MS-DOS с BIOS-вызовами. Как тогда делали высокоскоростные игры? Не использовали BIOS для вывода на экран, а копировали данные из ОЗУ напрямую в видеопамять. Позже эта технология была «переброшена» Microsoft под Windows и получила название DirectDraw. Т.е. для более быстрого вывода на экран можно пользоваться механизмами DirectDraw. Для реализации этой технологии нужно при помощи функции DirectDrawCreate() создать объект, адрес которого содержится в переменной типа LPDIRECTDRAW, установить режим работы приложения (метод SetCooperativeLevel() объекта DIRECTDRAW), создать первичную и вторичную поверхности LPDIRECTDRAWSURFACE (метод CreateSurface() объекта DIRECTDRAW) и в общем произвести всю инициализацию, более подробные сведения о которой можно прочитать в MSDN или во многих других ресурсах Интернета.

Далее есть такой метод объекта DIRECTDRAWSURFACE, который называется BltFast(). Он позволяет максимально быстро передать пиксели из первичной поверхности во вторичную и имеет следующие параметры:

HRESULT BltFast(
DWORD dwX, // X – координата поверхности-преемника
DWORD dwY, // Y – координата поверхности-преемника
LPDIRECTDRAWSURFACE lpDdsSrcSurface, // указатель на объект-источник
LPRECT lpSrcRect, // указатель на прямоугольную область, содержащую координаты источника
DWORD dwTrans // тип передачи (обычно DDBLTFAST_NOCOLORKEY); можно задать прозрачную передачу
);

Также есть метод Flip() объекта DIRECTDRAWSURFACE, который позволяет быстро переключить поверхности, отображаемые на экране:

HRESULT Flip(
LPDIRECTDRAWSURFACE7 lpDDSSurfaceTargetOverride, // обычно NULL
DWORD dwFlags // набор флагов, по умолчанию DDFLIP_WAIT
);

Более подробные сведения об этих функциях можно найти в
MSDN.

В общем, идея такая: используем метод BltFast() для «перебрасывания» пикселей из одной поверхности в другую, затем переключаем поверхности для отображения на экран при помощи метода Flip(). И так повторяем каждую прорисовку. Это позволяет добиться довольно высокой её скорости, однако оптимальный эффект будет в полноэкранном или эксклюзивном режиме.

Альтернативой DirectX является библиотека OpenGL, причем ее несомненное достоинство – легкая переносимость на другие платформы типа Linux, OS/2, QNX. Реализация быстрой прорисовки с использованием OpenGL относительно проста: устанавливается формат вывода на экран (функция SetPixelFormat()), создается GL-контекст при помощи функции wglCreateContext(), затем этот GL-контекст «подключается» к текущему графическому контексту при помощи функции wglMakeCurrent(). Все! После этого можно рисовать сколько угодно. Помещаем между вызовами glBegin() и glEnd() ну прямо как между операторами C++ { и } функции прорисовки glVertex() и рисуем квадраты, линии, треугольники, многоугольники… А если еще воспользоваться механизмом компиляции GL-списков (см. функции glNewList(), glEndList(), glGenLists(), glCallList(), glCallLists()), то, как вроде бы анонсировано фирмами-разработчиками аппаратного обеспечения, все должно выводиться на экран быстрее. Однако, с точки зрения автора, использование списков практически не повышает скорость прорисовки. Хотя автор, может быть, не прав? В общем, суть такова: ряд команд OpenGL компилируется в некий список, и затем каждый раз в функции прорисовки выполняется этот уже готовый список.

Однако, как потом приходится убеждаться, в некоторых случаях ни DirectDraw, ни OpenGL не дают желаемого эффекта прорисовки. Т.е. изображение в обычном окне рисуется уже не «полсекунды», но все еще недостаточно быстро. Ситуация вполне удовлетворительна, когда меняется лишь небольшая часть изображения (например, часть верхних строк), а остальная часть изображения лишь копируется в другую область, например, при воспроизведении сейсмической записи. Но вот если вдруг имеется некая последовательность кадров, упакованных в JPEG-формате, и эту последовательность нужно быстро вывести на экран… Конечно, можно использовать DirectDraw, но есть более удобный способ быстрого вывода на экран с использованием Windows
API.

В чем этот способ заключается? Обратимся к примеру. Есть такая API-функция SetDIBits(), которая предназначена для быстрого копирования пикселей из некоей области памяти в bitmap-буфер. Чтобы ее использовать, нужно проделать следующее:

— заполнить область памяти нужными графическими данными;
— создать или получить дескриптор текущего графического контекста;
— получить дескриптор bitmap-изображения;
— выполнить функцию SelectObject(), передав ей в качестве параметров полученные дескрипторы;
— заполнить структуру BITMAPINFO характеристиками изображения;
— вызвать функцию SetDIBits, передав ей следующие параметры:

SetDIBits(
HDC hdc, // дескриптор контекста устройства (DC)
HBITMAP hbmp, // дескриптор bitmap-изображения
UINT uStartScan, // первая строка сканирования массива (изображения)
UINT cScanLines, // число строк сканирования (изображения)
CONST VOID* lpvBits, // массив бит изображения
CONST BITMAPINFO* lpbmi, // информация о bitmap’е 
UINT fuColorUse, // тип lpbmi->bmiColors: это RGB или записи в палитре
);

Это работает намного быстрее, чем если рисовать при помощи стандартных API-функций прорисовки типа MoveTo(), LineTo(), Polyline() и т.п.

Аналогично можно и «быстрым способом» скопировать биты из области памяти в графический контекст не изображения, а экрана. Для этого используется API-функция SetDIBitsToDevice(). Более того, ее использование дает массу возможностей. Допустим, у нас в памяти имеется полутоновое изображение, а нам его надо вывести на цветной экран. Что нам делать – создавать полутоновой контекст? Но тогда все, что мы будем выводить далее в этот контекст, уже не может быть иметь цвет (если, конечно, не прибегнуть к «хитрым приемам»). Или вначале из полутонового изображения, в котором 1 пиксель занимает 1 байт, сделать полноцветное изображение, в котором 1 пиксель равен 3 байтам (т.е. «размножить» 1 байт 3 раза)? Но ведь эта операция в памяти займет много времени. 

В этом случае можно использовать функцию SetDIBitsToDevice(), которая без проблем все нужные вещи сделает сама. Ее использование очень похоже на использование предыдущей функции SetDIBits(). Для этого нужно:

— создать или получить дескриптор текущего графического контекста;
— заполнить структуру BITMAPINFO характеристиками изображения в памяти;
— вызвать функцию SetDIBitsToDevice(), передав ей следующий параметры:

SetDIBitsToDevice(
HDC hdc, // дескриптор контекста устройства (DC)
int XDest, // x-координата верхнего левого угла DC
int YDest, // y-координата верхнего левого угла DC
DWORD dwWidth, // ширина исходной области 
DWORD dwHeight, // высота исходной области 
int XSrc, // x-координата нижнего левого угла исходной области 
int YSrc, // y-координата нижнего левого угла исходной области 
UINT uStartScan, // первая строка сканирования массива (изображения)
UINT cScanLines, // число строк сканирования (изображения)
CONST VOID* lpvBits, // массив памяти (с полутоновым изображением)
CONST BITMAPINFO* lbmpi, // информация о bitmap’е
DIB_RGB_COLORS // RGB или записи в палитре (DIB_PAL_COLORS)
);

Т.е. в данном случае, если у нас изображение полутоновое, то в массиве lbmi->bmiColors нужно установить значения палитры от 0 до 255 так:

for (int i=0; i<256; i++) {
lbmi->bmiColors[i].rgbBlue = i;
lbmi->bmiColors[i].rgbGreen = i;
lbmi->bmiColors[i].rgbRed = i;
lbmi->bmiColors[i].rgbReserved = 0;
}

Для любителей Delphi: автор слышал, что альтернативным (вернее, не альтернативным, а «обернутым в класс») способом быстрой прорисовки в Delphi является использование свойства TBitmap-объекта ScanLine. Т.е. нужно создать TImage и воспользоваться его свойством Picture.Bitmap.ScanLine, причем для объекта TImage нет необходимости следить за перерисовкой.

Оставить мнение

Check Also

Цифровой паноптикон. Настоящее и будущее тотальной слежки за пользователями

Даже если ты тщательно заботишься о защите своих данных, это не даст тебе желаемой приватн…