Технический разбор Illusion от X-Trade
Давно хотел попробовать написать новый тип заметки, специально для кодеров. Я хочу попробовать поговорить о внутренних циклах дем, о том, на чём держаться эффекты и об идеях, которые позволяют эти эффекты реализовать. Я не буду лезть в подробности реализации и не дам вам дизассемблер для сборки чего-то выдранного. Только идеи. Только самое интересное.
Ну и поскольку мне нужно с чего-то начать, я хочу начать с Illusion от X-Trade. Мне это демо всегда было симпатично.
Первый эффект в демо, это скроллер натянутый на шар. По сути, этот же самый эффект повторён ближе к концу, там где на шар натянута картинка из робокопа:
Итак, как это делается? У нас в памяти есть чёрно-белая картинка, либо скроллер, либо робокоп, но хранится она в виде байтов (байт на пиксель). Байт = 1 означает «есть пиксель», байт = 0 — «нет пикселя». Это неэкономно по памяти, но позволяет собрать из пикселей новую картинку. В чём идея? Алгоритм рисует сферу горизонтальными полосами вот в таком вот цикле:
В HL — наша исходная картинка (байт на пиксель), в DE — буфер для результата, а в А накапливаются пиксели. Т.е. мы идём слева направо, умножаем А на 2 и добавляем в А текущий пиксель. Теперь фокус, почему я написал DUP? — потому что следующий пиксель может быть тем же, может быть действительно следующим, через 1, или даже, когда мы рисуем самый верх или самый низ сферы, через несколько пикселей от предыдущего. Т.е. программа генерирует такой код для вывода сферы заранее, и кол-во INC L вообще говоря всё время меняется, во время движения по сфере. Самое большое что я нашёл — пропуск 4 пикселей, но я не могу сказал что искал так уж старательно, где-то могло быть пропущено и 5.
Насколько эффективный это вывод? Можно предположить, что у нас в среднем один пропущенный байт. Размер шарика — 56 х 56 пикселей. Тогда выходит, что сфера занимает 56*56/8=392 байт экранной памяти, и поэтому выведется за 133*392 ~ 52136 тактов. Если бы пропущенных байтов было в среднем два, вышло бы (133+32)*392 ~ 64680 тактов, что с учётом вывода на экран, плейера (4.5 тысячи тактов) и задержек памяти, уже не влезало бы во фрейм на классике. Плюс вывод буфера на экран и т.д. Фреймовый эффект.
Второй заинтересовавший меня эффект — это большой поточечный скроллер с прыгающим по нему мячом:
Мячик это конечно спрайт, физика, конечно, ненастоящая. Меня интересовал сам скроллер. Рисуется он в текстуру в памяти, а потом выводится в экран вот таким образом:
В BC — текстура в памяти. Стек настроен на таблицу адресов в экране, куда нужно будет выводить пиксели. Конкретные биты пикселей не меняются (движение батута строго вверх-вниз, движение получено просто сменой таблицы адресов пикселей). Читаем адрес и проверяем, зажжён ли текущий бит, если да — ставим его в экран.
Время вывода одного пикселя — 36 тактов (или меньше, если пиксель погашен), поэтому экран из 1024 пикселей выводится за 1024*36 ~ 36864 тысяч тактов. Заметили, что батут в нижней трети? Значит у нас есть верхний бордер (64 строки на пентагоне), плюс две трети экрана на рисование (ещё 128 строк), т.о. у нас есть до начала отрисовки батута где-то (64+128)*224 = 43008 тактов. Т.е. эффект спокойно успевает в один экран, могли ещё больше точек нарисовать без проблем :)
Теперь, два «главных» эффекта в демо, moving shit и ротосоник:
Идея реализации в обоих случаях одна и та же, стандартная для ротаторов и вобблеров. Мы опять хотим текстуру с пикселем на байт. В случае этих двух эффектов у нас пиксели 2х2, ну т.е. всё равно храним в байте #03 если пиксель есть или #00 если пиксель пустой. В начале кадра нам нужно сгенерировать процедуру, которая будет идти по экрану строго горизонтально, слева направо, и, одновременно — по текстуре. По текстуре идти придётся под углом (чтобы получилось вращение) или вообще по какой-нибудь кривульке, чтобы вышло moving shit. Такая процедура в Illusion выглядит вот так:
В HL адрес в текстуре. Чанк 2х2 добавляется в А, а потом мы сдвигаемся по текстуре inc l — влево, dec h — вверх. Конкретные команды пересчитываются каждый фрейм. В DE — адрес буфера в памяти, куда осуществляется вывод. Когда буфер целиком собран, кадр выводится на экран используя стандартный код:
Считаем скорость вывода. Размер экрана в Moving Shit — 128 x 96 чанков, которые обсчитываются за 95*(128*96/4) = 291840 тактов. С учётом времени на вывод из буфера и музыки, это 4-5 фреймов на каждый кадр. У соника вывод на весь экран, поэтому скорость на треть медленнее, 5-6 фреймов на кадр.
Один эффект я что-то не понял — zoomscroller в конце. Там несколько кусков кода, который выглядит не так понятно как примеры выше. Если кто-то может рассказать, как он устроен, буду очень благодарен. На этом всё на сегодня, счастливо и попутного кода :)
Ну и поскольку мне нужно с чего-то начать, я хочу начать с Illusion от X-Trade. Мне это демо всегда было симпатично.
Первый эффект в демо, это скроллер натянутый на шар. По сути, этот же самый эффект повторён ближе к концу, там где на шар натянута картинка из робокопа:
Итак, как это делается? У нас в памяти есть чёрно-белая картинка, либо скроллер, либо робокоп, но хранится она в виде байтов (байт на пиксель). Байт = 1 означает «есть пиксель», байт = 0 — «нет пикселя». Это неэкономно по памяти, но позволяет собрать из пикселей новую картинку. В чём идея? Алгоритм рисует сферу горизонтальными полосами вот в таком вот цикле:
DUP 8
add a : add (hl)
DUP ?
inc l
EDUP
EDUP
ld (de),a : inc de ; (4+7+4*x)*8+7+6 = 101+32x тактов на байт (в среднем)
В HL — наша исходная картинка (байт на пиксель), в DE — буфер для результата, а в А накапливаются пиксели. Т.е. мы идём слева направо, умножаем А на 2 и добавляем в А текущий пиксель. Теперь фокус, почему я написал DUP? — потому что следующий пиксель может быть тем же, может быть действительно следующим, через 1, или даже, когда мы рисуем самый верх или самый низ сферы, через несколько пикселей от предыдущего. Т.е. программа генерирует такой код для вывода сферы заранее, и кол-во INC L вообще говоря всё время меняется, во время движения по сфере. Самое большое что я нашёл — пропуск 4 пикселей, но я не могу сказал что искал так уж старательно, где-то могло быть пропущено и 5.
Насколько эффективный это вывод? Можно предположить, что у нас в среднем один пропущенный байт. Размер шарика — 56 х 56 пикселей. Тогда выходит, что сфера занимает 56*56/8=392 байт экранной памяти, и поэтому выведется за 133*392 ~ 52136 тактов. Если бы пропущенных байтов было в среднем два, вышло бы (133+32)*392 ~ 64680 тактов, что с учётом вывода на экран, плейера (4.5 тысячи тактов) и задержек памяти, уже не влезало бы во фрейм на классике. Плюс вывод буфера на экран и т.д. Фреймовый эффект.
Второй заинтересовавший меня эффект — это большой поточечный скроллер с прыгающим по нему мячом:
Мячик это конечно спрайт, физика, конечно, ненастоящая. Меня интересовал сам скроллер. Рисуется он в текстуру в памяти, а потом выводится в экран вот таким образом:
ld a,(bc) : inc c
DUP 8
pop hl : rla : jr nc,0f
set ?,(hl)
0:
EDUP ; 10+4+7+15 = 36t на пиксель
В BC — текстура в памяти. Стек настроен на таблицу адресов в экране, куда нужно будет выводить пиксели. Конкретные биты пикселей не меняются (движение батута строго вверх-вниз, движение получено просто сменой таблицы адресов пикселей). Читаем адрес и проверяем, зажжён ли текущий бит, если да — ставим его в экран.
Время вывода одного пикселя — 36 тактов (или меньше, если пиксель погашен), поэтому экран из 1024 пикселей выводится за 1024*36 ~ 36864 тысяч тактов. Заметили, что батут в нижней трети? Значит у нас есть верхний бордер (64 строки на пентагоне), плюс две трети экрана на рисование (ещё 128 строк), т.о. у нас есть до начала отрисовки батута где-то (64+128)*224 = 43008 тактов. Т.е. эффект спокойно успевает в один экран, могли ещё больше точек нарисовать без проблем :)
Теперь, два «главных» эффекта в демо, moving shit и ротосоник:
Идея реализации в обоих случаях одна и та же, стандартная для ротаторов и вобблеров. Мы опять хотим текстуру с пикселем на байт. В случае этих двух эффектов у нас пиксели 2х2, ну т.е. всё равно храним в байте #03 если пиксель есть или #00 если пиксель пустой. В начале кадра нам нужно сгенерировать процедуру, которая будет идти по экрану строго горизонтально, слева направо, и, одновременно — по текстуре. По текстуре идти придётся под углом (чтобы получилось вращение) или вообще по какой-нибудь кривульке, чтобы вышло moving shit. Такая процедура в Illusion выглядит вот так:
ld a,(hl)
inc l : dec h ; эти команды меняются в зависимости как наша линия идёт по текстуре
add a : add a : add (hl)
inc l : dec h
add a : add a : add (hl)
inc l
add a : add a : add (hl)
inc l : dec h
ld (de),a : inc e ; 7+4+4 + 4+4+7+4+4 + 4+4+7+4+4 + 4+4+7+4+4 + 7+4 = 95t на 4 чанка (один байт)
В HL адрес в текстуре. Чанк 2х2 добавляется в А, а потом мы сдвигаемся по текстуре inc l — влево, dec h — вверх. Конкретные команды пересчитываются каждый фрейм. В DE — адрес буфера в памяти, куда осуществляется вывод. Когда буфер целиком собран, кадр выводится на экран используя стандартный код:
pop hl : ld (),hl : ld (),hl ; 10+16+16=42t на 4 экранных байта
Считаем скорость вывода. Размер экрана в Moving Shit — 128 x 96 чанков, которые обсчитываются за 95*(128*96/4) = 291840 тактов. С учётом времени на вывод из буфера и музыки, это 4-5 фреймов на каждый кадр. У соника вывод на весь экран, поэтому скорость на треть медленнее, 5-6 фреймов на кадр.
Один эффект я что-то не понял — zoomscroller в конце. Там несколько кусков кода, который выглядит не так понятно как примеры выше. Если кто-то может рассказать, как он устроен, буду очень благодарен. На этом всё на сегодня, счастливо и попутного кода :)
116 комментариев
Всё просто. Каждая горизонтальная линия может отрисовываться одним из двух вариантов: или по OR с текущим содержимым экрана, или то же самое, но с накладыванием маски по AND. Хитрость в чередовании линий и масок.
Всего декранчится четыре процедуры скролла, в которых меняется порядок отрисовки линий. Неизменным остается только их соотношение: 144 линии по OR, оставшиеся 48 отрисовываются по AND + XOR.
В каждой итерации вызывается одна из этих четырех процедура скролла. И каждый раз меняется накладываемая маска. Масок всего 16, циклятся по кругу.
После того, как текст останавливается, процедуры какое-то время продолжают вызываться. Но уже с совпадающими данными источника и назначения. Тем самым, за счет накладывания маски, экран постепенно очищается от «мусора».
Есть в ней что-то человечное, не сюжет конечно, но что-то большее чем набор эффектов.
Что позволяло ее показывать случайнам людям, друзьям, родственникам. )
Однако осталось не понятным, возможно ли перечисленные эффекты ускорить/улучшить/решить иначе?
Ну и интересно твое мнение на счет этой демы как демы, в контексте тех лет конечно.
Еще про музыку вопрос, там обычный pt2 плеер жрущий 2-3к тактов?
А вообще — «Illusion» — одна из немногих дем 1990х, которая нравилась мне тогда и нравится сейчас. Она быстрая, без передержанных сцен, с мелодичным треком, с чувством самоиронии и без типичных для того времени понтов. Впечатляющая не смотря на это всё. Подытоживая, я не думаю, что когда-нибудь сделаю дему похожую на «Illusion», но отношение у меня к ней очень тёплое.
Плейер я даже не посмотрел, — там в бегущей строке под робокопом авторы сами написали что у них плейер 4500 тысячи тактов.
Ни у кого больше нет такого ощущения?
Почерпнул для себя некоторые идеи
Мое личное пожелание:
Вообще циклы выводящие текстуру, лично мне и так понятны, равно как и ухищрения со стеком и т.п.
А вот мне как раз интересно, как это оно «идет по кривуле», как эту «кривулю» посчитать. И считают ли они
ее в теле демы, или тупо загружают координаты? В каком виде удобно хранить именно эти данные?
А теперь посмотри на линию между H и I, и сравни её с правой кромкой буквы T.
Думаю, это отвечает на твой вопрос :)
Интересно как авторы в каждом случае устраивают матрицы этих кривуль, как их лучше хранить
для каждого кадра (при прекалькуляции), или где-то выгодно считать на лету по таблицам того же исходного синуса — тогда
какие используются алгоритмы для быстрых вычислений.
во вторых, часто дело синусом не ограничивается, и идет в ход матан пожощще. А вот авторы
статей про демо эффекты, почему-то стараются всегда этот вопрос обходить стороной.
Какие-то авторы статей про демо-эффекты тебя не удовлетворяют…
Ну вот скажу тебе симметрично, что комментаторы у статей про демо-эффекты тоже бывают весьма замысловатые.
«В HL — наша исходная картинка (байт на пиксель), в DE — буфер для результата, а в А накапливаются пиксели. Т.е. мы идём слева направо, умножаем А на 2 и добавляем в А текущий пиксель. Теперь фокус, почему я написал DUP? — потому что следующий пиксель может быть тем же, может быть действительно следующим, через 1, или даже, когда мы рисуем самый верх или самый низ сферы, через несколько пикселей от предыдущего. Т.е. программа генерирует такой код для вывода сферы заранее, и кол-во INC L вообще говоря всё время меняется, во время движения по сфере.»
Лично мне интересно, как сформировали этот развернутый цикл. Например в разбираемой деме ILLUSION. чтобы в DUB стоял не знак вопроса а описание формулы и ее реализация, поскольку ЛИЧНО ДЛЯ МЕНЯ, это наиболее интересная часть эффекта. Конечно, твое личное дело как автора писать это или нет, а мое как комментатора — написать свое пожелание. Возможно в будущих статьях кто-то учтет мое пожелание и расскажет об этом. Иначе зачем вообще тогда комментарии? достаточно кнопки +
И меня это злит. Потому что этот «кто-то» — это ведь подразумеваюсь я, но задать мне прямой вопрос тебе видимо не позволила гордость или х.з. даже что. В итоге сначала я должен тянуть из тебя клещами информацию о том, чего же тебе собственно хочется (ну, за пределами очевидного понтования о том, как тебе всё это кажется тривиальным). И после всего этого я же и оказываюсь виноватым, потому что ты, якобы, меня покритиковал, а я, якобы, адекватно воспринимаю только плюсики. Твой комментарий — это типичнейшее в стиле zx-pk.ru потребительство, где ты полон мыслей, что всё нужно делать не так, а лучше, но сам явно ничего делать не собираешься; тебе оказалось лень сформулировать даже прямой вопрос.
Генератор развёрнутого кода начинается с адреса #6133. Данные о пропусках хранятся в таблице с адреса #6944.
Формат таблицы такой:
Мне было лень сейчас разбираться, как там согласовано то, что ширина хранится не точно, а +1 или +2. Непосредственно число в начале строки преобразовывается в число пикселей на половину рисунка, потом пересчитывается в сдвиг в байтах (для вывода на экран) и там возня не интересная, приспособленная к конкретному коду.
Насчет понтований: тут понтов никаких, я действительно знал эти методы, и видел синус в мувинг шите, я спрашивал не про него. Но их реализации в конкретной деме, тем не менее, интересны, ибо всегда полезно иметь ввиду чужой успешный опыт, ибо свои представления могут быть ошибчно и неэффективны.
Сейчас мы конечно же можем на писюке посчитать матрицу и вогнать ее на спек, более того, можно на писюке сгенерить
сразу ассемблерный код развернутого цикла. Но тогда ведь считали все прямо на спеке? или я не прав?
Один мой товарищ Joker делал вращающуюся сферу размером в пол экрана. в районе 8 фаз анимации.
Так вот, сначала он считал сферу (естественно по формуле сферы) вызовами пзу-шного калькулятора. И считалось все это дело добрых 15 минут.
Потом он переделал на собственные процедуры арифметических операций и табличный синус — и о чудо, 30 секунд. Что вполне нормально.
Конечно можно было заранее сгенерить массив координат в текстуре, заархивировать его, и подсасывать с диска при загрузке интры. Но вы все сами понимаете, что следующий шаг — запилятор (который в некоторых случаях может оказаться даже эффективнее предыдущего метода).
ибо ты говоришь о спрайте «4 фазы анимации маленького шарика 2 на 2 знакоместа», а я о экранном выводе заданного гифа, в котором уже заданы кадры и координаты вывода.
С моей точки зрения, это какие-то бессмысленные ограничения, которые портят впечатление с т.зр. зрителя. Т.к. я стараюсь делать демы для зрителей, я не считаю принципиальным кто и где считал таблицы и, даже больше, я уже неоднократно задействовал кодогенераторы написанные на PC. Это позволяет при прочих равных условиях повысить качество сгенерированного кода (качество в плане объёма и/или скорости), даёт сопоставимую скорость распаковки, а так же в разы упрощает отладку.
Долго смеялся :) Самое главное демы не забывайте делать, а то в теории то все крутые и принципиальные!
надо покопаться по старым запасам, где-то у меня валялся sphere texture mapping.
2. Храни sin в таблице 256 байт, рассчитай заранее.
3. Загрузи старшую половину регистровой пары чтобы получить адрес таблицы, пусть это будет H
4. Тогда L — будет произвольный аргумент функции.
5. Крути L как угодно — будешь ходить по функции туда-сюда.
6. Читай значения sin как LD reg,(HL)
7. cos это то же самое что sin, но сдвинутый на четверть периода. И наоборот. Четверть периода это регистр L +- 64
8. Удобно вместо значений sin/cos сразу прошить таблицу адресами.
9. Значения таблиц sin можно подготовить на старте.
Этот топик мне вряд ли что объяснит, например, неясно, как строится табличка для Robocop.
с Moving Shit ясно итак: я попытался повторить похожий эффект в Reality, помог разобраться с работой исходник от пейсишной bbstro:
Нарисуйте спрайт, где колеблется синусная линия. Затем добавьте смешение по горизонтали, смещение определяется как sin(A)+sin(B).
Твой Moving Shit, насколько я помню, и был причиной я влез в Illusion, потому что у тебя было душераздирающе медленно (в 4 раза медленнее, насколко я помню). Если бы ты научился думать о внутренних циклах описанным мной образом, м.б. и у тебя вышло бы пошустрее.
Я помню для Chaos Reconstruction мне помогал Alex Rider, но… на шар оно так и не было похоже.
Вот таблица из Википедии: en.wikipedia.org/wiki/List_of_map_projections#Cylindrical
Авторы Illusion приложили только таблицу, поэтому можно потратить несколько дней исследуя, какую именно из проекций они применили. Я этим заниматься не буду. Наиболее вероятные кандидаты — Equirectangular, Mercator, Gall stereographic, Miller, Lambert cylindrical equal-area, Gall–Peters, т.е. различные «старые» проекции.
Собственно, это одна из причин почему мне кажется бессмысленным давать рецепты такого рода. kotsoft прав, что самые интересные секреты — это вычисления в этом роде. Но какой смысл научить сейчас всех, допустим, Ламберту, чтобы потом все как попугаи шпарили шары с одними и теми же искажениями?
Рискну предположить, что непосредственно в теле демы по этой таблице генерится статичный развернутый цикл.
То, что авторы не слышали ни об одном из описанных методов, тем не менее, совсем не означает, что они не задействовали одну из описанных по моей ссылке формул. Я собственно уверен на 99% что одну из них они и сделали.
Вот тут есть пример выкладок: www.javaworld.com/article/2076696/learn-java/draw-textured-spheres.html
Первые 2 прохода, невзирая на недостатки того, как я их сделал, довольно эффективны. Третий проход писался последним и на него не было времени, он фактически не оптимизирован…
Короче, ты прав, может и нужно написать ужастик, хоть кодеры поржут.
Я про то, что какой бы ужастик в коде не был, интересен путь, а также проблемы и нюансы решаемые и возникающие в процессе.
www.petesqbsite.com/sections/express/issue25/index.html
Отличный анализ, отличный разбор, читал и другим рекомендую.
Каждая строка пентагона рисуется за 224 такта процессора. У меня было написано: «Заметили, что батут в нижней трети? Значит у нас есть верхний бордер (64 строки на пентагоне), плюс две трети экрана на рисование (ещё 128 строк), т.о. у нас есть до начала отрисовки батута где-то (64+128)*224 = 43008 тактов.» Экран спектрума хранится в памяти кусками по 2кб, таких кусков три, поэтому многие эффекты, не только в этом демо, организованы так, чтобы воспользоваться такой организацией памяти. Поэтому идёт речь о третях экрана.
Но до сих пор не понятно, как же строятся всякого рода бегущие строки… ведь у бордера «нету своей видеопамяти»..?
Кто бы в деталях разжевал, так сказать, для окончательного понимания построения подобных эффектов…
Еще раз и Много-много раз — Спасибо!
учитывая то, что вывод в порт — out (#fe),a / out (c),a — занимают 11/12 тактов, то это минимальный размер бордюрного пикселя…
[28.07.2017 17:44:04] TSL: каждые полтакта
[28.07.2017 17:44:17] TSL: пиксель имеет длительность = периоду частоты 7мгц
А лучше несколько, друг за другом. Желательно код.
Видимо мы в разных вселенных существуем, но меньше чем 12 тактов\24 пикселя мне не удается. ЧЯНТД?
Я возмутился вот этому в первую очередь: «покажите мне на примере линию на бордюре в 22 пикселя \11 тактов». Показал. А потом было столько уточнений и поправок, что сам чёрт не разберёт уже.
ну и в подтверждение своей неправоты :((((
ЧЯА5ДНТ? Или «не верь глазам своим! ©»?
В четырех эмулях (уточняйю — под ПЕНТАГОН!) — полоски равны между собой и равны 24 пикселя. У вас же у всех есть реальные Пентево-гоны, код выше, правильный) Вот и проверьте. Теорию — практикой.
out (#fe),a — не совсем 11-ти тактовая команда :) я бы сказал это что то типа «12-1», что вовсе не равнозначно одиннадцати :))))))))
подождем объяснений более вумных людей.
я раньше не проверял, ибо надобности не было — эти 11 тактов не пришей туда рукав. но чисто теоретически ожидал адекватную реакцию в 22 пиксела)
верхняя линия 12 тактов, нижняя — 11.
отсюда вопрос — какого хрена так происходит?
Возможно OUT (port) и 11 тактовая, во всех остальных случаях, кроме бордюра, на бордюре она ведет себя странно, прикидывается 12-тактовой, НО!!! отжирает один такт от ПРЕДЫДУЩЕЙ! команды,
в сумме то как бы и правильно, только наеборот все, предыдущая перед ней команда укорачивается на такт, это видно по раскраске пикселя, а сама она становится 12 тактов, ну и общая сумма не меняется. вот потому я ее и назвал «12-1». Зато я не прав, что радует )
www.dropbox.com/s/selbdtzr5b37n7h/out11.sna?dl=0
azesmbog, ты забыл, что собственно запись в порт происходит совсем не обязательно на последнем такте команды. Тот пример, что я написал из головы, очевидно, записал ровно 12 тактов, хотя между 12-тактовыми командами сидела 11-тактовая.
zx-pk.ru/attachment.php?attachmentid=61855&d=1501537431
надо бы на реале проверить
Думаем по шагам
Если написать наоборот, будет
Т.е. если бы я подумал чуть-чуть мозгом перед тем как писать самый первый пример кода, ничего бы этого не случилось. Так что, извиняйте :)
Шах и мат, товарищи :))))
out (254),a
out (c),l
out (254),a
out (c),l
out (c),h
рисуем команду
out (c),l за ней out (254),a
в l — 1 (синий) в а — 2 ( красный)
синяя полоска нарисуется 22 пикселя, красная — 24
такие полоски я и сам нарисую, я говорю как оно последовательно выходит.
Осталось разобраться, только в эмулях так, или на железном проце то жи
Но это не одна из таких вещей.