Virtuelle Kunstsessions

Art Session-Details

Zusammenfassung

Sechs Künstler wurden eingeladen, in VR zu malen, zu designen und zu formen. Auf diese Weise haben wir die Sitzungen aufgezeichnet, die Daten konvertiert und in Echtzeit mit Webbrowsern dargestellt.

https://g.co/VirtualArtSessions

Was für eine Zeit zum Leben! Mit der Einführung der Virtual Reality als Verbraucherprodukt entdecken wir neue und unentdeckte Möglichkeiten. Mit Tilt Brush, einem Google-Produkt, das auf HTC Vive verfügbar ist, können Sie in dreidimensionalen Räumen zeichnen. Als wir Tilt Brush zum ersten Mal ausprobiert haben, bleibt Ihnen das Gefühl, mit bewegungsbasierten Controllern und einem Raum mit Superkräften zu zeichnen, immer wieder aufs Neue. Es ist wirklich kein Erlebnis, den leeren Raum um Sie herum zu zeichnen.

Virtuelles Kunstwerk

Das Data Arts-Team bei Google stand vor der Herausforderung, dieses Erlebnis jenen ohne VR-Headset im Web zu präsentieren, in dem Tilt Brush noch nicht verfügbar ist. Zu diesem Zweck holte das Team Bildhauer, Illustrator, Konzeptdesigner, Modekünstler, Installationskünstler und Street-Art-Künstler ein, um mit diesem neuen Medium Kunstwerke in ihrem eigenen Stil zu erschaffen.

Zeichnungen in Virtual Reality aufnehmen

Die in Unity integrierte Tilt Brush-Software ist eine Desktop-Anwendung, die mit VR-Technologie im Raum deine Kopfposition (Head-mounted Display, kurz HMD) und die Controller in deinen Händen verfolgt. In Tilt Brush erstellte Artwork wird standardmäßig als .tilt-Datei exportiert. Um dieses Erlebnis im Web verfügbar zu machen, brauchen wir mehr als nur die Artwork-Daten. Wir haben eng mit dem Tilt Brush-Team zusammengearbeitet, um Tilt Brush so anzupassen, dass Aktionen zum Rückgängigmachen und Löschen sowie die Kopf- und Handpositionen des Künstlers 90-mal pro Sekunde exportiert werden.

Beim Zeichnen übernimmt 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 Plug-ins geschrieben, die diese Striche extrahiert und als JSON-Rohdaten 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
      ]
    }

Das obige Snippet beschreibt das Format des Sketch-JSON-Formats.

Hier wird jeder Strich als Aktion mit dem Typ „STROKE“ gespeichert. Neben den Strichaktionen wollten wir zeigen, dass ein Künstler Fehler macht und seine Meinung mitten in der Skizze ändert. Daher war es wichtig, „LÖSCHEN“-Aktionen zu speichern, die entweder zum Löschen oder Rückgängigmachen für einen gesamten Strich dienen.

Die Basisinformationen für die einzelnen Striche werden gespeichert, sodass Pinseltyp, Pinselgröße und RGB-Farbraum erfasst werden.

Schließlich wird jeder Scheitelpunkt des Strichs gespeichert und enthält die Position, den Winkel, die Zeit sowie die Auslösedruckstärke des Controllers (in jedem Punkt als p angegeben).

Beachten Sie, dass die Rotation eine Vier-Komponenten-Quaternion ist. Das ist später beim Rendern der Striche wichtig, um eine kardanische Sperrung zu vermeiden.

Skizzen mit WebGL abspielen

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

Während Tilt Brush in Echtzeit basierend auf der Handbewegung der Nutzenden Dreiecksstreifen erzeugt, ist die gesamte Skizze zu dem Zeitpunkt, zu dem wir sie im Web zeigen, bereits "fertig". Dadurch können wir einen Großteil der Echtzeitberechnung umgehen und die Geometrie beim Laden verankern.

WebGL-Skizzen

Jedes Eckpunktpaar in einem Strich erzeugt einen Richtungsvektor (die blauen Linien, die die einzelnen Punkte miteinander verbinden, wie oben dargestellt, moveVector im Code-Snippet unten). Jeder Punkt enthält auch eine Ausrichtung, ein Quaternion, das den aktuellen Winkel des Controllers darstellt. Um einen Dreiecksstreifen zu erzeugen, iterieren wir über jeden dieser Punkte und erzeugen so Normalen, die senkrecht zur Richtung und Controller-Ausrichtung sind.

Der Prozess zum Berechnen des Dreiecksstreifens für jeden Strich ist fast identisch mit dem in Tilt Brush verwendeten Code:

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 von Strichrichtung und -ausrichtung allein führt zu mathematisch mehrdeutigen Ergebnissen. Es können mehrere Normalen abgeleitet werden, was häufig zu einer „Drehung“ in der Geometrie führt.

Beim Iterieren über die Punkte eines Strichs wird der Vektor "Bevorzugtes Recht" beibehalten und an die Funktion computeSurfaceFrame() übergeben. Diese Funktion gibt uns eine Normalität an, aus der wir ein Quad im Quad-Streifen basierend auf der Richtung des Strichs (vom letzten Punkt zum aktuellen Punkt) und der Ausrichtung der Steuerung (eine Quaternion) ableiten können. Außerdem gibt sie einen neuen Vektor für das bevorzugte Recht für den nächsten Satz von Berechnungen zurück.

Striche

Nachdem wir Quads basierend auf den Kontrollpunkten der einzelnen Striche generiert haben, verschmelzen wir die Quadrate, indem wir ihre Ecken von einem Quad 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 );
}
Kombinierte Vierecken
Zusammengeführte Vierecken

Jedes Quadrat enthält außerdem UVs, die als nächster Schritt generiert werden. Einige Pinsel enthalten eine Vielzahl von Strichmustern, die den Eindruck vermitteln, dass sich jeder Pinselstrich wie ein anderer Pinselstrich anfühlt. Dies wird durch die _texture-Atlasierung erreicht, bei der jede Pinseltextur alle möglichen Varianten 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 Texturatlas für Ölpinsel
In Tilt Brush
In Tilt Brush
In WebGL
In WebGL

Da jede Skizze eine unbegrenzte Anzahl von Strichen hat und die Striche während der Laufzeit nicht geändert werden müssen, berechnen wir die Strichgeometrie im Voraus und führen sie in einem einzigen Netz zusammen. Auch wenn jeder neue Zeichentricktyp ein eigenes Material sein muss, werden unsere Zeichenaufrufe dennoch auf einen pro Pinsel reduziert.

Die gesamte obige Skizze wird in einem einzigen Zeichenaufruf in WebGL ausgeführt.
Die gesamte Skizze oben wird in einem einzigen Zeichenaufruf in WebGL ausgeführt.

Zum Belastungstest des Systems haben wir eine Skizze erstellt, die 20 Minuten dauerte und den Raum mit so vielen Eckpunkten wie möglich füllte. Die daraus resultierende Skizze wurde in WebGL weiterhin mit 60 fps abgespielt.

Da jeder ursprüngliche Eckpunkt eines Strichs auch Zeit enthielt, können die Daten einfach wiedergegeben werden. Die Neuberechnung der Striche pro Frame wäre wirklich langsam. Deshalb berechnen wir stattdessen die gesamte Skizze beim Laden und zeigen einfach jedes Quad an, wenn es Zeit dafür war.

Das Ausblenden eines Quadrats bedeutete einfach, seine Eckpunkte bis zum Punkt 0,0,0 zu minimieren. Wenn der Zeitpunkt erreicht ist, an dem das Quadrat sichtbar sein soll, setzen wir die Eckpunkte wieder an ihre Position.

Ein Bereich mit Verbesserungspotenzial besteht darin, die Eckpunkte vollständig auf der GPU mit Shadern zu bearbeiten. In der aktuellen Implementierung werden die Punkte durch eine Schleife des Scheitelpunktarrays des aktuellen Zeitstempels platziert, geprüft, welche Eckpunkte offengelegt werden müssen, und dann die Geometrie aktualisiert. Dies beansprucht die CPU stark, was dazu führt, dass der Lüfter dreht, und gleichzeitig die Akkulaufzeit vergeudet.

Virtuelles Kunstwerk

Aufnahmen der Künstler

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

Wir nutzten Kameras von Microsoft Kinect, um die Tiefendaten des Körpers der Künstler im Weltraum zu erfassen. So können wir die dreidimensionalen Figuren im selben Raum zeigen, in dem die Zeichnungen erscheinen.

Da sich der Körper des Künstlers verdecken würde, sodass wir nicht sehen können, was dahinter steckt, haben wir ein Doppel-Kinect-System verwendet, das sich an den gegenüberliegenden Seiten des Raums befindet und auf die Mitte zeigt.

Zusätzlich zu den Tiefeninformationen haben wir auch die Farbinformationen der Szene mit standardmäßigen DSLR-Kameras erfasst. Wir haben die erstklassige DepthKit-Software verwendet, um die Aufnahmen der Tiefenkamera und der Farbkameras zu kalibrieren und zusammenzuführen. Kinect ist in der Lage, Farben aufzuzeichnen, aber wir haben uns für DSLRs entschieden, weil wir die Belichtungseinstellungen steuern, schöne High-End-Objektive nutzen und in High Definition aufnehmen konnten.

Für die Aufnahmen haben wir einen speziellen Raum für HTC Vive, den Künstler und die Kamera gebaut. Alle Oberflächen waren mit Material bedeckt, das Infrarotlicht absorbierte, um eine sauberere Punktwolke zu erzeugen (Duvetyne an den Wänden, gerippte Gummimatten auf dem Boden). Für den Fall, dass das Material in den Aufnahmen der Punktwolken auftauchte, haben wir schwarzes Material gewählt, damit es nicht so ablenkend wäre wie etwas, das weiß.

Künstler

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

Alle vier Kanäle einer aufgezeichneten Videositzung (zwei Farbkanäle oben und zwei Tiefen darunter)
Alle vier Kanäle einer Videositzung (zwei Farbkanäle oben und zwei Tiefen unten)

Wir wollten nicht nur die Künstler zeigen, sondern auch das HMD und die Controller in 3D rendern. Dies war nicht nur wichtig für die klare Darstellung von HMD in der Endausgabe (die reflektierenden Linsen von HTC Vive verstörten die IR-Messungen von Kinect), sondern gab uns Ansprechpartner für die Fehlersuche im Partikelausgabebereich und die Ausrichtung der Videos an der Skizze.

Das am Kopf montierte Display, die Controller und die Partikel nebeneinander
Das Head-Mounted Display, die Controller und die Partikel nebeneinander

Dazu wurde ein benutzerdefiniertes Plug-in in Tilt Brush geschrieben, das die Positionen von HMD und Controllern der einzelnen Frames extrahiert. Da Tilt Brush mit 90 fps läuft, werden viele Daten gestreamt und die Eingabedaten einer Skizze waren unkomprimiert 20 MB. Wir haben diese Technik auch verwendet, um Ereignisse zu erfassen, die in der typischen Tilt Brush-Speicherdatei nicht aufgezeichnet sind, z. B. wenn der Künstler eine Option im Bereich „Tools“ auswählt und die Position des Spiegel-Widgets.

Bei der Verarbeitung der 4 TB an erfassten Daten bestand eine der größten Herausforderungen darin, die verschiedenen visuellen Quellen und Datenquellen miteinander abzustimmen. Jedes Video einer digitalen Spiegelreflexkamera muss am entsprechenden Kinect ausgerichtet werden, damit die Pixel sowohl im Raum als auch zeitlich ausgerichtet sind. Dann musste das Videomaterial der beiden Kamerastative aufeinander ausgerichtet werden, um einen einzigen Künstler zu bilden. Dann mussten wir den 3D-Kunststoff mit den Daten aus der Zeichnung abstimmen. Geschafft! Für die meisten dieser Aufgaben haben wir browserbasierte Tools entwickelt, die Sie hier selbst ausprobieren können.

Plattenspieler

Nachdem die Daten ausgerichtet waren, haben wir einige in NodeJS geschriebene Skripts verwendet, um alles zu verarbeiten und eine Videodatei sowie eine Reihe von JSON-Dateien auszugeben, die geschnitten und synchronisiert wurden. Um die Dateigröße zu verringern, haben wir drei Dinge getan. Zuerst haben wir die Genauigkeit jeder Gleitkommazahl so reduziert, dass sie eine Genauigkeit von 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 priorisiert, sodass anstelle von einfachem JSON mit Schlüssel/Wert-Paaren eine Wertereihenfolge für die Position und Rotation des HMD und der Controller erstellt wird. Dadurch wurde die Dateigröße auf knapp 3 MB reduziert, was über die Leitung akzeptabel war.

Musiker

Da das Video selbst als HTML5-Videoelement bereitgestellt wird, das von einer WebGL-Textur eingelesen wird und zu Partikeln wird, muss 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 großartiges Beispiel dafür gezeigt, wie man Aufnahmen direkt aus DepthKit erstellen kann.

iOS hat Einschränkungen für die Inline-Videowiedergabe, bei der wir davon ausgehen, dass Nutzer nicht von automatisch wiedergegebenen Webvideoanzeigen geärgert werden. Wir haben eine Methode verwendet, die anderen Problemumgehungen im Web ähnelt, nämlich den Videoframe in einen Canvas zu kopieren und die Videosuchzeit alle 1/30 Sekunden manuell zu aktualisieren.

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 den unglücklichen Nebeneffekt der deutlichen Senkung der iOS-Framerate, da das Kopieren des Pixelzwischenspeichers vom Video in den Canvas sehr CPU-intensiv ist. Um dieses Problem zu umgehen, haben wir auf dem iPhone 6 verkleinerte Versionen desselben Videos bereitgestellt, die mindestens 30 fps ermöglichen.

Fazit

Bei der Entwicklung von VR-Software seit 2016 besteht der allgemeine Konsens darin, Geometrien und Shader einfach zu halten, damit ein HMD mit 90+ fps ausgeführt werden kann. Das war ein tolles Ziel für WebGL-Demos, da die in Tilt Brush verwendeten Techniken sehr gut auf WebGL abgestimmt sind.

Zwar sind Webbrowser, die komplexe 3D-Mesh-Netzwerke darstellen, an sich nicht aufregend, aber dies war ein Proof of Concept, dass die gegenseitige Bestäubung von VR-Arbeit und dem Web durchaus möglich ist.