Эта статья посвящена созданию на ассемблере графического приложения весом в несколько сотен байт. После создания полноценной рабочей версии на 420 байт пришлось попотеть, чтобы запихать всё это добро в 256 байт. Результат вы можете лицезреть на видео. В статье описывается процесс создания и общие принципы функционирования.
Предупреждение: Если вы страдаете приступами эпилепсии — НЕ СМОТРИТЕ.
В Win7 и Vista работать не будет. Нужна Windows XP/2000/98.
Скачать исполняемый файл: k29.com в DropBox (256 байт)
Скачать исходный код: k29.asm в DropBox (Компилировать FASM-ом)
Клавиши управления:
1. R,G,B — включение и отключение цветовых компонент
2. <--,SPACE,--> — менять направление и скорость вращения
3. UP, DOWN — менять масштаб спирали
4. 0,1,2,3,4,5,6,7,8,9 — менять число ветвей у спирали
5. ESC — выход
Уже довольно долго эта статья валяется у меня в черновиках. Ещё в черновиках на Blogger-е валялась. И сегодня я решил, что если не допишу её сейчас — это не произойдёт никогда. Дописал, в конце немного оборвал и закончил) Ура!!!
Меня всегда интересовали работы с demoparty, особенно категории исчисляющиеся в сотнях байт. Одно дело писать прогу в 64 кило, используя DirectX/OpenGL, и совсем другое — в 512/256/128 байт, используя видеопамять напрямую и т.д. Тут необходимо знание и понимание ассемблера уже на интуитивном уровне. Необходимо уметь оптимизировать код по объёму, а значит понимать и учитывать все тонкости машинного языка программирования. Мы попробуем сделать что-то подобное. Я никогда раньше не писал на ассемблере, но разобраться в существующем коде получалось. Будем считать, что данная статья ориентирована на новичков в ассемблере вроде меня. Потому не претендую на идеальное решение задачи.
Теперь нужно придумать себе задачу и воплотить её. Мне на ум сразу пришла небольшая программа, написанная в школьные годы на языке Pascal. Программа была проста до безобразия (2 экрана кода — 50 строк), но в то же время она доставляла. Программа рисовала в графическом режиме (320x200x256) во весь экран вращающуюся спираль. Были задействованы все 256 цветов, для плавного изменения цвета. Было удивительно, что спираль движется без видимой перерисовки. Это можно было бы объяснить использованием нескольких видеостраниц, если бы не скорость вращения. Очевидно, что для отрисовки спирали необходимы вычисления с вещественными числами, что тоже вносит значительную задержку. Спираль вращалась со скорость 3-5 оборотов в секунду (см. рис. 2.1).
[ Рис. 2.1. Снимок спирали с тремя «руками» ]
А вся фишка была в том, что спираль рисовалась всего один раз — при запуске программы. После отрисовки спирали программа циклически сдвигала цвета в палитре, что незамедлительно отображалось на экране. Помимо основной функции программа поддерживала изменение цвета спирали и изменение направление вращения. Всего восемь цветов:
Так как для отображения на экране используется цветовая модель RGB, то эти восемь цветов можно получать, комбинируя соответствующие цветовые компоненты (3 бита данных могут кодировать 8 различных значений). Программа использовала клавиши 'R', 'G' и 'B' для вкючения/отключения соответствующих цветовых компонент.
Программа была написана на языке Pascal в среде разработки Turbo Pascal 7.0. Некоторые функции программы были реализованы в виде ассемблерных вставок. Например, функция установки RGB-значения конкретному элементу из палитры. Код школьных времен (форматирование изменено чтобы не травмировать ничью психику):
Сперва разберёмся в том, как функционирует программа и какие функции нам потребуется написать. Формальное описание алгоритма:
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.1. Блок-схема алгоритма ]
Прочитав об имеющихся компиляторах языка ассемблер в википедии и книге «Искусство дизассемблирования» Криса Касперски и Евы Рокко, я сделал свой выбор в пользу компилятора FASM. Код буду писать в Notepad++, из него же и буду экспортировать в статью с подсветкой синтаксиса.
Ну чтож, приступим.
Мы собираемся заполнять палитру следующими значениями: (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). Запишем алгоритм на формальном языке высокого уровня:
Теперь ассемблерный код с комментариями:
A. Сохраняем в стеке и восстанавливаем из стека регистры, которые используются в функции.
B. Регистр CX пробегает в цикле от 0 до 127 включительно.
C. Формируем параметры и вызываем функцию setPalette. В AL загружаем индекс цвета, в AH загружаем значение яркости, равное половине индекса.
D. Меняем индекс на (255-i) используя операцию инверии всех битов и вызывает функцию setPalette.
Подумаем над функцией описания градиентной спирали.
Функция зависящая только от радиуса даёт градиентные окружности:
[ Рис. 4.1. Градиентные окружности ]
Функция зависящая только от угла даёт градиентные лучи из центра:
[ Рис. 4.2. Градиентные лучи ]
Функция же линейно зависящая от радиуса и угла даст нам искомую градиентную спираль:
[ Рис. 4.3. Градиентная спираль ]
Необходимо лишь подобрать необходимые коэффициенты, для 360-тиградусной правильной отрисовки спирали. Коэффициент k1 влияет лишь на степень закрученности спирали. Для правильного заполнения 360-ти градусов выберем коэффициент k3 таким:
[ Рис. 4.4. Правильная 360-тиградусная градиентная спираль ]
Коэффициент k2 будет принимать целочисленные значения: 1, 2, 3, 4... Меняя этот кофициент, мы получаем соответствующее число ветвей у спирали. Примеры градиентных спиралей при k2 равном единице, двум и пяти представлены на рис. 4.5, 4.6 и 4.7:
[ Рис. 4.5. Спираль с одной ветвью ]
[ Рис. 4.6. Спираль с двумя ветвями ]
[ Рис. 4.7. Спираль с пятью ветвями ]
Код отрисовки спирали на псевдо-языке:
(Без учёта возможных ошибок, в т.ч. деления на ноль)
Теперь реализуем алгоритм на ассемблере.
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 при вычислении длинны вектора:
Теперь код, вычисляющий акртангенс угла по заданным sin и cos:
Думаю код уже всех достал)) Вот где он лежит целиком: http://codepad.org/mEDX1Z2X
Ещё уменьшить объём кода можно, убрав лишние клавиши управления. Я думаю, если выкинуть всё управление можно легко уложится в 128 байт. Но без управления не так интересно.
Предупреждение: Если вы страдаете приступами эпилепсии — НЕ СМОТРИТЕ.
В Win7 и Vista работать не будет. Нужна Windows XP/2000/98.
Скачать исполняемый файл: k29.com в DropBox (256 байт)
Скачать исходный код: k29.asm в DropBox (Компилировать FASM-ом)
Клавиши управления:
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 255 do
begin
setRGBpalette(i,k,k,k);
mas[i,1]:=k;mas[i,2]:=k;mas[i,3]:=k;
k:=k+flag;
if k=63 then flag:=-1 else
if k=0 then 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])<4 do insert('0',ints,1);
if int and 3=0 then
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 else
if upcase(key)='R' then rr:=not(rr) and 1 else
if upcase(key)='G' then gg:=not(gg) and 1 else
if upcase(key)='B' then bb:=not(bb) and 1 else
if key=#27 then break;
end;
for i:=0 to 127 do
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 |
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. Градиентные лучи ]
Функция же линейно зависящая от радиуса и угла даст нам искомую градиентную спираль:
pixel[x][y] = k1*sqrt(x*x + y*y) + k2*k3*arctan(x/y));
[ Рис. 4.3. Градиентная спираль ]
Необходимо лишь подобрать необходимые коэффициенты, для 360-тиградусной правильной отрисовки спирали. Коэффициент k1 влияет лишь на степень закрученности спирали. Для правильного заполнения 360-ти градусов выберем коэффициент k3 таким:
k3 = 128 / 3.1415927;
[ Рис. 4.4. Правильная 360-тиградусная градиентная спираль ]
Коэффициент 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 |
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 при вычислении длинны вектора:
push ax
push bx
; dx = ax^2 + bx^2
mul ax, ax
xchg ax, bx
mul ax, ax
add ax, bx
mov dx, ax
pop bx
pop ax
Теперь код, вычисляющий акртангенс угла по заданным sin и cos:
; Вычисляем арктангенс угла * k2
; dx += arctan(alpha * k2)
mov [cos], ax
mov [sin], bx
finit
fild word [cos]
fild word [sin]
fpatan
fimul word [k2]
fimul word [glad1]
fidiv word [glad2]
fist word [tmp]
add dx, [tmp]
4.3. Проверка нажатий клавиш
; Проверка на нажатие клавиши
mov ah, 0Bh ; AX := 0B00h
int 21h
cmp al, 0ffh
jmp_loop_pal_exit:
jne loop_pal_out
; Получаем какое именно нажатие
mov ah, 08h
int 21h
label_push_space:
cmp al, ' '
jne label_push_left
mov ch, 0
label_push_left:
cmp al, 75
jne label_push_right
dec ch
.......
Думаю код уже всех достал)) Вот где он лежит целиком: http://codepad.org/mEDX1Z2X
Ещё уменьшить объём кода можно, убрав лишние клавиши управления. Я думаю, если выкинуть всё управление можно легко уложится в 128 байт. Но без управления не так интересно.