Анимация с максимальной эффективностью

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

Анимация в средах DOS и Windows
    Подавляющее большинство дисплеев PC хранит выводимые на экран данные в специальной области памяти — видеобуфере (видеопамяти), и для того, что-бы изменить картинку на экране, программа должна изменить содержимое этой области.
    Программисты, пишущие программы для DOS, отлично знают, что максимальной производительности можно достичь только в том случае, если обращаться к этой памяти напрямую. Вместо того, чтобы выводить изображение на экран стандартными средствами BIOS и операционной системы, они получают указатель на область видеопамяти и напрямую копируют в нее необходимые данные. Такой подход гарантирует скорость и качество анимации, которые вы видите в большинстве видеоигр, работающих в среде DOS.
    Программистам для Windows повезло меньше. Одно из основных преимуществ Windows — независимость от устройств — становится и одним из наиболее серьезных недостатков, делая невозможным прямой доступ к видеопамяти. Все программы должны выводить данные на экран при помощи функций стандартного интерфейса GDI, который имеет две стороны. Со стороны Windows это набор функций для управления дисплеем (BitBlt(), StretchBlt(), LineTo(), Ellipse() и т.п.). Со стороны оборудования этот интерфейс взаимодействует с аппаратурой вашего компьютера, совершая все необходимые конкретные действия. Со стороны Windows интерфейс обычно представляется контекстом устройства, который в программе на VC++ имеет вид объекта CDC.
    Многообразие графических программ для Windows доказывает эффективность такого подхода к графическому программированию. Все графические операции происходят достаточно быстро, и причину этому отыскать несложно — записывая информацию в контекст устройства при помощи функций GDI, вы, в сущности, пишете прямо в видеопамять. Но есть и ограничение (а где их нет?): для вывода информации в контекст устройства вы можете пользоваться ТОЛЬКО функциями GDI. Скоро вы поймете, почему это порождает проблемы.
Как вы, наверное, заметили, большинство программ с быстрой графской и видео все еще пишутся для DOS, а не для Windows. Почему? Ведь интерфейс GDI так быстр и удобен! Проблема заключается в том, что этот интерфейс не содержит многих полезных функций, в частности тех, которые требуются для работы с анимацией.

Анимация спрайтов в среде DOS
    Пользуясь ничем не ограниченным доступом к видеопамяти, DOS-программа может выполнять анимацию спрайтов с максимально возможной эффективностью. В качестве примера можно рассмотреть анимацию "прозрачного" спрайта. Программа анализирует его пиксел за пикселом и игнорирует те пикселы, которые имеют "прозрачный" цвет. Те же, которые окрашены по-другому, просто котируются в необходимую точку видеопамяти. Если, например, в нашем спрайте размером 50х50 пикселов половина из них имеет "прозрачный" цвет фона, то для вывода такого спрайта нам потребуется скопировать 0.5х50х50= =1 250 байтов (если исходить из того, что каждый байт занимает один пиксел). Теперь давайте сравним это с тем, что происходит в системе Windows.

Анимация спрайтов в среде Windows
    Давайте рассмотрим ту технику анимации, которой мы пользовались в предыдущей главе. Сначала мы копируем на экран функцией StretchBlt() маску, имеющую размер 50х50 пикселов, то есть 2 500 байтов. После этого мы копируем сам спрайт, то есть еще 2 500 байтов — всего 5 000 байтов, то есть в четыре раза больше того количества, которое требуется при программировании в среде DOS. Таким образом, анимация под Windows оказывается как минимум в четыре раза менее эффективной, чем в среде DOS. Если учесть сохранение и восстановление фона, то разница становится еще более ощутимой. Кроме того, есть проблема мерцания. От нее можно избавиться, производя все предварительные операции копирования в контексте устройства, хранящемся в памяти, и потом просто копируя законченный участок на необходимое место экрана. Однако при этом добавляется еще одна блит-операция, и эффективность снова снижается.
    Почему мы не можем просто скопировать наш спрайт на экран, имитируя то, что происходит в среде DOS? Дело в том, что BitBlt() и ее приятельница StretchBItO могут копировать только прямоугольные области, поэтому нельзя скопировать только пикселы спрайта, игнорируя фон.

"WinG, это Алиса. Алиса, это WinG. Унесите WinG."
    Поспешим вас успокоить: парни из Microsoft знают о проблеме и уже попытались ее решить. Их решение называется "графическая библиотека WinG"(или DirectDRAW из DirectX комплекта) — мы кратко знакомили вас с ней в пятой главе. Буква "G" в ее названии означает "Games" — игры. Основная цель WinG — облегчить жизнь разработчикам игр и других проектов, требующих быстрой анимации.
    Несмотря на сложность реализации WinG, ее идея достаточно проста: эта библиотека обеспечивает непосредственный доступ к видеопамяти. Ну, может быть, не совсем так. Мы все еще не можем добраться к самой видеопамяти, так как это противоречило бы требованию независимости от конкретного устройства, однако предоставляемые WinG возможности почти так же хороши. WinG позволяет вам создать контекст устройства, который можно обрабатывать как при помощи стандартных функций GDI, так и при помощи непосредственных манипуляций с битами. После того, как изображение полностью нарисовано, вы можете скопировать его на экран одной операцией BitBlt(). Конечно, по сравнению с DOS добавляется один лишний шаг — копирование, — но разница не очень заметна, особенно если учесть скорость работы современных видеокарт и процессоров. WinG позволяет Windows-программам выводить анимацию почти так же эффективно, как и в среде DOS.
    Теперь, когда мы познакомили вас в общих чертах с библиотекой WinG, мы скажем вам, что вы должны о ней забыть. Как забыть?! Очень просто. WinG разрабатывалась как расширение старой версии Windows 3.11. Новая система Windows 95 (а также Windows NT) уже содержит в себе возможности WinG, в частности, функцию CreateDIBSection(), которая создает контекст устройства для непосредственной работы с битами. Именно это и ляжет в основу нашего следующего проекта.

Реализация внеэкранного буфера
    Внеэкранный буфер, создаваемый функцией CreateDIBSection(), прост только на словах, но не на деле. Информация, приведенная в документации на VC++, не отличается полнотой. Для того, чтобы заставить нашу программу работать так, как надо, нам пришлось провести ряд экспериментов, и мы все еще не
уверены, что понимаем, ПОЧЕМУ она работает. Но будем надеяться, что сам факт ее работы (и, отметим, правильной работы!) вас несколько утешит.
Вот примерное описание примененного нами подхода — о деталях мы поговорим чуть позже.

  1. Вызвать CreateDIBSection() для создания совместимого с дисплеем внеэкранного  буфера.
  2. Создать в занятой внеэкранным буфером памяти объект CDIBitmap.
  3. Скопировать во внеэкранный буфер объект CDIBitmap с изображениемфоном.  После этого для вывода фона на экран нам потребуется только одна блит-операция.
  4. Для вывода спрайта его "непрозрачные" пикселы должны копироваться в  соответствующие места буфера. "Прозрачные" пикселы при копировании просто  игнорируются.
  5. Для стирания спрайта нам достаточно скопировать в буфер необходимый участок  изображения фона.
    Обратите внимание на хитрость, использованную нами в шагах 1 и 2. Внеэкранный буфер и объект CDIBitmap пользуются одной и той же областью реальной памяти (конечно, они должны иметь для этого одинаковый размер). Это позволяет нам пользоваться одновременно как функциями класса CDIBitmap, так и особенностями внеэкранного буфера.

Ускорение работы с палитрами
    Как вы уже убедились, главное при программировании анимации — это скорость. Конечно, значение CreateDIBSection() трудно переоценить, однако есть и другие способы ускорить работу программы. Давайте посмотрим: быть может, нам удастся усовершенствовать механизм работы с палитрами. Начнем с того, что изучим стандартную блит-операцию, например, StretchDIBits(), использованную в функции DrawBitmap() класса CDIBitmap. Для каждого пиксела битового изображения GDI делает следующее:

  1. Получает значение пиксела, соответствующее элементу таблицы цветов DIB.
  2. Получает из таблицы цветов соответствующие значения RGB.
  3. Находит ближайшее соответствие в текущей логической палитре.
  4. Находит необходимый элемент в системной палитре.
  5. Передает полученное значение (номер цвета в системной палитре) драйверу устройства, который и записывает пиксел в видеопамять.
    Обратите внимание на то, что этот процесс происходит даже в том случае, если палитра DIB выбрана и реализована как основная. Почему? Потому что выбор и реализация палитры гарантируют, что системная палитра будет содержать все необходимые цвета, но не гарантирует, что номера этих цветов будут соответствовать их номерам в логической палитре DIB. В итоге мы получаем сложный процесс, который, к тому же, выполняется для каждого пиксела. Давайте попробуем кое-что упростить.
    Что, если значения пикселов в битовом изображении будут указывать сразу на элементы системной палитры? Тогда для вывода изображения нам достаточно будет просто скопировать все его пикселы в видеопамять, не занимаясь описанными выше сложными преобразованиями. Так мы и поступим. Функция StretchDIBits() предусматривает возможность интерпретации значений пикселов в качестве ссылок на элементы системной Палитры. Для этого необходимо установить аргумент Usage в значение, определенное константой DIB_PAL_COLORS. Другое значение этого аргумента, DIB_RGB_COLORS, использовалось нами в функции CDIBitmap::DrawBitmap() и указывает на то, что значения пикселов ссылаются на логическую палитру DIB.
    Как вы догадываетесь, использовать режим DIB_PAL_COLORS можно только после определенной подготовки. Нам нужно добиться того, чтобы палитра DIB в точности совпадала с системной палитрой, то есть создать уже упоминавшуюся в пятой главе стандартную палитру (identity palette). При этом мы должны сохранить 20 стандартных цветов Windows и не забыть о приоритетах цветов. Под приоритетами цветов мы понимаем тот факт, что в палитре DIB цвета обычно располагаются по значимости: наиболее важные имеют меньшие номера. При изменении цветов в первую очередь затрагиваются наименее важные цвета.
 Мы будем пользоваться следующей техникой: начнем с создания логической палитры DIB, ее выбора и реализации в экранном контексте устройства. При этом GDI разместит цвета DIB внутри системной палитры. После этого мы получим значения элементов системной палитры при помощи GetSystemPaletteEntries() и скопируем их в логическую палитру при помощи SetPaletteEntries() — и все! Стандартная палитра создана!
 Но есть еще одна небольшая проблема. Положения цветов в логической палитре изменились, и байты пикселов DIB теперь указывают на неправильные цвета. Если вы выведете изображение на экран, то получите совсем не то, что нужно. Для того, чтобы исправить это, мы берем RGB-значение пиксела из таблицы цветов DIB (ее-то мы не изменяли) и находим соответствующий цвет в новой логической палитре при помощи GetNearestPaletteColor(). Повторив этот процесс для каждого пиксела, мы приводим наше изображение к его исходному виду, только палитра теперь стала стандартной.
    После всего этого мы наконец-то получаем то, что хотим — пикселы нашего битового изображения указывают на цвета в системной палитре. Разместив эти пикселы в буфере, мы можем прямо копировать их в экранньй контекст устройства.
    Описанная нами процедура "портит" изображение, перемешивая его таблицу цветов, и вы не можете записать его на диск, не восстанавливая исходную таблицу цветов и значения пикселов. Однако программы анимации очень редко занимаются записью битовых изображений на диск, и поэтому касаться этой темы мы не будем.

Создание класса спрайтов
    Мы с гордостью несем почетное звание "объектно-ориентированных программистов", так давайте же оправдаем его и создадим специальный класс для спрайтов, который впоследствии можно будет использовать в других программах. Этот класс должен обладать гибкостью, необходимой для воспроизведения самых разных последовательностей анимации, и кроме того, должен учитывать механизм анимации, использующий CresteDIBSection(). Класс спрайта должен заниматься всеми операциями, необходимыми для загрузки битового изображения, переключения между отдельными кадрами последовательности и вывода спрайта. "Вывод" в данном случае означает копирование всех пикселов спрайта, кроме имеющих "прозрачный" цвет, в определенное место внеэкранного контекста устройства, созданного функцией CreateDIBSection().
    В предыдущем проекте для хранения каждого кадра спрайта мы использовали разные битовые изображения. Это ненужная роскошь. Гораздо удобнее хранить все кадры спрайта в одном изображении, подобно тому, как мы это делали при анимации перелистывающейся книги. Дополнительное преимущество такого метода заключается в том, что классу спрайта приходится загружать только одно битовое изображение. Мы уже создали класс CDIBitmap, поэтому давайте произведем класс спрайта именно от него. Обратите внимание на то, что класс спрайта не может самостоятельно определить размеры и количество строк и столбцов в изображении с кадрами, а также общее количество кадров — всю эту информацию необходимо указывать в явном виде.
    Для нашего нового проекта мы будем использовать битовое изображение с четырьмя кадрами, полученными простым копированием спрайтов, созданных нами в предыдущей главе. Это изображение приведено на рис .12.1 и может быть создано при помощи любого графического редактора.  Анимация с использованием CresteDIBSection() быстрой и качественной анимации с использованием возможностей функции CreateDIBSection().

  1. Создайте диалоговое приложение SPRITE2.
  2. Скопируйте в каталог проекта файлы DIB.CPP, DIB.H, DIBPAL.CPP и DIBPAL.H. Подключите их к проекту и измените в соответствии с листингами 12.1—12.7.
  3. Создайте два новых файла SPRITE.CPP и SPRITE.Н - в них будет размещаться описание и реализация класса CSprite. Подключите их к проекту и добавьте необходимый код, приведенный в листингах 12.8 и 12.9.
Запуск SPRITE2
Когда вы запустите SPRITE2, то увидите точно такое же изображение, как и в программе SPRITE1. Нажатие левой кнопки мыши запустит анимацию, а "правый" щелчок ее остановит. Отличие заключается в качестве самой анимации: как видите, теперь отсутствует раздражающее мерцание изображения, а скорость анимации стала значительно выше. Чтобы убедиться в этом, попробуйте изменять значение константы ANIM_SPEED в файле SPRITDLG.H, подставляя вместо 100 меньшую задержку — 50 или даже 25. Если вы сделаете то же самое в SPRITE1, то увидите, что скорости ей явно не хватает. Может показаться, что скорость вполне достаточна для простой анимации одного спрайта, однако когда их несколько и необходимо анализировать столкновения между ними, вы будете рады даже самому небольшому увеличению производительности.
 
Рисунок 12.1. Спрайт мотылька в MOTHS.BMP