Virtuelle Kunstsessions

Details zur Art-Sitzung

Zusammenfassung

Sechs Künstler wurden eingeladen, in VR zu malen, zu designen und zu modellieren. So haben wir die Sitzungen aufgezeichnet, die Daten konvertiert und in Echtzeit in Webbrowsern präsentiert.

https://g.co/VirtualArtSessions

Was für eine Zeit, um am Leben zu sein! Mit der Einführung von Virtual Reality als Verbraucherprodukt werden neue und bisher unerforschte Möglichkeiten entdeckt. Mit Tilt Brush, einem Google-Produkt, das für HTC Vive verfügbar ist, können Sie dreidimensional malen. Als wir Tilt Brush zum ersten Mal ausprobiert haben, hat uns das Gefühl, mit Bewegungssensoren gesteuerten Controllern zu zeichnen und gleichzeitig „in einem Raum mit Superkräften“ zu sein, nicht mehr losgelassen. Es gibt wirklich nichts Vergleichbares, als im leeren Raum um sich herum zu zeichnen.

Virtuelles Kunstwerk

Das Data Arts-Team von Google stand vor der Herausforderung, diese Funktion auch Nutzern ohne VR-Headset im Web zu präsentieren, wo Tilt Brush noch nicht verfügbar ist. Dazu holte das Team einen Bildhauer, einen Illustrator, einen Konzeptdesigner, einen Modekünstler, einen Installationskünstler und Straßenkünstler hinzu, um Artwork in ihrem eigenen Stil in diesem neuen Medium zu erstellen.

Zeichnungen in Virtual Reality aufzeichnen

Die Tilt Brush-Software ist eine Desktopanwendung, die in Unity erstellt wurde und die Kopfposition (Head-Mounted-Display, HMD) und die Controller in Ihren Händen mithilfe von VR in Raumskala erfasst. In Tilt Brush erstelltes Artwork wird standardmäßig als .tilt-Datei exportiert. Um diese Funktion im Web anbieten zu können, brauchten wir aber mehr als nur die Artwork-Daten. Wir haben eng mit dem Tilt Brush-Team zusammengearbeitet, um Tilt Brush so zu modifizieren, dass es 90 Mal pro Sekunde rückgängig gemachte/gelöschte Aktionen sowie die Kopf- und Handpositionen des Künstlers exportiert.

Beim Zeichnen berücksichtigt Tilt Brush die Position und den Winkel des Controllers und wandelt mehrere Punkte im Laufe der Zeit in einen „Strich“ um. Ein Beispiel finden Sie hier. Wir haben Plugins geschrieben, die diese Striche extrahieren und als Roh-JSON ausgeben.

    {
      "metadata": {
        "BrushIndex": [
          "d229d335-c334-495a-a801-660ac8a87360"
        ]
      },
      "actions": [
        {
          "type": "STROKE",
          "time": 12854,
          "data": {
            "id": 0,
            "brush": 0,
            "b_size": 0.081906750798225,
            "color": [
              0.69848710298538,
              0.39136275649071,
              0.211316883564
            ],
            "points": [
              [
                {
                  "t": 12854,
                  "p": 0.25791856646538,
                  "pos": [
                    [
                      1.9832634925842,
                      17.915264129639,
                      8.6014995574951
                    ],
                    [
                      -0.32014992833138,
                      0.82291424274445,
                      -0.41208130121231,
                      -0.22473378479481
                    ]
                  ]
                }, ...many more points
              ]
            ]
          }
        }, ... many more actions
      ]
    }

Im obigen Snippet wird das JSON-Format für Sketch beschrieben.

Hier wird jeder Strich als Aktion mit dem Typ „STROKE“ gespeichert. Neben den Zeichenaktionen wollten wir zeigen, wie ein Künstler Fehler macht und seine Meinung während des Zeichnens ändert. Daher war es wichtig, „LÖSCHEN“-Aktionen zu speichern, die entweder zum Löschen oder Rückgängigmachen eines gesamten Zeichenstrichs dienen.

Für jeden Strich werden grundlegende Informationen gespeichert, also der Pinseltyp, die Pinselgröße und die RGB-Farbe.

Schließlich werden alle Eckpunkte des Strokes gespeichert. Dazu gehören die Position, der Winkel, die Zeit sowie die Stärke des Auslöserdrucks des Controllers (in jedem Punkt als p angegeben).

Die Rotation ist ein 4-Komponenten-Quaternion. Das ist später wichtig, wenn wir die Striche rendern, um ein Gimbal-Lock zu vermeiden.

Skizzen mit WebGL abspielen

Um die Skizzen in einem Webbrowser anzuzeigen, haben wir THREE.js verwendet und Code zur Geometriegenerierung geschrieben, der die Funktionsweise von Tilt Brush nachahmt.

Während Tilt Brush Dreiecksstreifen in Echtzeit basierend auf der Handbewegung des Nutzers erstellt, ist der gesamte Entwurf bereits fertig, wenn er im Web angezeigt wird. So können wir einen Großteil der Echtzeitberechnung umgehen und die Geometrie beim Laden einbetten.

WebGL-Skizzen

Jedes Paar von Eckpunkten in einem Strich erzeugt einen Richtungsvektor (die blauen Linien, die die einzelnen Punkte wie oben gezeigt verbinden, moveVector im Code-Snippet unten). Jeder Punkt enthält auch eine Orientierung, einen Quaternion, der den aktuellen Winkel des Controllers darstellt. Um einen Dreieckstreifen zu erstellen, iterieren wir über jeden dieser Punkte und erstellen Normalen, die senkrecht zur Richtung und zur Ausrichtung des Controllers stehen.

Die Berechnung des Dreieckstreifens für jeden Strich ist fast identisch mit dem Code, der in Tilt Brush verwendet wird:

const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );

function computeSurfaceFrame( previousRight, moveVector, orientation ){
    const pointerF = V_FORWARD.clone().applyQuaternion( orientation );

    const pointerU = V_UP.clone().applyQuaternion( orientation );

    const crossF = pointerF.clone().cross( moveVector );
    const crossU = pointerU.clone().cross( moveVector );

    const right1 = inDirectionOf( previousRight, crossF );
    const right2 = inDirectionOf( previousRight, crossU );

    right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );

    const newRight = ( right1.clone().add( right2 ) ).normalize();
    const normal = moveVector.clone().cross( newRight );
    return { newRight, normal };
}

function inDirectionOf( desired, v ){
    return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}

Die Kombination aus Strichrichtung und -ausrichtung führt zu mathematisch mehrdeutigen Ergebnissen. Es können mehrere Normalen abgeleitet werden, was häufig zu einer „Verdrehung“ der Geometrie führt.

Wenn wir die Punkte eines Strokes durchgehen, behalten wir einen „bevorzugten rechten“ Vektor bei und übergeben ihn an die Funktion computeSurfaceFrame(). Diese Funktion liefert eine Normale, aus der wir ein Quad im Quad-Streifen ableiten können, basierend auf der Richtung des Strichs (vom letzten Punkt zum aktuellen Punkt) und der Ausrichtung des Controllers (ein Quaternion). Noch wichtiger ist, dass es auch einen neuen „bevorzugten rechten“ Vektor für die nächsten Berechnungen zurückgibt.

Striche

Nachdem wir anhand der Kontrollpunkte der einzelnen Striche Quadrate generiert haben, verschmelzen wir sie, indem wir ihre Ecken von einem Quadrat zum nächsten interpolieren.

function fuseQuads( lastVerts, nextVerts) {
    const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
    const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );

    lastVerts[1].copy( vTopPos );
    lastVerts[4].copy( vTopPos );
    lastVerts[5].copy( vBottomPos );
    nextVerts[0].copy( vTopPos );
    nextVerts[2].copy( vBottomPos );
    nextVerts[3].copy( vBottomPos );
}
Zusammengeführte Quads
Verbundene Quads

Jedes Quad enthält auch UVs, die im nächsten Schritt generiert werden. Einige Pinsel enthalten eine Vielzahl von Strichmustern, um den Eindruck zu erwecken, dass jeder Strich wie ein anderer Pinselstrich aussieht. Dazu wird ein _Texturatlas_ verwendet, bei dem jede Pinseltextur alle möglichen Variationen enthält. Die richtige Textur wird durch Ändern der UV-Werte des Strichs ausgewählt.

function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
    let fYStart = 0.0;
    let fYEnd = 1.0;

    if( useAtlas ){
    const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
    fYStart = fYWidth * atlasIndex;
    fYEnd = fYWidth * (atlasIndex + 1.0);
    }

    //get length of current segment
    const totalLength = quadLengths.reduce( function( total, length ){
    return total + length;
    }, 0 );

    //then, run back through the last segment and update our UVs
    let currentLength = 0.0;
    quadUVs.forEach( function( uvs, index ){
    const segmentLength = quadLengths[ index ];
    const fXStart = currentLength / totalLength;
    const fXEnd = ( currentLength + segmentLength ) / totalLength;
    currentLength += segmentLength;

    uvs[ 0 ].set( fXStart, fYStart );
    uvs[ 1 ].set( fXEnd, fYStart );
    uvs[ 2 ].set( fXStart, fYEnd );
    uvs[ 3 ].set( fXStart, fYEnd );
    uvs[ 4 ].set( fXEnd, fYStart );
    uvs[ 5 ].set( fXEnd, fYEnd );

    });

}
Vier Texturen in einem Texturatlas für Ölpinsel
Vier Texturen in einem Texturenatlas für den Ölpinsel
In Tilt Brush
Im Tilt-Pinsel
In WebGL
In WebGL

Da jede Skizze eine unbegrenzte Anzahl von Strichen hat und die Striche nicht zur Laufzeit geändert werden müssen, berechnen wir die Strichgeometrie vorab und verschmelzen sie zu einem einzigen Mesh. Auch wenn jeder neue Pinseltyp ein eigenes Material sein muss, reduziert sich die Anzahl der Draw-Aufrufe auf einen pro Pinsel.

Die gesamte Skizze oben wird in WebGL in einem einzigen Draw-Aufruf ausgeführt.
Der gesamte Sketch oben wird in WebGL in einem einzigen Draw-Aufruf ausgeführt.

Für einen Stresstest des Systems haben wir eine Skizze erstellt, bei der wir 20 Minuten lang den Raum mit so vielen Eckpunkten wie möglich gefüllt haben. Die resultierende Skizze wurde weiterhin mit 60 fps in WebGL wiedergegeben.

Da jeder der ursprünglichen Eckpunkte eines Strichs auch die Zeit enthält, können wir die Daten ganz einfach wiedergeben. Die einzelnen Striche pro Frame neu zu berechnen, wäre sehr langsam. Deshalb haben wir die gesamte Skizze beim Laden vorab berechnet und die einzelnen Quads einfach zum richtigen Zeitpunkt sichtbar gemacht.

Um ein Quad auszublenden, mussten Sie einfach nur seine Eckpunkte auf den Punkt 0,0,0 zusammenziehen. Wenn die Zeit den Punkt erreicht hat, an dem das Quad sichtbar werden soll, bringen wir die Eckpunkte wieder an ihre Position zurück.

Ein Verbesserungspotenzial besteht darin, die Vertexes vollständig auf der GPU mit Shadern zu bearbeiten. Bei der aktuellen Implementierung werden sie platziert, indem der Vertex-Array vom aktuellen Zeitstempel aus durchlaufen wird, um zu prüfen, welche Vertexe sichtbar gemacht werden müssen, und dann die Geometrie aktualisiert wird. Das führt zu einer hohen CPU-Auslastung, was den Lüfter in Bewegung setzt und die Akkulaufzeit verkürzt.

Virtuelles Kunstwerk

Künstler aufnehmen

Wir waren der Meinung, dass die Skizzen allein nicht ausreichen würden. Wir wollten die Künstler in ihren Skizzen zeigen, wie sie jeden Pinselstrich malen.

Um die Künstler zu erfassen, haben wir Microsoft Kinect-Kameras verwendet, um die Tiefendaten des Körpers der Künstler im Raum zu erfassen. So können wir die dreidimensionalen Figuren im selben Raum wie die Zeichnungen anzeigen.

Da der Körper des Künstlers die Sicht auf das dahinter befindliche Bild verdeckt hätte, haben wir ein doppeltes Kinect-System verwendet, das sich an gegenüberliegenden Seiten des Raums befand und auf die Mitte gerichtet war.

Zusätzlich zu den Tiefeninformationen haben wir auch die Farbinformationen der Szene mit Standard-DSLR-Kameras erfasst. Wir haben die hervorragende Software DepthKit verwendet, um die Aufnahmen der Tiefenkamera und der Farbkameras zu kalibrieren und zu kombinieren. Kinect kann Farben aufnehmen, aber wir haben uns für DSLRs entschieden, weil wir die Belichtungseinstellungen steuern, hochwertige Objektive verwenden und in High Definition aufnehmen konnten.

Für die Aufnahmen haben wir einen speziellen Raum gebaut, in dem die HTC Vive, der Künstler und die Kamera Platz fanden. Alle Oberflächen wurden mit einem Material bedeckt, das Infrarotlicht absorbiert, um eine sauberere Punktwolke zu erhalten (Duvetyn an den Wänden, geriffelte Gummimatten auf dem Boden). Für den Fall, dass das Material in den Aufnahmen der Punktwolke zu sehen ist, haben wir schwarzes Material gewählt, damit es nicht so ablenkt wie ein weißes Material.

Künstler

Die daraus resultierenden Videoaufnahmen lieferten uns genügend Informationen, um ein Partikelsystem zu projizieren. Wir haben einige zusätzliche Tools in openFrameworks geschrieben, um das Video weiter zu bereinigen, insbesondere um Böden, Wände und Decken zu entfernen.

Alle vier Kanäle einer aufgezeichneten Videosession (zwei Farbkanäle oben und zwei Tiefenkanäle unten)
Alle vier Kanäle einer aufgezeichneten Videosession (zwei Farbkanäle oben und zwei Tiefenkanäle unten)

Neben den Künstlern wollten wir auch das HMD und die Controller in 3D rendern. Das war nicht nur wichtig, um das HMD in der finalen Ausgabe deutlich zu zeigen (die reflektierenden Objektive des HTC Vive störten die IR-Messungen von Kinect), sondern gab uns auch Ansatzpunkte, um die Partikelausgabe zu debuggen und die Videos mit der Skizze auszurichten.

Das Head-Mounted-Display, die Controller und die Partikel sind ausgerichtet.
Head-Mounted-Display, Controller und Partikel sind ausgerichtet

Dazu wurde ein benutzerdefiniertes Plug-in in Tilt Brush geschrieben, das die Positionen des HMD und der Controller für jeden Frame extrahierte. Da Tilt Brush mit 90 fps läuft, wurden Unmengen an Daten gestreamt und die Eingabedaten einer Skizze hatten ein unkomprimiertes Volumen von über 20 MB. Mit dieser Technik konnten wir auch Ereignisse erfassen, die in der typischen Tilt Brush-Speicherdatei nicht aufgezeichnet werden, z. B. wenn der Künstler eine Option im Werkzeugbereich und die Position des Spiegel-Widgets auswählt.

Bei der Verarbeitung der 4 TB an Daten, die wir erfasst haben, bestand eine der größten Herausforderungen darin, alle verschiedenen visuellen/Datenquellen auszurichten. Jedes Video von einer DSLR-Kamera muss mit der entsprechenden Kinect ausgerichtet sein, damit die Pixel sowohl räumlich als auch zeitlich ausgerichtet sind. Anschließend mussten die Aufnahmen dieser beiden Kamera-Rigs aufeinander abgestimmt werden, um einen einzelnen Künstler zu bilden. Dann mussten wir unseren 3D-Künstler an die Daten aus der Zeichnung ausrichten. Geschafft! Wir haben browserbasierte Tools für die meisten dieser Aufgaben entwickelt. Hier können Sie sie ausprobieren.

Künstler

Nachdem die Daten ausgerichtet waren, haben wir sie mithilfe von Scripts in NodeJS verarbeitet und eine Videodatei und eine Reihe von JSON-Dateien ausgegeben, die alle zugeschnitten und synchronisiert waren. Um die Dateigröße zu reduzieren, haben wir drei Dinge getan. Zuerst haben wir die Genauigkeit jeder Gleitkommazahl so reduziert, dass sie maximal drei Dezimalstellen hat. Zweitens haben wir die Anzahl der Punkte um ein Drittel auf 30 fps reduziert und die Positionen clientseitig interpoliert. Schließlich haben wir die Daten serialisiert. Anstatt einfaches JSON mit Schlüssel/Wert-Paaren zu verwenden, wird eine Reihenfolge von Werten für die Position und Drehung des HMDs und der Controller erstellt. Dadurch wurde die Dateigröße auf knapp unter 3 MB reduziert, was für die Übertragung über das Netzwerk akzeptabel war.

Musikkünstler

Da das Video selbst als HTML5-Videoelement ausgeliefert wird, das von einer WebGL-Textur gelesen wird, um zu Partikeln zu werden, musste das Video selbst im Hintergrund abgespielt werden. Ein Shader wandelt die Farben in den Tiefenbildern in Positionen im 3D-Raum um. James George hat ein tolles Beispiel geteilt, wie du mit DepthKit-Material arbeiten kannst.

Unter iOS gibt es Einschränkungen für die Inline-Videowiedergabe. Wir gehen davon aus, dass dies verhindern soll, dass Nutzer von automatisch wiedergegebenen Webvideoanzeigen belästigt werden. Wir haben eine ähnliche Methode wie andere Lösungen im Web verwendet: Wir haben den Videoframe in einen Canvas kopiert und die Videosuchzeit alle 1/30 Sekunden manuell aktualisiert.

videoElement.addEventListener( 'timeupdate', function(){
    videoCanvas.paintFrame( videoElement );
});

function loopCanvas(){

    if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){

    const time = Date.now();
    const elapsed = ( time - lastTime ) / 1000;

    if( videoState.playing && elapsed >= ( 1 / 30 ) ){
        videoElement.currentTime = videoElement.currentTime + elapsed;
        lastTime = time;
    }

    }

}

frameLoop.add( loopCanvas );

Unser Ansatz hatte jedoch den unglücklichen Nebeneffekt, dass die Framerate auf iOS-Geräten deutlich gesenkt wurde, da das Kopieren des Pixelpuffers vom Video zum Canvas sehr CPU-intensiv ist. Um dieses Problem zu umgehen, haben wir einfach kleinere Versionen derselben Videos ausgeliefert, die auf einem iPhone 6 mindestens 30 fps ermöglichen.

Fazit

Der allgemeine Konsens bei der VR-Softwareentwicklung im Jahr 2016 besteht darin, Geometrien und Shader einfach zu halten, damit eine Ausführung mit mehr als 90 fps in einem HMD möglich ist. Das erwies sich als sehr gutes Ziel für WebGL-Demos, da die in Tilt Brush verwendeten Techniken sehr gut zu WebGL passen.

Das Anzeigen komplexer 3D-Meshes in Webbrowsern ist zwar an sich nicht besonders spannend, aber es war ein Proof of Concept dafür, dass die gegenseitige Befruchtung von VR-Arbeit und dem Web durchaus möglich ist.