WebGL-Oszilloskop



    In der elektronischen Musik gibt es eine interessante Richtung - Musik für Oszilloskope, die interessante Bilder zeichnet, wenn Sie den Ausgang einer Audiokarte mit einem Oszilloskop im XY-Modus verbinden.
    Zum Beispiel Youscope , Oscillofun und Khrậng .

    Alle schönen Videos, die durch solche Musik erzeugt werden, werden durch Aufzeichnen der Arbeit eines echten Oszilloskops auf einer Videokamera erzeugt. Als ich im Netzwerk nach Oszilloskop-Emulatoren gesucht habe, konnte ich keine finden, die weiche Linien zeichnen, wie in einem echten Oszilloskop.

    Dies führte mich dazu, meinen eigenen Oszilloskop-Emulator auf WebGL zu erstellen: woscope .

    In diesem Beitrag werde ich darüber sprechen, wie genau die Oszilloskoplinien in einem Woskop gezeichnet werden.

    Erklärung des Problems


    Es gibt eine Stereo-Audiodatei. Jedes Sample wird als die Koordinaten eines Punktes in der Ebene interpretiert.
    Wir möchten eine Linie erhalten, die auf dem Bildschirm des Oszilloskops wie eine Linie aussieht, wenn es im XY-Modus angeschlossen ist.

    Ich beschloss, jedes Liniensegment mit einem Rechteck zu zeichnen, das den vom Strahl berührten Bildschirmbereich abdeckt.

    Bild

    Die Helligkeit aller Segmente wird mit erfasst gl.blendFunc(gl.SRC_ALPHA, gl.ONE);.

    Vertex-Generierung


    Für ein Liniensegment werden die Koordinaten der vier Eckpunkte des Rechtecks ​​basierend auf dem Anfang des Segments, dem Ende des Segments und dem Index des Eckpunkts im Rechteck berechnet.

    Bild

    Die ersten beiden Punkte befinden sich näher am Anfang des Segments und die letzten beiden am Ende des Segments.
    Die geraden Punkte werden „nach links“ des Segments verschoben, und die ungeraden Punkte werden „nach rechts“ verschoben.

    Eine solche Konvertierung ist im Vertex-Shader ganz einfach zu schreiben:

    #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);
    }
    


    Wir berechnen die Helligkeit an einem Punkt


    Wenn Sie die Koordinaten der Scheitelpunkte des Rechtecks ​​kennen, müssen Sie die Gesamtintensität aus dem sich bewegenden Strahl an einem Punkt des Rechtecks ​​berechnen.

    In meinem Modell wird die Strahlintensität durch eine Normalverteilung beschrieben, die in der realen Welt durchaus üblich ist.

    Wobei σ die Strahlaufweitung ist.

    Um die Gesamtintensität an einem Punkt zu berechnen, integriere ich die Strahlintensität über die Zeit, wenn sich der Strahl vom Anfang bis zum Ende des Segments bewegt.


    Bild


    Wenn Sie einen Referenzrahmen verwenden, in dem der Anfang des Segments Koordinaten (0,0) und das Ende (Länge, 0) hat, können Sie den Abstand (t) wie folgt eingeben:


    Jetzt


    Da es sich um eine Konstante handelt, kann sie dem Zeichen der Integration entzogen werden:


    Vereinfachen Sie das Integral ein wenig, indem Sie t durch u / l ersetzen:


    Das Normalverteilungsintegral ist eine Funktion von Fehlern.


    Endlich


    Mit Kenntnis der Approximation der Fehlerfunktion ist es einfach, diese Formel in den Fragment-Shader zu schreiben

    Fragment-Shader


    Deruvl im Vertex-Shader generierte Parameter enthält die Koordinaten des Punkts im Referenzrahmen, an dem der Anfang des Segments Koordinaten (0,0) und das Ende Koordinaten (Länge, 0) hat.
    Dieser Parameter wird linear zwischen den Eckpunkten der Dreiecke interpoliert, was wir brauchen.

    #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);
    }
    


    Was kann verbessert werden?


    • In diesem Emulator bewegt sich der Punkt in jedem Segment auf einer geraden Linie, was manchmal zu anscheinend unterbrochenen Linien führt. Um dies zu vermeiden, können Sie eine Sinusinterpolation verwenden und die Anzahl der Abtastungen um ein Vielfaches erhöhen
    • Die Pixelsättigung ist zu schnell, dies könnte mit Float-Texturen vermieden werden, aber es gibt Probleme mit ihrer Unterstützung in WebGL. Momentan gibt es kleine Rot- und Blauwerte im Strahl, die den Wert in weißen Pixeln "überfluten"
    • Monitor-Gammakorrektur nicht berücksichtigt
    • Es gibt keine Ausblühung, aber es ist möglicherweise nicht erforderlich, wenn die Methode zum Erzeugen von Linien angewendet wird
    • Ein natives Programm mit dieser Funktionalität erstellen?


    Zusammenfassung


    Das Ergebnis war ein ziemlich realistischer Emulator eines Oszilloskops in WebGL, und die Mathematik spielte eine große Rolle bei der Erstellung eines schönen Bildes.
    Mit dieser Methode können andere weiche Linien erzeugt werden.
    Ich hoffe, der Artikel hat sich für den Leser als informativ und interessant herausgestellt.

    Der Shader-Code ist gemeinfrei. Vollständiger Woscope-Code auf Github verfügbar

    Jetzt auch beliebt: