Чуть больше чем год назад, когда я еще был с++-ером, мне очень понравилась визуализация для AIMP(на самом деле в AIMP встроена библиотека от древнего плеера Sonique, для которого и писались визуализации) — Phthalo's Corona.
Я захотел разобраться как она работает и сделать точно такую же. В итоге у меня получился только прототип движения частиц как в этой визуализации.
Это тот прототип.
Перед Новым Годом, я просматривал свои программы и вспомнил про этот прототип. И мне захотелось написать его на флеше, причем не останавливаясь на прототипе, сделать все на высшем уровне :).
И спустя 2 дня, вот он, клон(почти клон, сделаны некоторые изменения) визуализации.
Клик мыши — сменить режим размытия.
Смотреть во весь экран
Для того, чтобы она корректно работала, нужно открыть какой-нибудь музыкальный плеер(например в контакте) в браузере, и закрыть все лишние вкладки с аудио данными, потому что при получении спектральных данных, через SoundMixer.computeSpectrum флешка может вылетать из-за, того что другие приложения не позволяют брать у них аудио данные. Если не падает, то можно ничего не закрывать. Ну как-то так :)
Физика
В области расположены частицы(в данном случае 650). 600 активных частиц, и 50 частиц которые появляются в случайной точке области и падают в низ, когда такая частица касается пола, она заново респаунится в области. Само собой в на все частицы действует гравитация.
По мимо гравитации на частицы можно влиять аттрактором и репульсором. Сейчас я объясню, что это за штуки такие волшебные :)
На самом деле, они никакие не волшебные, просто названия у них такие же заумные, какими любят выражаться всякие «профессора».
Аттрактор — это такая штука, которая притягивает к себе все, что только можно. В данном случае частицы, причем чем дальше частица от аттрактора, тем слабее она притягивается.
Репульсор — тоже самое, что и аттрактор, только он наоборот отталкивает все от себя.
Так вот, на частицы при определенных условиях(основываясь на звуковом спектре) действует аттрактор и репульсор. Из-за этого они так скачут в области.
Отрисовка
Создается bitmapdata для частиц, одна для всех(это логично :)). Это маленький белый кружок, почему белый объясню ниже.
var radius: Number = PARTICLE_SIZE * 0.5;
var particleShape: Shape = new Shape();
particleShape.graphics.beginFill(0xFFFFFF);
particleShape.graphics.drawCircle(PARTICLE_SIZE * 0.5, PARTICLE_SIZE * 0.5, radius);
m_ParticleBmp = new BitmapData(PARTICLE_SIZE, PARTICLE_SIZE, true, 0x0);
m_ParticleBmp.draw(particleShape);
Рисуются частицы так:
var velDir: Number = Math.atan2(part.vel.y, part.vel.x);
var speed: Number = Math.max(Math.abs(part.vel.y), Math.abs(part.vel.x));
if (speed > 20)
speed = 20;
matr.identity();
matr.scale(1.3 + speed / 20.0 * 4.0, 0.5);
matr.rotate(velDir);
matr.translate(part.pos.x, part.pos.y);
m_BackBuffer.draw(m_ParticleBmp, matr);
Матрица трансформирует частицы так, что они растягиваются при движении. Ничего сложного.
Эффекты
Палитра:
При запуске визуализации, создаются палитры из которых идет выборка цвета при отрисовке конечного изображения(стандартная фитча демокодеров :))
Они рандомно выбираются в процессе работы.
Для работы с палитрой, необходимо заготовить 3 массива размером 256 значений. Это будут массивы для красного, зеленого и синего цветов.
Для того чтобы их задействовать, необходимо воспользоваться функцией paletteMap класса BitmapData . Она получает в качестве параметров как-раз таки эти массивы и исходное изображение.
Работает это так:
1) берется цвет пикселя из исходного изображения, integer;
2) этот цвет разбивается на rgb(byte) компоненты;
3) по этим компонентам, берется значение из соответствующего массива, это и будут новые rgb значения;
4) новые rgb значения собираются обратно в integer.
Теперь объясню, почему частицы нужно рисовать белым, а фон черным.
При применении блур фильтра, все пиксели будут размываться и в итоге, если ничего не рисовать, они станут черными. При применении палитры, черный заменяется на любой цвет, таким образом при размытии цвет будет плавно переходить в нужный нам цвет(в буфере, в который рисуются частицы, фон останется черным).
Палитра строится так, что в верхних индексах (255 — белый цвет) rgb массивов, записывается цвет частиц, а в нижних (0 — черный цвет) цвет фона.
Во всех остальных индексах [1..254] записываются цвета градиента от цвета фона до цвета частицы. Конечно никто не мешает добавить больше 2 цветов в палитру и сделать градиент между ними.
Distortion map
В оригинальной визуализации, сделано очень интересное завихрение следа частиц. Я долго всматривался в него, чтобы понять как оно работает.
В итоге я пришел к выводу, что это гиперболическая спираль.
Уравнение гиперболической спирали(в полярных координатах) :
r * phi = a, где a — это скорость расхождения спирали
В декартовых координатах:
x = a * cos(phi) / phi,
y = a * sin(phi) / phi, где a / phi = r
Встал вопрос как теперь это сделать О_О. Во первых, нужно как-то задать формулу для такого дисторшина, во вторых как это сделать на флеше.
Конечно же сначала я попробовал сделать все по старинке, пройдясь по всем пикселям изображения и сдвинуть пиксели в определенных направлениях, к тому же это позволило бы сразу же туда засунуть и размытие. Но на практике оказалось все намного иначе. Цикл по изображению убивал fps. Пришлось искать другое решение.
В голову пришла идея сделать twirl эффект, сразу нашел код через DisplacementMapFilter . Но это было не то, что было в оригинале.
Пришлось вернутся к обзору гиперболической спирали . Вся трудность заключалась в том, что спираль четко задается через угол и радиус в полярных координатах, а мне нужно было добавить еще один параметр — толщину. В итоге после вывода некоторых формул, я написал алгоритм генерации карты смещений.
Карта смещений строится по такому алгоритму:
Пробегаясь по всем пикселям изображения, каждый пиксель проверяется на принадлежность спирали(учитывая толщину). Если пиксель принадлежит спирали, то в этой точке необходимо вычислить смещение.
Для нахождения направления смещения можно взять касательную в данной точке. Для упрощения расчетов я решил находить касательную к окружности в данной точке. При таком подходе, погрешность расчетов будет заметна только в хвосте спирали. Т.к. при увеличении радиуса(или угла) y координата стремится к a, т.е. к линии.
f'(x,y) = ArcTan(y/x) — Pi / 2
Теперь просто находим вектор по полученому углу и умножаем его на коэффициент смещения.
В итоге получилась такая карта смещений(используется зеленый и синий каналы)
Во втором режиме, накладывается другая карта смещений которая генерируется шумом Перлина.
m_NoiseBuffer.perlinNoise(SCREEN_WIDTH, SCREEN_HEIGHT - FLOOR_POS, 2, 1,
false, true, BitmapDataChannel.RED | BitmapDataChannel.BLUE, false, [m_OctaveOffset, m_OctaveOffset]);
Вот такое симпатичное изображение получается на выходе:
Можно было бы добавить больше октав при генерации, но шум генерируется каждый апдейт(для этого и используется смещение октав, для эффекта движения).
В итоге шаги отрисовки такие :
1) К backBuffer применяется DisplacementMapFilter фильтр со смещениями.
2) К backBuffer применяется фильтр размытия(x: 8, y: 8, quality: 1).
3) К backBuffer применяется ColorTransform, так мы регулируем скорость затухания изображения.
4) Рисуем частицы в backBuffer.
5) К screeBuffer применяем paletteMap, в качестве исходного изображение применяем backBuffer.
6) Ну и последнее рисуем перевернутый кусочек пола :)
На этом все, спасибо за внимание!
З.Ы. Вот еще видео визуализации для WMP, которую я писал полтора года назад.
З.Ы.2 Буду признателен за ссылки на ресурсы, где описано как можно посчитать темп музыки.
Хочу узнать как сделано в некоторых визуализация, например чем быстрее играют инструменты в песне тем быстрее крутится шарик.
Комментарии (9)
Просто запустить bat файл. На xp работает, на 7-ке не тестил. Давно это было
В игрульке audiosurf скорость глайдера определялась по интенсивности высоких частот, это было разумно для треков темп в которых задаётся не бочкой (которая может вовсе отсутствовать), а гитаркой например.
Берутся например первые 24(низких) частот, находится среднее значение и это значение служит порогом.
if (m_Bass > threshold)
MoveParticles();
Но похоже правильнее будет учитывать сам коэффициент.