Загрузка и копирование оптимизированных поверхностей

Результат

Оригинальный заголовок: Optimized Surface Loading and Blitting.
В английском варианте используется термин BLIT — BLock Image Transfer. Дословно его можно перевести как «перенос блоков изображения». В сущности «блитинг» заключается в копировании прямоугольника с одной поверхности в такой же или меньший прямоугольник другой поверхности. Поэтому в переводе я буду использовать термин «копирование», как перевод для «блитинга»


Теперь, когда вы получили изображение на экране во второй половине предыдущей части руководства, самое время научиться загружать и копировать поверхности в более эффективной манере.

//Заголовки
#include <SDL/SDL.h>
#include <string>

Тут мы подключаем заголовочые файлы для нашей программы.
SDL.h подключен потому, что мы, очевидно, будем использовать функции SDL.
Заголовок string используется потому, что … эмм, я просто больше предпочитаю std::string чем char*.

//Аттрибуты экрана
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

Тут мы определяем различные аттрибуты для экрана.
Я вполне уверен что вы и сами понимаете зачем нужны SCREEN_WIDTH и SCREEN_HEIGHT. SCREEN_BP это количество битов на пиксель. Во всех частях руководства будет использоваться значение в 32 бита на пиксель.

//Поверхности, которые будут использоваться
SDL_Surface *message = NULL;
SDL_Surface *background = NULL;
SDL_Surface *screen = NULL;

Это три изображения, которые мы будем использовать.

"background" очевидно соответствует фоновому изображению, "message" это растровое изображение говорящее «Hello», ну и "screen" — очевидно экран.

Помните: хорошей практикой будет всегда устанавливать ваши указатели в NULL, если они пока что никуда не указывают.

SDL_Surface *load_image( std::string filename ) {
    //Временное хранилище для загружаемого изображения
    SDL_Surface* loadedImage = NULL;
    //Оптимизированное изображение, которое и будет использоваться
    SDL_Surface* optimizedImage = NULL;

Это наша функция, загружающая изображения.
Она загружает изображение и возвращает указатель на оптимизированную версию загруженной картинки.

Аргумент "filename" это путь к изображению, которое нужно загрузить. "loadedImage" — поверхность в которую изначально загружается изображение. "optimizedImage" — поверхность, которая будет использоваться впоследствии.

    //Загрузить изображение
    loadedImage = SDL_LoadBMP( filename.c_str() );

Перво наперво загружаем изображение при помощи SDL_LoadBMP()

Но не следует использовать его сразу, потому что оно 24-битное. Экран 32-битный и не самой лучшей идеей будет копировать одну поверхность на другую, отличающуюся форматом, потому что SDL будет вынуждена изменять формат на лету, что вызовет падение производительности.

    //Если при загрузке изображения не произошло ничего непредвиденного
    if( loadedImage != NULL ) {
        //Создать оптимизированное изображение
        optimizedImage = SDL_DisplayFormat( loadedImage );
        //Освободить ресурсы из-под старого изображения
        SDL_FreeSurface( loadedImage );
    }

Дальше мы проверяем успешно ли было загружено изображение. Если произошла ошибка, loadedImage будет равно NULL

Если изображение загружено правильно, мы вызываем SDL_DisplayFormat(), которая создаст новую версию "loadedImage" в формате экрана. Причина по которой мы это делаем, заключается в том, что когда вы попытаетесь поместить одну поверхность на другую с отличающимся форматом, SDL преобразует поверхности так, чтобы они были в одном формате.

Создание преобразованных поверхностей каждый раз, когда вы их копируете, в пустую тратит процессорное время, что выливается в пониженную скорость выполнения. Так как мы преобразуем поверхность в момент загрузки, то когда вы захотите перенести ее на экран, они уже будут в одинаковых форматах. И тогда SDL не придется преобразовывать их на лету.

И так, теперь у нас есть две поверхности: старое загруженное изображение и новое оптимизированное.
SDL_DisplayFormat
SDL_DisplayFormat() создала новое оптимизированное изображение, но не избавилась от старого.

Поэтому мы вызываем SDL_FreeSurface() чтобы избавится от старого загруженного изображения.
SDL_FreeSurface

    //Вернуть оптимизированное изображение
    return optimizedImage;
}

Теперь мы возвращаем только что созданное оптимизированное изображение.

void apply_surface( int x, int y, SDL_Surface* source, SDL_Surface* destination ) {
    //Делаем прямоугольник, для временного хранения смещений
    SDL_Rect offset;
    //Кладем смещения в прямоугольник
    offset.x = x;
    offset.y = y;

Это наша функция для копирования поверхностей.

Она принимает координаты места, куда вы хотите скопировать поверхность, поверхность которую вы собираетесь копировать и поверхность на которую вы собираетесь копировать.

В начале мы берем смещения и присваиваем их полям структуры SDL_Rect. Мы делаем это потому, что SDL_BlitSurface() принимает смещения только внутри SDL_Rect.

SDL_Rect — тип данных, представляющий прямоугольник. Он содержит четыре поля, представляющих смещения по X, по Y, а также ширину и высоту прямоугольника. В данном случае нас интересуют только поля x и y.

    //Копировать поверхность
    SDL_BlitSurface( source, NULL, destination, &offset );
}

Теперь мы на самом деле копируем поверхность при помощи SDL_BlitSurface().

Первый аргумент это поверхность, которую мы используем.
Не волнуйтесь по поводу второго аргумента, мы просто пока что присваиваем ему NULL.
Третий аргумент это поверхность на которую мы собираемся копировать.
Четвертый аргумент хранит смещения, по которым наша поверхность (source) будет скопирована на целевую (destination).

int main( int argc, char* args[] ) {

Теперь мы напишем нашу главную функцию — main

При использовании SDL, вам всегда следует использовать:

int main( int argc, char* args[] )

или

int main( int argc, char** args )

Использование int main(), void main() или любого другого типа не сработает.

    //Инициализировать все подсистемы SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 ) {
        return 1;
    }

Тут мы запускаем SDL при помощи SDL_Init()

Мы передаем в SDL_Init() флаг SDL_INIT_EVERYTHING, который запускает все подсистемы SDL. Подсистемы SDL это сущности типа видео, аудио, таймеров и т.д., которые являются отдельными компонентам движка, используемыми для создания игры.

Мы не собираемся использовать все подсистемы, но не будет ничего страшного если мы тем не менее инициализируем их.

Если SDL не удается запуститься, она возвращает -1. В этом случае мы обрабатываем ошибку, возвращая 1, тем самым завершая выполнение приложения.

    //Подготовить экран
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,
        SDL_SWSURFACE );

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

Вы уже знаете зачем нужны первые три аргумента. Четвертый агрумент создает поверхность экрана в системной памяти.

    //Если при подготовке экрана произошла ошибка
    if( screen == NULL ) {
        return 1;
    }

Если при создании экрана что-то пошло не так, screen будет равно NULL

    //Установить заголовок окна
    SDL_WM_SetCaption( "Hello World", NULL );

Здесь мы устанавливаем текст заголовка окна: «Hello World»

Заголовок это часть окна:
Заголовок окна

    //Загрузить изображения
    message = load_image( "hello.bmp" );
    background = load_image( "background.bmp" );

Теперь изображения загружены при помощи функции которую мы сделали.

    //Копировать фоновое изображение на экран
    apply_surface( 0, 0, background, screen );

Теперь пора скопировать фоновое изображение при помощи нашей функции.

До того как мы скопировали фон, экран выглядит так:
Пустой экран
А теперь, когда мы скопировали фоновое изображение, экран в памяти выглядит так:
Фоновое изображение
Когда вы копируете поверхности, вы копируете пиксели с одной поверхности на другую. Поэтому теперь наш экран содержит фоновое изображение в верхнем левом углу, но мы хотим заполнить им все доступное место. Значит ли это что мы должны загрузить фоновое изображение еще три раза?

    apply_surface( 320, 0, background, screen );
    apply_surface( 0, 240, background, screen );
    apply_surface( 320, 240, background, screen );

Нет. Мы просто напросто можем скопировать ту же самую поверхность еще три раза.

    //Скопировать сообщение на экран
    apply_surface( 180, 140, message, screen );

Теперь мы собираемся скопировать поверхность с сообщением на экран со смещениями 180 по оси x и 140 по оси y.
Суть в том, что координатная система в SDL не выглядит как то так:
Декартова система координат
Система координат SDL выглядит так:
Система координат SDL
Поэтому начало координат (0, 0) в левом верхнем углу, вместо левого нижнего.
И поэтому когда вы копируете поверхность сообщения, она скопируется на 180 пикселей правее и на 140 пикселей ниже относительно начала координат в левом верхнем углу:
Пример системы координат
Система координат SDL по началу кажется неудобной, но вы привыкнете к ней.

    //Обновить экран
    if( SDL_Flip( screen ) == -1 ) {
        return 1;
    }

Хотя мы и скопировали наши поверхности, экран по прежнему пуст.

Теперь мы должны обновить экран при помоще SDL_Flip(), чтобы поверхность экрана в памяти совпадала с тем, что изображено на мониторе.

Если при этом произошла ошибка, мы вернем 1;

    //Подождать две секунды
    SDL_Delay( 2000 );

Мы вызываем SDL_Delay() для того, чтобы окно не исчезло через долю секунды, сразу после появления. SDL_Delay() принимает время в миллисекундах, тоесть 1/1000 секунды.

И так, окно будет будет показываться в течение 2000/1000 секунды, проще говоря две секунды.

    //Освободить поверхности
    SDL_FreeSurface( message );
    SDL_FreeSurface( background );
    //Завершаем SDL
    SDL_Quit();
    //Возвращаем 0
    return 0;
}

И наконец мы выполняем очищающие процедуры.
SDL_FreeSurface() используется для того чтобы избавится от загруженных изображений, так как мы больше ими не пользуемся. Если мы не будет освобождать память которую мы использовали, это приведет к утечкам памяти.

Потом SDL_Quit() вызывается для завершения работы SDL. Затем мы возвращаем 0, завершая выполнение программы.

Вы возможно спрашиваете себя, «почему мы не освобождаем поверхность экрана (screen). Не волнуйтесь. SDL_Quit() позаботится об этом для нас.


Если при запуске приложения, изображения не появляются или окно появляется и тут же исчезает, и вы видите в консоли (или в файле stderr.txt):
Ошибка сегментирования или Fatal signal: Segmentation Fault
То это потому, что программа пытается обратится по адресу памяти, по которому ей не следует этого делать. Скорее всего это происходит потому, что apply_surface() попыталась обратится по нулевому указателю (NULL). Это означает что вам нужно убедится что файлы с изображениями находятся в той же директории что и сама программа.

Если окно появляется, но изображений не видно, опять таки, убедитесь что файлы изображений в той же директории что и сама программа или в директории проекта.


Скачать исходники и материалы

Загрузка и копирование оптимизированных поверхностей: 6 комментариев

  1. Никита

    Я, чтобы background вывести, вот такое придумал:
    for (int y=0; y<SCREEN_HEIGHT;y+=256)
    for (int x=0; x<SCREEN_WIDTH;x+=256)
    //Копировать фоновое изображение на экран
    apply_surface( x, y, background, screen );//x, y, что, куда
    где, 256 это высота и ширина картинки с фоном

    А чтобы по центру вывести message:
    apply_surface( SCREEN_WIDTH/2-128, SCREEN_HEIGHT/2-128, message, screen );//x, y, что, куда
    где 128 — это половина высоты(и ширины)

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