Зона кода

А давайте немного попрограммируем...

Построение фигур Лиссажу на C99

C99Графические программы

Просматривая книгу Донована и Кернигана "Язык программирования Go", наткнулся я на раздел, в котором рассматривается программа, выполняющая построение фигур Лиссажу. Написана программа, разумеется, на языке Go. И решил я, в качестве развлечения, написать аналогичную программу, но на языке C99. Тем более, что давно уже без дела пылится созданная нами графическая библиотека, написанная на данном языке.

Упомянутый раздел содержит короткий рассказ о фигурах Лиссажу, сам программный код и достаточное подробное его описание. Я решил первым делом обратиться к коду, чтобы "вытащить" из него алгоритм построения фигур, для дальнейшей реализации его на C99.

Замечу, что у меня есть опыт программирования, в частности, на C, C++, Java и, в далёком прошлом, — на Паскале. А вот язык Go я не знаю совсем! Но, к своему удивлению, я обнаружил, что 95% кода на этом незнакомом для меня языке мне совершенно понятны! А оставшиеся 5% для понимания сути программы не требуются. Всё-таки, императивные языки программирования являются между собой родственниками, в одних случаях, близкими, а в других — дальними.

Разумеется, роль сыграл не только мой опыт. Программа расположена в книге достаточно близко к её началу и является весьма простой. В ней, судя по всему, задействованы, в основном, базовые конструкции языка. Если бы я рассмотрел программу, напечатанную ближе к концу, то ни о каких 95%, скорее всего, не могло бы идти и речи. Но, с другой стороны, даже простая программа на языке Clojure, чуть более сложная, чем "Hello World!", вводит меня в 100%-й ступор (проверено!). Тут уже ни о каких родственных связях с императивными языками говорить не приходится.

Но вернёмся к нашей программе на Go. Я открыл редактор кода и, читая текст программы, тут же начал "переводить" её с Go на C99, сразу же набирая результирующий код. В итоге, я, фактически, выполнил трансляцию программы с одного языка на другой. Получившаяся программа успешно сработала.

В моих планах — познакомиться с языком Go более плотно, при условии, что удастся найти время. А я надеюсь, что найти его удастся. Своим опытом освоения Go, если он появится, обязательно поделюсь на страницах данного сайта.

Коротко о фигурах Лиссажу

Фигура Лиссажу представляет собой кривую, которая в прямоугольной системе координат Oxy задаётся следующей системой параметрических уравнений:

xt=Asinatyt=Bsinbt+δ.

Здесь параметр t пробегает все возможные значения. Как мы видим, каждая из функций x(t) и y(t) описывает гармонические колебания с амплитудами A и B, частотами a и b, начальными фазами 0 и δ соответственно (δ можно рассматривать и как разность фаз).

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

Программа для построения фигур Лиссажу

Программа на языке Go, приведённая в книге, в ходе своего выполнения строит целое семейство фигур Лиссажу. Каждая фигура рисуется на прямоугольной области, а все такие области в качестве кадров анимации собираются в отдельный файл формата GIF. Таким образом, результатом работы программы является анимированный GIF-файл, при просмотре которого можно наблюдать, как фигуры Лиссажу плавно переходят друг в друга. Первый кадр является "продолжением" последнего, поэтому при циклическом повторении анимации плавность никогда не нарушается.

В программе на Go выбраны следующие значения параметров кривых. Амплитуды: A = B = 0. Относительная частота колебаний (т. е. отношение b / a) генерируется с помощью датчика псевдослучайных чисел и принадлежит промежутку от 0 до 3. В качестве a всегда берётся единица. А вот начальная фаза δ пробегает значения от 0 до 6,3 (т. е. примерно до 2π) c шагом 0,1; каждому кадру анимации соответствует своё значение δ.

В нашем распоряжении нет библиотеки на C99, позволяющей создавать GIF-файлы. Поэтому действовать мы будем немного иначе. С помощью нашей графической библиотеки создадим кадры анимации в виде отдельных BMP-файлов, после чего сконструируем из них анимацию в виде видеофайла формата AVI. Для создания видеофайла воспользуемся свободно распространяемым видеоредактором VirtualDub.

Программа на языке Go рисует чёрные кривые на белом фоне; в книге в качестве самостоятельного задания предлагается изменить цвет фона на чёрный, а линий — на зелёный, чтобы вызвать ассоциации с изображениями на экране осциллографа. Ну а мы сразу же будем рисовать зелёным по чёрному.

Ниже приведён код программы, выполняющей построение фигур Лиссажу на языке C99.

 1.#include "pgraph.h"
 2.
 3.const int cycles = 5;
 4.const double res = 0.001;
 5.const double frec = 1.5;
 6.const int size = 175;
 7.const int sizeX = 320;
 8.const int sizeY = 180;
 9.const int nframes = 63;
10.const int TWO_PI = 6.28318530717959;
11.
12.int main()
13.{
14.    double phase = 0.0;
15.    image *img = create_image(2 * sizeX, 2 * sizeY);
16.    char filename[14];
17.    for (int i = 1; i <= nframes; i++)
18.    {
19.        fill_all(img, BLACK);
20.        for (double t = 0.0; t < cycles * TWO_PI; t += res)
21.        {
22.            double x = sin(t);
23.            double y = sin(t * frec + phase);
24.            set_color(img, sizeX + round(x * size + 0.5), 
25.                      sizeY + round(y * size + 0.5), GREEN);
26.        }            
27.        sprintf(filename, "results\\%02d.bmp", i);
28.        save_to_file(img, filename);
29.        phase += 0.1;    
30.    }
31.    free(img);
32.    return 0;
33.}

Рассмотрим подробно приведённый код.

В 1-й строке ожидаемо подключается графическая библиотека pgraph.

Далее объявляются следующие константные переменные:

  • cycles — количество периодов, помещающихся в отрезок времени, в течение которого происходят гармонические колебания по оси X (один период равен 2π). Я оставил то значение (5), которое установлено в программе на Go.
  • res — шаг параметра t. При построении каждой фигуры Лиссажу этот параметр пробегает значения от нуля до 2π, умноженных на число периодов, хранящееся в cycles, с шагом res. Обычно шаг подбирается эмпирически. Слишком маленькое значение приведёт к разрывам кривых, а слишком большое — к увеличению времени работы программы. Я выбрал то же значение, что используется и в программе на Go: 0,001.
  • frec — относительная частота колебаний. Как уже было сказано, в программе на Go она генерируется с помощью датчика псевдослучайных чисел. Однако я отказался от такого подхода, поскольку, в случае, если значение относительной частоты мало, фигуры окажутся "некрасивыми" (сплюснутыми). Я решил, что лучше подобрать это значение вручную. Как мне показалось, 1,5 — достаточно неплохой вариант, хотя можно использовать и другие значения (всё, как обычно, в руках читателя).
  • size — параметр, отвечающий за размер той части прямоугольной области, которая отводится под фигуру. Эта часть представляет собой квадрат стороной 2 * size + 1. В программе на Go использовалось значение 100, но я решил сделать изображение более крупным и установил значение 175. Таким образом, в нашем случае сторона квадрата равна 351.
  • sizeX и sizeY — половины ширины и длины изображения (кадра) соответственно. В программе на Go изображение представляет собой тот самый квадрат, о котором шла речь в предыдущем пункте списка. Однако, поскольку из кадров в дальнейшем будет смонтировано видео, я решил, что кадр должен иметь стандартные размеры. Значения рассматриваемых переменных равны 320 и 180 соответственно, откуда следует, что кадр будет иметь размеры 640x360. А квадрат размером 351x351, содержащий фигуру Лиссажу, будет располагаться внутри этого кадра.
  • nframes — количество кадров в анимации. Поскольку мы хотим, чтобы первый кадр был "продолжением" последнего, то главное требование к значению данной переменной — согласованность с шагом, с которым изменяется начальная фаза. А именно: шаг, умноженный на количество кадров без единицы, должен быть наиболее близок к числу 2π, но не превышать его. В программе на Go количество кадров равно 64, а шаг — 0,1. Шаг я выбрал таким же, но, поскольку 6,3 превышает 2π, то количество кадров я решил уменьшить на единицу; в нашем случае оно равно 63.
  • TWO_PI — число 2π, взятое с точностью 15 знаков после запятой.

А теперь переходим к основной части программы, полностью содержащейся в функции main() (стр. 12).

В 14-й строке устанавливаем начальное значение фазы, а в 15-й создаём изображение необходимого нам размера. Мы не будем создавать для каждого кадра своё изображение; после того, как очередной кадр отрисован и сохранён в файле, будем использовать для построение нового кадра "старое" изображение. А удалим его лишь один раз — после того, как все кадры будут сохранены в файлах.

В 16-й строке создаём строковый массив filename для хранения имён (с учётом директории) графических файлов, создаваемых нашей программой.

Далее переходим к внешнему циклу (стр. 17), на каждой итерации которого создаётся один кадр и записывается в файл. Количество итераций, очевидно, совпадает с числом кадров. Заливаем изображение чёрным цветом цветом (стр. 19) и переходим к внутреннему циклу (стр. 20).

На каждой итерации внутреннего цикла рисуется одна точка фигуры Лиссажу, соответствующая одному значению параметра t (данные значения в ходе цикла принимает переменная цикла t). Сначала вычисляем координаты точки (стр. 22, 23), а затем "рисуем" эту точку на чёрном фоне зелёным цветом (стр. 24, 25).

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

После окончания внутреннего цикла помещаем в filename имя (с учётом директории), под которым мы хотим сохранить файл с кадром (стр. 27). Оно имеет вид "result\XX.bmp". Здесь result — это имя директории, находящейся в корневом каталоге исполняемого файла, в котором будут храниться графические файлы (к моменту выполнения программы она должна существовать), а XX — это двухзначные номера кадров (если номер кадра состоит из одной цифры, до она дополняется слева нулём); нумерация кадров начинается с единицы.

В 28-й строке сохраняем графический файл под сформированным ранее именем, а в 29-й — изменяем значение фазы. Теперь на следующей итерации внешнего цикла (если она будет выполнена) очередная фигура Лиссажу будет построена для нового значения фазы.

По окончании внешнего цикла нам остаётся лишь удалить изображение (стр. 31) и вернуть из main() нулевое значение (стр. 32).

Создание видеофайла

После выполнения нашей программы в директории result, находящейся в корневом каталоге исполняемого файла, появляются 63 графических файла с именами 01.bmp, 02.bmp, ..., 63.bmp. Наша задача теперь — создать из них, как из кадров, видеофайл.

Для этой цели будем использовать свободно распространяемый видеоредактор VirtualDub (я использую русифицированную версию под номером 1.10.4). После запуска программы в меню "Файл" выбираем пункт "Открыть видео файл...". Появляется диалоговое окно открытия файла, в котором нужно выбрать файл 01.BMP, убедиться в том, что отмечена галочка слева от надписи "Automatically load linked segments" и нажать на кнопу "Открыть" (см. рисунок).

Диалоговое окно редактора VirtualDub, предназначенное для открытия файлов

Открытие упорядоченного набора графических файлов в VirtualDub

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

При желании, ролик можно тут же просмотреть, щёлкнув по второй кнопке слева в ряду кнопок, расположенном в нижней части окна видеоредактора (рисунок можно увеличить, щёлкнув по нему):

Окно программы VirtualDub во время воспроизведения анимации

Воспроизведение анимации в окне VirtualDub

По умолчанию устанавливается частота кадров 10 кадров/сек. При желании, её можно изменить (в программе на Go, кстати, используется частота 12,5 кадров/сек.), но я не стал этого делать. Также можно перед сохранением ролика настроить параметры сжатия. И этого я делать не стал.

А для сохранения анимации в виде AVI-файла нужно в меню "Файл" выбрать пункт "Сохранить как AVI...", после чего в раскрывшемся диалоговом окне указать имя файла и нажать на кнопку "Сохранить".

В итоге получается ролик длительностью 6,3 с. Это время показалось мне недостаточным для наблюдения над анимацией, поэтому я решил его увеличить. Сделал я это следующим образом. Открыл получившийся ролик в VirtualDub, затем в меню "Файл" выбрал пункт "Добавить AVI сегмент...", после чего в открывшемся диалоговом окне выбрал тот же самый ролик. Далее повторил операцию добавления сегмента ещё 8 раз.

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

Для этого, непосредственно перед сохранением ролика, в меню "Видео" я щёлкнул по пункту "Компрессия...", в левом списке открывшегося диалогового окна выбрал кодек Huffyuv и нажал на кнопку "OK":

Диалоговое окно настройки компресии редактора VirtualDub

Выбор кодека для сжатия видеофайла

Теперь при сохранении ролика в формате AVI происходит его автоматическое сжатие (благодаря используемому кодеку сжатие выполняется без потерь). Результирующий ролик занимает примерно 124 МБ а длительность его, на этот раз, составляет 63 секунды.

Разумеется, можно было бы предварительно сжать начальный 6-секундный ролик, а новый получить объединением 10 одинаковых уже сжатых роликов. Но эконеомия времени была бы несущественной.

Демонстрация видеофайла

Созданный программой VirtualDub ролик я разместил на Ютьюбе. Вы можете просмотреть его прямо здесь и сейчас:

К сожалению, из-за сжатия Ютьюб показывает ролик в качестве, уступающем оригинальному. Яркий зелёный цвет фигур Лиссажу (см. скриншот окна VirtualDub) превратился в блекло-зелёный с грязноватыми разводами. Тем не менее, представление об исходной анимации получить можно.

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

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

Заключение

Ну что ж, мы убедились в том, что с помощью языка C99 и редактора VirtualDub можно создавать простейшие анимации. Возможно, к этой теме мы ещё вернёмся в будущем.

Скачать исходный код