Главная » 2012»Февраль»20 » Assembler Управляемая градиентная спираль на ассемблере в 256 байт (k29)
14:03
Assembler Управляемая градиентная спираль на ассемблере в 256 байт (k29)
Управляемая градиентная спираль на ассемблере в 256 байт (k29)
Эта статья посвящена созданию на ассемблере графического приложения
весом в несколько сотен байт. После создания полноценной рабочей версии
на 420 байт пришлось попотеть, чтобы запихать всё это добро в 256 байт.
Результат вы можете лицезреть на видео. В статье описывается процесс
создания и общие принципы функционирования.
Предупреждение: Если вы страдаете приступами эпилепсии — НЕ СМОТРИТЕ.
===================================================
Клавиши управления:
1. R,G,B — включение и отключение цветовых компонент
2. <--,SPACE,--> — менять направление и скорость вращения
3. UP, DOWN — менять масштаб спирали
4. 0,1,2,3,4,5,6,7,8,9 — менять число ветвей у спирали
5. ESC — выход
Эпилог
Уже довольно долго эта статья валяется у меня в черновиках. Ещё в
черновиках на Blogger-е валялась. И сегодня я решил, что если не допишу
её сейчас — это не произойдёт никогда. Дописал, в конце немного оборвал и
закончил) Ура!!!
1. Завязка
Меня всегда интересовали работы с demoparty, особенно категории
исчисляющиеся в сотнях байт. Одно дело писать прогу в 64 кило, используя
DirectX/OpenGL, и совсем другое — в 512/256/128 байт, используя
видеопамять напрямую и т.д. Тут необходимо знание и понимание ассемблера
уже на интуитивном уровне. Необходимо уметь оптимизировать код по
объёму, а значит понимать и учитывать все тонкости машинного языка
программирования. Мы попробуем сделать что-то подобное. Я никогда раньше
не писал на ассемблере, но разобраться в существующем коде получалось.
Будем считать, что данная статья ориентирована на новичков в ассемблере вроде меня. Потому не претендую на идеальное решение задачи.
2. Выбор цели
Теперь нужно придумать себе задачу и воплотить её. Мне на ум сразу
пришла небольшая программа, написанная в школьные годы на языке Pascal.
Программа была проста до безобразия (2 экрана кода — 50 строк), но в то
же время она доставляла. Программа рисовала в графическом режиме
(320x200x256) во весь экран вращающуюся спираль. Были задействованы все
256 цветов, для плавного изменения цвета. Было удивительно, что спираль
движется без видимой перерисовки. Это можно было бы объяснить
использованием нескольких видеостраниц, если бы не скорость вращения.
Очевидно, что для отрисовки спирали необходимы вычисления с
вещественными числами, что тоже вносит значительную задержку. Спираль
вращалась со скорость 3-5 оборотов в секунду (см. рис. 2.1).
[ Рис. 2.1. Снимок спирали с тремя «руками» ]
А вся фишка была в том, что спираль рисовалась всего один раз — при
запуске программы. После отрисовки спирали программа циклически сдвигала
цвета в палитре, что незамедлительно отображалось на экране. Помимо
основной функции программа поддерживала изменение цвета спирали и
изменение направление вращения. Всего восемь цветов:
0
#000000
Чёрный (спираль не видно)
1
#0000FF
Голубой
2
#00FF00
Ярко-зелёный
3
#00FFFF
Бирюзовый
4
#FF0000
Красный
5
#FF00FF
Пурпурный
6
#FFFF00
Желтый
7
#FFFFFF
Белый
Так как для отображения на экране используется цветовая модель RGB, то
эти восемь цветов можно получать, комбинируя соответствующие цветовые
компоненты (3 бита данных могут кодировать 8 различных значений).
Программа использовала клавиши 'R', 'G' и 'B' для вкючения/отключения
соответствующих цветовых компонент.
Программа была написана на языке Pascal в среде разработки Turbo Pascal
7.0. Некоторые функции программы были реализованы в виде ассемблерных
вставок. Например, функция установки RGB-значения конкретному элементу
из палитры. Код школьных времен (форматирование изменено чтобы не
травмировать ничью психику):
program P_16_B_4;
uses
crt,graph,MUSE_OTH,MT_MAIN;
const
koef1=3;{ Количество вихрей }
koef2=3;{ Плотность вихрей }
var
gd,gm,gmx,gmy,i,flag,x0,y0,x,y:integer;
r,alpha:extended;
k,int:longint;
key:char;rr,gg,bb:byte;
ints:string;
mas:array[0..255,1..3] of byte;
BEGIN
gd:=installuserdriver('SVGA256m.BGI',NIL);gm:=2;
initgraph(gd,gm,'');
gmx:=getmaxx;
gmy:=getmaxy;
flag:=1;k:=1;
for i:=0 to 255do
begin
setRGBpalette(i,k,k,k);
mas[i,1]:=k;mas[i,2]:=k;mas[i,3]:=k;
k:=k+flag;
if k=63then flag:=-1elseif k=0then flag:=1;
end;
setcolor(63);
settextstyle(1,horizdir,2);
settextjustify(centertext,centertext);
outtextxy(gmx div 2,gmy div 2-textheight('!<06@')*2 div 2,
'!<06@ freeware');
outtextxy(gmx div 2,gmy div 2+textheight('!<06@') div 2,
'! ! ! Press "R", "G", "B", " " or "Esc" ! ! !');
x0:=gmx div 2;
y0:=gmy div 2;
r:=400;
repeat
alpha:=0;
repeat
x:=round(x0+r*cos(alpha/180*Pi));
y:=round(y0-r*sin(alpha/180*Pi));
putpixel(x,y,round(r*koef2+alpha*256/360*koef1/2) mod 128);
alpha:=alpha+20/(r+1);
until alpha>=360;
if keypressed then halt;
r:=r-1;
until r<=0;
k:=1;flag:=-1;rr:=1;gg:=1;bb:=1;int:=0;
repeat
str(int SHR 2,ints);
while byte(ints[0])<4do insert('0',ints,1);
if int and 3=0then
SAVE_MONITOR((gmx+1) div 2-75,(gmy+1) div 2-75,(gmx+1) div 2+74,
(gmy+1) div 2+74,'c:\AVATAR\'+ints+'.bmp');
if keypressed then
begin
key:=readkey;
if key=' ' then flag:=-flag elseif upcase(key)='R' then rr:=not(rr) and 1elseif upcase(key)='G' then gg:=not(gg) and 1elseif upcase(key)='B' then bb:=not(bb) and 1elseif key=#27 then break;
end;
for i:=0 to 127do
setRGBpalette(i,mas[(i+k+512) mod 256,1]*rr,
mas[(i+k+512) mod 256,2]*gg,
mas[(i+k+512) mod 256,3]*bb);
inc(k,flag);k:=k mod 256;
inc(int);
until false;
closegraph;
END.
3. Разработка алгоритма
Сперва разберёмся в том, как функционирует программа и какие функции нам потребуется написать. Формальное описание алгоритма:
1) Начальное заполнение палитры следующими значениями: (0,0,0)...
(63,63,63)... (0,0,0). Иными словами, на протяжении 256-ти элементов
палитры цвет плавно меняется от черного к белому и снова возвращается к
черному. В данном графическом режиме поддерживается до 256 цветов,
каждый из цветов состоит из трёх цветовых компонент. Каждая из цветовых
компонент задаётся шестью битами (число от 0 до 63). Белому цвету
соответсвует вектор цветовых компонент (63,63,63), а чёрному
соответственно — (0,0,0).
2) Отрисовка спирали включает в себя проход по всем пикселям экрана и
заполнение их данными. Формула спирали достаточно проста — зависит лишь
от вектора (пара значений: расстояние и угол), указывающего на
конкретный пиксель из центра экрана. Иными словами цвет зависит только
от длины вектора и угла вектора с подобранными коэфициентами. Перебирая
различные коэффициенты можно получить как различное число «ветвей» у
спирали, так и различную степень её закрученности.
3) Циклический сдвиг палитры на одну позицию. Это и даёт иллюзию
вращения спирали. То есть, изменяя 256 элементов цветов, мы получаем
сдвиг спирали на 1/(256*k) полного оборота. Где k — количество «ветвей»
спирали. Таким образом мы избежали перерисовки всех пикселей экрана для
вращения спирали. На самом деле сдвигать мы будем не на «одну» позицию,
величина и направление сдвига хранятся в специальной целочисленной
переменной. Это позволит нам динамически менять направление и скорость
вращения спирали.
4) Проверка нажатий клавиш. Нажатие клавиш R, G и B ведет к включению,
либо отключению закреплённой за каждой из клавиш цветовой компоненты.
Нажатие на клавиши «вправо»/«влево» увеличивает/уменьшает значение
переменной, которая явным образом используется при сдвиге палитры.
Переход на пункт 3.
4. Реализация алгоритма
Чтож, теперь определим, какие функции нам потребуется написать на
ассемблере. Очевидно, это будут: функция первоначального заполнения
палитры, функция отрисовки спирали, функция циклического сдвига палитры и
главная функция программы, которая помимо вызова вышеперечисленных
функций будет обеспечивать проверку нажатий на клавиатуре управляющих
клавиш. Блок-схема алгоритма:
[ Рис. 4.1. Блок-схема алгоритма ]
Прочитав об имеющихся компиляторах языка ассемблер в википедии и книге
«Искусство дизассемблирования» Криса Касперски и Евы Рокко, я сделал
свой выбор в пользу компилятора FASM. Код буду писать в Notepad++, из
него же и буду экспортировать в статью с подсветкой синтаксиса.
Ну чтож, приступим.
4.1. Функция первоначального заполнения палитры
Мы собираемся заполнять палитру следующими значениями: (0,0,0),...
(63,63,63),... (0,0,0). Нетрудно посчитать, что их всего 127, для
равномерного заполнения всех 256 элементов будем заполнять по 2
одинаковых элемента: (0,0,0), (0,0,0),... (63,63,63), (63,63,63),...
(0,0,0), (0,0,0). Запишем алгоритм на формальном языке высокого уровня:
for (int i = 0; i <= 127; i++)
{
setPalette(i, i/2, i/2, i/2);
setPalette(255-i, i/2, i/2, i/2);
}
Теперь ассемблерный код с комментариями:
A
B
C
D
A. Сохраняем в стеке и восстанавливаем из стека регистры, которые используются в функции. B. Регистр CX пробегает в цикле от 0 до 127 включительно. C. Формируем параметры и вызываем функцию setPalette. В AL
загружаем индекс цвета, в AH загружаем значение яркости, равное половине
индекса. D. Меняем индекс на (255-i) используя операцию инверии всех битов и вызывает функцию setPalette.
4.2. Функция отрисовки спирали
Подумаем над функцией описания градиентной спирали.
Функция зависящая только от радиуса даёт градиентные окружности:
pixel[x][y] = k1*sqrt(x*x + y*y);
[ Рис. 4.1. Градиентные окружности ]
Функция зависящая только от угла даёт градиентные лучи из центра:
pixel[x][y] = k1*arctan(x/y);
[ Рис. 4.2. Градиентные лучи ]
Функция же линейно зависящая от радиуса и угла даст нам искомую градиентную спираль:
Необходимо лишь подобрать необходимые коэффициенты, для 360-тиградусной
правильной отрисовки спирали. Коэффициент k1 влияет лишь на степень
закрученности спирали. Для правильного заполнения 360-ти градусов
выберем коэффициент k3 таким:
Коэффициент k2 будет принимать целочисленные значения: 1, 2, 3, 4...
Меняя этот кофициент, мы получаем соответствующее число ветвей у
спирали. Примеры градиентных спиралей при k2 равном единице, двум и пяти
представлены на рис. 4.5, 4.6 и 4.7:
[ Рис. 4.5. Спираль с одной ветвью ]
[ Рис. 4.6. Спираль с двумя ветвями ]
[ Рис. 4.7. Спираль с пятью ветвями ]
Код отрисовки спирали на псевдо-языке:
(Без учёта возможных ошибок, в т.ч. деления на ноль)
for (int y = 0; y < 200; y++)
for (int x = 0; x < 320; x++)
{
y -= 100;
x -= 160;
int color = k1*sqrt(x*x + y*y) + k2*128/3.1415927*arctan(x/y);
y += 100;
x += 160;
pixel[x][y] = color;
}
Теперь реализуем алгоритм на ассемблере.
A
B
C
D
E
A. Сохраняем в стеке и восстанавливаем из стека регистры, которые используются в функции. B. Регистр AX пробегает в цикле от 0 до 199 включительно. C. Регистр BX пробегает в цикле от 0 до 319 включительно. D. Сохраняем AX и BX в стеке. Выравниваем координаты центра
спирали на экране, для разрешения 320x200 сдвигаем центр на середину —
(160, 100). Восстанавливаем AX и BX из стека. E. Возводим AX в квадрат, меняем местами значение регистров AX и
BX, ещё раз возводим AX в квадрат. Имеем в регистрах AX и BX квадраты
координат. Складываем значения регистров и загружаем результат в DX.
Вычислением корня вполне можно пожертвовать в пользу уменьшения размера
кода приложения. Хорошо бы сохранить в стеке и восстановить обратно
регистры AX и BX при вычислении длинны вектора:
Ещё уменьшить объём кода можно, убрав лишние клавиши управления. Я
думаю, если выкинуть всё управление можно легко уложится в 128 байт. Но
без управления не так интересно.