В прошлый раз мы получили анимированный паровоз, используя некоммутирующие дуальные комплексные числа. Теперь добавим фон, причём лежащий в нескольких "планах", один совсем вдали (облачка), другой поближе (верхушки деревьев), Солнце бесконечно далеко, а столбы и вовсе ПЕРЕД ПАРОВОЗОМ.
Реализовать это на удивление просто, ОДНУ ЦИФЕРКУ ПОМЕНЯТЬ!
Под катом также разъяснено, как "ставить камеру на объект", чтобы паровоз вполне себе ехал, но оставался в центре кадра. И, как водится, некоторое количество укуренных гифок.
Напомним: эти числа - частный случай кватернионов, только масштаб по осям Y/Z бесконечно отличается от масштаба по оси X и по действительным числам (скалярам). Мы представили, будто начало координат поместили в центр Земли, ось X выходит из центра и "протыкает" нашу рабочую плоскость, расположенную на "письменном столе". Все объекты на плоскости имеют координату X, равную единице (что соответствует радиусу Земли), а координаты Y,Z отсчитываются в миллиметрах или сантиметрах, и далеко мы не уходим, из-за чего повороты вокруг осей Y, Z могут быть лишь совсем ничтожными, и мы их вообще не воспримем как повороты, для нас это будет поступательное движение по осям Z или Y. Таким немудрёным способом мы "обменяли" произвольные повороты в пространстве на произвольные движения (и поступательные, и вращательные) на плоскости!
Именно поэтому точки (xn, yn), загруженные из SVG-файла, мы превращали в неком. дуал. компл. числа i + jxn + kyn, или (0; 1; xn, yn). Так мы помещали все эти рисунки себе на "письменный стол". [Скалярная компонента может быть любой]Ещё мы имели право поставить произвольную скалярную компоненту, она не оказывает влияния на дальнейшее, она вообще в теории не должна изменяться при любых движениях (т.е поворотах и параллельных переносах), но на неё может повлиять масштабирование. Как это использовать - я пока не придумал. Произвольные данные немного боязно сюда класть, т.к при реализации "в лоб" они участвуют в вычислениях и по законам математики должны остаться неизменными, но при работе с плавающей точкой либо с фиксированной точкой, но с округлением промежуточных результатов, это не так...
Пока будем придерживаться этого принципа и добавим железную дорогу. Она вообще преобразовываться изначально не должна, т.к "неподвижна". Поэтому прямо в SVG установлю ей координаты "внизу кадра", а паровоз чуть-чуть приподниму. Дорога вполне себе конечна, простираясь лишь на 2048 пикселей по горизонтали. Вообще, можно было бы придумать некую "закольцовку", но пока делаем "честно". В одном месте проскакивает стык с накладкой, но без болтов с гайками - их пока лениво было описывать.
Отрисуем это хозяйство: координаты "железной дороги" не трогаем, координаты точек паровоза преобразуем с помощью числа engine, первого ведущего колеса - с помощью engine*wheel1, сцепного дышла - с помощью engine*wheel2*connector, и так далее:
Паровоз буксует на месте... Оно и понятно: мы же и не задали его движения! Число engine, выражающее его положение в пространстве (перемещения и повороты) пока что оставалось неизменным. Исправим это, задав ему поступательное движение. Если мы каждый кадр поворачивали колесо радиусом 73 пикселя на 3 градуса, значит, за тот же кадр паровоз должен сдвигаться вправо на 3,8 пикселя. Поэтому оператор перемещения будет иметь вид locomotion = 1 + (3.8/2)k. На каждом кадре будем делать следующее:
engine = engine * locomotion;
Посмотрим теперь:
Ага, поехал, и тут же скрылся из виду...
А мы теперь хотим, чтобы паровоз оставался неподвижен и перемещал весь мир вокруг себя. Этого добиться очень легко. По сути, все наши итоговые координаты надо теперь домножить слева на Conj(engine), т.е на число, сопряжённое (Conjunction) к числу engine. Такое сопряжённое число выражает обратное движение. Если исходное выражало перемещение паровоза на 100 метров вправо, то сопряжённое будет выражать перемещение на те же 100 метров влево.
При этом, по определению, Conj(engine) * engine = engine * Conj(engine) = 1, что и понятно, два движения полностью друг друга компенсируют. [если число изначально выражало ДВИЖЕНИЕ]Так-то в некомм. дуал. компл. число можно ещё добавить МАСШТАБИРОВАНИЕ, вот тогда вместо взятия сопряженной величины придётся находить обратную, т.е сопряжённое делить на квадрат нормы. С масштабированием поиграемся чуть попозже.
А поэтому нам нет нужды считать Conj(engine)*engine*wheel1, чтобы пересчитывать точки первого колеса. Два левых числа сократятся, оставив попросту wheel1.
Получится, что железную дорогу мы отрисуем, преобразуя точки числом Conj(engine), точки паровоза мы не преобразуем, точки колеса мы преобразуем числом wheel1, и так далее. Везде, где слева стоял engine, его не будет, а где его не было, появится Conj(engine).
Посмотрим, что получается:
Ага, паровоз в центре мира, потому как центром мира мы определили левый верхний угол... Что ж, значит, добавим ещё одно число, EngineViewport = 1 - 240j + 256k, или (1; 0; -240; 256). В начальный момент оно совпадает с engine, но затем engine начнёт меняться, а вот EngineViewport останется неизменным, он указывает "окошечко" (определённое в координатах паровоза, то есть как бы "приваренное" к паровозу), через которое мы наблюдаем за миром.
Теперь, преобразуя точки, будем всё время выполнять слева домножение на EngineViewport, т.е:
- железную дорогу преобразуем через EngineViewport*Conj(engine), - паровоз преобразуем через EngineViewport, - первое ведущее колесо преобразуем через EngineViewport*wheel1, - поршневое дышло преобразуем через EngineViewport*wheel2*rod, и так далее. Вот что получается:
Фух, пошли дела кое-как... Паровоз катится по рельсам, и мы видим, как он проезжает стык. Гифка довольно короткая, 240 кадров, т.е ровно два оборота колеса, затем всё закольцовывается.
Теперь добавим облачка, сначала обычным образом, т.е координаты точек, образующих облака, будут иметь вид i+xnj+ynk, или (0;1;xn;yn). Преобразовывать их будем точно также, как железную дорогу, никаких новых чисел нам и не понадобится. Получаем:
Облака уплыли влево с той же скоростью, будто они гвоздями приколочены к рельсам. Не особо "реалистично". Сделаем простейшую вещь: вместо i поставим 0,1i. Т.е теперь все координаты облаков будут иметь вид 0,1i+xnj+ynk, или (0;0,1;xn;yn). Результат:
Если мы расположили свою рабочую плоскость (где паровоз и рельсы) на расстоянии ЕДИНИЦА от центра, то облака мы расположили на расстоянии в 10 раз меньше. Поворот вокруг оси X (то, что и мы воспринимаем как поворот на плоскости) будет работать в точности также, а вот повороты вокруг осей Y,Z, которые мы воспринимаем как сдвиги, будут иметь в 10 раз меньший эффект!
Давайте ещё добавим некий силуэт на горизонте, то ли горы, то ли леса, в общем, какой-то рельеф. И он будет ползти чуть быстрее, т.к мы его поставим на расстоянии 0,2:
Ещё добавим солнышко "на бесконечность", что в нашем случае означает: В НУЛЕ! Так оно вообще не будет двигаться при поступательном движении паровоза. Конечно, такие неподвижные картинки можно и без всех этих премудростей на экран выводить, но чуть ниже мы поймём разницу. А пока вот:
Ещё одна деталь - и я успокоюсь. СТОЛБЫ! Причём те, что расположены ближе к нам, чем паровоз. Для них поставим расстояние 1,5. Небольшое преувеличение, но для пущего эффекта почему бы нет:
Они нужны были в первую очередь продемонстрировать: нет в нашей "единице" какого-то сакрального значения. Можно и меньше единицы использовать, и больше единицы.
Теперь, как обычно, давайте поиздеваемся, и "приделаем гоупрошку" к сцепному дышлу. Для этого все выражения, перед EngineViewport, будем домножать не на Conj(engine), как ранее, а на Conj(engine*wheel2*connector), или, что то же самое, на Conj(connector)*Conj(wheel2)*Conj(engine).И ещё, вместо engineViewport у нас будет ConnectorViewport, чтобы уместиться в экран поудобнее. Итого:
- железную дорогу, облака, солнышко, горизонт и столбы преобразуем через ConnectorViewport*Conj(connector)*Conj(wheel2)*Conj(engine), - паровоз преобразуем через ConnectorViewport*Conj(connector)*Conj(wheel2). Переменная engine сократится, - первое ведущее колесо преобразуем через ConnectorViewport*Conj(connector)*Conj(wheel2)*wheel1, - второе ведущее колесо преобразуем через ConnectorViewport*Conj(connector). Переменные engine и wheel2 сократятся, - само сцепное дышло преобразуем через ConnectorViewport, и всё тут! Вот от того оно и станет неподвижным. - поршневое дышло преобразуем через ConnectorViewport*Conj(connector)*rod.
И напоследок прицепимся к поршневому дышлу! Можно было бы ещё и к колесу, но это, блин, и голова закружиться может. Итак:
Вот и солнышко пришло в движение! И в целом мы увидели, как все эти облачка, горизонт и прочее поворачиваются единообразно, что является вполне реалистичным поведением. Как мы знаем, горизонт заваливается совершенно замечательно, каким бы бесконечно отдалённым он ни был!
Как видим, один из "багов" некомм. дуал. компл. чисел (необходимость выставить "волшебное значение", единичку в мнимой компоненте незнамо зачем) оказалась хорошей такой, добротной фичей. Но возможности данного формализма мы до сих пор не исчерпали. Как минимум, хочу в дальнейшем показать интерполяцию движений и "оператор масштабирования". Продолжение следует!