Бесплатная реклама

Автор: Евгений Бронников

Опубликовано: 06.11.2007

Изменено: 06.11.2007

Постоянная ссылка

Комментарии [0]

Графические спецэффекты в SDL


В статье описаны способы реализации графических спецэффектов в 2D программах, при помощи кросс-платформенной библиотеки SDL. В прилагаемых исходниках содержится код для реализации эффекта отражения, осветления, затемнения, растворения и изменения насыщенности картинки.



Страницы: 1 2

Disclaimer

Данный документ распространяется «как есть» без всяких гарантий и поддержки. Автор не несет ответственность за содержание данного документа, поэтому используйте его на свой страх и риск. Документ может распространяться в любых целях (исключая коммерческие) и любым способом совершенно свободно и бесплатно, но с небольшими ограничениями - обязательно должен быть указан автор статьи и первоначальный источник, т.е. сайт http://plg.lrn.ru.

Содержание

1. SDL_Surface изнутри
2. Зеркальное отражение
3. Осветление и затемнение
4. Насыщенность цвета
5. Растворение
6. Примечания (исходный код)

1. SDL_Surface изнутри

Прежде чем начать разработку спецэффектов, необходимо хорошо разобраться со структурой SDL_Surface. Интересующие нас поля представлены ниже:

typedef struct SDL_Surface
{
        SDL_PixelFormat* format;             /* Только чтение */
        int w, h;                            /* Только чтение */

        Uint16 pitch;                        /* Только чтение */
        void* pixels;                        /* Только чтение */

} SDL_Surface;
Листинг 1.1. Объявление полей структуры SDL_Surface.

Поля w и h содержат размеры изображения. Поле pitch содержит ширину поверхности в байтах. Это значение не является шириной изображения в пикселях, т.к. каждый пиксель содержится в нескольких байтах (от 2-х до 4-х) для не индексированных изображений. Таким образом, поле pitch содержит значение: ширина * кол-во байт на 1 пиксель. Указатель pixels содержит адрес первого пикселя в изображении. В отличие от других полей, pixels доступно на чтение и запись, то есть, вы можете изменять изображение, которое содержится в структуре SDL_Surface, модифицируя его по-пиксельно.

Изображения, хранимые в SDL_Surface, могут иметь разное значение глубины цвета: 8 бит (индексированные изображения) и 15 бит, 16 бит, 24 бита и 32 бита для кодирования цвета одного пикселя (RGB). Поле format содержит необходимую информацию для работы с пикселями изображения:

typedef struct SDL_PixelFormat
{
        SDL_Palette* palette;
        Uint8 BitsPerPixel;
        Uint8 BytesPerPixel;
        Uint8 Rloss, Gloss, Bloss, Aloss;
        Uint8 Rshift, Gshift, Bshift, Ashift;
        Uint32 Rmask, Gmask, Bmask, Amask;
        Uint32 colorkey;
        Uint8 alpha;
} SDL_PixelFormat;
Листинг 1.2. Объявление структуры SDL_PixelFormat.

В дальнейшей работе мы не будем использовать индексированные изображения, поэтому не будем рассматривать поле palette. Поля BitsPerPixel и BytesPerPixel содержат, соответственно, количество бит и количество байт на один пиксель изображения. Поля colorkey и alpha содержат значение пикселя цветового ключа и значение прозрачности поверхности.

Поля [RGBA]mask содержат маску для извлечения индивидуальных цветовых компонентов из пикселя. Каждый цветовой компонент обычно размещается в одном байте (8 бит). Обычно для работы с пикселями изображения используют тип Uint32 (4 байта), например, в функциях SDL_GetRGB[A] и SDL_MapRGB[A]. Значение маски содержит единички в битах, содержащих соответствующий цвет и нули для битов, содержащих остальные цвета. Для того, чтобы изолировать цветовые компоненты из пикселя можно воспользоваться значением полей [RGBA]mask – выполнить побитовую операцию AND над пикселом.

Поля [RGBA]shift содержат смещение для соответствующего цветового компонента внутри 32-битного пикселя. На это значение необходимо сместить вправо (к младшим битам) полученное после изоляции (см. выше) значение каждого цветового компонента.

Поля [RGBA]loss содержат значение точности потери битов при кодировании цвета: 2 в степени loss для каждого цветового компонента. Эти значения актуальны для изображений с размером пикселя в 2 байта (глубина цвета 16 бит). Каждый пиксель хранит значение каждой цветовой компоненты (RGB), но в 16-ти битных изображениях для трех значений RGB выделяется только 2 байта, поэтому значения цветовых компонент «пакуются», чтобы их можно было разместить в двух байтах. При упаковке значений цветовых компонент отбрасываются младшие биты. Для 15-ти битной глубины цвета, каждый компонент пикселя кодируется в 5-ти битах (rgb555). Соответственно, поля loss для этого режима содержат значения 3 – при кодировании цвета пикселя отсекаются 3 младших бита из байта, кодирующего значение отдельного цветового компонента. Для 16-ти битного режима (RGB565) на кодирование красной и синей компоненты выделяется по 5 бит, а на кодирование зеленой компоненты 6 бит. Таким образом, поля Rloss и Bloss содержат значение 3, а поле Gloss – значение 2. Для пикселей с глубиной цвета 24 и 32 бита, каждый цветовой компонент кодируется в одном байте (RGB для 24-битных и RGBA для 32-битных – rgb888 и rgba888), поэтому значения [RGBA]loss для них можно не использовать, так как они нулевые.

В качестве примера, использующего вышеперечисленные поля, внимательно рассмотрите листинг, представленный ниже:

/* Извлечение цветовых компонент из 32-битного пикселя */
SDL_PixelFormat *fmt;
SDL_Surface *surface;

Uint32 temp, pixel;
Uint8 red, green, blue;
.
.
fmt = surface->format;
SDL_LockSurface(surface);
pixel = *((Uint32*)surface->pixels); /* первый пиксель */
SDL_UnlockSurface(surface);


/* Получение красного компонента */
temp = pixel & fmt->Rmask;  /* Изолируем */

temp = temp >> fmt->Rshift; /* Сдвигаем к младшим битам */
temp = temp << fmt->Rloss;  /* Расширяем значение до 8-ми бит */
red = (Uint8)temp;

/* Получение зеленого компонента */
temp = pixel & fmt->Gmask;  /* Изолируем */

temp = temp >> fmt->Gshift; /* Сдвигаем к младшим битам */
temp = temp << fmt->Gloss;  /* Расширяем значение до 8-ми бит */
green = (Uint8)temp;

/* Получение зеленого компонента */
temp = pixel & fmt->Bmask;  /* Изолируем */

temp = temp >> fmt->Bshift; /* Сдвигаем к младшим битам */
temp = temp << fmt->Bloss;  /* Расширяем значение до 8-ми бит */
blue = (Uint8)temp;


/* Распечатываем значения цветовых компонент пикселя */
printf("Pixel -> R: %d,  G: %d,  B: %d", red, green, blue);
Листинг 1.3. Извлечение цветовых компонент из пикселя.

Чтобы не выполнять данную процедуру вручную, можно воспользоваться функциями SDL_MapRGB / SDL_MapRGBA и SDL_GetRGB / SDL_GetRGBA

Функция SDL_MapRGB возвращает 32-битное значение пикселя, построенное по переданным в качестве параметров цветовым компонентам. Объявление этой функции выглядит следующим образом:

Uint32 SDL_MapRGB(SDL_PixelFormat* fmt, Uint8 r, Uint8 g, Uint8 b);

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

void SDL_GetRGB(Uint32 pix, SDL_PixelFormat* fmt, Uint8* r, Uint8* g, Uint8* b);
Значения цветовых компонент доступны по адресам, переданные в качестве параметров.

Еще один важный момент, который необходимо знать, это итерация пикселей изображения – перебор в цикле. Для разной глубины цвета пиксели кодируются различным количеством байт – от двух до четырех (мы не рассматриваем индексированные поверхности SDL, в которых поле pixels содержат индексы массива палитры). Для 15 и 16 битных изображений для итерации можно использовать типы данных unsigned char или Uint8 с шагом цикла 2 или Uint16 с шагом 1. Для 32-битных изображений мы будем использовать тип данных Uint32 с шагом цикла 1, а для 24-битных изображений unsigned char или Uint8 с шагом цикла 3. Указанные типы данных будут использоваться в примерах ниже. Для прямого доступа к пикселям SDL_Surface необходимо устанавливать блокировку:

if(SDL_MUSTLOCK(surface))
	SDL_LockSurface(surface);

/* Прямой доступ к пикселям */

if(SDL_MUSTLOCK(surface))
	SDL_UnlockSurface(surface);
Листинг 1.4. Блокировка SDL_Surface для прямого доступа к пикселям.

Перед установкой и снятием блокировки можно выполнять макрос SDL_MUSTLOCK, который возвращает значение 0, если поверхность не нужно подготавливать для прямого доступа к пикселям. Будьте внимательны с порядком вызова SDL_LockSurface и SDL_UnlockSurface: эти функции выполняются рекурсивно, поэтому снимать блокировку нужно в обратном порядке.

При загрузке изображения в SDL_Surface (например при помощи функции SDL_LoadBMP), формат пикселей поверхности устанавливается в соответствии с форматом изображения. Но экранная поверхность, возвращаемая функцией SDL_GetVideoSurface, и, соответственно, глубина цвета экрана SDL-приложения, могут не совпадать с форматом поверхностей, загруженных из файлов. При рисовании таких поверхностей тратится достаточное количество ресурсов на преобразование формата поверхности к экранному, если они не совпадают. К тому же, становится достаточно сложно и громоздко работать сразу с двумя поверхностями в разных форматах при тесном взаимодействии с друг другом, как, например, при реализации эффекта растворения двух изображений, рассмотренном ниже. Для решения этой проблемы существует функция SDL_DisplayFormat, которая принимает в параметрах поверхность и возвращает ее точную копию, преобразованную к формату экрана. В дальнейшей работе мы постоянно будем использовать эту функцию.

Все алгоритмы, приведенные в статье, будут работать только на системах Little Endian.

2. Зеркальное отражение

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

Начнем с наиболее простого отражения – вертикального. В предыдущем разделе упоминалось поле структуры SDL_Surface - pitch, которое содержит ширину изображения в байтах. Эта ширина является строкой, а высота (то есть количество строк) – высота изображения в пикселях. Таким образом, нужно сформировать новую поверхность, строки которой будут расположены в обратном порядке. Алгоритм очень простой: устанавливаем два указателя: один на первую строку исходной поверхности, а другой на последнюю строку новой поверхности. В цикле, с количеством итераций равным высоте изображения, копируем каждую строку с начала исходной картинки в конец новой и обновляем указатели – указатель на исходное изображение увеличиваем на длину строки, а указатель на новое изображение уменьшаем на длину строки.

/* Устанавливаем указатели: */

/* на первую строку исходной картинки */
char* psrc = (char*)src->pixels;


/* на последнюю строку новой картинки */
char* pdst = (char *)dst->pixels + (dst->h - 1) * dst->pitch;

/* Копируем в цикле строки с начала исходной картинки */
/* в конец новой картинки: */
for(i = 0; i < dst->h; i++)
{
	/* копируем одну строку */
	memcpy((void*)pdst,(void*)psrc, dst->pitch);

	/* Обновляем указатели */

	pdst -= dst->pitch; /* на предыдущую строку */
	psrc += src->pitch; /* на следующую строку */
}
Листинг 2.1. Реализация вертикального зеркального отражения.

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

Следующий вид зеркального отражения - горизонтальное - более сложен и требует больше ресурсов, поскольку необходимо по-пиксельное копирование из исходной картинки в зеркальную копию. В этом случае мы также будем использовать строки. Нам необходимо по-пиксельно скопировать содержимое строки исходного изображения в его зеркальную копию, поэтому копирование происходит в обратном порядке – с начала исходной строки в конец зеркальной. Как только все пиксели из строки скопированы, нужно переместить указатель зеркальной копии на конец следующей строки. Указатель исходного изображения при этом уже обновлен и указывает на начало следующей строки. Более наглядно этот алгоритм объясняет код и комментарии к нему:

/* Устанавливаем указатели: */

/* на первый пиксель исходной картинки */
char* psrc = (char*)src->pixels;

/* на последний пиксель первой строки новой картинки */
char* ptemp = (char*)dst->pixels + (dst->w-1) * bpp;

/* запоминаем кол-во байт на 1 пиксель */

int bpp = dst->format->BytesPerPixel;

/* для каждой строки */
for(i = 0; i < src->h; i++)
{
	/* для каждого пикселя из строки */
	for(j = 0; j < src->w; j++)
	{
		/* копируем по одному пикселю с начала строки */
		/* исходной картинки в конец строки новой картинки*/
		memcpy((void*)pdst, (void*)psrc, bpp);
		pdst -= bpp;  /* переход на предыдущий пиксель */

		psrc += bpp;  /* переход на следующий пиксель */
	}

	/* переход в конец следующей строки новой картинки */
	pdst += (dst->pitch) * 2;
}
Листинг 2.2. Реализация горизонтального зеркального отражения.

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

/* Инициализируем поверхности, приводя их к формату экрана */
SDL_Surface* src = SDL_DisplayFormat(surface);
SDL_Surface* dst = SDL_DisplayFormat(surface);

/* Подготавливаем к прямому доступу к пикселям */
if(SDL_MUSTLOCK(dst)) SDL_LockSurface(dst);
if(SDL_MUSTLOCK(src)) SDL_LockSurface(src);

/* Выполняем копирование пикселей */
...

/* Снимаем блокировку */

if(SDL_MUSTLOCK(src)) SDL_UnlockSurface(src);
if(SDL_MUSTLOCK(dst)) SDL_UnlockSurface(dst);

/* Освобождаем копию исходной поверхности */
SDL_FreeSurface(src);
Листинг 2.3. Подготовка к копированию пикселей.

Этот код будет работать для поверхностей с любым форматом пикселей (в исходном коде к статье не используется 8-битный режим). Для больших изображений создание зеркальной копии может занять достаточно большой промежуток времени, поэтому рекомендуется выполнять зеркалирование при загрузке изображений, подготавливая зеркальные копии заранее. Применение эффекта в процессе отрисовки каждого кадра не является хорошей идеей, поскольку будет очень сильно замедлять выполнение программы.

Страницы: 1 2