В электронной музыке есть интересное направление — музыка для осциллоскопов, которая рисует интересные картинки, если выход аудиокарты подключить к осциллоскопу в режиме XY.
К примеру, Youscope, Oscillofun и Khr?ng.
Все красивые видео, генерируемые такой музыкой созданы с помощью записи работы настоящего осциллоскопа на видеокамеру. Когда я поискал в сети эмуляторы осциллоскопов, мне не удалось найти такие, которые рисуют мягкие линии, как в настоящем осциллоскопе.
Это сподвигло меня на создацие своего эмулятора осциллоскопа на WebGL: woscope.
В этом посте я расскажу о том как именно происходит рисование линий осциллоскопа в woscope.
Постановка задачи
Есть стерео аудио файл. Каждый сэмпл интерпретируется как координаты точки на плоскости.
Мы хотим получить линию, которая выглядит как линия на экране осциллоскопа, когда тот подключен в режиме XY.
Я решил, что буду рисовать каждый сегмент линии с использованием прямоугольника, который покрывает область экрана, задеваемую пучком.
Яркость всех сегментов будет собираться с помощью
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
.Генерация вершин
Для сегмента линии, координаты четырех вершин прямоугольника рассчитываются исходя из начала сегмента, конца сегмента и индекса вершины в прямоугольнике.
Две первых точки находятся ближе к началу сегмента, и две последних — к концу сегмента.
Четные точки смещены «налево» от сегмента, а нечетные — «направо».
Такое преобразование довольно просто написать в vertex shader:
#define EPS 1E-6
uniform float uInvert;
uniform float uSize;
attribute vec2 aStart, aEnd;
attribute float aIdx;
// uvl.xy is used later in fragment shader
varying vec4 uvl;
varying float vLen;
void main () {
float tang;
vec2 current;
// All points in quad contain the same data:
// segment start point and segment end point.
// We determine point position using its index.
float idx = mod(aIdx,4.0);
// `dir` vector is storing the normalized difference
// between end and start
vec2 dir = aEnd-aStart;
uvl.z = length(dir);
if (uvl.z > EPS) {
dir = dir / uvl.z;
} else {
// If the segment is too short, just draw a square
dir = vec2(1.0, 0.0);
}
// norm stores direction normal to the segment difference
vec2 norm = vec2(-dir.y, dir.x);
// `tang` corresponds to shift "forward" or "backward"
if (idx >= 2.0) {
current = aEnd;
tang = 1.0;
uvl.x = -uSize;
} else {
current = aStart;
tang = -1.0;
uvl.x = uvl.z + uSize;
}
// `side` corresponds to shift to the "right" or "left"
float side = (mod(idx, 2.0)-0.5)*2.0;
uvl.y = side * uSize;
uvl.w = floor(aIdx / 4.0 + 0.5);
gl_Position = vec4((current+(tang*dir+norm*side)*uSize)*uInvert,0.0,1.0);
}
Рассчитываем яркость в точке
Зная координаты вершин прямоугольника, нужно рассчитать общую интенсивность от движущегося пучка в точке на прямоугольнике.
В моей модели, интенсивность пучка описана нормальным распределением, что довольно распространено в реальном мире.
Где ? — разброс пучка.
Для того чтобы рассчитать общую интенсивность в точке, я интегрирую интенсивность пучка по времени, когда пучек движется от начала к концу сегмента.
Если использовать систему отсчета в которой начало сегмента имеет координаты (0,0) а конец — (length,0), можно записать distance(t) как:
Теперь,
Поскольку является константой, можно вынести за знак интегрирования:
Немного упростим интеграл, заменив t на u/l:
Интеграл нормального распределения — функция ошибок.
Наконец,
Зная аппроксимацию функции ошибок, несложно записать эту формулу в fragment shader'е
Fragment shader
Параметр
uvl
, сгенерированный в vertex shader содержит координаты точки в системе отсчета где начало сегмента имеет координаты (0,0) а конец — (length,0).Этот параметр будет линейно интерполироваться между вершинами треугольников, что нам и нужно.
#define EPS 1E-6
#define TAU 6.283185307179586
#define TAUR 2.5066282746310002
#define SQRT2 1.4142135623730951
uniform float uSize;
uniform float uIntensity;
precision highp float;
varying vec4 uvl;
float gaussian(float x, float sigma) {
return exp(-(x * x) / (2.0 * sigma * sigma)) / (TAUR * sigma);
}
float erf(float x) {
float s = sign(x), a = abs(x);
x = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
x *= x;
return s - s / (x * x);
}
void main (void)
{
float len = uvl.z;
vec2 xy = uvl.xy;
float alpha;
float sigma = uSize/4.0;
if (len < EPS) {
// If the beam segment is too short, just calculate intensity at the position.
alpha = exp(-pow(length(xy),2.0)/(2.0*sigma*sigma))/2.0/sqrt(uSize);
} else {
// Otherwise, use analytical integral for accumulated intensity.
alpha = erf(xy.x/SQRT2/sigma) - erf((xy.x-len)/SQRT2/sigma);
alpha *= exp(-xy.y*xy.y/(2.0*sigma*sigma))/2.0/len*uSize;
}
float afterglow = smoothstep(0.0, 0.33, uvl.w/2048.0);
alpha *= afterglow * uIntensity;
gl_FragColor = vec4(1./32., 1.0, 1./32., alpha);
}
Что можно улучшить
- В этом эмуляторе точка движется по прямой линии в каждом сегменте, что иногда приводит к видимо ломанным линиям, чтобы этого избежать можно использовать интерполяцию sinc, увеличив число семплов в несколько раз
- Насыщение пикселов происходит слишком быстро, этого можно было бы избежать, используя Float-текстуры, но есть проблемы с их поддержкой в WebGL. На текущий момент в луче есть маленькие значение красного и синего цвета, что «переполняет» значение в белые пикселы
- Не учитывается гамма-коррекция монитора
- Нет блума, но он может быть и не нужен, учитывая метод генерации линий
- Сделать нативную программу с этим функционалом?
Итоги
Получился довольно реалистичный эмулятор осциллоскопа на WebGL, и математика сыграла большую роль в создании красивой картинки.
Этот метод можно использовать, для генерации других мягких линий.
Надеюсь, статья оказалась познавательной и интересной для читателя.
Код шейдеров отдается в общественное достояние. Полный код woscope доступен на github
Комментарии (22)
deniskreshikhin
14.10.2015 13:25Кстати, такой способ разбиения не без глюков, эти сегменты накладываются и получаются светящиеся точки в местах сцепления.
На случайной фигуре этого не видно, а вот если нарисовать окружность или прямую линию будет заметно.m1el
14.10.2015 13:33+1>эти сегменты накладываются и получаются светящиеся точки в местах сцепления.
Как раз, нет. Благодаря тому, что я расчитываю интеграл интенсивности, два смежных отрезка будут выглядеть хорошо.
Именно эта «проблема» с яркими точками на соединении отрезков отлично решается с помощью математики :)deniskreshikhin
14.10.2015 14:00Какова тогда природа этих точек?
Они выглядят немного странно, которые образуют «пилу».
Может конечно это связано с музыкой. Просто интересно.
m1el
14.10.2015 14:03Конкретно эти точки так и должны выглядеть — сигнал «замирает» на месте.
i.imgur.com/rg7x1dI.png
lockywolf
14.10.2015 13:36+7Может, всё-таки «осциллограф»? Как-то это слово распространённее в русском языке.
m1el
14.10.2015 14:11-3Для вас можно и «осциллограф», но в статье будет «осциллоскоп» :)
mickvav
15.10.2015 09:39+1Ну не надо делать для слов, которые давно и уверенно есть в русском языке, новые кальки с английского, ну пожалуйста!
m1el
15.10.2015 09:43+1Ну, я бы начал с того, что «осциллоскоп» и «осциллограф» — равносильно слова позаимствованные.
Оба слова есть в русском словаре. Не понимаю, в чем обвинение «кальки» с английского.mickvav
15.10.2015 15:46+3Ну, в английском обычно этот прибор называют oscilloscope. В русском — осциллограф. Единственное исключение, которое я смог нагуглить — осциллоскоп САГА производства вильнюсского завода (в литовском он osciloscopas) и пару скопированных упоминаний в словарях ( прибор для наблюдения за процессами в электрических цепях, представляющий собою упрощённый осциллограф). Ключевое слово «собою» выдаёт, что это определение списано с какого-то довольно старого словаря — так не говорят и не пишут лет 30-40 уже, наверное. Так что в русском языке норма, всё-таки — осциллограф.
gorbln
16.10.2015 12:50Не по-джентльменски хамить на первое сообщение.
Человек сказал правильно — по-русски будет «осциллограф». А что такое осциллоскоп — вообще неведомо.m1el
16.10.2015 12:54+1Я не вижу хамства в своем комментарии и вижу слово «осциллоскоп» в словарях.
gorbln
16.10.2015 13:15Ну как бы фраза «для вас можно и...» автоматически причисляет человека к какому-то меньшинству, причём, фраза построена так, что вроде как большинство образовано — а человек — нет.
Это я очень мягко попытался донести смысл своей мысли«у всех осциллоскоп, а ты, быдло, называй как хочешь»m1el
16.10.2015 13:18+1Хорошо. Я понял почему мой комментарий можно было принять как оскорбление.
Он не подразумервался как оскорбление.
kozyabka
14.10.2015 16:37«если выход аудиокарты подключить к осциллоскопу в режиме XY» — то очевидно, что будет вовсе не то что рисуется на приведенных в пример видео с ютуба.
m1el
14.10.2015 16:46+2А что же, по-вашему, будет?
Не считая youscope, в котором трек для музыки и для осциллоскопа — разный.
Предлагаю скачать треки (которые используются в демо или oscillofun.flac, alpha_molecule.wav), подключить к осциллоскопу в режиме XY, два канала аудио и посмотреть что будет.
seokirill
Плюсую. Но я не понял, разве в статье приведён js код оО?!
m1el
Технически — да. «gl.blendFunc(gl.SRC_ALPHA, gl.ONE);»
Но я включил JS в хабы потому что woscope написан с использованием JS. Интересующиеся могут почитать на гитхабе.
Да и WebGL использовать без JS не получится :)
Код на JS не включен в статью потому что он, в основном, является бойлерплейтом для загрузки данных и работы с WebGL.
degorov
Это GLSL — язык для написания шейдеров. На С похож, но без указателей.