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



    В электронной музыке есть интересное направление — музыка для осциллоскопов, которая рисует интересные картинки, если выход аудиокарты подключить к осциллоскопу в режиме 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);
    }
    


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


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

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

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

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


    image


    Если использовать систему отсчета в которой начало сегмента имеет координаты (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
    Share post

    Comments 23

      0
      Плюсую. Но я не понял, разве в статье приведён js код оО?!
        0
        Технически — да. «gl.blendFunc(gl.SRC_ALPHA, gl.ONE);»

        Но я включил JS в хабы потому что woscope написан с использованием JS. Интересующиеся могут почитать на гитхабе.
        Да и WebGL использовать без JS не получится :)
        Код на JS не включен в статью потому что он, в основном, является бойлерплейтом для загрузки данных и работы с WebGL.
        • UFO just landed and posted this here
          0
          Кстати, такой способ разбиения не без глюков, эти сегменты накладываются и получаются светящиеся точки в местах сцепления.
          На случайной фигуре этого не видно, а вот если нарисовать окружность или прямую линию будет заметно.
            +1
            >эти сегменты накладываются и получаются светящиеся точки в местах сцепления.
            Как раз, нет. Благодаря тому, что я расчитываю интеграл интенсивности, два смежных отрезка будут выглядеть хорошо.
            Именно эта «проблема» с яркими точками на соединении отрезков отлично решается с помощью математики :)
              0
              Какова тогда природа этих точек?
              Они выглядят немного странно, которые образуют «пилу».
              Может конечно это связано с музыкой. Просто интересно.



                0
                Конкретно эти точки так и должны выглядеть — сигнал «замирает» на месте.
                i.imgur.com/rg7x1dI.png
          +7
          Может, всё-таки «осциллограф»? Как-то это слово распространённее в русском языке.
            –3
            Для вас можно и «осциллограф», но в статье будет «осциллоскоп» :)
              +1
              Ну не надо делать для слов, которые давно и уверенно есть в русском языке, новые кальки с английского, ну пожалуйста!
                +1
                Ну, я бы начал с того, что «осциллоскоп» и «осциллограф» — равносильно слова позаимствованные.
                Оба слова есть в русском словаре. Не понимаю, в чем обвинение «кальки» с английского.
                  +3
                  Ну, в английском обычно этот прибор называют oscilloscope. В русском — осциллограф. Единственное исключение, которое я смог нагуглить — осциллоскоп САГА производства вильнюсского завода (в литовском он osciloscopas) и пару скопированных упоминаний в словарях ( прибор для наблюдения за процессами в электрических цепях, представляющий собою упрощённый осциллограф). Ключевое слово «собою» выдаёт, что это определение списано с какого-то довольно старого словаря — так не говорят и не пишут лет 30-40 уже, наверное. Так что в русском языке норма, всё-таки — осциллограф.
                    0
                    Простыми словами «осциллоскоп» — это индикатор. А «осциллограф» — это измерительный прибор. Пока не возникает задача получить измерения с осциллографа, он идентичен осциллоскопу.
                  0
                  Не по-джентльменски хамить на первое сообщение.
                  Человек сказал правильно — по-русски будет «осциллограф». А что такое осциллоскоп — вообще неведомо.
                    +1
                    Я не вижу хамства в своем комментарии и вижу слово «осциллоскоп» в словарях.
                      0
                      Ну как бы фраза «для вас можно и...» автоматически причисляет человека к какому-то меньшинству, причём, фраза построена так, что вроде как большинство образовано — а человек — нет.
                      Это я очень мягко попытался донести смысл своей мысли
                      «у всех осциллоскоп, а ты, быдло, называй как хочешь»
                        +1
                        Хорошо. Я понял почему мой комментарий можно было принять как оскорбление.
                        Он не подразумервался как оскорбление.
                +1
                Ещё бы теперь «разоблачение»: как делать такую музыку? =)
                  0
                  В левом канале X, в правом Y. Что тут разоблачать?
                  0
                  «если выход аудиокарты подключить к осциллоскопу в режиме XY» — то очевидно, что будет вовсе не то что рисуется на приведенных в пример видео с ютуба.
                  0
                  признаюсь, дальше youscope видео не смотрел. Благодарю, за разъяснения.

                  Only users with full accounts can post comments. Log in, please.