Спрайты и их масштабирование
Что такое спрайт я думаю, знают все, ну а те, кто не знает, напомню, это прямоугольная часть изображения, которая храниться в памяти и может быть выведена на экран в любом месте. А вот как это делается, я попытаюсь описать ниже.
Сначала опишем тип спрайта в блоке TYPE
TYPE
TSprite = Record
XRES,YRES:Word;
Data:Pointer;
End;
где
XRES - ширина спрайта
YRES - высота спрайта
DATA - указатель на место в памяти где храниться спрайт
Можно конечно сделать спрайт в виде массива, но тогда мы не сможем создавать спрайты разного размера, а это не очень хорошо. Хотя ограничения будут и здесь, размер данных спрайта в памяти не должен превышать 64Кб. Поэтому создадим некую переменную, скажем SPRITE_ERROR типа BOOLEAN которая будет нам говорить об ошибке создания спрайта.
VAR SPRITE_ERROR:Boolean;
Этой переменной мы можем пользоваться всегда, когда мы хотим быть уверенными в том, что спрайт создан верно.
Теперь описав тип спрайта, посмотрим, что нам нужно с ним делать. Для работы со спрайтами нам понадобиться:
Прозрачность спрайтов
Спрайт это прямоугольная область и вывод всех точек спрайта приведет к некоторым нежелательным результатам, т.е. если у вас есть задний фон то либо изображение спрайта должно быть таким же квадратным как спрайт, либо пустые точки закрасят фон. В палитровых (256 цветов) режимах прозрачность делается следующим образом, один цвет палитры просто не рисуется, он и является прозрачностью (зачастую это нулевой цвет). В не палитровых режимах (True Color и Hi Color) для этого дела выделяется бит, который указывает на то, что точку не рисовать. Хорошо для этого дела подходят 15 Битные видео режимы, там на каждый компонент цвета уходит по 5 бит, и 16 бит остается неиспользованным, его мы и используем для канала прозрачности.
Да еще, выше речь идет о простой прозрачности спрайта, то есть либо ставить точку, либо нет. Если у нас появилась надобность рисовать полупрозрачный спрайт то нам придется выделить еще один байт, в котором мы укажем, насколько прозрачным является данный пиксель спрайта.
Резервирование
памяти под спрайт
Выше мы решили, что наш спрайт храниться в динамической памяти и размеры его
будут ограничиваться только размером одного сегмента памяти компьютера (не более
64Кб). Для резерва памяти под спрайт напишем следующую процедуру.
Procedure
NewSprite(Xres,Yres:Word;Var Sp:TSprite);
Begin
{Проверяем на выход размера спрайта за приделы сегмента}
If Xres * Yres <= $FFFF Then Begin{Задаем ширину и высоту спрайта}
Sp.Xres:=Xres;
Sp.Yres:=Xres;
{Выделяем динамическую память}
GetMem(Sp.Data,Sp.Xres*Sp.Yres);
SpriteError:=False; {говорим что ошибки не было}End Else SpriteError:=True; {иначе спрайт не создан}
End;
Этот пример прекрасно подходит для палитровых режимов (256 цветов) т.к. на одну точку у нас уходит один байт. Для режима Hi Color нам нужно будет на одну точку отводить два байта, а в режимах True Color 3 байта плюс прозрачность (4 байта). Поэтому нам под спрайт нам нужно будет выделять память по следующей формуле Xres*Yres*N, где N - количество байт на точку.
Для 15 битных режимов это будет выглядеть так:
Procedure
NewSprite(Xres,Yres:Word;Var Sp:TSprite);
Begin
{Проверяем на выход размера спрайта за приделы сегмента}
If Xres * Yres*2 <= $FFFF Then Begin{Задаем ширину и высоту спрайта}
Sp.Xres:=Xres;
Sp.Yres:=Xres;
{Выделяем динамическую память}
GetMem(Sp.Data,Sp.Xres*Sp.Yres*2);
SpriteError:=False; {говорим что ошибки не было}End Else SpriteError:=True; {иначе спрайт не создан}
End;
К сожалению, спрайт получается в два раза меньше, зато работает. Хотя вы все еще спокойно можете создать спрайт размером 127x255. А вот в True Color'е максимальный спрайт может быть 128x128. Решить эту проблему можно следующим образом, используя карту цветов, то есть грузим палитру 256 цветового режима в память, и точки в спрайте используем как индексы в палитре, теперь спрайт может быть размером как в палитровых режимах, но появляется ограничение по цвету, хотя я думаю нам это не существенно. Тогда создаем спрайт по старой схеме, но не забываем подгружать цветовую карту.
Освобождение памяти
занятой спрайтом.
Зачем это, я думаю не стоит объяснять, а делается это просто.
Procedure
DelSprite(Var Sp:TSprite);
Begin
Sp.Xres:=0;
Sp.Yres:=0;
FreeMem(Sp.Data,Sp.Xres*Sp.Yres*2);
End;
Получение спрайта
с экрана.
Получить спрайт с экрана можно двумя способами. Первый работает относительно
медленно, потому что используем цикл FOR и каждый раз производит умножение для
каждой точки. Другой более длинный, зато более быстрый.
Вариант 1й(простой)
Procedure
GetSprite(X,Y:Integer;Sp:TSprite);
Var I,J:Integer;
Begin
For J:=0 to Sp.Yres-1 do
For I:=0 to Sp.Xres-1 doMem[Seg(Sp.Data^):Ofs(Sp.Data^)+J*Sp.Xres+I]:=GetPix(I+X,J+Y);
End;
Вариант 2й(оптимизированный)
Procedure
GetSprite(X,Y:Inetger;Sp:TSprite);
Var I,J:Integer;
Col:Byte;
P:^Byte;
W:^Word;
Begin
{обнуляем счетчики}
I:=0;
J:=0;
{делаем дублирующий указатель на Sp.Data при этом как указатель на байт, заодно смотрим а есть ли мальчик (спрайт в памяти), че брать, если некуда положить}
If Sp.Data = Nil Then Exit Else P:=Sp.Data;
{Делаем указатель на сам указатель, при этом указываем на него как на тип WORD. Теперь мы можем смело увеличивать смещение указателя P}
W:=@P;
Repeat{получаем точку}
P^:=GetPix(i+X,j+Y);
Inc(W^); {Переходим на следующую точку в спрайте}
{пересчет шагов}
Inc(I);
If I=Sp.Xres then beginI:=0;
Inc(J);End;
Until J=Sp.Xres;
End;
Вывод спрайта
Вывод такой же, как и получение спрайта, разница в том, что вместо получения
точки, мы ее ставим.
Приведем теже два
примера.
Вариант 1й(простой)
Procedure
PutSprite(X,Y:Integer;Sp:TSprite);
Var I,J:Integer;
Begin
For J:=0 to Sp.Yres-1 do
For I:=0 to Sp.Xres-1 do
{Вот и вся разница}
If Mem[Seg(Sp.Data^):Ofs(Sp.Data^)+J*Sp.Xres+I] <> 0 Then
Pix(I+X,J+Y,Mem[Seg(Sp.Data^):Ofs(Sp.Data^)+J*Sp.Xres+I]);
End;
Вариант 2й(оптимизированный)
Procedure
PutSprite(X,Y:Inetger;Sp:TSprite);
Var I,J:Integer;
Col:Byte;
P:^Byte;
W:^Word;
Begin
I:=0;
J:=0;
If Sp.Data = Nil Then Exit
Else P:=Sp.Data;
W:=@P;
Repeat{Ставим точку}
If P^<>0 then Pix(i+X,j+Y,P^); {<<< Вот и вся разница}
Inc(W^);
Inc(I);
If I=Sp.Xres then beginI:=0;
Inc(J);End;
Until J=Sp.Xres;
End;
Сохранение и загрузка
спрайта в файл
Тут стоит подумать о будущем, то есть введем некоторое понятие формата файла,
где сначала будет идти заголовок, а потом данные. В заголовке мы сохраним следующие
вещи, Type - 1 байт, Xres, Yres - по два байта, ну и в поле данных поместим
сам спрайт. А сделаем это так.
Параметры ф-ции.
Fname - имя файла
Fofs - смещение от начала файла (для простых спрайтов 0)
Sp - сам спрайт
Function
SaveSprite(Fname:String;Fofs:LongInt;Sp:TSprite):LongInt;
Var F:File;
Tp:byte;
Begin
{смотрим а есть ли у нас что записать}
If Sp.Data = Nil Then BeginSpriteError:=True;
SaveSprite:=Fofs;
Exit;End Else SpriteError:=False;
Assign(F,Fname);
{смотрим, а вдруг файл у нас уже есть}
{$I-}Reset(F,1);{$I+}
{если нет, то создаем новый}
If IoResult <> 0 then Rewrite(F,1);
{смещаемся на нужное место}
Seek(Fofs);
Tp:=1;
{Задаем тип спрайта, предположим 1 это спрайт для палитровых режимов}
BlockWrite(F,Tp,1);
{Пишем ширину и высоту}
BlockWrite(F,Sp.Xres,2);
BlockWrite(F,Sp.Yres,2);
BlockWrite(F,Sp.Data^,Sp.Xres*Sp.Yres);
{текущее положение нам может пригодиться}
SaveSprite:=FilePos(F);
{Закрываем спрайт}
Close(F)
End;
Теперь дальше, загрузка спрайта
Function
LoadSprite(Fname:String;Fofs:LongInt;Var Sp:TSprite):LongInt;
Var F:File;
Tp:byte;
Begin
Assign(F,Fname);
{открываем файл и смотрим, а есть ли он у нас }
{$I-}Reset(F,1);{$I+}
If IoResult = 0 then Begin{смещаемся на нужное место}
Seek(Fofs);
{проверяем тип спрайта}
BlockRead(F,Tp,1);
If Tp<>1 Then BeginSpriteError:=True;
Exit;End;
{читаем ширину и высоту}
BlockRead(F,Sp.Xres,2);
BlockRead(F,Sp.Yres,2);
{создаем новый спрайт}
NewSprite(Sp.Xres,Sp.Yres,Sp);
{читаем данные}
BlockRead(F,Sp.Data^,Sp.Xres*Sp.Yres);
{текущее положение нам может пригодиться}
LoadSprite:=FilePos(F);
{Закрываем спрайт}
Close(F);
SpriteRrror:=False;
End Else Begin
{если файл не найден}
SpriteError:=True;
LoadSprite:=Fofs;End;
End;
Вот и все, правда есть один маленький баг, запись спрайта не удаляет файл если он есть, но это я сделал нарочно, для того, чтобы в последствии можно было создавать библиотеки спрайтов. Для создания спрайтов также была введена переменная Fofs, которая задает смещение в файле, и также обе функции я описал в виде Function(…):LongInt а не в виде Procedure. Идея тут в следующем. Предположим записав спрайт в файл, нам нужно записать за ним еще один, тут и срабатывают все хитроумные свойства. А делается это так.
{Предположим
у нас есть 10 спрайтов}
Var Spr : Array [1..10] of TSprite;
SS : LongInt;
Begin
...
{Предположим мы получили спрайты с экрана, и нам нужно их все сохранить в один файл, делаем цикл от 1 до 10}
SS:=0;
For i:=1 to 10 do{сохранили I-тый спрайт, а SS получает новое смещение, как раз за спрайтом}
SS:=SaveSprite('Sp.dat',SS,Spr[i]);
End;
Опа как !!!
Масштабирование
спрайта (растягивание)
Ну вот и последняя часть сказочки. Мы умеем брать, ставить, сохранять, грузить
и т.д. и т.п. Теперь начинаются извращения, нам нужно натянуть спрайт на весь
экран. Как это сделать? А просто.
Описывать в лом (пальцы устали) скажу кратко, находим отношение между сторонами спрайта и сторонами новой области, идем не по спрайту, а по области и находим нужные точки в спрайте, если сделать на оборот то получатся дырки. Вообще, по моему, это в школе проходят в той или иной степени толи по геометрии, толи по алгебре.
Вот вам процедура
вывода натянутого спрайта.
P.S. ни какой билинейной фильтрации.
Procedure
SPutImage(X,Y,X1,Y1:Integer;Im:TImage);
Var i,j:Integer;
SX,SY,XC,YC:Real;
YP,XR,YR:Integer;
A:Byte;
SG,FS:Word;
Begin
{смотрим, а нам это надо}
If Sp.Data=Nil then Exit;
{смотрим на кривость введенных координат}
If X>X1 Then Begin I:=X;X:=X1;X1:=I End Else If X=X1 then X1:=X+1;
If Y>Y1 Then Begin I:=Y;Y:=Y1;Y1:=I End Else If Y=Y1 then Y1:=Y+1;
{находим ширину и высоту нашей области}
XR:=X1-X;YR:=Y1-Y;
{находим отношения сторон спрайта, к сторонам области}
XC:=(Im.Xres-1) / XR;
YC:=(Im.Yres-1) / YR;
{обнуляем счетчик}
SX:=0;SY:=0;
{берем адрес спрайта}
SG:=Seg(Im.Data^);
FS:=Ofs(Im.Data^);
{поехали сначало по Y}
For j:=0 to YR do Begin{находим Y, и все для него (пытаемся оптимизировать)}
YP:=FS+Round(SY) * Im.Xres;
{смещение в спрайте}
SX:=YP;
{теперь цикл по X}
For i:=0 to XR do Begin{взяли нужный цвет}
A:=Mem[SG:Round(SX)];
{поставили точку, если она не прозрачная}
If A>0 then Pix(i+X,j+Y,A);
{сместились в спрайте на отношение по X}
SX:=SX+XC;End;
{сместились по Y на отношение по Y}
SY:=SY+YCEnd;
End;
P.S. по алгебре
у меня всегда была 3, поэтому если я что-то неправильно описал, то извините,
а лучше напишите, ну а самое интересное то, что до этой процедуры я дошел сам.
(а вот по Русскому у меня было угадайте что :) <4>).