Wirtualne sesje artystyczne

Szczegóły sesji dotyczącej tworzenia grafiki

Podsumowanie

Sześciu artystów zostało zaproszonych do malowania, projektowania i rzeźbienia w rzeczywistości wirtualnej. Oto proces rejestrowania sesji, konwertowania danych i ich prezentowania w czasie rzeczywistym w przeglądarkach internetowych.

https://g.co/VirtualArtSessions

Jakie to czasy! Wprowadzenie wirtualnej rzeczywistości jako produktu dla konsumentów powoduje odkrywanie nowych, nieodkrytych dotąd możliwości. Tilt Brush to usługa Google dostępna na urządzeniu HTC Vive, która umożliwia tworzenie rysunków w przestrzeni trójwymiarowej. Gdy po raz pierwszy wypróbowaliśmy Tilt Brush, poczuliśmy, jak to jest rysować za pomocą kontrolerów z wykrywanie ruchów, i jak to jest „być w pokoju z supermocami”. To naprawdę niesamowite uczucie, gdy można rysować w powietrzu dookoła siebie.

Wirtualny element artystyczny

Zespół Data Arts w Google stanął przed wyzwaniem zaprezentowania tej funkcji osobom bez zestawu VR w internecie, gdzie Tilt Brush nie działa. W tym celu zespół zaprosił rzeźbiarza, ilustratora, projektanta koncepcji, artystę mody, artystę instalacji i artystów ulicznych, aby stworzyli grafiki w swoim stylu w ramach tego nowego medium.

Rejestrowanie rysunków w rzeczywistości wirtualnej

Aplikacja Tilt Brush, która została stworzona w Unity, to aplikacja na komputer, która korzysta z systemu VR w pomieszczeniu, aby śledzić pozycję głowy (wyświetlacz na głowie) i kontrolerów w obu rękach. Elementy graficzne utworzone w programie Tilt Brush są domyślnie eksportowane jako pliki .tilt. Aby udostępnić tę funkcję w internecie, zdaliśmy sobie sprawę, że potrzebujemy czegoś więcej niż tylko danych dotyczących grafiki. Współpracowaliśmy z zespołem Tilt Brush, aby zmodyfikować tę aplikację tak, aby eksportowała działania cofnięcia/usunięcia oraz pozycje głowy i ręki artysty z częstotliwością 90 razy na sekundę.

Podczas rysowania aplikacja Tilt Brush rejestruje położenie i kąt kontrolera, a następnie przekształca wiele punktów w ciągłą linię. Przykład znajdziesz tutaj. Napisałem wtyczki, które wyodrębniają te linie i wyświetlają je jako surowy kod JSON.

    {
      "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
      ]
    }

Powyższy fragment kodu przedstawia format pliku JSON szkiców.

Każdy obrys jest zapisywany jako działanie o typie „STROKE”. Oprócz działań związanych z wykreślaniem chcieliśmy pokazać, jak artysta popełnia błędy i zmienia pomysły w trakcie tworzenia szkicu. Dlatego tak ważne było zapisanie działań „DELETE”, które służą do wymazywania lub odwoływania całego gestu.

Zapisywane są podstawowe informacje o każdym pociągnięciu, czyli typ pędzla, jego rozmiar i kolor w formacie RGB.

Na koniec zapisywany jest każdy wierzchołek ścieżki, w tym położenie, kąt, czas oraz siła nacisku na przycisk kontrolera (oznaczona jako pw każdym punkcie).

Pamiętaj, że rotacja jest kwateranionem 4-wymiarowym. Jest to ważne, gdy później renderujemy linie, aby uniknąć blokowania gimbala.

Odtwarzanie szkiców wstecz za pomocą WebGL

Aby wyświetlać szkice w przeglądarce, użyliśmy biblioteki THREE.js i napisali kod generowania geometrii, który naśladuje działanie programu Tilt Brush.

Aplikacja Tilt Brush tworzy paski trójkątów w czasie rzeczywistym na podstawie ruchów ręki użytkownika, ale cały szkic jest już „ukończony”, gdy wyświetlamy go w internecie. Dzięki temu możemy pominąć większość obliczeń w czasie rzeczywistym i przetworzyć geometrię podczas wczytywania.

Szkice WebGL

Każda para wierzchołków w ścieżce tworzy wektor kierunkowy (niebieskie linie łączące poszczególne punkty, jak pokazano powyżej, moveVector w fragmentie kodu poniżej). Każdy punkt zawiera też orientację, czyli macierz kwantową, która reprezentuje bieżący kąt kontrolera. Aby utworzyć pasek trójkątów, przechodzimy po kolei przez te punkty, tworząc wektory normalne, które są prostopadłe do kierunku i orientacji kontrolera.

Proces obliczania paska trójkąta dla każdego gestu jest prawie identyczny z kodem używanym w programie Tilt Brush:

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

Połączenie kierunku i orientacji ścieżki daje matematycznie niejednoznaczne wyniki. Może się okazać, że wygenerowano kilka normalnych kierunków, co często powoduje „skręcanie” geometrii.

Podczas iteracji punktów obrysu zachowujemy „preferowany prawy” wektor i przekazujemy go do funkcji computeSurfaceFrame(). Ta funkcja daje nam wektor normalny, z którego możemy wyprowadzić kwadrat w pasku kwadratowym na podstawie kierunku obrysu (od ostatniego punktu do bieżącego) i orientacji kontrolera (kwadratu). Co ważniejsze, zwraca on również nowy „preferowany prawy” wektor dla następnego zestawu obliczeń.

Pociągnięcia

Po wygenerowaniu kwadratów na podstawie punktów kontrolnych każdego skrótu złączamy te kwadraty, interpolując ich rogi od jednego kwadratu do drugiego.

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 );
}
Uśrednione kwadraty
Połączone kwadraty.

Każdy kwadrat zawiera też współczynniki UV, które są generowane w następnym kroku. Niektóre pędzle zawierają różne wzory pociągnięć, aby sprawiać wrażenie, że każde pociągnięcie pędzla jest inne. Aby to osiągnąć, należy użyć _atlasu tekstur_, w którym każda tekstura pędzla zawiera wszystkie możliwe warianty. Prawidłowa tekstura jest wybierana przez modyfikowanie wartości UV pędzla.

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

    });

}
4 tekstury w atlasie tekstur dla pędzla olejnego
4 tekstury w atlasie tekstur dla pędzla olejnego
W aplikacji Tilt Brush
W przypadku pędzla Tilt Brush
W WebGL
W przypadku WebGL

Każdy szkic może zawierać nieograniczoną liczbę pociągnięć, których nie trzeba modyfikować w czasie wykonywania, dlatego z wyprzedzeniem obliczamy geometrię pociągnięć i łączymy je w jeden mesh. Mimo że każdy nowy typ pędzla musi być osobnym materiałem, nadal ogranicza to nasze wywołania do rysowania do jednego na pędzel.

Cały szkic powyżej jest wykonywany w jednym wywołaniu funkcji rysowania w WebGL.
Cały szkic powyżej jest wykonywany w jednym wywołaniu funkcji draw w WebGL

Aby przeprowadzić test obciążeniowy systemu, stworzyliśmy szkic, który wypełniał przestrzeń tak wieloma wierzchołkami, jak to było możliwe, przez 20 minut. Wygenerowany szkic nadal odtwarzany jest z prędkością 60 FPS w WebGL.

Ponieważ każdy z pierwotnych wierzchołków ścieżki zawierał też czas, możemy łatwo odtworzyć dane. Ponowna kalkulacja uderzeń na ramkę byłaby bardzo powolna, dlatego zamiast tego obliczyliśmy cały szkic podczas wczytywania i po prostu odkrywaliśmy każdy kwadrat, gdy nadszedł odpowiedni czas.

Ukrywanie kwadratu polegało na złożeniu jego wierzchołków do punktu 0,0,0. Gdy czas osiągnie punkt, w którym kwadrat ma zostać odsłonięty, przestawiamy wierzchołki z powrotem na miejsce.

Możliwości poprawy obejmują manipulowanie wierzchołkami wyłącznie na GPU za pomocą shaderów. Obecna implementacja umieszcza je, przechodząc w pętli przez tablicę wierzchołków od bieżącego sygnatury czasowej, sprawdzając, które wierzchołki należy odsłonić, a następnie aktualizując geometrię. To powoduje duże obciążenie procesora, co powoduje włączenie wentylatora i marnowanie energii.

Wirtualny element artystyczny

Nagrywanie wykonawców

Uznaliśmy, że same szkice nie wystarczą. Chcieliśmy pokazać artystów w trakcie tworzenia szkiców, malujących poszczególne pociągnięcia pędzla.

Aby uchwycić artystów, użyliśmy kamer Microsoft Kinect do rejestrowania danych głębi ciała artystów w przestrzeni. Dzięki temu możemy wyświetlać trójwymiarowe modele w tej samej przestrzeni, w której znajdują się rysunki.

Ciało artysty zasłaniało to, co znajduje się za nim, więc użyliśmy podwójnego systemu Kinect, ustawionego po przeciwnych stronach pokoju i wycelowanego na środek.

Oprócz informacji o głębi uchwyciliśmy też informacje o kolorach sceny za pomocą standardowych lustrzanek cyfrowych. Do kalibrowania i scalania materiału z kamery głębi oraz kamer kolorowych użyliśmy doskonałego oprogramowania DepthKit. Kinect może nagrywać kolor, ale zdecydowaliśmy się użyć aparatów DSLR, ponieważ mogliśmy kontrolować ustawienia ekspozycji, używać pięknych, wysokiej klasy obiektywów i nagrywać w wysokiej rozdzielczości.

Aby nagrać materiał, zbudowaliśmy specjalny pokój, w którym umieściliśmy HTC Vive, artystę i kamerę. Wszystkie powierzchnie zostały pokryte materiałem, który pochłania światło podczerwonę, aby uzyskać czystszą chmurę punktów (dywetyna na ścianach, żebrowana mata gumowa na podłodze). W przypadku, gdy materiały były widoczne na filmie z chmury punktów, wybraliśmy czarne materiały, aby nie rozpraszały tak bardzo jak białe.

wykonawca muzyczny,

Dzięki uzyskanym nagraniom wideo mieliśmy wystarczającą ilość informacji, aby stworzyć projekcję układu cząsteczkowego. Stworzyliśmy kilka dodatkowych narzędzi w openFrameworks, aby jeszcze bardziej wyczyścić materiał, w szczególności usuwając podłogi, ściany i sufit.

Wszystkie 4 kanały nagranej sesji wideo (2 kanały kolorów u góry i 2 kanały głębokości u dołu)
Wszystkie 4 kanały nagranej sesji wideo (2 kanały koloru powyżej i 2 kanały głębi poniżej)

Oprócz pokazania wykonawców chcieliśmy też wyrenderować HMD i kontrolery w 3D. Było to ważne nie tylko ze względu na wyraźne pokazanie HMD w końcowym pliku wyjściowym (odblaskowe soczewki HTC Vive zakłócały odczyty IR Kinecta), ale też dało nam punkty kontaktowe do debugowania danych wyjściowych z cząstkami i dopasowania filmów do szkicu.

Wyświetlacz, kontrolery i cząsteczki ustawione w rzędzie
Wyświetlacz na hełmie, kontrolery i cząsteczki ustawione w rzędzie

Aby to zrobić, napisaliśmy niestandardowy wtyczek do Tilt Brush, który wyodrębniał pozycje HMD i kontrolerów w każdej klatce. Aplikacja Tilt Brush działa z prędkością 90 FPS, więc generuje mnóstwo danych, a dane wejściowe szkicu nieskompresowane zajmują ponad 20 MB. Użyliśmy tej metody również do rejestrowania zdarzeń, które nie są rejestrowane w typowym pliku zapisanym w programie Tilt Brush, np. gdy artysta wybiera opcję na panelu narzędzi lub ustawia pozycję widżetu lustra.

Podczas przetwarzania 4 TB danych jednym z największych wyzwań było dopasowanie wszystkich różnych źródeł wizualnych i źródeł danych. Każdy film z kamery DSLR musi być wyrównany z odpowiednim Kinectem, tak aby piksele były wyrównane w przestrzeni i czasie. Następnie trzeba było dopasować do siebie materiały z tych dwóch urządzeń, aby powstało jedno ujęcie. Następnie musieliśmy dopasować naszego artysty 3D do danych uzyskanych z rysunku. Uff... Aby ułatwić wykonywanie większości tych zadań, opracowaliśmy narzędzia działające w przeglądarce. Możesz je wypróbować tutaj.

wykonawcy nagrywający,

Po wyrównaniu danych użyliśmy skryptów napisanych w NodeJS, aby przetworzyć je wszystkie i wygenerować plik wideo oraz serię plików JSON, które są przycięte i zsynchronizowane. Aby zmniejszyć rozmiar pliku, zrobiliśmy 3 rzeczy. Po pierwsze, zmniejszyliśmy dokładność każdej liczby zmiennoprzecinkowej, tak aby miała ona maksymalnie 3 cyfry po przecinku. Po drugie, zmniejszyliśmy liczbę punktów o 1/3, aby uzyskać 30 FPS, i przeskalowaliśmy pozycje po stronie klienta. Na koniec serializowaliśmy dane, aby zamiast zwykłego formatu JSON z parami klucz-wartość utworzyć kolejność wartości dla pozycji i obrotu HMD oraz kontrolerów. Rozmiar pliku zmniejszył się do niecałych 3 MB, co było akceptowalne w przypadku przesyłania przez sieć.

wykonawcy muzyczni;

Ponieważ sam film jest wyświetlany jako element wideo HTML5, który jest odczytywany przez teksturę WebGL, aby stać się cząstkami, sam film musiał być odtwarzany w tle. Shader konwertuje kolory z obrazu głębi na pozycje w przestrzeni 3D. James George udostępnił świetny przykład tego, jak można wykorzystać materiały z DepthKit.

iOS ma ograniczenia dotyczące odtwarzania filmów wbudowanych, co naszym zdaniem ma na celu zapobieganie irytowaniu użytkowników przez automatycznie odtwarzane reklamy wideo. Użyliśmy techniki podobnej do innych obejść w internecie, czyli skopiowaliśmy kadr filmu do obszaru roboczego i ręcznie aktualizowaliśmy czas przewijania filmu co 1/30 sekund.

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

Nasz sposób działania miał niefortunny efekt uboczny w postaci znacznego obniżenia częstotliwości wyświetlania klatek na iOS, ponieważ kopiowanie bufora pikseli z filmu na kanwę jest bardzo obciążające dla procesora. Aby temu zaradzić, wyświetlaliśmy po prostu mniejsze wersje tych samych filmów, które umożliwiają wyświetlanie co najmniej 30 FPS na iPhonie 6.

Podsumowanie

Ogólnie zaleca się, aby w przypadku tworzenia oprogramowania VR w 2016 r. stosować proste geometrie i shadery, aby można było uzyskać 90 i więcej klatek na sekundę w przypadku HMD. Okazało się, że jest to świetny cel dla wersji demonstracyjnych WebGL, ponieważ techniki używane w programie Tilt Brush bardzo dobrze pasują do WebGL.

Chociaż przeglądarki wyświetlające złożone siatki 3D nie są same w sobie ekscytujące, stanowiły one dowód na to, że przenikanie VR i sieci jest całkowicie możliwe.