Процедурная графика на ZX Spectrum

by Shiru 08'11 mailto:shiru at mail dot ru



Краткая история

На демопати ASCiI 2008 впервые и пока единственный раз в истории ZX сцены был проведён конкурс процедурной графики. В нём приняли участие всего два человека, я и Tiboh. Я также предлагал проведение конкурса на ArtField 2009 и сделал работу, но инициатива не была поддержана другими участниками. Ещё одна работа для ZX Spectrum от n-Discovery была выставлена на Chaos Constructions 2009 в совмещённом конкурсе, вместе с работами для PC. Таким образом, дисциплина процедурной графики успела немного засветиться на ZX сцене, но пока не получила поддержки и популярности. Тем не менее, думаю, что она могла бы немного разбавить традиционный набор ZX конкурсов, и потому расскажу о ней подробно.



Что это такое

Процедурная графика в целом – это алгоритмическая генерация изображений, то есть графика, создаваемая некими хитроумными вычислениями, а не нарисованная рукой художника. В нашем контексте это компактная программа, генерирующая и отображающая статичную картинку.

В отличие от интро схожего размера, которое также показывает что-то на экране, программист не придумывает анимацию или действие, а ищет способ описать придуманное им изображение таким образом, чтобы оно занимало мало места в памяти, но при этом было максимально интересным, красивым и качественным. Время генерации изображения может быть довольно длительным. Это ставит перед программистом другие задачи и требует иной набор навыков, чем при работе над интро, а также позволяет использовать навыки и алгоритмы, по разным причинам не применимые или не востребованные в других дисциплинах. Отличие от обычной графики, соответственно, заключается в том, что картинка нарисована не рукой художника, а программным кодом, в который заложена идея автора.

Простейший пример процедурной графики – программное рисование звёздного неба. Вместо того, чтобы руками рисовать каждую точку, изображающую звезду, можно написать такую условную программу: поставить точку в случайных координатах, повторить много раз. Такая программа будет представлена всего несколькими байтами, тогда как финальная картинка будет иметь существенно больший размер.

В настоящее время конкурсы по процедурной графике проводятся в основном для компьютеров уровня PC, Mac и Amiga. Стандартный размер работы – 4 килобайта, то есть меньше размера несжатой картинки в сотни раз. При этом разрешается использовать ресурсы видеокарты, но время просчёта как правило ограничено, например 30 секундами. Обычно рекомендуют прилагать заранее просчитанную картинку, чтобы иметь возможность убедиться в соответствии результата, выдаваемого работой, тому, что задумал автор. Для генерации изображения используются различные подходы, в частности генерация геометрии и текстур различными математическими алгоритмами для создания трёхмерной сцены и рейтрейсинг для её качественной визуализации. Уровень работ часто бывает весьма высоким.

Существует некоторое количество работ для Commodore 64, которые выставлялись в совмещённых конкурсах процедурной графики на разных демопати в 2005-2010 годах. Они также имели размер 4 килобайта при размере несжатой картинки в 16 килобайт и были довольно простыми, что позволяло легко сжать финальную картинку в несколько раз обычными упаковщиками.

На ZX Spectrum с размером несжатой картинки в 6 с небольшим килобайт размер работы в 4 килобайта неприемлем, так как многие нарисованные вручную картинки достаточно легко могут быть сжаты до такого размера стандартными упаковщиками, что лишает использование процедурной графики смысла. На ASCiI и ArtField было установлено ограничение в 1024 байта. Это неплохой компромисс, оставляющий достаточно места для создания интересных работ, но не позволяющий использовать готовые упакованные картинки.

В зависимости от правил конкурса изображение может отображаться в стандартном видеорежиме, как все существующие пока работы, или использовать программные трюки типа мультиколора и Gigascreen.



Как сделать

Рассмотрим возможные подходы для создания процедурной графики на ZX Spectrum. Очевидно, что рейтрейсинг на этом компьютере мало применим, если вообще возможен в таких ограничениях. Поэтому нужны другие, менее ресурсоёмкие подходы.

Наиболее очевидная идея, сразу приходящая в голову – использовать рисование линиями и окружностями через процедуры ПЗУ и заливать замкнутые области своей процедурой заливки. Многие начинали программировать, создавая подобные программы на Бейсике. Этот подход также использовался в ранних текстово-графических приключенческих играх на ZX Spectrum именно для того, чтобы уместить как можно больше изображений в ограниченный объём памяти. При объёме работы в 1024 байта на данные о координатах точек остаётся не так уж много места. Даже если занять весь этот объём только координатами точек, их будет всего 512. Поэтому данный метод годится для создания очень простых изображений, либо для создания части изображения в комбинации с другими подходами. С помощью многократного использования описания одного и того же объекта с внесением изменений, например масштабирования или псевдослучайности, можно получить более интересные результаты.

Другая простая идея – хранить графику в низком разрешении и отображать с масштабированием. Даже без использования сжатия 1024 байт достаточно для хранения всей атрибутной области экрана, либо полноэкранного ASCII, либо чанкового или мультиколорного изображения 64x48 с 2 битами на точку, либо монохромного 128x48, и ещё 256 байт остаётся на код вывода. Сам по себе такой подход не очень интересен, так как оставляет мало места для творчества в коде, но он может быть использован в комбинации с другими методами.

Некоторые из прочих возможных идей – отрисовка полигонов с генерацией модели посредством некоторых вычислений, воксельный ландшафт с генерацией карты, бамп для создания выпуклых изображений на плоскости, эффект линзы для отрисовки шариков с текстурой.

Вместо отрисовки непосредственно на экран можно выполнять отрисовку в растровый буфер или несколько буферов, где каждая точка описана одним байтом, содержащим яркость этой точки. После отрисовки буфер конвертируется в стандартный экран тем или иным способом. Это даёт возможность применять к буферу различные фильтры, например размытие, создавать градиенты, а также добавлять цвет через сопоставление определённых цветов с уровнями яркости точки.

Комбинируя различные подходы можно получить весьма интересные результаты.

v Несколько слов о случайности. Если в работе требуется генерация случайных чисел, лучше сделать их последовательность одинаковой при каждом запуске. Хотя работы с вариациями в конечном результате иногда встречаются на конкурсах процедурной графики для PC, случайность может испортить впечатление от работы, если при показе выпадет менее удачный или даже ошибочный вариант, который автор не наблюдал во время создания работы.



Как сделал я

Далее я подробно расскажу о создании двух моих работ.

Обе работы прототипировались на PC в виде программы на C с библиотекой SDL. Программа сразу отображала результат, близкий к тому, который был получен на ZX, что существенно ускорило процесс отладки алгоритмов и проверки идей.

Обе работы используют идею растрового буфера в формате байт на точку. Буфер имеет разрешение 256x192 и располагается в трёх страницах ОЗУ (работы требуют 128K). В коде есть специальная процедура, преобразующая координаты точки в адрес в странице и выбирающая нужную страницу. Она вызывается в большинстве других процедур отрисовки. Это несколько замедляет процесс рисования, но сокращает код.

Для преобразования буфера в обычный экран я решил использовать диффузный дитеринг (floyd-steinberg), так как он даёт более интересный результат по сравнению с обычным упорядоченным дитерингом, хотя и работает значительно медленее. В моих работах процесс преобразования скрыт нулевыми атрибутами, но если сделать экран видимым, можно наблюдать его выполнение в виде построчной отрисовки изображения сверху вниз в течении примерно 12 секунд.



City Storm



Точно не помню, как возникла идея для этой работы. Я начал делать совсем другую, технически более сложную работу, но понял, что не успею написать и отладить код такой сложности вовремя, и решил сделать вместо этого что-то попроще.

Небо сделано заполнением буфера размером 32*256 (байт на точку) псевдослучайным шумом, который затем восемь раз размывается простым фильтром. К результату добавляется вертикальный градиент, за счёт которого нижняя часть неба темнее, чем верхняя. Этот буфер в растянутом по горизонтали виде копируется в основной буфер растра. Если отобразить такой буфер настоящей яркостью, он будет выглядеть очень пиксельным, с пикселями шириной в знакоместо, но при последующем дитеринге пиксельность сглаживается и становится незаметной. Промежуточный буфер нужен для ускорения отрисовки, многократное размытие в полном разрешении было бы очень медленным.

После отрисовки неба рисуются силуэты зданий на горизонте. Они выполнены чёрными полосками с псевдослучайной высотой и шириной.

Башня рисуется горизонтальными чёрными полосками разной ширины. На краях и в точке с небольшим смещением от центра, зависящим от ширины полоски, рисуется яркая точка, чтобы подчеркнуть контур и добавить блик. Форма башни задана последовательностью чисел – ширина и высота каждой секции.

Для вывода молнии используется процедура рисования полупрозрачных окружностей. Полупрозрачность сделана добавлением к яркости точек внутри круга заданного значения. Линии молнии выводятся процедурой рисования линии по алгоритму Брезенхема, вместо точек рисуются окружности. Форма молнии задана яркостью и парами координат для каждой линии. Место попадания молнии в башню изображено окружностями увеличивающегося диаметра, накладывающимися друг на друга, из-за чего яркость падает от центра к краям.

Генерация и вывод изображения занимают около 30 секунд.



Ball Man



Основной идеей работы было нарисовать некий объект из шариков, которые легко выводить. Сразу вспомнилась игра Vectorman и её герой, но я решил сделать просто похожего персонажа, а не копировать героя целиком.

Сначала рисуется диагональный градиент размером во весь буфер, чтобы разбавить правильность форм на фоне.

Рисуется название работы. Для этого используются процедуры ПЗУ, рисование идёт на обычном экране. Надпись рисуется выше, чем она будет в итоге, потому что процедуры не могут рисовать в двух нижних строках экрана. Название описано последовательностью координат для процедур PLOT и DRAW. Оно рисуется несколько раз подряд с горизонтальным сдвигом, чтобы буквы стали широкими. Из-за низкой скорости работы стандартных процедур отрисовка названия занимает значительное время.

Продолжается отрисовка фона – рисуются большие залитые круги разного диаметра.

После этого выводятся по списку шарики, составляющие модель. В списке содержатся координаты каждого шарика и его размер. Всего 44 шарика, по три байта на каждый. Этот список был составлен в прототипе на PC, для упрощения работы в нём реализован простой редактор, позволяющий передвигать и менять размер всех шариков, и выдающий в итоге данные в текстовом виде для вставки в исходник.

Шарики рисуются всё той же процедурой вывода залитых кругов. Для создания тёмной части яркость точек внутри круга убавляется через вычитание значения, для светлых частей значение прибавляется. Смещение и размер кругов для блика рассчитывается процедурой из координат центра и размера шарика. Тень под персонажем рисуется автоматически для каждого шарика, для этого рисуется сплющенная по вертикали окружность с вертикальной координатой на линии условного пола.

После отрисовки персонажа рисуется фон в области названия и с обычного экрана в растровый буфер копируется ранее отрисованное название с нужным вертикальным сдвигом. Выполняется конверсия буфера в обычный экран с дитерингом.

После отрисовки графики на экран добавляются атрибуты. Они хранятся в виде списка из шести прямоугольных областей, по пять байт на область. Эти области частично пересекаются, но цвет следующей заменяет нужный фрагмент предыдущей.

На генерацию и вывод этого изображения уходит около 55 секунд.