Визуализация для музыкального плеера

в 15:06, , рубрики: Action Script, as3, Flash-платформа, графика, Работа со звуком, метки: ,

Информация в статье затронет тему создания визуализации для музыкального плеера. Так сложилось, что программа была написана на as3, т.к. это язык на котором я сейчас программирую.
Все началось из увиденной в плеере AIMP визуализации Phthalo's Corona. Я долго думал как она работает и наконец кое-что придумал.

Первый прототип был написан на C++.
image

В итоге у меня получился только прототип движения частиц как в этой визуализации. На этом я и остановился и через пол года снова захотелось поработать со звуком. Через 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

Теперь просто находим вектор по полученому углу и умножаем его на коэффициент смещения. В итоге получилась такая карта смещений(используется зеленый и синий каналы)
image

Во втором режиме, накладывается другая карта смещений которая генерируется шумом Перлина.

m_NoiseBuffer.perlinNoise(SCREEN_WIDTH, SCREEN_HEIGHT - FLOOR_POS, 2, 1,
 false, true, BitmapDataChannel.RED | BitmapDataChannel.BLUE,
false, [m_OctaveOffset, m_OctaveOffset]);

Вот такое симпатичное изображение получается на выходе:
image

Можно было бы добавить больше октав при генерации, но шум генерируется каждый апдейт(для этого и используется смещение октав, для эффекта движения).

В итоге шаги отрисовки такие:

  1. К backBuffer применяется DisplacementMapFilter фильтр со смещениями.
  2. К backBuffer применяется фильтр размытия(x: 8, y: 8, quality: 1).
  3. К backBuffer применяется ColorTransform, так мы регулируем скорость затухания изображения.
  4. Рисуем частицы в backBuffer.
  5. К screeBuffer применяем paletteMap, в качестве исходного изображение применяем backBuffer.
  6. Ну и последнее рисуем перевернутый кусочек пола

ИСХОДНИКИ.

На этом все, спасибо за внимание!

Автор: vizgl

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js