Секреты DAINGY Вот, попросили меня тут написать про мою интру DAINGY, которая, как известно, заняла третье место на demoparty CC'999. Честно говоря, я уже толком и не помню, как там все устроено, но попытаюсь по ходу этой статьи вспомнить все тонкости и раскрыть вам суть некоторых процедур. Сразу хочу оговориться, что я не утверждаю, что использованные в интре методы являются самыми крутыми и не подлежащими дальнейшей оптимизации. Более того, сейчас некоторые моменты мне кажутся весьма неудачно выполненными, но что сделано, то сделано, тем более, что конечный результат, по-моему, весьма неплох. Ну а теперь приступим непосредственно к рассмотрению исходников DAINGY. Прежде всего необходимо описать распределение памяти в порядке возрастания адресов, итак: [line](этот адрес рассчитывается исходя из адреса chunks)-процедура построения в теневом буфере линии изображения, повернутого на некоторый угол. [chunks]-здесь расположены текстуры. [sin]-синусоида. [#8000]-само изображение, длина=#100, высота=#1000, 1 байт=1 pixel. [tenbuf]-буфер кадра. tenbuf EQU #fa00 chunks EQU #7600 sin EQU #7900 line EQU chunks-(linep2-linep1)*32-1 Адреса sin и chunks выбраны таким образом, чтобы разность между ними была равна #300, так как здесь используется одна процедура заливки байтом посредством LDIR для обнуления участка памяти и для установления атрибутов, если немного посидеть и поразбираться, то можно понять... Традиционно программа начинается с decrunching'а. Рассмотрим более подробно все его этапы. Сначала идет построение процедуры поворота одной линии изображения. ORG #6200 decrun LD de,line LD a,32 d1 LD hl,linep1 LD bc,linep2-linep1 LDIR DEC a JR nz,d1 LDI;ставим команду RET Далее идет построение chunks по таблице. Замечу, что они имеют размер 8*4 pixels, и составлены из пар стандартных чанкoв 4*4. LD b,32 CALL raspch CALL raspch INC hl DJNZ $-7 Подпрограмма raspch расположена в конце текста, но для наглядности приведу ее сейчас: raspch XOR a RLD LD c,A RLCA RLCA RLCA RLCA OR c JR linep2-2;переход на кусок ;кода ;LD (de),A ;INC de ;RET ;тем самым ;экономится один ;байт. Думаю, в пояснениях этот фрагмент не нуждается, отмечу лишь, что таблица содержит все 16*4=64 байта чанков, но так как в каждом байте значимыми являются только 4 бита, то она приведена к размеру 64/2=32 байта. Можно было сделать табличку и меньше, но данный способ компенсируется минимизацией процедур распаковки. Следующий кусок кода обеспечивает заполнение нулями области chunks, коды которых больше чем #0f. EX de,HL XOR a CALL fill2 Этого можно было и не делать при условии, что память при запуске не была забита разным мусором. Теперь пришло время сформировать некоторое подобие синуса: LD l,C;на входе имеем ;следующие значения: INC a ;C=0 ;А=0 ;DE=sin+#7f ;H=sin&h-старший байт ;адреса sin LD c,6 msin2b EX af,AF' LD a,C ADD a,7 LD b,A EX af,AF' msin2a LD (hl),A LD (de),A INC l DEC e ADD a,C BIT 6,l JR nz,makstr DJNZ msin2a DEC c JR msin2b Этот способ придуман мной лично и является самым коротким из всех мне известных. Попробую объяснить его суть. Как нетрудно заметить, разность между текущим и последующим значениями в функции синуса изменяется от максимума до минимума при изменении аргумента от 0 до PI/2. Фактически, эта разность равна значению первой производной функции sin (x) при стремлении разности аргументов к нулю. Так вот, если мы будем некоторую переменную в цикле увеличивать на константу, которую время от времени нужно уменьшать (причем чем больше константа, тем больше раз мы на нее увеличиваем переменную), то мы получим некоторое подобие четверти периода синуса. Этим и занимается вышеприведенный фрагмент кода. В данном случае мы получаем полупериод синусоиды с амплитудой изменяющейся от #01 до #ef без учета знака. Дальше идет формирование основной надписи, вращение над которой вы и можете лицезреть после запуска DAINGY. Здесь все достаточно тривиально, разве что для уменьшения объема кода буквы строки расположены в формате (letter-#20)*4. Используется стандартный фонт, расположенный по адресу #3d00, литеры которого утолщаются в горизонтальном направлении. makstr LD ix,string LD de,#8000 ff1 LD l,(ix) INC ix LD bc,#3d00 LD h,C ADD hl,HL ADD hl,BC EX de,HL LD b,8 ff3 PUSH bc PUSH hl EX de,HL LD a,(HL) RRCA OR (hl) EX de,HL INC de LD b,8 ff2 RLCA LD c,0 JR nc,$+4 LD c,15 LD (hl),C INC h LD (hl),C INC l LD (hl),C DEC h LD (hl),C INC l DJNZ ff2 POP hl INC h INC h POP bc DJNZ ff3 LD de,#f010 ADD hl,DE EX de,HL INC e ;проверка DEC e ;на конец JR nz,ff1;строки Пришло время установить цвет BORDER'а и ATTRIBUTES в обоих экранах OUT (#fe),a;А=0 CALL fill Подпрограмма fill выглядит следующим образом: fill LD a,#55 CALL fill1 LD a,#57+8 fill1 OUT (#fd),a LD a,65 LD hl,#d800 fill2 LD d,H LD e,L INC de LD (hl),A LD bc,sin+#7e-chunks-64; LDIR ;BC>=#300 след-но RET ;атрибуты мы установили ;правда с небольшим ;избытком... На этом процесс подготовки заканчивается, и начинается основной цикл, повторяющийся 256 раз, т.к. при достижении этого участка регистр B содержит 0. start PUSH bc CALL wave;построение очередной ;фазы так называемой ;"бегущей волны" Вообще-то сюда нужно было сразу вставить текст этой подпрограммы, тем самым сэкономив 4 байта, но впопыхах я забыл это сделать... ugol LD a,(kx+1);координата X ADD a,#97-#f0;в зависимости ;от нее ;рассчитывается ;приращение по ;одной из осей LD e,A CALL takesin ;dy=1*sin(alpha) LD c,A LD a,E RLCA RLCA AND #7f LD l,A LD d,(HL) LD a,#f0;попробуйте XOR а RLD CPL LD (hl),D LD (plus+1),a;устанавливаем ;основной уровень ;интенсивности фона LD a,E RLCA ADD a,C;A=beta EXX CALL takesin;исходя из этого ;рассчитывается ;приращение по другой оси LD c,A; dx=1*cos(beta) Этот участок кода формирует траекторию "полета", приближения, возникающие в результате того, что при вычислении приращений значения аргументов alpha и beta не всегда отличаются на #40, и , вдобавок ко всему, устанавливает текущий цвет фона. После получения приращений в BC и BC' осуществляем поворот надписи, с занесением полученного результата в буфер по адресу tenbuf. kx LD a,#10;координата X INC a LD (kx+1),a;A=X+1 LD h,A JR nz,$+7;если X=#100, то ;начинаем процесс затемнения LD a,#c6;ADD A,n LD (zatemn),a EXX LD de,tenbuf w2 PUSH bc PUSH bc EXX PUSH hl PUSH bc EXX PUSH hl CALL line POP hl POP bc ADD hl,BC EXX POP hl POP de SBC hl,DE EXX POP bc DEC bc;изменяем одно из ;приращений для ;внесения искажения. DEC bc LD a,D OR a JR nz,w2;если не достигли ;конца буфера, то ;повторяем поворот ;для следующей линии Итак, поворот мы осуществили, теперь настало время создать некую иллюзию трехмерности, путем создания боковых граней у букв. Делается это весьма просто: поочередно мы смотрим каждый столбец в tenbuf, как только встречается байт, отличный от нуля, начинаем заполнять все следующие байты столбца значением, каждый раз уменьшающимся на единицу. Делается это до тех пор, пока не получим отрицательное число, либо не достигнем нижней границы буфера. Вот текст этой процедуры: LD c,15 LD de,32 LD l,D m3d6 LD h,tenbuf&h m3d4 LD a,(HL) LD b,C OR a JR z,m3d1 m3d2 ADD hl,DE JR nc,m3d4 JR m3d5 m3d3 LD a,(HL) OR a JR nz,m3d2 m3d1 DEC b JP m,$+4 LD (hl),B ADD hl,DE JR nc,m3d3 m3d5 INC l BIT 5,l JR z,m3d6 Данный метод построения имеет один очень существенный недостаток: если буква целиком не помещается в буфере, то мы будем наблюдать резкий разрыв боковых граней, уходящих за пределы буфера. Чтобы сгладить этот эффект я затемняю верхнюю часть изображения, изменяя яркость от 0 до максимума начиная от верхней границы. LD hl,tenbuf PUSH hl LD c,15 LD b,61 de3a LD a,(HL) SUB c JR nc,$+3 XOR a LD (hl),A INC hl DJNZ de3a DEC c JR nz,de3a-2 А для того, чтобы плавно появить и в конце-концов затемнить картинку я просто сдвигаю информацию о чанках. LD hl,chunks LD d,H INC d swet LD a,64 LD e,A LD c,64 LDIR zatemn SUB 4 JR c,$+5 LD (swet+1),a Тот же эффект можно было получить непосредственно при выводе, но тогда бы и без того невысокая скорость работы несколько уменьшилась. Но так или иначе все преобразования кадра выполнены и осталось только его вывести на экран. Замечу, что я не "раскручивал" для ускорения программу вывода, а выполнял ее просто циклом. HALT LD b,D;в B устанавливаем ;старший байт адреса ;информации о чанках LD a,#55+8 XOR 10 LD ($-3),a OUT (#fd),a LD de,#c000 POP hl ;в HL получаем ;адрес tenbuf ch4 PUSH de LD a,32 ch1 EX af,AF' LD a,(HL) plus ADD a,0 ;прибавляя здесь ;некоторое значение ;мы устанавливаем CP 16 ;цвет фона и JR c,$+5;получаем яркостные CPL ;разводы на буквах AND 15 RLCA RLCA LD c,A PUSH de !ASSM 3 LD a,(BC);этот фрагмент INC bc ;кода LD (de),A;повторяется INC d ;3 раза !CONT LD a,(BC) LD (de),A POP de INC e INC hl EX af,AF' DEC a JR nz,ch1;проверка на конец ;линии POP de LD a,D ADD a,4 LD d,A AND 7 JR nz,ch4 LD a,E ADD a,32 LD e,A JR c,ch2 LD a,D SUB 8 LD d,A ch2 LD a,H;проверка на конец ;изображения OR a JR nz,ch4 st1 POP bc DEC b JP nz,start;проверка ;окончания ;основного ;цикла LD hl,10072;выход EXX ;из RET ;интро Далее идут различные подпрограмки и таблички, вкратце рассмотрим и их. Табличное вычисление синуса takesin LD l,A LD h,sin&h RES 7,l RLCA LD a,(HL) LD b,0 RET nc NEG DEC b RET на входе : в A-аргумент для синуса на выходе: в A-младший байт синуса B-старший байт синуса Данные для надписи. string DB #28,#8c,#c8,#e4 DB #cc,#cc,#3c,#c8 DB #e8,#b0,#28,#8c DB #8c,#64,#64,#64 Строка "*CRYSS/RZL*CC999" Итерация поворота для одного пиксела linep1 ADD hl,BC;смещaемся по Y LD a,H EXX AND %00010 ;используется для ;некоторого ;увеличения картинки RRCA ;в высоту ADD a,#80 LD d,A ADD hl,BC;смещаемся по X LD e,H LD a,(DE) EXX LD (de),A;занесение пиксела INC de ;в tenbuf linep2 RET Описание chunks. DB 0,0,#80,0,#80,#20,#a0,#20 DB #a0,#a0,#a4,#a0,#a4,#a1 DB #a5,#a1,#a5,#a5,#e5,#a5 DB #e5,#b5,#f5,#b5,#f5,#f5 DB #fd,#f5,#fd,#f7,#ff,#ff raspch см. выше fill см. выше "Бегущая волна" wave LD hl,#8000;координаты PUSH hl ;"фронта волны" LD b,16 wave1 LD a,B CP 9 ;проверка на wave2 JR c,wave3 ;"пик волны" LD c,A LD a,17 SUB c INC l JR $+3 wave3 DEC l XOR (hl) ;преобразование LD (hl),A;изображения под ;"фронтом волны" INC h ;переход к ;следующей линии DJNZ wave1 POP hl INC l ;изменение LD (wave+1),hl;координат RET ;"фронта" Ну вот вроде бы и все, что касается кода DAINGY. Возможно я и забыл описать кой-какие тонкости, но общая идея должна быть вам понятна, а это, как мне кажется, и есть основная задача этой статьи. Ведь для дальнейшего развития нашего любимого SPECCY мы должны делиться нашими знаниями и опытом, чтобы облегчить и ускорить выход нового качественного софта, чтобы продвигать в свет новые методы кодинга, да и вообще, чтобы просто повышать уровень интеллекта. И напоследок, хочется снять завесу тайны со слова "DAINGY". Если вы попытаетесь просто найти его в словаре, то у вас ничего не выйдет, так как это аббревиатура двух английских слов: dainty и gyration, что в переводе означает "изящное вращение". А теперь попытайтесь составить аббревиатуру из этих слов, но на русском языке... Догадались? Если да, то вы по достоинству оцените наши старания в подборе оригинального названия для интры. За сим прощаюсь с вами до встречи в наших новых программах. BYE! (c) Cryss/Razzlers