Осциллоскоп на WebGL

beeca9ab6f764261958f16c93fba50d0.png

В электронной музыке есть интересное направление — музыка для осциллоскопов, которая рисует интересные картинки, если выход аудиокарты подключить к осциллоскопу в режиме XY.
К примеру, Youscope, Oscillofun и Khrậng.

Все красивые видео, генерируемые такой музыкой созданы с помощью записи работы настоящего осциллоскопа на видеокамеру. Когда я поискал в сети эмуляторы осциллоскопов, мне не удалось найти такие, которые рисуют мягкие линии, как в настоящем осциллоскопе.

Это сподвигло меня на создацие своего эмулятора осциллоскопа на WebGL: woscope.

В этом посте я расскажу о том как именно происходит рисование линий осциллоскопа в woscope.

Постановка задачи


Есть стерео аудио файл. Каждый сэмпл интерпретируется как координаты точки на плоскости.
Мы хотим получить линию, которая выглядит как линия на экране осциллоскопа, когда тот подключен в режиме XY.

Я решил, что буду рисовать каждый сегмент линии с использованием прямоугольника, который покрывает область экрана, задеваемую пучком.

image

Яркость всех сегментов будет собираться с помощью gl.blendFunc(gl.SRC_ALPHA, gl.ONE);.

Генерация вершин


Для сегмента линии, координаты четырех вершин прямоугольника рассчитываются исходя из начала сегмента, конца сегмента и индекса вершины в прямоугольнике.

image

Две первых точки находятся ближе к началу сегмента, и две последних — к концу сегмента.
Четные точки смещены «налево» от сегмента, а нечетные — «направо».

Такое преобразование довольно просто написать в 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);
}

Рассчитываем яркость в точке


Зная координаты вершин прямоугольника, нужно рассчитать общую интенсивность от движущегося пучка в точке на прямоугольнике.

В моей модели, интенсивность пучка описана нормальным распределением, что довольно распространено в реальном мире.

31353b33ea8e43e1ac95012abe83090c.png


Где σ — разброс пучка.

Для того чтобы рассчитать общую интенсивность в точке, я интегрирую интенсивность пучка по времени, когда пучек движется от начала к концу сегмента.

832ee2e3d4174abda96aad22a88de228.png


image

Если использовать систему отсчета в которой начало сегмента имеет координаты (0,0) а конец — (length,0), можно записать distance(t) как:

aaba5ece5a1c42db8d66c7b858a8c12a.png

Теперь,

60ebeaccfc73438bbe14d0f364d8a09b.png

Поскольку e108e5dbf2fa460f8cd3cb8fe274257e.png является константой, dd6224e07f6946f9ba9e6ba1633db60a.png можно вынести за знак интегрирования:

132deda99ab943b395fe0fa3b2e35402.png

Немного упростим интеграл, заменив t на u/l:

480fc80f833e4cca8a9ebf257ab1334b.png

Интеграл нормального распределения — функция ошибок.

8960c219f558477a9661f9143f62bd61

Наконец,

3540ebd925004d3e95a1ebff457cb770

Зная аппроксимацию функции ошибок, несложно записать эту формулу в 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

© Habrahabr.ru